Skip to content

Commit

Permalink
Add support for sending back background and foreground command result…
Browse files Browse the repository at this point in the history
…s for RUN_COMMAND intent and foreground command results for Termux:Tasker

Previously, termux only supported getting result of BACKGROUND commands back if they were started via Termux:Tasker plugin. Getting back result of foreground commands was not possible with any way.

Now with RUN_COMMAND intent or Termux:Tasker, the third party apps and users can get the foreground command results as well. Note that by "foreground results" we only mean the session transcript. The session transcript will contain both stdout and stderr combined, basically anything sent to the the pseudo terminal /dev/pts, including PS1 prefixes for interactive sessions. Getting separate stdout and stderr can currently only be done with background commands.

Moreover, with RUN_COMMAND intent, third party apps and users can get the background commands results as well. This means separate extras for stdout and stderr.

The exit code will also be returned for either case.

### RUN_COMMAND intent

The result extras are returned in the TERMUX_SERVICE.EXTRA_PLUGIN_RESULT_BUNDLE bundle via the pending intent received.

The RUN_COMMAND_SERVICE.EXTRA_PENDING_INTENT extra can be used to send the pending intent with which termux should return the result bundle. The pending intent can be received back by the app with an IntentService. Check RunCommandService for reference implementation.

For foreground commands (RUN_COMMAND_SERVICE.EXTRA_BACKGROUND is false):
- EXTRA_PLUGIN_RESULT_BUNDLE_STDOUT will contain session transcript.
- EXTRA_PLUGIN_RESULT_BUNDLE_STDERR will be null since its not used.
- EXTRA_PLUGIN_RESULT_BUNDLE_EXIT_CODE will contain exit code of session.

For background commands (RUN_COMMAND_SERVICE.EXTRA_BACKGROUND is true):
- EXTRA_PLUGIN_RESULT_BUNDLE_STDOUT will contain stdout of commands.
- EXTRA_PLUGIN_RESULT_BUNDLE_STDERR will contain stderr of commands.
- EXTRA_PLUGIN_RESULT_BUNDLE_EXIT_CODE will contain exit code of command.

The internal errors raised by termux outside the shell will be sent in the the EXTRA_PLUGIN_RESULT_BUNDLE_ERR and EXTRA_PLUGIN_RESULT_BUNDLE_ERRMSG extras. These will contain errors like if starting a termux command failed or if the user manually exited the termux sessions or android killed the termux service before the commands had finished executing. The err value will be Activity.RESULT_OK(-1) if no internal errors are raised.

The stdout and stderr will be truncated from the start to max 100KB combined and errmsg will also be truncated from end to max 25KB. This is necessary to prevent TransactionTooLargeException exceptions from being raised if stdout or stderr are too large in length. The original length of stdout and stderr will be provided in EXTRA_PLUGIN_RESULT_BUNDLE_STDOUT_ORIGINAL_LENGTH and EXTRA_PLUGIN_RESULT_BUNDLE_STDERR_ORIGINAL_LENGTH extras respectively, so that the caller can check if either of them were truncated.

### Termux:Tasker

Support for Termux:Tasker for getting back result of foreground commands will require an update to it since it currently immediately returns control to plugin host app like Tasker without waiting if a foreground command is to be executed.
  • Loading branch information
agnostic-apollo committed Mar 25, 2021
1 parent 2cc6285 commit a2209dd
Show file tree
Hide file tree
Showing 6 changed files with 204 additions and 30 deletions.
137 changes: 128 additions & 9 deletions app/src/main/java/com/termux/app/RunCommandService.java
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
package com.termux.app;

