All files / src/client-side-encryption mongocryptd_manager.ts

96.55% Statements 28/29
100% Branches 21/21
83.33% Functions 5/6
96.55% Lines 28/29

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101    135x             135x 135x       3597x 3597x       3597x         3597x   3597x 4x   3597x       98x   3597x   188x 184x   3585x                 3473x     3473x       3473x         3473x                                 3473x             7478x 7478x 6310x     1168x 1168x 1136x     32x 32x        
import type { ChildProcess } from 'child_process';
 
import { MongoNetworkTimeoutError } from '../error';
import { type AutoEncryptionExtraOptions } from './auto_encrypter';
 
/**
 * @internal
 * An internal class that handles spawning a mongocryptd.
 */
export class MongocryptdManager {
  static DEFAULT_MONGOCRYPTD_URI = 'mongodb://localhost:27020';
 
  uri: string;
  bypassSpawn: boolean;
  spawnPath = '';
  spawnArgs: Array<string> = [];
  _child?: ChildProcess;
 
  constructor(extraOptions: AutoEncryptionExtraOptions = {}) {
    this.uri =
      typeof extraOptions.mongocryptdURI === 'string' && extraOptions.mongocryptdURI.length > 0
        ? extraOptions.mongocryptdURI
        : MongocryptdManager.DEFAULT_MONGOCRYPTD_URI;
 
    this.bypassSpawn = !!extraOptions.mongocryptdBypassSpawn;
 
    if (Object.hasOwn(extraOptions, 'mongocryptdSpawnPath') && extraOptions.mongocryptdSpawnPath) {
      this.spawnPath = extraOptions.mongocryptdSpawnPath;
    }
    if (
      Object.hasOwn(extraOptions, 'mongocryptdSpawnArgs') &&
      Array.isArray(extraOptions.mongocryptdSpawnArgs)
    ) {
      this.spawnArgs = this.spawnArgs.concat(extraOptions.mongocryptdSpawnArgs);
    }
    if (
      this.spawnArgs
        .filter(arg => typeof arg === 'string')
        .every(arg => arg.indexOf('--idleShutdownTimeoutSecs') < 0)
    ) {
      this.spawnArgs.push('--idleShutdownTimeoutSecs', '60');
    }
  }
 
  /**
   * Will check to see if a mongocryptd is up. If it is not up, it will attempt
   * to spawn a mongocryptd in a detached process, and then wait for it to be up.
   */
  async spawn(): Promise<void> {
    const cmdName = this.spawnPath || 'mongocryptd';
 
    // eslint-disable-next-line @typescript-eslint/no-require-imports
    const { spawn } = require('child_process') as typeof import('child_process');
 
    // Spawned with stdio: ignore and detached: true
    // to ensure child can outlive parent.
    this._child = spawn(cmdName, this.spawnArgs, {
      stdio: 'ignore',
      detached: true
    });
 
    this._child.on('error', () => {
      // From the FLE spec:
      // "The stdout and stderr of the spawned process MUST not be exposed in the driver
      // (e.g. redirect to /dev/null). Users can pass the argument --logpath to
      // extraOptions.mongocryptdSpawnArgs if they need to inspect mongocryptd logs.
      // If spawning is necessary, the driver MUST spawn mongocryptd whenever server
      // selection on the MongoClient to mongocryptd fails. If the MongoClient fails to
      // connect after spawning, the server selection error is propagated to the user."
      // The AutoEncrypter and MongoCryptdManager should work together to spawn
      // mongocryptd whenever necessary.  Additionally, the `mongocryptd` intentionally
      // shuts down after 60s and gets respawned when necessary.  We rely on server
      // selection timeouts when connecting to the `mongocryptd` to inform users that something
      // has been configured incorrectly.  For those reasons, we suppress stderr from
      // the `mongocryptd` process and immediately unref the process.
    });
 
    // unref child to remove handle from event loop
    this._child.unref();
  }
 
  /**
   * @returns the result of `fn` or rejects with an error.
   */
  async withRespawn<T>(fn: () => Promise<T>): ReturnType<typeof fn> {
    try {
      const result = await fn();
      return result;
    } catch (err) {
      // If we are not bypassing spawning, then we should retry once on a MongoTimeoutError (server selection error)
      const shouldSpawn = err instanceof MongoNetworkTimeoutError && !this.bypassSpawn;
      if (!shouldSpawn) {
        throw err;
      }
    }
    await this.spawn();
    const result = await fn();
    return result;
  }
}