@animalabs/agent-framework 0.28.0 → 0.30.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/README.md +88 -25
- package/dist/src/admission/admit.d.ts +90 -0
- package/dist/src/admission/admit.d.ts.map +1 -0
- package/dist/src/admission/admit.js +43 -0
- package/dist/src/admission/admit.js.map +1 -0
- package/dist/src/agent.d.ts +21 -0
- package/dist/src/agent.d.ts.map +1 -1
- package/dist/src/agent.js +47 -7
- package/dist/src/agent.js.map +1 -1
- package/dist/src/api/mcp-server-core.d.ts.map +1 -1
- package/dist/src/api/mcp-server-core.js +3 -12
- package/dist/src/api/mcp-server-core.js.map +1 -1
- package/dist/src/api/server.d.ts.map +1 -1
- package/dist/src/api/server.js +7 -3
- package/dist/src/api/server.js.map +1 -1
- package/dist/src/author-presentation.d.ts +50 -0
- package/dist/src/author-presentation.d.ts.map +1 -0
- package/dist/src/author-presentation.js +58 -0
- package/dist/src/author-presentation.js.map +1 -0
- package/dist/src/auto-continue.d.ts +2 -0
- package/dist/src/auto-continue.d.ts.map +1 -1
- package/dist/src/auto-continue.js +5 -2
- package/dist/src/auto-continue.js.map +1 -1
- package/dist/src/code-execution/isolate/runner.d.ts +7 -5
- package/dist/src/code-execution/isolate/runner.d.ts.map +1 -1
- package/dist/src/code-execution/isolate/runner.js +7 -5
- package/dist/src/code-execution/isolate/runner.js.map +1 -1
- package/dist/src/code-execution/isolate/scripts.d.ts.map +1 -1
- package/dist/src/code-execution/isolate/scripts.js +3 -2
- package/dist/src/code-execution/isolate/scripts.js.map +1 -1
- package/dist/src/dendrite/index.d.ts +6 -0
- package/dist/src/dendrite/index.d.ts.map +1 -0
- package/dist/src/dendrite/index.js +3 -0
- package/dist/src/dendrite/index.js.map +1 -0
- package/dist/src/dendrite/presets.d.ts +60 -0
- package/dist/src/dendrite/presets.d.ts.map +1 -0
- package/dist/src/dendrite/presets.js +117 -0
- package/dist/src/dendrite/presets.js.map +1 -0
- package/dist/src/dendrite/registry.d.ts +260 -0
- package/dist/src/dendrite/registry.d.ts.map +1 -0
- package/dist/src/dendrite/registry.js +742 -0
- package/dist/src/dendrite/registry.js.map +1 -0
- package/dist/src/dendrite/types.d.ts +392 -0
- package/dist/src/dendrite/types.d.ts.map +1 -0
- package/dist/src/dendrite/types.js +24 -0
- package/dist/src/dendrite/types.js.map +1 -0
- package/dist/src/framework.d.ts +846 -67
- package/dist/src/framework.d.ts.map +1 -1
- package/dist/src/framework.js +4213 -924
- package/dist/src/framework.js.map +1 -1
- package/dist/src/gate/event-gate.d.ts +46 -14
- package/dist/src/gate/event-gate.d.ts.map +1 -1
- package/dist/src/gate/event-gate.js +143 -53
- package/dist/src/gate/event-gate.js.map +1 -1
- package/dist/src/gate/index.d.ts +1 -1
- package/dist/src/gate/index.d.ts.map +1 -1
- package/dist/src/gate/index.js +1 -1
- package/dist/src/gate/index.js.map +1 -1
- package/dist/src/gate/types.d.ts +12 -2
- package/dist/src/gate/types.d.ts.map +1 -1
- package/dist/src/heartbeat/heartbeat.d.ts +18 -0
- package/dist/src/heartbeat/heartbeat.d.ts.map +1 -1
- package/dist/src/heartbeat/heartbeat.js +34 -8
- package/dist/src/heartbeat/heartbeat.js.map +1 -1
- package/dist/src/held-writes.d.ts +3 -3
- package/dist/src/held-writes.js +3 -3
- package/dist/src/host-presentation.d.ts +174 -0
- package/dist/src/host-presentation.d.ts.map +1 -0
- package/dist/src/host-presentation.js +565 -0
- package/dist/src/host-presentation.js.map +1 -0
- package/dist/src/index.d.ts +7 -4
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +7 -2
- package/dist/src/index.js.map +1 -1
- package/dist/src/line-reader.d.ts +4 -0
- package/dist/src/line-reader.d.ts.map +1 -0
- package/dist/src/line-reader.js +59 -0
- package/dist/src/line-reader.js.map +1 -0
- package/dist/src/mcpl/channel-registry.d.ts +118 -15
- package/dist/src/mcpl/channel-registry.d.ts.map +1 -1
- package/dist/src/mcpl/channel-registry.js +266 -110
- package/dist/src/mcpl/channel-registry.js.map +1 -1
- package/dist/src/mcpl/conversation-router.d.ts.map +1 -1
- package/dist/src/mcpl/conversation-router.js +2 -1
- package/dist/src/mcpl/conversation-router.js.map +1 -1
- package/dist/src/mcpl/convert-block.d.ts +1 -0
- package/dist/src/mcpl/convert-block.d.ts.map +1 -1
- package/dist/src/mcpl/convert-block.js +16 -4
- package/dist/src/mcpl/convert-block.js.map +1 -1
- package/dist/src/mcpl/hook-orchestrator.d.ts.map +1 -1
- package/dist/src/mcpl/hook-orchestrator.js +4 -3
- package/dist/src/mcpl/hook-orchestrator.js.map +1 -1
- package/dist/src/mcpl/inbound-source.d.ts +3 -1
- package/dist/src/mcpl/inbound-source.d.ts.map +1 -1
- package/dist/src/mcpl/inbound-source.js +3 -1
- package/dist/src/mcpl/inbound-source.js.map +1 -1
- package/dist/src/mcpl/modern-connection.d.ts.map +1 -1
- package/dist/src/mcpl/modern-connection.js +11 -7
- package/dist/src/mcpl/modern-connection.js.map +1 -1
- package/dist/src/mcpl/protocol-family.d.ts +3 -2
- package/dist/src/mcpl/protocol-family.d.ts.map +1 -1
- package/dist/src/mcpl/protocol-family.js +25 -3
- package/dist/src/mcpl/protocol-family.js.map +1 -1
- package/dist/src/mcpl/push-coalescer.d.ts +9 -0
- package/dist/src/mcpl/push-coalescer.d.ts.map +1 -1
- package/dist/src/mcpl/push-coalescer.js +21 -0
- package/dist/src/mcpl/push-coalescer.js.map +1 -1
- package/dist/src/mcpl/push-handler.js +2 -2
- package/dist/src/mcpl/push-handler.js.map +1 -1
- package/dist/src/mcpl/references.d.ts +12 -40
- package/dist/src/mcpl/references.d.ts.map +1 -1
- package/dist/src/mcpl/references.js +29 -23
- package/dist/src/mcpl/references.js.map +1 -1
- package/dist/src/mcpl/server-connection.d.ts.map +1 -1
- package/dist/src/mcpl/server-connection.js +20 -20
- package/dist/src/mcpl/server-connection.js.map +1 -1
- package/dist/src/mcpl/skip-reply-wake.d.ts +25 -0
- package/dist/src/mcpl/skip-reply-wake.d.ts.map +1 -1
- package/dist/src/mcpl/skip-reply-wake.js +25 -0
- package/dist/src/mcpl/skip-reply-wake.js.map +1 -1
- package/dist/src/mcpl/tool-presence.d.ts.map +1 -1
- package/dist/src/mcpl/tool-presence.js +18 -17
- package/dist/src/mcpl/tool-presence.js.map +1 -1
- package/dist/src/mcpl/tool-signature.d.ts.map +1 -1
- package/dist/src/mcpl/tool-signature.js +17 -16
- package/dist/src/mcpl/tool-signature.js.map +1 -1
- package/dist/src/mcpl/toolset-host.d.ts +8 -26
- package/dist/src/mcpl/toolset-host.d.ts.map +1 -1
- package/dist/src/mcpl/toolset-host.js +16 -6
- package/dist/src/mcpl/toolset-host.js.map +1 -1
- package/dist/src/mcpl/toolsets-module.d.ts +14 -25
- package/dist/src/mcpl/toolsets-module.d.ts.map +1 -1
- package/dist/src/mcpl/toolsets-module.js +187 -50
- package/dist/src/mcpl/toolsets-module.js.map +1 -1
- package/dist/src/mcpl/transport.d.ts +1 -1
- package/dist/src/mcpl/transport.d.ts.map +1 -1
- package/dist/src/mcpl/transport.js +16 -16
- package/dist/src/mcpl/transport.js.map +1 -1
- package/dist/src/mcpl/types.d.ts +28 -12
- package/dist/src/mcpl/types.d.ts.map +1 -1
- package/dist/src/mcpl/types.js.map +1 -1
- package/dist/src/module-registry.d.ts +6 -7
- package/dist/src/module-registry.d.ts.map +1 -1
- package/dist/src/module-registry.js +6 -7
- package/dist/src/module-registry.js.map +1 -1
- package/dist/src/modules/api/index.d.ts.map +1 -1
- package/dist/src/modules/api/index.js +4 -2
- package/dist/src/modules/api/index.js.map +1 -1
- package/dist/src/modules/discord/index.d.ts.map +1 -1
- package/dist/src/modules/discord/index.js +33 -25
- package/dist/src/modules/discord/index.js.map +1 -1
- package/dist/src/modules/history/index.js +8 -6
- package/dist/src/modules/history/index.js.map +1 -1
- package/dist/src/modules/sender-name.d.ts +7 -0
- package/dist/src/modules/sender-name.d.ts.map +1 -0
- package/dist/src/modules/sender-name.js +10 -0
- package/dist/src/modules/sender-name.js.map +1 -0
- package/dist/src/modules/workspace/answers.d.ts +5 -1
- package/dist/src/modules/workspace/answers.d.ts.map +1 -1
- package/dist/src/modules/workspace/answers.js +16 -1
- package/dist/src/modules/workspace/answers.js.map +1 -1
- package/dist/src/modules/workspace/direct-write.d.ts.map +1 -1
- package/dist/src/modules/workspace/direct-write.js +6 -4
- package/dist/src/modules/workspace/direct-write.js.map +1 -1
- package/dist/src/modules/workspace/effects.d.ts +4 -2
- package/dist/src/modules/workspace/effects.d.ts.map +1 -1
- package/dist/src/modules/workspace/effects.js +50 -40
- package/dist/src/modules/workspace/effects.js.map +1 -1
- package/dist/src/modules/workspace/index.d.ts +1 -0
- package/dist/src/modules/workspace/index.d.ts.map +1 -1
- package/dist/src/modules/workspace/index.js +66 -36
- package/dist/src/modules/workspace/index.js.map +1 -1
- package/dist/src/modules/workspace/observe.d.ts +51 -20
- package/dist/src/modules/workspace/observe.d.ts.map +1 -1
- package/dist/src/modules/workspace/observe.js +120 -82
- package/dist/src/modules/workspace/observe.js.map +1 -1
- package/dist/src/modules/workspace/reconcile.d.ts +7 -1
- package/dist/src/modules/workspace/reconcile.d.ts.map +1 -1
- package/dist/src/modules/workspace/reconcile.js +133 -37
- package/dist/src/modules/workspace/reconcile.js.map +1 -1
- package/dist/src/notice-tags.d.ts +81 -0
- package/dist/src/notice-tags.d.ts.map +1 -0
- package/dist/src/notice-tags.js +85 -0
- package/dist/src/notice-tags.js.map +1 -0
- package/dist/src/reasoning-loss-notice.d.ts.map +1 -1
- package/dist/src/reasoning-loss-notice.js +5 -3
- package/dist/src/reasoning-loss-notice.js.map +1 -1
- package/dist/src/silent-heartbeat.d.ts +5 -55
- package/dist/src/silent-heartbeat.d.ts.map +1 -1
- package/dist/src/silent-heartbeat.js +63 -7
- package/dist/src/silent-heartbeat.js.map +1 -1
- package/dist/src/stored-injections.d.ts +4 -0
- package/dist/src/stored-injections.d.ts.map +1 -1
- package/dist/src/stored-injections.js +6 -0
- package/dist/src/stored-injections.js.map +1 -1
- package/dist/src/supplied-text.d.ts +35 -0
- package/dist/src/supplied-text.d.ts.map +1 -0
- package/dist/src/supplied-text.js +141 -0
- package/dist/src/supplied-text.js.map +1 -0
- package/dist/src/tool-args.d.ts.map +1 -1
- package/dist/src/tool-args.js +9 -8
- package/dist/src/tool-args.js.map +1 -1
- package/dist/src/tool-image-ledger.d.ts.map +1 -1
- package/dist/src/tool-image-ledger.js +2 -1
- package/dist/src/tool-image-ledger.js.map +1 -1
- package/dist/src/tool-presentation.d.ts.map +1 -1
- package/dist/src/tool-presentation.js +5 -4
- package/dist/src/tool-presentation.js.map +1 -1
- package/dist/src/tune-out/coordinator.d.ts +382 -11
- package/dist/src/tune-out/coordinator.d.ts.map +1 -1
- package/dist/src/tune-out/coordinator.js +1100 -119
- package/dist/src/tune-out/coordinator.js.map +1 -1
- package/dist/src/tune-out/tools.d.ts +101 -5
- package/dist/src/tune-out/tools.d.ts.map +1 -1
- package/dist/src/tune-out/tools.js +55 -18
- package/dist/src/tune-out/tools.js.map +1 -1
- package/dist/src/types/agent.d.ts +48 -9
- package/dist/src/types/agent.d.ts.map +1 -1
- package/dist/src/types/framework.d.ts +142 -0
- package/dist/src/types/framework.d.ts.map +1 -1
- package/dist/src/types/module.d.ts +22 -11
- package/dist/src/types/module.d.ts.map +1 -1
- package/dist/src/types/trace.d.ts +136 -0
- package/dist/src/types/trace.d.ts.map +1 -1
- package/package.json +3 -3
- package/src/admission/admit.ts +113 -0
- package/src/agent.ts +52 -7
- package/src/api/mcp-server-core.ts +3 -14
- package/src/api/server.ts +7 -3
- package/src/author-presentation.ts +81 -0
- package/src/auto-continue.ts +8 -2
- package/src/code-execution/isolate/runner.ts +7 -5
- package/src/code-execution/isolate/scripts.ts +3 -2
- package/src/dendrite/index.ts +32 -0
- package/src/dendrite/presets.ts +149 -0
- package/src/dendrite/registry.ts +808 -0
- package/src/dendrite/types.ts +397 -0
- package/src/framework.ts +4362 -852
- package/src/gate/event-gate.ts +159 -52
- package/src/gate/index.ts +1 -1
- package/src/gate/types.ts +12 -2
- package/src/heartbeat/heartbeat.ts +48 -9
- package/src/held-writes.ts +3 -3
- package/src/host-presentation.ts +612 -0
- package/src/index.ts +17 -4
- package/src/line-reader.ts +58 -0
- package/src/mcpl/channel-registry.ts +306 -111
- package/src/mcpl/conversation-router.ts +2 -1
- package/src/mcpl/convert-block.ts +17 -4
- package/src/mcpl/hook-orchestrator.ts +4 -3
- package/src/mcpl/inbound-source.ts +3 -1
- package/src/mcpl/modern-connection.ts +11 -7
- package/src/mcpl/protocol-family.ts +24 -3
- package/src/mcpl/push-coalescer.ts +19 -0
- package/src/mcpl/push-handler.ts +2 -2
- package/src/mcpl/references.ts +31 -23
- package/src/mcpl/server-connection.ts +20 -18
- package/src/mcpl/skip-reply-wake.ts +35 -0
- package/src/mcpl/tool-presence.ts +18 -17
- package/src/mcpl/tool-signature.ts +17 -16
- package/src/mcpl/toolset-host.ts +18 -6
- package/src/mcpl/toolsets-module.ts +149 -50
- package/src/mcpl/transport.ts +18 -14
- package/src/mcpl/types.ts +29 -12
- package/src/module-registry.ts +6 -7
- package/src/modules/api/index.ts +4 -2
- package/src/modules/discord/index.ts +37 -25
- package/src/modules/history/index.ts +8 -6
- package/src/modules/sender-name.ts +9 -0
- package/src/modules/workspace/answers.ts +20 -2
- package/src/modules/workspace/direct-write.ts +4 -3
- package/src/modules/workspace/effects.ts +50 -40
- package/src/modules/workspace/index.ts +58 -34
- package/src/modules/workspace/observe.ts +139 -88
- package/src/modules/workspace/reconcile.ts +159 -36
- package/src/notice-tags.ts +86 -0
- package/src/reasoning-loss-notice.ts +5 -3
- package/src/silent-heartbeat.ts +11 -7
- package/src/stored-injections.ts +7 -0
- package/src/supplied-text.ts +154 -0
- package/src/tool-args.ts +9 -8
- package/src/tool-image-ledger.ts +2 -1
- package/src/tool-presentation.ts +5 -4
- package/src/tune-out/coordinator.ts +1252 -123
- package/src/tune-out/tools.ts +152 -11
- package/src/types/agent.ts +51 -9
- package/src/types/framework.ts +146 -0
- package/src/types/module.ts +23 -11
- package/src/types/trace.ts +150 -0
|
@@ -47,7 +47,7 @@ import { lstat, mkdir, open, readdir, readlink, realpath, stat, statfs } from 'n
|
|
|
47
47
|
import type { FileHandle } from 'node:fs/promises';
|
|
48
48
|
import { basename, dirname, isAbsolute, join, posix, relative, resolve, sep } from 'node:path';
|
|
49
49
|
import { sameRootIdentity, type Fingerprint, type RootIdentity } from './disk-agreement.js';
|
|
50
|
-
import { errnoCode, systemWords } from './answers.js';
|
|
50
|
+
import { OUTSIDE_STEP, UNFOLLOWED_STEP, errnoClause, errnoCode, systemWords } from './answers.js';
|
|
51
51
|
|
|
52
52
|
export interface MountView {
|
|
53
53
|
/**
|
|
@@ -115,7 +115,17 @@ export type Observation =
|
|
|
115
115
|
| { kind: 'absent' }
|
|
116
116
|
/** `mode`: the file's permission bits (`st_mode & 0o777`). */
|
|
117
117
|
| { kind: 'file'; size: number; mtimeMs: number; mode: number; fp: StatFingerprint }
|
|
118
|
-
| { kind: 'unobserved'
|
|
118
|
+
| ({ kind: 'unobserved' } & NotShown);
|
|
119
|
+
|
|
120
|
+
export interface Cause {
|
|
121
|
+
errno?: string;
|
|
122
|
+
again?: true;
|
|
123
|
+
ownStep?: true;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
export interface NotShown extends Cause {
|
|
127
|
+
reason: string;
|
|
128
|
+
}
|
|
119
129
|
|
|
120
130
|
export interface ReadResult {
|
|
121
131
|
kind: 'read';
|
|
@@ -157,13 +167,21 @@ export const ROOT_REPLACED =
|
|
|
157
167
|
'the mount root is not the directory disk last agreed with (an unmounted drive, or a replaced directory): ' +
|
|
158
168
|
'reconnect it; if it was replaced on purpose, sync the mount with acceptRoot to take it as it is now';
|
|
159
169
|
export const CHANGED = 'it changed while it was observed';
|
|
160
|
-
const CHANGED_WALK = 'the directory changed while the walk ran';
|
|
161
|
-
const
|
|
170
|
+
export const CHANGED_WALK = 'the directory changed while the walk ran';
|
|
171
|
+
export const FILE_CHANGED = 'the file changed while it was read';
|
|
172
|
+
export const RESOLVES_OUTSIDE = 'the directory resolves outside the mount';
|
|
173
|
+
export const LINK_TO_DIRECTORY = 'a symlink to a directory, not walked';
|
|
174
|
+
export const BENEATH_SYMLINK = 'beneath a symlink that does not lead to a directory inside the mount';
|
|
162
175
|
|
|
163
176
|
export function errno(err: unknown): string | undefined {
|
|
164
177
|
return (err as { code?: string } | null)?.code;
|
|
165
178
|
}
|
|
166
179
|
|
|
180
|
+
function failedOn(what: string, err: unknown): NotShown {
|
|
181
|
+
const code = errno(err);
|
|
182
|
+
return { reason: `${what} (${systemWords(code, err)})`, ...(code !== undefined ? { errno: code } : {}) };
|
|
183
|
+
}
|
|
184
|
+
|
|
167
185
|
export function contained(rootReal: string, candidate: string): boolean {
|
|
168
186
|
return candidate === rootReal || candidate.startsWith(rootReal.endsWith(sep) ? rootReal : rootReal + sep);
|
|
169
187
|
}
|
|
@@ -265,7 +283,9 @@ export async function linkRefusal(
|
|
|
265
283
|
/**
|
|
266
284
|
* Where a symlink the walk found leads, for its refusal: to a path inside
|
|
267
285
|
* the mount (mount-relative, as disk spells it, and whether it's a
|
|
268
|
-
* directory), outside the mount, or nowhere
|
|
286
|
+
* directory), outside the mount, or nowhere (nothing is there, or the links
|
|
287
|
+
* loop); null where the realpath fails otherwise, as on a directory this
|
|
288
|
+
* process can't search, so where it leads isn't known. One
|
|
269
289
|
* realpath, one stat and the respelling, made only when refusing. realpath
|
|
270
290
|
* keeps the spelling the link's target gives, which on a disk that folds
|
|
271
291
|
* names need not be disk's, so the target is respelled before anything
|
|
@@ -273,9 +293,14 @@ export async function linkRefusal(
|
|
|
273
293
|
*/
|
|
274
294
|
export type LinkLeads = { kind: 'inside'; target: string; directory: boolean } | { kind: 'outside' } | { kind: 'nowhere' };
|
|
275
295
|
|
|
276
|
-
export async function linkLeads(root: string, rootReal: string, link: string): Promise<LinkLeads> {
|
|
277
|
-
|
|
278
|
-
|
|
296
|
+
export async function linkLeads(root: string, rootReal: string, link: string): Promise<LinkLeads | null> {
|
|
297
|
+
let leads: string;
|
|
298
|
+
try {
|
|
299
|
+
leads = await realpath(join(root, link));
|
|
300
|
+
} catch (err) {
|
|
301
|
+
const code = errno(err);
|
|
302
|
+
return code === 'ENOENT' || code === 'ENOTDIR' || code === 'ELOOP' ? { kind: 'nowhere' } : null;
|
|
303
|
+
}
|
|
279
304
|
if (!contained(rootReal, leads)) return { kind: 'outside' };
|
|
280
305
|
const directory = await stat(leads).then((info) => info.isDirectory(), () => false);
|
|
281
306
|
return { kind: 'inside', target: await diskSpelling(rootReal, relative(rootReal, leads).split(sep).join('/')), directory };
|
|
@@ -397,16 +422,23 @@ export async function coversLanding(view: Pick<MountView, 'include'>, rootReal:
|
|
|
397
422
|
|
|
398
423
|
/**
|
|
399
424
|
* Why a path is refused for what linkOnTheWay found, given where a link
|
|
400
|
-
* leads (linkLeads). For a link, the link is the subject,
|
|
401
|
-
* leads: for a link inside the mount, its target,
|
|
402
|
-
*
|
|
403
|
-
*
|
|
404
|
-
*
|
|
405
|
-
*
|
|
406
|
-
*
|
|
407
|
-
*
|
|
408
|
-
*
|
|
409
|
-
*
|
|
425
|
+
* leads (linkLeads), and the way on. For a link, the link is the subject,
|
|
426
|
+
* with where it leads: for a link inside the mount, its target, as the path
|
|
427
|
+
* to use in its place, and for a link to a directory there, the setting
|
|
428
|
+
* whoever runs the host can change too; for a link to something that isn't
|
|
429
|
+
* a directory, that nothing lies beneath it (the link stands among the
|
|
430
|
+
* path's directories, so following it can't make the path beneath it
|
|
431
|
+
* exist); for one leading outside the mount, that it does, and that
|
|
432
|
+
* whoever runs the host can mount what it leads to, or a directory that
|
|
433
|
+
* holds it; for one leading nowhere, that whoever runs the host can remove it or make
|
|
434
|
+
* it lead to what it stood for; for one whose target the refusal doesn't
|
|
435
|
+
* tell, what whoever runs the host can do wherever it leads
|
|
436
|
+
* (UNFOLLOWED_STEP).
|
|
437
|
+
* A target in a nested mount's directory (`nested`) is that mount's: the
|
|
438
|
+
* refusal names the mount and the path to use there in place of the link,
|
|
439
|
+
* and offers no setting, since following the link would only reach into
|
|
440
|
+
* another mount's directory. For a directory the walk couldn't ask, which
|
|
441
|
+
* one, the answer it got, and the errno's way on (errnoClause). Paths
|
|
410
442
|
* are spelled as the tools take them, from the mount's name. On a mount with
|
|
411
443
|
* an include, an inside target is named only where a tool could name it: a
|
|
412
444
|
* file the include covers, or a directory, which a listing names whatever
|
|
@@ -417,23 +449,33 @@ export async function coversLanding(view: Pick<MountView, 'include'>, rootReal:
|
|
|
417
449
|
*/
|
|
418
450
|
export function linkWords(name: string, found: LinkFound, leads: LinkLeads | null, nested: readonly NestedMount[] = [], include?: readonly string[]): string {
|
|
419
451
|
const spell = (rel: string) => `"${rel === '' ? name : `${name}/${rel}`}"`;
|
|
420
|
-
if (!('link' in found))
|
|
452
|
+
if (!('link' in found)) {
|
|
453
|
+
const code = errnoCode({ code: found.code });
|
|
454
|
+
return `${UNSURE_PREFIX}${name}/${found.unsure}" is a symlink (${systemWords(code, found.code)}); ${errnoClause(code, 'calling again', 'calling again then gets past it')}`;
|
|
455
|
+
}
|
|
421
456
|
const link = `${spell(found.link)} is a symlink`;
|
|
422
|
-
|
|
423
|
-
if (leads
|
|
457
|
+
const unknown = `${link}, which this mount does not follow; ${UNFOLLOWED_STEP}`;
|
|
458
|
+
if (leads === null) return unknown;
|
|
459
|
+
if (leads.kind === 'nowhere') {
|
|
460
|
+
return `${link} that leads nowhere, which this mount does not follow; whoever runs this host can remove it or make it lead to what it stood for`;
|
|
461
|
+
}
|
|
462
|
+
if (leads.kind === 'outside') {
|
|
463
|
+
return `${link} leading outside the mount, which this mount does not follow; followed or not, ${OUTSIDE_STEP}`;
|
|
464
|
+
}
|
|
424
465
|
const holder = nestedHolder(leads.target, nested);
|
|
425
466
|
if (holder) {
|
|
426
|
-
if (!namesHolder(leads, nested)) return
|
|
467
|
+
if (!namesHolder(leads, nested)) return unknown;
|
|
427
468
|
const rest = restBeneath(leads.target, holder);
|
|
428
469
|
const guess = nestedGuess(name, leads.target.split('/'), holder);
|
|
429
470
|
return `${link} to ${spell(leads.target)}, which this mount does not follow; it ${guess === undefined ? 'lies' : 'may lie'} in mount ` +
|
|
430
471
|
`"${holder.name}", nested in mount "${name}": ${nestedStep({ name: holder.name, use: rest ? `${holder.name}/${rest}` : holder.name, guess }, { after: ` in place of ${spell(found.link)}` })}`;
|
|
431
472
|
}
|
|
432
|
-
if (!leads.directory && !insideMount(leads.target, include)) return
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
return `${link} to ${spell(leads.target)}, which this mount does not follow${
|
|
473
|
+
if (!leads.directory && !insideMount(leads.target, include)) return unknown;
|
|
474
|
+
if (!leads.directory) return `${link} to ${spell(leads.target)}, which this mount does not follow; that isn't a directory, so nothing lies beneath it`;
|
|
475
|
+
// Either way serves only what the include covers where it leads (judgedAt).
|
|
476
|
+
const covered = include === undefined ? '' : '; either way, the mount reaches only what its include covers there';
|
|
477
|
+
return `${link} to ${spell(leads.target)}, which this mount does not follow; use ${spell(leads.target)} in place of ${spell(found.link)}, ` +
|
|
478
|
+
`or whoever runs this host can set followSymlinks: true on mount "${name}"${covered}`;
|
|
437
479
|
}
|
|
438
480
|
|
|
439
481
|
/**
|
|
@@ -450,8 +492,8 @@ export async function shownLeads(root: string, rootReal: string, found: LinkFoun
|
|
|
450
492
|
}
|
|
451
493
|
|
|
452
494
|
/** Why a path is refused for what linkOnTheWay found (linkWords, with where the link leads, as shownLeads tells it). */
|
|
453
|
-
export async function linkReason(view: Pick<MountView, 'root' | 'name' | 'nested' | 'include'>, rootReal: string, found: LinkFound): Promise<
|
|
454
|
-
return linkWords(view.name, found, await shownLeads(view.root, rootReal, found, view.include), view.nested, view.include);
|
|
495
|
+
export async function linkReason(view: Pick<MountView, 'root' | 'name' | 'nested' | 'include'>, rootReal: string, found: LinkFound): Promise<NotShown> {
|
|
496
|
+
return { reason: linkWords(view.name, found, await shownLeads(view.root, rootReal, found, view.include), view.nested, view.include), ownStep: true };
|
|
455
497
|
}
|
|
456
498
|
|
|
457
499
|
export function realSegments(path: string): string[] {
|
|
@@ -963,7 +1005,7 @@ export type DirCheck =
|
|
|
963
1005
|
*/
|
|
964
1006
|
| { kind: 'absent'; by: string }
|
|
965
1007
|
/** Unproven either way; `outside` when it resolves outside the mount, `errno` when a system call failed. */
|
|
966
|
-
| { kind: 'unobserved';
|
|
1008
|
+
| ({ kind: 'unobserved'; outside?: true } & NotShown);
|
|
967
1009
|
|
|
968
1010
|
/** Errors meaning a directory can't be synced on this filesystem, not that syncing it failed. */
|
|
969
1011
|
const DIRECTORY_SYNC_UNSUPPORTED = new Set(['EINVAL', 'ENOTSUP']);
|
|
@@ -1070,13 +1112,13 @@ async function locateDir(view: MountView, rootReal: string, rel: string): Promis
|
|
|
1070
1112
|
// A link on the way that can't be resolved, such as a loop, is still a
|
|
1071
1113
|
// link this mount doesn't follow.
|
|
1072
1114
|
const found = rel === '' ? null : await linkOnTheWay(view, rel);
|
|
1073
|
-
if (found !== null) return { kind: 'unobserved',
|
|
1074
|
-
return { kind: 'unobserved',
|
|
1115
|
+
if (found !== null) return { kind: 'unobserved', ...(await linkReason(view, rootReal, found)) };
|
|
1116
|
+
return { kind: 'unobserved', ...failedOn('cannot resolve the directory', err) };
|
|
1075
1117
|
}
|
|
1076
1118
|
// A mount that doesn't follow symlinks goes through none on the way to a
|
|
1077
1119
|
// directory, inside the mount or out of it.
|
|
1078
1120
|
const found = await linkRefusal(view, rootReal, rel, real);
|
|
1079
|
-
if (found !== null) return { kind: 'unobserved',
|
|
1121
|
+
if (found !== null) return { kind: 'unobserved', ...(await linkReason(view, rootReal, found)) };
|
|
1080
1122
|
if (rel === '') {
|
|
1081
1123
|
// The root is whatever the configured path names, and it must still be
|
|
1082
1124
|
// the root the pass was given.
|
|
@@ -1091,7 +1133,7 @@ async function locateDir(view: MountView, rootReal: string, rel: string): Promis
|
|
|
1091
1133
|
const holder = await nestedAtReal(real, view.nested, { name: view.name, rootReal });
|
|
1092
1134
|
if (holder) {
|
|
1093
1135
|
const directory = holder.judgedByInclude && await stat(real).then((info) => info.isDirectory(), () => false);
|
|
1094
|
-
return holder.names(directory) ? { kind: 'unobserved', reason: leadsInto(view.name, rel, holder) } : { kind: 'missing' };
|
|
1136
|
+
return holder.names(directory) ? { kind: 'unobserved', reason: leadsInto(view.name, rel, holder), ownStep: true } : { kind: 'missing' };
|
|
1095
1137
|
}
|
|
1096
1138
|
}
|
|
1097
1139
|
const checked = await checkedAt(view, rel, real);
|
|
@@ -1111,7 +1153,7 @@ async function locateDir(view: MountView, rootReal: string, rel: string): Promis
|
|
|
1111
1153
|
* The mount root as it is now: its identity if it is available (a directory,
|
|
1112
1154
|
* the one the pass was given, and the one disk last agreed under), else why not.
|
|
1113
1155
|
*/
|
|
1114
|
-
export async function locateRoot(view: MountView, rootReal: string): Promise<{ identity: RootIdentity } | { reason: string }> {
|
|
1156
|
+
export async function locateRoot(view: MountView, rootReal: string): Promise<{ identity: RootIdentity } | { reason: string; errno?: string }> {
|
|
1115
1157
|
const found = await checkRoot(view, rootReal);
|
|
1116
1158
|
return 'dir' in found ? { identity: found.dir.identity() } : found;
|
|
1117
1159
|
}
|
|
@@ -1120,7 +1162,10 @@ export async function locateRoot(view: MountView, rootReal: string): Promise<{ i
|
|
|
1120
1162
|
export async function checkRoot(view: MountView, rootReal: string): Promise<{ dir: CheckedDir } | { reason: string; errno?: string }> {
|
|
1121
1163
|
const found = await locateDir(view, rootReal, '');
|
|
1122
1164
|
if (found.kind === 'checked') return { dir: found.dir };
|
|
1123
|
-
|
|
1165
|
+
const reason = found.kind === 'unobserved' ? found.reason : ROOT_UNAVAILABLE;
|
|
1166
|
+
// What is at the root's path now says whether the directory is gone from it.
|
|
1167
|
+
const code = found.kind === 'unobserved' && found.errno !== undefined ? found.errno : reason === ROOT_UNAVAILABLE ? await rootAbsence(view.root) : undefined;
|
|
1168
|
+
return { reason, ...(code !== undefined ? { errno: code } : {}) };
|
|
1124
1169
|
}
|
|
1125
1170
|
|
|
1126
1171
|
/**
|
|
@@ -1135,7 +1180,7 @@ export async function checkDir(view: MountView, rootReal: string, rel: string, o
|
|
|
1135
1180
|
if (found.kind !== 'missing') return found;
|
|
1136
1181
|
if (opts.create) return createDir(view, rootReal, rel);
|
|
1137
1182
|
const proof = await confirmAbsent(view, rootReal, rel, 'beneath');
|
|
1138
|
-
return
|
|
1183
|
+
return 'reason' in proof ? { kind: 'unobserved', ...proof } : { kind: 'absent', by: proof.by };
|
|
1139
1184
|
}
|
|
1140
1185
|
|
|
1141
1186
|
async function createDir(view: MountView, rootReal: string, rel: string): Promise<DirCheck> {
|
|
@@ -1156,12 +1201,12 @@ async function createDir(view: MountView, rootReal: string, rel: string): Promis
|
|
|
1156
1201
|
// Nothing is made in a nested mount's directory, its root included where
|
|
1157
1202
|
// it isn't made yet, by a path that reaches it through a link.
|
|
1158
1203
|
const holder = await nestedAtReal(base.at(parts[i]!), view.nested, { name: view.name, rootReal });
|
|
1159
|
-
if (holder) return { kind: 'unobserved', reason: leadsInto(view.name, childRel, holder) };
|
|
1204
|
+
if (holder) return { kind: 'unobserved', reason: leadsInto(view.name, childRel, holder), ownStep: true };
|
|
1160
1205
|
try {
|
|
1161
1206
|
await mkdir(base.at(parts[i]!));
|
|
1162
1207
|
} catch (err) {
|
|
1163
1208
|
const code = errno(err);
|
|
1164
|
-
if (code !== 'EEXIST') return { kind: 'unobserved',
|
|
1209
|
+
if (code !== 'EEXIST') return { kind: 'unobserved', ...failedOn(`cannot create the directory ${childRel}`, err) };
|
|
1165
1210
|
}
|
|
1166
1211
|
// Made, or there already (made meanwhile, or not a directory): it must
|
|
1167
1212
|
// be a real directory now, never a symlink or file in its place.
|
|
@@ -1170,7 +1215,7 @@ async function createDir(view: MountView, rootReal: string, rel: string): Promis
|
|
|
1170
1215
|
// A symlink in the way, such as one that leads nowhere, on a mount that
|
|
1171
1216
|
// doesn't follow symlinks, is named as one.
|
|
1172
1217
|
const there = view.followSymlinks ? null : await lstat(base.at(parts[i]!)).catch(() => null);
|
|
1173
|
-
if (there?.isSymbolicLink()) return { kind: 'unobserved',
|
|
1218
|
+
if (there?.isSymbolicLink()) return { kind: 'unobserved', ...(await linkReason(view, rootReal, { link: childRel })) };
|
|
1174
1219
|
return { kind: 'unobserved', reason: NOT_A_DIRECTORY };
|
|
1175
1220
|
}
|
|
1176
1221
|
base = checked;
|
|
@@ -1193,14 +1238,14 @@ async function createDir(view: MountView, rootReal: string, rel: string): Promis
|
|
|
1193
1238
|
* Returns the directory whose entries prove it, or why absence couldn't be
|
|
1194
1239
|
* shown.
|
|
1195
1240
|
*/
|
|
1196
|
-
export async function confirmAbsent(view: MountView, rootReal: string, rel: string, target: 'entry' | 'beneath' = 'entry'): Promise<{ by: string } |
|
|
1241
|
+
export async function confirmAbsent(view: MountView, rootReal: string, rel: string, target: 'entry' | 'beneath' = 'entry'): Promise<{ by: string } | NotShown> {
|
|
1197
1242
|
const root = await locateDir(view, rootReal, '');
|
|
1198
|
-
if (root.kind !== 'checked') return ROOT_UNAVAILABLE;
|
|
1243
|
+
if (root.kind !== 'checked') return { reason: ROOT_UNAVAILABLE };
|
|
1199
1244
|
let cur: CheckedDir = root.dir;
|
|
1200
1245
|
/** Every directory the walk went through, the root first. */
|
|
1201
1246
|
const walked: CheckedDir[] = [cur];
|
|
1202
|
-
const proven = async (): Promise<{ by: string } |
|
|
1203
|
-
for (const dir of walked) if (!(await dir.stillHere())) return CHANGED;
|
|
1247
|
+
const proven = async (): Promise<{ by: string } | NotShown> => {
|
|
1248
|
+
for (const dir of walked) if (!(await dir.stillHere())) return { reason: CHANGED };
|
|
1204
1249
|
return { by: cur.rel };
|
|
1205
1250
|
};
|
|
1206
1251
|
const parts = rel.split('/');
|
|
@@ -1213,41 +1258,44 @@ export async function confirmAbsent(view: MountView, rootReal: string, rel: stri
|
|
|
1213
1258
|
info = await lstat(cur.at(name), { bigint: true });
|
|
1214
1259
|
} catch (err) {
|
|
1215
1260
|
const code = errno(err);
|
|
1216
|
-
if (code === 'ENOTDIR') return CHANGED;
|
|
1217
|
-
if (code !== 'ENOENT') return
|
|
1261
|
+
if (code === 'ENOTDIR') return { reason: CHANGED };
|
|
1262
|
+
if (code !== 'ENOENT') return failedOn('cannot stat', err);
|
|
1218
1263
|
// Absent from a directory that, like each above it, is still the one its path names.
|
|
1219
1264
|
return proven();
|
|
1220
1265
|
}
|
|
1221
1266
|
if (info.isDirectory()) {
|
|
1222
|
-
if (last) return CHANGED; // present after all
|
|
1267
|
+
if (last) return { reason: CHANGED }; // present after all
|
|
1223
1268
|
cur = new CheckedDir(childRel, cur.at(name), lexicalOf(view, childRel), info.dev, info.ino, info.birthtimeNs);
|
|
1224
1269
|
walked.push(cur);
|
|
1225
1270
|
continue;
|
|
1226
1271
|
}
|
|
1227
1272
|
if (info.isSymbolicLink()) {
|
|
1228
|
-
if (last && target === 'entry') return CHANGED; // present: the link itself
|
|
1273
|
+
if (last && target === 'entry') return { reason: CHANGED }; // present: the link itself
|
|
1229
1274
|
// A mount that doesn't follow symlinks goes through none on the way.
|
|
1230
1275
|
if (!view.followSymlinks) return linkReason(view, rootReal, { link: childRel });
|
|
1231
1276
|
const located = await locateDir(view, rootReal, childRel);
|
|
1232
|
-
if (located.kind
|
|
1233
|
-
|
|
1277
|
+
if (located.kind === 'unobserved' && located.outside !== true) {
|
|
1278
|
+
return { reason: located.reason, ...(located.errno !== undefined ? { errno: located.errno } : {}), ...(located.ownStep === true ? { ownStep: true } : {}) };
|
|
1279
|
+
}
|
|
1280
|
+
if (located.kind !== 'checked') return { reason: BENEATH_SYMLINK };
|
|
1281
|
+
if (last) return { reason: CHANGED }; // a directory there after all
|
|
1234
1282
|
cur = located.dir;
|
|
1235
1283
|
walked.push(cur);
|
|
1236
1284
|
continue;
|
|
1237
1285
|
}
|
|
1238
|
-
if (last && target === 'entry') return CHANGED; // present after all
|
|
1286
|
+
if (last && target === 'entry') return { reason: CHANGED }; // present after all
|
|
1239
1287
|
// A non-directory: nothing can exist beneath it, while it is still this
|
|
1240
1288
|
// same one, in the directory still at its path.
|
|
1241
1289
|
const again = await lstat(cur.at(name), { bigint: true }).catch(() => null);
|
|
1242
|
-
if (again === null || again.isDirectory() || again.isSymbolicLink() || again.dev !== info.dev || again.ino !== info.ino) return CHANGED;
|
|
1290
|
+
if (again === null || again.isDirectory() || again.isSymbolicLink() || again.dev !== info.dev || again.ino !== info.ino) return { reason: CHANGED };
|
|
1243
1291
|
return proven();
|
|
1244
1292
|
}
|
|
1245
|
-
return CHANGED;
|
|
1293
|
+
return { reason: CHANGED };
|
|
1246
1294
|
}
|
|
1247
1295
|
|
|
1248
1296
|
async function absence(view: MountView, rootReal: string, rel: string): Promise<Exclude<Observation, { kind: 'file' }>> {
|
|
1249
1297
|
const proof = await confirmAbsent(view, rootReal, rel);
|
|
1250
|
-
return
|
|
1298
|
+
return 'reason' in proof ? { kind: 'unobserved', ...proof } : { kind: 'absent' };
|
|
1251
1299
|
}
|
|
1252
1300
|
|
|
1253
1301
|
// ===========================================================================
|
|
@@ -1323,26 +1371,26 @@ export async function filesystemTrustsCtime(root: string): Promise<boolean> {
|
|
|
1323
1371
|
* final component — which on a mount that doesn't follow symlinks is any
|
|
1324
1372
|
* symlink on the way.
|
|
1325
1373
|
*/
|
|
1326
|
-
async function parentPlacement(view: MountView, rootReal: string, relativePath: string): Promise<{ inside: string } | 'gone' |
|
|
1374
|
+
async function parentPlacement(view: MountView, rootReal: string, relativePath: string): Promise<{ inside: string } | 'gone' | NotShown> {
|
|
1327
1375
|
try {
|
|
1328
1376
|
const parent = await realpath(dirname(join(view.root, relativePath)));
|
|
1329
1377
|
const found = await linkRefusal(view, rootReal, relativePath, parent, 'parents');
|
|
1330
|
-
if (found !== null) return
|
|
1378
|
+
if (found !== null) return linkReason(view, rootReal, found);
|
|
1331
1379
|
if (!contained(rootReal, parent)) return { reason: OUTSIDE_PARENT };
|
|
1332
1380
|
const holder = await nestedAtReal(parent, view.nested, { name: view.name, rootReal });
|
|
1333
1381
|
if (!holder) return { inside: parent };
|
|
1334
1382
|
// A place there that the nested mount's include leaves out is no mount's,
|
|
1335
1383
|
// and holds nothing beneath it, as beneath any file (namesNestedPlace).
|
|
1336
1384
|
const directory = holder.judgedByInclude && await stat(parent).then((info) => info.isDirectory(), () => false);
|
|
1337
|
-
return holder.names(directory) ? { reason: leadsInto(view.name, parentOf(relativePath), holder) } : 'gone';
|
|
1385
|
+
return holder.names(directory) ? { reason: leadsInto(view.name, parentOf(relativePath), holder), ownStep: true } : 'gone';
|
|
1338
1386
|
} catch (err) {
|
|
1339
1387
|
const code = errno(err);
|
|
1340
1388
|
if (code === 'ENOENT' || code === 'ENOTDIR') return 'gone';
|
|
1341
1389
|
// A link on the way that can't be resolved, such as a loop, is still a
|
|
1342
1390
|
// link this mount doesn't follow.
|
|
1343
1391
|
const found = await linkOnTheWay(view, relativePath, 'parents');
|
|
1344
|
-
if (found !== null) return
|
|
1345
|
-
return
|
|
1392
|
+
if (found !== null) return linkReason(view, rootReal, found);
|
|
1393
|
+
return failedOn('cannot resolve the parent directory', err);
|
|
1346
1394
|
}
|
|
1347
1395
|
}
|
|
1348
1396
|
|
|
@@ -1356,14 +1404,14 @@ export async function observePath(view: MountView, rootReal: string, relativePat
|
|
|
1356
1404
|
const lexical = join(view.root, relativePath);
|
|
1357
1405
|
const placement = await parentPlacement(view, rootReal, relativePath);
|
|
1358
1406
|
if (placement === 'gone') return absence(view, rootReal, relativePath);
|
|
1359
|
-
if ('reason' in placement) return { kind: 'unobserved',
|
|
1407
|
+
if ('reason' in placement) return { kind: 'unobserved', ...placement };
|
|
1360
1408
|
let info;
|
|
1361
1409
|
try {
|
|
1362
1410
|
info = await lstat(lexical, { bigint: true });
|
|
1363
1411
|
} catch (err) {
|
|
1364
1412
|
const code = errno(err);
|
|
1365
1413
|
if (code === 'ENOENT' || code === 'ENOTDIR') return absence(view, rootReal, relativePath);
|
|
1366
|
-
return { kind: 'unobserved',
|
|
1414
|
+
return { kind: 'unobserved', ...failedOn('cannot stat', err) };
|
|
1367
1415
|
}
|
|
1368
1416
|
if (info.isSymbolicLink()) {
|
|
1369
1417
|
if (!view.followSymlinks) return { kind: 'unobserved', reason: NOT_FOLLOWED };
|
|
@@ -1371,23 +1419,23 @@ export async function observePath(view: MountView, rootReal: string, relativePat
|
|
|
1371
1419
|
try {
|
|
1372
1420
|
target = await stat(lexical, { bigint: true });
|
|
1373
1421
|
} catch (err) {
|
|
1374
|
-
return { kind: 'unobserved',
|
|
1422
|
+
return { kind: 'unobserved', ...failedOn('a symlink whose target cannot be read', err) };
|
|
1375
1423
|
}
|
|
1376
1424
|
let real: string;
|
|
1377
1425
|
try {
|
|
1378
1426
|
real = await realpath(lexical);
|
|
1379
1427
|
} catch (err) {
|
|
1380
|
-
return { kind: 'unobserved',
|
|
1428
|
+
return { kind: 'unobserved', ...failedOn('a symlink that cannot be resolved', err) };
|
|
1381
1429
|
}
|
|
1382
1430
|
if (!contained(rootReal, real)) return { kind: 'unobserved', reason: LEAVES_MOUNT };
|
|
1383
1431
|
const holder = await nestedAtReal(real, view.nested, { name: view.name, rootReal });
|
|
1384
1432
|
// A place a nested mount's include leaves out is no mount's, so the link
|
|
1385
1433
|
// leaves this one (namesNestedPlace).
|
|
1386
1434
|
if (holder) {
|
|
1387
|
-
return { kind: 'unobserved', reason:
|
|
1435
|
+
return holder.names(target.isDirectory()) ? { kind: 'unobserved', reason: leadsInto(view.name, relativePath, holder), ownStep: true } : { kind: 'unobserved', reason: LEAVES_MOUNT };
|
|
1388
1436
|
}
|
|
1389
1437
|
if (!target.isFile()) {
|
|
1390
|
-
return { kind: 'unobserved', reason: target.isDirectory() ?
|
|
1438
|
+
return { kind: 'unobserved', reason: target.isDirectory() ? LINK_TO_DIRECTORY : NOT_REGULAR };
|
|
1391
1439
|
}
|
|
1392
1440
|
// A file this mount's own include leaves out where the link lands is no
|
|
1393
1441
|
// file of this mount's (coversLanding). It's judged here, and not only
|
|
@@ -1436,35 +1484,35 @@ async function openContained(
|
|
|
1436
1484
|
if (code === 'ELOOP' && !view.followSymlinks) {
|
|
1437
1485
|
// The link at the path, or a looping one on its way.
|
|
1438
1486
|
const found = await linkOnTheWay(view, relativePath, 'parents');
|
|
1439
|
-
return { kind: 'unobserved', reason:
|
|
1487
|
+
return found === null ? { kind: 'unobserved', reason: NOT_FOLLOWED } : { kind: 'unobserved', ...(await linkReason(view, rootReal, found)) };
|
|
1440
1488
|
}
|
|
1441
1489
|
if (code === 'EISDIR') return { kind: 'unobserved', reason: DIRECTORY };
|
|
1442
|
-
return { kind: 'unobserved',
|
|
1490
|
+
return { kind: 'unobserved', ...failedOn('cannot open', err) };
|
|
1443
1491
|
}
|
|
1444
1492
|
let keep = false;
|
|
1445
1493
|
try {
|
|
1446
1494
|
const info = await handle.stat({ bigint: true });
|
|
1447
1495
|
const real = await realpath(lexical).catch(() => null);
|
|
1448
|
-
if (real === null) return { kind: 'unobserved', reason:
|
|
1496
|
+
if (real === null) return { kind: 'unobserved', reason: FILE_CHANGED };
|
|
1449
1497
|
// O_NOFOLLOW guards only the final component: a mount that doesn't
|
|
1450
1498
|
// follow symlinks goes through none among the directories on the way
|
|
1451
1499
|
// either. Asked of what the path resolved to after the open, which the
|
|
1452
1500
|
// check below holds to the descriptor's file, and before what the
|
|
1453
1501
|
// descriptor holds, so a link gets its own answer whatever it leads to.
|
|
1454
1502
|
const found = await linkRefusal(view, rootReal, relativePath, real);
|
|
1455
|
-
if (found !== null) return { kind: 'unobserved',
|
|
1503
|
+
if (found !== null) return { kind: 'unobserved', ...(await linkReason(view, rootReal, found)) };
|
|
1456
1504
|
if (!info.isFile()) return { kind: 'unobserved', reason: info.isDirectory() ? DIRECTORY : NOT_REGULAR };
|
|
1457
1505
|
const leaves = view.followSymlinks ? LEAVES_MOUNT : OUTSIDE_PARENT;
|
|
1458
1506
|
if (!contained(rootReal, real)) return { kind: 'unobserved', reason: leaves };
|
|
1459
1507
|
const holder = await nestedAtReal(real, view.nested, { name: view.name, rootReal });
|
|
1460
1508
|
// A place a nested mount's include leaves out is no mount's. Reached
|
|
1461
1509
|
// through a link this mount doesn't follow, the link is what's refused.
|
|
1462
|
-
if (holder) return { kind: 'unobserved', reason:
|
|
1510
|
+
if (holder) return holder.names(false) ? { kind: 'unobserved', reason: leadsInto(view.name, relativePath, holder), ownStep: true } : { kind: 'unobserved', reason: view.followSymlinks ? leaves : NOT_FOLLOWED };
|
|
1463
1511
|
// So is a file this mount's own include leaves out (coversLanding).
|
|
1464
1512
|
if (!(await coversLanding(view, rootReal, real, relativePath))) return { kind: 'unobserved', reason: view.followSymlinks ? leaves : NOT_FOLLOWED };
|
|
1465
1513
|
const named = await stat(real, { bigint: true }).catch(() => null);
|
|
1466
1514
|
if (named === null || named.dev !== info.dev || named.ino !== info.ino) {
|
|
1467
|
-
return { kind: 'unobserved', reason:
|
|
1515
|
+
return { kind: 'unobserved', reason: FILE_CHANGED };
|
|
1468
1516
|
}
|
|
1469
1517
|
if (!view.followSymlinks) {
|
|
1470
1518
|
// Where no symlink is followed, the path itself must be the file the
|
|
@@ -1473,7 +1521,7 @@ async function openContained(
|
|
|
1473
1521
|
const entry = await lstat(lexical, { bigint: true }).catch(() => null);
|
|
1474
1522
|
if (entry?.isSymbolicLink()) return { kind: 'unobserved', reason: NOT_FOLLOWED };
|
|
1475
1523
|
if (entry === null || !entry.isFile() || entry.dev !== info.dev || entry.ino !== info.ino) {
|
|
1476
|
-
return { kind: 'unobserved', reason:
|
|
1524
|
+
return { kind: 'unobserved', reason: FILE_CHANGED };
|
|
1477
1525
|
}
|
|
1478
1526
|
}
|
|
1479
1527
|
keep = true;
|
|
@@ -1481,7 +1529,7 @@ async function openContained(
|
|
|
1481
1529
|
} catch (err) {
|
|
1482
1530
|
const code = errno(err);
|
|
1483
1531
|
if (code === 'ENOENT') return absence(view, rootReal, relativePath);
|
|
1484
|
-
return { kind: 'unobserved',
|
|
1532
|
+
return { kind: 'unobserved', ...failedOn('cannot read', err) };
|
|
1485
1533
|
} finally {
|
|
1486
1534
|
if (!keep) await handle.close();
|
|
1487
1535
|
}
|
|
@@ -1579,7 +1627,7 @@ export async function readPath(
|
|
|
1579
1627
|
} catch (err) {
|
|
1580
1628
|
const code = errno(err);
|
|
1581
1629
|
if (code === 'ENOENT') return absence(view, rootReal, relativePath);
|
|
1582
|
-
return { kind: 'unobserved',
|
|
1630
|
+
return { kind: 'unobserved', ...failedOn('cannot read', err) };
|
|
1583
1631
|
} finally {
|
|
1584
1632
|
await handle.close();
|
|
1585
1633
|
}
|
|
@@ -1639,7 +1687,7 @@ export async function readBytes(
|
|
|
1639
1687
|
if (more) return { kind: 'bytes', size: Math.max(Number((await handle.stat({ bigint: true })).size), bytes.length + 1), bytes: null, head: bytes.subarray(0, HEAD_BYTES) };
|
|
1640
1688
|
return { kind: 'bytes', size: bytes.byteLength, bytes, head: bytes.subarray(0, HEAD_BYTES) };
|
|
1641
1689
|
} catch (err) {
|
|
1642
|
-
return { kind: 'unobserved',
|
|
1690
|
+
return { kind: 'unobserved', ...failedOn('cannot read', err) };
|
|
1643
1691
|
} finally {
|
|
1644
1692
|
await handle.close();
|
|
1645
1693
|
}
|
|
@@ -1663,7 +1711,7 @@ export async function readHead(
|
|
|
1663
1711
|
const { bytesRead } = await handle.read(head, 0, HEAD_BYTES, 0);
|
|
1664
1712
|
return { kind: 'head', size: Number(info.size), mtimeMs: Number(info.mtimeMs), head: head.subarray(0, bytesRead) };
|
|
1665
1713
|
} catch (err) {
|
|
1666
|
-
return { kind: 'unobserved',
|
|
1714
|
+
return { kind: 'unobserved', ...failedOn('cannot read', err) };
|
|
1667
1715
|
} finally {
|
|
1668
1716
|
await handle.close();
|
|
1669
1717
|
}
|
|
@@ -1735,7 +1783,7 @@ export interface Walk {
|
|
|
1735
1783
|
* `symlink` region is never walked; the pass reports the link itself by
|
|
1736
1784
|
* path.
|
|
1737
1785
|
*/
|
|
1738
|
-
incomplete: Array<{ path: string; reason: string; kind: 'error' | 'cap' | 'ignored' | 'symlink' }>;
|
|
1786
|
+
incomplete: Array<{ path: string; reason: string; kind: 'error' | 'cap' | 'ignored' | 'symlink'; errno?: string }>;
|
|
1739
1787
|
/**
|
|
1740
1788
|
* When the walk cap stopped the walk, the last entry it took. A walk that
|
|
1741
1789
|
* resumes after it takes the entries after it first, in the walk's order
|
|
@@ -1956,7 +2004,7 @@ const EDITOR_FILE = /^\..+\.sw[px]$|~$|\.subl.*\.tmp$/;
|
|
|
1956
2004
|
/** The walk's reason for an entry the mount's ignore list covers. */
|
|
1957
2005
|
export const IGNORED = 'ignored by the mount';
|
|
1958
2006
|
/** The walk's reason for an entry `EDITOR_FILE` names. */
|
|
1959
|
-
const EDITOR_FILE_REASON = "an editor's swap or backup file";
|
|
2007
|
+
export const EDITOR_FILE_REASON = "an editor's swap or backup file";
|
|
1960
2008
|
|
|
1961
2009
|
/**
|
|
1962
2010
|
* The walk's reason for a link it doesn't enter. On a mount with an include,
|
|
@@ -1965,7 +2013,7 @@ const EDITOR_FILE_REASON = "an editor's swap or backup file";
|
|
|
1965
2013
|
* that follows links, one that leads to a directory, and on one that
|
|
1966
2014
|
* doesn't, every one.
|
|
1967
2015
|
*/
|
|
1968
|
-
const LINK_NOT_WALKED = 'a symlink, not walked';
|
|
2016
|
+
export const LINK_NOT_WALKED = 'a symlink, not walked';
|
|
1969
2017
|
|
|
1970
2018
|
/** Whether a reason the walk gave an entry is one it passes over by design. */
|
|
1971
2019
|
export function passedOverByDesign(why: string | undefined): boolean {
|
|
@@ -2053,9 +2101,9 @@ async function checkChild(view: MountView, rootReal: string, parent: CheckedDir,
|
|
|
2053
2101
|
try {
|
|
2054
2102
|
info = await lstat(real, { bigint: true });
|
|
2055
2103
|
} catch (err) {
|
|
2056
|
-
if (errno(err) !== 'ENOENT') return { kind: 'unobserved',
|
|
2104
|
+
if (errno(err) !== 'ENOENT') return { kind: 'unobserved', ...failedOn('cannot stat', err) };
|
|
2057
2105
|
const proof = await confirmAbsent(view, rootReal, rel, 'beneath');
|
|
2058
|
-
return
|
|
2106
|
+
return 'reason' in proof ? { kind: 'unobserved', ...proof } : { kind: 'absent', by: proof.by };
|
|
2059
2107
|
}
|
|
2060
2108
|
if (!info.isDirectory()) return { kind: 'unobserved', reason: CHANGED_WALK };
|
|
2061
2109
|
return { kind: 'checked', dir: new CheckedDir(rel, real, lexicalOf(view, rel), info.dev, info.ino, info.birthtimeNs) };
|
|
@@ -2120,7 +2168,7 @@ export async function walkScope(
|
|
|
2120
2168
|
return walk;
|
|
2121
2169
|
}
|
|
2122
2170
|
if (top.kind === 'unobserved') {
|
|
2123
|
-
walk.incomplete.push({ path: scope, reason: top.outside ?
|
|
2171
|
+
walk.incomplete.push({ path: scope, reason: top.outside ? RESOLVES_OUTSIDE : top.reason, kind: 'error', ...(top.errno !== undefined ? { errno: top.errno } : {}) });
|
|
2124
2172
|
return walk;
|
|
2125
2173
|
}
|
|
2126
2174
|
// A scope a path sync names is walked even where the ignore list covers
|
|
@@ -2148,10 +2196,10 @@ export async function walkScope(
|
|
|
2148
2196
|
const visited = new Set<string>();
|
|
2149
2197
|
/** Paths already reported as errors, so a second visit doesn't report them again. */
|
|
2150
2198
|
const failed = new Set<string>();
|
|
2151
|
-
const fail = (path: string, reason: string): void => {
|
|
2199
|
+
const fail = (path: string, reason: string, code?: string): void => {
|
|
2152
2200
|
if (failed.has(path)) return;
|
|
2153
2201
|
failed.add(path);
|
|
2154
|
-
walk.incomplete.push({ path, reason, kind: 'error' });
|
|
2202
|
+
walk.incomplete.push({ path, reason, kind: 'error', ...(code !== undefined ? { errno: code } : {}) });
|
|
2155
2203
|
};
|
|
2156
2204
|
let left = cap;
|
|
2157
2205
|
let lastTaken: string | undefined;
|
|
@@ -2214,6 +2262,7 @@ export async function walkScope(
|
|
|
2214
2262
|
// Gone since it was checked: absent beneath only if that can be shown.
|
|
2215
2263
|
const proof = rel === '' ? ROOT_UNAVAILABLE : await confirmAbsent(view, rootReal, rel, 'beneath');
|
|
2216
2264
|
if (typeof proof === 'string') fail(rel, proof);
|
|
2265
|
+
else if ('reason' in proof) fail(rel, proof.reason, proof.errno);
|
|
2217
2266
|
else {
|
|
2218
2267
|
walk.missing.add(rel);
|
|
2219
2268
|
// Nothing was listed, so a later check that finds it again, on the
|
|
@@ -2222,7 +2271,8 @@ export async function walkScope(
|
|
|
2222
2271
|
}
|
|
2223
2272
|
return null;
|
|
2224
2273
|
}
|
|
2225
|
-
|
|
2274
|
+
const listed = failedOn('cannot list', err);
|
|
2275
|
+
fail(rel, listed.reason, listed.errno);
|
|
2226
2276
|
return null;
|
|
2227
2277
|
}
|
|
2228
2278
|
entries.sort((a, b) => compareNames(a.name, b.name));
|
|
@@ -2380,7 +2430,7 @@ export async function walkScope(
|
|
|
2380
2430
|
// directory there now: what the walk read of it describes another.
|
|
2381
2431
|
dropBeneath(sub);
|
|
2382
2432
|
if (child.kind === 'absent') walk.missing.add(sub);
|
|
2383
|
-
else fail(sub, child.reason);
|
|
2433
|
+
else fail(sub, child.reason, child.errno);
|
|
2384
2434
|
}
|
|
2385
2435
|
};
|
|
2386
2436
|
|
|
@@ -2395,18 +2445,19 @@ export async function walkScope(
|
|
|
2395
2445
|
// directories a listing showed that weren't listed. A walk that doesn't
|
|
2396
2446
|
// descend leaves its subdirectories by design.
|
|
2397
2447
|
const capped = `the walk cap (${cap}) was reached`;
|
|
2448
|
+
const raise = "whoever runs this host can raise the mount's walkCap";
|
|
2398
2449
|
const holdingUntaken = new Set<string>();
|
|
2399
2450
|
for (const f of walk.seen) holdingUntaken.add(parentOf(f));
|
|
2400
2451
|
for (const [p, why] of walk.others) if (!passedOverByDesign(why) && !walk.othersTaken.has(p)) holdingUntaken.add(parentOf(p));
|
|
2401
2452
|
for (const d of holdingUntaken) {
|
|
2402
|
-
walk.incomplete.push({ path: d, reason: `${capped} before everything here was taken`, kind: 'cap' });
|
|
2453
|
+
walk.incomplete.push({ path: d, reason: `${capped} before everything here was taken; naming a path beneath it walks only that path, and ${raise}`, kind: 'cap' });
|
|
2403
2454
|
}
|
|
2404
2455
|
let unlisted = 0;
|
|
2405
2456
|
if (recursive) {
|
|
2406
2457
|
for (const d of walk.dirs) {
|
|
2407
2458
|
if (visited.has(d) || walk.missing.has(d) || failed.has(d)) continue;
|
|
2408
2459
|
unlisted++;
|
|
2409
|
-
walk.incomplete.push({ path: d, reason: `not visited: ${capped}`, kind: 'cap' });
|
|
2460
|
+
walk.incomplete.push({ path: d, reason: `not visited: ${capped}; naming this path walks only it, and ${raise}`, kind: 'cap' });
|
|
2410
2461
|
}
|
|
2411
2462
|
}
|
|
2412
2463
|
// The cap stopped the walk only if it left something: a walk whose last
|