import android.app.Activity;
import android.app.IntentService;
import android.app.Notification;
import android.app.NotificationManager;
import android.app.Service;
Expand Down Expand Up @@ -104,6 +106,10 @@
* the command. This can add details about the command. 3rd party apps can provide more info
* to users for setting up commands. Ideally a url link should be provided that goes into full
* details.
* 9. The {@code Parcelable} {@link RUN_COMMAND_SERVICE#EXTRA_PENDING_INTENT} extra containing the
* pending intent with which result of commands should be returned to the caller. The results
* will be sent in the {@link TERMUX_SERVICE#EXTRA_PLUGIN_RESULT_BUNDLE} bundle. This is optional
* and only needed if caller wants the results back.
*
*
* The {@link RUN_COMMAND_SERVICE#EXTRA_COMMAND_PATH} and {@link RUN_COMMAND_SERVICE#EXTRA_WORKDIR}
Expand Down Expand Up @@ -132,26 +138,135 @@
* https://developer.android.com/training/basics/intents/package-visibility#package-name
*
*
* Its probably wiser for apps to import the {@link TermuxConstants} class and use the variables
* provided for actions and extras instead of using hardcoded string values.
*
* Sample code to run command "top" with java:
* Intent intent = new Intent();
* intent.setClassName("com.termux", "com.termux.app.RunCommandService");
* intent.setAction("com.termux.RUN_COMMAND");
* intent.putExtra("com.termux.RUN_COMMAND_PATH", "/data/data/com.termux/files/usr/bin/top");
* intent.putExtra("com.termux.RUN_COMMAND_ARGUMENTS", new String[]{"-n", "5"});
* intent.putExtra("com.termux.RUN_COMMAND_WORKDIR", "/data/data/com.termux/files/home");
* intent.putExtra("com.termux.RUN_COMMAND_BACKGROUND", false);
* intent.putExtra("com.termux.RUN_COMMAND_SESSION_ACTION", "0");
* startService(intent);
* ```
* intent.setClassName("com.termux", "com.termux.app.RunCommandService");
* intent.setAction("com.termux.RUN_COMMAND");
* intent.putExtra("com.termux.RUN_COMMAND_PATH", "/data/data/com.termux/files/usr/bin/top");
* intent.putExtra("com.termux.RUN_COMMAND_ARGUMENTS", new String[]{"-n", "5"});
* intent.putExtra("com.termux.RUN_COMMAND_WORKDIR", "/data/data/com.termux/files/home");
* intent.putExtra("com.termux.RUN_COMMAND_BACKGROUND", false);
* intent.putExtra("com.termux.RUN_COMMAND_SESSION_ACTION", "0");
* startService(intent);
* ```
*
* Sample code to run command "top" with "am startservice" command:
* ```
* am startservice --user 0 -n com.termux/com.termux.app.RunCommandService \
* -a com.termux.RUN_COMMAND \
* --es com.termux.RUN_COMMAND_PATH '/data/data/com.termux/files/usr/bin/top' \
* --esa com.termux.RUN_COMMAND_ARGUMENTS '-n,5' \
* --es com.termux.RUN_COMMAND_WORKDIR '/data/data/com.termux/files/home' \
* --ez com.termux.RUN_COMMAND_BACKGROUND 'false' \
* --es com.termux.RUN_COMMAND_SESSION_ACTION '0'
*
*
*
*
* The {@link RUN_COMMAND_SERVICE#ACTION_RUN_COMMAND} intent returns the following extras
* in the {@link TERMUX_SERVICE#EXTRA_PLUGIN_RESULT_BUNDLE} bundle if a pending intent is sent by the
* called in {@code Parcelable} {@link RUN_COMMAND_SERVICE#EXTRA_PENDING_INTENT} extra:
*
* For foreground commands ({@link RUN_COMMAND_SERVICE#EXTRA_BACKGROUND} is `false`):
* - {@link TERMUX_SERVICE#EXTRA_PLUGIN_RESULT_BUNDLE_STDOUT} will contain session transcript.
* - {@link TERMUX_SERVICE#EXTRA_PLUGIN_RESULT_BUNDLE_STDERR} will be null since its not used.
* - {@link TERMUX_SERVICE#EXTRA_PLUGIN_RESULT_BUNDLE_EXIT_CODE} will contain exit code of session.
* For background commands ({@link RUN_COMMAND_SERVICE#EXTRA_BACKGROUND} is `true`):
* - {@link TERMUX_SERVICE#EXTRA_PLUGIN_RESULT_BUNDLE_STDOUT} will contain stdout of commands.
* - {@link TERMUX_SERVICE#EXTRA_PLUGIN_RESULT_BUNDLE_STDERR} will contain stderr of commands.
* - {@link TERMUX_SERVICE#EXTRA_PLUGIN_RESULT_BUNDLE_EXIT_CODE} will contain exit code of command.
*
* The {@link TERMUX_SERVICE#EXTRA_PLUGIN_RESULT_BUNDLE_STDOUT_ORIGINAL_LENGTH} and
* {@link TERMUX_SERVICE#EXTRA_PLUGIN_RESULT_BUNDLE_STDERR_ORIGINAL_LENGTH} will contain
* the original length of stdout and stderr respectively. This is useful to detect cases where
* stdout and stderr was too large to be sent back via an intent, otherwise
*
* The internal errors raised by termux outside the shell will be sent in the the
* {@link TERMUX_SERVICE#EXTRA_PLUGIN_RESULT_BUNDLE_ERR} and {@link TERMUX_SERVICE#EXTRA_PLUGIN_RESULT_BUNDLE_ERRMSG}
* extras. These will contain errors like if starting a termux command failed or if the user manually
* exited the termux sessions or android killed the termux service before the commands had finished executing.
* The err value will be {@link Activity#RESULT_OK}(-1) if no internal errors are raised.
*
* Note that if stdout or stderr are too large in length, then a {@link android.os.TransactionTooLargeException}
* exception will be raised when the pending intent is sent back containing the results, But it cannot
* be caught by the intent sender and intent will silently fail with logcat entries for the exception
* raised internally by android os components. To prevent this, the stdout and stderr sent
* back will be truncated from the start to max 100KB combined. The original length of stdout and
* stderr will be provided in
* {@link TERMUX_SERVICE#EXTRA_PLUGIN_RESULT_BUNDLE_STDOUT_ORIGINAL_LENGTH} and
* {@link TERMUX_SERVICE#EXTRA_PLUGIN_RESULT_BUNDLE_STDERR_ORIGINAL_LENGTH} extras respectively, so
* that the caller can check if either of them were truncated. The errmsg will also be truncated
* from end to max 25KB to preserve start of stacktraces.
*
*
*
* If your app (not shell) wants to receive termux session command results, then put the
* pending intent for your app like for an {@link IntentService} in the "com.termux.RUN_COMMAND_PENDING_INTENT"
* extra.
* ```
* // Create intent for your IntentService class
* Intent pluginResultsServiceIntent = new Intent(MainActivity.this, PluginResultsService.class);
* // Create PendingIntent that will be used by termux service to send result of commands back to PluginResultsService
* PendingIntent pendingIntent = PendingIntent.getService(context, 1, pluginResultsServiceIntent, PendingIntent.FLAG_ONE_SHOT);
* intent.putExtra("com.termux.RUN_COMMAND_PENDING_INTENT", pendingIntent);
* ```
*
*
* Declare `PluginResultsService` entry in AndroidManifest.xml
* ```
* <service android:name=".PluginResultsService" />
* ```
*
*
* Define the `PluginResultsService` class
* ```
* public class PluginResultsService extends IntentService {
*
* public static final String PLUGIN_SERVICE_LABEL = "PluginResultsService";
*
* private static final String LOG_TAG = "PluginResultsService";
*
* public PluginResultsService(){
* super(PLUGIN_SERVICE_LABEL);
* }
*
* @Override
* protected void onHandleIntent(@Nullable Intent intent) {
* if (intent == null) return;
*
* if(intent.getComponent() != null)
* Log.d(LOG_TAG, PLUGIN_SERVICE_LABEL + " received execution result from " + intent.getComponent().toString());
*
*
* final Bundle resultBundle = intent.getBundleExtra("result");
* if (resultBundle == null) {
* Log.e(LOG_TAG, "The intent does not contain the result bundle at the \"result\" key.");
* return;
* }
*
* Log.d(LOG_TAG, "stdout:\n```\n" + resultBundle.getString("stdout", "") + "\n```\n" +
* "stdout_original_length: `" + resultBundle.getString("stdout_original_length") + "`\n" +
* "stderr:\n```\n" + resultBundle.getString("stderr", "") + "\n```\n" +
* "stderr_original_length: `" + resultBundle.getString("stderr_original_length") + "`\n" +
* "exitCode: `" + resultBundle.getInt("exitCode") + "`\n" +
* "errCode: `" + resultBundle.getInt("err") + "`\n" +
* "errmsg: `" + resultBundle.getString("errmsg", "") + "`");
* }
*
* }
*```
*
*
*
*
*
* A service that receives {@link RUN_COMMAND_SERVICE#ACTION_RUN_COMMAND} intent from third party apps and
* plugins that contains info on command execution and forwards the extras to {@link TermuxService}
* for the actual execution.
*/
public class RunCommandService extends Service {

Expand Down Expand Up @@ -206,6 +321,9 @@ public int onStartCommand(Intent intent, int flags, int startId) {
executionCommand.commandLabel = TextDataUtils.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.pluginPendingIntent = intent.getParcelableExtra(RUN_COMMAND_SERVICE.EXTRA_PENDING_INTENT);



if(!executionCommand.setState(ExecutionState.PRE_EXECUTION))
return Service.START_NOT_STICKY;
Expand Down Expand Up @@ -286,6 +404,7 @@ public int onStartCommand(Intent intent, int flags, int startId) {
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);

// Start TERMUX_SERVICE and pass it execution intent
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
Expand Down
29 changes: 23 additions & 6 deletions app/src/main/java/com/termux/app/TermuxService.java
Original file line number Diff line number Diff line change
Expand Up @@ -216,9 +216,22 @@ private void actionStopService() {

/** Finish all termux sessions by sending SIGKILL to their shells. */
private synchronized void finishAllTermuxSessions() {
ExecutionCommand executionCommand;

// TODO: Should SIGKILL also be send to background processes maintained by mTermuxTasks?
for (int i = 0; i < mTermuxSessions.size(); i++)
mTermuxSessions.get(i).getTerminalSession().finishIfRunning();
for (int i = 0; i < mTermuxSessions.size(); i++) {
TermuxSession termuxSession = mTermuxSessions.get(i);
executionCommand = termuxSession.getExecutionCommand();

// If the execution command was started for a plugin and is currently executing, then notify the callers
if(executionCommand.isPluginExecutionCommand && executionCommand.isExecuting()) {
if (executionCommand.setStateFailed(ExecutionCommand.RESULT_CODE_FAILED, this.getString(R.string.error_sending_sigkill_to_process), null)) {
TermuxSession.processTermuxSessionResult(this, termuxSession, null);
}
}

termuxSession.getTerminalSession().finishIfRunning();
}
}


Expand Down Expand Up @@ -360,7 +373,7 @@ public synchronized TermuxTask createTermuxTask(ExecutionCommand executionComman

TermuxTask newTermuxTask = TermuxTask.create(this, executionCommand);
if (newTermuxTask == null) {
// Logger.logError(LOG_TAG, "Failed to execute new termux task command for:\n" + executionCommand.toString());
Logger.logError(LOG_TAG, "Failed to execute new termux task command for:\n" + executionCommand.getCommandIdAndLabelLogString());
return null;
};

Expand Down Expand Up @@ -428,9 +441,9 @@ public synchronized TermuxSession createTermuxSession(ExecutionCommand execution
if(Logger.getLogLevel() >= Logger.LOG_LEVEL_VERBOSE)
Logger.logVerbose(LOG_TAG, executionCommand.toString());

TermuxSession newTermuxSession = TermuxSession.create(executionCommand, getTermuxSessionClient(), sessionName);
TermuxSession newTermuxSession = TermuxSession.create(this, executionCommand, getTermuxSessionClient(), sessionName);
if (newTermuxSession == null) {
Logger.logError(LOG_TAG, "Failed to execute new termux session command for:\n" + executionCommand.toString());
Logger.logError(LOG_TAG, "Failed to execute new termux session command for:\n" + executionCommand.getCommandIdAndLabelLogString());
return null;
};

Expand All @@ -455,7 +468,11 @@ public synchronized int removeTermuxSession(TerminalSession sessionToRemove) {
TermuxSession termuxSession = mTermuxSessions.get(index);

if (termuxSession.getExecutionCommand().setState(ExecutionState.EXECUTED)) {
;
// If the execution command was started for a plugin and is currently executing, then process the result
if(termuxSession.getExecutionCommand().isPluginExecutionCommand)
TermuxSession.processTermuxSessionResult(this, termuxSession, null);
else
termuxSession.getExecutionCommand().setState(ExecutionState.SUCCESS);
}

mTermuxSessions.remove(termuxSession);
Expand Down
38 changes: 33 additions & 5 deletions app/src/main/java/com/termux/app/terminal/TermuxSession.java
Original file line number Diff line number Diff line change
@@ -1,7 +1,12 @@
package com.termux.app.terminal;

import androidx.annotation.NonNull;

import com.termux.R;
import com.termux.app.TermuxConstants;
import com.termux.app.TermuxService;
import com.termux.app.utils.Logger;
import com.termux.app.utils.PluginUtils;
import com.termux.app.utils.ShellUtils;
import com.termux.models.ExecutionCommand;
import com.termux.terminal.TerminalSession;
Expand All @@ -25,14 +30,12 @@ private TermuxSession(TerminalSession terminalSession, ExecutionCommand executio
this.mExecutionCommand = executionCommand;
}

public static TermuxSession create(ExecutionCommand executionCommand, TermuxSessionClientBase termuxSessionClient, String sessionName) {
TermuxConstants.TERMUX_HOME_DIR.mkdirs();

public static TermuxSession create(@NonNull final TermuxService service, @NonNull ExecutionCommand executionCommand, @NonNull TermuxSessionClientBase termuxSessionClient, String sessionName) {
if (executionCommand.workingDirectory == null || executionCommand.workingDirectory.isEmpty()) executionCommand.workingDirectory = TermuxConstants.TERMUX_HOME_DIR_PATH;

String[] environment = ShellUtils.buildEnvironment(executionCommand.isFailsafe, executionCommand.workingDirectory);
boolean isLoginShell = false;

boolean isLoginShell = false;
if (executionCommand.executable == null) {
if (!executionCommand.isFailsafe) {
for (String shellBinary : new String[]{"login", "bash", "zsh"}) {
Expand Down Expand Up @@ -62,8 +65,13 @@ public static TermuxSession create(ExecutionCommand executionCommand, TermuxSess

executionCommand.arguments = arguments;

if(!executionCommand.setState(ExecutionCommand.ExecutionState.EXECUTING))
if(!executionCommand.setState(ExecutionCommand.ExecutionState.EXECUTING)) {
executionCommand.setStateFailed(ExecutionCommand.RESULT_CODE_FAILED, service.getString(R.string.error_failed_to_execute_termux_session_command, executionCommand.getCommandIdAndLabelLogString()), null);
if(executionCommand.isPluginExecutionCommand) {
TermuxSession.processTermuxSessionResult(service, null, executionCommand);
}
return null;
}

Logger.logDebug(LOG_TAG, executionCommand.toString());

Expand All @@ -76,6 +84,26 @@ public static TermuxSession create(ExecutionCommand executionCommand, TermuxSess
return new TermuxSession(terminalSession, executionCommand);
}

public static void processTermuxSessionResult(@NonNull final TermuxService service, final TermuxSession termuxSession, ExecutionCommand executionCommand) {
TerminalSession terminalSession = null;
if(termuxSession != null) {
executionCommand = termuxSession.mExecutionCommand;
terminalSession = termuxSession.mTerminalSession;
}

if(executionCommand == null) return;

if(!executionCommand.isPluginExecutionCommand) return;

if(terminalSession != null && !terminalSession.isRunning() && executionCommand.hasExecuted() && !executionCommand.isStateFailed()) {
executionCommand.stdout = terminalSession.getEmulator().getScreen().getTranscriptTextWithFullLinesJoined();
executionCommand.stderr = null;
executionCommand.exitCode = terminalSession.getExitStatus();
}

PluginUtils.processPluginExecutionCommandResult(service.getApplicationContext(), LOG_TAG, executionCommand);
}

public TerminalSession getTerminalSession() {
return mTerminalSession;
}
Expand Down
9 changes: 5 additions & 4 deletions app/src/main/java/com/termux/app/terminal/TermuxTask.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import androidx.annotation.NonNull;

import com.termux.R;
import com.termux.app.TermuxConstants;
import com.termux.app.TermuxService;
import com.termux.app.utils.Logger;
Expand Down Expand Up @@ -53,8 +54,8 @@ public static TermuxTask create(@NonNull final TermuxService service, @NonNull E
try {
process = Runtime.getRuntime().exec(commandArray, env, new File(executionCommand.workingDirectory));
} catch (IOException e) {
executionCommand.setStateFailed(ExecutionCommand.RESULT_CODE_FAILED, "Failed to run \"" + executionCommand.commandLabel + "\" background task", e);
TermuxTask.processTermuxTaskResult(service, null, executionCommand);
executionCommand.setStateFailed(ExecutionCommand.RESULT_CODE_FAILED, service.getString(R.string.error_failed_to_execute_termux_task_command, executionCommand.getCommandIdAndLabelLogString()), e);
TermuxSession.processTermuxSessionResult(service, null, executionCommand);
return null;
}

Expand Down Expand Up @@ -117,15 +118,15 @@ public void run() {
return termuxTask;
}

public static void processTermuxTaskResult(final TermuxService service, final TermuxTask termuxTask, ExecutionCommand executionCommand) {
public static void processTermuxTaskResult(@NonNull final TermuxService service, final TermuxTask termuxTask, ExecutionCommand executionCommand) {
if(termuxTask != null)
executionCommand = termuxTask.mExecutionCommand;

if(executionCommand == null) return;

PluginUtils.processPluginExecutionCommandResult(service.getApplicationContext(), LOG_TAG, executionCommand);

if(termuxTask != null && service != null)
if(termuxTask != null)
service.onTermuxTaskExited(termuxTask);
}

Expand Down
Loading

0 comments on commit a2209dd

Please sign in to comment.