Improve TermuxSession and TermuxTask

TermuxSession, TermuxTask and TermuxSessionClientBase have been moved to termux-shared. They are now not dependent on TermuxService anymore and have become abstract so that they can be called from anywhere. TermuxSession.TermuxSessionClient and TermuxTask.TermuxTaskClient interfaces have been created for callbacks, which the TermuxService now implements.

The TermuxTask now also supports synchronous command execution as well to run shell commands from anywhere for internal use by termux app and its plugins.

TermuxTask now also supports killing the process being run by sending it a SIGKILL. This is used by TermuxService to kill all the TermuxTasks (in addition to TermuxSessions) it manages when it is destroyed, either by user exiting it or by android killing it. Only the tasks that were started by a plugin which **expects** the result back via a pending intent will be killed, but the remaining background tasks will keep on running until the termux app process is killed by android, like by OOM. Check TermuxService.killAllTermuxExecutionCommands() for more details on how TermuxService kills TermuxTasks and TermuxSessions.

Fixed null pointer exception when getting terminal transcript if TerminalEmulator not initialized.
This commit is contained in:
agnostic-apollo
2021-04-12 14:26:53 +05:00
parent df4d8ac7e5
commit 0cd7cb8162
17 changed files with 747 additions and 330 deletions

View File

@@ -18,7 +18,7 @@ import com.termux.shared.logger.Logger;
import com.termux.shared.notification.NotificationUtils;
import com.termux.app.utils.PluginUtils;
import com.termux.shared.data.DataUtils;
import com.termux.app.models.ExecutionCommand;
import com.termux.shared.models.ExecutionCommand;
/**
* A service that receives {@link RUN_COMMAND_SERVICE#ACTION_RUN_COMMAND} intent from third party apps and

View File

@@ -20,21 +20,21 @@ import android.provider.Settings;
import android.widget.ArrayAdapter;
import com.termux.R;
import com.termux.app.terminal.TermuxSessionClient;
import com.termux.app.utils.PluginUtils;
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.settings.preferences.TermuxAppSharedPreferences;
import com.termux.app.terminal.TermuxSession;
import com.termux.app.terminal.TermuxSessionClient;
import com.termux.app.terminal.TermuxSessionClientBase;
import com.termux.shared.shell.TermuxSession;
import com.termux.shared.shell.TermuxSessionClientBase;
import com.termux.shared.logger.Logger;
import com.termux.shared.notification.NotificationUtils;
import com.termux.shared.packages.PermissionUtils;
import com.termux.shared.shell.ShellUtils;
import com.termux.shared.data.DataUtils;
import com.termux.app.models.ExecutionCommand;
import com.termux.app.models.ExecutionCommand.ExecutionState;
import com.termux.app.terminal.TermuxTask;
import com.termux.shared.models.ExecutionCommand;
import com.termux.shared.shell.TermuxTask;
import com.termux.terminal.TerminalEmulator;
import com.termux.terminal.TerminalSession;
import com.termux.terminal.TerminalSessionClient;
@@ -45,9 +45,10 @@ import java.util.List;
import javax.annotation.Nullable;
/**
* A service holding a list of termux sessions, {@link #mTermuxSessions}, 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
* A service holding a list of {@link TermuxSession} in {@link #mTermuxSessions} and background {@link TermuxTask}
* 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
@@ -56,7 +57,7 @@ import javax.annotation.Nullable;
* 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 {
public final class TermuxService extends Service implements TermuxTask.TermuxTaskClient, TermuxSession.TermuxSessionClient {
private static int EXECUTION_ID = 1000;
@@ -70,7 +71,7 @@ public final class TermuxService extends Service {
private final Handler mHandler = new Handler();
/**
* The foreground termux sessions which this service manages.
* 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()} }.
@@ -78,15 +79,20 @@ public final class TermuxService extends Service {
final List<TermuxSession> mTermuxSessions = new ArrayList<>();
/**
* The background termux tasks which this service manages.
* The background TermuxTasks which this service manages.
*/
final List<TermuxTask> 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.
*/
TermuxSessionClient mTermuxSessionClient;
com.termux.app.terminal.TermuxSessionClient mTermuxSessionClient;
/** The basic implementation of the {@link TerminalSessionClient} interface to be used by {@link TerminalSession}
* that does not hold activity references.
@@ -154,7 +160,8 @@ public final class TermuxService extends Service {
ShellUtils.clearTermuxTMPDIR(this);
actionReleaseWakeLock(false);
finishAllTermuxSessions();
if (!mWantsToStop)
killAllTermuxExecutionCommands();
runStopForeground();
}
@@ -197,34 +204,77 @@ public final class TermuxService extends Service {
/** Process action to stop service. */
private void actionStopService() {
mWantsToStop = true;
finishAllTermuxSessions();
killAllTermuxExecutionCommands();
requestStopService();
}
/** Finish all termux sessions by sending SIGKILL to their shells. */
private synchronized void finishAllTermuxSessions() {
ExecutionCommand executionCommand;
/** 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;
// TODO: Should SIGKILL also be send to background processes maintained by mTermuxTasks?
for (int i = 0; i < mTermuxSessions.size(); i++) {
TermuxSession termuxSession = mTermuxSessions.get(i);
executionCommand = termuxSession.getExecutionCommand();
Logger.logDebug(LOG_TAG, "Killing TermuxSessions=" + mTermuxSessions.size() + ", TermuxTasks=" + mTermuxTasks.size() + ", PendingPluginExecutionCommands=" + mPendingPluginExecutionCommands.size());
// 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);
List<TermuxSession> termuxSessions = new ArrayList<>(mTermuxSessions);
for (int i = 0; i < termuxSessions.size(); i++) {
ExecutionCommand executionCommand = termuxSessions.get(i).getExecutionCommand();
processResult = mWantsToStop || (executionCommand.isPluginExecutionCommand && executionCommand.pluginPendingIntent != null);
termuxSessions.get(i).killIfExecuting(this, processResult);
}
List<TermuxTask> termuxTasks = new ArrayList<>(mTermuxTasks);
for (int i = 0; i < termuxTasks.size(); i++) {
ExecutionCommand executionCommand = termuxTasks.get(i).getExecutionCommand();
if(executionCommand.isPluginExecutionCommand && executionCommand.pluginPendingIntent != null)
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.pluginPendingIntent != null) {
if (executionCommand.setStateFailed(ExecutionCommand.RESULT_CODE_CANCELED, this.getString(com.termux.shared.R.string.error_execution_cancelled), null)) {
PluginUtils.processPluginExecutionCommandResult(this, LOG_TAG, executionCommand);
}
}
termuxSession.getTerminalSession().finishIfRunning();
}
}
/** Process action to acquire Power and Wi-Fi WakeLocks. */
@SuppressLint({"WakelockTimeout", "BatteryLife"})
private void actionAcquireWakeLock() {
@@ -290,7 +340,7 @@ public final class TermuxService extends Service {
}
/** Process {@link TERMUX_SERVICE#ACTION_SERVICE_EXECUTE} intent to execute a shell command in
* a foreground termux session or in background. */
* 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");
@@ -317,6 +367,9 @@ public final class TermuxService extends Service {
executionCommand.isPluginExecutionCommand = true;
executionCommand.pluginPendingIntent = intent.getParcelableExtra(TERMUX_SERVICE.EXTRA_PENDING_INTENT);
// Add the execution command to pending plugin execution commands list
mPendingPluginExecutionCommands.add(executionCommand);
if (executionCommand.inBackground) {
executeTermuxTaskCommand(executionCommand);
} else {
@@ -332,7 +385,7 @@ public final class TermuxService extends Service {
private void executeTermuxTaskCommand(ExecutionCommand executionCommand) {
if (executionCommand == null) return;
Logger.logDebug(LOG_TAG, "Starting background termux task command");
Logger.logDebug(LOG_TAG, "Executing background \"" + executionCommand.getCommandIdAndLabelLogString() + "\" TermuxTask command");
TermuxTask newTermuxTask = createTermuxTask(executionCommand);
}
@@ -348,7 +401,7 @@ public final class TermuxService extends Service {
public synchronized TermuxTask createTermuxTask(ExecutionCommand executionCommand) {
if (executionCommand == null) return null;
Logger.logDebug(LOG_TAG, "Creating termux task");
Logger.logDebug(LOG_TAG, "Creating \"" + executionCommand.getCommandIdAndLabelLogString() + "\" TermuxTask");
if (!executionCommand.inBackground) {
Logger.logDebug(LOG_TAG, "Ignoring a foreground execution command passed to createTermuxTask()");
@@ -358,23 +411,40 @@ public final class TermuxService extends Service {
if(Logger.getLogLevel() >= Logger.LOG_LEVEL_VERBOSE)
Logger.logVerbose(LOG_TAG, executionCommand.toString());
TermuxTask newTermuxTask = TermuxTask.create(this, executionCommand);
TermuxTask newTermuxTask = TermuxTask.execute(this, executionCommand, this, false);
if (newTermuxTask == null) {
Logger.logError(LOG_TAG, "Failed to execute new termux task command for:\n" + executionCommand.getCommandIdAndLabelLogString());
Logger.logError(LOG_TAG, "Failed to execute new TermuxTask command for:\n" + executionCommand.getCommandIdAndLabelLogString());
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 {@link TermuxTask} finishes. */
public synchronized void onTermuxTaskExited(final TermuxTask task) {
@Override
public void onTermuxTaskExited(final TermuxTask termuxTask) {
mHandler.post(() -> {
mTermuxTasks.remove(task);
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();
});
}
@@ -386,8 +456,8 @@ public final class TermuxService extends Service {
/** Execute a shell command in a foreground {@link TermuxSession}. */
private void executeTermuxSessionCommand(ExecutionCommand executionCommand) {
if (executionCommand == null) return;
Logger.logDebug(LOG_TAG, "Starting foreground termux session command");
Logger.logDebug(LOG_TAG, "Executing foreground \"" + executionCommand.getCommandIdAndLabelLogString() + "\" TermuxSession command");
String sessionName = null;
@@ -406,7 +476,7 @@ public final class TermuxService extends Service {
/**
* Create a {@link TermuxSession}.
* Currently called by {@link TermuxSessionClient#addNewSession(boolean, String)} to add a new termux session.
* Currently called by {@link TermuxSessionClient#addNewSession(boolean, String)} to add a new {@link TermuxSession}.
*/
@Nullable
public TermuxSession createTermuxSession(String executablePath, String[] arguments, String workingDirectory, boolean isFailSafe, String sessionName) {
@@ -418,7 +488,7 @@ public final class TermuxService extends Service {
public synchronized TermuxSession createTermuxSession(ExecutionCommand executionCommand, String sessionName) {
if (executionCommand == null) return null;
Logger.logDebug(LOG_TAG, "Creating termux session");
Logger.logDebug(LOG_TAG, "Creating \"" + executionCommand.getCommandIdAndLabelLogString() + "\" TermuxSession");
if (executionCommand.inBackground) {
Logger.logDebug(LOG_TAG, "Ignoring a background execution command passed to createTermuxSession()");
@@ -427,15 +497,23 @@ public final class TermuxService extends Service {
if(Logger.getLogLevel() >= Logger.LOG_LEVEL_VERBOSE)
Logger.logVerbose(LOG_TAG, executionCommand.toString());
TermuxSession newTermuxSession = TermuxSession.create(this, executionCommand, getTermuxSessionClient(), sessionName);
// 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
TermuxSession newTermuxSession = TermuxSession.execute(this, executionCommand, getTermuxSessionClient(), this, sessionName, executionCommand.isPluginExecutionCommand);
if (newTermuxSession == null) {
Logger.logError(LOG_TAG, "Failed to execute new termux session command for:\n" + executionCommand.getCommandIdAndLabelLogString());
Logger.logError(LOG_TAG, "Failed to execute new TermuxSession command for:\n" + executionCommand.getCommandIdAndLabelLogString());
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(mTermuxSessionClient != null)
@@ -447,20 +525,27 @@ public final class TermuxService extends Service {
return newTermuxSession;
}
/** Remove a termux session. */
/** Remove a TermuxSession. */
public synchronized int removeTermuxSession(TerminalSession sessionToRemove) {
int index = getIndexOfSession(sessionToRemove);
if(index >= 0) {
TermuxSession termuxSession = mTermuxSessions.get(index);
if(index >= 0)
mTermuxSessions.get(index).finish();
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);
}
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);
@@ -471,13 +556,12 @@ public final class TermuxService extends Service {
}
if (mTermuxSessions.isEmpty() && mWakeLock == null) {
// Finish if there are no sessions left and the wake lock is not held, otherwise keep the service alive if
// Finish if there are no TermuxSessions left and the wake lock is not held, otherwise keep the service alive if
// holding wake lock since there may be daemon processes (e.g. sshd) running.
requestStopService();
} else {
updateNotification();
}
return index;
}
@@ -519,7 +603,7 @@ public final class TermuxService extends Service {
/** 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 termux sessions that are started will
// 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)) {
TermuxActivity.startTermuxActivity(this);
@@ -620,7 +704,7 @@ public final class TermuxService extends Service {
// Set background color for small notification icon
builder.setColor(0xFF607D8B);
// Termux sessions are always ongoing
// TermuxSessions are always ongoing
builder.setOngoing(true);
@@ -648,7 +732,7 @@ public final class TermuxService extends Service {
}
/** Update the shown foreground service notification after making any changes that affect it. */
void updateNotification() {
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();

View File

@@ -1,550 +0,0 @@
package com.termux.app.models;
import android.app.Activity;
import android.app.PendingIntent;
import android.net.Uri;
import androidx.annotation.NonNull;
import com.termux.shared.termux.TermuxConstants.TERMUX_APP.TERMUX_SERVICE;
import com.termux.shared.logger.Logger;
import com.termux.shared.markdown.MarkdownUtils;
import com.termux.shared.data.DataUtils;
import java.util.ArrayList;
import java.util.List;
public class ExecutionCommand {
/*
The {@link ExecutionState#SUCCESS} and {@link ExecutionState#FAILED} is defined based on
successful execution of command without any internal errors or exceptions being raised.
The shell command {@link #exitCode} being non-zero **does not** mean that execution command failed.
Only the {@link #errCode} being non-zero means that execution command failed from the Termux app
perspective.
*/
/** The {@link Enum} that defines {@link ExecutionCommand} state. */
public enum ExecutionState {
PRE_EXECUTION("Pre-Execution", 0),
EXECUTING("Executing", 1),
EXECUTED("Executed", 2),
SUCCESS("Success", 3),
FAILED("Failed", 4);
private final String name;
private final int value;
ExecutionState(final String name, final int value) {
this.name = name;
this.value = value;
}
public String getName() {
return name;
}
public int getValue() {
return value;
}
}
// Define errCode values
// TODO: Define custom values for different cases
public final static int RESULT_CODE_OK = Activity.RESULT_OK;
public final static int RESULT_CODE_OK_MINOR_FAILURES = Activity.RESULT_FIRST_USER;
public final static int RESULT_CODE_FAILED = Activity.RESULT_FIRST_USER + 1;
/** The optional unique id for the {@link ExecutionCommand}. */
public Integer id;
/** The current state of the {@link ExecutionCommand}. */
private ExecutionState currentState = ExecutionState.PRE_EXECUTION;
/** The previous state of the {@link ExecutionCommand}. */
private ExecutionState previousState = ExecutionState.PRE_EXECUTION;
/** The executable for the {@link ExecutionCommand}. */
public String executable;
/** The executable Uri for the {@link ExecutionCommand}. */
public Uri executableUri;
/** The executable arguments array for the {@link ExecutionCommand}. */
public String[] arguments;
/** The current working directory for the {@link ExecutionCommand}. */
public String workingDirectory;
/** If the {@link ExecutionCommand} is a background or a foreground terminal session command. */
public boolean inBackground;
/** If the {@link ExecutionCommand} is meant to start a failsafe terminal session. */
public boolean isFailsafe;
/** The session action of foreground commands. */
public String sessionAction;
/** The command label for the {@link ExecutionCommand}. */
public String commandLabel;
/** The markdown text for the command description for the {@link ExecutionCommand}. */
public String commandDescription;
/** The markdown text for the help of command for the {@link ExecutionCommand}. This can be used
* to provide useful info to the user if an internal error is raised. */
public String commandHelp;
/** Defines the markdown text for the help of the Termux plugin API that was used to start the
* {@link ExecutionCommand}. This can be used to provide useful info to the user if an internal
* error is raised. */
public String pluginAPIHelp;
/** 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;
/** The stdout of shell command. */
public String stdout;
/** The stderr of shell command. */
public String stderr;
/** The exit code of shell command. */
public Integer exitCode;
/** The internal error code of {@link ExecutionCommand}. */
public Integer errCode = RESULT_CODE_OK;
/** The internal error message of {@link ExecutionCommand}. */
public String errmsg;
/** The internal exceptions of {@link ExecutionCommand}. */
public List<Throwable> throwableList = new ArrayList<>();
public ExecutionCommand(){
}
public ExecutionCommand(Integer id){
this.id = id;
}
public ExecutionCommand(Integer id, String executable, String[] arguments, String workingDirectory, boolean inBackground, boolean isFailsafe) {
this.id = id;
this.executable = executable;
this.arguments = arguments;
this.workingDirectory = workingDirectory;
this.inBackground = inBackground;
this.isFailsafe = isFailsafe;
}
@NonNull
@Override
public String toString() {
if (!hasExecuted())
return getExecutionInputLogString(this, true);
else {
return getExecutionOutputLogString(this, true);
}
}
/**
* Get a log friendly {@link String} for {@link ExecutionCommand} execution input parameters.
*
* @param executionCommand The {@link ExecutionCommand} to convert.
* @param ignoreNull Set to {@code true} if non-critical {@code null} values are to be ignored.
* @return Returns the log friendly {@link String}.
*/
public static String getExecutionInputLogString(final ExecutionCommand executionCommand, boolean ignoreNull) {
if (executionCommand == null) return "null";
StringBuilder logString = new StringBuilder();
logString.append(executionCommand.getCommandIdAndLabelLogString()).append(":");
if (executionCommand.previousState != ExecutionState.PRE_EXECUTION)
logString.append("\n").append(executionCommand.getPreviousStateLogString());
logString.append("\n").append(executionCommand.getCurrentStateLogString());
logString.append("\n").append(executionCommand.getExecutableLogString());
logString.append("\n").append(executionCommand.getArgumentsLogString());
logString.append("\n").append(executionCommand.getWorkingDirectoryLogString());
logString.append("\n").append(executionCommand.getInBackgroundLogString());
logString.append("\n").append(executionCommand.getIsFailsafeLogString());
if (!ignoreNull || executionCommand.sessionAction != null)
logString.append("\n").append(executionCommand.getSessionActionLogString());
logString.append("\n").append(executionCommand.getIsPluginExecutionCommandLogString());
if (!ignoreNull || executionCommand.isPluginExecutionCommand) {
if (!ignoreNull || executionCommand.pluginPendingIntent != null)
logString.append("\n").append(executionCommand.getPendingIntentCreatorLogString());
}
return logString.toString();
}
/**
* Get a log friendly {@link String} for {@link ExecutionCommand} execution output parameters.
*
* @param executionCommand The {@link ExecutionCommand} to convert.
* @param ignoreNull Set to {@code true} if non-critical {@code null} values are to be ignored.
* @return Returns the log friendly {@link String}.
*/
public static String getExecutionOutputLogString(final ExecutionCommand executionCommand, boolean ignoreNull) {
if (executionCommand == null) return "null";
StringBuilder logString = new StringBuilder();
logString.append(executionCommand.getCommandIdAndLabelLogString()).append(":");
logString.append("\n").append(executionCommand.getPreviousStateLogString());
logString.append("\n").append(executionCommand.getCurrentStateLogString());
logString.append("\n").append(executionCommand.getStdoutLogString());
logString.append("\n").append(executionCommand.getStderrLogString());
logString.append("\n").append(executionCommand.getExitCodeLogString());
logString.append(getExecutionErrLogString(executionCommand, ignoreNull));
return logString.toString();
}
/**
* Get a log friendly {@link String} for {@link ExecutionCommand} execution error parameters.
*
* @param executionCommand The {@link ExecutionCommand} to convert.
* @param ignoreNull Set to {@code true} if non-critical {@code null} values are to be ignored.
* @return Returns the log friendly {@link String}.
*/
public static String getExecutionErrLogString(final ExecutionCommand executionCommand, boolean ignoreNull) {
StringBuilder logString = new StringBuilder();
if (!ignoreNull || (executionCommand.isStateFailed())) {
logString.append("\n").append(executionCommand.getErrCodeLogString());
logString.append("\n").append(executionCommand.getErrmsgLogString());
logString.append("\n").append(executionCommand.geStackTracesLogString());
} else {
logString.append("");
}
return logString.toString();
}
/**
* Get a log friendly {@link String} for {@link ExecutionCommand} with more details.
*
* @param executionCommand The {@link ExecutionCommand} to convert.
* @return Returns the log friendly {@link String}.
*/
public static String getDetailedLogString(final ExecutionCommand executionCommand) {
if (executionCommand == null) return "null";
StringBuilder logString = new StringBuilder();
logString.append(getExecutionInputLogString(executionCommand, false));
logString.append(getExecutionOutputLogString(executionCommand, false));
logString.append("\n").append(executionCommand.getCommandDescriptionLogString());
logString.append("\n").append(executionCommand.getCommandHelpLogString());
logString.append("\n").append(executionCommand.getPluginAPIHelpLogString());
return logString.toString();
}
/**
* Get a markdown {@link String} for {@link ExecutionCommand}.
*
* @param executionCommand The {@link ExecutionCommand} to convert.
* @return Returns the markdown {@link String}.
*/
public static String getExecutionCommandMarkdownString(final ExecutionCommand executionCommand) {
if (executionCommand == null) return "null";
if (executionCommand.commandLabel == null) executionCommand.commandLabel = "Execution Command";
StringBuilder markdownString = new StringBuilder();
markdownString.append("## ").append(executionCommand.commandLabel).append("\n");
markdownString.append("\n").append(MarkdownUtils.getSingleLineMarkdownStringEntry("Previous State", executionCommand.previousState.getName(), "-"));
markdownString.append("\n").append(MarkdownUtils.getSingleLineMarkdownStringEntry("Current State", executionCommand.currentState.getName(), "-"));
markdownString.append("\n").append(MarkdownUtils.getSingleLineMarkdownStringEntry("Executable", executionCommand.executable, "-"));
markdownString.append("\n").append(getArgumentsMarkdownString(executionCommand.arguments));
markdownString.append("\n").append(MarkdownUtils.getSingleLineMarkdownStringEntry("Working Directory", executionCommand.workingDirectory, "-"));
markdownString.append("\n").append(MarkdownUtils.getSingleLineMarkdownStringEntry("inBackground", executionCommand.inBackground, "-"));
markdownString.append("\n").append(MarkdownUtils.getSingleLineMarkdownStringEntry("isFailsafe", executionCommand.isFailsafe, "-"));
markdownString.append("\n").append(MarkdownUtils.getSingleLineMarkdownStringEntry("Session Action", executionCommand.sessionAction, "-"));
markdownString.append("\n").append(MarkdownUtils.getSingleLineMarkdownStringEntry("isPluginExecutionCommand", executionCommand.isPluginExecutionCommand, "-"));
if (executionCommand.pluginPendingIntent != null)
markdownString.append("\n").append(MarkdownUtils.getSingleLineMarkdownStringEntry("Pending Intent Creator", executionCommand.pluginPendingIntent.getCreatorPackage(), "-"));
else
markdownString.append("\n").append("**Pending Intent Creator:** - ");
markdownString.append("\n\n").append(MarkdownUtils.getMultiLineMarkdownStringEntry("Stdout", executionCommand.stdout, "-"));
markdownString.append("\n").append(MarkdownUtils.getMultiLineMarkdownStringEntry("Stderr", executionCommand.stderr, "-"));
markdownString.append("\n").append(MarkdownUtils.getSingleLineMarkdownStringEntry("Exit Code", executionCommand.exitCode, "-"));
markdownString.append("\n\n").append(MarkdownUtils.getSingleLineMarkdownStringEntry("Err Code", executionCommand.errCode, "-"));
markdownString.append("\n").append("**Errmsg:**\n").append(DataUtils.getDefaultIfNull(executionCommand.errmsg, "-"));
markdownString.append("\n\n").append(executionCommand.geStackTracesMarkdownString());
if (executionCommand.commandDescription != null || executionCommand.commandHelp != null) {
if (executionCommand.commandDescription != null)
markdownString.append("\n\n### Command Description\n\n").append(executionCommand.commandDescription).append("\n");
if (executionCommand.commandHelp != null)
markdownString.append("\n\n### Command Help\n\n").append(executionCommand.commandHelp).append("\n");
markdownString.append("\n##\n");
}
if (executionCommand.pluginAPIHelp != null) {
markdownString.append("\n\n### Plugin API Help\n\n").append(executionCommand.pluginAPIHelp);
markdownString.append("\n##\n");
}
return markdownString.toString();
}
public String getIdLogString() {
if (id != null)
return "(" + id + ") ";
else
return "";
}
public String getCurrentStateLogString() {
return "Current State: `" + currentState.getName() + "`";
}
public String getPreviousStateLogString() {
return "Previous State: `" + previousState.getName() + "`";
}
public String getCommandLabelLogString() {
if (commandLabel != null && !commandLabel.isEmpty())
return commandLabel;
else
return "Execution Command";
}
public String getCommandIdAndLabelLogString() {
return getIdLogString() + getCommandLabelLogString();
}
public String getExecutableLogString() {
return "Executable: `" + executable + "`";
}
public String getArgumentsLogString() {
return getArgumentsLogString(arguments);
}
public String getWorkingDirectoryLogString() {
return "Working Directory: `" + workingDirectory + "`";
}
public String getInBackgroundLogString() {
return "inBackground: `" + inBackground + "`";
}
public String getIsFailsafeLogString() {
return "isFailsafe: `" + isFailsafe + "`";
}
public String getIsPluginExecutionCommandLogString() {
return "isPluginExecutionCommand: `" + isPluginExecutionCommand + "`";
}
public String getSessionActionLogString() {
return Logger.getSingleLineLogStringEntry("Session Action", sessionAction, "-");
}
public String getPendingIntentCreatorLogString() {
if (pluginPendingIntent != null)
return "Pending Intent Creator: `" + pluginPendingIntent.getCreatorPackage() + "`";
else
return "Pending Intent Creator: -";
}
public String getCommandDescriptionLogString() {
return Logger.getSingleLineLogStringEntry("Command Description", commandDescription, "-");
}
public String getCommandHelpLogString() {
return Logger.getSingleLineLogStringEntry("Command Help", commandHelp, "-");
}
public String getPluginAPIHelpLogString() {
return Logger.getSingleLineLogStringEntry("Plugin API Help", pluginAPIHelp, "-");
}
public String getStdoutLogString() {
return Logger.getMultiLineLogStringEntry("Stdout", DataUtils.getTruncatedCommandOutput(stdout, Logger.LOGGER_ENTRY_SIZE_LIMIT_IN_BYTES / 5, false, false, true), "-");
}
public String getStderrLogString() {
return Logger.getMultiLineLogStringEntry("Stderr", DataUtils.getTruncatedCommandOutput(stderr, Logger.LOGGER_ENTRY_SIZE_LIMIT_IN_BYTES / 5, false, false, true), "-");
}
public String getExitCodeLogString() {
return Logger.getSingleLineLogStringEntry("Exit Code", exitCode, "-");
}
public String getErrCodeLogString() {
return Logger.getSingleLineLogStringEntry("Err Code", errCode, "-");
}
public String getErrmsgLogString() {
return Logger.getMultiLineLogStringEntry("Errmsg", errmsg, "-");
}
public String geStackTracesLogString() {
return Logger.getStackTracesString("StackTraces:", Logger.getStackTraceStringArray(throwableList));
}
public String geStackTracesMarkdownString() {
return Logger.getStackTracesMarkdownString("StackTraces", Logger.getStackTraceStringArray(throwableList));
}
/**
* Get a markdown {@link String} for {@link String[]} argumentsArray.
* If argumentsArray are null or of size 0, then `**Arguments:** -` is returned. Otherwise
* following format is returned:
*
* **Arguments:**
*
* **Arg 1:**
* ```
* value
* ```
* **Arg 2:**
* ```
* value
*```
*
* @param argumentsArray The {@link String[]} argumentsArray to convert.
* @return Returns the markdown {@link String}.
*/
public static String getArgumentsMarkdownString(final String[] argumentsArray) {
StringBuilder argumentsString = new StringBuilder("**Arguments:**");
if (argumentsArray != null && argumentsArray.length != 0) {
argumentsString.append("\n");
for (int i = 0; i != argumentsArray.length; i++) {
argumentsString.append(MarkdownUtils.getMultiLineMarkdownStringEntry("Arg " + (i + 1), argumentsArray[i], "-")).append("\n");
}
} else{
argumentsString.append(" - ");
}
return argumentsString.toString();
}
/**
* Get a log friendly {@link String} for {@link List<String>} argumentsArray.
* If argumentsArray are null or of size 0, then `Arguments: -` is returned. Otherwise
* following format is returned:
*
* Arguments:
* ```
* Arg 1: `value`
* Arg 2: 'value`
* ```
*
* @param argumentsArray The {@link String[]} argumentsArray to convert.
* @return Returns the log friendly {@link String}.
*/
public static String getArgumentsLogString(final String[] argumentsArray) {
StringBuilder argumentsString = new StringBuilder("Arguments:");
if (argumentsArray != null && argumentsArray.length != 0) {
argumentsString.append("\n```\n");
for (int i = 0; i != argumentsArray.length; i++) {
argumentsString.append(Logger.getSingleLineLogStringEntry("Arg " + (i + 1),
DataUtils.getTruncatedCommandOutput(argumentsArray[i], Logger.LOGGER_ENTRY_SIZE_LIMIT_IN_BYTES / 5, true, false, true),
"-")).append("`\n");
}
argumentsString.append("```");
} else{
argumentsString.append(" -");
}
return argumentsString.toString();
}
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() + "\"");
return false;
}
// The {@link ExecutionState#FAILED} can be set again, like to add more errors, but we don't update
// {@link #previousState} with the {@link #currentState} value if its at {@link ExecutionState#FAILED} to
// preserve the last valid state
if (currentState != ExecutionState.FAILED)
previousState = currentState;
currentState = newState;
return true;
}
public synchronized boolean setStateFailed(int errCode, String errmsg, Throwable throwable) {
if (errCode > RESULT_CODE_OK) {
this.errCode = errCode;
} else {
Logger.logWarn("Ignoring invalid " + getCommandIdAndLabelLogString() + " errCode value \"" + errCode + "\". Force setting it to RESULT_CODE_FAILED \"" + RESULT_CODE_FAILED + "\"");
this.errCode = RESULT_CODE_FAILED;
}
this.errmsg = errmsg;
if (!setState(ExecutionState.FAILED))
return false;
if (this.throwableList == null)
this.throwableList = new ArrayList<>();
if (throwable != null)
this.throwableList.add(throwable);
return true;
}
public synchronized boolean isStateFailed() {
if (currentState != ExecutionState.FAILED)
return false;
if (errCode <= RESULT_CODE_OK) {
Logger.logWarn("The " + getCommandIdAndLabelLogString() + " has an invalid errCode value \"" + errCode + "\" while having ExecutionState.FAILED state.");
return false;
} else {
return true;
}
}
public synchronized boolean hasExecuted() {
return currentState.getValue() >= ExecutionState.EXECUTED.getValue();
}
public synchronized boolean isExecuting() {
return currentState == ExecutionState.EXECUTING;
}
}

View File

@@ -1,116 +0,0 @@
package com.termux.app.terminal;
import androidx.annotation.NonNull;
import com.termux.R;
import com.termux.shared.termux.TermuxConstants;
import com.termux.app.TermuxService;
import com.termux.shared.logger.Logger;
import com.termux.app.utils.PluginUtils;
import com.termux.shared.shell.ShellUtils;
import com.termux.app.models.ExecutionCommand;
import com.termux.terminal.TerminalSession;
import java.io.File;
/**
* A class that maintains info for foreground Termux sessions.
* It also provides a way to link each {@link TerminalSession} with the {@link ExecutionCommand}
* that started it.
*/
public class TermuxSession {
private final TerminalSession mTerminalSession;
private final ExecutionCommand mExecutionCommand;
private static final String LOG_TAG = "TermuxSession";
private TermuxSession(TerminalSession terminalSession, ExecutionCommand executionCommand) {
this.mTerminalSession = terminalSession;
this.mExecutionCommand = executionCommand;
}
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(service, executionCommand.isFailsafe, executionCommand.workingDirectory);
boolean isLoginShell = false;
if (executionCommand.executable == null) {
if (!executionCommand.isFailsafe) {
for (String shellBinary : new String[]{"login", "bash", "zsh"}) {
File shellFile = new File(TermuxConstants.TERMUX_BIN_PREFIX_DIR_PATH, shellBinary);
if (shellFile.canExecute()) {
executionCommand.executable = shellFile.getAbsolutePath();
break;
}
}
}
if (executionCommand.executable == null) {
// Fall back to system shell as last resort:
executionCommand.executable = "/system/bin/sh";
}
isLoginShell = true;
}
String[] processArgs = ShellUtils.setupProcessArgs(executionCommand.executable, executionCommand.arguments);
executionCommand.executable = processArgs[0];
String processName = (isLoginShell ? "-" : "") + ShellUtils.getExecutableBasename(executionCommand.executable);
String[] arguments = new String[processArgs.length];
arguments[0] = processName;
if (processArgs.length > 1) System.arraycopy(processArgs, 1, arguments, 1, processArgs.length - 1);
executionCommand.arguments = arguments;
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());
TerminalSession terminalSession = new TerminalSession(executionCommand.executable, executionCommand.workingDirectory, executionCommand.arguments, environment, termuxSessionClient);
if (sessionName != null) {
terminalSession.mSessionName = sessionName;
}
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;
// Must be a normal command like foreground terminal session started by user, so just 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;
}
public ExecutionCommand getExecutionCommand() {
return mExecutionCommand;
}
}

View File

@@ -13,8 +13,10 @@ import android.text.TextUtils;
import android.widget.ListView;
import com.termux.R;
import com.termux.shared.shell.TermuxSession;
import com.termux.shared.interact.DialogUtils;
import com.termux.app.TermuxActivity;
import com.termux.shared.shell.TermuxSessionClientBase;
import com.termux.shared.termux.TermuxConstants;
import com.termux.app.TermuxService;
import com.termux.shared.settings.properties.TermuxPropertyConstants;
@@ -255,6 +257,7 @@ public class TermuxSessionClient extends TermuxSessionClientBase {
TermuxService service = mActivity.getTermuxService();
int index = service.removeTermuxSession(finishedSession);
int size = mActivity.getTermuxService().getTermuxSessionsSize();
if (size == 0) {
// There are no sessions to show, so finish the activity.

View File

@@ -1,71 +0,0 @@
package com.termux.app.terminal;
import com.termux.shared.logger.Logger;
import com.termux.terminal.TerminalSession;
import com.termux.terminal.TerminalSessionClient;
public class TermuxSessionClientBase implements TerminalSessionClient {
public TermuxSessionClientBase() {
}
@Override
public void onTextChanged(TerminalSession changedSession) {
}
@Override
public void onTitleChanged(TerminalSession updatedSession) {
}
@Override
public void onSessionFinished(final TerminalSession finishedSession) {
}
@Override
public void onClipboardText(TerminalSession session, String text) {
}
@Override
public void onBell(TerminalSession session) {
}
@Override
public void onColorsChanged(TerminalSession changedSession) {
}
@Override
public void logError(String tag, String message) {
Logger.logError(tag, message);
}
@Override
public void logWarn(String tag, String message) {
Logger.logWarn(tag, message);
}
@Override
public void logInfo(String tag, String message) {
Logger.logInfo(tag, message);
}
@Override
public void logDebug(String tag, String message) {
Logger.logDebug(tag, message);
}
@Override
public void logVerbose(String tag, String message) {
Logger.logVerbose(tag, message);
}
@Override
public void logStackTraceWithMessage(String tag, String message, Exception e) {
Logger.logStackTraceWithMessage(tag, message, e);
}
@Override
public void logStackTrace(String tag, Exception e) {
Logger.logStackTrace(tag, e);
}
}

View File

@@ -20,6 +20,7 @@ import androidx.core.content.ContextCompat;
import com.termux.R;
import com.termux.app.TermuxActivity;
import com.termux.shared.shell.TermuxSession;
import com.termux.terminal.TerminalSession;
import java.util.List;

View File

@@ -1,142 +0,0 @@
package com.termux.app.terminal;
import androidx.annotation.NonNull;
import com.termux.R;
import com.termux.shared.termux.TermuxConstants;
import com.termux.app.TermuxService;
import com.termux.shared.shell.StreamGobbler;
import com.termux.shared.logger.Logger;
import com.termux.app.utils.PluginUtils;
import com.termux.shared.shell.ShellUtils;
import com.termux.app.models.ExecutionCommand;
import com.termux.app.models.ExecutionCommand.ExecutionState;
import java.io.File;
import java.io.IOException;
/**
* A class that maintains info for background Termux tasks.
* It also provides a way to link each {@link Process} with the {@link ExecutionCommand}
* that started it.
*/
public final class TermuxTask {
private final Process mProcess;
private final ExecutionCommand mExecutionCommand;
private static final String LOG_TAG = "TermuxTask";
private TermuxTask(Process process, ExecutionCommand executionCommand) {
this.mProcess = process;
this.mExecutionCommand = executionCommand;
}
public static TermuxTask create(@NonNull final TermuxService service, @NonNull ExecutionCommand executionCommand) {
if (executionCommand.workingDirectory == null || executionCommand.workingDirectory.isEmpty()) executionCommand.workingDirectory = TermuxConstants.TERMUX_HOME_DIR_PATH;
String[] env = ShellUtils.buildEnvironment(service, false, executionCommand.workingDirectory);
final String[] commandArray = ShellUtils.setupProcessArgs(executionCommand.executable, executionCommand.arguments);
// final String commandDescription = Arrays.toString(commandArray);
if(!executionCommand.setState(ExecutionState.EXECUTING))
return null;
Logger.logDebug(LOG_TAG, executionCommand.toString());
String taskName = ShellUtils.getExecutableBasename(executionCommand.executable);
if(executionCommand.commandLabel == null)
executionCommand.commandLabel = taskName;
// Exec the process
final Process process;
try {
process = Runtime.getRuntime().exec(commandArray, env, new File(executionCommand.workingDirectory));
} catch (IOException e) {
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;
}
final int pid = ShellUtils.getPid(process);
Logger.logDebug(LOG_TAG, "Running \"" + executionCommand.commandLabel + "\" background task with pid " + pid);
final TermuxTask termuxTask = new TermuxTask(process, executionCommand);
StringBuilder stdout = new StringBuilder();
StringBuilder stderr = new StringBuilder();
new Thread() {
@Override
public void run() {
try {
// setup stdout and stderr gobblers
StreamGobbler STDOUT = new StreamGobbler(pid + "-stdout", process.getInputStream(), stdout);
StreamGobbler STDERR = new StreamGobbler(pid + "-stderr", process.getErrorStream(), stderr);
// start gobbling
STDOUT.start();
STDERR.start();
// wait for our process to finish, while we gobble away in the
// background
int exitCode = process.waitFor();
// make sure our threads are done gobbling
// and the process is destroyed - while the latter shouldn't be
// needed in theory, and may even produce warnings, in "normal" Java
// they are required for guaranteed cleanup of resources, so lets be
// safe and do this on Android as well
STDOUT.join();
STDERR.join();
process.destroy();
// Process result
if (exitCode == 0)
Logger.logDebug(LOG_TAG, "The \"" + executionCommand.commandLabel + "\" background task with pid " + pid + " exited normally");
else
Logger.logDebug(LOG_TAG, "The \"" + executionCommand.commandLabel + "\" background task with pid " + pid + " exited with code: " + exitCode);
executionCommand.stdout = stdout.toString();
executionCommand.stderr = stderr.toString();
executionCommand.exitCode = exitCode;
if(!executionCommand.setState(ExecutionState.EXECUTED))
return;
TermuxTask.processTermuxTaskResult(service, termuxTask, null);
} catch (IllegalThreadStateException | InterruptedException e) {
// TODO: Should either of these be handled or returned?
}
}
}.start();
return termuxTask;
}
public static void processTermuxTaskResult(@NonNull final TermuxService service, final TermuxTask termuxTask, ExecutionCommand executionCommand) {
if(termuxTask != null)
executionCommand = termuxTask.mExecutionCommand;
if(executionCommand == null) return;
PluginUtils.processPluginExecutionCommandResult(service.getApplicationContext(), LOG_TAG, executionCommand);
if(termuxTask != null)
service.onTermuxTaskExited(termuxTask);
}
public Process getTerminalSession() {
return mProcess;
}
public ExecutionCommand getExecutionCommand() {
return mExecutionCommand;
}
}

View File

@@ -20,6 +20,7 @@ import android.widget.Toast;
import com.termux.R;
import com.termux.app.TermuxActivity;
import com.termux.shared.shell.ShellUtils;
import com.termux.shared.termux.TermuxConstants;
import com.termux.app.activities.ReportActivity;
import com.termux.app.models.ReportInfo;
@@ -341,7 +342,7 @@ public class TermuxViewClient implements TerminalViewClient {
TerminalSession session = mActivity.getCurrentSession();
if (session == null) return;
String transcriptText = session.getEmulator().getScreen().getTranscriptTextWithoutJoinedLines().trim();
String transcriptText = ShellUtils.getTerminalSessionTranscriptText(session, false, true);
if (transcriptText == null) return;
try {
@@ -361,7 +362,7 @@ public class TermuxViewClient implements TerminalViewClient {
TerminalSession session = mActivity.getCurrentSession();
if (session == null) return;
String text = session.getEmulator().getScreen().getTranscriptTextWithFullLinesJoined();
String text = ShellUtils.getTerminalSessionTranscriptText(session, true, true);
LinkedHashSet<CharSequence> urlSet = DataUtils.extractUrls(text);
if (urlSet.isEmpty()) {
@@ -404,7 +405,7 @@ public class TermuxViewClient implements TerminalViewClient {
TerminalSession session = mActivity.getCurrentSession();
if (session == null) return;
String transcriptText = session.getEmulator().getScreen().getTranscriptTextWithoutJoinedLines().trim();
String transcriptText = ShellUtils.getTerminalSessionTranscriptText(session, false, true);
if (transcriptText == null) return;
transcriptText = DataUtils.getTruncatedCommandOutput(transcriptText, DataUtils.TRANSACTION_SIZE_LIMIT_IN_BYTES, false, true, false).trim();

View File

@@ -21,7 +21,7 @@ import com.termux.shared.settings.preferences.TermuxPreferenceConstants.TERMUX_A
import com.termux.shared.settings.properties.SharedProperties;
import com.termux.shared.settings.properties.TermuxPropertyConstants;
import com.termux.app.models.ReportInfo;
import com.termux.app.models.ExecutionCommand;
import com.termux.shared.models.ExecutionCommand;
import com.termux.app.models.UserAction;
import com.termux.shared.data.DataUtils;
import com.termux.shared.markdown.MarkdownUtils;
@@ -56,7 +56,7 @@ public class PluginUtils {
logTag = DataUtils.getDefaultIfNull(logTag, LOG_TAG);
if(!executionCommand.hasExecuted()) {
Logger.logWarn(logTag, "Ignoring call to processPluginExecutionCommandResult() since the execution command has not been ExecutionState.EXECUTED");
Logger.logWarn(logTag, "Ignoring call to processPluginExecutionCommandResult() since the execution command state is not higher than the ExecutionState.EXECUTED");
return;
}
@@ -114,7 +114,7 @@ public class PluginUtils {
logTag = DataUtils.getDefaultIfNull(logTag, LOG_TAG);
if(!executionCommand.isStateFailed()) {
Logger.logWarn(logTag, "Ignoring call to processPluginExecutionCommandError() since the execution command does not have ExecutionState.FAILED state");
Logger.logWarn(logTag, "Ignoring call to processPluginExecutionCommandError() since the execution command is not in ExecutionState.FAILED");
return;
}
@@ -255,6 +255,7 @@ public class PluginUtils {
pluginPendingIntent.send(context, Activity.RESULT_OK, resultIntent);
} catch (PendingIntent.CanceledException e) {
// The caller doesn't want the result? That's fine, just ignore
Logger.logDebug(logTag, "The Execution Command \"" + label + "\" creator " + pluginPendingIntent.getCreatorPackage() + " does not want the results anymore");
}
return true;

View File

@@ -100,9 +100,6 @@
<!-- Termux Execution Commands -->
<string name="msg_executable_absolute_path">Executable Absolute Path: \"%1$s\"</string>
<string name="msg_working_directory_absolute_path">Working Directory Absolute Path: \"%1$s\"</string>
<string name="error_sending_sigkill_to_process">Sending SIGKILL to process on user request or because android is killing the service</string>
<string name="error_failed_to_execute_termux_session_command">"Failed to execute \"%1$s\" termux session command"</string>
<string name="error_failed_to_execute_termux_task_command">"Failed to execute \"%1$s\" termux task command"</string>