esa-cli
Version:
A CLI for operating Alibaba Cloud ESA Functions and Pages.
371 lines (370 loc) • 15.2 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 chalk from 'chalk';
import spawn from 'cross-spawn';
import { HttpProxyAgent } from 'http-proxy-agent';
import fetch from 'node-fetch';
import t from '../../../i18n/index.js';
import logger from '../../../libs/logger.js';
import { getRoot } from '../../../utils/fileUtils/base.js';
import { EW2BinPath } from '../../../utils/installEw2.js';
import sleep from '../../../utils/sleep.js';
import CacheService from './cacheService.js';
import EdgeKV from './kvService.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.cache = null;
this.kv = 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();
this.cache = new CacheService();
this.kv = new EdgeKV();
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(() => {
var _a;
reject(new Error(t('dev_worker_timeout').d('Worker start timeout')));
(_a = this.worker) === null || _a === void 0 ? void 0 : _a.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', () => {
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', () => {
var _a;
(_a = this.worker) === null || _a === void 0 ? void 0 : _a.kill();
});
});
}
clearTimeout() {
clearTimeout(this.workerStartTimeout);
}
createServer() {
this.server = http.createServer((req, res) => __awaiter(this, void 0, void 0, function* () {
var _a, _b, _c;
if (req.url === '/favicon.ico') {
res.writeHead(204, {
'Content-Type': 'image/x-icon',
'Content-Length': 0
});
return res.end();
}
if ((_a = req.url) === null || _a === void 0 ? void 0 : _a.includes('/mock_cache')) {
const cacheResult = yield this.handleCache(req);
return res.end(JSON.stringify(cacheResult));
}
if ((_b = req.url) === null || _b === void 0 ? void 0 : _b.includes('/mock_kv')) {
const kvResult = yield this.handleKV(req);
if ((_c = req.url) === null || _c === void 0 ? void 0 : _c.includes('/get')) {
if (kvResult.success) {
return res.end(kvResult.value);
}
else {
res.setHeader('Kv-Get-Empty', 'true');
return res.end();
}
}
else {
return res.end(JSON.stringify(kvResult));
}
}
try {
const host = req.headers.host;
const url = req.url;
const method = req.method;
const headers = Object.entries(req.headers).reduce((acc, [key, value]) => {
if (Array.isArray(value)) {
acc[key] = value.join(', ');
}
else {
acc[key] = value;
}
return acc;
}, {});
// @ts-ignore
const ew2Port = global.ew2Port;
// @ts-ignore
const localUpstream = global.localUpstream;
const workerRes = yield fetch(`http://${localUpstream ? localUpstream : host}${url}`, {
method,
headers: Object.assign(Object.assign({}, headers), { 'x-er-context': 'eyJzaXRlX2lkIjogIjYyMjcxODQ0NjgwNjA4IiwgInNpdGVfbmFtZSI6ICJjb21wdXRlbHguYWxpY2RuLXRlc3QuY29tIiwgInNpdGVfcmVjb3JkIjogIm1vY2hlbi1uY2RuLmNvbXB1dGVseC5hbGljZG4tdGVzdC5jb20iLCAiYWxpdWlkIjogIjEzMjI0OTI2ODY2NjU2MDgiLCAic2NoZW1lIjoiaHR0cCIsICAiaW1hZ2VfZW5hYmxlIjogdHJ1ZX0=', 'x-er-id': 'a.bA' }),
body: req.method === 'GET' ? undefined : req,
agent: new HttpProxyAgent(`http://127.0.0.1:${ew2Port}`)
});
const workerHeaders = Object.fromEntries(workerRes.headers.entries());
// Solve gzip compatibility issue, prevent net::ERR_CONTENT_DECODING_FAILED
workerHeaders['content-encoding'] = 'identity';
if (workerRes.body) {
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}`);
});
}
handleCache(req) {
return __awaiter(this, void 0, void 0, function* () {
var _a, _b, _c, _d, _e, _f;
const body = yield this.parseCacheBody(req);
if ((_a = req.url) === null || _a === void 0 ? void 0 : _a.includes('/put')) {
(_b = this.cache) === null || _b === void 0 ? void 0 : _b.put(body.key, body);
return { success: true };
}
if ((_c = req.url) === null || _c === void 0 ? void 0 : _c.includes('/get')) {
const res = (_d = this.cache) === null || _d === void 0 ? void 0 : _d.get(body.key);
if (!res) {
return { success: false, key: body.key };
}
return { success: true, key: body.key, data: res === null || res === void 0 ? void 0 : res.serializedResponse };
}
if ((_e = req.url) === null || _e === void 0 ? void 0 : _e.includes('/delete')) {
const res = (_f = this.cache) === null || _f === void 0 ? void 0 : _f.delete(body.key);
return { success: !!res };
}
return { success: false };
});
}
handleKV(req) {
return __awaiter(this, void 0, void 0, function* () {
var _a, _b, _c, _d, _e, _f;
const url = new URL(req.url, 'http://localhost');
const key = url.searchParams.get('key');
const namespace = url.searchParams.get('namespace');
const body = yield this.parseKVBody(req);
if (!key || !namespace) {
return {
success: false
};
}
if ((_a = req.url) === null || _a === void 0 ? void 0 : _a.includes('/put')) {
(_b = this.kv) === null || _b === void 0 ? void 0 : _b.put(key, body, namespace);
return {
success: true
};
}
if ((_c = req.url) === null || _c === void 0 ? void 0 : _c.includes('/get')) {
const res = (_d = this.kv) === null || _d === void 0 ? void 0 : _d.get(key, namespace);
const params = { success: true, value: res };
if (!res) {
params.success = false;
}
return params;
}
if ((_e = req.url) === null || _e === void 0 ? void 0 : _e.includes('/delete')) {
const res = (_f = this.kv) === null || _f === void 0 ? void 0 : _f.delete(key, namespace);
return {
success: res
};
}
return {
success: false
};
});
}
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() {
if (this.restarting) {
this.restarting = false;
return;
}
this.stop().then(() => {
var _a;
logger.log(t('dev_server_closed').d('Worker server closed'));
logger.info('Worker server closed');
// @ts-ignore
global.port = undefined;
(_a = this.onClose) === null || _a === void 0 ? void 0 : _a.call(this);
});
}
parseCacheBody(req) {
return new Promise((resolve, reject) => {
const chunks = [];
let totalLength = 0;
req.on('data', (chunk) => {
chunks.push(chunk);
totalLength += chunk.length;
});
req.on('end', () => {
try {
const buffer = Buffer.concat(chunks, totalLength);
const rawBody = buffer.toString('utf8');
resolve(rawBody ? JSON.parse(rawBody) : {});
}
catch (err) {
reject(new Error(`Invalid JSON: ${err.message}`));
}
});
req.on('error', reject);
});
}
parseKVBody(req) {
return new Promise((resolve, reject) => {
const chunks = [];
let totalLength = 0;
req.on('data', (chunk) => {
chunks.push(chunk);
totalLength += chunk.length;
});
req.on('end', () => {
try {
const buffer = Buffer.concat(chunks, totalLength);
const rawBody = buffer.toString();
resolve(rawBody);
}
catch (err) {
reject(new Error(`Invalid JSON: ${err.message}`));
}
});
req.on('error', reject);
});
}
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 = () => {
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(devPack) {
return __awaiter(this, void 0, void 0, function* () {
this.restarting = true;
console.clear();
yield this.stop();
yield devPack();
this.start();
logger.log(t('dev_server_restart').d('Worker server restarted'));
logger.info('Worker server restarted');
});
}
}
export default Ew2Server;