1+ package com .foxdebug .acode .rk .exec .terminal ;
2+
3+ import org .java_websocket .WebSocket ;
4+ import org .java_websocket .handshake .ClientHandshake ;
5+ import org .java_websocket .server .WebSocketServer ;
6+
7+ import java .io .InputStream ;
8+ import java .io .OutputStream ;
9+ import java .net .InetSocketAddress ;
10+ import java .nio .ByteBuffer ;
11+ import java .nio .charset .StandardCharsets ;
12+ import java .util .concurrent .CountDownLatch ;
13+ import java .util .concurrent .atomic .AtomicReference ;
14+
15+ class ProcessServer extends WebSocketServer {
16+
17+ private final String [] cmd ;
18+ private final CountDownLatch readyLatch = new CountDownLatch (1 );
19+ private final AtomicReference <Exception > startError = new AtomicReference <>();
20+
21+ private static final class ConnState {
22+ final Process process ;
23+ final OutputStream stdin ;
24+
25+ ConnState (Process process , OutputStream stdin ) {
26+ this .process = process ;
27+ this .stdin = stdin ;
28+ }
29+ }
30+
31+ ProcessServer (int port , String [] cmd ) {
32+ super (new InetSocketAddress ("127.0.0.1" , port ));
33+ this .cmd = cmd ;
34+ }
35+
36+ void startAndAwait () throws Exception {
37+ start ();
38+ readyLatch .await ();
39+ Exception err = startError .get ();
40+ if (err != null ) throw err ;
41+ }
42+
43+ @ Override
44+ public void onStart () {
45+ readyLatch .countDown ();
46+ }
47+
48+ @ Override
49+ public void onError (WebSocket conn , Exception ex ) {
50+ if (conn == null ) {
51+ // Bind/startup failure — unblock startAndAwait() so it can throw.
52+ startError .set (ex );
53+ readyLatch .countDown ();
54+ }
55+ // Per-connection errors: do nothing. onClose fires immediately after
56+ // for the same connection, which is the single place cleanup happens.
57+ }
58+
59+ @ Override
60+ public void onOpen (WebSocket conn , ClientHandshake handshake ) {
61+ try {
62+ Process process = new ProcessBuilder (cmd ).redirectErrorStream (true ).start ();
63+ InputStream stdout = process .getInputStream ();
64+ OutputStream stdin = process .getOutputStream ();
65+
66+ conn .setAttachment (new ConnState (process , stdin ));
67+
68+ new Thread (() -> {
69+ try {
70+ byte [] buf = new byte [8192 ];
71+ int len ;
72+ while ((len = stdout .read (buf )) != -1 ) {
73+ conn .send (ByteBuffer .wrap (buf , 0 , len ));
74+ }
75+ } catch (Exception ignored ) {}
76+ conn .close (1000 , "process exited" );
77+ }).start ();
78+
79+ } catch (Exception e ) {
80+ conn .close (1011 , "Failed to start process: " + e .getMessage ());
81+ }
82+ }
83+
84+ @ Override
85+ public void onMessage (WebSocket conn , ByteBuffer msg ) {
86+ try {
87+ ConnState state = conn .getAttachment ();
88+ state .stdin .write (msg .array (), msg .position (), msg .remaining ());
89+ state .stdin .flush ();
90+ } catch (Exception ignored ) {}
91+ }
92+
93+ @ Override
94+ public void onMessage (WebSocket conn , String message ) {
95+ try {
96+ ConnState state = conn .getAttachment ();
97+ state .stdin .write (message .getBytes (StandardCharsets .UTF_8 ));
98+ state .stdin .flush ();
99+ } catch (Exception ignored ) {}
100+ }
101+
102+ @ Override
103+ public void onClose (WebSocket conn , int code , String reason , boolean remote ) {
104+ try {
105+ ConnState state = conn .getAttachment ();
106+ if (state != null ) state .process .destroy ();
107+ } catch (Exception ignored ) {}
108+
109+ // stop() calls w.join() on every worker thread. If called directly from
110+ // onClose (which runs on a WebSocketWorker thread), it deadlocks waiting
111+ // for itself to finish. A separate thread sidesteps that entirely.
112+ new Thread (() -> {
113+ try {
114+ stop ();
115+ } catch (Exception ignored ) {}
116+ }).start ();
117+ }
118+ }
0 commit comments