
dev.equo.ide.ScriptExec Maven / Gradle / Ivy
/*******************************************************************************
* Copyright (c) 2023 EquoTech, Inc. and others.
*
* This program and the accompanying materials
* are made available under the terms of the Eclipse Public License 2.0
* which accompanies this distribution, and is available at
* https://www.eclipse.org/legal/epl-2.0/
*
* SPDX-License-Identifier: EPL-2.0
*
* Contributors:
* EquoTech, Inc. - initial API and implementation
*******************************************************************************/
package dev.equo.ide;
import com.diffplug.common.swt.os.OS;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.function.BiConsumer;
import java.util.function.Consumer;
import javax.annotation.Nullable;
/**
* Facility for creating and executing scripts. All scripts are invisible to the user (in a gui
* sense). Scripts can spawned as child processes or totally separate. You MUST specify the content
* of the script to run.
*/
class ScriptExec {
/** Creates a new ScriptExec instance. */
public static ScriptExec script(String script) {
return new ScriptExec(script);
}
private final String script;
private Optional directory = Optional.empty();
private Map env = Map.of();
private ScriptExec(String script) {
this.script = Objects.requireNonNull(script);
}
/** Sets the directory that the script will be run in. */
public ScriptExec directory(File directory) {
this.directory = Optional.of(directory);
return this;
}
/** Sets the directory that the script will be run in. */
public ScriptExec environment(Map env) {
this.env = env;
return this;
}
/** Spawns the script as a separate task. */
public void execSeparate(@Nullable Consumer monitorProcess)
throws IOException, InterruptedException {
exec(true, monitorProcess);
}
/** Spawns the script as a child task. */
public void execBlocking(@Nullable Consumer monitorProcess)
throws IOException, InterruptedException {
exec(false, monitorProcess);
}
private void exec(boolean isSeparate, @Nullable Consumer monitorProcess)
throws IOException, InterruptedException {
// create the self-deleting script file
File scriptFile = createSelfDeletingScript(script);
// get the right arguments
List fullArgs = getPlatformCmds(scriptFile, isSeparate);
// set the cmds
int exitValue =
Launcher.launchAndInheritIO(directory.orElse(null), fullArgs, env, monitorProcess);
if (mavenWorkarounds()
&& ((OS.getNative().isLinux() && exitValue == 137)
|| (OS.getNative().isWindows() && exitValue == 1))) {
// not sure why this is happening, but it's working fine
return;
}
if (exitValue != EXIT_VALUE_SUCCESS) {
throw new RuntimeException("'" + script + "' exited with " + exitValue);
}
}
private static boolean mavenWorkarounds() {
return "true".equals(System.getProperty("equo-ide-maven-workarounds"));
}
/** The integer value which marks that a process exited successfully. */
private static final int EXIT_VALUE_SUCCESS = 0;
/**
* Creates a .bat or .sh file which will: - cd into the correct directory - run the script -
* delete itself
*/
private static File createSelfDeletingScript(String script) throws IOException {
String extension = OS.getRunning().isWindows() ? ".bat" : ".sh";
String header = OS.getRunning().isWindows() ? "" : "#!/bin/bash";
String callRobust = OS.getRunning().isWindows() ? "call " : "";
// put the script that we're going to run in its own file
File targetScriptFile =
createGenericScript(
extension,
(file, printer) -> {
printer.println(header);
printer.println(script);
});
// create a script which will call this script then delete it and itself
return createGenericScript(
extension,
(file, printer) -> {
// add the unix header
printer.println(header);
// call the script that we want to run
printer.println(callRobust + quote(targetScriptFile));
// make it self-deleting
if (OS.getNative().isWindows()) {
// http://stackoverflow.com/a/20333575/1153071
// start /b starts new application without a new window
// (https://technet.microsoft.com/en-us/library/bb491005.aspx)
// delete ourselves and the targetScript
printer.println(
"start /b \"\" cmd /c del " + quote(targetScriptFile) + " " + quote(file));
// make 100% sure that the script exits at the end
printer.println("exit");
} else {
// http://stackoverflow.com/a/8981176/1153071
printer.println("rm " + quote(targetScriptFile) + " " + quote(file));
}
});
}
/**
* Creates a temporary file with the given extension. The client is responsible for making the
* script self-deleting.
*/
private static File createGenericScript(String extension, BiConsumer client)
throws IOException {
// create the file
File file = File.createTempFile("DiffPlugScript", extension);
if (!OS.getNative().isWindows()) {
file.setExecutable(true);
}
// create the command buffer, and let the client populate it
StringPrinter fullScript = new StringPrinter();
client.accept(file, fullScript);
// write the script
return writeFlushed(file, fullScript.toString().getBytes(StandardCharsets.UTF_8));
}
/**
* Writes all of byte[] content to the given file, being sure to flush the file to the OS before
* returning.
*/
private static File writeFlushed(File file, byte[] content) throws IOException {
try (FileOutputStream output = new FileOutputStream(file)) {
output.write(content);
output.flush();
output.getFD().sync();
}
return file;
}
/** Returns the arguments needed to run the scriptFile with the given properties. */
private static List getPlatformCmds(File scriptFile, boolean isSeparate)
throws IOException {
if (OS.getNative().isWindows()) {
// wscript.exe invisible.vbs script.bat
return List.of(
"wscript.exe",
createInvisibleVbs(isSeparate).getAbsolutePath(),
scriptFile.getAbsolutePath());
} else {
// use sh to execute
if (isSeparate) {
File spawningScript =
createSelfDeletingScript(
""
+ ("nohup " + quote(scriptFile) + " &\n")
+ (mavenWorkarounds() ? "sleep 5\n" : "")
+ "disown\n");
return List.of("/bin/bash", spawningScript.getAbsolutePath());
} else {
return List.of("/bin/bash", scriptFile.getAbsolutePath());
}
}
}
/** Creates a .vbs file which will execute a batch command and then delete itself. */
private static File createInvisibleVbs(boolean isSeparate) throws IOException {
return createGenericScript(
".vbs",
(file, printer) -> {
// args are at http://ss64.com/vb/run.html
String windowStyle = "0";
String waitOnReturn = (isSeparate && !mavenWorkarounds()) ? "False" : "True";
// open the shell
printer.println(
String.format(
"CreateObject(\"Wscript.Shell\").Run \"\"\"\" & WScript.Arguments(0) & \"\"\"\", %s, %s",
windowStyle, waitOnReturn));
// then delete ourselves
printer.println(
"CreateObject(\"Scripting.FileSystemObject\").DeleteFile(\""
+ file.getAbsolutePath()
+ "\")");
});
}
/** Returns the file's absolute path, quoted if necessary. */
public static String quote(File file) {
return quote(file.getAbsolutePath());
}
/** Returns the file's absolute path, quoted if necessary. */
public static String quote(String arg) {
return arg.contains(" ") ? "\"" + arg + "\"" : arg;
}
/** Quotes either a String or a File. */
private static String quoteObj(Object arg) {
if (arg instanceof String) {
return quote((String) arg);
} else if (arg instanceof File) {
return quote((File) arg);
} else {
throw new IllegalArgumentException("Unexpected class " + arg.getClass());
}
}
/** Quotes a list of String or File objects. */
public static String quoteAll(List> args) {
StringBuilder completeArgs = new StringBuilder();
if (args.size() == 0) {
return "";
} else {
completeArgs.append(quoteObj(args.get(0)));
for (int i = 1; i < args.size(); ++i) {
completeArgs.append(" ");
completeArgs.append(quoteObj(args.get(i)));
}
return completeArgs.toString();
}
}
private static class StringPrinter {
final StringBuilder builder = new StringBuilder();
void println(String line) {
builder.append(line);
builder.append('\n');
}
public String toString() {
return builder.toString();
}
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy