Changed!: Move to package-by-feature hierarchy for classes not using it since termux-shared is growing too big and layers are getting out of hand

This commit is contained in:
agnostic-apollo
2021-10-26 07:04:08 +05:00
parent 549a772d45
commit 361bfb3961
47 changed files with 105 additions and 174 deletions

View File

@@ -0,0 +1,555 @@
package com.termux.shared.shell.command;
import android.content.Intent;
import android.net.Uri;
import androidx.annotation.NonNull;
import com.termux.shared.data.IntentUtils;
import com.termux.shared.shell.command.result.ResultConfig;
import com.termux.shared.shell.command.result.ResultData;
import com.termux.shared.errors.Error;
import com.termux.shared.logger.Logger;
import com.termux.shared.markdown.MarkdownUtils;
import com.termux.shared.data.DataUtils;
import com.termux.shared.shell.command.runner.app.TermuxTask;
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 TermuxTask}
* commands. By default, @link com.termux.shared.shell.StreamGobbler} only logs stdout and
* stderr if {@link Logger} `CURRENT_LOG_LEVEL` is >= {@link Logger#LOG_LEVEL_VERBOSE} and
* {@link TermuxTask} only logs stdin if `CURRENT_LOG_LEVEL` is >=
* {@link Logger#LOG_LEVEL_DEBUG}.
*/
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, true);
else {
return getExecutionOutputLogString(this, true, 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.
* @param logStdin Set to {@code true} if {@link #stdin} should be logged.
* @return Returns the log friendly {@link String}.
*/
public static String getExecutionInputLogString(final ExecutionCommand executionCommand, boolean ignoreNull, boolean logStdin) {
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) {
if (logStdin && (!ignoreNull || !DataUtils.isNullOrEmpty(executionCommand.stdin)))
logString.append("\n").append(executionCommand.getStdinLogString());
if (!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.
* @param logStdoutAndStderr Set to {@code true} if {@link ResultData#stdout} and {@link ResultData#stderr} should be logged.
* @return Returns the log friendly {@link String}.
*/
public static String getExecutionOutputLogString(final ExecutionCommand executionCommand, boolean ignoreNull, boolean logResultData, boolean logStdoutAndStderr) {
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, logStdoutAndStderr));
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, true));
logString.append(getExecutionOutputLogString(executionCommand, false, true, 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) {
if (!DataUtils.isNullOrEmpty(executionCommand.stdin))
markdownString.append("\n").append(MarkdownUtils.getMultiLineMarkdownStringEntry("Stdin", executionCommand.stdin, "-"));
if (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 getStdinLogString() {
if (DataUtils.isNullOrEmpty(stdin))
return "Stdin: -";
else
return Logger.getMultiLineLogStringEntry("Stdin", stdin, "-");
}
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();
}
}

View File

@@ -0,0 +1,170 @@
package com.termux.shared.shell.command.result;
import android.app.PendingIntent;
import androidx.annotation.NonNull;
import com.termux.shared.logger.Logger;
import com.termux.shared.markdown.MarkdownUtils;
import java.util.Formatter;
public class ResultConfig {
/** Defines {@link PendingIntent} that should be sent with the result of the command. We cannot
* implement {@link java.io.Serializable} because {@link PendingIntent} cannot be serialized. */
public PendingIntent resultPendingIntent;
/** The key with which to send result {@link android.os.Bundle} in {@link #resultPendingIntent}. */
public String resultBundleKey;
/** The key with which to send {@link ResultData#stdout} in {@link #resultPendingIntent}. */
public String resultStdoutKey;
/** The key with which to send {@link ResultData#stderr} in {@link #resultPendingIntent}. */
public String resultStderrKey;
/** The key with which to send {@link ResultData#exitCode} in {@link #resultPendingIntent}. */
public String resultExitCodeKey;
/** The key with which to send {@link ResultData#errorsList} errCode in {@link #resultPendingIntent}. */
public String resultErrCodeKey;
/** The key with which to send {@link ResultData#errorsList} errmsg in {@link #resultPendingIntent}. */
public String resultErrmsgKey;
/** The key with which to send original length of {@link ResultData#stdout} in {@link #resultPendingIntent}. */
public String resultStdoutOriginalLengthKey;
/** The key with which to send original length of {@link ResultData#stderr} in {@link #resultPendingIntent}. */
public String resultStderrOriginalLengthKey;
/** Defines the directory path in which to write the result of the command. */
public String resultDirectoryPath;
/** Defines the directory path under which {@link #resultDirectoryPath} can exist. */
public String resultDirectoryAllowedParentPath;
/** Defines whether the result should be written to a single file or multiple files
* (err, error, stdout, stderr, exit_code) in {@link #resultDirectoryPath}. */
public boolean resultSingleFile;
/** Defines the basename of the result file that should be created in {@link #resultDirectoryPath}
* if {@link #resultSingleFile} is {@code true}. */
public String resultFileBasename;
/** Defines the output {@link Formatter} format of the {@link #resultFileBasename} result file. */
public String resultFileOutputFormat;
/** Defines the error {@link Formatter} format of the {@link #resultFileBasename} result file. */
public String resultFileErrorFormat;
/** Defines the suffix of the result files that should be created in {@link #resultDirectoryPath}
* if {@link #resultSingleFile} is {@code true}. */
public String resultFilesSuffix;
public ResultConfig() {
}
public boolean isCommandWithPendingResult() {
return resultPendingIntent != null || resultDirectoryPath != null;
}
@NonNull
@Override
public String toString() {
return getResultConfigLogString(this, true);
}
/**
* Get a log friendly {@link String} for {@link ResultConfig} parameters.
*
* @param resultConfig The {@link ResultConfig} 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 getResultConfigLogString(final ResultConfig resultConfig, boolean ignoreNull) {
if (resultConfig == null) return "null";
StringBuilder logString = new StringBuilder();
logString.append("Result Pending: `").append(resultConfig.isCommandWithPendingResult()).append("`\n");
if (resultConfig.resultPendingIntent != null) {
logString.append(resultConfig.getResultPendingIntentVariablesLogString(ignoreNull));
if (resultConfig.resultDirectoryPath != null)
logString.append("\n");
}
if (resultConfig.resultDirectoryPath != null && !resultConfig.resultDirectoryPath.isEmpty())
logString.append(resultConfig.getResultDirectoryVariablesLogString(ignoreNull));
return logString.toString();
}
public String getResultPendingIntentVariablesLogString(boolean ignoreNull) {
if (resultPendingIntent == null) return "Result PendingIntent Creator: -";
StringBuilder resultPendingIntentVariablesString = new StringBuilder();
resultPendingIntentVariablesString.append("Result PendingIntent Creator: `").append(resultPendingIntent.getCreatorPackage()).append("`");
if (!ignoreNull || resultBundleKey != null)
resultPendingIntentVariablesString.append("\n").append(Logger.getSingleLineLogStringEntry("Result Bundle Key", resultBundleKey, "-"));
if (!ignoreNull || resultStdoutKey != null)
resultPendingIntentVariablesString.append("\n").append(Logger.getSingleLineLogStringEntry("Result Stdout Key", resultStdoutKey, "-"));
if (!ignoreNull || resultStderrKey != null)
resultPendingIntentVariablesString.append("\n").append(Logger.getSingleLineLogStringEntry("Result Stderr Key", resultStderrKey, "-"));
if (!ignoreNull || resultExitCodeKey != null)
resultPendingIntentVariablesString.append("\n").append(Logger.getSingleLineLogStringEntry("Result Exit Code Key", resultExitCodeKey, "-"));
if (!ignoreNull || resultErrCodeKey != null)
resultPendingIntentVariablesString.append("\n").append(Logger.getSingleLineLogStringEntry("Result Err Code Key", resultErrCodeKey, "-"));
if (!ignoreNull || resultErrmsgKey != null)
resultPendingIntentVariablesString.append("\n").append(Logger.getSingleLineLogStringEntry("Result Error Key", resultErrmsgKey, "-"));
if (!ignoreNull || resultStdoutOriginalLengthKey != null)
resultPendingIntentVariablesString.append("\n").append(Logger.getSingleLineLogStringEntry("Result Stdout Original Length Key", resultStdoutOriginalLengthKey, "-"));
if (!ignoreNull || resultStderrOriginalLengthKey != null)
resultPendingIntentVariablesString.append("\n").append(Logger.getSingleLineLogStringEntry("Result Stderr Original Length Key", resultStderrOriginalLengthKey, "-"));
return resultPendingIntentVariablesString.toString();
}
public String getResultDirectoryVariablesLogString(boolean ignoreNull) {
if (resultDirectoryPath == null) return "Result Directory Path: -";
StringBuilder resultDirectoryVariablesString = new StringBuilder();
resultDirectoryVariablesString.append(Logger.getSingleLineLogStringEntry("Result Directory Path", resultDirectoryPath, "-"));
resultDirectoryVariablesString.append("\n").append(Logger.getSingleLineLogStringEntry("Result Single File", resultSingleFile, "-"));
if (!ignoreNull || resultFileBasename != null)
resultDirectoryVariablesString.append("\n").append(Logger.getSingleLineLogStringEntry("Result File Basename", resultFileBasename, "-"));
if (!ignoreNull || resultFileOutputFormat != null)
resultDirectoryVariablesString.append("\n").append(Logger.getSingleLineLogStringEntry("Result File Output Format", resultFileOutputFormat, "-"));
if (!ignoreNull || resultFileErrorFormat != null)
resultDirectoryVariablesString.append("\n").append(Logger.getSingleLineLogStringEntry("Result File Error Format", resultFileErrorFormat, "-"));
if (!ignoreNull || resultFilesSuffix != null)
resultDirectoryVariablesString.append("\n").append(Logger.getSingleLineLogStringEntry("Result Files Suffix", resultFilesSuffix, "-"));
return resultDirectoryVariablesString.toString();
}
/**
* Get a markdown {@link String} for {@link ResultConfig}.
*
* @param resultConfig The {@link ResultConfig} to convert.
* @return Returns the markdown {@link String}.
*/
public static String getResultConfigMarkdownString(final ResultConfig resultConfig) {
if (resultConfig == null) return "null";
StringBuilder markdownString = new StringBuilder();
if (resultConfig.resultPendingIntent != null)
markdownString.append(MarkdownUtils.getSingleLineMarkdownStringEntry("Result PendingIntent Creator", resultConfig.resultPendingIntent.getCreatorPackage(), "-"));
else
markdownString.append("**Result PendingIntent Creator:** - ");
if (resultConfig.resultDirectoryPath != null) {
markdownString.append("\n").append(MarkdownUtils.getSingleLineMarkdownStringEntry("Result Directory Path", resultConfig.resultDirectoryPath, "-"));
markdownString.append("\n").append(MarkdownUtils.getSingleLineMarkdownStringEntry("Result Single File", resultConfig.resultSingleFile, "-"));
markdownString.append("\n").append(MarkdownUtils.getSingleLineMarkdownStringEntry("Result File Basename", resultConfig.resultFileBasename, "-"));
markdownString.append("\n").append(MarkdownUtils.getSingleLineMarkdownStringEntry("Result File Output Format", resultConfig.resultFileOutputFormat, "-"));
markdownString.append("\n").append(MarkdownUtils.getSingleLineMarkdownStringEntry("Result File Error Format", resultConfig.resultFileErrorFormat, "-"));
markdownString.append("\n").append(MarkdownUtils.getSingleLineMarkdownStringEntry("Result Files Suffix", resultConfig.resultFilesSuffix, "-"));
}
return markdownString.toString();
}
}

View File

@@ -0,0 +1,258 @@
package com.termux.shared.shell.command.result;
import androidx.annotation.NonNull;
import com.termux.shared.data.DataUtils;
import com.termux.shared.logger.Logger;
import com.termux.shared.markdown.MarkdownUtils;
import com.termux.shared.errors.Errno;
import com.termux.shared.errors.Error;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
public class ResultData implements Serializable {
/** The stdout of command. */
public final StringBuilder stdout = new StringBuilder();
/** The stderr of command. */
public final StringBuilder stderr = new StringBuilder();
/** The exit code of command. */
public Integer exitCode;
/** The internal errors list of command. */
public List<Error> errorsList = new ArrayList<>();
public ResultData() {
}
public void clearStdout() {
stdout.setLength(0);
}
public StringBuilder prependStdout(String message) {
return stdout.insert(0, message);
}
public StringBuilder prependStdoutLn(String message) {
return stdout.insert(0, message + "\n");
}
public StringBuilder appendStdout(String message) {
return stdout.append(message);
}
public StringBuilder appendStdoutLn(String message) {
return stdout.append(message).append("\n");
}
public void clearStderr() {
stderr.setLength(0);
}
public StringBuilder prependStderr(String message) {
return stderr.insert(0, message);
}
public StringBuilder prependStderrLn(String message) {
return stderr.insert(0, message + "\n");
}
public StringBuilder appendStderr(String message) {
return stderr.append(message);
}
public StringBuilder appendStderrLn(String message) {
return stderr.append(message).append("\n");
}
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 (errorsList == null)
errorsList = new ArrayList<>();
Error error = new Error();
errorsList.add(error);
return error.setStateFailed(type, code, message, throwablesList);
}
public boolean isStateFailed() {
if (errorsList != null) {
for (Error error : errorsList)
if (error.isStateFailed())
return true;
}
return false;
}
public int getErrCode() {
if (errorsList != null && errorsList.size() > 0)
return errorsList.get(errorsList.size() - 1).getCode();
else
return Errno.ERRNO_SUCCESS.getCode();
}
@NonNull
@Override
public String toString() {
return getResultDataLogString(this, true);
}
/**
* Get a log friendly {@link String} for {@link ResultData} parameters.
*
* @param resultData The {@link ResultData} to convert.
* @param logStdoutAndStderr Set to {@code true} if {@link #stdout} and {@link #stderr} should be logged.
* @return Returns the log friendly {@link String}.
*/
public static String getResultDataLogString(final ResultData resultData, boolean logStdoutAndStderr) {
if (resultData == null) return "null";
StringBuilder logString = new StringBuilder();
if (logStdoutAndStderr) {
logString.append("\n").append(resultData.getStdoutLogString());
logString.append("\n").append(resultData.getStderrLogString());
}
logString.append("\n").append(resultData.getExitCodeLogString());
logString.append("\n\n").append(getErrorsListLogString(resultData));
return logString.toString();
}
public String getStdoutLogString() {
if (stdout.toString().isEmpty())
return Logger.getSingleLineLogStringEntry("Stdout", null, "-");
else
return Logger.getMultiLineLogStringEntry("Stdout", DataUtils.getTruncatedCommandOutput(stdout.toString(), Logger.LOGGER_ENTRY_MAX_SAFE_PAYLOAD / 5, false, false, true), "-");
}
public String getStderrLogString() {
if (stderr.toString().isEmpty())
return Logger.getSingleLineLogStringEntry("Stderr", null, "-");
else
return Logger.getMultiLineLogStringEntry("Stderr", DataUtils.getTruncatedCommandOutput(stderr.toString(), Logger.LOGGER_ENTRY_MAX_SAFE_PAYLOAD / 5, false, false, true), "-");
}
public String getExitCodeLogString() {
return Logger.getSingleLineLogStringEntry("Exit Code", exitCode, "-");
}
public static String getErrorsListLogString(final ResultData resultData) {
if (resultData == null) return "null";
StringBuilder logString = new StringBuilder();
if (resultData.errorsList != null) {
for (Error error : resultData.errorsList) {
if (error.isStateFailed()) {
if (!logString.toString().isEmpty())
logString.append("\n");
logString.append(Error.getErrorLogString(error));
}
}
}
return logString.toString();
}
/**
* Get a markdown {@link String} for {@link ResultData}.
*
* @param resultData The {@link ResultData} to convert.
* @return Returns the markdown {@link String}.
*/
public static String getResultDataMarkdownString(final ResultData resultData) {
if (resultData == null) return "null";
StringBuilder markdownString = new StringBuilder();
if (resultData.stdout.toString().isEmpty())
markdownString.append(MarkdownUtils.getSingleLineMarkdownStringEntry("Stdout", null, "-"));
else
markdownString.append(MarkdownUtils.getMultiLineMarkdownStringEntry("Stdout", resultData.stdout.toString(), "-"));
if (resultData.stderr.toString().isEmpty())
markdownString.append("\n").append(MarkdownUtils.getSingleLineMarkdownStringEntry("Stderr", null, "-"));
else
markdownString.append("\n").append(MarkdownUtils.getMultiLineMarkdownStringEntry("Stderr", resultData.stderr.toString(), "-"));
markdownString.append("\n").append(MarkdownUtils.getSingleLineMarkdownStringEntry("Exit Code", resultData.exitCode, "-"));
markdownString.append("\n\n").append(getErrorsListMarkdownString(resultData));
return markdownString.toString();
}
public static String getErrorsListMarkdownString(final ResultData resultData) {
if (resultData == null) return "null";
StringBuilder markdownString = new StringBuilder();
if (resultData.errorsList != null) {
for (Error error : resultData.errorsList) {
if (error.isStateFailed()) {
if (!markdownString.toString().isEmpty())
markdownString.append("\n");
markdownString.append(Error.getErrorMarkdownString(error));
}
}
}
return markdownString.toString();
}
public static String getErrorsListMinimalString(final ResultData resultData) {
if (resultData == null) return "null";
StringBuilder minimalString = new StringBuilder();
if (resultData.errorsList != null) {
for (Error error : resultData.errorsList) {
if (error.isStateFailed()) {
if (!minimalString.toString().isEmpty())
minimalString.append("\n");
minimalString.append(Error.getMinimalErrorString(error));
}
}
}
return minimalString.toString();
}
}

View File

@@ -0,0 +1,349 @@
package com.termux.shared.shell.command.result;
import android.app.Activity;
import android.app.PendingIntent;
import android.content.Context;
import android.content.Intent;
import android.os.Bundle;
import com.termux.shared.R;
import com.termux.shared.data.DataUtils;
import com.termux.shared.markdown.MarkdownUtils;
import com.termux.shared.errors.Error;
import com.termux.shared.file.FileUtils;
import com.termux.shared.logger.Logger;
import com.termux.shared.errors.FunctionErrno;
import com.termux.shared.android.AndroidUtils;
import com.termux.shared.shell.command.ShellCommandConstants.RESULT_SENDER;
public class ResultSender {
private static final String LOG_TAG = "ResultSender";
/**
* Send result stored in {@link ResultConfig} to command caller via
* {@link ResultConfig#resultPendingIntent} and/or by writing it to files in
* {@link ResultConfig#resultDirectoryPath}. If both are not {@code null}, then result will be
* sent via both.
*
* @param context The {@link Context} for operations.
* @param logTag The log tag to use for logging.
* @param label The label for the command.
* @param resultConfig The {@link ResultConfig} object containing information on how to send the result.
* @param resultData The {@link ResultData} object containing result data.
* @param logStdoutAndStderr Set to {@code true} if {@link ResultData#stdout} and {@link ResultData#stderr}
* should be logged.
* @return Returns the {@link Error} if failed to send the result, otherwise {@code null}.
*/
public static Error sendCommandResultData(Context context, String logTag, String label, ResultConfig resultConfig, ResultData resultData, boolean logStdoutAndStderr) {
if (context == null || resultConfig == null || resultData == null)
return FunctionErrno.ERRNO_NULL_OR_EMPTY_PARAMETERS.getError("context, resultConfig or resultData", "sendCommandResultData");
Error error;
if (resultConfig.resultPendingIntent != null) {
error = sendCommandResultDataWithPendingIntent(context, logTag, label, resultConfig, resultData, logStdoutAndStderr);
if (error != null || resultConfig.resultDirectoryPath == null)
return error;
}
if (resultConfig.resultDirectoryPath != null) {
return sendCommandResultDataToDirectory(context, logTag, label, resultConfig, resultData, logStdoutAndStderr);
} else {
return FunctionErrno.ERRNO_UNSET_PARAMETERS.getError("resultConfig.resultPendingIntent or resultConfig.resultDirectoryPath", "sendCommandResultData");
}
}
/**
* Send result stored in {@link ResultConfig} to command caller via {@link ResultConfig#resultPendingIntent}.
*
* @param context The {@link Context} for operations.
* @param logTag The log tag to use for logging.
* @param label The label for the command.
* @param resultConfig The {@link ResultConfig} object containing information on how to send the result.
* @param resultData The {@link ResultData} object containing result data.
* @param logStdoutAndStderr Set to {@code true} if {@link ResultData#stdout} and {@link ResultData#stderr}
* should be logged.
* @return Returns the {@link Error} if failed to send the result, otherwise {@code null}.
*/
public static Error sendCommandResultDataWithPendingIntent(Context context, String logTag, String label, ResultConfig resultConfig, ResultData resultData, boolean logStdoutAndStderr) {
if (context == null || resultConfig == null || resultData == null || resultConfig.resultPendingIntent == null || resultConfig.resultBundleKey == null)
return FunctionErrno.ERRNO_NULL_OR_EMPTY_PARAMETER.getError("context, resultConfig, resultData, resultConfig.resultPendingIntent or resultConfig.resultBundleKey", "sendCommandResultDataWithPendingIntent");
logTag = DataUtils.getDefaultIfNull(logTag, LOG_TAG);
Logger.logDebugExtended(logTag, "Sending result for command \"" + label + "\":\n" + resultConfig.toString() + "\n" + ResultData.getResultDataLogString(resultData, logStdoutAndStderr));
String resultDataStdout = resultData.stdout.toString();
String resultDataStderr = resultData.stderr.toString();
String truncatedStdout = null;
String truncatedStderr = null;
String stdoutOriginalLength = String.valueOf(resultDataStdout.length());
String stderrOriginalLength = String.valueOf(resultDataStderr.length());
// Truncate stdout and stdout to max TRANSACTION_SIZE_LIMIT_IN_BYTES
if (resultDataStderr.isEmpty()) {
truncatedStdout = DataUtils.getTruncatedCommandOutput(resultDataStdout, DataUtils.TRANSACTION_SIZE_LIMIT_IN_BYTES, false, false, false);
} else if (resultDataStdout.isEmpty()) {
truncatedStderr = DataUtils.getTruncatedCommandOutput(resultDataStderr, DataUtils.TRANSACTION_SIZE_LIMIT_IN_BYTES, false, false, false);
} else {
truncatedStdout = DataUtils.getTruncatedCommandOutput(resultDataStdout, DataUtils.TRANSACTION_SIZE_LIMIT_IN_BYTES / 2, false, false, false);
truncatedStderr = DataUtils.getTruncatedCommandOutput(resultDataStderr, DataUtils.TRANSACTION_SIZE_LIMIT_IN_BYTES / 2, false, false, false);
}
if (truncatedStdout != null && truncatedStdout.length() < resultDataStdout.length()) {
Logger.logWarn(logTag, "The result for command \"" + label + "\" stdout length truncated from " + stdoutOriginalLength + " to " + truncatedStdout.length());
resultDataStdout = truncatedStdout;
}
if (truncatedStderr != null && truncatedStderr.length() < resultDataStderr.length()) {
Logger.logWarn(logTag, "The result for command \"" + label + "\" stderr length truncated from " + stderrOriginalLength + " to " + truncatedStderr.length());
resultDataStderr = truncatedStderr;
}
String resultDataErrmsg = null;
if (resultData.isStateFailed()) {
resultDataErrmsg = ResultData.getErrorsListLogString(resultData);
if (resultDataErrmsg.isEmpty()) resultDataErrmsg = null;
}
String errmsgOriginalLength = (resultDataErrmsg == null) ? null : String.valueOf(resultDataErrmsg.length());
// Truncate error to max TRANSACTION_SIZE_LIMIT_IN_BYTES / 4
// trim from end to preserve start of stacktraces
String truncatedErrmsg = DataUtils.getTruncatedCommandOutput(resultDataErrmsg, DataUtils.TRANSACTION_SIZE_LIMIT_IN_BYTES / 4, true, false, false);
if (truncatedErrmsg != null && truncatedErrmsg.length() < resultDataErrmsg.length()) {
Logger.logWarn(logTag, "The result for command \"" + label + "\" error length truncated from " + errmsgOriginalLength + " to " + truncatedErrmsg.length());
resultDataErrmsg = truncatedErrmsg;
}
final Bundle resultBundle = new Bundle();
resultBundle.putString(resultConfig.resultStdoutKey, resultDataStdout);
resultBundle.putString(resultConfig.resultStdoutOriginalLengthKey, stdoutOriginalLength);
resultBundle.putString(resultConfig.resultStderrKey, resultDataStderr);
resultBundle.putString(resultConfig.resultStderrOriginalLengthKey, stderrOriginalLength);
if (resultData.exitCode != null)
resultBundle.putInt(resultConfig.resultExitCodeKey, resultData.exitCode);
resultBundle.putInt(resultConfig.resultErrCodeKey, resultData.getErrCode());
resultBundle.putString(resultConfig.resultErrmsgKey, resultDataErrmsg);
Intent resultIntent = new Intent();
resultIntent.putExtra(resultConfig.resultBundleKey, resultBundle);
try {
resultConfig.resultPendingIntent.send(context, Activity.RESULT_OK, resultIntent);
} catch (PendingIntent.CanceledException e) {
// The caller doesn't want the result? That's fine, just ignore
Logger.logDebug(logTag, "The command \"" + label + "\" creator " + resultConfig.resultPendingIntent.getCreatorPackage() + " does not want the results anymore");
}
return null;
}
/**
* Send result stored in {@link ResultConfig} to command caller by writing it to files in
* {@link ResultConfig#resultDirectoryPath}.
*
* @param context The {@link Context} for operations.
* @param logTag The log tag to use for logging.
* @param label The label for the command.
* @param resultConfig The {@link ResultConfig} object containing information on how to send the result.
* @param resultData The {@link ResultData} object containing result data.
* @param logStdoutAndStderr Set to {@code true} if {@link ResultData#stdout} and {@link ResultData#stderr}
* should be logged.
* @return Returns the {@link Error} if failed to send the result, otherwise {@code null}.
*/
public static Error sendCommandResultDataToDirectory(Context context, String logTag, String label, ResultConfig resultConfig, ResultData resultData, boolean logStdoutAndStderr) {
if (context == null || resultConfig == null || resultData == null || DataUtils.isNullOrEmpty(resultConfig.resultDirectoryPath))
return FunctionErrno.ERRNO_NULL_OR_EMPTY_PARAMETER.getError("context, resultConfig, resultData or resultConfig.resultDirectoryPath", "sendCommandResultDataToDirectory");
logTag = DataUtils.getDefaultIfNull(logTag, LOG_TAG);
Error error;
String resultDataStdout = resultData.stdout.toString();
String resultDataStderr = resultData.stderr.toString();
String resultDataExitCode = "";
if (resultData.exitCode != null)
resultDataExitCode = String.valueOf(resultData.exitCode);
String resultDataErrmsg = null;
if (resultData.isStateFailed()) {
resultDataErrmsg = ResultData.getErrorsListLogString(resultData);
}
resultDataErrmsg = DataUtils.getDefaultIfNull(resultDataErrmsg, "");
resultConfig.resultDirectoryPath = FileUtils.getCanonicalPath(resultConfig.resultDirectoryPath, null);
Logger.logDebugExtended(logTag, "Writing result for command \"" + label + "\":\n" + resultConfig.toString() + "\n" + ResultData.getResultDataLogString(resultData, logStdoutAndStderr));
// If resultDirectoryPath is not a directory, or is not readable or writable, then just return
// Creation of missing directory and setting of read, write and execute permissions are
// only done if resultDirectoryPath is under resultDirectoryAllowedParentPath.
// We try to set execute permissions, but ignore if they are missing, since only read and write
// permissions are required for working directories.
error = FileUtils.validateDirectoryFileExistenceAndPermissions("result", resultConfig.resultDirectoryPath,
resultConfig.resultDirectoryAllowedParentPath, true,
FileUtils.APP_WORKING_DIRECTORY_PERMISSIONS, true, true,
true, true);
if (error != null) {
error.appendMessage("\n" + context.getString(R.string.msg_directory_absolute_path, "Result", resultConfig.resultDirectoryPath));
return error;
}
if (resultConfig.resultSingleFile) {
// If resultFileBasename is null, empty or contains forward slashes "/"
if (DataUtils.isNullOrEmpty(resultConfig.resultFileBasename) ||
resultConfig.resultFileBasename.contains("/")) {
error = ResultSenderErrno.ERROR_RESULT_FILE_BASENAME_NULL_OR_INVALID.getError(resultConfig.resultFileBasename);
return error;
}
String error_or_output;
if (resultData.isStateFailed()) {
try {
if (DataUtils.isNullOrEmpty(resultConfig.resultFileErrorFormat)) {
error_or_output = String.format(RESULT_SENDER.FORMAT_FAILED_ERR__ERRMSG__STDOUT__STDERR__EXIT_CODE,
MarkdownUtils.getMarkdownCodeForString(String.valueOf(resultData.getErrCode()), false),
MarkdownUtils.getMarkdownCodeForString(resultDataErrmsg, true),
MarkdownUtils.getMarkdownCodeForString(resultDataStdout, true),
MarkdownUtils.getMarkdownCodeForString(resultDataStderr, true),
MarkdownUtils.getMarkdownCodeForString(resultDataExitCode, false));
} else {
error_or_output = String.format(resultConfig.resultFileErrorFormat,
resultData.getErrCode(), resultDataErrmsg, resultDataStdout, resultDataStderr, resultDataExitCode);
}
} catch (Exception e) {
error = ResultSenderErrno.ERROR_FORMAT_RESULT_ERROR_FAILED_WITH_EXCEPTION.getError(e.getMessage());
return error;
}
} else {
try {
if (DataUtils.isNullOrEmpty(resultConfig.resultFileOutputFormat)) {
if (resultDataStderr.isEmpty() && resultDataExitCode.equals("0"))
error_or_output = String.format(RESULT_SENDER.FORMAT_SUCCESS_STDOUT, resultDataStdout);
else if (resultDataStderr.isEmpty())
error_or_output = String.format(RESULT_SENDER.FORMAT_SUCCESS_STDOUT__EXIT_CODE,
resultDataStdout,
MarkdownUtils.getMarkdownCodeForString(resultDataExitCode, false));
else
error_or_output = String.format(RESULT_SENDER.FORMAT_SUCCESS_STDOUT__STDERR__EXIT_CODE,
MarkdownUtils.getMarkdownCodeForString(resultDataStdout, true),
MarkdownUtils.getMarkdownCodeForString(resultDataStderr, true),
MarkdownUtils.getMarkdownCodeForString(resultDataExitCode, false));
} else {
error_or_output = String.format(resultConfig.resultFileOutputFormat,
resultDataStdout, resultDataStderr, resultDataExitCode);
}
} catch (Exception e) {
error = ResultSenderErrno.ERROR_FORMAT_RESULT_OUTPUT_FAILED_WITH_EXCEPTION.getError(e.getMessage());
return error;
}
}
// Write error or output to temp file
// Check errCode file creation below for explanation for why temp file is used
String temp_filename = resultConfig.resultFileBasename + "-" + AndroidUtils.getCurrentMilliSecondLocalTimeStamp();
error = FileUtils.writeTextToFile(temp_filename, resultConfig.resultDirectoryPath + "/" + temp_filename,
null, error_or_output, false);
if (error != null) {
return error;
}
// Move error or output temp file to final destination
error = FileUtils.moveRegularFile("error or output temp file", resultConfig.resultDirectoryPath + "/" + temp_filename,
resultConfig.resultDirectoryPath + "/" + resultConfig.resultFileBasename, false);
if (error != null) {
return error;
}
} else {
String filename;
// Default to no suffix, useful if user expects result in an empty directory, like created with mktemp
if (resultConfig.resultFilesSuffix == null)
resultConfig.resultFilesSuffix = "";
// If resultFilesSuffix contains forward slashes "/"
if (resultConfig.resultFilesSuffix.contains("/")) {
error = ResultSenderErrno.ERROR_RESULT_FILES_SUFFIX_INVALID.getError(resultConfig.resultFilesSuffix);
return error;
}
// Write result to result files under resultDirectoryPath
// Write stdout to file
if (!resultDataStdout.isEmpty()) {
filename = RESULT_SENDER.RESULT_FILE_STDOUT_PREFIX + resultConfig.resultFilesSuffix;
error = FileUtils.writeTextToFile(filename, resultConfig.resultDirectoryPath + "/" + filename,
null, resultDataStdout, false);
if (error != null) {
return error;
}
}
// Write stderr to file
if (!resultDataStderr.isEmpty()) {
filename = RESULT_SENDER.RESULT_FILE_STDERR_PREFIX + resultConfig.resultFilesSuffix;
error = FileUtils.writeTextToFile(filename, resultConfig.resultDirectoryPath + "/" + filename,
null, resultDataStderr, false);
if (error != null) {
return error;
}
}
// Write exitCode to file
if (!resultDataExitCode.isEmpty()) {
filename = RESULT_SENDER.RESULT_FILE_EXIT_CODE_PREFIX + resultConfig.resultFilesSuffix;
error = FileUtils.writeTextToFile(filename, resultConfig.resultDirectoryPath + "/" + filename,
null, resultDataExitCode, false);
if (error != null) {
return error;
}
}
// Write errmsg to file
if (resultData.isStateFailed() && !resultDataErrmsg.isEmpty()) {
filename = RESULT_SENDER.RESULT_FILE_ERRMSG_PREFIX + resultConfig.resultFilesSuffix;
error = FileUtils.writeTextToFile(filename, resultConfig.resultDirectoryPath + "/" + filename,
null, resultDataErrmsg, false);
if (error != null) {
return error;
}
}
// Write errCode to file
// This must be created after writing to other result files has already finished since
// caller should wait for this file to be created to be notified that the command has
// finished and should then start reading from the rest of the result files if they exist.
// Since there may be a delay between creation of errCode file and writing to it or flushing
// to disk, we create a temp file first and then move it to the final destination, since
// caller may otherwise read from an empty file in some cases.
// Write errCode to temp file
String temp_filename = RESULT_SENDER.RESULT_FILE_ERR_PREFIX + "-" + AndroidUtils.getCurrentMilliSecondLocalTimeStamp();
if (!resultConfig.resultFilesSuffix.isEmpty()) temp_filename = temp_filename + "-" + resultConfig.resultFilesSuffix;
error = FileUtils.writeTextToFile(temp_filename, resultConfig.resultDirectoryPath + "/" + temp_filename,
null, String.valueOf(resultData.getErrCode()), false);
if (error != null) {
return error;
}
// Move errCode temp file to final destination
filename = RESULT_SENDER.RESULT_FILE_ERR_PREFIX + resultConfig.resultFilesSuffix;
error = FileUtils.moveRegularFile(RESULT_SENDER.RESULT_FILE_ERR_PREFIX + " temp file", resultConfig.resultDirectoryPath + "/" + temp_filename,
resultConfig.resultDirectoryPath + "/" + filename, false);
if (error != null) {
return error;
}
}
return null;
}
}

View File

@@ -0,0 +1,22 @@
package com.termux.shared.shell.command.result;
import com.termux.shared.errors.Errno;
/** The {@link Class} that defines ResultSender error messages and codes. */
public class ResultSenderErrno extends Errno {
public static final String TYPE = "ResultSender Error";
/* Errors for null or empty parameters (100-150) */
public static final Errno ERROR_RESULT_FILE_BASENAME_NULL_OR_INVALID = new Errno(TYPE, 100, "The result file basename \"%1$s\" is null, empty or contains forward slashes \"/\".");
public static final Errno ERROR_RESULT_FILES_SUFFIX_INVALID = new Errno(TYPE, 101, "The result files suffix \"%1$s\" contains forward slashes \"/\".");
public static final Errno ERROR_FORMAT_RESULT_ERROR_FAILED_WITH_EXCEPTION = new Errno(TYPE, 102, "Formatting result error failed.\nException: %1$s");
public static final Errno ERROR_FORMAT_RESULT_OUTPUT_FAILED_WITH_EXCEPTION = new Errno(TYPE, 103, "Formatting result output failed.\nException: %1$s");
ResultSenderErrno(final String type, final int code, final String message) {
super(type, code, message);
}
}

View File

@@ -0,0 +1,318 @@
package com.termux.shared.shell.command.runner.app;
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.shell.command.ExecutionCommand;
import com.termux.shared.shell.command.result.ResultData;
import com.termux.shared.errors.Errno;
import com.termux.shared.logger.Logger;
import com.termux.shared.shell.command.ExecutionCommand.ExecutionState;
import com.termux.shared.shell.ShellEnvironmentClient;
import com.termux.shared.shell.ShellUtils;
import com.termux.shared.shell.StreamGobbler;
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);
}
}