Skip to content

Commit 406096a

Browse files
dorlugasigalCopilot
andcommitted
fix(windows): hide devtunnel console windows and fix DEP0190 deprecation
Add windowsHide: true to all child_process calls in tunnel and service modules to prevent console windows from flashing on Windows. Replace shell: true with cmd.exe /c pattern in PM2 commands to fix Node.js DEP0190 deprecation warning about unescaped shell args. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent 5e3d7f8 commit 406096a

4 files changed

Lines changed: 78 additions & 18 deletions

File tree

src/cli/service.js

Lines changed: 19 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ function findPm2() {
3232
encoding: 'utf8',
3333
stdio: ['pipe', 'pipe', 'ignore'],
3434
timeout: 5000,
35+
windowsHide: true,
3536
});
3637
return result.trim().split('\n')[0].trim();
3738
} catch {
@@ -43,10 +44,13 @@ function installPm2Global() {
4344
log.info('Installing PM2 globally');
4445
console.log(yellow('\nInstalling PM2 globally...'));
4546
try {
46-
execFileSync('npm', ['install', '-g', 'pm2'], {
47+
const isWin = os.platform() === 'win32';
48+
const cmd = isWin ? process.env.ComSpec || 'cmd.exe' : 'npm';
49+
const cmdArgs = isWin ? ['/c', 'npm', 'install', '-g', 'pm2'] : ['install', '-g', 'pm2'];
50+
execFileSync(cmd, cmdArgs, {
4751
stdio: 'inherit',
4852
timeout: 120000,
49-
shell: os.platform() === 'win32',
53+
windowsHide: true,
5054
});
5155
console.log(green('✓ PM2 installed successfully.\n'));
5256
return true;
@@ -141,13 +145,17 @@ function readEcosystemName() {
141145

142146
function pm2Exec(args, opts = {}) {
143147
log.debug(`PM2 command: pm2 ${args.join(' ')}`);
148+
const isWin = os.platform() === 'win32';
149+
// Windows npm globals are .cmd wrappers — use cmd.exe /c to resolve them
150+
// without shell:true (which triggers DEP0190 when combined with args).
151+
const cmd = isWin ? process.env.ComSpec || 'cmd.exe' : 'pm2';
152+
const cmdArgs = isWin ? ['/c', 'pm2', ...args] : args;
144153
try {
145-
return execFileSync('pm2', args, {
154+
return execFileSync(cmd, cmdArgs, {
146155
encoding: 'utf8',
147156
stdio: opts.inherit ? 'inherit' : ['pipe', 'pipe', 'pipe'],
148157
timeout: 30000,
149-
// Windows npm globals are .cmd wrappers; execFileSync needs shell to resolve them
150-
shell: os.platform() === 'win32',
158+
windowsHide: true,
151159
...opts,
152160
});
153161
} catch (err) {
@@ -665,9 +673,13 @@ function actionLogs() {
665673
process.exit(1);
666674
}
667675
const { spawn } = require('child_process');
668-
const child = spawn('pm2', ['logs', readEcosystemName(), '--lines', '200'], {
676+
const isWin = os.platform() === 'win32';
677+
const cmd = isWin ? process.env.ComSpec || 'cmd.exe' : 'pm2';
678+
const logsArgs = ['logs', readEcosystemName(), '--lines', '200'];
679+
const cmdArgs = isWin ? ['/c', 'pm2', ...logsArgs] : logsArgs;
680+
const child = spawn(cmd, cmdArgs, {
669681
stdio: 'inherit',
670-
shell: os.platform() === 'win32',
682+
windowsHide: true,
671683
});
672684
child.on('error', (err) => {
673685
console.error(red(`✗ Failed to stream logs: ${err.message}`));

src/tunnel/index.js

Lines changed: 24 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,7 @@ function isLoggedIn() {
106106
encoding: 'utf-8',
107107
stdio: ['pipe', 'pipe', 'pipe'],
108108
timeout: 10_000,
109+
windowsHide: true,
109110
});
110111
return out && !out.toLowerCase().includes('not logged in');
111112
} catch {
@@ -119,6 +120,7 @@ function getLoginInfo() {
119120
encoding: 'utf-8',
120121
stdio: ['pipe', 'pipe', 'pipe'],
121122
timeout: 10_000,
123+
windowsHide: true,
122124
});
123125
return parseLoginInfo(out);
124126
} catch {
@@ -148,6 +150,7 @@ function deviceCodeLogin(cmd) {
148150
return new Promise((resolve, reject) => {
149151
const proc = spawn(cmd, ['user', 'login', '-e', '-d'], {
150152
stdio: ['inherit', 'pipe', 'pipe'],
153+
windowsHide: true,
151154
});
152155

153156
let gotOutput = false;
@@ -197,7 +200,7 @@ function deviceCodeLogin(cmd) {
197200
function findDevtunnel() {
198201
// Try devtunnel directly
199202
try {
200-
execSync('devtunnel --version', { stdio: 'pipe' });
203+
execSync('devtunnel --version', { stdio: 'pipe', windowsHide: true });
201204
return 'devtunnel';
202205
} catch {}
203206

@@ -222,7 +225,7 @@ function findDevtunnel() {
222225
);
223226
if (fs.existsSync(homeBin)) {
224227
try {
225-
execFileSync(homeBin, ['--version'], { stdio: 'pipe' });
228+
execFileSync(homeBin, ['--version'], { stdio: 'pipe', windowsHide: true });
226229
return homeBin;
227230
} catch {}
228231
}
@@ -253,6 +256,7 @@ function isTunnelValid(id) {
253256
execFileSync(devtunnelCmd, ['show', id, '--json'], {
254257
encoding: 'utf-8',
255258
stdio: ['pipe', 'pipe', 'pipe'],
259+
windowsHide: true,
256260
});
257261
return true;
258262
} catch {
@@ -273,7 +277,7 @@ function checkTunnelHealth() {
273277
execFile(
274278
devtunnelCmd,
275279
['show', tunnelId],
276-
{ encoding: 'utf-8', signal: abortCtrl.signal },
280+
{ encoding: 'utf-8', signal: abortCtrl.signal, windowsHide: true },
277281
(err, stdout) => {
278282
clearTimeout(timer);
279283

@@ -401,6 +405,7 @@ function killTunnelProc() {
401405
execFileSync('taskkill', ['/pid', String(tunnelProc.pid), '/T', '/F'], {
402406
stdio: 'pipe',
403407
timeout: 5000,
408+
windowsHide: true,
404409
});
405410
} catch {
406411
/* best effort */
@@ -566,6 +571,7 @@ function scheduleRestart() {
566571
function hostTunnel() {
567572
const hostProc = spawn(devtunnelCmd, ['host', tunnelId], {
568573
stdio: ['pipe', 'pipe', 'pipe'],
574+
windowsHide: true,
569575
});
570576
tunnelProc = hostProc;
571577

@@ -657,7 +663,11 @@ async function startTunnel(port, options = {}) {
657663
if (!loggedIn) {
658664
log.info('Logging in to DevTunnel with Microsoft Entra (recommended for long sessions)...');
659665
try {
660-
execFileSync(devtunnelCmd, ['user', 'login', '-e'], { stdio: 'inherit', timeout: 30000 });
666+
execFileSync(devtunnelCmd, ['user', 'login', '-e'], {
667+
stdio: 'inherit',
668+
timeout: 30000,
669+
windowsHide: true,
670+
});
661671
} catch {
662672
log.info('Browser login failed or unavailable, falling back to device code flow...');
663673
log.info('A code will be displayed — open the URL on any device to authenticate.');
@@ -694,6 +704,7 @@ async function startTunnel(port, options = {}) {
694704
}
695705
const createOut = execFileSync(devtunnelCmd, ['create', '--expiration', '30d', '--json'], {
696706
encoding: 'utf-8',
707+
windowsHide: true,
697708
});
698709
const tunnelData = JSON.parse(createOut);
699710
tunnelId = tunnelData.tunnel.tunnelId;
@@ -706,6 +717,7 @@ async function startTunnel(port, options = {}) {
706717
// Ephemeral tunnel — create fresh, will be deleted on shutdown
707718
const createOut = execFileSync(devtunnelCmd, ['create', '--expiration', '1d', '--json'], {
708719
encoding: 'utf-8',
720+
windowsHide: true,
709721
});
710722
const tunnelData = JSON.parse(createOut);
711723
tunnelId = tunnelData.tunnel.tunnelId;
@@ -717,7 +729,7 @@ async function startTunnel(port, options = {}) {
717729
execFileSync(
718730
devtunnelCmd,
719731
['port', 'create', tunnelId, '-p', String(port), '--protocol', 'http'],
720-
{ stdio: 'pipe' },
732+
{ stdio: 'pipe', windowsHide: true },
721733
);
722734
} catch {}
723735
// Set tunnel access: public (anonymous) or private (owner-only via Microsoft login)
@@ -726,7 +738,7 @@ async function startTunnel(port, options = {}) {
726738
execFileSync(
727739
devtunnelCmd,
728740
['access', 'create', tunnelId, '-p', String(port), '--anonymous'],
729-
{ stdio: 'pipe' },
741+
{ stdio: 'pipe', windowsHide: true },
730742
);
731743
} catch {}
732744
log.info('Tunnel access: public (anonymous)');
@@ -735,6 +747,7 @@ async function startTunnel(port, options = {}) {
735747
try {
736748
execFileSync(devtunnelCmd, ['access', 'reset', tunnelId], {
737749
stdio: 'pipe',
750+
windowsHide: true,
738751
});
739752
} catch {}
740753
log.info('Tunnel access: private (owner-only via Microsoft login)');
@@ -773,7 +786,11 @@ function cleanupTunnel() {
773786
log.info('Tunnel host stopped (tunnel preserved for reuse)');
774787
} else {
775788
try {
776-
execFileSync(devtunnelCmd, ['delete', id, '-f'], { stdio: 'pipe', timeout: 10000 });
789+
execFileSync(devtunnelCmd, ['delete', id, '-f'], {
790+
stdio: 'pipe',
791+
timeout: 10000,
792+
windowsHide: true,
793+
});
777794
log.info('Tunnel cleaned up');
778795
} catch {
779796
/* best effort — tunnel will expire on its own */

src/tunnel/install.js

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -98,7 +98,7 @@ async function installDevtunnel() {
9898
function findInstalledBinary() {
9999
// Check PATH first
100100
try {
101-
execSync('devtunnel --version', { stdio: 'pipe', timeout: 10000 });
101+
execSync('devtunnel --version', { stdio: 'pipe', timeout: 10000, windowsHide: true });
102102
return 'devtunnel';
103103
} catch {}
104104

@@ -110,6 +110,7 @@ function findInstalledBinary() {
110110
encoding: 'utf-8',
111111
stdio: 'pipe',
112112
timeout: 10000,
113+
windowsHide: true,
113114
})
114115
.trim()
115116
.split(/\r?\n/)[0];
@@ -130,7 +131,7 @@ function findInstalledBinary() {
130131
const homeBin = path.join(os.homedir(), 'bin', getBinaryName());
131132
if (fs.existsSync(homeBin)) {
132133
try {
133-
execFileSync(homeBin, ['--version'], { stdio: 'pipe', timeout: 10000 });
134+
execFileSync(homeBin, ['--version'], { stdio: 'pipe', timeout: 10000, windowsHide: true });
134135
return homeBin;
135136
} catch {}
136137
}

test/cli/service.test.js

Lines changed: 32 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,20 @@ const _fs = require('fs');
44
const _path = require('path');
55
const os = require('os');
66

7+
// On Windows, pm2Exec/installPm2Global route through cmd.exe /c pm2 ...
8+
// to avoid DEP0190. Normalize so test mocks see (cmd='pm2', args=[...]).
9+
function normalizeCmdExeCall(cmd, args) {
10+
if (
11+
os.platform() === 'win32' &&
12+
(cmd === process.env.ComSpec || cmd === 'cmd.exe') &&
13+
args &&
14+
args[0] === '/c'
15+
) {
16+
return { cmd: args[1], args: args.slice(2) };
17+
}
18+
return { cmd, args };
19+
}
20+
721
// We test the pure/exported functions — not the interactive prompts
822
const {
923
buildArgs,
@@ -203,7 +217,23 @@ function loadServiceWithMocks(mocks = {}) {
203217

204218
const mockModules = {};
205219
if (mocks.childProcess) {
206-
mockModules['child_process'] = { ...require('child_process'), ...mocks.childProcess };
220+
const cp = { ...require('child_process'), ...mocks.childProcess };
221+
// Wrap execFileSync/spawn mocks to normalize cmd.exe /c calls on Windows
222+
if (mocks.childProcess.execFileSync) {
223+
const orig = mocks.childProcess.execFileSync;
224+
cp.execFileSync = (cmd, args, opts) => {
225+
const n = normalizeCmdExeCall(cmd, args);
226+
return orig(n.cmd, n.args, opts);
227+
};
228+
}
229+
if (mocks.childProcess.spawn) {
230+
const orig = mocks.childProcess.spawn;
231+
cp.spawn = (cmd, args, opts) => {
232+
const n = normalizeCmdExeCall(cmd, args);
233+
return orig(n.cmd, n.args, opts);
234+
};
235+
}
236+
mockModules['child_process'] = cp;
207237
}
208238
if (mocks.fs) {
209239
mockModules['fs'] = { ...require('fs'), ...mocks.fs };
@@ -520,7 +550,7 @@ describe('actionLogs', () => {
520550
assert.deepStrictEqual(spawnCalls[0].args, ['logs', 'termbeam', '--lines', '200']);
521551
assert.deepStrictEqual(spawnCalls[0].opts, {
522552
stdio: 'inherit',
523-
shell: os.platform() === 'win32',
553+
windowsHide: true,
524554
});
525555
});
526556

0 commit comments

Comments
 (0)