mirror of
https://github.com/fankes/termux-app.git
synced 2025-09-08 11:34:07 +08:00
Add support to send back or store RUN_COMMAND intent command results in files and provide way to fix argument splitting sent with am command
### `RUN_COMMAND` Results in Files Previously in `v0.109` witha2209dd
support was added in RUN_COMMAND intent to send back foreground and background command results with `PendingIntent` to the intent sender. However, this was only usable with java code by android apps. But if you were sending the intent with the `am` command from inside a shell, like tasker `Run Shell` action, you could not get the result back directly. You could technically manually save the output of your script in files under `/sdcard` with redirection and wait for them to be created in the `Run Shell` so that you could process the result. However, this was only possible for background commands and the caller would hang indefinitely if a termux internal `errmsg` was generated like it does for termux-tasker, likely caused by incorrect intent extra arguments, an exception being raised when executing the executable/script, or termux being closed with the exit button, etc. Now native support has been added inside termux to store results of both foreground and background commands inside files, that also sends back internal `errmsgs` as long as result files extras are valid. This can be used to run synchronous commands from inside termux, with other apps that have `Run commands in Termux environment` (`com.termux.permission.RUN_COMMAND`) like Tasker, from pc over `adb` or inside `adb shell` if you have a rooted device, or from pc if you have setup termux `sshd`. The `RUN_COMMAND` intent can only be sent by the `termux` user itself, by an app that has the permission or by the `root` user. The `shell` user of `adb` cannot send it. A script will be provided at a later time that will automatically detect these cases to easily run `RUN_COMMAND` intent commands which will also automatically create temp directories and do cleanup. This can also be useful inside termux itself, like if you want to start a new foreground session and to automatically store its output to a log file when you exit. Support can also be added for this to be done for termux-boot and termux-widget as well but will require updates for them. There is obviously a security and privacy concern for this if you use shared storage `/sdcard` to store the result files since malicious apps could read them and optionally modify them for MITM attacks if you are reading the result and processing it unsafely. But users access other files from shared storage anyways for other scripts. Saving the result files on shared storage would only be necessary if you want to read the result back, like in Tasker or over adb since non-termux and non-root users can't access termux private app data directory `/data/data/com.termux`. For internal termux usage, this shouldn't be a concern if files are saved inside termux private app data directory. The extra constant values are defined by [`TermuxConstants`](https://github.com/termux/termux-app/tree/master/termux-shared/src/main/java/com/termux/shared/termux/TermuxConstants.java) class of the [`termux-shared`](https://github.com/termux/termux-app/tree/master/termux-shared) library. The [`ResultSender`](https://github.com/termux/termux-app/tree/master/termux-shared/src/main/java/com/termux/shared/shell/ResultSender.java) class actually sends back the results. The following extras have been added: - The `String` `RUN_COMMAND_SERVICE.EXTRA_RESULT_DIRECTORY` extra for the directory path in which to write the result of the execution command for the execute command caller. - The `boolean` `RUN_COMMAND_SERVICE.EXTRA_RESULT_SINGLE_FILE` extra for whether the result should be written to a single file or multiple files (`err`, `errmsg`, `stdout`, `stderr`, `exit_code`) in `EXTRA_RESULT_DIRECTORY`. - The `String` `RUN_COMMAND_SERVICE.EXTRA_RESULT_FILE_BASENAME` extra for the basename of the result file that should be created in `EXTRA_RESULT_DIRECTORY` if `EXTRA_RESULT_SINGLE_FILE` is `true`. - The `String` `RUN_COMMAND_SERVICE.EXTRA_RESULT_FILE_OUTPUT_FORMAT` extra for the output [`Formatter`](https://docs.oracle.com/javase/7/docs/api/java/util/Formatter.html) format of the `EXTRA_RESULT_FILE_BASENAME` result file. - The `String` `RUN_COMMAND_SERVICE.EXTRA_RESULT_FILE_ERROR_FORMAT` extra for the error [`Formatter`](https://docs.oracle.com/javase/7/docs/api/java/util/Formatter.html) format of the `EXTRA_RESULT_FILE_BASENAME` result file. - The `String` `RUN_COMMAND_SERVICE.EXTRA_RESULT_FILES_SUFFIX` extra for the optional suffix of the result files that should be created in `EXTRA_RESULT_DIRECTORY` if `EXTRA_RESULT_SINGLE_FILE` is `false`. The `err` and `errmsg` are for internal termux errors like invalid intent extras, etc and not related to the shell commands itself. This is the same way Tasker actions and plugins system work with [`%err` and `%errmsg`](https://tasker.joaoapps.com/userguide/en/variables.html#localbuiltin). The `err` will be equal to `Errno.ERRNO_SUCCESS` (`-1`) if no internal errors are set. The `stdout`, `stderr` and `exit_code` are for the shell commands. The `exit_code` is normally `0` for success. There are two modes for getting back the result in results files. ##### `EXTRA_RESULT_SINGLE_FILE` extra is `true` Only a single file will be created under `EXTRA_RESULT_DIRECTORY` that will contain the `err`, `errmsg`, `stdout`, `stderr` and `exit_code` in a specific format defined by `RESULT_SENDER.FORMAT_*` constants in `TermuxConstants` class depending on the exit status of the command. By default if the `EXTRA_RESULT_FILE_BASENAME` extra is not passed, the basename of the result file will be set to `<command_path_basename>-<timestamp>.log` where `<timestamp>` will be in the `yyyy-MM-dd_HH.mm.ss.SSS` format. The `EXTRA_RESULT_FILE_OUTPUT_FORMAT` extra can be passed with a custom format that should be used when `err` equals `-1` and `EXTRA_RESULT_FILE_ERROR_FORMAT` extra for when its greater than `-1`. The value `0` is for `Errno.ERRNO_CANCELLED` and should also be considered a failure unlike `exit_code`. ``` am startservice --user 0 -n 'com.termux/com.termux.app.RunCommandService' -a 'com.termux.RUN_COMMAND' --es 'com.termux.RUN_COMMAND_PATH' '$PREFIX/bin/top' --esa 'com.termux.RUN_COMMAND_ARGUMENTS' '-n,5' --ez 'com.termux.RUN_COMMAND_BACKGROUND' '0' --es 'com.termux.RUN_COMMAND_RESULT_DIRECTORY' '/sdcard/.termux-app' --ez 'com.termux.RUN_COMMAND_RESULT_SINGLE_FILE' 'true' --es 'com.termux.RUN_COMMAND_RESULT_FILE_BASENAME' 'top.log' ``` ##### `EXTRA_RESULT_SINGLE_FILE` extra is `false` Separate files will be created under `EXTRA_RESULT_DIRECTORY` for each of the `err`, `errmsg`, `stdout`, `stderr` and `exit_code`. Their basenames (same as mentioned) are defined by the `RESULT_FILE_*` constants in `TermuxConstants` class. If the `EXTRA_RESULT_FILES_SUFFIX` extra is passed, then that will be suffixed to the basename of each file like `err<suffix>`, `stdout<suffix>`, etc. The `err` file will be created after writing to other result files has already finished and this is the file the caller should optionally wait for to be created to be notified that the command has finished, like with `test -f "$result_directory/err"` command in an infinite loop (with sleep+timeout) or with `inotify`. After it has been read, caller can start reading from the rest of the result files if they exist. The `errmsg`, `stdout`, `stderr` and `exit_code` files will not be created if nothing is to be written to them, so no do wait for these files. If you are not passing a unique suffix for each intent, then result files of multiple simultaneous intent commands will conflict with each other. So ideally a temp directory should be created for each intent command and that should be passed as `EXTRA_RESULT_DIRECTORY`. You can use `mktemp` command to create a unique name and create the directory for you. ``` temp_directory="$(/system/bin/mktemp -d --tmpdir="/sdcard/.termux-app" "top.XXXXXX")" || return $? am startservice --user 0 -n 'com.termux/com.termux.app.RunCommandService' -a 'com.termux.RUN_COMMAND' --es 'com.termux.RUN_COMMAND_PATH' '$PREFIX/bin/top' --esa 'com.termux.RUN_COMMAND_ARGUMENTS' '-n,5' --ez 'com.termux.RUN_COMMAND_BACKGROUND' '1' --es 'com.termux.RUN_COMMAND_RESULT_DIRECTORY' "$temp_directory" --ez 'com.termux.RUN_COMMAND_RESULT_SINGLE_FILE' 'false' ``` Use following if in termux and not in tasker/rooted shell. ``` temp_directory="$(PATH=/system/bin; LD_LIBRARY_PATH=/system/lib64:/system/lib; unset LD_PRELOAD; mktemp -d --tmpdir="/sdcard/.termux-app" "top.XXXXXX")" || return $? ``` Note that since there may be a delay between creation of `result_file`/`err` file and writing to it or flushing to disk, a temp file is created first suffixed with `-<timestamp>` which is then moved to the final destination, since caller may otherwise read from an empty file in some cases otherwise. Commands will automatically be killed and result up till that point returned if user exits termux app like with the `Exit` button in the notification. The exit code will be `137` (`SIGKILL`). -------------------- ### `RUN_COMMAND` Arguments Splitting with `am` Command If `am` command is used to send the `RUN_COMMAND` intent and you want to pass an argument with the `--esa com.termux.RUN_COMMAND_ARGUMENTS` string array extra that itself contains a normal comma `,` (`U+002C`, `,`, `,`, `comma`), it must be escaped with a backslash `\,` so that the argument isn't split into multiple arguments. The only problem is that, the arguments received by the termux will contain `\,` instead of `,` since the reversal isn't done as described in the [am command source](https://android.googlesource.com/platform/frameworks/base/+/21bdaf1/cmds/am/src/com/android/commands/am/Am.java#572) while converting to a string array. There is also no way for the `am` command or termux to know whether `\,` was done to prevent arguments splitting or `\,` was a literal string naturally part of the argument. ``` // Split on commas unless they are preceeded by an escape. // The escape character must be escaped for the string and // again for the regex, thus four escape characters become one. intent.putExtra(key, strings); ``` To fix this termux now supports an alternative method to handle such conditions. If an argument contains a normal comma `,`, then instead of escaping them with a backslash `\,`, replace all normal commas with the comma alternate character `‚` (`#U+201A`, `‚`, `‚`, `single low-9 quotation mark`) before sending the intent with the `am` command. This way argument splitting will not be done. You can pass the `com.termux.RUN_COMMAND_REPLACE_COMMA_ALTERNATIVE_CHARS_IN_ARGUMENTS` `boolean` extra in the `RUN_COMMAND` intent so that termux replaces all the comma alternate characters back to normal commas. It would be unlikely for the the arguments to naturally contain the comma alternate characters for this to be a problem. Even if they do, they might not be significant for any logic. If they are, then you can set a different character that should be replaced, by passing it in the `com.termux.RUN_COMMAND_COMMA_ALTERNATIVE_CHARS_IN_ARGUMENTS` `String` extra. If `tudo` or `sudo` are used, then simply using their `-r` and `--comma-alternative` command options can be used without passing the below extras, but native supports is helpful if they are not being used. https://github.com/agnostic-apollo/tudo#passing-arguments-using-run_command-intent The following extras have been added: - The `boolean` `RUN_COMMAND_SERVICE.EXTRA_REPLACE_COMMA_ALTERNATIVE_CHARS_IN_ARGUMENTS` extra for whether to replace comma alternative characters in arguments with normal comma `,` (`U+002C`, `,`, `,`, `comma`). - The `String` `RUN_COMMAND_SERVICE.EXTRA_COMMA_ALTERNATIVE_CHARS_IN_ARGUMENTS` extra for the comma alternative characters in arguments that should be replaced instead of the default comma alternate character `‚` (`#U+201A`, `‚`, `‚`, `single low-9 quotation mark`). ``` am startservice --user 0 -n 'com.termux/com.termux.app.RunCommandService' -a 'com.termux.RUN_COMMAND' --es 'com.termux.RUN_COMMAND_PATH' '$PREFIX/bin/bash' --esa 'com.termux.RUN_COMMAND_ARGUMENTS' '-c,echo "Argument with commas here _ and here _ that have been converted to an underscore before sending"; sleep 5' --ez 'com.termux.RUN_COMMAND_BACKGROUND' '0' --ez 'com.termux.RUN_COMMAND_REPLACE_COMMA_ALTERNATIVE_CHARS_IN_ARGUMENTS' 'true' --es 'com.termux.RUN_COMMAND_COMMA_ALTERNATIVE_CHARS_IN_ARGUMENTS' '_' ``` Note that since `0.109`, the `RUN_COMMAND` intent supports `RUN_COMMAND_SERVICE.EXTRA_STDIN`, so instead of passing arguments, just pass a script as `stdin` to the `bash` executable so that you don't have to deal with this "mess". You will have to surround the script with single quotes and escape any single quotes inside the script itself, like each single quote `'` with `'\''`. -------------------- ### Internal Changes This commit also adds onto679e0de0
and4494bc66
The `ExecutionCommand` has been updated and command result variables have been moved to `ResultData` and result configuration to `ResultConfig` since the later two should be agnostic of what type of command there are for. They don't necessarily have to be for terminal/shell commands and can be used for plugin APIs, etc. The `ResultData` instead of a `String` `errmsg` now stores a list of `Error` objects. This is necessary since multiple errors may be picked up while a command is run, like say working directory is invalid and an error is returned by FileUtils and while sending the result to the caller, the `ResultSender` returns an additional error because result configuration like result directory or result output format was invalid. In these situations `PluginUtils` will show a notification to the user with info of each error thrown. In addition to above, in `ResultData`, the `stdout` and `stderr` are converted to `StringBuilder` instead of a `String`. This allows for data to be appended to each from various places in code like log debug or error entries for API commands without having to create a new `String` object each time value needs to updated. This can be useful so that the caller doesn't have to check `logcat` for API commands. This does not apply to `ExecutionCommand` since only `TermuxSession` and `TermuxTask` set the data. The `ResultSender` class is what handles the result of commands whether they need to be sent via `PendingIntent` or to a result directory based on the `ResultConfig` object passed. Result will be sent through both if both of them are not `null`. The `TermuxConstants` class has been updated to `v0.24.0`. Check its Changelog section for info on changes.
This commit is contained in:
@@ -0,0 +1,335 @@
|
||||
package com.termux.shared.shell;
|
||||
|
||||
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.models.errors.Error;
|
||||
import com.termux.shared.file.FileUtils;
|
||||
import com.termux.shared.logger.Logger;
|
||||
import com.termux.shared.models.ResultConfig;
|
||||
import com.termux.shared.models.ResultData;
|
||||
import com.termux.shared.models.errors.FunctionErrno;
|
||||
import com.termux.shared.models.errors.ResultSenderErrno;
|
||||
import com.termux.shared.termux.TermuxConstants.RESULT_SENDER;
|
||||
import com.termux.shared.termux.TermuxUtils;
|
||||
|
||||
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.
|
||||
* @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) {
|
||||
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);
|
||||
if (error != null || resultConfig.resultDirectoryPath == null)
|
||||
return error;
|
||||
}
|
||||
|
||||
if (resultConfig.resultDirectoryPath != null) {
|
||||
return sendCommandResultDataToDirectory(context, logTag, label, resultConfig, resultData);
|
||||
} 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.
|
||||
* @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) {
|
||||
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.toString());
|
||||
|
||||
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.
|
||||
* @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) {
|
||||
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.toString());
|
||||
|
||||
// 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,
|
||||
resultData.getErrCode(), resultDataErrmsg, resultDataStdout, resultDataStderr, resultDataExitCode);
|
||||
} 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, resultDataExitCode);
|
||||
else
|
||||
error_or_output = String.format(RESULT_SENDER.FORMAT_SUCCESS_STDOUT__STDERR__EXIT_CODE, resultDataStdout, resultDataStderr, resultDataExitCode);
|
||||
} 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 + "-" + TermuxUtils.getCurrentMilliSecondLocalTimeStamp();
|
||||
error = FileUtils.writeStringToFile(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.writeStringToFile(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.writeStringToFile(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.writeStringToFile(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.writeStringToFile(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 + "-" + TermuxUtils.getCurrentMilliSecondLocalTimeStamp();
|
||||
if (!resultConfig.resultFilesSuffix.isEmpty()) temp_filename = temp_filename + "-" + resultConfig.resultFilesSuffix;
|
||||
error = FileUtils.writeStringToFile(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;
|
||||
}
|
||||
|
||||
}
|
Reference in New Issue
Block a user