Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow connection via a UNIX socket as a faster alternative #471

Merged
merged 2 commits into from
Dec 23, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,11 @@
<service
android:name=".SchedulerJobService"
android:permission="android.permission.BIND_JOB_SERVICE" />
<service
android:name=".KeepAliveService"
android:description="@string/keep_alive_service"
android:enabled="true"
android:exported="false" />
</application>

</manifest>
223 changes: 223 additions & 0 deletions app/src/main/java/com/termux/api/App.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,223 @@
package com.termux.api;

import android.app.Application;
import android.content.Intent;
import android.net.LocalServerSocket;
import android.net.LocalSocket;

import com.termux.api.util.TermuxApiLogger;

import java.io.BufferedWriter;
import java.io.DataInputStream;
import java.io.OutputStreamWriter;
import java.nio.charset.StandardCharsets;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

public class App extends Application
{
public static final String LISTEN_ADDRESS = "com.termux.api://listen";
private static final Pattern EXTRA_STRING = Pattern.compile("(-e|--es|--esa) +([^ ]+) +\"(.*?)(?<!\\\\)\"", Pattern.DOTALL);
private static final Pattern EXTRA_BOOLEAN = Pattern.compile("--ez +([^ ]+) +([^ ]+)");
private static final Pattern EXTRA_INT = Pattern.compile("--ei +([^ ]+) +(-?[0-9]+)");
private static final Pattern EXTRA_FLOAT = Pattern.compile("--ef +([^ ]+) +(-?[0-9]+(?:\\.[0-9]+))");
private static final Pattern EXTRA_INT_LIST = Pattern.compile("--eia +([^ ]+) +(-?[0-9]+(?:,-?[0-9]+)*)");
private static final Pattern EXTRA_LONG_LIST = Pattern.compile("--ela +([^ ]+) +(-?[0-9]+(?:,-?[0-9]+)*)");
private static final Pattern EXTRA_UNSUPPORTED = Pattern.compile("--e[^izs ] +[^ ]+ +[^ ]+");
private static final Pattern ACTION = Pattern.compile("-a *([^ ]+)");

@Override
public void onCreate() {
super.onCreate();
new Thread(() -> {
try (LocalServerSocket listen = new LocalServerSocket(LISTEN_ADDRESS)) {
while (true) {
try (LocalSocket con = listen.accept();
DataInputStream in = new DataInputStream(con.getInputStream());
BufferedWriter out = new BufferedWriter(new OutputStreamWriter(con.getOutputStream()))) {
// only accept connections from Termux programs
if (con.getPeerCredentials().getUid() != getApplicationInfo().uid) {
continue;
}
try {
//System.out.println("connection");
int length = in.readUnsignedShort();
byte[] b = new byte[length];
in.readFully(b);
String cmdline = new String(b, StandardCharsets.UTF_8);

Intent intent = new Intent(getApplicationContext(), TermuxApiReceiver.class);
//System.out.println(cmdline.replaceAll("--es socket_input \".*?\"","").replaceAll("--es socket_output \".*?\"",""));
HashMap<String, String> stringExtras = new HashMap<>();
HashMap<String, String[]> stringArrayExtras = new HashMap<>();
HashMap<String, Boolean> booleanExtras = new HashMap<>();
HashMap<String, Integer> intExtras = new HashMap<>();
HashMap<String, Float> floatExtras = new HashMap<>();
HashMap<String, int[]> intArrayExtras = new HashMap<>();
HashMap<String, long[]> longArrayExtras = new HashMap<>();
boolean err = false;

// extract and remove the string extras first, so another argument embedded in a string isn't counted as an argument
Matcher m = EXTRA_STRING.matcher(cmdline);
while (m.find()) {
String option = m.group(1);
if ("-e".equals(option) || "--es".equals(option)) {
// unescape "
stringExtras.put(m.group(2), Objects.requireNonNull(m.group(3)).replaceAll("\\\\\"","\""));
} else {
// split the list
String[] list = Objects.requireNonNull(m.group(3)).split("(?<!\\\\),");
for (int i = 0; i<list.length; i++) {
/// unescape the ","
list[i] = list[i].replaceFirst("\\\\,", ",");
}
stringArrayExtras.put(m.group(2), list);
}

}
cmdline = m.replaceAll("");

m = EXTRA_BOOLEAN.matcher(cmdline);
while (m.find()) {
booleanExtras.put(m.group(1), Boolean.parseBoolean(m.group(2)));
}
cmdline = m.replaceAll("");

m = EXTRA_INT.matcher(cmdline);
while (m.find()) {
try {
intExtras.put(m.group(1), Integer.parseInt(Objects.requireNonNull(m.group(2))));
} catch (NumberFormatException e) {
String msg = "Invalid integer extra: " + m.group(0) + "\n";
TermuxApiLogger.info(msg);
out.write(msg);
err = true;
break;
}
}
cmdline = m.replaceAll("");

m = EXTRA_FLOAT.matcher(cmdline);
while (m.find()) {
try {
floatExtras.put(m.group(1), Float.parseFloat(Objects.requireNonNull(m.group(2))));
} catch (NumberFormatException e) {
String msg = "Invalid float extra: " + m.group(0) + "\n";
TermuxApiLogger.info(msg);
out.write(msg);
err = true;
break;
}
}
cmdline = m.replaceAll("");

m = EXTRA_INT_LIST.matcher(cmdline);
while (m.find()) {
try {
String[] parts = Objects.requireNonNull(m.group(2)).split(",");
int[] ints = new int[parts.length];
for (int i = 0; i<parts.length; i++) {
ints[i] = Integer.parseInt(parts[i]);
}
intArrayExtras.put(m.group(1), ints);
} catch (NumberFormatException e) {
String msg = "Invalid int array extra: " + m.group(0) + "\n";
TermuxApiLogger.info(msg);
out.write(msg);
err = true;
break;
}
}
cmdline = m.replaceAll("");

m = EXTRA_LONG_LIST.matcher(cmdline);
while (m.find()) {
try {
String[] parts = Objects.requireNonNull(m.group(2)).split(",");
long[] longs = new long[parts.length];
for (int i = 0; i<parts.length; i++) {
longs[i] = Long.parseLong(parts[i]);
}
longArrayExtras.put(m.group(1), longs);
} catch (NumberFormatException e) {
String msg = "Invalid long array extra: " + m.group(0) + "\n";
TermuxApiLogger.info(msg);
out.write(msg);
err = true;
break;
}
}
cmdline = m.replaceAll("");

m = ACTION.matcher(cmdline);
while (m.find()) {
intent.setAction(m.group(1));
}
cmdline = m.replaceAll("");

m = EXTRA_UNSUPPORTED.matcher(cmdline);
if (m.find()) {
String msg = "Unsupported argument type: " + m.group(0) + "\n";
TermuxApiLogger.info(msg);
out.write(msg);
err = true;
}
cmdline = m.replaceAll("");

// check if there are any non-whitespace characters left after parsing all the options
cmdline = cmdline.replaceAll("\\s", "");
if (! "".equals(cmdline)) {
String msg = "Unsupported options: " + cmdline + "\n";
TermuxApiLogger.info(msg);
out.write(msg);
err = true;
}

if (err) {
out.flush();
continue;
}

// set the intent extras
for (Map.Entry<String, String> e : stringExtras.entrySet()) {
intent.putExtra(e.getKey(), e.getValue());
}
for (Map.Entry<String, String[]> e : stringArrayExtras.entrySet()) {
intent.putExtra(e.getKey(), e.getValue());
}
for (Map.Entry<String, Integer> e : intExtras.entrySet()) {
intent.putExtra(e.getKey(), e.getValue());
}
for (Map.Entry<String, Boolean> e : booleanExtras.entrySet()) {
intent.putExtra(e.getKey(), e.getValue());
}
for (Map.Entry<String, Float> e : floatExtras.entrySet()) {
intent.putExtra(e.getKey(), e.getValue());
}
for (Map.Entry<String, int[]> e : intArrayExtras.entrySet()) {
intent.putExtra(e.getKey(), e.getValue());
}
for (Map.Entry<String, long[]> e : longArrayExtras.entrySet()) {
intent.putExtra(e.getKey(), e.getValue());
}
getApplicationContext().sendOrderedBroadcast(intent, null);
// send a null byte as a sign that the arguments have been successfully received, parsed and the broadcast receiver is called
con.getOutputStream().write(0);
con.getOutputStream().flush();
} catch (Exception e) {
TermuxApiLogger.error("Error parsing arguments", e);
out.write("Exception in the plugin\n");
out.flush();
}
}
}
} catch (Exception e) {
TermuxApiLogger.error("Error listening for connections", e);
}
}).start();
}

}
21 changes: 21 additions & 0 deletions app/src/main/java/com/termux/api/KeepAliveService.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package com.termux.api;

import android.app.Service;
import android.content.Intent;
import android.os.IBinder;

import androidx.annotation.Nullable;

public class KeepAliveService extends Service
{
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
return Service.START_STICKY;
}

@Nullable
@Override
public IBinder onBind(Intent intent) {
return null;
}
}
2 changes: 1 addition & 1 deletion app/src/main/res/values/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,6 @@
<string name="app_name">&TERMUX_API_APP_NAME;</string>
<string name="share_file_chooser_title">Share with</string>
<string name="grant_permission">Grant permission</string>

<string name="keep_alive_service">This service keeps Termux:API running in the background for faster startup of termux-* commands.</string>
<string name="permission_description">This app needs the following permission(s):\n</string>
</resources>