diff --git a/app/src/main/java/com/termux/app/RunCommandService.java b/app/src/main/java/com/termux/app/RunCommandService.java index d81b3ec5..9817d785 100644 --- a/app/src/main/java/com/termux/app/RunCommandService.java +++ b/app/src/main/java/com/termux/app/RunCommandService.java @@ -10,6 +10,11 @@ import android.os.Build; import android.os.IBinder; import com.termux.R; +import com.termux.shared.data.DataUtils; +import com.termux.shared.data.IntentUtils; +import com.termux.shared.file.TermuxFileUtils; +import com.termux.shared.models.errors.Errno; +import com.termux.shared.models.errors.Error; import com.termux.shared.termux.TermuxConstants; import com.termux.shared.termux.TermuxConstants.TERMUX_APP.RUN_COMMAND_SERVICE; import com.termux.shared.termux.TermuxConstants.TERMUX_APP.TERMUX_SERVICE; @@ -17,7 +22,6 @@ import com.termux.shared.file.FileUtils; import com.termux.shared.logger.Logger; import com.termux.shared.notification.NotificationUtils; import com.termux.app.utils.PluginUtils; -import com.termux.shared.data.DataUtils; import com.termux.shared.models.ExecutionCommand; /** @@ -60,29 +64,57 @@ public class RunCommandService extends Service { ExecutionCommand executionCommand = new ExecutionCommand(); executionCommand.pluginAPIHelp = this.getString(R.string.error_run_command_service_api_help, RUN_COMMAND_SERVICE.RUN_COMMAND_API_HELP_URL); + Error error; String errmsg; // If invalid action passed, then just return if (!RUN_COMMAND_SERVICE.ACTION_RUN_COMMAND.equals(intent.getAction())) { errmsg = this.getString(R.string.error_run_command_service_invalid_intent_action, intent.getAction()); - executionCommand.setStateFailed(ExecutionCommand.RESULT_CODE_FAILED, errmsg, null); + executionCommand.setStateFailed(Errno.ERRNO_FAILED.getCode(), errmsg); PluginUtils.processPluginExecutionCommandError(this, LOG_TAG, executionCommand, false); return Service.START_NOT_STICKY; } - executionCommand.executable = intent.getStringExtra(RUN_COMMAND_SERVICE.EXTRA_COMMAND_PATH); - executionCommand.arguments = intent.getStringArrayExtra(RUN_COMMAND_SERVICE.EXTRA_ARGUMENTS); - executionCommand.stdin = intent.getStringExtra(RUN_COMMAND_SERVICE.EXTRA_STDIN); - executionCommand.workingDirectory = intent.getStringExtra(RUN_COMMAND_SERVICE.EXTRA_WORKDIR); + executionCommand.executable = IntentUtils.getStringExtraIfSet(intent, RUN_COMMAND_SERVICE.EXTRA_COMMAND_PATH, null); + + executionCommand.arguments = IntentUtils.getStringArrayExtraIfSet(intent, RUN_COMMAND_SERVICE.EXTRA_ARGUMENTS, null); + + /* + * If intent was sent with `am` command, then normal comma characters may have been replaced + * with alternate characters if a normal comma existed in an argument itself to prevent it + * splitting into multiple arguments by `am` command. + * 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 + * https://android.googlesource.com/platform/frameworks/base/+/21bdaf1/cmds/am/src/com/android/commands/am/Am.java#572 + */ + boolean replaceCommaAlternativeCharsInArguments = intent.getBooleanExtra(RUN_COMMAND_SERVICE.EXTRA_REPLACE_COMMA_ALTERNATIVE_CHARS_IN_ARGUMENTS, false); + if (replaceCommaAlternativeCharsInArguments) { + String commaAlternativeCharsInArguments = IntentUtils.getStringExtraIfSet(intent, RUN_COMMAND_SERVICE.EXTRA_COMMA_ALTERNATIVE_CHARS_IN_ARGUMENTS, null); + if (commaAlternativeCharsInArguments == null) + commaAlternativeCharsInArguments = TermuxConstants.COMMA_ALTERNATIVE; + // Replace any commaAlternativeCharsInArguments characters with normal commas + DataUtils.replaceSubStringsInStringArrayItems(executionCommand.arguments, commaAlternativeCharsInArguments, TermuxConstants.COMMA_NORMAL); + } + + executionCommand.stdin = IntentUtils.getStringExtraIfSet(intent, RUN_COMMAND_SERVICE.EXTRA_STDIN, null); + executionCommand.workingDirectory = IntentUtils.getStringExtraIfSet(intent, RUN_COMMAND_SERVICE.EXTRA_WORKDIR, null); executionCommand.inBackground = intent.getBooleanExtra(RUN_COMMAND_SERVICE.EXTRA_BACKGROUND, false); executionCommand.sessionAction = intent.getStringExtra(RUN_COMMAND_SERVICE.EXTRA_SESSION_ACTION); - executionCommand.commandLabel = DataUtils.getDefaultIfNull(intent.getStringExtra(RUN_COMMAND_SERVICE.EXTRA_COMMAND_LABEL), "RUN_COMMAND Execution Intent Command"); - executionCommand.commandDescription = intent.getStringExtra(RUN_COMMAND_SERVICE.EXTRA_COMMAND_DESCRIPTION); - executionCommand.commandHelp = intent.getStringExtra(RUN_COMMAND_SERVICE.EXTRA_COMMAND_HELP); + executionCommand.commandLabel = IntentUtils.getStringExtraIfSet(intent, RUN_COMMAND_SERVICE.EXTRA_COMMAND_LABEL, "RUN_COMMAND Execution Intent Command"); + executionCommand.commandDescription = IntentUtils.getStringExtraIfSet(intent, RUN_COMMAND_SERVICE.EXTRA_COMMAND_DESCRIPTION, null); + executionCommand.commandHelp = IntentUtils.getStringExtraIfSet(intent, RUN_COMMAND_SERVICE.EXTRA_COMMAND_HELP, null); executionCommand.isPluginExecutionCommand = true; - executionCommand.pluginPendingIntent = intent.getParcelableExtra(RUN_COMMAND_SERVICE.EXTRA_PENDING_INTENT); - - + executionCommand.resultConfig.resultPendingIntent = intent.getParcelableExtra(RUN_COMMAND_SERVICE.EXTRA_PENDING_INTENT); + executionCommand.resultConfig.resultDirectoryPath = IntentUtils.getStringExtraIfSet(intent, RUN_COMMAND_SERVICE.EXTRA_RESULT_DIRECTORY, null); + if (executionCommand.resultConfig.resultDirectoryPath != null) { + executionCommand.resultConfig.resultSingleFile = intent.getBooleanExtra(RUN_COMMAND_SERVICE.EXTRA_RESULT_SINGLE_FILE, false); + executionCommand.resultConfig.resultFileBasename = IntentUtils.getStringExtraIfSet(intent, RUN_COMMAND_SERVICE.EXTRA_RESULT_FILE_BASENAME, null); + executionCommand.resultConfig.resultFileOutputFormat = IntentUtils.getStringExtraIfSet(intent, RUN_COMMAND_SERVICE.EXTRA_RESULT_FILE_OUTPUT_FORMAT, null); + executionCommand.resultConfig.resultFileErrorFormat = IntentUtils.getStringExtraIfSet(intent, RUN_COMMAND_SERVICE.EXTRA_RESULT_FILE_ERROR_FORMAT, null); + executionCommand.resultConfig.resultFilesSuffix = IntentUtils.getStringExtraIfSet(intent, RUN_COMMAND_SERVICE.EXTRA_RESULT_FILES_SUFFIX, null); + } // If "allow-external-apps" property to not set to "true", then just return // We enable force notifications if "allow-external-apps" policy is violated so that the @@ -91,7 +123,7 @@ public class RunCommandService extends Service { // also sent, then its creator is also logged and shown. errmsg = PluginUtils.checkIfRunCommandServiceAllowExternalAppsPolicyIsViolated(this); if (errmsg != null) { - executionCommand.setStateFailed(ExecutionCommand.RESULT_CODE_FAILED, errmsg, null); + executionCommand.setStateFailed(Errno.ERRNO_FAILED.getCode(), errmsg); PluginUtils.processPluginExecutionCommandError(this, LOG_TAG, executionCommand, true); return Service.START_NOT_STICKY; } @@ -101,22 +133,22 @@ public class RunCommandService extends Service { // If executable is null or empty, then exit here instead of getting canonical path which would expand to "/" if (executionCommand.executable == null || executionCommand.executable.isEmpty()) { errmsg = this.getString(R.string.error_run_command_service_mandatory_extra_missing, RUN_COMMAND_SERVICE.EXTRA_COMMAND_PATH); - executionCommand.setStateFailed(ExecutionCommand.RESULT_CODE_FAILED, errmsg, null); + executionCommand.setStateFailed(Errno.ERRNO_FAILED.getCode(), errmsg); PluginUtils.processPluginExecutionCommandError(this, LOG_TAG, executionCommand, false); return Service.START_NOT_STICKY; } // Get canonical path of executable - executionCommand.executable = FileUtils.getCanonicalPath(executionCommand.executable, null, true); + executionCommand.executable = TermuxFileUtils.getCanonicalPath(executionCommand.executable, null, true); // If executable is not a regular file, or is not readable or executable, then just return // Setting of missing read and execute permissions is not done - errmsg = FileUtils.validateRegularFileExistenceAndPermissions(this, "executable", executionCommand.executable, null, - PluginUtils.PLUGIN_EXECUTABLE_FILE_PERMISSIONS, true, true, + error = FileUtils.validateRegularFileExistenceAndPermissions("executable", executionCommand.executable, null, + FileUtils.APP_EXECUTABLE_FILE_PERMISSIONS, true, true, false); - if (errmsg != null) { - errmsg += "\n" + this.getString(R.string.msg_executable_absolute_path, executionCommand.executable); - executionCommand.setStateFailed(ExecutionCommand.RESULT_CODE_FAILED, errmsg, null); + if (error != null) { + error.appendMessage("\n" + this.getString(R.string.msg_executable_absolute_path, executionCommand.executable)); + executionCommand.setStateFailed(error); PluginUtils.processPluginExecutionCommandError(this, LOG_TAG, executionCommand, false); return Service.START_NOT_STICKY; } @@ -126,19 +158,19 @@ public class RunCommandService extends Service { // If workingDirectory is not null or empty if (executionCommand.workingDirectory != null && !executionCommand.workingDirectory.isEmpty()) { // Get canonical path of workingDirectory - executionCommand.workingDirectory = FileUtils.getCanonicalPath(executionCommand.workingDirectory, null, true); + executionCommand.workingDirectory = TermuxFileUtils.getCanonicalPath(executionCommand.workingDirectory, null, true); // If workingDirectory 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 workingDirectory is - // under {@link TermuxConstants#TERMUX_FILES_DIR_PATH} + // under allowed termux working directory paths. // We try to set execute permissions, but ignore if they are missing, since only read and write permissions are required // for working directories. - errmsg = FileUtils.validateDirectoryFileExistenceAndPermissions(this, "working", executionCommand.workingDirectory, TermuxConstants.TERMUX_FILES_DIR_PATH, true, - PluginUtils.PLUGIN_WORKING_DIRECTORY_PERMISSIONS, true, true, - true, true); - if (errmsg != null) { - errmsg += "\n" + this.getString(R.string.msg_working_directory_absolute_path, executionCommand.workingDirectory); - executionCommand.setStateFailed(ExecutionCommand.RESULT_CODE_FAILED, errmsg, null); + error = TermuxFileUtils.validateDirectoryFileExistenceAndPermissions("working", executionCommand.workingDirectory, + true, true, true, + false, true); + if (error != null) { + error.appendMessage("\n" + this.getString(R.string.msg_working_directory_absolute_path, executionCommand.workingDirectory)); + executionCommand.setStateFailed(error); PluginUtils.processPluginExecutionCommandError(this, LOG_TAG, executionCommand, false); return Service.START_NOT_STICKY; } @@ -146,7 +178,7 @@ public class RunCommandService extends Service { - executionCommand.executableUri = new Uri.Builder().scheme(TERMUX_SERVICE.URI_SCHEME_SERVICE_EXECUTE).path(FileUtils.getExpandedTermuxPath(executionCommand.executable)).build(); + executionCommand.executableUri = new Uri.Builder().scheme(TERMUX_SERVICE.URI_SCHEME_SERVICE_EXECUTE).path(TermuxFileUtils.getExpandedTermuxPath(executionCommand.executable)).build(); Logger.logVerbose(LOG_TAG, executionCommand.toString()); @@ -162,7 +194,15 @@ public class RunCommandService extends Service { execIntent.putExtra(TERMUX_SERVICE.EXTRA_COMMAND_DESCRIPTION, executionCommand.commandDescription); execIntent.putExtra(TERMUX_SERVICE.EXTRA_COMMAND_HELP, executionCommand.commandHelp); execIntent.putExtra(TERMUX_SERVICE.EXTRA_PLUGIN_API_HELP, executionCommand.pluginAPIHelp); - execIntent.putExtra(TERMUX_SERVICE.EXTRA_PENDING_INTENT, executionCommand.pluginPendingIntent); + execIntent.putExtra(TERMUX_SERVICE.EXTRA_PENDING_INTENT, executionCommand.resultConfig.resultPendingIntent); + execIntent.putExtra(TERMUX_SERVICE.EXTRA_RESULT_DIRECTORY, executionCommand.resultConfig.resultDirectoryPath); + if (executionCommand.resultConfig.resultDirectoryPath != null) { + execIntent.putExtra(TERMUX_SERVICE.EXTRA_RESULT_SINGLE_FILE, executionCommand.resultConfig.resultSingleFile); + execIntent.putExtra(TERMUX_SERVICE.EXTRA_RESULT_FILE_BASENAME, executionCommand.resultConfig.resultFileBasename); + execIntent.putExtra(TERMUX_SERVICE.EXTRA_RESULT_FILE_OUTPUT_FORMAT, executionCommand.resultConfig.resultFileOutputFormat); + execIntent.putExtra(TERMUX_SERVICE.EXTRA_RESULT_FILE_ERROR_FORMAT, executionCommand.resultConfig.resultFileErrorFormat); + execIntent.putExtra(TERMUX_SERVICE.EXTRA_RESULT_FILES_SUFFIX, executionCommand.resultConfig.resultFilesSuffix); + } // Start TERMUX_SERVICE and pass it execution intent if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { diff --git a/app/src/main/java/com/termux/app/TermuxService.java b/app/src/main/java/com/termux/app/TermuxService.java index 66d56eda..6404bf46 100644 --- a/app/src/main/java/com/termux/app/TermuxService.java +++ b/app/src/main/java/com/termux/app/TermuxService.java @@ -23,6 +23,8 @@ import com.termux.R; import com.termux.app.settings.properties.TermuxAppSharedProperties; import com.termux.app.terminal.TermuxTerminalSessionClient; import com.termux.app.utils.PluginUtils; +import com.termux.shared.data.IntentUtils; +import com.termux.shared.models.errors.Errno; import com.termux.shared.termux.TermuxConstants; import com.termux.shared.termux.TermuxConstants.TERMUX_APP.TERMUX_ACTIVITY; import com.termux.shared.termux.TermuxConstants.TERMUX_APP.TERMUX_SERVICE; @@ -160,7 +162,7 @@ public final class TermuxService extends Service implements TermuxTask.TermuxTas public void onDestroy() { Logger.logVerbose(LOG_TAG, "onDestroy"); - ShellUtils.clearTermuxTMPDIR(this, true); + ShellUtils.clearTermuxTMPDIR(true); actionReleaseWakeLock(false); if (!mWantsToStop) @@ -254,22 +256,22 @@ public final class TermuxService extends Service implements TermuxTask.TermuxTas List termuxSessions = new ArrayList<>(mTermuxSessions); for (int i = 0; i < termuxSessions.size(); i++) { ExecutionCommand executionCommand = termuxSessions.get(i).getExecutionCommand(); - processResult = mWantsToStop || (executionCommand.isPluginExecutionCommand && executionCommand.pluginPendingIntent != null); + processResult = mWantsToStop || executionCommand.isPluginExecutionCommandWithPendingResult(); termuxSessions.get(i).killIfExecuting(this, processResult); } List termuxTasks = new ArrayList<>(mTermuxTasks); for (int i = 0; i < termuxTasks.size(); i++) { ExecutionCommand executionCommand = termuxTasks.get(i).getExecutionCommand(); - if (executionCommand.isPluginExecutionCommand && executionCommand.pluginPendingIntent != null) + if (executionCommand.isPluginExecutionCommandWithPendingResult()) termuxTasks.get(i).killIfExecuting(this, true); } List pendingPluginExecutionCommands = new ArrayList<>(mPendingPluginExecutionCommands); for (int i = 0; i < pendingPluginExecutionCommands.size(); i++) { ExecutionCommand executionCommand = pendingPluginExecutionCommands.get(i); - if (!executionCommand.shouldNotProcessResults() && executionCommand.pluginPendingIntent != null) { - if (executionCommand.setStateFailed(ExecutionCommand.RESULT_CODE_CANCELED, this.getString(com.termux.shared.R.string.error_execution_cancelled), null)) { + if (!executionCommand.shouldNotProcessResults() && executionCommand.isPluginExecutionCommandWithPendingResult()) { + if (executionCommand.setStateFailed(Errno.ERRNO_CANCELLED.getCode(), this.getString(com.termux.shared.R.string.error_execution_cancelled))) { PluginUtils.processPluginExecutionCommandResult(this, LOG_TAG, executionCommand); } } @@ -357,20 +359,28 @@ public final class TermuxService extends Service implements TermuxTask.TermuxTas if (executionCommand.executableUri != null) { executionCommand.executable = executionCommand.executableUri.getPath(); - executionCommand.arguments = intent.getStringArrayExtra(TERMUX_SERVICE.EXTRA_ARGUMENTS); + executionCommand.arguments = IntentUtils.getStringArrayExtraIfSet(intent, TERMUX_SERVICE.EXTRA_ARGUMENTS, null); if (executionCommand.inBackground) - executionCommand.stdin = intent.getStringExtra(TERMUX_SERVICE.EXTRA_STDIN); + executionCommand.stdin = IntentUtils.getStringExtraIfSet(intent, TERMUX_SERVICE.EXTRA_STDIN, null); } - executionCommand.workingDirectory = intent.getStringExtra(TERMUX_SERVICE.EXTRA_WORKDIR); + executionCommand.workingDirectory = IntentUtils.getStringExtraIfSet(intent, TERMUX_SERVICE.EXTRA_WORKDIR, null); executionCommand.isFailsafe = intent.getBooleanExtra(TERMUX_ACTIVITY.ACTION_FAILSAFE_SESSION, false); executionCommand.sessionAction = intent.getStringExtra(TERMUX_SERVICE.EXTRA_SESSION_ACTION); - executionCommand.commandLabel = DataUtils.getDefaultIfNull(intent.getStringExtra(TERMUX_SERVICE.EXTRA_COMMAND_LABEL), "Execution Intent Command"); - executionCommand.commandDescription = intent.getStringExtra(TERMUX_SERVICE.EXTRA_COMMAND_DESCRIPTION); - executionCommand.commandHelp = intent.getStringExtra(TERMUX_SERVICE.EXTRA_COMMAND_HELP); - executionCommand.pluginAPIHelp = intent.getStringExtra(TERMUX_SERVICE.EXTRA_PLUGIN_API_HELP); + executionCommand.commandLabel = IntentUtils.getStringExtraIfSet(intent, TERMUX_SERVICE.EXTRA_COMMAND_LABEL, "Execution Intent Command"); + executionCommand.commandDescription = IntentUtils.getStringExtraIfSet(intent, TERMUX_SERVICE.EXTRA_COMMAND_DESCRIPTION, null); + executionCommand.commandHelp = IntentUtils.getStringExtraIfSet(intent, TERMUX_SERVICE.EXTRA_COMMAND_HELP, null); + executionCommand.pluginAPIHelp = IntentUtils.getStringExtraIfSet(intent, TERMUX_SERVICE.EXTRA_PLUGIN_API_HELP, null); executionCommand.isPluginExecutionCommand = true; - executionCommand.pluginPendingIntent = intent.getParcelableExtra(TERMUX_SERVICE.EXTRA_PENDING_INTENT); + executionCommand.resultConfig.resultPendingIntent = intent.getParcelableExtra(TERMUX_SERVICE.EXTRA_PENDING_INTENT); + executionCommand.resultConfig.resultDirectoryPath = IntentUtils.getStringExtraIfSet(intent, TERMUX_SERVICE.EXTRA_RESULT_DIRECTORY, null); + if (executionCommand.resultConfig.resultDirectoryPath != null) { + executionCommand.resultConfig.resultSingleFile = intent.getBooleanExtra(TERMUX_SERVICE.EXTRA_RESULT_SINGLE_FILE, false); + executionCommand.resultConfig.resultFileBasename = IntentUtils.getStringExtraIfSet(intent, TERMUX_SERVICE.EXTRA_RESULT_FILE_BASENAME, null); + executionCommand.resultConfig.resultFileOutputFormat = IntentUtils.getStringExtraIfSet(intent, TERMUX_SERVICE.EXTRA_RESULT_FILE_OUTPUT_FORMAT, null); + executionCommand.resultConfig.resultFileErrorFormat = IntentUtils.getStringExtraIfSet(intent, TERMUX_SERVICE.EXTRA_RESULT_FILE_ERROR_FORMAT, null); + executionCommand.resultConfig.resultFilesSuffix = IntentUtils.getStringExtraIfSet(intent, TERMUX_SERVICE.EXTRA_RESULT_FILES_SUFFIX, null); + } // Add the execution command to pending plugin execution commands list mPendingPluginExecutionCommands.add(executionCommand); @@ -423,7 +433,7 @@ public final class TermuxService extends Service implements TermuxTask.TermuxTas if (executionCommand.isPluginExecutionCommand) PluginUtils.processPluginExecutionCommandError(this, LOG_TAG, executionCommand, false); else - Logger.logStackTracesWithMessage(LOG_TAG, "(" + executionCommand.errCode + ") " + executionCommand.errmsg, executionCommand.throwableList); + Logger.logErrorExtended(LOG_TAG, executionCommand.toString()); return null; } @@ -519,7 +529,7 @@ public final class TermuxService extends Service implements TermuxTask.TermuxTas if (executionCommand.isPluginExecutionCommand) PluginUtils.processPluginExecutionCommandError(this, LOG_TAG, executionCommand, false); else - Logger.logStackTracesWithMessage(LOG_TAG, "(" + executionCommand.errCode + ") " + executionCommand.errmsg, executionCommand.throwableList); + Logger.logErrorExtended(LOG_TAG, executionCommand.toString()); return null; } diff --git a/app/src/main/java/com/termux/app/utils/PluginUtils.java b/app/src/main/java/com/termux/app/utils/PluginUtils.java index 2ea2a0f2..6515fd00 100644 --- a/app/src/main/java/com/termux/app/utils/PluginUtils.java +++ b/app/src/main/java/com/termux/app/utils/PluginUtils.java @@ -1,18 +1,23 @@ package com.termux.app.utils; -import android.app.Activity; import android.app.Notification; import android.app.NotificationManager; import android.app.PendingIntent; import android.content.Context; import android.content.Intent; -import android.os.Bundle; import androidx.annotation.Nullable; import com.termux.R; import com.termux.shared.activities.ReportActivity; +import com.termux.shared.file.TermuxFileUtils; +import com.termux.shared.models.ResultConfig; +import com.termux.shared.models.ResultData; +import com.termux.shared.models.errors.Errno; +import com.termux.shared.models.errors.Error; import com.termux.shared.notification.NotificationUtils; +import com.termux.shared.shell.ResultSender; +import com.termux.shared.shell.ShellUtils; import com.termux.shared.termux.TermuxConstants; import com.termux.shared.termux.TermuxConstants.TERMUX_APP.TERMUX_SERVICE; import com.termux.shared.logger.Logger; @@ -29,12 +34,6 @@ import com.termux.shared.termux.TermuxUtils; public class PluginUtils { - /** Required file permissions for the executable file of execute intent. Executable file must have read and execute permissions */ - public static final String PLUGIN_EXECUTABLE_FILE_PERMISSIONS = "r-x"; // Default: "r-x" - /** Required file permissions for the working directory of execute intent. Working directory must have read and write permissions. - * Execute permissions should be attempted to be set, but ignored if they are missing */ - public static final String PLUGIN_WORKING_DIRECTORY_PERMISSIONS = "rwx"; // Default: "rwx" - private static final String LOG_TAG = "PluginUtils"; /** @@ -43,8 +42,8 @@ public class PluginUtils { * The ExecutionCommand currentState must be greater or equal to * {@link ExecutionCommand.ExecutionState#EXECUTED}. * If the {@link ExecutionCommand#isPluginExecutionCommand} is {@code true} and - * {@link ExecutionCommand#pluginPendingIntent} is not {@code null}, then the result of commands - * are sent back to the {@link PendingIntent} creator. + * {@link ResultConfig#resultPendingIntent} or {@link ResultConfig#resultDirectoryPath} + * is not {@code null}, then the result of commands are sent back to the command caller. * * @param context The {@link Context} that will be used to send result intent to the {@link PendingIntent} creator. * @param logTag The log tag to use for logging. @@ -54,31 +53,42 @@ public class PluginUtils { if (executionCommand == null) return; logTag = DataUtils.getDefaultIfNull(logTag, LOG_TAG); + Error error = null; + ResultData resultData = executionCommand.resultData; if (!executionCommand.hasExecuted()) { - Logger.logWarn(logTag, "Ignoring call to processPluginExecutionCommandResult() since the execution command state is not higher than the ExecutionState.EXECUTED"); + Logger.logWarn(logTag, executionCommand.getCommandIdAndLabelLogString() + ": Ignoring call to processPluginExecutionCommandResult() since the execution command state is not higher than the ExecutionState.EXECUTED"); return; } - Logger.logDebug(LOG_TAG, executionCommand.toString()); + boolean isPluginExecutionCommandWithPendingResult = executionCommand.isPluginExecutionCommandWithPendingResult(); - boolean result = true; + // Log the output. ResultData should not be logged if pending result since ResultSender will do it + Logger.logDebugExtended(logTag, ExecutionCommand.getExecutionOutputLogString(executionCommand, true, !isPluginExecutionCommandWithPendingResult)); - // If isPluginExecutionCommand is true and pluginPendingIntent is not null, then - // send pluginPendingIntent to its creator with the result - if (executionCommand.isPluginExecutionCommand && executionCommand.pluginPendingIntent != null) { - String errmsg = executionCommand.errmsg; + // If execution command was started by a plugin which expects the result back + if (isPluginExecutionCommandWithPendingResult) { + // Set variables which will be used by sendCommandResultData to send back the result + if (executionCommand.resultConfig.resultPendingIntent != null) + setPluginResultPendingIntentVariables(executionCommand); + if (executionCommand.resultConfig.resultDirectoryPath != null) + setPluginResultDirectoryVariables(executionCommand); - //Combine errmsg and stacktraces - if (executionCommand.isStateFailed()) { - errmsg = Logger.getMessageAndStackTracesString(executionCommand.errmsg, executionCommand.throwableList); + // Send result to caller + error = ResultSender.sendCommandResultData(context, logTag, executionCommand.getCommandIdAndLabelLogString(), executionCommand.resultConfig, executionCommand.resultData); + if (error != null) { + // error will be added to existing Errors + resultData.setStateFailed(error); + Logger.logDebugExtended(logTag, ExecutionCommand.getExecutionOutputLogString(executionCommand, true, true)); + + // Flash and send notification for the error + Logger.showToast(context, ResultData.getErrorsListMinimalString(resultData), true); + sendPluginCommandErrorNotification(context, logTag, executionCommand, ResultData.getErrorsListMinimalString(resultData)); } - // Send pluginPendingIntent to its creator - result = sendPluginExecutionCommandResultPendingIntent(context, logTag, executionCommand.getCommandIdAndLabelLogString(), executionCommand.stdout, executionCommand.stderr, executionCommand.exitCode, executionCommand.errCode, errmsg, executionCommand.pluginPendingIntent); } - if (!executionCommand.isStateFailed() && result) + if (!executionCommand.isStateFailed() && error == null) executionCommand.setState(ExecutionCommand.ExecutionState.SUCCESS); } @@ -86,14 +96,13 @@ public class PluginUtils { * Process {@link ExecutionCommand} error. * * The ExecutionCommand currentState must be equal to {@link ExecutionCommand.ExecutionState#FAILED}. - * The {@link ExecutionCommand#errCode} must have been set to a value greater than - * {@link ExecutionCommand#RESULT_CODE_OK}. - * The {@link ExecutionCommand#errmsg} and any {@link ExecutionCommand#throwableList} must also - * be set with appropriate error info. + * The {@link ResultData#getErrCode()} must have been set to a value greater than + * {@link Errno#ERRNO_SUCCESS}. + * The {@link ResultData#errorsList} must also be set with appropriate error info. * * If the {@link ExecutionCommand#isPluginExecutionCommand} is {@code true} and - * {@link ExecutionCommand#pluginPendingIntent} is not {@code null}, then the errors of commands - * are sent back to the {@link PendingIntent} creator. + * {@link ResultConfig#resultPendingIntent} or {@link ResultConfig#resultDirectoryPath} + * is not {@code null}, then the errors of commands are sent back to the command caller. * * Otherwise if the {@link TERMUX_APP#KEY_PLUGIN_ERROR_NOTIFICATIONS_ENABLED} is * enabled, then a flash and a notification will be shown for the error as well @@ -112,44 +121,93 @@ public class PluginUtils { if (context == null || executionCommand == null) return; logTag = DataUtils.getDefaultIfNull(logTag, LOG_TAG); + Error error; + ResultData resultData = executionCommand.resultData; if (!executionCommand.isStateFailed()) { - Logger.logWarn(logTag, "Ignoring call to processPluginExecutionCommandError() since the execution command is not in ExecutionState.FAILED"); + Logger.logWarn(logTag, executionCommand.getCommandIdAndLabelLogString() + ": Ignoring call to processPluginExecutionCommandError() since the execution command is not in ExecutionState.FAILED"); return; } - // Log the error and any exception - Logger.logStackTracesWithMessage(logTag, "(" + executionCommand.errCode + ") " + executionCommand.errmsg, executionCommand.throwableList); + boolean isPluginExecutionCommandWithPendingResult = executionCommand.isPluginExecutionCommandWithPendingResult(); + // Log the error and any exception. ResultData should not be logged if pending result since ResultSender will do it + Logger.logErrorExtended(logTag, ExecutionCommand.getExecutionOutputLogString(executionCommand, true, !isPluginExecutionCommandWithPendingResult)); - // If isPluginExecutionCommand is true and pluginPendingIntent is not null, then - // send pluginPendingIntent to its creator with the errors - if (executionCommand.isPluginExecutionCommand && executionCommand.pluginPendingIntent != null) { - String errmsg = executionCommand.errmsg; + // If execution command was started by a plugin which expects the result back + if (isPluginExecutionCommandWithPendingResult) { + // Set variables which will be used by sendCommandResultData to send back the result + if (executionCommand.resultConfig.resultPendingIntent != null) + setPluginResultPendingIntentVariables(executionCommand); + if (executionCommand.resultConfig.resultDirectoryPath != null) + setPluginResultDirectoryVariables(executionCommand); - //Combine errmsg and stacktraces - if (executionCommand.isStateFailed()) { - errmsg = Logger.getMessageAndStackTracesString(executionCommand.errmsg, executionCommand.throwableList); + // Send result to caller + error = ResultSender.sendCommandResultData(context, logTag, executionCommand.getCommandIdAndLabelLogString(), executionCommand.resultConfig, executionCommand.resultData); + if (error != null) { + // error will be added to existing Errors + resultData.setStateFailed(error); + Logger.logErrorExtended(logTag, ExecutionCommand.getExecutionOutputLogString(executionCommand, true, true)); + forceNotification = true; } - sendPluginExecutionCommandResultPendingIntent(context, logTag, executionCommand.getCommandIdAndLabelLogString(), executionCommand.stdout, executionCommand.stderr, executionCommand.exitCode, executionCommand.errCode, errmsg, executionCommand.pluginPendingIntent); - // No need to show notifications if a pending intent was sent, let the caller handle the result himself if (!forceNotification) return; } - TermuxAppSharedPreferences preferences = TermuxAppSharedPreferences.build(context); if (preferences == null) return; - // If user has disabled notifications for plugin, then just return + // If user has disabled notifications for plugin commands, then just return if (!preferences.arePluginErrorNotificationsEnabled() && !forceNotification) return; - // Flash the errmsg - Logger.showToast(context, executionCommand.errmsg, true); + // Flash and send notification for the error + Logger.showToast(context, ResultData.getErrorsListMinimalString(resultData), true); + sendPluginCommandErrorNotification(context, logTag, executionCommand, ResultData.getErrorsListMinimalString(resultData)); - // Send a notification to show the errmsg which when clicked will open the {@link ReportActivity} + } + + /** Set variables which will be used by {@link ResultSender#sendCommandResultData(Context, String, String, ResultConfig, ResultData)} + * to send back the result via {@link ResultConfig#resultPendingIntent}. */ + public static void setPluginResultPendingIntentVariables(ExecutionCommand executionCommand) { + ResultConfig resultConfig = executionCommand.resultConfig; + + resultConfig.resultBundleKey = TERMUX_SERVICE.EXTRA_PLUGIN_RESULT_BUNDLE; + resultConfig.resultStdoutKey = TERMUX_SERVICE.EXTRA_PLUGIN_RESULT_BUNDLE_STDOUT; + resultConfig.resultStdoutOriginalLengthKey = TERMUX_SERVICE.EXTRA_PLUGIN_RESULT_BUNDLE_STDOUT_ORIGINAL_LENGTH; + resultConfig.resultStderrKey = TERMUX_SERVICE.EXTRA_PLUGIN_RESULT_BUNDLE_STDERR; + resultConfig.resultStderrOriginalLengthKey = TERMUX_SERVICE.EXTRA_PLUGIN_RESULT_BUNDLE_STDERR_ORIGINAL_LENGTH; + resultConfig.resultExitCodeKey = TERMUX_SERVICE.EXTRA_PLUGIN_RESULT_BUNDLE_EXIT_CODE; + resultConfig.resultErrCodeKey = TERMUX_SERVICE.EXTRA_PLUGIN_RESULT_BUNDLE_ERR; + resultConfig.resultErrmsgKey = TERMUX_SERVICE.EXTRA_PLUGIN_RESULT_BUNDLE_ERRMSG; + } + + /** Set variables which will be used by {@link ResultSender#sendCommandResultData(Context, String, String, ResultConfig, ResultData)} + * to send back the result by writing it to files in {@link ResultConfig#resultDirectoryPath}. */ + public static void setPluginResultDirectoryVariables(ExecutionCommand executionCommand) { + ResultConfig resultConfig = executionCommand.resultConfig; + + resultConfig.resultDirectoryPath = TermuxFileUtils.getCanonicalPath(resultConfig.resultDirectoryPath, null, true); + resultConfig.resultDirectoryAllowedParentPath = TermuxFileUtils.getMatchedAllowedTermuxWorkingDirectoryParentPathForPath(resultConfig.resultDirectoryPath); + + // Set default resultFileBasename if resultSingleFile is true to `-.log` + if (resultConfig.resultSingleFile && resultConfig.resultFileBasename == null) + resultConfig.resultFileBasename = ShellUtils.getExecutableBasename(executionCommand.executable) + "-" + TermuxUtils.getCurrentMilliSecondLocalTimeStamp() + ".log"; + } + + + + /** + * Send an error notification for {@link TermuxConstants#TERMUX_PLUGIN_COMMAND_ERRORS_NOTIFICATION_CHANNEL_ID} + * and {@link TermuxConstants#TERMUX_PLUGIN_COMMAND_ERRORS_NOTIFICATION_CHANNEL_NAME}. + * + * @param context The {@link Context} for operations. + * @param executionCommand The {@link ExecutionCommand} that failed. + * @param notificationTextString The text of the notification. + */ + public static void sendPluginCommandErrorNotification(Context context, String logTag, ExecutionCommand executionCommand, String notificationTextString) { + // Send a notification to show the error which when clicked will open the ReportActivity // to show the details of the error String title = TermuxConstants.TERMUX_APP_NAME + " Plugin Execution Command Error"; @@ -166,105 +224,20 @@ public class PluginUtils { setupPluginCommandErrorsNotificationChannel(context); // Use markdown in notification - CharSequence notificationText = MarkdownUtils.getSpannedMarkdownText(context, executionCommand.errmsg); - //CharSequence notificationText = executionCommand.errmsg; + CharSequence notificationTextCharSequence = MarkdownUtils.getSpannedMarkdownText(context, notificationTextString); + //CharSequence notificationTextCharSequence = notificationTextString; // Build the notification - Notification.Builder builder = getPluginCommandErrorsNotificationBuilder(context, title, notificationText, notificationText, pendingIntent, NotificationUtils.NOTIFICATION_MODE_VIBRATE); - if (builder == null) return; + Notification.Builder builder = getPluginCommandErrorsNotificationBuilder(context, title, notificationTextCharSequence, notificationTextCharSequence, pendingIntent, NotificationUtils.NOTIFICATION_MODE_VIBRATE); + if (builder == null) return; // Send the notification int nextNotificationId = NotificationUtils.getNextNotificationId(context); NotificationManager notificationManager = NotificationUtils.getNotificationManager(context); if (notificationManager != null) notificationManager.notify(nextNotificationId, builder.build()); - } - /** - * Send {@link ExecutionCommand} result {@link PendingIntent} in the - * {@link TERMUX_SERVICE#EXTRA_PLUGIN_RESULT_BUNDLE} bundle. - * - * - * @param context The {@link Context} that will be used to send result intent to the {@link PendingIntent} creator. - * @param logTag The log tag to use for logging. - * @param label The label of {@link ExecutionCommand}. - * @param stdout The stdout of {@link ExecutionCommand}. - * @param stderr The stderr of {@link ExecutionCommand}. - * @param exitCode The exitCode of {@link ExecutionCommand}. - * @param errCode The errCode of {@link ExecutionCommand}. - * @param errmsg The errmsg of {@link ExecutionCommand}. - * @param pluginPendingIntent The pluginPendingIntent of {@link ExecutionCommand}. - * @return Returns {@code true} if pluginPendingIntent was successfully send, otherwise [@code false}. - */ - public static boolean sendPluginExecutionCommandResultPendingIntent(Context context, String logTag, String label, String stdout, String stderr, Integer exitCode, Integer errCode, String errmsg, PendingIntent pluginPendingIntent) { - if (context == null || pluginPendingIntent == null) return false; - - logTag = DataUtils.getDefaultIfNull(logTag, LOG_TAG); - - Logger.logDebug(logTag, "Sending execution result for Execution Command \"" + label + "\" to " + pluginPendingIntent.getCreatorPackage()); - - String truncatedStdout = null; - String truncatedStderr = null; - - String stdoutOriginalLength = (stdout == null) ? null: String.valueOf(stdout.length()); - String stderrOriginalLength = (stderr == null) ? null: String.valueOf(stderr.length()); - - // Truncate stdout and stdout to max TRANSACTION_SIZE_LIMIT_IN_BYTES - if (stderr == null || stderr.isEmpty()) { - truncatedStdout = DataUtils.getTruncatedCommandOutput(stdout, DataUtils.TRANSACTION_SIZE_LIMIT_IN_BYTES, false, false, false); - } else if (stdout == null || stdout.isEmpty()) { - truncatedStderr = DataUtils.getTruncatedCommandOutput(stderr, DataUtils.TRANSACTION_SIZE_LIMIT_IN_BYTES, false, false, false); - } else { - truncatedStdout = DataUtils.getTruncatedCommandOutput(stdout, DataUtils.TRANSACTION_SIZE_LIMIT_IN_BYTES / 2, false, false, false); - truncatedStderr = DataUtils.getTruncatedCommandOutput(stderr, DataUtils.TRANSACTION_SIZE_LIMIT_IN_BYTES / 2, false, false, false); - } - - if (truncatedStdout != null && truncatedStdout.length() < stdout.length()) { - Logger.logWarn(logTag, "Execution Result for Execution Command \"" + label + "\" stdout length truncated from " + stdoutOriginalLength + " to " + truncatedStdout.length()); - stdout = truncatedStdout; - } - - if (truncatedStderr != null && truncatedStderr.length() < stderr.length()) { - Logger.logWarn(logTag, "Execution Result for Execution Command \"" + label + "\" stderr length truncated from " + stderrOriginalLength + " to " + truncatedStderr.length()); - stderr = truncatedStderr; - } - - String errmsgOriginalLength = (errmsg == null) ? null: String.valueOf(errmsg.length()); - - // Truncate errmsg to max TRANSACTION_SIZE_LIMIT_IN_BYTES / 4 - // trim from end to preserve start of stacktraces - String truncatedErrmsg = DataUtils.getTruncatedCommandOutput(errmsg, DataUtils.TRANSACTION_SIZE_LIMIT_IN_BYTES / 4, true, false, false); - if (truncatedErrmsg != null && truncatedErrmsg.length() < errmsg.length()) { - Logger.logWarn(logTag, "Execution Result for Execution Command \"" + label + "\" errmsg length truncated from " + errmsgOriginalLength + " to " + truncatedErrmsg.length()); - errmsg = truncatedErrmsg; - } - - - final Bundle resultBundle = new Bundle(); - resultBundle.putString(TERMUX_SERVICE.EXTRA_PLUGIN_RESULT_BUNDLE_STDOUT, stdout); - resultBundle.putString(TERMUX_SERVICE.EXTRA_PLUGIN_RESULT_BUNDLE_STDOUT_ORIGINAL_LENGTH, stdoutOriginalLength); - resultBundle.putString(TERMUX_SERVICE.EXTRA_PLUGIN_RESULT_BUNDLE_STDERR, stderr); - resultBundle.putString(TERMUX_SERVICE.EXTRA_PLUGIN_RESULT_BUNDLE_STDERR_ORIGINAL_LENGTH, stderrOriginalLength); - if (exitCode != null) resultBundle.putInt(TERMUX_SERVICE.EXTRA_PLUGIN_RESULT_BUNDLE_EXIT_CODE, exitCode); - if (errCode != null) resultBundle.putInt(TERMUX_SERVICE.EXTRA_PLUGIN_RESULT_BUNDLE_ERR, errCode); - resultBundle.putString(TERMUX_SERVICE.EXTRA_PLUGIN_RESULT_BUNDLE_ERRMSG, errmsg); - - Intent resultIntent = new Intent(); - resultIntent.putExtra(TERMUX_SERVICE.EXTRA_PLUGIN_RESULT_BUNDLE, resultBundle); - - try { - pluginPendingIntent.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 Execution Command \"" + label + "\" creator " + pluginPendingIntent.getCreatorPackage() + " does not want the results anymore"); - } - - return true; - } - - - /** * Get {@link Notification.Builder} for {@link TermuxConstants#TERMUX_PLUGIN_COMMAND_ERRORS_NOTIFICATION_CHANNEL_ID} * and {@link TermuxConstants#TERMUX_PLUGIN_COMMAND_ERRORS_NOTIFICATION_CHANNEL_NAME}. @@ -318,7 +291,7 @@ public class PluginUtils { * Check if {@link TermuxConstants#PROP_ALLOW_EXTERNAL_APPS} property is not set to "true". * * @param context The {@link Context} to get error string. - * @return Returns the {@code errmsg} if policy is violated, otherwise {@code null}. + * @return Returns the {@code error} if policy is violated, otherwise {@code null}. */ public static String checkIfRunCommandServiceAllowExternalAppsPolicyIsViolated(final Context context) { String errmsg = null; diff --git a/termux-shared/src/main/java/com/termux/shared/data/DataUtils.java b/termux-shared/src/main/java/com/termux/shared/data/DataUtils.java index 9a6af5a0..368755c5 100644 --- a/termux-shared/src/main/java/com/termux/shared/data/DataUtils.java +++ b/termux-shared/src/main/java/com/termux/shared/data/DataUtils.java @@ -2,6 +2,8 @@ package com.termux.shared.data; import android.os.Bundle; +import androidx.annotation.Nullable; + import java.util.LinkedHashSet; import java.util.regex.Matcher; import java.util.regex.Pattern; @@ -23,7 +25,7 @@ public class DataUtils { if (maxLength < 0 || text.length() < maxLength) return text; if (fromEnd) { - text = text.substring(0, Math.min(text.length(), maxLength)); + text = text.substring(0, maxLength); } else { int cutOffIndex = text.length() - maxLength; @@ -42,6 +44,21 @@ public class DataUtils { return text; } + /** + * Replace a sub string in each item of a {@link String[]}. + * + * @param array The {@link String[]} to replace in. + * @param find The sub string to replace. + * @param replace The sub string to replace with. + */ + public static void replaceSubStringsInStringArrayItems(String[] array, String find, String replace) { + if(array == null || array.length == 0) return; + + for (int i = 0; i < array.length; i++) { + array[i] = array[i].replace(find, replace); + } + } + /** * Get the {@code float} from a {@link String}. * @@ -139,10 +156,14 @@ public class DataUtils { * @param def The default {@link Object}. * @return Returns {@code object} if it is not {@code null}, otherwise returns {@code def}. */ - public static T getDefaultIfNull(@androidx.annotation.Nullable T object, @androidx.annotation.Nullable T def) { + public static T getDefaultIfNull(@Nullable T object, @Nullable T def) { return (object == null) ? def : object; } + /** Check if a string is null or empty. */ + public static boolean isNullOrEmpty(String string) { + return string == null || string.isEmpty(); + } public static LinkedHashSet extractUrls(String text) { diff --git a/termux-shared/src/main/java/com/termux/shared/data/IntentUtils.java b/termux-shared/src/main/java/com/termux/shared/data/IntentUtils.java new file mode 100644 index 00000000..885eb423 --- /dev/null +++ b/termux-shared/src/main/java/com/termux/shared/data/IntentUtils.java @@ -0,0 +1,143 @@ +package com.termux.shared.data; + +import android.content.Intent; +import android.os.Bundle; +import android.os.Parcelable; + +import androidx.annotation.NonNull; + +import java.util.Arrays; + +public class IntentUtils { + + private static final String LOG_TAG = "IntentUtils"; + + + /** + * Get a {@link String} extra from an {@link Intent} if its not {@code null} or empty. + * + * @param intent The {@link Intent} to get the extra from. + * @param key The {@link String} key name. + * @param def The default value if extra is not set. + * @param throwExceptionIfNotSet If set to {@code true}, then an exception will be thrown if extra + * is not set. + * @return Returns the {@link String} extra if set, otherwise {@code null}. + */ + public static String getStringExtraIfSet(@NonNull Intent intent, String key, String def, boolean throwExceptionIfNotSet) throws Exception { + String value = getStringExtraIfSet(intent, key, def); + if (value == null && throwExceptionIfNotSet) + throw new Exception("The \"" + key + "\" key string value is null or empty"); + return value; + } + + /** + * Get a {@link String} extra from an {@link Intent} if its not {@code null} or empty. + * + * @param intent The {@link Intent} to get the extra from. + * @param key The {@link String} key name. + * @param def The default value if extra is not set. + * @return Returns the {@link String} extra if set, otherwise {@code null}. + */ + public static String getStringExtraIfSet(@NonNull Intent intent, String key, String def) { + String value = intent.getStringExtra(key); + if (value == null || value.isEmpty()) { + if (def != null && !def.isEmpty()) + return def; + else + return null; + } + return value; + } + + + + /** + * Get a {@link String[]} extra from an {@link Intent} if its not {@code null} or empty. + * + * @param intent The {@link Intent} to get the extra from. + * @param key The {@link String} key name. + * @param def The default value if extra is not set. + * @param throwExceptionIfNotSet If set to {@code true}, then an exception will be thrown if extra + * is not set. + * @return Returns the {@link String[]} extra if set, otherwise {@code null}. + */ + public static String[] getStringArrayExtraIfSet(@NonNull Intent intent, String key, String[] def, boolean throwExceptionIfNotSet) throws Exception { + String[] value = getStringArrayExtraIfSet(intent, key, def); + if (value == null && throwExceptionIfNotSet) + throw new Exception("The \"" + key + "\" key string array is null or empty"); + return value; + } + + /** + * Get a {@link String[]} extra from an {@link Intent} if its not {@code null} or empty. + * + * @param intent The {@link Intent} to get the extra from. + * @param key The {@link String} key name. + * @param def The default value if extra is not set. + * @return Returns the {@link String[]} extra if set, otherwise {@code null}. + */ + public static String[] getStringArrayExtraIfSet(Intent intent, String key, String[] def) { + String[] value = intent.getStringArrayExtra(key); + if (value == null || value.length == 0) { + if (def != null && def.length != 0) + return def; + else + return null; + } + return value; + } + + public static String getIntentString(Intent intent) { + if (intent == null) return null; + + return intent.toString() + "\n" + getBundleString(intent.getExtras()); + } + + public static String getBundleString(Bundle bundle) { + if (bundle == null || bundle.size() == 0) return "Bundle[]"; + + StringBuilder bundleString = new StringBuilder("Bundle[\n"); + boolean first = true; + for (String key : bundle.keySet()) { + if (!first) + bundleString.append("\n"); + + bundleString.append(key).append(": `"); + + Object value = bundle.get(key); + if (value instanceof int[]) { + bundleString.append(Arrays.toString((int[]) value)); + } else if (value instanceof byte[]) { + bundleString.append(Arrays.toString((byte[]) value)); + } else if (value instanceof boolean[]) { + bundleString.append(Arrays.toString((boolean[]) value)); + } else if (value instanceof short[]) { + bundleString.append(Arrays.toString((short[]) value)); + } else if (value instanceof long[]) { + bundleString.append(Arrays.toString((long[]) value)); + } else if (value instanceof float[]) { + bundleString.append(Arrays.toString((float[]) value)); + } else if (value instanceof double[]) { + bundleString.append(Arrays.toString((double[]) value)); + } else if (value instanceof String[]) { + bundleString.append(Arrays.toString((String[]) value)); + } else if (value instanceof CharSequence[]) { + bundleString.append(Arrays.toString((CharSequence[]) value)); + } else if (value instanceof Parcelable[]) { + bundleString.append(Arrays.toString((Parcelable[]) value)); + } else if (value instanceof Bundle) { + bundleString.append(getBundleString((Bundle) value)); + } else { + bundleString.append(value); + } + + bundleString.append("`"); + + first = false; + } + + bundleString.append("\n]"); + return bundleString.toString(); + } + +} diff --git a/termux-shared/src/main/java/com/termux/shared/models/ExecutionCommand.java b/termux-shared/src/main/java/com/termux/shared/models/ExecutionCommand.java index 7fb8f963..77f77160 100644 --- a/termux-shared/src/main/java/com/termux/shared/models/ExecutionCommand.java +++ b/termux-shared/src/main/java/com/termux/shared/models/ExecutionCommand.java @@ -1,18 +1,17 @@ package com.termux.shared.models; -import android.app.Activity; -import android.app.PendingIntent; import android.content.Intent; import android.net.Uri; import androidx.annotation.NonNull; -import com.termux.shared.termux.TermuxConstants.TERMUX_APP.TERMUX_SERVICE; +import com.termux.shared.data.IntentUtils; +import com.termux.shared.models.errors.Error; import com.termux.shared.logger.Logger; import com.termux.shared.markdown.MarkdownUtils; import com.termux.shared.data.DataUtils; -import java.util.ArrayList; +import java.util.Collections; import java.util.List; public class ExecutionCommand { @@ -52,12 +51,6 @@ public class ExecutionCommand { } - // Define errCode values - // TODO: Define custom values for different cases - public final static int RESULT_CODE_OK = Activity.RESULT_OK; - public final static int RESULT_CODE_OK_MINOR_FAILURES = Activity.RESULT_FIRST_USER; - public final static int RESULT_CODE_FAILED = Activity.RESULT_FIRST_USER + 1; - public final static int RESULT_CODE_CANCELED = Activity.RESULT_FIRST_USER + 2; /** The optional unique id for the {@link ExecutionCommand}. */ public Integer id; @@ -110,33 +103,26 @@ public class ExecutionCommand { 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 {@link TERMUX_SERVICE#ACTION_SERVICE_EXECUTE} intent or from within Termux app itself. */ + * like with an intent or from within Termux app itself. */ public boolean isPluginExecutionCommand; - /** Defines the {@link Intent} received from the external plugin which started the {@link ExecutionCommand}. */ - public Intent pluginIntent; - /** Defines {@link PendingIntent} that should be sent if an external plugin requested the execution. */ - public PendingIntent pluginPendingIntent; + /** Defines the {@link ResultConfig} for the {@link ExecutionCommand} containing information + * on how to handle the result. */ + public final ResultConfig resultConfig = new ResultConfig(); - /** The stdout of shell command. */ - public String stdout; - /** The stderr of shell command. */ - public String stderr; - /** The exit code of shell command. */ - public Integer exitCode; + /** Defines the {@link ResultData} for the {@link ExecutionCommand} containing information + * of the result. */ + public final ResultData resultData = new ResultData(); - /** The internal error code of {@link ExecutionCommand}. */ - public Integer errCode = RESULT_CODE_OK; - /** The internal error message of {@link ExecutionCommand}. */ - public String errmsg; - /** The internal exceptions of {@link ExecutionCommand}. */ - public List throwableList = new ArrayList<>(); - /** Defines if processing results already called for this {@link ExecutionCommand}. */ public boolean processingResultsAlreadyCalled; + private static final String LOG_TAG = "ExecutionCommand"; public ExecutionCommand() { @@ -156,13 +142,101 @@ public class ExecutionCommand { 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 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 throwablesList) { + return setStateFailed(null, code, message, throwablesList); + } + public synchronized boolean setStateFailed(String type, int code, String message, List throwablesList) { + if (!this.resultData.setStateFailed(type, code, message, throwablesList)) { + Logger.logWarn(LOG_TAG, "setStateFailed for " + getCommandIdAndLabelLogString() + " resultData encountered an error."); + } + + return setState(ExecutionState.FAILED); + } + + public synchronized boolean shouldNotProcessResults() { + if (processingResultsAlreadyCalled) { + return true; + } else { + processingResultsAlreadyCalled = true; + return false; + } + } + + public synchronized boolean isStateFailed() { + if (currentState != ExecutionState.FAILED) + return false; + + if (!resultData.isStateFailed()) { + Logger.logWarn(LOG_TAG, "The " + getCommandIdAndLabelLogString() + " has an invalid errCode value set in errors list while having ExecutionState.FAILED state.\n" + resultData.errorsList); + return false; + } else { + return true; + } + } + + @NonNull @Override public String toString() { if (!hasExecuted()) return getExecutionInputLogString(this, true); else { - return getExecutionOutputLogString(this, true); + return getExecutionOutputLogString(this, true, true); } } @@ -190,15 +264,15 @@ public class ExecutionCommand { logString.append("\n").append(executionCommand.getInBackgroundLogString()); logString.append("\n").append(executionCommand.getIsFailsafeLogString()); - 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 (!ignoreNull || executionCommand.isPluginExecutionCommand) { - if (!ignoreNull || executionCommand.pluginPendingIntent != null) - logString.append("\n").append(executionCommand.getPendingIntentCreatorLogString()); - } + if (executionCommand.isPluginExecutionCommand) + logString.append("\n").append(ResultConfig.getResultConfigLogString(executionCommand.resultConfig, ignoreNull)); return logString.toString(); } @@ -208,9 +282,10 @@ public class ExecutionCommand { * * @param executionCommand The {@link ExecutionCommand} to convert. * @param ignoreNull Set to {@code true} if non-critical {@code null} values are to be ignored. + * @param logResultData Set to {@code true} if {@link #resultData} should be logged. * @return Returns the log friendly {@link String}. */ - public static String getExecutionOutputLogString(final ExecutionCommand executionCommand, boolean ignoreNull) { + public static String getExecutionOutputLogString(final ExecutionCommand executionCommand, boolean ignoreNull, boolean logResultData) { if (executionCommand == null) return "null"; StringBuilder logString = new StringBuilder(); @@ -220,32 +295,8 @@ public class ExecutionCommand { logString.append("\n").append(executionCommand.getPreviousStateLogString()); logString.append("\n").append(executionCommand.getCurrentStateLogString()); - logString.append("\n").append(executionCommand.getStdoutLogString()); - logString.append("\n").append(executionCommand.getStderrLogString()); - logString.append("\n").append(executionCommand.getExitCodeLogString()); - - logString.append(getExecutionErrLogString(executionCommand, ignoreNull)); - - return logString.toString(); - } - - /** - * Get a log friendly {@link String} for {@link ExecutionCommand} execution error parameters. - * - * @param executionCommand The {@link ExecutionCommand} to convert. - * @param ignoreNull Set to {@code true} if non-critical {@code null} values are to be ignored. - * @return Returns the log friendly {@link String}. - */ - public static String getExecutionErrLogString(final ExecutionCommand executionCommand, boolean ignoreNull) { - StringBuilder logString = new StringBuilder(); - - if (!ignoreNull || (executionCommand.isStateFailed())) { - logString.append("\n").append(executionCommand.getErrCodeLogString()); - logString.append("\n").append(executionCommand.getErrmsgLogString()); - logString.append("\n").append(executionCommand.geStackTracesLogString()); - } else { - logString.append(""); - } + if (logResultData) + logString.append("\n").append(ResultData.getResultDataLogString(executionCommand.resultData, ignoreNull)); return logString.toString(); } @@ -262,7 +313,7 @@ public class ExecutionCommand { StringBuilder logString = new StringBuilder(); logString.append(getExecutionInputLogString(executionCommand, false)); - logString.append(getExecutionOutputLogString(executionCommand, false)); + logString.append(getExecutionOutputLogString(executionCommand, false, true)); logString.append("\n").append(executionCommand.getCommandDescriptionLogString()); logString.append("\n").append(executionCommand.getCommandHelpLogString()); @@ -299,18 +350,10 @@ public class ExecutionCommand { markdownString.append("\n").append(MarkdownUtils.getSingleLineMarkdownStringEntry("isPluginExecutionCommand", executionCommand.isPluginExecutionCommand, "-")); - if (executionCommand.pluginPendingIntent != null) - markdownString.append("\n").append(MarkdownUtils.getSingleLineMarkdownStringEntry("Pending Intent Creator", executionCommand.pluginPendingIntent.getCreatorPackage(), "-")); - else - markdownString.append("\n").append("**Pending Intent Creator:** - "); - markdownString.append("\n\n").append(MarkdownUtils.getMultiLineMarkdownStringEntry("Stdout", executionCommand.stdout, "-")); - markdownString.append("\n").append(MarkdownUtils.getMultiLineMarkdownStringEntry("Stderr", executionCommand.stderr, "-")); - markdownString.append("\n").append(MarkdownUtils.getSingleLineMarkdownStringEntry("Exit Code", executionCommand.exitCode, "-")); + markdownString.append("\n\n").append(ResultConfig.getResultConfigMarkdownString(executionCommand.resultConfig)); - markdownString.append("\n\n").append(MarkdownUtils.getSingleLineMarkdownStringEntry("Err Code", executionCommand.errCode, "-")); - markdownString.append("\n").append("**Errmsg:**\n").append(DataUtils.getDefaultIfNull(executionCommand.errmsg, "-")); - markdownString.append("\n\n").append(executionCommand.geStackTracesMarkdownString()); + markdownString.append("\n\n").append(ResultData.getResultDataMarkdownString(executionCommand.resultData)); if (executionCommand.commandDescription != null || executionCommand.commandHelp != null) { if (executionCommand.commandDescription != null) @@ -329,7 +372,6 @@ public class ExecutionCommand { } - public String getIdLogString() { if (id != null) return "(" + id + ") "; @@ -376,21 +418,10 @@ public class ExecutionCommand { return "isFailsafe: `" + isFailsafe + "`"; } - public String getIsPluginExecutionCommandLogString() { - return "isPluginExecutionCommand: `" + isPluginExecutionCommand + "`"; - } - public String getSessionActionLogString() { return Logger.getSingleLineLogStringEntry("Session Action", sessionAction, "-"); } - public String getPendingIntentCreatorLogString() { - if (pluginPendingIntent != null) - return "Pending Intent Creator: `" + pluginPendingIntent.getCreatorPackage() + "`"; - else - return "Pending Intent Creator: -"; - } - public String getCommandDescriptionLogString() { return Logger.getSingleLineLogStringEntry("Command Description", commandDescription, "-"); } @@ -403,35 +434,49 @@ public class ExecutionCommand { return Logger.getSingleLineLogStringEntry("Plugin API Help", pluginAPIHelp, "-"); } - public String getStdoutLogString() { - return Logger.getMultiLineLogStringEntry("Stdout", DataUtils.getTruncatedCommandOutput(stdout, Logger.LOGGER_ENTRY_SIZE_LIMIT_IN_BYTES / 5, false, false, true), "-"); + public String getCommandIntentLogString() { + if (commandIntent == null) + return "Command Intent: -"; + else + return Logger.getMultiLineLogStringEntry("Command Intent", IntentUtils.getIntentString(commandIntent), "-"); } - public String getStderrLogString() { - return Logger.getMultiLineLogStringEntry("Stderr", DataUtils.getTruncatedCommandOutput(stderr, Logger.LOGGER_ENTRY_SIZE_LIMIT_IN_BYTES / 5, false, false, true), "-"); - } - - public String getExitCodeLogString() { - return Logger.getSingleLineLogStringEntry("Exit Code", exitCode, "-"); - } - - public String getErrCodeLogString() { - return Logger.getSingleLineLogStringEntry("Err Code", errCode, "-"); - } - - public String getErrmsgLogString() { - return Logger.getMultiLineLogStringEntry("Errmsg", errmsg, "-"); - } - - public String geStackTracesLogString() { - return Logger.getStackTracesString("StackTraces:", Logger.getStackTraceStringArray(throwableList)); - } - - public String geStackTracesMarkdownString() { - return Logger.getStackTracesMarkdownString("StackTraces", Logger.getStackTraceStringArray(throwableList)); + public String getIsPluginExecutionCommandLogString() { + return "isPluginExecutionCommand: `" + isPluginExecutionCommand + "`"; } + /** + * Get a log friendly {@link String} for {@link List} 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. @@ -467,110 +512,4 @@ public class ExecutionCommand { return argumentsString.toString(); } - - /** - * Get a log friendly {@link String} for {@link List} 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_SIZE_LIMIT_IN_BYTES / 5, true, false, true), - "-")).append("`\n"); - } - argumentsString.append("```"); - } else{ - argumentsString.append(" -"); - } - - return argumentsString.toString(); - } - - - 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("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 setStateFailed(int errCode, String errmsg, Throwable throwable) { - if (errCode > RESULT_CODE_OK) { - this.errCode = errCode; - } else { - Logger.logWarn("Ignoring invalid " + getCommandIdAndLabelLogString() + " errCode value \"" + errCode + "\". Force setting it to RESULT_CODE_FAILED \"" + RESULT_CODE_FAILED + "\""); - this.errCode = RESULT_CODE_FAILED; - } - - this.errmsg = errmsg; - - if (!setState(ExecutionState.FAILED)) - return false; - - if (this.throwableList == null) - this.throwableList = new ArrayList<>(); - - if (throwable != null) - this.throwableList.add(throwable); - - return true; - } - - public synchronized boolean shouldNotProcessResults() { - if (processingResultsAlreadyCalled) { - return true; - } else { - processingResultsAlreadyCalled = true; - return false; - } - } - - public synchronized boolean isStateFailed() { - if (currentState != ExecutionState.FAILED) - return false; - - if (errCode <= RESULT_CODE_OK) { - Logger.logWarn("The " + getCommandIdAndLabelLogString() + " has an invalid errCode value \"" + errCode + "\" while having ExecutionState.FAILED state."); - return false; - } else { - 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; - } - } diff --git a/termux-shared/src/main/java/com/termux/shared/models/ResultConfig.java b/termux-shared/src/main/java/com/termux/shared/models/ResultConfig.java new file mode 100644 index 00000000..77bdad7f --- /dev/null +++ b/termux-shared/src/main/java/com/termux/shared/models/ResultConfig.java @@ -0,0 +1,170 @@ +package com.termux.shared.models; + +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(); + } + +} diff --git a/termux-shared/src/main/java/com/termux/shared/models/ResultData.java b/termux-shared/src/main/java/com/termux/shared/models/ResultData.java new file mode 100644 index 00000000..85e1b06b --- /dev/null +++ b/termux-shared/src/main/java/com/termux/shared/models/ResultData.java @@ -0,0 +1,256 @@ +package com.termux.shared.models; + +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.models.errors.Errno; +import com.termux.shared.models.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 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 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 throwablesList) { + return setStateFailed(null, code, message, throwablesList); + } + + public synchronized boolean setStateFailed(String type, int code, String message, List 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 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 getResultDataLogString(final ResultData resultData, boolean ignoreNull) { + if (resultData == null) return "null"; + + StringBuilder logString = new StringBuilder(); + + 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(); + } + +} diff --git a/termux-shared/src/main/java/com/termux/shared/models/errors/Errno.java b/termux-shared/src/main/java/com/termux/shared/models/errors/Errno.java index 0c21f02b..ce4c702d 100644 --- a/termux-shared/src/main/java/com/termux/shared/models/errors/Errno.java +++ b/termux-shared/src/main/java/com/termux/shared/models/errors/Errno.java @@ -17,9 +17,9 @@ public class Errno { public static final Errno ERRNO_SUCCESS = new Errno(TYPE, Activity.RESULT_OK, "Success"); + public static final Errno ERRNO_CANCELLED = new Errno(TYPE, Activity.RESULT_CANCELED, "Cancelled"); public static final Errno ERRNO_MINOR_FAILURES = new Errno(TYPE, Activity.RESULT_FIRST_USER, "Minor failure"); public static final Errno ERRNO_FAILED = new Errno(TYPE, Activity.RESULT_FIRST_USER + 1, "Failed"); - public static final Errno ERRNO_CANCELED = new Errno(TYPE, Activity.RESULT_FIRST_USER + 2, "Cancelled"); /** The errno type. */ protected String type; diff --git a/termux-shared/src/main/java/com/termux/shared/models/errors/ResultSenderErrno.java b/termux-shared/src/main/java/com/termux/shared/models/errors/ResultSenderErrno.java new file mode 100644 index 00000000..1cb92818 --- /dev/null +++ b/termux-shared/src/main/java/com/termux/shared/models/errors/ResultSenderErrno.java @@ -0,0 +1,20 @@ +package com.termux.shared.models.errors; + +/** 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); + } + +} diff --git a/termux-shared/src/main/java/com/termux/shared/shell/ResultSender.java b/termux-shared/src/main/java/com/termux/shared/shell/ResultSender.java new file mode 100644 index 00000000..2a5e9308 --- /dev/null +++ b/termux-shared/src/main/java/com/termux/shared/shell/ResultSender.java @@ -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; + } + +} diff --git a/termux-shared/src/main/java/com/termux/shared/shell/TermuxSession.java b/termux-shared/src/main/java/com/termux/shared/shell/TermuxSession.java index 5bc654ce..e40e3e31 100644 --- a/termux-shared/src/main/java/com/termux/shared/shell/TermuxSession.java +++ b/termux-shared/src/main/java/com/termux/shared/shell/TermuxSession.java @@ -7,6 +7,8 @@ import androidx.annotation.NonNull; import com.termux.shared.R; import com.termux.shared.models.ExecutionCommand; +import com.termux.shared.models.ResultData; +import com.termux.shared.models.errors.Errno; import com.termux.shared.termux.TermuxConstants; import com.termux.shared.logger.Logger; import com.termux.terminal.TerminalSession; @@ -51,7 +53,7 @@ public class TermuxSession { * @param terminalSessionClient The {@link TerminalSessionClient} interface implementation. * @param termuxSessionClient The {@link TermuxSessionClient} interface implementation. * @param sessionName The optional {@link TerminalSession} name. - * @param setStdoutOnExit If set to {@code true}, then the {@link ExecutionCommand#stdout} + * @param setStdoutOnExit If set to {@code true}, then the {@link ResultData#stdout} * available in the {@link TermuxSessionClient#onTermuxSessionExited(TermuxSession)} * callback will be set to the {@link TerminalSession} transcript. The session * transcript will contain both stdout and stderr combined, basically @@ -101,7 +103,7 @@ public class TermuxSession { executionCommand.commandLabel = processName; if (!executionCommand.setState(ExecutionCommand.ExecutionState.EXECUTING)) { - executionCommand.setStateFailed(ExecutionCommand.RESULT_CODE_FAILED, context.getString(R.string.error_failed_to_execute_termux_session_command, executionCommand.getCommandIdAndLabelLogString()), null); + executionCommand.setStateFailed(Errno.ERRNO_FAILED.getCode(), context.getString(R.string.error_failed_to_execute_termux_session_command, executionCommand.getCommandIdAndLabelLogString())); TermuxSession.processTermuxSessionResult(null, executionCommand); return null; } @@ -122,8 +124,8 @@ public class TermuxSession { * Signal that this {@link TermuxSession} has finished. This should be called when * {@link TerminalSessionClient#onSessionFinished(TerminalSession)} callback is received by the caller. * - * If the processes has finished, then sets {@link ExecutionCommand#stdout}, {@link ExecutionCommand#stderr} - * and {@link ExecutionCommand#exitCode} for the {@link #mExecutionCommand} of the {@code termuxTask} + * If the processes has finished, then sets {@link ResultData#stdout}, {@link ResultData#stderr} + * and {@link ResultData#exitCode} for the {@link #mExecutionCommand} of the {@code termuxTask} * and then calls {@link #processTermuxSessionResult(TermuxSession, ExecutionCommand)} to process the result}. * */ @@ -134,9 +136,9 @@ public class TermuxSession { int exitCode = mTerminalSession.getExitStatus(); if (exitCode == 0) - Logger.logDebug(LOG_TAG, "The \"" + mExecutionCommand.getCommandIdAndLabelLogString() + "\" TermuxSession with exited normally"); + Logger.logDebug(LOG_TAG, "The \"" + mExecutionCommand.getCommandIdAndLabelLogString() + "\" TermuxSession exited normally"); else - Logger.logDebug(LOG_TAG, "The \"" + mExecutionCommand.getCommandIdAndLabelLogString() + "\" TermuxSession with exited with code: " + exitCode); + Logger.logDebug(LOG_TAG, "The \"" + mExecutionCommand.getCommandIdAndLabelLogString() + "\" TermuxSession exited with code: " + exitCode); // If the execution command has already failed, like SIGKILL was sent, then don't continue if (mExecutionCommand.isStateFailed()) { @@ -144,13 +146,10 @@ public class TermuxSession { return; } - if (this.mSetStdoutOnExit) - mExecutionCommand.stdout = ShellUtils.getTerminalSessionTranscriptText(mTerminalSession, true, false); - else - mExecutionCommand.stdout = null; + mExecutionCommand.resultData.exitCode = exitCode; - mExecutionCommand.stderr = null; - mExecutionCommand.exitCode = exitCode; + if (this.mSetStdoutOnExit) + mExecutionCommand.resultData.stdout.append(ShellUtils.getTerminalSessionTranscriptText(mTerminalSession, true, false)); if (!mExecutionCommand.setState(ExecutionCommand.ExecutionState.EXECUTED)) return; @@ -162,8 +161,6 @@ public class TermuxSession { * Kill this {@link TermuxSession} by sending a {@link OsConstants#SIGILL} to its {@link #mTerminalSession} * if its still executing. * - * We process the results even if - * * @param context The {@link Context} for operations. * @param processResult If set to {@code true}, then the {@link #processTermuxSessionResult(TermuxSession, ExecutionCommand)} * will be called to process the failure. @@ -176,16 +173,13 @@ public class TermuxSession { } Logger.logDebug(LOG_TAG, "Send SIGKILL to \"" + mExecutionCommand.getCommandIdAndLabelLogString() + "\" TermuxSession"); - if (mExecutionCommand.setStateFailed(ExecutionCommand.RESULT_CODE_FAILED, context.getString(R.string.error_sending_sigkill_to_process), null)) { + if (mExecutionCommand.setStateFailed(Errno.ERRNO_FAILED.getCode(), context.getString(R.string.error_sending_sigkill_to_process))) { if (processResult) { + mExecutionCommand.resultData.exitCode = 137; // SIGKILL + // Get whatever output has been set till now in case its needed if (this.mSetStdoutOnExit) - mExecutionCommand.stdout = ShellUtils.getTerminalSessionTranscriptText(mTerminalSession, true, false); - else - mExecutionCommand.stdout = null; - - mExecutionCommand.stderr = null; - mExecutionCommand.exitCode = 137; // SIGKILL + mExecutionCommand.resultData.stdout.append(ShellUtils.getTerminalSessionTranscriptText(mTerminalSession, true, false)); TermuxSession.processTermuxSessionResult(this, null); } diff --git a/termux-shared/src/main/java/com/termux/shared/shell/TermuxTask.java b/termux-shared/src/main/java/com/termux/shared/shell/TermuxTask.java index 4507b30f..eac551d7 100644 --- a/termux-shared/src/main/java/com/termux/shared/shell/TermuxTask.java +++ b/termux-shared/src/main/java/com/termux/shared/shell/TermuxTask.java @@ -9,6 +9,8 @@ import androidx.annotation.NonNull; import com.termux.shared.R; import com.termux.shared.models.ExecutionCommand; +import com.termux.shared.models.ResultData; +import com.termux.shared.models.errors.Errno; import com.termux.shared.termux.TermuxConstants; import com.termux.shared.logger.Logger; import com.termux.shared.models.ExecutionCommand.ExecutionState; @@ -29,9 +31,6 @@ public final class TermuxTask { private final ExecutionCommand mExecutionCommand; private final TermuxTaskClient mTermuxTaskClient; - private final StringBuilder mStdout = new StringBuilder(); - private final StringBuilder mStderr = new StringBuilder(); - private static final String LOG_TAG = "TermuxTask"; private TermuxTask(@NonNull final Process process, @NonNull final ExecutionCommand executionCommand, @@ -71,7 +70,7 @@ public final class TermuxTask { final String[] commandArray = ShellUtils.setupProcessArgs(executionCommand.executable, executionCommand.arguments); if (!executionCommand.setState(ExecutionState.EXECUTING)) { - executionCommand.setStateFailed(ExecutionCommand.RESULT_CODE_FAILED, context.getString(R.string.error_failed_to_execute_termux_task_command, executionCommand.getCommandIdAndLabelLogString()), null); + 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; } @@ -88,7 +87,7 @@ public final class TermuxTask { try { process = Runtime.getRuntime().exec(commandArray, env, new File(executionCommand.workingDirectory)); } catch (IOException e) { - executionCommand.setStateFailed(ExecutionCommand.RESULT_CODE_FAILED, context.getString(R.string.error_failed_to_execute_termux_task_command, executionCommand.getCommandIdAndLabelLogString()), 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; } @@ -120,8 +119,8 @@ public final class 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 ExecutionCommand#stdout}, {@link ExecutionCommand#stderr} - * and {@link ExecutionCommand#exitCode} for the {@link #mExecutionCommand} of the {@code termuxTask} + * 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. @@ -131,15 +130,12 @@ public final class TermuxTask { Logger.logDebug(LOG_TAG, "Running \"" + mExecutionCommand.getCommandIdAndLabelLogString() + "\" TermuxTask with pid " + pid); - mExecutionCommand.stdout = null; - mExecutionCommand.stderr = null; - mExecutionCommand.exitCode = null; - + 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(), mStdout); - StreamGobbler STDERR = new StreamGobbler(pid + "-stderr", mProcess.getErrorStream(), mStderr); + StreamGobbler STDOUT = new StreamGobbler(pid + "-stdout", mProcess.getInputStream(), mExecutionCommand.resultData.stdout); + StreamGobbler STDERR = new StreamGobbler(pid + "-stderr", mProcess.getErrorStream(), mExecutionCommand.resultData.stderr); // start gobbling STDOUT.start(); @@ -153,7 +149,7 @@ public final class TermuxTask { //STDIN.write("exit\n".getBytes(StandardCharsets.UTF_8)); //STDIN.flush(); } catch(IOException e) { - if (e.getMessage().contains("EPIPE") || e.getMessage().contains("Stream closed")) { + 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. @@ -161,10 +157,8 @@ public final class TermuxTask { } else { // other issues we don't know how to handle, leads to // returning null - mExecutionCommand.setStateFailed(ExecutionCommand.RESULT_CODE_FAILED, context.getString(R.string.error_exception_received_while_executing_termux_task_command, mExecutionCommand.getCommandIdAndLabelLogString(), e.getMessage()), e); - mExecutionCommand.stdout = mStdout.toString(); - mExecutionCommand.stderr = mStderr.toString(); - mExecutionCommand.exitCode = -1; + 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; @@ -201,9 +195,7 @@ public final class TermuxTask { return; } - mExecutionCommand.stdout = mStdout.toString(); - mExecutionCommand.stderr = mStderr.toString(); - mExecutionCommand.exitCode = exitCode; + mExecutionCommand.resultData.exitCode = exitCode; if (!mExecutionCommand.setState(ExecutionState.EXECUTED)) return; @@ -228,13 +220,9 @@ public final class TermuxTask { Logger.logDebug(LOG_TAG, "Send SIGKILL to \"" + mExecutionCommand.getCommandIdAndLabelLogString() + "\" TermuxTask"); - if (mExecutionCommand.setStateFailed(ExecutionCommand.RESULT_CODE_FAILED, context.getString(R.string.error_sending_sigkill_to_process), null)) { + if (mExecutionCommand.setStateFailed(Errno.ERRNO_FAILED.getCode(), context.getString(R.string.error_sending_sigkill_to_process))) { if (processResult) { - // Get whatever output has been set till now in case its needed - mExecutionCommand.stdout = mStdout.toString(); - mExecutionCommand.stderr = mStderr.toString(); - mExecutionCommand.exitCode = 137; // SIGKILL - + mExecutionCommand.resultData.exitCode = 137; // SIGKILL TermuxTask.processTermuxTaskResult(this, null); } } diff --git a/termux-shared/src/main/java/com/termux/shared/termux/TermuxConstants.java b/termux-shared/src/main/java/com/termux/shared/termux/TermuxConstants.java index 79619201..e7985a14 100644 --- a/termux-shared/src/main/java/com/termux/shared/termux/TermuxConstants.java +++ b/termux-shared/src/main/java/com/termux/shared/termux/TermuxConstants.java @@ -2,12 +2,17 @@ package com.termux.shared.termux; import android.annotation.SuppressLint; +import com.termux.shared.models.ResultConfig; +import com.termux.shared.models.errors.Errno; + import java.io.File; import java.util.Arrays; +import java.util.Formatter; +import java.util.IllegalFormatException; import java.util.List; /* - * Version: v0.23.0 + * Version: v0.24.0 * * Changelog * @@ -152,6 +157,21 @@ import java.util.List; * * - 0.23.0 (2021-06-12) * - Rename `INTERNAL_PRIVATE_APP_DATA_DIR_PATH` to `TERMUX_INTERNAL_PRIVATE_APP_DATA_DIR_PATH`. + * + * - 0.24.0 (2021-06-27) + * - Add `COMMA_NORMAL`, `COMMA_ALTERNATIVE`. + * - Added following to `TERMUX_APP.TERMUX_SERVICE`: + * `EXTRA_RESULT_DIRECTORY`, `EXTRA_RESULT_SINGLE_FILE`, `EXTRA_RESULT_FILE_BASENAME`, + * `EXTRA_RESULT_FILE_OUTPUT_FORMAT`, `EXTRA_RESULT_FILE_ERROR_FORMAT`, `EXTRA_RESULT_FILES_SUFFIX`. + * - Added following to `TERMUX_APP.RUN_COMMAND_SERVICE`: + * `EXTRA_RESULT_DIRECTORY`, `EXTRA_RESULT_SINGLE_FILE`, `EXTRA_RESULT_FILE_BASENAME`, + * `EXTRA_RESULT_FILE_OUTPUT_FORMAT`, `EXTRA_RESULT_FILE_ERROR_FORMAT`, `EXTRA_RESULT_FILES_SUFFIX`, + * `EXTRA_REPLACE_COMMA_ALTERNATIVE_CHARS_IN_ARGUMENTS`, `EXTRA_COMMA_ALTERNATIVE_CHARS_IN_ARGUMENTS`. + * - Added following to `RESULT_SENDER`: + * `FORMAT_SUCCESS_STDOUT`, `FORMAT_SUCCESS_STDOUT__EXIT_CODE`, `FORMAT_SUCCESS_STDOUT__STDERR__EXIT_CODE` + * `FORMAT_FAILED_ERR__ERRMSG__STDOUT__STDERR__EXIT_CODE`, + * `RESULT_FILE_ERR_PREFIX`, `RESULT_FILE_ERRMSG_PREFIX` `RESULT_FILE_STDOUT_PREFIX`, + * `RESULT_FILE_STDERR_PREFIX`, `RESULT_FILE_EXIT_CODE_PREFIX`. */ /** @@ -636,19 +656,22 @@ public final class TermuxConstants { public static final File TERMUX_BOOT_SCRIPTS_DIR = new File(TERMUX_BOOT_SCRIPTS_DIR_PATH); - /** Termux app directory path to store foreground scripts that can be run by the termux launcher widget provided by Termux:Widget */ + /** Termux app directory path to store foreground scripts that can be run by the termux launcher + * widget provided by Termux:Widget */ public static final String TERMUX_SHORTCUT_SCRIPTS_DIR_PATH = TERMUX_DATA_HOME_DIR_PATH + "/shortcuts"; // Default: "/data/data/com.termux/files/home/.termux/shortcuts" /** Termux app directory to store foreground scripts that can be run by the termux launcher widget provided by Termux:Widget */ public static final File TERMUX_SHORTCUT_SCRIPTS_DIR = new File(TERMUX_SHORTCUT_SCRIPTS_DIR_PATH); - /** Termux app directory path to store background scripts that can be run by the termux launcher widget provided by Termux:Widget */ + /** Termux app directory path to store background scripts that can be run by the termux launcher + * widget provided by Termux:Widget */ public static final String TERMUX_SHORTCUT_TASKS_SCRIPTS_DIR_PATH = TERMUX_DATA_HOME_DIR_PATH + "/shortcuts/tasks"; // Default: "/data/data/com.termux/files/home/.termux/shortcuts/tasks" /** Termux app directory to store background scripts that can be run by the termux launcher widget provided by Termux:Widget */ public static final File TERMUX_SHORTCUT_TASKS_SCRIPTS_DIR = new File(TERMUX_SHORTCUT_TASKS_SCRIPTS_DIR_PATH); - /** Termux app directory path to store scripts to be run by 3rd party twofortyfouram locale plugin host apps like Tasker app via the Termux:Tasker plugin client */ + /** Termux app directory path to store scripts to be run by 3rd party twofortyfouram locale plugin + * host apps like Tasker app via the Termux:Tasker plugin client */ public static final String TERMUX_TASKER_SCRIPTS_DIR_PATH = TERMUX_DATA_HOME_DIR_PATH + "/tasker"; // Default: "/data/data/com.termux/files/home/.termux/tasker" /** Termux app directory to store scripts to be run by 3rd party twofortyfouram locale plugin host apps like Tasker app via the Termux:Tasker plugin client */ public static final File TERMUX_TASKER_SCRIPTS_DIR = new File(TERMUX_TASKER_SCRIPTS_DIR_PATH); @@ -693,10 +716,12 @@ public final class TermuxConstants { * Termux app and plugins miscellaneous variables. */ - /** Android OS permission declared by Termux app in AndroidManifest.xml which can be requested by 3rd party apps to run various commands in Termux app context */ + /** Android OS permission declared by Termux app in AndroidManifest.xml which can be requested by + * 3rd party apps to run various commands in Termux app context */ public static final String PERMISSION_RUN_COMMAND = TERMUX_PACKAGE_NAME + ".permission.RUN_COMMAND"; // Default: "com.termux.permission.RUN_COMMAND" - /** Termux property defined in termux.properties file as a secondary check to PERMISSION_RUN_COMMAND to allow 3rd party apps to run various commands in Termux app context */ + /** Termux property defined in termux.properties file as a secondary check to PERMISSION_RUN_COMMAND + * to allow 3rd party apps to run various commands in Termux app context */ public static final String PROP_ALLOW_EXTERNAL_APPS = "allow-external-apps"; // Default: "allow-external-apps" /** Default value for {@link #PROP_ALLOW_EXTERNAL_APPS} */ public static final String PROP_DEFAULT_VALUE_ALLOW_EXTERNAL_APPS = "false"; // Default: "false" @@ -707,6 +732,14 @@ public final class TermuxConstants { /** The Uri authority for Termux app file shares */ public static final String TERMUX_FILE_SHARE_URI_AUTHORITY = TERMUX_PACKAGE_NAME + ".files"; // Default: "com.termux.files" + /** The normal comma character (U+002C, ,, ,, comma) */ + public static final String COMMA_NORMAL = ","; // Default: "," + + /** The alternate comma character (U+201A, ‚, ‚, single low-9 quotation mark) that + * may be used instead of {@link #COMMA_NORMAL} */ + public static final String COMMA_ALTERNATIVE = "‚"; // Default: "‚" + + @@ -772,6 +805,7 @@ public final class TermuxConstants { /** Intent action to execute command with TERMUX_SERVICE */ public static final String ACTION_SERVICE_EXECUTE = TERMUX_PACKAGE_NAME + ".service_execute"; // Default: "com.termux.service_execute" + /** Uri scheme for paths sent via intent to TERMUX_SERVICE */ public static final String URI_SCHEME_SERVICE_EXECUTE = TERMUX_PACKAGE_NAME + ".file"; // Default: "com.termux.file" /** Intent {@code String[]} extra for arguments to the executable of the command for the TERMUX_SERVICE.ACTION_SERVICE_EXECUTE intent */ @@ -792,8 +826,30 @@ public final class TermuxConstants { public static final String EXTRA_COMMAND_HELP = TERMUX_PACKAGE_NAME + ".execute.command_help"; // Default: "com.termux.execute.command_help" /** Intent markdown {@code String} extra for help of the plugin API for the TERMUX_SERVICE.ACTION_SERVICE_EXECUTE intent (Internal Use Only) */ public static final String EXTRA_PLUGIN_API_HELP = TERMUX_PACKAGE_NAME + ".execute.plugin_api_help"; // Default: "com.termux.execute.plugin_help" - /** Intent {@code Parcelable} extra containing pending intent for the execute command caller */ + /** Intent {@code Parcelable} extra for the pending intent that should be sent with the + * result of the execution command to the execute command caller for the TERMUX_SERVICE.ACTION_SERVICE_EXECUTE intent */ public static final String EXTRA_PENDING_INTENT = "pendingIntent"; // Default: "pendingIntent" + /** Intent {@code String} extra for the directory path in which to write the result of the + * execution command for the execute command caller for the TERMUX_SERVICE.ACTION_SERVICE_EXECUTE intent */ + public static final String EXTRA_RESULT_DIRECTORY = TERMUX_PACKAGE_NAME + ".execute.result_directory"; // Default: "com.termux.execute.result_directory" + /** Intent {@code boolean} extra for whether the result should be written to a single file + * or multiple files (err, errmsg, stdout, stderr, exit_code) in + * {@link #EXTRA_RESULT_DIRECTORY} for the TERMUX_SERVICE.ACTION_SERVICE_EXECUTE intent */ + public static final String EXTRA_RESULT_SINGLE_FILE = TERMUX_PACKAGE_NAME + ".execute.result_single_file"; // Default: "com.termux.execute.result_single_file" + /** Intent {@code String} extra for the basename of the result file that should be created + * in {@link #EXTRA_RESULT_DIRECTORY} if {@link #EXTRA_RESULT_SINGLE_FILE} is {@code true} + * for the TERMUX_SERVICE.ACTION_SERVICE_EXECUTE intent */ + public static final String EXTRA_RESULT_FILE_BASENAME = TERMUX_PACKAGE_NAME + ".execute.result_file_basename"; // Default: "com.termux.execute.result_file_basename" + /** Intent {@code String} extra for the output {@link Formatter} format of the + * {@link #EXTRA_RESULT_FILE_BASENAME} result file for the TERMUX_SERVICE.ACTION_SERVICE_EXECUTE intent */ + public static final String EXTRA_RESULT_FILE_OUTPUT_FORMAT = TERMUX_PACKAGE_NAME + ".execute.result_file_output_format"; // Default: "com.termux.execute.result_file_output_format" + /** Intent {@code String} extra for the error {@link Formatter} format of the + * {@link #EXTRA_RESULT_FILE_BASENAME} result file for the TERMUX_SERVICE.ACTION_SERVICE_EXECUTE intent */ + public static final String EXTRA_RESULT_FILE_ERROR_FORMAT = TERMUX_PACKAGE_NAME + ".execute.result_file_error_format"; // Default: "com.termux.execute.result_file_error_format" + /** Intent {@code String} extra for the optional suffix of the result files that should + * be created in {@link #EXTRA_RESULT_DIRECTORY} if {@link #EXTRA_RESULT_SINGLE_FILE} is + * {@code false} for the TERMUX_SERVICE.ACTION_SERVICE_EXECUTE intent */ + public static final String EXTRA_RESULT_FILES_SUFFIX = TERMUX_PACKAGE_NAME + ".execute.result_files_suffix"; // Default: "com.termux.execute.result_files_suffix" @@ -864,12 +920,19 @@ public final class TermuxConstants { /** Termux RUN_COMMAND Intent help url */ public static final String RUN_COMMAND_API_HELP_URL = TERMUX_GITHUB_WIKI_REPO_URL + "/RUN_COMMAND-Intent"; // Default: "https://github.com/termux/termux-app/wiki/RUN_COMMAND-Intent" + /** Intent action to execute command with RUN_COMMAND_SERVICE */ public static final String ACTION_RUN_COMMAND = TERMUX_PACKAGE_NAME + ".RUN_COMMAND"; // Default: "com.termux.RUN_COMMAND" + /** Intent {@code String} extra for absolute path of command for the RUN_COMMAND_SERVICE.ACTION_RUN_COMMAND intent */ public static final String EXTRA_COMMAND_PATH = TERMUX_PACKAGE_NAME + ".RUN_COMMAND_PATH"; // Default: "com.termux.RUN_COMMAND_PATH" /** Intent {@code String[]} extra for arguments to the executable of the command for the RUN_COMMAND_SERVICE.ACTION_RUN_COMMAND intent */ public static final String EXTRA_ARGUMENTS = TERMUX_PACKAGE_NAME + ".RUN_COMMAND_ARGUMENTS"; // Default: "com.termux.RUN_COMMAND_ARGUMENTS" + /** Intent {@code boolean} extra for whether to replace comma alternative characters in arguments with comma characters for the RUN_COMMAND_SERVICE.ACTION_RUN_COMMAND intent */ + public static final String EXTRA_REPLACE_COMMA_ALTERNATIVE_CHARS_IN_ARGUMENTS = TERMUX_PACKAGE_NAME + ".RUN_COMMAND_REPLACE_COMMA_ALTERNATIVE_CHARS_IN_ARGUMENTS"; // Default: "com.termux.RUN_COMMAND_REPLACE_COMMA_ALTERNATIVE_CHARS_IN_ARGUMENTS" + /** Intent {@code String} extra for the comma alternative characters in arguments that should be replaced instead of the default {@link #COMMA_ALTERNATIVE} for the RUN_COMMAND_SERVICE.ACTION_RUN_COMMAND intent */ + public static final String EXTRA_COMMA_ALTERNATIVE_CHARS_IN_ARGUMENTS = TERMUX_PACKAGE_NAME + ".RUN_COMMAND_COMMA_ALTERNATIVE_CHARS_IN_ARGUMENTS"; // Default: "com.termux.RUN_COMMAND_COMMA_ALTERNATIVE_CHARS_IN_ARGUMENTS" + /** Intent {@code String} extra for stdin of the command for the RUN_COMMAND_SERVICE.ACTION_RUN_COMMAND intent */ public static final String EXTRA_STDIN = TERMUX_PACKAGE_NAME + ".RUN_COMMAND_STDIN"; // Default: "com.termux.RUN_COMMAND_STDIN" /** Intent {@code String} extra for current working directory of command for the RUN_COMMAND_SERVICE.ACTION_RUN_COMMAND intent */ @@ -884,8 +947,29 @@ public final class TermuxConstants { public static final String EXTRA_COMMAND_DESCRIPTION = TERMUX_PACKAGE_NAME + ".RUN_COMMAND_COMMAND_DESCRIPTION"; // Default: "com.termux.RUN_COMMAND_COMMAND_DESCRIPTION" /** Intent markdown {@code String} extra for help of the command for the RUN_COMMAND_SERVICE.ACTION_RUN_COMMAND intent */ public static final String EXTRA_COMMAND_HELP = TERMUX_PACKAGE_NAME + ".RUN_COMMAND_COMMAND_HELP"; // Default: "com.termux.RUN_COMMAND_COMMAND_HELP" - /** Intent {@code Parcelable} extra containing pending intent for the RUN_COMMAND_SERVICE.ACTION_RUN_COMMAND intent */ + /** Intent {@code Parcelable} extra for the pending intent that should be sent with the result of the execution command to the execute command caller for the RUN_COMMAND_SERVICE.ACTION_RUN_COMMAND intent */ public static final String EXTRA_PENDING_INTENT = TERMUX_PACKAGE_NAME + ".RUN_COMMAND_PENDING_INTENT"; // Default: "com.termux.RUN_COMMAND_PENDING_INTENT" + /** Intent {@code String} extra for the directory path in which to write the result of + * the execution command for the execute command caller for the RUN_COMMAND_SERVICE.ACTION_RUN_COMMAND intent */ + public static final String EXTRA_RESULT_DIRECTORY = TERMUX_PACKAGE_NAME + ".RUN_COMMAND_RESULT_DIRECTORY"; // Default: "com.termux.RUN_COMMAND_RESULT_DIRECTORY" + /** Intent {@code boolean} extra for whether the result should be written to a single file + * or multiple files (err, errmsg, stdout, stderr, exit_code) in + * {@link #EXTRA_RESULT_DIRECTORY} for the RUN_COMMAND_SERVICE.ACTION_RUN_COMMAND intent */ + public static final String EXTRA_RESULT_SINGLE_FILE = TERMUX_PACKAGE_NAME + ".RUN_COMMAND_RESULT_SINGLE_FILE"; // Default: "com.termux.RUN_COMMAND_RESULT_SINGLE_FILE" + /** Intent {@code String} extra for the basename of the result file that should be created + * in {@link #EXTRA_RESULT_DIRECTORY} if {@link #EXTRA_RESULT_SINGLE_FILE} is {@code true} + * for the RUN_COMMAND_SERVICE.ACTION_RUN_COMMAND intent */ + public static final String EXTRA_RESULT_FILE_BASENAME = TERMUX_PACKAGE_NAME + ".RUN_COMMAND_RESULT_FILE_BASENAME"; // Default: "com.termux.RUN_COMMAND_RESULT_FILE_BASENAME" + /** Intent {@code String} extra for the output {@link Formatter} format of the + * {@link #EXTRA_RESULT_FILE_BASENAME} result file for the RUN_COMMAND_SERVICE.ACTION_RUN_COMMAND intent */ + public static final String EXTRA_RESULT_FILE_OUTPUT_FORMAT = TERMUX_PACKAGE_NAME + ".RUN_COMMAND_RESULT_FILE_OUTPUT_FORMAT"; // Default: "com.termux.RUN_COMMAND_RESULT_FILE_OUTPUT_FORMAT" + /** Intent {@code String} extra for the error {@link Formatter} format of the + * {@link #EXTRA_RESULT_FILE_BASENAME} result file for the RUN_COMMAND_SERVICE.ACTION_RUN_COMMAND intent */ + public static final String EXTRA_RESULT_FILE_ERROR_FORMAT = TERMUX_PACKAGE_NAME + ".RUN_COMMAND_RESULT_FILE_ERROR_FORMAT"; // Default: "com.termux.RUN_COMMAND_RESULT_FILE_ERROR_FORMAT" + /** Intent {@code String} extra for the optional suffix of the result files that should be + * created in {@link #EXTRA_RESULT_DIRECTORY} if {@link #EXTRA_RESULT_SINGLE_FILE} is + * {@code false} for the RUN_COMMAND_SERVICE.ACTION_RUN_COMMAND intent */ + public static final String EXTRA_RESULT_FILES_SUFFIX = TERMUX_PACKAGE_NAME + ".RUN_COMMAND_RESULT_FILES_SUFFIX"; // Default: "com.termux.RUN_COMMAND_RESULT_FILES_SUFFIX" } } @@ -894,6 +978,66 @@ public final class TermuxConstants { + /** + * Termux class to send back results of commands to their callers like plugin or 3rd party apps. + */ + public static final class RESULT_SENDER { + + /* + * The default `Formatter` format strings to use for `ResultConfig#resultFileBasename` + * if `ResultConfig#resultSingleFile` is `true`. + */ + + /** The {@link Formatter} format string for success if only `stdout` needs to be written to + * {@link ResultConfig#resultFileBasename} where `stdout` maps to `%1$s`. + * This is used when `err` equals {@link Errno#ERRNO_SUCCESS} (-1) and `stderr` is empty + * and `exit_code` equals `0` and {@link ResultConfig#resultFileOutputFormat} is not passed. */ + public static final String FORMAT_SUCCESS_STDOUT = "%1$s%n"; + /** The {@link Formatter} format string for success if `stdout` and `exit_code` need to be written to + * {@link ResultConfig#resultFileBasename} where `stdout` maps to `%1$s` and `exit_code` to `%2$s`. + * This is used when `err` equals {@link Errno#ERRNO_SUCCESS} (-1) and `stderr` is empty + * and `exit_code` does not equal `0` and {@link ResultConfig#resultFileOutputFormat} is not passed. */ + public static final String FORMAT_SUCCESS_STDOUT__EXIT_CODE = "%1$s%n%n%n%nexit_code=`%2$s`%n"; + /** The {@link Formatter} format string for success if `stdout`, `stderr` and `exit_code` need to be + * written to {@link ResultConfig#resultFileBasename} where `stdout` maps to `%1$s`, `stderr` + * maps to `%2$s` and `exit_code` to `%3$s`. + * This is used when `err` equals {@link Errno#ERRNO_SUCCESS} (-1) and `stderr` is not empty + * and {@link ResultConfig#resultFileOutputFormat} is not passed. */ + public static final String FORMAT_SUCCESS_STDOUT__STDERR__EXIT_CODE = "stdout=%n```%n%1$s%n```%n%n%n%nstderr=%n```%n%2$s%n```%n%n%n%nexit_code=`%3$s`%n"; + /** The {@link Formatter} format string for failure if `err`, `errmsg`(`error`), `stdout`, + * `stderr` and `exit_code` need to be written to {@link ResultConfig#resultFileBasename} where + * `err` maps to `%1$s`, `errmsg` maps to `%2$s`, `stdout` maps + * to `%3$s`, `stderr` to `%4$s` and `exit_code` maps to `%5$s`. + * Do not define an argument greater than `5`, like `%6$s` if you change this value since it will + * raise {@link IllegalFormatException}. + * This is used when `err` does not equal {@link Errno#ERRNO_SUCCESS} (-1) and + * {@link ResultConfig#resultFileErrorFormat} is not passed. */ + public static final String FORMAT_FAILED_ERR__ERRMSG__STDOUT__STDERR__EXIT_CODE = "err=`%1$s`%n%n%n%nerrmsg=%n```%n%2$s%n```%n%n%n%nstdout=%n```%n%3$s%n```%n%n%n%nstderr=%n```%n%4$s%n```%n%n%n%nexit_code=`%5$s`%n"; + + + + /* + * The default prefixes to use for result files under `ResultConfig#resultDirectoryPath` + * if `ResultConfig#resultSingleFile` is `false`. + */ + + /** The prefix for the err result file. */ + public static final String RESULT_FILE_ERR_PREFIX = "err"; + /** The prefix for the errmsg result file. */ + public static final String RESULT_FILE_ERRMSG_PREFIX = "errmsg"; + /** The prefix for the stdout result file. */ + public static final String RESULT_FILE_STDOUT_PREFIX = "stdout"; + /** The prefix for the stderr result file. */ + public static final String RESULT_FILE_STDERR_PREFIX = "stderr"; + /** The prefix for the exitCode result file. */ + public static final String RESULT_FILE_EXIT_CODE_PREFIX = "exit_code"; + + } + + + + + /** * Termux:Styling app constants. */ diff --git a/termux-shared/src/main/java/com/termux/shared/termux/TermuxUtils.java b/termux-shared/src/main/java/com/termux/shared/termux/TermuxUtils.java index 6ad9a18e..1296def9 100644 --- a/termux-shared/src/main/java/com/termux/shared/termux/TermuxUtils.java +++ b/termux-shared/src/main/java/com/termux/shared/termux/TermuxUtils.java @@ -400,18 +400,18 @@ public class TermuxUtils { ExecutionCommand executionCommand = new ExecutionCommand(1, TermuxConstants.TERMUX_BIN_PREFIX_DIR_PATH + "/bash", null, aptInfoScript, null, true, false); TermuxTask termuxTask = TermuxTask.execute(context, executionCommand, null, true); - if (termuxTask == null || !executionCommand.isSuccessful() || executionCommand.exitCode != 0) { + if (termuxTask == null || !executionCommand.isSuccessful() || executionCommand.resultData.exitCode != 0) { Logger.logError(LOG_TAG, executionCommand.toString()); return null; } - if (executionCommand.stderr != null && !executionCommand.stderr.isEmpty()) + if (!executionCommand.resultData.stderr.toString().isEmpty()) Logger.logError(LOG_TAG, executionCommand.toString()); StringBuilder markdownString = new StringBuilder(); markdownString.append("## ").append(TermuxConstants.TERMUX_APP_NAME).append(" APT Info\n\n"); - markdownString.append(executionCommand.stdout); + markdownString.append(executionCommand.resultData.stdout.toString()); return markdownString.toString(); } @@ -496,6 +496,13 @@ public class TermuxUtils { return df.format(new Date()); } + public static String getCurrentMilliSecondLocalTimeStamp() { + @SuppressLint("SimpleDateFormat") + final SimpleDateFormat df = new SimpleDateFormat("yyyy-MM-dd_HH.mm.ss.SSS"); + df.setTimeZone(TimeZone.getDefault()); + return df.format(new Date()); + } + public static String getAPKRelease(String signingCertificateSHA256Digest) { if (signingCertificateSHA256Digest == null) return "null";