Skip to content

Commit b4995ef

Browse files
Refactor RunCommandService
- The `FileUtils` and `PluginUtils` have been added to provide utility functions. - The executable and working directory validation has been added to check for existence and missing permissions. - The `expandPath()` function is removed from `RunCommandService`. - Working directory will automatically be created if under `TermuxConstants.TERMUX_FILES_DIR_PATH` if missing. - Better logging has been added. This will later be used to notify the user in foreground. - Javadocs have been updated.
1 parent ec7568d commit b4995ef

File tree

4 files changed

+696
-54
lines changed

4 files changed

+696
-54
lines changed

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

+116-54
Original file line numberDiff line numberDiff line change
@@ -10,27 +10,79 @@
1010
import android.os.Binder;
1111
import android.os.Build;
1212
import android.os.IBinder;
13+
import android.util.Log;
1314

1415
import com.termux.R;
1516
import com.termux.app.TermuxConstants.TERMUX_APP.RUN_COMMAND_SERVICE;
1617
import com.termux.app.TermuxConstants.TERMUX_APP.TERMUX_SERVICE;
17-
import com.termux.app.settings.properties.SharedProperties;
18-
import com.termux.app.settings.properties.TermuxPropertyConstants;
18+
import com.termux.app.utils.FileUtils;
1919
import com.termux.app.utils.Logger;
20+
import com.termux.app.utils.PluginUtils;
21+
22+
import java.util.Arrays;
23+
import java.util.HashMap;
2024

