-
Notifications
You must be signed in to change notification settings - Fork 7
Expand file tree
/
Copy pathCliServerManager.java.html
More file actions
338 lines (290 loc) · 19.9 KB
/
Copy pathCliServerManager.java.html
File metadata and controls
338 lines (290 loc) · 19.9 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
<?xml version="1.0" encoding="UTF-8"?><!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd"><html xmlns="http://www.w3.org/1999/xhtml" lang=""><head><meta http-equiv="Content-Type" content="text/html;charset=UTF-8"/><link rel="stylesheet" href="../jacoco-resources/report.css" type="text/css"/><link rel="shortcut icon" href="../jacoco-resources/report.gif" type="image/gif"/><title>CliServerManager.java</title><link rel="stylesheet" href="../jacoco-resources/prettify.css" type="text/css"/><script type="text/javascript" src="../jacoco-resources/prettify.js"></script></head><body onload="window['PR_TAB_WIDTH']=4;prettyPrint()"><div class="breadcrumb" id="breadcrumb"><span class="info"><a href="../jacoco-sessions.html" class="el_session">Sessions</a></span><a href="../index.html" class="el_report">GitHub Copilot SDK :: Java</a> > <a href="index.source.html" class="el_package">com.github.copilot</a> > <span class="el_source">CliServerManager.java</span></div><h1>CliServerManager.java</h1><pre class="source lang-java linenums">/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
*--------------------------------------------------------------------------------------------*/
package com.github.copilot;
import java.io.BufferedReader;
import java.io.File;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.Socket;
import java.net.URI;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import com.github.copilot.rpc.CopilotClientOptions;
/**
* Manages the lifecycle of the Copilot CLI server process.
* <p>
* This class handles spawning the CLI server process, building command lines,
* detecting the listening port, and establishing connections.
*/
final class CliServerManager {
<span class="nc" id="L32"> private static final Logger LOG = Logger.getLogger(CliServerManager.class.getName());</span>
private static final int STDERR_READER_JOIN_TIMEOUT_MS = 5000;
private final CopilotClientOptions options;
<span class="nc" id="L36"> private final StringBuilder stderrBuffer = new StringBuilder();</span>
private volatile Thread stderrThread;
private String connectionToken;
<span class="nc" id="L40"> CliServerManager(CopilotClientOptions options) {</span>
<span class="nc" id="L41"> this.options = options;</span>
<span class="nc" id="L42"> }</span>
/**
* Sets the connection token to pass to the CLI process via environment
* variable.
*
* @param connectionToken
* the token, or {@code null} if not applicable
*/
void setConnectionToken(String connectionToken) {
<span class="nc" id="L52"> this.connectionToken = connectionToken;</span>
<span class="nc" id="L53"> }</span>
/**
* Starts the CLI server process.
*
* @return information about the started process including detected port
* @throws IOException
* if the process cannot be started
* @throws InterruptedException
* if interrupted while waiting for port detection
*/
ProcessInfo startCliServer() throws IOException, InterruptedException {
<span class="nc" id="L65"> clearStderrBuffer();</span>
<span class="nc bnc" id="L67" title="All 2 branches missed."> String cliPath = options.getCliPath() != null ? options.getCliPath() : "copilot";</span>
<span class="nc" id="L68"> var args = new ArrayList<String>();</span>
<span class="nc bnc" id="L70" title="All 2 branches missed."> if (options.getCliArgs() != null) {</span>
<span class="nc" id="L71"> args.addAll(Arrays.asList(options.getCliArgs()));</span>
}
<span class="nc" id="L74"> args.add("--server");</span>
<span class="nc" id="L75"> args.add("--no-auto-update");</span>
<span class="nc" id="L76"> args.add("--log-level");</span>
<span class="nc" id="L77"> args.add(options.getLogLevel());</span>
<span class="nc bnc" id="L79" title="All 2 branches missed."> if (options.isUseStdio()) {</span>
<span class="nc" id="L80"> args.add("--stdio");</span>
<span class="nc bnc" id="L81" title="All 2 branches missed."> } else if (options.getPort() > 0) {</span>
<span class="nc" id="L82"> args.add("--port");</span>
<span class="nc" id="L83"> args.add(String.valueOf(options.getPort()));</span>
}
// Add auth-related flags
<span class="nc bnc" id="L87" title="All 4 branches missed."> if (options.getGitHubToken() != null && !options.getGitHubToken().isEmpty()) {</span>
<span class="nc" id="L88"> args.add("--auth-token-env");</span>
<span class="nc" id="L89"> args.add("COPILOT_SDK_AUTH_TOKEN");</span>
}
// Default UseLoggedInUser to false when GitHubToken is provided
<span class="nc" id="L93"> boolean useLoggedInUser = options.getUseLoggedInUser()</span>
<span class="nc bnc" id="L94" title="All 4 branches missed."> .orElse(options.getGitHubToken() == null || options.getGitHubToken().isEmpty());</span>
<span class="nc bnc" id="L95" title="All 2 branches missed."> if (!useLoggedInUser) {</span>
<span class="nc" id="L96"> args.add("--no-auto-login");</span>
}
<span class="nc bnc" id="L99" title="All 2 branches missed."> if (options.getSessionIdleTimeoutSeconds().isPresent()</span>
<span class="nc bnc" id="L100" title="All 2 branches missed."> && options.getSessionIdleTimeoutSeconds().getAsInt() > 0) {</span>
<span class="nc" id="L101"> args.add("--session-idle-timeout");</span>
<span class="nc" id="L102"> args.add(String.valueOf(options.getSessionIdleTimeoutSeconds().getAsInt()));</span>
}
<span class="nc bnc" id="L105" title="All 2 branches missed."> if (options.isRemote()) {</span>
<span class="nc" id="L106"> args.add("--remote");</span>
}
<span class="nc" id="L109"> List<String> command = resolveCliCommand(cliPath, args);</span>
<span class="nc" id="L111"> var pb = new ProcessBuilder(command);</span>
<span class="nc" id="L112"> pb.redirectErrorStream(false);</span>
// Note: On Windows, console window visibility depends on how the parent Java
// process was launched. GUI applications started with 'javaw' will not create
// visible console windows for subprocesses. Console applications started with
// 'java' will share their console with subprocesses. Java's ProcessBuilder
// doesn't provide explicit CREATE_NO_WINDOW flags like native Windows APIs,
// but the default behavior is appropriate for most use cases.
<span class="nc bnc" id="L121" title="All 2 branches missed."> if (options.getCwd() != null) {</span>
<span class="nc" id="L122"> pb.directory(new File(options.getCwd()));</span>
}
<span class="nc bnc" id="L125" title="All 2 branches missed."> if (options.getEnvironment() != null) {</span>
<span class="nc" id="L126"> pb.environment().clear();</span>
<span class="nc" id="L127"> pb.environment().putAll(options.getEnvironment());</span>
}
<span class="nc" id="L129"> pb.environment().remove("NODE_DEBUG");</span>
// Set auth token in environment if provided
<span class="nc bnc" id="L132" title="All 4 branches missed."> if (options.getGitHubToken() != null && !options.getGitHubToken().isEmpty()) {</span>
<span class="nc" id="L133"> pb.environment().put("COPILOT_SDK_AUTH_TOKEN", options.getGitHubToken());</span>
}
// Set Copilot home directory if configured
<span class="nc bnc" id="L137" title="All 4 branches missed."> if (options.getCopilotHome() != null && !options.getCopilotHome().isEmpty()) {</span>
<span class="nc" id="L138"> pb.environment().put("COPILOT_HOME", options.getCopilotHome());</span>
}
// Set connection token for TCP mode
<span class="nc bnc" id="L142" title="All 4 branches missed."> if (connectionToken != null && !connectionToken.isEmpty()) {</span>
<span class="nc" id="L143"> pb.environment().put("COPILOT_CONNECTION_TOKEN", connectionToken);</span>
}
// Set telemetry environment variables if configured
<span class="nc bnc" id="L147" title="All 2 branches missed."> if (options.getTelemetry() != null) {</span>
<span class="nc" id="L148"> var telemetry = options.getTelemetry();</span>
<span class="nc" id="L149"> pb.environment().put("COPILOT_OTEL_ENABLED", "true");</span>
<span class="nc bnc" id="L150" title="All 2 branches missed."> if (telemetry.getOtlpEndpoint() != null) {</span>
<span class="nc" id="L151"> pb.environment().put("OTEL_EXPORTER_OTLP_ENDPOINT", telemetry.getOtlpEndpoint());</span>
}
<span class="nc bnc" id="L153" title="All 2 branches missed."> if (telemetry.getFilePath() != null) {</span>
<span class="nc" id="L154"> pb.environment().put("COPILOT_OTEL_FILE_EXPORTER_PATH", telemetry.getFilePath());</span>
}
<span class="nc bnc" id="L156" title="All 2 branches missed."> if (telemetry.getExporterType() != null) {</span>
<span class="nc" id="L157"> pb.environment().put("COPILOT_OTEL_EXPORTER_TYPE", telemetry.getExporterType());</span>
}
<span class="nc bnc" id="L159" title="All 2 branches missed."> if (telemetry.getSourceName() != null) {</span>
<span class="nc" id="L160"> pb.environment().put("COPILOT_OTEL_SOURCE_NAME", telemetry.getSourceName());</span>
}
<span class="nc bnc" id="L162" title="All 2 branches missed."> if (telemetry.getCaptureContent().isPresent()) {</span>
<span class="nc" id="L163"> pb.environment().put("OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT",</span>
<span class="nc bnc" id="L164" title="All 2 branches missed."> telemetry.getCaptureContent().get() ? "true" : "false");</span>
}
}
<span class="nc" id="L168"> Process process = pb.start();</span>
// Forward stderr to logger in background
<span class="nc" id="L171"> startStderrReader(process);</span>
<span class="nc" id="L173"> Integer detectedPort = null;</span>
<span class="nc bnc" id="L174" title="All 2 branches missed."> if (!options.isUseStdio()) {</span>
<span class="nc" id="L175"> detectedPort = waitForPortAnnouncement(process);</span>
}
<span class="nc" id="L178"> return new ProcessInfo(process, detectedPort);</span>
}
/**
* Connects to a running Copilot server.
*
* @param process
* the CLI process (null if connecting to external server)
* @param tcpHost
* the host to connect to (null for stdio mode)
* @param tcpPort
* the port to connect to (null for stdio mode)
* @return the JSON-RPC client connected to the server
* @throws IOException
* if connection fails
*/
JsonRpcClient connectToServer(Process process, String tcpHost, Integer tcpPort) throws IOException {
<span class="nc bnc" id="L195" title="All 4 branches missed."> if (tcpHost != null && tcpPort != null) {</span>
// TCP mode: external server or child process with explicit port
<span class="nc" id="L197"> Socket socket = new Socket(tcpHost, tcpPort);</span>
<span class="nc" id="L198"> return JsonRpcClient.fromSocket(socket);</span>
<span class="nc bnc" id="L199" title="All 2 branches missed."> } else if (process != null) {</span>
// Stdio mode: child process
<span class="nc" id="L201"> return JsonRpcClient.fromProcess(process);</span>
} else {
<span class="nc" id="L203"> throw new IllegalStateException("Cannot connect: no process for stdio and no host:port for TCP");</span>
}
}
private void startStderrReader(Process process) {
<span class="nc" id="L208"> var thread = new Thread(() -> {</span>
<span class="nc" id="L209"> try (BufferedReader reader = new BufferedReader(</span>
<span class="nc" id="L210"> new InputStreamReader(process.getErrorStream(), StandardCharsets.UTF_8))) {</span>
String line;
<span class="nc bnc" id="L212" title="All 2 branches missed."> while ((line = reader.readLine()) != null) {</span>
<span class="nc" id="L213"> synchronized (stderrBuffer) {</span>
<span class="nc" id="L214"> stderrBuffer.append(line).append('\n');</span>
<span class="nc" id="L215"> }</span>
<span class="nc" id="L216"> LOG.fine("[CLI] " + line);</span>
}
<span class="nc" id="L218"> } catch (IOException e) {</span>
<span class="nc" id="L219"> LOG.log(Level.FINE, "Error reading stderr", e);</span>
<span class="nc" id="L220"> }</span>
<span class="nc" id="L221"> }, "cli-stderr-reader");</span>
<span class="nc" id="L222"> thread.setDaemon(true);</span>
<span class="nc" id="L223"> thread.start();</span>
<span class="nc" id="L224"> this.stderrThread = thread;</span>
<span class="nc" id="L225"> }</span>
private Integer waitForPortAnnouncement(Process process) throws IOException {
<span class="nc" id="L228"> try (BufferedReader reader = new BufferedReader(</span>
<span class="nc" id="L229"> new InputStreamReader(process.getInputStream(), StandardCharsets.UTF_8))) {</span>
<span class="nc" id="L230"> Pattern portPattern = Pattern.compile("listening on port (\\d+)", Pattern.CASE_INSENSITIVE);</span>
<span class="nc" id="L231"> long deadline = System.currentTimeMillis() + 30000;</span>
<span class="nc bnc" id="L233" title="All 2 branches missed."> while (System.currentTimeMillis() < deadline) {</span>
<span class="nc" id="L234"> String line = reader.readLine();</span>
<span class="nc bnc" id="L235" title="All 2 branches missed."> if (line == null) {</span>
<span class="nc" id="L236"> awaitStderrReader();</span>
<span class="nc" id="L237"> String stderr = getStderrOutput();</span>
<span class="nc" id="L238"> throw new IOException(formatCliExitedMessage("CLI process exited unexpectedly.", stderr));</span>
}
<span class="nc" id="L241"> Matcher matcher = portPattern.matcher(line);</span>
<span class="nc bnc" id="L242" title="All 2 branches missed."> if (matcher.find()) {</span>
<span class="nc" id="L243"> return Integer.parseInt(matcher.group(1));</span>
}
<span class="nc" id="L245"> }</span>
<span class="nc" id="L247"> process.destroyForcibly();</span>
<span class="nc" id="L248"> throw new IOException("Timeout waiting for CLI to announce port");</span>
}
}
String getStderrOutput() {
<span class="nc" id="L253"> synchronized (stderrBuffer) {</span>
<span class="nc" id="L254"> return stderrBuffer.toString().trim();</span>
}
}
private void awaitStderrReader() {
<span class="nc" id="L259"> Thread t = this.stderrThread;</span>
<span class="nc bnc" id="L260" title="All 2 branches missed."> if (t != null) {</span>
try {
<span class="nc" id="L262"> t.join(STDERR_READER_JOIN_TIMEOUT_MS);</span>
<span class="nc" id="L263"> } catch (InterruptedException e) {</span>
<span class="nc" id="L264"> Thread.currentThread().interrupt();</span>
<span class="nc" id="L265"> }</span>
}
<span class="nc" id="L267"> }</span>
private void clearStderrBuffer() {
<span class="nc" id="L270"> synchronized (stderrBuffer) {</span>
<span class="nc" id="L271"> stderrBuffer.setLength(0);</span>
<span class="nc" id="L272"> }</span>
<span class="nc" id="L273"> }</span>
static String formatCliExitedMessage(String message, String stderrOutput) {
<span class="nc bnc" id="L276" title="All 4 branches missed."> if (stderrOutput == null || stderrOutput.isEmpty()) {</span>
<span class="nc" id="L277"> return message;</span>
}
<span class="nc" id="L279"> return message + "\nstderr: " + stderrOutput;</span>
}
private List<String> resolveCliCommand(String cliPath, List<String> args) {
<span class="nc" id="L283"> boolean isJsFile = cliPath.toLowerCase().endsWith(".js");</span>
<span class="nc bnc" id="L285" title="All 2 branches missed."> if (isJsFile) {</span>
<span class="nc" id="L286"> var result = new ArrayList<String>();</span>
<span class="nc" id="L287"> result.add("node");</span>
<span class="nc" id="L288"> result.add(cliPath);</span>
<span class="nc" id="L289"> result.addAll(args);</span>
<span class="nc" id="L290"> return result;</span>
}
// On Windows, use cmd /c to resolve the executable
<span class="nc" id="L294"> String os = System.getProperty("os.name").toLowerCase();</span>
<span class="nc bnc" id="L295" title="All 4 branches missed."> if (os.contains("win") && !new File(cliPath).isAbsolute()) {</span>
<span class="nc" id="L296"> var result = new ArrayList<String>();</span>
<span class="nc" id="L297"> result.add("cmd");</span>
<span class="nc" id="L298"> result.add("/c");</span>
<span class="nc" id="L299"> result.add(cliPath);</span>
<span class="nc" id="L300"> result.addAll(args);</span>
<span class="nc" id="L301"> return result;</span>
}
<span class="nc" id="L304"> var result = new ArrayList<String>();</span>
<span class="nc" id="L305"> result.add(cliPath);</span>
<span class="nc" id="L306"> result.addAll(args);</span>
<span class="nc" id="L307"> return result;</span>
}
static URI parseCliUrl(String url) {
// If it's just a port number, treat as localhost
try {
<span class="nc" id="L313"> int port = Integer.parseInt(url);</span>
<span class="nc" id="L314"> return URI.create("http://localhost:" + port);</span>
<span class="nc" id="L315"> } catch (NumberFormatException e) {</span>
// Not a port number, continue
}
// Add scheme if missing
<span class="nc bnc" id="L320" title="All 4 branches missed."> if (!url.toLowerCase().startsWith("http://") && !url.toLowerCase().startsWith("https://")) {</span>
<span class="nc" id="L321"> url = "https://" + url;</span>
}
<span class="nc" id="L324"> return URI.create(url);</span>
}
/**
* Information about a started CLI server process.
*
* @param process
* the CLI process
* @param port
* the detected TCP port (null for stdio mode)
*/
<span class="nc" id="L335"> record ProcessInfo(Process process, Integer port) {</span>
}
}
</pre><div class="footer"><span class="right">Created with <a href="http://www.jacoco.org/jacoco">JaCoCo</a> 0.8.14.202510111229</span></div></body></html>