Socket
Socket
Sign inDemoInstall

custody-cli

Package Overview
Dependencies
320
Maintainers
23
Versions
11
Alerts
File Explorer

Advanced tools

Install Socket

Detect and block malicious and high-risk dependencies

Install

Comparing version 0.3.1 to 0.4.0

2

CHANGELOG.md
## Release History
* 0.4.0 Add support for displaying maintail output #72
* 0.3.1 Build on local installation as well as on publishing (#71)

@@ -4,0 +6,0 @@

906

dist/index.js

@@ -8,2 +8,3 @@ 'use strict';

var path = require('path');
var path__default = _interopDefault(path);
var promiseCallbacks = require('promise-callbacks');

@@ -13,2 +14,4 @@ var sanitizeFilename = _interopDefault(require('sanitize-filename'));

var _ = _interopDefault(require('underscore'));
var ini = _interopDefault(require('ini'));
var supervisord = _interopDefault(require('supervisord'));
var memoize = _interopDefault(require('memoize-one'));

@@ -18,5 +21,5 @@ var PropTypes = _interopDefault(require('prop-types'));

var React__default = _interopDefault(React);
var tail = require('tail');
var fuzzy = _interopDefault(require('fuzzy'));
var xor = _interopDefault(require('lodash.xor'));
var tail = require('tail');
var notifier = _interopDefault(require('node-notifier'));

@@ -26,3 +29,2 @@ var kexec = _interopDefault(require('kexec'));

var EventEmitter = _interopDefault(require('events'));
var supervisord = _interopDefault(require('supervisord'));
var reactBlessed = require('react-blessed');

@@ -188,16 +190,2 @@

class Restart {
constructor(process) {
this.process = process;
}
get verb() {
return 'restart';
}
async toggle() {
await this.process.restart();
}
}
/**

@@ -303,77 +291,263 @@ * States of a process' lifecycle.

function processIsHalted(process) {
return _.contains([STATES.STOPPED, STATES.FATAL], process.effectiveState.state);
}
const exec = promiseCallbacks.promisify(require('child_process').exec);
class ToggleStopStart {
constructor(process) {
this.process = process;
/**
* Determines whether the process has crashed due to a port conflict, and if so, returns the
* port in question.
*
* @param {Process} process - The process to analyze.
*
* @return {Int|null} The port in conflict, if any, or `null` if none.
*/
function detectPortConflict(process) {
const { state, description } = process.effectiveState;
if (state === STATES.FATAL) {
const portString = _.last(description.match(/EADDRINUSE.+?(\d+)/));
if (portString) return parseInt(portString, 10);
}
return null;
}
get verb() {
if (processIsHalted(this.process)) {
return 'stop/{underline}start{/}';
} else {
return '{underline}stop{/}/start';
}
}
/**
* Clears a port conflict by killing the process listening to that port.
*
* @param {Int} port - The port to clear.
*
* @return {Promise<Error>} A promise that resolves to `undefined` if the port is successfully
* cleared, otherwise rejects with the error.
*/
async function clearPortConflict(port) {
return exec(`kill -9 $(lsof -ti :${port})`);
}
async toggle() {
if (processIsHalted(this.process)) {
await this.process.start();
} else {
await this.process.stop();
/**
* Determines the working directory of the specified process.
*
* @param {Int} pid - A process ID.
*
* @return {Promise<String>} The working directory of the process.
*/
async function getWorkingDirectory(pid) {
// https://unix.stackexchange.com/a/94365
// https://unix.stackexchange.com/a/255806
return (await exec(`lsof -a -Fn -p ${pid} -d cwd | sed -n 3p | cut -c 2-`)).trim();
}
const readFile$1 = promiseCallbacks.promisify.method(require('fs'), 'readFile');
/**
* Creates a promisified client for the supervisord instance listening on the specified port.
*
* @param {Int} port - The port on which the supervisord instance is listening.
*
* @return {Promisified Supervisord} A promisified supervisord client.
*/
function createClient(port) {
// TODO(jeff): Log connection errors here; for some reason this crashes the process without even
// an uncaught exception.
return promiseCallbacks.promisify.all(supervisord.connect(`http://localhost:${port}`));
}
/**
* Determines the path of the main logfile in use by Supervisor.
*
* @param {Promisified Supervisord} supervisor - a promisified supervisord client.
*
* @return {String} The path of the main logfile.
*/
async function getMainLogfile(supervisor) {
// Supervisor does not expose an XML-RPC API for determining the path of the main logfile as of
// API version 3.0: http://supervisord.org/api.html. To identify the main logfile, we must read it
// from the configuration file. Supervisor _also_ lacks APIs for returning its full configuration
// and/or the path of the configuration file, so we identify the configuration file by determining
// `supervisord`'s working directory, then looking up the configuration file as described at
// http://supervisord.org/configuration.html#configuration-file.
const supervisorPid = await supervisor.getPID();
const supervisorCwd = await getWorkingDirectory(supervisorPid);
// TODO(jeff): Look up configuration files at the other possible locations (see
// http://supervisord.org/configuration.html#configuration-file).
const supervisorConfigFile = path__default.join(supervisorCwd, 'supervisord.conf');
const supervisorConfig = ini.parse((await readFile$1(supervisorConfigFile, 'utf-8')));
return supervisorConfig.supervisord.logfile;
}
/**
* Utilities for managing the command menu's data structure, a list of commands.
*
* Commands are added and removed in "sets" using the `addCommandSet` and `removeCommandSet` APIs.
* The list of commands is available as `commands`.
*
* This data is intended to be provided to the command menu through React context, so that different
* components can add/remove command sets as they appear.
*/
/**
* @typedef {Object} CommandDescription
*
* @property {String} verb - A description of what the command does, of the form
* "<action> [<subject>]". Examples: 'restart' or "show/hide commands".
* @property {AsyncFunction|Function} toggle - A function that, when invoked, will perform the
* command.
* @property {Boolean=false} isSeparator - `true` if this command should be rendered as a separator
* in the command list rather than a command that performs an action.
*/
/**
* @typedef {[String, CommandDescription]} Command
*
* A command is an array containing two elements:
*
* 1. The full name of the key which will trigger the command. Examples: 's' or 'tab'.
* 2. An object describing the command.
*/
/**
* @typedef {Array<Command>} CommandList
*/
/**
* @type {Command}
*
* A command that represents a separator in the command list.
*/
const separatorCommand = ['', { isSeparator: true }];
let commands = [];
const commandSetMap = new Map();
/**
* Updates the list of commands to reflect the current command sets.
*
* @return {CommandList} The new list of commands.
*/
function updateCommands() {
commands = [...commandSetMap].reverse() // Most recently-added commands first.
.reduce((commands, [, set], idx) => {
commands.push(...set);
if (idx < commandSetMap.size - 1) {
commands.push(separatorCommand);
}
}
return commands;
}, []);
return commands;
}
// Currently, show keyboard shortcuts every time that a user launches custody, then hide them for
/**
* @type {CommandList}
*
* The list of commands, a live binding kept up to date by `addCommandSet` and `removeCommandSet`.
* You should use those APIs to manipulate the list rather than editing it directly.
*/
var commands$1 = commands;
/**
* Adds a set of commands to the command list.
*
* Commands will be rendered most-recently-added first, by set; and then within the set, in the
* order in which they appear when passed to this function.
*
* If a set was already registered under `name`, this function will replace the old set with the
* new one.
*
* @param {String} name - A name by which to identify the set of commands and remove the commands
* later, using `removeCommandSet`.
* @param {CommandList} commands - The set of commands.
*
* @return {CommandList} The new list of commands.
*/
function addCommandSet(name, commands) {
commandSetMap.set(name, commands);
return updateCommands();
}
/**
* Removes a set of commands from the command list.
*
* This function is a no-op if no set was registered for `name`.
*
* @param {String} name - The name of a set of commands to remove from the command list.
*
* @return {CommandList} The new list of commands.
*/
function removeCommandSet(name) {
commandSetMap.delete(name);
return updateCommands();
}
// Currently, show the command menu every time that a user launches custody, then hide them for
// the lifetime of the process (unless the user shows them again). This compensates for the user
// forgetting how to show the keyboard shortcuts. We could persist this using the `storage` APIs
// forgetting how to show the command menu. We could persist this using the `storage` APIs
// if we wanted, although loading would be a bit racy because it's async.
let CONTROLS_ARE_HIDDEN = false;
let COMMANDS_ARE_HIDDEN = false;
const getControls = component => (process, controls) => {
return new Map([['r', new Restart(process)], ['s', new ToggleStopStart(process)], ...controls, ['/', {
verb: 'show/hide shortcuts',
const getCommands = component => commands => {
const allCommands = [...commands];
if (!_.isEmpty(allCommands)) {
allCommands.push(separatorCommand);
}
allCommands.push(['/', {
verb: 'show/hide commands',
toggle() {
CONTROLS_ARE_HIDDEN = !CONTROLS_ARE_HIDDEN;
component.setState({ hidden: CONTROLS_ARE_HIDDEN });
COMMANDS_ARE_HIDDEN = !COMMANDS_ARE_HIDDEN;
component.setState({ hidden: COMMANDS_ARE_HIDDEN });
}
}]]);
}]);
return allCommands;
};
class ProcessControls extends React.Component {
class CommandMenu extends React.Component {
constructor(props) {
super(props);
this.controlGetter = memoize(getControls(this));
// Store the commands both as a list and as a map (rather than just enumerating the map when
// necessary) because the map will not let us enumerate separator commands in their true order,
// as all separator commands share the same key.
this.commandGetter = memoize(getCommands(this));
this.commandMapGetter = memoize(commands => new Map(commands));
this.state = {
hidden: CONTROLS_ARE_HIDDEN
hidden: COMMANDS_ARE_HIDDEN
};
}
onKeypress(ch) {
const control = this.controls.get(ch);
if (!control) return;
componentDidUpdate() {
// The menu should float in front of other components. If we do not reset the z-index when the
// menu re-renders, it may be hidden behind another component.
if (this.el) this.el.setFront();
}
const process = this.props.process;
willHandleKeypress(key) {
return !!this.commandMap.get(key.full);
}
// `Promise.resolve` the result of `toggle` to support both synchronous and asynchronous actions.
Promise.resolve(control.toggle()).catch(err => {
// HACK(jeff): We don't know for sure that the control manipulates the process.
// But it's a fair bet.
screen.debug(`Could not ${control.verb} ${process.name}:`, err);
onKeypress(key) {
const command = this.commandMap.get(key.full);
if (!command) return;
// `Promise.resolve` the result of `toggle` to support both synchronous and asynchronous commands.
Promise.resolve(command.toggle()).catch(err => {
screen.debug(`Could not ${command.verb}:`, err);
});
}
get controls() {
return this.controlGetter(this.props.process, this.props.controls);
get commands() {
return this.commandGetter(this.context.commands || []);
}
get commandMap() {
return this.commandMapGetter(this.commands);
}
render() {
if (this.state.hidden) return null;
let boxContent = [...this.controls].map(([ch, { verb }]) => `'${ch}' to ${verb}`).join('\n');
let boxContent = [...this.commands].map(([ch, { verb, isSeparator }]) => {
return isSeparator ? '' : `'${ch}' to ${verb}`;
}).join('\n');

@@ -386,2 +560,3 @@ // For some reason the `tags` attribute doesn't work on the box.

{
ref: el => this.el = el,
border: { type: 'line' },

@@ -397,7 +572,212 @@ shrink: true // Shrink the border to fit the content.

ProcessControls.propTypes = {
// react-blessed does not support the new context API introduced with version 16.3 :(
// https://github.com/Yomguithereal/react-blessed/issues/83
CommandMenu.contextTypes = {
/**
* @type {CommandList}
*
* See `/components/commandMenu/commands` for more information.
*/
commands: PropTypes.array
};
var _extends = Object.assign || function (target) {
for (var i = 1; i < arguments.length; i++) {
var source = arguments[i];
for (var key in source) {
if (Object.prototype.hasOwnProperty.call(source, key)) {
target[key] = source[key];
}
}
}
return target;
};
// It might be nice to render the entire log file. However this is probably (?) unnecessary and
// (more to the point) for complex/active services like app, the log file is quite large and, if we
// try to load it all into the `log` component, can take several seconds to render.
// We can probably afford a larger scrollback after the logs have grown though:
// https://github.com/mixmaxhq/custody/issues/49.
const INITIAL_SCROLLBACK = 100 /* lines */;
const SCROLLBACK = 1000 /* lines */;
class FileLog extends React.Component {
componentDidMount() {
// See comment about `mouse` in `render`.
enableMouse(false);
this.startTailing();
}
componentWillUnmount() {
this.stopTailing();
enableMouse(true);
}
startTailing() {
// Safety belts.
if (this.tail) return;
const { logfile } = this.props;
function onTailError(err) {
screen.debug(`Could not tail logfile ${logfile}:`, err);
}
// We need to check that the logfile exists before initializing `Tail` because we can't handle
// such an error if we let `Tail` emit it: https://github.com/lucagrulla/node-tail/issues/66
try {
fs.statSync(logfile);
} catch (e) {
onTailError(e);
return;
}
// As documented on `INITIAL_SCROLLBACK`, we can't render the entire log file. However, the
// 'tail' module lacks a `-n`-like option to get the last `INITIAL_SCROLLBACK` lines. So what we
// do is load the entire file, but wait to render only the last `INITIAL_SCROLLBACK` lines, then
// start streaming.
this.tail = new tail.Tail(logfile, { fromBeginning: true });
let logs = [];
let initialDataFlushed = false;
this.tail.on('line', line => {
if (initialDataFlushed) {
this.log.add(line);
} else {
logs.push(line);
if (logs.length > INITIAL_SCROLLBACK) logs.shift();
}
}).on('historicalDataEnd', () => {
logs.forEach(line => this.log.add(line));
logs = [];
initialDataFlushed = true;
}).on('error', onTailError);
}
stopTailing() {
if (this.tail) {
this.tail.unwatch();
this.tail = null;
}
}
render() {
return (
// Unfortunately react-blessed@0.2.1 doesn't support `React.createRef`.
React__default.createElement('log', _extends({
ref: log => this.log = log
}, this.props.layout, {
// Enables 'keypress' events, for the use of `keys`.
input: true
// Pass `keys` to enable the use of the up and down arrow keys to navigate the keyboard.
// Note that we do _not_ pass `mouse`, because having blessed scroll will a) break native
// text selection https://github.com/chjj/blessed/issues/263 b) be too fast
// https://github.com/mixmaxhq/custody/issues/37#issuecomment-390855414. Instead, we disable
// blessed's mouse handling while this component is mounted to let the terminal take over
// scrolling.
, keys: true,
focused: this.props.focused,
scrollback: SCROLLBACK
// TODO(jeff): Enable pinning to the bottom of the logs. Possible using the `scrollOnInput`
// prop, just requires us to give the user a keyboard shortcut to do so. Not totally sure
// this is necessary though since the component will follow the logs if the user is scrolled
// to the bottom--sounds like enabling this would only jump the user to the end if they had
// scrolled away.
}))
);
}
}
FileLog.propTypes = {
logfile: PropTypes.string.isRequired,
focused: PropTypes.bool,
layout: PropTypes.object
};
FileLog.defaultProps = {
focused: false
};
class ProcessLog extends React.Component {
constructor(props) {
super(props);
this.state = {
logfileIsLoaded: false
};
}
componentDidMount() {
this._isMounted = true;
this.loadLogfile();
}
componentWillUnmount() {
this._isMounted = false;
}
loadLogfile() {
const { name, logfile } = this.props.process;
// Make sure that the process' logfile exists for the benefit of `FileLog`.
try {
fs.statSync(logfile);
} catch (e) {
if (e.code === 'ENOENT') {
// This logfile does not exist (has disappeared?) for some reason:
// https://github.com/mixmaxhq/custody/issues/6 Restart the process to fix.
screen.debug(`${name}'s logfile is missing. Restarting process to fix…`);
this.props.process.restart().then(() => {
if (!this._isMounted) return;
this.loadLogfile();
}).catch(err => {
if (!this._isMounted) return;
screen.debug(`Could not restart ${name}: ${err}`);
});
} else {
screen.debug(`Could not tail ${name}'s logfile ${logfile}:`, e);
}
return;
}
this.setState({ logfileIsLoaded: true });
}
render() {
if (!this.state.logfileIsLoaded) return null;
return React__default.createElement(FileLog, {
logfile: this.props.process.logfile,
focused: this.props.focused,
layout: this.props.layout
});
}
}
ProcessLog.propTypes = {
process: PropTypes.object.isRequired,
controls: PropTypes.array
focused: PropTypes.bool,
layout: PropTypes.object
};
ProcessLog.defaultProps = {
focused: false
};
class Restart {
constructor(process) {
this.process = process;
}
get verb() {
return 'restart';
}
async toggle() {
await this.process.restart();
}
}
/**

@@ -568,3 +948,4 @@ * Returns the last index in `string` at which `regex` matches.

this.setState({ search: '' });
} else if (ch && !_.contains(['enter', 'return'], key.name)) {
} else if (ch && this.props.shouldHandleKeypress(ch, key) && !_.contains(['enter', 'return'], key.name)) {
this.setState(prevState => {

@@ -636,5 +1017,10 @@ const newSearch = prevState.search + ch;

processes: PropTypes.array.isRequired,
onSelect: PropTypes.func
onSelect: PropTypes.func,
shouldHandleKeypress: PropTypes.func
};
ProcessTable.defaultProps = {
shouldHandleKeypress: () => true
};
function ProcessSummary({ process }) {

@@ -688,145 +1074,53 @@ const data = HEADERS.map(key => {

var _extends = Object.assign || function (target) {
for (var i = 1; i < arguments.length; i++) {
var source = arguments[i];
function processIsHalted(process) {
return _.contains([STATES.STOPPED, STATES.FATAL], process.effectiveState.state);
}
for (var key in source) {
if (Object.prototype.hasOwnProperty.call(source, key)) {
target[key] = source[key];
}
}
class ToggleStopStart {
constructor(process) {
this.process = process;
}
return target;
};
// It might be nice to render the entire log file. However this is probably (?) unnecessary and
// (more to the point) for complex/active services like app, the log file is quite large and, if we
// try to load it all into the `log` component, can take several seconds to render.
// We can probably afford a larger scrollback after the logs have grown though:
// https://github.com/mixmaxhq/custody/issues/49.
const INITIAL_SCROLLBACK = 100 /* lines */;
const SCROLLBACK = 1000 /* lines */;
class ProcessLog extends React.Component {
componentDidMount() {
this._isMounted = true;
// See comment about `mouse` in `render`.
enableMouse(false);
this.startTailing();
get verb() {
if (processIsHalted(this.process)) {
return 'stop/{underline}start{/}';
} else {
return '{underline}stop{/}/start';
}
}
componentWillUnmount() {
this.stopTailing();
enableMouse(true);
this._isMounted = false;
async toggle() {
if (processIsHalted(this.process)) {
await this.process.start();
} else {
await this.process.stop();
}
}
}
startTailing() {
// Safety belts.
if (this.tail) return;
const COMMAND_SET_NAME = 'process-details';
const { name, logfile } = this.props.process;
function onTailError(err) {
screen.debug(`Could not tail ${name}'s logfile ${logfile}:`, err);
function getCommands$1(process) {
return [['r', new Restart(process)], ['s', new ToggleStopStart(process)], ['Esc', {
verb: 'go back',
toggle() {
// Nothing to do since we already handle escape in `onElementKeypress` below.
}
}]];
}
// We need to check that the logfile exists before initializing `Tail` because we can't handle
// such an error if we let `Tail` emit it: https://github.com/lucagrulla/node-tail/issues/66
try {
fs.statSync(logfile);
} catch (e) {
if (e.code === 'ENOENT') {
// This logfile does not exist (has disappeared?) for some reason:
// https://github.com/mixmaxhq/custody/issues/6 Restart the process to fix.
this.log.add(`${name}'s logfile is missing. Restarting process to fix…`);
this.props.process.restart().then(() => {
if (!this._isMounted) return;
this.startTailing();
}).catch(err => {
if (!this._isMounted) return;
this.log.add(`Could not restart ${name}: ${err}`);
});
} else {
onTailError(e);
}
return;
}
class ProcessDetails extends React.Component {
constructor(props) {
super(props);
// As documented on `INITIAL_SCROLLBACK`, we can't render the entire log file. However, the
// 'tail' module lacks a `-n`-like option to get the last `INITIAL_SCROLLBACK` lines. So what we
// do is load the entire file, but wait to render only the last `INITIAL_SCROLLBACK` lines, then
// start streaming.
this.tail = new tail.Tail(logfile, { fromBeginning: true });
let logs = [];
let initialDataFlushed = false;
this.tail.on('line', line => {
if (initialDataFlushed) {
this.log.add(line);
} else {
logs.push(line);
if (logs.length > INITIAL_SCROLLBACK) logs.shift();
}
}).on('historicalDataEnd', () => {
logs.forEach(line => this.log.add(line));
logs = [];
initialDataFlushed = true;
}).on('error', onTailError);
this.commandGetter = memoize(getCommands$1);
}
stopTailing() {
if (this.tail) {
this.tail.unwatch();
this.tail = null;
}
get commands() {
return this.commandGetter(this.props.process);
}
render() {
return (
// Unfortunately react-blessed@0.2.1 doesn't support `React.createRef`.
React__default.createElement('log', _extends({
ref: log => this.log = log
}, this.props.layout, {
// Enables 'keypress' events, for the use of `keys`.
input: true
// Pass `keys` to enable the use of the up and down arrow keys to navigate the keyboard.
// Note that we do _not_ pass `mouse`, because having blessed scroll will a) break native
// text selection https://github.com/chjj/blessed/issues/263 b) be too fast
// https://github.com/mixmaxhq/custody/issues/37#issuecomment-390855414. Instead, we disable
// blessed's mouse handling while this component is mounted to let the terminal take over
// scrolling.
, keys: true,
focused: this.props.focused,
scrollback: SCROLLBACK
// TODO(jeff): Enable pinning to the bottom of the logs. Possible using the `scrollOnInput`
// prop, just requires us to give the user a keyboard shortcut to do so. Not totally sure
// this is necessary though since the component will follow the logs if the user is scrolled
// to the bottom--sounds like enabling this would only jump the user to the end if they had
// scrolled away.
}))
);
}
}
componentDidMount() {
this.context.addCommandSet(COMMAND_SET_NAME, this.commands);
ProcessLog.propTypes = {
process: PropTypes.object.isRequired,
focused: PropTypes.bool,
layout: PropTypes.object
};
ProcessLog.defaultProps = {
focused: false
};
const DEFAULT_CONTROLS = [['Esc', {
verb: 'go back',
toggle() {
// Nothing to do since we already handle escape in `onElementKeypress` below.
}
}]];
class ProcessDetails extends React.Component {
componentDidMount() {
// The log has to be focused--not our root element--in order to enable keyboard navigation

@@ -839,8 +1133,15 @@ // thereof. But then this means that we have to listen for the log's keypress events, using the

componentDidUpdate(prevProps) {
if (this.props.process !== prevProps.process) {
this.context.addCommandSet(COMMAND_SET_NAME, this.commands);
}
}
componentWillUnmount() {
this.context.removeCommandSet(COMMAND_SET_NAME);
}
onElementKeypress(el, ch, key) {
if (key.name === 'escape') {
this.props.onClose();
} else {
// Forward keypresses to the controls since we have to keep the log focused.
this.controls.onKeypress(ch, key);
}

@@ -859,7 +1160,2 @@ }

, layout: { top: 1 }
}),
React__default.createElement(ProcessControls, {
ref: controls => this.controls = controls,
process: this.props.process,
controls: DEFAULT_CONTROLS
})

@@ -875,4 +1171,9 @@ );

const exec = promiseCallbacks.promisify(require('child_process').exec);
ProcessDetails.contextTypes = {
addCommandSet: PropTypes.func.isRequired,
removeCommandSet: PropTypes.func.isRequired
};
const exec$1 = promiseCallbacks.promisify(require('child_process').exec);
function processHasChangedState(prevProps) {

@@ -891,2 +1192,20 @@ return process => {

const COMMAND_SET_NAME$1 = 'console';
function getCommandSet(component) {
let maintailDescription;
if (component.state.tailingMainLogfile) {
maintailDescription = 'switch to/{underline}from{/} maintail';
} else {
maintailDescription = 'switch {underline}to{/}/from maintail';
}
return [['tab', {
verb: maintailDescription,
toggle() {
component.setState(({ tailingMainLogfile }) => ({ tailingMainLogfile: !tailingMainLogfile }));
}
}]];
}
class Console extends React.Component {

@@ -897,7 +1216,37 @@ constructor(props) {

this.state = {
selectedProcess: null
tailingMainLogfile: false,
selectedProcess: null,
commands: commands$1
};
}
/**
* Permit child components to add/remove command sets, and re-render the command menu when they
* do so.
*/
getChildContext() {
return {
commands: this.state.commands,
addCommandSet: this.addCommandSet.bind(this),
removeCommandSet: this.removeCommandSet.bind(this)
};
}
addCommandSet(name, commands) {
this.setState({ commands: addCommandSet(name, commands) });
}
removeCommandSet(name) {
this.setState({ commands: removeCommandSet(name) });
}
componentDidMount() {
this.addCommandSet(COMMAND_SET_NAME$1, getCommandSet(this));
// Our various children have to be focused--not our root element--in order to enable keyboard
// navigation thereof. But then this means that we have to listen for the children's keypress
// events, using the special bubbling syntax https://github.com/chjj/blessed#event-bubbling, and
// have to do so manually (ugh): https://github.com/Yomguithereal/react-blessed/issues/61
this.el.on('element keypress', this.onElementKeypress.bind(this));
// Restore the last-selected process when we load, if we didn't cleanly shut down.

@@ -930,2 +1279,17 @@ // Perhaps at some later point we will wish to always restore the last-selected process.

componentWillUnmount() {
this.removeCommandSet(COMMAND_SET_NAME$1);
}
// Whatever keys we handle here, should also be withheld from the process table in
// `tableShouldHandleKeypress` below.
onElementKeypress(el, ch, key) {
// Forward keypresses to the command menu since we have to keep our various children focused.
this.commandMenu.onKeypress(key);
}
tableShouldHandleKeypress(ch, key) {
return !this.commandMenu.willHandleKeypress(key);
}
// I don't think that react-blessed@0.2.1 supports `getDerivedStateFromProps`, that wasn't being

@@ -955,2 +1319,6 @@ // called. react-blessed doesn't support `UNSAFE_componentWillReceiveProps` either so hopefully

}
if (this.state.tailingMainLogfile !== prevState.tailingMainLogfile) {
this.addCommandSet(COMMAND_SET_NAME$1, getCommandSet(this));
}
}

@@ -976,3 +1344,3 @@

// through `notifier.notify` to `terminal-notifier` but it doesn't work for some reason. :\
exec('open -a Terminal').catch(err => screen.debug('Could not activate Terminal:', err));
exec$1('open -a Terminal').catch(err => screen.debug('Could not activate Terminal:', err));

@@ -998,9 +1366,20 @@ // Show the logs for the (current version of) the process if it's still running (safety

render() {
return this.state.selectedProcess ? React__default.createElement(ProcessDetails, {
process: this.state.selectedProcess,
onClose: this.onDeselect.bind(this)
}) : React__default.createElement(ProcessTable, {
processes: this.props.processes,
onSelect: this.onSelect.bind(this)
});
return React__default.createElement(
'box',
{ ref: el => this.el = el },
this.state.tailingMainLogfile ? React__default.createElement(FileLog, {
logfile: this.props.mainLogfile,
focused: true
}) : this.state.selectedProcess ? React__default.createElement(ProcessDetails, {
process: this.state.selectedProcess,
onClose: this.onDeselect.bind(this)
}) : React__default.createElement(ProcessTable, {
processes: this.props.processes,
onSelect: this.onSelect.bind(this),
shouldHandleKeypress: this.tableShouldHandleKeypress.bind(this)
}),
React__default.createElement(CommandMenu, {
ref: menu => this.commandMenu = menu
})
);
}

@@ -1010,2 +1389,3 @@ }

Console.propTypes = {
mainLogfile: PropTypes.string.isRequired,
processes: PropTypes.array.isRequired,

@@ -1019,2 +1399,8 @@ notifications: PropTypes.bool

Console.childContextTypes = {
commands: PropTypes.array,
addCommandSet: PropTypes.func.isRequired,
removeCommandSet: PropTypes.func.isRequired
};
const OOM_CHECK_DELAY = 30 * 1000;

@@ -1059,34 +1445,3 @@

const exec$1 = promiseCallbacks.promisify(require('child_process').exec);
/**
* Determines whether the process has crashed due to a port conflict, and if so, returns the
* port in question.
*
* @param {Process} process - The process to analyze.
*
* @return {Int|null} The port in conflict, if any, or `null` if none.
*/
function detectPortConflict(process) {
const { state, description } = process.effectiveState;
if (state === STATES.FATAL) {
const portString = _.last(description.match(/EADDRINUSE.+?(\d+)/));
if (portString) return parseInt(portString, 10);
}
return null;
}
/**
* Clears a port conflict by killing the process listening to that port.
*
* @param {Int} port - The port to clear.
*
* @return {Promise<Error>} A promise that resolves to `undefined` if the port is successfully
* cleared, otherwise rejects with the error.
*/
async function clearPortConflict(port) {
return exec$1(`kill -9 $(lsof -ti :${port})`);
}
const { readFile: readFile$1, readdir, unlink } = promiseCallbacks.promisify.methods(require('fs'), ['readFile', 'readdir', 'unlink']);
const { readFile: readFile$2, readdir, unlink } = promiseCallbacks.promisify.methods(require('fs'), ['readFile', 'readdir', 'unlink']);
const mkdirp$1 = promiseCallbacks.promisify(require('mkdirp'));

@@ -1142,3 +1497,3 @@

try {
state = JSON.parse((await readFile$1(path$$1, 'utf8')).trim());
state = JSON.parse((await readFile$2(path$$1, 'utf8')).trim());
} catch (e) {

@@ -1155,6 +1510,6 @@ // This update was the file being deleted.

supervisor: {
port,
pollInterval = 1000,
fixPortConflicts = true
}
client,
pollInterval = 1000
},
fixPortConflicts = true
}) {

@@ -1168,5 +1523,3 @@ super();

// TODO(jeff): Log connection errors here; for some reason this crashes the process without even
// an uncaught exception.
this._supervisor = promiseCallbacks.promisify.all(supervisord.connect(`http://localhost:${port}`));
this._supervisor = client;

@@ -1270,3 +1623,3 @@ this._probeMonitor = new ProbeMonitor().on('update', this._onProcessUpdate.bind(this)).on('error', err => screen.debug('Error monitoring process states:', err));

function start({ port, notifications }) {
var start = (async function start({ port, notifications }) {
let stopOOMCheck;

@@ -1279,3 +1632,3 @@

return new Promise((resolve, reject) => {
try {
clearShutdown();

@@ -1285,32 +1638,37 @@

// TODO(jeff): Distinguish between fatal and non-fatal errors.
const processMonitor = new ProcessMonitor({ supervisor: { port } }).on('error', reject);
const supervisor = createClient(port);
const mainLogfile = await getMainLogfile(supervisor);
// Load all processes, then render, to avoid a flash as they load in (including probe states).
processMonitor.start().then(() => {
function renderApp() {
reactBlessed.render(React__default.createElement(Console, {
processes: processMonitor.processes,
notifications: notifications
}), screen);
}
renderApp();
processMonitor.on('update', renderApp);
}).catch(reject);
await new Promise((resolve, reject) => {
// TODO(jeff): Distinguish between fatal and non-fatal errors.
const processMonitor = new ProcessMonitor({ supervisor: { client: supervisor } }).on('error', reject);
// Don't allow components to lock-out our control keys, Ctrl-C (exit) and F12 (debug log).
screen.ignoreLocked = ['C-c', 'f12'];
// Load all processes, then render, to avoid a flash as they load in (including probe states).
processMonitor.start().then(() => {
function renderApp() {
reactBlessed.render(React__default.createElement(Console, {
mainLogfile: mainLogfile,
processes: processMonitor.processes,
notifications: notifications
}), screen);
}
renderApp();
processMonitor.on('update', renderApp);
}).catch(reject);
screen.key(['C-c'], () => {
markCleanShutdown();
resolve();
// Don't allow components to lock-out our control keys, Ctrl-C (exit) and F12 (debug log).
screen.ignoreLocked = ['C-c', 'f12'];
screen.key(['C-c'], () => {
markCleanShutdown();
resolve();
});
});
}).then(teardown).catch(err => {
// Make sure to reset the terminal before returning the error, otherwise the client won't be
// able to print the error to the logs.
} finally {
// Note that it's important that this resets the terminal before returning the error, otherwise
// the client won't be able to print the error to the logs.
teardown();
throw err;
});
}
}
});
module.exports = start;
{
"name": "custody-cli",
"version": "0.3.1",
"version": "0.4.0",
"description": "A developer-oriented frontend for Supervisor",

@@ -25,2 +25,3 @@ "bin": "bin/custody-cli.js",

"fuzzy": "^0.1.3",
"ini": "^1.3.5",
"kexec": "^3.0.0",

@@ -27,0 +28,0 @@ "lodash.xor": "^4.5.0",

SocketSocket SOC 2 Logo

Product

  • Package Alerts
  • Integrations
  • Docs
  • Pricing
  • FAQ
  • Roadmap

Stay in touch

Get open source security insights delivered straight into your inbox.


  • Terms
  • Privacy
  • Security

Made with ⚡️ by Socket Inc