trufflesuite/truffle

View on GitHub
packages/environment/develop.js

Summary

Maintainability
A
45 mins
Test Coverage
const { IPC } = require("@achrinza/node-ipc");
const path = require("path");
const { spawn } = require("child_process");
const debug = require("debug");
const chalk = require("chalk");

const Develop = {
  start: async function (ipcNetwork, ganacheOptions = {}) {
    let chainPath;

    // The path to the dev env process depends on whether or not
    // we're running in the bundled version. If not, use chain.js
    // directly, otherwise let the bundle point at the bundled version.
    if (typeof BUNDLE_CHAIN_FILENAME !== "undefined") {
      // Remember: In the bundled version, __dirname refers to the
      // build directory where cli.bundled.js and cli.chain.js live.
      chainPath = path.join(__dirname, BUNDLE_CHAIN_FILENAME);
    } else {
      chainPath = path.join(__dirname, "./", "chain.js");
    }

    const logger = ganacheOptions.logger || console;
    //check that genesis-time config option passed through the
    //truffle-config.js file is a valid time.
    if (ganacheOptions.time && isNaN(Date.parse(ganacheOptions.time))) {
      ganacheOptions.time = Date.now();
      logger.log(
        "\x1b[31m%s\x1b[0m",
        "Invalid Date passed to genesis-time, using current Date instead",
        "\x1b[0m"
      );
    }

    const stringifiedOptions = JSON.stringify(ganacheOptions);
    const optionsBuffer = Buffer.from(stringifiedOptions);
    const base64OptionsString = optionsBuffer.toString("base64");

    return spawn("node", [chainPath, ipcNetwork, base64OptionsString], {
      detached: true,
      stdio: "ignore"
    });
  },

  /**
   * Connect to an existing Ganache server or start a new one.
   * @param {object} options
   * @param {object} options.ipcOptions - options for IPC connection
   * @param {boolean} options.ipcOptions.log - whether to log IPC messages. Defaults to false.
   * @param {string} options.ipcOptions.network - network name. Defaults to "develop".
   * @param {boolean} options.ipcOptions.retry - whether to retry connection. Defaults to false.
   * @param {string} options.solidityLogDisplayPrefix - prefix to display before solidity log messages. Defaults to "".
   * @returns {Promise<(): void>} - IPC disconnection function.
   */
  connect: function ({ ipcOptions, solidityLogDisplayPrefix }) {
    const debugServer = debug("develop:ipc:server");
    const debugClient = debug("develop:ipc:client");
    const debugRPC = debug("develop:ganache");
    const ganacheColor = {
      hex: "#ffaf5f", // ganache's color in hex
      xterm: 215 // Xterm's number equivalent
    };
    debugRPC.color = ganacheColor.xterm;

    ipcOptions.retry = ipcOptions.retry || false;
    ipcOptions.log = ipcOptions.log || false;
    ipcOptions.network = ipcOptions.network || "develop";
    solidityLogDisplayPrefix = solidityLogDisplayPrefix || "";
    var ipcNetwork = ipcOptions.network;

    var ipc = new IPC();
    ipc.config.appspace = "truffle.";

    // set connectPath explicitly
    var dirname = ipc.config.socketRoot;
    var basename = `${ipc.config.appspace}${ipcNetwork}`;
    var connectPath = path.join(dirname, basename);
    var loggers = {};

    ipc.config.silent = !debugClient.enabled;
    ipc.config.logger = debugClient;

    const sanitizeAndCallFn =
      fn =>
      (...args) => {
        // HACK-y: replace `{}` that is getting logged instead of ""
        if (
          args.length === 1 &&
          typeof args[0] === "object" &&
          Object.keys(args[0]).length === 0
        ) {
          args[0] = "";
        }
        fn.apply(undefined, args);
      };

    if (debugServer.enabled) {
      loggers.ipc = debugServer;
    }

    // create a logger to present Ganache's console log messages
    const createSolidityLogger = prefix => {
      return maybeMultipleLines =>
        maybeMultipleLines.split("\n").forEach(
          // decorate each line's prefix.
          line => console.log(chalk.hex(ganacheColor.hex)(` ${prefix}`), line)
        );
    };

    // enable output/logger for solidity console.log
    loggers.solidity = sanitizeAndCallFn(
      createSolidityLogger(solidityLogDisplayPrefix)
    );

    if (ipcOptions.log) {
      debugRPC.enabled = true;
      loggers.ganache = sanitizeAndCallFn(debugRPC);
    }

    if (!ipcOptions.retry) {
      ipc.config.maxRetries = 0;
    }

    var disconnect = function () {
      ipc.disconnect(ipcNetwork);
    };

    return new Promise((resolve, reject) => {
      ipc.connectTo(ipcNetwork, connectPath, function () {
        ipc.of[ipcNetwork].on("destroy", function () {
          reject(new Error("IPC connection destroyed"));
        });

        ipc.of[ipcNetwork].on("truffle.ready", function () {
          resolve(disconnect);
        });

        Object.keys(loggers).forEach(function (key) {
          var log = loggers[key];
          if (log) {
            var message = `truffle.${key}.log`;
            ipc.of[ipcNetwork].on(message, log);
          }
        });
      });
    });
  },

  /**
   * Connect to a managed Ganache service. This will connect to an existing
   * Ganache service if one exists, or, create a new one to connect to.
   *
   * @param {Object} ipcOptions - IPC connection options.
   * @param {string} ipcOptions.network - the network name.
   * @param {Object} ganacheOptions - Ganache options if service is necessary.
   * @param {string} solidityLogDisplayPrefix - solidity log messages prefix.
   * @returns {Promise<Object>} - object with `disconnect` function and
   *     `started` boolean. The `disconnect` function is used to disconnect
   *     from the Ganache service. The `started` boolean is true if a new
   *     Ganache service was started, false otherwise.
   */
  connectOrStart: async function (
    ipcOptions,
    ganacheOptions,
    solidityLogDisplayPrefix = ""
  ) {
    ipcOptions.retry = false;

    const ipcNetwork = ipcOptions.network || "develop";

    let started = false;
    let disconnect;

    try {
      disconnect = await this.connect({ ipcOptions, solidityLogDisplayPrefix });
    } catch (_error) {
      await this.start(ipcNetwork, ganacheOptions);
      ipcOptions.retry = true;
      disconnect = await this.connect({ ipcOptions, solidityLogDisplayPrefix });
      started = true;
    } finally {
      return {
        disconnect,
        started
      };
    }
  }
};

module.exports = Develop;