Skip to content

feat: spawn stream support for Executor#1972

Draft
RohitKushvaha01 wants to merge 2 commits intoAcode-Foundation:mainfrom
RohitKushvaha01:main
Draft

feat: spawn stream support for Executor#1972
RohitKushvaha01 wants to merge 2 commits intoAcode-Foundation:mainfrom
RohitKushvaha01:main

Conversation

@RohitKushvaha01
Copy link
Member

No description provided.

@RohitKushvaha01 RohitKushvaha01 self-assigned this Mar 22, 2026
@RohitKushvaha01 RohitKushvaha01 marked this pull request as draft March 22, 2026 12:02
@greptile-apps
Copy link
Contributor

greptile-apps bot commented Mar 22, 2026

Greptile Summary

This PR introduces a spawn streaming mechanism for the Executor plugin, allowing a command to be launched on Android and have its stdio piped over a local WebSocket connection. A new ProcessServer class (backed by Java-WebSocket 1.6.0) is added, along with a spawnStream JavaScript wrapper and the corresponding Cordova spawn action in Executor.java.

The concept is sound and the overall plumbing works end-to-end, but there are several issues that should be addressed before merging:

  • Security (P0): ProcessServer binds to 0.0.0.0 instead of 127.0.0.1, exposing spawned processes to any device on the local network.
  • Process leak (P1): The process field is instance-level; a second WebSocket connection silently overwrites it, orphaning the first child process with no way to kill it.
  • Silent failures (P1): server.start() exceptions are not caught in the spawn case, so port conflicts or I/O errors cause the Cordova callback to never resolve or reject. Additionally, the JS spawnStream passes null as its error handler, swallowing all Cordova-level errors.
  • TOCTOU + setReuseAddress ordering (P1): The free-port check opens and closes a ServerSocket, but another process can claim that port before ProcessServer binds to it. Separately, setReuseAddress(true) is called after ServerSocket construction (i.e. after binding), where it has no effect. Using new ServerSocket(0) would eliminate both issues.
  • Stderr ignored (P2): process.getErrorStream() is never read, which can block the child process if it writes enough to stderr.

Confidence Score: 1/5

  • Not safe to merge — a P0 security vulnerability (WebSocket binding to all interfaces) and multiple P1 reliability bugs need to be resolved first.
  • The P0 issue (binding to 0.0.0.0) exposes arbitrary spawned commands to every device on the local network. The P1 issues (silent server.start() failures, process leak on multiple connections, TOCTOU race) mean the feature is unreliable and can leave orphaned processes. These are not cosmetic; they affect correctness, security, and stability of the primary user path.
  • ProcessServer.java requires the most attention (interface binding, multiple-connection guard, stderr); Executor.java needs the spawn error handling and port-finding logic revised.

Important Files Changed

Filename Overview
src/plugins/terminal/src/android/ProcessServer.java New WebSocket server that bridges a spawned process's stdio to a WebSocket client — binds to all interfaces (0.0.0.0) instead of loopback, has no guard against multiple connections overwriting the tracked process, and silently drops stderr output which can cause the child process to hang.
src/plugins/terminal/src/android/Executor.java Adds the spawn action and port-finding helpers; spawn is unnecessarily gated behind service binding it never uses, server.start() failures are uncaught, and the isPortFree / TOCTOU pattern introduces a race condition with a mis-ordered setReuseAddress call.
src/plugins/terminal/www/Executor.js Adds spawnStream JS wrapper; passes null as the error handler so Cordova errors are silently swallowed and there is no way for callers to detect failures. Also lacks JSDoc unlike every other method.
src/plugins/terminal/plugin.xml Registers the new ProcessServer.java source file and adds the org.java-websocket:Java-WebSocket:1.6.0 Gradle dependency — straightforward and correct.

Sequence Diagram

sequenceDiagram
    participant JS as Executor.js (spawnStream)
    participant Cordova as Cordova Bridge
    participant EJ as Executor.java (spawn)
    participant PS as ProcessServer (WebSocketServer)
    participant Proc as Child Process

    JS->>Cordova: exec("spawn", [cmd])
    Cordova->>EJ: execute("spawn", args)
    EJ->>EJ: findFreePort(nextPort++)
    EJ->>PS: new ProcessServer(port, cmd)
    EJ->>PS: server.start()
    EJ->>Cordova: callbackContext.success(port)
    Cordova->>JS: onSuccess(port)
    JS->>PS: new WebSocket("ws://127.0.0.1:{port}")
    PS->>JS: onopen
    PS->>Proc: ProcessBuilder(cmd).start()
    JS->>PS: ws.send(input) [binary/text]
    PS->>Proc: stdin.write(input)
    Proc->>PS: stdout data
    PS->>JS: ws.send(ByteBuffer)
    JS->>PS: ws.close()
    PS->>Proc: process.destroy()
    PS->>PS: stop() WebSocket server
Loading

Reviews (1): Last reviewed commit: "feat: spawn stream support for Executor" | Re-trigger Greptile

