diff --git a/app/src/main/java/com/termux/app/TermuxService.java b/app/src/main/java/com/termux/app/TermuxService.java index 379bcdafc6..35058c133b 100644 --- a/app/src/main/java/com/termux/app/TermuxService.java +++ b/app/src/main/java/com/termux/app/TermuxService.java @@ -32,6 +32,7 @@ import com.termux.app.utils.TextDataUtils; import com.termux.models.ExecutionCommand; import com.termux.models.ExecutionCommand.ExecutionState; +import com.termux.app.terminal.TermuxTask; import com.termux.terminal.TerminalEmulator; import com.termux.terminal.TerminalSession; import com.termux.terminal.TerminalSessionClient; @@ -71,7 +72,7 @@ class LocalBinder extends Binder { private final Handler mHandler = new Handler(); /** - * The termux sessions which this service manages. + * The foreground termux sessions 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()} }. @@ -79,9 +80,9 @@ class LocalBinder extends Binder { final List mTermuxSessions = new ArrayList<>(); /** - * The background jobs which this service manages. + * The background termux tasks which this service manages. */ - final List mBackgroundTasks = new ArrayList<>(); + final List mTermuxTasks = new ArrayList<>(); /** The full implementation of the {@link TerminalSessionClient} interface to be used by {@link TerminalSession} * that holds activity references for activity related functions. @@ -204,8 +205,6 @@ private void requestStopService() { stopSelf(); } - - /** Process action to stop service. */ private void actionStopService() { mWantsToStop = true; @@ -213,6 +212,17 @@ private void actionStopService() { requestStopService(); } + /** Finish all termux sessions by sending SIGKILL to their shells. */ + private synchronized void finishAllTermuxSessions() { + // TODO: Should SIGKILL also be send to background processes maintained by mTermuxTasks? + for (int i = 0; i < mTermuxSessions.size(); i++) + mTermuxSessions.get(i).getTerminalSession().finishIfRunning(); + } + + + + + /** Process action to acquire Power and Wi-Fi WakeLocks. */ @SuppressLint({"WakelockTimeout", "BatteryLife"}) private void actionAcquireWakeLock() { @@ -306,36 +316,72 @@ private void actionServiceExecute(Intent intent) { executionCommand.pluginPendingIntent = intent.getParcelableExtra(TERMUX_SERVICE.EXTRA_PENDING_INTENT); if (executionCommand.inBackground) { - executeBackgroundCommand(executionCommand); + executeTermuxTaskCommand(executionCommand); } else { executeTermuxSessionCommand(executionCommand); } } - /** Execute a shell command in background with {@link BackgroundJob}. */ - private void executeBackgroundCommand(ExecutionCommand executionCommand) { + + + + + /** Execute a shell command in background {@link TermuxTask}. */ + private void executeTermuxTaskCommand(ExecutionCommand executionCommand) { if (executionCommand == null) return; - - Logger.logDebug(LOG_TAG, "Starting background command"); + + Logger.logDebug(LOG_TAG, "Starting background termux task command"); + + TermuxTask newTermuxTask = createTermuxTask(executionCommand); + } + + /** Create a {@link TermuxTask}. */ + @Nullable + public TermuxTask createTermuxTask(String executablePath, String[] arguments, String workingDirectory) { + return createTermuxTask(new ExecutionCommand(getNextExecutionId(), executablePath, arguments, workingDirectory, true, false)); + } + + /** Create a {@link TermuxTask}. */ + @Nullable + public synchronized TermuxTask createTermuxTask(ExecutionCommand executionCommand) { + if (executionCommand == null) return null; + + Logger.logDebug(LOG_TAG, "Creating termux task"); + + if (!executionCommand.inBackground) { + Logger.logDebug(LOG_TAG, "Ignoring a foreground execution command passed to createTermuxTask()"); + return null; + } if(Logger.getLogLevel() >= Logger.LOG_LEVEL_VERBOSE) Logger.logVerbose(LOG_TAG, executionCommand.toString()); - BackgroundJob task = new BackgroundJob(executionCommand, this); + TermuxTask newTermuxTask = TermuxTask.create(this, executionCommand); + if (newTermuxTask == null) { + // Logger.logError(LOG_TAG, "Failed to execute new termux task command for:\n" + executionCommand.toString()); + return null; + }; + + mTermuxTasks.add(newTermuxTask); - mBackgroundTasks.add(task); updateNotification(); + + return newTermuxTask; } - /** Callback received when a {@link BackgroundJob} finishes. */ - public void onBackgroundJobExited(final BackgroundJob task) { + /** Callback received when a {@link TermuxTask} finishes. */ + public synchronized void onTermuxTaskExited(final TermuxTask task) { mHandler.post(() -> { - mBackgroundTasks.remove(task); + mTermuxTasks.remove(task); updateNotification(); }); } - /** Execute a shell command in a foreground terminal session. */ + + + + + /** Execute a shell command in a foreground {@link TermuxSession}. */ private void executeTermuxSessionCommand(ExecutionCommand executionCommand) { if (executionCommand == null) return; @@ -357,7 +403,7 @@ private void executeTermuxSessionCommand(ExecutionCommand executionCommand) { } /** - * Create a termux session. + * Create a {@link TermuxSession}. * Currently called by {@link TermuxSessionClient#addNewSession(boolean, String)} to add a new termux session. */ @Nullable @@ -365,7 +411,7 @@ public TermuxSession createTermuxSession(String executablePath, String[] argumen return createTermuxSession(new ExecutionCommand(getNextExecutionId(), executablePath, arguments, workingDirectory, false, isFailSafe), sessionName); } - /** Create a termux session. */ + /** Create a {@link TermuxSession}. */ @Nullable public synchronized TermuxSession createTermuxSession(ExecutionCommand executionCommand, String sessionName) { if (executionCommand == null) return null; @@ -428,11 +474,7 @@ public synchronized int removeTermuxSession(TerminalSession sessionToRemove) { return index; } - /** Finish all termux sessions by sending SIGKILL to their shells. */ - private synchronized void finishAllTermuxSessions() { - for (int i = 0; i < mTermuxSessions.size(); i++) - mTermuxSessions.get(i).getTerminalSession().finishIfRunning(); - } + @@ -473,6 +515,10 @@ private void startTermuxActivity() { startActivity(new Intent(this, TermuxActivity.class).addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)); } + + + + /** 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 #mTermuxSessionClientBase}. Once {@link TermuxActivity} bind @@ -519,6 +565,8 @@ public synchronized void unsetTermuxSessionClient() { + + private Notification buildNotification() { Intent notifyIntent = new Intent(this, TermuxActivity.class); // PendingIntent#getActivity(): "Note that the activity will be started outside of the context of an existing @@ -527,7 +575,7 @@ private Notification buildNotification() { PendingIntent pendingIntent = PendingIntent.getActivity(this, 0, notifyIntent, 0); int sessionCount = getTermuxSessionsSize(); - int taskCount = mBackgroundTasks.size(); + int taskCount = mTermuxTasks.size(); String contentText = sessionCount + " session" + (sessionCount == 1 ? "" : "s"); if (taskCount > 0) { contentText += ", " + taskCount + " task" + (taskCount == 1 ? "" : "s"); @@ -587,7 +635,7 @@ private void setupNotificationChannel() { /** Update the shown foreground service notification after making any changes that affect it. */ void updateNotification() { - if (mWakeLock == null && mTermuxSessions.isEmpty() && mBackgroundTasks.isEmpty()) { + 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 { @@ -597,6 +645,8 @@ void updateNotification() { + + private void setCurrentStoredTerminalSession(TerminalSession session) { if(session == null) return; // Make the newly created session the current one to be displayed: diff --git a/app/src/main/java/com/termux/app/terminal/StreamGobbler.java b/app/src/main/java/com/termux/app/terminal/StreamGobbler.java new file mode 100644 index 0000000000..48536eeb23 --- /dev/null +++ b/app/src/main/java/com/termux/app/terminal/StreamGobbler.java @@ -0,0 +1,298 @@ +/* + * Copyright (C) 2012-2019 Jorrit "Chainfire" Jongma + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.termux.app.terminal; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.util.List; +import java.util.Locale; + +import androidx.annotation.AnyThread; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.annotation.WorkerThread; + +import com.termux.app.utils.Logger; + +/** + * Thread utility class continuously reading from an InputStream + */ +@SuppressWarnings({"WeakerAccess"}) +public class StreamGobbler extends Thread { + private static int threadCounter = 0; + private static int incThreadCounter() { + synchronized (StreamGobbler.class) { + int ret = threadCounter; + threadCounter++; + return ret; + } + } + + /** + * Line callback interface + */ + public interface OnLineListener { + /** + *

Line callback

+ * + *

This callback should process the line as quickly as possible. + * Delays in this callback may pause the native process or even + * result in a deadlock

+ * + * @param line String that was gobbled + */ + void onLine(String line); + } + + /** + * Stream closed callback interface + */ + public interface OnStreamClosedListener { + /** + *

Stream closed callback

+ */ + void onStreamClosed(); + } + + @NonNull + private final String shell; + @NonNull + private final InputStream inputStream; + @NonNull + private final BufferedReader reader; + @Nullable + private final List listWriter; + @Nullable + private final StringBuilder stringWriter; + @Nullable + private final OnLineListener lineListener; + @Nullable + private final OnStreamClosedListener streamClosedListener; + private volatile boolean active = true; + private volatile boolean calledOnClose = false; + + private static final String LOG_TAG = "StreamGobbler"; + + /** + *

StreamGobbler constructor

+ * + *

We use this class because shell STDOUT and STDERR should be read as quickly as + * possible to prevent a deadlock from occurring, or Process.waitFor() never + * returning (as the buffer is full, pausing the native process)

+ * + * @param shell Name of the shell + * @param inputStream InputStream to read from + * @param outputList {@literal List} to write to, or null + */ + @AnyThread + public StreamGobbler(@NonNull String shell, @NonNull InputStream inputStream, @Nullable List outputList) { + super("Gobbler#" + incThreadCounter()); + this.shell = shell; + this.inputStream = inputStream; + reader = new BufferedReader(new InputStreamReader(inputStream)); + streamClosedListener = null; + + listWriter = outputList; + stringWriter = null; + lineListener = null; + } + + /** + *

StreamGobbler constructor

+ * + *

We use this class because shell STDOUT and STDERR should be read as quickly as + * possible to prevent a deadlock from occurring, or Process.waitFor() never + * returning (as the buffer is full, pausing the native process)

+ * Do not use this for concurrent reading for STDOUT and STDERR for the same StringBuilder since + * its not synchronized. + * + * @param shell Name of the shell + * @param inputStream InputStream to read from + * @param outputString {@literal List} to write to, or null + */ + @AnyThread + public StreamGobbler(@NonNull String shell, @NonNull InputStream inputStream, @Nullable StringBuilder outputString) { + super("Gobbler#" + incThreadCounter()); + this.shell = shell; + this.inputStream = inputStream; + reader = new BufferedReader(new InputStreamReader(inputStream)); + streamClosedListener = null; + + listWriter = null; + stringWriter = outputString; + lineListener = null; + } + + /** + *

StreamGobbler constructor

+ * + *

We use this class because shell STDOUT and STDERR should be read as quickly as + * possible to prevent a deadlock from occurring, or Process.waitFor() never + * returning (as the buffer is full, pausing the native process)

+ * + * @param shell Name of the shell + * @param inputStream InputStream to read from + * @param onLineListener OnLineListener callback + * @param onStreamClosedListener OnStreamClosedListener callback + */ + @AnyThread + public StreamGobbler(@NonNull String shell, @NonNull InputStream inputStream, @Nullable OnLineListener onLineListener, @Nullable OnStreamClosedListener onStreamClosedListener) { + super("Gobbler#" + incThreadCounter()); + this.shell = shell; + this.inputStream = inputStream; + reader = new BufferedReader(new InputStreamReader(inputStream)); + streamClosedListener = onStreamClosedListener; + + listWriter = null; + stringWriter = null; + lineListener = onLineListener; + } + + @Override + public void run() { + // keep reading the InputStream until it ends (or an error occurs) + // optionally pausing when a command is executed that consumes the InputStream itself + int logLevel = Logger.getLogLevel(); + try { + String line; + while ((line = reader.readLine()) != null) { + + if(logLevel >= Logger.LOG_LEVEL_VERBOSE) + Logger.logVerbose(LOG_TAG, String.format(Locale.ENGLISH, "[%s] %s", shell, line)); // This will get truncated by LOGGER_ENTRY_MAX_LEN, likely 4KB + + if (stringWriter != null) stringWriter.append(line).append("\n"); + if (listWriter != null) listWriter.add(line); + if (lineListener != null) lineListener.onLine(line); + while (!active) { + synchronized (this) { + try { + this.wait(128); + } catch (InterruptedException e) { + // no action + } + } + } + } + } catch (IOException e) { + // reader probably closed, expected exit condition + if (streamClosedListener != null) { + calledOnClose = true; + streamClosedListener.onStreamClosed(); + } + } + + // make sure our stream is closed and resources will be freed + try { + reader.close(); + } catch (IOException e) { + // read already closed + } + + if (!calledOnClose) { + if (streamClosedListener != null) { + calledOnClose = true; + streamClosedListener.onStreamClosed(); + } + } + } + + /** + *

Resume consuming the input from the stream

+ */ + @AnyThread + public void resumeGobbling() { + if (!active) { + synchronized (this) { + active = true; + this.notifyAll(); + } + } + } + + /** + *

Suspend gobbling, so other code may read from the InputStream instead

+ * + *

This should only be called from the OnLineListener callback!

+ */ + @AnyThread + public void suspendGobbling() { + synchronized (this) { + active = false; + this.notifyAll(); + } + } + + /** + *

Wait for gobbling to be suspended

+ * + *

Obviously this cannot be called from the same thread as {@link #suspendGobbling()}

+ */ + @WorkerThread + public void waitForSuspend() { + synchronized (this) { + while (active) { + try { + this.wait(32); + } catch (InterruptedException e) { + // no action + } + } + } + } + + /** + *

Is gobbling suspended ?

+ * + * @return is gobbling suspended? + */ + @AnyThread + public boolean isSuspended() { + synchronized (this) { + return !active; + } + } + + /** + *

Get current source InputStream

+ * + * @return source InputStream + */ + @NonNull + @AnyThread + public InputStream getInputStream() { + return inputStream; + } + + /** + *

Get current OnLineListener

+ * + * @return OnLineListener + */ + @Nullable + @AnyThread + public OnLineListener getOnLineListener() { + return lineListener; + } + + void conditionalJoin() throws InterruptedException { + if (calledOnClose) return; // deadlock from callback, we're inside exit procedure + if (Thread.currentThread() == this) return; // can't join self + join(); + } +} diff --git a/app/src/main/java/com/termux/app/terminal/TermuxTask.java b/app/src/main/java/com/termux/app/terminal/TermuxTask.java new file mode 100644 index 0000000000..547066b72b --- /dev/null +++ b/app/src/main/java/com/termux/app/terminal/TermuxTask.java @@ -0,0 +1,140 @@ +package com.termux.app.terminal; + +import androidx.annotation.NonNull; + +import com.termux.app.TermuxConstants; +import com.termux.app.TermuxService; +import com.termux.app.utils.Logger; +import com.termux.app.utils.PluginUtils; +import com.termux.app.utils.ShellUtils; +import com.termux.models.ExecutionCommand; +import com.termux.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(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, "Failed to run \"" + executionCommand.commandLabel + "\" background task", e); + TermuxTask.processTermuxTaskResult(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(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 != null) + service.onTermuxTaskExited(termuxTask); + } + + public Process getTerminalSession() { + return mProcess; + } + + public ExecutionCommand getExecutionCommand() { + return mExecutionCommand; + } + +} diff --git a/app/src/main/java/com/termux/app/utils/PluginUtils.java b/app/src/main/java/com/termux/app/utils/PluginUtils.java index 387c156046..f64142f519 100644 --- a/app/src/main/java/com/termux/app/utils/PluginUtils.java +++ b/app/src/main/java/com/termux/app/utils/PluginUtils.java @@ -36,75 +36,116 @@ public class PluginUtils { private static final String LOG_TAG = "PluginUtils"; /** - * Send execution result of commands to the {@link PendingIntent} creator received by - * execution service if {@code pendingIntent} is not {@code null}. + * Process {@link ExecutionCommand} result. + * + * The ExecutionCommand currentState must be greater or equal to {@link ExecutionCommand.ExecutionState#EXECUTED}. + * If the {@link ExecutionCommand#isPluginExecutionCommand} is {@code true} and {@link ExecutionCommand#pluginPendingIntent} + * is not {@code null}, then the result of commands is sent back to the {@link PendingIntent} creator. * * @param context The {@link Context} that will be used to send result intent to the {@link PendingIntent} creator. - * @param logLevel The log level to dump the result. * @param logTag The log tag to use for logging. - * @param pendingIntent The {@link PendingIntent} sent by creator to the execution service. - * @param stdout The value for {@link TERMUX_SERVICE#EXTRA_PLUGIN_RESULT_BUNDLE_STDOUT} extra of {@link TERMUX_SERVICE#EXTRA_PLUGIN_RESULT_BUNDLE} bundle of intent. - * @param stderr The value for {@link TERMUX_SERVICE#EXTRA_PLUGIN_RESULT_BUNDLE_STDERR} extra of {@link TERMUX_SERVICE#EXTRA_PLUGIN_RESULT_BUNDLE} bundle of intent. - * @param exitCode The value for {@link TERMUX_SERVICE#EXTRA_PLUGIN_RESULT_BUNDLE_EXIT_CODE} extra of {@link TERMUX_SERVICE#EXTRA_PLUGIN_RESULT_BUNDLE} bundle of intent. - * @param errCode The value for {@link TERMUX_SERVICE#EXTRA_PLUGIN_RESULT_BUNDLE_ERR} extra of {@link TERMUX_SERVICE#EXTRA_PLUGIN_RESULT_BUNDLE} bundle of intent. - * @param errmsg The value for {@link TERMUX_SERVICE#EXTRA_PLUGIN_RESULT_BUNDLE_ERRMSG} extra of {@link TERMUX_SERVICE#EXTRA_PLUGIN_RESULT_BUNDLE} bundle of intent. + * @param executionCommand The {@link ExecutionCommand} to process. */ - public static void sendExecuteResultToResultsService(final Context context, final int logLevel, final String logTag, final PendingIntent pendingIntent, final String stdout, final String stderr, final String exitCode, final String errCode, final String errmsg) { - String label; + public static void processPluginExecutionCommandResult(final Context context, String logTag, final ExecutionCommand executionCommand) { + if (executionCommand == null) return; + + if(!executionCommand.hasExecuted()) { + Logger.logWarn(LOG_TAG, "Ignoring call to processPluginExecutionCommandResult() since the execution command has not been ExecutionState.EXECUTED"); + return; + } + + // Must be a normal command like foreground terminal session started by user + if(!executionCommand.isPluginExecutionCommand) + return; + + logTag = TextDataUtils.getDefaultIfNull(logTag, LOG_TAG); - if(pendingIntent == null) - label = "Execution Result"; - else - label = "Sending execution result to " + pendingIntent.getCreatorPackage(); + Logger.logDebug(LOG_TAG, executionCommand.toString()); - Logger.logMesssage(logLevel, logTag, label + ":\n" + - TERMUX_SERVICE.EXTRA_PLUGIN_RESULT_BUNDLE_STDOUT + ":\n```\n" + stdout + "\n```\n" + - TERMUX_SERVICE.EXTRA_PLUGIN_RESULT_BUNDLE_STDERR + ":\n```\n" + stderr + "\n```\n" + - TERMUX_SERVICE.EXTRA_PLUGIN_RESULT_BUNDLE_EXIT_CODE + ": `" + exitCode + "`\n" + - TERMUX_SERVICE.EXTRA_PLUGIN_RESULT_BUNDLE_ERR + ": `" + errCode + "`\n" + - TERMUX_SERVICE.EXTRA_PLUGIN_RESULT_BUNDLE_ERRMSG + ": `" + errmsg + "`"); - // If pendingIntent is null, then just return - if(pendingIntent == null) return; + // If pluginPendingIntent is null, then just return + if(executionCommand.pluginPendingIntent == null) return; + + + // Send pluginPendingIntent to its creator final Bundle resultBundle = new Bundle(); - resultBundle.putString(TERMUX_SERVICE.EXTRA_PLUGIN_RESULT_BUNDLE_STDOUT, stdout); - resultBundle.putString(TERMUX_SERVICE.EXTRA_PLUGIN_RESULT_BUNDLE_STDERR, stderr); - if (exitCode != null && !exitCode.isEmpty()) resultBundle.putInt(TERMUX_SERVICE.EXTRA_PLUGIN_RESULT_BUNDLE_EXIT_CODE, Integer.parseInt(exitCode)); - if (errCode != null && !errCode.isEmpty()) resultBundle.putInt(TERMUX_SERVICE.EXTRA_PLUGIN_RESULT_BUNDLE_ERR, Integer.parseInt(errCode)); - resultBundle.putString(TERMUX_SERVICE.EXTRA_PLUGIN_RESULT_BUNDLE_ERRMSG, errmsg); + Logger.logDebug(LOG_TAG, "Sending execution result for Execution Command \"" + executionCommand.getCommandIdAndLabelLogString() + "\" to " + executionCommand.pluginPendingIntent.getCreatorPackage()); + + String truncatedStdout = null; + String truncatedStderr = null; + String truncatedErrmsg = null; + + String stdoutOriginalLength = (executionCommand.stdout == null) ? null: String.valueOf(executionCommand.stdout.length()); + String stderrOriginalLength = (executionCommand.stderr == null) ? null: String.valueOf(executionCommand.stderr.length()); + + if(executionCommand.stderr == null || executionCommand.stderr.isEmpty()) { + truncatedStdout = TextDataUtils.getTruncatedCommandOutput(executionCommand.stdout, TextDataUtils.TRANSACTION_SIZE_LIMIT_IN_BYTES, false, false, false); + } else if (executionCommand.stdout == null || executionCommand.stdout.isEmpty()) { + truncatedStderr = TextDataUtils.getTruncatedCommandOutput(executionCommand.stderr, TextDataUtils.TRANSACTION_SIZE_LIMIT_IN_BYTES, false, false, false); + } else { + truncatedStdout = TextDataUtils.getTruncatedCommandOutput(executionCommand.stdout, TextDataUtils.TRANSACTION_SIZE_LIMIT_IN_BYTES / 2, false, false, false); + truncatedStderr = TextDataUtils.getTruncatedCommandOutput(executionCommand.stderr, TextDataUtils.TRANSACTION_SIZE_LIMIT_IN_BYTES / 2, false, false, false); + } + + if(truncatedStdout != null && executionCommand.stdout != null && truncatedStdout.length() < executionCommand.stdout.length()){ + Logger.logWarn(logTag, "Execution Result for Execution Command \"" + executionCommand.getCommandIdAndLabelLogString() + "\" stdout length truncated from " + stdoutOriginalLength + " to " + truncatedStdout.length()); + executionCommand.stdout = truncatedStdout; + } + + if(truncatedStderr != null && executionCommand.stderr != null && truncatedStderr.length() < executionCommand.stderr.length()){ + Logger.logWarn(logTag, "Execution Result for Execution Command \"" + executionCommand.getCommandIdAndLabelLogString() + "\" stderr length truncated from " + stderrOriginalLength + " to " + truncatedStderr.length()); + executionCommand.stderr = truncatedStderr; + } + + + //Combine errmsg and stacktraces + if(executionCommand.isStateFailed()) { + executionCommand.errmsg = Logger.getMessageAndStackTracesString(executionCommand.errmsg, executionCommand.throwableList); + } + + String errmsgOriginalLength = (executionCommand.errmsg == null) ? null: String.valueOf(executionCommand.errmsg.length()); + + // trim from end to preseve start of stacktraces + truncatedErrmsg = TextDataUtils.getTruncatedCommandOutput(executionCommand.errmsg, TextDataUtils.TRANSACTION_SIZE_LIMIT_IN_BYTES / 4, true, false, false); + if(truncatedErrmsg != null && executionCommand.errmsg != null && truncatedErrmsg.length() < executionCommand.errmsg.length()){ + Logger.logWarn(logTag, "Execution Result for Execution Command \"" + executionCommand.getCommandIdAndLabelLogString() + "\" errmsg length truncated from " + errmsgOriginalLength + " to " + truncatedErrmsg.length()); + executionCommand.errmsg = truncatedErrmsg; + } + + + resultBundle.putString(TERMUX_SERVICE.EXTRA_PLUGIN_RESULT_BUNDLE_STDOUT, executionCommand.stdout); + resultBundle.putString(TERMUX_SERVICE.EXTRA_PLUGIN_RESULT_BUNDLE_STDOUT_ORIGINAL_LENGTH, stdoutOriginalLength); + resultBundle.putString(TERMUX_SERVICE.EXTRA_PLUGIN_RESULT_BUNDLE_STDERR, executionCommand.stderr); + resultBundle.putString(TERMUX_SERVICE.EXTRA_PLUGIN_RESULT_BUNDLE_STDERR_ORIGINAL_LENGTH, stderrOriginalLength); + if (executionCommand.exitCode != null) resultBundle.putInt(TERMUX_SERVICE.EXTRA_PLUGIN_RESULT_BUNDLE_EXIT_CODE, executionCommand.exitCode); + if (executionCommand.errCode != null) resultBundle.putInt(TERMUX_SERVICE.EXTRA_PLUGIN_RESULT_BUNDLE_ERR, executionCommand.errCode); + resultBundle.putString(TERMUX_SERVICE.EXTRA_PLUGIN_RESULT_BUNDLE_ERRMSG, executionCommand.errmsg); Intent resultIntent = new Intent(); resultIntent.putExtra(TERMUX_SERVICE.EXTRA_PLUGIN_RESULT_BUNDLE, resultBundle); if(context != null) { try { - pendingIntent.send(context, Activity.RESULT_OK, resultIntent); + executionCommand.pluginPendingIntent.send(context, Activity.RESULT_OK, resultIntent); } catch (PendingIntent.CanceledException e) { // The caller doesn't want the result? That's fine, just ignore } } - } - /** - * Check if {@link TermuxConstants#PROP_ALLOW_EXTERNAL_APPS} property is not set to "true". - * - * @param context The {@link Context} to get error string. - * @return Returns the {@code errmsg} if policy is violated, otherwise {@code null}. - */ - public static String checkIfRunCommandServiceAllowExternalAppsPolicyIsViolated(final Context context) { - String errmsg = null; - if (!SharedProperties.isPropertyValueTrue(context, TermuxPropertyConstants.getTermuxPropertiesFile(), TermuxConstants.PROP_ALLOW_EXTERNAL_APPS)) { - errmsg = context.getString(R.string.error_run_command_service_allow_external_apps_ungranted); - } + if(!executionCommand.isStateFailed()) + executionCommand.setState(ExecutionCommand.ExecutionState.SUCCESS); - return errmsg; } + + /** * Proceses {@link ExecutionCommand} error. - * The {@link ExecutionCommand#errCode} must have been set to a non-zero value. + * + * The ExecutionCommand currentState must be equal to {@link ExecutionCommand.ExecutionState#FAILED}. + * The {@link ExecutionCommand#errCode} must have been set to a value greater than {@link ExecutionCommand#RESULT_CODE_OK}. * The {@link ExecutionCommand#errmsg} and any {@link ExecutionCommand#throwableList} must also * be set with appropriate error info. * If the {@link TermuxPreferenceConstants.TERMUX_APP#KEY_PLUGIN_ERROR_NOTIFICATIONS_ENABLED} is @@ -118,14 +159,14 @@ public static String checkIfRunCommandServiceAllowExternalAppsPolicyIsViolated(f public static void processPluginExecutionCommandError(final Context context, String logTag, final ExecutionCommand executionCommand) { if(context == null || executionCommand == null) return; - if(executionCommand.errCode == null || executionCommand.errCode == 0) { - Logger.logWarn(LOG_TAG, "Ignoring call to processPluginExecutionCommandError() since the execution command errCode has not been set to a non-zero value"); + if(!executionCommand.isStateFailed()) { + Logger.logWarn(LOG_TAG, "Ignoring call to processPluginExecutionCommandError() since the execution command does not have ExecutionState.FAILED state"); return; } // Log the error and any exception logTag = TextDataUtils.getDefaultIfNull(logTag, LOG_TAG); - Logger.logStackTracesWithMessage(logTag, executionCommand.errmsg, executionCommand.throwableList); + Logger.logStackTracesWithMessage(logTag, "(" + executionCommand.errCode + ") " + executionCommand.errmsg, executionCommand.throwableList); TermuxAppSharedPreferences preferences = new TermuxAppSharedPreferences(context); // If user has disabled notifications for plugin, then just return @@ -160,6 +201,8 @@ public static void processPluginExecutionCommandError(final Context context, Str notificationManager.notify(nextNotificationId, builder.build()); } + + /** * Get {@link Notification.Builder} for {@link #NOTIFICATION_CHANNEL_ID_PLUGIN_COMMAND_ERRORS} * and {@link #NOTIFICATION_CHANNEL_NAME_PLUGIN_COMMAND_ERRORS}. @@ -207,4 +250,21 @@ public static void setupPluginCommandErrorsNotificationChannel(final Context con NOTIFICATION_CHANNEL_NAME_PLUGIN_COMMAND_ERRORS, NotificationManager.IMPORTANCE_HIGH); } + + + /** + * Check if {@link TermuxConstants#PROP_ALLOW_EXTERNAL_APPS} property is not set to "true". + * + * @param context The {@link Context} to get error string. + * @return Returns the {@code errmsg} if policy is violated, otherwise {@code null}. + */ + public static String checkIfRunCommandServiceAllowExternalAppsPolicyIsViolated(final Context context) { + String errmsg = null; + if (!SharedProperties.isPropertyValueTrue(context, TermuxPropertyConstants.getTermuxPropertiesFile(), TermuxConstants.PROP_ALLOW_EXTERNAL_APPS)) { + errmsg = context.getString(R.string.error_run_command_service_allow_external_apps_ungranted); + } + + return errmsg; + } + }