amalgm 0.1.239 → 0.1.241
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/lib/shared-realtime-tunnel.js +18 -4
- package/package.json +1 -1
- package/runtime/scripts/amalgm-mcp/browser/cookie-jar.js +68 -20
- package/runtime/scripts/amalgm-mcp/project-context/store.js +87 -19
- package/runtime/scripts/amalgm-mcp/server/routes/state.js +13 -0
- package/runtime/scripts/amalgm-mcp/state/attachments.js +40 -12
- package/runtime/scripts/amalgm-mcp/state/db.js +216 -17
- package/runtime/scripts/amalgm-mcp/state/doc-disk.js +550 -0
- package/runtime/scripts/amalgm-mcp/state/docs.js +1101 -301
- package/runtime/scripts/amalgm-mcp/state/mutation-contracts.js +99 -0
- package/runtime/scripts/amalgm-mcp/state/mutations.js +507 -130
- package/runtime/scripts/amalgm-mcp/state/promotions.js +21 -1
- package/runtime/scripts/amalgm-mcp/state/replicas.js +134 -13
- package/runtime/scripts/amalgm-mcp/state/shared-rest.js +14 -8
- package/runtime/scripts/amalgm-mcp/tests/browser-cookie-cloud.test.js +193 -0
- package/runtime/scripts/amalgm-mcp/tests/code-project-stream.test.js +313 -94
- package/runtime/scripts/amalgm-mcp/tests/fixtures/code-project-stream-v2.json +1 -0
- package/runtime/scripts/amalgm-mcp/tests/project-context.test.js +32 -17
- package/runtime/scripts/amalgm-mcp/tests/shared-tunnel-rehydration.test.js +84 -0
- package/runtime/scripts/amalgm-mcp/tests/state-docs.test.js +376 -13
- package/runtime/scripts/amalgm-mcp/tests/state-mutations.test.js +309 -0
- package/runtime/scripts/amalgm-mcp/tests/state-shared-replica.test.js +758 -30
- package/runtime/scripts/amalgm-mcp/tests/workspace-cards-store.test.js +72 -3
- package/runtime/scripts/amalgm-mcp/tests/workspace-cards.test.js +33 -6
- package/runtime/scripts/amalgm-mcp/tests/workspace-checkpoint-reducer.test.js +27 -0
- package/runtime/scripts/amalgm-mcp/tests/workspace-checkpoint.test.js +334 -191
- package/runtime/scripts/amalgm-mcp/tests/workspace-durable.test.js +24 -0
- package/runtime/scripts/amalgm-mcp/tests/workspace-local-intent.test.js +1999 -0
- package/runtime/scripts/amalgm-mcp/tests/workspace-native-file-transaction.test.js +364 -0
- package/runtime/scripts/amalgm-mcp/tests/workspace-object-tunnel.test.js +98 -4
- package/runtime/scripts/amalgm-mcp/tests/workspace-objects.test.js +156 -8
- package/runtime/scripts/amalgm-mcp/tests/workspace-provenance.test.js +185 -0
- package/runtime/scripts/amalgm-mcp/tests/workspace-publication-invariants.test.js +174 -269
- package/runtime/scripts/amalgm-mcp/tests/workspace-reducer-liveness.test.js +26 -0
- package/runtime/scripts/amalgm-mcp/tests/workspace-reducer.test.js +2060 -407
- package/runtime/scripts/amalgm-mcp/tests/workspace-sync-stats.test.js +119 -0
- package/runtime/scripts/amalgm-mcp/tests/workspace-transition.test.js +1446 -0
- package/runtime/scripts/amalgm-mcp/tests/workspace-tree-cloud.test.js +5319 -3
- package/runtime/scripts/amalgm-mcp/tests/workspace-tree-core.test.js +191 -1
- package/runtime/scripts/amalgm-mcp/tests/workspace-tree-store.test.js +3140 -17
- package/runtime/scripts/amalgm-mcp/tests/workspace-wall.test.js +135 -637
- package/runtime/scripts/amalgm-mcp/workspace/access-store.js +5 -1
- package/runtime/scripts/amalgm-mcp/workspace/card.js +7 -7
- package/runtime/scripts/amalgm-mcp/workspace/cards.js +36 -12
- package/runtime/scripts/amalgm-mcp/workspace/checkpoint.js +436 -173
- package/runtime/scripts/amalgm-mcp/workspace/content.js +32 -0
- package/runtime/scripts/amalgm-mcp/workspace/durable.js +45 -0
- package/runtime/scripts/amalgm-mcp/workspace/git.js +391 -37
- package/runtime/scripts/amalgm-mcp/workspace/intent.js +535 -0
- package/runtime/scripts/amalgm-mcp/workspace/merge.js +89 -6
- package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/BUILD.md +31 -0
- package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/bin/darwin-arm64/amalgm-file-transaction +0 -0
- package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/bin/linux-x64/amalgm-file-transaction +0 -0
- package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/build.mjs +72 -0
- package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/client.js +165 -0
- package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/go.mod +5 -0
- package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/go.sum +2 -0
- package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/main.go +2201 -0
- package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/manifest.json +19 -0
- package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/move_darwin.go +18 -0
- package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/move_linux.go +18 -0
- package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/process_darwin.go +33 -0
- package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/process_linux.go +47 -0
- package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/verify.mjs +43 -0
- package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/writers_darwin.go +114 -0
- package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/writers_linux.go +84 -0
- package/runtime/scripts/amalgm-mcp/workspace/objects.js +185 -55
- package/runtime/scripts/amalgm-mcp/workspace/project-stream.js +616 -168
- package/runtime/scripts/amalgm-mcp/workspace/provenance.js +240 -0
- package/runtime/scripts/amalgm-mcp/workspace/reducer.js +1979 -570
- package/runtime/scripts/amalgm-mcp/workspace/rest.js +49 -28
- package/runtime/scripts/amalgm-mcp/workspace/store.js +77 -4
- package/runtime/scripts/amalgm-mcp/workspace/sync-stats.js +240 -0
- package/runtime/scripts/amalgm-mcp/workspace/transition.js +1729 -192
- package/runtime/scripts/amalgm-mcp/workspace/tree/floor.js +100 -0
- package/runtime/scripts/amalgm-mcp/workspace/tree/inclusion.js +7 -1
- package/runtime/scripts/amalgm-mcp/workspace/tree/local-state.js +73 -61
- package/runtime/scripts/amalgm-mcp/workspace/tree/observation-holds.js +127 -0
- package/runtime/scripts/amalgm-mcp/workspace/tree/path-state.js +12 -0
- package/runtime/scripts/amalgm-mcp/workspace/tree/reducer.js +51 -6
- package/runtime/scripts/amalgm-mcp/workspace/tree/root-binding.js +125 -0
- package/runtime/scripts/amalgm-mcp/workspace/tree/snapshot.js +79 -8
- package/runtime/scripts/amalgm-mcp/workspace/tree/symlinks.js +76 -0
- package/runtime/scripts/amalgm-mcp/workspace/tree-cloud.js +3480 -165
- package/runtime/scripts/amalgm-mcp/workspace/tree-materialization.js +3766 -0
- package/runtime/scripts/amalgm-mcp/workspace/tree-store.js +899 -71
- package/runtime/scripts/amalgm-mcp/workspace/wall.js +352 -497
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
{
|
|
2
|
+
"contract": "amalgm-native-file-transaction-build@1",
|
|
3
|
+
"sourceSha256": "728af934075f99e5d102edb375e0f4d1ea7fcae3e35751125d9e4fb9ec0b95f7",
|
|
4
|
+
"sources": [
|
|
5
|
+
"go.mod",
|
|
6
|
+
"go.sum",
|
|
7
|
+
"main.go",
|
|
8
|
+
"move_darwin.go",
|
|
9
|
+
"move_linux.go",
|
|
10
|
+
"process_darwin.go",
|
|
11
|
+
"process_linux.go",
|
|
12
|
+
"writers_darwin.go",
|
|
13
|
+
"writers_linux.go"
|
|
14
|
+
],
|
|
15
|
+
"binaries": {
|
|
16
|
+
"darwin-arm64": "5bcef1a258b8988a56bfbe57acf3d1df4f94351d9e5e693670b47b101bd2367a",
|
|
17
|
+
"linux-x64": "b559ae0ac5668f075720cc1e26239c5a0bc47599ba09d91f1c14918ac60206e7"
|
|
18
|
+
}
|
|
19
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
//go:build darwin
|
|
2
|
+
|
|
3
|
+
package main
|
|
4
|
+
|
|
5
|
+
import "golang.org/x/sys/unix"
|
|
6
|
+
|
|
7
|
+
// moveNoReplace consumes the prepared transaction artifact only when the
|
|
8
|
+
// destination is still absent. The artifact's disappearance is therefore
|
|
9
|
+
// the operation-owned receipt that installation crossed.
|
|
10
|
+
func moveNoReplace(fromFD int, from string, toFD int, to string) error {
|
|
11
|
+
return unix.RenameatxNp(fromFD, from, toFD, to, unix.RENAME_EXCL)
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
// exchangeEntries atomically replaces a shared namespace with its guard
|
|
15
|
+
// marker while retaining the original namespace at the prepared path.
|
|
16
|
+
func exchangeEntries(leftFD int, left string, rightFD int, right string) error {
|
|
17
|
+
return unix.RenameatxNp(leftFD, left, rightFD, right, unix.RENAME_SWAP)
|
|
18
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
//go:build linux
|
|
2
|
+
|
|
3
|
+
package main
|
|
4
|
+
|
|
5
|
+
import "golang.org/x/sys/unix"
|
|
6
|
+
|
|
7
|
+
// moveNoReplace consumes the prepared transaction artifact only when the
|
|
8
|
+
// destination is still absent. The artifact's disappearance is therefore
|
|
9
|
+
// the operation-owned receipt that installation crossed.
|
|
10
|
+
func moveNoReplace(fromFD int, from string, toFD int, to string) error {
|
|
11
|
+
return unix.Renameat2(fromFD, from, toFD, to, unix.RENAME_NOREPLACE)
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
// exchangeEntries atomically replaces a shared namespace with its guard
|
|
15
|
+
// marker while retaining the original namespace at the prepared path.
|
|
16
|
+
func exchangeEntries(leftFD int, left string, rightFD int, right string) error {
|
|
17
|
+
return unix.Renameat2(leftFD, left, rightFD, right, unix.RENAME_EXCHANGE)
|
|
18
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
//go:build darwin
|
|
2
|
+
|
|
3
|
+
package main
|
|
4
|
+
|
|
5
|
+
import (
|
|
6
|
+
"errors"
|
|
7
|
+
"fmt"
|
|
8
|
+
|
|
9
|
+
"golang.org/x/sys/unix"
|
|
10
|
+
)
|
|
11
|
+
|
|
12
|
+
const darwinZombieState = int8(5)
|
|
13
|
+
|
|
14
|
+
func processIdentity(pid int) (string, bool, error) {
|
|
15
|
+
if err := unix.Kill(pid, 0); err != nil && errors.Is(err, unix.ESRCH) {
|
|
16
|
+
return "", false, nil
|
|
17
|
+
}
|
|
18
|
+
info, err := unix.SysctlKinfoProc("kern.proc.pid", pid)
|
|
19
|
+
if err != nil {
|
|
20
|
+
if errors.Is(err, unix.ESRCH) || errors.Is(err, unix.ENOENT) {
|
|
21
|
+
return "", false, nil
|
|
22
|
+
}
|
|
23
|
+
return "", false, err
|
|
24
|
+
}
|
|
25
|
+
if int(info.Proc.P_pid) != pid {
|
|
26
|
+
return "", false, nil
|
|
27
|
+
}
|
|
28
|
+
if info.Proc.P_stat == darwinZombieState {
|
|
29
|
+
return "", false, nil
|
|
30
|
+
}
|
|
31
|
+
started := info.Proc.P_starttime
|
|
32
|
+
return fmt.Sprintf("%d:%d:%d", pid, started.Sec, started.Usec), true, nil
|
|
33
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
//go:build linux
|
|
2
|
+
|
|
3
|
+
package main
|
|
4
|
+
|
|
5
|
+
import (
|
|
6
|
+
"fmt"
|
|
7
|
+
"os"
|
|
8
|
+
"strconv"
|
|
9
|
+
"strings"
|
|
10
|
+
)
|
|
11
|
+
|
|
12
|
+
func processIdentity(pid int) (string, bool, error) {
|
|
13
|
+
bootID, err := os.ReadFile("/proc/sys/kernel/random/boot_id")
|
|
14
|
+
if err != nil {
|
|
15
|
+
return "", false, err
|
|
16
|
+
}
|
|
17
|
+
boot := strings.TrimSpace(string(bootID))
|
|
18
|
+
if boot == "" {
|
|
19
|
+
return "", false, fmt.Errorf("Linux boot identity is empty")
|
|
20
|
+
}
|
|
21
|
+
content, err := os.ReadFile(fmt.Sprintf("/proc/%d/stat", pid))
|
|
22
|
+
if err != nil {
|
|
23
|
+
if os.IsNotExist(err) {
|
|
24
|
+
return "", false, nil
|
|
25
|
+
}
|
|
26
|
+
return "", false, err
|
|
27
|
+
}
|
|
28
|
+
closing := strings.LastIndexByte(string(content), ')')
|
|
29
|
+
if closing < 0 || closing+2 >= len(content) {
|
|
30
|
+
return "", false, errorsNewProcessStat(pid)
|
|
31
|
+
}
|
|
32
|
+
fields := strings.Fields(string(content[closing+2:]))
|
|
33
|
+
if len(fields) <= 19 {
|
|
34
|
+
return "", false, errorsNewProcessStat(pid)
|
|
35
|
+
}
|
|
36
|
+
if fields[0] == "Z" || fields[0] == "X" || fields[0] == "x" {
|
|
37
|
+
return "", false, nil
|
|
38
|
+
}
|
|
39
|
+
if _, err := strconv.ParseUint(fields[19], 10, 64); err != nil {
|
|
40
|
+
return "", false, errorsNewProcessStat(pid)
|
|
41
|
+
}
|
|
42
|
+
return fmt.Sprintf("%s:%d:%s", boot, pid, fields[19]), true, nil
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
func errorsNewProcessStat(pid int) error {
|
|
46
|
+
return fmt.Errorf("process %d has an invalid stat identity", pid)
|
|
47
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import crypto from 'node:crypto';
|
|
2
|
+
import fs from 'node:fs';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { fileURLToPath } from 'node:url';
|
|
5
|
+
|
|
6
|
+
const root = path.dirname(fileURLToPath(import.meta.url));
|
|
7
|
+
const manifest = JSON.parse(fs.readFileSync(path.join(root, 'manifest.json'), 'utf8'));
|
|
8
|
+
if (manifest.contract !== 'amalgm-native-file-transaction-build@1') {
|
|
9
|
+
throw new Error('native file transaction manifest contract is invalid');
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function digestFiles(names) {
|
|
13
|
+
const hash = crypto.createHash('sha256');
|
|
14
|
+
for (const name of names) {
|
|
15
|
+
hash.update(name);
|
|
16
|
+
hash.update('\0');
|
|
17
|
+
hash.update(fs.readFileSync(path.join(root, name)));
|
|
18
|
+
hash.update('\0');
|
|
19
|
+
}
|
|
20
|
+
return hash.digest('hex');
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const sourceDigest = digestFiles(manifest.sources);
|
|
24
|
+
if (sourceDigest !== manifest.sourceSha256) {
|
|
25
|
+
throw new Error(
|
|
26
|
+
`native file transaction source is ${sourceDigest}, expected ${manifest.sourceSha256}`,
|
|
27
|
+
);
|
|
28
|
+
}
|
|
29
|
+
for (const [platform, expected] of Object.entries(manifest.binaries)) {
|
|
30
|
+
const binary = path.join(root, 'bin', platform, 'amalgm-file-transaction');
|
|
31
|
+
const actual = crypto.createHash('sha256').update(fs.readFileSync(binary)).digest('hex');
|
|
32
|
+
if (actual !== expected) {
|
|
33
|
+
throw new Error(`native file transaction ${platform} is ${actual}, expected ${expected}`);
|
|
34
|
+
}
|
|
35
|
+
if ((fs.statSync(binary).mode & 0o111) === 0) {
|
|
36
|
+
throw new Error(`native file transaction ${platform} is not executable`);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
console.log(
|
|
41
|
+
`[native-file-transaction] verified source ${manifest.sourceSha256} `
|
|
42
|
+
+ `and ${Object.keys(manifest.binaries).length} binaries`,
|
|
43
|
+
);
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
//go:build darwin
|
|
2
|
+
|
|
3
|
+
package main
|
|
4
|
+
|
|
5
|
+
/*
|
|
6
|
+
#cgo LDFLAGS: -lproc
|
|
7
|
+
|
|
8
|
+
#include <errno.h>
|
|
9
|
+
#include <libproc.h>
|
|
10
|
+
#include <stdint.h>
|
|
11
|
+
#include <stdlib.h>
|
|
12
|
+
#include <sys/fcntl.h>
|
|
13
|
+
#include <sys/proc_info.h>
|
|
14
|
+
|
|
15
|
+
static int amalgm_has_writable_vnode(uint64_t device, uint64_t inode) {
|
|
16
|
+
int pid_bytes = proc_listpids(PROC_ALL_PIDS, 0, NULL, 0);
|
|
17
|
+
if (pid_bytes <= 0) {
|
|
18
|
+
return -1;
|
|
19
|
+
}
|
|
20
|
+
pid_bytes *= 2;
|
|
21
|
+
pid_t *pids = (pid_t *)calloc(1, (size_t)pid_bytes);
|
|
22
|
+
if (pids == NULL) {
|
|
23
|
+
return -1;
|
|
24
|
+
}
|
|
25
|
+
int listed_pid_bytes = proc_listpids(PROC_ALL_PIDS, 0, pids, pid_bytes);
|
|
26
|
+
if (listed_pid_bytes < 0) {
|
|
27
|
+
free(pids);
|
|
28
|
+
return -1;
|
|
29
|
+
}
|
|
30
|
+
int pid_count = listed_pid_bytes / (int)sizeof(pid_t);
|
|
31
|
+
for (int pid_index = 0; pid_index < pid_count; pid_index++) {
|
|
32
|
+
pid_t pid = pids[pid_index];
|
|
33
|
+
if (pid <= 0) {
|
|
34
|
+
continue;
|
|
35
|
+
}
|
|
36
|
+
int fd_bytes = proc_pidinfo(pid, PROC_PIDLISTFDS, 0, NULL, 0);
|
|
37
|
+
if (fd_bytes <= 0) {
|
|
38
|
+
continue;
|
|
39
|
+
}
|
|
40
|
+
fd_bytes *= 2;
|
|
41
|
+
struct proc_fdinfo *fds = (struct proc_fdinfo *)calloc(1, (size_t)fd_bytes);
|
|
42
|
+
if (fds == NULL) {
|
|
43
|
+
free(pids);
|
|
44
|
+
return -1;
|
|
45
|
+
}
|
|
46
|
+
int listed_fd_bytes = proc_pidinfo(pid, PROC_PIDLISTFDS, 0, fds, fd_bytes);
|
|
47
|
+
if (listed_fd_bytes <= 0) {
|
|
48
|
+
free(fds);
|
|
49
|
+
continue;
|
|
50
|
+
}
|
|
51
|
+
int fd_count = listed_fd_bytes / PROC_PIDLISTFD_SIZE;
|
|
52
|
+
for (int fd_index = 0; fd_index < fd_count; fd_index++) {
|
|
53
|
+
if (fds[fd_index].proc_fdtype != PROX_FDTYPE_VNODE) {
|
|
54
|
+
continue;
|
|
55
|
+
}
|
|
56
|
+
struct vnode_fdinfo info;
|
|
57
|
+
int info_bytes = proc_pidfdinfo(
|
|
58
|
+
pid,
|
|
59
|
+
fds[fd_index].proc_fd,
|
|
60
|
+
PROC_PIDFDVNODEINFO,
|
|
61
|
+
&info,
|
|
62
|
+
PROC_PIDFDVNODEINFO_SIZE
|
|
63
|
+
);
|
|
64
|
+
if (info_bytes != PROC_PIDFDVNODEINFO_SIZE) {
|
|
65
|
+
continue;
|
|
66
|
+
}
|
|
67
|
+
struct vinfo_stat stat = info.pvi.vi_stat;
|
|
68
|
+
if (
|
|
69
|
+
(uint64_t)stat.vst_dev == device
|
|
70
|
+
&& stat.vst_ino == inode
|
|
71
|
+
&& (info.pfi.fi_openflags & FWRITE) != 0
|
|
72
|
+
) {
|
|
73
|
+
free(fds);
|
|
74
|
+
free(pids);
|
|
75
|
+
return 1;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
free(fds);
|
|
79
|
+
}
|
|
80
|
+
free(pids);
|
|
81
|
+
return 0;
|
|
82
|
+
}
|
|
83
|
+
*/
|
|
84
|
+
import "C"
|
|
85
|
+
|
|
86
|
+
import (
|
|
87
|
+
"errors"
|
|
88
|
+
|
|
89
|
+
"golang.org/x/sys/unix"
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
// The source pathname has already been removed, so only a descriptor opened
|
|
93
|
+
// before extraction can still mutate its inode. Two complete libproc scans
|
|
94
|
+
// avoid process-spawn cost and require two independently empty observations
|
|
95
|
+
// before the old inode can be retired.
|
|
96
|
+
func hasWritableOpen(dirFD int, name, _ string) (bool, error) {
|
|
97
|
+
var source unix.Stat_t
|
|
98
|
+
if err := unix.Fstatat(dirFD, name, &source, unix.AT_SYMLINK_NOFOLLOW); err != nil {
|
|
99
|
+
return false, err
|
|
100
|
+
}
|
|
101
|
+
for pass := 0; pass < 2; pass++ {
|
|
102
|
+
result := int(C.amalgm_has_writable_vnode(
|
|
103
|
+
C.uint64_t(uint64(source.Dev)),
|
|
104
|
+
C.uint64_t(uint64(source.Ino)),
|
|
105
|
+
))
|
|
106
|
+
if result < 0 {
|
|
107
|
+
return false, errors.New("libproc could not enumerate writable vnode owners")
|
|
108
|
+
}
|
|
109
|
+
if result > 0 {
|
|
110
|
+
return true, nil
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
return false, nil
|
|
114
|
+
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
//go:build linux
|
|
2
|
+
|
|
3
|
+
package main
|
|
4
|
+
|
|
5
|
+
import (
|
|
6
|
+
"errors"
|
|
7
|
+
"fmt"
|
|
8
|
+
"os"
|
|
9
|
+
"path/filepath"
|
|
10
|
+
"strconv"
|
|
11
|
+
"strings"
|
|
12
|
+
|
|
13
|
+
"golang.org/x/sys/unix"
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
// A successful kernel write lease proves that no other process has the inode
|
|
17
|
+
// open. If the filesystem cannot grant leases, /proc supplies a conservative
|
|
18
|
+
// fallback that treats every writable descriptor as ownership.
|
|
19
|
+
func hasWritableOpen(dirFD int, name, _ string) (bool, error) {
|
|
20
|
+
fd, err := unix.Openat(dirFD, name, unix.O_RDWR|unix.O_NOFOLLOW|unix.O_CLOEXEC, 0)
|
|
21
|
+
if err == nil {
|
|
22
|
+
if _, leaseErr := unix.FcntlInt(uintptr(fd), unix.F_SETLEASE, unix.F_WRLCK); leaseErr == nil {
|
|
23
|
+
_, _ = unix.FcntlInt(uintptr(fd), unix.F_SETLEASE, unix.F_UNLCK)
|
|
24
|
+
_ = unix.Close(fd)
|
|
25
|
+
return false, nil
|
|
26
|
+
}
|
|
27
|
+
_ = unix.Close(fd)
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
var source unix.Stat_t
|
|
31
|
+
if err := unix.Fstatat(dirFD, name, &source, unix.AT_SYMLINK_NOFOLLOW); err != nil {
|
|
32
|
+
return false, err
|
|
33
|
+
}
|
|
34
|
+
processes, err := os.ReadDir("/proc")
|
|
35
|
+
if err != nil {
|
|
36
|
+
return false, fmt.Errorf("read /proc: %w", err)
|
|
37
|
+
}
|
|
38
|
+
for _, process := range processes {
|
|
39
|
+
if !process.IsDir() {
|
|
40
|
+
continue
|
|
41
|
+
}
|
|
42
|
+
if _, ok := parseFD(process.Name()); !ok {
|
|
43
|
+
continue
|
|
44
|
+
}
|
|
45
|
+
fdDirectory := filepath.Join("/proc", process.Name(), "fd")
|
|
46
|
+
descriptors, readErr := os.ReadDir(fdDirectory)
|
|
47
|
+
if readErr != nil {
|
|
48
|
+
if errors.Is(readErr, os.ErrPermission) || errors.Is(readErr, os.ErrNotExist) {
|
|
49
|
+
continue
|
|
50
|
+
}
|
|
51
|
+
return false, readErr
|
|
52
|
+
}
|
|
53
|
+
for _, descriptor := range descriptors {
|
|
54
|
+
fdNumber, ok := parseFD(descriptor.Name())
|
|
55
|
+
if !ok {
|
|
56
|
+
continue
|
|
57
|
+
}
|
|
58
|
+
var held unix.Stat_t
|
|
59
|
+
fdPath := filepath.Join(fdDirectory, descriptor.Name())
|
|
60
|
+
if err := unix.Stat(fdPath, &held); err != nil {
|
|
61
|
+
continue
|
|
62
|
+
}
|
|
63
|
+
if uint64(held.Dev) != uint64(source.Dev) || uint64(held.Ino) != uint64(source.Ino) {
|
|
64
|
+
continue
|
|
65
|
+
}
|
|
66
|
+
info, readErr := os.ReadFile(
|
|
67
|
+
filepath.Join("/proc", process.Name(), "fdinfo", strconv.Itoa(fdNumber)),
|
|
68
|
+
)
|
|
69
|
+
if readErr != nil {
|
|
70
|
+
continue
|
|
71
|
+
}
|
|
72
|
+
for _, line := range strings.Split(string(info), "\n") {
|
|
73
|
+
if !strings.HasPrefix(line, "flags:") {
|
|
74
|
+
continue
|
|
75
|
+
}
|
|
76
|
+
flags, parseErr := strconv.ParseUint(strings.TrimSpace(strings.TrimPrefix(line, "flags:")), 8, 64)
|
|
77
|
+
if parseErr == nil && int(flags)&unix.O_ACCMODE != unix.O_RDONLY {
|
|
78
|
+
return true, nil
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
return false, nil
|
|
84
|
+
}
|