Comment on lines +23 to +50
public void onOpen(WebSocket conn, ClientHandshake handshake){

try{

process = new ProcessBuilder(cmd).start();

InputStream stdout = process.getInputStream();
OutputStream stdin = process.getOutputStream();

conn.setAttachment(stdin);

new Thread(() -> {

try{

byte[] buf = new byte[8192];
int len;

while((len = stdout.read(buf)) != -1){
conn.send(ByteBuffer.wrap(buf,0,len));
}

}catch(Exception ignored){}

}).start();

}catch(Exception ignored){}
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Multiple connections orphan previous processes

process is stored at the instance level. If a second WebSocket client connects before the first disconnects (or if the server is inadvertently reused), onOpen overwrites this.process with a new Process. The original process is no longer reachable via stopProcess(), so it will never be destroyed — it leaks indefinitely.

Consider either rejecting connections when a process is already running, or tracking processes per-connection rather than on the instance:

@Override
public void onOpen(WebSocket conn, ClientHandshake handshake) {
    // Reject additional connections if a process is already active
    if (process != null && process.isAlive()) {
        conn.close(1008, "Server already has an active process");
        return;
    }
    // ... rest of onOpen
}

Comment on lines +329 to +336
private boolean isPortFree(int port) {
try (ServerSocket socket = new ServerSocket(port)) {
socket.setReuseAddress(true);
return true;
} catch (IOException e) {
return false;
}
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 setReuseAddress has no effect after binding; TOCTOU race condition

Two separate issues here:

  1. new ServerSocket(port) already binds the socket. Calling socket.setReuseAddress(true) after construction has no effect on whether the port is actually reusable — it must be set before binding. This means the check may behave unexpectedly on some JVM implementations.

  2. There is a time-of-check/time-of-use (TOCTOU) race: isPortFree opens and immediately closes a ServerSocket, then ProcessServer tries to bind to the same port. Between those two steps, another process could claim that port, causing server.start() to throw silently (the exception is swallowed in spawn).

A safer pattern is to let the OS assign a free port with new ServerSocket(0) and read back the assigned port:

private int findFreePort() throws IOException {
    try (ServerSocket socket = new ServerSocket(0)) {
        socket.setReuseAddress(true);
        return socket.getLocalPort();
    }
}

This eliminates both the TOCTOU window and the setReuseAddress ordering issue.

Comment on lines +291 to +305
case "spawn":
JSONArray cmdArr = args.getJSONArray(0);

String[] cmd = new String[cmdArr.length()];
for(int i=0;i<cmdArr.length();i++){
cmd[i] = cmdArr.getString(i);
}

int port = findFreePort(nextPort++);

ProcessServer server = new ProcessServer(port, cmd);
server.start();

callbackContext.success(port);
return true;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 spawn requires service binding it never uses; server.start() failures are silent

Two issues in this block:

  1. The spawn case falls through the ensureServiceBound check at line 260, even though it never communicates with serviceMessenger. If the terminal service fails to start (e.g. notification permission denied), spawn will also fail with a misleading error.

  2. server.start() can throw a runtime exception (e.g. port already in use due to the TOCTOU race described elsewhere, or an I/O error). That exception propagates uncaught out of execute(), bypassing callbackContext.error(...) and crashing silently from the JS side — the promise never resolves or rejects.

At minimum wrap server.start() in a try-catch:

case "spawn":
    JSONArray cmdArr = args.getJSONArray(0);
    String[] cmd = new String[cmdArr.length()];
    for (int i = 0; i < cmdArr.length(); i++) {
        cmd[i] = cmdArr.getString(i);
    }
    try {
        int port = findFreePort();
        ProcessServer server = new ProcessServer(port, cmd);
        server.start();
        callbackContext.success(port);
    } catch (Exception e) {
        callbackContext.error("Failed to start process server: " + e.getMessage());
    }
    return true;

Comment on lines +29 to +47
InputStream stdout = process.getInputStream();
OutputStream stdin = process.getOutputStream();

conn.setAttachment(stdin);

new Thread(() -> {

try{

byte[] buf = new byte[8192];
int len;

while((len = stdout.read(buf)) != -1){
conn.send(ByteBuffer.wrap(buf,0,len));
}

}catch(Exception ignored){}

}).start();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 stderr is not captured

process.getErrorStream() is never read. If the spawned command writes to stderr, its output pipe will fill up and the process will block indefinitely, hanging the stdout reader thread. Consider forwarding stderr to the WebSocket as well (or merging it with stdout via redirectErrorStream(true) on ProcessBuilder).

Comment on lines +16 to +28
spawnStream(cmd, callback){

exec((port)=>{
const ws = new WebSocket(`ws://127.0.0.1:${port}`);
ws.binaryType = "arraybuffer";

ws.onopen = ()=>{
callback(ws);
};

}, null, "Executor", "spawn", [cmd]);

}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 spawnStream has no error handling and no JSDoc

Two issues:

  1. The null error handler means any Cordova error (service binding failure, port allocation failure, etc.) is silently dropped — the callback is never called and there is no rejection mechanism. Consider accepting an onError parameter or returning a Promise.
  2. All other methods in this class are documented with JSDoc. spawnStream has none, making its signature (cmd type, callback signature, return value) unclear to callers.

Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant