trufflesuite/truffle

View on GitHub
packages/environment/chain.js

Summary

Maintainability
A
0 mins
Test Coverage
#!/usr/bin/env node
require("source-map-support/register");

const IPC = require("@achrinza/node-ipc").IPC;
const Ganache = require("ganache");
const path = require("path");
const debug = require("debug");
const util = require("util");

/*
 * Loggers
 */
const ipcDebug = debug("chain:ipc");

/*
 * Options
 */

// This script is expected to take two arguments: The first a networkName string,
// the second an options string encoded as base64.
// The options string is decoded, parsed, & then passed to Ganache.server().
const args = process.argv.slice(2);
const ipcNetwork = args[0];
const base64OptionsString = args[1];
const optionsBuffer = Buffer.from(base64OptionsString, "base64");
let optionsString = optionsBuffer.toString();
let options;

try {
  options = JSON.parse(optionsString);
} catch (e) {
  throw new Error(
    "Fatal: Error parsing arguments; please contact the Truffle developers for help."
  );
}

options.time = options.time ? new Date(options.time) : new Date();

/*
 * Logging
 */

// constructor
class Logger {
  constructor() {
    this.messages = [];

    this.nextSubscriberID = 1;
    this.subscribers = {};
  }

  // subscribe to log events with provided callback
  // sends prior unsent messages, as well as new messages
  // returns `unsubscribe` cleanup function
  subscribe(callback) {
    // flush messages
    const messages = this.messages;
    this.messages = [];
    messages.forEach(message => {
      callback(message);
    });

    // save subscriber
    const subscriberID = this.nextSubscriberID++;
    this.subscribers[subscriberID] = callback;

    // return cleanup func
    const unsubscribe = () => {
      delete this.subscribers[subscriberID];
    };

    return unsubscribe;
  }

  // log a message to be sent to all active subscribers
  // buffers if there are no active subscribers (to send on first subscribe)
  log(...messages) {
    const subscriberIDs = Object.keys(this.subscribers);
    const formattedMessages = util.formatWithOptions(
      { colors: true },
      ...messages
    );
    if (subscriberIDs.length === 0) {
      this.messages.push(formattedMessages);
      return;
    }

    subscriberIDs.forEach(subscriberID => {
      const callback = this.subscribers[subscriberID];
      callback(formattedMessages);
    });
  }
}

/*
 * Supervisor
 */

// constructor - accepts an object to assign to `ipc.config`
class Supervisor {
  constructor(ipcConfig) {
    // init IPC
    this.ipc = new IPC();
    // set config
    Object.keys(ipcConfig).forEach(key => {
      this.ipc.config[key] = ipcConfig[key];
    });

    this.mixins = [];
  }

  // include mixin
  use(mixin) {
    this.mixins.push(mixin);
  }

  // dispatch event to all relevant mixins (ones that define `event` method)
  handle(event, args) {
    args = Array.prototype.slice.call(args);

    this.mixins.forEach(mixin => {
      if (mixin[event]) {
        mixin[event].apply(mixin, [this].concat(args));
      }
    });
  }

  // start the IPC server and hook up all the mixins
  start() {
    const self = this;
    const ipc = this.ipc;

    // socket filename
    const dirname = ipc.config.socketRoot;
    const basename = `${ipc.config.appspace}${ipc.config.id}`;
    const servePath = path.join(dirname, basename);

    ipc.serve(servePath, function () {
      self.handle("start", arguments);

      ipc.server.on("connect", function () {
        self.handle("connect", arguments);
      });

      ipc.server.on("socket.disconnected", function () {
        self.handle("disconnect", arguments);
      });
    });

    ipc.server.start();
  }

  // external interface for mixin to emit socket events
  emit(socket, message, data, options = {}) {
    options.silent = options.silent || false;

    // possibly override silent
    const currentlySilent = this.ipc.config.silent;
    if (options.silent) {
      this.ipc.config.silent = true;
    }

    this.ipc.server.emit(socket, message, data);

    // reset
    this.ipc.config.silent = currentlySilent;
  }

  // external interface for mixin to exit
  exit() {
    this.ipc.server.stop();
    this.handle("exit", arguments);
  }
}

/*
 * Lifecycle
 * (quit on last connection)
 */
class LifecycleMixin {
  // start counting active connections
  start(_supervisor) {
    this.connections = 0;
  }

  // increment
  connect(_supervisor) {
    this.connections++;
  }

  // decrement - invoke supervisor exit if no connections remain
  disconnect(supervisor) {
    this.connections--;

    if (this.connections <= 0) {
      supervisor.exit();
    }
  }
}

/*
 * Ganache Server
 */

// constructor - accepts options for Ganache
class GanacheMixin {
  constructor(options, ganacheConsoleLogger) {
    this.ganache = Ganache.server(options);
    this.ganacheConsoleLogger = ganacheConsoleLogger;
  }

  // start Ganache and capture promise that resolves when ready
  start(_supervisor) {
    this.ready = new Promise((accept, reject) => {
      this.ganache.listen(options.port, options.hostname, (err, state) => {
        if (err) {
          reject(err);
        }

        accept(state);
      });
    });
  }

  // wait for Ganache to be ready then emit signal to client socket
  connect(supervisor, socket) {
    this.ready.then(() => {
      supervisor.emit(socket, "truffle.ready");

      // hook into ganache console.log events
      this.ganache.provider.on("ganache:vm:tx:console.log", ({ logs }) => {
        this.ganacheConsoleLogger.log(
          // Format and colorize ganache log data which may or may not
          // include a format string as the first argument.
          util.formatWithOptions({ colors: true }, ...logs)
        );
      });
    });
  }

  // cleanup Ganache process on exit
  exit(_supervisor) {
    this.ganache
      .close()
      .then(() => (process.exitCode = 0))
      .catch(err => {
        console.error(err.stack || err);
        process.exitCode = 1;
      });
  }
}

/*
 * Logging over IPC
 */

// constructor - takes Logger instance and message key (e.g. `truffle.ipc.log`)
class LoggerMixin {
  constructor(logger, message) {
    this.logger = logger;
    this.message = message;
  }

  // on connect, subscribe client socket to logger
  connect(supervisor, socket) {
    const unsubscribe = this.logger.subscribe(data => {
      supervisor.emit(socket, this.message, data, { silent: true });
    });

    socket.on("close", unsubscribe);
  }
}

/*
 * Process event handling
 */
process.on("uncaughtException", ({ stack }) => {
  console.error(stack);
  process.exit(1);
});

/*
 * Main
 */
const ipcLogger = new Logger();
const ganacheLogger = new Logger();
const ganacheConsoleLogger = new Logger();

const supervisor = new Supervisor({
  appspace: "truffle.",
  id: ipcNetwork,
  retry: 1500,
  logger: ipcLogger.log.bind(ipcLogger)
});

ipcLogger.subscribe(ipcDebug);

options.logger = { log: ganacheLogger.log.bind(ganacheLogger) };
ganacheConsoleLogger.log = ganacheConsoleLogger.log.bind(ganacheConsoleLogger);
const ganacheMixin = new GanacheMixin(options, ganacheConsoleLogger);

supervisor.use(new LifecycleMixin());
supervisor.use(ganacheMixin);
supervisor.use(new LoggerMixin(ipcLogger, "truffle.ipc.log"));
supervisor.use(new LoggerMixin(ganacheLogger, "truffle.ganache.log"));
supervisor.use(new LoggerMixin(ganacheConsoleLogger, "truffle.solidity.log"));

supervisor.start();