From f62febbfb7b2221d66413fe588c7ad5b631d4b20 Mon Sep 17 00:00:00 2001 From: agnostic-apollo Date: Thu, 25 Mar 2021 09:47:59 +0500 Subject: [PATCH] Add the TermuxTask class for linking a Process to an ExecutionCommand. TermuxTask will maintain info for background Termux tasks. Each task started by TermuxService will now be linked to a ExecutionCommand that started it. - StreamGobbler class has also been imported from https://github.com/Chainfire/libsuperuser and partially modified to read stdout and stderr of background commands. This should likely be much safer and efficient. - Logging of every line has been disabled unless log level is set to verbose. This should have a performance increase and also prevent potentially private user data to be sent to logcat. - This also solves the bug where Termux:Tasker would hang indefinitely if Runtime.getRuntime().exec raised an exception, like for invalid or missing interpreter errors and Termux:Tasker wasn't notified of it. Now the errmsg will be used to send any exceptions back to Termux:Tasker and other 3rd party calls. - This also solves the bug where stdout or stderr were too large in size and TransactionTooLargeException exception was raised and result TERMUX_SERVICE.EXTRA_PENDING_INTENT pending intent failed to be sent to the caller. This would have also hung up Termux:Tasker. Now the stdout and stderr sent back in TERMUX_SERVICE.EXTRA_PLUGIN_RESULT_BUNDLE bundle will be truncated from the start to max 100KB combined. The original size of stdout and stderr will be provided in TERMUX_SERVICE.EXTRA_PLUGIN_RESULT_BUNDLE_STDOUT_ORIGINAL_LENGTH and TERMUX_SERVICE.EXTRA_PLUGIN_RESULT_BUNDLE_STDERR_ORIGINAL_LENGTH extras respectively so that the caller can check if either of them were truncated. The errmsg will also be truncated from end to max 25KB to preserve start of stacktraces. - The PluginUtils.processPluginExecutionCommandResult() has been updated to fully handle the result of plugin execution intents. --- .../java/com/termux/app/TermuxService.java | 100 ++++-- .../termux/app/terminal/StreamGobbler.java | 298 ++++++++++++++++++ .../com/termux/app/terminal/TermuxTask.java | 140 ++++++++ .../com/termux/app/utils/PluginUtils.java | 152 ++++++--- 4 files changed, 619 insertions(+), 71 deletions(-) create mode 100644 app/src/main/java/com/termux/app/terminal/StreamGobbler.java create mode 100644 app/src/main/java/com/termux/app/terminal/TermuxTask.java 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; + } + }