-
-
Notifications
You must be signed in to change notification settings - Fork 5.5k
RUN_COMMAND Intent
Since 0.95, third-party apps that are not part of Termux world can run commands in Termux app context by either sending an intent to RunCommandService or becoming a plugin host for the termux-tasker plugin client.
The intent can either be sent with am startservice command or with Java. Getting command result back is also possible for intents sent with Java, but not possible with am startservice command and it will require Termux app version >= 0.109.
You may also want to check out termux-tasker README.
The Tasker app comes with native support for this with basic command extras with the TermuxCommand() function in Tasker Function action. You can also use am command with the Run Shell action.
It's probably wiser for third-party apps to declare the termux-shared library as a dependency and import the TermuxConstants class and use the variables provided for actions and extras instead of using hardcoded extra key values. Check Termux Libraries for import instructions.
If your third-party app has set targetSdkVersion to >=30 (android >= 11), then check Target SDK 30 Package Visibility, otherwise you RUN_COMMAND intent will not work.
- Setup Instructions
- RUN_COMMAND Intent Command Extras
- RUN_COMMAND Intent Result Extras
- Basic Examples
- Advance Examples
- Privacy
The Intent sender/third-party app must request the com.termux.permission.RUN_COMMAND permission in its AndroidManifest.xml and it should be granted by user to the app through the app's App Info Permissions page in Android Settings, likely under Additional Permissions. This is a security measure to prevent any other apps from running commands in Termux context which do not have the required permission granted to them.
For Tasker you can grant it with:
Android Settings -> Apps -> Tasker -> Permissions -> Additional permissions -> Run commands in Termux environment.
The allow-external-apps property must be set to true in ~/.termux/termux.properties in Termux app, regardless of if the executable path is inside or outside the ~/.termux/tasker/ directory. Check here for more info.
For android >= 10 there are new restrictions that prevent activities from starting from the background. This prevents the background TermuxService from starting a terminal session in the foreground and running the commands until the user manually clicks Termux notification in the status bar drop-down notifications list. This only affects commands that are to be executed in a terminal session and not the background ones. Termux version >= 0.100 requests the Draw Over Apps permission so that users can bypass this restriction so that commands can automatically start running without user intervention.
You can grant Termux the Draw Over Apps permission from its App Info activity:
Android Settings -> Apps -> Termux -> Advanced -> Draw over other apps.
Termux app must be granted Storage permission if the executable is accessing or working directory is set to path in external shared storage. The common paths which usually refer to it are ~/storage, /sdcard, /storage/emulated/0 etc.
You can grant Termux the Storage permission from its App Info activity:
For Android version < 11:
Android Settings -> Apps -> Termux -> Permissions -> Storage.
For Android version >= 11
Android Settings -> Apps -> Termux -> Permissions -> Files and media -> Allowed management of all files.
NOTE: For Android version >= 11, sometimes you will get Permission Denied errors for external shared storage even when you have granted Files and media permission. To solve this, Deny the permission and then Allow it again and restart Termux. Also check termux-setup-storage.
Some devices kill apps aggressively or prevent apps from starting from background. If Termux is running into such problems, then exempt it from such restrictions. The user may also disable battery optimizations for Termux to reduce the chances of Termux being killed by Android even further due to violation of not being able to call startForeground() within ~5s of service start in android >= 8. Check dontkillmyapp for device specific info to opt-out of battery optimizations.
The RUN_COMMAND intent expects the following extras that should contain the info of the command to be executed.
The extra constant values are defined by TermuxConstants class of the termux-shared library.
- The
StringRUN_COMMAND_SERVICE.EXTRA_COMMAND_PATHextra for absolute path of command. (mandatory) - The
String[]RUN_COMMAND_SERVICE.EXTRA_ARGUMENTSextra for arguments to the executable of the command (notstdinscript). - The
booleanRUN_COMMAND_SERVICE.EXTRA_REPLACE_COMMA_ALTERNATIVE_CHARS_IN_ARGUMENTSextra whether to for whether to replace comma alternative characters in arguments with comma characters. This defaults tofalse. Check 2aafcf84 for details. Requires Termux app version>= 0.115. - The
StringRUN_COMMAND_SERVICE.EXTRA_COMMA_ALTERNATIVE_CHARS_IN_ARGUMENTSextra for the comma alternative characters in arguments that should be replaced instead of the default alternate comma character(U+201A, ‚, ‚, single low-9 quotation mark). Requires Termux app version>= 0.115. - The
StringRUN_COMMAND_SERVICE.EXTRA_STDINextra forstdinof the command. Requires Termux app version>= 0.109. - The
StringRUN_COMMAND_SERVICE.EXTRA_WORKDIRextra for current working directory of command. This defaults toTermuxConstants.TERMUX_HOME_DIR_PATH. - The
booleanRUN_COMMAND_SERVICE.EXTRA_BACKGROUNDextra whether to run command in background or foreground terminal session. This defaults tofalse. - The
StringRUN_COMMAND_SERVICE.EXTRA_BACKGROUND_CUSTOM_LOG_LEVELextra for custom log level for background commands that should be used by Termux. Check Custom Log Level and 60f37bde for details. Requires Termux app version>= 0.118. - The
StringRUN_COMMAND_SERVICE.EXTRA_SESSION_ACTIONextra for session action of foreground commands. This defaults toTERMUX_SERVICE.VALUE_EXTRA_SESSION_ACTION_SWITCH_TO_NEW_SESSION_AND_OPEN_ACTIVITY. Requires Termux app version>= 0.109. - The
StringRUN_COMMAND_SERVICE.EXTRA_COMMAND_LABELextra for label of the command. Requires Termux app version>= 0.109. - The markdown
StringRUN_COMMAND_SERVICE.EXTRA_COMMAND_DESCRIPTIONextra for description of the command. This should ideally be get short. Requires Termux app version>= 0.109. - The markdown
StringRUN_COMMAND_SERVICE.EXTRA_COMMAND_HELPextra for help of 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. Requires Termux app version>= 0.109. - The
ParcelableRUN_COMMAND_SERVICE.EXTRA_PENDING_INTENTextra containing the pending intent with which result of commands should be returned to the caller. The results will be sent in theTERMUX_SERVICE.EXTRA_PLUGIN_RESULT_BUNDLEbundle. This is optional and only needed if caller wants the results back. Requires Termux app version>= 0.109. - The
StringRUN_COMMAND_SERVICE.EXTRA_RESULT_DIRECTORYextra for the directory path in which to write the result of the execution command for the caller. Check 2aafcf84 for additional sub extras and how this works. Requires Termux app version>= 0.115.
The RUN_COMMAND_SERVICE.EXTRA_STDIN can be used to pass a script or other data via stdin to the executable, like bash or python.
The RUN_COMMAND_SERVICE.EXTRA_COMMAND_PATH and RUN_COMMAND_SERVICE.EXTRA_WORKDIR can optionally be prefixed with $PREFIX/ or ~/ if an absolute path is not to be given. The $PREFIX/ will expand to TermuxConstants.TERMUX_PREFIX_DIR_PATH and ~/ will expand to TermuxConstants.TERMUX_HOME_DIR_PATH.
The EXTRA_COMMAND_* extras are used for logging and their values are provided to users in case of failure in a popup. The popup shown is in commonmark-spec markdown using markwon library so make sure to follow its formatting rules. Also make sure to end lines with 2 blank spaces to prevent word-wrap wherever needed. It's the users and 3rd party apps responsibility to use them wisely. There are also android internal intent size limits (roughly 500KB) that must not exceed when sending intents so make sure the combined size of ALL extras is less than that. There are also limits on the arguments size you can pass to commands or the full command string length that can be run, which is likely equal to 131072 bytes or 128KB on an android device. Check Arguments and Result Data Limits for more details.
Requires Termux app version >= 0.109.
The RUN_COMMAND intent returns the following extras in the TERMUX_SERVICE.EXTRA_PLUGIN_RESULT_BUNDLE Bundle if a pending intent is sent by the caller in the Parcelable RUN_COMMAND_SERVICE.EXTRA_PENDING_INTENT extra. Getting result of commands back is not possible for intents sent with am startservice command and so Java should be used for that case.
For foreground commands (RUN_COMMAND_SERVICE.EXTRA_BACKGROUND is false):
-
TERMUX_SERVICE.EXTRA_PLUGIN_RESULT_BUNDLE_STDOUTwill containsession transcript. -
TERMUX_SERVICE.EXTRA_PLUGIN_RESULT_BUNDLE_STDERRwill benullsince its not used. -
TERMUX_SERVICE.EXTRA_PLUGIN_RESULT_BUNDLE_EXIT_CODEwill containexit codeof session.
For background commands (RUN_COMMAND_SERVICE.EXTRA_BACKGROUND is true):
-
TERMUX_SERVICE.EXTRA_PLUGIN_RESULT_BUNDLE_STDOUTwill containstdoutof commands. -
TERMUX_SERVICE.EXTRA_PLUGIN_RESULT_BUNDLE_STDERRwill containstderrof commands. -
TERMUX_SERVICE.EXTRA_PLUGIN_RESULT_BUNDLE_EXIT_CODEwill containexit codeof command.
The session transcript for foreground commands 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.
The TERMUX_SERVICE.EXTRA_PLUGIN_RESULT_BUNDLE_STDOUT_ORIGINAL_LENGTH and TERMUX_SERVICE.EXTRA_PLUGIN_RESULT_BUNDLE_STDERR_ORIGINAL_LENGTH will contain the original length of stdout and stderr respectively.
The internal errors raised by termux outside the shell will be sent in the the TERMUX_SERVICE.EXTRA_PLUGIN_RESULT_BUNDLE_ERR (err and TERMUX_SERVICE.EXTRA_PLUGIN_RESULT_BUNDLE_ERRMSG (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.
Note that if stdout or stderr are too large in length, then a 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 TERMUX_SERVICE.EXTRA_PLUGIN_RESULT_BUNDLE_STDOUT_ORIGINAL_LENGTH and 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.
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);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'Define the RUN_COMMAND intent sender code.
If your app wants to receive termux command results, then put the pending intent for your app like for an IntentService in the RUN_COMMAND_SERVICE.EXTRA_PENDING_INTENT extra.
import com.termux.shared.termux.TermuxConstants;
import com.termux.shared.termux.TermuxConstants.TERMUX_APP.RUN_COMMAND_SERVICE;
...
String LOG_TAG = "MainActivity";
Intent intent = new Intent();
intent.setClassName(TermuxConstants.TERMUX_PACKAGE_NAME, TermuxConstants.TERMUX_APP.RUN_COMMAND_SERVICE_NAME);
intent.setAction(RUN_COMMAND_SERVICE.ACTION_RUN_COMMAND);
intent.putExtra(RUN_COMMAND_SERVICE.EXTRA_COMMAND_PATH, "/data/data/com.termux/files/usr/bin/top");
intent.putExtra(RUN_COMMAND_SERVICE.EXTRA_ARGUMENTS, new String[]{"-n", "2"});
intent.putExtra(RUN_COMMAND_SERVICE.EXTRA_WORKDIR, "/data/data/com.termux/files/home");
intent.putExtra(RUN_COMMAND_SERVICE.EXTRA_BACKGROUND, false);
intent.putExtra(RUN_COMMAND_SERVICE.EXTRA_SESSION_ACTION, "0");
intent.putExtra(RUN_COMMAND_SERVICE.EXTRA_COMMAND_LABEL, "top command");
intent.putExtra(RUN_COMMAND_SERVICE.EXTRA_COMMAND_DESCRIPTION, "Runs the top command to show processes using the most resources.");
// Create the intent for the IntentService class that should be sent the result by TermuxService
Intent pluginResultsServiceIntent = new Intent(MainActivity.this, PluginResultsService.class);
// Generate a unique execution id for this execution command
int executionId = PluginResultsService.getNextExecutionId();
// Optional put an extra that uniquely identifies the command internally for your app.
// This can be an Intent extra as well with more extras instead of just an int.
pluginResultsServiceIntent.putExtra(PluginResultsService.EXTRA_EXECUTION_ID, executionId);
// Create the PendingIntent that will be used by TermuxService to send result of
// commands back to the IntentService
// Note that the requestCode (currently executionId) must be unique for each pending
// intent, even if extras are different, otherwise only the result of only the first
// execution will be returned since pending intent will be cancelled by android
// after the first result has been sent back via the pending intent and termux
// will not be able to send more.
PendingIntent pendingIntent = PendingIntent.getService(MainActivity.this, executionId, pluginResultsServiceIntent, PendingIntent.FLAG_ONE_SHOT);
intent.putExtra(RUN_COMMAND_SERVICE.EXTRA_PENDING_INTENT, pendingIntent);
try {
// Send command intent for execution
Log.d(LOG_TAG, "Sending execution command with id " + executionId);
startService(intent);
} catch (Exception e) {
Log.e(LOG_TAG, "Failed to start execution command with id " + executionId + ": " + e.getMessage());
}Define the PluginResultsService class which extends from IntentService.
import com.termux.shared.termux.TermuxConstants.TERMUX_APP.TERMUX_SERVICE;
...
public class PluginResultsService extends IntentService {
public static final String EXTRA_EXECUTION_ID = "execution_id";
private static int EXECUTION_ID = 1000;
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;
Log.d(LOG_TAG, PLUGIN_SERVICE_LABEL + " received execution result");
final Bundle resultBundle = intent.getBundleExtra(TERMUX_SERVICE.EXTRA_PLUGIN_RESULT_BUNDLE);
if (resultBundle == null) {
Log.e(LOG_TAG, "The intent does not contain the result bundle at the \"" + TERMUX_SERVICE.EXTRA_PLUGIN_RESULT_BUNDLE + "\" key.");
return;
}
final int executionId = intent.getIntExtra(EXTRA_EXECUTION_ID, 0);
Log.d(LOG_TAG, "Execution id " + executionId + " result:\n" +
"stdout:\n```\n" + resultBundle.getString(TERMUX_SERVICE.EXTRA_PLUGIN_RESULT_BUNDLE_STDOUT, "") + "\n```\n" +
"stdout_original_length: `" + resultBundle.getString(TERMUX_SERVICE.EXTRA_PLUGIN_RESULT_BUNDLE_STDOUT_ORIGINAL_LENGTH) + "`\n" +
"stderr:\n```\n" + resultBundle.getString(TERMUX_SERVICE.EXTRA_PLUGIN_RESULT_BUNDLE_STDERR, "") + "\n```\n" +
"stderr_original_length: `" + resultBundle.getString(TERMUX_SERVICE.EXTRA_PLUGIN_RESULT_BUNDLE_STDERR_ORIGINAL_LENGTH) + "`\n" +
"exitCode: `" + resultBundle.getInt(TERMUX_SERVICE.EXTRA_PLUGIN_RESULT_BUNDLE_EXIT_CODE) + "`\n" +
"errCode: `" + resultBundle.getInt(TERMUX_SERVICE.EXTRA_PLUGIN_RESULT_BUNDLE_ERR) + "`\n" +
"errmsg: `" + resultBundle.getString(TERMUX_SERVICE.EXTRA_PLUGIN_RESULT_BUNDLE_ERRMSG, "") + "`");
}
public static synchronized int getNextExecutionId() {
return EXECUTION_ID++;
}
}Declare com.termux.permission.RUN_COMMAND permission and PluginResultsService service entry in AndroidManifest.xml.
<uses-permission android:name="com.termux.permission.RUN_COMMAND" />
<application
...
<service android:name=".PluginResultsService" />
</application>If a third party app ran a termux command for a user, then it can get the session transcript back for the terminal session, and stdout/stderr for background commands using PendingIntent. Even with the dual RUN_COMMAND permission and allow-external-app requirement, this may not be something that the user wants, since it could give the 3rd party app access to private user data. So use this wisely. In future, a whitelist/blacklist may be implemented to give further control to the user for which app's can get the result back or show prompts before running commands. Although, the 3rd party app can still use physical files or intents inside the commands run to get the result back, but this can likely be solved by approving a script before its run each time or permanently by storing the script hash in an internal termux database.