UNPKG

esa-cli

Version:

A CLI for operating Alibaba Cloud ESA Functions and Pages.

371 lines (370 loc) 15.2 kB
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;