diff --git a/app/src/main/java/com/termux/app/RunCommandService.java b/app/src/main/java/com/termux/app/RunCommandService.java
index c18ed3f6..0b206bd8 100644
--- a/app/src/main/java/com/termux/app/RunCommandService.java
+++ b/app/src/main/java/com/termux/app/RunCommandService.java
@@ -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;
@@ -104,6 +106,10 @@ import com.termux.models.ExecutionCommand.ExecutionState;
* 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}
@@ -132,19 +138,23 @@ import com.termux.models.ExecutionCommand.ExecutionState;
* 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' \
@@ -152,6 +162,111 @@ import com.termux.models.ExecutionCommand.ExecutionState;
* --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
+ * ```
+ *
+ * ```
+ *
+ *
+ * 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 {
@@ -206,6 +321,9 @@ public class RunCommandService extends Service {
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;
@@ -286,6 +404,7 @@ public class RunCommandService extends Service {
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) {
diff --git a/app/src/main/java/com/termux/app/TermuxService.java b/app/src/main/java/com/termux/app/TermuxService.java
index f81d26dc..5277a4e6 100644
--- a/app/src/main/java/com/termux/app/TermuxService.java
+++ b/app/src/main/java/com/termux/app/TermuxService.java
@@ -216,9 +216,22 @@ public final class TermuxService extends Service {
/** 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();
+ }
}
@@ -360,7 +373,7 @@ public final class TermuxService extends Service {
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;
};
@@ -428,9 +441,9 @@ public final class TermuxService extends Service {
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;
};
@@ -455,7 +468,11 @@ public final class TermuxService extends Service {
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);
diff --git a/app/src/main/java/com/termux/app/terminal/TermuxSession.java b/app/src/main/java/com/termux/app/terminal/TermuxSession.java
index 3af9f73d..e085cf5e 100644
--- a/app/src/main/java/com/termux/app/terminal/TermuxSession.java
+++ b/app/src/main/java/com/termux/app/terminal/TermuxSession.java
@@ -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;
@@ -25,14 +30,12 @@ public class TermuxSession {
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"}) {
@@ -62,8 +65,13 @@ public class TermuxSession {
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());
@@ -76,6 +84,26 @@ public class TermuxSession {
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;
}
diff --git a/app/src/main/java/com/termux/app/terminal/TermuxTask.java b/app/src/main/java/com/termux/app/terminal/TermuxTask.java
index 547066b7..c6e505e3 100644
--- a/app/src/main/java/com/termux/app/terminal/TermuxTask.java
+++ b/app/src/main/java/com/termux/app/terminal/TermuxTask.java
@@ -2,6 +2,7 @@ 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;
@@ -53,8 +54,8 @@ public final class TermuxTask {
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;
}
@@ -117,7 +118,7 @@ public final class TermuxTask {
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;
@@ -125,7 +126,7 @@ public final class TermuxTask {
PluginUtils.processPluginExecutionCommandResult(service.getApplicationContext(), LOG_TAG, executionCommand);
- if(termuxTask != null && service != null)
+ if(termuxTask != null)
service.onTermuxTaskExited(termuxTask);
}
diff --git a/app/src/main/java/com/termux/models/ExecutionCommand.java b/app/src/main/java/com/termux/models/ExecutionCommand.java
index 0205f018..8d4b9b33 100644
--- a/app/src/main/java/com/termux/models/ExecutionCommand.java
+++ b/app/src/main/java/com/termux/models/ExecutionCommand.java
@@ -6,6 +6,7 @@ import android.net.Uri;
import androidx.annotation.NonNull;
+import com.termux.app.TermuxConstants.TERMUX_APP.TERMUX_SERVICE;
import com.termux.app.utils.Logger;
import com.termux.app.utils.MarkdownUtils;
import com.termux.app.utils.TextDataUtils;
@@ -101,8 +102,9 @@ public class ExecutionCommand {
public String pluginAPIHelp;
- /** Defines if {@link ExecutionCommand} was started because of an external plugin request or from
- * within Termux app itself. */
+ /** Defines if {@link ExecutionCommand} was started because of an external plugin request
+ * like {@link TERMUX_SERVICE#ACTION_SERVICE_EXECUTE} intent or from within Termux app itself.
+ */
public boolean isPluginExecutionCommand;
/** Defines {@link PendingIntent} that should be sent if an external plugin requested the execution. */
public PendingIntent pluginPendingIntent;
@@ -503,7 +505,7 @@ public class ExecutionCommand {
- public boolean setState(ExecutionState newState) {
+ public synchronized boolean setState(ExecutionState newState) {
// The state transition cannot go back or change if already at {@link ExecutionState#SUCCESS}
if(newState.getValue() < currentState.getValue() || currentState == ExecutionState.SUCCESS) {
Logger.logError("Invalid "+ getCommandIdAndLabelLogString() + " state transition from \"" + currentState.getName() + "\" to " + "\"" + newState.getName() + "\"");
@@ -520,7 +522,7 @@ public class ExecutionCommand {
return true;
}
- public boolean setStateFailed(int errCode, String errmsg, Throwable throwable) {
+ public synchronized boolean setStateFailed(int errCode, String errmsg, Throwable throwable) {
if (errCode > RESULT_CODE_OK) {
this.errCode = errCode;
} else {
@@ -542,7 +544,7 @@ public class ExecutionCommand {
return true;
}
- public boolean isStateFailed() {
+ public synchronized boolean isStateFailed() {
if(currentState != ExecutionState.FAILED)
return false;
@@ -554,8 +556,12 @@ public class ExecutionCommand {
}
}
- public boolean hasExecuted() {
+ public synchronized boolean hasExecuted() {
return currentState.getValue() >= ExecutionState.EXECUTED.getValue();
}
+ public synchronized boolean isExecuting() {
+ return currentState == ExecutionState.EXECUTING;
+ }
+
}
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index 967650ea..4f290d51 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -96,6 +96,9 @@
Executable Absolute Path: \"%1$s\"
Working Directory Absolute Path: \"%1$s\"
+ Sending SIGKILL to process on user request or because android is killing the service
+ "Failed to execute \"%1$s\" termux session command"
+ "Failed to execute \"%1$s\" termux task command"