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    396x             396x 396x       3903x 3903x       3903x         3903x   3903x 4x   3903x       401x   3903x   794x 790x   3891x                 3643x     3643x       3643x         3643x                                 3643x             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;
  }
}