forked from I2P_Developers/i2p.i2p
Remove platform-specific workarounds from Java 8+ version of ShellService
This commit is contained in:
@ -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() {
|
||||
|
Reference in New Issue
Block a user