@yusukeshib/pi-babysit 0.6.20 → 0.6.21
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/index.ts +64 -13
- package/package.json +1 -1
package/index.ts
CHANGED
|
@@ -2851,34 +2851,55 @@ const remoteTerminalTails = new Set<string>();
|
|
|
2851
2851
|
// append-only mirror built from incremental `log --since --raw --json` reads.
|
|
2852
2852
|
// The cursor records the remote raw-byte offset and the local mirror size it
|
|
2853
2853
|
// corresponds to, so an interrupted append is truncated instead of duplicated.
|
|
2854
|
-
type RemoteLogCursor = { remoteOffset: number; localBytes: number; complete: boolean };
|
|
2854
|
+
type RemoteLogCursor = { remoteOffset: number; localBytes: number; timestampBytes: number; complete: boolean };
|
|
2855
2855
|
type RemoteLogSyncState = { inFlight?: Promise<boolean>; lastSyncAt: number; lastChanged: boolean; error?: string };
|
|
2856
2856
|
const remoteLogSyncs = new Map<string, RemoteLogSyncState>();
|
|
2857
2857
|
const REMOTE_LOG_SYNC_INTERVAL_MS = 1000;
|
|
2858
|
+
// Hosts whose babysit predates `log --timestamps`; their mirrors have no dates.
|
|
2859
|
+
const remoteLogTimestampUnsupported = new Set<string>();
|
|
2858
2860
|
|
|
2859
2861
|
export function remoteLogMirrorPath(id: string): string {
|
|
2860
2862
|
return path.join(ROOT, "remote-logs", id, "output.log");
|
|
2861
2863
|
}
|
|
2862
2864
|
|
|
2865
|
+
function remoteLogTimestampsPath(id: string): string {
|
|
2866
|
+
return path.join(path.dirname(remoteLogMirrorPath(id)), "output.timestamps.jsonl");
|
|
2867
|
+
}
|
|
2868
|
+
|
|
2863
2869
|
function remoteLogCursorPath(id: string): string {
|
|
2864
2870
|
return path.join(path.dirname(remoteLogMirrorPath(id)), "cursor.json");
|
|
2865
2871
|
}
|
|
2866
2872
|
|
|
2867
2873
|
function readRemoteLogCursor(id: string): RemoteLogCursor {
|
|
2868
2874
|
const file = remoteLogMirrorPath(id);
|
|
2869
|
-
|
|
2875
|
+
const sidecar = remoteLogTimestampsPath(id);
|
|
2876
|
+
const empty: RemoteLogCursor = { remoteOffset: 0, localBytes: 0, timestampBytes: 0, complete: false };
|
|
2877
|
+
let cursor = empty;
|
|
2870
2878
|
try {
|
|
2871
2879
|
const parsed = JSON.parse(fs.readFileSync(remoteLogCursorPath(id), "utf8"));
|
|
2872
|
-
|
|
2873
|
-
|
|
2880
|
+
const valid = (value: unknown) => Number.isSafeInteger(value) && (value as number) >= 0;
|
|
2881
|
+
if (valid(parsed.remoteOffset) && valid(parsed.localBytes)) {
|
|
2882
|
+
cursor = { remoteOffset: parsed.remoteOffset, localBytes: parsed.localBytes, timestampBytes: valid(parsed.timestampBytes) ? parsed.timestampBytes : 0, complete: parsed.complete === true };
|
|
2874
2883
|
}
|
|
2875
2884
|
} catch {}
|
|
2876
|
-
|
|
2877
|
-
|
|
2878
|
-
|
|
2879
|
-
|
|
2880
|
-
|
|
2885
|
+
const sizeOf = (target: string) => {
|
|
2886
|
+
try {
|
|
2887
|
+
return fs.statSync(target).size;
|
|
2888
|
+
} catch {
|
|
2889
|
+
return 0;
|
|
2890
|
+
}
|
|
2891
|
+
};
|
|
2892
|
+
const size = sizeOf(file);
|
|
2893
|
+
const sidecarSize = sizeOf(sidecar);
|
|
2894
|
+
if (size < cursor.localBytes || sidecarSize < cursor.timestampBytes) {
|
|
2895
|
+
// Committed data is missing; restart the mirror from scratch.
|
|
2896
|
+
if (size > 0) fs.truncateSync(file, 0);
|
|
2897
|
+
if (sidecarSize > 0) fs.truncateSync(sidecar, 0);
|
|
2898
|
+
return empty;
|
|
2899
|
+
}
|
|
2900
|
+
// Drop an append that was interrupted before its cursor was committed.
|
|
2881
2901
|
if (size > cursor.localBytes) fs.truncateSync(file, cursor.localBytes);
|
|
2902
|
+
if (sidecarSize > cursor.timestampBytes) fs.truncateSync(sidecar, cursor.timestampBytes);
|
|
2882
2903
|
return cursor;
|
|
2883
2904
|
}
|
|
2884
2905
|
|
|
@@ -2900,14 +2921,23 @@ export async function syncRemoteLog(id: string): Promise<boolean> {
|
|
|
2900
2921
|
if (!fs.existsSync(file)) fs.writeFileSync(file, "");
|
|
2901
2922
|
const cursor = readRemoteLogCursor(id);
|
|
2902
2923
|
if (cursor.complete) return false;
|
|
2903
|
-
const
|
|
2924
|
+
const host = readMeta(id)?.host ?? "";
|
|
2925
|
+
const readArgs = ["log", "-s", id, "--since", String(cursor.remoteOffset), "--raw", "--json"];
|
|
2926
|
+
let withTimestamps = !remoteLogTimestampUnsupported.has(host);
|
|
2927
|
+
let result = await bs(withTimestamps ? [...readArgs, "--timestamps"] : readArgs, { unboundedStdout: true });
|
|
2928
|
+
if (withTimestamps && result.code !== 0 && /--timestamps/.test(result.stderr)) {
|
|
2929
|
+
// Older remote babysit: keep mirroring text, without capture dates.
|
|
2930
|
+
remoteLogTimestampUnsupported.add(host);
|
|
2931
|
+
withTimestamps = false;
|
|
2932
|
+
result = await bs(readArgs, { unboundedStdout: true });
|
|
2933
|
+
}
|
|
2904
2934
|
if (result.code !== 0) {
|
|
2905
2935
|
const error = clip(result.stderr.trim() || result.stdout.trim() || `babysit log exited ${result.code}`, 500);
|
|
2906
2936
|
const changed = state.error !== error;
|
|
2907
2937
|
state.error = error;
|
|
2908
2938
|
return changed;
|
|
2909
2939
|
}
|
|
2910
|
-
let response: { text?: unknown; offset?: unknown; done?: unknown };
|
|
2940
|
+
let response: { text?: unknown; offset?: unknown; done?: unknown; timestamps?: unknown };
|
|
2911
2941
|
try {
|
|
2912
2942
|
response = JSON.parse(result.stdout);
|
|
2913
2943
|
} catch {
|
|
@@ -2921,18 +2951,38 @@ export async function syncRemoteLog(id: string): Promise<boolean> {
|
|
|
2921
2951
|
if ((response.offset as number) < cursor.remoteOffset) {
|
|
2922
2952
|
// The remote log was replaced or truncated (e.g. restart); rebuild the mirror.
|
|
2923
2953
|
fs.truncateSync(file, 0);
|
|
2924
|
-
|
|
2954
|
+
fs.rmSync(remoteLogTimestampsPath(id), { force: true });
|
|
2955
|
+
writeRemoteLogCursor(id, { remoteOffset: 0, localBytes: 0, timestampBytes: 0, complete: false });
|
|
2925
2956
|
return true;
|
|
2926
2957
|
}
|
|
2927
2958
|
const hadError = state.error !== undefined;
|
|
2928
2959
|
state.error = undefined;
|
|
2929
2960
|
const bytes = Buffer.from(response.text, "utf8");
|
|
2930
2961
|
if (bytes.length > 0) fs.appendFileSync(file, bytes);
|
|
2962
|
+
const remoteStart = cursor.remoteOffset;
|
|
2963
|
+
const remoteEnd = response.offset as number;
|
|
2964
|
+
let timestampBytes = cursor.timestampBytes;
|
|
2965
|
+
// Remote rows use remote raw offsets. They map 1:1 onto the mirror only
|
|
2966
|
+
// when the chunk arrived byte-for-byte; a lossy UTF-8 replacement shifts
|
|
2967
|
+
// offsets, so that chunk is left undated rather than mis-dated.
|
|
2968
|
+
if (withTimestamps && Array.isArray(response.timestamps) && bytes.length === remoteEnd - remoteStart) {
|
|
2969
|
+
const rows = response.timestamps.flatMap((row) => {
|
|
2970
|
+
if (!Array.isArray(row) || row.length !== 3 || !row.every((value) => Number.isSafeInteger(value) && value >= 0)) return [];
|
|
2971
|
+
const [start, length, time] = row as number[];
|
|
2972
|
+
const begin = Math.max(start, remoteStart);
|
|
2973
|
+
const end = Math.min(start + length, remoteEnd);
|
|
2974
|
+
return end > begin ? [`[${cursor.localBytes + begin - remoteStart},${end - begin},${time}]\n`] : [];
|
|
2975
|
+
}).join("");
|
|
2976
|
+
if (rows) {
|
|
2977
|
+
fs.appendFileSync(remoteLogTimestampsPath(id), rows);
|
|
2978
|
+
timestampBytes += Buffer.byteLength(rows);
|
|
2979
|
+
}
|
|
2980
|
+
}
|
|
2931
2981
|
const advanced = (response.offset as number) > cursor.remoteOffset || bytes.length > 0;
|
|
2932
2982
|
// `done` is reported before the worker's final flush, so completion
|
|
2933
2983
|
// requires a terminal read that returned nothing new.
|
|
2934
2984
|
const complete = response.done === true && !advanced;
|
|
2935
|
-
writeRemoteLogCursor(id, { remoteOffset:
|
|
2985
|
+
writeRemoteLogCursor(id, { remoteOffset: remoteEnd, localBytes: cursor.localBytes + bytes.length, timestampBytes, complete });
|
|
2936
2986
|
return advanced || complete || hadError;
|
|
2937
2987
|
})().then((changed) => {
|
|
2938
2988
|
state.lastChanged = changed;
|
|
@@ -4127,6 +4177,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
4127
4177
|
remoteTails.clear();
|
|
4128
4178
|
remoteTerminalTails.clear();
|
|
4129
4179
|
remoteLogSyncs.clear();
|
|
4180
|
+
remoteLogTimestampUnsupported.clear();
|
|
4130
4181
|
pollNeeded = true;
|
|
4131
4182
|
const retentionDays = Number(process.env.PI_BABYSIT_RETENTION_DAYS ?? "3");
|
|
4132
4183
|
if (
|