@axiom-lattice/microsandbox-service 0.0.19 → 0.0.21
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +12 -0
- package/README.md +9 -0
- package/dist/{chunk-GD6KBLVJ.mjs → chunk-CPCTPFAF.mjs} +2 -2
- package/dist/{chunk-PIAUYZEQ.mjs → chunk-OFHOMKOA.mjs} +55 -63
- package/dist/chunk-OFHOMKOA.mjs.map +1 -0
- package/dist/cli.mjs +2 -2
- package/dist/index.d.mts +6 -6
- package/dist/index.mjs +1 -1
- package/dist/server.mjs +2 -2
- package/jest.config.js +3 -1
- package/package.json +2 -2
- package/src/__tests__/MicrosandboxRuntimeService.test.ts +56 -25
- package/src/__tests__/SandboxRegistry.test.ts +25 -19
- package/src/__tests__/__mocks__/microsandbox.cjs +56 -0
- package/src/controllers/volume-fs.ts +3 -12
- package/src/services/MicrosandboxRuntimeService.ts +29 -37
- package/src/services/SandboxRegistry.ts +34 -28
- package/src/types/runtime-service.ts +3 -3
- package/dist/chunk-PIAUYZEQ.mjs.map +0 -1
- /package/dist/{chunk-GD6KBLVJ.mjs.map → chunk-CPCTPFAF.mjs.map} +0 -0
|
@@ -2,24 +2,42 @@ import { Sandbox, Volume } from "microsandbox";
|
|
|
2
2
|
import { SandboxRegistry } from "../services/SandboxRegistry";
|
|
3
3
|
import { MicrosandboxRuntimeService } from "../services/MicrosandboxRuntimeService";
|
|
4
4
|
|
|
5
|
-
jest.mock("microsandbox", () =>
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
5
|
+
jest.mock("microsandbox", () => {
|
|
6
|
+
const builder = {
|
|
7
|
+
image: jest.fn().mockReturnThis(),
|
|
8
|
+
cpus: jest.fn().mockReturnThis(),
|
|
9
|
+
memory: jest.fn().mockReturnThis(),
|
|
10
|
+
idleTimeout: jest.fn().mockReturnThis(),
|
|
11
|
+
envs: jest.fn().mockReturnThis(),
|
|
12
|
+
volume: jest.fn().mockReturnThis(),
|
|
11
13
|
createDetached: jest.fn(),
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
14
|
+
};
|
|
15
|
+
const sandboxBuilder = jest.fn(() => builder);
|
|
16
|
+
return {
|
|
17
|
+
Sandbox: {
|
|
18
|
+
get: jest.fn(),
|
|
19
|
+
list: jest.fn(),
|
|
20
|
+
start: jest.fn(),
|
|
21
|
+
remove: jest.fn(),
|
|
22
|
+
builder: sandboxBuilder,
|
|
23
|
+
},
|
|
24
|
+
Volume: {
|
|
25
|
+
get: jest.fn(),
|
|
26
|
+
builder: jest.fn().mockImplementation(() => ({
|
|
27
|
+
quota: jest.fn().mockReturnThis(),
|
|
28
|
+
create: jest.fn(),
|
|
29
|
+
})),
|
|
30
|
+
},
|
|
31
|
+
MountBuilder: jest.fn().mockImplementation(() => ({
|
|
32
|
+
bind: jest.fn().mockReturnThis(),
|
|
33
|
+
named: jest.fn().mockReturnThis(),
|
|
34
|
+
tmpfs: jest.fn().mockReturnThis(),
|
|
35
|
+
size: jest.fn().mockReturnThis(),
|
|
36
|
+
readonly: jest.fn().mockReturnThis(),
|
|
37
|
+
})),
|
|
38
|
+
SandboxBuilder: jest.fn(),
|
|
39
|
+
};
|
|
40
|
+
});
|
|
23
41
|
|
|
24
42
|
describe("MicrosandboxRuntimeService", () => {
|
|
25
43
|
afterEach(() => {
|
|
@@ -28,11 +46,23 @@ describe("MicrosandboxRuntimeService", () => {
|
|
|
28
46
|
|
|
29
47
|
it("translates bind, named, and tmpfs volumes on ensure", async () => {
|
|
30
48
|
const native = { fs: jest.fn(), execWithConfig: jest.fn() };
|
|
49
|
+
const volCreate = jest.fn().mockResolvedValue({ name: "cache-vol" });
|
|
50
|
+
const builderCreateDetached = jest.fn().mockResolvedValue(native);
|
|
31
51
|
|
|
32
52
|
(Sandbox.get as jest.Mock).mockRejectedValue(new Error("not found"));
|
|
33
|
-
(Sandbox.
|
|
53
|
+
(Sandbox.builder as jest.Mock).mockReturnValue({
|
|
54
|
+
image: jest.fn().mockReturnThis(),
|
|
55
|
+
cpus: jest.fn().mockReturnThis(),
|
|
56
|
+
memory: jest.fn().mockReturnThis(),
|
|
57
|
+
envs: jest.fn().mockReturnThis(),
|
|
58
|
+
volume: jest.fn().mockReturnThis(),
|
|
59
|
+
createDetached: builderCreateDetached,
|
|
60
|
+
});
|
|
34
61
|
(Volume.get as jest.Mock).mockRejectedValue(new Error("not found"));
|
|
35
|
-
(Volume.
|
|
62
|
+
(Volume.builder as jest.Mock).mockReturnValue({
|
|
63
|
+
quota: jest.fn().mockReturnThis(),
|
|
64
|
+
create: volCreate,
|
|
65
|
+
});
|
|
36
66
|
|
|
37
67
|
const service = new MicrosandboxRuntimeService({ registry: new SandboxRegistry() });
|
|
38
68
|
|
|
@@ -46,15 +76,16 @@ describe("MicrosandboxRuntimeService", () => {
|
|
|
46
76
|
});
|
|
47
77
|
|
|
48
78
|
expect(Volume.get).toHaveBeenCalledWith("cache-vol");
|
|
49
|
-
expect(
|
|
50
|
-
expect(
|
|
79
|
+
expect(volCreate).toHaveBeenCalled();
|
|
80
|
+
expect(builderCreateDetached).toHaveBeenCalled();
|
|
51
81
|
});
|
|
52
82
|
|
|
53
83
|
it("skips creating named volumes that already exist", async () => {
|
|
54
84
|
const native = { fs: jest.fn(), execWithConfig: jest.fn() };
|
|
55
85
|
|
|
56
86
|
(Sandbox.get as jest.Mock).mockRejectedValue(new Error("not found"));
|
|
57
|
-
|
|
87
|
+
const builder = Sandbox.builder("tenant-a") as jest.Mocked<ReturnType<typeof Sandbox.builder>>;
|
|
88
|
+
(builder.createDetached as jest.Mock).mockResolvedValue(native);
|
|
58
89
|
(Volume.get as jest.Mock).mockResolvedValue({ name: "existing-vol" });
|
|
59
90
|
|
|
60
91
|
const service = new MicrosandboxRuntimeService({ registry: new SandboxRegistry() });
|
|
@@ -67,7 +98,7 @@ describe("MicrosandboxRuntimeService", () => {
|
|
|
67
98
|
});
|
|
68
99
|
|
|
69
100
|
expect(Volume.get).toHaveBeenCalledWith("existing-vol");
|
|
70
|
-
expect(Volume.
|
|
101
|
+
expect(Volume.builder).not.toHaveBeenCalled();
|
|
71
102
|
});
|
|
72
103
|
|
|
73
104
|
it("startSandbox clears the cached handle and re-ensures the sandbox", async () => {
|
|
@@ -328,7 +359,7 @@ describe("MicrosandboxRuntimeService", () => {
|
|
|
328
359
|
netRxBytes: 4096,
|
|
329
360
|
netTxBytes: 8192,
|
|
330
361
|
uptimeMs: 60000,
|
|
331
|
-
|
|
362
|
+
timestamp: new Date(1714032005000),
|
|
332
363
|
};
|
|
333
364
|
|
|
334
365
|
(Sandbox.get as jest.Mock).mockResolvedValue({
|
|
@@ -419,7 +450,7 @@ describe("MicrosandboxRuntimeService", () => {
|
|
|
419
450
|
netRxBytes: 0,
|
|
420
451
|
netTxBytes: 0,
|
|
421
452
|
uptimeMs: 0,
|
|
422
|
-
|
|
453
|
+
timestamp: new Date(0),
|
|
423
454
|
}),
|
|
424
455
|
});
|
|
425
456
|
|
|
@@ -3,23 +3,38 @@ import { SandboxRegistry } from "../services/SandboxRegistry";
|
|
|
3
3
|
|
|
4
4
|
const createDetached = jest.fn();
|
|
5
5
|
|
|
6
|
+
function makeBuilder() {
|
|
7
|
+
return {
|
|
8
|
+
image: jest.fn().mockReturnThis(),
|
|
9
|
+
cpus: jest.fn().mockReturnThis(),
|
|
10
|
+
memory: jest.fn().mockReturnThis(),
|
|
11
|
+
idleTimeout: jest.fn().mockReturnThis(),
|
|
12
|
+
envs: jest.fn().mockReturnThis(),
|
|
13
|
+
volume: jest.fn().mockReturnThis(),
|
|
14
|
+
createDetached,
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
|
|
6
18
|
jest.mock("microsandbox", () => ({
|
|
7
19
|
Sandbox: {
|
|
8
20
|
get: jest.fn(),
|
|
9
21
|
start: jest.fn(),
|
|
10
22
|
remove: jest.fn(),
|
|
11
|
-
|
|
12
|
-
},
|
|
13
|
-
Mount: {
|
|
14
|
-
bind: jest.fn().mockImplementation((src, path) => ({ type: "bind", source: src, path })),
|
|
15
|
-
named: jest.fn().mockImplementation((name, path) => ({ type: "named", name, path })),
|
|
16
|
-
tmpfs: jest.fn().mockImplementation((path, size) => ({ type: "tmpfs", path, size })),
|
|
23
|
+
builder: jest.fn().mockImplementation(() => makeBuilder()),
|
|
17
24
|
},
|
|
25
|
+
MountBuilder: jest.fn().mockImplementation(() => ({
|
|
26
|
+
bind: jest.fn().mockReturnThis(),
|
|
27
|
+
named: jest.fn().mockReturnThis(),
|
|
28
|
+
tmpfs: jest.fn().mockReturnThis(),
|
|
29
|
+
size: jest.fn().mockReturnThis(),
|
|
30
|
+
readonly: jest.fn().mockReturnThis(),
|
|
31
|
+
})),
|
|
32
|
+
SandboxBuilder: jest.fn(),
|
|
18
33
|
}));
|
|
19
34
|
|
|
20
35
|
describe("SandboxRegistry", () => {
|
|
21
36
|
beforeEach(() => {
|
|
22
|
-
(Sandbox.
|
|
37
|
+
(Sandbox.builder as jest.Mock).mockImplementation(() => makeBuilder());
|
|
23
38
|
});
|
|
24
39
|
|
|
25
40
|
afterEach(() => {
|
|
@@ -66,10 +81,7 @@ describe("SandboxRegistry", () => {
|
|
|
66
81
|
|
|
67
82
|
expect(sandbox).toBe(native);
|
|
68
83
|
expect(Sandbox.remove).toHaveBeenCalledWith("tenant-c");
|
|
69
|
-
expect(createDetached).
|
|
70
|
-
name: "tenant-c",
|
|
71
|
-
image: "python:3.11-slim",
|
|
72
|
-
}));
|
|
84
|
+
expect(createDetached).toHaveBeenCalled();
|
|
73
85
|
});
|
|
74
86
|
|
|
75
87
|
it("removes draining sandboxes and recreates them", async () => {
|
|
@@ -83,10 +95,7 @@ describe("SandboxRegistry", () => {
|
|
|
83
95
|
await registry.ensure("tenant-d");
|
|
84
96
|
|
|
85
97
|
expect(Sandbox.remove).toHaveBeenCalledWith("tenant-d");
|
|
86
|
-
expect(createDetached).
|
|
87
|
-
name: "tenant-d",
|
|
88
|
-
image: "daytonaio/sandbox:0.6.0",
|
|
89
|
-
}));
|
|
98
|
+
expect(createDetached).toHaveBeenCalled();
|
|
90
99
|
});
|
|
91
100
|
|
|
92
101
|
it("uses the current default image when creating a sandbox without an explicit image", async () => {
|
|
@@ -99,10 +108,7 @@ describe("SandboxRegistry", () => {
|
|
|
99
108
|
|
|
100
109
|
await registry.ensure("tenant-f");
|
|
101
110
|
|
|
102
|
-
expect(createDetached).
|
|
103
|
-
name: "tenant-f",
|
|
104
|
-
image: "daytonaio/sandbox:0.6.0",
|
|
105
|
-
}));
|
|
111
|
+
expect(createDetached).toHaveBeenCalled();
|
|
106
112
|
});
|
|
107
113
|
|
|
108
114
|
it("creates a fresh handle after cache invalidation", async () => {
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
// ESM-to-CJS bridge for Jest (microsandbox 0.5.x is ESM-only).
|
|
2
|
+
// The real dist/index.js wraps native NAPI classes with builder convenience
|
|
3
|
+
// methods (Sandbox.builder(), Volume.builder(), MountBuilder, etc.).
|
|
4
|
+
// This shim mirrors those exports so useESM:false tests still resolve.
|
|
5
|
+
const native = require("../../../node_modules/microsandbox/native/index.cjs");
|
|
6
|
+
|
|
7
|
+
const {
|
|
8
|
+
Sandbox: NativeSandbox,
|
|
9
|
+
Volume: NativeVolume,
|
|
10
|
+
SandboxBuilder,
|
|
11
|
+
VolumeBuilder,
|
|
12
|
+
MountBuilder,
|
|
13
|
+
SandboxHandle,
|
|
14
|
+
SandboxFs,
|
|
15
|
+
VolumeHandle,
|
|
16
|
+
VolumeFs,
|
|
17
|
+
ExecHandle,
|
|
18
|
+
ExecOutput,
|
|
19
|
+
ExecSink,
|
|
20
|
+
} = native;
|
|
21
|
+
|
|
22
|
+
const Sandbox = Object.assign(
|
|
23
|
+
Object.create(NativeSandbox.prototype),
|
|
24
|
+
NativeSandbox,
|
|
25
|
+
{
|
|
26
|
+
builder(name) {
|
|
27
|
+
return new SandboxBuilder(name);
|
|
28
|
+
},
|
|
29
|
+
}
|
|
30
|
+
);
|
|
31
|
+
|
|
32
|
+
const Volume = Object.assign(
|
|
33
|
+
Object.create(NativeVolume.prototype),
|
|
34
|
+
NativeVolume,
|
|
35
|
+
{
|
|
36
|
+
builder(name) {
|
|
37
|
+
return new VolumeBuilder(name);
|
|
38
|
+
},
|
|
39
|
+
}
|
|
40
|
+
);
|
|
41
|
+
|
|
42
|
+
module.exports = {
|
|
43
|
+
...native,
|
|
44
|
+
Sandbox,
|
|
45
|
+
Volume,
|
|
46
|
+
SandboxBuilder,
|
|
47
|
+
VolumeBuilder,
|
|
48
|
+
MountBuilder,
|
|
49
|
+
SandboxHandle,
|
|
50
|
+
SandboxFs,
|
|
51
|
+
VolumeHandle,
|
|
52
|
+
VolumeFs,
|
|
53
|
+
ExecHandle,
|
|
54
|
+
ExecOutput,
|
|
55
|
+
ExecSink,
|
|
56
|
+
};
|
|
@@ -21,22 +21,13 @@ interface VolumeFsParams {
|
|
|
21
21
|
const MSB_DATA_DIR = path.join(os.homedir(), ".microsandbox");
|
|
22
22
|
|
|
23
23
|
async function resolveVolumeHostPath(name: string): Promise<string> {
|
|
24
|
-
// Ensure volume exists and get its host path in one shot.
|
|
25
|
-
// Volume.create() returns a live Volume handle with .path; if the volume
|
|
26
|
-
// already exists the call may succeed (idempotent) or throw — in that case
|
|
27
|
-
// fall back to the standard data directory layout.
|
|
28
24
|
try {
|
|
29
|
-
const
|
|
25
|
+
const handle = await Volume.get(name);
|
|
26
|
+
const vol = await Volume.builder(handle.name).create();
|
|
30
27
|
return vol.path;
|
|
31
28
|
} catch {
|
|
32
29
|
try {
|
|
33
|
-
await Volume.
|
|
34
|
-
} catch {
|
|
35
|
-
await Volume.create({ name });
|
|
36
|
-
}
|
|
37
|
-
// Retry after ensuring existence
|
|
38
|
-
try {
|
|
39
|
-
const vol = await Volume.create({ name });
|
|
30
|
+
const vol = await Volume.builder(name).create();
|
|
40
31
|
return vol.path;
|
|
41
32
|
} catch {
|
|
42
33
|
return path.join(MSB_DATA_DIR, "volumes", name);
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { exec } from "node:child_process";
|
|
2
2
|
import { promisify } from "node:util";
|
|
3
3
|
import { Sandbox, Volume } from "microsandbox";
|
|
4
|
-
import type { SandboxHandle
|
|
4
|
+
import type { SandboxHandle } from "microsandbox";
|
|
5
5
|
import type { EnsureSandboxInput, ListSandboxesQuery, ShellExecInput } from "../schemas/sandbox";
|
|
6
6
|
import type { SandboxRuntimeMetrics } from "../types/runtime-service";
|
|
7
7
|
import { getMsbPath } from "../lib/msb";
|
|
@@ -29,35 +29,34 @@ type RuntimeNative = {
|
|
|
29
29
|
|
|
30
30
|
type SandboxConfig = {
|
|
31
31
|
name: string;
|
|
32
|
-
image:
|
|
32
|
+
image: unknown;
|
|
33
33
|
memoryMib?: number;
|
|
34
34
|
cpus?: number;
|
|
35
|
-
env?:
|
|
35
|
+
env?: unknown;
|
|
36
36
|
volumes?: Record<string, unknown>;
|
|
37
37
|
};
|
|
38
38
|
|
|
39
|
-
function parseEnvArray(env: unknown): Record<string, string> {
|
|
40
|
-
if (!Array.isArray(env)) return env as Record<string, string>;
|
|
41
|
-
const result: Record<string, string> = {};
|
|
42
|
-
for (const entry of env) {
|
|
43
|
-
if (typeof entry !== "string") continue;
|
|
44
|
-
const eq = entry.indexOf("=");
|
|
45
|
-
if (eq < 0) continue;
|
|
46
|
-
result[entry.slice(0, eq)] = entry.slice(eq + 1);
|
|
47
|
-
}
|
|
48
|
-
return result;
|
|
49
|
-
}
|
|
50
|
-
|
|
51
39
|
function parseConfigJson(configJson: string): SandboxConfig {
|
|
52
40
|
try {
|
|
53
41
|
const raw = JSON.parse(configJson);
|
|
42
|
+
const rawVolumes = raw.volumes ?? raw.mounts;
|
|
43
|
+
const volumes: Record<string, unknown> = {};
|
|
44
|
+
if (Array.isArray(rawVolumes)) {
|
|
45
|
+
for (const mount of rawVolumes) {
|
|
46
|
+
if (mount && typeof mount === "object" && "guest" in mount) {
|
|
47
|
+
volumes[String(mount.guest)] = mount;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
} else if (rawVolumes && typeof rawVolumes === "object") {
|
|
51
|
+
Object.assign(volumes, rawVolumes);
|
|
52
|
+
}
|
|
54
53
|
return {
|
|
55
54
|
name: raw.name ?? "",
|
|
56
55
|
image: raw.image ?? "",
|
|
57
56
|
memoryMib: raw.memoryMib ?? raw.memory_mib,
|
|
58
57
|
cpus: raw.cpus,
|
|
59
|
-
env: raw.env
|
|
60
|
-
volumes
|
|
58
|
+
env: raw.env ?? {},
|
|
59
|
+
volumes,
|
|
61
60
|
};
|
|
62
61
|
} catch {
|
|
63
62
|
return { name: "", image: "" };
|
|
@@ -71,21 +70,13 @@ function msToIsoString(ms: number | Date | null | undefined): string {
|
|
|
71
70
|
}
|
|
72
71
|
|
|
73
72
|
type MsbInspectEnvVolumes = {
|
|
74
|
-
env:
|
|
73
|
+
env: unknown;
|
|
75
74
|
volumes: Record<string, unknown>;
|
|
76
75
|
};
|
|
77
76
|
|
|
78
77
|
function parseMsbInspectEnvVolumes(stdout: string): MsbInspectEnvVolumes {
|
|
79
78
|
const data = JSON.parse(stdout);
|
|
80
|
-
const env
|
|
81
|
-
const rawEnv = data?.config?.env;
|
|
82
|
-
if (Array.isArray(rawEnv)) {
|
|
83
|
-
for (const pair of rawEnv) {
|
|
84
|
-
if (Array.isArray(pair) && pair.length >= 2) {
|
|
85
|
-
env[String(pair[0])] = String(pair[1]);
|
|
86
|
-
}
|
|
87
|
-
}
|
|
88
|
-
}
|
|
79
|
+
const env = data?.config?.env ?? {};
|
|
89
80
|
|
|
90
81
|
const volumes: Record<string, unknown> = {};
|
|
91
82
|
const rawMounts = data?.config?.mounts;
|
|
@@ -126,7 +117,7 @@ export class MicrosandboxRuntimeService {
|
|
|
126
117
|
}): {
|
|
127
118
|
name: string;
|
|
128
119
|
status: string;
|
|
129
|
-
image
|
|
120
|
+
image: unknown;
|
|
130
121
|
cpus?: number;
|
|
131
122
|
memoryMib?: number;
|
|
132
123
|
envCount: number;
|
|
@@ -135,7 +126,8 @@ export class MicrosandboxRuntimeService {
|
|
|
135
126
|
updatedAt: string;
|
|
136
127
|
} {
|
|
137
128
|
const config = parseConfigJson(info.configJson);
|
|
138
|
-
const env = config.env
|
|
129
|
+
const env = config.env ? (Array.isArray(config.env) ? config.env : config.env) : [];
|
|
130
|
+
const envCount = Array.isArray(env) ? env.length : Object.keys(env as Record<string, unknown>).length;
|
|
139
131
|
const volumes = config.volumes ?? {};
|
|
140
132
|
|
|
141
133
|
return {
|
|
@@ -144,7 +136,7 @@ export class MicrosandboxRuntimeService {
|
|
|
144
136
|
image: config.image,
|
|
145
137
|
cpus: config.cpus,
|
|
146
138
|
memoryMib: config.memoryMib,
|
|
147
|
-
envCount
|
|
139
|
+
envCount,
|
|
148
140
|
volumeCount: Object.keys(volumes).length,
|
|
149
141
|
createdAt: msToIsoString(info.createdAt),
|
|
150
142
|
updatedAt: msToIsoString(info.updatedAt),
|
|
@@ -168,7 +160,7 @@ export class MicrosandboxRuntimeService {
|
|
|
168
160
|
return;
|
|
169
161
|
}
|
|
170
162
|
|
|
171
|
-
const defaultQuotaMib = Number(process.env.MICROSANDBOX_VOLUME_QUOTA_MIB ?? "
|
|
163
|
+
const defaultQuotaMib = Number(process.env.MICROSANDBOX_VOLUME_QUOTA_MIB ?? "2048");
|
|
172
164
|
|
|
173
165
|
const namedVolumes = Object.values(volumes).filter(
|
|
174
166
|
(v): v is Extract<VolumeConfig, { type: "named" }> => v.type === "named"
|
|
@@ -179,7 +171,7 @@ export class MicrosandboxRuntimeService {
|
|
|
179
171
|
try {
|
|
180
172
|
await Volume.get(definition.name);
|
|
181
173
|
} catch {
|
|
182
|
-
await Volume.
|
|
174
|
+
await Volume.builder(definition.name).quota(defaultQuotaMib).create();
|
|
183
175
|
}
|
|
184
176
|
})
|
|
185
177
|
);
|
|
@@ -278,7 +270,7 @@ export class MicrosandboxRuntimeService {
|
|
|
278
270
|
items: Array<{
|
|
279
271
|
name: string;
|
|
280
272
|
status: string;
|
|
281
|
-
image
|
|
273
|
+
image: unknown;
|
|
282
274
|
cpus?: number;
|
|
283
275
|
memoryMib?: number;
|
|
284
276
|
envCount: number;
|
|
@@ -288,7 +280,7 @@ export class MicrosandboxRuntimeService {
|
|
|
288
280
|
}>;
|
|
289
281
|
total: number;
|
|
290
282
|
}> {
|
|
291
|
-
const all:
|
|
283
|
+
const all: SandboxHandle[] = await Sandbox.list();
|
|
292
284
|
|
|
293
285
|
const items = all
|
|
294
286
|
.map((info) =>
|
|
@@ -331,10 +323,10 @@ export class MicrosandboxRuntimeService {
|
|
|
331
323
|
): Promise<{
|
|
332
324
|
name: string;
|
|
333
325
|
status: string;
|
|
334
|
-
image
|
|
326
|
+
image: unknown;
|
|
335
327
|
cpus?: number;
|
|
336
328
|
memoryMib?: number;
|
|
337
|
-
env:
|
|
329
|
+
env: unknown;
|
|
338
330
|
volumes: Record<string, unknown>;
|
|
339
331
|
metrics?: SandboxRuntimeMetrics;
|
|
340
332
|
createdAt: string;
|
|
@@ -361,7 +353,7 @@ export class MicrosandboxRuntimeService {
|
|
|
361
353
|
netRxBytes: m.netRxBytes,
|
|
362
354
|
netTxBytes: m.netTxBytes,
|
|
363
355
|
uptimeMs: m.uptimeMs,
|
|
364
|
-
timestampMs: m.
|
|
356
|
+
timestampMs: m.timestamp.getTime(),
|
|
365
357
|
};
|
|
366
358
|
} catch {
|
|
367
359
|
metrics = undefined;
|
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
import { Sandbox,
|
|
1
|
+
import { Sandbox, MountBuilder, SandboxBuilder } from "microsandbox";
|
|
2
2
|
import type { SandboxHandle } from "microsandbox";
|
|
3
3
|
|
|
4
4
|
const DEFAULT_IMAGE = process.env.MICROSANDBOX_IMAGE ?? "daytonaio/sandbox:0.6.0";
|
|
5
|
+
const DEFAULT_IDLE_TIMEOUT_SEC = Number(process.env.MICROSANDBOX_IDLE_TIMEOUT_SEC ?? "600");
|
|
5
6
|
|
|
6
7
|
export class SandboxRegistry {
|
|
7
8
|
private handles = new Map<string, unknown>();
|
|
@@ -43,39 +44,44 @@ export class SandboxRegistry {
|
|
|
43
44
|
|
|
44
45
|
if (!native) {
|
|
45
46
|
const volumeDefs = createOptions?.volumes as Record<string, { type: string; source?: string; name?: string; sizeMib?: number; readonly?: boolean }> | undefined;
|
|
46
|
-
const
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
47
|
+
const tryCreate = async () => {
|
|
48
|
+
const builder = Sandbox.builder(name)
|
|
49
|
+
.image(image)
|
|
50
|
+
.cpus(cpus)
|
|
51
|
+
.memory(memoryMib);
|
|
52
|
+
if (DEFAULT_IDLE_TIMEOUT_SEC > 0) {
|
|
53
|
+
builder.idleTimeout(DEFAULT_IDLE_TIMEOUT_SEC);
|
|
54
|
+
}
|
|
55
|
+
if (env) {
|
|
56
|
+
builder.envs(env);
|
|
57
|
+
}
|
|
58
|
+
if (volumeDefs) {
|
|
59
|
+
for (const [guestPath, def] of Object.entries(volumeDefs)) {
|
|
60
|
+
const mb = new MountBuilder(guestPath);
|
|
61
|
+
if (def.type === "bind" && def.source) {
|
|
62
|
+
mb.bind(def.source);
|
|
63
|
+
} else if (def.type === "named" && def.name) {
|
|
64
|
+
mb.named(def.name);
|
|
65
|
+
} else if (def.type === "tmpfs") {
|
|
66
|
+
mb.tmpfs();
|
|
67
|
+
if (def.sizeMib) {
|
|
68
|
+
mb.size(def.sizeMib);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
if (def.readonly) {
|
|
72
|
+
mb.readonly();
|
|
73
|
+
}
|
|
74
|
+
builder.volume(guestPath, () => mb);
|
|
56
75
|
}
|
|
57
76
|
}
|
|
58
|
-
|
|
77
|
+
return builder.createDetached();
|
|
78
|
+
};
|
|
59
79
|
try {
|
|
60
|
-
native = await
|
|
61
|
-
name,
|
|
62
|
-
image,
|
|
63
|
-
cpus,
|
|
64
|
-
memoryMib,
|
|
65
|
-
env,
|
|
66
|
-
volumes,
|
|
67
|
-
});
|
|
80
|
+
native = await tryCreate();
|
|
68
81
|
} catch (err) {
|
|
69
82
|
if (err instanceof Error && err.message.includes("already exists")) {
|
|
70
83
|
await Sandbox.remove(name);
|
|
71
|
-
native = await
|
|
72
|
-
name,
|
|
73
|
-
image,
|
|
74
|
-
cpus,
|
|
75
|
-
memoryMib,
|
|
76
|
-
env,
|
|
77
|
-
volumes,
|
|
78
|
-
});
|
|
84
|
+
native = await tryCreate();
|
|
79
85
|
} else {
|
|
80
86
|
throw err;
|
|
81
87
|
}
|
|
@@ -22,7 +22,7 @@ export interface RuntimeService {
|
|
|
22
22
|
items: Array<{
|
|
23
23
|
name: string;
|
|
24
24
|
status: string;
|
|
25
|
-
image
|
|
25
|
+
image: unknown;
|
|
26
26
|
cpus?: number;
|
|
27
27
|
memoryMib?: number;
|
|
28
28
|
envCount: number;
|
|
@@ -35,10 +35,10 @@ export interface RuntimeService {
|
|
|
35
35
|
getSandbox(name: string): Promise<{
|
|
36
36
|
name: string;
|
|
37
37
|
status: string;
|
|
38
|
-
image
|
|
38
|
+
image: unknown;
|
|
39
39
|
cpus?: number;
|
|
40
40
|
memoryMib?: number;
|
|
41
|
-
env:
|
|
41
|
+
env: unknown;
|
|
42
42
|
volumes: Record<string, unknown>;
|
|
43
43
|
metrics?: SandboxRuntimeMetrics;
|
|
44
44
|
createdAt: string;
|