From af7959596e8d1cded4e499540855b5ac5ecc0ca1 Mon Sep 17 00:00:00 2001 From: "Michael Bradley, Jr" Date: Thu, 13 Dec 2018 18:23:57 -0600 Subject: [PATCH] feature(@embark/core): IPC adapts when run in Docker Linux on Win When embark is running on Linux and macOS, unix socket files are used for geth's and embark's IPC files. If the context is a Linux container running on a Windows Docker host, and if the DApp is mounted from the host's file system, there is a problem: unix socket files are incompatible with the Windows file system. Detect the problematic scenario and use a path outside of the mounted directory for embark's and geth's IPC files. To avoid a circular dependency problem, remove the `utils/utils.js` dependency from `core/env.js`, don't have `utils/host.ts` depdend on `core/fs.js`, and implement the `ipcPath()` helper as a static method on the class exported from `core/ipc.js`. --- src/lib/core/env.js | 11 ++-- src/lib/core/ipc.js | 25 +++++++- src/lib/core/processes/processManager.js | 1 - .../modules/blockchain_process/gethClient.js | 5 +- src/lib/modules/blockchain_process/miner.js | 10 +--- src/lib/utils/host.ts | 60 ++++++++++++++----- src/lib/utils/utils.js | 7 +++ 7 files changed, 85 insertions(+), 34 deletions(-) diff --git a/src/lib/core/env.js b/src/lib/core/env.js index 9d59850eb2..6e1bb47586 100644 --- a/src/lib/core/env.js +++ b/src/lib/core/env.js @@ -1,8 +1,7 @@ /* global __dirname module process require */ const {execSync} = require('child_process'); -const {delimiter} = require('path'); -const {joinPath} = require('../utils/utils.js'); +const {delimiter, join} = require('path'); function anchoredValue(anchor, value) { if (!arguments.length) { @@ -39,11 +38,11 @@ const DEFAULT_CMD_HISTORY_SIZE = 20; anchoredValue(CMD_HISTORY_SIZE, DEFAULT_CMD_HISTORY_SIZE); const DIAGRAM_PATH = 'DIAGRAM_PATH'; -const DEFAULT_DIAGRAM_PATH = joinPath(anchoredValue(DAPP_PATH), 'diagram.svg'); +const DEFAULT_DIAGRAM_PATH = join(anchoredValue(DAPP_PATH), 'diagram.svg'); anchoredValue(DIAGRAM_PATH, DEFAULT_DIAGRAM_PATH); const EMBARK_PATH = 'EMBARK_PATH'; -const DEFAULT_EMBARK_PATH = joinPath(__dirname, '../../..'); +const DEFAULT_EMBARK_PATH = join(__dirname, '../../..'); anchoredValue(EMBARK_PATH, DEFAULT_EMBARK_PATH); const PKG_PATH = 'PKG_PATH'; @@ -52,7 +51,7 @@ anchoredValue(PKG_PATH, DEFAULT_PKG_PATH); let YARN_GLOBAL_PATH; try { - YARN_GLOBAL_PATH = joinPath( + YARN_GLOBAL_PATH = join( execSync('yarn global dir', {stdio: ['pipe', 'pipe', 'ignore']}) .toString() .trim(), @@ -65,7 +64,7 @@ try { const NODE_PATH = 'NODE_PATH'; // NOTE: setting NODE_PATH at runtime won't effect lookup behavior in the // current process, but will take effect in child processes -process.env[NODE_PATH] = joinPath(anchoredValue(EMBARK_PATH), 'node_modules') + +process.env[NODE_PATH] = join(anchoredValue(EMBARK_PATH), 'node_modules') + (YARN_GLOBAL_PATH ? delimiter : '') + (YARN_GLOBAL_PATH || '') + (process.env[NODE_PATH] ? delimiter : '') + diff --git a/src/lib/core/ipc.js b/src/lib/core/ipc.js index 8b073935ab..8a9aac451f 100644 --- a/src/lib/core/ipc.js +++ b/src/lib/core/ipc.js @@ -1,17 +1,36 @@ const fs = require('./fs.js'); const ipc = require('node-ipc'); +const {isDappMountedFromWindowsDockerHost} = require('../utils/host'); +const os = require('os'); const {parse, stringify} = require('flatted/cjs'); +const utils = require('../utils/utils.js'); class IPC { - constructor(options) { this.logger = options.logger; - this.socketPath = options.socketPath || fs.dappPath(".embark/embark.ipc"); + this.socketPath = options.socketPath || IPC.ipcPath('embark.ipc'); this.ipcRole = options.ipcRole; ipc.config.silent = true; this.connected = false; } + static ipcPath(basename, usePipePathOnWindows = false) { + if (!(basename && typeof basename === 'string')) { + throw new TypeError('first argument must be a non-empty string'); + } + let ipcDir; + if (process.platform === 'win32' && usePipePathOnWindows) { + return `\\\\.\\pipe\\${basename}`; + } else if (isDappMountedFromWindowsDockerHost) { + ipcDir = utils.joinPath( + os.tmpdir(), `embark-${utils.sha512(fs.dappPath()).slice(0, 8)}` + ); + } else { + ipcDir = fs.dappPath('.embark'); + } + return utils.joinPath(ipcDir, basename); + } + connect(done) { const self = this; function connecting(_socket) { @@ -39,7 +58,7 @@ class IPC { } serve() { - fs.mkdirpSync(fs.dappPath(".embark")); + fs.mkdirpSync(utils.dirname(this.socketPath)); ipc.serve(this.socketPath, () => {}); ipc.server.start(); diff --git a/src/lib/core/processes/processManager.js b/src/lib/core/processes/processManager.js index 4922fa7165..4f314598d3 100644 --- a/src/lib/core/processes/processManager.js +++ b/src/lib/core/processes/processManager.js @@ -1,4 +1,3 @@ - const ProcessState = { Unstarted: 'unstarted', Starting: 'starting', diff --git a/src/lib/modules/blockchain_process/gethClient.js b/src/lib/modules/blockchain_process/gethClient.js index 6e276e64bd..40fe9175f9 100644 --- a/src/lib/modules/blockchain_process/gethClient.js +++ b/src/lib/modules/blockchain_process/gethClient.js @@ -1,5 +1,6 @@ const async = require('async'); const GethMiner = require('./miner'); +const IPC = require('../../core/ipc'); const semver = require('semver'); const constants = require('../../constants'); @@ -50,7 +51,7 @@ class GethClient { needKeepAlive() { // TODO: check version also (geth version < 1.8.15) if (this.isDev) { - // Trigger regular txs due to a bug in geth (< 1.8.15) and stuck transactions in --dev mode. + // Trigger regular txs due to a bug in geth (< 1.8.15) and stuck transactions in --dev mode. return true; } return false; @@ -78,6 +79,8 @@ class GethClient { cmd.push("--verbosity=" + config.verbosity); } + cmd.push(`--ipcpath=${IPC.ipcPath('geth.ipc', true)}`); + return cmd; } diff --git a/src/lib/modules/blockchain_process/miner.js b/src/lib/modules/blockchain_process/miner.js index 79f132b83c..559a5e9e23 100644 --- a/src/lib/modules/blockchain_process/miner.js +++ b/src/lib/modules/blockchain_process/miner.js @@ -1,4 +1,5 @@ const async = require('async'); +const IPC = require('../../core/ipc'); const NetcatClient = require('netcat/client'); //Constants @@ -43,14 +44,7 @@ class GethMiner { } } - const isWin = process.platform === "win32"; - - let ipcPath; - if (isWin) { - ipcPath = '\\\\.\\pipe\\geth.ipc'; - } else { - ipcPath = this.datadir + '/geth.ipc'; - } + const ipcPath = IPC.ipcPath('geth.ipc', true); this.client = new NetcatClient(); this.client.unixSocket(ipcPath) diff --git a/src/lib/utils/host.ts b/src/lib/utils/host.ts index 3e1a1c3795..89b6e2fd1d 100644 --- a/src/lib/utils/host.ts +++ b/src/lib/utils/host.ts @@ -1,30 +1,59 @@ -const isDocker = (() => { - let isDockerProcess; +const {execSync} = require("child_process"); +const {anchoredValue, DAPP_PATH} = require("../core/env"); +const {hostname} = require("os"); + +const dappPath = anchoredValue(DAPP_PATH); - const hostname = require("os").hostname(); - const pattern = new RegExp( - "[0-9]+\:[a-z_-]+\:\/docker\/" + hostname + "[0-9a-z]+", "i", - ); +function subdir(pdir_, dir_) { + let pdir = path.resolve(path.normalize(pdir_)) + (path.sep || "/"); + const dir = path.resolve(pdir, path.normalize(dir_)); + if (pdir === "//") { pdir = "/"; } + if (pdir === dir) { return false; } + return dir.slice(0, pdir.length) === pdir; +} + +const isDocker = (() => { + // assumption: an Embark container is always a Linux Docker container, though + // the Docker host may be Linux, macOS, or Windows + if (process.platform !== "linux") { return false; } + try { + return ( + new RegExp(`[0-9]+\:[a-z_-]+\:\/docker\/${hostname()}[0-9a-z]+`, "i") + ).test( + execSync( + "cat /proc/self/cgroup", + {stdio: ["ignore", "pipe", "ignore"]}, + ).toString(), + ); + } catch (e) { + return false; + } +})(); +const isDappMountedFromWindowsDockerHost = (() => { + if (!isDocker) { return false; } try { - isDockerProcess = require("child_process") - .execSync( - "cat /proc/self/cgroup", + return execSync( + "mount", {stdio: ["ignore", "pipe", "ignore"]}, ) - .toString().match(pattern) !== null; + .toString() + .split("\n") + .filter((line) => /nounix/.test(line)) + .some((line) => { + const mount = line.match(/on (\/.*) type/)[1]; + return mount === dappPath || subdir(mount, dappPath); + }); } catch (e) { - isDockerProcess = false; + return false; } - - return isDockerProcess; })(); const defaultHost = isDocker ? "0.0.0.0" : "localhost"; -// when we"re runing in Docker, we can expect (generally, in a development +// when we're runing in Docker, we can expect (generally, in a development // scenario) that the user would like to connect to the service in the -// container via the **host"s** loopback address, so this helper can be used to +// container via the **host's** loopback address, so this helper can be used to // swap 0.0.0.0 for localhost in code/messages that pertain to client-side function canonicalHost(host: string): string { return isDocker && host === "0.0.0.0" ? "localhost" : host; @@ -41,5 +70,6 @@ export { defaultCorsHost, defaultHost, dockerHostSwap, + isDappMountedFromWindowsDockerHost, isDocker, }; diff --git a/src/lib/utils/utils.js b/src/lib/utils/utils.js index c747377b2a..dbc680be37 100644 --- a/src/lib/utils/utils.js +++ b/src/lib/utils/utils.js @@ -390,6 +390,12 @@ function sha3(arg) { return Web3.utils.sha3(arg); } +function sha512(arg) { + const crypto = require('crypto'); + const hash = crypto.createHash('sha512'); + return hash.update(arg, 'utf8').digest('hex'); +} + function soliditySha3(arg) { const Web3 = require('web3'); return Web3.utils.soliditySha3(arg); @@ -640,6 +646,7 @@ module.exports = { getExternalContractUrl, toChecksumAddress, sha3, + sha512, soliditySha3, normalizeInput, buildUrl,