Skip to content

Commit a2209dd

Browse files
Add support for sending back background and foreground command results 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.
1 parent 2cc6285 commit a2209dd

File tree

6 files changed

+204
-30
lines changed

6 files changed

+204
-30
lines changed

app/src/main/java/com/termux/app/RunCommandService.java

+128-9
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
package com.termux.app;
22

3+
import android.app.Activity;
4+
import android.app.IntentService;
35
import android.app.Notification;
46
import android.app.NotificationManager;
57
import android.app.Service;
@@ -104,6 +106,10 @@
104106
* the command. This can add details about the command. 3rd party apps can provide more info
105107
* to users for setting up commands. Ideally a url link should be provided that goes into full
106108
* details.
109+
* 9. The {@code Parcelable} {@link RUN_COMMAND_SERVICE#EXTRA_PENDING_INTENT} extra containing the
110+
* pending intent with which result of commands should be returned to the caller. The results
111+
* will be sent in the {@link TERMUX_SERVICE#EXTRA_PLUGIN_RESULT_BUNDLE} bundle. This is optional
112+
* and only needed if caller wants the results back.
107113
*
108114
*
109115
* The {@link RUN_COMMAND_SERVICE#EXTRA_COMMAND_PATH} and {@link RUN_COMMAND_SERVICE#EXTRA_WORKDIR}
@@ -132,26 +138,135 @@
132138
* https://developer.android.com/training/basics/intents/package-visibility#package-name
133139
*
134140
*
141+
* Its probably wiser for apps to import the {@link TermuxConstants} class and use the variables
142+
* provided for actions and extras instead of using hardcoded string values.
135143
*
136144
* Sample code to run command "top" with java:
137-
* Intent intent = new Intent();
138-
* intent.setClassName("com.termux", "com.termux.app.RunCommandService");
139-
* intent.setAction("com.termux.RUN_COMMAND");
140-
* intent.putExtra("com.termux.RUN_COMMAND_PATH", "/data/data/com.termux/files/usr/bin/top");
141-
* intent.putExtra("com.termux.RUN_COMMAND_ARGUMENTS", new String[]{"-n", "5"});
142-
* intent.putExtra("com.termux.RUN_COMMAND_WORKDIR", "/data/data/com.termux/files/home");
143-
* intent.putExtra("com.termux.RUN_COMMAND_BACKGROUND", false);
144-
* intent.putExtra("com.termux.RUN_COMMAND_SESSION_ACTION", "0");
145-
* startService(intent);
145+
* ```
146+
* intent.setClassName("com.termux", "com.termux.app.RunCommandService");
147+
* intent.setAction("com.termux.RUN_COMMAND");
148+
* intent.putExtra("com.termux.RUN_COMMAND_PATH", "/data/data/com.termux/files/usr/bin/top");
149+
* intent.putExtra("com.termux.RUN_COMMAND_ARGUMENTS", new String[]{"-n", "5"});
150+
* intent.putExtra("com.termux.RUN_COMMAND_WORKDIR", "/data/data/com.termux/files/home");
151+
* intent.putExtra("com.termux.RUN_COMMAND_BACKGROUND", false);
152+
* intent.putExtra("com.termux.RUN_COMMAND_SESSION_ACTION", "0");
153+
* startService(intent);
154+
* ```
146155
*
147156
* Sample code to run command "top" with "am startservice" command:
157+
* ```
148158
* am startservice --user 0 -n com.termux/com.termux.app.RunCommandService \
149159
* -a com.termux.RUN_COMMAND \
150160
* --es com.termux.RUN_COMMAND_PATH '/data/data/com.termux/files/usr/bin/top' \
151161
* --esa com.termux.RUN_COMMAND_ARGUMENTS '-n,5' \
152162
* --es com.termux.RUN_COMMAND_WORKDIR '/data/data/com.termux/files/home' \
153163
* --ez com.termux.RUN_COMMAND_BACKGROUND 'false' \
154164
* --es com.termux.RUN_COMMAND_SESSION_ACTION '0'
165+
*
166+
*
167+
*
168+
*
169+
* The {@link RUN_COMMAND_SERVICE#ACTION_RUN_COMMAND} intent returns the following extras
170+
* in the {@link TERMUX_SERVICE#EXTRA_PLUGIN_RESULT_BUNDLE} bundle if a pending intent is sent by the
171+
* called in {@code Parcelable} {@link RUN_COMMAND_SERVICE#EXTRA_PENDING_INTENT} extra:
172+
*
173+
* For foreground commands ({@link RUN_COMMAND_SERVICE#EXTRA_BACKGROUND} is `false`):
174+
* - {@link TERMUX_SERVICE#EXTRA_PLUGIN_RESULT_BUNDLE_STDOUT} will contain session transcript.
175+
* - {@link TERMUX_SERVICE#EXTRA_PLUGIN_RESULT_BUNDLE_STDERR} will be null since its not used.
176+
* - {@link TERMUX_SERVICE#EXTRA_PLUGIN_RESULT_BUNDLE_EXIT_CODE} will contain exit code of session.
177+
178+
* For background commands ({@link RUN_COMMAND_SERVICE#EXTRA_BACKGROUND} is `true`):
179+
* - {@link TERMUX_SERVICE#EXTRA_PLUGIN_RESULT_BUNDLE_STDOUT} will contain stdout of commands.
180+
* - {@link TERMUX_SERVICE#EXTRA_PLUGIN_RESULT_BUNDLE_STDERR} will contain stderr of commands.
181+
* - {@link TERMUX_SERVICE#EXTRA_PLUGIN_RESULT_BUNDLE_EXIT_CODE} will contain exit code of command.
182+
*
183+
* The {@link TERMUX_SERVICE#EXTRA_PLUGIN_RESULT_BUNDLE_STDOUT_ORIGINAL_LENGTH} and
184+
* {@link TERMUX_SERVICE#EXTRA_PLUGIN_RESULT_BUNDLE_STDERR_ORIGINAL_LENGTH} will contain
185+
* the original length of stdout and stderr respectively. This is useful to detect cases where
186+
* stdout and stderr was too large to be sent back via an intent, otherwise
187+
*
188+
* The internal errors raised by termux outside the shell will be sent in the the
189+
* {@link TERMUX_SERVICE#EXTRA_PLUGIN_RESULT_BUNDLE_ERR} and {@link TERMUX_SERVICE#EXTRA_PLUGIN_RESULT_BUNDLE_ERRMSG}
190+
* extras. These will contain errors like if starting a termux command failed or if the user manually
191+
* exited the termux sessions or android killed the termux service before the commands had finished executing.
192+
* The err value will be {@link Activity#RESULT_OK}(-1) if no internal errors are raised.
193+
*
194+
* Note that if stdout or stderr are too large in length, then a {@link android.os.TransactionTooLargeException}
195+
* exception will be raised when the pending intent is sent back containing the results, But it cannot
196+
* be caught by the intent sender and intent will silently fail with logcat entries for the exception
197+
* raised internally by android os components. To prevent this, the stdout and stderr sent
198+
* back will be truncated from the start to max 100KB combined. The original length of stdout and
199+
* stderr will be provided in
200+
* {@link TERMUX_SERVICE#EXTRA_PLUGIN_RESULT_BUNDLE_STDOUT_ORIGINAL_LENGTH} and
201+
* {@link TERMUX_SERVICE#EXTRA_PLUGIN_RESULT_BUNDLE_STDERR_ORIGINAL_LENGTH} extras respectively, so
202+
* that the caller can check if either of them were truncated. The errmsg will also be truncated
203+
* from end to max 25KB to preserve start of stacktraces.
204+
*
205+
*
206+
*
207+
* If your app (not shell) wants to receive termux session command results, then put the
208+
* pending intent for your app like for an {@link IntentService} in the "com.termux.RUN_COMMAND_PENDING_INTENT"
209+
* extra.
210+
* ```
211+
* // Create intent for your IntentService class
212+
* Intent pluginResultsServiceIntent = new Intent(MainActivity.this, PluginResultsService.class);
213+
* // Create PendingIntent that will be used by termux service to send result of commands back to PluginResultsService
214+
* PendingIntent pendingIntent = PendingIntent.getService(context, 1, pluginResultsServiceIntent, PendingIntent.FLAG_ONE_SHOT);
215+
* intent.putExtra("com.termux.RUN_COMMAND_PENDING_INTENT", pendingIntent);
216+
* ```
217+
*
218+
*
219+
* Declare `PluginResultsService` entry in AndroidManifest.xml
220+
* ```
221+
* <service android:name=".PluginResultsService" />
222+
* ```
223+
*
224+
*
225+
* Define the `PluginResultsService` class
226+
* ```
227+
* public class PluginResultsService extends IntentService {
228+
*
229+
* public static final String PLUGIN_SERVICE_LABEL = "PluginResultsService";
230+
*
231+
* private static final String LOG_TAG = "PluginResultsService";
232+
*
233+
* public PluginResultsService(){
234+
* super(PLUGIN_SERVICE_LABEL);
235+
* }
236+
*
237+
* @Override
238+
* protected void onHandleIntent(@Nullable Intent intent) {
239+
* if (intent == null) return;
240+
*
241+
* if(intent.getComponent() != null)
242+
* Log.d(LOG_TAG, PLUGIN_SERVICE_LABEL + " received execution result from " + intent.getComponent().toString());
243+
*
244+
*
245+
* final Bundle resultBundle = intent.getBundleExtra("result");
246+
* if (resultBundle == null) {
247+
* Log.e(LOG_TAG, "The intent does not contain the result bundle at the \"result\" key.");
248+
* return;
249+
* }
250+
*
251+
* Log.d(LOG_TAG, "stdout:\n```\n" + resultBundle.getString("stdout", "") + "\n```\n" +
252+
* "stdout_original_length: `" + resultBundle.getString("stdout_original_length") + "`\n" +
253+
* "stderr:\n```\n" + resultBundle.getString("stderr", "") + "\n```\n" +
254+
* "stderr_original_length: `" + resultBundle.getString("stderr_original_length") + "`\n" +
255+
* "exitCode: `" + resultBundle.getInt("exitCode") + "`\n" +
256+
* "errCode: `" + resultBundle.getInt("err") + "`\n" +
257+
* "errmsg: `" + resultBundle.getString("errmsg", "") + "`");
258+
* }
259+
*
260+
* }
261+
*```
262+
*
263+
*
264+
*
265+
*
266+
*
267+
* A service that receives {@link RUN_COMMAND_SERVICE#ACTION_RUN_COMMAND} intent from third party apps and
268+
* plugins that contains info on command execution and forwards the extras to {@link TermuxService}
269+
* for the actual execution.
155270
*/
156271
public class RunCommandService extends Service {
157272

@@ -206,6 +321,9 @@ public int onStartCommand(Intent intent, int flags, int startId) {
206321
executionCommand.commandLabel = TextDataUtils.getDefaultIfNull(intent.getStringExtra(RUN_COMMAND_SERVICE.EXTRA_COMMAND_LABEL), "RUN_COMMAND Execution Intent Command");
207322
executionCommand.commandDescription = intent.getStringExtra(RUN_COMMAND_SERVICE.EXTRA_COMMAND_DESCRIPTION);
208323
executionCommand.commandHelp = intent.getStringExtra(RUN_COMMAND_SERVICE.EXTRA_COMMAND_HELP);
324+
executionCommand.pluginPendingIntent = intent.getParcelableExtra(RUN_COMMAND_SERVICE.EXTRA_PENDING_INTENT);
325+
326+
209327

210328
if(!executionCommand.setState(ExecutionState.PRE_EXECUTION))
211329
return Service.START_NOT_STICKY;
@@ -286,6 +404,7 @@ public int onStartCommand(Intent intent, int flags, int startId) {
286404
execIntent.putExtra(TERMUX_SERVICE.EXTRA_COMMAND_DESCRIPTION, executionCommand.commandDescription);
287405
execIntent.putExtra(TERMUX_SERVICE.EXTRA_COMMAND_HELP, executionCommand.commandHelp);
288406
execIntent.putExtra(TERMUX_SERVICE.EXTRA_PLUGIN_API_HELP, executionCommand.pluginAPIHelp);
407+
execIntent.putExtra(TERMUX_SERVICE.EXTRA_PENDING_INTENT, executionCommand.pluginPendingIntent);
289408

290409
// Start TERMUX_SERVICE and pass it execution intent
291410
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {

app/src/main/java/com/termux/app/TermuxService.java

+23-6
Original file line numberDiff line numberDiff line change
@@ -216,9 +216,22 @@ private void actionStopService() {
216216

217217
/** Finish all termux sessions by sending SIGKILL to their shells. */
218218
private synchronized void finishAllTermuxSessions() {
219+
ExecutionCommand executionCommand;
220+
219221
// TODO: Should SIGKILL also be send to background processes maintained by mTermuxTasks?
220-
for (int i = 0; i < mTermuxSessions.size(); i++)
221-
mTermuxSessions.get(i).getTerminalSession().finishIfRunning();
222+
for (int i = 0; i < mTermuxSessions.size(); i++) {
223+
TermuxSession termuxSession = mTermuxSessions.get(i);
224+
executionCommand = termuxSession.getExecutionCommand();
225+
226+
// If the execution command was started for a plugin and is currently executing, then notify the callers
227+
if(executionCommand.isPluginExecutionCommand && executionCommand.isExecuting()) {
228+
if (executionCommand.setStateFailed(ExecutionCommand.RESULT_CODE_FAILED, this.getString(R.string.error_sending_sigkill_to_process), null)) {
229+
TermuxSession.processTermuxSessionResult(this, termuxSession, null);
230+
}
231+
}
232+
233+
termuxSession.getTerminalSession().finishIfRunning();
234+
}
222235
}
223236

224237

@@ -360,7 +373,7 @@ public synchronized TermuxTask createTermuxTask(ExecutionCommand executionComman
360373

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

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

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

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

457470
if (termuxSession.getExecutionCommand().setState(ExecutionState.EXECUTED)) {
458-
;
471+
// If the execution command was started for a plugin and is currently executing, then process the result
472+
if(termuxSession.getExecutionCommand().isPluginExecutionCommand)
473+
TermuxSession.processTermuxSessionResult(this, termuxSession, null);
474+
else
475+
termuxSession.getExecutionCommand().setState(ExecutionState.SUCCESS);
459476
}
460477

461478
mTermuxSessions.remove(termuxSession);

app/src/main/java/com/termux/app/terminal/TermuxSession.java

+33-5
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,12 @@
11
package com.termux.app.terminal;
22

3+
import androidx.annotation.NonNull;
4+
5+
import com.termux.R;
36
import com.termux.app.TermuxConstants;
7+
import com.termux.app.TermuxService;
48
import com.termux.app.utils.Logger;
9+
import com.termux.app.utils.PluginUtils;
510
import com.termux.app.utils.ShellUtils;
611
import com.termux.models.ExecutionCommand;
712
import com.termux.terminal.TerminalSession;
@@ -25,14 +30,12 @@ private TermuxSession(TerminalSession terminalSession, ExecutionCommand executio
2530
this.mExecutionCommand = executionCommand;
2631
}
2732

28-
public static TermuxSession create(ExecutionCommand executionCommand, TermuxSessionClientBase termuxSessionClient, String sessionName) {
29-
TermuxConstants.TERMUX_HOME_DIR.mkdirs();
30-
33+
public static TermuxSession create(@NonNull final TermuxService service, @NonNull ExecutionCommand executionCommand, @NonNull TermuxSessionClientBase termuxSessionClient, String sessionName) {
3134
if (executionCommand.workingDirectory == null || executionCommand.workingDirectory.isEmpty()) executionCommand.workingDirectory = TermuxConstants.TERMUX_HOME_DIR_PATH;
3235

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

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

6366
executionCommand.arguments = arguments;
6467

65-
if(!executionCommand.setState(ExecutionCommand.ExecutionState.EXECUTING))
68+
if(!executionCommand.setState(ExecutionCommand.ExecutionState.EXECUTING)) {
69+
executionCommand.setStateFailed(ExecutionCommand.RESULT_CODE_FAILED, service.getString(R.string.error_failed_to_execute_termux_session_command, executionCommand.getCommandIdAndLabelLogString()), null);
70+
if(executionCommand.isPluginExecutionCommand) {
71+
TermuxSession.processTermuxSessionResult(service, null, executionCommand);
72+
}
6673
return null;
74+
}
6775

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

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

87+
public static void processTermuxSessionResult(@NonNull final TermuxService service, final TermuxSession termuxSession, ExecutionCommand executionCommand) {
88+
TerminalSession terminalSession = null;
89+
if(termuxSession != null) {
90+
executionCommand = termuxSession.mExecutionCommand;
91+
terminalSession = termuxSession.mTerminalSession;
92+
}
93+
94+
if(executionCommand == null) return;
95+
96+
if(!executionCommand.isPluginExecutionCommand) return;
97+
98+
if(terminalSession != null && !terminalSession.isRunning() && executionCommand.hasExecuted() && !executionCommand.isStateFailed()) {
99+
executionCommand.stdout = terminalSession.getEmulator().getScreen().getTranscriptTextWithFullLinesJoined();
100+
executionCommand.stderr = null;
101+
executionCommand.exitCode = terminalSession.getExitStatus();
102+
}
103+
104+
PluginUtils.processPluginExecutionCommandResult(service.getApplicationContext(), LOG_TAG, executionCommand);
105+
}
106+
79107
public TerminalSession getTerminalSession() {
80108
return mTerminalSession;
81109
}

app/src/main/java/com/termux/app/terminal/TermuxTask.java

+5-4
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
import androidx.annotation.NonNull;
44

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

@@ -117,15 +118,15 @@ public void run() {
117118
return termuxTask;
118119
}
119120

120-
public static void processTermuxTaskResult(final TermuxService service, final TermuxTask termuxTask, ExecutionCommand executionCommand) {
121+
public static void processTermuxTaskResult(@NonNull final TermuxService service, final TermuxTask termuxTask, ExecutionCommand executionCommand) {
121122
if(termuxTask != null)
122123
executionCommand = termuxTask.mExecutionCommand;
123124

124125
if(executionCommand == null) return;
125126

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

128-
if(termuxTask != null && service != null)
129+
if(termuxTask != null)
129130
service.onTermuxTaskExited(termuxTask);
130131
}
131132

0 commit comments

Comments
 (0)