querysub 0.572.0 → 0.573.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "querysub",
3
- "version": "0.572.0",
3
+ "version": "0.573.0",
4
4
  "main": "index.js",
5
5
  "license": "MIT",
6
6
  "note1": "note on node-forge fork, see https://github.com/digitalbazaar/forge/issues/744 for details",
@@ -221,6 +221,8 @@ let launchesPerService = new Map<string, number>();
221
221
  let lastLaunchedTimePerService = new Map<string, number>();
222
222
 
223
223
  const LAUNCH_VERIFY_DELAY = 2000;
224
+ // How far a re-read start time may drift from the recorded one and still be the same process. Well under any interval a pid could be reused in, and wide enough for sources that only resolve to the second.
225
+ const START_TIME_TOLERANCE = timeInSecond * 5;
224
226
 
225
227
  /** Every instance of every service that belongs to this machine, with its config already resolved for that instance - the command has this entry's template variables substituted, and a version with no entry at this index is marked as deploying nothing here. */
226
228
  function getMachineInstances(configs: ServiceConfig[], machineId: string): { config: ServiceConfig; index: number }[] {
@@ -497,7 +499,8 @@ const resyncServicesBase = runInSerial(measureWrap(async function resyncServices
497
499
  await syncProcessRecords({
498
500
  isAlive: async record => {
499
501
  if (!running.has(record.pid)) return false;
500
- return await getProcessStartTime(record.pid) === record.startTime;
502
+ // Compared with a tolerance rather than exactly: the sources we read a start time from only resolve to the second, and they do not all round the same way, so an exact match would call a live process dead
503
+ return Math.abs(await getProcessStartTime(record.pid) - record.startTime) <= START_TIME_TOLERANCE;
501
504
  },
502
505
  now: Date.now(),
503
506
  });
@@ -99,6 +99,11 @@ export async function readProcessRecord(folder: string, pid: string, startTime:
99
99
  return JSON.parse(await fs.promises.readFile(file, "utf8")) as ProcessRecord;
100
100
  }
101
101
 
102
+ /** The live record for a pid in this folder. The pid is what identifies a process, so a start time that reads back a second off - the sources only resolve to the second - must not make us miss the record and write a duplicate. */
103
+ export async function findProcessRecordByPid(folder: string, pid: string): Promise<ProcessRecord | undefined> {
104
+ return (await readFolderRecords(folder)).find(x => x.pid === pid && x.deadTime === undefined);
105
+ }
106
+
102
107
  export async function writeProcessRecord(record: ProcessRecord): Promise<void> {
103
108
  await fs.promises.mkdir(path.join(record.folder, PROCESS_FOLDER), { recursive: true });
104
109
  await fs.promises.writeFile(getRecordPath(record.folder, record.pid, record.startTime), JSON.stringify(record));
@@ -13,7 +13,7 @@ import { forceRemoveNode } from "../-f-node-discovery/NodeDiscovery";
13
13
  import { fsExistsAsync } from "../fs";
14
14
  import { PromiseObj } from "../promise";
15
15
  import { SERVICE_FOLDER, SERVICE_NODE_FILE_NAME } from "./machineSchema";
16
- import { ProcessRecord, getLogPipeScript, getLogTailScript, getProcessLogPath, getPipeScriptPath, getTailScriptPath, readProcessRecord, writeProcessRecord } from "./processLogs";
16
+ import { ProcessRecord, getLogPipeScript, getLogTailScript, getProcessLogPath, getPipeScriptPath, getTailScriptPath, findProcessRecordByPid, writeProcessRecord } from "./processLogs";
17
17
  import { setPreciseTimeout } from "../misc";
18
18
 
19
19
  // Running, inspecting and killing the tmux screens services run in. This layer only knows "here is a configuration, run it" - which version should be running when is the deploy logic's problem.
@@ -163,11 +163,23 @@ const getAllParentPids = measureWrap(async function getAllParentPids(): Promise<
163
163
  let obj = textTableLineToObj(table) as { PID: string; PPID: string }[];
164
164
  return new Set(obj.map(x => x.PPID));
165
165
  });
166
- /** The start time the OS reports for a pid, as an epoch time in milliseconds like every other time we store. Asked of the machine rather than remembered, so it stays true across our restarts and catches a reused pid. Returns 0 when the process is gone. */
166
+ /** The start time the OS reports for a pid, as an epoch time in milliseconds like every other time we store. Asked of the machine rather than remembered, so it stays true across our restarts and catches a reused pid. Returns 0 when the process is gone, or when neither source can answer. */
167
167
  export const getProcessStartTime = measureWrap(async function getProcessStartTime(pid: string): Promise<number> {
168
- // ps only resolves to the second, so this is second-accurate - which is plenty to tell two processes on the same pid apart
169
- let started = await runPromise(`date -d "$(ps -o lstart= -p ${pid})" +%s`, { quiet: true });
170
- return (parseInt(started.trim()) || 0) * 1000;
168
+ // /proc/<pid> is created when the process is, so its ctime IS the start time - no date parsing, and nothing to get wrong about locales or ps output formats
169
+ try {
170
+ let started = await runPromise(`stat -c %Y /proc/${pid}`, { quiet: true });
171
+ let seconds = parseInt(started.trim()) || 0;
172
+ if (seconds) return seconds * 1000;
173
+ } catch {
174
+ // No procfs (or the process is gone), so fall through to ps
175
+ }
176
+ try {
177
+ // Second-accurate, which is plenty to tell two processes on the same pid apart
178
+ let started = await runPromise(`date -d "$(ps -o lstart= -p ${pid})" +%s`, { quiet: true });
179
+ return (parseInt(started.trim()) || 0) * 1000;
180
+ } catch {
181
+ return 0;
182
+ }
171
183
  });
172
184
 
173
185
  export const isScreenRunningProcess = measureWrap(async function isScreenRunningProcess(pid: string): Promise<boolean> {
@@ -263,9 +275,9 @@ export async function ensureProcessRecord(config: {
263
275
  record: Omit<ProcessRecord, "pid" | "startTime" | "folder" | "screenName">;
264
276
  }): Promise<void> {
265
277
  let pid = config.panePid;
278
+ // A start time we could not read is still recorded: a process with no record at all is invisible everywhere, which is worse than one whose identity is only its pid
266
279
  let startTime = await getProcessStartTime(pid);
267
- if (!startTime) return;
268
- let existing = await readProcessRecord(config.folder, pid, startTime);
280
+ let existing = await findProcessRecordByPid(config.folder, pid);
269
281
  if (existing) {
270
282
  // A takeover renames the session, so the record follows the name it now runs under
271
283
  if (existing.screenName === config.screenName) return;
@@ -340,9 +352,7 @@ export async function takeoverScreenLocked(config: {
340
352
 
341
353
  /** Points a process's record at the session name it now runs under. The pid is the key, so this reads exactly one file. */
342
354
  async function renameProcessScreen(config: { folder: string; panePid: string; screenName: string }): Promise<void> {
343
- let startTime = await getProcessStartTime(config.panePid);
344
- if (!startTime) return;
345
- let record = await readProcessRecord(config.folder, config.panePid, startTime);
355
+ let record = await findProcessRecordByPid(config.folder, config.panePid);
346
356
  if (!record || record.screenName === config.screenName) return;
347
357
  await writeProcessRecord({ ...record, screenName: config.screenName });
348
358
  }