Files
termux-app/termux-shared/src/main/java/com/termux/shared/models/ExecutionCommand.java
agnostic-apollo 60f37bde8d Changed!: StreamGobbler needs to be passed log level parameter
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'
```
2021-08-20 06:19:25 +05:00

533 lines
21 KiB
Java

package com.termux.shared.models;
import android.content.Intent;
import android.net.Uri;
import androidx.annotation.NonNull;
import com.termux.shared.data.IntentUtils;
import com.termux.shared.models.errors.Error;
import com.termux.shared.logger.Logger;
import com.termux.shared.markdown.MarkdownUtils;
import com.termux.shared.data.DataUtils;
import java.util.Collections;
import java.util.List;
public class ExecutionCommand {
/*
The {@link ExecutionState#SUCCESS} and {@link ExecutionState#FAILED} is defined based on
successful execution of command without any internal errors or exceptions being raised.
The shell command {@link #exitCode} being non-zero **does not** mean that execution command failed.
Only the {@link #errCode} being non-zero means that execution command failed from the Termux app
perspective.
*/
/** The {@link Enum} that defines {@link ExecutionCommand} state. */
public enum ExecutionState {
PRE_EXECUTION("Pre-Execution", 0),
EXECUTING("Executing", 1),
EXECUTED("Executed", 2),
SUCCESS("Success", 3),
FAILED("Failed", 4);
private final String name;
private final int value;
ExecutionState(final String name, final int value) {
this.name = name;
this.value = value;
}
public String getName() {
return name;
}
public int getValue() {
return value;
}
}
/** The optional unique id for the {@link ExecutionCommand}. */
public Integer id;
/** The current state of the {@link ExecutionCommand}. */
private ExecutionState currentState = ExecutionState.PRE_EXECUTION;
/** The previous state of the {@link ExecutionCommand}. */
private ExecutionState previousState = ExecutionState.PRE_EXECUTION;
/** The executable for the {@link ExecutionCommand}. */
public String executable;
/** The executable Uri for the {@link ExecutionCommand}. */
public Uri executableUri;
/** The executable arguments array for the {@link ExecutionCommand}. */
public String[] arguments;
/** The stdin string for the {@link ExecutionCommand}. */
public String stdin;
/** The current working directory for the {@link ExecutionCommand}. */
public String workingDirectory;
/** The terminal transcript rows for the {@link ExecutionCommand}. */
public Integer terminalTranscriptRows;
/** If the {@link ExecutionCommand} is a background or a foreground terminal session command. */
public boolean inBackground;
/** If the {@link ExecutionCommand} is meant to start a failsafe terminal session. */
public boolean isFailsafe;
/**
* The {@link ExecutionCommand} custom log level for background {@link com.termux.shared.shell.TermuxTask}
* commands. By default, @link com.termux.shared.shell.StreamGobbler} only logs if {@link Logger}
* `CURRENT_LOG_LEVEL` is >= {@link Logger#LOG_LEVEL_VERBOSE}.
*/
public Integer backgroundCustomLogLevel;
/** The session action of foreground commands. */
public String sessionAction;
/** The command label for the {@link ExecutionCommand}. */
public String commandLabel;
/** The markdown text for the command description for the {@link ExecutionCommand}. */
public String commandDescription;
/** The markdown text for the help of command for the {@link ExecutionCommand}. This can be used
* to provide useful info to the user if an internal error is raised. */
public String commandHelp;
/** Defines the markdown text for the help of the Termux plugin API that was used to start the
* {@link ExecutionCommand}. This can be used to provide useful info to the user if an internal
* error is raised. */
public String pluginAPIHelp;
/** Defines the {@link Intent} received which started the command. */
public Intent commandIntent;
/** Defines if {@link ExecutionCommand} was started because of an external plugin request
* like with an intent or from within Termux app itself. */
public boolean isPluginExecutionCommand;
/** Defines the {@link ResultConfig} for the {@link ExecutionCommand} containing information
* on how to handle the result. */
public final ResultConfig resultConfig = new ResultConfig();
/** Defines the {@link ResultData} for the {@link ExecutionCommand} containing information
* of the result. */
public final ResultData resultData = new ResultData();
/** Defines if processing results already called for this {@link ExecutionCommand}. */
public boolean processingResultsAlreadyCalled;
private static final String LOG_TAG = "ExecutionCommand";
public ExecutionCommand() {
}
public ExecutionCommand(Integer id) {
this.id = id;
}
public ExecutionCommand(Integer id, String executable, String[] arguments, String stdin, String workingDirectory, boolean inBackground, boolean isFailsafe) {
this.id = id;
this.executable = executable;
this.arguments = arguments;
this.stdin = stdin;
this.workingDirectory = workingDirectory;
this.inBackground = inBackground;
this.isFailsafe = isFailsafe;
}
public boolean isPluginExecutionCommandWithPendingResult() {
return isPluginExecutionCommand && resultConfig.isCommandWithPendingResult();
}
public synchronized boolean setState(ExecutionState newState) {
// The state transition cannot go back or change if already at {@link ExecutionState#SUCCESS}
if (newState.getValue() < currentState.getValue() || currentState == ExecutionState.SUCCESS) {
Logger.logError(LOG_TAG, "Invalid "+ getCommandIdAndLabelLogString() + " state transition from \"" + currentState.getName() + "\" to " + "\"" + newState.getName() + "\"");
return false;
}
// The {@link ExecutionState#FAILED} can be set again, like to add more errors, but we don't update
// {@link #previousState} with the {@link #currentState} value if its at {@link ExecutionState#FAILED} to
// preserve the last valid state
if (currentState != ExecutionState.FAILED)
previousState = currentState;
currentState = newState;
return true;
}
public synchronized boolean hasExecuted() {
return currentState.getValue() >= ExecutionState.EXECUTED.getValue();
}
public synchronized boolean isExecuting() {
return currentState == ExecutionState.EXECUTING;
}
public synchronized boolean isSuccessful() {
return currentState == ExecutionState.SUCCESS;
}
public synchronized boolean setStateFailed(@NonNull Error error) {
return setStateFailed(error.getType(), error.getCode(), error.getMessage(), null);
}
public synchronized boolean setStateFailed(@NonNull Error error, Throwable throwable) {
return setStateFailed(error.getType(), error.getCode(), error.getMessage(), Collections.singletonList(throwable));
}
public synchronized boolean setStateFailed(@NonNull Error error, List<Throwable> throwablesList) {
return setStateFailed(error.getType(), error.getCode(), error.getMessage(), throwablesList);
}
public synchronized boolean setStateFailed(int code, String message) {
return setStateFailed(null, code, message, null);
}
public synchronized boolean setStateFailed(int code, String message, Throwable throwable) {
return setStateFailed(null, code, message, Collections.singletonList(throwable));
}
public synchronized boolean setStateFailed(int code, String message, List<Throwable> throwablesList) {
return setStateFailed(null, code, message, throwablesList);
}
public synchronized boolean setStateFailed(String type, int code, String message, List<Throwable> throwablesList) {
if (!this.resultData.setStateFailed(type, code, message, throwablesList)) {
Logger.logWarn(LOG_TAG, "setStateFailed for " + getCommandIdAndLabelLogString() + " resultData encountered an error.");
}
return setState(ExecutionState.FAILED);
}
public synchronized boolean shouldNotProcessResults() {
if (processingResultsAlreadyCalled) {
return true;
} else {
processingResultsAlreadyCalled = true;
return false;
}
}
public synchronized boolean isStateFailed() {
if (currentState != ExecutionState.FAILED)
return false;
if (!resultData.isStateFailed()) {
Logger.logWarn(LOG_TAG, "The " + getCommandIdAndLabelLogString() + " has an invalid errCode value set in errors list while having ExecutionState.FAILED state.\n" + resultData.errorsList);
return false;
} else {
return true;
}
}
@NonNull
@Override
public String toString() {
if (!hasExecuted())
return getExecutionInputLogString(this, true);
else {
return getExecutionOutputLogString(this, true, true);
}
}
/**
* Get a log friendly {@link String} for {@link ExecutionCommand} execution input parameters.
*
* @param executionCommand The {@link ExecutionCommand} to convert.
* @param ignoreNull Set to {@code true} if non-critical {@code null} values are to be ignored.
* @return Returns the log friendly {@link String}.
*/
public static String getExecutionInputLogString(final ExecutionCommand executionCommand, boolean ignoreNull) {
if (executionCommand == null) return "null";
StringBuilder logString = new StringBuilder();
logString.append(executionCommand.getCommandIdAndLabelLogString()).append(":");
if (executionCommand.previousState != ExecutionState.PRE_EXECUTION)
logString.append("\n").append(executionCommand.getPreviousStateLogString());
logString.append("\n").append(executionCommand.getCurrentStateLogString());
logString.append("\n").append(executionCommand.getExecutableLogString());
logString.append("\n").append(executionCommand.getArgumentsLogString());
logString.append("\n").append(executionCommand.getWorkingDirectoryLogString());
logString.append("\n").append(executionCommand.getInBackgroundLogString());
logString.append("\n").append(executionCommand.getIsFailsafeLogString());
if (executionCommand.inBackground && (!ignoreNull || executionCommand.backgroundCustomLogLevel != null))
logString.append("\n").append(executionCommand.getBackgroundCustomLogLevelLogString());
if (!ignoreNull || executionCommand.sessionAction != null)
logString.append("\n").append(executionCommand.getSessionActionLogString());
if (!ignoreNull || executionCommand.commandIntent != null)
logString.append("\n").append(executionCommand.getCommandIntentLogString());
logString.append("\n").append(executionCommand.getIsPluginExecutionCommandLogString());
if (executionCommand.isPluginExecutionCommand)
logString.append("\n").append(ResultConfig.getResultConfigLogString(executionCommand.resultConfig, ignoreNull));
return logString.toString();
}
/**
* Get a log friendly {@link String} for {@link ExecutionCommand} execution output parameters.
*
* @param executionCommand The {@link ExecutionCommand} to convert.
* @param ignoreNull Set to {@code true} if non-critical {@code null} values are to be ignored.
* @param logResultData Set to {@code true} if {@link #resultData} should be logged.
* @return Returns the log friendly {@link String}.
*/
public static String getExecutionOutputLogString(final ExecutionCommand executionCommand, boolean ignoreNull, boolean logResultData) {
if (executionCommand == null) return "null";
StringBuilder logString = new StringBuilder();
logString.append(executionCommand.getCommandIdAndLabelLogString()).append(":");
logString.append("\n").append(executionCommand.getPreviousStateLogString());
logString.append("\n").append(executionCommand.getCurrentStateLogString());
if (logResultData)
logString.append("\n").append(ResultData.getResultDataLogString(executionCommand.resultData, ignoreNull));
return logString.toString();
}
/**
* Get a log friendly {@link String} for {@link ExecutionCommand} with more details.
*
* @param executionCommand The {@link ExecutionCommand} to convert.
* @return Returns the log friendly {@link String}.
*/
public static String getDetailedLogString(final ExecutionCommand executionCommand) {
if (executionCommand == null) return "null";
StringBuilder logString = new StringBuilder();
logString.append(getExecutionInputLogString(executionCommand, false));
logString.append(getExecutionOutputLogString(executionCommand, false, true));
logString.append("\n").append(executionCommand.getCommandDescriptionLogString());
logString.append("\n").append(executionCommand.getCommandHelpLogString());
logString.append("\n").append(executionCommand.getPluginAPIHelpLogString());
return logString.toString();
}
/**
* Get a markdown {@link String} for {@link ExecutionCommand}.
*
* @param executionCommand The {@link ExecutionCommand} to convert.
* @return Returns the markdown {@link String}.
*/
public static String getExecutionCommandMarkdownString(final ExecutionCommand executionCommand) {
if (executionCommand == null) return "null";
if (executionCommand.commandLabel == null) executionCommand.commandLabel = "Execution Command";
StringBuilder markdownString = new StringBuilder();
markdownString.append("## ").append(executionCommand.commandLabel).append("\n");
markdownString.append("\n").append(MarkdownUtils.getSingleLineMarkdownStringEntry("Previous State", executionCommand.previousState.getName(), "-"));
markdownString.append("\n").append(MarkdownUtils.getSingleLineMarkdownStringEntry("Current State", executionCommand.currentState.getName(), "-"));
markdownString.append("\n").append(MarkdownUtils.getSingleLineMarkdownStringEntry("Executable", executionCommand.executable, "-"));
markdownString.append("\n").append(getArgumentsMarkdownString(executionCommand.arguments));
markdownString.append("\n").append(MarkdownUtils.getSingleLineMarkdownStringEntry("Working Directory", executionCommand.workingDirectory, "-"));
markdownString.append("\n").append(MarkdownUtils.getSingleLineMarkdownStringEntry("inBackground", executionCommand.inBackground, "-"));
markdownString.append("\n").append(MarkdownUtils.getSingleLineMarkdownStringEntry("isFailsafe", executionCommand.isFailsafe, "-"));
if (executionCommand.inBackground && executionCommand.backgroundCustomLogLevel != null)
markdownString.append("\n").append(MarkdownUtils.getSingleLineMarkdownStringEntry("Background Custom Log Level", executionCommand.backgroundCustomLogLevel, "-"));
markdownString.append("\n").append(MarkdownUtils.getSingleLineMarkdownStringEntry("Session Action", executionCommand.sessionAction, "-"));
markdownString.append("\n").append(MarkdownUtils.getSingleLineMarkdownStringEntry("isPluginExecutionCommand", executionCommand.isPluginExecutionCommand, "-"));
markdownString.append("\n\n").append(ResultConfig.getResultConfigMarkdownString(executionCommand.resultConfig));
markdownString.append("\n\n").append(ResultData.getResultDataMarkdownString(executionCommand.resultData));
if (executionCommand.commandDescription != null || executionCommand.commandHelp != null) {
if (executionCommand.commandDescription != null)
markdownString.append("\n\n### Command Description\n\n").append(executionCommand.commandDescription).append("\n");
if (executionCommand.commandHelp != null)
markdownString.append("\n\n### Command Help\n\n").append(executionCommand.commandHelp).append("\n");
markdownString.append("\n##\n");
}
if (executionCommand.pluginAPIHelp != null) {
markdownString.append("\n\n### Plugin API Help\n\n").append(executionCommand.pluginAPIHelp);
markdownString.append("\n##\n");
}
return markdownString.toString();
}
public String getIdLogString() {
if (id != null)
return "(" + id + ") ";
else
return "";
}
public String getCurrentStateLogString() {
return "Current State: `" + currentState.getName() + "`";
}
public String getPreviousStateLogString() {
return "Previous State: `" + previousState.getName() + "`";
}
public String getCommandLabelLogString() {
if (commandLabel != null && !commandLabel.isEmpty())
return commandLabel;
else
return "Execution Command";
}
public String getCommandIdAndLabelLogString() {
return getIdLogString() + getCommandLabelLogString();
}
public String getExecutableLogString() {
return "Executable: `" + executable + "`";
}
public String getArgumentsLogString() {
return getArgumentsLogString(arguments);
}
public String getWorkingDirectoryLogString() {
return "Working Directory: `" + workingDirectory + "`";
}
public String getInBackgroundLogString() {
return "inBackground: `" + inBackground + "`";
}
public String getIsFailsafeLogString() {
return "isFailsafe: `" + isFailsafe + "`";
}
public String getBackgroundCustomLogLevelLogString() {
return "Background Custom Log Level: `" + backgroundCustomLogLevel + "`";
}
public String getSessionActionLogString() {
return Logger.getSingleLineLogStringEntry("Session Action", sessionAction, "-");
}
public String getCommandDescriptionLogString() {
return Logger.getSingleLineLogStringEntry("Command Description", commandDescription, "-");
}
public String getCommandHelpLogString() {
return Logger.getSingleLineLogStringEntry("Command Help", commandHelp, "-");
}
public String getPluginAPIHelpLogString() {
return Logger.getSingleLineLogStringEntry("Plugin API Help", pluginAPIHelp, "-");
}
public String getCommandIntentLogString() {
if (commandIntent == null)
return "Command Intent: -";
else
return Logger.getMultiLineLogStringEntry("Command Intent", IntentUtils.getIntentString(commandIntent), "-");
}
public String getIsPluginExecutionCommandLogString() {
return "isPluginExecutionCommand: `" + isPluginExecutionCommand + "`";
}
/**
* Get a log friendly {@link String} for {@link List<String>} argumentsArray.
* If argumentsArray are null or of size 0, then `Arguments: -` is returned. Otherwise
* following format is returned:
*
* Arguments:
* ```
* Arg 1: `value`
* Arg 2: 'value`
* ```
*
* @param argumentsArray The {@link String[]} argumentsArray to convert.
* @return Returns the log friendly {@link String}.
*/
public static String getArgumentsLogString(final String[] argumentsArray) {
StringBuilder argumentsString = new StringBuilder("Arguments:");
if (argumentsArray != null && argumentsArray.length != 0) {
argumentsString.append("\n```\n");
for (int i = 0; i != argumentsArray.length; i++) {
argumentsString.append(Logger.getSingleLineLogStringEntry("Arg " + (i + 1),
DataUtils.getTruncatedCommandOutput(argumentsArray[i], Logger.LOGGER_ENTRY_MAX_SAFE_PAYLOAD / 5, true, false, true),
"-")).append("\n");
}
argumentsString.append("```");
} else{
argumentsString.append(" -");
}
return argumentsString.toString();
}
/**
* Get a markdown {@link String} for {@link String[]} argumentsArray.
* If argumentsArray are null or of size 0, then `**Arguments:** -` is returned. Otherwise
* following format is returned:
*
* **Arguments:**
*
* **Arg 1:**
* ```
* value
* ```
* **Arg 2:**
* ```
* value
*```
*
* @param argumentsArray The {@link String[]} argumentsArray to convert.
* @return Returns the markdown {@link String}.
*/
public static String getArgumentsMarkdownString(final String[] argumentsArray) {
StringBuilder argumentsString = new StringBuilder("**Arguments:**");
if (argumentsArray != null && argumentsArray.length != 0) {
argumentsString.append("\n");
for (int i = 0; i != argumentsArray.length; i++) {
argumentsString.append(MarkdownUtils.getMultiLineMarkdownStringEntry("Arg " + (i + 1), argumentsArray[i], "-")).append("\n");
}
} else{
argumentsString.append(" - ");
}
return argumentsString.toString();
}
}