fix: stabilize windows ci
This commit is contained in:
parent
8bd82cbb65
commit
d59539778b
10
src/cli.ts
10
src/cli.ts
@ -20,13 +20,13 @@ import { getActiveLogger, getActiveLogLevel, logError, logInfo, logWarn, setLogL
|
||||
import { consumeOutputFormat } from './cli/output-format.js';
|
||||
import { DEBUG_HANG, dumpActiveHandles, terminateChildProcesses } from './cli/runtime-debug.js';
|
||||
import { boldText, dimText, extraDimText, supportsAnsiColor } from './cli/terminal.js';
|
||||
import { analyzeConnectionError } from './error-classifier.js';
|
||||
import { parseLogLevel } from './logging.js';
|
||||
import { resolveConfigPath } from './config.js';
|
||||
import { createRuntime, MCPORTER_VERSION } from './runtime.js';
|
||||
import { DaemonClient } from './daemon/client.js';
|
||||
import { createKeepAliveRuntime } from './daemon/runtime-wrapper.js';
|
||||
import { analyzeConnectionError } from './error-classifier.js';
|
||||
import { isKeepAliveServer } from './lifecycle.js';
|
||||
import { parseLogLevel } from './logging.js';
|
||||
import { createRuntime, MCPORTER_VERSION } from './runtime.js';
|
||||
|
||||
export { parseCallArguments } from './cli/call-arguments.js';
|
||||
export { handleCall } from './cli/call-command.js';
|
||||
@ -142,9 +142,7 @@ export async function runCli(argv: string[]): Promise<void> {
|
||||
.map((entry) => entry.name)
|
||||
);
|
||||
const daemonClient =
|
||||
keepAliveServers.size > 0
|
||||
? new DaemonClient({ configPath: configResolution.path, rootDir: rootOverride })
|
||||
: null;
|
||||
keepAliveServers.size > 0 ? new DaemonClient({ configPath: configResolution.path, rootDir: rootOverride }) : null;
|
||||
const runtime = createKeepAliveRuntime(baseRuntime, { daemonClient, keepAliveServers });
|
||||
|
||||
const inference = inferCommandRouting(command, args, runtime.getDefinitions());
|
||||
|
||||
@ -1,8 +1,8 @@
|
||||
import { createRuntime } from '../runtime.js';
|
||||
import { isKeepAliveServer } from '../lifecycle.js';
|
||||
import { DaemonClient, resolveDaemonPaths } from '../daemon/client.js';
|
||||
import { launchDaemonDetached } from '../daemon/launch.js';
|
||||
import { runDaemonHost } from '../daemon/host.js';
|
||||
import { launchDaemonDetached } from '../daemon/launch.js';
|
||||
import { isKeepAliveServer } from '../lifecycle.js';
|
||||
import { createRuntime } from '../runtime.js';
|
||||
|
||||
interface DaemonCliOptions {
|
||||
readonly configPath: string;
|
||||
|
||||
@ -1,19 +1,18 @@
|
||||
import crypto from 'node:crypto';
|
||||
import crypto, { randomUUID } from 'node:crypto';
|
||||
import net from 'node:net';
|
||||
import path from 'node:path';
|
||||
import { randomUUID } from 'node:crypto';
|
||||
import {
|
||||
type CallToolParams,
|
||||
type CloseServerParams,
|
||||
type DaemonRequest,
|
||||
type DaemonResponse,
|
||||
type DaemonRequestMethod,
|
||||
type ListResourcesParams,
|
||||
type ListToolsParams,
|
||||
type StatusResult,
|
||||
} from './protocol.js';
|
||||
import { getDaemonMetadataPath, getDaemonSocketPath } from './paths.js';
|
||||
import { launchDaemonDetached } from './launch.js';
|
||||
import { getDaemonMetadataPath, getDaemonSocketPath } from './paths.js';
|
||||
import type {
|
||||
CallToolParams,
|
||||
CloseServerParams,
|
||||
DaemonRequest,
|
||||
DaemonRequestMethod,
|
||||
DaemonResponse,
|
||||
ListResourcesParams,
|
||||
ListToolsParams,
|
||||
StatusResult,
|
||||
} from './protocol.js';
|
||||
|
||||
export interface DaemonClientOptions {
|
||||
readonly configPath: string;
|
||||
@ -36,14 +35,12 @@ export function resolveDaemonPaths(configPath: string): DaemonPaths {
|
||||
}
|
||||
|
||||
export class DaemonClient {
|
||||
private readonly configKey: string;
|
||||
private readonly socketPath: string;
|
||||
private readonly metadataPath: string;
|
||||
private startingPromise: Promise<void> | null = null;
|
||||
|
||||
constructor(private readonly options: DaemonClientOptions) {
|
||||
const paths = resolveDaemonPaths(options.configPath);
|
||||
this.configKey = paths.key;
|
||||
this.socketPath = paths.socketPath;
|
||||
this.metadataPath = paths.metadataPath;
|
||||
}
|
||||
@ -118,16 +115,18 @@ export class DaemonClient {
|
||||
await this.startingPromise;
|
||||
return;
|
||||
}
|
||||
this.startingPromise = Promise.resolve().then(() => {
|
||||
launchDaemonDetached({
|
||||
configPath: this.options.configPath,
|
||||
rootDir: this.options.rootDir,
|
||||
metadataPath: this.metadataPath,
|
||||
socketPath: this.socketPath,
|
||||
this.startingPromise = Promise.resolve()
|
||||
.then(() => {
|
||||
launchDaemonDetached({
|
||||
configPath: this.options.configPath,
|
||||
rootDir: this.options.rootDir,
|
||||
metadataPath: this.metadataPath,
|
||||
socketPath: this.socketPath,
|
||||
});
|
||||
})
|
||||
.finally(() => {
|
||||
this.startingPromise = null;
|
||||
});
|
||||
}).finally(() => {
|
||||
this.startingPromise = null;
|
||||
});
|
||||
await this.startingPromise;
|
||||
}
|
||||
|
||||
@ -207,7 +206,7 @@ export class DaemonClient {
|
||||
let parsed: DaemonResponse<T>;
|
||||
try {
|
||||
parsed = JSON.parse(trimmed) as DaemonResponse<T>;
|
||||
} catch (error) {
|
||||
} catch {
|
||||
const parseError = new Error('Failed to parse daemon response.');
|
||||
(parseError as NodeJS.ErrnoException).code = 'ECONNRESET';
|
||||
throw parseError;
|
||||
|
||||
@ -1,17 +1,17 @@
|
||||
import net from 'node:net';
|
||||
import fs from 'node:fs/promises';
|
||||
import net from 'node:net';
|
||||
import path from 'node:path';
|
||||
import { createRuntime, type Runtime } from '../runtime.js';
|
||||
import type { ServerDefinition } from '../config.js';
|
||||
import { keepAliveIdleTimeout, isKeepAliveServer } from '../lifecycle.js';
|
||||
import {
|
||||
type CallToolParams,
|
||||
type CloseServerParams,
|
||||
type DaemonRequest,
|
||||
type DaemonResponse,
|
||||
type ListResourcesParams,
|
||||
type ListToolsParams,
|
||||
type StatusResult,
|
||||
import { isKeepAliveServer, keepAliveIdleTimeout } from '../lifecycle.js';
|
||||
import { createRuntime, type Runtime } from '../runtime.js';
|
||||
import type {
|
||||
CallToolParams,
|
||||
CloseServerParams,
|
||||
DaemonRequest,
|
||||
DaemonResponse,
|
||||
ListResourcesParams,
|
||||
ListToolsParams,
|
||||
StatusResult,
|
||||
} from './protocol.js';
|
||||
|
||||
interface DaemonHostOptions {
|
||||
@ -160,13 +160,7 @@ async function handleSocketRequest(
|
||||
metadata: { configPath: string; socketPath: string; startedAt: number },
|
||||
shutdown: () => Promise<void>
|
||||
): Promise<void> {
|
||||
const { response, shouldShutdown } = await processRequest(
|
||||
rawPayload,
|
||||
runtime,
|
||||
managedServers,
|
||||
activity,
|
||||
metadata
|
||||
);
|
||||
const { response, shouldShutdown } = await processRequest(rawPayload, runtime, managedServers, activity, metadata);
|
||||
socket.write(JSON.stringify(response), () => {
|
||||
socket.end(() => {
|
||||
if (shouldShutdown) {
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import path from 'node:path';
|
||||
import { spawn } from 'node:child_process';
|
||||
import path from 'node:path';
|
||||
|
||||
export interface DaemonLaunchOptions {
|
||||
readonly configPath: string;
|
||||
|
||||
@ -1,10 +1,4 @@
|
||||
export type DaemonRequestMethod =
|
||||
| 'callTool'
|
||||
| 'listTools'
|
||||
| 'listResources'
|
||||
| 'closeServer'
|
||||
| 'status'
|
||||
| 'stop';
|
||||
export type DaemonRequestMethod = 'callTool' | 'listTools' | 'listResources' | 'closeServer' | 'status' | 'stop';
|
||||
|
||||
export interface DaemonRequest<T extends DaemonRequestMethod = DaemonRequestMethod, P = unknown> {
|
||||
readonly id: string;
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import type { ListResourcesRequest } from '@modelcontextprotocol/sdk/types.js';
|
||||
import type { ServerDefinition } from '../config.js';
|
||||
import { isKeepAliveServer } from '../lifecycle.js';
|
||||
import type { CallOptions, ListToolsOptions, Runtime } from '../runtime.js';
|
||||
import type { ListResourcesRequest } from '@modelcontextprotocol/sdk/types.js';
|
||||
import type { DaemonClient } from './client.js';
|
||||
|
||||
interface KeepAliveRuntimeOptions {
|
||||
@ -9,10 +9,7 @@ interface KeepAliveRuntimeOptions {
|
||||
readonly keepAliveServers: Set<string>;
|
||||
}
|
||||
|
||||
export function createKeepAliveRuntime(
|
||||
base: Runtime,
|
||||
options: KeepAliveRuntimeOptions
|
||||
): Runtime {
|
||||
export function createKeepAliveRuntime(base: Runtime, options: KeepAliveRuntimeOptions): Runtime {
|
||||
if (!options.daemonClient || options.keepAliveServers.size === 0) {
|
||||
return base;
|
||||
}
|
||||
@ -47,10 +44,7 @@ class KeepAliveRuntime implements Runtime {
|
||||
}
|
||||
}
|
||||
|
||||
async listTools(
|
||||
server: string,
|
||||
options?: ListToolsOptions
|
||||
): Promise<Awaited<ReturnType<Runtime['listTools']>>> {
|
||||
async listTools(server: string, options?: ListToolsOptions): Promise<Awaited<ReturnType<Runtime['listTools']>>> {
|
||||
if (this.shouldUseDaemon(server)) {
|
||||
return (await this.daemon.listTools({
|
||||
server,
|
||||
|
||||
@ -1,17 +1,15 @@
|
||||
import os from 'node:os';
|
||||
import path from 'node:path';
|
||||
import { beforeEach, afterEach, describe, expect, it, vi } from 'vitest';
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
import { pathsForImport } from '../src/config-imports.js';
|
||||
|
||||
describe('pathsForImport on Windows', () => {
|
||||
let platformSpy: ReturnType<typeof vi.spyOn>;
|
||||
let homeSpy: ReturnType<typeof vi.spyOn>;
|
||||
const homeDir = path.join(os.tmpdir(), 'mcporter-win-home');
|
||||
const appData = path.join(homeDir, 'AppData', 'Roaming');
|
||||
|
||||
beforeEach(() => {
|
||||
platformSpy = vi.spyOn(process, 'platform', 'get').mockReturnValue('win32');
|
||||
homeSpy = vi.spyOn(os, 'homedir').mockReturnValue(homeDir);
|
||||
vi.spyOn(process, 'platform', 'get').mockReturnValue('win32');
|
||||
vi.spyOn(os, 'homedir').mockReturnValue(homeDir);
|
||||
process.env.HOME = homeDir;
|
||||
process.env.USERPROFILE = homeDir;
|
||||
process.env.APPDATA = appData;
|
||||
@ -19,8 +17,7 @@ describe('pathsForImport on Windows', () => {
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
platformSpy.mockRestore();
|
||||
homeSpy.mockRestore();
|
||||
vi.restoreAllMocks();
|
||||
delete process.env.HOME;
|
||||
delete process.env.USERPROFILE;
|
||||
delete process.env.APPDATA;
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import { describe, expect, it, vi } from 'vitest';
|
||||
import type { ServerDefinition } from '../src/config.js';
|
||||
import type { CallOptions, ListToolsOptions, Runtime } from '../src/runtime.js';
|
||||
import { createKeepAliveRuntime } from '../src/daemon/runtime-wrapper.js';
|
||||
import type { CallOptions, ListToolsOptions, Runtime } from '../src/runtime.js';
|
||||
|
||||
class FakeRuntime implements Runtime {
|
||||
private readonly definitions: ServerDefinition[];
|
||||
@ -34,16 +34,16 @@ class FakeRuntime implements Runtime {
|
||||
// no-op for tests
|
||||
}
|
||||
|
||||
async listTools(server: string, options?: ListToolsOptions): Promise<ReturnType<Runtime['listTools']>> {
|
||||
return this.listToolsMock(server, options);
|
||||
async listTools(server: string, options?: ListToolsOptions): Promise<Awaited<ReturnType<Runtime['listTools']>>> {
|
||||
return await this.listToolsMock(server, options);
|
||||
}
|
||||
|
||||
async callTool(server: string, toolName: string, options?: CallOptions): Promise<unknown> {
|
||||
return this.callToolMock(server, toolName, options);
|
||||
return await this.callToolMock(server, toolName, options);
|
||||
}
|
||||
|
||||
async listResources(server: string, options?: unknown): Promise<unknown> {
|
||||
return this.listResourcesMock(server, options);
|
||||
return await this.listResourcesMock(server, options);
|
||||
}
|
||||
|
||||
async connect(): Promise<never> {
|
||||
@ -91,8 +91,8 @@ describe('createKeepAliveRuntime', () => {
|
||||
await keepAliveRuntime.listTools('alpha', { includeSchema: true });
|
||||
expect(daemon.listTools).toHaveBeenCalledWith({ server: 'alpha', includeSchema: true, autoAuthorize: undefined });
|
||||
|
||||
await keepAliveRuntime.listResources('alpha', { cursor: 1 });
|
||||
expect(daemon.listResources).toHaveBeenCalledWith({ server: 'alpha', params: { cursor: 1 } });
|
||||
await keepAliveRuntime.listResources('alpha', { cursor: '1' });
|
||||
expect(daemon.listResources).toHaveBeenCalledWith({ server: 'alpha', params: { cursor: '1' } });
|
||||
|
||||
await keepAliveRuntime.close('alpha');
|
||||
expect(daemon.closeServer).toHaveBeenCalledWith({ server: 'alpha' });
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import type { ChildProcess } from 'node:child_process';
|
||||
import { beforeEach, afterEach, describe, expect, it, vi } from 'vitest';
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
|
||||
const execFileMock = vi.fn();
|
||||
|
||||
@ -12,16 +12,14 @@ vi.mock('node:child_process', async () => {
|
||||
});
|
||||
|
||||
describe('runtime-process-utils Windows process tree', () => {
|
||||
let platformSpy: ReturnType<typeof vi.spyOn>;
|
||||
|
||||
beforeEach(() => {
|
||||
vi.resetModules();
|
||||
execFileMock.mockReset();
|
||||
platformSpy = vi.spyOn(process, 'platform', 'get').mockReturnValue('win32');
|
||||
vi.spyOn(process, 'platform', 'get').mockReturnValue('win32');
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
platformSpy.mockRestore();
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
it('parses PowerShell output to enumerate descendants', async () => {
|
||||
@ -33,7 +31,7 @@ describe('runtime-process-utils Windows process tree', () => {
|
||||
{ ProcessId: rootPid + 3, ParentProcessId: 42 },
|
||||
]);
|
||||
|
||||
execFileMock.mockImplementation((command, args, options, callback) => {
|
||||
execFileMock.mockImplementation((command, _args, options, callback) => {
|
||||
const cb = typeof options === 'function' ? options : callback;
|
||||
if (command === 'powershell.exe') {
|
||||
cb?.(null, powershellOutput, '');
|
||||
|
||||
Loading…
Reference in New Issue
Block a user