diff --git a/app/build.gradle b/app/build.gradle index 85343569..676d7a17 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -25,6 +25,10 @@ android { implementation project(":terminal-view") implementation project(":termux-shared") + + + implementation 'com.github.tareksander:termux-am-library:main-SNAPSHOT' + } defaultConfig { diff --git a/app/src/main/java/com/termux/app/TermuxApplication.java b/app/src/main/java/com/termux/app/TermuxApplication.java index 3d438f71..1d0eed04 100644 --- a/app/src/main/java/com/termux/app/TermuxApplication.java +++ b/app/src/main/java/com/termux/app/TermuxApplication.java @@ -3,13 +3,16 @@ package com.termux.app; import android.app.Application; import android.content.Context; +import com.termux.am.Am; import com.termux.shared.termux.TermuxConstants; import com.termux.shared.termux.crash.TermuxCrashUtils; import com.termux.shared.termux.settings.preferences.TermuxAppSharedPreferences; import com.termux.shared.logger.Logger; import com.termux.shared.termux.settings.properties.TermuxAppSharedProperties; import com.termux.shared.termux.theme.TermuxThemeUtils; +import com.termux.shared.shell.LocalSocketListener; +import java.io.IOException; public class TermuxApplication extends Application { @@ -31,6 +34,20 @@ public class TermuxApplication extends Application { // Set NightMode.APP_NIGHT_MODE TermuxThemeUtils.setAppNightMode(properties.getNightMode()); + + try { + new LocalSocketListener(this, (args, out, err) -> { + try { + new Am(out, err, this).run(args); + return 0; + } catch (Exception e) { + return 1; + } + }, TermuxConstants.TERMUX_PACKAGE_NAME+"://call-am", 1000); + } + catch (IOException e) { + Logger.logDebug("TermuxApplication", "am socket already in use"); + } } public static void setLogConfig(Context context) { diff --git a/termux-shared/src/main/java/com/termux/shared/shell/LocalSocketListener.java b/termux-shared/src/main/java/com/termux/shared/shell/LocalSocketListener.java new file mode 100644 index 00000000..3e58d328 --- /dev/null +++ b/termux-shared/src/main/java/com/termux/shared/shell/LocalSocketListener.java @@ -0,0 +1,161 @@ +package com.termux.shared.shell; + +import android.app.Application; +import android.net.LocalServerSocket; +import android.net.LocalSocket; + +import androidx.annotation.NonNull; + +import com.termux.shared.logger.Logger; + +import java.io.BufferedWriter; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStreamReader; +import java.io.OutputStream; +import java.io.OutputStreamWriter; +import java.io.PrintStream; + +public class LocalSocketListener +{ + public interface LocalSocketHandler { + int handle(String[] args, PrintStream out, PrintStream err); + } + + + private static final String LOG_TAG = "LocalSocketListener"; + + private final Thread thread; + private final LocalServerSocket server; + private final int timeoutMillis; + + public LocalSocketListener(@NonNull Application a, @NonNull LocalSocketHandler h, String address, int timeoutMillis) throws IOException { + this.timeoutMillis = timeoutMillis; + server = new LocalServerSocket(address); + thread = new Thread(new LocalSocketListenerRunnable(a, h)); + thread.setUncaughtExceptionHandler((t, e) -> Logger.logStackTraceWithMessage(LOG_TAG, "Uncaught exception in LocalSocketListenerRunnable", e)); + thread.start(); + } + + @SuppressWarnings("unused") + public void stop() { + try { + thread.interrupt(); + server.close(); + } catch (Exception ignored) {} + } + + private class LocalSocketListenerRunnable implements Runnable { + private final Application a; + private final TimeoutWatcher timeoutWatcher; + private final Thread timeoutWatcherThread; + private final LocalSocketHandler h; + public LocalSocketListenerRunnable(@NonNull Application a, @NonNull LocalSocketHandler h) { + this.a = a; + this.h = h; + timeoutWatcher = new TimeoutWatcher(); + timeoutWatcherThread = new Thread(timeoutWatcher); + timeoutWatcherThread.start(); + } + + // the socket timeout for LocalSocket doesn't seem to work, so close the socket if the timeout is over, so the processing Thread doesn't get blocked. + private class TimeoutWatcher implements Runnable { + private final Object lock = new Object(); + private LocalSocket current = null; + @Override + public void run() { + while (! Thread.currentThread().isInterrupted()) { + LocalSocket watch = current; + synchronized (lock) { + while (watch == null) { + try { + lock.wait(); + } + catch (InterruptedException ignored) { + Thread.currentThread().interrupt(); + } + watch = current; + } + } + try { + //noinspection BusyWait + Thread.sleep(timeoutMillis); + } + catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + try { + watch.shutdownInput(); + } catch (Exception ignored) {} + try { + watch.shutdownOutput(); + } catch (Exception ignored) {} + try { + watch.close(); + } catch (Exception ignored) {} + } + } + } + + @Override + public void run() { + try { + while (! Thread.currentThread().isInterrupted()) { + try (LocalSocket s = server.accept(); + OutputStream sockout = s.getOutputStream(); + InputStreamReader r = new InputStreamReader(s.getInputStream())) { + timeoutWatcher.current = s; + synchronized (timeoutWatcher.lock) { + timeoutWatcher.lock.notifyAll(); + } + // ensure only Termux programs can connect + if (s.getPeerCredentials().getUid() != a.getApplicationInfo().uid) { + Logger.logDebug(LOG_TAG, "A program with another UID tried to connect"); + continue; + } + StringBuilder b = new StringBuilder(); + int c; + while ((c = r.read()) > 0) { + b.append((char) c); + } + String outString; + String errString; + int ret; + try (ByteArrayOutputStream out = new ByteArrayOutputStream(); + PrintStream outp = new PrintStream(out); + ByteArrayOutputStream err = new ByteArrayOutputStream(); + PrintStream errp = new PrintStream(err)) { + + ret = h.handle(ArgumentTokenizer.tokenize(b.toString()).toArray(new String[0]), outp, errp); + + outp.flush(); + outString = out.toString("UTF-8"); + + errp.flush(); + errString = err.toString("UTF-8"); + } + try (BufferedWriter w = new BufferedWriter(new OutputStreamWriter(sockout))) { + w.write(Integer.toString(ret)); + w.write('\0'); + w.write(outString); + w.write('\0'); + w.write(errString); + w.flush(); + } + } catch (Exception e) { + Logger.logStackTraceWithMessage(LOG_TAG, "Exception while handling connection", e); + } + } + } + finally { + try { + server.close(); + } catch (Exception ignored) {} + if (timeoutWatcherThread.isAlive()) { + timeoutWatcherThread.interrupt(); + } + } + Logger.logDebug(LOG_TAG, "LocalSocketListenerRunnable returned"); + } + } +}