Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions packages/plugin-multi-process-app/.npmignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
/node_modules
/src
1 change: 1 addition & 0 deletions packages/plugin-multi-process-app/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
# @tachybase/multi-process-app
2 changes: 2 additions & 0 deletions packages/plugin-multi-process-app/client.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export * from './dist/client';
export { default } from './dist/client';
1 change: 1 addition & 0 deletions packages/plugin-multi-process-app/client.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
module.exports = require('./dist/client/index.js');
18 changes: 18 additions & 0 deletions packages/plugin-multi-process-app/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
{
"name": "@tachybase/plugin-multi-process-app",
"version": "0.23.54",
"main": "dist/server/index.js",
"dependencies": {
"koa-proxies": "^0.12.4"
},
"devDependencies": {
"koa": "^2.15.3"
},
"peerDependencies": {
"@tachybase/client": "workspace:*",
"@tachybase/server": "workspace:*",
"@tachybase/test": "workspace:*"
},
"description.zh-CN": "创建各自独立进程的子应用,通过主应用反向代理请求到子应用,并可以通过主应用统一管理子应用启动关闭",
"displayName.zh-CN": "多进程应用"
}
2 changes: 2 additions & 0 deletions packages/plugin-multi-process-app/server.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export * from './dist/server';
export { default } from './dist/server';
1 change: 1 addition & 0 deletions packages/plugin-multi-process-app/server.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
module.exports = require('./dist/server/index.js');
1 change: 1 addition & 0 deletions packages/plugin-multi-process-app/src/client/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { default } from './plugin';
21 changes: 21 additions & 0 deletions packages/plugin-multi-process-app/src/client/plugin.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { Plugin } from '@tachybase/client';

class MultiProcessAppClient extends Plugin {
async afterAdd() {
// await this.app.pm.add()
}

async beforeLoad() {}

// You can get and modify the app instance here
async load() {
console.log(this.app);
// this.app.addComponents({})
// this.app.addScopes({})
// this.app.addProvider()
// this.app.addProviders()
// this.app.router.add()
}
}

export default MultiProcessAppClient;
2 changes: 2 additions & 0 deletions packages/plugin-multi-process-app/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export * from './server';
export { default } from './server';
122 changes: 122 additions & 0 deletions packages/plugin-multi-process-app/src/server/ProcessAppManager.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
import { Repository } from '@tachybase/database';
import Application from '@tachybase/server';

import Koa from 'koa';
import proxy from 'koa-proxies';

import { getPidByPort, runCommand, runCommandAsync } from './utils';

