Description
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 executes am broadcast
with passing names of these abstract sockets as an arguments (extras).
It is problematic for two reasons.
- We can not pass abstract sockets bypassing android application sandbox because of selinux restrictions.
- We can not pin abstract or any other type of socket to intent because of
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 by termux-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 if socketpair
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).
// Java
android.content.pm.PackageInfo targetInfo = (android.os.Build.VERSION.SDK_INT <= 32) ?
android.app.ActivityThread.getPackageManager().getPackageInfo(BuildConfig.APPLICATION_ID, android.content.pm.PackageManager.GET_SIGNATURES, 0) :
android.app.ActivityThread.getPackageManager().getPackageInfo(BuildConfig.APPLICATION_ID, (long) android.content.pm.PackageManager.GET_SIGNATURES, 0);
assert targetInfo.signatures.length == 1 && BuildConfig.SIGNATURE == targetInfo.signatures[0].hashCode() : packageSignatureMismatchErrorText;
// build.gradle
android.defaultConfig.buildConfigField "int", "SIGNATURE", String.valueOf(Arrays.hashCode(keyStore.getCertificate(signingConfig.keyAlias).getEncoded()))
Second way
You can add an ability to pass file descriptors through Binder to regular TermuxAm
and termux-am-socket
in similar way. In this case we will be able to go on using am 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 both TermuxAm
and termux-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 send stdin
and stdout
contents in both ways directly. Instead of that you can implement some simple command system to send buffers got from stdin
/stdout
, create/modify/delete files, pass file descriptors (i.e. for termux-usb
) and do other stuff.
To avoid creating complicated IPC system you can go libxcb way and pass smth like union 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.
@tareksander probably all of that is applicable to termux-gui
too since it is based on termux-api
and it is incompatible with F-Droid builds of Termux and its plugins.
Ping @agnostic-apollo @Grimler91 @tareksander.