121 lines
8.5 KiB
JavaScript
121 lines
8.5 KiB
JavaScript
// Команды снаружи. Панель, бот или сама рука владельца кладут файл в `.night/commands/`,
|
|
// а читает его НАДЗИРАТЕЛЬ — работник этого модуля не видит вовсе: он другой процесс
|
|
// в другом каталоге и разговаривает только файлами своего почтового ящика.
|
|
//
|
|
// 🔴 Файл заведён ЧАСТЬЮ. Здесь только приём команд; `closeItem`, `afterItemDone`,
|
|
// `afterTaskDone` и `deferredTaskGate` дописывает в этот же файл кусок 4 — заводить
|
|
// его заново ему не надо.
|
|
import { existsSync, readdirSync, unlinkSync } from 'node:fs';
|
|
import { join } from 'node:path';
|
|
import { readJson, writeJsonAtomic } from './state.mjs';
|
|
|
|
// Закрытый список, и порядок в нём значимый: «стоп всё» главнее «стопа», «стоп» главнее
|
|
// «паузы». Слово не из списка не читается вовсе — выдумывать, что оно значит, нельзя.
|
|
// 🔴 Список вывезен наружу нарочно: надзиратель называет его владельцу в сводке, когда
|
|
// команду не понял. Второй такой же список рядом разошёлся бы с этим в первый же месяц.
|
|
export const COMMAND_WORDS = ['stopall', 'stop', 'pause', 'resume'];
|
|
const CMDS = COMMAND_WORDS;
|
|
|
|
export function readCommand(commandsDirPath, workerId) {
|
|
// 🪤 Каталог команд заводит рука владельца, и в первую ночь его может не быть.
|
|
// Брошенная тут ошибка убила бы надзирателя молча — тот самый класс беды, из-за
|
|
// которого 334 прогона умирали три месяца незамеченными.
|
|
if (!commandsDirPath || !existsSync(commandsDirPath)) return null;
|
|
let files;
|
|
try {
|
|
files = readdirSync(commandsDirPath);
|
|
} catch {
|
|
return null; // каталог недоступен — это отсутствие команды, а не повод падать
|
|
}
|
|
for (const cmd of CMDS) {
|
|
const all = `all.${cmd}.json`;
|
|
const mine = `${workerId}.${cmd}.json`;
|
|
// Общая идёт первой: она касается и того, кому лично ничего не давали.
|
|
for (const name of [all, mine]) {
|
|
if (!files.includes(name)) continue;
|
|
// Порванный файл — отсутствие сведений, а не мусор: слово команды стоит
|
|
// в самом имени файла, и его достаточно.
|
|
const body = readJson(join(commandsDirPath, name)) ?? {};
|
|
// 🪤 Общую команду, которую ЭТОТ работник уже исполнил, второй раз не отдаём:
|
|
// иначе он гасился бы по ней снова и снова, пока файл ждёт остальных.
|
|
if (name === all && (body.done ?? []).includes(workerId)) continue;
|
|
// 🔴🔴 `cmd` стоит ПОСЛЕ тела, а не до него, и это не вкус. Слово команды берётся
|
|
// из ИМЕНИ файла — оно и есть закрытый список. Поставь мы `cmd` перед `...body`,
|
|
// и тело файла перебило бы его: положи кто-нибудь в `w-1.pause.json` тело
|
|
// {"cmd":"stopall"} — работник был бы погашен, хотя имя файла говорит «пауза».
|
|
// Тем же способом обходился бы и весь закрытый список `CMDS`. Тело команды несёт
|
|
// сведения (кто, когда, почему), а не слово; слово — только из имени.
|
|
return { ...body, cmd, file: name, common: name === all };
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
|
|
// 🔴🔴 Владелец узнаёт, что его команду НЕ ПОНЯЛИ (решение диспетчера). Слово не из
|
|
// закрытого списка исполнять нельзя — выдумывать его смысл мы не станем. Но и молчать
|
|
// нельзя: владелец положил файл, ушёл спать и думает, что «пауза» нажата, а её никто
|
|
// не понял. Молчание в ответ на команду хозяина недопустимо.
|
|
// 🪤 Эта работа НИЧЕГО не удаляет и ничего не исполняет — только называет. Файл владельца
|
|
// остаётся лежать: он и есть доказательство, и утром хозяин увидит его на месте, а не
|
|
// будет гадать, куда тот делся.
|
|
export function unknownCommands(commandsDirPath, workerId) {
|
|
if (!commandsDirPath || !existsSync(commandsDirPath)) return [];
|
|
let files;
|
|
try {
|
|
files = readdirSync(commandsDirPath);
|
|
} catch {
|
|
return []; // каталог недоступен — сказать нечего, и падать не о чем
|
|
}
|
|
const out = [];
|
|
for (const name of files) {
|
|
// Команда — это ровно `<кому>.<слово>.json`. Всё прочее в каталоге не наше дело:
|
|
// объявлять чужой файл «непонятой командой» — значит пугать владельца на пустом месте.
|
|
const razbor = /^(.+)\.([^.]+)\.json$/.exec(name);
|
|
if (!razbor) continue;
|
|
const [, komu, slovo] = razbor;
|
|
// Чужая личная команда — не наша забота: о ней скажет надзиратель того работника,
|
|
// иначе одну и ту же строку написали бы семеро разом.
|
|
if (komu !== 'all' && komu !== workerId) continue;
|
|
if (CMDS.includes(slovo)) continue; // понятную здесь не называем
|
|
out.push({ file: name, word: slovo, common: komu === 'all' });
|
|
}
|
|
return out;
|
|
}
|
|
|
|
// 🪤 Прежде эта работа стирала И общий, И личный файл разом. Из-за этого «стоп всё»
|
|
// съедал первый же прочитавший, а остальные шестеро продолжали работать при отданной
|
|
// команде — и владелец видел «остановлено», когда ночь шла дальше. Теперь личная команда
|
|
// убирается сразу, а общая — только когда её исполнили ВСЕ, кого она касается; до тех пор
|
|
// в её файле копится список исполнивших.
|
|
export function consumeCommand(commandsDirPath, workerId, cmd, { concerns = [] } = {}) {
|
|
const result = { personal_removed: false, common_removed: false, done: [] };
|
|
if (!commandsDirPath || !existsSync(commandsDirPath)) return result;
|
|
|
|
const mine = join(commandsDirPath, `${workerId}.${cmd}.json`);
|
|
if (existsSync(mine)) {
|
|
unlinkSync(mine);
|
|
result.personal_removed = true;
|
|
}
|
|
|
|
const all = join(commandsDirPath, `all.${cmd}.json`);
|
|
if (!existsSync(all)) return result;
|
|
|
|
// 🔴 Список исполнивших живёт В ФАЙЛЕ, а не в памяти надзирателя: надзиратели — разные
|
|
// процессы, и каждый видел бы свой собственный список. Перечитываем прямо перед записью,
|
|
// чтобы не затереть отметку соседа, поставленную секунду назад.
|
|
const body = readJson(all) ?? {};
|
|
const done = Array.from(new Set([...(body.done ?? []), workerId]));
|
|
result.done = done;
|
|
|
|
// Кого команда касается, знает зовущий: надзиратель берёт состав прогона из `run.json`.
|
|
// Списка нет — файл НЕ удаляем: лучше лишний раз промолчать, чем снять команду с тех,
|
|
// до кого она не дошла.
|
|
if (concerns.length && concerns.every(w => done.includes(w))) {
|
|
unlinkSync(all);
|
|
result.common_removed = true;
|
|
return result;
|
|
}
|
|
writeJsonAtomic(all, { ...body, done });
|
|
return result;
|
|
}
|