var os = require('os') var fs = require('fs') var zlib = require('zlib') var http = require('http') var https = require('https') var stream = require('stream') var path = require('path') var STORE_CONTENT = 1 var settings = JSON.parse(fs.readFileSync(3)) function httpGet(url, cb) { return /^https:\/\//.test(url) ? https.get(url, cb) : http.get(url, cb) } function ReadBlobs(links) { if (!Array.isArray(links)) throw new TypeError('blob links should be array') this.links = links stream.Readable.call(this, links) } ReadBlobs.prototype = new stream.Readable() ReadBlobs.prototype.constructor = ReadBlobs ReadBlobs.prototype.i = 0 ReadBlobs.prototype.blobsBase = process.env.SSB_BLOBS_BASE || 'http://localhost:8989/blobs/get/' ReadBlobs.prototype._read = function (size) { if (!this.req) this._makeReq(this) else if (this.res) this.res.resume() } ReadBlobs.prototype._destroy = function (err, cb) { if (this.req) this.req.destroy(), this.req = null if (this.res) this.res.destroy(), this.res = null cb(err) } ReadBlobs.prototype._makeReq = function () { if (this.i >= this.links.length) return this.push(null) var link = this.links[this.i++] var id = typeof link === 'string' ? link : link && link.link var url = this.blobsBase + id console.error(id) var onResp = (res) => { this.res = res if (res.statusCode !== 200) { var reqStatus = res.statusCode + ' ' + res.statusMessage return this.destroy(new Error(reqStatus)) } res.on('data', (data) => { if (this.push(data) === false) res.pause() }) res.on('end', () => { this._makeReq() }) } var onError = (err) => { if (err.code === 'ECONNRESET') { // retry after timeout setTimeout(() => { this.req = httpGet(url, onResp).on('error', onError) }, 1e3) } else if (err.code === 'ECONNREFUSED' && err.address === '127.0.0.1' && this.blobsBase.indexOf('localhost') > -1 ) { // sometimes localhost resolves to 127.0.0.1 but ssb-ws listens on ::1 this.blobsBase = this.blobsBase.replace('localhost', '[::1]') url = this.blobsBase + id httpGet(url, onResp).on('error', onError) } else { this.destroy(err) } } this.req = httpGet(url, onResp).on('error', onError) } function Meter() { stream.Transform.call(this) } Meter.prototype = new stream.Transform() Meter.prototype.constructor = Meter Meter.prototype.length = 0 Meter.prototype._transform = function (buf, encoding, cb) { this.length += buf.length cb(null, buf) } function collect(cb) { var bufs = [] return new stream.Writable({ write: function (chunk, encoding, cb) { bufs.push(Buffer.from(chunk, encoding)) cb() } }).on('finish', function () { cb(null, Buffer.concat(bufs)) }).on('error', cb) } function substitute(template, vars) { return Object.keys(vars).reduce(function (str, key) { return str.replace(key, vars[key]) }, template) } function discoverPlaceholder (binaryBuffer, searchString, padder) { const placeholder = Buffer.from(searchString); const position = binaryBuffer.indexOf(placeholder); if (position === -1) return { notFound: true }; return { position, size: placeholder.length, padder }; } function injectPlaceholder (fd, placeholder, value, cb) { const { notFound, position, size, padder } = placeholder; if (notFound) assert(false, 'Placeholder for not found'); if (typeof value === 'number') value = value.toString(); if (typeof value === 'string') value = Buffer.from(value); const padding = Buffer.from(padder.repeat(size - value.length)); value = Buffer.concat([ value, padding ]); fs.write(fd, value, 0, value.length, position, cb); } function discoverPlaceholders (binaryBuffer) { return { BAKERY: discoverPlaceholder(binaryBuffer, '\0' + '// BAKERY '.repeat(20), '\0'), PAYLOAD_POSITION: discoverPlaceholder(binaryBuffer, '// PAYLOAD_POSITION //', ' '), PAYLOAD_SIZE: discoverPlaceholder(binaryBuffer, '// PAYLOAD_SIZE //', ' '), PRELUDE_POSITION: discoverPlaceholder(binaryBuffer, '// PRELUDE_POSITION //', ' '), PRELUDE_SIZE: discoverPlaceholder(binaryBuffer, '// PRELUDE_SIZE //', ' ') }; } function injectPlaceholders (fd, placeholders, values, cb) { injectPlaceholder(fd, placeholders.BAKERY, values.BAKERY, (error) => { if (error) return cb(error); injectPlaceholder(fd, placeholders.PAYLOAD_POSITION, values.PAYLOAD_POSITION, (error2) => { if (error2) return cb(error2); injectPlaceholder(fd, placeholders.PAYLOAD_SIZE, values.PAYLOAD_SIZE, (error3) => { if (error3) return cb(error3); injectPlaceholder(fd, placeholders.PRELUDE_POSITION, values.PRELUDE_POSITION, (error4) => { if (error4) return cb(error4); injectPlaceholder(fd, placeholders.PRELUDE_SIZE, values.PRELUDE_SIZE, cb); }); }); }); }); } function makeBakeryValueFromBakes (bakes) { const parts = []; if (bakes.length) { for (let i = 0; i < bakes.length; i += 1) { parts.push(Buffer.from(bakes[i])); parts.push(Buffer.alloc(1)); } parts.push(Buffer.alloc(1)); } return Buffer.concat(parts); } function makePreludeBufferFromPrelude (prelude) { return Buffer.from( '(function(process, require, console, EXECPATH_FD, PAYLOAD_POSITION, PAYLOAD_SIZE) { ' + prelude + '\n})' // dont remove \n ); } function plusx(file) { const s = fs.statSync(file) const newMode = s.mode | 64 | 8 | 1 if (s.mode === newMode) return const base8 = newMode.toString(8).slice(-3) fs.chmodSync(file, base8) } function mkdirp(dir) { try { fs.mkdirSync(path.dirname(path.dirname(dir))) } catch(e) {} try { fs.mkdirSync(path.dirname(dir)) } catch(e) {} fs.mkdirSync(dir) console.error(dir) } function writableDir(dir) { try { fs.accessSync(dir, fs.constants.W_OK) return true } catch(e) { if (e.code === 'ENOENT') { try { mkdirp(dir) return true } catch(e) {} } } } function first(array, fn) { for (var i = 0; i < array.length; i++) { var value = array[i] var result = fn(value) if (result) return value } } function isReadable(file) { try { fs.accessSync(file, fs.constants.R_OK) return true } catch(e) {} } function readProfile() { try { var profile = first([ process.env.PROFILE, path.join(homeDir, '.profile') ].filter(Boolean), isReadable) if (profile) return fs.readFileSync(profile, 'utf8') } catch(e) {} } function useConditionalDirFromProfile() { // Use ~/.local/bin or ~/bin if ~/.profile would detect it. var profile = readProfile() if (!profile) return if (profile.indexOf( 'if [ -d "$HOME/.local/bin" ] ; then\n PATH="$HOME/.local/bin:$PATH"\nfi' ) > 0) { dir = path.join(homeDir, '.local', 'bin') if (writableDir(dir)) return dir } if (profile.indexOf( 'if [ -d "$HOME/bin" ] ; then\n PATH="$HOME/bin:$PATH"\nfi' ) > 0) { dir = path.join(homeDir, 'bin') if (writableDir(dir)) return dir } } function pickBinDir() { var isRoot = (process.env.UID || process.geteuid()) === 0 var userBinDirs = isRoot ? [ // If root user, install system-wide. ] : [ path.join(homeDir, '.local', 'bin'), path.join(homeDir, 'bin') ] var favoriteDirs = [ process.env.PREFIX && path.join(process.env.PREFIX, 'bin') ].concat(userBinDirs).concat([ '/usr/local/bin', '/opt/bin' ]).filter(Boolean) var dirsInPath = process.env.PATH ? process.env.PATH.split(':') : [] function isDirInPath(dir) { return dirsInPath.indexOf(dir) > -1 } var favoriteDirsInPath = favoriteDirs.filter(isDirInPath) var dir = first(favoriteDirsInPath, writableDir) if (dir) return dir dir = useConditionalDirFromProfile() if (dir) { console.error('Note: restart your shell to use new $PATH') return dir } dir = first(favoriteDirs, writableDir) if (dir) { console.error(`Warning: installing to directory not in $PATH. Run this and add it to your ~/.profile or similar: export PATH="${dir.replace(homeDir, '$HOME')}:$PATH"`) return dir } console.error('Error: no directory to install to. Set $BIN_DIR, or add ' + (isRoot ? '/usr/local/bin' : '~/.local/bin') + ' to your $PATH, and make sure you can write to it.') process.exit(1) } var homeDir = process.env.HOME || os.homedir() var binDir = process.env.BIN_DIR || pickBinDir() var binPath = path.join(binDir, settings.binName) var binTmp = binPath + '~' // assume the node binary is the pkg one to use var nodeBuffer = fs.readFileSync(process.execPath) var placeholders = discoverPlaceholders(nodeBuffer) var payloadPosition = nodeBuffer.length, payloadSize var preludePosition, preludeSize var vfs var binFd, writeStream try { fs.statSync(binDir) } catch(e) { mkdirp(binDir) } function writeNodeBinary() { fs.open(binTmp, 'w+', function (err, fd) { if (err) throw err binFd = fd writeStream = fs.createWriteStream(null, {fd: fd}) writeStream.write(nodeBuffer, function (err) { if (err) throw err getVfs() }) }) } function getVfs() { new ReadBlobs(settings.vfsBlobs) .pipe(collect(function (err, vfsData) { if (err) throw err vfs = JSON.parse(vfsData) getPayload() })) } function getPayload() { var payloadMeter = new Meter() var readPayload = new ReadBlobs(settings.payloadBlobs) .pipe(zlib.createGunzip()) .pipe(payloadMeter) readPayload.pipe(writeStream, {end: false}) readPayload.on('end', function () { payloadSize = payloadMeter.length addFileBlobs() }) } function addFileBlobs() { var offset = payloadSize var fileBlobs = settings.fileBlobs var queue = [] function enqueue(fileBlobsToAdd) { for (var snap in fileBlobsToAdd) { var ids = fileBlobsToAdd[snap] queue.push({snap: snap, ids: ids}) } } if (Array.isArray(fileBlobs)) { enqueue(fileBlobs) } else [ os.platform() + '-' + os.arch(), '*' ].forEach(function (group) { enqueue(fileBlobs[group]) }) ;(function next() { if (!queue.length) return getPrelude() var item = queue.shift() var snap = item.snap, ids = item.ids var fileMeter = new Meter() var readFile = new ReadBlobs(ids) .pipe(fileMeter) readFile.pipe(writeStream, {end: false}) readFile.on('end', function () { var fileSize = fileMeter.length vfs[snap][STORE_CONTENT] = [offset, fileSize] offset += fileSize payloadSize += fileSize next() }) })() } function getPrelude() { preludePosition = payloadPosition + payloadSize new ReadBlobs(settings.preludeBlobs) .pipe(collect(function (err, preludeData) { if (err) throw err preludeData = makePreludeBufferFromPrelude( substitute(preludeData.toString('utf8'), { '%VIRTUAL_FILESYSTEM%': JSON.stringify(vfs), '%DEFAULT_ENTRYPOINT%': JSON.stringify(settings.entrypoint) }) ) preludeSize = preludeData.length writeStream.write(preludeData, function (err) { if (err) throw err inject() }) })) } function inject() { injectPlaceholders(binFd, placeholders, { BAKERY: makeBakeryValueFromBakes(settings.bakes), PAYLOAD_POSITION: payloadPosition, PAYLOAD_SIZE: payloadSize, PRELUDE_POSITION: preludePosition, PRELUDE_SIZE: preludeSize }, function (err) { if (err) throw err fs.close(binFd, function (err) { if (err) throw err putBins() }) }) } function addOtherBin(name, snap) { var script = `#!/bin/sh PKG_ENTRYPOINT=${snap} exec ${settings.binName} "$@" ` var binPath = path.join(binDir, name) var binTmp = binPath + '~' fs.writeFileSync(binTmp, script) plusx(binTmp) fs.renameSync(binTmp, binPath) console.log(binPath) } function putBins() { plusx(binTmp) fs.renameSync(binTmp, binPath) var others = settings.otherEntrypoints if (others) for (var name in others) { var snap = others[name] addOtherBin(name, snap) } console.log(binPath) process.exit(0) } writeNodeBinary()