esa-cli
Version:
A CLI for operating Alibaba Cloud ESA EdgeRoutine (Edge Functions).
236 lines (235 loc) • 9.74 kB
JavaScript
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
};
import * as http from 'http';
import spawn from 'cross-spawn';
import fetch from 'node-fetch';
import logger from '../../../libs/logger.js';
import { getRoot } from '../../../utils/fileUtils/base.js';
import { EW2BinPath } from '../../../utils/installEw2.js';
import { HttpProxyAgent } from 'http-proxy-agent';
import chalk from 'chalk';
import t from '../../../i18n/index.js';
import sleep from '../../../utils/sleep.js';
const getColorForStatusCode = (statusCode, message) => {
if (statusCode >= 100 && statusCode < 200) {
return chalk.blue(`${statusCode} ${message}`);
}
else if (statusCode >= 200 && statusCode < 300) {
return chalk.green(`${statusCode} ${message}`);
}
else if (statusCode >= 300 && statusCode < 400) {
return chalk.yellow(`${statusCode} ${message}`);
}
else if (statusCode >= 400 && statusCode < 500) {
return chalk.red(`${statusCode} ${message}`);
}
else if (statusCode >= 500) {
return chalk.magenta(chalk.bold(`${statusCode} ${message}`));
}
else {
return `${statusCode} ${message}`;
}
};
class Ew2Server {
constructor(props) {
this.worker = null;
this.startingWorker = false;
this.workerStartTimeout = undefined;
this.server = null;
this.restarting = false;
this.port = 18080;
// @ts-ignore
if (global.port)
this.port = global.port;
if (props.port)
this.port = props.port;
if (props.onClose)
this.onClose = props.onClose;
}
start() {
return __awaiter(this, void 0, void 0, function* () {
this.startingWorker = true;
const result = yield this.openEdgeWorker();
if (!result) {
throw new Error('Worker start failed');
}
this.createServer();
});
}
openEdgeWorker() {
if (this.worker) {
return Promise.resolve();
}
const root = getRoot();
// @ts-ignore
const id = global.id || '';
return new Promise((resolve, reject) => {
var _a, _b, _c;
this.worker = spawn(EW2BinPath, [
'--config_file',
`${root}/.dev/config-${id}.toml`,
'--log_stdout',
'-v'
], {
stdio: ['pipe', 'pipe', 'pipe']
});
this.workerStartTimeout = setTimeout(() => {
reject(new Error(t('dev_worker_timeout').d('Worker start timeout')));
this.worker && this.worker.kill();
}, 60000);
const sendToRuntime = () => {
return new Promise((resolveStart) => {
// @ts-ignore
const ew2Port = global.ew2Port;
const options = {
hostname: '127.0.0.1',
port: ew2Port,
method: 'GET'
};
const req = http.get(options, (res) => {
resolveStart(res.statusCode);
});
req.on('error', (err) => {
resolveStart(null);
});
req.end();
});
};
const checkRuntimeStart = () => __awaiter(this, void 0, void 0, function* () {
while (this.startingWorker) {
const [result] = yield Promise.all([sendToRuntime(), sleep(500)]);
if (result) {
this.startingWorker = false;
this.clearTimeout();
resolve(result);
}
}
});
checkRuntimeStart();
(_a = this.worker.stdout) === null || _a === void 0 ? void 0 : _a.setEncoding('utf8');
(_b = this.worker.stdout) === null || _b === void 0 ? void 0 : _b.on('data', this.stdoutHandler.bind(this));
(_c = this.worker.stderr) === null || _c === void 0 ? void 0 : _c.on('data', this.stderrHandler.bind(this));
this.worker.on('close', this.closeHandler.bind(this));
this.worker.on('error', this.errorHandler.bind(this));
process.on('SIGTERM', () => {
this.worker && this.worker.kill();
});
});
}
clearTimeout() {
clearTimeout(this.workerStartTimeout);
}
createServer() {
this.server = http.createServer((req, res) => __awaiter(this, void 0, void 0, function* () {
try {
const host = req.headers.host;
const url = req.url;
// @ts-ignore
const ew2Port = global.ew2Port;
// @ts-ignore
const localUpstream = global.localUpstream;
const workerRes = yield fetch(`http://${localUpstream ? localUpstream : host}${url}`, {
method: 'GET',
headers: {
'x-er-context': 'eyJzaXRlX2lkIjogIjYyMjcxODQ0NjgwNjA4IiwgInNpdGVfbmFtZSI6ICJjb21wdXRlbHguYWxpY2RuLXRlc3QuY29tIiwgInNpdGVfcmVjb3JkIjogIm1vY2hlbi1uY2RuLmNvbXB1dGVseC5hbGljZG4tdGVzdC5jb20iLCAiYWxpdWlkIjogIjEzMjI0OTI2ODY2NjU2MDgiLCAic2NoZW1lIjoiaHR0cCIsICAiaW1hZ2VfZW5hYmxlIjogdHJ1ZX0=',
'x-er-id': 'a.bA'
},
agent: new HttpProxyAgent(`http://127.0.0.1:${ew2Port}`)
});
const workerHeaders = Object.fromEntries(workerRes.headers.entries());
// 解决 gzip 兼容性问题,防止net::ERR_CONTENT_DECODING_FAILED
workerHeaders['content-encoding'] = 'identity';
if (workerRes.body) {
// if (workerRes.headers.get('content-type')?.includes('text/')) {
// const text = await workerRes.text();
// // 出现换行符之类会导致 content-length 不一致
// workerHeaders['content-length'] =
// Buffer.byteLength(text).toString();
// console.log(workerHeaders['content-length']);
// res.writeHead(workerRes.status, workerHeaders);
// res.end(text);
// } else {
// res.writeHead(workerRes.status, workerHeaders);
// workerRes.body.pipe(res);
// }
res.writeHead(workerRes.status, workerHeaders);
workerRes.body.pipe(res);
logger.log(`[ESA Dev] ${req.method} ${url} ${getColorForStatusCode(workerRes.status, workerRes.statusText)}`);
}
else {
res.writeHead(500, { 'Content-Type': 'text/plain' });
res.end('EW2 return null');
}
}
catch (err) {
console.log(err);
}
}));
this.server.listen(this.port, () => {
logger.log(`listening on port ${this.port}`);
});
}
stdoutHandler(chunk) {
logger.log(`${chalk.bgGreen('[Worker]')} ${chunk.toString().trim()}`);
}
stderrHandler(chunk) {
logger.subError(`${chalk.bgGreen('[Worker Error]')} ${chunk.toString().trim()}`);
}
errorHandler(error) {
logger.error(error.message ? error.message : error);
if (error.code && error.code === 'EACCES') {
logger.pathEacces(EW2BinPath);
}
this.stop();
}
closeHandler(code, signal) {
if (this.restarting) {
this.restarting = false;
return;
}
this.stop().then(() => {
logger.log(t('dev_server_closed').d('Worker server closed'));
logger.info('Worker server closed');
// @ts-ignore
global.port = undefined;
this.onClose && this.onClose();
});
}
runCommand(command) {
var _a, _b;
(_b = (_a = this.worker) === null || _a === void 0 ? void 0 : _a.stdin) === null || _b === void 0 ? void 0 : _b.write(command);
}
stop() {
return new Promise((resolve) => {
var _a;
if (!this.worker) {
resolve(false);
return;
}
const onExit = (code, signal) => {
this.worker = null;
resolve(true);
};
this.worker.on('exit', onExit);
this.worker.kill('SIGTERM');
(_a = this.server) === null || _a === void 0 ? void 0 : _a.close();
});
}
restart() {
return __awaiter(this, void 0, void 0, function* () {
this.restarting = true;
yield this.stop();
this.start();
logger.log(t('dev_server_restart').d('Worker server restarted'));
logger.info('Worker server restarted');
});
}
}
export default Ew2Server;