typescript-virtual-container 1.5.11 → 1.6.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 +236 -456
- package/dist/.tsbuildinfo +1 -1
- package/dist/Honeypot/index.d.ts +9 -0
- package/dist/Honeypot/index.js +57 -0
- package/dist/SSHMimic/exec.d.ts +4 -0
- package/dist/SSHMimic/exec.js +4 -0
- package/dist/SSHMimic/executor.d.ts +10 -1
- package/dist/SSHMimic/executor.js +18 -8
- package/dist/SSHMimic/hostKey.d.ts +5 -0
- package/dist/SSHMimic/hostKey.js +5 -0
- package/dist/SSHMimic/loginBanner.d.ts +7 -0
- package/dist/SSHMimic/loginBanner.js +4 -0
- package/dist/SSHMimic/loginFormat.d.ts +4 -0
- package/dist/SSHMimic/loginFormat.js +4 -0
- package/dist/SSHMimic/prompt.d.ts +9 -0
- package/dist/SSHMimic/prompt.js +9 -0
- package/dist/SSHMimic/scp.d.ts +18 -0
- package/dist/SSHMimic/scp.js +14 -0
- package/dist/VirtualFileSystem/binaryPack.d.ts +7 -3
- package/dist/VirtualFileSystem/binaryPack.js +32 -10
- package/dist/VirtualFileSystem/index.d.ts +29 -0
- package/dist/VirtualFileSystem/index.js +126 -5
- package/dist/VirtualFileSystem/internalTypes.d.ts +4 -0
- package/dist/VirtualFileSystem/journal.d.ts +10 -4
- package/dist/VirtualFileSystem/journal.js +12 -2
- package/dist/VirtualFileSystem/path.d.ts +23 -1
- package/dist/VirtualFileSystem/path.js +23 -3
- package/dist/VirtualPackageManager/index.js +1 -1
- package/dist/VirtualShell/index.d.ts +3 -0
- package/dist/VirtualShell/index.js +12 -3
- package/dist/VirtualUserManager/index.d.ts +20 -1
- package/dist/VirtualUserManager/index.js +52 -15
- package/dist/commands/bc.d.ts +5 -0
- package/dist/commands/bc.js +5 -0
- package/dist/commands/cat.js +2 -2
- package/dist/commands/chgrp.d.ts +7 -0
- package/dist/commands/chgrp.js +42 -0
- package/dist/commands/chown.d.ts +7 -0
- package/dist/commands/chown.js +79 -0
- package/dist/commands/cp.js +4 -3
- package/dist/commands/dd.d.ts +7 -0
- package/dist/commands/dd.js +60 -0
- package/dist/commands/declare.js +0 -2
- package/dist/commands/expr.d.ts +7 -0
- package/dist/commands/expr.js +63 -0
- package/dist/commands/fun.d.ts +5 -0
- package/dist/commands/fun.js +5 -1
- package/dist/commands/help.d.ts +5 -0
- package/dist/commands/help.js +5 -0
- package/dist/commands/helpers.d.ts +43 -0
- package/dist/commands/helpers.js +61 -0
- package/dist/commands/id.d.ts +5 -0
- package/dist/commands/id.js +5 -0
- package/dist/commands/ip.d.ts +1 -0
- package/dist/commands/ip.js +50 -23
- package/dist/commands/jobs.js +43 -9
- package/dist/commands/kill.d.ts +1 -0
- package/dist/commands/kill.js +13 -5
- package/dist/commands/last.js +1 -1
- package/dist/commands/ln.d.ts +5 -0
- package/dist/commands/ln.js +5 -0
- package/dist/commands/ls.d.ts +5 -0
- package/dist/commands/ls.js +19 -4
- package/dist/commands/lsb-release.js +1 -1
- package/dist/commands/man.d.ts +5 -0
- package/dist/commands/man.js +5 -0
- package/dist/commands/manuals-bundle.js +242 -0
- package/dist/commands/miscutils.d.ts +43 -0
- package/dist/commands/miscutils.js +233 -0
- package/dist/commands/mkdir.js +3 -2
- package/dist/commands/mv.js +4 -3
- package/dist/commands/netcat.d.ts +7 -0
- package/dist/commands/netcat.js +64 -0
- package/dist/commands/nice.d.ts +7 -0
- package/dist/commands/nice.js +22 -0
- package/dist/commands/nohup.d.ts +7 -0
- package/dist/commands/nohup.js +18 -0
- package/dist/commands/ping.d.ts +2 -1
- package/dist/commands/ping.js +46 -8
- package/dist/commands/procUtils.d.ts +13 -0
- package/dist/commands/procUtils.js +72 -0
- package/dist/commands/pwd.d.ts +5 -0
- package/dist/commands/pwd.js +5 -0
- package/dist/commands/python.js +0 -4
- package/dist/commands/read.js +0 -1
- package/dist/commands/registry.d.ts +37 -0
- package/dist/commands/registry.js +73 -0
- package/dist/commands/rm.js +3 -2
- package/dist/commands/runtime.d.ts +47 -1
- package/dist/commands/runtime.js +60 -5
- package/dist/commands/sh.d.ts +5 -0
- package/dist/commands/sh.js +5 -0
- package/dist/commands/stat.js +3 -2
- package/dist/commands/strace.js +0 -1
- package/dist/commands/sysinfo.d.ts +19 -0
- package/dist/commands/sysinfo.js +73 -0
- package/dist/commands/test.d.ts +5 -0
- package/dist/commands/test.js +5 -0
- package/dist/commands/textutils.d.ts +25 -0
- package/dist/commands/textutils.js +171 -0
- package/dist/commands/top.d.ts +7 -0
- package/dist/commands/top.js +54 -0
- package/dist/commands/touch.js +6 -2
- package/dist/commands/tr.d.ts +5 -0
- package/dist/commands/tr.js +5 -0
- package/dist/commands/w.js +1 -1
- package/dist/commands/which.d.ts +5 -0
- package/dist/commands/which.js +5 -0
- package/dist/index.d.ts +10 -2
- package/dist/index.js +4 -0
- package/dist/modules/VirtualNetworkManager.d.ts +54 -0
- package/dist/modules/VirtualNetworkManager.js +144 -0
- package/dist/modules/linuxRootfs.d.ts +4 -3
- package/dist/modules/linuxRootfs.js +115 -74
- package/dist/modules/neofetch.d.ts +2 -0
- package/dist/modules/neofetch.js +3 -2
- package/dist/modules/pacmanGame.d.ts +2 -0
- package/dist/modules/pacmanGame.js +1 -0
- package/dist/modules/shellInteractive.d.ts +2 -0
- package/dist/modules/shellInteractive.js +2 -0
- package/dist/modules/shellRuntime.d.ts +7 -0
- package/dist/modules/shellRuntime.js +6 -0
- package/dist/modules/webTermRenderer.js +0 -7
- package/dist/types/commands.d.ts +1 -1
- package/dist/types/vfs.d.ts +8 -0
- package/dist/utils/argv.d.ts +22 -3
- package/dist/utils/argv.js +22 -3
- package/dist/utils/perfLogger.d.ts +10 -2
- package/dist/utils/perfLogger.js +7 -14
- package/dist/utils/shellSession.d.ts +35 -0
- package/dist/utils/shellSession.js +35 -0
- package/package.json +1 -1
|
@@ -50,6 +50,12 @@ class VirtualFileSystem extends EventEmitter {
|
|
|
50
50
|
mounts = new Map();
|
|
51
51
|
/** Sorted mounts cache (longest-path-first). Rebuilt lazily on mount/unmount. */
|
|
52
52
|
_sortedMounts = null;
|
|
53
|
+
/** Read hooks: path prefix → callback invoked before reading any file under that prefix. */
|
|
54
|
+
readHooks = new Map();
|
|
55
|
+
/** Sorted read hook prefixes (longest-first) for matching. */
|
|
56
|
+
_sortedReadHooks = null;
|
|
57
|
+
/** Re-entrancy guard for read hooks — prevents infinite loop when hook triggers another read. */
|
|
58
|
+
_inReadHook = false;
|
|
53
59
|
/** True when running in a browser environment (no host FS access). */
|
|
54
60
|
static isBrowser = typeof process === "undefined" || typeof process.versions?.node === "undefined";
|
|
55
61
|
constructor(options = {}) {
|
|
@@ -85,12 +91,14 @@ class VirtualFileSystem extends EventEmitter {
|
|
|
85
91
|
this.root = this.makeDir("", 0o755);
|
|
86
92
|
}
|
|
87
93
|
// ── Internal helpers ──────────────────────────────────────────────────────
|
|
88
|
-
makeDir(name, mode) {
|
|
94
|
+
makeDir(name, mode, uid = 0, gid = 0) {
|
|
89
95
|
const now = Date.now();
|
|
90
96
|
return {
|
|
91
97
|
type: "directory",
|
|
92
98
|
name,
|
|
93
99
|
mode,
|
|
100
|
+
uid,
|
|
101
|
+
gid,
|
|
94
102
|
createdAt: now,
|
|
95
103
|
updatedAt: now,
|
|
96
104
|
children: Object.create(null),
|
|
@@ -98,21 +106,23 @@ class VirtualFileSystem extends EventEmitter {
|
|
|
98
106
|
_sortedKeys: null,
|
|
99
107
|
};
|
|
100
108
|
}
|
|
101
|
-
makeFile(name, content, mode, compressed) {
|
|
109
|
+
makeFile(name, content, mode, compressed, uid = 0, gid = 0) {
|
|
102
110
|
const now = Date.now();
|
|
103
111
|
return {
|
|
104
112
|
type: "file",
|
|
105
113
|
name,
|
|
106
114
|
content,
|
|
107
115
|
mode,
|
|
116
|
+
uid,
|
|
117
|
+
gid,
|
|
108
118
|
compressed,
|
|
109
119
|
createdAt: now,
|
|
110
120
|
updatedAt: now,
|
|
111
121
|
};
|
|
112
122
|
}
|
|
113
|
-
makeStub(name, content, mode) {
|
|
123
|
+
makeStub(name, content, mode, uid = 0, gid = 0) {
|
|
114
124
|
const now = Date.now();
|
|
115
|
-
return { type: "stub", name, stubContent: content, mode, createdAt: now, updatedAt: now };
|
|
125
|
+
return { type: "stub", name, stubContent: content, mode, uid, gid, createdAt: now, updatedAt: now };
|
|
116
126
|
}
|
|
117
127
|
/**
|
|
118
128
|
* Write a lazy stub — stores content as a plain string with no Buffer allocation.
|
|
@@ -520,6 +530,43 @@ class VirtualFileSystem extends EventEmitter {
|
|
|
520
530
|
vPath, ...opts,
|
|
521
531
|
}));
|
|
522
532
|
}
|
|
533
|
+
/**
|
|
534
|
+
* Register a callback that is invoked before any read under `prefix`.
|
|
535
|
+
* Used by /proc to refresh dynamic content on every access.
|
|
536
|
+
*/
|
|
537
|
+
onBeforeRead(prefix, cb) {
|
|
538
|
+
const normalized = normalizePath(prefix);
|
|
539
|
+
this.readHooks.set(normalized, cb);
|
|
540
|
+
this._sortedReadHooks = [...this.readHooks.keys()].sort((a, b) => b.length - a.length);
|
|
541
|
+
}
|
|
542
|
+
/** Remove a previously registered read hook. */
|
|
543
|
+
offBeforeRead(prefix) {
|
|
544
|
+
const normalized = normalizePath(prefix);
|
|
545
|
+
this.readHooks.delete(normalized);
|
|
546
|
+
this._sortedReadHooks = [...this.readHooks.keys()].sort((a, b) => b.length - a.length);
|
|
547
|
+
}
|
|
548
|
+
/** Invoke any matching read hook for `normalizedPath`. */
|
|
549
|
+
_triggerReadHook(normalizedPath) {
|
|
550
|
+
if (this._inReadHook)
|
|
551
|
+
return;
|
|
552
|
+
if (!this._sortedReadHooks)
|
|
553
|
+
return;
|
|
554
|
+
for (const prefix of this._sortedReadHooks) {
|
|
555
|
+
if (normalizedPath === prefix || normalizedPath.startsWith(`${prefix}/`)) {
|
|
556
|
+
const cb = this.readHooks.get(prefix);
|
|
557
|
+
if (cb) {
|
|
558
|
+
this._inReadHook = true;
|
|
559
|
+
try {
|
|
560
|
+
cb();
|
|
561
|
+
}
|
|
562
|
+
finally {
|
|
563
|
+
this._inReadHook = false;
|
|
564
|
+
}
|
|
565
|
+
return;
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
}
|
|
523
570
|
/**
|
|
524
571
|
* If `targetPath` is inside a mount, return `{ hostPath, readOnly, relPath }`.
|
|
525
572
|
* `relPath` is the path relative to the mount's host directory.
|
|
@@ -614,6 +661,7 @@ class VirtualFileSystem extends EventEmitter {
|
|
|
614
661
|
return fsSync.readFileSync(m.fullHostPath, "utf8");
|
|
615
662
|
}
|
|
616
663
|
const normalized = normalizePath(targetPath);
|
|
664
|
+
this._triggerReadHook(normalized);
|
|
617
665
|
const node = getNodeNormalized(this.root, normalized);
|
|
618
666
|
if (node.type === "stub") {
|
|
619
667
|
this.emit("file:read", { path: normalized, size: node.stubContent.length });
|
|
@@ -638,6 +686,7 @@ class VirtualFileSystem extends EventEmitter {
|
|
|
638
686
|
return fsSync.readFileSync(m.fullHostPath);
|
|
639
687
|
}
|
|
640
688
|
const normalized = normalizePath(targetPath);
|
|
689
|
+
this._triggerReadHook(normalized);
|
|
641
690
|
const node = getNodeNormalized(this.root, normalized);
|
|
642
691
|
if (node.type === "stub") {
|
|
643
692
|
const buf = Buffer.from(node.stubContent, "utf8");
|
|
@@ -659,8 +708,9 @@ class VirtualFileSystem extends EventEmitter {
|
|
|
659
708
|
const m = this.resolveMount(targetPath);
|
|
660
709
|
if (m)
|
|
661
710
|
return fsSync.existsSync(m.fullHostPath);
|
|
711
|
+
const normalized = normalizePath(targetPath);
|
|
662
712
|
try {
|
|
663
|
-
getNodeNormalized(this.root,
|
|
713
|
+
getNodeNormalized(this.root, normalized);
|
|
664
714
|
return true;
|
|
665
715
|
}
|
|
666
716
|
catch {
|
|
@@ -673,6 +723,51 @@ class VirtualFileSystem extends EventEmitter {
|
|
|
673
723
|
getNodeNormalized(this.root, normalized).mode = mode;
|
|
674
724
|
this._journal({ op: JournalOp.CHMOD, path: normalized, mode });
|
|
675
725
|
}
|
|
726
|
+
/** Changes ownership (uid/gid) of a file or directory. */
|
|
727
|
+
chown(targetPath, uid, gid) {
|
|
728
|
+
const normalized = normalizePath(targetPath);
|
|
729
|
+
const node = getNodeNormalized(this.root, normalized);
|
|
730
|
+
node.uid = uid;
|
|
731
|
+
node.gid = gid;
|
|
732
|
+
this._journal({ op: JournalOp.CHMOD, path: normalized, mode: node.mode });
|
|
733
|
+
}
|
|
734
|
+
/** Returns the uid and gid of a node. */
|
|
735
|
+
getOwner(targetPath) {
|
|
736
|
+
const node = getNodeNormalized(this.root, normalizePath(targetPath));
|
|
737
|
+
return { uid: node.uid, gid: node.gid };
|
|
738
|
+
}
|
|
739
|
+
/**
|
|
740
|
+
* POSIX-style access check: does `uid`/`gid` have `want` permission on `targetPath`?
|
|
741
|
+
* `want` is a bitmask of R_OK (4), W_OK (2), X_OK (1).
|
|
742
|
+
* Root (uid === 0) is granted everything except X_OK without at least one x bit set.
|
|
743
|
+
* Returns true when access is granted.
|
|
744
|
+
*/
|
|
745
|
+
checkAccess(targetPath, uid, gid, want) {
|
|
746
|
+
try {
|
|
747
|
+
const node = getNodeNormalized(this.root, normalizePath(targetPath));
|
|
748
|
+
const mode = node.mode;
|
|
749
|
+
// Root: allowed everything except execute (at least one x bit needed)
|
|
750
|
+
if (uid === 0) {
|
|
751
|
+
if (want & 1)
|
|
752
|
+
return (mode & 0o111) !== 0;
|
|
753
|
+
return true;
|
|
754
|
+
}
|
|
755
|
+
let perm = 0;
|
|
756
|
+
if (uid === node.uid) {
|
|
757
|
+
perm = (mode >> 6) & 7; // owner bits
|
|
758
|
+
}
|
|
759
|
+
else if (gid === node.gid) {
|
|
760
|
+
perm = (mode >> 3) & 7; // group bits
|
|
761
|
+
}
|
|
762
|
+
else {
|
|
763
|
+
perm = mode & 7; // other bits
|
|
764
|
+
}
|
|
765
|
+
return (perm & want) === want;
|
|
766
|
+
}
|
|
767
|
+
catch {
|
|
768
|
+
return false;
|
|
769
|
+
}
|
|
770
|
+
}
|
|
676
771
|
/** Returns metadata for a file or directory. */
|
|
677
772
|
stat(targetPath) {
|
|
678
773
|
const m = this.resolveMount(targetPath);
|
|
@@ -688,6 +783,8 @@ class VirtualFileSystem extends EventEmitter {
|
|
|
688
783
|
name,
|
|
689
784
|
path: normalizePath(targetPath),
|
|
690
785
|
mode: 0o755,
|
|
786
|
+
uid: 0,
|
|
787
|
+
gid: 0,
|
|
691
788
|
createdAt: hst.birthtime,
|
|
692
789
|
updatedAt: now,
|
|
693
790
|
childrenCount: fsSync.readdirSync(m.fullHostPath).length,
|
|
@@ -698,6 +795,8 @@ class VirtualFileSystem extends EventEmitter {
|
|
|
698
795
|
name,
|
|
699
796
|
path: normalizePath(targetPath),
|
|
700
797
|
mode: m.readOnly ? 0o444 : 0o644,
|
|
798
|
+
uid: 0,
|
|
799
|
+
gid: 0,
|
|
701
800
|
createdAt: hst.birthtime,
|
|
702
801
|
updatedAt: now,
|
|
703
802
|
compressed: false,
|
|
@@ -705,6 +804,8 @@ class VirtualFileSystem extends EventEmitter {
|
|
|
705
804
|
};
|
|
706
805
|
}
|
|
707
806
|
const normalized = normalizePath(targetPath);
|
|
807
|
+
if (normalized.startsWith("/proc"))
|
|
808
|
+
this._triggerReadHook(normalized);
|
|
708
809
|
const node = getNodeNormalized(this.root, normalized);
|
|
709
810
|
const name = normalized === "/" ? "" : path.posix.basename(normalized);
|
|
710
811
|
if (node.type === "stub") {
|
|
@@ -714,6 +815,8 @@ class VirtualFileSystem extends EventEmitter {
|
|
|
714
815
|
name,
|
|
715
816
|
path: normalized,
|
|
716
817
|
mode: s.mode,
|
|
818
|
+
uid: s.uid,
|
|
819
|
+
gid: s.gid,
|
|
717
820
|
createdAt: new Date(s.createdAt),
|
|
718
821
|
updatedAt: new Date(s.updatedAt),
|
|
719
822
|
compressed: false,
|
|
@@ -727,6 +830,8 @@ class VirtualFileSystem extends EventEmitter {
|
|
|
727
830
|
name,
|
|
728
831
|
path: normalized,
|
|
729
832
|
mode: f.mode,
|
|
833
|
+
uid: f.uid,
|
|
834
|
+
gid: f.gid,
|
|
730
835
|
createdAt: new Date(f.createdAt),
|
|
731
836
|
updatedAt: new Date(f.updatedAt),
|
|
732
837
|
compressed: f.compressed,
|
|
@@ -739,6 +844,8 @@ class VirtualFileSystem extends EventEmitter {
|
|
|
739
844
|
name,
|
|
740
845
|
path: normalized,
|
|
741
846
|
mode: d.mode,
|
|
847
|
+
uid: d.uid,
|
|
848
|
+
gid: d.gid,
|
|
742
849
|
createdAt: new Date(d.createdAt),
|
|
743
850
|
updatedAt: new Date(d.updatedAt),
|
|
744
851
|
childrenCount: d._childCount,
|
|
@@ -778,6 +885,8 @@ class VirtualFileSystem extends EventEmitter {
|
|
|
778
885
|
}
|
|
779
886
|
}
|
|
780
887
|
const normalized = normalizePath(dirPath);
|
|
888
|
+
if (normalized.startsWith("/proc"))
|
|
889
|
+
this._triggerReadHook(normalized);
|
|
781
890
|
const node = getNodeNormalized(this.root, normalized);
|
|
782
891
|
if (node.type !== "directory") {
|
|
783
892
|
throw new Error(`Cannot list '${dirPath}': not a directory.`);
|
|
@@ -873,6 +982,8 @@ class VirtualFileSystem extends EventEmitter {
|
|
|
873
982
|
name,
|
|
874
983
|
content: Buffer.from(normalizedTarget, "utf8"),
|
|
875
984
|
mode: 0o120777,
|
|
985
|
+
uid: 0,
|
|
986
|
+
gid: 0,
|
|
876
987
|
compressed: false,
|
|
877
988
|
createdAt: Date.now(),
|
|
878
989
|
updatedAt: Date.now(),
|
|
@@ -997,6 +1108,8 @@ class VirtualFileSystem extends EventEmitter {
|
|
|
997
1108
|
type: "file",
|
|
998
1109
|
name: child.name,
|
|
999
1110
|
mode: child.mode,
|
|
1111
|
+
uid: child.uid,
|
|
1112
|
+
gid: child.gid,
|
|
1000
1113
|
createdAt: new Date(child.createdAt).toISOString(),
|
|
1001
1114
|
updatedAt: new Date(child.updatedAt).toISOString(),
|
|
1002
1115
|
compressed: false,
|
|
@@ -1014,6 +1127,8 @@ class VirtualFileSystem extends EventEmitter {
|
|
|
1014
1127
|
type: "directory",
|
|
1015
1128
|
name: dir.name,
|
|
1016
1129
|
mode: dir.mode,
|
|
1130
|
+
uid: dir.uid,
|
|
1131
|
+
gid: dir.gid,
|
|
1017
1132
|
createdAt: new Date(dir.createdAt).toISOString(),
|
|
1018
1133
|
updatedAt: new Date(dir.updatedAt).toISOString(),
|
|
1019
1134
|
children,
|
|
@@ -1024,6 +1139,8 @@ class VirtualFileSystem extends EventEmitter {
|
|
|
1024
1139
|
type: "file",
|
|
1025
1140
|
name: file.name,
|
|
1026
1141
|
mode: file.mode,
|
|
1142
|
+
uid: file.uid,
|
|
1143
|
+
gid: file.gid,
|
|
1027
1144
|
createdAt: new Date(file.createdAt).toISOString(),
|
|
1028
1145
|
updatedAt: new Date(file.updatedAt).toISOString(),
|
|
1029
1146
|
compressed: file.compressed,
|
|
@@ -1061,6 +1178,8 @@ class VirtualFileSystem extends EventEmitter {
|
|
|
1061
1178
|
type: "directory",
|
|
1062
1179
|
name,
|
|
1063
1180
|
mode: snap.mode,
|
|
1181
|
+
uid: snap.uid ?? 0,
|
|
1182
|
+
gid: snap.gid ?? 0,
|
|
1064
1183
|
createdAt: Date.parse(snap.createdAt),
|
|
1065
1184
|
updatedAt: Date.parse(snap.updatedAt),
|
|
1066
1185
|
children: Object.create(null),
|
|
@@ -1074,6 +1193,8 @@ class VirtualFileSystem extends EventEmitter {
|
|
|
1074
1193
|
type: "file",
|
|
1075
1194
|
name: f.name,
|
|
1076
1195
|
mode: f.mode,
|
|
1196
|
+
uid: f.uid ?? 0,
|
|
1197
|
+
gid: f.gid ?? 0,
|
|
1077
1198
|
createdAt: Date.parse(f.createdAt),
|
|
1078
1199
|
updatedAt: Date.parse(f.updatedAt),
|
|
1079
1200
|
compressed: f.compressed,
|
|
@@ -3,6 +3,10 @@ export type InternalNode = InternalFileNode | InternalStubNode | InternalDirecto
|
|
|
3
3
|
interface InternalBaseNode {
|
|
4
4
|
name: string;
|
|
5
5
|
mode: number;
|
|
6
|
+
/** Owner user ID (0 = root). */
|
|
7
|
+
uid: number;
|
|
8
|
+
/** Owner group ID (0 = root). */
|
|
9
|
+
gid: number;
|
|
6
10
|
/** Unix timestamp in ms — avoids Date object overhead (~80 bytes each). */
|
|
7
11
|
createdAt: number;
|
|
8
12
|
/** Unix timestamp in ms. */
|
|
@@ -18,6 +18,16 @@
|
|
|
18
18
|
* 0x06 SYMLINK — [2B target_len] [target bytes]
|
|
19
19
|
*/
|
|
20
20
|
/** biome-ignore-all lint/style/useNamingConvention: JournalOp modes */
|
|
21
|
+
/**
|
|
22
|
+
* Journal operation type codes used in VFS write-ahead log entries.
|
|
23
|
+
*
|
|
24
|
+
* - `WRITE` — Write file content and mode.
|
|
25
|
+
* - `MKDIR` — Create a directory.
|
|
26
|
+
* - `REMOVE` — Delete a file or directory.
|
|
27
|
+
* - `CHMOD` — Change file mode.
|
|
28
|
+
* - `MOVE` — Rename / move a node.
|
|
29
|
+
* - `SYMLINK`— Create a symbolic link.
|
|
30
|
+
*/
|
|
21
31
|
export declare const JournalOp: {
|
|
22
32
|
readonly WRITE: 1;
|
|
23
33
|
readonly MKDIR: 2;
|
|
@@ -35,10 +45,6 @@ export interface JournalEntry {
|
|
|
35
45
|
mode?: number;
|
|
36
46
|
dest?: string;
|
|
37
47
|
}
|
|
38
|
-
/** Serialise one entry to a Buffer. */
|
|
39
|
-
export declare function encodeEntry(e: JournalEntry): Buffer;
|
|
40
|
-
/** Parse all entries from a journal Buffer. Returns empty array on corrupt data. */
|
|
41
|
-
export declare function decodeJournal(buf: Buffer): JournalEntry[];
|
|
42
48
|
/** Append a single entry to the journal file (O_APPEND, atomic write). */
|
|
43
49
|
export declare function appendJournalEntry(journalPath: string, entry: JournalEntry): void;
|
|
44
50
|
/** Read and decode all entries from a journal file. Returns [] if file is absent/empty. */
|
|
@@ -19,6 +19,16 @@
|
|
|
19
19
|
*/
|
|
20
20
|
/** biome-ignore-all lint/style/useNamingConvention: JournalOp modes */
|
|
21
21
|
import * as fsSync from "node:fs";
|
|
22
|
+
/**
|
|
23
|
+
* Journal operation type codes used in VFS write-ahead log entries.
|
|
24
|
+
*
|
|
25
|
+
* - `WRITE` — Write file content and mode.
|
|
26
|
+
* - `MKDIR` — Create a directory.
|
|
27
|
+
* - `REMOVE` — Delete a file or directory.
|
|
28
|
+
* - `CHMOD` — Change file mode.
|
|
29
|
+
* - `MOVE` — Rename / move a node.
|
|
30
|
+
* - `SYMLINK`— Create a symbolic link.
|
|
31
|
+
*/
|
|
22
32
|
export const JournalOp = {
|
|
23
33
|
WRITE: 0x01,
|
|
24
34
|
MKDIR: 0x02,
|
|
@@ -35,7 +45,7 @@ function writeString2(buf, offset, s) {
|
|
|
35
45
|
return 2 + b.length;
|
|
36
46
|
}
|
|
37
47
|
/** Serialise one entry to a Buffer. */
|
|
38
|
-
|
|
48
|
+
function encodeEntry(e) {
|
|
39
49
|
const pathBuf = Buffer.from(e.path, ENC);
|
|
40
50
|
let payloadLen = 0;
|
|
41
51
|
if (e.op === JournalOp.WRITE) {
|
|
@@ -84,7 +94,7 @@ export function encodeEntry(e) {
|
|
|
84
94
|
return buf;
|
|
85
95
|
}
|
|
86
96
|
/** Parse all entries from a journal Buffer. Returns empty array on corrupt data. */
|
|
87
|
-
|
|
97
|
+
function decodeJournal(buf) {
|
|
88
98
|
const entries = [];
|
|
89
99
|
let off = 0;
|
|
90
100
|
try {
|
|
@@ -1,12 +1,34 @@
|
|
|
1
1
|
import type { InternalDirectoryNode, InternalNode } from "./internalTypes";
|
|
2
|
+
/**
|
|
3
|
+
* Normalizes a VFS path by resolving `.` and `..` segments and removing trailing slash.
|
|
4
|
+
* Empty or blank input returns `"/"`.
|
|
5
|
+
* @param rawPath - The path to normalize (may be relative — leading `/` is added if missing).
|
|
6
|
+
* @returns The normalized absolute POSIX path.
|
|
7
|
+
*/
|
|
2
8
|
export declare function normalizePath(rawPath: string): string;
|
|
3
|
-
|
|
9
|
+
/**
|
|
10
|
+
* Retrieves a node from the VFS tree by path string. Normalizes the path first.
|
|
11
|
+
* @param root - The root directory node of the virtual filesystem.
|
|
12
|
+
* @param targetPath - The path to look up (e.g. `"/usr/bin"`).
|
|
13
|
+
* @returns The internal node at the given path.
|
|
14
|
+
* @throws If any path component does not exist or an intermediate entry is not a directory.
|
|
15
|
+
*/
|
|
4
16
|
export declare function getNode(root: InternalDirectoryNode, targetPath: string): InternalNode;
|
|
5
17
|
/**
|
|
6
18
|
* Like getNode but skips normalization — caller must pass an already-normalized path.
|
|
7
19
|
* Avoids double normalizePath() when the caller has already normalized.
|
|
8
20
|
*/
|
|
9
21
|
export declare function getNodeNormalized(root: InternalDirectoryNode, normalized: string): InternalNode;
|
|
22
|
+
/**
|
|
23
|
+
* Resolves a path to its parent directory node and the final path component name.
|
|
24
|
+
* Optionally creates intermediate directories when `createIfMissing` is set.
|
|
25
|
+
* @param root - The root directory node of the virtual filesystem.
|
|
26
|
+
* @param targetPath - The path whose parent to resolve.
|
|
27
|
+
* @param createIfMissing - If true, missing parent directories are created via `createPath`.
|
|
28
|
+
* @param createPath - Callback invoked with each missing directory path to create it.
|
|
29
|
+
* @returns An object with the parent directory node and the final component name.
|
|
30
|
+
* @throws If the path is root or the resolved parent is not a directory.
|
|
31
|
+
*/
|
|
10
32
|
export declare function getParentDirectory(root: InternalDirectoryNode, targetPath: string, createIfMissing: boolean, createPath: (pathToCreate: string) => void): {
|
|
11
33
|
parent: InternalDirectoryNode;
|
|
12
34
|
name: string;
|
|
@@ -1,4 +1,10 @@
|
|
|
1
1
|
import * as path from "node:path";
|
|
2
|
+
/**
|
|
3
|
+
* Normalizes a VFS path by resolving `.` and `..` segments and removing trailing slash.
|
|
4
|
+
* Empty or blank input returns `"/"`.
|
|
5
|
+
* @param rawPath - The path to normalize (may be relative — leading `/` is added if missing).
|
|
6
|
+
* @returns The normalized absolute POSIX path.
|
|
7
|
+
*/
|
|
2
8
|
export function normalizePath(rawPath) {
|
|
3
9
|
if (!rawPath || rawPath.trim() === "") {
|
|
4
10
|
return "/";
|
|
@@ -6,9 +12,13 @@ export function normalizePath(rawPath) {
|
|
|
6
12
|
const normalized = path.posix.normalize(rawPath.startsWith("/") ? rawPath : `/${rawPath}`);
|
|
7
13
|
return normalized === "" ? "/" : normalized;
|
|
8
14
|
}
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
15
|
+
/**
|
|
16
|
+
* Retrieves a node from the VFS tree by path string. Normalizes the path first.
|
|
17
|
+
* @param root - The root directory node of the virtual filesystem.
|
|
18
|
+
* @param targetPath - The path to look up (e.g. `"/usr/bin"`).
|
|
19
|
+
* @returns The internal node at the given path.
|
|
20
|
+
* @throws If any path component does not exist or an intermediate entry is not a directory.
|
|
21
|
+
*/
|
|
12
22
|
export function getNode(root, targetPath) {
|
|
13
23
|
const normalized = normalizePath(targetPath);
|
|
14
24
|
return getNodeNormalized(root, normalized);
|
|
@@ -41,6 +51,16 @@ export function getNodeNormalized(root, normalized) {
|
|
|
41
51
|
}
|
|
42
52
|
return current;
|
|
43
53
|
}
|
|
54
|
+
/**
|
|
55
|
+
* Resolves a path to its parent directory node and the final path component name.
|
|
56
|
+
* Optionally creates intermediate directories when `createIfMissing` is set.
|
|
57
|
+
* @param root - The root directory node of the virtual filesystem.
|
|
58
|
+
* @param targetPath - The path whose parent to resolve.
|
|
59
|
+
* @param createIfMissing - If true, missing parent directories are created via `createPath`.
|
|
60
|
+
* @param createPath - Callback invoked with each missing directory path to create it.
|
|
61
|
+
* @returns An object with the parent directory node and the final component name.
|
|
62
|
+
* @throws If the path is root or the resolved parent is not a directory.
|
|
63
|
+
*/
|
|
44
64
|
export function getParentDirectory(root, targetPath, createIfMissing, createPath) {
|
|
45
65
|
const normalized = normalizePath(targetPath);
|
|
46
66
|
if (normalized === "/") {
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { EventEmitter } from "node:events";
|
|
2
|
+
import { VirtualNetworkManager } from "../modules/VirtualNetworkManager";
|
|
2
3
|
import type { CommandContext, CommandResult } from "../types/commands";
|
|
3
4
|
import type { ShellStream } from "../types/streams";
|
|
4
5
|
import type { VfsNodeStats } from "../types/vfs";
|
|
@@ -88,6 +89,8 @@ declare class VirtualShell extends EventEmitter {
|
|
|
88
89
|
users: VirtualUserManager;
|
|
89
90
|
/** APT/dpkg package manager backed by the built-in package registry. */
|
|
90
91
|
packageManager: VirtualPackageManager;
|
|
92
|
+
/** Virtual network stack with interfaces, routes, and ARP cache. */
|
|
93
|
+
network: VirtualNetworkManager;
|
|
91
94
|
/** Hostname shown in the shell prompt and SSH ident string. */
|
|
92
95
|
hostname: string;
|
|
93
96
|
/** Distro identity strings surfaced by `uname`, `neofetch`, etc. */
|
|
@@ -2,6 +2,7 @@ import { EventEmitter } from "node:events";
|
|
|
2
2
|
import { createCustomCommand, registerCommand } from "../commands/registry";
|
|
3
3
|
import { runCommand } from "../commands/runtime";
|
|
4
4
|
import { bootstrapLinuxRootfs, refreshProc, syncEtcPasswd, } from "../modules/linuxRootfs";
|
|
5
|
+
import { VirtualNetworkManager } from "../modules/VirtualNetworkManager";
|
|
5
6
|
import { createPerfLogger } from "../utils/perfLogger";
|
|
6
7
|
import VirtualFileSystem, {} from "../VirtualFileSystem";
|
|
7
8
|
import { VirtualPackageManager } from "../VirtualPackageManager";
|
|
@@ -70,6 +71,8 @@ class VirtualShell extends EventEmitter {
|
|
|
70
71
|
users;
|
|
71
72
|
/** APT/dpkg package manager backed by the built-in package registry. */
|
|
72
73
|
packageManager;
|
|
74
|
+
/** Virtual network stack with interfaces, routes, and ARP cache. */
|
|
75
|
+
network;
|
|
73
76
|
/** Hostname shown in the shell prompt and SSH ident string. */
|
|
74
77
|
hostname;
|
|
75
78
|
/** Distro identity strings surfaced by `uname`, `neofetch`, etc. */
|
|
@@ -103,18 +106,24 @@ class VirtualShell extends EventEmitter {
|
|
|
103
106
|
}
|
|
104
107
|
this.users = new VirtualUserManager(this.vfs, resolveAutoSudoForNewUsers());
|
|
105
108
|
this.packageManager = new VirtualPackageManager(this.vfs, this.users);
|
|
109
|
+
this.network = new VirtualNetworkManager();
|
|
106
110
|
// Store references to avoid TypeScript "used before assigned" errors
|
|
107
111
|
const vfs = this.vfs;
|
|
108
112
|
const users = this.users;
|
|
109
113
|
const shellProps = this.properties;
|
|
110
114
|
const shellHostname = this.hostname;
|
|
111
115
|
const startTime = this.startTime;
|
|
116
|
+
const network = this.network;
|
|
112
117
|
// Initialize both VFS mirror and users, ensuring all is ready before auth
|
|
113
118
|
this.initialized = (async () => {
|
|
114
119
|
await vfs.restoreMirror();
|
|
115
120
|
await users.initialize();
|
|
116
121
|
// Bootstrap Linux rootfs (idempotent)
|
|
117
|
-
bootstrapLinuxRootfs(vfs, users, shellHostname, shellProps, startTime);
|
|
122
|
+
bootstrapLinuxRootfs(vfs, users, shellHostname, shellProps, startTime, [], network);
|
|
123
|
+
// Register read hook: refresh /proc dynamically on every access
|
|
124
|
+
vfs.onBeforeRead("/proc", () => {
|
|
125
|
+
refreshProc(vfs, shellProps, shellHostname, startTime, users.listActiveSessions(), network);
|
|
126
|
+
});
|
|
118
127
|
this.emit("initialized");
|
|
119
128
|
})();
|
|
120
129
|
}
|
|
@@ -195,7 +204,7 @@ class VirtualShell extends EventEmitter {
|
|
|
195
204
|
* reading `/proc` files for up-to-date values.
|
|
196
205
|
*/
|
|
197
206
|
refreshProcFs() {
|
|
198
|
-
refreshProc(this.vfs, this.properties, this.hostname, this.startTime, this.users.listActiveSessions());
|
|
207
|
+
refreshProc(this.vfs, this.properties, this.hostname, this.startTime, this.users.listActiveSessions(), this.network);
|
|
199
208
|
}
|
|
200
209
|
/**
|
|
201
210
|
* Mount a host directory into the VFS at `vPath`.
|
|
@@ -232,7 +241,7 @@ class VirtualShell extends EventEmitter {
|
|
|
232
241
|
* whenever a session is registered or unregistered.
|
|
233
242
|
*/
|
|
234
243
|
refreshProcSessions() {
|
|
235
|
-
refreshProc(this.vfs, this.properties, this.hostname, this.startTime, this.users.listActiveSessions());
|
|
244
|
+
refreshProc(this.vfs, this.properties, this.hostname, this.startTime, this.users.listActiveSessions(), this.network);
|
|
236
245
|
}
|
|
237
246
|
/**
|
|
238
247
|
* Syncs `/etc/passwd`, `/etc/group`, and `/etc/shadow` from the current
|
|
@@ -7,11 +7,16 @@ import type VirtualFileSystem from "../VirtualFileSystem";
|
|
|
7
7
|
export interface VirtualUserRecord {
|
|
8
8
|
/** Unique login name. */
|
|
9
9
|
username: string;
|
|
10
|
+
/** Numeric user ID. */
|
|
11
|
+
uid: number;
|
|
12
|
+
/** Primary group ID. */
|
|
13
|
+
gid: number;
|
|
10
14
|
/** Per-user random salt used for password hashing. */
|
|
11
15
|
salt: string;
|
|
12
16
|
/** Scrypt-derived password hash in hex encoding. */
|
|
13
17
|
passwordHash: string;
|
|
14
18
|
}
|
|
19
|
+
export type ProcessStatus = "running" | "stopped" | "done";
|
|
15
20
|
/** Runtime representation of a command currently executing in a session. */
|
|
16
21
|
export interface VirtualProcess {
|
|
17
22
|
/** Unique process identifier (auto-incremented). */
|
|
@@ -26,6 +31,10 @@ export interface VirtualProcess {
|
|
|
26
31
|
tty: string;
|
|
27
32
|
/** ISO-8601 start timestamp. */
|
|
28
33
|
startedAt: string;
|
|
34
|
+
/** Current process state. */
|
|
35
|
+
status: ProcessStatus;
|
|
36
|
+
/** AbortController for terminating the process. */
|
|
37
|
+
abortController?: AbortController;
|
|
29
38
|
}
|
|
30
39
|
/** Runtime representation of authenticated SSH session. */
|
|
31
40
|
export interface VirtualActiveSession {
|
|
@@ -61,6 +70,8 @@ export declare class VirtualUserManager extends EventEmitter {
|
|
|
61
70
|
private readonly activeProcesses;
|
|
62
71
|
private nextTty;
|
|
63
72
|
private nextPid;
|
|
73
|
+
private nextUid;
|
|
74
|
+
private nextGid;
|
|
64
75
|
/**
|
|
65
76
|
* Creates a user manager instance backed by a virtual filesystem.
|
|
66
77
|
*
|
|
@@ -213,15 +224,23 @@ export declare class VirtualUserManager extends EventEmitter {
|
|
|
213
224
|
* @returns Array of username strings sorted alphabetically.
|
|
214
225
|
*/
|
|
215
226
|
listUsers(): string[];
|
|
227
|
+
/** Returns the numeric UID for a username, or 0 if unknown. */
|
|
228
|
+
getUid(username: string): number;
|
|
229
|
+
/** Returns the primary GID for a username, or 0 if unknown. */
|
|
230
|
+
getGid(username: string): number;
|
|
216
231
|
/**
|
|
217
232
|
* Registers a running command as a virtual process.
|
|
218
233
|
* Returns the assigned PID so the caller can deregister on completion.
|
|
219
234
|
*/
|
|
220
|
-
registerProcess(username: string, command: string, argv: string[], tty: string): number;
|
|
235
|
+
registerProcess(username: string, command: string, argv: string[], tty: string, abortController?: AbortController): number;
|
|
221
236
|
/** Removes a process record when the command exits. */
|
|
222
237
|
unregisterProcess(pid: number): void;
|
|
238
|
+
/** Marks a process as done (keeps it in the table briefly for jobs/ps). */
|
|
239
|
+
markProcessDone(pid: number): void;
|
|
223
240
|
/** Returns all currently running processes sorted by PID. */
|
|
224
241
|
listProcesses(): VirtualProcess[];
|
|
242
|
+
/** Terminate a process by PID. Returns true if the process was found and signalled. */
|
|
243
|
+
killProcess(pid: number): boolean;
|
|
225
244
|
private loadFromVfs;
|
|
226
245
|
private loadSudoersFromVfs;
|
|
227
246
|
private loadQuotasFromVfs;
|