Files
termux-app/termux-shared/src/main/java/com/termux/shared/shell/TermuxTask.java
agnostic-apollo 53c1a49b5b Make TermuxTask and TermuxSession agnostic to termux environment
Those classes shouldn't be tied to termux environment like variables, interpreters and working directory since commands may need to be executed with a different environment like android's or with a different logic. Now both classes use the ShellEnvironmentClient interface to dynamically get the environment to be used which currently for Termux's case is implemented by TermuxShellEnvironmentClient which is just a wrapper for TermuxShellUtils since later implements static functions.
2021-06-28 05:57:45 +05:00

313 lines
14 KiB
Java

package com.termux.shared.shell;
import android.content.Context;
import android.system.ErrnoException;
import android.system.Os;
import android.system.OsConstants;
import androidx.annotation.NonNull;
import com.termux.shared.R;
import com.termux.shared.models.ExecutionCommand;
import com.termux.shared.models.ResultData;
import com.termux.shared.models.errors.Errno;
import com.termux.shared.logger.Logger;
import com.termux.shared.models.ExecutionCommand.ExecutionState;
import java.io.DataOutputStream;
import java.io.File;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
/**
* A class that maintains info for background Termux tasks run with {@link Runtime#exec(String[], String[], File)}.
* It also provides a way to link each {@link Process} with the {@link ExecutionCommand}
* that started it.
*/
public final class TermuxTask {
private final Process mProcess;
private final ExecutionCommand mExecutionCommand;
private final TermuxTaskClient mTermuxTaskClient;
private static final String LOG_TAG = "TermuxTask";
private TermuxTask(@NonNull final Process process, @NonNull final ExecutionCommand executionCommand,
final TermuxTaskClient termuxTaskClient) {
this.mProcess = process;
this.mExecutionCommand = executionCommand;
this.mTermuxTaskClient = termuxTaskClient;
}
/**
* Start execution of an {@link ExecutionCommand} with {@link Runtime#exec(String[], String[], File)}.
*
* The {@link ExecutionCommand#executable}, must be set.
* The {@link ExecutionCommand#commandLabel}, {@link ExecutionCommand#arguments} and
* {@link ExecutionCommand#workingDirectory} may optionally be set.
*
* @param context The {@link Context} for operations.
* @param executionCommand The {@link ExecutionCommand} containing the information for execution command.
* @param termuxTaskClient The {@link TermuxTaskClient} interface implementation.
* The {@link TermuxTaskClient#onTermuxTaskExited(TermuxTask)} will
* be called regardless of {@code isSynchronous} value but not if
* {@code null} is returned by this method. This can
* optionally be {@code null}.
* @param shellEnvironmentClient The {@link ShellEnvironmentClient} interface implementation.
* @param isSynchronous If set to {@code true}, then the command will be executed in the
* caller thread and results returned synchronously in the {@link ExecutionCommand}
* sub object of the {@link TermuxTask} returned.
* If set to {@code false}, then a new thread is started run the commands
* asynchronously in the background and control is returned to the caller thread.
* @return Returns the {@link TermuxTask}. This will be {@code null} if failed to start the execution command.
*/
public static TermuxTask execute(@NonNull final Context context, @NonNull ExecutionCommand executionCommand,
final TermuxTaskClient termuxTaskClient,
@NonNull final ShellEnvironmentClient shellEnvironmentClient,
final boolean isSynchronous) {
if (executionCommand.workingDirectory == null || executionCommand.workingDirectory.isEmpty())
executionCommand.workingDirectory = shellEnvironmentClient.getDefaultWorkingDirectoryPath();
if (executionCommand.workingDirectory.isEmpty())
executionCommand.workingDirectory = "/";
String[] env = shellEnvironmentClient.buildEnvironment(context, false, executionCommand.workingDirectory);
final String[] commandArray = shellEnvironmentClient.setupProcessArgs(executionCommand.executable, executionCommand.arguments);
if (!executionCommand.setState(ExecutionState.EXECUTING)) {
executionCommand.setStateFailed(Errno.ERRNO_FAILED.getCode(), context.getString(R.string.error_failed_to_execute_termux_task_command, executionCommand.getCommandIdAndLabelLogString()));
TermuxTask.processTermuxTaskResult(null, executionCommand);
return null;
}
Logger.logDebug(LOG_TAG, executionCommand.toString());
String taskName = ShellUtils.getExecutableBasename(executionCommand.executable);
if (executionCommand.commandLabel == null)
executionCommand.commandLabel = taskName;
// Exec the process
final Process process;
try {
process = Runtime.getRuntime().exec(commandArray, env, new File(executionCommand.workingDirectory));
} catch (IOException e) {
executionCommand.setStateFailed(Errno.ERRNO_FAILED.getCode(), context.getString(R.string.error_failed_to_execute_termux_task_command, executionCommand.getCommandIdAndLabelLogString()), e);
TermuxTask.processTermuxTaskResult(null, executionCommand);
return null;
}
final TermuxTask termuxTask = new TermuxTask(process, executionCommand, termuxTaskClient);
if (isSynchronous) {
try {
termuxTask.executeInner(context);
} catch (IllegalThreadStateException | InterruptedException e) {
// TODO: Should either of these be handled or returned?
}
} else {
new Thread() {
@Override
public void run() {
try {
termuxTask.executeInner(context);
} catch (IllegalThreadStateException | InterruptedException e) {
// TODO: Should either of these be handled or returned?
}
}
}.start();
}
return termuxTask;
}
/**
* Sets up stdout and stderr readers for the {@link #mProcess} and waits for the process to end.
*
* If the processes finishes, then sets {@link ResultData#stdout}, {@link ResultData#stderr}
* and {@link ResultData#exitCode} for the {@link #mExecutionCommand} of the {@code termuxTask}
* and then calls {@link #processTermuxTaskResult(TermuxTask, ExecutionCommand) to process the result}.
*
* @param context The {@link Context} for operations.
*/
private void executeInner(@NonNull final Context context) throws IllegalThreadStateException, InterruptedException {
final int pid = ShellUtils.getPid(mProcess);
Logger.logDebug(LOG_TAG, "Running \"" + mExecutionCommand.getCommandIdAndLabelLogString() + "\" TermuxTask with pid " + pid);
mExecutionCommand.resultData.exitCode = null;
// setup stdin, and stdout and stderr gobblers
DataOutputStream STDIN = new DataOutputStream(mProcess.getOutputStream());
StreamGobbler STDOUT = new StreamGobbler(pid + "-stdout", mProcess.getInputStream(), mExecutionCommand.resultData.stdout);
StreamGobbler STDERR = new StreamGobbler(pid + "-stderr", mProcess.getErrorStream(), mExecutionCommand.resultData.stderr);
// start gobbling
STDOUT.start();
STDERR.start();
if (mExecutionCommand.stdin != null && !mExecutionCommand.stdin.isEmpty()) {
try {
STDIN.write((mExecutionCommand.stdin + "\n").getBytes(StandardCharsets.UTF_8));
STDIN.flush();
STDIN.close();
//STDIN.write("exit\n".getBytes(StandardCharsets.UTF_8));
//STDIN.flush();
} catch(IOException e) {
if (e.getMessage() != null && (e.getMessage().contains("EPIPE") || e.getMessage().contains("Stream closed"))) {
// Method most horrid to catch broken pipe, in which case we
// do nothing. The command is not a shell, the shell closed
// STDIN, the script already contained the exit command, etc.
// these cases we want the output instead of returning null.
} else {
// other issues we don't know how to handle, leads to
// returning null
mExecutionCommand.setStateFailed(Errno.ERRNO_FAILED.getCode(), context.getString(R.string.error_exception_received_while_executing_termux_task_command, mExecutionCommand.getCommandIdAndLabelLogString(), e.getMessage()), e);
mExecutionCommand.resultData.exitCode = 1;
TermuxTask.processTermuxTaskResult(this, null);
kill();
return;
}
}
}
// wait for our process to finish, while we gobble away in the background
int exitCode = mProcess.waitFor();
// make sure our threads are done gobbling
// and the process is destroyed - while the latter shouldn't be
// needed in theory, and may even produce warnings, in "normal" Java
// they are required for guaranteed cleanup of resources, so lets be
// safe and do this on Android as well
try {
STDIN.close();
} catch (IOException e) {
// might be closed already
}
STDOUT.join();
STDERR.join();
mProcess.destroy();
// Process result
if (exitCode == 0)
Logger.logDebug(LOG_TAG, "The \"" + mExecutionCommand.getCommandIdAndLabelLogString() + "\" TermuxTask with pid " + pid + " exited normally");
else
Logger.logDebug(LOG_TAG, "The \"" + mExecutionCommand.getCommandIdAndLabelLogString() + "\" TermuxTask with pid " + pid + " exited with code: " + exitCode);
// If the execution command has already failed, like SIGKILL was sent, then don't continue
if (mExecutionCommand.isStateFailed()) {
Logger.logDebug(LOG_TAG, "Ignoring setting \"" + mExecutionCommand.getCommandIdAndLabelLogString() + "\" TermuxTask state to ExecutionState.EXECUTED and processing results since it has already failed");
return;
}
mExecutionCommand.resultData.exitCode = exitCode;
if (!mExecutionCommand.setState(ExecutionState.EXECUTED))
return;
TermuxTask.processTermuxTaskResult(this, null);
}
/**
* Kill this {@link TermuxTask} by sending a {@link OsConstants#SIGILL} to its {@link #mProcess}
* if its still executing.
*
* @param context The {@link Context} for operations.
* @param processResult If set to {@code true}, then the {@link #processTermuxTaskResult(TermuxTask, ExecutionCommand)}
* will be called to process the failure.
*/
public void killIfExecuting(@NonNull final Context context, boolean processResult) {
// If execution command has already finished executing, then no need to process results or send SIGKILL
if (mExecutionCommand.hasExecuted()) {
Logger.logDebug(LOG_TAG, "Ignoring sending SIGKILL to \"" + mExecutionCommand.getCommandIdAndLabelLogString() + "\" TermuxTask since it has already finished executing");
return;
}
Logger.logDebug(LOG_TAG, "Send SIGKILL to \"" + mExecutionCommand.getCommandIdAndLabelLogString() + "\" TermuxTask");
if (mExecutionCommand.setStateFailed(Errno.ERRNO_FAILED.getCode(), context.getString(R.string.error_sending_sigkill_to_process))) {
if (processResult) {
mExecutionCommand.resultData.exitCode = 137; // SIGKILL
TermuxTask.processTermuxTaskResult(this, null);
}
}
if (mExecutionCommand.isExecuting()) {
kill();
}
}
/**
* Kill this {@link TermuxTask} by sending a {@link OsConstants#SIGILL} to its {@link #mProcess}.
*/
public void kill() {
int pid = ShellUtils.getPid(mProcess);
try {
// Send SIGKILL to process
Os.kill(pid, OsConstants.SIGKILL);
} catch (ErrnoException e) {
Logger.logWarn(LOG_TAG, "Failed to send SIGKILL to \"" + mExecutionCommand.getCommandIdAndLabelLogString() + "\" TermuxTask with pid " + pid + ": " + e.getMessage());
}
}
/**
* Process the results of {@link TermuxTask} or {@link ExecutionCommand}.
*
* Only one of {@code termuxTask} and {@code executionCommand} must be set.
*
* If the {@code termuxTask} and its {@link #mTermuxTaskClient} are not {@code null},
* then the {@link TermuxTaskClient#onTermuxTaskExited(TermuxTask)} callback will be called.
*
* @param termuxTask The {@link TermuxTask}, which should be set if
* {@link #execute(Context, ExecutionCommand, TermuxTaskClient, ShellEnvironmentClient, boolean)}
* successfully started the process.
* @param executionCommand The {@link ExecutionCommand}, which should be set if
* {@link #execute(Context, ExecutionCommand, TermuxTaskClient, ShellEnvironmentClient, boolean)}
* failed to start the process.
*/
private static void processTermuxTaskResult(final TermuxTask termuxTask, ExecutionCommand executionCommand) {
if (termuxTask != null)
executionCommand = termuxTask.mExecutionCommand;
if (executionCommand == null) return;
if (executionCommand.shouldNotProcessResults()) {
Logger.logDebug(LOG_TAG, "Ignoring duplicate call to process \"" + executionCommand.getCommandIdAndLabelLogString() + "\" TermuxTask result");
return;
}
Logger.logDebug(LOG_TAG, "Processing \"" + executionCommand.getCommandIdAndLabelLogString() + "\" TermuxTask result");
if (termuxTask != null && termuxTask.mTermuxTaskClient != null) {
termuxTask.mTermuxTaskClient.onTermuxTaskExited(termuxTask);
} else {
// If a callback is not set and execution command didn't fail, then we set success state now
// Otherwise, the callback host can set it himself when its done with the termuxTask
if (!executionCommand.isStateFailed())
executionCommand.setState(ExecutionCommand.ExecutionState.SUCCESS);
}
}
public Process getProcess() {
return mProcess;
}
public ExecutionCommand getExecutionCommand() {
return mExecutionCommand;
}
public interface TermuxTaskClient {
/**
* Callback function for when {@link TermuxTask} exits.
*
* @param termuxTask The {@link TermuxTask} that exited.
*/
void onTermuxTaskExited(TermuxTask termuxTask);
}
}