1313import farm .query .vgirpc .RpcServer ;
1414import farm .query .vgirpc .http .HttpServer ;
1515import farm .query .vgirpc .transport .StdioTransport ;
16+ import farm .query .vgirpc .transport .TcpSocketTransport ;
1617import farm .query .vgirpc .transport .UnixSocketTransport ;
1718
1819import java .io .IOException ;
@@ -707,6 +708,56 @@ public void runUnixSocket(Path socketPath, long idleTimeoutMs) throws IOExceptio
707708 UnixSocketTransport .serveForever (socketPath , buildServer (false ), idleTimeoutMs );
708709 }
709710
711+ /**
712+ * Block accepting TCP connections on {@code host}:{@code port}, dispatching
713+ * each client on a virtual thread, implementing the VGI launcher protocol:
714+ * the worker prints {@code TCP:<host>:<port>\n} to stdout once the listener
715+ * is bound (the actual port, so {@code port == 0} ephemeral binds are
716+ * discoverable), then serves until killed or the idle watchdog fires.
717+ *
718+ * <p>Raw TCP framing carries <strong>no authentication or encryption</strong>
719+ * — bind it to loopback / a trusted network only; use {@link #runHttp} for
720+ * untrusted networks.
721+ *
722+ * @param host bind host ({@code "127.0.0.1"} for loopback)
723+ * @param port bind port; {@code 0} selects a free port
724+ * @param idleTimeoutMs idle watchdog in milliseconds; {@code <= 0} disables it
725+ * @throws IOException if the socket cannot be bound or served
726+ */
727+ public void runTcp (String host , int port , long idleTimeoutMs ) throws IOException {
728+ TcpSocketTransport .serveForever (host , port , buildServer (false ), idleTimeoutMs ,
729+ (boundHost , boundPort ) -> {
730+ System .out .println ("TCP:" + boundHost + ":" + boundPort );
731+ System .out .flush ();
732+ });
733+ }
734+
735+ /**
736+ * Parsed {@code [HOST:]PORT} TCP bind spec. Host defaults to loopback.
737+ *
738+ * @param host bind host
739+ * @param port bind port ({@code 0} = ephemeral)
740+ */
741+ public record TcpAddr (String host , int port ) {}
742+
743+ /**
744+ * Parse a {@code [HOST:]PORT} TCP bind spec as accepted by {@code --tcp}.
745+ * A bare {@code PORT} binds {@code 127.0.0.1}; an empty host (leading
746+ * {@code ":"}) also defaults to loopback.
747+ *
748+ * @param spec the {@code [HOST:]PORT} string
749+ * @return the parsed host/port
750+ */
751+ public static TcpAddr parseTcpAddr (String spec ) {
752+ int idx = spec .lastIndexOf (':' );
753+ if (idx >= 0 ) {
754+ String h = spec .substring (0 , idx );
755+ return new TcpAddr (h .isEmpty () ? "127.0.0.1" : h ,
756+ Integer .parseInt (spec .substring (idx + 1 )));
757+ }
758+ return new TcpAddr ("127.0.0.1" , Integer .parseInt (spec ));
759+ }
760+
710761 /**
711762 * Run as an HTTP server bound to {@code host}/{@code port}, blocking until shutdown.
712763 *
@@ -723,8 +774,9 @@ public void runHttp(String host, int port) throws Exception {
723774 * the four flags every VGI worker accepts and runs the matching transport:
724775 * <ul>
725776 * <li>{@code --unix <path>}: AF_UNIX socket (launcher protocol)
777+ * <li>{@code --tcp [<host>:]<port>}: TCP socket (launcher protocol)
726778 * <li>{@code --http} with optional {@code --host}, {@code --port}: HTTP
727- * <li>{@code --idle-timeout <seconds>}: passed to {@code runUnixSocket}
779+ * <li>{@code --idle-timeout <seconds>}: passed to {@code runUnixSocket} / {@code runTcp}
728780 * <li>(default): stdio
729781 * </ul>
730782 * Also honours {@code VGI_WORKER_STDERR}: redirects {@link System#err} to
@@ -752,13 +804,15 @@ public void runFromArgs(String[] args,
752804 String host = "127.0.0.1" ;
753805 int port = 0 ;
754806 String unixSocket = null ;
807+ String tcpAddr = null ;
755808 long idleTimeoutMs = 0 ;
756809 for (int i = 0 ; i < args .length ; i ++) {
757810 switch (args [i ]) {
758811 case "--http" -> http = true ;
759812 case "--host" -> host = args [++i ];
760813 case "--port" -> port = Integer .parseInt (args [++i ]);
761814 case "--unix" -> unixSocket = args [++i ];
815+ case "--tcp" -> tcpAddr = args [++i ];
762816 case "--idle-timeout" -> idleTimeoutMs =
763817 (long ) (Double .parseDouble (args [++i ]) * 1000.0 );
764818 default -> { System .err .println ("unknown arg: " + args [i ]); System .exit (2 ); }
@@ -767,6 +821,9 @@ public void runFromArgs(String[] args,
767821 try {
768822 if (unixSocket != null ) {
769823 runUnixSocket (Path .of (unixSocket ), idleTimeoutMs );
824+ } else if (tcpAddr != null ) {
825+ TcpAddr a = parseTcpAddr (tcpAddr );
826+ runTcp (a .host (), a .port (), idleTimeoutMs );
770827 } else if (http ) {
771828 HttpServer .Config .Builder b = HttpServer .Config .builder ().host (host ).port (port );
772829 if (httpCustomizer != null ) b = httpCustomizer .apply (b );
0 commit comments