mirror of
https://github.com/whyour/qinglong.git
synced 2026-08-12 19:30:48 +08:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c329c8acd4 | ||
|
|
d43d563622 | ||
|
|
aa52cfb29d |
@@ -10,6 +10,7 @@ import Logger from '../loaders/logger';
|
|||||||
import { writeFileWithLock } from '../shared/utils';
|
import { writeFileWithLock } from '../shared/utils';
|
||||||
import { DependenceTypes } from '../data/dependence';
|
import { DependenceTypes } from '../data/dependence';
|
||||||
import { FormData } from 'undici';
|
import { FormData } from 'undici';
|
||||||
|
import os from 'os';
|
||||||
|
|
||||||
export * from './share';
|
export * from './share';
|
||||||
|
|
||||||
@@ -590,3 +591,162 @@ export function getUninstallCommand(
|
|||||||
export function isDemoEnv() {
|
export function isDemoEnv() {
|
||||||
return process.env.DeployEnv === 'demo';
|
return process.env.DeployEnv === 'demo';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// OS detection for Linux mirror configuration
|
||||||
|
let osType: 'Debian' | 'Ubuntu' | 'Alpine' | undefined;
|
||||||
|
|
||||||
|
async function getOSReleaseInfo(): Promise<string> {
|
||||||
|
try {
|
||||||
|
const osRelease = await fs.readFile('/etc/os-release', 'utf8');
|
||||||
|
return osRelease;
|
||||||
|
} catch (error) {
|
||||||
|
Logger.error(`Failed to read /etc/os-release: ${error}`);
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function isDebian(osReleaseInfo: string): boolean {
|
||||||
|
return osReleaseInfo.includes('Debian');
|
||||||
|
}
|
||||||
|
|
||||||
|
function isUbuntu(osReleaseInfo: string): boolean {
|
||||||
|
return osReleaseInfo.includes('Ubuntu');
|
||||||
|
}
|
||||||
|
|
||||||
|
function isAlpine(osReleaseInfo: string): boolean {
|
||||||
|
return osReleaseInfo.includes('Alpine');
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function detectOS(): Promise<
|
||||||
|
'Debian' | 'Ubuntu' | 'Alpine' | undefined
|
||||||
|
> {
|
||||||
|
if (osType) return osType;
|
||||||
|
const platform = os.platform();
|
||||||
|
|
||||||
|
if (platform === 'linux') {
|
||||||
|
const osReleaseInfo = await getOSReleaseInfo();
|
||||||
|
// Check Ubuntu before Debian since Ubuntu is based on Debian
|
||||||
|
if (isUbuntu(osReleaseInfo)) {
|
||||||
|
osType = 'Ubuntu';
|
||||||
|
} else if (isDebian(osReleaseInfo)) {
|
||||||
|
osType = 'Debian';
|
||||||
|
} else if (isAlpine(osReleaseInfo)) {
|
||||||
|
osType = 'Alpine';
|
||||||
|
} else {
|
||||||
|
Logger.error(`Unknown Linux Distribution: ${osReleaseInfo}`);
|
||||||
|
console.error(`Unknown Linux Distribution: ${osReleaseInfo}`);
|
||||||
|
}
|
||||||
|
} else if (platform === 'darwin') {
|
||||||
|
osType = undefined;
|
||||||
|
} else {
|
||||||
|
Logger.error(`Unsupported platform: ${platform}`);
|
||||||
|
console.error(`Unsupported platform: ${platform}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return osType;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getCurrentMirrorDomain(
|
||||||
|
filePath: string,
|
||||||
|
): Promise<string | null> {
|
||||||
|
try {
|
||||||
|
const fileContent = await fs.readFile(filePath, 'utf8');
|
||||||
|
const lines = fileContent.split('\n');
|
||||||
|
for (const line of lines) {
|
||||||
|
if (line.trim().startsWith('#')) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const match = line.match(/https?:\/\/[^\/]+/);
|
||||||
|
if (match) {
|
||||||
|
return match[0];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
} catch (error) {
|
||||||
|
Logger.error(`Failed to read mirror configuration file ${filePath}: ${error}`);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function escapeRegExp(string: string): string {
|
||||||
|
return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
||||||
|
}
|
||||||
|
|
||||||
|
async function replaceDomainInFile(
|
||||||
|
filePath: string,
|
||||||
|
oldDomainWithScheme: string,
|
||||||
|
newDomainWithScheme: string,
|
||||||
|
): Promise<void> {
|
||||||
|
// Ensure the new domain has a trailing slash before replacement
|
||||||
|
if (!newDomainWithScheme.endsWith('/')) {
|
||||||
|
newDomainWithScheme += '/';
|
||||||
|
}
|
||||||
|
|
||||||
|
let fileContent = await fs.readFile(filePath, 'utf8');
|
||||||
|
// Escape special regex characters in the old domain
|
||||||
|
const escapedOldDomain = escapeRegExp(oldDomainWithScheme);
|
||||||
|
let updatedContent = fileContent.replace(
|
||||||
|
new RegExp(escapedOldDomain, 'g'),
|
||||||
|
newDomainWithScheme,
|
||||||
|
);
|
||||||
|
|
||||||
|
await writeFileWithLock(filePath, updatedContent);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function _updateLinuxMirror(
|
||||||
|
osType: string,
|
||||||
|
mirrorDomainWithScheme: string,
|
||||||
|
): Promise<string> {
|
||||||
|
let filePath: string, currentDomainWithScheme: string | null;
|
||||||
|
switch (osType) {
|
||||||
|
case 'Debian':
|
||||||
|
filePath = '/etc/apt/sources.list.d/debian.sources';
|
||||||
|
currentDomainWithScheme = await getCurrentMirrorDomain(filePath);
|
||||||
|
if (currentDomainWithScheme) {
|
||||||
|
await replaceDomainInFile(
|
||||||
|
filePath,
|
||||||
|
currentDomainWithScheme,
|
||||||
|
mirrorDomainWithScheme || 'http://deb.debian.org',
|
||||||
|
);
|
||||||
|
return 'apt-get update';
|
||||||
|
} else {
|
||||||
|
throw Error(`Current mirror domain not found.`);
|
||||||
|
}
|
||||||
|
case 'Ubuntu':
|
||||||
|
filePath = '/etc/apt/sources.list.d/ubuntu.sources';
|
||||||
|
currentDomainWithScheme = await getCurrentMirrorDomain(filePath);
|
||||||
|
if (currentDomainWithScheme) {
|
||||||
|
await replaceDomainInFile(
|
||||||
|
filePath,
|
||||||
|
currentDomainWithScheme,
|
||||||
|
mirrorDomainWithScheme || 'http://archive.ubuntu.com',
|
||||||
|
);
|
||||||
|
return 'apt-get update';
|
||||||
|
} else {
|
||||||
|
throw Error(`Current mirror domain not found.`);
|
||||||
|
}
|
||||||
|
case 'Alpine':
|
||||||
|
filePath = '/etc/apk/repositories';
|
||||||
|
currentDomainWithScheme = await getCurrentMirrorDomain(filePath);
|
||||||
|
if (currentDomainWithScheme) {
|
||||||
|
await replaceDomainInFile(
|
||||||
|
filePath,
|
||||||
|
currentDomainWithScheme,
|
||||||
|
mirrorDomainWithScheme || 'http://dl-cdn.alpinelinux.org',
|
||||||
|
);
|
||||||
|
return 'apk update';
|
||||||
|
} else {
|
||||||
|
throw Error(`Current mirror domain not found.`);
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
throw Error('Unsupported OS type for updating mirrors.');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function updateLinuxMirrorFile(mirror: string): Promise<string> {
|
||||||
|
const detectedOS = await detectOS();
|
||||||
|
if (!detectedOS) {
|
||||||
|
throw Error(`Unknown Linux Distribution`);
|
||||||
|
}
|
||||||
|
return await _updateLinuxMirror(detectedOS, mirror);
|
||||||
|
}
|
||||||
|
|||||||
+10
-24
@@ -17,6 +17,7 @@ import {
|
|||||||
readDirs,
|
readDirs,
|
||||||
rmPath,
|
rmPath,
|
||||||
setSystemTimezone,
|
setSystemTimezone,
|
||||||
|
updateLinuxMirrorFile,
|
||||||
} from '../config/util';
|
} from '../config/util';
|
||||||
import {
|
import {
|
||||||
DependenceModel,
|
DependenceModel,
|
||||||
@@ -214,33 +215,11 @@ export default class SystemService {
|
|||||||
onEnd?: () => void,
|
onEnd?: () => void,
|
||||||
) {
|
) {
|
||||||
const oDoc = await this.getSystemConfig();
|
const oDoc = await this.getSystemConfig();
|
||||||
await this.updateAuthDb({
|
|
||||||
...oDoc,
|
|
||||||
info: { ...oDoc.info, ...info },
|
|
||||||
});
|
|
||||||
let defaultDomain = 'https://dl-cdn.alpinelinux.org';
|
|
||||||
let targetDomain = 'https://dl-cdn.alpinelinux.org';
|
|
||||||
if (os.platform() !== 'linux') {
|
if (os.platform() !== 'linux') {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const content = await fs.promises.readFile('/etc/apk/repositories', {
|
const command = await updateLinuxMirrorFile(info.linuxMirror || '');
|
||||||
encoding: 'utf-8',
|
let hasError = false;
|
||||||
});
|
|
||||||
const domainMatch = content.match(/(http.*)\/alpine\/.*/);
|
|
||||||
if (domainMatch) {
|
|
||||||
defaultDomain = domainMatch[1];
|
|
||||||
}
|
|
||||||
if (info.linuxMirror) {
|
|
||||||
targetDomain = info.linuxMirror;
|
|
||||||
}
|
|
||||||
const command = `sed -i 's/${defaultDomain.replace(
|
|
||||||
/\//g,
|
|
||||||
'\\/',
|
|
||||||
)}/${targetDomain.replace(
|
|
||||||
/\//g,
|
|
||||||
'\\/',
|
|
||||||
)}/g' /etc/apk/repositories && apk update -f`;
|
|
||||||
|
|
||||||
this.scheduleService.runTask(
|
this.scheduleService.runTask(
|
||||||
command,
|
command,
|
||||||
{
|
{
|
||||||
@@ -254,8 +233,15 @@ export default class SystemService {
|
|||||||
message: 'update linux mirror end',
|
message: 'update linux mirror end',
|
||||||
});
|
});
|
||||||
onEnd?.();
|
onEnd?.();
|
||||||
|
if (!hasError) {
|
||||||
|
await this.updateAuthDb({
|
||||||
|
...oDoc,
|
||||||
|
info: { ...oDoc.info, ...info },
|
||||||
|
});
|
||||||
|
}
|
||||||
},
|
},
|
||||||
onError: async (message: string) => {
|
onError: async (message: string) => {
|
||||||
|
hasError = true;
|
||||||
this.sockService.sendMessage({ type: 'updateLinuxMirror', message });
|
this.sockService.sendMessage({ type: 'updateLinuxMirror', message });
|
||||||
},
|
},
|
||||||
onLog: async (message: string) => {
|
onLog: async (message: string) => {
|
||||||
|
|||||||
+2
-28
@@ -14,7 +14,6 @@ import {
|
|||||||
import config from '../config';
|
import config from '../config';
|
||||||
import { credentials } from '@grpc/grpc-js';
|
import { credentials } from '@grpc/grpc-js';
|
||||||
import { ApiClient } from '../protos/api';
|
import { ApiClient } from '../protos/api';
|
||||||
import { CrontabModel } from '../data/cron';
|
|
||||||
|
|
||||||
class TaskLimit {
|
class TaskLimit {
|
||||||
private dependenyLimit = new PQueue({ concurrency: 1 });
|
private dependenyLimit = new PQueue({ concurrency: 1 });
|
||||||
@@ -132,38 +131,13 @@ class TaskLimit {
|
|||||||
let runs = this.queuedCrons.get(cron.id);
|
let runs = this.queuedCrons.get(cron.id);
|
||||||
const result = runs?.length ? [...runs, fn] : [fn];
|
const result = runs?.length ? [...runs, fn] : [fn];
|
||||||
const repeatTimes = this.repeatCronNotifyMap.get(cron.id) || 0;
|
const repeatTimes = this.repeatCronNotifyMap.get(cron.id) || 0;
|
||||||
|
if (result?.length > 5) {
|
||||||
// Check instance mode from database to determine queue limit
|
|
||||||
let maxQueueSize = 10; // Default for multi-instance mode (increased from 5)
|
|
||||||
let isSingleInstanceMode = false;
|
|
||||||
try {
|
|
||||||
const cronRecord = await CrontabModel.findOne({
|
|
||||||
where: { id: Number(cron.id) },
|
|
||||||
});
|
|
||||||
|
|
||||||
// Default to single instance mode (0) for backward compatibility
|
|
||||||
// allow_multiple_instances is 1 for multi-instance, 0 or null/undefined for single instance
|
|
||||||
isSingleInstanceMode = cronRecord?.allow_multiple_instances !== 1;
|
|
||||||
|
|
||||||
if (isSingleInstanceMode) {
|
|
||||||
// For single instance mode, allow up to 2 queued tasks
|
|
||||||
// This allows the new task to be queued while the old one is being killed
|
|
||||||
maxQueueSize = 2;
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
Logger.error(
|
|
||||||
`[schedule][检查实例模式失败] 任务ID: ${cron.id}, 错误: ${error}`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (result?.length > maxQueueSize) {
|
|
||||||
if (repeatTimes < 3) {
|
if (repeatTimes < 3) {
|
||||||
this.repeatCronNotifyMap.set(cron.id, repeatTimes + 1);
|
this.repeatCronNotifyMap.set(cron.id, repeatTimes + 1);
|
||||||
const modeStr = isSingleInstanceMode ? '单实例' : '多实例';
|
|
||||||
this.client.systemNotify(
|
this.client.systemNotify(
|
||||||
{
|
{
|
||||||
title: '任务重复运行',
|
title: '任务重复运行',
|
||||||
content: `任务:${cron.name}(${modeStr}模式),命令:${cron.command},定时:${cron.schedule},处于运行中的超过 ${maxQueueSize} 个,请检查定时设置`,
|
content: `任务:${cron.name},命令:${cron.command},定时:${cron.schedule},处于运行中的超过 5 个,请检查定时设置`,
|
||||||
},
|
},
|
||||||
(err, res) => {
|
(err, res) => {
|
||||||
if (err) {
|
if (err) {
|
||||||
|
|||||||
+3
-27
@@ -15,12 +15,11 @@ export function runCron(cmd: string, cron: ICron): Promise<number | void> {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Default to single instance mode (0) for backward compatibility
|
// Default to single instance mode (0) for backward compatibility
|
||||||
// allow_multiple_instances is 1 for multi-instance, 0 or null/undefined for single instance
|
const allowSingleInstances =
|
||||||
const isSingleInstanceMode =
|
existingCron?.allow_multiple_instances === 0;
|
||||||
existingCron?.allow_multiple_instances !== 1;
|
|
||||||
|
|
||||||
if (
|
if (
|
||||||
isSingleInstanceMode &&
|
allowSingleInstances &&
|
||||||
existingCron &&
|
existingCron &&
|
||||||
existingCron.pid &&
|
existingCron.pid &&
|
||||||
(existingCron.status === CrontabStatus.running ||
|
(existingCron.status === CrontabStatus.running ||
|
||||||
@@ -50,18 +49,6 @@ export function runCron(cmd: string, cron: ICron): Promise<number | void> {
|
|||||||
);
|
);
|
||||||
const cp = spawn(cmd, { shell: '/bin/bash' });
|
const cp = spawn(cmd, { shell: '/bin/bash' });
|
||||||
|
|
||||||
// Update status to running after spawning the process
|
|
||||||
try {
|
|
||||||
await CrontabModel.update(
|
|
||||||
{ status: CrontabStatus.running, pid: cp.pid },
|
|
||||||
{ where: { id: Number(cron.id) } },
|
|
||||||
);
|
|
||||||
} catch (error) {
|
|
||||||
Logger.error(
|
|
||||||
`[schedule][更新任务状态失败] 任务ID: ${cron.id}, 错误: ${error}`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
cp.stderr.on('data', (data) => {
|
cp.stderr.on('data', (data) => {
|
||||||
Logger.info(
|
Logger.info(
|
||||||
'[schedule][执行任务失败] 命令: %s, 错误信息: %j',
|
'[schedule][执行任务失败] 命令: %s, 错误信息: %j',
|
||||||
@@ -79,17 +66,6 @@ export function runCron(cmd: string, cron: ICron): Promise<number | void> {
|
|||||||
|
|
||||||
cp.on('exit', async (code) => {
|
cp.on('exit', async (code) => {
|
||||||
taskLimit.removeQueuedCron(cron.id);
|
taskLimit.removeQueuedCron(cron.id);
|
||||||
// Update status to idle after task completes
|
|
||||||
try {
|
|
||||||
await CrontabModel.update(
|
|
||||||
{ status: CrontabStatus.idle, pid: undefined },
|
|
||||||
{ where: { id: Number(cron.id) } },
|
|
||||||
);
|
|
||||||
} catch (error) {
|
|
||||||
Logger.error(
|
|
||||||
`[schedule][更新任务状态失败] 任务ID: ${cron.id}, 错误: ${error}`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
Logger.info(
|
Logger.info(
|
||||||
'[schedule][执行任务结束] 参数: %s, 退出码: %j',
|
'[schedule][执行任务结束] 参数: %s, 退出码: %j',
|
||||||
JSON.stringify({
|
JSON.stringify({
|
||||||
|
|||||||
Reference in New Issue
Block a user