From f3ebad80022fe13cdce45af43bc9c043b076f5c8 Mon Sep 17 00:00:00 2001 From: tareksander <57038324+tareksander@users.noreply.github.com> Date: Mon, 25 Apr 2022 16:27:11 +0200 Subject: [PATCH 01/10] Added: Methods sendFD and readFD with sendFDNative and readFDNative to send and receive file descriptors over UNIX sockets as ancillary data. --- termux-shared/src/main/cpp/local-socket.cpp | 121 ++++++++++++++++++ .../net/socket/local/LocalSocketManager.java | 54 ++++++++ 2 files changed, 175 insertions(+) diff --git a/termux-shared/src/main/cpp/local-socket.cpp b/termux-shared/src/main/cpp/local-socket.cpp index 6457aca8af..785b65ee77 100644 --- a/termux-shared/src/main/cpp/local-socket.cpp +++ b/termux-shared/src/main/cpp/local-socket.cpp @@ -5,6 +5,7 @@ #include #include #include +#include #include @@ -601,3 +602,123 @@ Java_com_termux_shared_net_socket_local_LocalSocketManager_getPeerCredNative(JNI // Return success since PeerCred was filled successfully return getJniResult(env, logTitle); } + +extern "C" +JNIEXPORT jobject JNICALL +Java_com_termux_shared_net_socket_local_LocalSocketManager_readFDNative(JNIEnv *env, jclass clazz, + jstring logTitle, + jint fd, jbyteArray dataArray, + jintArray readFDArray) { + if (fd < 0) { + return getJniResult(env, logTitle, -1, "readFDNative(): Invalid fd \"" + to_string(fd) + "\" passed"); + } + + jint* readFD = env->GetIntArrayElements(readFDArray, nullptr); + if (checkJniException(env)) return NULL; + if (readFD == NULL) { + return getJniResult(env, logTitle, -1, "readFDNative(): readFD passed is null"); + } + + if (env->GetArrayLength(readFDArray) < 1) { + return getJniResult(env, logTitle, -1, "readFDNative(): readFD length is < 1"); + } + if (checkJniException(env)) return NULL; + + readFD[0] = -1; // set to an invalid value to signify if an fd was received or not + + jbyte* data = env->GetByteArrayElements(dataArray, nullptr); + if (checkJniException(env)) return NULL; + if (data == nullptr) { + return getJniResult(env, logTitle, -1, "readFDNative(): data passed is null"); + } + + if (env->GetArrayLength(dataArray) < 1) { + return getJniResult(env, logTitle, -1, "readFDNative(): data length is < 1"); + } + if (checkJniException(env)) return NULL; + + + // enough size for exactly one control message with one fd, so the excess fds are automatically closed + constexpr int CONTROLLEN = CMSG_SPACE(sizeof(int)); + union { + cmsghdr _; // for alignment + char controlBuffer[CONTROLLEN]; + } controlBufferUnion; + memset(&controlBufferUnion, 0, CONTROLLEN); // clear the buffer to be sure + + iovec buffer{data, 1}; + msghdr receiveHeader{NULL, 0, &buffer, 1, controlBufferUnion.controlBuffer, sizeof(controlBufferUnion.controlBuffer), 0}; + + int ret = recvmsg(fd, &receiveHeader, 0); + if (ret == -1) { + int errnoBackup = errno; + env->ReleaseByteArrayElements(dataArray, data, 0); + if (checkJniException(env)) return NULL; + env->ReleaseIntArrayElements(readFDArray, readFD, 0); + if (checkJniException(env)) return NULL; + return getJniResult(env, logTitle, -1, errnoBackup, "readNative(): Failed to read on fd " + to_string(fd)); + } + + for (struct cmsghdr* cmsg = CMSG_FIRSTHDR(&receiveHeader); cmsg != NULL; cmsg = CMSG_NXTHDR(&receiveHeader, cmsg)) { + if (cmsg->cmsg_level == SOL_SOCKET && cmsg->cmsg_type == SCM_RIGHTS) { + int recfd; + memcpy(&recfd, CMSG_DATA(cmsg), sizeof(recfd)); + readFD[0] = recfd; + break; + } + } + + env->ReleaseByteArrayElements(dataArray, data, 0); + if (checkJniException(env)) return NULL; + + env->ReleaseIntArrayElements(readFDArray, readFD, 0); + if (checkJniException(env)) return NULL; + + // Return success and bytes read in JniResult.intData field + return getJniResult(env, logTitle, ret); +} +extern "C" +JNIEXPORT jobject JNICALL +Java_com_termux_shared_net_socket_local_LocalSocketManager_sendFDNative(JNIEnv *env, jclass clazz, + jstring logTitle, + jint fd, jbyte data, + jint sendFD) { + if (fd < 0) { + return getJniResult(env, logTitle, -1, "sendFDNative(): Invalid fd \"" + to_string(fd) + "\" passed"); + } + + if (sendFD < 0) { + return getJniResult(env, logTitle, -1, "sendFDNative(): Invalid sendFD \"" + to_string(fd) + "\" passed"); + } + + const int sendFDInt = sendFD; // in case a platform has an int that isn't the same size as jint + + // enough size for exactly one control message with one fd + constexpr int CONTROLLEN = CMSG_SPACE(sizeof(int)); + union { + cmsghdr _; // for alignment + char controlBuffer[CONTROLLEN]; + } controlBufferUnion; + memset(&controlBufferUnion, 0, CONTROLLEN); // clear the buffer to be sure + + iovec buffer{&data, 1}; + msghdr sendHeader{nullptr, 0, &buffer, 1, controlBufferUnion.controlBuffer, sizeof(controlBufferUnion.controlBuffer), 0}; + + struct cmsghdr* cmsg = CMSG_FIRSTHDR(&sendHeader); + if (cmsg == NULL) { + return getJniResult(env, logTitle, -1, "sendFDNative(): CMSG_FIRSTHDR returned NULL"); + } + cmsg->cmsg_level = SOL_SOCKET; + cmsg->cmsg_type = SCM_RIGHTS; + cmsg->cmsg_len = CMSG_LEN(sizeof(sendFDInt)); + memcpy(CMSG_DATA(cmsg), &sendFDInt, sizeof(sendFDInt)); + + int ret = sendmsg(fd, &sendHeader, MSG_NOSIGNAL); + if (ret == -1) { + int errnoBackup = errno; + return getJniResult(env, logTitle, -1, errnoBackup, "sendFDNative(): Failed to send on fd " + to_string(fd)); + } + + // Return success + return getJniResult(env, logTitle); +} diff --git a/termux-shared/src/main/java/com/termux/shared/net/socket/local/LocalSocketManager.java b/termux-shared/src/main/java/com/termux/shared/net/socket/local/LocalSocketManager.java index 3cd959c34c..e9d8cdd816 100644 --- a/termux-shared/src/main/java/com/termux/shared/net/socket/local/LocalSocketManager.java +++ b/termux-shared/src/main/java/com/termux/shared/net/socket/local/LocalSocketManager.java @@ -198,6 +198,34 @@ public static JniResult read(@NonNull String serverTitle, int fd, @NonNull byte[ return new JniResult(message, t); } } + + /** + * Attempts to read one byte from file descriptor {@code fd} into the data buffer and + * a received file descriptor into the file descriptor buffer {@code readFD}. + * On success, the number of bytes read is returned (zero indicates end of file). + * It is not an error if bytes read is smaller than the number of bytes requested; this may happen + * for example because fewer bytes are actually available right now (maybe because we were close + * to end-of-file, or because we are reading from a pipe), or because read() was interrupted by + * a signal. On error, the {@link JniResult#errno} and {@link JniResult#errmsg} will be set. + * If no file descriptor was attached to the data byte, {@code readFD[0]} is -1. + * + * @param serverTitle The server title used for logging and errors. + * @param fd The socket fd. + * @param data The data buffer to read bytes into. Has to have a length of at least one, but only the first byte is used. + * @param readFD The file descriptor buffer to read file descriptors into. Has to have a length of at least one, but only the first int is used. + * @return Returns the {@link JniResult}. If reading was successful, then {@link JniResult#retval} + * will be 0 and {@link JniResult#intData} will contain the bytes read. + */ + @Nullable + public static JniResult readFD(@NonNull String serverTitle, int fd, @NonNull byte[] data, @NonNull int[] readFD) { + try { + return readFDNative(serverTitle, fd, data, readFD); + } catch (Throwable t) { + String message = "Exception in readFDNative()"; + Logger.logStackTraceWithMessage(LOG_TAG, message, t); + return new JniResult(message, t); + } + } /** * Attempts to send data buffer to the file descriptor. On error, the {@link JniResult#errno} and @@ -222,6 +250,28 @@ public static JniResult send(@NonNull String serverTitle, int fd, @NonNull byte[ return new JniResult(message, t); } } + + /** + * Attempts to send a byte and the passed file descriptor {@code sendFD} to the file descriptor {@code fd}. On error, the {@link JniResult#errno} and + * {@link JniResult#errmsg} will be set. + * + * @param serverTitle The server title used for logging and errors. + * @param fd The socket fd. + * @param data The byte to send. + * @param sendFD The file descriptor to send. + * @return Returns the {@link JniResult}. If sending was successful, then {@link JniResult#retval} + * will be 0. + */ + @Nullable + public static JniResult sendFD(@NonNull String serverTitle, int fd, byte data, int sendFD) { + try { + return sendFDNative(serverTitle, fd, data, sendFD); + } catch (Throwable t) { + String message = "Exception in sendFDNative()"; + Logger.logStackTraceWithMessage(LOG_TAG, message, t); + return new JniResult(message, t); + } + } /** * Gets the number of bytes available to read on the socket. @@ -436,8 +486,12 @@ public static String getErrorMarkdownString(@NonNull Error error, @Nullable private static native JniResult acceptNative(@NonNull String serverTitle, int fd); @Nullable private static native JniResult readNative(@NonNull String serverTitle, int fd, @NonNull byte[] data, long deadline); + + @Nullable private static native JniResult readFDNative(@NonNull String serverTitle, int socketFD, @NonNull byte[] data, @NonNull int[] readFD); @Nullable private static native JniResult sendNative(@NonNull String serverTitle, int fd, @NonNull byte[] data, long deadline); + + @Nullable private static native JniResult sendFDNative(@NonNull String serverTitle, int socketFD, byte data, int sendFD); @Nullable private static native JniResult availableNative(@NonNull String serverTitle, int fd); From 868cd13af1b8edc2ca8e5f980980319fef22cf4c Mon Sep 17 00:00:00 2001 From: tareksander <57038324+tareksander@users.noreply.github.com> Date: Mon, 2 May 2022 00:34:31 +0200 Subject: [PATCH 02/10] Added: New dangerous permission com.termux.permission.TERMUX_PLUGIN, signature permission com.termux.permission.TERMUX_SIGNATURE, PluginService bound service for plugins. The methods for PluginService are defined in AIDL in the new module plugin-aidl. The module can be imported by plugins to get the method definitions. The TERMUX_PLUGIN permission is needed to bind to the PluginService, and external apps have to be enabled in the config. AIDL methods: setCallbackBinder and setCallbackService: Used to get a Binder for the PluginService to identify each plugin and get notified when it dies. runTask: Runs a command as a Termux task in the background if the caller also has the RUN_COMMAND permission. listenOnSocketFile: Creates a server socket in the app directory for the plugins (TermuxConstants#TERMUX_APPS_DIR_PATH/) and returns the file descriptor, which can be used for a LocalServerSocket. openFile: Opens a file in the app directory for the plugin and returns the file descriptor. --- app/build.gradle | 1 + app/src/main/AndroidManifest.xml | 15 + .../java/com/termux/app/TermuxService.java | 2 +- .../com/termux/app/plugin/PluginService.java | 325 ++++++++++++++++++ app/src/main/res/values/strings.xml | 4 +- plugin-aidl/.gitignore | 1 + plugin-aidl/build.gradle | 49 +++ plugin-aidl/consumer-rules.pro | 2 + plugin-aidl/proguard-rules.pro | 24 ++ plugin-aidl/src/main/AndroidManifest.xml | 5 + .../termux/plugin_aidl/IPluginCallback.aidl | 40 +++ .../termux/plugin_aidl/IPluginService.aidl | 73 ++++ settings.gradle | 2 +- .../termux/shared/android/BinderUtils.java | 80 +++++ .../net/socket/local/LocalServerSocket.java | 69 ++++ .../net/socket/local/LocalSocketManager.java | 19 + .../termux/shared/termux/TermuxConstants.java | 17 +- 17 files changed, 724 insertions(+), 4 deletions(-) create mode 100644 app/src/main/java/com/termux/app/plugin/PluginService.java create mode 100644 plugin-aidl/.gitignore create mode 100644 plugin-aidl/build.gradle create mode 100644 plugin-aidl/consumer-rules.pro create mode 100644 plugin-aidl/proguard-rules.pro create mode 100644 plugin-aidl/src/main/AndroidManifest.xml create mode 100644 plugin-aidl/src/main/aidl/com/termux/plugin_aidl/IPluginCallback.aidl create mode 100644 plugin-aidl/src/main/aidl/com/termux/plugin_aidl/IPluginService.aidl create mode 100644 termux-shared/src/main/java/com/termux/shared/android/BinderUtils.java diff --git a/app/build.gradle b/app/build.gradle index 3df93fef10..1152fce0f3 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -35,6 +35,7 @@ android { implementation project(":terminal-view") implementation project(":termux-shared") + implementation project(":plugin-aidl") } defaultConfig { diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 4e95702bc3..1271f6c7c4 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -20,6 +20,17 @@ android:label="@string/permission_run_command_label" android:protectionLevel="dangerous" /> + + + + @@ -207,6 +218,10 @@ + diff --git a/app/src/main/java/com/termux/app/TermuxService.java b/app/src/main/java/com/termux/app/TermuxService.java index 8025d0bd2c..6f7381cadb 100644 --- a/app/src/main/java/com/termux/app/TermuxService.java +++ b/app/src/main/java/com/termux/app/TermuxService.java @@ -68,7 +68,7 @@ public final class TermuxService extends Service implements AppShell.AppShellClient, TermuxSession.TermuxSessionClient { /** This service is only bound from inside the same process and never uses IPC. */ - class LocalBinder extends Binder { + public class LocalBinder extends Binder { public final TermuxService service = TermuxService.this; } diff --git a/app/src/main/java/com/termux/app/plugin/PluginService.java b/app/src/main/java/com/termux/app/plugin/PluginService.java new file mode 100644 index 0000000000..934f6991d3 --- /dev/null +++ b/app/src/main/java/com/termux/app/plugin/PluginService.java @@ -0,0 +1,325 @@ +package com.termux.app.plugin; + +import android.app.Service; +import android.content.ComponentName; +import android.content.Context; +import android.content.Intent; +import android.content.ServiceConnection; +import android.os.Binder; +import android.os.IBinder; +import android.os.Parcel; +import android.os.ParcelFileDescriptor; +import android.os.Process; +import android.os.RemoteException; +import android.util.Log; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import com.termux.app.TermuxService; +import com.termux.plugin_aidl.IPluginCallback; +import com.termux.plugin_aidl.IPluginService; +import com.termux.shared.android.BinderUtils; +import com.termux.shared.errors.Error; +import com.termux.shared.file.FileUtils; +import com.termux.shared.logger.Logger; +import com.termux.shared.net.socket.local.ILocalSocketManager; +import com.termux.shared.net.socket.local.LocalClientSocket; +import com.termux.shared.net.socket.local.LocalServerSocket; +import com.termux.shared.net.socket.local.LocalSocketManager; +import com.termux.shared.net.socket.local.LocalSocketRunConfig; +import com.termux.shared.termux.plugins.TermuxPluginUtils; + +import java.io.File; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; + +/** + * This is a bound service that can be used by plugins. + * The available methods are defined in {@link com.termux.plugin_aidl.IPluginService}. + */ +public class PluginService extends Service +{ + private final static String LOG_TAG = "PluginService"; + + /** + * Internal representation of a connected plugin for the service. + */ + private class Plugin { + int pid, uid; + + @NonNull IPluginCallback callback; + int cachedCallbackVersion; + + Plugin(int pid, int uid, @NonNull IPluginCallback callback) throws RemoteException { + this.pid = pid; + this.uid = uid; + this.callback = callback; + callback.asBinder().linkToDeath(() -> mConnectedPlugins.remove(pid), 0); // remove self when the callback binder dies + cachedCallbackVersion = callback.getCallbackVersion(); + } + + + @Override + public int hashCode() { + return pid + uid; + } + + @Override + public boolean equals(@Nullable Object obj) { + if (!(obj instanceof Plugin)) return false; + Plugin p = (Plugin) obj; + return p.pid == pid && p.uid == uid; + } + } + + @Override + public void onCreate() { + super.onCreate(); + if (! bindService(new Intent(this, TermuxService.class), mTermuxServiceConnection, Context.BIND_AUTO_CREATE)) { + Logger.logError("Could not bind to TermuxService"); + } + } + + @Override + public void onDestroy() { + unbindService(mTermuxServiceConnection); + } + + // map of connected clients by PID + private final Map 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) { + // If TermuxConstants.PROP_ALLOW_EXTERNAL_APPS property to not set to "true", don't enable binding + String errmsg = TermuxPluginUtils.checkIfAllowExternalAppsPolicyIsViolated(this, LOG_TAG); + if (errmsg != null) { + return null; + } + return mBinder; + } + + /** + * If not already, this creates an entry in the map of connected plugins for the current Binder client. + */ + private void addClient(@NonNull IPluginCallback callback) { + int pid = Binder.getCallingPid(), uid = Binder.getCallingPid(); + if (pid == Process.myPid()) return; // no client connected + if (mConnectedPlugins.get(pid) != null) return; // client already in list + try { + mConnectedPlugins.put(pid, new Plugin(pid, uid, callback)); + } catch (RemoteException ignored) {} + } + + + private void checkClient() throws IllegalStateException { + if (! mConnectedPlugins.containsKey(Binder.getCallingPid())) Logger.logDebug("client not in list"); + if (! mConnectedPlugins.containsKey(Binder.getCallingPid())) throw new IllegalStateException("Please call setCallbackBinder first"); + } + + + + private class PluginServiceBinder extends IPluginService.Stub { + + /** + * Convenience function to throw an {@link IllegalArgumentException} if external apps aren't enabled. + */ + private void externalAppsOrThrow() { + // If TermuxConstants.PROP_ALLOW_EXTERNAL_APPS property to not set to "true", then throw exception + String errmsg = TermuxPluginUtils.checkIfAllowExternalAppsPolicyIsViolated(PluginService.this, LOG_TAG); + if (errmsg != null) { + throw new IllegalArgumentException(errmsg); + } + } + + @NonNull + private String pluginDirOrThrow() { + String pdir = BinderUtils.getCallingPluginDir(mTermuxService); + if (pdir == null) throw new NullPointerException("Could not get apps dir of calling package"); + return pdir; + } + + @NonNull + private String fileInPluginDirOrThrow(String name) { + String pluginDir = pluginDirOrThrow(); + String filePath = FileUtils.getCanonicalPath(pluginDir + "/" +name, null); + if (FileUtils.isPathInDirPath(filePath, pluginDir, true)) { + return filePath; + } else { + throw new IllegalArgumentException("A plugin cannot access paths outside of the plugin directory: "+filePath); + } + } + + @Override + public void setCallbackBinder(IPluginCallback callback) { + externalAppsOrThrow(); + if (callback == null) throw new NullPointerException("Passed callback binder is null"); + addClient(callback); + } + + @Override + public void setCallbackService(String componentNameString) { + externalAppsOrThrow(); + if (componentNameString == null) throw new NullPointerException("Passed componentName is null"); + + String callerPackageName = BinderUtils.getCallerPackageNameOrNull(PluginService.this); + if (callerPackageName == null) throw new NullPointerException("Caller package is null"); + + ComponentName componentName = ComponentName.createRelative(callerPackageName, componentNameString); + Intent callbackStartIntent = new Intent(); + callbackStartIntent.setComponent(componentName); + + + final boolean[] bindingFinished = {false}; + final IBinder[] callbackBinder = new IBinder[] {null}; + + ServiceConnection con = new ServiceConnection() + { + @Override + public void onServiceConnected(ComponentName name, IBinder service) { + callbackBinder[0] = service; + bindingFinished[0] = true; + synchronized (callbackBinder) { + callbackBinder.notifyAll(); + } + } + @Override + public void onServiceDisconnected(ComponentName name) { + unbindService(this); + } + @Override + public void onBindingDied(ComponentName name) { + unbindService(this); + } + @Override + public void onNullBinding(ComponentName name) { + bindingFinished[0] = true; + synchronized (callbackBinder) { + callbackBinder.notifyAll(); + } + unbindService(this); + } + }; + + PluginService.this.bindService(callbackStartIntent, con, Context.BIND_ALLOW_OOM_MANAGEMENT); + + while (! bindingFinished[0]) { + try { + synchronized (callbackBinder) { + callbackBinder.wait(); + } + } + catch (InterruptedException ignored) {} + } + + if (callbackBinder[0] == null) { + throw new IllegalArgumentException("Could not bind callback service: "+componentNameString); + } + + addClient(IPluginCallback.Stub.asInterface(callbackBinder[0])); + } + + + @Override + public ParcelFileDescriptor[] runTask(String commandPath, String[] arguments, ParcelFileDescriptor stdin, String workdir, String commandLabel, String commandDescription, String commandHelp) { + externalAppsOrThrow(); + if (commandPath == null) throw new NullPointerException("Passed commandPath is null"); + checkClient(); + BinderUtils.enforceRunCommandPermission(PluginService.this); + + + // TODO run the task with mTermuxService + + + return null; + } + + + @NonNull + @Override + public ParcelFileDescriptor listenOnSocketFile(String name) { + externalAppsOrThrow(); + if (name == null) throw new NullPointerException("Passed name is null"); + checkClient(); + + String socketPath = fileInPluginDirOrThrow(name); + + + // passing null for localSocketManagerClient here should be okay, because the handler methods don't get called + @SuppressWarnings("ConstantConditions") + LocalSocketRunConfig conf = new LocalSocketRunConfig(BinderUtils.getCallerPackageName(PluginService.this) + " socket: " + name, socketPath, null); + LocalSocketManager m = new LocalSocketManager(PluginService.this, conf); + Error err = m.createSocket(); + if (err != null) { + throw new UnsupportedOperationException("Error: "+err.getErrorLogString()); + } + + ParcelFileDescriptor socketPfd; + + try { + socketPfd = ParcelFileDescriptor.fromFd(conf.getFD()); + } + catch (IOException e) { + throw new UnsupportedOperationException(e); + } finally { + m.getServerSocket().closeServerSocket(true); + } + + return socketPfd; + } + + + @Override + public ParcelFileDescriptor openFile(String name, String mode) { + externalAppsOrThrow(); + if (name == null) throw new NullPointerException("Passed name is null"); + if (mode == null) throw new NullPointerException("Passed mode is null"); + checkClient(); + + String filePath = fileInPluginDirOrThrow(name); + + Error err = FileUtils.createRegularFile("("+BinderUtils.getCallerPackageName(PluginService.this)+")", filePath, "rw", true, false); + if (err != null) { + throw new UnsupportedOperationException(err.getErrorLogString()); + } + try { + return ParcelFileDescriptor.open(new File(filePath), ParcelFileDescriptor.parseMode(mode)); + } + catch (FileNotFoundException e) { + throw new UnsupportedOperationException(e); + } + } + + + } +} diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 794d8df3e0..c6c12b4920 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -23,7 +23,9 @@ execute arbitrary commands within &TERMUX_APP_NAME; environment and access files - + + Connect as a plugin to &TERMUX_APP_NAME; + manage an app directory under /data/data/&TERMUX_PACKAGE_NAME;/files/apps Installing bootstrap packages… diff --git a/plugin-aidl/.gitignore b/plugin-aidl/.gitignore new file mode 100644 index 0000000000..42afabfd2a --- /dev/null +++ b/plugin-aidl/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/plugin-aidl/build.gradle b/plugin-aidl/build.gradle new file mode 100644 index 0000000000..31475ee8e3 --- /dev/null +++ b/plugin-aidl/build.gradle @@ -0,0 +1,49 @@ +plugins { + id 'com.android.library' + id 'maven-publish' +} + +android { + + + compileSdkVersion project.properties.compileSdkVersion.toInteger() + + defaultConfig { + minSdkVersion project.properties.minSdkVersion.toInteger() + targetSdkVersion project.properties.targetSdkVersion.toInteger() + + consumerProguardFiles "consumer-rules.pro" + } + + buildTypes { + release { + minifyEnabled false + proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' + } + } + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } +} + +task sourceJar(type: Jar) { + from android.sourceSets.main.java.srcDirs, "build/generated/aidl_source_output_dir/release/out" + classifier "sources" + dependsOn "compileReleaseAidl" +} + +afterEvaluate { + publishing { + publications { + // Creates a Maven publication called "release". + release(MavenPublication) { + from components.release + groupId = 'com.termux' + artifactId = 'plugin-aidl' + version = '1.0.0' + artifact sourceJar + } + } + } +} diff --git a/plugin-aidl/consumer-rules.pro b/plugin-aidl/consumer-rules.pro new file mode 100644 index 0000000000..a9298c72c6 --- /dev/null +++ b/plugin-aidl/consumer-rules.pro @@ -0,0 +1,2 @@ +# keep all classes +-keep class com.termux.plugin_aidl.** { *; } diff --git a/plugin-aidl/proguard-rules.pro b/plugin-aidl/proguard-rules.pro new file mode 100644 index 0000000000..6273d1acd8 --- /dev/null +++ b/plugin-aidl/proguard-rules.pro @@ -0,0 +1,24 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile + +# keep all classes +-keep class com.termux.plugin_aidl.** { *; } diff --git a/plugin-aidl/src/main/AndroidManifest.xml b/plugin-aidl/src/main/AndroidManifest.xml new file mode 100644 index 0000000000..ecae224c6c --- /dev/null +++ b/plugin-aidl/src/main/AndroidManifest.xml @@ -0,0 +1,5 @@ + + + + diff --git a/plugin-aidl/src/main/aidl/com/termux/plugin_aidl/IPluginCallback.aidl b/plugin-aidl/src/main/aidl/com/termux/plugin_aidl/IPluginCallback.aidl new file mode 100644 index 0000000000..8c4e55081d --- /dev/null +++ b/plugin-aidl/src/main/aidl/com/termux/plugin_aidl/IPluginCallback.aidl @@ -0,0 +1,40 @@ +package com.termux.plugin_aidl; + + +import android.os.ParcelFileDescriptor; + +/** +* This can be passed from a plugin to Termux to enable callback functionality. +* To maintain backwards compatibility callbacks cannot be removed (without deprecation). +* New callbacks can be added and CURRENT_CALLBACK_VERSION incremented, but the callbacks can only be called +* when the callback version via getCallbackVersion() is at least the new value of CURRENT_CALLBACK_VERSION. +* Previous callback versions have to be supported by Termux, so an out-of-date plugin doesn't receive transactions it doesn't recognize. +* In the javadoc write the first and last (if there is one) callback version code where the callback will be called. +*/ +interface IPluginCallback { + + /** + * This defines to most up-to-date version code for callback binders. + */ + const int CURRENT_CALLBACK_VERSION = 1; + + /** + * Returns the callback version supported by this Binder. + * This is the first method called after Termux receives the Binder. + * Only methods compatible with the callback version will be called. + */ + int getCallbackVersion() = 1; + + + + + + + + + + + + + +} diff --git a/plugin-aidl/src/main/aidl/com/termux/plugin_aidl/IPluginService.aidl b/plugin-aidl/src/main/aidl/com/termux/plugin_aidl/IPluginService.aidl new file mode 100644 index 0000000000..7106c0233e --- /dev/null +++ b/plugin-aidl/src/main/aidl/com/termux/plugin_aidl/IPluginService.aidl @@ -0,0 +1,73 @@ +package com.termux.plugin_aidl; + +import android.os.ParcelFileDescriptor; +import android.app.PendingIntent; + +import com.termux.plugin_aidl.IPluginCallback; + + +/** +* All available methods in {@link com.termux.app.plugin.PluginService}. +* When modifying methods here, use the following scheme to maintain backwards compatibility: +*
    +*
  • Append the version code of the last app version where the method exists with the current method signature as a postfix to the method you want to modify.
  • +*
  • Also rename the method in {@link com.termux.app.plugin.PluginService.PluginServiceBinder}.
  • +*
  • create a method with the original name, a new transaction id and the updated method signature. +* You should always count the transaction id up from the last created method to prevent clashing with old methods.
  • +*
