Skip to content

Commit 16fbd29

Browse files
CopilotedburnsCopilot
committed
[Java] Add in-process FFI runtime host lifecycle and stream transport primitives (#2233)
* Initial plan * java: add FFI runtime host streams and lifecycle Co-authored-by: edburns <75821+edburns@users.noreply.github.com> * fix: address Copilot review comments on PR #2233 1. FfiRuntimeHost: serialize start() with close() via operationLock to prevent race where close() completes while hostStart blocks, then start() publishes handles after teardown. 2. FfiRuntimeHostTest: add Javadoc explaining the native integration test is intentionally skipped in CI and how to run it locally. 3. JsonRpcClient.fromStreams(): take ownership of supplied streams and close them in close() to prevent resource leaks. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: edburns <75821+edburns@users.noreply.github.com> Co-authored-by: Ed Burns <edburns@microsoft.com> Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent eac5c44 commit 16fbd29

8 files changed

Lines changed: 1085 additions & 0 deletions

File tree

java/sdk/src/main/java/com/github/copilot/JsonRpcClient.java

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,17 +46,24 @@ class JsonRpcClient implements AutoCloseable {
4646
private final OutputStream outputStream;
4747
private final Socket socket;
4848
private final Process process;
49+
private final boolean ownsStreams;
4950
private final AtomicLong requestIdCounter = new AtomicLong(0);
5051
private final Map<Long, CompletableFuture<JsonNode>> pendingRequests = new ConcurrentHashMap<>();
5152
private final Map<String, BiConsumer<String, JsonNode>> notificationHandlers = new ConcurrentHashMap<>();
5253
private final ExecutorService readerExecutor;
5354
private volatile boolean running = true;
5455

5556
private JsonRpcClient(InputStream inputStream, OutputStream outputStream, Socket socket, Process process) {
57+
this(inputStream, outputStream, socket, process, false);
58+
}
59+
60+
private JsonRpcClient(InputStream inputStream, OutputStream outputStream, Socket socket, Process process,
61+
boolean ownsStreams) {
5662
this.inputStream = inputStream;
5763
this.outputStream = outputStream;
5864
this.socket = socket;
5965
this.process = process;
66+
this.ownsStreams = ownsStreams;
6067
this.readerExecutor = Executors.newSingleThreadExecutor(r -> {
6168
Thread t = new Thread(r, "jsonrpc-reader");
6269
t.setDaemon(true);
@@ -93,6 +100,15 @@ public static JsonRpcClient fromSocket(Socket socket) throws IOException {
93100
return new JsonRpcClient(socket.getInputStream(), socket.getOutputStream(), socket, null);
94101
}
95102

103+
/**
104+
* Creates a JSON-RPC client over arbitrary input/output streams. The client
105+
* takes ownership of the streams and closes them when {@link #close()} is
106+
* called.
107+
*/
108+
public static JsonRpcClient fromStreams(InputStream inputStream, OutputStream outputStream) {
109+
return new JsonRpcClient(inputStream, outputStream, null, null, true);
110+
}
111+
96112
/**
97113
* Registers a handler for JSON-RPC method calls (requests/notifications from
98114
* server).
@@ -344,6 +360,19 @@ public void close() {
344360
if (process != null) {
345361
process.destroy();
346362
}
363+
364+
if (ownsStreams) {
365+
try {
366+
inputStream.close();
367+
} catch (IOException e) {
368+
LOG.log(Level.FINE, "Error closing input stream", e);
369+
}
370+
try {
371+
outputStream.close();
372+
} catch (IOException e) {
373+
LOG.log(Level.FINE, "Error closing output stream", e);
374+
}
375+
}
347376
}
348377

349378
public boolean isConnected() {
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
/*---------------------------------------------------------------------------------------------
2+
* Copyright (c) Microsoft Corporation. All rights reserved.
3+
*--------------------------------------------------------------------------------------------*/
4+
5+
package com.github.copilot.ffi;
6+
7+
import java.io.IOException;
8+
import java.io.OutputStream;
9+
import java.util.Arrays;
10+
import java.util.Objects;
11+
import java.util.concurrent.atomic.AtomicBoolean;
12+
import java.util.concurrent.atomic.AtomicInteger;
13+
import java.util.concurrent.locks.ReentrantLock;
14+
15+
final class FfiOutputStream extends OutputStream {
16+
17+
private final NativeBinding nativeBinding;
18+
private final AtomicInteger connectionId;
19+
private final AtomicBoolean closing;
20+
private final ReentrantLock operationLock;
21+
22+
FfiOutputStream(NativeBinding nativeBinding, AtomicInteger connectionId, AtomicBoolean closing,
23+
ReentrantLock operationLock) {
24+
this.nativeBinding = Objects.requireNonNull(nativeBinding, "nativeBinding must not be null");
25+
this.connectionId = Objects.requireNonNull(connectionId, "connectionId must not be null");
26+
this.closing = Objects.requireNonNull(closing, "closing must not be null");
27+
this.operationLock = Objects.requireNonNull(operationLock, "operationLock must not be null");
28+
}
29+
30+
@Override
31+
public void write(int b) throws IOException {
32+
write(new byte[]{(byte) b}, 0, 1);
33+
}
34+
35+
@Override
36+
public void write(byte[] b, int off, int len) throws IOException {
37+
Objects.requireNonNull(b, "buffer must not be null");
38+
if (off < 0 || len < 0 || off + len > b.length) {
39+
throw new IndexOutOfBoundsException("Invalid off/len for buffer of length " + b.length);
40+
}
41+
if (len == 0) {
42+
return;
43+
}
44+
45+
operationLock.lock();
46+
try {
47+
if (closing.get()) {
48+
throw new IOException("The in-process runtime connection is closed.");
49+
}
50+
int id = connectionId.get();
51+
if (id == 0) {
52+
throw new IOException("The in-process runtime connection is closed.");
53+
}
54+
55+
byte[] payload = (off == 0 && len == b.length) ? b : Arrays.copyOfRange(b, off, off + len);
56+
if (!nativeBinding.connectionWrite(id, payload, payload.length)) {
57+
throw new IOException("Failed to write a frame to the in-process runtime connection.");
58+
}
59+
} finally {
60+
operationLock.unlock();
61+
}
62+
}
63+
}

0 commit comments

Comments
 (0)