export class ProcessAppManager {
private pidMap: Record<number, number> = {};
private middlewares: Record<number, Koa.Middleware> = {};

private repo: Repository;
transportMap: Record<string, number> = {};

constructor(public mainApp: Application) {
this.repo = this.mainApp.db.getRepository('process_apps');
}

// app,afterStart之后要启动子应用
async afterStart() {
const appList = await this.repo.find({
filter: {
enabled: true,
},
});
for (const appItem of appList) {
const proxyFunc = proxy('/', {
target: appItem.host,
changeOrigin: true,
});
this.mainApp.use(proxyFunc);
this.middlewares[appItem.id] = proxyFunc;
}

this.mainApp.use(async (ctx, next) => {
const host = ctx.host; // 获取请求的 Host,如 api.example.com

const subdomains = Object.keys(this.transportMap);
// 查找匹配的 target
const matchedRule = subdomains.find((subdomain) => new RegExp(`^${subdomain}\\.`).test(host));

const target = `http://127.0.0.1:${this.transportMap[matchedRule]}`;

if (matchedRule) {
return proxy('/', {
target,
changeOrigin: true,
})(ctx, next);
}

return next();
});
}

async addListener() {}

async startApp(remote: string, localPath: string, branch: string = 'main', prId = 0) {
// TODO: git clone加速
if (remote) {
await runCommand(`git clone --branch ${branch} --single-branch ${remote} ${localPath}`);
}
const APP_PORT = `${prId + 15_000}`;
const DB_DATABASE = `pr_${prId}`;
// TODO: install 加速
await runCommand(`pnpm install && pnpm build`, localPath);
const pid = await runCommandAsync('pnpm', ['start', '--quickstart'], localPath, {
DB_DATABASE,
APP_PORT,
});
const insertRecord = await this.repo.create({
values: {
enabled: true,
pid,
database: DB_DATABASE,
port: +APP_PORT,
remote,
branch,
cname: `${prId}`,
},
});
// 保存到数据库, 并指定端口,数据库
this.pidMap[insertRecord.id] = pid;
}

async refreshApp(remote: string, localPath: string, branch: string = 'main', prId = 0) {}

async stopApp(id: number) {
const repo = this.mainApp.db.getRepository('process_apps');
let pid = this.pidMap[id];
if (!pid) {
const appItem = await repo.findOne({
filter: {
id: true,
},
});
const port = appItem.port;
if (!port) {
return;
}
pid = await getPidByPort(port);
}

if (!pid) {
this.mainApp.logger.error(`can not find pid, id: ${id}`);
}

try {
process.kill(pid);
} catch (e) {
this.mainApp.logger.error(e);
return;
}

await repo.destroy({
filter: {
id,
},
});
delete this.pidMap[id];
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import { defineCollection } from '@tachybase/database';

export default defineCollection({
dumpRules: 'required',
name: 'process_apps',
autoGenId: false,
sortable: 'sort',
filterTargetKey: 'name',
createdBy: true,
updatedBy: true,
fields: [
{
name: 'id',
type: 'bigInt',
autoIncrement: true,
primaryKey: true,
allowNull: false,
interface: 'id',
},
{
type: 'boolean',
name: 'enabled',
},
{
type: 'string',
name: 'displayName',
},
{
type: 'string',
name: 'cname', //域名前缀
unique: true,
},
{
type: 'string',
name: 'remote',
},
{
type: 'string',
name: 'branch',
},
{
type: 'number',
name: 'port',
},
{
type: 'number',
name: 'pid',
},
],
indexes: [
{
unique: true,
fields: ['database'],
},
],
});
1 change: 1 addition & 0 deletions packages/plugin-multi-process-app/src/server/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { default } from './plugin';
33 changes: 33 additions & 0 deletions packages/plugin-multi-process-app/src/server/plugin.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { Plugin } from '@tachybase/server';

import proxy from 'koa-proxies';

export class MultiProcessAppServer extends Plugin {
async afterAdd() {}

async beforeLoad() {}

async load() {
const repo = this.db.getRepository('process_apps');
const appList = await repo.find();
const middlewares: Record<number, any> = {};
for (const appItem of appList) {
const proxyFunc = proxy('/', {
target: appItem.host,
changeOrigin: true,
});
this.app.use(proxyFunc);
middlewares[appItem.id] = proxyFunc;
}
}

async install() {}

async afterEnable() {}

async afterDisable() {}

async remove() {}
}

export default MultiProcessAppServer;
101 changes: 101 additions & 0 deletions packages/plugin-multi-process-app/src/server/utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
import { exec, execSync, spawn } from 'child_process';

/**
* 同步执行 Shell 命令
* @param command 要执行的完整命令字符串
* @param cwd 运行命令的目录
*/
export function runCommand(command: string, cwd?: string): void {
try {
console.log(`执行命令: ${command} (目录: ${cwd || '当前目录'})`);
execSync(command, { stdio: 'inherit', cwd });
} catch (error) {
console.error(`命令执行失败: ${command}`, (error as Error).message);
process.exit(1);
}
}

/**
* 异步执行 Shell 命令,返回进程 PID
* @param command 命令名称(例如 "pnpm")
* @param args 命令参数(例如 ["start", "--quickstart"])
* @param cwd 运行命令的目录
* @param env 自定义环境变量
* @returns Promise<number> 返回进程的 PID
*/
export function runCommandAsync(
command: string,
args: string[],
cwd?: string,
env?: Record<string, string>,
): Promise<number> {
return new Promise((resolve, reject) => {
console.log(`启动进程: ${command} ${args.join(' ')} (目录: ${cwd || '当前目录'})`);

// 合并 process.env 和用户传入的 env
const child = spawn(command, args, {
cwd,
stdio: 'inherit',
shell: true,
env: { ...process.env, ...env }, // 继承系统环境变量并合并用户传入的变量
});

console.log(`进程 PID: ${child.pid}`);

child.on('error', (error) => {
console.error(`进程启动失败: ${error.message}`);
reject(error);
});

child.on('exit', (code) => {
console.log(`进程退出,代码: ${code}`);
resolve(code ?? -1);
});

resolve(child.pid ?? -1);
});
}

/**
* 根据端口号查找进程 PID
* @param port 端口号
* @returns Promise<number | null> 返回 PID(如果找到)
*/
export function getPidByPort(port: number): Promise<number | null> {
return new Promise((resolve, reject) => {
const platform = process.platform;

let command = '';
if (platform === 'win32') {
command = `netstat -ano | findstr :${port}`;
} else {
command = `lsof -i :${port} | grep LISTEN`;
}

exec(command, (error, stdout) => {
if (error) {
return resolve(null);
}

const lines = stdout.split('\n').filter(Boolean);
if (lines.length === 0) {
return resolve(null); // 没有找到进程
}

let pid: number | null = null;
if (platform === 'win32') {
const match = lines[0].trim().split(/\s+/).pop();
if (match) {
pid = parseInt(match, 10);
}
} else {
const match = lines[0].trim().split(/\s+/)[1];
if (match) {
pid = parseInt(match, 10);
}
}

resolve(pid || null);
});
});
}
Loading