retold-remote 0.0.23 → 0.0.26
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/css/retold-remote.css +343 -20
- package/docs/.nojekyll +0 -0
- package/docs/README.md +64 -12
- package/docs/_cover.md +6 -6
- package/docs/_sidebar.md +2 -0
- package/docs/_topbar.md +1 -1
- package/docs/_version.json +7 -0
- package/docs/collections.md +30 -0
- package/docs/css/docuserve.css +327 -0
- package/docs/ebook-reader.md +75 -1
- package/docs/image-explorer.md +62 -2
- package/docs/index.html +39 -0
- package/docs/retold-catalog.json +254 -0
- package/docs/retold-keyword-index.json +31216 -0
- package/docs/server-setup.md +122 -91
- package/docs/stack-launcher.md +218 -0
- package/docs/synology.md +585 -0
- package/docs/ultravisor-configuration.md +5 -5
- package/docs/ultravisor-integration.md +4 -2
- package/package.json +20 -14
- package/source/Pict-Application-RetoldRemote.js +22 -0
- package/source/RetoldRemote-ExtensionMaps.js +1 -1
- package/source/cli/RetoldRemote-Server-Setup.js +460 -7
- package/source/cli/RetoldRemote-Stack-Launcher.js +563 -0
- package/source/cli/RetoldRemote-Stack-Run.js +41 -0
- package/source/cli/commands/RetoldRemote-Command-Serve.js +129 -54
- package/source/providers/CollectionManager-AddItems.js +166 -0
- package/source/providers/Pict-Provider-GalleryNavigation.js +55 -0
- package/source/providers/Pict-Provider-OperationStatus.js +597 -0
- package/source/providers/keyboard-handlers/KeyHandler-ImageExplorer.js +20 -1
- package/source/providers/keyboard-handlers/KeyHandler-Viewer.js +23 -0
- package/source/server/RetoldRemote-AudioWaveformService.js +49 -3
- package/source/server/RetoldRemote-CollectionExportService.js +763 -0
- package/source/server/RetoldRemote-CollectionService.js +5 -0
- package/source/server/RetoldRemote-EbookService.js +218 -3
- package/source/server/RetoldRemote-ImageService.js +221 -46
- package/source/server/RetoldRemote-MediaService.js +63 -4
- package/source/server/RetoldRemote-MetadataCache.js +25 -5
- package/source/server/RetoldRemote-OperationBroadcaster.js +363 -0
- package/source/server/RetoldRemote-SubimageService.js +680 -0
- package/source/server/RetoldRemote-ToolDetector.js +50 -0
- package/source/server/RetoldRemote-UltravisorBeacon.js +18 -3
- package/source/server/RetoldRemote-UltravisorDispatcher.js +65 -491
- package/source/server/RetoldRemote-UltravisorOperations.js +133 -20
- package/source/server/RetoldRemote-VideoFrameService.js +302 -9
- package/source/views/MediaViewer-EbookViewer.js +419 -1
- package/source/views/MediaViewer-PdfViewer.js +1050 -0
- package/source/views/PictView-Remote-AudioExplorer.js +77 -1
- package/source/views/PictView-Remote-CollectionsPanel.js +213 -0
- package/source/views/PictView-Remote-Gallery.js +365 -64
- package/source/views/PictView-Remote-ImageExplorer.js +1529 -44
- package/source/views/PictView-Remote-ImageViewer.js +2 -2
- package/source/views/PictView-Remote-Layout.js +58 -0
- package/source/views/PictView-Remote-MediaViewer.js +100 -25
- package/source/views/PictView-Remote-RegionsBrowser.js +554 -0
- package/source/views/PictView-Remote-SubimagesPanel.js +353 -0
- package/source/views/PictView-Remote-TopBar.js +1 -0
- package/source/views/PictView-Remote-VideoExplorer.js +77 -1
- package/web-application/css/docuserve.css +277 -23
- package/web-application/css/retold-remote.css +343 -20
- package/web-application/docs/README.md +64 -12
- package/web-application/docs/_cover.md +6 -6
- package/web-application/docs/_sidebar.md +2 -0
- package/web-application/docs/_topbar.md +1 -1
- package/web-application/docs/collections.md +30 -0
- package/web-application/docs/ebook-reader.md +75 -1
- package/web-application/docs/image-explorer.md +62 -2
- package/web-application/docs/server-setup.md +122 -91
- package/web-application/docs/stack-launcher.md +218 -0
- package/web-application/docs/synology.md +585 -0
- package/web-application/docs/ultravisor-configuration.md +5 -5
- package/web-application/docs/ultravisor-integration.md +4 -2
- package/web-application/js/pict-docuserve.min.js +12 -12
- package/web-application/js/pict.min.js +2 -2
- package/web-application/js/pict.min.js.map +1 -1
- package/web-application/retold-remote.js +6596 -1784
- package/web-application/retold-remote.js.map +1 -1
- package/web-application/retold-remote.min.js +75 -23
- package/web-application/retold-remote.min.js.map +1 -1
|
@@ -0,0 +1,563 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Retold Remote -- Stack Launcher
|
|
3
|
+
*
|
|
4
|
+
* Spawns the full Retold stack as a unit:
|
|
5
|
+
* - Ultravisor (mesh coordinator) as a child process
|
|
6
|
+
* - Retold Remote (this process) connecting to it as a beacon
|
|
7
|
+
* - Orator-Conversion (embedded inside Retold Remote)
|
|
8
|
+
*
|
|
9
|
+
* Provides XDG-style default data paths so the stack runs sanely
|
|
10
|
+
* from anywhere without configuration.
|
|
11
|
+
*
|
|
12
|
+
* Usage:
|
|
13
|
+
* const libStackLauncher = require('./RetoldRemote-Stack-Launcher');
|
|
14
|
+
* libStackLauncher.start({ Logger: log }, (pError, pStackInfo) => { ... });
|
|
15
|
+
*
|
|
16
|
+
* @license MIT
|
|
17
|
+
*/
|
|
18
|
+
const libFs = require('fs');
|
|
19
|
+
const libPath = require('path');
|
|
20
|
+
const libOs = require('os');
|
|
21
|
+
const libHttp = require('http');
|
|
22
|
+
const libChildProcess = require('child_process');
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Resolve XDG-style data paths for the Retold stack.
|
|
26
|
+
*
|
|
27
|
+
* Uses XDG Base Directory Specification environment variables
|
|
28
|
+
* with sensible defaults under the user's home directory.
|
|
29
|
+
*
|
|
30
|
+
* @returns {object} { ConfigDir, DataDir, CacheDir, UltravisorData, UltravisorStaging, UltravisorCache, RetoldCache }
|
|
31
|
+
*/
|
|
32
|
+
function resolveStackPaths()
|
|
33
|
+
{
|
|
34
|
+
let tmpHome = libOs.homedir();
|
|
35
|
+
let tmpConfigBase = process.env.XDG_CONFIG_HOME || libPath.join(tmpHome, '.config');
|
|
36
|
+
let tmpDataBase = process.env.XDG_DATA_HOME || libPath.join(tmpHome, '.local', 'share');
|
|
37
|
+
let tmpCacheBase = process.env.XDG_CACHE_HOME || libPath.join(tmpHome, '.cache');
|
|
38
|
+
|
|
39
|
+
return {
|
|
40
|
+
ConfigDir: libPath.join(tmpConfigBase, 'retold-stack'),
|
|
41
|
+
DataDir: libPath.join(tmpDataBase, 'retold-stack'),
|
|
42
|
+
CacheDir: libPath.join(tmpCacheBase, 'retold-stack'),
|
|
43
|
+
UltravisorData: libPath.join(tmpDataBase, 'ultravisor', 'datastore'),
|
|
44
|
+
UltravisorStaging: libPath.join(tmpDataBase, 'ultravisor', 'staging'),
|
|
45
|
+
UltravisorCache: libPath.join(tmpCacheBase, 'ultravisor'),
|
|
46
|
+
RetoldCache: libPath.join(tmpCacheBase, 'retold-remote')
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Ensure a directory exists, creating it (and parents) if necessary.
|
|
52
|
+
*
|
|
53
|
+
* @param {string} pDir - Directory path
|
|
54
|
+
*/
|
|
55
|
+
function ensureDir(pDir)
|
|
56
|
+
{
|
|
57
|
+
if (!libFs.existsSync(pDir))
|
|
58
|
+
{
|
|
59
|
+
libFs.mkdirSync(pDir, { recursive: true });
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Find the absolute path to the ultravisor CLI runner script.
|
|
65
|
+
* Resolves through node's module resolution so it works no matter
|
|
66
|
+
* where retold-remote is installed.
|
|
67
|
+
*
|
|
68
|
+
* @returns {string|null} Absolute path or null if not found
|
|
69
|
+
*/
|
|
70
|
+
function resolveUltravisorBin()
|
|
71
|
+
{
|
|
72
|
+
try
|
|
73
|
+
{
|
|
74
|
+
// Resolve the package.json so we can read its bin entry
|
|
75
|
+
let tmpPackageJsonPath = require.resolve('ultravisor/package.json');
|
|
76
|
+
let tmpPackageDir = libPath.dirname(tmpPackageJsonPath);
|
|
77
|
+
let tmpPackage = JSON.parse(libFs.readFileSync(tmpPackageJsonPath, 'utf8'));
|
|
78
|
+
|
|
79
|
+
let tmpBinEntry = null;
|
|
80
|
+
if (typeof tmpPackage.bin === 'string')
|
|
81
|
+
{
|
|
82
|
+
tmpBinEntry = tmpPackage.bin;
|
|
83
|
+
}
|
|
84
|
+
else if (tmpPackage.bin && typeof tmpPackage.bin === 'object')
|
|
85
|
+
{
|
|
86
|
+
tmpBinEntry = tmpPackage.bin.ultravisor || Object.values(tmpPackage.bin)[0];
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
if (!tmpBinEntry)
|
|
90
|
+
{
|
|
91
|
+
return null;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
return libPath.resolve(tmpPackageDir, tmpBinEntry);
|
|
95
|
+
}
|
|
96
|
+
catch (pError)
|
|
97
|
+
{
|
|
98
|
+
return null;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Check whether a TCP port is accepting connections.
|
|
104
|
+
*
|
|
105
|
+
* @param {number} pPort - Port to test
|
|
106
|
+
* @param {string} pHost - Host to test (default localhost)
|
|
107
|
+
* @param {Function} fCallback - Callback(pIsOpen)
|
|
108
|
+
*/
|
|
109
|
+
function checkPortOpen(pPort, pHost, fCallback)
|
|
110
|
+
{
|
|
111
|
+
let tmpRequest = libHttp.get(
|
|
112
|
+
{
|
|
113
|
+
host: pHost || '127.0.0.1',
|
|
114
|
+
port: pPort,
|
|
115
|
+
path: '/',
|
|
116
|
+
timeout: 1000
|
|
117
|
+
},
|
|
118
|
+
(pResponse) =>
|
|
119
|
+
{
|
|
120
|
+
// Any HTTP response means the port is open
|
|
121
|
+
pResponse.resume();
|
|
122
|
+
fCallback(true);
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
tmpRequest.on('error', () => fCallback(false));
|
|
126
|
+
tmpRequest.on('timeout', () =>
|
|
127
|
+
{
|
|
128
|
+
tmpRequest.destroy();
|
|
129
|
+
fCallback(false);
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Wait for ultravisor to be ready by polling its HTTP port.
|
|
135
|
+
*
|
|
136
|
+
* @param {number} pPort - Port to poll
|
|
137
|
+
* @param {number} pTimeoutMs - Total wait timeout in milliseconds
|
|
138
|
+
* @param {Function} fCallback - Callback(pError) — pError null if ready
|
|
139
|
+
*/
|
|
140
|
+
function waitForUltravisor(pPort, pTimeoutMs, fCallback)
|
|
141
|
+
{
|
|
142
|
+
let tmpStart = Date.now();
|
|
143
|
+
let tmpAttempts = 0;
|
|
144
|
+
|
|
145
|
+
let _attempt = () =>
|
|
146
|
+
{
|
|
147
|
+
tmpAttempts++;
|
|
148
|
+
checkPortOpen(pPort, '127.0.0.1', (pIsOpen) =>
|
|
149
|
+
{
|
|
150
|
+
if (pIsOpen)
|
|
151
|
+
{
|
|
152
|
+
return fCallback(null, tmpAttempts);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
if (Date.now() - tmpStart > pTimeoutMs)
|
|
156
|
+
{
|
|
157
|
+
return fCallback(new Error(`Ultravisor did not become ready within ${pTimeoutMs}ms`));
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
setTimeout(_attempt, 500);
|
|
161
|
+
});
|
|
162
|
+
};
|
|
163
|
+
|
|
164
|
+
// Initial delay so we don't poll before the process has started
|
|
165
|
+
setTimeout(_attempt, 750);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Spawn ultravisor as a child process with sane defaults.
|
|
170
|
+
*
|
|
171
|
+
* Writes a temporary config file with the user-specified data paths,
|
|
172
|
+
* then launches `node ultravisor-bin start -c <config>`.
|
|
173
|
+
*
|
|
174
|
+
* @param {object} pOptions - { Port, DataPath, StagingPath, ConfigDir, Logger }
|
|
175
|
+
* @param {Function} fCallback - Callback(pError, pChildProcess)
|
|
176
|
+
*/
|
|
177
|
+
function spawnUltravisor(pOptions, fCallback)
|
|
178
|
+
{
|
|
179
|
+
let tmpLog = pOptions.Logger || console;
|
|
180
|
+
let tmpUltravisorBin = resolveUltravisorBin();
|
|
181
|
+
|
|
182
|
+
if (!tmpUltravisorBin)
|
|
183
|
+
{
|
|
184
|
+
return fCallback(new Error('Could not locate the ultravisor package. Run `npm install ultravisor` in retold-remote.'));
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
if (!libFs.existsSync(tmpUltravisorBin))
|
|
188
|
+
{
|
|
189
|
+
return fCallback(new Error(`Ultravisor binary not found at ${tmpUltravisorBin}`));
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
ensureDir(pOptions.ConfigDir);
|
|
193
|
+
ensureDir(pOptions.DataPath);
|
|
194
|
+
ensureDir(pOptions.StagingPath);
|
|
195
|
+
|
|
196
|
+
// Write a config file pointing at the user-specified paths
|
|
197
|
+
let tmpConfig =
|
|
198
|
+
{
|
|
199
|
+
UltravisorAPIServerPort: pOptions.Port,
|
|
200
|
+
UltravisorFileStorePath: pOptions.DataPath,
|
|
201
|
+
UltravisorStagingRoot: pOptions.StagingPath
|
|
202
|
+
};
|
|
203
|
+
|
|
204
|
+
let tmpConfigPath = libPath.join(pOptions.ConfigDir, 'ultravisor-stack.json');
|
|
205
|
+
libFs.writeFileSync(tmpConfigPath, JSON.stringify(tmpConfig, null, '\t'));
|
|
206
|
+
|
|
207
|
+
tmpLog.info(`[stack] launching ultravisor (port ${pOptions.Port})`);
|
|
208
|
+
tmpLog.info(`[stack] data: ${pOptions.DataPath}`);
|
|
209
|
+
tmpLog.info(`[stack] staging: ${pOptions.StagingPath}`);
|
|
210
|
+
|
|
211
|
+
let tmpChild = libChildProcess.spawn(
|
|
212
|
+
process.execPath,
|
|
213
|
+
[tmpUltravisorBin, 'start', '-c', tmpConfigPath],
|
|
214
|
+
{
|
|
215
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
216
|
+
detached: false
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
// Stream child output with a prefix for clarity
|
|
220
|
+
tmpChild.stdout.on('data', (pChunk) =>
|
|
221
|
+
{
|
|
222
|
+
let tmpLines = pChunk.toString().split('\n');
|
|
223
|
+
for (let i = 0; i < tmpLines.length; i++)
|
|
224
|
+
{
|
|
225
|
+
if (tmpLines[i].length > 0)
|
|
226
|
+
{
|
|
227
|
+
tmpLog.info('[ultravisor] ' + tmpLines[i]);
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
tmpChild.stderr.on('data', (pChunk) =>
|
|
233
|
+
{
|
|
234
|
+
let tmpLines = pChunk.toString().split('\n');
|
|
235
|
+
for (let i = 0; i < tmpLines.length; i++)
|
|
236
|
+
{
|
|
237
|
+
if (tmpLines[i].length > 0)
|
|
238
|
+
{
|
|
239
|
+
tmpLog.warn('[ultravisor] ' + tmpLines[i]);
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
tmpChild.on('exit', (pCode, pSignal) =>
|
|
245
|
+
{
|
|
246
|
+
if (pCode !== 0 && pCode !== null)
|
|
247
|
+
{
|
|
248
|
+
tmpLog.warn(`[stack] ultravisor exited with code ${pCode}`);
|
|
249
|
+
}
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
tmpChild.on('error', (pError) =>
|
|
253
|
+
{
|
|
254
|
+
tmpLog.error(`[stack] ultravisor failed to launch: ${pError.message}`);
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
return fCallback(null, tmpChild);
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
/**
|
|
261
|
+
* Detect emulation (QEMU user-mode, Rosetta-on-Docker-Desktop, etc.)
|
|
262
|
+
* which kills performance for native code (sharp/libvips, ffmpeg,
|
|
263
|
+
* ImageMagick, LibreOffice). Logs a loud warning if detected so users
|
|
264
|
+
* know to rebuild for the right architecture.
|
|
265
|
+
*
|
|
266
|
+
* Detection signals (any one of these triggers the warning):
|
|
267
|
+
* - /proc/cpuinfo contains "qemu" or "VirtualApple" (Docker Desktop on Mac)
|
|
268
|
+
* - /proc/cpuinfo vendor_id is anything other than the expected native vendor
|
|
269
|
+
* - /proc/version mentions an arch different from process.arch
|
|
270
|
+
* - A binfmt_misc qemu handler is registered AND we have a mismatch signal
|
|
271
|
+
* - A short native CPU loop runs at significantly less than expected speed
|
|
272
|
+
*
|
|
273
|
+
* @param {object} pLog - Logger
|
|
274
|
+
*/
|
|
275
|
+
function checkQemuEmulation(pLog)
|
|
276
|
+
{
|
|
277
|
+
try
|
|
278
|
+
{
|
|
279
|
+
let tmpNodeArch = process.arch;
|
|
280
|
+
let tmpEmulated = false;
|
|
281
|
+
let tmpReason = '';
|
|
282
|
+
let tmpCpuModel = null;
|
|
283
|
+
let tmpCpuVendor = null;
|
|
284
|
+
|
|
285
|
+
// 1. /proc/cpuinfo — most reliable signal
|
|
286
|
+
if (libFs.existsSync('/proc/cpuinfo'))
|
|
287
|
+
{
|
|
288
|
+
try
|
|
289
|
+
{
|
|
290
|
+
let tmpCpuInfo = libFs.readFileSync('/proc/cpuinfo', 'utf8');
|
|
291
|
+
|
|
292
|
+
// QEMU user-mode emulation often leaves the string "qemu" in cpuinfo
|
|
293
|
+
if (/qemu/i.test(tmpCpuInfo))
|
|
294
|
+
{
|
|
295
|
+
tmpEmulated = true;
|
|
296
|
+
tmpReason = '/proc/cpuinfo contains "qemu"';
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
// Docker Desktop on Apple Silicon emulating x86_64 reports vendor_id
|
|
300
|
+
// as "VirtualApple" instead of "GenuineIntel" or "AuthenticAMD"
|
|
301
|
+
let tmpVendorMatch = tmpCpuInfo.match(/^vendor_id\s*:\s*(.+)$/m);
|
|
302
|
+
if (tmpVendorMatch)
|
|
303
|
+
{
|
|
304
|
+
tmpCpuVendor = tmpVendorMatch[1].trim();
|
|
305
|
+
if (tmpNodeArch === 'x64' && tmpCpuVendor === 'VirtualApple')
|
|
306
|
+
{
|
|
307
|
+
tmpEmulated = true;
|
|
308
|
+
tmpReason = 'x86_64 binary on Apple Silicon (VirtualApple vendor)';
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
// Extract the model line for diagnostics
|
|
313
|
+
let tmpModelMatch = tmpCpuInfo.match(/^model name\s*:\s*(.+)$/m);
|
|
314
|
+
if (tmpModelMatch)
|
|
315
|
+
{
|
|
316
|
+
tmpCpuModel = tmpModelMatch[1].trim();
|
|
317
|
+
if (/qemu/i.test(tmpCpuModel) || /VirtualApple/i.test(tmpCpuModel))
|
|
318
|
+
{
|
|
319
|
+
tmpEmulated = true;
|
|
320
|
+
if (!tmpReason) tmpReason = 'CPU model name indicates emulation';
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
catch (pError)
|
|
325
|
+
{
|
|
326
|
+
// ignore
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
// 2. /proc/version — Linux kernel build banner
|
|
331
|
+
if (!tmpEmulated && libFs.existsSync('/proc/version'))
|
|
332
|
+
{
|
|
333
|
+
try
|
|
334
|
+
{
|
|
335
|
+
let tmpProcVersion = libFs.readFileSync('/proc/version', 'utf8');
|
|
336
|
+
let tmpVersionLower = tmpProcVersion.toLowerCase();
|
|
337
|
+
// If node says arm64 but the kernel banner says x86_64 (or vice versa)
|
|
338
|
+
// we're definitely emulated.
|
|
339
|
+
if (tmpNodeArch === 'arm64' && tmpVersionLower.indexOf('x86_64') >= 0)
|
|
340
|
+
{
|
|
341
|
+
tmpEmulated = true;
|
|
342
|
+
tmpReason = 'arm64 binary, x86_64 kernel';
|
|
343
|
+
}
|
|
344
|
+
if (tmpNodeArch === 'x64' && tmpVersionLower.indexOf('aarch64') >= 0)
|
|
345
|
+
{
|
|
346
|
+
tmpEmulated = true;
|
|
347
|
+
tmpReason = 'x86_64 binary, aarch64 kernel';
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
catch (pError)
|
|
351
|
+
{
|
|
352
|
+
// ignore
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
// 3. Performance heuristic — a tight CPU loop. Native arm64/amd64 should
|
|
357
|
+
// finish 10M trivial integer ops in <100ms. Under emulation it's 5-20x slower.
|
|
358
|
+
// We only run this if we haven't already decided we're emulated, and we keep
|
|
359
|
+
// it small enough not to slow startup noticeably.
|
|
360
|
+
if (!tmpEmulated)
|
|
361
|
+
{
|
|
362
|
+
let tmpLoopStart = Date.now();
|
|
363
|
+
let tmpAccumulator = 0;
|
|
364
|
+
for (let i = 0; i < 10000000; i++)
|
|
365
|
+
{
|
|
366
|
+
tmpAccumulator += i;
|
|
367
|
+
}
|
|
368
|
+
let tmpLoopMs = Date.now() - tmpLoopStart;
|
|
369
|
+
// Native: typically 30-80ms. Emulated: typically 250-900ms.
|
|
370
|
+
// Use 250ms as the threshold — generous enough to avoid false positives
|
|
371
|
+
// on slow native NAS CPUs but still catches QEMU.
|
|
372
|
+
if (tmpLoopMs > 250)
|
|
373
|
+
{
|
|
374
|
+
tmpEmulated = true;
|
|
375
|
+
tmpReason = 'native CPU loop took ' + tmpLoopMs + 'ms (expected < 250ms — likely emulated)';
|
|
376
|
+
}
|
|
377
|
+
else
|
|
378
|
+
{
|
|
379
|
+
pLog.info('[stack] CPU loop self-test: ' + tmpLoopMs + 'ms (healthy)');
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
if (tmpEmulated)
|
|
384
|
+
{
|
|
385
|
+
pLog.warn('==========================================================');
|
|
386
|
+
pLog.warn(' WARNING: container is running under emulation!');
|
|
387
|
+
pLog.warn('==========================================================');
|
|
388
|
+
pLog.warn(' Reason: ' + tmpReason);
|
|
389
|
+
pLog.warn(' Node arch: ' + tmpNodeArch);
|
|
390
|
+
if (tmpCpuVendor)
|
|
391
|
+
{
|
|
392
|
+
pLog.warn(' CPU vendor: ' + tmpCpuVendor);
|
|
393
|
+
}
|
|
394
|
+
if (tmpCpuModel)
|
|
395
|
+
{
|
|
396
|
+
pLog.warn(' CPU model: ' + tmpCpuModel);
|
|
397
|
+
}
|
|
398
|
+
pLog.warn('');
|
|
399
|
+
pLog.warn(' Emulation is extremely slow for native code:');
|
|
400
|
+
pLog.warn(' - sharp / libvips (image processing)');
|
|
401
|
+
pLog.warn(' - ffmpeg / ffprobe (video and audio)');
|
|
402
|
+
pLog.warn(' - ImageMagick (image fallback)');
|
|
403
|
+
pLog.warn(' - LibreOffice (document conversion)');
|
|
404
|
+
pLog.warn(' - Calibre (ebook conversion)');
|
|
405
|
+
pLog.warn('');
|
|
406
|
+
pLog.warn(' Symptoms you may see:');
|
|
407
|
+
pLog.warn(' - Image previews/thumbnails take many seconds');
|
|
408
|
+
pLog.warn(' - Video frame extraction times out');
|
|
409
|
+
pLog.warn(' - Document conversion fails or hangs');
|
|
410
|
+
pLog.warn(' - Random crashes in native modules');
|
|
411
|
+
pLog.warn('');
|
|
412
|
+
pLog.warn(' FIX: rebuild the image for the host architecture.');
|
|
413
|
+
pLog.warn(' On the build machine:');
|
|
414
|
+
pLog.warn(' ./docker-build-and-save.sh --amd64 # for Intel/AMD hosts');
|
|
415
|
+
pLog.warn(' ./docker-build-and-save.sh --arm64 # for ARM hosts');
|
|
416
|
+
pLog.warn(' Then transfer and load the new tar.gz.');
|
|
417
|
+
pLog.warn('==========================================================');
|
|
418
|
+
}
|
|
419
|
+
else
|
|
420
|
+
{
|
|
421
|
+
pLog.info('[stack] arch: ' + tmpNodeArch + ' (native, no emulation detected)');
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
catch (pError)
|
|
425
|
+
{
|
|
426
|
+
// Detection itself failed — not critical, just skip
|
|
427
|
+
pLog.warn('[stack] arch detection failed: ' + pError.message);
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
/**
|
|
432
|
+
* Start the full stack: spawn ultravisor as a child process, wait for
|
|
433
|
+
* it to be ready, then return so the caller can start retold-remote.
|
|
434
|
+
*
|
|
435
|
+
* @param {object} pOptions
|
|
436
|
+
* @param {object} pOptions.Logger - A fable-style logger ({ info, warn, error })
|
|
437
|
+
* @param {number} [pOptions.UltravisorPort=54321] - Port for ultravisor
|
|
438
|
+
* @param {string} [pOptions.DataPath] - Override ultravisor data path
|
|
439
|
+
* @param {string} [pOptions.StagingPath] - Override ultravisor staging path
|
|
440
|
+
* @param {string} [pOptions.ConfigDir] - Override ultravisor config dir
|
|
441
|
+
* @param {Function} fCallback - Callback(pError, pStackInfo)
|
|
442
|
+
* pStackInfo: { UltravisorURL, UltravisorChild, UltravisorPort, Paths }
|
|
443
|
+
*/
|
|
444
|
+
function start(pOptions, fCallback)
|
|
445
|
+
{
|
|
446
|
+
let tmpLog = pOptions.Logger || console;
|
|
447
|
+
let tmpPaths = resolveStackPaths();
|
|
448
|
+
|
|
449
|
+
let tmpPort = pOptions.UltravisorPort || 54321;
|
|
450
|
+
let tmpDataPath = pOptions.DataPath || tmpPaths.UltravisorData;
|
|
451
|
+
let tmpStagingPath = pOptions.StagingPath || tmpPaths.UltravisorStaging;
|
|
452
|
+
let tmpConfigDir = pOptions.ConfigDir || tmpPaths.ConfigDir;
|
|
453
|
+
|
|
454
|
+
tmpLog.info('==========================================================');
|
|
455
|
+
tmpLog.info(' Retold Stack Launcher');
|
|
456
|
+
tmpLog.info('==========================================================');
|
|
457
|
+
|
|
458
|
+
// QEMU emulation detection: if /proc/sys/kernel/osrelease contains "linuxkit"
|
|
459
|
+
// or the binfmt entries indicate qemu, native operations will be slow.
|
|
460
|
+
// Cross-architecture binaries running under QEMU show up here too.
|
|
461
|
+
checkQemuEmulation(tmpLog);
|
|
462
|
+
|
|
463
|
+
// Check if ultravisor is already running on the target port
|
|
464
|
+
checkPortOpen(tmpPort, '127.0.0.1', (pAlreadyRunning) =>
|
|
465
|
+
{
|
|
466
|
+
if (pAlreadyRunning)
|
|
467
|
+
{
|
|
468
|
+
tmpLog.info(`[stack] ultravisor already running on port ${tmpPort}, reusing`);
|
|
469
|
+
return fCallback(null,
|
|
470
|
+
{
|
|
471
|
+
UltravisorURL: 'http://localhost:' + tmpPort,
|
|
472
|
+
UltravisorChild: null,
|
|
473
|
+
UltravisorPort: tmpPort,
|
|
474
|
+
Paths: tmpPaths,
|
|
475
|
+
AlreadyRunning: true
|
|
476
|
+
});
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
// Spawn ultravisor as a child process
|
|
480
|
+
spawnUltravisor(
|
|
481
|
+
{
|
|
482
|
+
Port: tmpPort,
|
|
483
|
+
DataPath: tmpDataPath,
|
|
484
|
+
StagingPath: tmpStagingPath,
|
|
485
|
+
ConfigDir: tmpConfigDir,
|
|
486
|
+
Logger: tmpLog
|
|
487
|
+
},
|
|
488
|
+
(pSpawnError, pChild) =>
|
|
489
|
+
{
|
|
490
|
+
if (pSpawnError)
|
|
491
|
+
{
|
|
492
|
+
return fCallback(pSpawnError);
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
// Wait for ultravisor to start accepting connections
|
|
496
|
+
waitForUltravisor(tmpPort, 30000, (pWaitError, pAttempts) =>
|
|
497
|
+
{
|
|
498
|
+
if (pWaitError)
|
|
499
|
+
{
|
|
500
|
+
try { pChild.kill(); } catch (e) { /* ignore */ }
|
|
501
|
+
return fCallback(pWaitError);
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
tmpLog.info(`[stack] ultravisor ready (after ${pAttempts} attempts)`);
|
|
505
|
+
|
|
506
|
+
return fCallback(null,
|
|
507
|
+
{
|
|
508
|
+
UltravisorURL: 'http://localhost:' + tmpPort,
|
|
509
|
+
UltravisorChild: pChild,
|
|
510
|
+
UltravisorPort: tmpPort,
|
|
511
|
+
Paths: tmpPaths,
|
|
512
|
+
AlreadyRunning: false
|
|
513
|
+
});
|
|
514
|
+
});
|
|
515
|
+
});
|
|
516
|
+
});
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
/**
|
|
520
|
+
* Stop the stack — kill the ultravisor child process if we spawned it.
|
|
521
|
+
*
|
|
522
|
+
* @param {object} pStackInfo - The object returned from start()
|
|
523
|
+
* @param {Function} fCallback - Callback() called after shutdown
|
|
524
|
+
*/
|
|
525
|
+
function stop(pStackInfo, fCallback)
|
|
526
|
+
{
|
|
527
|
+
if (pStackInfo && pStackInfo.UltravisorChild && !pStackInfo.AlreadyRunning)
|
|
528
|
+
{
|
|
529
|
+
try
|
|
530
|
+
{
|
|
531
|
+
pStackInfo.UltravisorChild.kill('SIGTERM');
|
|
532
|
+
}
|
|
533
|
+
catch (pError)
|
|
534
|
+
{
|
|
535
|
+
// ignore
|
|
536
|
+
}
|
|
537
|
+
// Give it a moment to exit gracefully
|
|
538
|
+
setTimeout(() =>
|
|
539
|
+
{
|
|
540
|
+
try
|
|
541
|
+
{
|
|
542
|
+
pStackInfo.UltravisorChild.kill('SIGKILL');
|
|
543
|
+
}
|
|
544
|
+
catch (pError)
|
|
545
|
+
{
|
|
546
|
+
// ignore — already gone
|
|
547
|
+
}
|
|
548
|
+
if (fCallback) fCallback();
|
|
549
|
+
}, 1000);
|
|
550
|
+
}
|
|
551
|
+
else if (fCallback)
|
|
552
|
+
{
|
|
553
|
+
fCallback();
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
module.exports =
|
|
558
|
+
{
|
|
559
|
+
resolveStackPaths: resolveStackPaths,
|
|
560
|
+
resolveUltravisorBin: resolveUltravisorBin,
|
|
561
|
+
start: start,
|
|
562
|
+
stop: stop
|
|
563
|
+
};
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Retold Stack — convenience entry point.
|
|
4
|
+
*
|
|
5
|
+
* Equivalent to running: retold-remote serve --stack [args...]
|
|
6
|
+
*
|
|
7
|
+
* Spawns Ultravisor as a child process, embeds Orator-Conversion,
|
|
8
|
+
* and starts the Retold Remote media browser pointed at the supplied
|
|
9
|
+
* directory (or the current working directory by default).
|
|
10
|
+
*
|
|
11
|
+
* Data paths default to XDG-style locations:
|
|
12
|
+
* ~/.local/share/ultravisor/ — Ultravisor datastore + staging
|
|
13
|
+
* ~/.cache/retold-remote/ — Retold Remote cache
|
|
14
|
+
* ~/.config/retold-stack/ — Stack config files
|
|
15
|
+
*
|
|
16
|
+
* @license MIT
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
// Inject 'serve --stack' as the first arguments if the user did not
|
|
20
|
+
// already specify a subcommand. This makes `retold-stack /some/path`
|
|
21
|
+
// equivalent to `retold-remote serve --stack /some/path`.
|
|
22
|
+
let tmpArgs = process.argv.slice(2);
|
|
23
|
+
|
|
24
|
+
// Detect whether the user already passed a known subcommand
|
|
25
|
+
let tmpKnownCommands = { 'serve': true };
|
|
26
|
+
let tmpHasSubcommand = tmpArgs.length > 0 && tmpKnownCommands[tmpArgs[0]];
|
|
27
|
+
|
|
28
|
+
if (!tmpHasSubcommand)
|
|
29
|
+
{
|
|
30
|
+
tmpArgs = ['serve', '--stack'].concat(tmpArgs);
|
|
31
|
+
}
|
|
32
|
+
else if (tmpArgs.indexOf('--stack') === -1)
|
|
33
|
+
{
|
|
34
|
+
// User passed `retold-stack serve <path>` — append --stack
|
|
35
|
+
tmpArgs.splice(1, 0, '--stack');
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
process.argv = [process.argv[0], process.argv[1]].concat(tmpArgs);
|
|
39
|
+
|
|
40
|
+
const libRetoldRemoteProgram = require('./RetoldRemote-CLI-Program.js');
|
|
41
|
+
libRetoldRemoteProgram.run();
|