mirror of
https://github.com/whyour/qinglong.git
synced 2026-08-10 03:16:48 +08:00
Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 0bbff927b1 | |||
| 58eb9feec0 | |||
| 802ca93a3d |
@@ -10,7 +10,6 @@ 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';
|
||||||
|
|
||||||
@@ -591,162 +590,3 @@ 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);
|
|
||||||
}
|
|
||||||
|
|||||||
+24
-10
@@ -17,7 +17,6 @@ import {
|
|||||||
readDirs,
|
readDirs,
|
||||||
rmPath,
|
rmPath,
|
||||||
setSystemTimezone,
|
setSystemTimezone,
|
||||||
updateLinuxMirrorFile,
|
|
||||||
} from '../config/util';
|
} from '../config/util';
|
||||||
import {
|
import {
|
||||||
DependenceModel,
|
DependenceModel,
|
||||||
@@ -215,11 +214,33 @@ 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 command = await updateLinuxMirrorFile(info.linuxMirror || '');
|
const content = await fs.promises.readFile('/etc/apk/repositories', {
|
||||||
let hasError = false;
|
encoding: 'utf-8',
|
||||||
|
});
|
||||||
|
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,
|
||||||
{
|
{
|
||||||
@@ -233,15 +254,8 @@ 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) => {
|
||||||
|
|||||||
+28
-2
@@ -14,6 +14,7 @@ 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 });
|
||||||
@@ -131,13 +132,38 @@ 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},命令:${cron.command},定时:${cron.schedule},处于运行中的超过 5 个,请检查定时设置`,
|
content: `任务:${cron.name}(${modeStr}模式),命令:${cron.command},定时:${cron.schedule},处于运行中的超过 ${maxQueueSize} 个,请检查定时设置`,
|
||||||
},
|
},
|
||||||
(err, res) => {
|
(err, res) => {
|
||||||
if (err) {
|
if (err) {
|
||||||
|
|||||||
+27
-3
@@ -15,11 +15,12 @@ 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
|
||||||
const allowSingleInstances =
|
// allow_multiple_instances is 1 for multi-instance, 0 or null/undefined for single instance
|
||||||
existingCron?.allow_multiple_instances === 0;
|
const isSingleInstanceMode =
|
||||||
|
existingCron?.allow_multiple_instances !== 1;
|
||||||
|
|
||||||
if (
|
if (
|
||||||
allowSingleInstances &&
|
isSingleInstanceMode &&
|
||||||
existingCron &&
|
existingCron &&
|
||||||
existingCron.pid &&
|
existingCron.pid &&
|
||||||
(existingCron.status === CrontabStatus.running ||
|
(existingCron.status === CrontabStatus.running ||
|
||||||
@@ -49,6 +50,18 @@ 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',
|
||||||
@@ -66,6 +79,17 @@ 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