Remove platform-specific workarounds from Java 8+ version of ShellService

This commit is contained in:
idk
2022-01-03 14:18:16 -05:00
parent 13f910be70
commit d1192f74f2

View File

@ -9,11 +9,7 @@
package net.i2p.router.web;
import java.io.BufferedInputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.util.Arrays;
import java.util.ArrayList;
@ -23,20 +19,18 @@ import net.i2p.app.ClientApp;
import net.i2p.app.ClientAppManager;
import net.i2p.app.ClientAppState;
import net.i2p.util.Log;
import net.i2p.util.ShellCommand;
import net.i2p.util.SystemVersion;
/**
* Alternative to ShellCommand based on ProcessBuilder, which manages
* a process and keeps track of it's state by PID when a plugin cannot be
* managed otherwise. Eliminates the need for a bespoke shell script to manage
* application state for forked plugins.
* Alternative to ShellCommand for plugins based on ProcessBuilder, which
* manages
* a process and keeps track of it's state by maintaining a Process object.
*
* Keeps track of the PID of the plugin, reports start/stop status correctly
* Keeps track of the process, and reports start/stop status correctly
* on configplugins. When running a ShellService from a clients.config file,
* the user MUST pass -shellservice.name in the args field in clients.config
* to override the plugin name. The name passed to -shellservice.name should
* be unique to avoid causing issues. (https://i2pgit.org/i2p-hackers/i2p.i2p/-/merge_requests/39#note_4234)
* be unique to avoid causing issues.
* (https://i2pgit.org/i2p-hackers/i2p.i2p/-/merge_requests/39#note_4234)
* -shellservice.displayName is optional and configures the name of the plugin
* which is shown on the console. In most cases, the -shellservice.name must be
* the same as the plugin name in order for the $PLUGIN field in clients.config
@ -44,7 +38,8 @@ import net.i2p.util.SystemVersion;
* (-shellservice.name != plugin.name), you must not use $PLUGIN in your
* clients.config file.
*
* The recommended way to use this tool is to manage a single forked app/process,
* The recommended way to use this tool is to manage a single forked
* app/process,
* with a single ShellService, in a single plugin.
*
* When you are writing your clients.config file, please take note that $PLUGIN
@ -71,7 +66,6 @@ public class ShellService implements ClientApp {
private volatile String displayName = "unnamedClient";
private Process _p;
private volatile long _pid;
public ShellService(I2PAppContext context, ClientAppManager listener, String[] args) {
_context = context;
@ -80,7 +74,7 @@ public class ShellService implements ClientApp {
String[] procArgs = trimArgs(args);
String process = writeScript(procArgs);
String process = procArgs.toString();
if (_log.shouldLog(Log.DEBUG)) {
_log.debug("Process: " + process);
@ -94,198 +88,6 @@ public class ShellService implements ClientApp {
changeState(ClientAppState.INITIALIZED, "ShellService: " + getName() + " set up and initialized");
}
private String scriptArgs(String[] procArgs) {
StringBuilder tidiedArgs = new StringBuilder();
for (int i = 0; i < procArgs.length; i++) {
tidiedArgs.append(" \"").append(procArgs[i]).append("\" ");
}
return tidiedArgs.toString();
}
private String batchScript(String[] procArgs) {
if (_log.shouldLog(Log.DEBUG)) {
String cmd = procArgs[0];
_log.debug("cmd: " + cmd);
}
String script = "start \""+getName()+"\" "+scriptArgs(procArgs)+System.lineSeparator() +
"tasklist /V /FI \"WindowTitle eq "+getName()+"*\""+System.lineSeparator();
return script;
}
private String shellScript(String[] procArgs) {
String cmd = procArgs[0];
if(_log.shouldLog(Log.DEBUG))
_log.debug("cmd: " + cmd);
File file = new File(cmd);
if(file.exists()){
if (!file.isDirectory() && !file.canExecute()) {
file.setExecutable(true);
}
}
String Script = "nohup "+scriptArgs(procArgs)+" 1>/dev/null 2>/dev/null & echo $!"+System.lineSeparator();
return Script;
}
private void deleteScript() {
File dir = _context.getTempDir();
if (SystemVersion.isWindows()) {
File bat = new File(dir, "shellservice-"+getName()+".bat");
bat.delete();
} else {
File sh = new File(dir, "shellservice-"+getName()+".sh");
sh.delete();
}
}
private String writeScript(File dir, String extension, String[] procArgs){
File script = new File(dir, "shellservice-"+getName()+extension);
script.delete();
if (_log.shouldLog(Log.DEBUG))
_log.debug("Writing Batch Script " + script.toString());
FileWriter scriptWriter = null;
try {
script.createNewFile();
scriptWriter = new FileWriter(script);
if (extension.equals(".bat") || extension.equals(""))
scriptWriter.write(batchScript(procArgs));
else if (extension.equals(".sh"))
scriptWriter.write(shellScript(procArgs));
changeState(ClientAppState.INITIALIZED, "ShellService: "+getName()+" initialized");
} catch (IOException ioe) {
if (_log.shouldLog(Log.ERROR))
_log.error("Error writing wrapper script shellservice-" + getName() + extension, ioe);
script.delete();
changeState(ClientAppState.START_FAILED, "ShellService: "+getName()+" failed to start, error writing script.", ioe);
} finally {
try {
if (scriptWriter != null)
scriptWriter.close();
} catch (IOException ioe) {
if (_log.shouldLog(Log.ERROR)){
_log.error("Error writing wrapper script shellservice-" + getName() + extension, ioe);
changeState(ClientAppState.START_FAILED, "ShellService: "+getName()+" failed to start, error closing script writer", ioe);
}
}
}
script.setExecutable(true);
return script.getAbsolutePath();
}
private String writeScript(String[] procArgs){
File dir = _context.getTempDir();
if (SystemVersion.isWindows()) {
return writeScript(dir, ".bat", procArgs);
} else {
return writeScript(dir, ".sh", procArgs);
}
}
private String getPID() {
return String.valueOf(_pid);
}
/**
* Queries {@code tasklist} if the process ID {@code pid} is running.
*
* Contain code from Stack Overflow(https://stackoverflow.com/questions/2533984/java-checking-if-any-process-id-is-currently-running-on-windows/41489635)
*
* @param pid the PID to check
* @return {@code true} if the PID is running, {@code false} otherwise
*/
private boolean isProcessIdRunningOnWindows(String pid){
try {
String cmds[] = {"cmd", "/c", "tasklist /FI \"PID eq " + pid + "\""};
ShellCommand _shellCommand = new ShellCommand();
return _shellCommand.executeSilentAndWaitTimed(cmds, 240);
} catch (Exception ex) {
if (_log.shouldLog(Log.DEBUG))
_log.debug("Error checking if process is running", ex);
changeState(ClientAppState.CRASHED, "ShellService: "+getName()+" status unknowable", ex);
}
return false;
}
private boolean isProcessIdRunningOnUnix(String pid) {
try {
String cmds[] = {"ps", "-p", pid};
ShellCommand _shellCommand = new ShellCommand();
return _shellCommand.executeSilentAndWaitTimed(cmds, 240);
} catch (Exception ex) {
if (_log.shouldLog(Log.DEBUG))
_log.debug("Error checking if process is running", ex);
changeState(ClientAppState.CRASHED, "ShellService: "+getName()+" status unknowable", ex);
}
return false;
}
private boolean isProcessIdRunning(String pid) {
boolean running = false;
if (SystemVersion.isWindows()) {
running = isProcessIdRunningOnWindows(pid);
} else {
running = isProcessIdRunningOnUnix(pid);
}
return running;
}
private long getPidOfProcess() {
if (_log.shouldLog(Log.DEBUG))
_log.debug("Finding the PID of: " + getName());
if (isProcessIdRunning(getPID())) {
if (_log.shouldLog(Log.DEBUG))
_log.debug("Read PID in from " + getPID());
return _pid;
}
BufferedInputStream bis = null;
ByteArrayOutputStream buf = null;
try {
if (_log.shouldLog(Log.DEBUG))
_log.debug("Getting PID from output");
if (_p == null) {
if (_log.shouldLog(Log.WARN)) {
_log.warn("Process is null, something is wrong");
}
changeState(ClientAppState.CRASHED, "ShellService: "+getName()+" should be runnning but the process is null.");
return -1;
}
bis = new BufferedInputStream(_p.getInputStream());
buf = new ByteArrayOutputStream();
for (int result = bis.read(); result != -1; result = bis.read()) {
if (result == '\n')
break;
buf.write((byte) result);
}
String pidString = buf.toString("UTF-8").replaceAll("[\\r\\n\\t ]", "");
long pid = Long.parseLong(pidString);
if (_log.shouldLog(Log.DEBUG))
_log.debug("Found " + getName() + "process with PID: " + pid);
return pid;
} catch (IOException ioe) {
if (_log.shouldLog(Log.ERROR))
_log.error("Error getting PID of application started by shellservice-" + getName() , ioe);
changeState(ClientAppState.CRASHED, "ShellService: "+getName()+" PID could not be discovered", ioe);
} finally {
if (bis != null) {
try {
bis.close(); // close the input stream
} catch (IOException ioe) {
if (_log.shouldLog(Log.ERROR))
_log.error("Error closing input stream", ioe);
}
}
if (buf != null) {
try {
buf.close(); // close the output stream
} catch (IOException ioe) {
if (_log.shouldLog(Log.ERROR))
_log.error("Error closing output stream", ioe);
}
}
}
return -1;
}
private String[] trimArgs(String[] args) {
ArrayList<String> newargs = new ArrayList<String>();
for (int i = 0; i < args.length; i++) {
@ -308,7 +110,8 @@ public class ShellService implements ClientApp {
}
}
if (getName() == null)
throw new IllegalArgumentException("ShellService: ShellService passed with args=" + Arrays.toString(args) + " must have a name");
throw new IllegalArgumentException(
"ShellService: ShellService passed with args=" + Arrays.toString(args) + " must have a name");
if (getDisplayName() == null)
displayName = name;
String arr[] = new String[newargs.size()];
@ -338,17 +141,15 @@ public class ShellService implements ClientApp {
return;
}
changeState(ClientAppState.STARTING, "ShellService: " + getName() + " starting");
boolean start = checkIsStopped();
boolean start = isProcessStopped();
if (start) {
_p = _pb.start();
long pid = getPidOfProcess();
if (pid == -1 && _log.shouldLog(Log.ERROR))
_log.error("Error getting PID of application from recently instantiated shellservice" + getName());
if (!_p.isAlive() && _log.shouldLog(Log.ERROR))
_log.error("Error getting Process of application from recently instantiated shellservice" + getName());
if (_log.shouldLog(Log.DEBUG))
_log.debug("Started " + getName() + "process with PID: " + pid);
this._pid = pid;
deleteScript();
_log.debug("Started " + getName() + "process");
}
if (!_p.isAlive())
changeState(ClientAppState.RUNNING, "ShellService: " + getName() + " started");
Boolean reg = _cmgr.register(this);
if (reg) {
@ -364,31 +165,35 @@ public class ShellService implements ClientApp {
}
/**
* Determine if the PID found in "shellservice"+getName()+".pid" is
* running or not. Result is the answer to the question "Should I attempt
* to start the process" so returns false when PID corresponds to a running
* process and true if it does not.
* Determine if the process running or not.
*
* Usage in PluginStarter.isClientThreadRunning requires the !inverse of
* the result.
*
* @return {@code true} if the PID is NOT running, {@code false} if the PID is running
* @return {@code true} if the Process is NOT running, {@code false} if the
* Process is
* running
*/
public boolean checkIsStopped() {
if (_log.shouldLog(Log.DEBUG))
_log.debug("Checking process status " + getName());
return !isProcessIdRunning(getPID());
public boolean isProcessStopped() {
return !isProcessRunning();
}
/**
* Query the stored PID of the previously launched ShellService and attempt
* to send SIGINT on Unix, SIGKILL on Windows in order to stop the wrapped
* application.
* Determine if the process running or not.
*
* @return {@code true} if the Process is running, {@code false} if the Process
* is
* not running
*/
public boolean isProcessRunning() {
if (_log.shouldLog(Log.DEBUG))
_log.debug("Checking process status " + getName() + _p.isAlive());
return _p.isAlive();
}
/**
* Shut down the process by calling Process.destroy()
*
* @param args generally null but could be stopArgs from clients.config
*/
public synchronized void shutdown(String[] args) throws Throwable {
String pid = getPID();
if (getName().equals("unnamedClient")) {
if (_log.shouldLog(Log.WARN))
_log.warn("ShellService has no name, not shutting down");
@ -397,36 +202,22 @@ public class ShellService implements ClientApp {
changeState(ClientAppState.STOPPING, "ShellService: " + getName() + " stopping");
if (_p != null) {
if (_log.shouldLog(Log.DEBUG))
_log.debug("Stopping " + getName() + "process started with ShellService, PID: " + pid);
_log.debug("Stopping " + getName() + "process started with ShellService " + getName());
_p.destroy();
}
ShellCommand _shellCommand = new ShellCommand();
if (SystemVersion.isWindows()) {
if (_log.shouldLog(Log.DEBUG))
_log.debug("Stopping " + getName() + "process with PID: " + pid + "on Windows");
String cmd[] = {"cmd", "/c", "taskkill /F /T /PID " + pid};
_shellCommand.executeSilentAndWaitTimed(cmd, 240);
} else {
if (_log.shouldLog(Log.DEBUG))
_log.debug("Stopping " + getName() + "process with PID: " + pid + "on Unix");
String cmd[] = {"kill", pid};
_shellCommand.executeSilentAndWaitTimed(cmd, 240);
}
deleteScript();
changeState(ClientAppState.STOPPED, "ShellService: " + getName() + " stopped");
_cmgr.unregister(this);
}
/**
* Query the PID of the wrapped application and determine if it is running
* Query the state of managed process and determine if it is running
* or not. Convert to corresponding ClientAppState and return the correct
* value.
*
* @return non-null
*/
public ClientAppState getState() {
String pid = getPID();
if (!isProcessIdRunning(pid)) {
if (!isProcessRunning()) {
changeState(ClientAppState.STOPPED, "ShellService: " + getName() + " stopped");
_cmgr.unregister(this);
}
@ -436,8 +227,7 @@ public class ShellService implements ClientApp {
/**
* The generic name of the ClientApp, used for registration,
* e.g. "console". Do not translate. Has a special use in the context of
* ShellService, it is used to name the file which contains the PID of the
* process ShellService is wrapping.
* ShellService, must match the plugin name.
*
* @return non-null
*/
@ -448,6 +238,7 @@ public class ShellService implements ClientApp {
/**
* The display name of the ClientApp, used in user interfaces.
* The app must translate.
*
* @return non-null
*/
public String getDisplayName() {