Skip to content

Commit 294ba93

Browse files
authored
fix: redirect Winston logger to stderr to prevent IPC stream corruption + fix binary tests (#11914)
1 parent a3dbeb3 commit 294ba93

File tree

2 files changed

+145
-37
lines changed

2 files changed

+145
-37
lines changed

binary/test/binary.test.ts

Lines changed: 141 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { ModelDescription, SerializedContinueConfig } from "core";
2-
// import Mock from "core/llm/llms/Mock.js";
2+
import { IDE } from "core/index.js";
33
import { FromIdeProtocol, ToIdeProtocol } from "core/protocol/index.js";
44
import { IMessenger } from "core/protocol/messenger";
55
import FileSystemIde from "core/util/filesystem";
@@ -15,7 +15,115 @@ import {
1515
CoreBinaryTcpMessenger,
1616
} from "../src/IpcMessenger";
1717

18-
// jest.setTimeout(100_000);
18+
/**
19+
* Handles IDE messages from the binary subprocess, responding with plain data
20+
* matching the Kotlin CoreMessenger format: { messageType, data, messageId }.
21+
*
22+
* This bypasses the JS _handleLine auto-wrapper which would double-wrap
23+
* responses in { done, content, status }.
24+
*/
25+
class BinaryIdeHandler {
26+
private ide: IDE;
27+
private subprocess: ChildProcessWithoutNullStreams;
28+
private handlers: Record<string, (data: any) => Promise<any> | any> = {};
29+
private unfinishedLine: string | undefined;
30+
31+
constructor(subprocess: ChildProcessWithoutNullStreams, ide: IDE) {
32+
this.ide = ide;
33+
this.subprocess = subprocess;
34+
this.registerHandlers();
35+
36+
// Listen on stdout alongside CoreBinaryMessenger (EventEmitter allows multiple listeners)
37+
// Use setEncoding so split multibyte UTF-8 characters are decoded correctly
38+
subprocess.stdout.setEncoding("utf8");
39+
subprocess.stdout.on("data", (data: string) => this.handleData(data));
40+
}
41+
42+
private registerHandlers() {
43+
const ide = this.ide;
44+
const h = this.handlers;
45+
h["getIdeInfo"] = () => ide.getIdeInfo();
46+
h["getIdeSettings"] = () => ide.getIdeSettings();
47+
h["getControlPlaneSessionInfo"] = () => undefined;
48+
h["getWorkspaceDirs"] = () => ide.getWorkspaceDirs();
49+
h["readFile"] = (d) => ide.readFile(d.filepath);
50+
h["writeFile"] = (d) => ide.writeFile(d.path, d.contents);
51+
h["fileExists"] = (d) => ide.fileExists(d.filepath);
52+
h["showLines"] = (d) => ide.showLines(d.filepath, d.startLine, d.endLine);
53+
h["openFile"] = (d) => ide.openFile(d.path);
54+
h["openUrl"] = (d) => ide.openUrl(d.url);
55+
h["runCommand"] = (d) => ide.runCommand(d.command);
56+
h["saveFile"] = (d) => ide.saveFile(d.filepath);
57+
h["readRangeInFile"] = (d) => ide.readRangeInFile(d.filepath, d.range);
58+
h["getFileStats"] = (d) => ide.getFileStats(d.files);
59+
h["getGitRootPath"] = (d) => ide.getGitRootPath(d.dir);
60+
h["listDir"] = (d) => ide.listDir(d.dir);
61+
h["getRepoName"] = (d) => ide.getRepoName(d.dir);
62+
h["getTags"] = (d) => ide.getTags(d);
63+
h["isTelemetryEnabled"] = () => ide.isTelemetryEnabled();
64+
h["isWorkspaceRemote"] = () => false;
65+
h["getUniqueId"] = () => ide.getUniqueId();
66+
h["getDiff"] = (d) => ide.getDiff(d.includeUnstaged);
67+
h["getTerminalContents"] = () => ide.getTerminalContents();
68+
h["getOpenFiles"] = () => ide.getOpenFiles();
69+
h["getCurrentFile"] = () => ide.getCurrentFile();
70+
h["getPinnedFiles"] = () => ide.getPinnedFiles();
71+
h["getSearchResults"] = (d) => ide.getSearchResults(d.query, d.maxResults);
72+
h["getFileResults"] = (d) => ide.getFileResults(d.pattern);
73+
h["getProblems"] = (d) => ide.getProblems(d.filepath);
74+
h["getBranch"] = (d) => ide.getBranch(d.dir);
75+
h["subprocess"] = (d) => ide.subprocess(d.command, d.cwd);
76+
h["getDebugLocals"] = (d) => ide.getDebugLocals(d.threadIndex);
77+
h["getAvailableThreads"] = () => ide.getAvailableThreads();
78+
h["getTopLevelCallStackSources"] = (d) =>
79+
ide.getTopLevelCallStackSources(d.threadIndex, d.stackDepth);
80+
h["showToast"] = () => {};
81+
h["readSecrets"] = (d) => ide.readSecrets(d.keys);
82+
h["writeSecrets"] = (d) => ide.writeSecrets(d.secrets);
83+
h["removeFile"] = (d) => ide.removeFile(d.path);
84+
}
85+
86+
private handleData(data: string) {
87+
const d = data;
88+
const lines = d.split(/\r\n/).filter((line) => line.trim() !== "");
89+
if (lines.length === 0) return;
90+
91+
if (this.unfinishedLine) {
92+
lines[0] = this.unfinishedLine + lines[0];
93+
this.unfinishedLine = undefined;
94+
}
95+
if (!d.endsWith("\r\n")) {
96+
this.unfinishedLine = lines.pop();
97+
}
98+
lines.forEach((line) => this.handleLine(line));
99+
}
100+
101+
private async handleLine(line: string) {
102+
let msg: { messageType: string; messageId: string; data?: any };
103+
try {
104+
msg = JSON.parse(line);
105+
} catch {
106+
return; // not JSON, ignore
107+
}
108+
109+
const handler = this.handlers[msg.messageType];
110+
if (!handler) return; // not an IDE message, let CoreBinaryMessenger handle it
111+
112+
try {
113+
const result = await handler(msg.data);
114+
this.respond(msg.messageType, result, msg.messageId);
115+
} catch (e) {
116+
this.respond(msg.messageType, undefined, msg.messageId);
117+
}
118+
}
119+
120+
private respond(messageType: string, data: any, messageId: string) {
121+
const response = JSON.stringify({ messageType, data, messageId });
122+
this.subprocess.stdin.write(response + "\r\n");
123+
}
124+
}
125+
126+
jest.setTimeout(30_000);
19127

20128
const USE_TCP = false;
21129

@@ -122,6 +230,11 @@ describe("Test Suite", () => {
122230
console.error("Error spawning subprocess:", error);
123231
throw error;
124232
}
233+
234+
subprocess.stderr.on("data", (data: Buffer) => {
235+
console.error(`[stderr] ${data.toString()}`);
236+
});
237+
125238
messenger = new CoreBinaryMessenger<ToIdeProtocol, FromIdeProtocol>(
126239
subprocess,
127240
);
@@ -132,7 +245,9 @@ describe("Test Suite", () => {
132245
fs.mkdirSync(testDir);
133246
}
134247
const ide = new FileSystemIde(testDir);
135-
// const reverseIde = new ReverseMessageIde(messenger.on.bind(messenger), ide);
248+
if (!USE_TCP && subprocess) {
249+
new BinaryIdeHandler(subprocess, ide);
250+
}
136251

137252
// Wait for core to set itself up
138253
await new Promise((resolve) => setTimeout(resolve, 1000));
@@ -151,28 +266,25 @@ describe("Test Suite", () => {
151266
}
152267
});
153268

269+
// Binary responses are wrapped in { done, content, status } by _handleLine.
270+
// This helper unwraps them, matching how the Kotlin CoreMessenger reads responses.
271+
async function request(messageType: string, data: any): Promise<any> {
272+
const resp = await messenger.request(messageType as any, data);
273+
return resp?.content !== undefined ? resp.content : resp;
274+
}
275+
154276
it("should respond to ping with pong", async () => {
155-
const resp = await messenger.request("ping", "ping");
277+
const resp = await request("ping", "ping");
156278
expect(resp).toBe("pong");
157279
});
158280

159281
it("should create .continue directory at the specified location with expected files", async () => {
160282
expect(fs.existsSync(CONTINUE_GLOBAL_DIR)).toBe(true);
161283

162284
// Many of the files are only created when trying to load the config
163-
const config = await messenger.request(
164-
"config/getSerializedProfileInfo",
165-
undefined,
166-
);
285+
await request("config/getSerializedProfileInfo", undefined);
167286

168-
const expectedFiles = [
169-
"config.json",
170-
"config.ts",
171-
"package.json",
172-
"logs/core.log",
173-
"index/autocompleteCache.sqlite",
174-
"types/core/index.d.ts",
175-
];
287+
const expectedFiles = ["logs/core.log", "index/autocompleteCache.sqlite"];
176288

177289
const missingFiles = expectedFiles.filter((file) => {
178290
const filePath = path.join(CONTINUE_GLOBAL_DIR, file);
@@ -186,38 +298,36 @@ describe("Test Suite", () => {
186298
});
187299

188300
it("should return valid config object", async () => {
189-
const { result } = await messenger.request(
301+
const { result } = await request(
190302
"config/getSerializedProfileInfo",
191303
undefined,
192304
);
193305
const { config } = result;
194-
expect(config).toHaveProperty("models");
195-
expect(config).toHaveProperty("embeddingsProvider");
306+
expect(config).toHaveProperty("modelsByRole");
196307
expect(config).toHaveProperty("contextProviders");
197308
expect(config).toHaveProperty("slashCommands");
198309
});
199310

200311
it("should properly handle history requests", async () => {
201312
const sessionId = "test-session-id";
202-
await messenger.request("history/save", {
313+
await request("history/save", {
203314
history: [],
204315
sessionId,
205316
title: "test-title",
206-
207317
workspaceDirectory: "test-workspace-directory",
208318
});
209-
const sessions = await messenger.request("history/list", {});
319+
const sessions = await request("history/list", {});
210320
expect(sessions.length).toBeGreaterThan(0);
211321

212-
const session = await messenger.request("history/load", {
322+
const session = await request("history/load", {
213323
id: sessionId,
214324
});
215325
expect(session).toHaveProperty("history");
216326

217-
await messenger.request("history/delete", {
327+
await request("history/delete", {
218328
id: sessionId,
219329
});
220-
const sessionsAfterDelete = await messenger.request("history/list", {});
330+
const sessionsAfterDelete = await request("history/list", {});
221331
expect(sessionsAfterDelete.length).toBe(sessions.length - 1);
222332
});
223333

@@ -228,23 +338,21 @@ describe("Test Suite", () => {
228338
model: "gpt-3.5-turbo",
229339
underlyingProviderName: "openai",
230340
};
231-
await messenger.request("config/addModel", {
232-
model,
233-
});
341+
await request("config/addModel", { model });
234342
const {
235343
result: { config },
236-
} = await messenger.request("config/getSerializedProfileInfo", undefined);
344+
} = await request("config/getSerializedProfileInfo", undefined);
237345

238346
expect(
239347
config!.modelsByRole.chat.some(
240348
(m: ModelDescription) => m.title === model.title,
241349
),
242350
).toBe(true);
243351

244-
await messenger.request("config/deleteModel", { title: model.title });
352+
await request("config/deleteModel", { title: model.title });
245353
const {
246354
result: { config: configAfterDelete },
247-
} = await messenger.request("config/getSerializedProfileInfo", undefined);
355+
} = await request("config/getSerializedProfileInfo", undefined);
248356
expect(
249357
configAfterDelete!.modelsByRole.chat.some(
250358
(m: ModelDescription) => m.title === model.title,
@@ -259,11 +367,9 @@ describe("Test Suite", () => {
259367
model: "gpt-3.5-turbo",
260368
underlyingProviderName: "mock",
261369
};
262-
await messenger.request("config/addModel", {
263-
model,
264-
});
370+
await request("config/addModel", { model });
265371

266-
const resp = await messenger.request("llm/complete", {
372+
const resp = await request("llm/complete", {
267373
prompt: "Say 'Hello' and nothing else",
268374
completionOptions: {},
269375
title: "Test Model",

core/util/Logger.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,8 +28,10 @@ class LoggerClass {
2828
}),
2929
]
3030
: []),
31-
// Normal console.log behavior
32-
new winston.transports.Console(),
31+
// Use stderr to avoid corrupting IPC stdout stream in the binary
32+
new winston.transports.Console({
33+
stderrLevels: ["error", "warn", "info", "debug"],
34+
}),
3335
],
3436
});
3537
}

0 commit comments

Comments
 (0)