cron-daemon-owner.js 2.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112
  1. import crypto from 'crypto';
  2. import { promises as fs } from 'fs';
  3. import net from 'net';
  4. import os from 'os';
  5. import path from 'path';
  6. export const CRON_DAEMON_OWNER_KIND = 'pilotdeck-server';
  7. export const CRON_DAEMON_OWNER_KIND_ENV = 'PILOTDECK_CRON_DAEMON_OWNER_KIND';
  8. export const CRON_DAEMON_OWNER_TOKEN_ENV = 'PILOTDECK_CRON_DAEMON_OWNER_TOKEN';
  9. export const CRON_DAEMON_OWNER_PROCESS_PID_ENV = 'PILOTDECK_CRON_DAEMON_OWNER_PROCESS_PID';
  10. function getPilotDeckConfigHomeDir() {
  11. return process.env.PILOTDECK_CONFIG_DIR || process.env.PILOT_HOME || path.join(os.homedir(), '.pilotdeck');
  12. }
  13. function getCronDaemonOwnerPath() {
  14. return path.join(getPilotDeckConfigHomeDir(), 'cron-daemon', 'owner.json');
  15. }
  16. function getCronDaemonSocketPath() {
  17. return path.join(getPilotDeckConfigHomeDir(), 'cron-daemon.sock');
  18. }
  19. async function readCronDaemonOwner() {
  20. try {
  21. const raw = await fs.readFile(getCronDaemonOwnerPath(), 'utf-8');
  22. const parsed = JSON.parse(raw);
  23. if (
  24. !parsed ||
  25. typeof parsed.kind !== 'string' ||
  26. typeof parsed.token !== 'string' ||
  27. typeof parsed.createdAt !== 'number'
  28. ) {
  29. return null;
  30. }
  31. return parsed;
  32. } catch {
  33. return null;
  34. }
  35. }
  36. function isCurrentProcessCronDaemonOwner(owner) {
  37. return Boolean(
  38. owner &&
  39. owner.kind === process.env[CRON_DAEMON_OWNER_KIND_ENV] &&
  40. owner.token === process.env[CRON_DAEMON_OWNER_TOKEN_ENV]
  41. );
  42. }
  43. function sendCronDaemonRequest(request) {
  44. return new Promise((resolve, reject) => {
  45. const socket = net.createConnection(getCronDaemonSocketPath());
  46. let settled = false;
  47. let buffer = '';
  48. const finalize = (callback, value) => {
  49. if (settled) return;
  50. settled = true;
  51. socket.destroy();
  52. callback(value);
  53. };
  54. socket.setTimeout(1000, () => {
  55. finalize(reject, new Error('Timed out waiting for Cron daemon response'));
  56. });
  57. socket.on('connect', () => {
  58. socket.write(`${JSON.stringify(request)}\n`);
  59. });
  60. socket.on('data', (chunk) => {
  61. buffer += chunk.toString('utf-8');
  62. const newlineIndex = buffer.indexOf('\n');
  63. if (newlineIndex === -1) {
  64. return;
  65. }
  66. const line = buffer.slice(0, newlineIndex);
  67. try {
  68. finalize(resolve, JSON.parse(line));
  69. } catch (error) {
  70. finalize(reject, error);
  71. }
  72. });
  73. socket.on('error', (error) => {
  74. finalize(reject, error);
  75. });
  76. });
  77. }
  78. export async function shutdownOwnedCronDaemon() {
  79. const owner = await readCronDaemonOwner();
  80. if (!isCurrentProcessCronDaemonOwner(owner)) {
  81. return false;
  82. }
  83. try {
  84. const response = await sendCronDaemonRequest({ type: 'shutdown' });
  85. return Boolean(response?.ok);
  86. } catch {
  87. return false;
  88. }
  89. }
  90. export async function _readCronDaemonOwnerForTest() {
  91. return await readCronDaemonOwner();
  92. }
  93. export {
  94. sendCronDaemonRequest
  95. };