|
10 | 10 | import android.os.Binder;
|
11 | 11 | import android.os.Build;
|
12 | 12 | import android.os.IBinder;
|
13 |
| -import android.util.Log; |
14 | 13 |
|
15 | 14 | import com.termux.R;
|
16 | 15 | import com.termux.app.TermuxConstants.TERMUX_APP.RUN_COMMAND_SERVICE;
|
17 | 16 | import com.termux.app.TermuxConstants.TERMUX_APP.TERMUX_SERVICE;
|
18 | 17 | import com.termux.app.utils.FileUtils;
|
19 | 18 | import com.termux.app.utils.Logger;
|
20 | 19 | import com.termux.app.utils.PluginUtils;
|
21 |
| - |
22 |
| -import java.util.Arrays; |
23 |
| -import java.util.HashMap; |
| 20 | +import com.termux.app.utils.TextDataUtils; |
| 21 | +import com.termux.models.ExecutionCommand; |
| 22 | +import com.termux.models.ExecutionCommand.ExecutionState; |
24 | 23 |
|
25 | 24 | /**
|
26 | 25 | * Third-party apps that are not part of termux world can run commands in termux context by either
|
|
88 | 87 | *
|
89 | 88 | * The {@link RUN_COMMAND_SERVICE#ACTION_RUN_COMMAND} intent expects the following extras:
|
90 | 89 | *
|
91 |
| - * 1. The {@code String} {@link RUN_COMMAND_SERVICE#EXTRA_COMMAND_PATH} extra for absolute path of |
92 |
| - * command. This is mandatory. |
| 90 | + * 1. The **mandatory** {@code String} {@link RUN_COMMAND_SERVICE#EXTRA_COMMAND_PATH} extra for |
| 91 | + * absolute path of command. |
93 | 92 | * 2. The {@code String[]} {@link RUN_COMMAND_SERVICE#EXTRA_ARGUMENTS} extra for any arguments to
|
94 |
| - * pass to command. This is optional. |
| 93 | + * pass to command. |
95 | 94 | * 3. The {@code String} {@link RUN_COMMAND_SERVICE#EXTRA_WORKDIR} extra for current working directory
|
96 |
| - * of command. This is optional and defaults to {@link TermuxConstants#TERMUX_HOME_DIR_PATH}. |
| 95 | + * of command. This defaults to {@link TermuxConstants#TERMUX_HOME_DIR_PATH}. |
97 | 96 | * 4. The {@code boolean} {@link RUN_COMMAND_SERVICE#EXTRA_BACKGROUND} extra whether to run command
|
98 |
| - * in background or foreground terminal session. This is optional and defaults to {@code false}. |
| 97 | + * in background or foreground terminal session. This defaults to {@code false}. |
99 | 98 | * 5. The {@code String} {@link RUN_COMMAND_SERVICE#EXTRA_SESSION_ACTION} extra for for session action
|
100 |
| - * of foreground commands. This is optional and defaults to |
| 99 | + * of foreground commands. This defaults to |
101 | 100 | * {@link TERMUX_SERVICE#VALUE_EXTRA_SESSION_ACTION_SWITCH_TO_NEW_SESSION_AND_OPEN_ACTIVITY}.
|
102 |
| - * |
| 101 | + * 6. The {@code String} {@link RUN_COMMAND_SERVICE#EXTRA_COMMAND_LABEL} extra for label of the command. |
| 102 | + * 7. The markdown {@code String} {@link RUN_COMMAND_SERVICE#EXTRA_COMMAND_DESCRIPTION} extra for |
| 103 | + * description of the command. This should ideally be get short. |
| 104 | + * 8. The markdown {@code String} {@link RUN_COMMAND_SERVICE#EXTRA_COMMAND_HELP} extra for help of |
| 105 | + * the command. This can add details about the command. 3rd party apps can provide more info |
| 106 | + * to users for setting up commands. Ideally a url link should be provided that goes into full |
| 107 | + * details. |
103 | 108 | *
|
104 | 109 | *
|
105 | 110 | * The {@link RUN_COMMAND_SERVICE#EXTRA_COMMAND_PATH} and {@link RUN_COMMAND_SERVICE#EXTRA_WORKDIR}
|
106 | 111 | * can optionally be prefixed with "$PREFIX/" or "~/" if an absolute path is not to be given.
|
107 | 112 | * The "$PREFIX/" will expand to {@link TermuxConstants#TERMUX_PREFIX_DIR_PATH} and
|
108 | 113 | * "~/" will expand to {@link TermuxConstants#TERMUX_HOME_DIR_PATH}, followed by a forward slash "/".
|
109 | 114 | *
|
| 115 | + * |
| 116 | + * The `EXTRA_COMMAND_*` extras are used for logging and are their values are provided to users in case |
| 117 | + * of failure in a popup. The popup shown is in commonmark-spec markdown using markwon library so |
| 118 | + * make sure to follow its formatting rules. Also make sure to end lines with 2 blank spaces to prevent |
| 119 | + * word-wrap wherever needed. |
| 120 | + * It's the users and 3rd party apps responsibility to use them wisely. There are also android |
| 121 | + * internal intent size limits (roughly 500KB) that must not exceed when sending intents so make sure |
| 122 | + * the combined size of ALL extras is less than that. |
| 123 | + * There are also limits on the arguments size you can pass to commands or the full command string |
| 124 | + * length that can be run, which is likely equal to 131072 bytes or 128KB on an android device. |
| 125 | + * Check https://github.com/termux/termux-tasker#arguments-and-result-data-limits for more details. |
| 126 | + * |
| 127 | + * |
| 128 | + * |
110 | 129 | * If your third-party app is targeting sdk 30 (android 11), then it needs to add `com.termux`
|
111 | 130 | * package to the `queries` element or request `QUERY_ALL_PACKAGES` permission in its
|
112 | 131 | * `AndroidManifest.xml`. Otherwise it will get `PackageSetting{...... com.termux/......} BLOCKED`
|
|
138 | 157 | public class RunCommandService extends Service {
|
139 | 158 |
|
140 | 159 | private static final String NOTIFICATION_CHANNEL_ID = "termux_run_command_notification_channel";
|
141 |
| - private static final int NOTIFICATION_ID = 1338; |
| 160 | + public static final int NOTIFICATION_ID = 1338; |
142 | 161 |
|
143 | 162 | private static final String LOG_TAG = "RunCommandService";
|
144 | 163 |
|
@@ -166,78 +185,91 @@ public int onStartCommand(Intent intent, int flags, int startId) {
|
166 | 185 | // Run again in case service is already started and onCreate() is not called
|
167 | 186 | runStartForeground();
|
168 | 187 |
|
| 188 | + ExecutionCommand executionCommand = new ExecutionCommand(); |
| 189 | + executionCommand.pluginAPIHelp = this.getString(R.string.run_command_service_api_help); |
| 190 | + |
169 | 191 | String errmsg;
|
170 | 192 |
|
171 | 193 | // If invalid action passed, then just return
|
172 | 194 | if (!RUN_COMMAND_SERVICE.ACTION_RUN_COMMAND.equals(intent.getAction())) {
|
173 | 195 | errmsg = this.getString(R.string.run_command_service_invalid_action, intent.getAction());
|
174 |
| - Logger.logError(LOG_TAG, errmsg); |
| 196 | + executionCommand.setStateFailed(1, errmsg, null); |
| 197 | + PluginUtils.processPluginExecutionCommandError(this, LOG_TAG, executionCommand); |
175 | 198 | return Service.START_NOT_STICKY;
|
176 | 199 | }
|
177 | 200 |
|
| 201 | + executionCommand.executable = intent.getStringExtra(RUN_COMMAND_SERVICE.EXTRA_COMMAND_PATH); |
| 202 | + executionCommand.arguments = intent.getStringArrayExtra(RUN_COMMAND_SERVICE.EXTRA_ARGUMENTS); |
| 203 | + executionCommand.workingDirectory = intent.getStringExtra(RUN_COMMAND_SERVICE.EXTRA_WORKDIR); |
| 204 | + executionCommand.inBackground = intent.getBooleanExtra(RUN_COMMAND_SERVICE.EXTRA_BACKGROUND, false); |
| 205 | + executionCommand.sessionAction = intent.getStringExtra(RUN_COMMAND_SERVICE.EXTRA_SESSION_ACTION); |
| 206 | + executionCommand.commandLabel = TextDataUtils.getDefaultIfNull(intent.getStringExtra(RUN_COMMAND_SERVICE.EXTRA_COMMAND_LABEL), "RUN_COMMAND Execution Intent Command"); |
| 207 | + executionCommand.commandDescription = intent.getStringExtra(RUN_COMMAND_SERVICE.EXTRA_COMMAND_DESCRIPTION); |
| 208 | + executionCommand.commandHelp = intent.getStringExtra(RUN_COMMAND_SERVICE.EXTRA_COMMAND_HELP); |
| 209 | + |
| 210 | + if(!executionCommand.setState(ExecutionState.PRE_EXECUTION)) |
| 211 | + return Service.START_NOT_STICKY; |
| 212 | + |
178 | 213 | // If "allow-external-apps" property to not set to "true", then just return
|
179 | 214 | errmsg = PluginUtils.checkIfRunCommandServiceAllowExternalAppsPolicyIsViolated(this);
|
180 | 215 | if (errmsg != null) {
|
181 |
| - Logger.logError(LOG_TAG, errmsg); |
| 216 | + executionCommand.setStateFailed(1, errmsg, null); |
| 217 | + PluginUtils.processPluginExecutionCommandError(this, LOG_TAG, executionCommand); |
182 | 218 | return Service.START_NOT_STICKY;
|
183 | 219 | }
|
184 | 220 |
|
185 |
| - |
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 | 221 | // Get canonical path of executable
|
193 |
| - executable = FileUtils.getCanonicalPath(executable, null, true); |
| 222 | + executionCommand.executable = FileUtils.getCanonicalPath(executionCommand.executable, null, true); |
194 | 223 |
|
195 | 224 | // If executable is not a regular file, or is not readable or executable, then just return
|
196 | 225 | // Setting of missing read and execute permissions is not done
|
197 |
| - errmsg = FileUtils.validateRegularFileExistenceAndPermissions(this, executable, |
| 226 | + errmsg = FileUtils.validateRegularFileExistenceAndPermissions(this, executionCommand.executable, |
198 | 227 | null, PluginUtils.PLUGIN_EXECUTABLE_FILE_PERMISSIONS,
|
199 | 228 | false, false);
|
200 | 229 | if (errmsg != null) {
|
201 |
| - errmsg += "\n" + this.getString(R.string.executable_absolute_path, executable); |
202 |
| - Logger.logError(LOG_TAG, errmsg); |
| 230 | + errmsg += "\n" + this.getString(R.string.executable_absolute_path, executionCommand.executable); |
| 231 | + executionCommand.setStateFailed(1, errmsg, null); |
| 232 | + PluginUtils.processPluginExecutionCommandError(this, LOG_TAG, executionCommand); |
203 | 233 | return Service.START_NOT_STICKY;
|
204 | 234 | }
|
205 | 235 |
|
206 | 236 | // If workingDirectory is not null or empty
|
207 |
| - if (workingDirectory != null && !workingDirectory.isEmpty()) { |
| 237 | + if (executionCommand.workingDirectory != null && !executionCommand.workingDirectory.isEmpty()) { |
208 | 238 | // Get canonical path of workingDirectory
|
209 |
| - workingDirectory = FileUtils.getCanonicalPath(workingDirectory, null, true); |
| 239 | + executionCommand.workingDirectory = FileUtils.getCanonicalPath(executionCommand.workingDirectory, null, true); |
210 | 240 |
|
211 | 241 | // If workingDirectory is not a directory, or is not readable or writable, then just return
|
212 | 242 | // Creation of missing directory and setting of read, write and execute permissions are only done if workingDirectory is
|
213 | 243 | // under {@link TermuxConstants#TERMUX_FILES_DIR_PATH}
|
214 | 244 | // We try to set execute permissions, but ignore if they are missing, since only read and write permissions are required
|
215 | 245 | // for working directories.
|
216 |
| - errmsg = FileUtils.validateDirectoryExistenceAndPermissions(this, workingDirectory, |
| 246 | + errmsg = FileUtils.validateDirectoryExistenceAndPermissions(this, executionCommand.workingDirectory, |
217 | 247 | TermuxConstants.TERMUX_FILES_DIR_PATH, PluginUtils.PLUGIN_WORKING_DIRECTORY_PERMISSIONS,
|
218 | 248 | true, true, false,
|
219 | 249 | true);
|
220 | 250 | if (errmsg != null) {
|
221 |
| - errmsg += "\n" + this.getString(R.string.working_directory_absolute_path, workingDirectory); |
222 |
| - Logger.logError(LOG_TAG, errmsg); |
| 251 | + errmsg += "\n" + this.getString(R.string.working_directory_absolute_path, executionCommand.workingDirectory); |
| 252 | + executionCommand.setStateFailed(1, errmsg, null); |
| 253 | + PluginUtils.processPluginExecutionCommandError(this, LOG_TAG, executionCommand); |
223 | 254 | return Service.START_NOT_STICKY;
|
224 | 255 | }
|
225 | 256 | }
|
226 | 257 |
|
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 |
| - }}); |
| 258 | + executionCommand.executableUri = new Uri.Builder().scheme(TERMUX_SERVICE.URI_SCHEME_SERVICE_EXECUTE).path(FileUtils.getExpandedTermuxPath(executionCommand.executable)).build(); |
230 | 259 |
|
231 |
| - Uri executableUri = new Uri.Builder().scheme(TERMUX_SERVICE.URI_SCHEME_SERVICE_EXECUTE).path(FileUtils.getExpandedTermuxPath(executable)).build(); |
| 260 | + Logger.logVerbose(LOG_TAG, executionCommand.toString()); |
232 | 261 |
|
233 | 262 | // 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); |
| 263 | + Intent execIntent = new Intent(TERMUX_SERVICE.ACTION_SERVICE_EXECUTE, executionCommand.executableUri); |
235 | 264 | execIntent.setClass(this, TermuxService.class);
|
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); |
240 |
| - |
| 265 | + execIntent.putExtra(TERMUX_SERVICE.EXTRA_ARGUMENTS, executionCommand.arguments); |
| 266 | + if (executionCommand.workingDirectory != null && !executionCommand.workingDirectory.isEmpty()) execIntent.putExtra(TERMUX_SERVICE.EXTRA_WORKDIR, executionCommand.workingDirectory); |
| 267 | + execIntent.putExtra(TERMUX_SERVICE.EXTRA_BACKGROUND, executionCommand.inBackground); |
| 268 | + execIntent.putExtra(TERMUX_SERVICE.EXTRA_SESSION_ACTION, executionCommand.sessionAction); |
| 269 | + execIntent.putExtra(TERMUX_SERVICE.EXTRA_COMMAND_LABEL, executionCommand.commandLabel); |
| 270 | + execIntent.putExtra(TERMUX_SERVICE.EXTRA_COMMAND_DESCRIPTION, executionCommand.commandDescription); |
| 271 | + execIntent.putExtra(TERMUX_SERVICE.EXTRA_COMMAND_HELP, executionCommand.commandHelp); |
| 272 | + execIntent.putExtra(TERMUX_SERVICE.EXTRA_PLUGIN_API_HELP, executionCommand.pluginAPIHelp); |
241 | 273 |
|
242 | 274 | // Start TERMUX_SERVICE and pass it execution intent
|
243 | 275 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
@@ -292,8 +324,8 @@ private void setupNotificationChannel() {
|
292 | 324 | int importance = NotificationManager.IMPORTANCE_LOW;
|
293 | 325 |
|
294 | 326 | NotificationChannel channel = new NotificationChannel(NOTIFICATION_CHANNEL_ID, channelName, importance);
|
295 |
| - NotificationManager manager = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE); |
296 |
| - manager.createNotificationChannel(channel); |
| 327 | + NotificationManager notificationManager = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE); |
| 328 | + notificationManager.createNotificationChannel(channel); |
297 | 329 | }
|
298 | 330 |
|
299 | 331 | }
|
0 commit comments