Open twaik opened 5 months ago
The first approach with a persistent connection sound almost like what I made for termux/termux-app#2921, just that my version is integrated into the Termux app and (as usual in contrast to your approaches lol) only uses the public Android APIs.
If you're running as root, can't you just drop back to the Termux user? I think apollo made a utility that drops the user and applies the correct SELinux stuff.
I don't know much about Android internals, is a process created with app_process
and using Android APIs registered with the system somehow and a prime target for killing because it has no active components, or does app_process
slip under the radar?
Regarding protocols, just sending unions is probably fine, but could leak stack data through uninitialized struct padding bytes. I don't think any plugin to date has to handle confidential/secure data or cryptographic keys or something like that, so it shouldn't be an issue. Another issue with that is that it only supports C or native languages that can have struct layouts like C. For something like Termux:API it could be nice to write the libraries directly in the actual language you want to use instead of relying on C bindings. For Termux:GUI I use protocol buffers, which is probably overkill for most things, but flatbuffers seems to be more lightweight. A custom Wayland protocol where you strip out the display stuff would also be an option: There's tools to generate Wayland bindings in many languages, and it comes with file descriptor passing out-of-the-box. Wayland is async, but all requests are handled in-order, so you can just block for a specific event to make it sync if you need to.
as usual in contrast to your approaches lol
I always had problems with making termux-app
more compatible with termux-x11
so I decided to make it as more independent and self-sufficient as I can do. So now termux-x11
works even without termux-app
or plugins installed in system (with chroot or non-termux proot environments).
If you're running as root, can't you just drop back to the Termux user?
Dropping to termux user is process-wide operation. It will work in the case if you want to launch some commands but it will may fail to pass you file descriptors (i.e. in the case if you use termux-usb, but I am not so sure).
I don't know much about Android internals, is a process created with
app_process
and using Android APIs registered with the system somehow and a prime target for killing because it has no active components, or doesapp_process
slip under the radar?
System treats it as a regular child process, like bash
or ls
. It can run as long as other commands.
could leak stack data through uninitialized struct padding bytes.
I always fill the whole allocated area of event I am planning to send with zeros, right after allocation. I am pretty sure that is not a problem.
For something like Termux:API it could be nice to write the libraries directly in the actual language you want to use instead of relying on C bindings.
I experimented with writing Java binding for Wayland which should run just fine in Android. https://github.com/twaik/wayland-java-experiment . Of course currently it is not complete, but potentially we can make it a base of Termux:API or Termux:GUI but it may be an overkill.
sharedUserId
is ever removed. But then again, there are plans to add certain API support to termux-app as well.targetSdkVersion
. Does yours?apps
directory, then both termux or root owned processes could just create sockets that api app should be able to access.action
and its result. External apps can use PendingIntent or possibly use contentprovider to create sockets if they want streamed data instead, it "should" work.I'll test tomorrow if it still works in new Android versions.
EDIT: Welp, I get a build error when trying to build termuy-app, and from some googling I'm apparently having a too up-to-date JDK...
Found a version that doesn't break the build, but works on my machine. I fixed everything now for API 34, still works, including FD passing over unix socket connection.
5/6. Ah, yes, that would work too.
Thanks for the fixes. Will your design even work with app_process
started processes to bind to the service, as there is no real context. How would it work for native processes like termux-api-package ones to connect to the termux-api-app? Haven't given thought into it myself.
You'd check if the filesystem socket accepts connections, if it doesn't you poke the plugin with am broadcast
to let it re-connect to Termux and set up its listener for the filesystem socket again. After that each connection on the filesystem socket gets passed to the plugin.
For the plugin to not get killed there are 2 options:
Because the service interaction is managed through the Termux app, the only thing you need is to call am
or termux-am
and access a filesystem socket.
hmmm, I see. Hopefully, synchronization or incoming queue delay won't be an issue.
A foreground service is already going to be used by plugin apps to manage API actions in future, and notification can be disabled on Android >= 8
.
Callback service could be looked into too.
Feature description
Lot of users have problems with sharedUserId. Termux:X11 a lot of time does not have this problem. Probably it will be good for Termux:API to implement something similar.
Connection establishing problem
Currently the most complicated problem is establishing connection.
termux-api
command creates two abstract sockets and executesam broadcast
with passing names of these abstract sockets as an arguments (extras). It is problematic for two reasons.am
restrictions.I see 2 ways to handle this.
First way
You can use some Java code (yeah, invoking app_process for this) to send broadcast with pinned binder, which will return file descriptors of sockets (probably created by
socketpair
, abstract sockets can not be shared this way) created bytermux-api
command when Termux:API's BroadcastReceiver invokes this Binder. Something like this:some code
``` /** @noinspection DataFlowIssue*/ @SuppressLint("DiscouragedPrivateApi") public static Context createContext() { Context context = null; PrintStream err = System.err; try { // `android.app.ActivityThread.systemMain().getSystemContext()` is not used to avoid `java.lang.RuntimeException: Unable to instantiate Application():android.content.res.Resources$NotFoundException: Resource ID #0x600a6` java.lang.reflect.Field f = Class.forName("sun.misc.Unsafe").getDeclaredField("theUnsafe"); f.setAccessible(true); Object unsafe = f.get(null); // Hiding harmless framework errors, like this: // java.io.FileNotFoundException: /data/system/theme_config/theme_compatibility.xml: open failed: ENOENT (No such file or directory) System.setErr(new PrintStream(new OutputStream() { public void write(int arg0) {} })); context = ((android.app.ActivityThread) Class. forName("sun.misc.Unsafe"). getMethod("allocateInstance", Class.class). invoke(unsafe, android.app.ActivityThread.class)) .getSystemContext(); } catch (Exception e) { Log.e("Context", "Failed to instantiate context:", e); context = null; } finally { System.setErr(err); } return context; } @SuppressLint({"WrongConstant", "PrivateApi"}) void sendBroadcast() { Bundle bundle = new Bundle(); bundle.putBinder("", new Binder() { @Override protected boolean onTransact(int code, Parcel data, Parcel reply, int flags) { if (code == Binder.FIRST_CALL_TRANSACTION) { /// ... Sending ParcelFileDescriptors obtained with `adoptFd` calls } return true; } }); Intent intent = new Intent(ACTION_START); intent.putExtra("", bundle); intent.setPackage("com.termux.api"); if (getuid() == 0 || getuid() == 2000) intent.setFlags(0x00400000 /* FLAG_RECEIVER_FROM_SHELL */); try { ctx.sendBroadcast(intent); } catch (Exception e) { if (e instanceof NullPointerException && ctx == null) Log.i("Broadcast", "Context is null, falling back to manual broadcasting"); else Log.e("Broadcast", "Falling back to manual broadcasting, failed to broadcast intent through Context:", e); String packageName; try { packageName = android.app.ActivityThread.getPackageManager().getPackagesForUid(getuid())[0]; } catch (RemoteException ex) { throw new RuntimeException(ex); } IActivityManager am; try { //noinspection JavaReflectionMemberAccess am = (IActivityManager) android.app.ActivityManager.class .getMethod("getService") .invoke(null); } catch (Exception e2) { try { am = (IActivityManager) Class.forName("android.app.ActivityManagerNative") .getMethod("getDefault") .invoke(null); } catch (Exception e3) { throw new RuntimeException(e3); } } assert am != null; IIntentSender sender = am.getIntentSender(1, packageName, null, null, 0, new Intent[] { intent }, null, PendingIntent.FLAG_CANCEL_CURRENT | PendingIntent.FLAG_ONE_SHOT, null, 0); try { //noinspection JavaReflectionMemberAccess IIntentSender.class .getMethod("send", int.class, Intent.class, String.class, IBinder.class, IIntentReceiver.class, String.class, Bundle.class) .invoke(sender, 0, intent, null, null, new IIntentReceiver.Stub() { @Override public void performReceive(Intent i, int r, String d, Bundle e, boolean o, boolean s, int a) {} }, null, null); } catch (Exception ex) { throw new RuntimeException(ex); } } } ```This code is got from Termux:X11, but maybe most of it may be replaced with similar code from
TermuxAm
.This code must be called on every
termux-api
invocation only in the case ifsocketpair
created sockets are used. In the case if Java code will create server socket (i.e. in$TMPDIR/termux-api-socket
) and stay in background (and will send sockets of newly-created connections to Termux:API through Broadcasts or already-established socketpair connection) you will need to launch this code only once.termux-api
can check if Unix socket in$TMPDIR/termux-api-socket
is connectable and if there is a lock file that should be created by Java code. OF COURSE this java code can be used to verify if we use Termux:API apk with right signature (hardcoded on compilation stage).Second way
You can add an ability to pass file descriptors through Binder to regular
TermuxAm
andtermux-am-socket
in similar way. In this case we will be able to go on usingam broadcast
command. But in this case you can not verify if right Termux:API apk is installed. Or probably you may integrate signature verification to bothTermuxAm
andtermux-am-socket
too :).Restrictions
That will be problematic for root users. For some reason SELinux blocks Unix sockets from being passed through Binder. Probably you will need to create two pipes (one for reading and one for writing) and pass one side of both pipes through Binder.
File access problem
In the case of disabling
sharedUserId
Termux:API will lose access to files in$PREFIX
and$HOME
. I am proposing to not sendstdin
andstdout
contents in both ways directly. Instead of that you can implement some simple command system to send buffers got fromstdin
/stdout
, create/modify/delete files, pass file descriptors (i.e. fortermux-usb
) and do other stuff. To avoid creating complicated IPC system you can go libxcb way and pass smth likeunion Event
which will contain all possible events. I did this in Termux:X11:some code
``` typedef enum { EVENT_SCREEN_SIZE, EVENT_TOUCH, EVENT_MOUSE, EVENT_KEY, EVENT_UNICODE, EVENT_CLIPBOARD_ENABLE, EVENT_CLIPBOARD_ANNOUNCE, EVENT_CLIPBOARD_REQUEST, EVENT_CLIPBOARD_SEND, } eventType; typedef union { uint8_t type; struct { uint8_t t; uint16_t width, height, framerate; } screenSize; struct { uint8_t t; uint16_t type, id, x, y; } touch; struct { uint8_t t; float x, y; uint8_t detail, down, relative; } mouse; struct { uint8_t t; uint16_t key; uint8_t state; } key; struct { uint8_t t; uint32_t code; } unicode; struct { uint8_t t; uint8_t enable; } clipboardEnable; struct { uint8_t t; uint32_t count; } clipboardSend; } lorieEvent; // and some code to handle that JNIEXPORT void JNICALL Java_com_termux_x11_LorieView_handleXEvents(JNIEnv *env, jobject thiz) { checkConnection(env); if (conn_fd != -1) { lorieEvent e = {0}; again: if (read(conn_fd, &e, sizeof(e)) == sizeof(e)) { switch(e.type) { case EVENT_CLIPBOARD_SEND: { char clipboard[e.clipboardSend.count + 1]; read(conn_fd, clipboard, sizeof(clipboard)); clipboard[e.clipboardSend.count] = 0; log(DEBUG, "Clipboard content (%zu symbols) is %s", strlen(clipboard), clipboard); jmethodID id = (*env)->GetMethodID(env, (*env)->GetObjectClass(env, thiz), "setClipboardText","(Ljava/lang/String;)V"); jobject bb = (*env)->NewDirectByteBuffer(env, clipboard, strlen(clipboard)); jobject charset = (*env)->CallStaticObjectMethod(env, Charset.self, Charset.forName, (*env)->NewStringUTF(env, "UTF-8")); jobject cb = (*env)->CallObjectMethod(env, charset, Charset.decode, bb); (*env)->DeleteLocalRef(env, bb); jstring str = (*env)->CallObjectMethod(env, cb, CharBuffer.toString); (*env)->CallVoidMethod(env, thiz, id, str); break; } case EVENT_CLIPBOARD_REQUEST: { (*env)->CallVoidMethod(env, thiz, (*env)->GetMethodID(env, (*env)->GetObjectClass(env, thiz), "requestClipboard", "()V")); break; } } } int n; if (ioctl(conn_fd, FIONREAD, &n) >= 0 && n > sizeof(e)) goto again; } } ``` This code can be combined with `poll` or `select` to not create threads for handling stdin/stdout/socket events separately.In the case if you need to send/receive some additional data you can simply use
write
/read
orsendv
/recv
(for the case when you pass file descriptors) after reading theEvent
struct/union.@tareksander probably all of that is applicable to
termux-gui
too since it is based ontermux-api
and it is incompatible with F-Droid builds of Termux and its plugins. Ping @agnostic-apollo @Grimler91 @tareksander.