pinokiod 7.2.8 → 7.2.10

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.
@@ -417,6 +417,12 @@ class Api {
417
417
  await this.process(script.on.stop)
418
418
  }
419
419
  }
420
+ if (this.kernel.watch && typeof this.kernel.watch.stop === "function") {
421
+ if (req.params.id) {
422
+ await this.kernel.watch.stop(req.params.id)
423
+ }
424
+ await this.kernel.watch.stop(requestPath)
425
+ }
420
426
  // reset modules
421
427
  let modpath = this.resolvePath(cwd, req.params.uri)
422
428
  if (this.child_procs[modpath]) {
@@ -1456,6 +1462,25 @@ class Api {
1456
1462
  }
1457
1463
  return false
1458
1464
  }
1465
+ async startWatchersForRequest(request, script, scriptDir, input) {
1466
+ if (!this.kernel.watch || typeof this.kernel.watch.startForScript !== "function") {
1467
+ return
1468
+ }
1469
+ const id = request.id || request.path
1470
+ if (!id) {
1471
+ return
1472
+ }
1473
+ const cwd = request.cwd || scriptDir
1474
+ await this.kernel.watch.startForScript({
1475
+ id,
1476
+ request,
1477
+ script,
1478
+ cwd,
1479
+ dirname: scriptDir,
1480
+ input,
1481
+ args: input
1482
+ })
1483
+ }
1459
1484
  createQueue(queue_id, concurrency) {
1460
1485
  this.queues[queue_id] = fastq.promise(async ({ request, rawrpc, input, step, total, cwd, args }) => {
1461
1486
  try {
@@ -1697,6 +1722,7 @@ class Api {
1697
1722
  }
1698
1723
 
1699
1724
  const initialPayload = typeof request.input === "undefined" ? {} : request.input
1725
+ await this.startWatchersForRequest(request, script, cwd, initialPayload)
1700
1726
  this.queue(request, steps[0], initialPayload, 0, steps.length, cwd, initialPayload)
1701
1727
 
1702
1728
  } else {
@@ -3,7 +3,139 @@ const portfinder = require('portfinder-cp')
3
3
  const os = require('os')
4
4
  const fs = require('fs')
5
5
  const Util = require('./util')
6
- const platform = os.platform()
6
+ const TEMP_ENV_KEYS = ["TMP", "TEMP", "TMPDIR", "PIP_TMPDIR"]
7
+ const CACHE_ENV_KEYS = ["UV_CACHE_DIR", "PIP_CACHE_DIR"]
8
+
9
+ const envKey = (env, key) => {
10
+ return Object.keys(env).find((candidate) => candidate.toLowerCase() === key.toLowerCase())
11
+ }
12
+
13
+ const setEnv = (env, key, value) => {
14
+ for (const candidate of Object.keys(env)) {
15
+ if (candidate.toLowerCase() === key.toLowerCase() && candidate !== key) {
16
+ delete env[candidate]
17
+ }
18
+ }
19
+ env[key] = value
20
+ }
21
+
22
+ const normalizeEnvPath = (value, root) => {
23
+ if (typeof value !== "string") {
24
+ return null
25
+ }
26
+ const trimmed = value.trim()
27
+ if (!trimmed) {
28
+ return null
29
+ }
30
+ if (path.isAbsolute(trimmed)) {
31
+ return path.resolve(trimmed)
32
+ }
33
+ if (trimmed.startsWith("./") || trimmed.startsWith(".\\")) {
34
+ return path.resolve(root, trimmed)
35
+ }
36
+ return path.resolve(root, trimmed)
37
+ }
38
+
39
+ const isInsidePath = (candidate, root) => {
40
+ const rel = path.relative(path.resolve(root), path.resolve(candidate))
41
+ return rel === "" || (!!rel && !rel.startsWith("..") && !path.isAbsolute(rel))
42
+ }
43
+
44
+ const canWriteDirectory = async (dirPath) => {
45
+ if (!dirPath) {
46
+ return false
47
+ }
48
+ const testDir = path.resolve(
49
+ dirPath,
50
+ `.pinokio-write-test-${process.pid}-${Date.now()}-${Math.random().toString(16).slice(2)}`
51
+ )
52
+ const testFile = path.resolve(testDir, "probe.tmp")
53
+ const renamedFile = path.resolve(testDir, "probe-renamed.tmp")
54
+ try {
55
+ await fs.promises.mkdir(testDir, { recursive: true })
56
+ await fs.promises.writeFile(testFile, "pinokio")
57
+ await fs.promises.appendFile(testFile, "-probe")
58
+ await fs.promises.rename(testFile, renamedFile)
59
+ await fs.promises.unlink(renamedFile)
60
+ await fs.promises.rmdir(testDir)
61
+ return true
62
+ } catch (error) {
63
+ await fs.promises.rm(testDir, { recursive: true, force: true }).catch(() => {})
64
+ return false
65
+ }
66
+ }
67
+
68
+ const ensureWritableDirectory = async (dirPath, options = {}) => {
69
+ if (!dirPath) {
70
+ return false
71
+ }
72
+ const repair = !!options.repair
73
+ try {
74
+ await fs.promises.mkdir(dirPath, { recursive: true })
75
+ } catch (error) {
76
+ if (!repair) {
77
+ return false
78
+ }
79
+ await fs.promises.rm(dirPath, { recursive: true, force: true }).catch(() => {})
80
+ try {
81
+ await fs.promises.mkdir(dirPath, { recursive: true })
82
+ } catch (mkdirError) {
83
+ return false
84
+ }
85
+ }
86
+ if (await canWriteDirectory(dirPath)) {
87
+ return true
88
+ }
89
+ if (!repair) {
90
+ return false
91
+ }
92
+ await fs.promises.rm(dirPath, { recursive: true, force: true }).catch(() => {})
93
+ try {
94
+ await fs.promises.mkdir(dirPath, { recursive: true })
95
+ } catch (error) {
96
+ return false
97
+ }
98
+ return canWriteDirectory(dirPath)
99
+ }
100
+
101
+ const canRepairManagedCachePath = (candidate, cacheRoot) => {
102
+ const rel = path.relative(path.resolve(cacheRoot), path.resolve(candidate))
103
+ return !!rel && !rel.startsWith("..") && !path.isAbsolute(rel)
104
+ }
105
+
106
+ const managedCacheEnvDefaults = () => {
107
+ const defaults = {}
108
+ for (const key of TEMP_ENV_KEYS.concat(CACHE_ENV_KEYS)) {
109
+ defaults[key] = `./cache/${key}`
110
+ }
111
+ return defaults
112
+ }
113
+
114
+ const ensurePinokioCacheDirs = async (kernel) => {
115
+ if (!kernel || !kernel.homedir) {
116
+ return {}
117
+ }
118
+ const root = path.resolve(kernel.homedir)
119
+ const cacheRoot = path.resolve(root, "cache")
120
+ const envPath = path.resolve(root, "ENVIRONMENT")
121
+ const defaults = managedCacheEnvDefaults()
122
+ await Util.update_env(envPath, defaults)
123
+ const env = await get(root, kernel)
124
+
125
+ for (const key of TEMP_ENV_KEYS.concat(CACHE_ENV_KEYS)) {
126
+ const existingKey = envKey(env, key)
127
+ const dirPath = normalizeEnvPath(existingKey ? env[existingKey] : defaults[key], root)
128
+ const managedPath = path.resolve(cacheRoot, key)
129
+ const targetPath = dirPath && isInsidePath(dirPath, root) ? dirPath : managedPath
130
+ const repair = canRepairManagedCachePath(targetPath, cacheRoot)
131
+ if (!repair || !(await ensureWritableDirectory(targetPath, { repair: true }))) {
132
+ throw new Error(`Pinokio could not create a writable ${key} directory: ${targetPath}`)
133
+ }
134
+ setEnv(env, key, targetPath)
135
+ }
136
+
137
+ return env
138
+ }
7
139
  const ENVS = async () => {
8
140
  // const primary_port = 80
9
141
  // const secondary_port = 42000
@@ -828,4 +960,4 @@ const init = async (options, kernel) => {
828
960
  env_path: current
829
961
  }
830
962
  }
831
- module.exports = { ENV, get, get2, init_folders, requirements, init, get_root }
963
+ module.exports = { ENV, get, get2, init_folders, ensurePinokioCacheDirs, requirements, init, get_root }
package/kernel/index.js CHANGED
@@ -40,6 +40,7 @@ const Git = require('./git')
40
40
  const Connect = require('./connect')
41
41
  const Favicon = require('./favicon')
42
42
  const AppLauncher = require('./app_launcher')
43
+ const WatchManager = require('./watch')
43
44
  const { DownloaderHelper } = require('node-downloader-helper');
44
45
  const { ProxyAgent } = require('proxy-agent');
45
46
  const fakeUa = require('fake-useragent');
@@ -975,6 +976,7 @@ class Kernel {
975
976
  this.loader = new Loader()
976
977
  this.bin = new Bin(this)
977
978
  this.api = new Api(this)
979
+ this.watch = new WatchManager(this)
978
980
  this.python = new Python(this)
979
981
  this.shell = new Shells(this)
980
982
  this.appLauncher = new AppLauncher(this)
@@ -1051,6 +1053,7 @@ class Kernel {
1051
1053
 
1052
1054
  // 2. mkdir all the folders if not already created
1053
1055
  await Environment.init_folders(this.homedir, this)
1056
+ await Environment.ensurePinokioCacheDirs(this)
1054
1057
 
1055
1058
  // if key.json doesn't exist, create an empty json file
1056
1059
  let ee = await this.exists(this.homedir, "key.json")
@@ -0,0 +1,42 @@
1
+ const path = require("path")
2
+ const { watchFs } = require("./drivers/fs")
3
+ const { poll } = require("./drivers/poll")
4
+
5
+ class WatchContext {
6
+ constructor(options = {}) {
7
+ this.kernel = options.kernel
8
+ this.manager = options.manager
9
+ this.id = options.id
10
+ this.cwd = path.resolve(options.cwd)
11
+ this.dirname = path.resolve(options.dirname || options.cwd)
12
+ this.request = options.request
13
+ this.script = options.script
14
+ this.declaration = options.declaration
15
+ this.input = options.input || {}
16
+ this.args = options.args || this.input
17
+ this.watch = {
18
+ fs: (targetPath, callback, watchOptions = {}) => {
19
+ return watchFs(this.resolve(targetPath), callback, {
20
+ ...watchOptions,
21
+ onError: watchOptions.onError || ((error) => {
22
+ console.warn("[watch.fs]", error && error.message ? error.message : error)
23
+ })
24
+ })
25
+ }
26
+ }
27
+ }
28
+
29
+ resolve(targetPath) {
30
+ return this.kernel.api.resolvePath(this.cwd, String(targetPath || "."))
31
+ }
32
+
33
+ resolveModule(targetPath) {
34
+ return this.kernel.api.resolvePath(this.dirname, String(targetPath || "."))
35
+ }
36
+
37
+ poll(interval, callback, options = {}) {
38
+ return poll(interval, callback, options)
39
+ }
40
+ }
41
+
42
+ module.exports = WatchContext
@@ -0,0 +1,71 @@
1
+ const fs = require("fs")
2
+ const path = require("path")
3
+ const ParcelWatcher = require("@parcel/watcher")
4
+
5
+ const DEFAULT_IGNORE = [
6
+ "**/.git/**",
7
+ "**/node_modules/**",
8
+ "**/__pycache__/**",
9
+ "**/.venv/**",
10
+ "**/venv/**",
11
+ "**/env/**"
12
+ ]
13
+
14
+ function isInside(candidate, parent) {
15
+ const relative = path.relative(parent, candidate)
16
+ return relative === "" || (!!relative && !relative.startsWith("..") && !path.isAbsolute(relative))
17
+ }
18
+
19
+ async function nearestExistingDirectory(targetPath) {
20
+ let current = path.resolve(targetPath)
21
+ while (current && current !== path.dirname(current)) {
22
+ const stats = await fs.promises.stat(current).catch(() => null)
23
+ if (stats && stats.isDirectory()) {
24
+ return current
25
+ }
26
+ current = path.dirname(current)
27
+ }
28
+ return current || path.parse(path.resolve(targetPath)).root
29
+ }
30
+
31
+ async function watchFs(targetPath, callback, options = {}) {
32
+ const resolvedTarget = path.resolve(targetPath)
33
+ const targetStats = await fs.promises.stat(resolvedTarget).catch(() => null)
34
+ const watchRoot = targetStats && targetStats.isDirectory()
35
+ ? resolvedTarget
36
+ : await nearestExistingDirectory(resolvedTarget)
37
+ const filterToTarget = watchRoot !== resolvedTarget
38
+
39
+ const subscription = await ParcelWatcher.subscribe(
40
+ watchRoot,
41
+ (error, events) => {
42
+ if (error) {
43
+ if (typeof options.onError === "function") {
44
+ options.onError(error)
45
+ }
46
+ return
47
+ }
48
+ const normalizedEvents = Array.isArray(events) ? events : []
49
+ const filteredEvents = filterToTarget
50
+ ? normalizedEvents.filter((event) => event && event.path && isInside(path.resolve(event.path), resolvedTarget))
51
+ : normalizedEvents
52
+ if (filteredEvents.length === 0) {
53
+ return
54
+ }
55
+ callback(filteredEvents)
56
+ },
57
+ {
58
+ ignore: Array.isArray(options.ignore) ? options.ignore : DEFAULT_IGNORE
59
+ }
60
+ )
61
+
62
+ return async () => {
63
+ if (subscription && typeof subscription.unsubscribe === "function") {
64
+ await subscription.unsubscribe().catch(() => {})
65
+ }
66
+ }
67
+ }
68
+
69
+ module.exports = {
70
+ watchFs
71
+ }
@@ -0,0 +1,33 @@
1
+ function poll(interval, callback, options = {}) {
2
+ const delay = Math.max(100, Number(interval || options.interval || 1000))
3
+ let stopped = false
4
+ let running = false
5
+
6
+ const tick = async () => {
7
+ if (stopped || running) return
8
+ running = true
9
+ try {
10
+ await callback()
11
+ } catch (error) {
12
+ if (typeof options.onError === "function") {
13
+ options.onError(error)
14
+ }
15
+ } finally {
16
+ running = false
17
+ }
18
+ }
19
+
20
+ if (options.immediate !== false) {
21
+ setTimeout(tick, 0)
22
+ }
23
+ const timer = setInterval(tick, delay)
24
+
25
+ return async () => {
26
+ stopped = true
27
+ clearInterval(timer)
28
+ }
29
+ }
30
+
31
+ module.exports = {
32
+ poll
33
+ }
@@ -0,0 +1,158 @@
1
+ const path = require("path")
2
+ const WatchContext = require("./context")
3
+
4
+ class WatchManager {
5
+ constructor(kernel) {
6
+ this.kernel = kernel
7
+ this.handlers = new Map()
8
+ this.sessions = new Map()
9
+ }
10
+
11
+ registerHandler(name, handler) {
12
+ const normalized = typeof name === "string" ? name.trim() : ""
13
+ if (!normalized) {
14
+ throw new Error("watch handler name is required")
15
+ }
16
+ this.handlers.set(normalized, handler)
17
+ }
18
+
19
+ hasHandler(script, handlerName) {
20
+ const watches = script && Array.isArray(script.watch) ? script.watch : []
21
+ return watches.some((watch) => watch && watch.handler === handlerName)
22
+ }
23
+
24
+ renderDeclaration(raw, memory) {
25
+ let rendered = raw
26
+ let pass = 0
27
+ while (true) {
28
+ rendered = this.kernel.template.render(rendered, memory)
29
+ if (this.kernel.template.istemplate(rendered)) {
30
+ pass += 1
31
+ if (pass >= 4) break
32
+ } else {
33
+ break
34
+ }
35
+ }
36
+ return this.kernel.template.flatten(rendered)
37
+ }
38
+
39
+ buildMemory({ request, script, cwd, dirname, input, args }) {
40
+ return {
41
+ script: this.kernel.script,
42
+ input,
43
+ args,
44
+ cwd,
45
+ dirname,
46
+ uri: request.uri,
47
+ self: script,
48
+ kernel: this.kernel,
49
+ ...this.kernel.vars
50
+ }
51
+ }
52
+
53
+ async resolveExternalHandler(ctx, uri) {
54
+ const modpath = ctx.resolveModule(uri)
55
+ const loaded = await this.kernel.loader.load(modpath)
56
+ let handler = loaded && loaded.resolved
57
+ if (typeof handler === "function") {
58
+ handler = new handler()
59
+ }
60
+ return handler
61
+ }
62
+
63
+ async startForScript(options = {}) {
64
+ const script = options.script
65
+ const declarations = script && Array.isArray(script.watch) ? script.watch : []
66
+ if (declarations.length === 0) {
67
+ return
68
+ }
69
+
70
+ const id = options.id
71
+ if (!id) {
72
+ return
73
+ }
74
+ await this.stop(id)
75
+
76
+ const input = options.input || {}
77
+ const args = options.args || input
78
+ const cwd = path.resolve(options.cwd)
79
+ const dirname = path.resolve(options.dirname || options.cwd)
80
+ const memory = this.buildMemory({
81
+ request: options.request || {},
82
+ script,
83
+ cwd,
84
+ dirname,
85
+ input,
86
+ args
87
+ })
88
+ const disposers = []
89
+
90
+ for (const rawDeclaration of declarations) {
91
+ if (!rawDeclaration || typeof rawDeclaration !== "object") {
92
+ continue
93
+ }
94
+ try {
95
+ const declaration = this.renderDeclaration(rawDeclaration, memory)
96
+ const ctx = new WatchContext({
97
+ kernel: this.kernel,
98
+ manager: this,
99
+ id,
100
+ cwd,
101
+ dirname,
102
+ request: options.request,
103
+ script,
104
+ declaration,
105
+ input,
106
+ args
107
+ })
108
+ const methodName = typeof declaration.method === "string" ? declaration.method.trim() : ""
109
+ let handler = null
110
+ if (declaration.handler) {
111
+ handler = this.handlers.get(String(declaration.handler).trim())
112
+ } else if (declaration.uri) {
113
+ handler = await this.resolveExternalHandler(ctx, declaration.uri)
114
+ }
115
+ if (!handler || !methodName || typeof handler[methodName] !== "function") {
116
+ console.warn("[watch] handler not found", declaration)
117
+ continue
118
+ }
119
+ const cleanup = await handler[methodName](ctx, declaration.params || {})
120
+ if (cleanup) {
121
+ disposers.push(cleanup)
122
+ }
123
+ } catch (error) {
124
+ console.warn("[watch] failed to start", error && error.message ? error.message : error)
125
+ }
126
+ }
127
+
128
+ if (disposers.length > 0) {
129
+ this.sessions.set(id, disposers)
130
+ }
131
+ }
132
+
133
+ async stop(id) {
134
+ const normalized = typeof id === "string" ? id : ""
135
+ if (!normalized || !this.sessions.has(normalized)) {
136
+ return
137
+ }
138
+ const disposers = this.sessions.get(normalized) || []
139
+ this.sessions.delete(normalized)
140
+ for (const disposer of disposers.reverse()) {
141
+ try {
142
+ if (typeof disposer === "function") {
143
+ await disposer()
144
+ } else if (disposer && typeof disposer.stop === "function") {
145
+ await disposer.stop()
146
+ } else if (disposer && typeof disposer.dispose === "function") {
147
+ await disposer.dispose()
148
+ } else if (disposer && typeof disposer.unsubscribe === "function") {
149
+ await disposer.unsubscribe()
150
+ }
151
+ } catch (error) {
152
+ console.warn("[watch] cleanup failed", error && error.message ? error.message : error)
153
+ }
154
+ }
155
+ }
156
+ }
157
+
158
+ module.exports = WatchManager
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pinokiod",
3
- "version": "7.2.8",
3
+ "version": "7.2.10",
4
4
  "description": "",
5
5
  "main": "index.js",
6
6
  "scripts": {
@@ -0,0 +1,41 @@
1
+ const { createDraftService } = require("./service")
2
+ const registerDraftRoutes = require("./routes")
3
+ const DraftWatcher = require("./watcher")
4
+
5
+ function createDraftFeature(options = {}) {
6
+ const { app, kernel } = options
7
+ if (!app) {
8
+ throw new Error("app is required")
9
+ }
10
+ if (!kernel) {
11
+ throw new Error("kernel is required")
12
+ }
13
+
14
+ const service = createDraftService({
15
+ kernel,
16
+ taskWorkspaceLinks: options.taskWorkspaceLinks
17
+ })
18
+
19
+ registerDraftRoutes(app, {
20
+ ...options,
21
+ drafts: service
22
+ })
23
+
24
+ if (kernel.watch && typeof kernel.watch.registerHandler === "function") {
25
+ kernel.watch.registerHandler("draft", new DraftWatcher({ drafts: service }))
26
+ }
27
+
28
+ return {
29
+ service,
30
+ async start() {
31
+ await service.start()
32
+ },
33
+ async stop() {
34
+ await service.stop()
35
+ }
36
+ }
37
+ }
38
+
39
+ module.exports = {
40
+ createDraftFeature
41
+ }