Skip to content

Commit

Permalink
Make TermuxTask and TermuxSession agnostic to termux environment
Browse files Browse the repository at this point in the history
Those classes shouldn't be tied to termux environment like variables, interpreters and working directory since commands may need to be executed with a different environment like android's or with a different logic. Now both classes use the ShellEnvironmentClient interface to dynamically get the environment to be used which currently for Termux's case is implemented by TermuxShellEnvironmentClient which is just a wrapper for TermuxShellUtils since later implements static functions.
  • Loading branch information
agnostic-apollo committed Jun 28, 2021
1 parent 2aafcf8 commit 53c1a49
Show file tree
Hide file tree
Showing 8 changed files with 265 additions and 153 deletions.
10 changes: 6 additions & 4 deletions app/src/main/java/com/termux/app/TermuxService.java
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,9 @@
import com.termux.app.utils.PluginUtils;
import com.termux.shared.data.IntentUtils;
import com.termux.shared.models.errors.Errno;
import com.termux.shared.shell.ShellUtils;
import com.termux.shared.shell.TermuxShellEnvironmentClient;
import com.termux.shared.shell.TermuxShellUtils;
import com.termux.shared.termux.TermuxConstants;
import com.termux.shared.termux.TermuxConstants.TERMUX_APP.TERMUX_ACTIVITY;
import com.termux.shared.termux.TermuxConstants.TERMUX_APP.TERMUX_SERVICE;
Expand All @@ -34,7 +37,6 @@
import com.termux.shared.logger.Logger;
import com.termux.shared.notification.NotificationUtils;
import com.termux.shared.packages.PermissionUtils;
import com.termux.shared.shell.ShellUtils;
import com.termux.shared.data.DataUtils;
import com.termux.shared.models.ExecutionCommand;
import com.termux.shared.shell.TermuxTask;
Expand Down Expand Up @@ -162,7 +164,7 @@ public int onStartCommand(Intent intent, int flags, int startId) {
public void onDestroy() {
Logger.logVerbose(LOG_TAG, "onDestroy");

ShellUtils.clearTermuxTMPDIR(true);
TermuxShellUtils.clearTermuxTMPDIR(true);

actionReleaseWakeLock(false);
if (!mWantsToStop)
Expand Down Expand Up @@ -426,7 +428,7 @@ public synchronized TermuxTask createTermuxTask(ExecutionCommand executionComman
if (Logger.getLogLevel() >= Logger.LOG_LEVEL_VERBOSE)
Logger.logVerbose(LOG_TAG, executionCommand.toString());

TermuxTask newTermuxTask = TermuxTask.execute(this, executionCommand, this, false);
TermuxTask newTermuxTask = TermuxTask.execute(this, executionCommand, this, new TermuxShellEnvironmentClient(), 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
Expand Down Expand Up @@ -522,7 +524,7 @@ public synchronized TermuxSession createTermuxSession(ExecutionCommand execution
// Otherwise if command was manually started by the user like by adding a new terminal session,
// then no need to set stdout
executionCommand.terminalTranscriptRows = getTerminalTranscriptRows();
TermuxSession newTermuxSession = TermuxSession.execute(this, executionCommand, getTermuxTerminalSessionClient(), this, sessionName, executionCommand.isPluginExecutionCommand);
TermuxSession newTermuxSession = TermuxSession.execute(this, executionCommand, getTermuxTerminalSessionClient(), this, new TermuxShellEnvironmentClient(), sessionName, 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
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
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);

}
134 changes: 0 additions & 134 deletions termux-shared/src/main/java/com/termux/shared/shell/ShellUtils.java
Original file line number Diff line number Diff line change
@@ -1,82 +1,13 @@
package com.termux.shared.shell;

import android.content.Context;

import androidx.annotation.NonNull;

import com.termux.shared.models.errors.Error;
import com.termux.shared.termux.TermuxConstants;
import com.termux.shared.file.FileUtils;
import com.termux.shared.logger.Logger;
import com.termux.shared.packages.PackageUtils;
import com.termux.shared.termux.TermuxUtils;
import com.termux.terminal.TerminalBuffer;
import com.termux.terminal.TerminalEmulator;
import com.termux.terminal.TerminalSession;

import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

public class ShellUtils {

public static String[] buildEnvironment(Context currentPackageContext, boolean isFailSafe, String workingDirectory) {
TermuxConstants.TERMUX_HOME_DIR.mkdirs();

if (workingDirectory == null || workingDirectory.isEmpty()) workingDirectory = TermuxConstants.TERMUX_HOME_DIR_PATH;

List<String> environment = new ArrayList<>();

// This function may be called by a different package like a plugin, so we get version for Termux package via its context
Context termuxPackageContext = TermuxUtils.getTermuxPackageContext(currentPackageContext);
if (termuxPackageContext != null) {
String termuxVersionName = PackageUtils.getVersionNameForPackage(termuxPackageContext);
if (termuxVersionName != null)
environment.add("TERMUX_VERSION=" + termuxVersionName);
}

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("PATH=" + TermuxConstants.TERMUX_BIN_PREFIX_DIR_PATH);
environment.add("PWD=" + workingDirectory);
environment.add("TMPDIR=" + TermuxConstants.TERMUX_TMP_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 int getPid(Process p) {
try {
Field f = p.getClass().getDeclaredField("pid");
Expand All @@ -91,77 +22,12 @@ public static int getPid(Process p) {
}
}

public static String[] setupProcessArgs(@NonNull String fileToExecute, 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
// system /system/bin/sh. The system shell may vary and may not work at all due to LD_LIBRARY_PATH.
// - 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);
try (FileInputStream in = new FileInputStream(file)) {
byte[] buffer = new byte[256];
int bytesRead = in.read(buffer);
if (bytesRead > 4) {
if (buffer[0] == 0x7F && buffer[1] == 'E' && buffer[2] == 'L' && buffer[3] == 'F') {
// Elf file, do nothing.
} else if (buffer[0] == '#' && buffer[1] == '!') {
// Try to parse shebang.
StringBuilder builder = new StringBuilder();
for (int i = 2; i < bytesRead; i++) {
char c = (char) buffer[i];
if (c == ' ' || c == '\n') {
if (builder.length() == 0) {
// Skip whitespace after shebang.
} else {
// End of shebang.
String executable = builder.toString();
if (executable.startsWith("/usr") || executable.startsWith("/bin")) {
String[] parts = executable.split("/");
String binary = parts[parts.length - 1];
interpreter = TermuxConstants.TERMUX_BIN_PREFIX_DIR_PATH + "/" + binary;
}
break;
}
} else {
builder.append(c);
}
}
} else {
// No shebang and no ELF, use standard shell.
interpreter = TermuxConstants.TERMUX_BIN_PREFIX_DIR_PATH + "/sh";
}
}
}
} catch (IOException e) {
// Ignore.
}

List<String> result = new ArrayList<>();
if (interpreter != null) result.add(interpreter);
result.add(fileToExecute);
if (arguments != null) Collections.addAll(result, arguments);
return result.toArray(new String[0]);
}

public static String getExecutableBasename(String executable) {
if (executable == null) return null;
int lastSlash = executable.lastIndexOf('/');
return (lastSlash == -1) ? executable : executable.substring(lastSlash + 1);
}

public static void clearTermuxTMPDIR(boolean onlyIfExists) {
if(onlyIfExists && !FileUtils.directoryFileExists(TermuxConstants.TERMUX_TMP_PREFIX_DIR_PATH, false))
return;

Error error;
error = FileUtils.clearDirectory("$TMPDIR", FileUtils.getCanonicalPath(TermuxConstants.TERMUX_TMP_PREFIX_DIR_PATH, null));
if (error != null) {
Logger.logErrorExtended(error.toString());
}
}

public static String getTerminalSessionTranscriptText(TerminalSession terminalSession, boolean linesJoined, boolean trim) {
if (terminalSession == null) return null;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@
import com.termux.shared.models.ExecutionCommand;
import com.termux.shared.models.ResultData;
import com.termux.shared.models.errors.Errno;
import com.termux.shared.termux.TermuxConstants;
import com.termux.shared.logger.Logger;
import com.termux.terminal.TerminalSession;
import com.termux.terminal.TerminalSessionClient;
Expand Down Expand Up @@ -52,6 +51,7 @@ private TermuxSession(@NonNull final TerminalSession terminalSession, @NonNull f
* @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 sessionName The optional {@link TerminalSession} name.
* @param setStdoutOnExit If set to {@code true}, then the {@link ResultData#stdout}
* available in the {@link TermuxSessionClient#onTermuxSessionExited(TermuxSession)}
Expand All @@ -64,16 +64,24 @@ private TermuxSession(@NonNull final TerminalSession terminalSession, @NonNull f
*/
public static TermuxSession execute(@NonNull final Context context, @NonNull ExecutionCommand executionCommand,
@NonNull final TerminalSessionClient terminalSessionClient, final TermuxSessionClient termuxSessionClient,
@NonNull final ShellEnvironmentClient shellEnvironmentClient,
final String sessionName, final boolean setStdoutOnExit) {
if (executionCommand.workingDirectory == null || executionCommand.workingDirectory.isEmpty()) executionCommand.workingDirectory = TermuxConstants.TERMUX_HOME_DIR_PATH;
if (executionCommand.workingDirectory == null || executionCommand.workingDirectory.isEmpty())
executionCommand.workingDirectory = shellEnvironmentClient.getDefaultWorkingDirectoryPath();
if (executionCommand.workingDirectory.isEmpty())
executionCommand.workingDirectory = "/";

String[] environment = ShellUtils.buildEnvironment(context, executionCommand.isFailsafe, executionCommand.workingDirectory);
String[] environment = shellEnvironmentClient.buildEnvironment(context, executionCommand.isFailsafe, executionCommand.workingDirectory);

String defaultBinPath = shellEnvironmentClient.getDefaultBinPath();
if (defaultBinPath.isEmpty())
defaultBinPath = "/system/bin";

boolean isLoginShell = false;
if (executionCommand.executable == null) {
if (!executionCommand.isFailsafe) {
for (String shellBinary : new String[]{"login", "bash", "zsh"}) {
File shellFile = new File(TermuxConstants.TERMUX_BIN_PREFIX_DIR_PATH, shellBinary);
File shellFile = new File(defaultBinPath, shellBinary);
if (shellFile.canExecute()) {
executionCommand.executable = shellFile.getAbsolutePath();
break;
Expand All @@ -88,7 +96,7 @@ public static TermuxSession execute(@NonNull final Context context, @NonNull Exe
isLoginShell = true;
}

String[] processArgs = ShellUtils.setupProcessArgs(executionCommand.executable, executionCommand.arguments);
String[] processArgs = shellEnvironmentClient.setupProcessArgs(executionCommand.executable, executionCommand.arguments);

executionCommand.executable = processArgs[0];
String processName = (isLoginShell ? "-" : "") + ShellUtils.getExecutableBasename(executionCommand.executable);
Expand Down Expand Up @@ -199,10 +207,10 @@ public void killIfExecuting(@NonNull final Context context, boolean processResul
* callback will be called.
*
* @param termuxSession The {@link TermuxSession}, which should be set if
* {@link #execute(Context, ExecutionCommand, TerminalSessionClient, TermuxSessionClient, String, boolean)}
* {@link #execute(Context, ExecutionCommand, TerminalSessionClient, TermuxSessionClient, ShellEnvironmentClient, String, boolean)}
* successfully started the process.
* @param executionCommand The {@link ExecutionCommand}, which should be set if
* {@link #execute(Context, ExecutionCommand, TerminalSessionClient, TermuxSessionClient, String, boolean)}
* {@link #execute(Context, ExecutionCommand, TerminalSessionClient, TermuxSessionClient, ShellEnvironmentClient, String, boolean)}
* failed to start the process.
*/
private static void processTermuxSessionResult(final TermuxSession termuxSession, ExecutionCommand executionCommand) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package com.termux.shared.shell;

import android.content.Context;

import androidx.annotation.NonNull;

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

}
Loading

0 comments on commit 53c1a49

Please sign in to comment.