Skip to content

Commit

Permalink
Manually search for executables under POSIX when the PATH is overriden
Browse files Browse the repository at this point in the history
  • Loading branch information
Greg Gibeling committed Mar 4, 2025
1 parent 2187e1d commit 31449dc
Show file tree
Hide file tree
Showing 4 changed files with 123 additions and 27 deletions.
Original file line number Diff line number Diff line change
@@ -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<Path> 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<Path>() {
@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;
}
};
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
Original file line number Diff line number Diff line change
@@ -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 <I, O> CommandInvocation<I, O> wrap(CommandInvocation<I, O> 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<String> arguments = new ArrayList<>(invocation.getArguments());
arguments.set(0, resolved.toString());
return invocation.toBuilder().clearArguments().arguments(arguments).build();
}
}
return invocation;
}
}
Original file line number Diff line number Diff line change
@@ -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;

Expand All @@ -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;
Expand All @@ -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
Expand Down

0 comments on commit 31449dc

Please sign in to comment.