qinglong/back/services/dependence.ts
2025-05-18 22:04:08 +08:00

437 lines
14 KiB
TypeScript

import { Service, Inject } from 'typedi';
import winston from 'winston';
import config from '../config';
import {
Dependence,
DependenceStatus,
DependenceTypes,
DependenceModel,
GetDependenceCommandTypes,
versionDependenceCommandTypes,
} from '../data/dependence';
import { spawn } from 'cross-spawn';
import SockService from './sock';
import { FindOptions, Op } from 'sequelize';
import {
fileExist,
getPid,
killTask,
promiseExecSuccess,
getInstallCommand,
getUninstallCommand,
} from '../config/util';
import dayjs from 'dayjs';
import taskLimit from '../shared/pLimit';
import { detectOS } from '../config/util';
import { LINUX_DEPENDENCE_COMMAND } from '../config/const';
@Service()
export default class DependenceService {
constructor(
@Inject('logger') private logger: winston.Logger,
private sockService: SockService,
) {}
public async create(payloads: Dependence[]): Promise<Dependence[]> {
const tabs = payloads.map((x) => {
const tab = new Dependence({ ...x, status: DependenceStatus.queued });
return tab;
});
const docs = await this.insert(tabs);
this.installDependenceOneByOne(docs);
return docs;
}
public async insert(payloads: Dependence[]): Promise<Dependence[]> {
const docs = await DependenceModel.bulkCreate(payloads);
return docs;
}
public async update(
payload: Dependence & { id: string },
): Promise<Dependence> {
const { id, ...other } = payload;
const doc = await this.getDb({ id });
const tab = new Dependence({
...doc,
...other,
status: DependenceStatus.queued,
});
const newDoc = await this.updateDb(tab);
this.installDependenceOneByOne([newDoc]);
return newDoc;
}
private async updateDb(payload: Dependence): Promise<Dependence> {
await DependenceModel.update(payload, { where: { id: payload.id } });
return await this.getDb({ id: payload.id });
}
public async remove(ids: number[], force = false): Promise<Dependence[]> {
const docs = await DependenceModel.findAll({ where: { id: ids } });
for (const doc of docs) {
taskLimit.removeQueuedDependency(doc);
}
const unInstalledDeps = docs.filter(
(x) => x.status !== DependenceStatus.installed,
);
const installedDeps = docs.filter(
(x) => x.status === DependenceStatus.installed,
);
await this.removeDb(unInstalledDeps.map((x) => x.id!));
if (installedDeps.length) {
await DependenceModel.update(
{ status: DependenceStatus.queued, log: [] },
{ where: { id: ids } },
);
this.installDependenceOneByOne(docs, false, force);
}
return docs;
}
public async removeDb(ids: number[]) {
await DependenceModel.destroy({ where: { id: ids } });
}
public async dependencies(
{
searchValue,
type,
status,
}: { searchValue: string; type: string; status: string },
sort: any = [],
query: any = {},
): Promise<Dependence[]> {
let condition = {
...query,
type: DependenceTypes[type as any],
};
if (status) {
condition.status = status.split(',').map(Number);
}
if (searchValue) {
const encodeText = encodeURI(searchValue);
const reg = {
[Op.or]: [
{ [Op.like]: `%${searchValue}%` },
{ [Op.like]: `%${encodeText}%` },
],
};
condition = {
...condition,
name: reg,
};
}
try {
const result = await this.find(condition, sort);
return result as any;
} catch (error) {
throw error;
}
}
public installDependenceOneByOne(
docs: Dependence[],
isInstall: boolean = true,
force: boolean = false,
): Promise<void> {
docs.forEach((dep) => {
this.installOrUninstallDependency(dep, isInstall, force);
});
return taskLimit.waitDependencyQueueDone();
}
public async reInstall(ids: number[]): Promise<Dependence[]> {
await DependenceModel.update(
{ status: DependenceStatus.queued, log: [] },
{ where: { id: ids } },
);
const docs = await DependenceModel.findAll({ where: { id: ids } });
for (const doc of docs) {
taskLimit.removeQueuedDependency(doc);
}
this.installDependenceOneByOne(docs, true, true);
return docs;
}
public async cancel(ids: number[]) {
const docs = await DependenceModel.findAll({ where: { id: ids } });
for (const doc of docs) {
taskLimit.removeQueuedDependency(doc);
let depInstallCommand = getInstallCommand(doc.type, doc.name);
let depUnInstallCommand = getUninstallCommand(doc.type, doc.name);
const isLinuxDependence = doc.type === DependenceTypes.linux;
if (isLinuxDependence) {
const osType = await detectOS();
if (!osType) {
continue;
}
const linuxCommand = LINUX_DEPENDENCE_COMMAND[osType];
depInstallCommand = linuxCommand.install;
depUnInstallCommand = linuxCommand.uninstall;
}
const installCmd = `${depInstallCommand} ${doc.name.trim()}`;
const unInstallCmd = `${depUnInstallCommand} ${doc.name.trim()}`;
const pids = await Promise.all([
getPid(installCmd),
getPid(unInstallCmd),
]);
for (const pid of pids) {
pid && (await killTask(pid));
}
}
await DependenceModel.update(
{ status: DependenceStatus.cancelled },
{ where: { id: ids } },
);
}
private async find(query: any, sort: any = []): Promise<Dependence[]> {
const docs = await DependenceModel.findAll({
where: { ...query },
order: [...sort, ['createdAt', 'DESC']],
});
return docs;
}
public async getDb(
query: FindOptions<Dependence>['where'],
): Promise<Dependence> {
const doc: any = await DependenceModel.findOne({ where: { ...query } });
if (!doc) {
throw new Error(`Dependency ${JSON.stringify(query)} not found`);
}
return doc.get({ plain: true });
}
private async updateLog(ids: number[], log: string): Promise<void> {
taskLimit.updateDepLog(async () => {
const docs = await DependenceModel.findAll({ where: { id: ids } });
for (const doc of docs) {
const newLog = doc?.log ? [...doc.log, log] : [log];
await DependenceModel.update(
{ log: newLog },
{ where: { id: doc.id } },
);
}
return null;
});
}
public installOrUninstallDependency(
dependency: Dependence,
isInstall: boolean = true,
force: boolean = false,
) {
return taskLimit.runDependeny(dependency, () => {
return new Promise(async (resolve) => {
if (taskLimit.firstDependencyId !== dependency.id) {
return resolve(null);
}
const depIds = [dependency.id!];
let depName = dependency.name.trim();
const actionText = isInstall ? '安装' : '删除';
const socketMessageType = isInstall
? 'installDependence'
: 'uninstallDependence';
const isNodeDependence = dependency.type === DependenceTypes.nodejs;
const isLinuxDependence = dependency.type === DependenceTypes.linux;
const isPythonDependence = dependency.type === DependenceTypes.python3;
const osType = await detectOS();
let linuxCommand = {} as typeof LINUX_DEPENDENCE_COMMAND.Alpine;
taskLimit.removeQueuedDependency(dependency);
if (isLinuxDependence) {
if (!osType) {
await DependenceModel.update(
{ status: DependenceStatus.installFailed },
{ where: { id: depIds } },
);
const startTime = dayjs();
const message = `开始${actionText}依赖 ${depName},开始时间 ${startTime.format(
'YYYY-MM-DD HH:mm:ss',
)}\n\n当前系统不支持\n\n依赖${actionText}失败,结束时间 ${startTime.format(
'YYYY-MM-DD HH:mm:ss',
)},耗时 ${startTime.diff(startTime, 'second')}`;
this.sockService.sendMessage({
type: socketMessageType,
message,
references: depIds,
});
this.updateLog(depIds, message);
return resolve(null);
}
linuxCommand = LINUX_DEPENDENCE_COMMAND[osType];
}
const status = isInstall
? DependenceStatus.installing
: DependenceStatus.removing;
await DependenceModel.update({ status }, { where: { id: depIds } });
let command = isInstall
? getInstallCommand(dependency.type, depName)
: getUninstallCommand(dependency.type, depName);
if (isLinuxDependence) {
command = isInstall
? `${linuxCommand.install} ${depName.trim()}`
: `${linuxCommand.uninstall} ${depName.trim()}`;
}
const startTime = dayjs();
const message = `开始${actionText}依赖 ${depName},开始时间 ${startTime.format(
'YYYY-MM-DD HH:mm:ss',
)}\n\n`;
this.sockService.sendMessage({
type: socketMessageType,
message,
references: depIds,
});
this.updateLog(depIds, message);
// 判断是否已经安装过依赖
if (isInstall && !force) {
let getCommandPrefix = GetDependenceCommandTypes[dependency.type];
const depVersionStr = versionDependenceCommandTypes[dependency.type];
if (isLinuxDependence) {
getCommandPrefix = linuxCommand.info;
}
let depVersion = '';
if (depName.includes(depVersionStr)) {
const symbolRegx = new RegExp(
`(.*)${depVersionStr}([0-9\\.\\-\\+a-zA-Z]*)`,
);
const [, _depName, _depVersion] = depName.match(symbolRegx) || [];
if (_depVersion && _depName) {
depName = _depName;
depVersion = _depVersion;
}
}
const depInfo = (
await promiseExecSuccess(
isNodeDependence
? `${getCommandPrefix} | grep "${depName}" | head -1`
: `${getCommandPrefix} ${depName}`,
)
)
.replace(/\s{2,}/, ' ')
.replace(/\s+$/, '');
if (
depInfo &&
((isNodeDependence && depInfo.split(' ')?.[0] === depName) ||
(isLinuxDependence &&
linuxCommand.check(depInfo.toLocaleLowerCase())) ||
isPythonDependence) &&
(!depVersion || depInfo.includes(depVersion))
) {
const endTime = dayjs();
const _message = `检测到已经安装 ${depName}\n\n${depInfo}\n\n跳过安装\n\n依赖${actionText}成功,结束时间 ${endTime.format(
'YYYY-MM-DD HH:mm:ss',
)},耗时 ${endTime.diff(startTime, 'second')}`;
this.sockService.sendMessage({
type: socketMessageType,
message: _message,
references: depIds,
});
this.updateLog(depIds, _message);
await DependenceModel.update(
{ status: DependenceStatus.installed },
{ where: { id: depIds } },
);
return resolve(null);
}
}
const dependenceProxyFileExist = await fileExist(
config.dependenceProxyFile,
);
const proxyStr = dependenceProxyFileExist
? `source ${config.dependenceProxyFile} &&`
: '';
const cp = spawn(`${proxyStr} ${command}`, {
shell: '/bin/bash',
});
cp.stdout.on('data', async (data) => {
this.sockService.sendMessage({
type: socketMessageType,
message: data.toString(),
references: depIds,
});
this.updateLog(depIds, data.toString());
});
cp.stderr.on('data', async (data) => {
this.sockService.sendMessage({
type: socketMessageType,
message: data.toString(),
references: depIds,
});
this.updateLog(depIds, data.toString());
});
cp.on('error', async (err) => {
this.sockService.sendMessage({
type: socketMessageType,
message: JSON.stringify(err),
references: depIds,
});
this.updateLog(depIds, JSON.stringify(err));
});
cp.on('exit', async (code) => {
const endTime = dayjs();
const isSucceed = code === 0;
const resultText = isSucceed ? '成功' : '失败';
const message = `\n依赖${actionText}${resultText},结束时间 ${endTime.format(
'YYYY-MM-DD HH:mm:ss',
)},耗时 ${endTime.diff(startTime, 'second')}`;
this.sockService.sendMessage({
type: socketMessageType,
message,
references: depIds,
});
this.updateLog(depIds, message);
let status: number;
if (isSucceed) {
status = isInstall
? DependenceStatus.installed
: DependenceStatus.removed;
} else {
status = isInstall
? DependenceStatus.installFailed
: DependenceStatus.removeFailed;
}
const docs = await DependenceModel.findAll({ where: { id: depIds } });
const _docIds = docs
.filter((x) => x.status !== DependenceStatus.cancelled)
.map((x) => x.id!);
if (_docIds.length > 0) {
await DependenceModel.update(
{ status },
{ where: { id: _docIds } },
);
}
// 如果删除依赖成功或者强制删除
if ((isSucceed || force) && !isInstall) {
this.removeDb(depIds);
}
resolve(null);
});
});
});
}
}