mirror of
https://github.com/whyour/qinglong.git
synced 2026-11-05 11:09:57 +08:00
Translate UI, scheduler, validation, preload and maintenance messages dynamically, and propagate locale changes between workers. Harden update checks, downloads and terminal states, verify Debian pnpm, and ignore generated test artifacts.
328 lines
12 KiB
TypeScript
328 lines
12 KiB
TypeScript
// Runtime language adapters derived from the retained panel preloaders.
|
|
// Keep these sources readable: the build embeds them without extra runtime files.
|
|
export const nodePreloadSource: string = [
|
|
"const { execFileSync } = require('child_process');",
|
|
"const Module = require('module');",
|
|
"const path = require('path');",
|
|
"const client = require('./client.js');",
|
|
'require(`./env.js`);',
|
|
'',
|
|
'// 注册 ESM loader,使全局安装的包也可通过 import 导入',
|
|
'try {',
|
|
" Module.register(require('node:url').pathToFileURL(path.join(__dirname, 'esm-loader.mjs')).href);",
|
|
'} catch (_) {}',
|
|
'',
|
|
'function preferGlobalNodeModules() {',
|
|
' const { QL_NODE_GLOBAL_PATH } = process.env;',
|
|
' if (!QL_NODE_GLOBAL_PATH || Module._qlGlobalPathPatched) {',
|
|
' return;',
|
|
' }',
|
|
'',
|
|
' const originalResolveFilename = Module._resolveFilename;',
|
|
' Module._resolveFilename = function (request, parent, isMain, options) {',
|
|
' if (',
|
|
' !Module.builtinModules.includes(request) &&',
|
|
" !request.startsWith('node:') &&",
|
|
" !request.startsWith('.') &&",
|
|
' !path.isAbsolute(request)',
|
|
' ) {',
|
|
' try {',
|
|
' return originalResolveFilename.call(this, request, parent, isMain, {',
|
|
' ...options,',
|
|
' paths: [QL_NODE_GLOBAL_PATH],',
|
|
' });',
|
|
' } catch (error) {}',
|
|
' }',
|
|
'',
|
|
' return originalResolveFilename.call(this, request, parent, isMain, options);',
|
|
' };',
|
|
' Module._qlGlobalPathPatched = true;',
|
|
'}',
|
|
'',
|
|
'function expandRange(rangeStr, max) {',
|
|
' const tempRangeStr = rangeStr',
|
|
' .trim()',
|
|
' .replace(/-max/g, `-${max}`)',
|
|
' .replace(/max-/g, `${max}-`);',
|
|
'',
|
|
" return tempRangeStr.split(' ').flatMap((part) => {",
|
|
' const rangeMatch = part.match(/^(\\d+)([-~_])(\\d+)$/);',
|
|
' if (rangeMatch) {',
|
|
' const [, start, , end] = rangeMatch.map(Number);',
|
|
' const step = start < end ? 1 : -1;',
|
|
' return Array.from(',
|
|
' { length: Math.abs(end - start) + 1 },',
|
|
' (_, i) => start + i * step,',
|
|
' );',
|
|
' }',
|
|
' return Number(part);',
|
|
' });',
|
|
'}',
|
|
'',
|
|
'function run() {',
|
|
' const {',
|
|
' envParam,',
|
|
' numParam,',
|
|
' file_task_before,',
|
|
' file_task_before_js,',
|
|
' dir_scripts,',
|
|
' task_before,',
|
|
' PREV_NODE_OPTIONS,',
|
|
' } = process.env;',
|
|
'',
|
|
' try {',
|
|
' process.env.NODE_OPTIONS = PREV_NODE_OPTIONS;',
|
|
'',
|
|
" const splitStr = '__sitecustomize__';",
|
|
" const fileName = process.argv[1].replace(`${dir_scripts}/`, '');",
|
|
' const tempFile = `/tmp/env_${process.pid}.json`;',
|
|
'',
|
|
' const commands = `file="$1"; output="$2"; inline="$3"; shift 3',
|
|
'source "$file" "$@" &&',
|
|
'{ if [ -n "$inline" ]; then eval "$inline"; fi; } &&',
|
|
"printf '%s\\\\n' '__sitecustomize__' &&",
|
|
'node -e \'require("fs").writeFileSync(process.argv[1], JSON.stringify(process.env))\' "$output"`;',
|
|
" if (task_before) console.log(process.env.QL_LANG === 'en' ? 'Executing before command\\n' : '执行前置命令\\n');",
|
|
' const hookArgs = JSON.parse(process.env.QL_CLI_PRELOAD_ARGS || JSON.stringify([fileName]));',
|
|
" const res = execFileSync('/bin/bash', ['-c', commands, 'ql-before', file_task_before, tempFile, task_before || '', ...hookArgs], {",
|
|
" encoding: 'utf-8', maxBuffer: 50 * 1024 * 1024,",
|
|
' });',
|
|
'',
|
|
' const [output] = res.split(splitStr);',
|
|
'',
|
|
' try {',
|
|
" const envStr = require('fs').readFileSync(tempFile, 'utf-8');",
|
|
' const newEnvObject = JSON.parse(envStr);',
|
|
" if (typeof newEnvObject === 'object' && newEnvObject !== null) {",
|
|
' for (const key in newEnvObject) {',
|
|
' if (Object.prototype.hasOwnProperty.call(newEnvObject, key)) {',
|
|
' process.env[key] = newEnvObject[key];',
|
|
' }',
|
|
' }',
|
|
' }',
|
|
" require('fs').unlinkSync(tempFile);",
|
|
' } catch (jsonError) {',
|
|
' console.log(',
|
|
" '\\ue926 Failed to parse environment variables:',",
|
|
' jsonError.message,',
|
|
' );',
|
|
' try {',
|
|
" require('fs').unlinkSync(tempFile);",
|
|
' } catch (e) {}',
|
|
' }',
|
|
'',
|
|
' if (output) {',
|
|
' console.log(output);',
|
|
' }',
|
|
' if (task_before) {',
|
|
" console.log(process.env.QL_LANG === 'en' ? 'Before command finished\\n' : '执行前置命令结束\\n');",
|
|
' }',
|
|
' } catch (error) {',
|
|
" if (!error.message.includes('spawnSync /bin/bash E2BIG')) {",
|
|
' console.log(`\\ue926 run task before error: `, error);',
|
|
' } else {',
|
|
' // environment variable is too large',
|
|
' }',
|
|
' if (task_before) {',
|
|
" console.log(process.env.QL_LANG === 'en' ? 'Before command finished\\n' : '执行前置命令结束\\n');",
|
|
' }',
|
|
' }',
|
|
'',
|
|
' require(file_task_before_js);',
|
|
'',
|
|
' if (envParam && numParam) {',
|
|
" const array = (process.env[envParam] || '').split('&');",
|
|
' const runArr = expandRange(numParam, array.length);',
|
|
' const arrayRun = runArr.map((i) => array[i - 1]);',
|
|
" const envStr = arrayRun.join('&');",
|
|
' process.env[envParam] = envStr;',
|
|
' }',
|
|
'}',
|
|
'',
|
|
'try {',
|
|
' if (!process.argv[1]) {',
|
|
' return;',
|
|
' }',
|
|
'',
|
|
' preferGlobalNodeModules();',
|
|
'',
|
|
" process.on('SIGTERM', (code) => {",
|
|
' process.exit(15);',
|
|
' });',
|
|
'',
|
|
' run();',
|
|
'',
|
|
" const { sendNotify } = require('./__ql_notify__.js');",
|
|
' global.QLAPI = {',
|
|
' notify: sendNotify,',
|
|
' ...client,',
|
|
' };',
|
|
'} catch (error) {',
|
|
" console.log(`run builtin code error: `, error, '\\n');",
|
|
'}',
|
|
'',
|
|
].join('\n');
|
|
export const pythonPreloadSource: string = [
|
|
'import os',
|
|
'import re',
|
|
'import subprocess',
|
|
'import json',
|
|
'import builtins',
|
|
'import sys',
|
|
'import env',
|
|
'import signal',
|
|
'from client import Client',
|
|
'',
|
|
'',
|
|
'def try_parse_int(value):',
|
|
' try:',
|
|
' return int(value)',
|
|
' except ValueError:',
|
|
' return None',
|
|
'',
|
|
'',
|
|
'def expand_range(range_str, max_value):',
|
|
' temp_range_str = (',
|
|
' range_str.strip()',
|
|
' .replace("-max", f"-{max_value}")',
|
|
' .replace("max-", f"{max_value}-")',
|
|
' )',
|
|
'',
|
|
' result = []',
|
|
' for part in temp_range_str.split(" "):',
|
|
' range_match = re.match(r"^(\\d+)([-~_])(\\d+)$", part)',
|
|
' if range_match:',
|
|
' start, _, end = map(try_parse_int, range_match.groups())',
|
|
' step = 1 if start < end else -1',
|
|
' result.extend(range(start, end + step, step))',
|
|
' else:',
|
|
' result.append(int(part))',
|
|
'',
|
|
' return result',
|
|
'',
|
|
'',
|
|
'def run():',
|
|
' try:',
|
|
' prev_pythonpath = os.getenv("PREV_PYTHONPATH", "")',
|
|
' os.environ["PYTHONPATH"] = prev_pythonpath',
|
|
'',
|
|
' split_str = "__sitecustomize__"',
|
|
' file_name = sys.argv[0].replace(f"{os.getenv(\'dir_scripts\')}/", "")',
|
|
' ',
|
|
' # 创建临时文件路径',
|
|
' temp_file = f"/tmp/env_{os.getpid()}.json"',
|
|
' ',
|
|
' task_before = os.getenv("task_before", "")',
|
|
' if task_before:',
|
|
' print("Executing before command\\n" if os.environ.get("QL_LANG") == "en" else "执行前置命令\\n")',
|
|
' command = """file="$1"; output="$2"; inline="$3"; shift 3',
|
|
'source "$file" "$@" &&',
|
|
'{ if [ -n "$inline" ]; then eval "$inline"; fi; } &&',
|
|
"printf '%s\\\\n' '__sitecustomize__' &&",
|
|
'python3 -c \'import os,json,sys; f=open(sys.argv[1],"w"); json.dump(dict(os.environ),f); f.close()\' "$output"',
|
|
'"""',
|
|
' hook_args = json.loads(os.getenv("QL_CLI_PRELOAD_ARGS", json.dumps([file_name])))',
|
|
' res = subprocess.check_output(["/bin/bash", "-c", command, "ql-before",',
|
|
' os.environ["file_task_before"], temp_file, task_before, *hook_args], encoding="utf-8")',
|
|
' output = res.split(split_str)[0]',
|
|
'',
|
|
' try:',
|
|
" with open(temp_file, 'r') as f:",
|
|
' env_json = json.loads(f.read())',
|
|
'',
|
|
' for key, value in env_json.items():',
|
|
' os.environ[key] = value',
|
|
'',
|
|
' os.unlink(temp_file)',
|
|
' except Exception as json_error:',
|
|
' print(f"\\ue926 Failed to parse environment variables: {json_error}")',
|
|
' try:',
|
|
' os.unlink(temp_file)',
|
|
' except:',
|
|
' pass',
|
|
'',
|
|
' if len(output) > 0:',
|
|
' print(output)',
|
|
' if task_before:',
|
|
' print("Before command finished\\n" if os.environ.get("QL_LANG") == "en" else "执行前置命令结束\\n")',
|
|
'',
|
|
' except subprocess.CalledProcessError as error:',
|
|
' print(f"\\ue926 run task before error: {error}")',
|
|
' if task_before:',
|
|
' print("Before command finished\\n" if os.environ.get("QL_LANG") == "en" else "执行前置命令结束\\n")',
|
|
' except OSError as error:',
|
|
' error_message = str(error)',
|
|
' if "Argument list too long" not in error_message:',
|
|
' print(f"\\ue926 run task before error: {error}")',
|
|
' # else:',
|
|
' # environment variable is too large',
|
|
' if task_before:',
|
|
' print("Before command finished\\n" if os.environ.get("QL_LANG") == "en" else "执行前置命令结束\\n")',
|
|
' except Exception as error:',
|
|
' print(f"\\ue926 run task before error: {error}")',
|
|
' if task_before:',
|
|
' print("Before command finished\\n" if os.environ.get("QL_LANG") == "en" else "执行前置命令结束\\n")',
|
|
'',
|
|
' import task_before',
|
|
'',
|
|
' env_param = os.getenv("envParam")',
|
|
' num_param = os.getenv("numParam")',
|
|
'',
|
|
' if env_param and num_param:',
|
|
' array = (os.getenv(env_param) or "").split("&")',
|
|
' run_arr = expand_range(num_param, len(array))',
|
|
' array_run = [array[i - 1] for i in run_arr if i - 1 < len(array) and i > 0]',
|
|
' env_str = "&".join(array_run)',
|
|
' os.environ[env_param] = env_str',
|
|
'',
|
|
'',
|
|
'def handle_sigterm(signum, frame):',
|
|
' sys.exit(15)',
|
|
'',
|
|
'',
|
|
'try:',
|
|
' signal.signal(signal.SIGTERM, handle_sigterm)',
|
|
'',
|
|
' run()',
|
|
'',
|
|
' from __ql_notify__ import send',
|
|
'',
|
|
' class BaseApi(Client):',
|
|
' def notify(self, *args, **kwargs):',
|
|
' return send(*args, **kwargs)',
|
|
'',
|
|
' QLAPI = BaseApi()',
|
|
' builtins.QLAPI = QLAPI',
|
|
'except Exception as error:',
|
|
' print(f"run builtin code error: {error}\\n")',
|
|
'',
|
|
].join('\n');
|
|
|
|
// Native ESM resolution with global-first package selection.
|
|
export const esmLoaderSource: string = [
|
|
"import { existsSync } from 'node:fs';",
|
|
"import { isBuiltin } from 'node:module';",
|
|
"import { join } from 'node:path';",
|
|
"import { pathToFileURL } from 'node:url';",
|
|
'',
|
|
'export function resolve(specifier, context, nextResolve) {',
|
|
' // Leave builtins, package imports, paths and URL schemes to Node unchanged.',
|
|
' if (isBuiltin(specifier) || /^[.#/]/.test(specifier) || /^[a-zA-Z][a-zA-Z\\d+.-]*:/.test(specifier))',
|
|
' return nextResolve(specifier, context);',
|
|
" const parts = specifier.split('/');",
|
|
" const name = specifier.startsWith('@') ? parts.slice(0, 2).join('/') : parts[0];",
|
|
" for (const base of [process.env.QL_NODE_GLOBAL_PATH, '/usr/local/lib/node_modules'].filter(Boolean)) {",
|
|
' const directory = join(base, name);',
|
|
' if (!existsSync(directory)) continue;',
|
|
" // Resolve from inside the package, retaining Node's exports, import conditions",
|
|
' // and package-boundary checks. A virtual file need not exist for resolution.',
|
|
' return nextResolve(specifier, {',
|
|
' ...context,',
|
|
" parentURL: pathToFileURL(join(directory, '__ql_resolve__.mjs')).href,",
|
|
' });',
|
|
' }',
|
|
' return nextResolve(specifier, context);',
|
|
'}',
|
|
'',
|
|
].join('\n');
|