diff --git a/ax-command/src/main/java/com/g2forge/alexandria/command/clireport/HCLIReport.java b/ax-command/src/main/java/com/g2forge/alexandria/command/clireport/HCLIReport.java new file mode 100644 index 00000000..e0584197 --- /dev/null +++ b/ax-command/src/main/java/com/g2forge/alexandria/command/clireport/HCLIReport.java @@ -0,0 +1,61 @@ +package com.g2forge.alexandria.command.clireport; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.net.URL; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.nio.file.attribute.PosixFilePermission; +import java.util.EnumSet; + +import com.g2forge.alexandria.java.close.ICloseableSupplier; +import com.g2forge.alexandria.java.io.HBinaryIO; +import com.g2forge.alexandria.java.io.RuntimeIOException; +import com.g2forge.alexandria.java.platform.HPlatform; + +public class HCLIReport { + public static final String CLIREPORT_VERSION = "v0.0.1"; + + public static final String CLIREPORT_FILENAME = "clireport"; + + public static final String CLIREPORT_DOWNLOADFORMAT = "https://github.com/g2forge/clireport/releases/download/%1$s/%2$s"; + + public static ICloseableSupplier download(Path directory) { + final String filename = HPlatform.getPlatform().getExeSpecs()[0].fromBase(CLIREPORT_FILENAME); + final Path path = directory == null ? Paths.get(filename) : directory.resolve(filename); + if (!Files.exists(path)) { + final String url = String.format(CLIREPORT_DOWNLOADFORMAT, CLIREPORT_VERSION, path.getFileName().toString()); + try (final InputStream input = new URL(url).openStream(); final OutputStream output = Files.newOutputStream(path)) { + HBinaryIO.copy(input, output); + } catch (IOException e) { + throw new RuntimeIOException("Failed to download clireport", e); + } + if (!Files.exists(path)) throw new RuntimeException(String.format("Failed to download %1$s to %2$s", url, path)); + try { + Files.setPosixFilePermissions(path, EnumSet.allOf(PosixFilePermission.class)); + } catch (UnsupportedOperationException exception) { + // Ignore this - it's not required on platforms where it's not supported + } catch (IOException exception) { + throw new RuntimeIOException(String.format("Failed to mark %1$s executable", path), exception); + } + if (!Files.isExecutable(path)) throw new RuntimeException(String.format("%1$s is not executable", path)); + } + return new ICloseableSupplier() { + @Override + public void close() { + try { + Files.deleteIfExists(path); + } catch (IOException e) { + throw new RuntimeException("Failed to delete downloaded " + CLIREPORT_FILENAME, e); + } + } + + @Override + public Path get() { + return path; + } + }; + } +} diff --git a/ax-command/src/main/java/com/g2forge/alexandria/command/invocation/runner/ICommandRunner.java b/ax-command/src/main/java/com/g2forge/alexandria/command/invocation/runner/ICommandRunner.java index fa1bc695..aa38a16c 100644 --- a/ax-command/src/main/java/com/g2forge/alexandria/command/invocation/runner/ICommandRunner.java +++ b/ax-command/src/main/java/com/g2forge/alexandria/command/invocation/runner/ICommandRunner.java @@ -8,11 +8,27 @@ @FunctionalInterface public interface ICommandRunner { + /** + * Create a command runner for the local machine. This method will take into account the current platform (OS) and JVM and compensate for any quirks in + * might introduce. + * + * @return A command runner for the local machine which does nothing to wrap the commands, other than compensate for platform issues. + */ + public static ICommandRunner create() { + final PlatformCategory category = HPlatform.getPlatform().getCategory(); + switch (category) { + case Posix: + return PosixPathCommandRunner.create(); + default: + return IdentityCommandRunner.create(); + } + } + public static ICommandRunner create(Shell shell) { final Shell actualShell = (shell == null) ? HPlatform.getPlatform().getShell() : shell; switch (actualShell.getCategory()) { case Posix: - if (shell == null) return IdentityCommandRunner.create(); + if (shell == null) return ICommandRunner.create(); return new PosixShellCommandRunner(actualShell); case Microsoft: switch (actualShell) { diff --git a/ax-command/src/main/java/com/g2forge/alexandria/command/invocation/runner/PosixPathCommandRunner.java b/ax-command/src/main/java/com/g2forge/alexandria/command/invocation/runner/PosixPathCommandRunner.java new file mode 100644 index 00000000..ce1d140e --- /dev/null +++ b/ax-command/src/main/java/com/g2forge/alexandria/command/invocation/runner/PosixPathCommandRunner.java @@ -0,0 +1,43 @@ +package com.g2forge.alexandria.command.invocation.runner; + +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; + +import com.g2forge.alexandria.command.invocation.CommandInvocation; +import com.g2forge.alexandria.command.invocation.environment.SystemEnvironment; +import com.g2forge.alexandria.java.core.marker.ISingleton; +import com.g2forge.alexandria.java.platform.HPlatform; + +public class PosixPathCommandRunner implements ICommandRunner, ISingleton { + protected static final PosixPathCommandRunner INSTANCE = new PosixPathCommandRunner(); + + public static PosixPathCommandRunner create() { + return INSTANCE; + } + + private PosixPathCommandRunner() {} + + @Override + public CommandInvocation wrap(CommandInvocation invocation) { + final String pathAsString = invocation.getEnvironment().apply(HPlatform.PATH); + // If the invocation PATH and system PATH are the same, then we can delegate to the underlying JVM code + if (Objects.equals(SystemEnvironment.create().apply(HPlatform.PATH), pathAsString)) return invocation; + + // Since the user is overriding the PATH, let's search that PATH for the executable + // We have to do this here because the JVM doesn't allow us to do this down at the process builder level + final String[] pathAsArray = HPlatform.getPlatform().getPathSpec().splitPaths(pathAsString); + for (String directory : pathAsArray) { + final Path resolved = Paths.get(directory).resolve(invocation.getArguments().get(0)); + if (Files.exists(resolved) && Files.isExecutable(resolved)) { + final List arguments = new ArrayList<>(invocation.getArguments()); + arguments.set(0, resolved.toString()); + return invocation.toBuilder().clearArguments().arguments(arguments).build(); + } + } + return invocation; + } +} diff --git a/ax-command/src/test/java/com/g2forge/alexandria/command/invocation/runner/TestICommandRunner.java b/ax-command/src/test/java/com/g2forge/alexandria/command/invocation/runner/TestICommandRunner.java index 59226c55..ef749980 100644 --- a/ax-command/src/test/java/com/g2forge/alexandria/command/invocation/runner/TestICommandRunner.java +++ b/ax-command/src/test/java/com/g2forge/alexandria/command/invocation/runner/TestICommandRunner.java @@ -1,15 +1,9 @@ package com.g2forge.alexandria.command.invocation.runner; import java.io.IOException; -import java.io.InputStream; -import java.io.OutputStream; -import java.net.URL; -import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; -import java.nio.file.attribute.PosixFilePermission; import java.util.ArrayList; -import java.util.EnumSet; import java.util.List; import java.util.stream.Collectors; @@ -18,16 +12,15 @@ import org.junit.Ignore; import org.junit.Test; +import com.g2forge.alexandria.command.clireport.HCLIReport; import com.g2forge.alexandria.command.invocation.CommandInvocation; import com.g2forge.alexandria.command.invocation.environment.SystemEnvironment; import com.g2forge.alexandria.command.invocation.format.ICommandFormat; import com.g2forge.alexandria.command.process.HProcess; import com.g2forge.alexandria.command.stdio.StandardIO; import com.g2forge.alexandria.java.core.helpers.HCollection; -import com.g2forge.alexandria.java.io.HBinaryIO; import com.g2forge.alexandria.java.io.HIO; import com.g2forge.alexandria.java.io.HTextIO; -import com.g2forge.alexandria.java.io.RuntimeIOException; import com.g2forge.alexandria.java.platform.HPlatform; import com.g2forge.alexandria.java.platform.PlatformCategory; import com.g2forge.alexandria.test.HAssert; @@ -52,24 +45,7 @@ protected void assumePosix() { @Before public void before() { - cliReport = Paths.get(HPlatform.getPlatform().getExeSpecs()[0].fromBase(CLIREPORT_FILENAME)); - if (!Files.exists(cliReport)) { - try (final InputStream input = new URL(String.format("https://github.com/g2forge/clireport/releases/download/%1$s/%2$s", CLIREPORT_VERSION, cliReport.getFileName().toString())).openStream(); - final OutputStream output = Files.newOutputStream(cliReport)) { - HBinaryIO.copy(input, output); - } catch (IOException e) { - throw new RuntimeIOException("Failed to download clireport", e); - } - HAssert.assertTrue(Files.exists(cliReport)); - try { - Files.setPosixFilePermissions(cliReport, EnumSet.allOf(PosixFilePermission.class)); - } catch (UnsupportedOperationException e) { - // Ignore this - it's not required on platforms where it's not supported - } catch (IOException e) { - throw new RuntimeIOException("Failed to mark clireport executable", e); - } - HAssert.assertTrue(Files.isExecutable(cliReport)); - } + cliReport = HCLIReport.download(null).get(); } @Test