2125
/**
2226
* Third-party apps that are not part of termux world can run commands in termux context by either
2327
* sending an intent to RunCommandService or becoming a plugin host for the termux-tasker plugin
2428
* client.
2529
*
26-
* For the {@link RUN_COMMAND_SERVICE#ACTION_RUN_COMMAND} intent to work, there are 2 main requirements:
30+
* For the {@link RUN_COMMAND_SERVICE#ACTION_RUN_COMMAND} intent to work, here are the requirements:
2731
*
28-
* 1. The `allow-external-apps` property must be set to "true" in ~/.termux/termux.properties in
29-
* termux app, regardless of if the executable path is inside or outside the `~/.termux/tasker/`
30-
* directory.
31-
* 2. The intent sender/third-party app must request the `com.termux.permission.RUN_COMMAND`
32+
* 1. `com.termux.permission.RUN_COMMAND` permission (Mandatory)
33+
* The Intent sender/third-party app must request the `com.termux.permission.RUN_COMMAND`
3234
* permission in its `AndroidManifest.xml` and it should be granted by user to the app through the
33-
* app's App Info permissions page in android settings, likely under Additional Permissions.
35+
* app's `App Info` `Permissions` page in Android Settings, likely under `Additional Permissions`.
36+
* This is a security measure to prevent any other apps from running commands in `Termux` context
37+
* which do not have the required permission granted to them.
38+
*
39+
* For `Tasker` you can grant it with:
40+
* `Android Settings` -> `Apps` -> `Tasker` -> `Permissions` -> `Additional permissions` ->
41+
* `Run commands in Termux environment`.
42+
*
43+
* 2. `allow-external-apps` property (Mandatory)
44+
* The `allow-external-apps` property must be set to "true" in `~/.termux/termux.properties` in
45+
* Termux app, regardless of if the executable path is inside or outside the `~/.termux/tasker/`
46+
* directory. Check https://github.com/termux/termux-tasker#allow-external-apps-property-optional
47+
* for more info.
48+
*
49+
* 3. `Draw Over Apps` permission (Optional)
50+
* For android `>= 10` there are new
51+
* [restrictions](https://developer.android.com/guide/components/activities/background-starts)
52+
* that prevent activities from starting from the background. This prevents the background
53+
* {@link TermuxService} from starting a terminal session in the foreground and running the
54+
* commands until the user manually clicks `Termux` notification in the status bar dropdown
55+
* notifications list. This only affects commands that are to be executed in a terminal
56+
* session and not the background ones. `Termux` version `>= 0.100`
57+
* requests the `Draw Over Apps` permission so that users can bypass this restriction so
58+
* that commands can automatically start running without user intervention.
59+
* You can grant `Termux` the `Draw Over Apps` permission from its `App Info` activity:
60+
* `Android Settings` -> `Apps` -> `Termux` -> `Advanced` -> `Draw over other apps`.
61+
*
62+
* 4. `Storage` permission (Optional)
63+
* Termux app must be granted `Storage` permission if the executable is accessing or working
64+
* directory is set to path in external shared storage. The common paths which usually refer to
65+
* it are `~/storage`, `/sdcard`, `/storage/emulated/0` etc.
66+
* You can grant `Termux` the `Storage` permission from its `App Info` activity:
67+
* For Android version < 11:
68+
* `Android Settings` -> `Apps` -> `Termux` -> `Permissions` -> `Storage`.
69+
* For Android version >= 11
70+
* `Android Settings` -> `Apps` -> `Termux` -> `Permissions` -> `Files and media` ->
71+
* `Allowed management of all files`.
72+
* NOTE: For Android version >= 11, sometimes you will get `Permission Denied` errors for
73+
* external shared storage even when you have granted `Files and media` permission. To solve
74+
* this, Deny the permission and then Allow it again and restart Termux.
75+
* Also check https://wiki.termux.com/wiki/Termux-setup-storage
76+
*
77+
* 5. Battery Optimizations (May be mandatory depending on device)
78+
* Some devices kill apps aggressively or prevent apps from starting from background.
79+
* If Termux is running into such problems, then exempt it from such restrictions.
80+
* The user may also disable battery optimizations for Termux to reduce the chances of Termux
81+
* being killed by Android even further due to violation of not being able to call
82+
* `startForeground()` within ~5s of service start in android >= 8.
83+
* Check https://dontkillmyapp.com/ for device specfic info to opt-out of battery optimiations.
84+
*
85+
* You may also want to check https://github.com/termux/termux-tasker
3486
*
3587
*
3688
*
@@ -55,21 +107,6 @@
55107
* The "$PREFIX/" will expand to {@link TermuxConstants#TERMUX_PREFIX_DIR_PATH} and
56108
* "~/" will expand to {@link TermuxConstants#TERMUX_HOME_DIR_PATH}, followed by a forward slash "/".
57109
*
58-
*
59-
* To automatically bring termux session to foreground and start termux commands that were started
60-
* with background mode "false" in android >= 10 without user having to click the notification
61-
* manually requires termux to be granted draw over apps permission due to new restrictions
62-
* of starting activities from the background, this also applies to Termux:Tasker plugin.
63-
*
64-
* Check https://github.com/termux/termux-tasker for more details on allow-external-apps and draw
65-
* over apps and other limitations.
66-
*
67-
*
68-
* To reduce the chance of termux being killed by android even further due to violation of not
69-
* being able to call startForeground() within ~5s of service start in android >= 8, the user
70-
* may disable battery optimizations for termux.
71-
*
72-
*
73110
* If your third-party app is targeting sdk 30 (android 11), then it needs to add `com.termux`
74111
* package to the `queries` element or request `QUERY_ALL_PACKAGES` permission in its
75112
* `AndroidManifest.xml`. Otherwise it will get `PackageSetting{...... com.termux/......} BLOCKED`
@@ -129,44 +166,80 @@ public int onStartCommand(Intent intent, int flags, int startId) {
129166
// Run again in case service is already started and onCreate() is not called
130167
runStartForeground();
131168

169+
String errmsg;
170+
132171
// If invalid action passed, then just return
133172
if (!RUN_COMMAND_SERVICE.ACTION_RUN_COMMAND.equals(intent.getAction())) {
134-
Logger.logError(LOG_TAG, "Invalid intent action to RunCommandService: \"" + intent.getAction() + "\"");
173+
errmsg = this.getString(R.string.run_command_service_invalid_action, intent.getAction());
174+
Logger.logError(LOG_TAG, errmsg);
135175
return Service.START_NOT_STICKY;
136176
}
137177

138-
// If allow-external-apps property is not set to "true"
139-
if (!SharedProperties.isPropertyValueTrue(this, TermuxPropertyConstants.getTermuxPropertiesFile(), TermuxConstants.PROP_ALLOW_EXTERNAL_APPS)) {
140-
Logger.logError(LOG_TAG, "RunCommandService requires allow-external-apps property to be set to \"true\" in \"" + TermuxConstants.TERMUX_PROPERTIES_PRIMARY_FILE_PATH + "\" file");
178+
// If "allow-external-apps" property to not set to "true", then just return
179+
errmsg = PluginUtils.checkIfRunCommandServiceAllowExternalAppsPolicyIsViolated(this);
180+
if (errmsg != null) {
181+
Logger.logError(LOG_TAG, errmsg);
141182
return Service.START_NOT_STICKY;
142183
}
143184

144185

145-
146-
String commandPath = intent.getStringExtra(RUN_COMMAND_SERVICE.EXTRA_COMMAND_PATH);
147-
// If invalid commandPath passed, then just return
148-
if (commandPath == null || commandPath.isEmpty()) {
149-
Logger.logError(LOG_TAG, "Invalid coommand path to RunCommandService: \"" + commandPath + "\"");
186+
String executable = intent.getStringExtra(RUN_COMMAND_SERVICE.EXTRA_COMMAND_PATH);
187+
String[] arguments = intent.getStringArrayExtra(RUN_COMMAND_SERVICE.EXTRA_ARGUMENTS);
188+
boolean inBackground = intent.getBooleanExtra(RUN_COMMAND_SERVICE.EXTRA_BACKGROUND, false);
189+
String workingDirectory = intent.getStringExtra(RUN_COMMAND_SERVICE.EXTRA_WORKDIR);
190+
String sessionAction = intent.getStringExtra(RUN_COMMAND_SERVICE.EXTRA_SESSION_ACTION);
191+
192+
// Get canonical path of executable
193+
executable = FileUtils.getCanonicalPath(executable, null, true);
194+
195+
// If executable is not a regular file, or is not readable or executable, then just return
196+
// Setting of missing read and execute permissions is not done
197+
errmsg = FileUtils.validateRegularFileExistenceAndPermissions(this, executable,
198+
null, PluginUtils.PLUGIN_EXECUTABLE_FILE_PERMISSIONS,
199+
false, false);
200+
if (errmsg != null) {
201+
errmsg += "\n" + this.getString(R.string.executable_absolute_path, executable);
202+
Logger.logError(LOG_TAG, errmsg);
150203
return Service.START_NOT_STICKY;
151204
}
152205

153-
Uri programUri = new Uri.Builder().scheme(TERMUX_SERVICE.URI_SCHEME_SERVICE_EXECUTE).path(getExpandedTermuxPath(commandPath)).build();
206+
// If workingDirectory is not null or empty
207+
if (workingDirectory != null && !workingDirectory.isEmpty()) {
208+
// Get canonical path of workingDirectory
209+
workingDirectory = FileUtils.getCanonicalPath(workingDirectory, null, true);
210+
211+
// If workingDirectory is not a directory, or is not readable or writable, then just return
212+
// Creation of missing directory and setting of read, write and execute permissions are only done if workingDirectory is
213+
// under {@link TermuxConstants#TERMUX_FILES_DIR_PATH}
214+
// We try to set execute permissions, but ignore if they are missing, since only read and write permissions are required
215+
// for working directories.
216+
errmsg = FileUtils.validateDirectoryExistenceAndPermissions(this, workingDirectory,
217+
TermuxConstants.TERMUX_FILES_DIR_PATH, PluginUtils.PLUGIN_WORKING_DIRECTORY_PERMISSIONS,
218+
true, true, false,
219+
true);
220+
if (errmsg != null) {
221+
errmsg += "\n" + this.getString(R.string.working_directory_absolute_path, workingDirectory);
222+
Logger.logError(LOG_TAG, errmsg);
223+
return Service.START_NOT_STICKY;
224+
}
225+
}
154226

227+
PluginUtils.dumpExecutionIntentToLog(Log.VERBOSE, LOG_TAG, "RUN_COMMAND Intent", executable, Arrays.asList(arguments), workingDirectory, inBackground, new HashMap<String, Object>() {{
228+
put("sessionAction", sessionAction);
229+
}});
155230

231+
Uri executableUri = new Uri.Builder().scheme(TERMUX_SERVICE.URI_SCHEME_SERVICE_EXECUTE).path(FileUtils.getExpandedTermuxPath(executable)).build();
156232

157-
Intent execIntent = new Intent(TERMUX_SERVICE.ACTION_SERVICE_EXECUTE, programUri);
233+
// Create execution intent with the action TERMUX_SERVICE#ACTION_SERVICE_EXECUTE to be sent to the TERMUX_SERVICE
234+
Intent execIntent = new Intent(TERMUX_SERVICE.ACTION_SERVICE_EXECUTE, executableUri);
158235
execIntent.setClass(this, TermuxService.class);
159-
execIntent.putExtra(TERMUX_SERVICE.EXTRA_ARGUMENTS, intent.getStringArrayExtra(RUN_COMMAND_SERVICE.EXTRA_ARGUMENTS));
160-
execIntent.putExtra(TERMUX_SERVICE.EXTRA_BACKGROUND, intent.getBooleanExtra(RUN_COMMAND_SERVICE.EXTRA_BACKGROUND, false));
161-
execIntent.putExtra(TERMUX_SERVICE.EXTRA_SESSION_ACTION, intent.getStringExtra(RUN_COMMAND_SERVICE.EXTRA_SESSION_ACTION));
162-
163-
String workingDirectory = intent.getStringExtra(RUN_COMMAND_SERVICE.EXTRA_WORKDIR);
164-
if (workingDirectory != null && !workingDirectory.isEmpty()) {
165-
execIntent.putExtra(TERMUX_SERVICE.EXTRA_WORKDIR, getExpandedTermuxPath(workingDirectory));
166-
}
167-
236+
execIntent.putExtra(TERMUX_SERVICE.EXTRA_ARGUMENTS, arguments);
237+
execIntent.putExtra(TERMUX_SERVICE.EXTRA_BACKGROUND, inBackground);
238+
if (workingDirectory != null && !workingDirectory.isEmpty()) execIntent.putExtra(TERMUX_SERVICE.EXTRA_WORKDIR, workingDirectory);
239+
execIntent.putExtra(TERMUX_SERVICE.EXTRA_SESSION_ACTION, sessionAction);
168240

169241

242+
// Start TERMUX_SERVICE and pass it execution intent
170243
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
171244
this.startForegroundService(execIntent);
172245
} else {
@@ -223,15 +296,4 @@ private void setupNotificationChannel() {
223296
manager.createNotificationChannel(channel);
224297
}
225298

226-
/** Replace "$PREFIX/" or "~/" prefix with termux absolute paths */
227-
public static String getExpandedTermuxPath(String path) {
228-
if(path != null && !path.isEmpty()) {
229-
path = path.replaceAll("^\\$PREFIX$", TermuxConstants.TERMUX_PREFIX_DIR_PATH);
230-
path = path.replaceAll("^\\$PREFIX/", TermuxConstants.TERMUX_PREFIX_DIR_PATH + "/");
231-
path = path.replaceAll("^~/$", TermuxConstants.TERMUX_HOME_DIR_PATH);
232-
path = path.replaceAll("^~/", TermuxConstants.TERMUX_HOME_DIR_PATH + "/");
233-
}
234-
235-
return path;
236-
}
237299
}

0 commit comments

Comments
 (0)