Files
termux-app/app/src/main/java/com/termux/app/TermuxService.java
agnostic-apollo 5794ab9a56 Added|Changed: Add support to switch to existing session instead of creating duplicate session for RUN_COMMAND intent
This is done via addition of the `com.termux.RUN_COMMAND_SESSION_CREATE_MODE` extra, which currently supports two values.

- `always` to always create a new session every time.
- `no-session-with-name` to create a new session only if no existing session exits with the same terminal session name.

The terminal session name will equal executable basename by default and dashes `-` in the basename will no longer be replaced with spaces when session name as done previously. The `com.termux.RUN_COMMAND_SESSION_NAME` extra can be used to set custom session name.

Usage:

You can use this with `Termux:Tasker` or `Termux:Widget`.

For example for `Termux:Widget`

- Create a wrapper script at `~/.shortcuts/tasks/my-script.sh` with following contents under `tasks` directory so that it runs in background app shell instead of a terminal session. Do not use terminal session runner for wrapper script, since it will open two sessions everytime otherwise, first for wrapper script, then for actual target executable. There would also be conflicts if both wrapper script and target executable have the same basename and it would be incorrectly assumed that session is already running.
- Replace the `bash` executable with actual target executable that you want to run in the terminal session if its not already running.
- Optionally set custom session name. By default it will set to executable basename and not the wrapper script name. To set it to wrapper script name, you can pass `$(basename "$0")`.
- Launch the wrapper script with widget. On first launch, a new terminal session should open but on subsequent launches, same terminal session should open.

Note that you can also pass `com.termux.RUN_COMMAND_SESSION_ACTION` to modify session action behaviour. Check https://github.com/termux/termux-app/wiki/RUN_COMMAND-Intent#run_command-intent-command-extras.

```

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/bash' \
--es com.termux.RUN_COMMAND_SESSION_CREATE_MODE 'no-session-with-name' \
--es com.termux.RUN_COMMAND_SESSION_NAME "custom-name"
```
2022-04-08 02:42:54 +05:00

910 lines
42 KiB
Java

