mirror of
https://github.com/fankes/termux-app.git
synced 2025-09-07 03:05:18 +08:00
316 lines
15 KiB
Java
316 lines
15 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.data.DataUtils;
|
|
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;
|
|
}
|
|
|
|
// No need to log stdin if logging is disabled, like for app internal scripts
|
|
Logger.logDebugExtended(LOG_TAG, ExecutionCommand.getExecutionInputLogString(executionCommand,
|
|
true, Logger.shouldEnableLoggingForCustomLogLevel(executionCommand.backgroundCustomLogLevel)));
|
|
|
|
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, mExecutionCommand.backgroundCustomLogLevel);
|
|
StreamGobbler STDERR = new StreamGobbler(pid + "-stderr", mProcess.getErrorStream(), mExecutionCommand.resultData.stderr, mExecutionCommand.backgroundCustomLogLevel);
|
|
|
|
// start gobbling
|
|
STDOUT.start();
|
|
STDERR.start();
|
|
|
|
if (!DataUtils.isNullOrEmpty(mExecutionCommand.stdin)) {
|
|
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);
|
|
|
|
}
|
|
|
|
}
|