mirror of
https://github.com/balena-io/etcher.git
synced 2025-04-24 07:17:18 +00:00

As we're not escaping env vars properly in `permissions.getEnvironmentCommandPrefix()`, passing the entire environment along can cause another instance of the "stuck at starting" problem, thus we limit the vars explicitly passed along again, keeping PATH and DEBUG. Change-Type: patch
216 lines
6.4 KiB
JavaScript
216 lines
6.4 KiB
JavaScript
/*
|
|
* Copyright 2016 resin.io
|
|
*
|
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
* you may not use this file except in compliance with the License.
|
|
* You may obtain a copy of the License at
|
|
*
|
|
* http://www.apache.org/licenses/LICENSE-2.0
|
|
*
|
|
* Unless required by applicable law or agreed to in writing, software
|
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
* See the License for the specific language governing permissions and
|
|
* limitations under the License.
|
|
*/
|
|
|
|
'use strict'
|
|
|
|
const Bluebird = require('bluebird')
|
|
const childProcess = require('child_process')
|
|
const ipc = require('node-ipc')
|
|
const _ = require('lodash')
|
|
const os = require('os')
|
|
const path = require('path')
|
|
const utils = require('./utils')
|
|
const EXIT_CODES = require('../shared/exit-codes')
|
|
const robot = require('../shared/robot')
|
|
const permissions = require('../shared/permissions')
|
|
const packageJSON = require('../../package.json')
|
|
const CONSTANTS = require('./constants')
|
|
|
|
/* eslint-disable no-eq-null */
|
|
|
|
// This script is in charge of spawning the writer process and
|
|
// ensuring it has the necessary privileges. It might look a bit
|
|
// complex at first sight, but this is only because elevation
|
|
// modules don't work in a spawn/fork fashion.
|
|
//
|
|
// This script spawns the writer process and redirects its `stdout`
|
|
// and `stderr` to the parent process using IPC communication,
|
|
// taking care of the writer elevation as needed.
|
|
|
|
/**
|
|
* @summary The Etcher executable file path
|
|
* @constant
|
|
* @private
|
|
* @type {String}
|
|
*/
|
|
const executable = _.first(process.argv)
|
|
|
|
/**
|
|
* @summary The first index that represents an actual option argument
|
|
* @constant
|
|
* @private
|
|
* @type {Number}
|
|
*
|
|
* @description
|
|
* The first arguments are usually the program executable itself, etc.
|
|
*/
|
|
const OPTIONS_INDEX_START = 2
|
|
|
|
/**
|
|
* @summary The list of Etcher argument options
|
|
* @constant
|
|
* @private
|
|
* @type {String[]}
|
|
*/
|
|
const etcherArguments = process.argv.slice(OPTIONS_INDEX_START)
|
|
|
|
ipc.config.id = process.env.IPC_CLIENT_ID
|
|
ipc.config.socketRoot = CONSTANTS.TMP_DIRECTORY
|
|
ipc.config.silent = true
|
|
|
|
// > If set to 0, the client will NOT try to reconnect.
|
|
// See https://github.com/RIAEvangelist/node-ipc/
|
|
//
|
|
// The purpose behind this change is for this process
|
|
// to emit a "disconnect" event as soon as the GUI
|
|
// process is closed, so we can kill the CLI as well.
|
|
ipc.config.stopRetrying = 0
|
|
|
|
permissions.isElevated().then((elevated) => {
|
|
if (!elevated) {
|
|
console.log('Attempting to elevate')
|
|
|
|
const commandArguments = _.attempt(() => {
|
|
if (os.platform() === 'linux' && process.env.APPIMAGE && process.env.APPDIR) {
|
|
// Translate the current arguments to point to the AppImage
|
|
// Relative paths are resolved from `/tmp/.mount_XXXXXX/usr`
|
|
const translatedArguments = _.chain(process.argv)
|
|
.tail()
|
|
.invokeMap('replace', path.join(process.env.APPDIR, 'usr/'), '')
|
|
.value()
|
|
|
|
return _.concat([ process.env.APPIMAGE ], translatedArguments)
|
|
}
|
|
|
|
return process.argv
|
|
})
|
|
|
|
// For debugging purposes
|
|
console.log(`Running: ${commandArguments.join(' ')}`)
|
|
|
|
const commandEnv = {
|
|
PATH: process.env.PATH,
|
|
DEBUG: process.env.DEBUG,
|
|
XDG_RUNTIME_DIR: process.env.XDG_RUNTIME_DIR,
|
|
ELECTRON_RUN_AS_NODE: 1,
|
|
IPC_SERVER_ID: process.env.IPC_SERVER_ID,
|
|
IPC_CLIENT_ID: process.env.IPC_CLIENT_ID,
|
|
|
|
// This environment variable prevents the AppImages
|
|
// desktop integration script from presenting the
|
|
// "installation" dialog.
|
|
SKIP: 1
|
|
}
|
|
|
|
return permissions.elevateCommand(commandArguments, {
|
|
applicationName: packageJSON.displayName,
|
|
environment: commandEnv
|
|
}).then((results) => {
|
|
if (results.cancelled) {
|
|
process.exit(EXIT_CODES.CANCELLED)
|
|
}
|
|
})
|
|
}
|
|
|
|
console.log('Re-spawning with elevation')
|
|
|
|
return new Bluebird((resolve, reject) => {
|
|
let child = null
|
|
|
|
/**
|
|
* @summary Emit an object message to the IPC server
|
|
* @function
|
|
* @private
|
|
*
|
|
* @param {Buffer} data - json message data
|
|
*
|
|
* @example
|
|
* emitMessage(Buffer.from(JSON.stringify({
|
|
* foo: 'bar'
|
|
* })));
|
|
*/
|
|
const emitMessage = (data) => {
|
|
// Output from stdout/stderr coming from the CLI might be buffered,
|
|
// causing several progress lines to come up at once as single message.
|
|
// Trying to parse multiple JSON objects separated by new lines will
|
|
// of course make the parser confused, causing errors later on.
|
|
_.each(utils.splitObjectLines(data.toString()), (object) => {
|
|
ipc.of[process.env.IPC_SERVER_ID].emit('message', object)
|
|
})
|
|
}
|
|
|
|
/**
|
|
* @summary Emit an error message over the IPC and shut down the child
|
|
* @function
|
|
* @private
|
|
* @param {Error} error - error
|
|
* @example
|
|
* onError(error)
|
|
*/
|
|
const onError = (error) => {
|
|
ipc.of[process.env.IPC_SERVER_ID].emit('message', {
|
|
error: error.message,
|
|
data: error.stack
|
|
})
|
|
child && child.kill()
|
|
reject(error)
|
|
}
|
|
|
|
ipc.connectTo(process.env.IPC_SERVER_ID, () => {
|
|
ipc.of[process.env.IPC_SERVER_ID].on('error', onError)
|
|
ipc.of[process.env.IPC_SERVER_ID].on('disconnect', () => {
|
|
onError(new Error('Writer process disconnected'))
|
|
})
|
|
|
|
ipc.of[process.env.IPC_SERVER_ID].on('connect', () => {
|
|
// Inherit the parent evnironment
|
|
const childEnv = _.assign({}, process.env, {
|
|
ELECTRON_RUN_AS_NODE: 1,
|
|
ETCHER_CLI_ROBOT: 1,
|
|
|
|
// Enable extra logging from mountutils
|
|
// See https://github.com/resin-io-modules/mountutils
|
|
MOUNTUTILS_DEBUG: 1
|
|
})
|
|
|
|
child = childProcess.spawn(executable, etcherArguments, {
|
|
env: childEnv
|
|
})
|
|
|
|
child.on('error', onError)
|
|
child.on('exit', (code, signal) => {
|
|
if (code != null && signal == null) {
|
|
resolve(code)
|
|
} else {
|
|
const error = new Error(`Exited with code ${code}, signal ${signal}`)
|
|
error.code = code
|
|
error.signal = signal
|
|
reject(error)
|
|
}
|
|
})
|
|
|
|
child.stdout.on('data', emitMessage)
|
|
child.stderr.on('data', emitMessage)
|
|
})
|
|
})
|
|
}).then((exitCode) => {
|
|
process.exit(exitCode)
|
|
})
|
|
}).catch((error) => {
|
|
robot.printError(error)
|
|
process.exit(EXIT_CODES.GENERAL_ERROR)
|
|
})
|