feat: spawn stream support for Executor#1972
feat: spawn stream support for Executor#1972RohitKushvaha01 wants to merge 2 commits intoAcode-Foundation:mainfrom
Conversation
Greptile SummaryThis PR introduces a The concept is sound and the overall plumbing works end-to-end, but there are several issues that should be addressed before merging:
Confidence Score: 1/5
Important Files Changed
Sequence DiagramsequenceDiagram
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
Reviews (1): Last reviewed commit: "feat: spawn stream support for Executor" | Re-trigger Greptile |
| 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){} | ||
| } |
There was a problem hiding this comment.
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
}| private boolean isPortFree(int port) { | ||
| try (ServerSocket socket = new ServerSocket(port)) { | ||
| socket.setReuseAddress(true); | ||
| return true; | ||
| } catch (IOException e) { | ||
| return false; | ||
| } | ||
| } |
There was a problem hiding this comment.
setReuseAddress has no effect after binding; TOCTOU race condition
Two separate issues here:
-
new ServerSocket(port)already binds the socket. Callingsocket.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. -
There is a time-of-check/time-of-use (TOCTOU) race:
isPortFreeopens and immediately closes aServerSocket, thenProcessServertries to bind to the same port. Between those two steps, another process could claim that port, causingserver.start()to throw silently (the exception is swallowed inspawn).
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.
| 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; |
There was a problem hiding this comment.
spawn requires service binding it never uses; server.start() failures are silent
Two issues in this block:
-
The
spawncase falls through theensureServiceBoundcheck at line 260, even though it never communicates withserviceMessenger. If the terminal service fails to start (e.g. notification permission denied),spawnwill also fail with a misleading error. -
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 ofexecute(), bypassingcallbackContext.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;| 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(); |
There was a problem hiding this comment.
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).
| 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]); | ||
|
|
||
| } |
There was a problem hiding this comment.
spawnStream has no error handling and no JSDoc
Two issues:
- The
nullerror handler means any Cordova error (service binding failure, port allocation failure, etc.) is silently dropped — thecallbackis never called and there is no rejection mechanism. Consider accepting anonErrorparameter or returning a Promise. - All other methods in this class are documented with JSDoc.
spawnStreamhas none, making its signature (cmdtype,callbacksignature, return value) unclear to callers.
Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>
No description provided.