第三方App與Termux命令建立IO通道

RainbowC0發表於2024-06-27

目錄
  • 前言
  • 一、Android 程序間通訊(IPC)
  • 二、Netcat 網路瑞士軍刀
  • 三、第三方 App 與 Termux 建立 TCP/Socket 通訊
  • 四、應用:呼叫 LSP 語言伺服器
  • 參見

前言

繼上一篇 Android 呼叫 Termux 執行命令,執行命令的問題基本解決,但是 bashawkclangd 這類命令可以從標準輸入讀取資訊並維持執行,Termux 第三方呼叫缺乏有效支援。而 RunCommandService 可以允許命令後臺執行,然後我們以某種方式取得該後臺程式的標準輸入/輸出,便可以實現前後端的持續通訊。


一、Android 程序間通訊(IPC)

程序間通訊(In-Process Communication, IPC)主要實現多程序間的資料通訊問題。比如一個 UI 程式後臺呼叫一個 CLI 程式執行某功能,每個程式會啟動一個程序,UI 程序給 CLI 程序傳送輸入資料,CLI 程序接收後返回處理結果給 UI 程序。

Android 實現跨程序通訊有多種方式,其各有優缺點:

檔案:直接在裝置上建立檔案實現資料通訊。原理簡單,操作方便,但是效率低。

Binder:結構上 Binder 是一個虛擬的裝置驅動(/dev/binder),連線 Service 程序、Client 程序和 Service Manager 程序。其資料只在核心空間與使用者空間複製一次,效率較高,但是限制 1M 資料。另外,基於 Binder 的方法有 AIDL、ContentProvider、Messenger 等。

SharedMemory:共享記憶體在 Android SDK 27 引入,允許開闢一塊共享記憶體空間用於程序間的資料互動。SharedMemory 配合 AIDL/Binder 使用,可以破除 1M 的限制,傳輸大檔案。但是注意直接對記憶體進行操作,使用完畢需要手動銷燬。

Unix Domain Socket:又叫 Local Domain Socket,本地套位元組是 Linux 核心提供的功能,資料經過核心,實現本地程序間通訊。其效率高,但是 Android 9+ 限制使用者 App 間使用 UDS 通訊。

Socket:套位元組本質上是網路通訊,採用 TCP 或 UDP 協議,主要用於網路通訊。其中 TCP 協議較複雜,用於建立穩定的通訊;UDP 則速度快而不安全。

受安卓不同應用之間的許可權限制,支援程序間位元組資料 IO 通訊的方案較少,本次採用 Socket 實現。

二、Netcat 網路瑞士軍刀

Netcat 是一個小巧強大的網路工具,用於網路監聽測試等。Netcat 可建立網路通訊,支援 TCP/UDP/Unix 協議。在 Termux 端使用 Netcat 建立套位元組通訊,並將 stdin/stdout 重定向到一個子程序,如此實現通訊。

透過以下方式建立 TCP 通訊。伺服器端:

nc -l -s 127.0.0.1 -p 1234

客戶端:

nc 127.0.0.1 1234

此時在客戶端輸入的內容可傳至伺服器端,而伺服器端輸入的內容可傳回客戶端,二者間實現通訊。

另外,使用 Netcat 可以方便反彈 shell 程式。下面是伺服器端命令:

nc -l -s 127.0.0.1 -p 1234 bash

用客戶端登入到 127.0.0.1:1234,建立連線後,服務端會啟動 bash 程式,並接收來自客戶端的標準輸入,將標準輸出傳送給客戶端。

三、第三方 App 與 Termux 建立 TCP/Socket 通訊

透過 RunCommandService 呼叫 Termux 執行 nc 命令反彈某個程式,然後透過 java.net.Socket 建立 Socket 連線,取得 Socket 的 IO 流,即可實現程序間通訊。

呼叫 Termux。注意,Termux 可使用兩個版本的 Netcat:安卓自帶的 /system/bin/nc 和 Termux 倉庫的 netcat-openbsd。前者隨 ToyBox 在 Android Marshmallow 被引入,支援反彈 shell,而後者不支援;後者支援抽象名稱空間 UDS。所以我們使用 /system/bin/nc

intent.setClassName("com.termux", "com.termux.app.RunCommandService");
intent.setAction("com.termux.RUN_COMMAND");
intent.putExtra("com.termux.RUN_COMMAND_PATH", "/system/bin/nc");
intent.putExtra("com.termux.RUN_COMMAND_ARGUMENTS", new String[]{"-l", "-s", "127.0.0.1", "-p", "1234", "bash"});
intent.putExtra("com.termux.RUN_COMMAND_WORKDIR", "/data/data/com.termux/files/home");
intent.putExtra("com.termux.RUN_COMMAND_BACKGROUND", true);
intent.putExtra("com.termux.RUN_COMMAND_SESSION_ACTION", "0");
startService(intent);

建立 Socket 連線:

Socket mSocket;
InputStream mInput;
OutputStream mOutput;
public void connect() {
  mSocket = new Socket();
  new Thread(){
    public run() {
      try {
        sk.connect(new InetSocketAddress("127.0.0.1", 1234));
        mInput = sk.getInputStream();
        // 寫入命令/發出資料
        mOutput = sk.getOutputStream();
        mOutput.write("ls\n");
        mOutput.flush();
        // 讀取結果
        Thread.sleep(200L);
        int l = mInput.avaliable();
        byte[] bs = new byte[l];
        mInput.read(bs);
        System.out.println(new String(bs));
      } catch (Exception e) {
        e.printStackTrace();
      }
    }
  }.start();
}

四、應用:呼叫 LSP 語言伺服器

語言伺服器協議(Language Server Protocol, LSP)是微軟推出的一個基於 JSONRPC 的資料協議,為語言伺服器與客戶端提供一種標準通訊協議,使不同編輯器可以將 IDE 相關功能獨立出來,由語言伺服器提供。

Termux 的軟體倉庫里正好有 clangdccls 兩個 C/C++ 的 LSP 伺服器端。筆者測試 ccls 時遇到 BUG,故選用 clangd 測試。

安裝 clangd

apt install clangd

nc 反彈 clangd

/system/bin/nc -l -s 127.0.0.1 -p 48455 clangd

Android 客戶端建立 Socket IO 通訊:

Socket sk = new Socket(new InetAddress("127.0.0.1", 48455));

注意,安卓中 Socket 的 IO 流不允許在 UI 主執行緒進行操作,需要另起執行緒,以免阻塞主執行緒引起卡頓。

讀取執行緒:

new Thread() {
  public void run() {
    InputStream mIn = sk.getInputStream();
    final int L = 1024;
    byte[] buf = new byte[L];
    while (mIn.read(buf, 0, 16)!=-1) {
      if (new String(buf, 0, 16).equals("Content-Length: ")) {
        // read int c
        // skip \r\n\r\n
        // read c bytes
      }
    }
  }
}.start();

寫入執行緒:

Thread td = new Thread() {
  public void run() {
    try {
      OutputStream mOut = sk.getOutputStream();
      byte[] s="{\"jsonrpc\":\"2.0\",\"id\":0,\"method\":\"initialize\",\"params\":{}}".getBytes(StandardCharsets.UTF_8);
      mOut.write(("Content-Length: "+s.length+"\r\n\r\n").getBytes());
      mOut.write(s);
      mOut.flush();
    } catch (IOException ioe) {
      ioe.printStackTrace();
    }
  }
};
td.start();
td.join(); // 阻塞寫入執行緒,避免同時寫入

筆者的開源專案:TermuC - github.com/RainbowC0


參見

  1. android共享記憶體(ShareMemory)的實現 - 簡書
  2. 徹底弄懂netcat命令的使用 - CSDN
  3. What is toybox? - Landley
  4. 語言伺服器協議概述 - Microsoft Learn

相關文章