Skip to content

Commit d579abe

Browse files
authored
feat(cli-repl): ask for connection string on win32 doubleclick spawn MONGOSH-638 (#1057)
1 parent 1543418 commit d579abe

File tree

7 files changed

+144
-12
lines changed

7 files changed

+144
-12
lines changed

packages/build/src/compile/signable-compiler.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,10 @@ export class SignableCompiler {
8181
path: await findModulePath('cli-repl', 'macos-export-certificate-and-key'),
8282
requireRegexp: /\bmacos_export_certificate_and_key\.node$/
8383
} : null;
84+
const winConsoleProcessListAddon = process.platform === 'win32' ? {
85+
path: await findModulePath('cli-repl', 'get-console-process-list'),
86+
requireRegexp: /\bget_console_process_list\.node$/
87+
} : null;
8488

8589
// This compiles the executable along with Node from source.
8690
// Evergreen and XCode don't have up to date libraries to compile
@@ -105,6 +109,8 @@ export class SignableCompiler {
105109
kerberosAddon
106110
].concat(winCAAddon ? [
107111
winCAAddon
112+
] : []).concat(winConsoleProcessListAddon ? [
113+
winConsoleProcessListAddon
108114
] : []).concat(macKeychainAddon ? [
109115
macKeychainAddon
110116
] : []),

packages/cli-repl/package-lock.json

Lines changed: 19 additions & 9 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/cli-repl/package.json

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,8 @@
8181
"moment": "^2.29.1"
8282
},
8383
"optionalDependencies": {
84-
"macos-export-certificate-and-key": "^1.0.1",
85-
"win-export-certificate-and-key": "^1.0.2"
84+
"macos-export-certificate-and-key": "^1.0.2",
85+
"win-export-certificate-and-key": "^1.0.4",
86+
"get-console-process-list": "^1.0.2"
8687
}
8788
}

packages/cli-repl/src/mongosh-repl.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -340,6 +340,7 @@ class MongoshNodeRepl implements EvaluationListener {
340340
// Only start reading from the input *after* we set up everything, including
341341
// internalState.setCtx().
342342
this.lineByLineInput.start();
343+
this.input.resume();
343344
repl.setPrompt(await this.getShellPrompt());
344345
repl.displayPrompt();
345346
}

packages/cli-repl/src/run.spec.ts

Lines changed: 38 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,15 +2,17 @@ import childProcess from 'child_process';
22
import path from 'path';
33
import { promisify } from 'util';
44
import { expect } from 'chai';
5+
import { once } from 'events';
56
const execFile = promisify(childProcess.execFile);
67

78
describe('CLI entry point', () => {
9+
const pathToRun = ['-r', 'ts-node/register', path.resolve(__dirname, 'run.ts')];
810
async function run(args: string[], env?: Record<string, string>): Promise<{ stdout: string, stderr: string }> {
911
// Use ts-node to run the .ts files directly so nyc can pick them up for
1012
// coverage.
1113
return await execFile(
1214
process.execPath,
13-
['-r', 'ts-node/register', path.resolve(__dirname, 'run.ts'), ...args],
15+
[...pathToRun, ...args],
1416
{ env: { ...process.env, ...(env ?? {}) } });
1517
}
1618

@@ -33,4 +35,39 @@ describe('CLI entry point', () => {
3335
const { stdout } = await run([path.resolve(__dirname, '..', 'test', 'fixtures', 'nodescript.js')], { MONGOSH_RUN_NODE_SCRIPT: '1' });
3436
expect(stdout).to.include('works!');
3537
});
38+
39+
it('can load get-console-process-list on Windows', function() {
40+
if (process.platform !== 'win32') {
41+
return this.skip();
42+
}
43+
expect(require('get-console-process-list')).to.be.a('function');
44+
});
45+
46+
it('asks for connection string when configured to do so', async() => {
47+
const proc = childProcess.spawn(process.execPath, pathToRun, {
48+
stdio: 'pipe',
49+
env: { ...process.env, MONGOSH_FORCE_CONNECTION_STRING_PROMPT: '1' }
50+
});
51+
let stdout = '';
52+
let stderr = '';
53+
let wroteConnectionString = false;
54+
proc.stdout.setEncoding('utf8').on('data', (chunk) => {
55+
stdout += chunk;
56+
if (!wroteConnectionString &&
57+
stdout.includes('Please enter a MongoDB connection string')) {
58+
proc.stdin.write('/\n');
59+
wroteConnectionString = true;
60+
}
61+
if (stdout.includes('Press any key to exit')) {
62+
proc.stdin.write('x');
63+
}
64+
});
65+
proc.stderr.setEncoding('utf8').on('data', (chunk) => {
66+
stderr += chunk;
67+
});
68+
const [code] = await once(proc, 'exit');
69+
expect(code).to.equal(1);
70+
expect(stdout).to.include('Press any key to exit');
71+
expect(stderr).to.include('MongoshInvalidInputError: [COMMON-10001] Invalid URI: /');
72+
});
3673
});

