Added: NativeShell as a task runner that is only available ofer the plugin system.

This commit is contained in:
tareksander 2022-06-11 16:52:26 +02:00
parent 9bc59ac0a5
commit 44267d6582
9 changed files with 429 additions and 46 deletions

View File

@ -22,6 +22,7 @@ import com.termux.R;
import com.termux.app.event.SystemEventReceiver;
import com.termux.app.terminal.TermuxTerminalSessionActivityClient;
import com.termux.app.terminal.TermuxTerminalSessionServiceClient;
import com.termux.shared.shell.command.runner.nativerunner.NativeShell;
import com.termux.shared.termux.plugins.TermuxPluginUtils;
import com.termux.shared.data.IntentUtils;
import com.termux.shared.net.uri.UriUtils;
@ -287,6 +288,11 @@ public final class TermuxService extends Service implements AppShell.AppShellCli
else
mShellManager.mTermuxTasks.remove(termuxTasks.get(i));
}
List<NativeShell> termuxNativeTasks = new ArrayList<>(mShellManager.mTermuxNativeTasks);
for (int i = 0; i < termuxNativeTasks.size(); i++) {
termuxNativeTasks.get(i).kill();
}
for (int i = 0; i < pendingPluginExecutionCommands.size(); i++) {
ExecutionCommand executionCommand = pendingPluginExecutionCommands.get(i);
@ -423,8 +429,22 @@ public final class TermuxService extends Service implements AppShell.AppShellCli
TermuxPluginUtils.processPluginExecutionCommandError(this, LOG_TAG, executionCommand, false);
}
}
/**
* Executes a NativeShell as a TermuxTask.
*/
public NativeShell executeNativeShell(ExecutionCommand executionCommand, String[] environment, NativeShell.Client client) {
final NativeShell[] shell = new NativeShell[1];
shell[0] = new NativeShell(executionCommand, (exitCode, error) -> {
mHandler.post(() -> {
mShellManager.mTermuxNativeTasks.remove(shell[0]);
updateNotification();
});
client.terminated(exitCode, error);
}, environment);
shell[0].execute();
return shell[0];
}
@ -789,7 +809,7 @@ public final class TermuxService extends Service implements AppShell.AppShellCli
// Set notification text
int sessionCount = getTermuxSessionsSize();
int taskCount = mShellManager.mTermuxTasks.size();
int taskCount = mShellManager.mTermuxTasks.size() + mShellManager.mTermuxNativeTasks.size();
String notificationText = sessionCount + " session" + (sessionCount == 1 ? "" : "s");
if (taskCount > 0) {
notificationText += ", " + taskCount + " task" + (taskCount == 1 ? "" : "s");

View File

@ -7,11 +7,14 @@ import android.content.Intent;
import android.content.ServiceConnection;
import android.os.BadParcelableException;
import android.os.Binder;
import android.os.Handler;
import android.os.IBinder;
import android.os.Looper;
import android.os.NetworkOnMainThreadException;
import android.os.ParcelFileDescriptor;
import android.os.Process;
import android.os.RemoteException;
import android.util.Log;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
@ -19,6 +22,7 @@ import androidx.annotation.Nullable;
import com.termux.app.TermuxService;
import com.termux.plugin_aidl.IPluginCallback;
import com.termux.plugin_aidl.IPluginService;
import com.termux.plugin_aidl.Task;
import com.termux.shared.android.BinderUtils;
import com.termux.shared.errors.Error;
import com.termux.shared.file.FileUtils;
@ -28,13 +32,21 @@ import com.termux.shared.net.socket.local.LocalClientSocket;
import com.termux.shared.net.socket.local.LocalSocketManager;
import com.termux.shared.net.socket.local.LocalSocketRunConfig;
import com.termux.shared.net.socket.local.PeerCred;
import com.termux.shared.shell.command.ExecutionCommand;
import com.termux.shared.shell.command.runner.nativerunner.NativeShell;
import com.termux.shared.termux.plugins.TermuxPluginUtils;
import java.io.BufferedWriter;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileWriter;
import java.io.IOException;
import java.io.OutputStreamWriter;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
@ -45,11 +57,46 @@ public class PluginService extends Service
{
private final static String LOG_TAG = "PluginService";
// map of connected clients by PID
private final Map<Integer, Plugin> mConnectedPlugins = Collections.synchronizedMap(new HashMap<>());
private final PluginServiceBinder mBinder = new PluginServiceBinder();
private TermuxService mTermuxService; // can be null if TermuxService gets temporarily destroyed
private final ServiceConnection mTermuxServiceConnection = new ServiceConnection()
{
@Override
public void onServiceConnected(ComponentName name, IBinder service) {
mTermuxService = ((TermuxService.LocalBinder) service).service;
}
@Override
public void onServiceDisconnected(ComponentName name) {
mTermuxService = null;
}
@Override
public void onBindingDied(ComponentName name) {
mTermuxService = null;
Logger.logError("Binding to TermuxService died"); // this should never happen, as TermuxService is in the same process
}
@Override
public void onNullBinding(ComponentName name) {
// this should never happen, as TermuxService returns its Binder
Logger.logError("TermuxService onBind returned no Binder");
}
};
private final Handler mMainThreadHandler = new Handler(Looper.getMainLooper());
/**
* Internal representation of a connected plugin for the service.
*/
private class Plugin {
int pid, uid;
Map<Integer, NativeShell> tasks = Collections.synchronizedMap(new HashMap<>());
@NonNull IPluginCallback callback;
int cachedCallbackVersion;
@ -89,35 +136,6 @@ public class PluginService extends Service
unbindService(mTermuxServiceConnection);
}
// map of connected clients by PID
private final Map<Integer, Plugin> mConnectedPlugins = Collections.synchronizedMap(new HashMap<>());
private final PluginServiceBinder mBinder = new PluginServiceBinder();
private TermuxService mTermuxService; // can be null if TermuxService gets temporarily destroyed
private final ServiceConnection mTermuxServiceConnection = new ServiceConnection()
{
@Override
public void onServiceConnected(ComponentName name, IBinder service) {
mTermuxService = ((TermuxService.LocalBinder) service).service;
}
@Override
public void onServiceDisconnected(ComponentName name) {
mTermuxService = null;
}
@Override
public void onBindingDied(ComponentName name) {
mTermuxService = null;
Logger.logError("Binding to TermuxService died"); // this should never happen, as TermuxService is in the same process
}
@Override
public void onNullBinding(ComponentName name) {
// this should never happen, as TermuxService returns its Binder
Logger.logError("TermuxService onBind returned no Binder");
}
};
@Override
public IBinder onBind(Intent intent) {
@ -263,17 +281,144 @@ public class PluginService extends Service
@Override
public ParcelFileDescriptor[] runTask(String commandPath, String[] arguments, ParcelFileDescriptor stdin, String workdir, String commandLabel, String commandDescription, String commandHelp) {
public Task runTask(String commandPath, String[] arguments, ParcelFileDescriptor stdin, String workdir, String[] environment) {
externalAppsOrThrow();
if (commandPath == null) throw new NullPointerException("Passed commandPath is null");
checkClient();
if (stdin == null) throw new NullPointerException("Passed stdin is null");
Plugin p = checkClient();
BinderUtils.enforceRunCommandPermission(PluginService.this);
// TODO run the task with mTermuxService
final Object sync = new Object();
final RuntimeException[] ex = new RuntimeException[1];
final boolean[] finished = {false};
final NativeShell[] shell = new NativeShell[1];
// create pipes
final ParcelFileDescriptor[] out = new ParcelFileDescriptor[2];
final ParcelFileDescriptor[] in = new ParcelFileDescriptor[2];
ParcelFileDescriptor[] pipes;
try {
pipes = ParcelFileDescriptor.createPipe();
}
catch (IOException e) {
try {
stdin.close();
} catch (IOException ignored) {}
throw new RuntimeException(e);
}
in[0] = pipes[0];
out[0] = pipes[1];
return null;
try {
pipes = ParcelFileDescriptor.createPipe();
}
catch (IOException e) {
try {
stdin.close();
} catch (IOException ignored) {}
try {
out[0].close();
} catch (IOException ignored) {}
try {
in[0].close();
} catch (IOException ignored) {}
throw new RuntimeException(e);
}
in[1] = pipes[0];
out[1] = pipes[1];
mMainThreadHandler.post(() -> {
TermuxService s = mTermuxService;
if (s == null) {
synchronized (sync) {
ex[0] = new IllegalStateException("Termux service unavailable");
finished[0] = true;
sync.notifyAll();
return;
}
}
try {
ExecutionCommand cmd = new ExecutionCommand();
cmd.executable = commandPath;
cmd.workingDirectory = workdir;
cmd.arguments = arguments;
cmd.stdinFD = stdin;
cmd.stdoutFD = out[0];
cmd.stderrFD = out[1];
/*
try {
ParcelFileDescriptor od = out[0].dup();
new Thread(() -> {
try {
BufferedWriter w = new BufferedWriter(new FileWriter(od.getFileDescriptor()));
w.write("test");
w.flush();
w.close();
} catch (Exception ignored) {ignored.printStackTrace();}
}).start();
}
catch (IOException e) {
e.printStackTrace();
}
*/
shell[0] = s.executeNativeShell(cmd, environment, (exitCode, error) -> {
try {
Logger.logDebug("NativeShell", "exit: "+exitCode);
// TODO callback
} catch (Exception ignored) {}
});
p.tasks.put(shell[0].getPid(), shell[0]);
synchronized (sync) {
finished[0] = true;
sync.notifyAll();
}
} catch (RuntimeException e) {
synchronized (sync) {
ex[0] = e;
finished[0] = true;
sync.notifyAll();
}
}
});
while (! finished[0]) {
synchronized (sync) {
try {
sync.wait();
}
catch (InterruptedException e) {
throw new IllegalStateException(e);
}
}
}
// make sure to not leak file descriptors
if (ex[0] != null) {
try {
stdin.close();
} catch (IOException ignored) {}
try {
out[0].close();
} catch (IOException ignored) {}
try {
out[1].close();
} catch (IOException ignored) {}
try {
in[0].close();
} catch (IOException ignored) {}
try {
in[1].close();
} catch (IOException ignored) {}
throw ex[0];
}
Task t = new Task();
t.stdout = in[0];
t.stderr = in[1];
t.pid = shell[0].getPid();
return t;
}

View File

@ -4,7 +4,7 @@ import android.os.ParcelFileDescriptor;
import android.app.PendingIntent;
import com.termux.plugin_aidl.IPluginCallback;
import com.termux.plugin_aidl.Task;
/**
* All available methods in {@link com.termux.app.plugin.PluginService}.
@ -42,13 +42,10 @@ interface IPluginService {
/**
* Runs a command like through a RUN_COMMAND intent.
* For documentation of the parameters, see <a href="https://github.com/termux/termux-app/wiki/RUN_COMMAND-Intent#run_command-intent-command-extras">the wiki</a>.
* If a parameter is null it is treated the same as if the extra isn't in the intent.
* <br><br>
* This method runs synchronously and returns stout in [0] of the result array and stderr in [1].
* Runs a command in a Termux task in the background.
* stdin, commandPath and workdir are required parameters.
*/
ParcelFileDescriptor[] runTask(String commandPath, in String[] arguments, in ParcelFileDescriptor stdin, String workdir, String commandLabel, String commandDescription, String commandHelp) = 3;
Task runTask(String commandPath, in String[] arguments, in ParcelFileDescriptor stdin, String workdir, in String[] environment) = 3;
/**
* This creates a socket file with name under {@link com.termux.shared.termux.TermuxConstants#TERMUX_PLUGINS_DIR_PATH}/&lt;package name of caller&gt;.

View File

@ -0,0 +1,9 @@
package com.termux.plugin_aidl;
import android.os.ParcelFileDescriptor;
parcelable Task {
ParcelFileDescriptor stdout;
ParcelFileDescriptor stderr;
int pid;
}

View File

@ -3,7 +3,7 @@ package com.termux.terminal;
/**
* Native methods for creating and managing pseudoterminal subprocesses. C code is in jni/termux.c.
*/
final class JNI {
public final class JNI {
static {
System.loadLibrary("termux");
@ -24,6 +24,23 @@ final class JNI {
* slave device counterpart (/dev/pts/$N) and have it as stdint, stdout and stderr.
*/
public static native int createSubprocess(String cmd, String cwd, String[] args, String[] envVars, int[] processId, int rows, int columns);
/**
* Create a subprocess. Differs from {@link #createSubprocess(String, String, String[], String[], int[], int, int)} in that there is no
* pseudoterminal, but all input and output is redirected through the given file descriptors without the need for {@link com.termux.shared.shell.StreamGobbler}
* or additional threads to do the IO operations for that. Because file descriptors are used, this can also transmit more data than the normal Binder
* transaction size limit for Intents.
*
* @param cmd The command to execute
* @param cwd The current working directory for the executed command
* @param args An array of arguments to the command
* @param envVars An array of strings of the form "VAR=value" to be added to the environment of the process
* @param stdin The file descriptor that should be used for stdin for the process
* @param stdout The file descriptor that should be used for stdout for the process
* @param stderr The file descriptor that should be used for stderr for the process
* @return The pid of the created subprocess.
*/
public static native int createTask(String cmd, String cwd, String[] args, String[] envVars, int stdin, int stdout, int stderr);
/** Set the window size for a given pty, which allows connected programs to learn how large their screen is. */
public static native void setPtyWindowSize(int fd, int rows, int cols);

View File

@ -212,3 +212,107 @@ JNIEXPORT void JNICALL Java_com_termux_terminal_JNI_close(JNIEnv* TERMUX_UNUSED(
{
close(fileDescriptor);
}
int create_task(JNIEnv* env, const char* cmd, const char* cwd, char** argv, char** envp, int stdinfd, int stdoutfd, int stderrfd) {
pid_t pid = fork();
if (pid < 0) {
return throw_runtime_exception(env, "Fork failed");
} else if (pid > 0) {
return pid;
} else {
// Clear signals which the Android java process may have blocked:
sigset_t signals_to_unblock;
sigfillset(&signals_to_unblock);
sigprocmask(SIG_UNBLOCK, &signals_to_unblock, 0);
setsid();
dup2(stdinfd, 0);
dup2(stdoutfd, 1);
dup2(stderrfd, 2);
DIR* self_dir = opendir("/proc/self/fd");
if (self_dir != NULL) {
int self_dir_fd = dirfd(self_dir);
struct dirent* entry;
while ((entry = readdir(self_dir)) != NULL) {
int fd = atoi(entry->d_name);
if (fd > 2 && fd != self_dir_fd) close(fd);
}
closedir(self_dir);
}
clearenv();
if (envp) for (; *envp; ++envp) putenv(*envp);
if (chdir(cwd) != 0) {
char* error_message;
// No need to free asprintf()-allocated memory since doing execvp() or exit() below.
if (asprintf(&error_message, "chdir(\"%s\")", cwd) == -1) error_message = "chdir()";
perror(error_message);
fflush(stderr);
}
execvp(cmd, argv);
// Show terminal output about failing exec() call:
char* error_message;
if (asprintf(&error_message, "exec(\"%s\")", cmd) == -1) error_message = "exec()";
perror(error_message);
_exit(1);
}
}
JNIEXPORT jint JNICALL
Java_com_termux_terminal_JNI_createTask(JNIEnv *env, jclass TERMUX_UNUSED(clazz), jstring cmd, jstring cwd,
jobjectArray args, jobjectArray envVars, jint stdinfd,
jint stdoutfd, jint stderrfd) {
jsize size = args ? (*env)->GetArrayLength(env, args) : 0;
char** argv = NULL;
if (size > 0) {
argv = (char**) malloc((size + 1) * sizeof(char*));
if (!argv) return throw_runtime_exception(env, "Couldn't allocate argv array");
for (int i = 0; i < size; ++i) {
jstring arg_java_string = (jstring) (*env)->GetObjectArrayElement(env, args, i);
char const* arg_utf8 = (*env)->GetStringUTFChars(env, arg_java_string, NULL);
if (!arg_utf8) return throw_runtime_exception(env, "GetStringUTFChars() failed for argv");
argv[i] = strdup(arg_utf8);
(*env)->ReleaseStringUTFChars(env, arg_java_string, arg_utf8);
}
argv[size] = NULL;
}
size = envVars ? (*env)->GetArrayLength(env, envVars) : 0;
char** envp = NULL;
if (size > 0) {
envp = (char**) malloc((size + 1) * sizeof(char *));
if (!envp) return throw_runtime_exception(env, "malloc() for envp array failed");
for (int i = 0; i < size; ++i) {
jstring env_java_string = (jstring) (*env)->GetObjectArrayElement(env, envVars, i);
char const* env_utf8 = (*env)->GetStringUTFChars(env, env_java_string, 0);
if (!env_utf8) return throw_runtime_exception(env, "GetStringUTFChars() failed for env");
envp[i] = strdup(env_utf8);
(*env)->ReleaseStringUTFChars(env, env_java_string, env_utf8);
}
envp[size] = NULL;
}
int procId;
char const* cmd_cwd = (*env)->GetStringUTFChars(env, cwd, NULL);
char const* cmd_utf8 = (*env)->GetStringUTFChars(env, cmd, NULL);
procId = create_task(env, cmd_utf8, cmd_cwd, argv, envp, stdinfd, stdoutfd, stderrfd);
(*env)->ReleaseStringUTFChars(env, cmd, cmd_utf8);
(*env)->ReleaseStringUTFChars(env, cmd, cmd_cwd);
if (argv) {
for (char** tmp = argv; *tmp; ++tmp) free(*tmp);
free(argv);
}
if (envp) {
for (char** tmp = envp; *tmp; ++tmp) free(*tmp);
free(envp);
}
return procId;
}

View File

@ -2,6 +2,7 @@ package com.termux.shared.shell.command;
import android.content.Intent;
import android.net.Uri;
import android.os.ParcelFileDescriptor;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
@ -163,8 +164,15 @@ public class ExecutionCommand {
public String stdin;
/** The current working directory for the {@link ExecutionCommand}. */
public String workingDirectory;
/** The file descriptor the executable will read from. */
public ParcelFileDescriptor stdinFD;
/** The file descriptor the executable will write standard output to. */
public ParcelFileDescriptor stdoutFD;
/** The file descriptor the executable will write error output to. */
public ParcelFileDescriptor stderrFD;
/** The terminal transcript rows for the {@link ExecutionCommand}. */
public Integer terminalTranscriptRows;

View File

@ -0,0 +1,77 @@
package com.termux.shared.shell.command.runner.nativerunner;
import android.os.ParcelFileDescriptor;
import android.os.Process;
import androidx.annotation.Nullable;
import com.termux.shared.shell.command.ExecutionCommand;
import com.termux.terminal.JNI;
import java.io.IOException;
/**
* This Runner is only available over Binder IPC, because it requires transferring file descriptors to Termux and back
* to the client, which is not possible over Intents.
*
*/
public final class NativeShell
{
private final ExecutionCommand exe;
private final Client client;
private final String[] env;
private int pid = -1;
public NativeShell(ExecutionCommand exe, Client client, String[] env) {
this.exe = exe;
if (exe.executable == null) throw new IllegalArgumentException("NativeShell: Command cannot be null");
if (exe.workingDirectory == null) throw new IllegalArgumentException("NativeShell: Working directory cannot be null");
if (exe.stdinFD == null) throw new IllegalArgumentException("NativeShell: stdin cannot be null");
if (exe.stdoutFD == null) throw new IllegalArgumentException("NativeShell: stdout cannot be null");
if (exe.stderrFD == null) throw new IllegalArgumentException("NativeShell: stderr cannot be null");
this.client = client;
this.env = env;
}
public interface Client {
/**
* @param exitCode The exit code of the process. Undefined if error is not null. Negative numbers mean a signal terminated the process.
* @param error An exception that was thrown while trying to execute the command. Can be null.
*/
void terminated(int exitCode, Exception error);
}
public synchronized void execute() {
try {
pid = JNI.createTask(exe.executable, exe.workingDirectory, exe.arguments, env, exe.stdinFD.getFd(), exe.stdoutFD.getFd(), exe.stderrFD.getFd());
new Thread(() -> {
int exit = JNI.waitFor(pid);
client.terminated(exit, null);
pid = -1;
}).start();
} catch (RuntimeException e) {
client.terminated(0, e);
} finally {
// close the ParcelFileDescriptors
try {
exe.stdinFD.close();
} catch (IOException ignored) {}
try {
exe.stdoutFD.close();
} catch (IOException ignored) {}
try {
exe.stderrFD.close();
} catch (IOException ignored) {}
}
}
public synchronized void kill() {
if (pid != -1)
Process.killProcess(pid);
}
public synchronized int getPid() {
return pid;
}
}

View File

@ -8,6 +8,7 @@ import androidx.annotation.NonNull;
import com.termux.shared.shell.command.ExecutionCommand;
import com.termux.shared.shell.command.runner.app.AppShell;
import com.termux.shared.shell.command.runner.nativerunner.NativeShell;
import com.termux.shared.termux.settings.preferences.TermuxAppSharedPreferences;
import com.termux.shared.termux.shell.command.runner.terminal.TermuxSession;
@ -35,6 +36,11 @@ public class TermuxShellManager {
*/
public final List<AppShell> mTermuxTasks = new ArrayList<>();
/**
* The background NativeShell tasks which this service manages.
*/
public final List<NativeShell> mTermuxNativeTasks = new ArrayList<>();
/**
* The pending plugin ExecutionCommands that have yet to be processed by this service.
*/