Skip to content

Commit 53c1a49

Browse files
Make TermuxTask and TermuxSession agnostic to termux environment
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.
1 parent 2aafcf8 commit 53c1a49

File tree

8 files changed

+265
-153
lines changed

8 files changed

+265
-153
lines changed

app/src/main/java/com/termux/app/TermuxService.java

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,9 @@
2525
import com.termux.app.utils.PluginUtils;
2626
import com.termux.shared.data.IntentUtils;
2727
import com.termux.shared.models.errors.Errno;
28+
import com.termux.shared.shell.ShellUtils;
29+
import com.termux.shared.shell.TermuxShellEnvironmentClient;
30+
import com.termux.shared.shell.TermuxShellUtils;
2831
import com.termux.shared.termux.TermuxConstants;
2932
import com.termux.shared.termux.TermuxConstants.TERMUX_APP.TERMUX_ACTIVITY;
3033
import com.termux.shared.termux.TermuxConstants.TERMUX_APP.TERMUX_SERVICE;
@@ -34,7 +37,6 @@
3437
import com.termux.shared.logger.Logger;
3538
import com.termux.shared.notification.NotificationUtils;
3639
import com.termux.shared.packages.PermissionUtils;
37-
import com.termux.shared.shell.ShellUtils;
3840
import com.termux.shared.data.DataUtils;
3941
import com.termux.shared.models.ExecutionCommand;
4042
import com.termux.shared.shell.TermuxTask;
@@ -162,7 +164,7 @@ public int onStartCommand(Intent intent, int flags, int startId) {
162164
public void onDestroy() {
163165
Logger.logVerbose(LOG_TAG, "onDestroy");
164166

165-
ShellUtils.clearTermuxTMPDIR(true);
167+
TermuxShellUtils.clearTermuxTMPDIR(true);
166168

167169
actionReleaseWakeLock(false);
168170
if (!mWantsToStop)
@@ -426,7 +428,7 @@ public synchronized TermuxTask createTermuxTask(ExecutionCommand executionComman
426428
if (Logger.getLogLevel() >= Logger.LOG_LEVEL_VERBOSE)
427429
Logger.logVerbose(LOG_TAG, executionCommand.toString());
428430

429-
TermuxTask newTermuxTask = TermuxTask.execute(this, executionCommand, this, false);
431+
TermuxTask newTermuxTask = TermuxTask.execute(this, executionCommand, this, new TermuxShellEnvironmentClient(), false);
430432
if (newTermuxTask == null) {
431433
Logger.logError(LOG_TAG, "Failed to execute new TermuxTask command for:\n" + executionCommand.getCommandIdAndLabelLogString());
432434
// If the execution command was started for a plugin, then process the error
@@ -522,7 +524,7 @@ public synchronized TermuxSession createTermuxSession(ExecutionCommand execution
522524
// Otherwise if command was manually started by the user like by adding a new terminal session,
523525
// then no need to set stdout
524526
executionCommand.terminalTranscriptRows = getTerminalTranscriptRows();
525-
TermuxSession newTermuxSession = TermuxSession.execute(this, executionCommand, getTermuxTerminalSessionClient(), this, sessionName, executionCommand.isPluginExecutionCommand);
527+
TermuxSession newTermuxSession = TermuxSession.execute(this, executionCommand, getTermuxTerminalSessionClient(), this, new TermuxShellEnvironmentClient(), sessionName, executionCommand.isPluginExecutionCommand);
526528
if (newTermuxSession == null) {
527529
Logger.logError(LOG_TAG, "Failed to execute new TermuxSession command for:\n" + executionCommand.getCommandIdAndLabelLogString());
528530
// If the execution command was started for a plugin, then process the error
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
package com.termux.shared.shell;
2+
3+
import android.content.Context;
4+
5+
import androidx.annotation.NonNull;
6+
7+
public interface ShellEnvironmentClient {
8+
9+
/**
10+
* Get the default working directory path for the environment in case the path that was passed
11+
* was {@code null} or empty.
12+
*
13+
* @return Should return the default working directory path.
14+
*/
15+
@NonNull
16+
String getDefaultWorkingDirectoryPath();
17+
18+
/**
19+
* Get the default "/bin" path, likely $PREFIX/bin.
20+
*
21+
* @return Should return the "/bin" path.
22+
*/
23+
@NonNull
24+
String getDefaultBinPath();
25+
26+
/**
27+
* Build the shell environment to be used for commands.
28+
*
29+
* @param currentPackageContext The {@link Context} for the current package.
30+
* @param isFailSafe If running a failsafe session.
31+
* @param workingDirectory The working directory for the environment.
32+
* @return Should return the build environment.
33+
*/
34+
@NonNull
35+
String[] buildEnvironment(Context currentPackageContext, boolean isFailSafe, String workingDirectory);
36+
37+
/**
38+
* Setup process arguments for the file to execute, like interpreter, etc.
39+
*
40+
* @param fileToExecute The file to execute.
41+
* @param arguments The arguments to pass to the executable.
42+
* @return Should return the final process arguments.
43+
*/
44+
@NonNull
45+
String[] setupProcessArgs(@NonNull String fileToExecute, String[] arguments);
46+
47+
}

termux-shared/src/main/java/com/termux/shared/shell/ShellUtils.java

Lines changed: 0 additions & 134 deletions
Original file line numberDiff line numberDiff line change
@@ -1,82 +1,13 @@
11
package com.termux.shared.shell;
22

3-
import android.content.Context;
4-
5-
import androidx.annotation.NonNull;
6-
7-
import com.termux.shared.models.errors.Error;
8-
import com.termux.shared.termux.TermuxConstants;
9-
import com.termux.shared.file.FileUtils;
10-
import com.termux.shared.logger.Logger;
11-
import com.termux.shared.packages.PackageUtils;
12-
import com.termux.shared.termux.TermuxUtils;
133
import com.termux.terminal.TerminalBuffer;
144
import com.termux.terminal.TerminalEmulator;
155
import com.termux.terminal.TerminalSession;
166

17-
import java.io.File;
18-
import java.io.FileInputStream;
19-
import java.io.IOException;
207
import java.lang.reflect.Field;
21-
import java.util.ArrayList;
22-
import java.util.Collections;
23-
import java.util.List;
248

259
public class ShellUtils {
2610

27-
public static String[] buildEnvironment(Context currentPackageContext, boolean isFailSafe, String workingDirectory) {
28-
TermuxConstants.TERMUX_HOME_DIR.mkdirs();
29-
30-
if (workingDirectory == null || workingDirectory.isEmpty()) workingDirectory = TermuxConstants.TERMUX_HOME_DIR_PATH;
31-
32-
List<String> environment = new ArrayList<>();
33-
34-
// This function may be called by a different package like a plugin, so we get version for Termux package via its context
35-
Context termuxPackageContext = TermuxUtils.getTermuxPackageContext(currentPackageContext);
36-
if (termuxPackageContext != null) {
37-
String termuxVersionName = PackageUtils.getVersionNameForPackage(termuxPackageContext);
38-
if (termuxVersionName != null)
39-
environment.add("TERMUX_VERSION=" + termuxVersionName);
40-
}
41-
42-
environment.add("TERM=xterm-256color");
43-
environment.add("COLORTERM=truecolor");
44-
environment.add("HOME=" + TermuxConstants.TERMUX_HOME_DIR_PATH);
45-
environment.add("PREFIX=" + TermuxConstants.TERMUX_PREFIX_DIR_PATH);
46-
environment.add("BOOTCLASSPATH=" + System.getenv("BOOTCLASSPATH"));
47-
environment.add("ANDROID_ROOT=" + System.getenv("ANDROID_ROOT"));
48-
environment.add("ANDROID_DATA=" + System.getenv("ANDROID_DATA"));
49-
// EXTERNAL_STORAGE is needed for /system/bin/am to work on at least
50-
// Samsung S7 - see https://plus.google.com/110070148244138185604/posts/gp8Lk3aCGp3.
51-
environment.add("EXTERNAL_STORAGE=" + System.getenv("EXTERNAL_STORAGE"));
52-
53-
// These variables are needed if running on Android 10 and higher.
54-
addToEnvIfPresent(environment, "ANDROID_ART_ROOT");
55-
addToEnvIfPresent(environment, "DEX2OATBOOTCLASSPATH");
56-
addToEnvIfPresent(environment, "ANDROID_I18N_ROOT");
57-
addToEnvIfPresent(environment, "ANDROID_RUNTIME_ROOT");
58-
addToEnvIfPresent(environment, "ANDROID_TZDATA_ROOT");
59-
60-
if (isFailSafe) {
61-
// Keep the default path so that system binaries can be used in the failsafe session.
62-
environment.add("PATH= " + System.getenv("PATH"));
63-
} else {
64-
environment.add("LANG=en_US.UTF-8");
65-
environment.add("PATH=" + TermuxConstants.TERMUX_BIN_PREFIX_DIR_PATH);
66-
environment.add("PWD=" + workingDirectory);
67-
environment.add("TMPDIR=" + TermuxConstants.TERMUX_TMP_PREFIX_DIR_PATH);
68-
}
69-
70-
return environment.toArray(new String[0]);
71-
}
72-
73-
public static void addToEnvIfPresent(List<String> environment, String name) {
74-
String value = System.getenv(name);
75-
if (value != null) {
76-
environment.add(name + "=" + value);
77-
}
78-
}
79-
8011
public static int getPid(Process p) {
8112
try {
8213
Field f = p.getClass().getDeclaredField("pid");
@@ -91,77 +22,12 @@ public static int getPid(Process p) {
9122
}
9223
}
9324

94-
public static String[] setupProcessArgs(@NonNull String fileToExecute, String[] arguments) {
95-
// The file to execute may either be:
96-
// - An elf file, in which we execute it directly.
97-
// - A script file without shebang, which we execute with our standard shell $PREFIX/bin/sh instead of the
98-
// system /system/bin/sh. The system shell may vary and may not work at all due to LD_LIBRARY_PATH.
99-
// - A file with shebang, which we try to handle with e.g. /bin/foo -> $PREFIX/bin/foo.
100-
String interpreter = null;
101-
try {
102-
File file = new File(fileToExecute);
103-
try (FileInputStream in = new FileInputStream(file)) {
104-
byte[] buffer = new byte[256];
105-
int bytesRead = in.read(buffer);
106-
if (bytesRead > 4) {
107-
if (buffer[0] == 0x7F && buffer[1] == 'E' && buffer[2] == 'L' && buffer[3] == 'F') {
108-
// Elf file, do nothing.
109-
} else if (buffer[0] == '#' && buffer[1] == '!') {
110-
// Try to parse shebang.
111-
StringBuilder builder = new StringBuilder();
112-
for (int i = 2; i < bytesRead; i++) {
113-
char c = (char) buffer[i];
114-
if (c == ' ' || c == '\n') {
115-
if (builder.length() == 0) {
116-
// Skip whitespace after shebang.
117-
} else {
118-
// End of shebang.
119-
String executable = builder.toString();
120-
if (executable.startsWith("/usr") || executable.startsWith("/bin")) {
121-
String[] parts = executable.split("/");
122-
String binary = parts[parts.length - 1];
123-
interpreter = TermuxConstants.TERMUX_BIN_PREFIX_DIR_PATH + "/" + binary;
124-
}
125-
break;
126-
}
127-
} else {
128-
builder.append(c);
129-
}
130-
}
131-
} else {
132-
// No shebang and no ELF, use standard shell.
133-
interpreter = TermuxConstants.TERMUX_BIN_PREFIX_DIR_PATH + "/sh";
134-
}
135-
}
136-
}
137-
} catch (IOException e) {
138-
// Ignore.
139-
}
140-
141-
List<String> result = new ArrayList<>();
142-
if (interpreter != null) result.add(interpreter);
143-
result.add(fileToExecute);
144-
if (arguments != null) Collections.addAll(result, arguments);
145-
return result.toArray(new String[0]);
146-
}
147-
14825
public static String getExecutableBasename(String executable) {
14926
if (executable == null) return null;
15027
int lastSlash = executable.lastIndexOf('/');
15128
return (lastSlash == -1) ? executable : executable.substring(lastSlash + 1);
15229
}
15330

154-
public static void clearTermuxTMPDIR(boolean onlyIfExists) {
155-
if(onlyIfExists && !FileUtils.directoryFileExists(TermuxConstants.TERMUX_TMP_PREFIX_DIR_PATH, false))
156-
return;
157-
158-
Error error;
159-
error = FileUtils.clearDirectory("$TMPDIR", FileUtils.getCanonicalPath(TermuxConstants.TERMUX_TMP_PREFIX_DIR_PATH, null));
160-
if (error != null) {
161-
Logger.logErrorExtended(error.toString());
162-
}
163-
}
164-
16531
public static String getTerminalSessionTranscriptText(TerminalSession terminalSession, boolean linesJoined, boolean trim) {
16632
if (terminalSession == null) return null;
16733

termux-shared/src/main/java/com/termux/shared/shell/TermuxSession.java

Lines changed: 15 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@
99
import com.termux.shared.models.ExecutionCommand;
1010
import com.termux.shared.models.ResultData;
1111
import com.termux.shared.models.errors.Errno;
12-
import com.termux.shared.termux.TermuxConstants;
1312
import com.termux.shared.logger.Logger;
1413
import com.termux.terminal.TerminalSession;
1514
import com.termux.terminal.TerminalSessionClient;
@@ -52,6 +51,7 @@ private TermuxSession(@NonNull final TerminalSession terminalSession, @NonNull f
5251
* @param executionCommand The {@link ExecutionCommand} containing the information for execution command.
5352
* @param terminalSessionClient The {@link TerminalSessionClient} interface implementation.
5453
* @param termuxSessionClient The {@link TermuxSessionClient} interface implementation.
54+
* @param shellEnvironmentClient The {@link ShellEnvironmentClient} interface implementation.
5555
* @param sessionName The optional {@link TerminalSession} name.
5656
* @param setStdoutOnExit If set to {@code true}, then the {@link ResultData#stdout}
5757
* available in the {@link TermuxSessionClient#onTermuxSessionExited(TermuxSession)}
@@ -64,16 +64,24 @@ private TermuxSession(@NonNull final TerminalSession terminalSession, @NonNull f
6464
*/
6565
public static TermuxSession execute(@NonNull final Context context, @NonNull ExecutionCommand executionCommand,
6666
@NonNull final TerminalSessionClient terminalSessionClient, final TermuxSessionClient termuxSessionClient,
67+
@NonNull final ShellEnvironmentClient shellEnvironmentClient,
6768
final String sessionName, final boolean setStdoutOnExit) {
68-
if (executionCommand.workingDirectory == null || executionCommand.workingDirectory.isEmpty()) executionCommand.workingDirectory = TermuxConstants.TERMUX_HOME_DIR_PATH;
69+
if (executionCommand.workingDirectory == null || executionCommand.workingDirectory.isEmpty())
70+
executionCommand.workingDirectory = shellEnvironmentClient.getDefaultWorkingDirectoryPath();
71+
if (executionCommand.workingDirectory.isEmpty())
72+
executionCommand.workingDirectory = "/";
6973

70-
String[] environment = ShellUtils.buildEnvironment(context, executionCommand.isFailsafe, executionCommand.workingDirectory);
74+
String[] environment = shellEnvironmentClient.buildEnvironment(context, executionCommand.isFailsafe, executionCommand.workingDirectory);
75+
76+
String defaultBinPath = shellEnvironmentClient.getDefaultBinPath();
77+
if (defaultBinPath.isEmpty())
78+
defaultBinPath = "/system/bin";
7179

7280
boolean isLoginShell = false;
7381
if (executionCommand.executable == null) {
7482
if (!executionCommand.isFailsafe) {
7583
for (String shellBinary : new String[]{"login", "bash", "zsh"}) {
76-
File shellFile = new File(TermuxConstants.TERMUX_BIN_PREFIX_DIR_PATH, shellBinary);
84+
File shellFile = new File(defaultBinPath, shellBinary);
7785
if (shellFile.canExecute()) {
7886
executionCommand.executable = shellFile.getAbsolutePath();
7987
break;
@@ -88,7 +96,7 @@ public static TermuxSession execute(@NonNull final Context context, @NonNull Exe
8896
isLoginShell = true;
8997
}
9098

91-
String[] processArgs = ShellUtils.setupProcessArgs(executionCommand.executable, executionCommand.arguments);
99+
String[] processArgs = shellEnvironmentClient.setupProcessArgs(executionCommand.executable, executionCommand.arguments);
92100

93101
executionCommand.executable = processArgs[0];
94102
String processName = (isLoginShell ? "-" : "") + ShellUtils.getExecutableBasename(executionCommand.executable);
@@ -199,10 +207,10 @@ public void killIfExecuting(@NonNull final Context context, boolean processResul
199207
* callback will be called.
200208
*
201209
* @param termuxSession The {@link TermuxSession}, which should be set if
202-
* {@link #execute(Context, ExecutionCommand, TerminalSessionClient, TermuxSessionClient, String, boolean)}
210+
* {@link #execute(Context, ExecutionCommand, TerminalSessionClient, TermuxSessionClient, ShellEnvironmentClient, String, boolean)}
203211
* successfully started the process.
204212
* @param executionCommand The {@link ExecutionCommand}, which should be set if
205-
* {@link #execute(Context, ExecutionCommand, TerminalSessionClient, TermuxSessionClient, String, boolean)}
213+
* {@link #execute(Context, ExecutionCommand, TerminalSessionClient, TermuxSessionClient, ShellEnvironmentClient, String, boolean)}
206214
* failed to start the process.
207215
*/
208216
private static void processTermuxSessionResult(final TermuxSession termuxSession, ExecutionCommand executionCommand) {
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
package com.termux.shared.shell;
2+
3+
import android.content.Context;
4+
5+
import androidx.annotation.NonNull;
6+
7+
public class TermuxShellEnvironmentClient implements ShellEnvironmentClient {
8+
9+
@NonNull
10+
@Override
11+
public String getDefaultWorkingDirectoryPath() {
12+
return TermuxShellUtils.getDefaultWorkingDirectoryPath();
13+
}
14+
15+
@NonNull
16+
@Override
17+
public String getDefaultBinPath() {
18+
return TermuxShellUtils.getDefaultBinPath();
19+
}
20+
21+
@NonNull
22+
@Override
23+
public String[] buildEnvironment(Context currentPackageContext, boolean isFailSafe, String workingDirectory) {
24+
return TermuxShellUtils.buildEnvironment(currentPackageContext, isFailSafe, workingDirectory);
25+
}
26+
27+
@NonNull
28+
@Override
29+
public String[] setupProcessArgs(@NonNull String fileToExecute, String[] arguments) {
30+
return TermuxShellUtils.setupProcessArgs(fileToExecute, arguments);
31+
}
32+
33+
}

0 commit comments

Comments
 (0)