package com.termux.app;
import android.annotation.SuppressLint;
import android.app.Notification;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.app.Service;
import android.content.Context;
import android.content.Intent;
import android.content.res.Resources;
import android.net.wifi.WifiManager;
import android.os.Binder;
import android.os.Build;
import android.os.Handler;
import android.os.IBinder;
import android.os.PowerManager;
import android.widget.ArrayAdapter;
import androidx.annotation.Nullable;
import com.termux.R;
import com.termux.app.terminal.TermuxTerminalSessionClient;
import com.termux.app.utils.PluginUtils;
import com.termux.shared.data.IntentUtils;
import com.termux.shared.net.uri.UriUtils;
import com.termux.shared.errors.Errno;
import com.termux.shared.shell.ShellUtils;
import com.termux.shared.shell.command.runner.app.AppShell;
import com.termux.shared.termux.settings.properties.TermuxAppSharedProperties;
import com.termux.shared.termux.shell.TermuxShellEnvironmentClient;
import com.termux.shared.termux.shell.TermuxShellUtils;
import com.termux.shared.termux.TermuxConstants;
import com.termux.shared.termux.TermuxConstants.TERMUX_APP.TERMUX_ACTIVITY;
import com.termux.shared.termux.TermuxConstants.TERMUX_APP.TERMUX_SERVICE;
import com.termux.shared.termux.settings.preferences.TermuxAppSharedPreferences;
import com.termux.shared.termux.shell.command.runner.terminal.TermuxSession;
import com.termux.shared.termux.terminal.TermuxTerminalSessionClientBase;
import com.termux.shared.logger.Logger;
import com.termux.shared.notification.NotificationUtils;
import com.termux.shared.android.PermissionUtils;
import com.termux.shared.data.DataUtils;
import com.termux.shared.shell.command.ExecutionCommand;
import com.termux.shared.shell.command.ExecutionCommand.Runner;
import com.termux.shared.shell.command.ExecutionCommand.SessionCreateMode;
import com.termux.terminal.TerminalEmulator;
import com.termux.terminal.TerminalSession;
import com.termux.terminal.TerminalSessionClient;
import java.util.ArrayList;
import java.util.List;
/**
* A service holding a list of {@link TermuxSession} in {@link #mTermuxSessions} and background {@link AppShell}
* in {@link #mTermuxTasks}, showing a foreground notification while running so that it is not terminated.
* The user interacts with the session through {@link TermuxActivity}, but this service may outlive
* the activity when the user or the system disposes of the activity. In that case the user may
* restart {@link TermuxActivity} later to yet again access the sessions.
* <p/>
* In order to keep both terminal sessions and spawned processes (who may outlive the terminal sessions) alive as long
* as wanted by the user this service is a foreground service, {@link Service#startForeground(int, Notification)}.
* <p/>
* Optionally may hold a wake and a wifi lock, in which case that is shown in the notification - see
* {@link #buildNotification()}.
*/
public final class TermuxService extends Service implements AppShell.AppShellClient, TermuxSession.TermuxSessionClient {
private static int EXECUTION_ID = 1000;
/** This service is only bound from inside the same process and never uses IPC. */
class LocalBinder extends Binder {
public final TermuxService service = TermuxService.this;
}
private final IBinder mBinder = new LocalBinder();
private final Handler mHandler = new Handler();
/**
* The foreground TermuxSessions which this service manages.
* Note that this list is observed by {@link TermuxActivity#mTermuxSessionListViewController},
* so any changes must be made on the UI thread and followed by a call to
* {@link ArrayAdapter#notifyDataSetChanged()} }.
*/
final List<TermuxSession> mTermuxSessions = new ArrayList<>();
/**
* The background TermuxTasks which this service manages.
*/
final List<AppShell> mTermuxTasks = new ArrayList<>();
/**
* The pending plugin ExecutionCommands that have yet to be processed by this service.
*/
final List<ExecutionCommand> mPendingPluginExecutionCommands = new ArrayList<>();
/** The full implementation of the {@link TerminalSessionClient} interface to be used by {@link TerminalSession}
* that holds activity references for activity related functions.
* Note that the service may often outlive the activity, so need to clear this reference.
*/
TermuxTerminalSessionClient mTermuxTerminalSessionClient;
/** The basic implementation of the {@link TerminalSessionClient} interface to be used by {@link TerminalSession}
* that does not hold activity references.
*/
final TermuxTerminalSessionClientBase mTermuxTerminalSessionClientBase = new TermuxTerminalSessionClientBase();
/**
* Termux app shared properties manager, loaded from termux.properties
*/
private TermuxAppSharedProperties mProperties;
/** The wake lock and wifi lock are always acquired and released together. */
private PowerManager.WakeLock mWakeLock;
private WifiManager.WifiLock mWifiLock;
/** If the user has executed the {@link TERMUX_SERVICE#ACTION_STOP_SERVICE} intent. */
boolean mWantsToStop = false;
private static final String LOG_TAG = "TermuxService";
@Override
public void onCreate() {
Logger.logVerbose(LOG_TAG, "onCreate");
// Get Termux app SharedProperties without loading from disk since TermuxApplication handles
// load and TermuxActivity handles reloads
mProperties = TermuxAppSharedProperties.getProperties();
runStartForeground();
}
@SuppressLint("Wakelock")
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
Logger.logDebug(LOG_TAG, "onStartCommand");
// Run again in case service is already started and onCreate() is not called
runStartForeground();
String action = null;
if (intent != null) {
Logger.logVerboseExtended(LOG_TAG, "Intent Received:\n" + IntentUtils.getIntentString(intent));
action = intent.getAction();
}
if (action != null) {
switch (action) {
case TERMUX_SERVICE.ACTION_STOP_SERVICE:
Logger.logDebug(LOG_TAG, "ACTION_STOP_SERVICE intent received");
actionStopService();
break;
case TERMUX_SERVICE.ACTION_WAKE_LOCK:
Logger.logDebug(LOG_TAG, "ACTION_WAKE_LOCK intent received");
actionAcquireWakeLock();
break;
case TERMUX_SERVICE.ACTION_WAKE_UNLOCK:
Logger.logDebug(LOG_TAG, "ACTION_WAKE_UNLOCK intent received");
actionReleaseWakeLock(true);
break;
case TERMUX_SERVICE.ACTION_SERVICE_EXECUTE:
Logger.logDebug(LOG_TAG, "ACTION_SERVICE_EXECUTE intent received");
actionServiceExecute(intent);
break;
default:
Logger.logError(LOG_TAG, "Invalid action: \"" + action + "\"");
break;
}
}
// If this service really do get killed, there is no point restarting it automatically - let the user do on next
// start of {@link Term):
return Service.START_NOT_STICKY;
}
@Override
public void onDestroy() {
Logger.logVerbose(LOG_TAG, "onDestroy");
TermuxShellUtils.clearTermuxTMPDIR(true);
actionReleaseWakeLock(false);
if (!mWantsToStop)
killAllTermuxExecutionCommands();
runStopForeground();
}
@Override
public IBinder onBind(Intent intent) {
Logger.logVerbose(LOG_TAG, "onBind");
return mBinder;
}
@Override
public boolean onUnbind(Intent intent) {
Logger.logVerbose(LOG_TAG, "onUnbind");
// Since we cannot rely on {@link TermuxActivity.onDestroy()} to always complete,
// we unset clients here as well if it failed, so that we do not leave service and session
// clients with references to the activity.
if (mTermuxTerminalSessionClient != null)
unsetTermuxTerminalSessionClient();
return false;
}
/** Make service run in foreground mode. */
private void runStartForeground() {
setupNotificationChannel();
startForeground(TermuxConstants.TERMUX_APP_NOTIFICATION_ID, buildNotification());
}
/** Make service leave foreground mode. */
private void runStopForeground() {
stopForeground(true);
}
/** Request to stop service. */
private void requestStopService() {
Logger.logDebug(LOG_TAG, "Requesting to stop service");
runStopForeground();
stopSelf();
}
/** Process action to stop service. */
private void actionStopService() {
mWantsToStop = true;
killAllTermuxExecutionCommands();
requestStopService();
}
/** Kill all TermuxSessions and TermuxTasks by sending SIGKILL to their processes.
*
* For TermuxSessions, all sessions will be killed, whether user manually exited Termux or if
* onDestroy() was directly called because of unintended shutdown. The processing of results
* will only be done if user manually exited termux or if the session was started by a plugin
* which **expects** the result back via a pending intent.
*
* For TermuxTasks, only tasks that were started by a plugin which **expects** the result
* back via a pending intent will be killed, whether user manually exited Termux or if
* onDestroy() was directly called because of unintended shutdown. The processing of results
* will always be done for the tasks that are killed. The remaining processes will keep on
* running until the termux app process is killed by android, like by OOM, so we let them run
* as long as they can.
*
* Some plugin execution commands may not have been processed and added to mTermuxSessions and
* mTermuxTasks lists before the service is killed, so we maintain a separate
* mPendingPluginExecutionCommands list for those, so that we can notify the pending intent
* creators that execution was cancelled.
*
* Note that if user didn't manually exit Termux and if onDestroy() was directly called because
* of unintended shutdown, like android deciding to kill the service, then there will be no
* guarantee that onDestroy() will be allowed to finish and termux app process may be killed before
* it has finished. This means that in those cases some results may not be sent back to their
* creators for plugin commands but we still try to process whatever results can be processed
* despite the unreliable behaviour of onDestroy().
*
* Note that if don't kill the processes started by plugins which **expect** the result back
* and notify their creators that they have been killed, then they may get stuck waiting for
* the results forever like in case of commands started by Termux:Tasker or RUN_COMMAND intent,
* since once TermuxService has been killed, no result will be sent back. They may still get
* stuck if termux app process gets killed, so for this case reasonable timeout values should
* be used, like in Tasker for the Termux:Tasker actions.
*
* We make copies of each list since items are removed inside the loop.
*/
private synchronized void killAllTermuxExecutionCommands() {
boolean processResult;
Logger.logDebug(LOG_TAG, "Killing TermuxSessions=" + mTermuxSessions.size() + ", TermuxTasks=" + mTermuxTasks.size() + ", PendingPluginExecutionCommands=" + mPendingPluginExecutionCommands.size());
List<TermuxSession> termuxSessions = new ArrayList<>(mTermuxSessions);
for (int i = 0; i < termuxSessions.size(); i++) {
ExecutionCommand executionCommand = termuxSessions.get(i).getExecutionCommand();
processResult = mWantsToStop || executionCommand.isPluginExecutionCommandWithPendingResult();
termuxSessions.get(i).killIfExecuting(this, processResult);
}
List<AppShell> termuxTasks = new ArrayList<>(mTermuxTasks);
for (int i = 0; i < termuxTasks.size(); i++) {
ExecutionCommand executionCommand = termuxTasks.get(i).getExecutionCommand();
if (executionCommand.isPluginExecutionCommandWithPendingResult())
termuxTasks.get(i).killIfExecuting(this, true);
}
List<ExecutionCommand> pendingPluginExecutionCommands = new ArrayList<>(mPendingPluginExecutionCommands);
for (int i = 0; i < pendingPluginExecutionCommands.size(); i++) {
ExecutionCommand executionCommand = pendingPluginExecutionCommands.get(i);
if (!executionCommand.shouldNotProcessResults() && executionCommand.isPluginExecutionCommandWithPendingResult()) {
if (executionCommand.setStateFailed(Errno.ERRNO_CANCELLED.getCode(), this.getString(com.termux.shared.R.string.error_execution_cancelled))) {
PluginUtils.processPluginExecutionCommandResult(this, LOG_TAG, executionCommand);
}
}
}
}
/** Process action to acquire Power and Wi-Fi WakeLocks. */
@SuppressLint({"WakelockTimeout", "BatteryLife"})
private void actionAcquireWakeLock() {
if (mWakeLock != null) {
Logger.logDebug(LOG_TAG, "Ignoring acquiring WakeLocks since they are already held");
return;
}
Logger.logDebug(LOG_TAG, "Acquiring WakeLocks");
PowerManager pm = (PowerManager) getSystemService(Context.POWER_SERVICE);
mWakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, TermuxConstants.TERMUX_APP_NAME.toLowerCase() + ":service-wakelock");
mWakeLock.acquire();
// http://tools.android.com/tech-docs/lint-in-studio-2-3#TOC-WifiManager-Leak
WifiManager wm = (WifiManager) getApplicationContext().getSystemService(Context.WIFI_SERVICE);
mWifiLock = wm.createWifiLock(WifiManager.WIFI_MODE_FULL_HIGH_PERF, TermuxConstants.TERMUX_APP_NAME.toLowerCase());
mWifiLock.acquire();
if (!PermissionUtils.checkIfBatteryOptimizationsDisabled(this)) {
PermissionUtils.requestDisableBatteryOptimizations(this);
}
updateNotification();
Logger.logDebug(LOG_TAG, "WakeLocks acquired successfully");
}
/** Process action to release Power and Wi-Fi WakeLocks. */
private void actionReleaseWakeLock(boolean updateNotification) {
if (mWakeLock == null && mWifiLock == null) {
Logger.logDebug(LOG_TAG, "Ignoring releasing WakeLocks since none are already held");
return;
}
Logger.logDebug(LOG_TAG, "Releasing WakeLocks");
if (mWakeLock != null) {
mWakeLock.release();
mWakeLock = null;
}
if (mWifiLock != null) {
mWifiLock.release();
mWifiLock = null;
}
if (updateNotification)
updateNotification();
Logger.logDebug(LOG_TAG, "WakeLocks released successfully");
}
/** Process {@link TERMUX_SERVICE#ACTION_SERVICE_EXECUTE} intent to execute a shell command in
* a foreground TermuxSession or in a background TermuxTask. */
private void actionServiceExecute(Intent intent) {
if (intent == null) {
Logger.logError(LOG_TAG, "Ignoring null intent to actionServiceExecute");
return;
}
ExecutionCommand executionCommand = new ExecutionCommand(getNextExecutionId());
executionCommand.executableUri = intent.getData();
executionCommand.isPluginExecutionCommand = true;
// If EXTRA_RUNNER is passed, use that, otherwise check EXTRA_BACKGROUND and default to Runner.TERMINAL_SESSION
executionCommand.runner = IntentUtils.getStringExtraIfSet(intent, TERMUX_SERVICE.EXTRA_RUNNER,
(intent.getBooleanExtra(TERMUX_SERVICE.EXTRA_BACKGROUND, false) ? Runner.APP_SHELL.getName() : Runner.TERMINAL_SESSION.getName()));
if (Runner.runnerOf(executionCommand.runner) == null) {
String errmsg = this.getString(R.string.error_termux_service_invalid_execution_command_runner, executionCommand.runner);
executionCommand.setStateFailed(Errno.ERRNO_FAILED.getCode(), errmsg);
PluginUtils.processPluginExecutionCommandError(this, LOG_TAG, executionCommand, false);
return;
}
if (executionCommand.executableUri != null) {
Logger.logVerbose(LOG_TAG, "uri: \"" + executionCommand.executableUri + "\", path: \"" + executionCommand.executableUri.getPath() + "\", fragment: \"" + executionCommand.executableUri.getFragment() + "\"");
// Get full path including fragment (anything after last "#")
executionCommand.executable = UriUtils.getUriFilePathWithFragment(executionCommand.executableUri);
executionCommand.arguments = IntentUtils.getStringArrayExtraIfSet(intent, TERMUX_SERVICE.EXTRA_ARGUMENTS, null);
if (Runner.APP_SHELL.equalsRunner(executionCommand.runner))
executionCommand.stdin = IntentUtils.getStringExtraIfSet(intent, TERMUX_SERVICE.EXTRA_STDIN, null);
executionCommand.backgroundCustomLogLevel = IntentUtils.getIntegerExtraIfSet(intent, TERMUX_SERVICE.EXTRA_BACKGROUND_CUSTOM_LOG_LEVEL, null);
}
executionCommand.workingDirectory = IntentUtils.getStringExtraIfSet(intent, TERMUX_SERVICE.EXTRA_WORKDIR, null);
executionCommand.isFailsafe = intent.getBooleanExtra(TERMUX_ACTIVITY.EXTRA_FAILSAFE_SESSION, false);
executionCommand.sessionAction = intent.getStringExtra(TERMUX_SERVICE.EXTRA_SESSION_ACTION);
executionCommand.sessionName = IntentUtils.getStringExtraIfSet(intent, TERMUX_SERVICE.EXTRA_SESSION_NAME, null);
executionCommand.sessionCreateMode = IntentUtils.getStringExtraIfSet(intent, TERMUX_SERVICE.EXTRA_SESSION_CREATE_MODE, null);
executionCommand.commandLabel = IntentUtils.getStringExtraIfSet(intent, TERMUX_SERVICE.EXTRA_COMMAND_LABEL, "Execution Intent Command");
executionCommand.commandDescription = IntentUtils.getStringExtraIfSet(intent, TERMUX_SERVICE.EXTRA_COMMAND_DESCRIPTION, null);
executionCommand.commandHelp = IntentUtils.getStringExtraIfSet(intent, TERMUX_SERVICE.EXTRA_COMMAND_HELP, null);
executionCommand.pluginAPIHelp = IntentUtils.getStringExtraIfSet(intent, TERMUX_SERVICE.EXTRA_PLUGIN_API_HELP, null);
executionCommand.resultConfig.resultPendingIntent = intent.getParcelableExtra(TERMUX_SERVICE.EXTRA_PENDING_INTENT);
executionCommand.resultConfig.resultDirectoryPath = IntentUtils.getStringExtraIfSet(intent, TERMUX_SERVICE.EXTRA_RESULT_DIRECTORY, null);
if (executionCommand.resultConfig.resultDirectoryPath != null) {
executionCommand.resultConfig.resultSingleFile = intent.getBooleanExtra(TERMUX_SERVICE.EXTRA_RESULT_SINGLE_FILE, false);
executionCommand.resultConfig.resultFileBasename = IntentUtils.getStringExtraIfSet(intent, TERMUX_SERVICE.EXTRA_RESULT_FILE_BASENAME, null);
executionCommand.resultConfig.resultFileOutputFormat = IntentUtils.getStringExtraIfSet(intent, TERMUX_SERVICE.EXTRA_RESULT_FILE_OUTPUT_FORMAT, null);
executionCommand.resultConfig.resultFileErrorFormat = IntentUtils.getStringExtraIfSet(intent, TERMUX_SERVICE.EXTRA_RESULT_FILE_ERROR_FORMAT, null);
executionCommand.resultConfig.resultFilesSuffix = IntentUtils.getStringExtraIfSet(intent, TERMUX_SERVICE.EXTRA_RESULT_FILES_SUFFIX, null);
}
// Add the execution command to pending plugin execution commands list
mPendingPluginExecutionCommands.add(executionCommand);
if (Runner.APP_SHELL.equalsRunner(executionCommand.runner))
executeTermuxTaskCommand(executionCommand);
else if (Runner.TERMINAL_SESSION.equalsRunner(executionCommand.runner))
executeTermuxSessionCommand(executionCommand);
else {
String errmsg = getString(R.string.error_termux_service_unsupported_execution_command_runner, executionCommand.runner);
executionCommand.setStateFailed(Errno.ERRNO_FAILED.getCode(), errmsg);
PluginUtils.processPluginExecutionCommandError(this, LOG_TAG, executionCommand, false);
}
}
/** Execute a shell command in background TermuxTask. */
private void executeTermuxTaskCommand(ExecutionCommand executionCommand) {
if (executionCommand == null) return;
Logger.logDebug(LOG_TAG, "Executing background \"" + executionCommand.getCommandIdAndLabelLogString() + "\" TermuxTask command");
AppShell newTermuxTask = createTermuxTask(executionCommand);
}
/** Create a TermuxTask. */
@Nullable
public AppShell createTermuxTask(String executablePath, String[] arguments, String stdin, String workingDirectory) {
return createTermuxTask(new ExecutionCommand(getNextExecutionId(), executablePath, arguments, stdin, workingDirectory, Runner.APP_SHELL.getName(), false));
}
/** Create a TermuxTask. */
@Nullable
public synchronized AppShell createTermuxTask(ExecutionCommand executionCommand) {
if (executionCommand == null) return null;
Logger.logDebug(LOG_TAG, "Creating \"" + executionCommand.getCommandIdAndLabelLogString() + "\" TermuxTask");
if (!Runner.APP_SHELL.equalsRunner(executionCommand.runner)) {
Logger.logDebug(LOG_TAG, "Ignoring wrong runner \"" + executionCommand.runner + "\" command passed to createTermuxTask()");
return null;
}
if (Logger.getLogLevel() >= Logger.LOG_LEVEL_VERBOSE)
Logger.logVerboseExtended(LOG_TAG, executionCommand.toString());
AppShell newTermuxTask = AppShell.execute(this, executionCommand, this, new TermuxShellEnvironmentClient(), false);
if (newTermuxTask == null) {
Logger.logError(LOG_TAG, "Failed to execute new TermuxTask command for:\n" + executionCommand.getCommandIdAndLabelLogString());
// If the execution command was started for a plugin, then process the error
if (executionCommand.isPluginExecutionCommand)
PluginUtils.processPluginExecutionCommandError(this, LOG_TAG, executionCommand, false);
else
Logger.logErrorExtended(LOG_TAG, executionCommand.toString());
return null;
}
mTermuxTasks.add(newTermuxTask);
// Remove the execution command from the pending plugin execution commands list since it has
// now been processed
if (executionCommand.isPluginExecutionCommand)
mPendingPluginExecutionCommands.remove(executionCommand);
updateNotification();
return newTermuxTask;
}
/** Callback received when a TermuxTask finishes. */
@Override
public void onAppShellExited(final AppShell termuxTask) {
mHandler.post(() -> {
if (termuxTask != null) {
ExecutionCommand executionCommand = termuxTask.getExecutionCommand();
Logger.logVerbose(LOG_TAG, "The onTermuxTaskExited() callback called for \"" + executionCommand.getCommandIdAndLabelLogString() + "\" TermuxTask command");
// If the execution command was started for a plugin, then process the results
if (executionCommand != null && executionCommand.isPluginExecutionCommand)
PluginUtils.processPluginExecutionCommandResult(this, LOG_TAG, executionCommand);
mTermuxTasks.remove(termuxTask);
}
updateNotification();
});
}
/** Execute a shell command in a foreground {@link TermuxSession}. */
private void executeTermuxSessionCommand(ExecutionCommand executionCommand) {
if (executionCommand == null) return;
if (executionCommand.sessionCreateMode == null)
executionCommand.sessionCreateMode = SessionCreateMode.ALWAYS.getMode();
Logger.logDebug(LOG_TAG, "Executing foreground \"" + executionCommand.getCommandIdAndLabelLogString() + "\" TermuxSession command");
// Transform executable path to session name, e.g. "/bin/do-something.sh" => "do-something.sh".
if (executionCommand.sessionName == null && executionCommand.executable != null) {
executionCommand.sessionName = ShellUtils.getExecutableBasename(executionCommand.executable);
}
TermuxSession newTermuxSession = null;
if (SessionCreateMode.ALWAYS.equalsMode(executionCommand.sessionCreateMode))
; // Default
else if (SessionCreateMode.NO_SESSION_WITH_NAME.equalsMode(executionCommand.sessionCreateMode))
if (DataUtils.isNullOrEmpty(executionCommand.sessionName)) {
String errmsg = getString(R.string.error_termux_service_execution_command_session_name_unset, executionCommand.sessionCreateMode);
executionCommand.setStateFailed(Errno.ERRNO_FAILED.getCode(), errmsg);
PluginUtils.processPluginExecutionCommandError(this, LOG_TAG, executionCommand, false);
return;
} else {
newTermuxSession = getTermuxSessionForName(executionCommand.sessionName);
if (newTermuxSession != null)
Logger.logInfo(LOG_TAG, "Existing session with \"" + executionCommand.sessionName + "\" session name found");
}
else {
String errmsg = getString(R.string.error_termux_service_unsupported_execution_command_session_create_mode, executionCommand.sessionCreateMode);
executionCommand.setStateFailed(Errno.ERRNO_FAILED.getCode(), errmsg);
PluginUtils.processPluginExecutionCommandError(this, LOG_TAG, executionCommand, false);
return;
}
if (newTermuxSession == null)
newTermuxSession = createTermuxSession(executionCommand);
if (newTermuxSession == null) return;
handleSessionAction(DataUtils.getIntFromString(executionCommand.sessionAction,
TERMUX_SERVICE.VALUE_EXTRA_SESSION_ACTION_SWITCH_TO_NEW_SESSION_AND_OPEN_ACTIVITY),
newTermuxSession.getTerminalSession());
}
/**
* Create a {@link TermuxSession}.
* Currently called by {@link TermuxTerminalSessionClient#addNewSession(boolean, String)} to add a new {@link TermuxSession}.
*/
@Nullable
public TermuxSession createTermuxSession(String executablePath, String[] arguments, String stdin, String workingDirectory, boolean isFailSafe, String sessionName) {
ExecutionCommand executionCommand = new ExecutionCommand(getNextExecutionId(), executablePath, arguments, stdin, workingDirectory, Runner.TERMINAL_SESSION.getName(), isFailSafe);
executionCommand.sessionName = sessionName;
return createTermuxSession(executionCommand);
}
/** Create a {@link TermuxSession}. */
@Nullable
public synchronized TermuxSession createTermuxSession(ExecutionCommand executionCommand) {
if (executionCommand == null) return null;
Logger.logDebug(LOG_TAG, "Creating \"" + executionCommand.getCommandIdAndLabelLogString() + "\" TermuxSession");
if (!Runner.TERMINAL_SESSION.equalsRunner(executionCommand.runner)) {
Logger.logDebug(LOG_TAG, "Ignoring wrong runner \"" + executionCommand.runner + "\" command passed to createTermuxSession()");
return null;
}
if (Logger.getLogLevel() >= Logger.LOG_LEVEL_VERBOSE)
Logger.logVerboseExtended(LOG_TAG, executionCommand.toString());
// If the execution command was started for a plugin, only then will the stdout be set
// Otherwise if command was manually started by the user like by adding a new terminal session,
// then no need to set stdout
executionCommand.terminalTranscriptRows = mProperties.getTerminalTranscriptRows();
TermuxSession newTermuxSession = TermuxSession.execute(this, executionCommand, getTermuxTerminalSessionClient(), this, new TermuxShellEnvironmentClient(), executionCommand.isPluginExecutionCommand);
if (newTermuxSession == null) {
Logger.logError(LOG_TAG, "Failed to execute new TermuxSession command for:\n" + executionCommand.getCommandIdAndLabelLogString());
// If the execution command was started for a plugin, then process the error
if (executionCommand.isPluginExecutionCommand)
PluginUtils.processPluginExecutionCommandError(this, LOG_TAG, executionCommand, false);
else
Logger.logErrorExtended(LOG_TAG, executionCommand.toString());
return null;
}
mTermuxSessions.add(newTermuxSession);
// Remove the execution command from the pending plugin execution commands list since it has
// now been processed
if (executionCommand.isPluginExecutionCommand)
mPendingPluginExecutionCommands.remove(executionCommand);
// Notify {@link TermuxSessionsListViewController} that sessions list has been updated if
// activity in is foreground
if (mTermuxTerminalSessionClient != null)
mTermuxTerminalSessionClient.termuxSessionListNotifyUpdated();
updateNotification();
// No need to recreate the activity since it likely just started and theme should already have applied
TermuxActivity.updateTermuxActivityStyling(this, false);
return newTermuxSession;
}
/** Remove a TermuxSession. */
public synchronized int removeTermuxSession(TerminalSession sessionToRemove) {
int index = getIndexOfSession(sessionToRemove);
if (index >= 0)
mTermuxSessions.get(index).finish();
return index;
}
/** Callback received when a {@link TermuxSession} finishes. */
@Override
public void onTermuxSessionExited(final TermuxSession termuxSession) {
if (termuxSession != null) {
ExecutionCommand executionCommand = termuxSession.getExecutionCommand();
Logger.logVerbose(LOG_TAG, "The onTermuxSessionExited() callback called for \"" + executionCommand.getCommandIdAndLabelLogString() + "\" TermuxSession command");
// If the execution command was started for a plugin, then process the results
if (executionCommand != null && executionCommand.isPluginExecutionCommand)
PluginUtils.processPluginExecutionCommandResult(this, LOG_TAG, executionCommand);
mTermuxSessions.remove(termuxSession);
// Notify {@link TermuxSessionsListViewController} that sessions list has been updated if
// activity in is foreground
if (mTermuxTerminalSessionClient != null)
mTermuxTerminalSessionClient.termuxSessionListNotifyUpdated();
}
updateNotification();
}
/** Process session action for new session. */
private void handleSessionAction(int sessionAction, TerminalSession newTerminalSession) {
Logger.logDebug(LOG_TAG, "Processing sessionAction \"" + sessionAction + "\" for session \"" + newTerminalSession.mSessionName + "\"");
switch (sessionAction) {
case TERMUX_SERVICE.VALUE_EXTRA_SESSION_ACTION_SWITCH_TO_NEW_SESSION_AND_OPEN_ACTIVITY:
setCurrentStoredTerminalSession(newTerminalSession);
if (mTermuxTerminalSessionClient != null)
mTermuxTerminalSessionClient.setCurrentSession(newTerminalSession);
startTermuxActivity();
break;
case TERMUX_SERVICE.VALUE_EXTRA_SESSION_ACTION_KEEP_CURRENT_SESSION_AND_OPEN_ACTIVITY:
if (getTermuxSessionsSize() == 1)
setCurrentStoredTerminalSession(newTerminalSession);
startTermuxActivity();
break;
case TERMUX_SERVICE.VALUE_EXTRA_SESSION_ACTION_SWITCH_TO_NEW_SESSION_AND_DONT_OPEN_ACTIVITY:
setCurrentStoredTerminalSession(newTerminalSession);
if (mTermuxTerminalSessionClient != null)
mTermuxTerminalSessionClient.setCurrentSession(newTerminalSession);
break;
case TERMUX_SERVICE.VALUE_EXTRA_SESSION_ACTION_KEEP_CURRENT_SESSION_AND_DONT_OPEN_ACTIVITY:
if (getTermuxSessionsSize() == 1)
setCurrentStoredTerminalSession(newTerminalSession);
break;
default:
Logger.logError(LOG_TAG, "Invalid sessionAction: \"" + sessionAction + "\". Force using default sessionAction.");
handleSessionAction(TERMUX_SERVICE.VALUE_EXTRA_SESSION_ACTION_SWITCH_TO_NEW_SESSION_AND_OPEN_ACTIVITY, newTerminalSession);
break;
}
}
/** Launch the {@link }TermuxActivity} to bring it to foreground. */
private void startTermuxActivity() {
// For android >= 10, apps require Display over other apps permission to start foreground activities
// from background (services). If it is not granted, then TermuxSessions that are started will
// show in Termux notification but will not run until user manually clicks the notification.
if (PermissionUtils.validateDisplayOverOtherAppsPermissionForPostAndroid10(this, true)) {
TermuxActivity.startTermuxActivity(this);
} else {
TermuxAppSharedPreferences preferences = TermuxAppSharedPreferences.build(this);
if (preferences == null) return;
if (preferences.arePluginErrorNotificationsEnabled(false))
Logger.showToast(this, this.getString(R.string.error_display_over_other_apps_permission_not_granted), true);
}
}
/** If {@link TermuxActivity} has not bound to the {@link TermuxService} yet or is destroyed, then
* interface functions requiring the activity should not be available to the terminal sessions,
* so we just return the {@link #mTermuxTerminalSessionClientBase}. Once {@link TermuxActivity} bind
* callback is received, it should call {@link #setTermuxTerminalSessionClient} to set the
* {@link TermuxService#mTermuxTerminalSessionClient} so that further terminal sessions are directly
* passed the {@link TermuxTerminalSessionClient} object which fully implements the
* {@link TerminalSessionClient} interface.
*
* @return Returns the {@link TermuxTerminalSessionClient} if {@link TermuxActivity} has bound with
* {@link TermuxService}, otherwise {@link TermuxTerminalSessionClientBase}.
*/
public synchronized TermuxTerminalSessionClientBase getTermuxTerminalSessionClient() {
if (mTermuxTerminalSessionClient != null)
return mTermuxTerminalSessionClient;
else
return mTermuxTerminalSessionClientBase;
}
/** This should be called when {@link TermuxActivity#onServiceConnected} is called to set the
* {@link TermuxService#mTermuxTerminalSessionClient} variable and update the {@link TerminalSession}
* and {@link TerminalEmulator} clients in case they were passed {@link TermuxTerminalSessionClientBase}
* earlier.
*
* @param termuxTerminalSessionClient The {@link TermuxTerminalSessionClient} object that fully
* implements the {@link TerminalSessionClient} interface.
*/
public synchronized void setTermuxTerminalSessionClient(TermuxTerminalSessionClient termuxTerminalSessionClient) {
mTermuxTerminalSessionClient = termuxTerminalSessionClient;
for (int i = 0; i < mTermuxSessions.size(); i++)
mTermuxSessions.get(i).getTerminalSession().updateTerminalSessionClient(mTermuxTerminalSessionClient);
}
/** This should be called when {@link TermuxActivity} has been destroyed and in {@link #onUnbind(Intent)}
* so that the {@link TermuxService} and {@link TerminalSession} and {@link TerminalEmulator}
* clients do not hold an activity references.
*/
public synchronized void unsetTermuxTerminalSessionClient() {
for (int i = 0; i < mTermuxSessions.size(); i++)
mTermuxSessions.get(i).getTerminalSession().updateTerminalSessionClient(mTermuxTerminalSessionClientBase);
mTermuxTerminalSessionClient = null;
}
private Notification buildNotification() {
Resources res = getResources();
// Set pending intent to be launched when notification is clicked
Intent notificationIntent = TermuxActivity.newInstance(this);
PendingIntent contentIntent = PendingIntent.getActivity(this, 0, notificationIntent, 0);
// Set notification text
int sessionCount = getTermuxSessionsSize();
int taskCount = mTermuxTasks.size();
String notificationText = sessionCount + " session" + (sessionCount == 1 ? "" : "s");
if (taskCount > 0) {
notificationText += ", " + taskCount + " task" + (taskCount == 1 ? "" : "s");
}
final boolean wakeLockHeld = mWakeLock != null;
if (wakeLockHeld) notificationText += " (wake lock held)";
// Set notification priority
// If holding a wake or wifi lock consider the notification of high priority since it's using power,
// otherwise use a low priority
int priority = (wakeLockHeld) ? Notification.PRIORITY_HIGH : Notification.PRIORITY_LOW;
// Build the notification
Notification.Builder builder = NotificationUtils.geNotificationBuilder(this,
TermuxConstants.TERMUX_APP_NOTIFICATION_CHANNEL_ID, priority,
TermuxConstants.TERMUX_APP_NAME, notificationText, null,
contentIntent, null, NotificationUtils.NOTIFICATION_MODE_SILENT);
if (builder == null) return null;
// No need to show a timestamp:
builder.setShowWhen(false);
// Set notification icon
builder.setSmallIcon(R.drawable.ic_service_notification);
// Set background color for small notification icon
builder.setColor(0xFF607D8B);
// TermuxSessions are always ongoing
builder.setOngoing(true);
// Set Exit button action
Intent exitIntent = new Intent(this, TermuxService.class).setAction(TERMUX_SERVICE.ACTION_STOP_SERVICE);
builder.addAction(android.R.drawable.ic_delete, res.getString(R.string.notification_action_exit), PendingIntent.getService(this, 0, exitIntent, 0));
// Set Wakelock button actions
String newWakeAction = wakeLockHeld ? TERMUX_SERVICE.ACTION_WAKE_UNLOCK : TERMUX_SERVICE.ACTION_WAKE_LOCK;
Intent toggleWakeLockIntent = new Intent(this, TermuxService.class).setAction(newWakeAction);
String actionTitle = res.getString(wakeLockHeld ? R.string.notification_action_wake_unlock : R.string.notification_action_wake_lock);
int actionIcon = wakeLockHeld ? android.R.drawable.ic_lock_idle_lock : android.R.drawable.ic_lock_lock;
builder.addAction(actionIcon, actionTitle, PendingIntent.getService(this, 0, toggleWakeLockIntent, 0));
return builder.build();
}
private void setupNotificationChannel() {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) return;
NotificationUtils.setupNotificationChannel(this, TermuxConstants.TERMUX_APP_NOTIFICATION_CHANNEL_ID,
TermuxConstants.TERMUX_APP_NOTIFICATION_CHANNEL_NAME, NotificationManager.IMPORTANCE_LOW);
}
/** Update the shown foreground service notification after making any changes that affect it. */
private synchronized void updateNotification() {
if (mWakeLock == null && mTermuxSessions.isEmpty() && mTermuxTasks.isEmpty()) {
// Exit if we are updating after the user disabled all locks with no sessions or tasks running.
requestStopService();
} else {
((NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE)).notify(TermuxConstants.TERMUX_APP_NOTIFICATION_ID, buildNotification());
}
}
private void setCurrentStoredTerminalSession(TerminalSession terminalSession) {
if (terminalSession == null) return;
// Make the newly created session the current one to be displayed
TermuxAppSharedPreferences preferences = TermuxAppSharedPreferences.build(this);
if (preferences == null) return;
preferences.setCurrentSession(terminalSession.mHandle);
}
public synchronized boolean isTermuxSessionsEmpty() {
return mTermuxSessions.isEmpty();
}
public synchronized int getTermuxSessionsSize() {
return mTermuxSessions.size();
}
public synchronized List<TermuxSession> getTermuxSessions() {
return mTermuxSessions;
}
@Nullable
public synchronized TermuxSession getTermuxSession(int index) {
if (index >= 0 && index < mTermuxSessions.size())
return mTermuxSessions.get(index);
else
return null;
}
@Nullable
public synchronized TermuxSession getTermuxSessionForTerminalSession(TerminalSession terminalSession) {
if (terminalSession == null) return null;
for (int i = 0; i < mTermuxSessions.size(); i++) {
if (mTermuxSessions.get(i).getTerminalSession().equals(terminalSession))
return mTermuxSessions.get(i);
}
return null;
}
public synchronized TermuxSession getLastTermuxSession() {
return mTermuxSessions.isEmpty() ? null : mTermuxSessions.get(mTermuxSessions.size() - 1);
}
public synchronized int getIndexOfSession(TerminalSession terminalSession) {
if (terminalSession == null) return -1;
for (int i = 0; i < mTermuxSessions.size(); i++) {
if (mTermuxSessions.get(i).getTerminalSession().equals(terminalSession))
return i;
}
return -1;
}
public synchronized TerminalSession getTerminalSessionForHandle(String sessionHandle) {
TerminalSession terminalSession;
for (int i = 0, len = mTermuxSessions.size(); i < len; i++) {
terminalSession = mTermuxSessions.get(i).getTerminalSession();
if (terminalSession.mHandle.equals(sessionHandle))
return terminalSession;
}
return null;
}
public synchronized TermuxSession getTermuxSessionForName(String name) {
if (DataUtils.isNullOrEmpty(name)) return null;
TermuxSession termuxSession;
for (int i = 0, len = mTermuxSessions.size(); i < len; i++) {
termuxSession = mTermuxSessions.get(i);
TerminalSession terminalSession = termuxSession.getTerminalSession();
if (terminalSession.mSessionName != null && terminalSession.mSessionName.equals(name))
return termuxSession;
}
return null;
}
public static synchronized int getNextExecutionId() {
return EXECUTION_ID++;
}
public boolean wantsToStop() {
return mWantsToStop;
}
}