packages/cli-repl/src/run.ts

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,9 @@ import { CliRepl, parseCliArgs, mapCliToDriver, getStoragePaths, getMongocryptdP
22
import { generateUri } from '@mongosh/service-provider-server';
33
import { redactURICredentials } from '@mongosh/history';
44
import { runMain } from 'module';
5+
import readline from 'readline';
6+
import askcharacter from 'askcharacter';
7+
import stream from 'stream';
58

69
// eslint-disable-next-line complexity, @typescript-eslint/no-floating-promises
710
(async() => {
@@ -15,6 +18,7 @@ import { runMain } from 'module';
1518
}
1619

1720
let repl;
21+
let isSingleConsoleProcess = false;
1822
try {
1923
const options = parseCliArgs(process.argv);
2024
const { version } = require('../package.json');
@@ -53,6 +57,25 @@ import { runMain } from 'module';
5357
process.removeAllListeners('SIGINT');
5458
}
5559

60+
// If we are spawned via Windows doubleclick, ask the user for an URI to
61+
// connect to. Allow an environment variable to override this for testing.
62+
isSingleConsoleProcess = !!process.env.MONGOSH_FORCE_CONNECTION_STRING_PROMPT;
63+
if ((!options.connectionSpecifier &&
64+
process.platform === 'win32' &&
65+
process.stdin.isTTY &&
66+
process.stdout.isTTY) ||
67+
isSingleConsoleProcess) {
68+
try {
69+
isSingleConsoleProcess ||= require('get-console-process-list')().length === 1;
70+
} catch { /* ignore */ }
71+
if (isSingleConsoleProcess) {
72+
const result = await ask('Please enter a MongoDB connection string (Default: mongodb://localhost/): ');
73+
if (result.trim() !== '') {
74+
options.connectionSpecifier = result.trim();
75+
}
76+
}
77+
}
78+
5679
const driverOptions = mapCliToDriver(options);
5780
const driverUri = generateUri(options);
5881

@@ -79,6 +102,14 @@ import { runMain } from 'module';
79102
if (repl !== undefined) {
80103
repl.bus.emit('mongosh:error', e);
81104
}
105+
if (isSingleConsoleProcess) {
106+
// In single-process-console mode, it's confusing for the window to be
107+
// closed immediately after receiving an error. In that case, ask the
108+
// user to explicitly close the window.
109+
process.stdout.write('Press any key to exit: ');
110+
await askcharacter({ input: process.stdin, output: process.stdout });
111+
process.stdout.write('\n');
112+
}
82113
process.exit(1);
83114
}
84115
})();
@@ -96,3 +127,22 @@ function setTerminalWindowTitle(title: string): void {
96127
process.stdout.write(`\u001bk${title}\u001b\\`);
97128
}
98129
}
130+
131+
async function ask(prompt: string): Promise<string> {
132+
const stdinCopy = process.stdin.pipe(new stream.PassThrough());
133+
try {
134+
const readlineInterface = readline.createInterface({
135+
input: stdinCopy,
136+
output: process.stdout,
137+
prompt,
138+
});
139+
readlineInterface.prompt();
140+
// for-await automatically reads input lines + closes the readline instance again
141+
for await (const line of readlineInterface) {
142+
return line;
143+
}
144+
return ''; // Unreachable
145+
} finally {
146+
process.stdin.unpipe(stdinCopy);
147+
}
148+
}

packages/cli-repl/test/e2e.spec.ts

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1092,5 +1092,32 @@ describe('e2e', function() {
10921092
});
10931093
});
10941094
});
1095+
1096+
describe('ask-for-connection-string mode', () => {
1097+
let shell: TestShell;
1098+
1099+
beforeEach(() => {
1100+
shell = TestShell.start({
1101+
args: [],
1102+
env: { ...process.env, MONGOSH_FORCE_CONNECTION_STRING_PROMPT: '1' },
1103+
forceTerminal: true
1104+
});
1105+
});
1106+
1107+
it('allows connecting to a host and running commands against it', async() => {
1108+
const connectionString = await testServer.connectionString();
1109+
await eventually(() => {
1110+
shell.assertContainsOutput('Please enter a MongoDB connection string');
1111+
});
1112+
shell.writeInputLine(connectionString);
1113+
await shell.waitForPrompt();
1114+
1115+
expect(await shell.executeLine('db.runCommand({whatsmyuri:1})')).to.include('you:');
1116+
1117+
shell.writeInputLine('exit');
1118+
await shell.waitForExit();
1119+
shell.assertNoErrors();
1120+
});
1121+
});
10951122
});
10961123

0 commit comments

Comments
 (0)