pinokiod 7.3.0 → 7.3.3
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/kernel/api/github/index.js +444 -0
- package/kernel/api/index.js +199 -11
- package/kernel/api/process/index.js +124 -44
- package/kernel/api/shell_run_template.js +273 -0
- package/kernel/api/uri/index.js +51 -0
- package/kernel/bin/{conda-python.js → conda-pins.js} +23 -0
- package/kernel/bin/conda.js +15 -5
- package/kernel/bin/git.js +9 -10
- package/kernel/bin/huggingface.js +1 -1
- package/kernel/bin/index.js +5 -2
- package/kernel/bin/zip.js +9 -1
- package/kernel/connect/providers/github/README.md +5 -4
- package/kernel/environment.js +195 -92
- package/kernel/git.js +98 -19
- package/kernel/gitconfig_template +7 -0
- package/kernel/gpu/amd.js +72 -0
- package/kernel/gpu/apple.js +8 -0
- package/kernel/gpu/common.js +12 -0
- package/kernel/gpu/intel.js +47 -0
- package/kernel/gpu/nvidia.js +8 -0
- package/kernel/index.js +11 -1
- package/kernel/managed_skills.js +871 -0
- package/kernel/plugin.js +6 -58
- package/kernel/plugin_sources.js +316 -0
- package/kernel/resource_usage/gpu.js +349 -0
- package/kernel/resource_usage/index.js +322 -0
- package/kernel/resource_usage/macos_footprint.js +197 -0
- package/kernel/resource_usage/preferences.js +92 -0
- package/kernel/resource_usage/process_tree.js +303 -0
- package/kernel/scripts/git/create +4 -4
- package/kernel/scripts/git/fork +7 -8
- package/kernel/shell.js +23 -2
- package/kernel/shells.js +41 -0
- package/kernel/sysinfo.js +62 -9
- package/kernel/util.js +60 -0
- package/package.json +1 -1
- package/server/index.js +984 -156
- package/server/lib/app_log_report.js +543 -0
- package/server/lib/content_validation.js +55 -33
- package/server/lib/launcher_instruction_bootstrap.js +4 -96
- package/server/lib/terminal_session_helpers.js +0 -3
- package/server/public/common.js +77 -31
- package/server/public/create-launcher.js +4 -32
- package/server/public/logs.js +1428 -0
- package/server/public/nav.js +7 -0
- package/server/public/plugin-detail.js +93 -10
- package/server/public/privacy_filter_worker.js +391 -0
- package/server/public/style.css +1104 -154
- package/server/public/task-launcher.js +8 -29
- package/server/public/universal-launcher.css +8 -6
- package/server/public/universal-launcher.js +3 -27
- package/server/routes/apps.js +195 -1
- package/server/views/app.ejs +3041 -717
- package/server/views/autolaunch.ejs +917 -0
- package/server/views/bootstrap.ejs +7 -1
- package/server/views/d.ejs +408 -65
- package/server/views/editor.ejs +85 -19
- package/server/views/index.ejs +661 -111
- package/server/views/init/index.ejs +1 -1
- package/server/views/install.ejs +1 -1
- package/server/views/logs.ejs +164 -86
- package/server/views/net.ejs +7 -1
- package/server/views/partials/d_terminal_column.ejs +2 -2
- package/server/views/partials/d_terminal_options.ejs +0 -8
- package/server/views/partials/fs_status.ejs +47 -0
- package/server/views/partials/home_action_modal.ejs +86 -0
- package/server/views/partials/home_run_menu.ejs +87 -0
- package/server/views/partials/main_sidebar.ejs +2 -0
- package/server/views/partials/menu.ejs +1 -1
- package/server/views/plugin_detail.ejs +19 -4
- package/server/views/plugins.ejs +201 -3
- package/server/views/pre.ejs +1 -1
- package/server/views/pro.ejs +1 -1
- package/server/views/shell.ejs +40 -18
- package/server/views/skills.ejs +506 -0
- package/server/views/terminal.ejs +45 -19
- package/spec/INSTRUCTION_SYNC.md +20 -10
- package/system/plugin/antigravity-cli/antigravity.png +0 -0
- package/system/plugin/antigravity-cli/common.js +155 -0
- package/system/plugin/antigravity-cli/install.js +272 -0
- package/system/plugin/antigravity-cli/pinokio.js +13 -0
- package/system/plugin/antigravity-cli-auto/antigravity.png +0 -0
- package/system/plugin/antigravity-cli-auto/pinokio.js +13 -0
- package/system/plugin/claude/claude.png +0 -0
- package/system/plugin/claude/pinokio.js +47 -0
- package/system/plugin/claude-auto/claude.png +0 -0
- package/system/plugin/claude-auto/pinokio.js +58 -0
- package/system/plugin/claude-desktop/icon.jpeg +0 -0
- package/system/plugin/claude-desktop/pinokio.js +23 -0
- package/system/plugin/codex/openai.webp +0 -0
- package/system/plugin/codex/pinokio.js +42 -0
- package/system/plugin/codex-auto/openai.webp +0 -0
- package/system/plugin/codex-auto/pinokio.js +49 -0
- package/system/plugin/codex-desktop/icon.png +0 -0
- package/system/plugin/codex-desktop/pinokio.js +23 -0
- package/system/plugin/crush/crush.png +0 -0
- package/system/plugin/crush/pinokio.js +15 -0
- package/system/plugin/cursor/cursor.jpeg +0 -0
- package/system/plugin/cursor/pinokio.js +23 -0
- package/system/plugin/qwen/pinokio.js +34 -0
- package/system/plugin/qwen/qwen.png +0 -0
- package/system/plugin/vscode/pinokio.js +20 -0
- package/system/plugin/vscode/vscode.png +0 -0
- package/system/plugin/windsurf/pinokio.js +23 -0
- package/system/plugin/windsurf/windsurf.png +0 -0
- package/test/antigravity-cli-plugin.test.js +185 -0
- package/test/app-api.test.js +239 -0
- package/test/app-log-report.test.js +67 -0
- package/test/environment-cache-preflight.test.js +98 -0
- package/test/git-bin.test.js +59 -0
- package/test/git-defaults.test.js +97 -0
- package/test/github-api.test.js +158 -0
- package/test/github-connection.test.js +117 -0
- package/test/huggingface-bin.test.js +25 -0
- package/test/managed-skills.test.js +351 -0
- package/test/plugin-action-functions.test.js +337 -0
- package/test/plugin-dev-iframe.test.js +17 -0
- package/test/plugin-sources.test.js +203 -0
- package/test/privacy-filter-worker-heuristics.test.js +69 -0
- package/test/process-wait.test.js +169 -0
- package/test/script-api.test.js +97 -0
- package/test/shell-api.test.js +134 -0
- package/test/shell-run-template.test.js +209 -0
- package/test/storage-api.test.js +137 -0
- package/test/uri-api.test.js +100 -0
|
@@ -0,0 +1,349 @@
|
|
|
1
|
+
"use strict"
|
|
2
|
+
|
|
3
|
+
const fs = require("fs")
|
|
4
|
+
const os = require("os")
|
|
5
|
+
const path = require("path")
|
|
6
|
+
const { execFileText, normalizePid } = require("./process_tree")
|
|
7
|
+
|
|
8
|
+
const DEFAULT_GPU_TTL_MS = 10000
|
|
9
|
+
const DEFAULT_GPU_TIMEOUT_MS = 2500
|
|
10
|
+
const MIB = 1024 * 1024
|
|
11
|
+
|
|
12
|
+
function unique(values) {
|
|
13
|
+
const seen = new Set()
|
|
14
|
+
const next = []
|
|
15
|
+
for (const value of values) {
|
|
16
|
+
if (!value || seen.has(value)) continue
|
|
17
|
+
seen.add(value)
|
|
18
|
+
next.push(value)
|
|
19
|
+
}
|
|
20
|
+
return next
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function pathExists(filepath) {
|
|
24
|
+
try {
|
|
25
|
+
fs.accessSync(filepath, fs.constants.X_OK)
|
|
26
|
+
return true
|
|
27
|
+
} catch (_) {
|
|
28
|
+
return false
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function executableCandidates(candidates) {
|
|
33
|
+
return unique(candidates).filter((candidate) => {
|
|
34
|
+
if (!candidate) return false
|
|
35
|
+
if (path.isAbsolute(candidate)) {
|
|
36
|
+
return pathExists(candidate)
|
|
37
|
+
}
|
|
38
|
+
return true
|
|
39
|
+
})
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function getPinokioCondaCandidates(kernel, names) {
|
|
43
|
+
if (!kernel || !kernel.homedir) {
|
|
44
|
+
return []
|
|
45
|
+
}
|
|
46
|
+
const prefix = path.resolve(kernel.homedir, "bin", "miniconda")
|
|
47
|
+
const suffixes = os.platform() === "win32"
|
|
48
|
+
? ["", ".exe"]
|
|
49
|
+
: [""]
|
|
50
|
+
const folders = os.platform() === "win32"
|
|
51
|
+
? ["Library/bin", "Scripts", ""]
|
|
52
|
+
: ["bin", "Library/bin", ""]
|
|
53
|
+
const candidates = []
|
|
54
|
+
for (const name of names) {
|
|
55
|
+
for (const folder of folders) {
|
|
56
|
+
for (const suffix of suffixes) {
|
|
57
|
+
candidates.push(path.resolve(prefix, folder, `${name}${suffix}`))
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
return candidates
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function parseMemoryToBytes(value, defaultUnit = "") {
|
|
65
|
+
if (value == null) return null
|
|
66
|
+
if (typeof value === "number") {
|
|
67
|
+
if (!Number.isFinite(value) || value < 0) return null
|
|
68
|
+
if (defaultUnit === "mib") return Math.round(value * MIB)
|
|
69
|
+
if (defaultUnit === "kb") return Math.round(value * 1024)
|
|
70
|
+
return Math.round(value)
|
|
71
|
+
}
|
|
72
|
+
const raw = String(value).trim()
|
|
73
|
+
if (!raw || /N\/A|not supported|none/i.test(raw)) {
|
|
74
|
+
return null
|
|
75
|
+
}
|
|
76
|
+
const match = /(-?\d+(?:\.\d+)?)\s*([KMGT]?i?B|[KMGT]B|bytes?)?/i.exec(raw)
|
|
77
|
+
if (!match) return null
|
|
78
|
+
const amount = Number.parseFloat(match[1])
|
|
79
|
+
if (!Number.isFinite(amount) || amount < 0) return null
|
|
80
|
+
const unit = (match[2] || defaultUnit || "bytes").toLowerCase()
|
|
81
|
+
if (unit === "mib" || unit === "mb") return Math.round(amount * MIB)
|
|
82
|
+
if (unit === "gib" || unit === "gb") return Math.round(amount * 1024 * MIB)
|
|
83
|
+
if (unit === "kib" || unit === "kb") return Math.round(amount * 1024)
|
|
84
|
+
if (unit === "tib" || unit === "tb") return Math.round(amount * 1024 * 1024 * MIB)
|
|
85
|
+
return Math.round(amount)
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function addGpuProcess(processes, pid, bytes) {
|
|
89
|
+
const normalizedPid = normalizePid(pid)
|
|
90
|
+
if (!normalizedPid || !Number.isFinite(bytes) || bytes < 0) {
|
|
91
|
+
return
|
|
92
|
+
}
|
|
93
|
+
const current = processes.get(normalizedPid) || {
|
|
94
|
+
pid: normalizedPid,
|
|
95
|
+
usedGpuMemoryBytes: 0
|
|
96
|
+
}
|
|
97
|
+
current.usedGpuMemoryBytes += bytes
|
|
98
|
+
processes.set(normalizedPid, current)
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function parseNvidiaCsv(stdout) {
|
|
102
|
+
const processes = new Map()
|
|
103
|
+
for (const line of String(stdout || "").split(/\r?\n/)) {
|
|
104
|
+
const trimmed = line.trim()
|
|
105
|
+
if (!trimmed) continue
|
|
106
|
+
const parts = trimmed.split(",").map((part) => part.trim())
|
|
107
|
+
const pid = normalizePid(parts[0])
|
|
108
|
+
const bytes = parseMemoryToBytes(parts[1], "mib")
|
|
109
|
+
addGpuProcess(processes, pid, bytes)
|
|
110
|
+
}
|
|
111
|
+
return processes
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function findObjectValue(object, predicate) {
|
|
115
|
+
if (!object || typeof object !== "object" || Array.isArray(object)) {
|
|
116
|
+
return null
|
|
117
|
+
}
|
|
118
|
+
for (const [key, value] of Object.entries(object)) {
|
|
119
|
+
if (predicate(key, value)) {
|
|
120
|
+
return value
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
return null
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function extractAmdProcessesFromJson(value, processes = new Map()) {
|
|
127
|
+
if (Array.isArray(value)) {
|
|
128
|
+
for (const item of value) {
|
|
129
|
+
extractAmdProcessesFromJson(item, processes)
|
|
130
|
+
}
|
|
131
|
+
return processes
|
|
132
|
+
}
|
|
133
|
+
if (!value || typeof value !== "object") {
|
|
134
|
+
return processes
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const pidValue = findObjectValue(value, (key) => /(^|[_\s-])pid$|process[_\s-]*id/i.test(key))
|
|
138
|
+
const memoryValue = findObjectValue(value, (key) => {
|
|
139
|
+
const normalized = key.toLowerCase()
|
|
140
|
+
if (/total|free|available|limit/.test(normalized)) return false
|
|
141
|
+
return /vram|memory/.test(normalized) && /usage|used|mem|size/.test(normalized)
|
|
142
|
+
})
|
|
143
|
+
const pid = normalizePid(pidValue)
|
|
144
|
+
const bytes = parseMemoryToBytes(memoryValue)
|
|
145
|
+
addGpuProcess(processes, pid, bytes)
|
|
146
|
+
|
|
147
|
+
for (const child of Object.values(value)) {
|
|
148
|
+
if (child && typeof child === "object") {
|
|
149
|
+
extractAmdProcessesFromJson(child, processes)
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
return processes
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function parseAmdJson(stdout) {
|
|
156
|
+
const parsed = JSON.parse(stdout || "[]")
|
|
157
|
+
return extractAmdProcessesFromJson(parsed)
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
class GpuSampler {
|
|
161
|
+
constructor(options = {}) {
|
|
162
|
+
this.kernel = options.kernel || null
|
|
163
|
+
this.ttlMs = options.ttlMs || DEFAULT_GPU_TTL_MS
|
|
164
|
+
this.timeoutMs = options.timeoutMs || DEFAULT_GPU_TIMEOUT_MS
|
|
165
|
+
this.current = null
|
|
166
|
+
this.inFlight = null
|
|
167
|
+
this.providerBackoff = new Map()
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
nvidiaCandidates() {
|
|
171
|
+
const platform = os.platform()
|
|
172
|
+
const candidates = [
|
|
173
|
+
process.env.NVIDIA_SMI,
|
|
174
|
+
"nvidia-smi",
|
|
175
|
+
...getPinokioCondaCandidates(this.kernel, ["nvidia-smi"])
|
|
176
|
+
]
|
|
177
|
+
if (platform === "win32") {
|
|
178
|
+
candidates.push(
|
|
179
|
+
"C:\\Program Files\\NVIDIA Corporation\\NVSMI\\nvidia-smi.exe",
|
|
180
|
+
"C:\\Windows\\System32\\nvidia-smi.exe"
|
|
181
|
+
)
|
|
182
|
+
} else if (platform === "linux") {
|
|
183
|
+
candidates.push(
|
|
184
|
+
"/usr/bin/nvidia-smi",
|
|
185
|
+
"/usr/local/bin/nvidia-smi",
|
|
186
|
+
"/usr/local/nvidia/bin/nvidia-smi",
|
|
187
|
+
"/usr/local/cuda/bin/nvidia-smi"
|
|
188
|
+
)
|
|
189
|
+
}
|
|
190
|
+
return executableCandidates(candidates)
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
amdCandidates() {
|
|
194
|
+
const candidates = [
|
|
195
|
+
process.env.AMD_SMI,
|
|
196
|
+
"amd-smi",
|
|
197
|
+
...getPinokioCondaCandidates(this.kernel, ["amd-smi"])
|
|
198
|
+
]
|
|
199
|
+
if (os.platform() === "linux") {
|
|
200
|
+
candidates.push("/opt/rocm/bin/amd-smi", "/usr/bin/amd-smi", "/usr/local/bin/amd-smi")
|
|
201
|
+
}
|
|
202
|
+
return executableCandidates(candidates)
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
isBackedOff(provider) {
|
|
206
|
+
const until = this.providerBackoff.get(provider) || 0
|
|
207
|
+
return Date.now() < until
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
backoff(provider, ms = 60000) {
|
|
211
|
+
this.providerBackoff.set(provider, Date.now() + ms)
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
async collectNvidia() {
|
|
215
|
+
if (this.isBackedOff("nvidia")) {
|
|
216
|
+
return null
|
|
217
|
+
}
|
|
218
|
+
const args = [
|
|
219
|
+
"--query-compute-apps=pid,used_gpu_memory",
|
|
220
|
+
"--format=csv,noheader,nounits"
|
|
221
|
+
]
|
|
222
|
+
let lastError = null
|
|
223
|
+
for (const command of this.nvidiaCandidates()) {
|
|
224
|
+
try {
|
|
225
|
+
const { stdout } = await execFileText(command, args, { timeoutMs: this.timeoutMs })
|
|
226
|
+
return {
|
|
227
|
+
provider: "nvidia-smi",
|
|
228
|
+
processes: parseNvidiaCsv(stdout),
|
|
229
|
+
error: null
|
|
230
|
+
}
|
|
231
|
+
} catch (error) {
|
|
232
|
+
lastError = error
|
|
233
|
+
if (error && error.code === "ENOENT") {
|
|
234
|
+
continue
|
|
235
|
+
}
|
|
236
|
+
break
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
this.backoff("nvidia", 60000)
|
|
240
|
+
return {
|
|
241
|
+
provider: "nvidia-smi",
|
|
242
|
+
processes: new Map(),
|
|
243
|
+
error: lastError && lastError.message ? lastError.message : "nvidia-smi unavailable"
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
async collectAmd() {
|
|
248
|
+
if (os.platform() !== "linux" || this.isBackedOff("amd")) {
|
|
249
|
+
return null
|
|
250
|
+
}
|
|
251
|
+
let lastError = null
|
|
252
|
+
for (const command of this.amdCandidates()) {
|
|
253
|
+
try {
|
|
254
|
+
const { stdout } = await execFileText(command, ["process", "--json", "-G"], { timeoutMs: this.timeoutMs })
|
|
255
|
+
return {
|
|
256
|
+
provider: "amd-smi",
|
|
257
|
+
processes: parseAmdJson(stdout),
|
|
258
|
+
error: null
|
|
259
|
+
}
|
|
260
|
+
} catch (error) {
|
|
261
|
+
lastError = error
|
|
262
|
+
if (error && error.code === "ENOENT") {
|
|
263
|
+
continue
|
|
264
|
+
}
|
|
265
|
+
break
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
this.backoff("amd", 90000)
|
|
269
|
+
return {
|
|
270
|
+
provider: "amd-smi",
|
|
271
|
+
processes: new Map(),
|
|
272
|
+
error: lastError && lastError.message ? lastError.message : "amd-smi unavailable"
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
async collect() {
|
|
277
|
+
const results = []
|
|
278
|
+
const nvidia = await this.collectNvidia()
|
|
279
|
+
if (nvidia) results.push(nvidia)
|
|
280
|
+
const amd = await this.collectAmd()
|
|
281
|
+
if (amd) results.push(amd)
|
|
282
|
+
|
|
283
|
+
const processes = new Map()
|
|
284
|
+
const providers = []
|
|
285
|
+
const errors = []
|
|
286
|
+
for (const result of results) {
|
|
287
|
+
if (!result) continue
|
|
288
|
+
if (result.provider) providers.push(result.provider)
|
|
289
|
+
if (result.error) errors.push({ provider: result.provider, error: result.error })
|
|
290
|
+
for (const entry of result.processes.values()) {
|
|
291
|
+
addGpuProcess(processes, entry.pid, entry.usedGpuMemoryBytes)
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
return {
|
|
295
|
+
available: providers.length > 0 && errors.length < providers.length,
|
|
296
|
+
stale: false,
|
|
297
|
+
collectedAt: Date.now(),
|
|
298
|
+
providers,
|
|
299
|
+
processes,
|
|
300
|
+
errors
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
async getSnapshot() {
|
|
305
|
+
const now = Date.now()
|
|
306
|
+
if (this.current && now - this.current.collectedAt < this.ttlMs) {
|
|
307
|
+
return this.current
|
|
308
|
+
}
|
|
309
|
+
if (this.inFlight) {
|
|
310
|
+
return this.inFlight
|
|
311
|
+
}
|
|
312
|
+
this.inFlight = this.collect().then((snapshot) => {
|
|
313
|
+
this.current = snapshot
|
|
314
|
+
return snapshot
|
|
315
|
+
}).catch((error) => {
|
|
316
|
+
if (this.current) {
|
|
317
|
+
return { ...this.current, stale: true }
|
|
318
|
+
}
|
|
319
|
+
return {
|
|
320
|
+
available: false,
|
|
321
|
+
stale: false,
|
|
322
|
+
collectedAt: Date.now(),
|
|
323
|
+
providers: [],
|
|
324
|
+
processes: new Map(),
|
|
325
|
+
errors: [{ provider: "gpu", error: error && error.message ? error.message : String(error) }]
|
|
326
|
+
}
|
|
327
|
+
}).finally(() => {
|
|
328
|
+
this.inFlight = null
|
|
329
|
+
})
|
|
330
|
+
return this.inFlight
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
function sumGpuMemory(snapshot, pids) {
|
|
335
|
+
const processes = snapshot && snapshot.processes instanceof Map ? snapshot.processes : new Map()
|
|
336
|
+
let bytes = 0
|
|
337
|
+
for (const pid of pids || []) {
|
|
338
|
+
const entry = processes.get(pid)
|
|
339
|
+
if (!entry) continue
|
|
340
|
+
bytes += entry.usedGpuMemoryBytes || 0
|
|
341
|
+
}
|
|
342
|
+
return { bytes }
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
module.exports = {
|
|
346
|
+
GpuSampler,
|
|
347
|
+
parseMemoryToBytes,
|
|
348
|
+
sumGpuMemory
|
|
349
|
+
}
|
|
@@ -0,0 +1,322 @@
|
|
|
1
|
+
"use strict"
|
|
2
|
+
|
|
3
|
+
const os = require("os")
|
|
4
|
+
const { GpuSampler, sumGpuMemory } = require("./gpu")
|
|
5
|
+
const { MacFootprintSampler } = require("./macos_footprint")
|
|
6
|
+
const { ProcessSampler, getDescendantPids, sumProcessMetrics } = require("./process_tree")
|
|
7
|
+
const { ResourceUsagePreferences } = require("./preferences")
|
|
8
|
+
|
|
9
|
+
function formatBytes(bytes) {
|
|
10
|
+
const value = Number(bytes)
|
|
11
|
+
if (!Number.isFinite(value) || value < 0) {
|
|
12
|
+
return "--"
|
|
13
|
+
}
|
|
14
|
+
const k = 1024
|
|
15
|
+
if (value >= k * k * k) {
|
|
16
|
+
return `${Math.floor((value / k / k / k) * 100) / 100} GB`
|
|
17
|
+
}
|
|
18
|
+
if (value >= k * k) {
|
|
19
|
+
return `${Math.floor((value / k / k) * 100) / 100} MB`
|
|
20
|
+
}
|
|
21
|
+
if (value >= k) {
|
|
22
|
+
return `${Math.floor((value / k) * 100) / 100} KB`
|
|
23
|
+
}
|
|
24
|
+
return `${Math.floor(value)} B`
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function normalizeWorkspaceName(value) {
|
|
28
|
+
if (typeof value !== "string") {
|
|
29
|
+
return ""
|
|
30
|
+
}
|
|
31
|
+
const trimmed = value.trim()
|
|
32
|
+
if (!trimmed || trimmed.includes("/") || trimmed.includes("\\") || trimmed === "." || trimmed === "..") {
|
|
33
|
+
return ""
|
|
34
|
+
}
|
|
35
|
+
return trimmed
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
class ResourceUsageService {
|
|
39
|
+
constructor(options = {}) {
|
|
40
|
+
if (!options.kernel) {
|
|
41
|
+
throw new Error("ResourceUsageService requires kernel")
|
|
42
|
+
}
|
|
43
|
+
this.kernel = options.kernel
|
|
44
|
+
this.platform = this.kernel.platform || os.platform()
|
|
45
|
+
this.preferences = new ResourceUsagePreferences({ kernel: this.kernel })
|
|
46
|
+
this.processSampler = new ProcessSampler({
|
|
47
|
+
platform: this.platform,
|
|
48
|
+
ttlMs: 4000,
|
|
49
|
+
timeoutMs: 2500
|
|
50
|
+
})
|
|
51
|
+
this.macFootprintSampler = new MacFootprintSampler({
|
|
52
|
+
platform: this.platform,
|
|
53
|
+
ttlMs: 15000,
|
|
54
|
+
timeoutMs: 2000
|
|
55
|
+
})
|
|
56
|
+
this.gpuSampler = new GpuSampler({
|
|
57
|
+
kernel: this.kernel,
|
|
58
|
+
ttlMs: 10000,
|
|
59
|
+
timeoutMs: 2500
|
|
60
|
+
})
|
|
61
|
+
this.cpuAverages = new Map()
|
|
62
|
+
this.workspaceCache = new Map()
|
|
63
|
+
this.collectInFlight = null
|
|
64
|
+
this.lastGlobalCollectAt = 0
|
|
65
|
+
this.globalTtlMs = options.globalTtlMs || 5000
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
async getPreferences() {
|
|
69
|
+
return this.preferences.read()
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
async updatePreferences(updates = {}) {
|
|
73
|
+
const preferences = await this.preferences.update(updates)
|
|
74
|
+
this.workspaceCache.clear()
|
|
75
|
+
this.lastGlobalCollectAt = 0
|
|
76
|
+
return preferences
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
getShellRootGroups() {
|
|
80
|
+
if (!this.kernel || !this.kernel.shell || typeof this.kernel.path !== "function") {
|
|
81
|
+
return new Map()
|
|
82
|
+
}
|
|
83
|
+
const apiRoot = this.kernel.path("api")
|
|
84
|
+
if (typeof this.kernel.shell.resourceRootsByWorkspace === "function") {
|
|
85
|
+
return this.kernel.shell.resourceRootsByWorkspace(apiRoot)
|
|
86
|
+
}
|
|
87
|
+
return new Map()
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
smoothCpu(workspaceName, value) {
|
|
91
|
+
if (!Number.isFinite(value)) {
|
|
92
|
+
this.cpuAverages.delete(workspaceName)
|
|
93
|
+
return null
|
|
94
|
+
}
|
|
95
|
+
const now = Date.now()
|
|
96
|
+
const previous = this.cpuAverages.get(workspaceName)
|
|
97
|
+
let smoothed = value
|
|
98
|
+
if (previous && Number.isFinite(previous.value) && now - previous.updatedAt < 30000) {
|
|
99
|
+
smoothed = (previous.value * 0.55) + (value * 0.45)
|
|
100
|
+
}
|
|
101
|
+
this.cpuAverages.set(workspaceName, { value: smoothed, updatedAt: now })
|
|
102
|
+
return smoothed
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
metric(enabled, available, value = {}) {
|
|
106
|
+
return {
|
|
107
|
+
enabled: !!enabled,
|
|
108
|
+
available: !!available,
|
|
109
|
+
...value
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
selectFootprintPids(pids) {
|
|
114
|
+
return Array.from(pids || []).sort((a, b) => a - b)
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
emptyWorkspaceUsage(name, preferences, options = {}) {
|
|
118
|
+
const updatedAt = options.updatedAt || Date.now()
|
|
119
|
+
return {
|
|
120
|
+
ok: true,
|
|
121
|
+
workspace: name,
|
|
122
|
+
running: false,
|
|
123
|
+
updated_at: new Date(updatedAt).toISOString(),
|
|
124
|
+
stale: !!options.stale,
|
|
125
|
+
preferences,
|
|
126
|
+
metrics: {
|
|
127
|
+
ram: this.metric(preferences.show_ram, false, {
|
|
128
|
+
bytes: 0,
|
|
129
|
+
formatted: "0 B"
|
|
130
|
+
}),
|
|
131
|
+
cpu: this.metric(preferences.show_cpu, false, {
|
|
132
|
+
percent: null
|
|
133
|
+
}),
|
|
134
|
+
vram: this.metric(preferences.show_vram, false, {
|
|
135
|
+
bytes: 0,
|
|
136
|
+
formatted: "0 MB"
|
|
137
|
+
})
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
sumFootprintBytes(footprintSnapshot, pids) {
|
|
143
|
+
const perPid = footprintSnapshot && footprintSnapshot.perPid instanceof Map ? footprintSnapshot.perPid : new Map()
|
|
144
|
+
let bytes = 0
|
|
145
|
+
for (const pid of pids || []) {
|
|
146
|
+
bytes += perPid.get(pid) || 0
|
|
147
|
+
}
|
|
148
|
+
return bytes
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
buildWorkspaceUsage(name, preferences, roots, processSnapshot, processData, gpuSnapshot, footprintSnapshot) {
|
|
152
|
+
const pids = processData && processData.pids instanceof Set ? processData.pids : new Set()
|
|
153
|
+
const processSummary = processData && processData.summary ? processData.summary : {
|
|
154
|
+
processCount: 0,
|
|
155
|
+
rssBytes: 0,
|
|
156
|
+
cpuPercent: null,
|
|
157
|
+
cpuPercentCores: null
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
let ramBytes = processSummary.rssBytes
|
|
161
|
+
const footprintBytes = preferences.show_ram && this.platform === "darwin"
|
|
162
|
+
? this.sumFootprintBytes(footprintSnapshot, pids)
|
|
163
|
+
: 0
|
|
164
|
+
if (footprintBytes > 0) {
|
|
165
|
+
ramBytes = footprintBytes
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
const smoothedCpu = preferences.show_cpu
|
|
169
|
+
? this.smoothCpu(name, processSummary.cpuPercent)
|
|
170
|
+
: null
|
|
171
|
+
|
|
172
|
+
const gpuSummary = preferences.show_vram && pids.size > 0
|
|
173
|
+
? sumGpuMemory(gpuSnapshot, pids)
|
|
174
|
+
: {
|
|
175
|
+
bytes: 0
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
const rootPids = roots.map((root) => root.pid).filter((pid) => Number.isFinite(pid))
|
|
179
|
+
const running = rootPids.length > 0 && (processSummary.processCount > 0 || pids.size > 0)
|
|
180
|
+
const processAvailable = !!(processSnapshot && processSnapshot.available)
|
|
181
|
+
const ramAvailable = running && !!(processAvailable || footprintBytes > 0)
|
|
182
|
+
const gpuAvailable = !!(gpuSnapshot && gpuSnapshot.available)
|
|
183
|
+
const updatedAt = Math.max(
|
|
184
|
+
processSnapshot && processSnapshot.collectedAt ? processSnapshot.collectedAt : 0,
|
|
185
|
+
footprintSnapshot && footprintSnapshot.collectedAt ? footprintSnapshot.collectedAt : 0,
|
|
186
|
+
gpuSnapshot && gpuSnapshot.collectedAt ? gpuSnapshot.collectedAt : 0,
|
|
187
|
+
Date.now()
|
|
188
|
+
)
|
|
189
|
+
|
|
190
|
+
return {
|
|
191
|
+
ok: true,
|
|
192
|
+
workspace: name,
|
|
193
|
+
running,
|
|
194
|
+
updated_at: new Date(updatedAt).toISOString(),
|
|
195
|
+
stale: !!((processSnapshot && processSnapshot.stale) || (footprintSnapshot && footprintSnapshot.stale) || (gpuSnapshot && gpuSnapshot.stale)),
|
|
196
|
+
preferences,
|
|
197
|
+
metrics: {
|
|
198
|
+
ram: this.metric(preferences.show_ram, ramAvailable, {
|
|
199
|
+
bytes: ramBytes,
|
|
200
|
+
formatted: formatBytes(ramBytes)
|
|
201
|
+
}),
|
|
202
|
+
cpu: this.metric(preferences.show_cpu, Number.isFinite(smoothedCpu), {
|
|
203
|
+
percent: Number.isFinite(smoothedCpu) ? Math.max(0, Math.round(smoothedCpu * 10) / 10) : null
|
|
204
|
+
}),
|
|
205
|
+
vram: this.metric(preferences.show_vram, gpuAvailable && running, {
|
|
206
|
+
bytes: gpuSummary.bytes,
|
|
207
|
+
formatted: gpuSummary.bytes > 0 ? formatBytes(gpuSummary.bytes) : "0 MB"
|
|
208
|
+
})
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
async collectGlobalUsage(preferencesOverride = null) {
|
|
214
|
+
const preferences = preferencesOverride || await this.getPreferences()
|
|
215
|
+
const rootGroups = this.getShellRootGroups()
|
|
216
|
+
const hasRoots = rootGroups.size > 0
|
|
217
|
+
const shouldCollectProcesses = hasRoots && (
|
|
218
|
+
preferences.show_ram ||
|
|
219
|
+
preferences.show_cpu ||
|
|
220
|
+
preferences.show_vram
|
|
221
|
+
)
|
|
222
|
+
|
|
223
|
+
const processSnapshot = shouldCollectProcesses ? await this.processSampler.getSnapshot() : null
|
|
224
|
+
const workspaceProcesses = new Map()
|
|
225
|
+
const allPids = new Set()
|
|
226
|
+
|
|
227
|
+
for (const [name, roots] of rootGroups.entries()) {
|
|
228
|
+
const rootPids = roots.map((root) => root.pid).filter((pid) => Number.isFinite(pid))
|
|
229
|
+
const pids = processSnapshot ? getDescendantPids(processSnapshot, rootPids) : new Set()
|
|
230
|
+
const summary = processSnapshot ? sumProcessMetrics(processSnapshot, pids) : {
|
|
231
|
+
processCount: 0,
|
|
232
|
+
rssBytes: 0,
|
|
233
|
+
cpuPercent: null,
|
|
234
|
+
cpuPercentCores: null
|
|
235
|
+
}
|
|
236
|
+
workspaceProcesses.set(name, { pids, summary })
|
|
237
|
+
for (const pid of pids) {
|
|
238
|
+
allPids.add(pid)
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
const footprintSnapshot = preferences.show_ram && this.platform === "darwin" && allPids.size > 0
|
|
243
|
+
? await this.macFootprintSampler.getFootprintByPid(this.selectFootprintPids(allPids))
|
|
244
|
+
: null
|
|
245
|
+
|
|
246
|
+
const gpuSnapshot = preferences.show_vram && allPids.size > 0
|
|
247
|
+
? await this.gpuSampler.getSnapshot()
|
|
248
|
+
: null
|
|
249
|
+
|
|
250
|
+
const nextCache = new Map()
|
|
251
|
+
for (const [name, roots] of rootGroups.entries()) {
|
|
252
|
+
const usage = this.buildWorkspaceUsage(
|
|
253
|
+
name,
|
|
254
|
+
preferences,
|
|
255
|
+
roots,
|
|
256
|
+
processSnapshot,
|
|
257
|
+
workspaceProcesses.get(name),
|
|
258
|
+
gpuSnapshot,
|
|
259
|
+
footprintSnapshot
|
|
260
|
+
)
|
|
261
|
+
nextCache.set(name, usage)
|
|
262
|
+
}
|
|
263
|
+
for (const name of this.cpuAverages.keys()) {
|
|
264
|
+
if (!rootGroups.has(name)) {
|
|
265
|
+
this.cpuAverages.delete(name)
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
this.workspaceCache = nextCache
|
|
270
|
+
this.lastGlobalCollectAt = Date.now()
|
|
271
|
+
return nextCache
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
async ensureGlobalRefresh(options = {}) {
|
|
275
|
+
const force = !!options.force
|
|
276
|
+
const wait = !!options.wait
|
|
277
|
+
const now = Date.now()
|
|
278
|
+
if (!force && this.lastGlobalCollectAt && now - this.lastGlobalCollectAt < this.globalTtlMs) {
|
|
279
|
+
return this.workspaceCache
|
|
280
|
+
}
|
|
281
|
+
if (!this.collectInFlight) {
|
|
282
|
+
this.collectInFlight = this.collectGlobalUsage().catch(() => {
|
|
283
|
+
this.lastGlobalCollectAt = Date.now()
|
|
284
|
+
return this.workspaceCache
|
|
285
|
+
}).finally(() => {
|
|
286
|
+
this.collectInFlight = null
|
|
287
|
+
})
|
|
288
|
+
}
|
|
289
|
+
if (wait) {
|
|
290
|
+
return this.collectInFlight
|
|
291
|
+
}
|
|
292
|
+
return this.workspaceCache
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
markCachedUsage(usage) {
|
|
296
|
+
if (!usage) return usage
|
|
297
|
+
const stale = !!(this.lastGlobalCollectAt && Date.now() - this.lastGlobalCollectAt >= this.globalTtlMs)
|
|
298
|
+
return stale ? { ...usage, stale: true } : usage
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
async getWorkspaceUsage(workspaceName) {
|
|
302
|
+
const name = normalizeWorkspaceName(workspaceName)
|
|
303
|
+
if (!name) {
|
|
304
|
+
return {
|
|
305
|
+
ok: false,
|
|
306
|
+
error: "Invalid workspace",
|
|
307
|
+
preferences: await this.getPreferences()
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
const cached = this.workspaceCache.get(name)
|
|
312
|
+
await this.ensureGlobalRefresh({ wait: !cached })
|
|
313
|
+
const updated = this.workspaceCache.get(name)
|
|
314
|
+
if (updated) {
|
|
315
|
+
return this.markCachedUsage(updated)
|
|
316
|
+
}
|
|
317
|
+
const preferences = await this.getPreferences()
|
|
318
|
+
return this.emptyWorkspaceUsage(name, preferences, { updatedAt: this.lastGlobalCollectAt || Date.now() })
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
module.exports = ResourceUsageService
|