ipsambeatae
Version:
Shared dependencies of Compass, the MongoDB extension for VSCode and MongoSH
207 lines (194 loc) • 6.09 kB
text/typescript
/* eslint-disable no-console */
import yargs from 'yargs';
import { MongoCluster } from './mongocluster';
import os from 'os';
import path from 'path';
import { promises as fs } from 'fs';
import { BSON } from 'mongodb';
import { spawn } from 'child_process';
import createDebug from 'debug';
import { once } from 'events';
interface StoredInstance {
id: string;
filepath: string;
serialized: string;
connectionString: string;
}
(async function () {
const defaultRunnerDir = path.join(os.homedir(), '.mongodb', 'runner2');
const argv = await yargs
.version(false)
.scriptName('mongodb-runner')
.env('MONGODB_RUNNER')
.option('topology', {
alias: 't',
choices: ['standalone', 'replset', 'sharded'] as const,
default: 'standalone' as const,
describe: 'Select a topology type',
})
.option('arbiters', {
type: 'number',
describe: 'number of arbiter nodes for each replica set',
})
.option('secondaries', {
type: 'number',
describe: 'number of secondaries for each replica set',
})
.option('shards', {
type: 'number',
describe: 'number of shards for sharded clusters',
})
.option('version', {
type: 'string',
describe: 'MongoDB server version to use',
})
.option('logDir', {
type: 'string',
describe: 'Directory to store server log files in',
})
.option('binDir', {
type: 'string',
describe: 'Directory containing mongod/mongos binaries',
})
.option('tmpDir', {
type: 'string',
default: os.tmpdir(),
describe: 'Directory for temporary files',
})
.option('runnerDir', {
type: 'string',
default: defaultRunnerDir,
describe: 'Directory for storing cluster metadata',
})
.option('docker', {
type: 'string',
describe: 'Docker image name to run server instances under',
})
.option('id', {
type: 'string',
describe: 'ID to save the cluster metadata under',
})
.option('all', {
type: 'boolean',
describe: 'for `stop`: stop all clusters',
})
.option('debug', { type: 'boolean', describe: 'Enable debug output' })
.command('start', 'Start a MongoDB instance')
.command('stop', 'Stop a MongoDB instance')
.command('ls', 'List currently running MongoDB instances')
.command(
'exec',
'Run a process with a MongoDB instance (as MONGODB_URI env var)'
)
.demandCommand(1, 'A command needs to be provided')
.help().argv;
const [command, ...args] = argv._.map(String);
if (argv.debug) {
createDebug.enable('mongodb-runner');
}
async function start() {
const id = argv.id || new BSON.UUID().toHexString();
if (!/^[a-zA-Z0-9_-]+$/.test(id)) {
throw new Error(`ID '${id}' contains non-alphanumeric characters`);
}
await fs.mkdir(argv.runnerDir, { recursive: true });
const cluster = await MongoCluster.start({
...argv,
args,
});
const serialized = await cluster.serialize();
const { connectionString } = cluster;
await fs.writeFile(
path.join(argv.runnerDir, `m-${id}.json`),
JSON.stringify({ id, serialized, connectionString })
);
console.log(`Server started and running at ${connectionString}`);
console.log('Run the following command to stop the instance:');
console.log(
`${argv.$0} stop --id=${id}` +
(argv.runnerDir !== defaultRunnerDir
? `--runnerDir=${argv.runnerDir}`
: '')
);
cluster.unref();
}
async function* instances(): AsyncIterable<StoredInstance> {
for await (const { name } of await fs.opendir(argv.runnerDir)) {
if (name.startsWith('m-') && name.endsWith('.json')) {
try {
const filepath = path.join(argv.runnerDir, name);
const stored = JSON.parse(await fs.readFile(filepath, 'utf8'));
yield { ...stored, filepath };
} catch {
/* ignore */
}
}
}
}
async function stop() {
if (!argv.id && !argv.all) {
throw new Error('Need --id or --all to know which server to stop');
}
const toStop: Array<StoredInstance> = [];
for await (const instance of instances()) {
if (instance.id === argv.id || argv.all) toStop.push(instance);
}
await Promise.all(
toStop.map(async ({ id, filepath, serialized, connectionString }) => {
await (await MongoCluster.deserialize(serialized)).close();
await fs.rm(filepath);
console.log(
`Stopped cluster '${id}' (was running at '${connectionString}')`
);
})
);
}
async function ls() {
for await (const { id, connectionString } of instances()) {
console.log(`${id}: ${connectionString}`);
}
}
async function exec() {
let mongodArgs: string[];
let execArgs: string[];
const doubleDashIndex = args.indexOf('--');
if (doubleDashIndex !== -1) {
mongodArgs = args.slice(0, doubleDashIndex);
execArgs = args.slice(doubleDashIndex + 1);
} else {
mongodArgs = [];
execArgs = args;
}
const cluster = await MongoCluster.start({
...argv,
args: mongodArgs,
});
try {
const [prog, ...progArgs] = execArgs;
const child = spawn(prog, progArgs, {
stdio: 'inherit',
env: {
...process.env,
// both spellings since otherwise I'd end up misspelling these half of the time
MONGODB_URI: cluster.connectionString,
MONGODB_URL: cluster.connectionString,
MONGODB_HOSTPORT: cluster.hostport,
},
});
[process.exitCode] = await once(child, 'exit');
} finally {
await cluster.close();
}
}
// eslint-disable-next-line @typescript-eslint/require-await
async function unknown() {
throw new Error(
`Unknown command: ${command}. See '${argv.$0} --help' for more information.`
);
}
await ({ start, stop, exec, ls }[command] ?? unknown)();
})().catch((err) => {
process.nextTick(() => {
throw err;
});
});