pinokiod 7.4.0 → 7.4.2
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/bin/ffmpeg.js +16 -782
- package/kernel/index.js +7 -0
- package/kernel/legacy_cleanup/ffmpeg.js +167 -0
- package/kernel/legacy_cleanup/index.js +47 -0
- package/kernel/procs.js +1 -1
- package/package.json +2 -3
- package/server/views/app.ejs +11 -8
- package/test/ffmpeg-bin.test.js +73 -0
- package/test/legacy-cleanup.test.js +161 -0
- package/test/procs.test.js +49 -0
- package/script/verify-ffmpeg.js +0 -459
package/kernel/index.js
CHANGED
|
@@ -43,6 +43,7 @@ const AppLauncher = require('./app_launcher')
|
|
|
43
43
|
const ReadyState = require('./ready')
|
|
44
44
|
const Autolaunch = require('./autolaunch')
|
|
45
45
|
const LaunchRequirements = require('./launch_requirements')
|
|
46
|
+
const LegacyCleanup = require('./legacy_cleanup')
|
|
46
47
|
const { DownloaderHelper } = require('node-downloader-helper');
|
|
47
48
|
const { ProxyAgent } = require('proxy-agent');
|
|
48
49
|
const fakeUa = require('fake-useragent');
|
|
@@ -100,6 +101,7 @@ class Kernel {
|
|
|
100
101
|
this.launchRequirements = new LaunchRequirements(this)
|
|
101
102
|
this.autolaunch = new Autolaunch(this)
|
|
102
103
|
this.autolaunch_status = this.autolaunch.startupStatus()
|
|
104
|
+
this.legacyCleanup = new LegacyCleanup(this)
|
|
103
105
|
this.sysReady = new Promise((resolve) => {
|
|
104
106
|
this._resolveSysReady = resolve
|
|
105
107
|
})
|
|
@@ -1192,6 +1194,11 @@ class Kernel {
|
|
|
1192
1194
|
await Environment.ensurePinokioCacheDirs(this, {
|
|
1193
1195
|
throwOnFailure: true
|
|
1194
1196
|
})
|
|
1197
|
+
await this.legacyCleanup.runOnce((event) => {
|
|
1198
|
+
if (event && typeof event.raw === "string") {
|
|
1199
|
+
console.log(event.raw.trimEnd())
|
|
1200
|
+
}
|
|
1201
|
+
})
|
|
1195
1202
|
|
|
1196
1203
|
// if key.json doesn't exist, create an empty json file
|
|
1197
1204
|
let ee = await this.exists(this.homedir, "key.json")
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
const fs = require('fs')
|
|
2
|
+
const path = require('path')
|
|
3
|
+
|
|
4
|
+
const LEGACY_FFMPEG_HOOK_NAMES = [
|
|
5
|
+
'zz_pinokio_ffmpeg.sh',
|
|
6
|
+
'zz_pinokio_ffmpeg.bat',
|
|
7
|
+
'zz_pinokio_ffmpeg.ps1',
|
|
8
|
+
]
|
|
9
|
+
|
|
10
|
+
const TRANSIENT_REMOVE_ERRORS = new Set([
|
|
11
|
+
'EBUSY',
|
|
12
|
+
'ENOTEMPTY',
|
|
13
|
+
'EPERM',
|
|
14
|
+
'EACCES',
|
|
15
|
+
])
|
|
16
|
+
|
|
17
|
+
const REMOVE_RETRY_COUNT = 5
|
|
18
|
+
const REMOVE_RETRY_DELAY_MS = 100
|
|
19
|
+
|
|
20
|
+
function delay(ms) {
|
|
21
|
+
return new Promise((resolve) => setTimeout(resolve, ms))
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
async function pathExists(target) {
|
|
25
|
+
return fs.promises.access(target).then(() => true).catch(() => false)
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function legacyFfmpegRuntimePaths(kernel) {
|
|
29
|
+
if (!kernel || !kernel.bin || typeof kernel.bin.path !== 'function') {
|
|
30
|
+
return []
|
|
31
|
+
}
|
|
32
|
+
return [
|
|
33
|
+
kernel.bin.path('ffmpeg-env'),
|
|
34
|
+
kernel.bin.path('ffmpeg-pkgs'),
|
|
35
|
+
kernel.bin.path('ffmpeg'),
|
|
36
|
+
kernel.bin.path('ffmpeg-tmp'),
|
|
37
|
+
]
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function legacyFfmpegHookFiles(kernel) {
|
|
41
|
+
if (!kernel || !kernel.bin || typeof kernel.bin.path !== 'function') {
|
|
42
|
+
return []
|
|
43
|
+
}
|
|
44
|
+
const dirs = [
|
|
45
|
+
kernel.bin.path('miniconda', 'etc', 'conda', 'activate.d'),
|
|
46
|
+
kernel.bin.path('miniconda', 'etc', 'conda', 'deactivate.d'),
|
|
47
|
+
]
|
|
48
|
+
return dirs.flatMap((dir) => LEGACY_FFMPEG_HOOK_NAMES.map((name) => path.resolve(dir, name)))
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function manualFfmpegHookCleanupMessage(failures) {
|
|
52
|
+
const files = failures.map((failure) => failure.target)
|
|
53
|
+
return [
|
|
54
|
+
'Pinokio could not remove old FFmpeg Conda activation hooks.',
|
|
55
|
+
'',
|
|
56
|
+
'These old hooks can break the base Conda environment by adding the removed ffmpeg-env runtime to PATH.',
|
|
57
|
+
'',
|
|
58
|
+
'Close all Pinokio windows and any terminals using Pinokio, then delete these files if they exist:',
|
|
59
|
+
...files.map((file) => `- ${file}`),
|
|
60
|
+
'',
|
|
61
|
+
'After deleting them, reopen Pinokio.',
|
|
62
|
+
].join('\n')
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
class LegacyFfmpegCleanupError extends Error {
|
|
66
|
+
constructor(failures) {
|
|
67
|
+
super(manualFfmpegHookCleanupMessage(failures))
|
|
68
|
+
this.name = 'LegacyFfmpegCleanupError'
|
|
69
|
+
this.failures = failures
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
async function hasAnyPath(paths) {
|
|
74
|
+
for (const target of paths) {
|
|
75
|
+
if (await pathExists(target)) {
|
|
76
|
+
return true
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
return false
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
async function hasCleanupTargets(kernel) {
|
|
83
|
+
return await hasAnyPath(legacyFfmpegHookFiles(kernel)) ||
|
|
84
|
+
await hasAnyPath(legacyFfmpegRuntimePaths(kernel))
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function isTransientRemoveError(error) {
|
|
88
|
+
return error && TRANSIENT_REMOVE_ERRORS.has(error.code)
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
async function retryTransient(operation) {
|
|
92
|
+
let lastError
|
|
93
|
+
for (let attempt = 0; attempt <= REMOVE_RETRY_COUNT; attempt += 1) {
|
|
94
|
+
try {
|
|
95
|
+
return await operation()
|
|
96
|
+
} catch (error) {
|
|
97
|
+
if (error && error.code === 'ENOENT') {
|
|
98
|
+
return null
|
|
99
|
+
}
|
|
100
|
+
lastError = error
|
|
101
|
+
if (!isTransientRemoveError(error) || attempt === REMOVE_RETRY_COUNT) {
|
|
102
|
+
break
|
|
103
|
+
}
|
|
104
|
+
await delay(REMOVE_RETRY_DELAY_MS * (attempt + 1))
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
throw lastError
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
async function removeLegacyPath(target, ondata) {
|
|
111
|
+
const existed = await pathExists(target)
|
|
112
|
+
if (!existed) {
|
|
113
|
+
return { target, found: false, removed: false }
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
try {
|
|
117
|
+
await retryTransient(() => fs.promises.rm(target, {
|
|
118
|
+
recursive: true,
|
|
119
|
+
force: true,
|
|
120
|
+
maxRetries: REMOVE_RETRY_COUNT,
|
|
121
|
+
retryDelay: REMOVE_RETRY_DELAY_MS,
|
|
122
|
+
}))
|
|
123
|
+
} catch (error) {
|
|
124
|
+
if (ondata) {
|
|
125
|
+
const reason = error && (error.code || error.message) ? error.code || error.message : String(error)
|
|
126
|
+
ondata({ raw: `could not remove legacy path: ${target} (${reason})\r\n` })
|
|
127
|
+
}
|
|
128
|
+
return { target, found: true, removed: false, error }
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
if (ondata) {
|
|
132
|
+
ondata({ raw: `removed legacy path: ${target}\r\n` })
|
|
133
|
+
}
|
|
134
|
+
return { target, found: true, removed: true }
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
async function run(kernel, ondata) {
|
|
138
|
+
const results = []
|
|
139
|
+
const hookFailures = []
|
|
140
|
+
|
|
141
|
+
for (const target of legacyFfmpegHookFiles(kernel)) {
|
|
142
|
+
const result = await removeLegacyPath(target, ondata)
|
|
143
|
+
results.push(result)
|
|
144
|
+
if (result.found && !result.removed) {
|
|
145
|
+
hookFailures.push(result)
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
if (hookFailures.length > 0) {
|
|
150
|
+
throw new LegacyFfmpegCleanupError(hookFailures)
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
for (const target of legacyFfmpegRuntimePaths(kernel)) {
|
|
154
|
+
results.push(await removeLegacyPath(target, ondata))
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
return results
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
module.exports = {
|
|
161
|
+
LegacyFfmpegCleanupError,
|
|
162
|
+
hasCleanupTargets,
|
|
163
|
+
legacyFfmpegHookFiles,
|
|
164
|
+
legacyFfmpegRuntimePaths,
|
|
165
|
+
name: 'ffmpeg',
|
|
166
|
+
run,
|
|
167
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
const path = require('path')
|
|
2
|
+
const ffmpeg = require('./ffmpeg')
|
|
3
|
+
|
|
4
|
+
const CLEANUPS = [
|
|
5
|
+
ffmpeg,
|
|
6
|
+
]
|
|
7
|
+
|
|
8
|
+
class LegacyCleanup {
|
|
9
|
+
constructor(kernel) {
|
|
10
|
+
this.kernel = kernel
|
|
11
|
+
this.promises = new Map()
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
async runOnce(ondata) {
|
|
15
|
+
const home = this.kernel && this.kernel.homedir ? path.resolve(this.kernel.homedir) : ''
|
|
16
|
+
if (this.promises.has(home)) {
|
|
17
|
+
return this.promises.get(home)
|
|
18
|
+
}
|
|
19
|
+
const promise = this.run(ondata).catch((error) => {
|
|
20
|
+
this.promises.delete(home)
|
|
21
|
+
throw error
|
|
22
|
+
})
|
|
23
|
+
this.promises.set(home, promise)
|
|
24
|
+
return promise
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
async run(ondata) {
|
|
28
|
+
if (!this.kernel || !this.kernel.homedir) {
|
|
29
|
+
return { skipped: true, results: [] }
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const cleanupResults = []
|
|
33
|
+
for (const cleanup of CLEANUPS) {
|
|
34
|
+
if (await cleanup.hasCleanupTargets(this.kernel)) {
|
|
35
|
+
cleanupResults.push({
|
|
36
|
+
name: cleanup.name || 'legacy',
|
|
37
|
+
results: await cleanup.run(this.kernel, ondata),
|
|
38
|
+
})
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
return { skipped: false, results: cleanupResults }
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
module.exports = LegacyCleanup
|
|
47
|
+
module.exports.ffmpeg = ffmpeg
|
package/kernel/procs.js
CHANGED
package/package.json
CHANGED
|
@@ -1,12 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pinokiod",
|
|
3
|
-
"version": "7.4.
|
|
3
|
+
"version": "7.4.2",
|
|
4
4
|
"description": "",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"scripts": {
|
|
7
7
|
"download_readme": "wget -O kernel/proto/PINOKIO.md https://raw.githubusercontent.com/pinokiocomputer/home/refs/heads/main/docs/README.md",
|
|
8
|
-
"start": "node script/index"
|
|
9
|
-
"verify:ffmpeg": "node script/verify-ffmpeg.js"
|
|
8
|
+
"start": "node script/index"
|
|
10
9
|
},
|
|
11
10
|
"author": "",
|
|
12
11
|
"license": "MIT",
|
package/server/views/app.ejs
CHANGED
|
@@ -19,6 +19,7 @@
|
|
|
19
19
|
<% if (agent === "electron") { %>
|
|
20
20
|
<link href="/electron.css" rel="stylesheet"/>
|
|
21
21
|
<% } %>
|
|
22
|
+
<% const appMobileBreakpointPx = 980 %>
|
|
22
23
|
<style>
|
|
23
24
|
:root {
|
|
24
25
|
--assist-surface-light: var(--pinokio-chrome-accent-bg-light);
|
|
@@ -6169,7 +6170,7 @@ body.dark .pinokio-fork-dropdown-remote, body.dark .pinokio-publish-dropdown-rem
|
|
|
6169
6170
|
}
|
|
6170
6171
|
}
|
|
6171
6172
|
*/
|
|
6172
|
-
@media only screen and (max-width:
|
|
6173
|
+
@media only screen and (max-width: <%= appMobileBreakpointPx %>px) {
|
|
6173
6174
|
body {
|
|
6174
6175
|
display: flex !important;
|
|
6175
6176
|
flex-direction: row !important;
|
|
@@ -6410,7 +6411,7 @@ header.navheader h1 {
|
|
|
6410
6411
|
margin: 0 10px;
|
|
6411
6412
|
}
|
|
6412
6413
|
}
|
|
6413
|
-
@media only screen and (max-width:
|
|
6414
|
+
@media only screen and (max-width: <%= appMobileBreakpointPx %>px) {
|
|
6414
6415
|
:root {
|
|
6415
6416
|
--mobile-bottom-bar-height: calc(46px + env(safe-area-inset-bottom));
|
|
6416
6417
|
}
|
|
@@ -8361,6 +8362,10 @@ body.dark .pinokio-custom-terminal-header {
|
|
|
8361
8362
|
let pendingHomeSelectionRetry = null
|
|
8362
8363
|
let initialWorkspaceDiskUsageRequested = false
|
|
8363
8364
|
const openWithoutLaunching = <%- JSON.stringify(type === "run" && !autoselect) %>
|
|
8365
|
+
const appMobileBreakpoint = <%= appMobileBreakpointPx %>
|
|
8366
|
+
const appMobileBreakpointQuery = `(max-width: ${appMobileBreakpoint}px)`
|
|
8367
|
+
window.PinokioAppMobileBreakpoint = appMobileBreakpoint
|
|
8368
|
+
window.PinokioAppMobileBreakpointQuery = appMobileBreakpointQuery
|
|
8364
8369
|
const scheduleSelectionRetry = (delay = 100) => {
|
|
8365
8370
|
if (pendingSelectionRetry !== null) {
|
|
8366
8371
|
return
|
|
@@ -11097,14 +11102,12 @@ const rerenderMenuSection = (container, html) => {
|
|
|
11097
11102
|
}
|
|
11098
11103
|
|
|
11099
11104
|
|
|
11100
|
-
|
|
11101
|
-
|
|
11102
|
-
const mobileMenuQuery = window.matchMedia ? window.matchMedia("(max-width: 768px)") : null
|
|
11105
|
+
const mobileMenuQuery = window.matchMedia ? window.matchMedia(appMobileBreakpointQuery) : null
|
|
11103
11106
|
const isMobileMenuOpen = () => document.body.classList.contains("mobile-menu-open")
|
|
11104
11107
|
const appcanvas = document.querySelector(".appcanvas")
|
|
11105
11108
|
const closeWindowButton = document.querySelector("#close-window")
|
|
11106
11109
|
const mobileMenuBackdrop = document.querySelector("[data-mobile-menu-backdrop]")
|
|
11107
|
-
const isMobilePanelLayout = () => mobileMenuQuery ? mobileMenuQuery.matches : window.innerWidth <=
|
|
11110
|
+
const isMobilePanelLayout = () => mobileMenuQuery ? mobileMenuQuery.matches : window.innerWidth <= appMobileBreakpoint
|
|
11108
11111
|
const normalizeMobilePanelName = (value) => {
|
|
11109
11112
|
return value === "logs" || value === "ask-ai" || value === "feed" ? value : ""
|
|
11110
11113
|
}
|
|
@@ -11844,7 +11847,7 @@ const rerenderMenuSection = (container, html) => {
|
|
|
11844
11847
|
if (appcanvas && sidebarResizer && sidebarContainer) {
|
|
11845
11848
|
const SIDEBAR_MIN_WIDTH = 140
|
|
11846
11849
|
const SIDEBAR_MAX_WIDTH = 560
|
|
11847
|
-
const sidebarMobileQuery = window.matchMedia ? window.matchMedia(
|
|
11850
|
+
const sidebarMobileQuery = window.matchMedia ? window.matchMedia(appMobileBreakpointQuery) : null
|
|
11848
11851
|
const workspaceName = "<%= typeof name === 'string' ? name : '' %>"
|
|
11849
11852
|
const sidebarWidthStorageKey = workspaceName
|
|
11850
11853
|
? `pinokio-app-sidebar-width:${workspaceName}`
|
|
@@ -17862,7 +17865,7 @@ document.addEventListener("DOMContentLoaded", () => {
|
|
|
17862
17865
|
enabled = false
|
|
17863
17866
|
}
|
|
17864
17867
|
|
|
17865
|
-
const communityModeQuery = window.matchMedia ? window.matchMedia("(max-width:
|
|
17868
|
+
const communityModeQuery = window.matchMedia ? window.matchMedia(window.PinokioAppMobileBreakpointQuery || "(max-width: <%= appMobileBreakpointPx %>px)") : null
|
|
17866
17869
|
let communityMode = false
|
|
17867
17870
|
|
|
17868
17871
|
const setCommunityMode = (next, opts) => {
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
const assert = require('node:assert/strict')
|
|
2
|
+
const test = require('node:test')
|
|
3
|
+
|
|
4
|
+
const Ffmpeg = require('../kernel/bin/ffmpeg')
|
|
5
|
+
|
|
6
|
+
function createKernel(platform = 'win32') {
|
|
7
|
+
return {
|
|
8
|
+
platform,
|
|
9
|
+
bin: {
|
|
10
|
+
installed: {
|
|
11
|
+
conda: new Set(),
|
|
12
|
+
conda_versions: {},
|
|
13
|
+
},
|
|
14
|
+
exec: async () => {},
|
|
15
|
+
},
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function createFfmpeg(kernel) {
|
|
20
|
+
const ffmpeg = new Ffmpeg()
|
|
21
|
+
ffmpeg.kernel = kernel
|
|
22
|
+
return ffmpeg
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
test('FFmpeg bin installs pinned ffmpeg into base Conda on Windows', async () => {
|
|
26
|
+
const calls = []
|
|
27
|
+
const kernel = createKernel('win32')
|
|
28
|
+
kernel.bin.exec = async (payload) => {
|
|
29
|
+
calls.push(payload)
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
await createFfmpeg(kernel).install({}, () => {})
|
|
33
|
+
|
|
34
|
+
assert.equal(calls.length, 1)
|
|
35
|
+
assert.deepEqual(calls[0].message, [
|
|
36
|
+
'conda clean -y --all',
|
|
37
|
+
'conda install -y --solver=classic --override-channels -c conda-forge "conda-libmamba-solver>=25.4.0" libmamba libmambapy libsolv libarchive',
|
|
38
|
+
'conda install -y --solver=classic --override-channels -c conda-forge ffmpeg=8.1.2',
|
|
39
|
+
])
|
|
40
|
+
assert.equal(calls[0].env, undefined)
|
|
41
|
+
assert.equal(calls[0].message.some((command) => /ffmpeg-env|conda create|-p /.test(command)), false)
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
test('FFmpeg bin installs pinned ffmpeg into base Conda without Windows solver repair elsewhere', async () => {
|
|
45
|
+
const calls = []
|
|
46
|
+
const kernel = createKernel('darwin')
|
|
47
|
+
kernel.bin.exec = async (payload) => {
|
|
48
|
+
calls.push(payload)
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
await createFfmpeg(kernel).install({}, () => {})
|
|
52
|
+
|
|
53
|
+
assert.deepEqual(calls[0].message, [
|
|
54
|
+
'conda clean -y --all',
|
|
55
|
+
'conda install -y --override-channels -c conda-forge ffmpeg=8.1.2',
|
|
56
|
+
])
|
|
57
|
+
})
|
|
58
|
+
|
|
59
|
+
test('FFmpeg installed check requires the base Conda package at the pinned version', async () => {
|
|
60
|
+
const kernel = createKernel('win32')
|
|
61
|
+
const ffmpeg = createFfmpeg(kernel)
|
|
62
|
+
|
|
63
|
+
kernel.bin.installed.conda = new Set(['ffmpeg'])
|
|
64
|
+
kernel.bin.installed.conda_versions = { ffmpeg: '8.1.2' }
|
|
65
|
+
assert.equal(await ffmpeg.installed(), true)
|
|
66
|
+
|
|
67
|
+
kernel.bin.installed.conda_versions = { ffmpeg: '8.1.1' }
|
|
68
|
+
assert.equal(await ffmpeg.installed(), false)
|
|
69
|
+
|
|
70
|
+
kernel.bin.installed.conda = new Set()
|
|
71
|
+
kernel.bin.installed.conda_versions = {}
|
|
72
|
+
assert.equal(await ffmpeg.installed(), false)
|
|
73
|
+
})
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
const assert = require('node:assert/strict')
|
|
2
|
+
const fsSync = require('node:fs')
|
|
3
|
+
const fs = require('node:fs/promises')
|
|
4
|
+
const os = require('node:os')
|
|
5
|
+
const path = require('node:path')
|
|
6
|
+
const test = require('node:test')
|
|
7
|
+
|
|
8
|
+
const LegacyCleanup = require('../kernel/legacy_cleanup')
|
|
9
|
+
|
|
10
|
+
function createKernel(root) {
|
|
11
|
+
return {
|
|
12
|
+
homedir: root,
|
|
13
|
+
bin: {
|
|
14
|
+
path: (...parts) => path.join(root, 'bin', ...parts),
|
|
15
|
+
},
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
async function writeFixtureFile(filepath) {
|
|
20
|
+
await fs.mkdir(path.dirname(filepath), { recursive: true })
|
|
21
|
+
await fs.writeFile(filepath, 'legacy\n')
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
async function exists(filepath) {
|
|
25
|
+
try {
|
|
26
|
+
await fs.access(filepath)
|
|
27
|
+
return true
|
|
28
|
+
} catch (_) {
|
|
29
|
+
return false
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
async function createLegacyFfmpegState(root) {
|
|
34
|
+
const activate = path.join(root, 'bin', 'miniconda', 'etc', 'conda', 'activate.d')
|
|
35
|
+
const deactivate = path.join(root, 'bin', 'miniconda', 'etc', 'conda', 'deactivate.d')
|
|
36
|
+
const hooks = [
|
|
37
|
+
path.join(activate, 'zz_pinokio_ffmpeg.sh'),
|
|
38
|
+
path.join(activate, 'zz_pinokio_ffmpeg.bat'),
|
|
39
|
+
path.join(activate, 'zz_pinokio_ffmpeg.ps1'),
|
|
40
|
+
path.join(deactivate, 'zz_pinokio_ffmpeg.sh'),
|
|
41
|
+
path.join(deactivate, 'zz_pinokio_ffmpeg.bat'),
|
|
42
|
+
path.join(deactivate, 'zz_pinokio_ffmpeg.ps1'),
|
|
43
|
+
]
|
|
44
|
+
for (const hook of hooks) {
|
|
45
|
+
await writeFixtureFile(hook)
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const dirs = [
|
|
49
|
+
path.join(root, 'bin', 'ffmpeg-env'),
|
|
50
|
+
path.join(root, 'bin', 'ffmpeg-pkgs'),
|
|
51
|
+
path.join(root, 'bin', 'ffmpeg'),
|
|
52
|
+
path.join(root, 'bin', 'ffmpeg-tmp'),
|
|
53
|
+
]
|
|
54
|
+
for (const dir of dirs) {
|
|
55
|
+
await fs.mkdir(dir, { recursive: true })
|
|
56
|
+
await fs.writeFile(path.join(dir, 'marker.txt'), 'legacy\n')
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
return { hooks, dirs }
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
test('LegacyCleanup removes stale FFmpeg Conda hooks and old runtime paths once at startup', async () => {
|
|
63
|
+
const root = await fs.mkdtemp(path.join(os.tmpdir(), 'pinokio-legacy-cleanup-'))
|
|
64
|
+
const { hooks, dirs } = await createLegacyFfmpegState(root)
|
|
65
|
+
const packets = []
|
|
66
|
+
const cleanup = new LegacyCleanup(createKernel(root))
|
|
67
|
+
|
|
68
|
+
const result = await cleanup.runOnce((packet) => packets.push(packet))
|
|
69
|
+
|
|
70
|
+
assert.equal(result.skipped, false)
|
|
71
|
+
assert.ok(result.results.some((cleanupResult) => {
|
|
72
|
+
return cleanupResult.name === 'ffmpeg' &&
|
|
73
|
+
cleanupResult.results.some((item) => item.found && item.removed)
|
|
74
|
+
}))
|
|
75
|
+
for (const hook of hooks) {
|
|
76
|
+
assert.equal(await exists(hook), false, `${hook} should be removed`)
|
|
77
|
+
}
|
|
78
|
+
for (const dir of dirs) {
|
|
79
|
+
assert.equal(await exists(dir), false, `${dir} should be removed`)
|
|
80
|
+
}
|
|
81
|
+
assert.ok(packets.some((packet) => /removed legacy path:/.test(packet.raw || '')))
|
|
82
|
+
})
|
|
83
|
+
|
|
84
|
+
test('LegacyCleanup runOnce does not repeat work in the same process', async () => {
|
|
85
|
+
const root = await fs.mkdtemp(path.join(os.tmpdir(), 'pinokio-legacy-once-'))
|
|
86
|
+
const kernel = createKernel(root)
|
|
87
|
+
const cleanup = new LegacyCleanup(kernel)
|
|
88
|
+
const first = await cleanup.runOnce(() => {})
|
|
89
|
+
|
|
90
|
+
const lateHook = path.join(root, 'bin', 'miniconda', 'etc', 'conda', 'activate.d', 'zz_pinokio_ffmpeg.bat')
|
|
91
|
+
await writeFixtureFile(lateHook)
|
|
92
|
+
const second = await cleanup.runOnce(() => {})
|
|
93
|
+
|
|
94
|
+
assert.strictEqual(second, first)
|
|
95
|
+
assert.equal(await exists(lateHook), true)
|
|
96
|
+
})
|
|
97
|
+
|
|
98
|
+
test('LegacyCleanup tells the user which stale FFmpeg hooks to delete when removal fails', async () => {
|
|
99
|
+
const root = await fs.mkdtemp(path.join(os.tmpdir(), 'pinokio-legacy-locked-'))
|
|
100
|
+
await createLegacyFfmpegState(root)
|
|
101
|
+
const kernel = createKernel(root)
|
|
102
|
+
const originalRm = fsSync.promises.rm
|
|
103
|
+
const lockedHook = path.join(root, 'bin', 'miniconda', 'etc', 'conda', 'activate.d', 'zz_pinokio_ffmpeg.bat')
|
|
104
|
+
const packets = []
|
|
105
|
+
|
|
106
|
+
fsSync.promises.rm = async (target, options) => {
|
|
107
|
+
if (target === lockedHook) {
|
|
108
|
+
const error = new Error('locked')
|
|
109
|
+
error.code = 'EPERM'
|
|
110
|
+
throw error
|
|
111
|
+
}
|
|
112
|
+
return originalRm(target, options)
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
try {
|
|
116
|
+
await assert.rejects(
|
|
117
|
+
new LegacyCleanup(kernel).runOnce((packet) => packets.push(packet)),
|
|
118
|
+
(error) => {
|
|
119
|
+
assert.equal(error.name, 'LegacyFfmpegCleanupError')
|
|
120
|
+
assert.match(error.message, /Close all Pinokio windows/)
|
|
121
|
+
assert.match(error.message, /delete these files/)
|
|
122
|
+
assert.match(error.message, /zz_pinokio_ffmpeg\.bat/)
|
|
123
|
+
assert.ok(error.failures.some((failure) => failure.target === lockedHook))
|
|
124
|
+
return true
|
|
125
|
+
}
|
|
126
|
+
)
|
|
127
|
+
assert.ok(packets.some((packet) => /could not remove legacy path:/.test(packet.raw || '')))
|
|
128
|
+
} finally {
|
|
129
|
+
fsSync.promises.rm = originalRm
|
|
130
|
+
}
|
|
131
|
+
})
|
|
132
|
+
|
|
133
|
+
test('LegacyCleanup does not block startup when only an old FFmpeg runtime directory is locked', async () => {
|
|
134
|
+
const root = await fs.mkdtemp(path.join(os.tmpdir(), 'pinokio-legacy-runtime-locked-'))
|
|
135
|
+
const runtimeDir = path.join(root, 'bin', 'ffmpeg-env')
|
|
136
|
+
await fs.mkdir(runtimeDir, { recursive: true })
|
|
137
|
+
await fs.writeFile(path.join(runtimeDir, 'marker.txt'), 'legacy\n')
|
|
138
|
+
const kernel = createKernel(root)
|
|
139
|
+
const originalRm = fsSync.promises.rm
|
|
140
|
+
const packets = []
|
|
141
|
+
|
|
142
|
+
fsSync.promises.rm = async (target, options) => {
|
|
143
|
+
if (target === runtimeDir) {
|
|
144
|
+
const error = new Error('locked')
|
|
145
|
+
error.code = 'EPERM'
|
|
146
|
+
throw error
|
|
147
|
+
}
|
|
148
|
+
return originalRm(target, options)
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
try {
|
|
152
|
+
const result = await new LegacyCleanup(kernel).runOnce((packet) => packets.push(packet))
|
|
153
|
+
assert.ok(result.results.some((cleanupResult) => {
|
|
154
|
+
return cleanupResult.name === 'ffmpeg' &&
|
|
155
|
+
cleanupResult.results.some((item) => item.target === runtimeDir && item.removed === false)
|
|
156
|
+
}))
|
|
157
|
+
assert.ok(packets.some((packet) => /could not remove legacy path:/.test(packet.raw || '')))
|
|
158
|
+
} finally {
|
|
159
|
+
fsSync.promises.rm = originalRm
|
|
160
|
+
}
|
|
161
|
+
})
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
const assert = require("node:assert/strict")
|
|
2
|
+
const os = require("node:os")
|
|
3
|
+
const test = require("node:test")
|
|
4
|
+
|
|
5
|
+
const procsPath = require.resolve("../kernel/procs")
|
|
6
|
+
|
|
7
|
+
function loadProcsForPlatform(platform) {
|
|
8
|
+
const originalPlatform = os.platform
|
|
9
|
+
delete require.cache[procsPath]
|
|
10
|
+
os.platform = () => platform
|
|
11
|
+
try {
|
|
12
|
+
return require("../kernel/procs")
|
|
13
|
+
} finally {
|
|
14
|
+
os.platform = originalPlatform
|
|
15
|
+
delete require.cache[procsPath]
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
test("Windows process parser only probes listening TCP rows", async () => {
|
|
20
|
+
const Procs = loadProcsForPlatform("win32")
|
|
21
|
+
const procs = new Procs({})
|
|
22
|
+
const probed = []
|
|
23
|
+
|
|
24
|
+
procs.isHttp = async (host, port) => {
|
|
25
|
+
probed.push(`${host}:${port}`)
|
|
26
|
+
return true
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const stdout = [
|
|
30
|
+
" Proto Local Address Foreign Address State PID",
|
|
31
|
+
" TCP 127.0.0.1:5173 0.0.0.0:0 LISTENING 1111",
|
|
32
|
+
" TCP 127.0.0.1:49153 127.0.0.1:5173 ESTABLISHED 2222",
|
|
33
|
+
" TCP 0.0.0.0:7860 0.0.0.0:0 LISTENING 3333",
|
|
34
|
+
" TCP [::1]:11434 [::]:0 LISTENING 4444"
|
|
35
|
+
].join("\n")
|
|
36
|
+
|
|
37
|
+
const results = await procs.get_pids(stdout)
|
|
38
|
+
|
|
39
|
+
assert.deepEqual(probed, [
|
|
40
|
+
"127.0.0.1:5173",
|
|
41
|
+
"0.0.0.0:7860",
|
|
42
|
+
"::1:11434"
|
|
43
|
+
])
|
|
44
|
+
assert.deepEqual(results.map((item) => `${item.pid}:${item.ip}`), [
|
|
45
|
+
"1111:127.0.0.1:5173",
|
|
46
|
+
"3333:0.0.0.0:7860",
|
|
47
|
+
"4444:[::1]:11434"
|
|
48
|
+
])
|
|
49
|
+
})
|