Files
readest/apps/readest-app/scripts/epub-reviewer-dev.mjs
T

140 lines
3.8 KiB
JavaScript

import { spawn, spawnSync } from 'node:child_process';
import fs from 'node:fs';
import http from 'node:http';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const appRoot = path.resolve(__dirname, '..');
const toolRoot = path.join(appRoot, 'tools', 'epub-review-editor');
const reviewRoot = path.resolve(process.env.READEST_REVIEW_ROOT || path.join(appRoot, 'epub_review_sessions'));
const defaultPort = Number(process.env.READEST_REVIEW_PORT || 5177);
const venvRoot = path.join(toolRoot, '.venv');
const venvPython =
process.platform === 'win32'
? path.join(venvRoot, 'Scripts', 'python.exe')
: path.join(venvRoot, 'bin', 'python');
const run = (command, args, options = {}) =>
spawnSync(command, args, {
cwd: options.cwd,
encoding: 'utf8',
timeout: options.timeout ?? 30_000,
windowsHide: true,
});
const pythonCandidates =
process.platform === 'win32'
? [
['py', ['-3']],
['python', []],
['python3', []],
]
: [
['python3', []],
['python', []],
];
const findPython = () => {
for (const [command, args] of pythonCandidates) {
const result = run(command, [...args, '--version'], { timeout: 10_000 });
if (result.status === 0) return { command, args };
}
throw new Error('Python 3 was not found.');
};
const requestJson = (port, pathname) =>
new Promise((resolve) => {
const req = http.get({ host: '127.0.0.1', port, path: pathname, timeout: 800 }, (res) => {
let body = '';
res.setEncoding('utf8');
res.on('data', (chunk) => {
body += chunk;
});
res.on('end', () => {
try {
resolve(res.statusCode === 200 ? JSON.parse(body) : null);
} catch {
resolve(null);
}
});
});
req.on('timeout', () => {
req.destroy();
resolve(null);
});
req.on('error', () => resolve(null));
});
const findRunning = async () => {
for (let port = defaultPort; port < defaultPort + 100; port++) {
const payload = await requestJson(port, '/api/version');
if (payload?.version) return `http://localhost:${port}`;
}
return '';
};
if (!fs.existsSync(path.join(toolRoot, 'server.py'))) {
throw new Error(`Missing review editor server: ${path.join(toolRoot, 'server.py')}`);
}
const existing = await findRunning();
if (existing) {
console.log(`EPUB review editor is already running: ${existing}`);
process.exit(0);
}
if (!fs.existsSync(venvPython)) {
console.log('Creating review editor Python environment...');
const python = findPython();
const result = run(python.command, [...python.args, '-m', 'venv', venvRoot], {
cwd: appRoot,
timeout: 120_000,
});
if (result.status !== 0) {
throw new Error(result.stderr || result.stdout || 'Failed to create virtual environment.');
}
}
const flaskCheck = run(
venvPython,
['-c', 'import importlib.util, sys; sys.exit(0 if importlib.util.find_spec("flask") else 1)'],
{ cwd: toolRoot, timeout: 30_000 },
);
if (flaskCheck.status !== 0) {
console.log('Installing review editor dependencies...');
const install = run(venvPython, ['-m', 'pip', 'install', '-r', path.join(toolRoot, 'requirements.txt')], {
cwd: toolRoot,
timeout: 180_000,
});
if (install.status !== 0) {
throw new Error(install.stderr || install.stdout || 'Failed to install dependencies.');
}
}
fs.mkdirSync(reviewRoot, { recursive: true });
const child = spawn(
venvPython,
[
path.join(toolRoot, 'server.py'),
'--review-root',
reviewRoot,
'--host',
'127.0.0.1',
'--port',
String(defaultPort),
'--no-browser',
],
{
cwd: toolRoot,
stdio: 'inherit',
windowsHide: true,
},
);
child.on('exit', (code) => {
process.exit(code ?? 0);
});