Skip to content

Commit ca6469c

Browse files
committed
fix(stdio): quiet existing WDIO loggers
1 parent 5309ffc commit ca6469c

6 files changed

Lines changed: 135 additions & 3 deletions

File tree

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,7 @@
7070
"dependencies": {
7171
"@appium/support": "^7.0.2",
7272
"@modelcontextprotocol/sdk": "^1.22.0",
73+
"@wdio/logger": "^9.29.1",
7374
"@xmldom/xmldom": "^0.9.8",
7475
"appium-adb": "^16.0.0",
7576
"appium-ios-device": "^3.1.0",

src/logger.ts

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import {logger} from '@appium/support';
22

33
import {isStdioTransportLoggingConfigured, markStdioTransportLoggingConfigured} from './stdio-logging-state.js';
4+
import {quietExistingWdioLoggers} from './utils/wdio-logging.js';
45

56
const log = logger.getLogger('appium-mcp');
67

@@ -20,9 +21,7 @@ export function configureStdioTransportLogging(): void {
2021
markStdioTransportLoggingConfigured();
2122
ensureLoggerWritesToStderr();
2223
log.level = 'warn';
23-
if (!process.env.WDIO_LOG_LEVEL) {
24-
process.env.WDIO_LOG_LEVEL = 'warn';
25-
}
24+
quietExistingWdioLoggers();
2625
}
2726

2827
export default log;
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
import http from 'node:http';
2+
3+
import {createAppiumMcpServer} from '../../core.js';
4+
import {attachToRemoteSession} from '../../utils/url.js';
5+
6+
const mockServer = http.createServer((_req, res) => {
7+
res.writeHead(200, {'Content-Type': 'application/json; charset=utf-8'});
8+
res.end(
9+
JSON.stringify({
10+
value: {
11+
sessionId: 'test-session',
12+
capabilities: {platformName: 'Android'},
13+
},
14+
}),
15+
);
16+
});
17+
18+
await new Promise<void>((resolve) => {
19+
mockServer.listen(0, '127.0.0.1', () => resolve());
20+
});
21+
22+
const {port} = mockServer.address() as {port: number};
23+
const remoteServerUrl = `http://127.0.0.1:${port}/`;
24+
25+
try {
26+
const server = await createAppiumMcpServer();
27+
void server.start({transportType: 'stdio'});
28+
29+
const client = await attachToRemoteSession({
30+
remoteServerUrl,
31+
sessionId: 'test-session',
32+
capabilities: {platformName: 'Android'},
33+
});
34+
35+
await client.deleteSession();
36+
process.stderr.write('child-done\n');
37+
process.exit(0);
38+
} catch (err) {
39+
process.stderr.write(`${err instanceof Error ? err.stack ?? err.message : String(err)}\n`);
40+
process.exit(1);
41+
} finally {
42+
mockServer.close();
43+
}

src/tests/logger.test.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import {afterEach, describe, expect, test} from '@jest/globals';
2+
import wdioLogger from '@wdio/logger';
23

34
import {configureStdioTransportLogging, ensureLoggerWritesToStderr, log} from '../logger.js';
45
import {isStdioTransportLoggingConfigured, markStdioTransportLoggingConfigured} from '../stdio-logging-state.js';
@@ -48,13 +49,20 @@ describe('stdio transport logging', () => {
4849
test('configureStdioTransportLogging quiets info logs and WDIO when unset', () => {
4950
delete process.env.WDIO_LOG_LEVEL;
5051
log.unwrap().stream = process.stdout;
52+
const utilsLogger = wdioLogger('@wdio/utils');
53+
const webdriverLogger = wdioLogger('webdriver');
54+
utilsLogger.setLevel('info');
55+
webdriverLogger.setLevel('info');
56+
const infoLevel = utilsLogger.getLevel();
5157

5258
configureStdioTransportLogging();
5359

5460
expect(isStdioTransportLoggingConfigured()).toBe(true);
5561
expect(log.unwrap().stream).toBe(process.stderr);
5662
expect(log.level).toBe('warn');
5763
expect(process.env.WDIO_LOG_LEVEL).toBe('warn');
64+
expect(utilsLogger.getLevel()).toBeGreaterThan(infoLevel);
65+
expect(webdriverLogger.getLevel()).toBeGreaterThan(infoLevel);
5866
});
5967

6068
test('withQuietWebDriverLogging sets warn after stdio logging is configured', () => {
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
import {spawn} from 'node:child_process';
2+
import {existsSync} from 'node:fs';
3+
import path from 'node:path';
4+
import {fileURLToPath} from 'node:url';
5+
6+
import {describe, expect, test} from '@jest/globals';
7+
8+
const rootDir = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../..');
9+
const fixturePath = path.join(rootDir, 'dist/tests/fixtures/programmatic-stdio-wdio-child.js');
10+
11+
function isJsonRpcLine(line: string): boolean {
12+
if (!line.trim()) {
13+
return true;
14+
}
15+
try {
16+
const message = JSON.parse(line);
17+
return typeof message === 'object' && message !== null && 'jsonrpc' in message;
18+
} catch {
19+
return false;
20+
}
21+
}
22+
23+
describe('programmatic stdio WDIO logging', () => {
24+
test('keeps non-JSON-RPC output off stdout after core stdio start', async () => {
25+
if (!existsSync(fixturePath)) {
26+
throw new Error(`Compiled fixture missing at ${fixturePath}. Run npm run build first.`);
27+
}
28+
29+
const {WDIO_LOG_LEVEL: _wdioLogLevel, ...env} = process.env;
30+
const child = spawn(process.execPath, [fixturePath], {
31+
cwd: rootDir,
32+
env,
33+
stdio: ['ignore', 'pipe', 'pipe'],
34+
});
35+
36+
let stdout = '';
37+
let stderr = '';
38+
child.stdout.on('data', (chunk: Buffer) => {
39+
stdout += chunk.toString();
40+
});
41+
child.stderr.on('data', (chunk: Buffer) => {
42+
stderr += chunk.toString();
43+
});
44+
45+
const exitCode = await new Promise<number>((resolve, reject) => {
46+
const timeout = setTimeout(() => {
47+
child.kill('SIGTERM');
48+
reject(new Error(`child process timed out\nstdout: ${stdout}\nstderr: ${stderr}`));
49+
}, 15_000);
50+
51+
child.on('error', reject);
52+
child.on('close', (code) => {
53+
clearTimeout(timeout);
54+
resolve(code ?? 1);
55+
});
56+
});
57+
58+
expect(exitCode).toBe(0);
59+
expect(stderr).toContain('child-done');
60+
61+
const nonEmptyStdoutLines = stdout.split('\n').filter((line) => line.trim().length > 0);
62+
for (const line of nonEmptyStdoutLines) {
63+
expect(isJsonRpcLine(line)).toBe(true);
64+
}
65+
66+
expect(stdout).not.toMatch(/INFO\s+@wdio\/utils:/);
67+
expect(stdout).not.toMatch(/INFO\s+webdriver:/);
68+
}, 20_000);
69+
});

src/utils/wdio-logging.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import wdioLogger from '@wdio/logger';
2+
3+
import {QUIET_WEBDRIVER_LOG_LEVEL} from './webdriver-client-options.js';
4+
5+
type WdioLogLevel = NonNullable<Parameters<typeof wdioLogger.setLogLevelsConfig>[1]>;
6+
7+
/** Quiet every WDIO logger that was created before stdio config ran. */
8+
export function quietExistingWdioLoggers(
9+
level: WdioLogLevel = (process.env.WDIO_LOG_LEVEL as WdioLogLevel | undefined) ?? QUIET_WEBDRIVER_LOG_LEVEL,
10+
): void {
11+
wdioLogger.setLogLevelsConfig({}, level);
12+
}

0 commit comments

Comments
 (0)