+* The old implementation should be kept for backwards compatibility, at least for some time.
+* Example:
+* Version code of the last app release: 118
+* Method: String foo(String bar) = 33;
+* Renamed: String foo_118(String bar) = 33;
+* New method: boolean foo(String bar) = 34;

+*/ +interface IPluginService { + + + /** + * This or {@link com.termux.plugin_aidl.IPluginService#setCallbackService} has to be called before any other method. + * It initializes the internal representation of the connected plugin and sets the callback binder. + */ + void setCallbackBinder(IPluginCallback callback) = 1; + + /** + * This or {@link com.termux.plugin_aidl.IPluginService#setCallbackBinder} has to be called before any other method. + * It initialized the internal representation of the connected plugin and sets the callback binder to the binder returned by the bound service. + * + * @param componentName This is the relative part of a component name string. + * The package name is always taken from the calling binder package for security reasons. + */ + void setCallbackService(String componentName) = 2; + + + /** + * Runs a command like through a RUN_COMMAND intent. + * For documentation of the parameters, see the wiki. + * If a parameter is null it is treated the same as if the extra isn't in the intent. + *

+ * This method runs synchronously and returns stout in [0] of the result array and stderr in [1]. + */ + ParcelFileDescriptor[] runTask(String commandPath, in String[] arguments, in ParcelFileDescriptor stdin, String workdir, String commandLabel, String commandDescription, String commandHelp) = 3; + + /** + * This creates a socket file with name under {@link TermuxConstants#TERMUX_APPS_DIR_PATH}/<package name of caller> and listens on it. + * + * @param name Name of the socket file. + * @return The file descriptor of the created local server socket. + */ + ParcelFileDescriptor listenOnSocketFile(String name) = 4; + + + /** + * Opens a file under{@link TermuxConstants#TERMUX_APPS_DIR_PATH}/<package name of caller> with mode. + * + * @param name Name of the file. + * @þaram mode Mode to use. + */ + ParcelFileDescriptor openFile(String name, String mode) = 5; + + + + +} diff --git a/settings.gradle b/settings.gradle index b8045c242a..a049a5d06c 100644 --- a/settings.gradle +++ b/settings.gradle @@ -1 +1 @@ -include ':app', ':termux-shared', ':terminal-emulator', ':terminal-view' +include ':app', ':termux-shared', ':terminal-emulator', ':terminal-view', ':plugin-aidl' diff --git a/termux-shared/src/main/java/com/termux/shared/android/BinderUtils.java b/termux-shared/src/main/java/com/termux/shared/android/BinderUtils.java new file mode 100644 index 0000000000..02a836fab5 --- /dev/null +++ b/termux-shared/src/main/java/com/termux/shared/android/BinderUtils.java @@ -0,0 +1,80 @@ +package com.termux.shared.android; + +import android.content.Context; +import android.os.Binder; +import android.os.Process; + +import com.termux.shared.termux.TermuxConstants; + +public class BinderUtils +{ + /** + * Get the package name of the calling process for the current Binder transaction of this thread. + * Returns null if no Binder transaction is being processed. + * + * @param c The context used to get the {@link android.content.pm.PackageManager}. + * @return The calling package name, or null if the thread isn't processing a Binder transaction. + */ + public static String getCallerPackageNameOrNull(Context c) { + if (c.getApplicationInfo().uid == Binder.getCallingUid()) { + if (Process.myPid() == Binder.getCallingPid()) { + return null; // same process as Termux + } else { + // calling process has the same uid: search all processes with the same uid for the pid + for (String pack : c.getPackageManager().getPackagesForUid(Binder.getCallingUid())) { + if (String.valueOf(Binder.getCallingPid()).equals(PackageUtils.getPackagePID(c, pack))) { + return pack; + } + } + return c.getPackageName(); // process could not be found under shared uid processes, user Termux as default + } + } else { + return c.getPackageManager().getNameForUid(Binder.getCallingUid()); // foreign uid, getNameForUid is correct + } + } + + /** + * Get the package name of the calling process for the current Binder transaction of this thread. + * Returns {@link Context#getPackageName()} if no Binder transaction is being processed. + * + * @param c The context used to get the {@link android.content.pm.PackageManager}. + * @return The calling package name, or {@link Context#getPackageName()} if the thread isn't processing a Binder transaction. + */ + public static String getCallerPackageName(Context c) { + String packageName = getCallerPackageNameOrNull(c); + if (packageName == null) { + return c.getPackageName(); + } else { + return packageName; + } + } + + /** + * @param c The context used to get the {@link android.content.pm.PackageManager}. + * @return The plugin directory of the calling package name under {@link TermuxConstants#TERMUX_APPS_DIR_PATH}, or null if {@link #getCallerPackageNameOrNull(Context)} returns null. + */ + public static String getCallingPluginDir(Context c) { + String packageName = getCallerPackageNameOrNull(c); + if (packageName == null) { + return null; + } + return TermuxConstants.TERMUX_APPS_DIR_PATH + "/" + packageName; + } + + + /** + * Throws a {@link SecurityException} if the calling process of the current Binder transaction doesn't have the RUN_COMMAND permission + * or the current thread isn't processing a Binder transaction. + * + * @param c The context used to get the {@link android.content.pm.PackageManager}. + */ + public static void enforceRunCommandPermission(Context c) { + c.enforceCallingPermission(TermuxConstants.PERMISSION_RUN_COMMAND, "Calling package "+ getCallerPackageName(c)+" does not have the" + TermuxConstants.PERMISSION_RUN_COMMAND + " permission."); + } + + + + + + +} diff --git a/termux-shared/src/main/java/com/termux/shared/net/socket/local/LocalServerSocket.java b/termux-shared/src/main/java/com/termux/shared/net/socket/local/LocalServerSocket.java index 385c203465..d3940167f7 100644 --- a/termux-shared/src/main/java/com/termux/shared/net/socket/local/LocalServerSocket.java +++ b/termux-shared/src/main/java/com/termux/shared/net/socket/local/LocalServerSocket.java @@ -122,6 +122,75 @@ public synchronized Error start() { return null; } + + /** + * Creates the socket and binds to it, but doesn't start the socket listener. + * You can use this to pass the server socket file descriptor to another application. + */ + public synchronized Error createSocket() { + Logger.logDebug(LOG_TAG, "start"); + + String path = mLocalSocketRunConfig.getPath(); + if (path == null || path.isEmpty()) { + return LocalSocketErrno.ERRNO_SERVER_SOCKET_PATH_NULL_OR_EMPTY.getError(mLocalSocketRunConfig.getTitle()); + } + if (!mLocalSocketRunConfig.isAbstractNamespaceSocket()) { + path = FileUtils.getCanonicalPath(path, null); + } + + // On Linux, sun_path is 108 bytes (UNIX_PATH_MAX) in size, so do an early check here to + // prevent useless parent directory creation since createServerSocket() call will fail since + // there is a native check as well. + if (path.getBytes(StandardCharsets.UTF_8).length > 108) { + return LocalSocketErrno.ERRNO_SERVER_SOCKET_PATH_TOO_LONG.getError(mLocalSocketRunConfig.getTitle(), path); + } + + int backlog = mLocalSocketRunConfig.getBacklog(); + if (backlog <= 0) { + return LocalSocketErrno.ERRNO_SERVER_SOCKET_BACKLOG_INVALID.getError(mLocalSocketRunConfig.getTitle(), backlog); + } + + Error error; + + // If server socket is not in abstract namespace + if (!mLocalSocketRunConfig.isAbstractNamespaceSocket()) { + if (!path.startsWith("/")) + return LocalSocketErrno.ERRNO_SERVER_SOCKET_PATH_NOT_ABSOLUTE.getError(mLocalSocketRunConfig.getTitle(), path); + + // Create the server socket file parent directory and set SERVER_SOCKET_PARENT_DIRECTORY_PERMISSIONS if missing + String socketParentPath = new File(path).getParent(); + error = FileUtils.validateDirectoryFileExistenceAndPermissions(mLocalSocketRunConfig.getTitle() + " server socket file parent", + socketParentPath, + null, true, + SERVER_SOCKET_PARENT_DIRECTORY_PERMISSIONS, true, true, + false, false); + if (error != null) + return error; + + + // Delete the server socket file to stop any existing servers and for bind() to succeed + error = deleteServerSocketFile(); + if (error != null) + return error; + } + + // Create the server socket + JniResult result = LocalSocketManager.createServerSocket(mLocalSocketRunConfig.getLogTitle() + " (server)", + path.getBytes(StandardCharsets.UTF_8), backlog); + if (result == null || result.retval != 0) { + return LocalSocketErrno.ERRNO_CREATE_SERVER_SOCKET_FAILED.getError(mLocalSocketRunConfig.getTitle(), JniResult.getErrorString(result)); + } + + int fd = result.intData; + if (fd < 0) { + return LocalSocketErrno.ERRNO_SERVER_SOCKET_FD_INVALID.getError(fd, mLocalSocketRunConfig.getTitle()); + } + + // Update fd to signify that server socket has been created successfully + mLocalSocketRunConfig.setFD(fd); + + return null; + } /** Stop server. */ public synchronized Error stop() { diff --git a/termux-shared/src/main/java/com/termux/shared/net/socket/local/LocalSocketManager.java b/termux-shared/src/main/java/com/termux/shared/net/socket/local/LocalSocketManager.java index e9d8cdd816..0af5d4e835 100644 --- a/termux-shared/src/main/java/com/termux/shared/net/socket/local/LocalSocketManager.java +++ b/termux-shared/src/main/java/com/termux/shared/net/socket/local/LocalSocketManager.java @@ -85,6 +85,25 @@ public synchronized Error start() { mIsRunning = true; return mServerSocket.start(); } + + /** + * Create the {@link LocalServerSocket} but don't start the handler thread. + * You can use this to pass the server socket file descriptor to another application. + */ + public synchronized Error createSocket() { + Logger.logDebugExtended(LOG_TAG, "start\n" + mLocalSocketRunConfig); + + if (!localSocketLibraryLoaded) { + try { + Logger.logDebug(LOG_TAG, "Loading \"" + LOCAL_SOCKET_LIBRARY + "\" library"); + System.loadLibrary(LOCAL_SOCKET_LIBRARY); + localSocketLibraryLoaded = true; + } catch (Exception e) { + return LocalSocketErrno.ERRNO_START_LOCAL_SOCKET_LIB_LOAD_FAILED_WITH_EXCEPTION.getError(e, LOCAL_SOCKET_LIBRARY, e.getMessage()); + } + } + return mServerSocket.createSocket(); + } /** * Stop the {@link LocalServerSocket} and stop listening for new {@link LocalClientSocket}. diff --git a/termux-shared/src/main/java/com/termux/shared/termux/TermuxConstants.java b/termux-shared/src/main/java/com/termux/shared/termux/TermuxConstants.java index 59f523af72..1ebff0308a 100644 --- a/termux-shared/src/main/java/com/termux/shared/termux/TermuxConstants.java +++ b/termux-shared/src/main/java/com/termux/shared/termux/TermuxConstants.java @@ -11,7 +11,7 @@ import java.util.List; /* - * Version: v0.52.0 + * Version: v0.53.0 * SPDX-License-Identifier: MIT * * Changelog @@ -277,6 +277,8 @@ * * - 0.52.0 (2022-06-18) * - Added `TERMUX_PREFIX_DIR_IGNORED_SUB_FILES_PATHS_TO_CONSIDER_AS_EMPTY`. + * - 0.53.0 (2022-08-22) + * - Added `PERMISSION_TERMUX_PLUGIN` and `PERMISSION_TERMUX_SIGNATURE`. */ /** @@ -879,6 +881,19 @@ public final class TermuxConstants { /** Android OS permission declared by Termux app in AndroidManifest.xml which can be requested by * 3rd party apps to run various commands in Termux app context */ public static final String PERMISSION_RUN_COMMAND = TERMUX_PACKAGE_NAME + ".permission.RUN_COMMAND"; // Default: "com.termux.permission.RUN_COMMAND" + + /** + * Android OS permission declared by Termux app in AndroidManifest.xml which can be requested by + * 3rd party apps to connect to the {@link com.termux.app.plugin.PluginService}. + */ + public static final String PERMISSION_TERMUX_PLUGIN = TERMUX_PACKAGE_NAME + ".permission.TERMUX_PLUGIN"; // Default: "com.termux.permission.TERMUX_PLUGIN" + + /** + * A permission only the Termux app can hold that can be used by 3rd party apps to restrict component access to Termux. + * The 3rd party apps should also verify the signature of the package {@link TermuxConstants#TERMUX_PACKAGE_NAME} + * and verify the request came from {@link TermuxConstants#TERMUX_PACKAGE_NAME}. + */ + public static final String PERMISSION_TERMUX_SIGNATURE = TERMUX_PACKAGE_NAME + ".permission.TERMUX_SIGNATURE"; // Default: "com.termux.permission.TERMUX_SIGNATURE" /** Termux property defined in termux.properties file as a secondary check to PERMISSION_RUN_COMMAND * to allow 3rd party apps to run various commands in Termux app context */ From d690e61b68a0331a9539b5a81258c1cc9e8c5c1d Mon Sep 17 00:00:00 2001 From: tareksander <57038324+tareksander@users.noreply.github.com> Date: Sun, 8 May 2022 20:25:43 +0200 Subject: [PATCH 03/10] Changed: The plugin directory is now under `/data/data/com.termux/files/apps/plugins`. --- .../main/aidl/com/termux/plugin_aidl/IPluginService.aidl | 4 ++-- .../main/java/com/termux/shared/android/BinderUtils.java | 2 +- .../java/com/termux/shared/termux/TermuxConstants.java | 9 ++++++++- 3 files changed, 11 insertions(+), 4 deletions(-) diff --git a/plugin-aidl/src/main/aidl/com/termux/plugin_aidl/IPluginService.aidl b/plugin-aidl/src/main/aidl/com/termux/plugin_aidl/IPluginService.aidl index 7106c0233e..4eb18d80b7 100644 --- a/plugin-aidl/src/main/aidl/com/termux/plugin_aidl/IPluginService.aidl +++ b/plugin-aidl/src/main/aidl/com/termux/plugin_aidl/IPluginService.aidl @@ -51,7 +51,7 @@ interface IPluginService { ParcelFileDescriptor[] runTask(String commandPath, in String[] arguments, in ParcelFileDescriptor stdin, String workdir, String commandLabel, String commandDescription, String commandHelp) = 3; /** - * This creates a socket file with name under {@link TermuxConstants#TERMUX_APPS_DIR_PATH}/<package name of caller> and listens on it. + * This creates a socket file with name under {@link TermuxConstants#TERMUX_APPS_DIR_PATH}/plugins/<package name of caller> and listens on it. * * @param name Name of the socket file. * @return The file descriptor of the created local server socket. @@ -60,7 +60,7 @@ interface IPluginService { /** - * Opens a file under{@link TermuxConstants#TERMUX_APPS_DIR_PATH}/<package name of caller> with mode. + * Opens a file under{@link TermuxConstants#TERMUX_APPS_DIR_PATH}/plugins/<package name of caller> with mode. * * @param name Name of the file. * @þaram mode Mode to use. diff --git a/termux-shared/src/main/java/com/termux/shared/android/BinderUtils.java b/termux-shared/src/main/java/com/termux/shared/android/BinderUtils.java index 02a836fab5..acfb2dacbf 100644 --- a/termux-shared/src/main/java/com/termux/shared/android/BinderUtils.java +++ b/termux-shared/src/main/java/com/termux/shared/android/BinderUtils.java @@ -58,7 +58,7 @@ public static String getCallingPluginDir(Context c) { if (packageName == null) { return null; } - return TermuxConstants.TERMUX_APPS_DIR_PATH + "/" + packageName; + return TermuxConstants.TERMUX_PLUGINS_DIR_PATH + "/" + packageName; } diff --git a/termux-shared/src/main/java/com/termux/shared/termux/TermuxConstants.java b/termux-shared/src/main/java/com/termux/shared/termux/TermuxConstants.java index 1ebff0308a..d1aedff386 100644 --- a/termux-shared/src/main/java/com/termux/shared/termux/TermuxConstants.java +++ b/termux-shared/src/main/java/com/termux/shared/termux/TermuxConstants.java @@ -11,7 +11,7 @@ import java.util.List; /* - * Version: v0.53.0 + * Version: v0.54.0 * SPDX-License-Identifier: MIT * * Changelog @@ -279,6 +279,8 @@ * - Added `TERMUX_PREFIX_DIR_IGNORED_SUB_FILES_PATHS_TO_CONSIDER_AS_EMPTY`. * - 0.53.0 (2022-08-22) * - Added `PERMISSION_TERMUX_PLUGIN` and `PERMISSION_TERMUX_SIGNATURE`. + * - 0.54.0 (2022-08-22) + * - Added `TERMUX_PLUGINS_DIR_PATH` and `TERMUX_PLUGINS_DIR`. */ /** @@ -684,6 +686,11 @@ public final class TermuxConstants { public static final String TERMUX_APPS_DIR_PATH = TERMUX_FILES_DIR_PATH + "/apps"; // Default: "/data/data/com.termux/files/apps" /** Termux and plugin apps directory */ public static final File TERMUX_APPS_DIR = new File(TERMUX_APPS_DIR_PATH); + + /** Termux and plugin apps directory path */ + public static final String TERMUX_PLUGINS_DIR_PATH = TERMUX_APPS_DIR_PATH + "/plugins"; // Default: "/data/data/com.termux/files/apps/plugins" + /** Termux and plugin apps directory */ + public static final File TERMUX_PLUGINS_DIR = new File(TERMUX_PLUGINS_DIR_PATH); /** Termux app $PREFIX directory path ignored sub file paths to consider it empty */ From dd246a64022150149593cceb870223e3c173f1d9 Mon Sep 17 00:00:00 2001 From: tareksander <57038324+tareksander@users.noreply.github.com> Date: Mon, 9 May 2022 00:50:25 +0200 Subject: [PATCH 04/10] Fixed: listenOnSocketFile() permission parameter error. --- app/src/main/java/com/termux/app/plugin/PluginService.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/java/com/termux/app/plugin/PluginService.java b/app/src/main/java/com/termux/app/plugin/PluginService.java index 934f6991d3..dba2218fdf 100644 --- a/app/src/main/java/com/termux/app/plugin/PluginService.java +++ b/app/src/main/java/com/termux/app/plugin/PluginService.java @@ -308,7 +308,7 @@ public ParcelFileDescriptor openFile(String name, String mode) { String filePath = fileInPluginDirOrThrow(name); - Error err = FileUtils.createRegularFile("("+BinderUtils.getCallerPackageName(PluginService.this)+")", filePath, "rw", true, false); + Error err = FileUtils.createRegularFile("("+BinderUtils.getCallerPackageName(PluginService.this)+")", filePath, "rw-", true, false); if (err != null) { throw new UnsupportedOperationException(err.getErrorLogString()); } From 9bc59ac0a5ca2d6b0768a533239b00f384ab6d11 Mon Sep 17 00:00:00 2001 From: tareksander <57038324+tareksander@users.noreply.github.com> Date: Fri, 27 May 2022 23:28:33 +0200 Subject: [PATCH 05/10] Changed: Listening to a socket from listenOnSocketFile() gives a permission error. listenOnSocketFile() now doesn't send the server socket, but sends the client sockets via new socketConnection() callback. --- .../com/termux/app/plugin/PluginService.java | 88 +++++++++++++------ .../termux/plugin_aidl/IPluginCallback.aidl | 9 +- .../termux/plugin_aidl/IPluginService.aidl | 8 +- .../net/socket/local/LocalServerSocket.java | 69 --------------- .../net/socket/local/LocalSocketManager.java | 19 ---- 5 files changed, 72 insertions(+), 121 deletions(-) diff --git a/app/src/main/java/com/termux/app/plugin/PluginService.java b/app/src/main/java/com/termux/app/plugin/PluginService.java index dba2218fdf..4214a9526c 100644 --- a/app/src/main/java/com/termux/app/plugin/PluginService.java +++ b/app/src/main/java/com/termux/app/plugin/PluginService.java @@ -5,13 +5,13 @@ import android.content.Context; import android.content.Intent; import android.content.ServiceConnection; +import android.os.BadParcelableException; import android.os.Binder; import android.os.IBinder; -import android.os.Parcel; +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; @@ -25,9 +25,9 @@ import com.termux.shared.logger.Logger; import com.termux.shared.net.socket.local.ILocalSocketManager; import com.termux.shared.net.socket.local.LocalClientSocket; -import com.termux.shared.net.socket.local.LocalServerSocket; 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.termux.plugins.TermuxPluginUtils; import java.io.File; @@ -138,16 +138,28 @@ private void addClient(@NonNull IPluginCallback callback) { if (mConnectedPlugins.get(pid) != null) return; // client already in list try { mConnectedPlugins.put(pid, new Plugin(pid, uid, callback)); - } catch (RemoteException ignored) {} + } catch (RemoteException ignored) {} // RemoteException is thrown if the callback binder is already dead, plugin isn't added to the list } - - private void checkClient() throws IllegalStateException { - if (! mConnectedPlugins.containsKey(Binder.getCallingPid())) Logger.logDebug("client not in list"); - if (! mConnectedPlugins.containsKey(Binder.getCallingPid())) throw new IllegalStateException("Please call setCallbackBinder first"); + @NonNull + private Plugin checkClient() throws IllegalStateException { + Plugin p = mConnectedPlugins.get(Binder.getCallingPid()); + if (p == null) { + Logger.logDebug("client not in list"); + throw new IllegalStateException("Please call setCallbackBinder first"); + } + return p; } + private void atPluginDeath(@NonNull Plugin p, Runnable r) { + try { + p.callback.asBinder().linkToDeath(r::run, 0); + } catch (RemoteException e) { + r.run(); + } + } + private class PluginServiceBinder extends IPluginService.Stub { @@ -265,37 +277,59 @@ public ParcelFileDescriptor[] runTask(String commandPath, String[] arguments, Pa } - @NonNull @Override - public ParcelFileDescriptor listenOnSocketFile(String name) { + public void listenOnSocketFile(String name) { externalAppsOrThrow(); if (name == null) throw new NullPointerException("Passed name is null"); - checkClient(); + Plugin p = checkClient(); String socketPath = fileInPluginDirOrThrow(name); - // passing null for localSocketManagerClient here should be okay, because the handler methods don't get called - @SuppressWarnings("ConstantConditions") - LocalSocketRunConfig conf = new LocalSocketRunConfig(BinderUtils.getCallerPackageName(PluginService.this) + " socket: " + name, socketPath, null); + LocalSocketRunConfig conf = new LocalSocketRunConfig(BinderUtils.getCallerPackageName(PluginService.this) + " socket: " + name, socketPath, new ILocalSocketManager() + { + @Nullable + @Override + public Thread.UncaughtExceptionHandler getLocalSocketManagerClientThreadUEH(@NonNull LocalSocketManager localSocketManager) { + return null; + } + + @Override + public void onError(@NonNull LocalSocketManager localSocketManager, @Nullable LocalClientSocket clientSocket, @NonNull Error error) { + Logger.logDebug("PluginService:ILocalSocketManager", "Error: "+error.getErrorLogString()); + } + + @Override + public void onDisallowedClientConnected(@NonNull LocalSocketManager localSocketManager, @NonNull LocalClientSocket clientSocket, @NonNull Error error) { + PeerCred pc = clientSocket.getPeerCred(); + Logger.logDebug("PluginService:ILocalSocketManager", "Connection from a disallowed UID on a plugin socket detected:\n UID: "+pc.uid+ + "\n Name: "+pc.pname+ "\n Cmdline: "+pc.cmdline+ "\n pid: "+pc.pid); + } + + @Override + public void onClientAccepted(@NonNull LocalSocketManager localSocketManager, @NonNull LocalClientSocket clientSocket) { + try { + p.callback.socketConnection(name, ParcelFileDescriptor.fromFd(clientSocket.getFD())); + } + catch (RemoteException | BadParcelableException | IllegalArgumentException | IllegalStateException + | NullPointerException | SecurityException | UnsupportedOperationException | NetworkOnMainThreadException | IOException e) { + Logger.logStackTrace("PluginService:ILocalSocketManager", e); + } + try { + clientSocket.close(); // close the socket, the plugin received a dup of the fd + } + catch (IOException e) { + Logger.logStackTrace("PluginService:ILocalSocketManager", e); + } + } + }); LocalSocketManager m = new LocalSocketManager(PluginService.this, conf); - Error err = m.createSocket(); + Error err = m.start(); if (err != null) { throw new UnsupportedOperationException("Error: "+err.getErrorLogString()); } - - ParcelFileDescriptor socketPfd; - - try { - socketPfd = ParcelFileDescriptor.fromFd(conf.getFD()); - } - catch (IOException e) { - throw new UnsupportedOperationException(e); - } finally { - m.getServerSocket().closeServerSocket(true); - } - return socketPfd; + atPluginDeath(p, m::stop); } diff --git a/plugin-aidl/src/main/aidl/com/termux/plugin_aidl/IPluginCallback.aidl b/plugin-aidl/src/main/aidl/com/termux/plugin_aidl/IPluginCallback.aidl index 8c4e55081d..b9a5aabe49 100644 --- a/plugin-aidl/src/main/aidl/com/termux/plugin_aidl/IPluginCallback.aidl +++ b/plugin-aidl/src/main/aidl/com/termux/plugin_aidl/IPluginCallback.aidl @@ -25,8 +25,13 @@ interface IPluginCallback { */ int getCallbackVersion() = 1; - - + /** + * This gets called when a connection is made on a socket created with {@link com.termux.plugin_aidl.IPluginService#listenOnSocketFile}. + * + * @param sockname The name of socket file the connection was made on (the relative path to the plugin directory). + * @param connection The connection file descriptor. + */ + void socketConnection(String sockname, in ParcelFileDescriptor connection) = 2; diff --git a/plugin-aidl/src/main/aidl/com/termux/plugin_aidl/IPluginService.aidl b/plugin-aidl/src/main/aidl/com/termux/plugin_aidl/IPluginService.aidl index 4eb18d80b7..668db9093c 100644 --- a/plugin-aidl/src/main/aidl/com/termux/plugin_aidl/IPluginService.aidl +++ b/plugin-aidl/src/main/aidl/com/termux/plugin_aidl/IPluginService.aidl @@ -51,16 +51,16 @@ interface IPluginService { ParcelFileDescriptor[] runTask(String commandPath, in String[] arguments, in ParcelFileDescriptor stdin, String workdir, String commandLabel, String commandDescription, String commandHelp) = 3; /** - * This creates a socket file with name under {@link TermuxConstants#TERMUX_APPS_DIR_PATH}/plugins/<package name of caller> and listens on it. + * This creates a socket file with name under {@link com.termux.shared.termux.TermuxConstants#TERMUX_PLUGINS_DIR_PATH}/<package name of caller>. + * Connections are transferred to the plugin via the {@link com.termux.plugin_aidl.IPluginService#socketConnection} method. * * @param name Name of the socket file. - * @return The file descriptor of the created local server socket. */ - ParcelFileDescriptor listenOnSocketFile(String name) = 4; + void listenOnSocketFile(String name) = 4; /** - * Opens a file under{@link TermuxConstants#TERMUX_APPS_DIR_PATH}/plugins/<package name of caller> with mode. + * Opens a file under {@link com.termux.shared.termux.TermuxConstants#TERMUX_PLUGINS_DIR_PATH}/<package name of caller> with mode. * * @param name Name of the file. * @þaram mode Mode to use. diff --git a/termux-shared/src/main/java/com/termux/shared/net/socket/local/LocalServerSocket.java b/termux-shared/src/main/java/com/termux/shared/net/socket/local/LocalServerSocket.java index d3940167f7..385c203465 100644 --- a/termux-shared/src/main/java/com/termux/shared/net/socket/local/LocalServerSocket.java +++ b/termux-shared/src/main/java/com/termux/shared/net/socket/local/LocalServerSocket.java @@ -122,75 +122,6 @@ public synchronized Error start() { return null; } - - /** - * Creates the socket and binds to it, but doesn't start the socket listener. - * You can use this to pass the server socket file descriptor to another application. - */ - public synchronized Error createSocket() { - Logger.logDebug(LOG_TAG, "start"); - - String path = mLocalSocketRunConfig.getPath(); - if (path == null || path.isEmpty()) { - return LocalSocketErrno.ERRNO_SERVER_SOCKET_PATH_NULL_OR_EMPTY.getError(mLocalSocketRunConfig.getTitle()); - } - if (!mLocalSocketRunConfig.isAbstractNamespaceSocket()) { - path = FileUtils.getCanonicalPath(path, null); - } - - // On Linux, sun_path is 108 bytes (UNIX_PATH_MAX) in size, so do an early check here to - // prevent useless parent directory creation since createServerSocket() call will fail since - // there is a native check as well. - if (path.getBytes(StandardCharsets.UTF_8).length > 108) { - return LocalSocketErrno.ERRNO_SERVER_SOCKET_PATH_TOO_LONG.getError(mLocalSocketRunConfig.getTitle(), path); - } - - int backlog = mLocalSocketRunConfig.getBacklog(); - if (backlog <= 0) { - return LocalSocketErrno.ERRNO_SERVER_SOCKET_BACKLOG_INVALID.getError(mLocalSocketRunConfig.getTitle(), backlog); - } - - Error error; - - // If server socket is not in abstract namespace - if (!mLocalSocketRunConfig.isAbstractNamespaceSocket()) { - if (!path.startsWith("/")) - return LocalSocketErrno.ERRNO_SERVER_SOCKET_PATH_NOT_ABSOLUTE.getError(mLocalSocketRunConfig.getTitle(), path); - - // Create the server socket file parent directory and set SERVER_SOCKET_PARENT_DIRECTORY_PERMISSIONS if missing - String socketParentPath = new File(path).getParent(); - error = FileUtils.validateDirectoryFileExistenceAndPermissions(mLocalSocketRunConfig.getTitle() + " server socket file parent", - socketParentPath, - null, true, - SERVER_SOCKET_PARENT_DIRECTORY_PERMISSIONS, true, true, - false, false); - if (error != null) - return error; - - - // Delete the server socket file to stop any existing servers and for bind() to succeed - error = deleteServerSocketFile(); - if (error != null) - return error; - } - - // Create the server socket - JniResult result = LocalSocketManager.createServerSocket(mLocalSocketRunConfig.getLogTitle() + " (server)", - path.getBytes(StandardCharsets.UTF_8), backlog); - if (result == null || result.retval != 0) { - return LocalSocketErrno.ERRNO_CREATE_SERVER_SOCKET_FAILED.getError(mLocalSocketRunConfig.getTitle(), JniResult.getErrorString(result)); - } - - int fd = result.intData; - if (fd < 0) { - return LocalSocketErrno.ERRNO_SERVER_SOCKET_FD_INVALID.getError(fd, mLocalSocketRunConfig.getTitle()); - } - - // Update fd to signify that server socket has been created successfully - mLocalSocketRunConfig.setFD(fd); - - return null; - } /** Stop server. */ public synchronized Error stop() { diff --git a/termux-shared/src/main/java/com/termux/shared/net/socket/local/LocalSocketManager.java b/termux-shared/src/main/java/com/termux/shared/net/socket/local/LocalSocketManager.java index 0af5d4e835..e9d8cdd816 100644 --- a/termux-shared/src/main/java/com/termux/shared/net/socket/local/LocalSocketManager.java +++ b/termux-shared/src/main/java/com/termux/shared/net/socket/local/LocalSocketManager.java @@ -85,25 +85,6 @@ public synchronized Error start() { mIsRunning = true; return mServerSocket.start(); } - - /** - * Create the {@link LocalServerSocket} but don't start the handler thread. - * You can use this to pass the server socket file descriptor to another application. - */ - public synchronized Error createSocket() { - Logger.logDebugExtended(LOG_TAG, "start\n" + mLocalSocketRunConfig); - - if (!localSocketLibraryLoaded) { - try { - Logger.logDebug(LOG_TAG, "Loading \"" + LOCAL_SOCKET_LIBRARY + "\" library"); - System.loadLibrary(LOCAL_SOCKET_LIBRARY); - localSocketLibraryLoaded = true; - } catch (Exception e) { - return LocalSocketErrno.ERRNO_START_LOCAL_SOCKET_LIB_LOAD_FAILED_WITH_EXCEPTION.getError(e, LOCAL_SOCKET_LIBRARY, e.getMessage()); - } - } - return mServerSocket.createSocket(); - } /** * Stop the {@link LocalServerSocket} and stop listening for new {@link LocalClientSocket}. From 44267d65822063b995bf7efb04bbc4b67ec131e8 Mon Sep 17 00:00:00 2001 From: tareksander <57038324+tareksander@users.noreply.github.com> Date: Sat, 11 Jun 2022 16:52:26 +0200 Subject: [PATCH 06/10] Added: NativeShell as a task runner that is only available ofer the plugin system. --- .../java/com/termux/app/TermuxService.java | 26 ++- .../com/termux/app/plugin/PluginService.java | 211 +++++++++++++++--- .../termux/plugin_aidl/IPluginService.aidl | 11 +- .../aidl/com/termux/plugin_aidl/Task.aidl | 9 + .../main/java/com/termux/terminal/JNI.java | 19 +- terminal-emulator/src/main/jni/termux.c | 104 +++++++++ .../shell/command/ExecutionCommand.java | 12 +- .../runner/nativerunner/NativeShell.java | 77 +++++++ .../termux/shell/TermuxShellManager.java | 6 + 9 files changed, 429 insertions(+), 46 deletions(-) create mode 100644 plugin-aidl/src/main/aidl/com/termux/plugin_aidl/Task.aidl create mode 100644 termux-shared/src/main/java/com/termux/shared/shell/command/runner/nativerunner/NativeShell.java diff --git a/app/src/main/java/com/termux/app/TermuxService.java b/app/src/main/java/com/termux/app/TermuxService.java index 6f7381cadb..cc5dcf16ac 100644 --- a/app/src/main/java/com/termux/app/TermuxService.java +++ b/app/src/main/java/com/termux/app/TermuxService.java @@ -22,6 +22,7 @@ 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 @@ private synchronized void killAllTermuxExecutionCommands() { else mShellManager.mTermuxTasks.remove(termuxTasks.get(i)); } + + List 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 @@ else if (Runner.TERMINAL_SESSION.equalsRunner(executionCommand.runner)) 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 @@ private Notification buildNotification() { // 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"); diff --git a/app/src/main/java/com/termux/app/plugin/PluginService.java b/app/src/main/java/com/termux/app/plugin/PluginService.java index 4214a9526c..f124357074 100644 --- a/app/src/main/java/com/termux/app/plugin/PluginService.java +++ b/app/src/main/java/com/termux/app/plugin/PluginService.java @@ -7,11 +7,14 @@ 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 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.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 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 tasks = Collections.synchronizedMap(new HashMap<>()); @NonNull IPluginCallback callback; int cachedCallbackVersion; @@ -89,35 +136,6 @@ public void onDestroy() { unbindService(mTermuxServiceConnection); } - // map of connected clients by PID - private final Map 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 void onNullBinding(ComponentName name) { @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; } diff --git a/plugin-aidl/src/main/aidl/com/termux/plugin_aidl/IPluginService.aidl b/plugin-aidl/src/main/aidl/com/termux/plugin_aidl/IPluginService.aidl index 668db9093c..2fcfd1f82d 100644 --- a/plugin-aidl/src/main/aidl/com/termux/plugin_aidl/IPluginService.aidl +++ b/plugin-aidl/src/main/aidl/com/termux/plugin_aidl/IPluginService.aidl @@ -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 the wiki. - * If a parameter is null it is treated the same as if the extra isn't in the intent. - *

- * 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}/<package name of caller>. diff --git a/plugin-aidl/src/main/aidl/com/termux/plugin_aidl/Task.aidl b/plugin-aidl/src/main/aidl/com/termux/plugin_aidl/Task.aidl new file mode 100644 index 0000000000..1cd11328c0 --- /dev/null +++ b/plugin-aidl/src/main/aidl/com/termux/plugin_aidl/Task.aidl @@ -0,0 +1,9 @@ +package com.termux.plugin_aidl; + +import android.os.ParcelFileDescriptor; + +parcelable Task { + ParcelFileDescriptor stdout; + ParcelFileDescriptor stderr; + int pid; +} diff --git a/terminal-emulator/src/main/java/com/termux/terminal/JNI.java b/terminal-emulator/src/main/java/com/termux/terminal/JNI.java index 99229a6528..4faf759a3b 100644 --- a/terminal-emulator/src/main/java/com/termux/terminal/JNI.java +++ b/terminal-emulator/src/main/java/com/termux/terminal/JNI.java @@ -3,7 +3,7 @@ /** * 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); diff --git a/terminal-emulator/src/main/jni/termux.c b/terminal-emulator/src/main/jni/termux.c index 22f1d507d1..92bb16f9ee 100644 --- a/terminal-emulator/src/main/jni/termux.c +++ b/terminal-emulator/src/main/jni/termux.c @@ -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; +} + diff --git a/termux-shared/src/main/java/com/termux/shared/shell/command/ExecutionCommand.java b/termux-shared/src/main/java/com/termux/shared/shell/command/ExecutionCommand.java index 5a97f9664b..c0954469eb 100644 --- a/termux-shared/src/main/java/com/termux/shared/shell/command/ExecutionCommand.java +++ b/termux-shared/src/main/java/com/termux/shared/shell/command/ExecutionCommand.java @@ -2,6 +2,7 @@ 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 static ShellCreateMode modeOf(String mode) { 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; diff --git a/termux-shared/src/main/java/com/termux/shared/shell/command/runner/nativerunner/NativeShell.java b/termux-shared/src/main/java/com/termux/shared/shell/command/runner/nativerunner/NativeShell.java new file mode 100644 index 0000000000..cdf082fc12 --- /dev/null +++ b/termux-shared/src/main/java/com/termux/shared/shell/command/runner/nativerunner/NativeShell.java @@ -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; + } +} diff --git a/termux-shared/src/main/java/com/termux/shared/termux/shell/TermuxShellManager.java b/termux-shared/src/main/java/com/termux/shared/termux/shell/TermuxShellManager.java index b5477b990b..77b89e10cf 100644 --- a/termux-shared/src/main/java/com/termux/shared/termux/shell/TermuxShellManager.java +++ b/termux-shared/src/main/java/com/termux/shared/termux/shell/TermuxShellManager.java @@ -8,6 +8,7 @@ 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 mTermuxTasks = new ArrayList<>(); + /** + * The background NativeShell tasks which this service manages. + */ + public final List mTermuxNativeTasks = new ArrayList<>(); + /** * The pending plugin ExecutionCommands that have yet to be processed by this service. */ From f459ee481e746e055d0e5526b25a3d6ee932f1d1 Mon Sep 17 00:00:00 2001 From: tareksander <57038324+tareksander@users.noreply.github.com> Date: Sat, 18 Jun 2022 20:11:17 +0200 Subject: [PATCH 07/10] Added|Fixed: NativeShell now works, fixed the Termux task notification not updating for NativeShell tasks. Added taskFinished callback for plugins and signalTask method for plugins to call. --- .../java/com/termux/app/TermuxService.java | 12 ++- .../com/termux/app/plugin/PluginService.java | 83 ++++++++++++------- .../termux/plugin_aidl/IPluginCallback.aidl | 8 +- .../termux/plugin_aidl/IPluginService.aidl | 22 ++++- .../runner/nativerunner/NativeShell.java | 13 ++- 5 files changed, 94 insertions(+), 44 deletions(-) diff --git a/app/src/main/java/com/termux/app/TermuxService.java b/app/src/main/java/com/termux/app/TermuxService.java index cc5dcf16ac..4a16bea50c 100644 --- a/app/src/main/java/com/termux/app/TermuxService.java +++ b/app/src/main/java/com/termux/app/TermuxService.java @@ -14,6 +14,7 @@ import android.os.Handler; import android.os.IBinder; import android.os.PowerManager; +import android.os.Process; import androidx.annotation.NonNull; import androidx.annotation.Nullable; @@ -291,7 +292,7 @@ private synchronized void killAllTermuxExecutionCommands() { List termuxNativeTasks = new ArrayList<>(mShellManager.mTermuxNativeTasks); for (int i = 0; i < termuxNativeTasks.size(); i++) { - termuxNativeTasks.get(i).kill(); + termuxNativeTasks.get(i).kill(Process.SIGNAL_KILL); } for (int i = 0; i < pendingPluginExecutionCommands.size(); i++) { @@ -442,7 +443,14 @@ public NativeShell executeNativeShell(ExecutionCommand executionCommand, String[ }); client.terminated(exitCode, error); }, environment); - shell[0].execute(); + mShellManager.mTermuxNativeTasks.add(shell[0]); + + if (shell[0].execute()) { + updateNotification(); + } else { + // gets removed automatically by the callback + return null; + } return shell[0]; } diff --git a/app/src/main/java/com/termux/app/plugin/PluginService.java b/app/src/main/java/com/termux/app/plugin/PluginService.java index f124357074..1f573dd446 100644 --- a/app/src/main/java/com/termux/app/plugin/PluginService.java +++ b/app/src/main/java/com/termux/app/plugin/PluginService.java @@ -35,15 +35,13 @@ import com.termux.shared.shell.command.ExecutionCommand; import com.termux.shared.shell.command.runner.nativerunner.NativeShell; import com.termux.shared.termux.plugins.TermuxPluginUtils; +import com.termux.shared.termux.shell.TermuxShellEnvironmentClient; -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.Arrays; import java.util.Collections; import java.util.HashMap; import java.util.List; @@ -284,13 +282,24 @@ public void onNullBinding(ComponentName name) { public Task runTask(String commandPath, String[] arguments, ParcelFileDescriptor stdin, String workdir, String[] environment) { externalAppsOrThrow(); if (commandPath == null) throw new NullPointerException("Passed commandPath is null"); + if (workdir == null) throw new NullPointerException("Passed workdir is null"); if (stdin == null) throw new NullPointerException("Passed stdin is null"); Plugin p = checkClient(); BinderUtils.enforceRunCommandPermission(PluginService.this); + + + List env = (environment != null) ? Arrays.asList(environment) : new ArrayList<>(); + env.addAll(Arrays.asList(new TermuxShellEnvironmentClient(). + buildEnvironment(PluginService.this, + false, + workdir))); + String[] realEnvironment = env.toArray(new String[0]); + + final Object sync = new Object(); - final RuntimeException[] ex = new RuntimeException[1]; + final Exception[] ex = new Exception[1]; final boolean[] finished = {false}; final NativeShell[] shell = new NativeShell[1]; // create pipes @@ -327,7 +336,8 @@ public Task runTask(String commandPath, String[] arguments, ParcelFileDescriptor } in[1] = pipes[0]; out[1] = pipes[1]; - + + // start the Task on the main thread, TermuxService is not synchronized itself mMainThreadHandler.post(() -> { TermuxService s = mTermuxService; if (s == null) { @@ -346,31 +356,27 @@ public Task runTask(String commandPath, String[] arguments, ParcelFileDescriptor 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) {} + shell[0] = s.executeNativeShell(cmd, realEnvironment, (exitCode, error) -> { + if (error != null) { + ex[0] = error; + } else { + p.tasks.remove(shell[0].getPid()); + try { + p.callback.taskFinished(shell[0].getPid(), exitCode); + } catch (Exception ignored) {} + } }); - p.tasks.put(shell[0].getPid(), shell[0]); + if (shell[0] != null) { + p.tasks.put(shell[0].getPid(), shell[0]); + } else { + while (ex[0] == null) { + // wait until the exception is caught if the Task could not be started + try { + Thread.sleep(1); + } catch (InterruptedException ignored) {} + } + } synchronized (sync) { finished[0] = true; sync.notifyAll(); @@ -411,7 +417,7 @@ public Task runTask(String commandPath, String[] arguments, ParcelFileDescriptor try { in[1].close(); } catch (IOException ignored) {} - throw ex[0]; + throw new RuntimeException(ex[0]); } Task t = new Task(); @@ -421,7 +427,22 @@ public Task runTask(String commandPath, String[] arguments, ParcelFileDescriptor return t; } - + @Override + public boolean signalTask(int pid, int signal) { + Plugin p = checkClient(); + BinderUtils.enforceRunCommandPermission(PluginService.this); + + // only allow to signal processes that were started as Tasks by the plugin + NativeShell shell = p.tasks.get(pid); + if (shell != null) { + shell.kill(signal); + return true; + } else { + return false; + } + } + + @Override public void listenOnSocketFile(String name) { externalAppsOrThrow(); diff --git a/plugin-aidl/src/main/aidl/com/termux/plugin_aidl/IPluginCallback.aidl b/plugin-aidl/src/main/aidl/com/termux/plugin_aidl/IPluginCallback.aidl index b9a5aabe49..662ecc2536 100644 --- a/plugin-aidl/src/main/aidl/com/termux/plugin_aidl/IPluginCallback.aidl +++ b/plugin-aidl/src/main/aidl/com/termux/plugin_aidl/IPluginCallback.aidl @@ -34,7 +34,13 @@ interface IPluginCallback { void socketConnection(String sockname, in ParcelFileDescriptor connection) = 2; - + /** + * Gets called when a started Task exits. + * + * @param pid The pid of the task that exited. + * @param code The exit code of the Task. Negative values indicate the Task was killed by a signal. The signal number is then {@code -code}. + */ + void taskFinished(int pid, int code) = 3; diff --git a/plugin-aidl/src/main/aidl/com/termux/plugin_aidl/IPluginService.aidl b/plugin-aidl/src/main/aidl/com/termux/plugin_aidl/IPluginService.aidl index 2fcfd1f82d..ebd6982544 100644 --- a/plugin-aidl/src/main/aidl/com/termux/plugin_aidl/IPluginService.aidl +++ b/plugin-aidl/src/main/aidl/com/termux/plugin_aidl/IPluginService.aidl @@ -43,17 +43,33 @@ interface IPluginService { /** * Runs a command in a Termux task in the background. - * stdin, commandPath and workdir are required parameters. + * stdin, commandPath and workdir are required parameters.
+ * When a Task exists, the {@link com.termux.plugin_aidl.IPluginCallback#taskFinished} callback gets invoked. + * + * @param commandPath The full absolute path of the command binary to run. + * @param arguments The command line arguments for the command. The first argument should be the same as {@code commandPath} (this fill be the 0th argument). + * @param stdin The {@link android.os.ParcelFileDescriptor} used for the standard input of the command. You can e.g. create a pipe and use the read end here. + * @param workdir The working directory of the command, also as an absolute path. + * @param environment Additional environment variables for the command. Can be {@code null}. + * @return A {@link com.termux.plugin_aidl.Task} that represents the running Task. It contains pipes for reading stdout and stderr. */ Task runTask(String commandPath, in String[] arguments, in ParcelFileDescriptor stdin, String workdir, in String[] environment) = 3; + /** + * Send a signal to a Task. + * @param pid The pid of the Task to kill. + * @param signal The signal number to use. Using {@code kill -l} in bash and other shells lists the signals with their numbers. + * @return Returns true if there was a Task with this pid, false if there was none. + */ + boolean signalTask(int pid, int signal) = 4; + /** * This creates a socket file with name under {@link com.termux.shared.termux.TermuxConstants#TERMUX_PLUGINS_DIR_PATH}/<package name of caller>. * Connections are transferred to the plugin via the {@link com.termux.plugin_aidl.IPluginService#socketConnection} method. * * @param name Name of the socket file. */ - void listenOnSocketFile(String name) = 4; + void listenOnSocketFile(String name) = 5; /** @@ -62,7 +78,7 @@ interface IPluginService { * @param name Name of the file. * @þaram mode Mode to use. */ - ParcelFileDescriptor openFile(String name, String mode) = 5; + ParcelFileDescriptor openFile(String name, String mode) = 6; diff --git a/termux-shared/src/main/java/com/termux/shared/shell/command/runner/nativerunner/NativeShell.java b/termux-shared/src/main/java/com/termux/shared/shell/command/runner/nativerunner/NativeShell.java index cdf082fc12..712706194a 100644 --- a/termux-shared/src/main/java/com/termux/shared/shell/command/runner/nativerunner/NativeShell.java +++ b/termux-shared/src/main/java/com/termux/shared/shell/command/runner/nativerunner/NativeShell.java @@ -1,11 +1,8 @@ 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; @@ -42,7 +39,7 @@ public interface Client { void terminated(int exitCode, Exception error); } - public synchronized void execute() { + public synchronized boolean execute() { try { pid = JNI.createTask(exe.executable, exe.workingDirectory, exe.arguments, env, exe.stdinFD.getFd(), exe.stdoutFD.getFd(), exe.stderrFD.getFd()); new Thread(() -> { @@ -50,7 +47,8 @@ public synchronized void execute() { client.terminated(exit, null); pid = -1; }).start(); - } catch (RuntimeException e) { + return true; + } catch (Exception e) { client.terminated(0, e); } finally { // close the ParcelFileDescriptors @@ -64,11 +62,12 @@ public synchronized void execute() { exe.stderrFD.close(); } catch (IOException ignored) {} } + return false; } - public synchronized void kill() { + public synchronized void kill(int signal) { if (pid != -1) - Process.killProcess(pid); + Process.sendSignal(pid, signal); } public synchronized int getPid() { From b1fe382dff5c5768bafe3d582ba0f998e73de9fc Mon Sep 17 00:00:00 2001 From: tareksander <57038324+tareksander@users.noreply.github.com> Date: Fri, 5 Aug 2022 14:59:35 +0200 Subject: [PATCH 08/10] Added|Fixed: Added the option to specify the binding priority for callback services, fixed callback services, fixed plugin UID accidentally being set to the PID. --- app/src/main/AndroidManifest.xml | 1 + .../com/termux/app/plugin/PluginService.java | 107 ++++++++++++------ .../termux/plugin_aidl/IPluginCallback.aidl | 2 + .../termux/plugin_aidl/IPluginService.aidl | 31 ++++- 4 files changed, 105 insertions(+), 36 deletions(-) diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 1271f6c7c4..3d934944b3 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -30,6 +30,7 @@ + diff --git a/app/src/main/java/com/termux/app/plugin/PluginService.java b/app/src/main/java/com/termux/app/plugin/PluginService.java index 1f573dd446..1dc75b26f4 100644 --- a/app/src/main/java/com/termux/app/plugin/PluginService.java +++ b/app/src/main/java/com/termux/app/plugin/PluginService.java @@ -14,7 +14,6 @@ import android.os.ParcelFileDescriptor; import android.os.Process; import android.os.RemoteException; -import android.util.Log; import androidx.annotation.NonNull; import androidx.annotation.Nullable; @@ -97,7 +96,8 @@ private class Plugin { Map tasks = Collections.synchronizedMap(new HashMap<>()); @NonNull IPluginCallback callback; - int cachedCallbackVersion; + final int cachedCallbackVersion; + ServiceConnection con = null; Plugin(int pid, int uid, @NonNull IPluginCallback callback) throws RemoteException { this.pid = pid; @@ -106,6 +106,11 @@ private class Plugin { callback.asBinder().linkToDeath(() -> mConnectedPlugins.remove(pid), 0); // remove self when the callback binder dies cachedCallbackVersion = callback.getCallbackVersion(); } + + Plugin(int pid, int uid, @NonNull IPluginCallback callback, ServiceConnection con) throws RemoteException { + this(pid, uid, callback); + this.con = con; + } @Override @@ -132,6 +137,13 @@ public void onCreate() { @Override public void onDestroy() { unbindService(mTermuxServiceConnection); + for (Map.Entry e : mConnectedPlugins.entrySet()) { + if (e.getValue().con != null) { + try { + unbindService(e.getValue().con); + } catch (IllegalArgumentException ignored) {} + } + } } @@ -149,7 +161,7 @@ public IBinder onBind(Intent intent) { * If not already, this creates an entry in the map of connected plugins for the current Binder client. */ private void addClient(@NonNull IPluginCallback callback) { - int pid = Binder.getCallingPid(), uid = Binder.getCallingPid(); + int pid = Binder.getCallingPid(), uid = Binder.getCallingUid(); if (pid == Process.myPid()) return; // no client connected if (mConnectedPlugins.get(pid) != null) return; // client already in list try { @@ -157,6 +169,16 @@ private void addClient(@NonNull IPluginCallback callback) { } catch (RemoteException ignored) {} // RemoteException is thrown if the callback binder is already dead, plugin isn't added to the list } + /** + * If not already, this creates an entry in the map of connected plugins for the current Binder client. + */ + private void addClientWithCallbackService(@NonNull IPluginCallback callback, int pid, int uid, @NonNull ServiceConnection con) { + if (mConnectedPlugins.get(pid) != null) return; // client already in list + try { + mConnectedPlugins.put(pid, new Plugin(pid, uid, callback, con)); + } catch (RemoteException ignored) {} // RemoteException is thrown if the callback binder is already dead, plugin isn't added to the list + } + @NonNull private Plugin checkClient() throws IllegalStateException { Plugin p = mConnectedPlugins.get(Binder.getCallingPid()); @@ -212,69 +234,86 @@ private String fileInPluginDirOrThrow(String name) { public void setCallbackBinder(IPluginCallback callback) { externalAppsOrThrow(); if (callback == null) throw new NullPointerException("Passed callback binder is null"); + if (mConnectedPlugins.get(Binder.getCallingPid()) != null) { + throw new IllegalStateException("Callback binder already set (there is only one global connection to the plugin service, all new ones use the already initialized one)"); + } addClient(callback); } @Override - public void setCallbackService(String componentNameString) { + public void setCallbackService(String componentNameString, int priority) { externalAppsOrThrow(); if (componentNameString == null) throw new NullPointerException("Passed componentName is null"); String callerPackageName = BinderUtils.getCallerPackageNameOrNull(PluginService.this); if (callerPackageName == null) throw new NullPointerException("Caller package is null"); + if (priority != PRIORITY_MIN && + priority != PRIORITY_NORMAL && + priority != PRIORITY_IMPORTANT && + priority != PRIORITY_MAX) + throw new IllegalArgumentException("Invalid priority parameter"); + + if (mConnectedPlugins.get(Binder.getCallingPid()) != null) { + throw new IllegalStateException("Callback binder already set (there is only one global connection to the plugin service, all new ones use the already initialized one)"); + } + ComponentName componentName = ComponentName.createRelative(callerPackageName, componentNameString); Intent callbackStartIntent = new Intent(); callbackStartIntent.setComponent(componentName); - - - final boolean[] bindingFinished = {false}; - final IBinder[] callbackBinder = new IBinder[] {null}; + + final int pid = Binder.getCallingPid(); + final int uid = Binder.getCallingUid(); ServiceConnection con = new ServiceConnection() { @Override public void onServiceConnected(ComponentName name, IBinder service) { - callbackBinder[0] = service; - bindingFinished[0] = true; - synchronized (callbackBinder) { - callbackBinder.notifyAll(); - } + addClientWithCallbackService(IPluginCallback.Stub.asInterface(service), pid, uid, this); } @Override public void onServiceDisconnected(ComponentName name) { - unbindService(this); + try { + PluginService.this.unbindService(this); + } catch (IllegalArgumentException ignored) {} } @Override public void onBindingDied(ComponentName name) { - unbindService(this); + try { + PluginService.this.unbindService(this); + } catch (IllegalArgumentException ignored) {} } @Override public void onNullBinding(ComponentName name) { - bindingFinished[0] = true; - synchronized (callbackBinder) { - callbackBinder.notifyAll(); - } - unbindService(this); + Logger.logDebug("Null binding for callback service"); + try { + PluginService.this.unbindService(this); + } catch (IllegalArgumentException ignored) {} } }; - PluginService.this.bindService(callbackStartIntent, con, Context.BIND_ALLOW_OOM_MANAGEMENT); - - while (! bindingFinished[0]) { - try { - synchronized (callbackBinder) { - callbackBinder.wait(); - } + try { + boolean ret; + switch (priority) { + case PRIORITY_MIN: + ret = PluginService.this.bindService(callbackStartIntent, con, Context.BIND_WAIVE_PRIORITY | Context.BIND_AUTO_CREATE); + break; + case PRIORITY_IMPORTANT: + ret = PluginService.this.bindService(callbackStartIntent, con, Context.BIND_IMPORTANT | Context.BIND_AUTO_CREATE); + break; + case PRIORITY_MAX: + ret = PluginService.this.bindService(callbackStartIntent, con, Context.BIND_ABOVE_CLIENT | Context.BIND_AUTO_CREATE); + break; + case PRIORITY_NORMAL: + default: + ret = PluginService.this.bindService(callbackStartIntent, con, Context.BIND_AUTO_CREATE); } - catch (InterruptedException ignored) {} - } - - if (callbackBinder[0] == null) { - throw new IllegalArgumentException("Could not bind callback service: "+componentNameString); + if (! ret) { + throw new IllegalStateException("Cannot start service"); + } + } catch (SecurityException e) { + throw new IllegalArgumentException("Service not found or requires permissions"); } - - addClient(IPluginCallback.Stub.asInterface(callbackBinder[0])); } diff --git a/plugin-aidl/src/main/aidl/com/termux/plugin_aidl/IPluginCallback.aidl b/plugin-aidl/src/main/aidl/com/termux/plugin_aidl/IPluginCallback.aidl index 662ecc2536..5fcb4bd249 100644 --- a/plugin-aidl/src/main/aidl/com/termux/plugin_aidl/IPluginCallback.aidl +++ b/plugin-aidl/src/main/aidl/com/termux/plugin_aidl/IPluginCallback.aidl @@ -27,6 +27,7 @@ interface IPluginCallback { /** * This gets called when a connection is made on a socket created with {@link com.termux.plugin_aidl.IPluginService#listenOnSocketFile}. + *
Added in version 1. * * @param sockname The name of socket file the connection was made on (the relative path to the plugin directory). * @param connection The connection file descriptor. @@ -36,6 +37,7 @@ interface IPluginCallback { /** * Gets called when a started Task exits. + *
Added in version 1. * * @param pid The pid of the task that exited. * @param code The exit code of the Task. Negative values indicate the Task was killed by a signal. The signal number is then {@code -code}. diff --git a/plugin-aidl/src/main/aidl/com/termux/plugin_aidl/IPluginService.aidl b/plugin-aidl/src/main/aidl/com/termux/plugin_aidl/IPluginService.aidl index ebd6982544..9282700e10 100644 --- a/plugin-aidl/src/main/aidl/com/termux/plugin_aidl/IPluginService.aidl +++ b/plugin-aidl/src/main/aidl/com/termux/plugin_aidl/IPluginService.aidl @@ -24,6 +24,30 @@ import com.termux.plugin_aidl.Task; */ interface IPluginService { + /** + * Binds the service with @{link android.content.Context#BIND_WAIVE_PRIORITY}, meaning the plugins needs other components for the system not to kill it. + */ + const int PRIORITY_MIN = 0; + + /** + * Uses no flags to bind the service. + */ + const int PRIORITY_NORMAL = 1; + + /** + * Binds the service with @{link android.content.Context#BIND_IMPORTANT}, which should give the plugin the same priority as Termux in terms of OOM killing. + */ + const int PRIORITY_IMPORTANT = 2; + + /** + * Binds the service with @{link android.content.Context#BIND_ABOVE_CLIENT}, which would make Termux get killed first on OOM. + * This should be used if you really don't want the plugin killed while a program uses it, and your plugin has a small memory footprint. + * Another solution for that is to make your own foreground service, but the undismissable notification could be annoying for users, so this is given as an option. + */ + const int PRIORITY_MAX = 3; + + + /** * This or {@link com.termux.plugin_aidl.IPluginService#setCallbackService} has to be called before any other method. @@ -33,12 +57,15 @@ interface IPluginService { /** * This or {@link com.termux.plugin_aidl.IPluginService#setCallbackBinder} has to be called before any other method. - * It initialized the internal representation of the connected plugin and sets the callback binder to the binder returned by the bound service. + * It initializes the internal representation of the connected plugin and sets the callback binder to the binder returned by the bound service. + * You can call other methods shortly after getCallbackVersion has been called in the supplied service. + * If it's too early the methods will throw an {@link IllegalStateException} which can be caught. * * @param componentName This is the relative part of a component name string. * The package name is always taken from the calling binder package for security reasons. + * @param priority This is the priority Termux should bind the service with. See the PRIORITY_* constants for more info. */ - void setCallbackService(String componentName) = 2; + void setCallbackService(String componentName, int priority) = 2; /** From ecf4f4a1b7ef03d52dd14ef83ac18b36299392f3 Mon Sep 17 00:00:00 2001 From: tareksander <57038324+tareksander@users.noreply.github.com> Date: Fri, 5 Aug 2022 16:21:03 +0200 Subject: [PATCH 09/10] Fixed: Fixed build error from using the old env variable system. --- .../main/java/com/termux/app/plugin/PluginService.java | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/app/src/main/java/com/termux/app/plugin/PluginService.java b/app/src/main/java/com/termux/app/plugin/PluginService.java index 1dc75b26f4..ac7c24f3ce 100644 --- a/app/src/main/java/com/termux/app/plugin/PluginService.java +++ b/app/src/main/java/com/termux/app/plugin/PluginService.java @@ -34,7 +34,7 @@ import com.termux.shared.shell.command.ExecutionCommand; import com.termux.shared.shell.command.runner.nativerunner.NativeShell; import com.termux.shared.termux.plugins.TermuxPluginUtils; -import com.termux.shared.termux.shell.TermuxShellEnvironmentClient; +import com.termux.shared.termux.shell.command.environment.TermuxShellEnvironment; import java.io.File; import java.io.FileNotFoundException; @@ -330,10 +330,10 @@ public Task runTask(String commandPath, String[] arguments, ParcelFileDescriptor List env = (environment != null) ? Arrays.asList(environment) : new ArrayList<>(); - env.addAll(Arrays.asList(new TermuxShellEnvironmentClient(). - buildEnvironment(PluginService.this, - false, - workdir))); + Map termuxEnv = new TermuxShellEnvironment().getEnvironment(PluginService.this, false); + for (Map.Entry evar : termuxEnv.entrySet()) { + env.add(evar.getKey()+"="+evar.getValue()); + } String[] realEnvironment = env.toArray(new String[0]); From 7ea61ee3dc1858f186d0581b423d1d58d6da5ef9 Mon Sep 17 00:00:00 2001 From: tareksander <57038324+tareksander@users.noreply.github.com> Date: Tue, 26 Mar 2024 13:35:18 +0100 Subject: [PATCH 10/10] Fixed|Changed: Updated Gradle version and AGP, the old one didn't run on my machine. --- build.gradle | 2 +- gradle/wrapper/gradle-wrapper.properties | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/build.gradle b/build.gradle index 80ddd4bde1..fd00105a4f 100644 --- a/build.gradle +++ b/build.gradle @@ -4,7 +4,7 @@ buildscript { google() } dependencies { - classpath "com.android.tools.build:gradle:4.2.2" + classpath 'com.android.tools.build:gradle:7.4.2' } } diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index a0f7639f7d..2ec77e51a9 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,5 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-7.2-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-7.5-all.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists