From f102ea20b2041723c94a6fd5dd27558250beb5bd Mon Sep 17 00:00:00 2001 From: agnostic-apollo Date: Sat, 11 Jun 2022 19:08:20 +0500 Subject: [PATCH] Added|Changed!: Implement new design for shell environment generation and add support for `MIT` licensed shell environment client - `ShellEnvironmentClient` has been renamed to `IShellEnvironment` with certain changes to its interface methods, including requirement for `Execution` command itself for `setupShellCommandEnvironment()`. - `UnixShellEnvironment` implements the `IShellEnvironment` interface as is the abstract base class of all other shell environments. - `AndroidShellEnvironment` extends from the `UnixShellEnvironment` class and provides an environment that would work for Android shells. This is `MIT` licensed and can be used by users importing the `termux-shared` library or the library itself to run `AppShell` shells. Previously, `TermuxShellEnvironmentClient` existed which was `GPLv3` licensed and it would not have been possible to use it for non-GPL code. - `TermuxShellEnvironment` extends from the `AndroidShellEnvironment` class and adds/overrides additional environment variables required for Termux shells to work, including setting `HOME`, `TMPDIR`, `PATH` and `LD_LIBRARY_PATH` appropriately. Termux app related variables will be added in a later commit. `TermuxShellEnvironment` replaces `TermuxShellEnvironmentClient` and is `GPLv3` licensed. --- .../java/com/termux/app/TermuxService.java | 8 +- .../shared/shell/ShellEnvironmentClient.java | 47 ------- .../com/termux/shared/shell/ShellUtils.java | 24 +++- .../environment/AndroidShellEnvironment.java | 91 +++++++++++++ .../environment/IShellEnvironment.java | 52 ++++++++ .../environment/ShellEnvironmentUtils.java | 126 ++++++++++++++++++ .../environment/UnixShellEnvironment.java | 80 +++++++++++ .../shell/command/runner/app/AppShell.java | 46 ++++--- .../com/termux/shared/termux/TermuxUtils.java | 6 +- .../shared/termux/file/TermuxFileUtils.java | 4 +- .../shell/TermuxShellEnvironmentClient.java | 35 ----- .../shared/termux/shell/TermuxShellUtils.java | 82 ++---------- .../environment/TermuxShellEnvironment.java | 74 ++++++++++ .../runner/terminal/TermuxSession.java | 48 ++++--- 14 files changed, 533 insertions(+), 190 deletions(-) delete mode 100644 termux-shared/src/main/java/com/termux/shared/shell/ShellEnvironmentClient.java create mode 100644 termux-shared/src/main/java/com/termux/shared/shell/command/environment/AndroidShellEnvironment.java create mode 100644 termux-shared/src/main/java/com/termux/shared/shell/command/environment/IShellEnvironment.java create mode 100644 termux-shared/src/main/java/com/termux/shared/shell/command/environment/ShellEnvironmentUtils.java create mode 100644 termux-shared/src/main/java/com/termux/shared/shell/command/environment/UnixShellEnvironment.java delete mode 100644 termux-shared/src/main/java/com/termux/shared/termux/shell/TermuxShellEnvironmentClient.java create mode 100644 termux-shared/src/main/java/com/termux/shared/termux/shell/command/environment/TermuxShellEnvironment.java diff --git a/app/src/main/java/com/termux/app/TermuxService.java b/app/src/main/java/com/termux/app/TermuxService.java index 7f10c6c8..202ac405 100644 --- a/app/src/main/java/com/termux/app/TermuxService.java +++ b/app/src/main/java/com/termux/app/TermuxService.java @@ -28,7 +28,7 @@ import com.termux.shared.errors.Errno; import com.termux.shared.shell.ShellUtils; import com.termux.shared.shell.command.runner.app.AppShell; import com.termux.shared.termux.settings.properties.TermuxAppSharedProperties; -import com.termux.shared.termux.shell.TermuxShellEnvironmentClient; +import com.termux.shared.termux.shell.command.environment.TermuxShellEnvironment; import com.termux.shared.termux.shell.TermuxShellUtils; import com.termux.shared.termux.TermuxConstants; import com.termux.shared.termux.TermuxConstants.TERMUX_APP.TERMUX_ACTIVITY; @@ -469,7 +469,8 @@ public final class TermuxService extends Service implements AppShell.AppShellCli if (Logger.getLogLevel() >= Logger.LOG_LEVEL_VERBOSE) Logger.logVerboseExtended(LOG_TAG, executionCommand.toString()); - AppShell newTermuxTask = AppShell.execute(this, executionCommand, this, new TermuxShellEnvironmentClient(), false); + AppShell newTermuxTask = AppShell.execute(this, executionCommand, this, + new TermuxShellEnvironment(),false); if (newTermuxTask == null) { Logger.logError(LOG_TAG, "Failed to execute new TermuxTask command for:\n" + executionCommand.getCommandIdAndLabelLogString()); // If the execution command was started for a plugin, then process the error @@ -578,7 +579,8 @@ public final class TermuxService extends Service implements AppShell.AppShellCli // Otherwise if command was manually started by the user like by adding a new terminal session, // then no need to set stdout executionCommand.terminalTranscriptRows = mProperties.getTerminalTranscriptRows(); - TermuxSession newTermuxSession = TermuxSession.execute(this, executionCommand, getTermuxTerminalSessionClient(), this, new TermuxShellEnvironmentClient(), executionCommand.isPluginExecutionCommand); + TermuxSession newTermuxSession = TermuxSession.execute(this, executionCommand, getTermuxTerminalSessionClient(), + this, new TermuxShellEnvironment(), executionCommand.isPluginExecutionCommand); if (newTermuxSession == null) { Logger.logError(LOG_TAG, "Failed to execute new TermuxSession command for:\n" + executionCommand.getCommandIdAndLabelLogString()); // If the execution command was started for a plugin, then process the error diff --git a/termux-shared/src/main/java/com/termux/shared/shell/ShellEnvironmentClient.java b/termux-shared/src/main/java/com/termux/shared/shell/ShellEnvironmentClient.java deleted file mode 100644 index f8ed9a10..00000000 --- a/termux-shared/src/main/java/com/termux/shared/shell/ShellEnvironmentClient.java +++ /dev/null @@ -1,47 +0,0 @@ -package com.termux.shared.shell; - -import android.content.Context; - -import androidx.annotation.NonNull; - -public interface ShellEnvironmentClient { - - /** - * Get the default working directory path for the environment in case the path that was passed - * was {@code null} or empty. - * - * @return Should return the default working directory path. - */ - @NonNull - String getDefaultWorkingDirectoryPath(); - - /** - * Get the default "/bin" path, likely $PREFIX/bin. - * - * @return Should return the "/bin" path. - */ - @NonNull - String getDefaultBinPath(); - - /** - * Build the shell environment to be used for commands. - * - * @param currentPackageContext The {@link Context} for the current package. - * @param isFailSafe If running a failsafe session. - * @param workingDirectory The working directory for the environment. - * @return Should return the build environment. - */ - @NonNull - String[] buildEnvironment(Context currentPackageContext, boolean isFailSafe, String workingDirectory); - - /** - * Setup process arguments for the file to execute, like interpreter, etc. - * - * @param fileToExecute The file to execute. - * @param arguments The arguments to pass to the executable. - * @return Should return the final process arguments. - */ - @NonNull - String[] setupProcessArgs(@NonNull String fileToExecute, String[] arguments); - -} diff --git a/termux-shared/src/main/java/com/termux/shared/shell/ShellUtils.java b/termux-shared/src/main/java/com/termux/shared/shell/ShellUtils.java index 302528b7..5c3ad576 100644 --- a/termux-shared/src/main/java/com/termux/shared/shell/ShellUtils.java +++ b/termux-shared/src/main/java/com/termux/shared/shell/ShellUtils.java @@ -1,5 +1,8 @@ package com.termux.shared.shell; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + import com.termux.shared.file.FileUtils; import com.termux.terminal.TerminalBuffer; import com.termux.terminal.TerminalEmulator; @@ -7,8 +10,13 @@ import com.termux.terminal.TerminalSession; import java.lang.reflect.Field; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + public class ShellUtils { + /** Get process id of {@link Process}. */ public static int getPid(Process p) { try { Field f = p.getClass().getDeclaredField("pid"); @@ -23,10 +31,24 @@ public class ShellUtils { } } - public static String getExecutableBasename(String executable) { + /** Setup shell command arguments for the execute. */ + @NonNull + public static String[] setupShellCommandArguments(@NonNull String executable, @Nullable String[] arguments) { + List result = new ArrayList<>(); + result.add(executable); + if (arguments != null) Collections.addAll(result, arguments); + return result.toArray(new String[0]); + } + + /** Get basename for executable. */ + @Nullable + public static String getExecutableBasename(@Nullable String executable) { return FileUtils.getFileBasename(executable); } + + + /** Get transcript for {@link TerminalSession}. */ public static String getTerminalSessionTranscriptText(TerminalSession terminalSession, boolean linesJoined, boolean trim) { if (terminalSession == null) return null; diff --git a/termux-shared/src/main/java/com/termux/shared/shell/command/environment/AndroidShellEnvironment.java b/termux-shared/src/main/java/com/termux/shared/shell/command/environment/AndroidShellEnvironment.java new file mode 100644 index 00000000..f87c1438 --- /dev/null +++ b/termux-shared/src/main/java/com/termux/shared/shell/command/environment/AndroidShellEnvironment.java @@ -0,0 +1,91 @@ +package com.termux.shared.shell.command.environment; + +import android.content.Context; + +import androidx.annotation.NonNull; + +import com.termux.shared.shell.command.ExecutionCommand; + +import java.io.File; +import java.util.HashMap; + +/** + * Environment for Android. + * + * https://cs.android.com/android/platform/superproject/+/android-12.0.0_r32:frameworks/base/core/java/android/os/Environment.java + * https://cs.android.com/android/platform/superproject/+/android-12.0.0_r32:system/core/rootdir/init.environ.rc.in + * https://cs.android.com/android/platform/superproject/+/android-5.0.0_r1.0.1:system/core/rootdir/init.environ.rc.in + * https://cs.android.com/android/_/android/platform/system/core/+/refs/tags/android-12.0.0_r32:rootdir/init.rc;l=910 + * https://cs.android.com/android/platform/superproject/+/android-12.0.0_r32:packages/modules/SdkExtensions/derive_classpath/derive_classpath.cpp;l=96 + */ +public class AndroidShellEnvironment extends UnixShellEnvironment { + + /** Get shell environment for Android. */ + @NonNull + @Override + public HashMap getEnvironment(@NonNull Context currentPackageContext, boolean isFailSafe) { + HashMap environment = new HashMap<>(); + + environment.put(ENV_HOME, "/"); + environment.put(ENV_LANG, "en_US.UTF-8"); + environment.put(ENV_PATH, System.getenv(ENV_PATH)); + environment.put(ENV_TMPDIR, "/data/local/tmp"); + + environment.put(ENV_COLORTERM, "truecolor"); + environment.put(ENV_TERM, "xterm-256color"); + + ShellEnvironmentUtils.putToEnvIfInSystemEnv(environment, "ANDROID_ASSETS"); + ShellEnvironmentUtils.putToEnvIfInSystemEnv(environment, "ANDROID_DATA"); + ShellEnvironmentUtils.putToEnvIfInSystemEnv(environment, "ANDROID_ROOT"); + ShellEnvironmentUtils.putToEnvIfInSystemEnv(environment, "ANDROID_STORAGE"); + + // EXTERNAL_STORAGE is needed for /system/bin/am to work on at least + // Samsung S7 - see https://plus.google.com/110070148244138185604/posts/gp8Lk3aCGp3. + // https://cs.android.com/android/_/android/platform/system/core/+/fc000489 + ShellEnvironmentUtils.putToEnvIfInSystemEnv(environment, "EXTERNAL_STORAGE"); + ShellEnvironmentUtils.putToEnvIfInSystemEnv(environment, "ASEC_MOUNTPOINT"); + ShellEnvironmentUtils.putToEnvIfInSystemEnv(environment, "LOOP_MOUNTPOINT"); + + ShellEnvironmentUtils.putToEnvIfInSystemEnv(environment, "ANDROID_RUNTIME_ROOT"); + ShellEnvironmentUtils.putToEnvIfInSystemEnv(environment, "ANDROID_ART_ROOT"); + ShellEnvironmentUtils.putToEnvIfInSystemEnv(environment, "ANDROID_I18N_ROOT"); + ShellEnvironmentUtils.putToEnvIfInSystemEnv(environment, "ANDROID_TZDATA_ROOT"); + + ShellEnvironmentUtils.putToEnvIfInSystemEnv(environment, "BOOTCLASSPATH"); + ShellEnvironmentUtils.putToEnvIfInSystemEnv(environment, "DEX2OATBOOTCLASSPATH"); + ShellEnvironmentUtils.putToEnvIfInSystemEnv(environment, "SYSTEMSERVERCLASSPATH"); + + return environment; + } + + + + @NonNull + @Override + public String getDefaultWorkingDirectoryPath() { + return "/"; + } + + + @NonNull + @Override + public String getDefaultBinPath() { + return "/system/bin"; + } + + @NonNull + @Override + public HashMap setupShellCommandEnvironment(@NonNull Context currentPackageContext, + @NonNull ExecutionCommand executionCommand) { + HashMap environment = getEnvironment(currentPackageContext, executionCommand.isFailsafe); + + String workingDirectory = executionCommand.workingDirectory; + environment.put(ENV_PWD, + workingDirectory != null && !workingDirectory.isEmpty() ? new File(workingDirectory).getAbsolutePath() : // PWD must be absolute path + getDefaultWorkingDirectoryPath()); + ShellEnvironmentUtils.createHomeDir(environment); + + return environment; + } + +} diff --git a/termux-shared/src/main/java/com/termux/shared/shell/command/environment/IShellEnvironment.java b/termux-shared/src/main/java/com/termux/shared/shell/command/environment/IShellEnvironment.java new file mode 100644 index 00000000..a8e75e9a --- /dev/null +++ b/termux-shared/src/main/java/com/termux/shared/shell/command/environment/IShellEnvironment.java @@ -0,0 +1,52 @@ +package com.termux.shared.shell.command.environment; + +import android.content.Context; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import com.termux.shared.shell.command.ExecutionCommand; + +import java.util.HashMap; + +public interface IShellEnvironment { + + /** + * Get the default working directory path for the environment in case the path that was passed + * was {@code null} or empty. + * + * @return Should return the default working directory path. + */ + @NonNull + String getDefaultWorkingDirectoryPath(); + + /** + * Get the default "/bin" path, like $PREFIX/bin. + * + * @return Should return the "/bin" path. + */ + @NonNull + String getDefaultBinPath(); + + /** + * Setup shell command arguments for the file to execute, like interpreter, etc. + * + * @param fileToExecute The file to execute. + * @param arguments The arguments to pass to the executable. + * @return Should return the final process arguments. + */ + @NonNull + String[] setupShellCommandArguments(@NonNull String fileToExecute, @Nullable String[] arguments); + + /** + * Setup shell command environment to be used for commands. + * + * @param currentPackageContext The {@link Context} for the current package. + * @param executionCommand The {@link ExecutionCommand} for which to set environment. + * @return Should return the shell environment. + */ + @NonNull + HashMap setupShellCommandEnvironment(@NonNull Context currentPackageContext, + @NonNull ExecutionCommand executionCommand); + +} diff --git a/termux-shared/src/main/java/com/termux/shared/shell/command/environment/ShellEnvironmentUtils.java b/termux-shared/src/main/java/com/termux/shared/shell/command/environment/ShellEnvironmentUtils.java new file mode 100644 index 00000000..03ec6ab5 --- /dev/null +++ b/termux-shared/src/main/java/com/termux/shared/shell/command/environment/ShellEnvironmentUtils.java @@ -0,0 +1,126 @@ +package com.termux.shared.shell.command.environment; + +import static com.termux.shared.shell.command.environment.UnixShellEnvironment.*; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import com.termux.shared.errors.Error; +import com.termux.shared.file.FileUtils; +import com.termux.shared.logger.Logger; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +public class ShellEnvironmentUtils { + + private static final String LOG_TAG = "ShellEnvironmentUtils"; + + /** + * Convert environment {@link HashMap} to `environ` {@link List }. + * + * The items in the environ will have the format `name=value`. + * + * Check {@link #isValidEnvironmentVariableName(String)} and {@link #isValidEnvironmentVariableValue(String)} + * for valid variable names and values. + * + * https://manpages.debian.org/testing/manpages/environ.7.en.html + * https://pubs.opengroup.org/onlinepubs/9699919799/basedefs/V1_chap08.html + */ + @NonNull + public static List convertEnvironmentToEnviron(@NonNull HashMap environmentMap) { + List environmentList = new ArrayList<>(environmentMap.size()); + String value; + for (String name : environmentMap.keySet()) { + value = environmentMap.get(name); + if (isValidEnvironmentVariableNameValuePair(name, value, true)) + environmentList.add(name + "=" + environmentMap.get(name)); + } + return environmentList; + } + + + /** + * Check if environment variable name and value pair is valid. Errors will be logged if + * {@code logErrors} is {@code true}. + * + * Check {@link #isValidEnvironmentVariableName(String)} and {@link #isValidEnvironmentVariableValue(String)} + * for valid variable names and values. + */ + public static boolean isValidEnvironmentVariableNameValuePair(@Nullable String name, @Nullable String value, boolean logErrors) { + if (!isValidEnvironmentVariableName(name)) { + if (logErrors) + Logger.logErrorPrivate(LOG_TAG, "Invalid environment variable name. name=`" + name + "`, value=`" + value + "`"); + return false; + } + + if (!isValidEnvironmentVariableValue(value)) { + if (logErrors) + Logger.logErrorPrivate(LOG_TAG, "Invalid environment variable value. name=`" + name + "`, value=`" + value + "`"); + return false; + } + + return true; + } + + /** + * Check if environment variable name is valid. It must not be {@code null} and must not contain + * the null byte ('\0') and must only contain alphanumeric and underscore characters and must not + * start with a digit. + */ + public static boolean isValidEnvironmentVariableName(@Nullable String name) { + return name != null && !name.contains("\0") && name.matches("[a-zA-Z_][a-zA-Z0-9_]*"); + } + + /** + * Check if environment variable value is valid. It must not be {@code null} and must not contain + * the null byte ('\0'). + */ + public static boolean isValidEnvironmentVariableValue(@Nullable String value) { + return value != null && !value.contains("\0"); + } + + + + /** Put value in environment if variable exists in {@link System) environment. */ + public static void putToEnvIfInSystemEnv(@NonNull HashMap environment, + @NonNull String name) { + String value = System.getenv(name); + if (value != null) { + environment.put(name, value); + } + } + + /** Put {@link String} value in environment if value set. */ + public static void putToEnvIfSet(@NonNull HashMap environment, @NonNull String name, + @Nullable String value) { + if (value != null) { + environment.put(name, value); + } + } + + /** Put {@link Boolean} value "true" or "false" in environment if value set. */ + public static void putToEnvIfSet(@NonNull HashMap environment, @NonNull String name, + @Nullable Boolean value) { + if (value != null) { + environment.put(name, String.valueOf(value)); + } + } + + + + /** Create HOME directory in environment {@link Map} if set. */ + public static void createHomeDir(@NonNull HashMap environment) { + String homeDirectory = environment.get(ENV_HOME); + if (homeDirectory != null && !homeDirectory.isEmpty()) { + Error error = FileUtils.createDirectoryFile("shell home", homeDirectory); + if (error != null) { + Logger.logErrorExtended(LOG_TAG, "Failed to create shell home directory\n" + error.toString()); + } + } + } + +} diff --git a/termux-shared/src/main/java/com/termux/shared/shell/command/environment/UnixShellEnvironment.java b/termux-shared/src/main/java/com/termux/shared/shell/command/environment/UnixShellEnvironment.java new file mode 100644 index 00000000..376879fa --- /dev/null +++ b/termux-shared/src/main/java/com/termux/shared/shell/command/environment/UnixShellEnvironment.java @@ -0,0 +1,80 @@ +package com.termux.shared.shell.command.environment; + +import android.content.Context; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import com.termux.shared.shell.ShellUtils; +import com.termux.shared.shell.command.ExecutionCommand; + +import java.util.HashMap; + +/** + * Environment for Unix-like systems. + * + * https://manpages.debian.org/testing/manpages/environ.7.en.html + * https://pubs.opengroup.org/onlinepubs/9699919799/basedefs/V1_chap08.html + */ +public abstract class UnixShellEnvironment implements IShellEnvironment { + + /** Environment variable for the terminal's colour capabilities. */ + public static final String ENV_COLORTERM = "COLORTERM"; + + /** Environment variable for the path of the user's home directory. */ + public static final String ENV_HOME = "HOME"; + + /** Environment variable for the locale category for native language, local customs, and coded + * character set in the absence of the LC_ALL and other LC_* environment variables. */ + public static final String ENV_LANG = "LANG"; + + /** Environment variable for the represent the sequence of directory paths separated with + * colons ":" that should be searched in for dynamic shared libraries to link programs against. */ + public static final String ENV_LD_LIBRARY_PATH = "LD_LIBRARY_PATH"; + + /** Environment variable for the represent the sequence of directory path prefixes separated with + * colons ":" that certain functions and utilities apply in searching for an executable file + * known only by a filename. */ + public static final String ENV_PATH = "PATH"; + + /** Environment variable for the absolute path of the current working directory. It shall not + * contain any components that are dot or dot-dot. The value is set by the cd utility, and by + * the sh utility during initialization. */ + public static final String ENV_PWD = "PWD"; + + /** Environment variable for the terminal type for which output is to be prepared. This information + * is used by utilities and application programs wishing to exploit special capabilities specific + * to a terminal. The format and allowable values of this environment variable are unspecified. */ + public static final String ENV_TERM = "TERM"; + + /** Environment variable for the path of a directory made available for programs that need a place + * to create temporary files. */ + public static final String ENV_TMPDIR = "TMPDIR"; + + + + + @NonNull + public abstract HashMap getEnvironment(@NonNull Context currentPackageContext, + boolean isFailSafe); + + @NonNull + @Override + public abstract String getDefaultWorkingDirectoryPath(); + + @NonNull + @Override + public abstract String getDefaultBinPath(); + + @NonNull + @Override + public String[] setupShellCommandArguments(@NonNull String executable, @Nullable String[] arguments) { + return ShellUtils.setupShellCommandArguments(executable, arguments); + } + + @NonNull + @Override + public abstract HashMap setupShellCommandEnvironment(@NonNull Context currentPackageContext, + @NonNull ExecutionCommand executionCommand); + +} diff --git a/termux-shared/src/main/java/com/termux/shared/shell/command/runner/app/AppShell.java b/termux-shared/src/main/java/com/termux/shared/shell/command/runner/app/AppShell.java index ad082392..614e6816 100644 --- a/termux-shared/src/main/java/com/termux/shared/shell/command/runner/app/AppShell.java +++ b/termux-shared/src/main/java/com/termux/shared/shell/command/runner/app/AppShell.java @@ -7,14 +7,16 @@ import android.system.OsConstants; import androidx.annotation.NonNull; +import com.google.common.base.Joiner; import com.termux.shared.R; import com.termux.shared.data.DataUtils; import com.termux.shared.shell.command.ExecutionCommand; +import com.termux.shared.shell.command.environment.ShellEnvironmentUtils; import com.termux.shared.shell.command.result.ResultData; import com.termux.shared.errors.Errno; import com.termux.shared.logger.Logger; import com.termux.shared.shell.command.ExecutionCommand.ExecutionState; -import com.termux.shared.shell.ShellEnvironmentClient; +import com.termux.shared.shell.command.environment.IShellEnvironment; import com.termux.shared.shell.ShellUtils; import com.termux.shared.shell.StreamGobbler; @@ -22,6 +24,9 @@ import java.io.DataOutputStream; import java.io.File; import java.io.IOException; import java.nio.charset.StandardCharsets; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; /** * A class that maintains info for background app shells run with {@link Runtime#exec(String[], String[], File)}. @@ -50,14 +55,16 @@ public final class AppShell { * The {@link ExecutionCommand#commandLabel}, {@link ExecutionCommand#arguments} and * {@link ExecutionCommand#workingDirectory} may optionally be set. * - * @param context The {@link Context} for operations. + * @param currentPackageContext The {@link Context} for operations. This must be the context for + * the current package and not the context of a `sharedUserId` package, + * since environment setup may be dependent on current package. * @param executionCommand The {@link ExecutionCommand} containing the information for execution command. * @param appShellClient The {@link AppShellClient} interface implementation. * The {@link AppShellClient#onAppShellExited(AppShell)} will * be called regardless of {@code isSynchronous} value but not if * {@code null} is returned by this method. This can * optionally be {@code null}. - * @param shellEnvironmentClient The {@link ShellEnvironmentClient} interface implementation. + * @param shellEnvironmentClient The {@link IShellEnvironment} interface implementation. * @param isSynchronous If set to {@code true}, then the command will be executed in the * caller thread and results returned synchronously in the {@link ExecutionCommand} * sub object of the {@link AppShell} returned. @@ -65,9 +72,9 @@ public final class AppShell { * asynchronously in the background and control is returned to the caller thread. * @return Returns the {@link AppShell}. This will be {@code null} if failed to start the execution command. */ - public static AppShell execute(@NonNull final Context context, @NonNull ExecutionCommand executionCommand, + public static AppShell execute(@NonNull final Context currentPackageContext, @NonNull ExecutionCommand executionCommand, final AppShellClient appShellClient, - @NonNull final ShellEnvironmentClient shellEnvironmentClient, + @NonNull final IShellEnvironment shellEnvironmentClient, final boolean isSynchronous) { if (executionCommand.executable == null || executionCommand.executable.isEmpty()) { executionCommand.setStateFailed(Errno.ERRNO_FAILED.getCode(), @@ -81,19 +88,27 @@ public final class AppShell { if (executionCommand.workingDirectory.isEmpty()) executionCommand.workingDirectory = "/"; - String[] env = shellEnvironmentClient.buildEnvironment(context, executionCommand.isFailsafe, executionCommand.workingDirectory); // Transform executable path to shell/session name, e.g. "/bin/do-something.sh" => "do-something.sh". String executableBasename = ShellUtils.getExecutableBasename(executionCommand.executable); if (executionCommand.shellName == null) executionCommand.shellName = executableBasename; - final String[] commandArray = shellEnvironmentClient.setupProcessArgs(executionCommand.executable, executionCommand.arguments); if (executionCommand.commandLabel == null) executionCommand.commandLabel = executableBasename; + // Setup command args + final String[] commandArray = shellEnvironmentClient.setupShellCommandArguments(executionCommand.executable, executionCommand.arguments); + + // Setup command environment + HashMap environment = shellEnvironmentClient.setupShellCommandEnvironment(currentPackageContext, + executionCommand); + List environmentList = ShellEnvironmentUtils.convertEnvironmentToEnviron(environment); + Collections.sort(environmentList); + String[] environmentArray = environmentList.toArray(new String[0]); + if (!executionCommand.setState(ExecutionState.EXECUTING)) { - executionCommand.setStateFailed(Errno.ERRNO_FAILED.getCode(), context.getString(R.string.error_failed_to_execute_app_shell_command, executionCommand.getCommandIdAndLabelLogString())); + executionCommand.setStateFailed(Errno.ERRNO_FAILED.getCode(), currentPackageContext.getString(R.string.error_failed_to_execute_app_shell_command, executionCommand.getCommandIdAndLabelLogString())); AppShell.processAppShellResult(null, executionCommand); return null; } @@ -101,22 +116,23 @@ public final class AppShell { // No need to log stdin if logging is disabled, like for app internal scripts Logger.logDebugExtended(LOG_TAG, ExecutionCommand.getExecutionInputLogString(executionCommand, true, Logger.shouldEnableLoggingForCustomLogLevel(executionCommand.backgroundCustomLogLevel))); + Logger.logVerboseExtended(LOG_TAG, "\"" + executionCommand.getCommandIdAndLabelLogString() + "\" AppShell Environment:\n" + + Joiner.on("\n").join(environmentArray)); // Exec the process final Process process; try { - process = Runtime.getRuntime().exec(commandArray, env, new File(executionCommand.workingDirectory)); + process = Runtime.getRuntime().exec(commandArray, environmentArray, new File(executionCommand.workingDirectory)); } catch (IOException e) { - executionCommand.setStateFailed(Errno.ERRNO_FAILED.getCode(), context.getString(R.string.error_failed_to_execute_app_shell_command, executionCommand.getCommandIdAndLabelLogString()), e); + executionCommand.setStateFailed(Errno.ERRNO_FAILED.getCode(), currentPackageContext.getString(R.string.error_failed_to_execute_app_shell_command, executionCommand.getCommandIdAndLabelLogString()), e); AppShell.processAppShellResult(null, executionCommand); return null; } final AppShell appShell = new AppShell(process, executionCommand, appShellClient); - if (isSynchronous) { try { - appShell.executeInner(context); + appShell.executeInner(currentPackageContext); } catch (IllegalThreadStateException | InterruptedException e) { // TODO: Should either of these be handled or returned? } @@ -125,7 +141,7 @@ public final class AppShell { @Override public void run() { try { - appShell.executeInner(context); + appShell.executeInner(currentPackageContext); } catch (IllegalThreadStateException | InterruptedException e) { // TODO: Should either of these be handled or returned? } @@ -274,10 +290,10 @@ public final class AppShell { * then the {@link AppShellClient#onAppShellExited(AppShell)} callback will be called. * * @param appShell The {@link AppShell}, which should be set if - * {@link #execute(Context, ExecutionCommand, AppShellClient, ShellEnvironmentClient, boolean)} + * {@link #execute(Context, ExecutionCommand, AppShellClient, IShellEnvironment, HashMap, boolean)} * successfully started the process. * @param executionCommand The {@link ExecutionCommand}, which should be set if - * {@link #execute(Context, ExecutionCommand, AppShellClient, ShellEnvironmentClient, boolean)} + * {@link #execute(Context, ExecutionCommand, AppShellClient, IShellEnvironment, HashMap, boolean)} * failed to start the process. */ private static void processAppShellResult(final AppShell appShell, ExecutionCommand executionCommand) { diff --git a/termux-shared/src/main/java/com/termux/shared/termux/TermuxUtils.java b/termux-shared/src/main/java/com/termux/shared/termux/TermuxUtils.java index 75e4a902..93ae2463 100644 --- a/termux-shared/src/main/java/com/termux/shared/termux/TermuxUtils.java +++ b/termux-shared/src/main/java/com/termux/shared/termux/TermuxUtils.java @@ -23,7 +23,7 @@ import com.termux.shared.shell.command.ExecutionCommand; import com.termux.shared.errors.Error; import com.termux.shared.android.PackageUtils; import com.termux.shared.termux.TermuxConstants.TERMUX_APP; -import com.termux.shared.termux.shell.TermuxShellEnvironmentClient; +import com.termux.shared.termux.shell.command.environment.TermuxShellEnvironment; import org.apache.commons.io.IOUtils; @@ -597,7 +597,7 @@ public class TermuxUtils { null, ExecutionCommand.Runner.APP_SHELL.getName(), false); executionCommand.commandLabel = "APT Info Command"; executionCommand.backgroundCustomLogLevel = Logger.LOG_LEVEL_OFF; - AppShell appShell = AppShell.execute(context, executionCommand, null, new TermuxShellEnvironmentClient(), true); + AppShell appShell = AppShell.execute(context, executionCommand, null, new TermuxShellEnvironment(), true); if (appShell == null || !executionCommand.isSuccessful() || executionCommand.resultData.exitCode != 0) { Logger.logErrorExtended(LOG_TAG, executionCommand.toString()); return null; @@ -656,7 +656,7 @@ public class TermuxUtils { null, logcatScript + "\n", "/", ExecutionCommand.Runner.APP_SHELL.getName(), true); executionCommand.commandLabel = "Logcat dump command"; executionCommand.backgroundCustomLogLevel = Logger.LOG_LEVEL_OFF; - AppShell appShell = AppShell.execute(context, executionCommand, null, new TermuxShellEnvironmentClient(), true); + AppShell appShell = AppShell.execute(context, executionCommand, null, new TermuxShellEnvironment(), true); if (appShell == null || !executionCommand.isSuccessful()) { Logger.logErrorExtended(LOG_TAG, executionCommand.toString()); return null; diff --git a/termux-shared/src/main/java/com/termux/shared/termux/file/TermuxFileUtils.java b/termux-shared/src/main/java/com/termux/shared/termux/file/TermuxFileUtils.java index 68c547dc..8af32d87 100644 --- a/termux-shared/src/main/java/com/termux/shared/termux/file/TermuxFileUtils.java +++ b/termux-shared/src/main/java/com/termux/shared/termux/file/TermuxFileUtils.java @@ -11,7 +11,7 @@ import com.termux.shared.markdown.MarkdownUtils; import com.termux.shared.shell.command.ExecutionCommand; import com.termux.shared.errors.Error; import com.termux.shared.file.FileUtilsErrno; -import com.termux.shared.termux.shell.TermuxShellEnvironmentClient; +import com.termux.shared.termux.shell.command.environment.TermuxShellEnvironment; import com.termux.shared.shell.command.runner.app.AppShell; import com.termux.shared.android.AndroidUtils; import com.termux.shared.termux.TermuxConstants; @@ -364,7 +364,7 @@ public class TermuxFileUtils { statScript.toString() + "\n", "/", ExecutionCommand.Runner.APP_SHELL.getName(), true); executionCommand.commandLabel = TermuxConstants.TERMUX_APP_NAME + " Files Stat Command"; executionCommand.backgroundCustomLogLevel = Logger.LOG_LEVEL_OFF; - AppShell appShell = AppShell.execute(context, executionCommand, null, new TermuxShellEnvironmentClient(), true); + AppShell appShell = AppShell.execute(context, executionCommand, null, new TermuxShellEnvironment(), true); if (appShell == null || !executionCommand.isSuccessful()) { Logger.logErrorExtended(LOG_TAG, executionCommand.toString()); return null; diff --git a/termux-shared/src/main/java/com/termux/shared/termux/shell/TermuxShellEnvironmentClient.java b/termux-shared/src/main/java/com/termux/shared/termux/shell/TermuxShellEnvironmentClient.java deleted file mode 100644 index 3d94a65c..00000000 --- a/termux-shared/src/main/java/com/termux/shared/termux/shell/TermuxShellEnvironmentClient.java +++ /dev/null @@ -1,35 +0,0 @@ -package com.termux.shared.termux.shell; - -import android.content.Context; - -import androidx.annotation.NonNull; - -import com.termux.shared.shell.ShellEnvironmentClient; - -public class TermuxShellEnvironmentClient implements ShellEnvironmentClient { - - @NonNull - @Override - public String getDefaultWorkingDirectoryPath() { - return TermuxShellUtils.getDefaultWorkingDirectoryPath(); - } - - @NonNull - @Override - public String getDefaultBinPath() { - return TermuxShellUtils.getDefaultBinPath(); - } - - @NonNull - @Override - public String[] buildEnvironment(Context currentPackageContext, boolean isFailSafe, String workingDirectory) { - return TermuxShellUtils.buildEnvironment(currentPackageContext, isFailSafe, workingDirectory); - } - - @NonNull - @Override - public String[] setupProcessArgs(@NonNull String fileToExecute, String[] arguments) { - return TermuxShellUtils.setupProcessArgs(fileToExecute, arguments); - } - -} diff --git a/termux-shared/src/main/java/com/termux/shared/termux/shell/TermuxShellUtils.java b/termux-shared/src/main/java/com/termux/shared/termux/shell/TermuxShellUtils.java index 35a0be27..3a005d8a 100644 --- a/termux-shared/src/main/java/com/termux/shared/termux/shell/TermuxShellUtils.java +++ b/termux-shared/src/main/java/com/termux/shared/termux/shell/TermuxShellUtils.java @@ -3,10 +3,10 @@ package com.termux.shared.termux.shell; import android.content.Context; import androidx.annotation.NonNull; +import androidx.annotation.Nullable; import com.termux.shared.errors.Error; import com.termux.shared.file.filesystem.FileTypes; -import com.termux.shared.termux.TermuxBootstrap; import com.termux.shared.termux.TermuxConstants; import com.termux.shared.file.FileUtils; import com.termux.shared.logger.Logger; @@ -25,31 +25,11 @@ import java.util.List; public class TermuxShellUtils { - public static String TERMUX_VERSION_NAME; - public static String TERMUX_IS_DEBUGGABLE_BUILD; - public static String TERMUX_APP_PID; - public static String TERMUX_APK_RELEASE; - public static Boolean TERMUX_APP_AM_SOCKET_SERVER_ENABLED; - - public static String TERMUX_API_VERSION_NAME; - - private static final String LOG_TAG = "TermuxShellUtils"; - public static String getDefaultWorkingDirectoryPath() { - return TermuxConstants.TERMUX_HOME_DIR_PATH; - } - - public static String getDefaultBinPath() { - return TermuxConstants.TERMUX_BIN_PREFIX_DIR_PATH; - } public static String[] buildEnvironment(Context currentPackageContext, boolean isFailSafe, String workingDirectory) { TermuxConstants.TERMUX_HOME_DIR.mkdirs(); - - if (workingDirectory == null || workingDirectory.isEmpty()) - workingDirectory = getDefaultWorkingDirectoryPath(); - List environment = new ArrayList<>(); loadTermuxEnvVariables(currentPackageContext); @@ -71,51 +51,14 @@ public class TermuxShellUtils { if (TERMUX_API_VERSION_NAME != null) environment.add("TERMUX_API_VERSION=" + TERMUX_API_VERSION_NAME); - - environment.add("TERM=xterm-256color"); - environment.add("COLORTERM=truecolor"); - environment.add("HOME=" + TermuxConstants.TERMUX_HOME_DIR_PATH); - environment.add("PREFIX=" + TermuxConstants.TERMUX_PREFIX_DIR_PATH); - environment.add("BOOTCLASSPATH=" + System.getenv("BOOTCLASSPATH")); - environment.add("ANDROID_ROOT=" + System.getenv("ANDROID_ROOT")); - environment.add("ANDROID_DATA=" + System.getenv("ANDROID_DATA")); - // EXTERNAL_STORAGE is needed for /system/bin/am to work on at least - // Samsung S7 - see https://plus.google.com/110070148244138185604/posts/gp8Lk3aCGp3. - environment.add("EXTERNAL_STORAGE=" + System.getenv("EXTERNAL_STORAGE")); - - // These variables are needed if running on Android 10 and higher. - addToEnvIfPresent(environment, "ANDROID_ART_ROOT"); - addToEnvIfPresent(environment, "DEX2OATBOOTCLASSPATH"); - addToEnvIfPresent(environment, "ANDROID_I18N_ROOT"); - addToEnvIfPresent(environment, "ANDROID_RUNTIME_ROOT"); - addToEnvIfPresent(environment, "ANDROID_TZDATA_ROOT"); - - if (isFailSafe) { - // Keep the default path so that system binaries can be used in the failsafe session. - environment.add("PATH= " + System.getenv("PATH")); - } else { - environment.add("LANG=en_US.UTF-8"); - environment.add("PWD=" + workingDirectory); - environment.add("TMPDIR=" + TermuxConstants.TERMUX_TMP_PREFIX_DIR_PATH); - if (TermuxBootstrap.isAppPackageVariantAPTAndroid5()) { - environment.add("PATH=" + TermuxConstants.TERMUX_BIN_PREFIX_DIR_PATH + ":" + TermuxConstants.TERMUX_BIN_PREFIX_DIR_PATH + "/applets"); - environment.add("LD_LIBRARY_PATH=" + TermuxConstants.TERMUX_LIB_PREFIX_DIR_PATH); - } else { - environment.add("PATH=" + TermuxConstants.TERMUX_BIN_PREFIX_DIR_PATH); - } - } - return environment.toArray(new String[0]); } - - public static void addToEnvIfPresent(List environment, String name) { - String value = System.getenv(name); - if (value != null) { - environment.add(name + "=" + value); - } - } - - public static String[] setupProcessArgs(@NonNull String fileToExecute, String[] arguments) { + /** + * Setup shell command arguments for the execute. The file interpreter may be prefixed to + * command arguments if needed. + */ + @NonNull + public static String[] setupShellCommandArguments(@NonNull String executable, @Nullable String[] arguments) { // The file to execute may either be: // - An elf file, in which we execute it directly. // - A script file without shebang, which we execute with our standard shell $PREFIX/bin/sh instead of the @@ -123,7 +66,7 @@ public class TermuxShellUtils { // - A file with shebang, which we try to handle with e.g. /bin/foo -> $PREFIX/bin/foo. String interpreter = null; try { - File file = new File(fileToExecute); + File file = new File(executable); try (FileInputStream in = new FileInputStream(file)) { byte[] buffer = new byte[256]; int bytesRead = in.read(buffer); @@ -140,9 +83,9 @@ public class TermuxShellUtils { // Skip whitespace after shebang. } else { // End of shebang. - String executable = builder.toString(); - if (executable.startsWith("/usr") || executable.startsWith("/bin")) { - String[] parts = executable.split("/"); + String shebangExecutable = builder.toString(); + if (shebangExecutable.startsWith("/usr") || shebangExecutable.startsWith("/bin")) { + String[] parts = shebangExecutable.split("/"); String binary = parts[parts.length - 1]; interpreter = TermuxConstants.TERMUX_BIN_PREFIX_DIR_PATH + "/" + binary; } @@ -164,11 +107,12 @@ public class TermuxShellUtils { List result = new ArrayList<>(); if (interpreter != null) result.add(interpreter); - result.add(fileToExecute); + result.add(executable); if (arguments != null) Collections.addAll(result, arguments); return result.toArray(new String[0]); } + /** Clear files under {@link TermuxConstants#TERMUX_TMP_PREFIX_DIR_PATH}. */ public static void clearTermuxTMPDIR(boolean onlyIfExists) { // Existence check before clearing may be required since clearDirectory() will automatically // re-create empty directory if doesn't exist, which should not be done for things like diff --git a/termux-shared/src/main/java/com/termux/shared/termux/shell/command/environment/TermuxShellEnvironment.java b/termux-shared/src/main/java/com/termux/shared/termux/shell/command/environment/TermuxShellEnvironment.java new file mode 100644 index 00000000..24d10f7b --- /dev/null +++ b/termux-shared/src/main/java/com/termux/shared/termux/shell/command/environment/TermuxShellEnvironment.java @@ -0,0 +1,74 @@ +package com.termux.shared.termux.shell.command.environment; + +import android.content.Context; + +import androidx.annotation.NonNull; + +import com.termux.shared.shell.command.ExecutionCommand; +import com.termux.shared.shell.command.environment.AndroidShellEnvironment; +import com.termux.shared.termux.TermuxBootstrap; +import com.termux.shared.termux.TermuxConstants; +import com.termux.shared.termux.shell.TermuxShellUtils; + +import java.nio.charset.Charset; +import java.util.HashMap; + +/** + * Environment for Termux. + */ +public class TermuxShellEnvironment extends AndroidShellEnvironment { + + private static final String LOG_TAG = "TermuxShellEnvironment"; + + /** Environment variable for the termux {@link TermuxConstants#TERMUX_PREFIX_DIR_PATH}. */ + public static final String ENV_PREFIX = "PREFIX"; + + + /** Get shell environment for Termux. */ + @NonNull + @Override + public HashMap getEnvironment(@NonNull Context currentPackageContext, boolean isFailSafe) { + + // Termux environment builds upon the Android environment + HashMap environment = super.getEnvironment(currentPackageContext, isFailSafe); + + environment.put(ENV_HOME, TermuxConstants.TERMUX_HOME_DIR_PATH); + environment.put(ENV_PREFIX, TermuxConstants.TERMUX_PREFIX_DIR_PATH); + + // If failsafe is not enabled, then we keep default PATH and TMPDIR so that system binaries can be used + if (!isFailSafe) { + environment.put(ENV_TMPDIR, TermuxConstants.TERMUX_TMP_PREFIX_DIR_PATH); + if (TermuxBootstrap.isAppPackageVariantAPTAndroid5()) { + // Termux in android 5/6 era shipped busybox binaries in applets directory + environment.put(ENV_PATH, TermuxConstants.TERMUX_BIN_PREFIX_DIR_PATH + ":" + TermuxConstants.TERMUX_BIN_PREFIX_DIR_PATH + "/applets"); + environment.put(ENV_LD_LIBRARY_PATH, TermuxConstants.TERMUX_LIB_PREFIX_DIR_PATH); + } else { + // Termux binaries on Android 7+ rely on DT_RUNPATH, so LD_LIBRARY_PATH should be unset by default + environment.put(ENV_PATH, TermuxConstants.TERMUX_BIN_PREFIX_DIR_PATH); + environment.remove(ENV_LD_LIBRARY_PATH); + } + } + + return environment; + } + + + @NonNull + @Override + public String getDefaultWorkingDirectoryPath() { + return TermuxConstants.TERMUX_HOME_DIR_PATH; + } + + @NonNull + @Override + public String getDefaultBinPath() { + return TermuxConstants.TERMUX_BIN_PREFIX_DIR_PATH; + } + + @NonNull + @Override + public String[] setupShellCommandArguments(@NonNull String executable, String[] arguments) { + return TermuxShellUtils.setupShellCommandArguments(executable, arguments); + } + +} diff --git a/termux-shared/src/main/java/com/termux/shared/termux/shell/command/runner/terminal/TermuxSession.java b/termux-shared/src/main/java/com/termux/shared/termux/shell/command/runner/terminal/TermuxSession.java index a356449b..d9ad1a61 100644 --- a/termux-shared/src/main/java/com/termux/shared/termux/shell/command/runner/terminal/TermuxSession.java +++ b/termux-shared/src/main/java/com/termux/shared/termux/shell/command/runner/terminal/TermuxSession.java @@ -5,17 +5,23 @@ import android.system.OsConstants; import androidx.annotation.NonNull; +import com.google.common.base.Joiner; import com.termux.shared.R; import com.termux.shared.shell.command.ExecutionCommand; +import com.termux.shared.shell.command.environment.ShellEnvironmentUtils; +import com.termux.shared.shell.command.environment.UnixShellEnvironment; import com.termux.shared.shell.command.result.ResultData; import com.termux.shared.errors.Errno; import com.termux.shared.logger.Logger; -import com.termux.shared.shell.ShellEnvironmentClient; +import com.termux.shared.shell.command.environment.IShellEnvironment; import com.termux.shared.shell.ShellUtils; import com.termux.terminal.TerminalSession; import com.termux.terminal.TerminalSessionClient; import java.io.File; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; /** * A class that maintains info for foreground Termux sessions. @@ -49,11 +55,13 @@ public class TermuxSession { * If {@link ExecutionCommand#executable} is {@code null}, then a default shell is automatically * chosen. * - * @param context The {@link Context} for operations. + * @param currentPackageContext The {@link Context} for operations. This must be the context for + * the current package and not the context of a `sharedUserId` package, + * since environment setup may be dependent on current package. * @param executionCommand The {@link ExecutionCommand} containing the information for execution command. * @param terminalSessionClient The {@link TerminalSessionClient} interface implementation. * @param termuxSessionClient The {@link TermuxSessionClient} interface implementation. - * @param shellEnvironmentClient The {@link ShellEnvironmentClient} interface implementation. + * @param shellEnvironmentClient The {@link IShellEnvironment} interface implementation. * @param setStdoutOnExit If set to {@code true}, then the {@link ResultData#stdout} * available in the {@link TermuxSessionClient#onTermuxSessionExited(TermuxSession)} * callback will be set to the {@link TerminalSession} transcript. The session @@ -63,9 +71,9 @@ public class TermuxSession { * since this requires extra processing to get it. * @return Returns the {@link TermuxSession}. This will be {@code null} if failed to start the execution command. */ - public static TermuxSession execute(@NonNull final Context context, @NonNull ExecutionCommand executionCommand, + public static TermuxSession execute(@NonNull final Context currentPackageContext, @NonNull ExecutionCommand executionCommand, @NonNull final TerminalSessionClient terminalSessionClient, final TermuxSessionClient termuxSessionClient, - @NonNull final ShellEnvironmentClient shellEnvironmentClient, + @NonNull final IShellEnvironment shellEnvironmentClient, final boolean setStdoutOnExit) { if (executionCommand.executable != null && executionCommand.executable.isEmpty()) executionCommand.executable = null; @@ -74,8 +82,6 @@ public class TermuxSession { if (executionCommand.workingDirectory.isEmpty()) executionCommand.workingDirectory = "/"; - String[] environment = shellEnvironmentClient.buildEnvironment(context, executionCommand.isFailsafe, executionCommand.workingDirectory); - String defaultBinPath = shellEnvironmentClient.getDefaultBinPath(); if (defaultBinPath.isEmpty()) defaultBinPath = "/system/bin"; @@ -108,30 +114,42 @@ public class TermuxSession { } - String[] processArgs = shellEnvironmentClient.setupProcessArgs(executionCommand.executable, executionCommand.arguments); + // Setup command args + String[] commandArgs = shellEnvironmentClient.setupShellCommandArguments(executionCommand.executable, executionCommand.arguments); - executionCommand.executable = processArgs[0]; + executionCommand.executable = commandArgs[0]; String processName = (isLoginShell ? "-" : "") + ShellUtils.getExecutableBasename(executionCommand.executable); - String[] arguments = new String[processArgs.length]; + String[] arguments = new String[commandArgs.length]; arguments[0] = processName; - if (processArgs.length > 1) System.arraycopy(processArgs, 1, arguments, 1, processArgs.length - 1); + if (commandArgs.length > 1) System.arraycopy(commandArgs, 1, arguments, 1, commandArgs.length - 1); executionCommand.arguments = arguments; if (executionCommand.commandLabel == null) executionCommand.commandLabel = processName; + // Setup command environment + HashMap environment = shellEnvironmentClient.setupShellCommandEnvironment(currentPackageContext, + executionCommand); + List environmentList = ShellEnvironmentUtils.convertEnvironmentToEnviron(environment); + Collections.sort(environmentList); + String[] environmentArray = environmentList.toArray(new String[0]); + if (!executionCommand.setState(ExecutionCommand.ExecutionState.EXECUTING)) { - executionCommand.setStateFailed(Errno.ERRNO_FAILED.getCode(), context.getString(R.string.error_failed_to_execute_termux_session_command, executionCommand.getCommandIdAndLabelLogString())); + executionCommand.setStateFailed(Errno.ERRNO_FAILED.getCode(), currentPackageContext.getString(R.string.error_failed_to_execute_termux_session_command, executionCommand.getCommandIdAndLabelLogString())); TermuxSession.processTermuxSessionResult(null, executionCommand); return null; } Logger.logDebugExtended(LOG_TAG, executionCommand.toString()); + Logger.logVerboseExtended(LOG_TAG, "\"" + executionCommand.getCommandIdAndLabelLogString() + "\" TermuxSession Environment:\n" + + Joiner.on("\n").join(environmentArray)); Logger.logDebug(LOG_TAG, "Running \"" + executionCommand.getCommandIdAndLabelLogString() + "\" TermuxSession"); - TerminalSession terminalSession = new TerminalSession(executionCommand.executable, executionCommand.workingDirectory, executionCommand.arguments, environment, executionCommand.terminalTranscriptRows, terminalSessionClient); + TerminalSession terminalSession = new TerminalSession(executionCommand.executable, + executionCommand.workingDirectory, executionCommand.arguments, environmentArray, + executionCommand.terminalTranscriptRows, terminalSessionClient); if (executionCommand.shellName != null) { terminalSession.mSessionName = executionCommand.shellName; @@ -219,10 +237,10 @@ public class TermuxSession { * callback will be called. * * @param termuxSession The {@link TermuxSession}, which should be set if - * {@link #execute(Context, ExecutionCommand, TerminalSessionClient, TermuxSessionClient, ShellEnvironmentClient, boolean)} + * {@link #execute(Context, ExecutionCommand, TerminalSessionClient, TermuxSessionClient, IShellEnvironment, HashMap, boolean)} * successfully started the process. * @param executionCommand The {@link ExecutionCommand}, which should be set if - * {@link #execute(Context, ExecutionCommand, TerminalSessionClient, TermuxSessionClient, ShellEnvironmentClient, boolean)} + * {@link #execute(Context, ExecutionCommand, TerminalSessionClient, TermuxSessionClient, IShellEnvironment, HashMap, boolean)} * failed to start the process. */ private static void processTermuxSessionResult(final TermuxSession termuxSession, ExecutionCommand executionCommand) {