packages/environment/chain.js
#!/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();