◆ restart 待ち状態が考慮されていない
◆ すでに停止状態なので restart タイマーを解除して exit イベント時の処理を行う

けっこう前からなのですが forever で起動したスクリプトが再起動を待機しているときに stop コマンドを実行するといつまで経っても

forever stop <name>

のコマンドが終了しません

Ctrl-C で止めて forever list で状態をみると STOPPED にはなっています

大した問題じゃないし しばらくすれば修正されてるだろうと放置していましたが全然修正される気配がないので自分で対応しました
そもそも forever 自体があまり活発なプロジェクトじゃないようでコミットもほとんどないようです

問題点

再現するコードはこういうのです


[index.js]
setTimeout(() => console.log(1), 1000)

[app.json]
{
"uid": "app",
"append": true,
"watch": false,
"sourceDir": "C:\\tmp\\forever1",
"script": "index.js",
"logFile": "C:\\tmp\\forever1\\log",
"minUptime": 3000,
"spinSleepTime": 5000
}

すぐに終了するスクリプトを minUptime と spinSleepTime を指定して起動します
オプションの意味は minUptime より短い起動時間で終了した場合に spinSleepTime だけ待ってから再起動するというものです

これを起動して終了させます

forever start app.json

# (1 秒まって)

forever stop app

これで 最後の stop コマンドが終了しなくなります

forever start app.json & forever stop app

のように起動後すぐに stop コマンドを実行して 1000ms 経過するまえに stop コマンドを実行できれば正常に stop します
forever stop コマンドで終了したスクリプトの情報が出てコマンド自体も終了します

原因

調べたところ cli で stop コマンドを送信したあと結果を通知するイベントがいつまで経っても発生しなくてコマンドが終了しない状態になっていました
実際にスクリプトの停止とイベントを送信する forever のデーモンプロセスを見てみました
https://github.com/foreversd/forever-monitor/blob/1.7.1/lib/forever-monitor/monitor.js

kill を行って exit イベントが起きたら stop イベントを送信してるようです
しかし restart 待ちの状態ではすでに停止しているので exit イベントは発生しません
また restart のタイマーをセットする部分を見てもタイマーの返り値は捨てているので restart 待ちかどうかもわかりません

修正

restart 待機中なら すでに停止してるのでタイマーを解除して stop のイベントを送るように修正します

まずは exit イベントが起きたときの処理です
self.restartTimer にタイマー ID を保存します
restartChild が正常に行えた場合は restartTimer に null を入れて restart 待ち状態じゃないとわかるようにします

  child.on('exit', function (code, signal) {
var spinning = Date.now() - self.ctime < self.minUptime;
child.removeListener('message', onMessage);
self.emit('exit:code', code, signal);

function letChildDie() {
self.running = false;
self.forceStop = false;
self.emit('exit', self, spinning);
}

function restartChild() {
self.restartTimer = null;
self.forceRestart = false;
process.nextTick(function () {
self.start(true);
});
}

self.times++;

if (self.forceStop || (self.times >= self.max && !self.forceRestart)
|| (spinning && typeof self.spinSleepTime !== 'number') && !self.forceRestart) {
letChildDie();
}
else if (spinning) {
self.restartTimer = setTimeout(restartChild, self.spinSleepTime);
}
else {
restartChild();
}
});

次に kill メソッドを修正します
stop でも kill メソッドを実行しています

once で設定されている exit イベントのリスナを onExit 関数にします
定義は var が並んでる下辺りに持ってきます
そしてその下の if 文に else if を追加して さっき作った restartTimer があるかをチェックします
restartTimer があれば restart 待機中なのでタイマーを解除して restartTimer を null にしたあとに onExit 関数を実行します
最後に once で設定されていた exit リスナの関数を onExit にしておしまいです

Monitor.prototype.kill = function (forceStop) {
var child = this.child,
self = this,
timer;

function onExit() {
self.emit('stop', self.childData);
if (self.forceRestart && !self.running) {
self.start(true);
}
}


if (!child || (!this.running && !this.forceRestart)) {
process.nextTick(function () {
self.emit('error', new Error('Cannot stop process that is not running.'));
});
}
else if (this.restartTimer) {
clearTimeout(this.restartTimer);
this.restartTimer = null;
onExit();
}

else {
//
// Set an instance variable here to indicate this
// stoppage is forced so that when `child.on('exit', ..)`
// fires in `Monitor.prototype.start` we can short circuit
// and prevent auto-restart
//
if (forceStop) {
this.forceStop = true;
//
// If we have a time before we truly kill forcefully, set up a timer
//
if (this.killTTL) {
timer = setTimeout(function () {
common.kill(self.child.pid, self.killTree, self.killSignal || 'SIGKILL');
}, this.killTTL);

child.once('exit', function () {
clearTimeout(timer);
});
}
}

child.once('exit', onExit);

common.kill(this.child.pid, this.killTree, this.killSignal);
}

return this;
};

kill など別のコマンドも絡んできそうで 他に影響してないかはわかりませんがとりあえずこれで restart 待ち状態で stop をした場合にコマンドが終了することはなくなりました

パッチファイル

今 npm でダウンロードできる 1.7.1 を元にしたパッチファイルです

--- old-monitor.js	2019-11-26 22:17:40.145681100 +0900
+++ new-monitor.js 2019-11-26 22:17:48.956340600 +0900
@@ -196,6 +196,7 @@
}

function restartChild() {
+ self.restartTimer = null;
self.forceRestart = false;
process.nextTick(function () {
self.start(true);
@@ -209,7 +210,7 @@
letChildDie();
}
else if (spinning) {
- setTimeout(restartChild, self.spinSleepTime);
+ self.restartTimer = setTimeout(restartChild, self.spinSleepTime);
}
else {
restartChild();
@@ -352,11 +353,23 @@
self = this,
timer;

+ function onExit() {
+ self.emit('stop', self.childData);
+ if (self.forceRestart && !self.running) {
+ self.start(true);
+ }
+ }
+
if (!child || (!this.running && !this.forceRestart)) {
process.nextTick(function () {
self.emit('error', new Error('Cannot stop process that is not running.'));
});
}
+ else if (this.restartTimer) {
+ clearTimeout(this.restartTimer);
+ this.restartTimer = null;
+ onExit();
+ }
else {
//
// Set an instance variable here to indicate this
@@ -380,12 +393,7 @@
}
}

- child.once('exit', function () {
- self.emit('stop', self.childData);
- if (self.forceRestart && !self.running) {
- self.start(true);
- }
- });
+ child.once('exit', onExit);

common.kill(this.child.pid, this.killTree, this.killSignal);
}