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.
This commit is contained in:
agnostic-apollo 2022-06-11 19:08:20 +05:00
parent 0328d15ea7
commit f102ea20b2
14 changed files with 533 additions and 190 deletions

View File

@ -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

View File

@ -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);
}

View File

@ -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<String> 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;

View File

@ -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<String, String> getEnvironment(@NonNull Context currentPackageContext, boolean isFailSafe) {
HashMap<String, String> 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<String, String> setupShellCommandEnvironment(@NonNull Context currentPackageContext,
@NonNull ExecutionCommand executionCommand) {
HashMap<String, String> 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;
}
}

View File

@ -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<String, String> setupShellCommandEnvironment(@NonNull Context currentPackageContext,
@NonNull ExecutionCommand executionCommand);
}

View File

@ -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 <String>}.
*
* 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<String> convertEnvironmentToEnviron(@NonNull HashMap<String, String> environmentMap) {
List<String> 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<String, String> 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<String, String> 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<String, String> 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<String, String> 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());
}
}
}
}

View File

@ -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<String, String> 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<String, String> setupShellCommandEnvironment(@NonNull Context currentPackageContext,
@NonNull ExecutionCommand executionCommand);
}

View File

@ -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<String, String> environment = shellEnvironmentClient.setupShellCommandEnvironment(currentPackageContext,
executionCommand);
List<String> 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) {

View File

@ -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;

View File

@ -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;

View File

@ -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);
}
}

View File

@ -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<String> 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<String> 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<String> 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

View File

@ -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<String, String> getEnvironment(@NonNull Context currentPackageContext, boolean isFailSafe) {
// Termux environment builds upon the Android environment
HashMap<String, String> 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);
}
}

View File

@ -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<String, String> environment = shellEnvironmentClient.setupShellCommandEnvironment(currentPackageContext,
executionCommand);
List<String> 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) {