mirror of
https://github.com/fankes/termux-app.git
synced 2025-09-07 03:05:18 +08:00
When `Logger.CURRENT_LOG_LEVEL` set by user is `Logger.LOG_VERBOSE`, then background (not foreground sessions) command output was being logged to logcat, however, if command outputted too much data to logcat, then logcat clients like in Android Studio would crash. Also if a logcat dump is being taken inside termux, then duplicate lines would occur, first one due to of original entry, and second one due to StreamGobbler logging output at verbose level for logcat command. This would be a concern for plugins as well like `RUN_COMMAND` intent or Termux:Tasker, etc if they ran commands with lot of data and user had set log level to verbose. For plugins, TermuxService now supports `com.termux.execute.background_custom_log_level` `String` extra for custom log level. Termux:Tasker, etc will have to be updated with support. For `RUN_COMMAND` intent, the `com.termux.RUN_COMMAND_BACKGROUND_CUSTOM_LOG_LEVEL` `String` extra is now provided to set custom log level for only the command output. Check `TermuxConstants`. So one can pass a custom log level that is `>=` to the log level set it termux settings where (OFF=0, NORMAL=1, DEBUG=2, VERBOSE=3). If you pass `0`, it will completely disable logging. If you pass `1`, logging will only be enabled if log level in termux settings is `NORMAL` or higher. If custom log level is not passed, then old behaviour will remain and log level in termux settings must be `VERBOSE` or higher for logging to be enabled. Note that the log entries will still be logged with priority `Log.VERBOSE` regardless of log level, i.e `logcat` will have `V/`. The entries logcat component has now changed from `StreamGobbler` to `TermuxCommand`. For output at `stdout`, the entry format is `[<pid>-stdout] ...` and for the output at `stderr`, the entry format is `[<pid>-stderr] ...`. The `<pid>` will be process id as an integer that was started by termux. For example: `V/TermuxCommand: [66666-stdout] ...`. While doing this I realize that instead of using `am` command to send messages back to tasker, you can use tasker `Logcat Entry` profile event to listen to messages from termux at both `stdout` and `stderr`. This might be faster than `am` command intent systems or at least possibly more convenient in some use cases. So setup a profile with the `Component` value set to `TermuxCommand` and `Filter` value set to `-E 'TermuxCommand: \[[0-9]+-((stdout)|(stderr))\] message_tag: .*'` and enable the `Grep Filter` toggle so that entry matching is done in native code. Check https://github.com/joaomgcd/TaskerDocumentation/blob/master/en/help/logcat%20info.md for details. Also enable `Enforce Task Order` in profile settings and set collision handling to `Run Both Together` so that if two or more entries are sent quickly, entry task is run for all. Tasker currently (v5.13.16) is not maintaining order of entry tasks despite the setting. Then you can send an intent from tasker via `Run Shell` action with `root` (since `am` command won't work without it on android >=8) or normally in termux from a script, you should be able to receive the entries as `@lc_text` in entry task of tasker `Logcat Entry` profile. The following just passes two `echo` commands to `bash` as a script via `stdin`. If you don't have root, then you can call a wrapper script with `TermuxCommand` function in `Tasker Function` action that sends another `RUN_COMMAND` intent with termux provide `am` command which will work without root. ``` am startservice --user 0 -n com.termux/com.termux.app.RunCommandService -a com.termux.RUN_COMMAND --es com.termux.RUN_COMMAND_PATH '/data/data/com.termux/files/usr/bin/bash' --es com.termux.RUN_COMMAND_STDIN 'echo "message_tag: Sending message from tasker to termux"' --ez com.termux.RUN_COMMAND_BACKGROUND true --es com.termux.RUN_COMMAND_BACKGROUND_CUSTOM_LOG_LEVEL '1' ```
313 lines
14 KiB
Java
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, mExecutionCommand.backgroundCustomLogLevel);
|
|
StreamGobbler STDERR = new StreamGobbler(pid + "-stderr", mProcess.getErrorStream(), mExecutionCommand.resultData.stderr, mExecutionCommand.backgroundCustomLogLevel);
|
|
|
|
// 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);
|
|
|
|
}
|
|
|
|
}
|