tguichat and tgui4.1?

This commit is contained in:
Letter N
2020-08-15 16:08:10 +08:00
parent 90234ca065
commit 15e659fc9c
154 changed files with 9353 additions and 1091 deletions

View File

@@ -0,0 +1,87 @@
/**
* @file
* @copyright 2020 Aleksej Komarov
* @license MIT
*/
import axios from 'axios';
import { exec } from 'child_process';
import { createLogger } from 'common/logging.js';
import { promisify } from 'util';
const logger = createLogger('dreamseeker');
const instanceByPid = new Map();
export class DreamSeeker {
constructor(pid, addr) {
this.pid = pid;
this.addr = addr;
this.client = axios.create({
baseURL: `http://${addr}/`,
});
}
topic(params = {}) {
const query = Object.keys(params)
.map(key => encodeURIComponent(key)
+ '=' + encodeURIComponent(params[key]))
.join('&');
return this.client.get('/dummy?' + query);
}
}
/**
* @param {number[]} pids
* @returns {DreamSeeker[]}
*/
DreamSeeker.getInstancesByPids = async pids => {
if (process.platform !== 'win32') {
return [];
}
const instances = [];
const pidsToResolve = [];
for (let pid of pids) {
const instance = instanceByPid.get(pid);
if (instance) {
instances.push(instance);
}
else {
pidsToResolve.push(pid);
}
}
if (pidsToResolve.length > 0) {
try {
const command = 'netstat -a -n -o';
const { stdout } = await promisify(exec)(command);
// Line format:
// proto addr mask mode pid
const entries = stdout
.split('\r\n')
.filter(line => line.includes('LISTENING'))
.map(line => {
const words = line.match(/\S+/g);
return {
addr: words[1],
pid: parseInt(words[4], 10),
};
})
.filter(entry => pidsToResolve.includes(entry.pid));
const len = entries.length;
logger.log('found', len, plural('instance', len));
for (let entry of entries) {
const { pid, addr } = entry;
const instance = new DreamSeeker(pid, addr);
instances.push(instance);
instanceByPid.set(pid, instance);
}
}
catch (err) {
logger.error(err);
return [];
}
}
return instances;
};
const plural = (word, n) => n !== 1 ? word + 's' : word;

View File

@@ -8,12 +8,15 @@ import { setupWebpack, getWebpackConfig } from './webpack.js';
import { reloadByondCache } from './reloader.js';
const noHot = process.argv.includes('--no-hot');
const noTmp = process.argv.includes('--no-tmp');
const reloadOnce = process.argv.includes('--reload');
const setupServer = async () => {
const config = await getWebpackConfig({
mode: 'development',
hot: !noHot,
devServer: true,
useTmpFolder: !noTmp,
});
// Reload cache once
if (reloadOnce) {

View File

@@ -37,7 +37,7 @@ if (process.env.NODE_ENV !== 'production') {
window.onunload = () => socket && socket.close();
}
const subscribe = fn => subscribers.push(fn);
export const subscribe = fn => subscribers.push(fn);
/**
* A json serializer which handles circular references and other junk.
@@ -68,7 +68,10 @@ const serializeObject = obj => {
}
refs.push(value);
// Error object
if (value instanceof Error) {
const isError = value instanceof Error || (
value.code && value.message && value.message.includes('Error')
);
if (isError) {
return {
__error__: true,
string: String(value),
@@ -88,7 +91,7 @@ const serializeObject = obj => {
return json;
};
const sendRawMessage = msg => {
export const sendMessage = msg => {
if (process.env.NODE_ENV !== 'production') {
const json = serializeObject(msg);
// Send message using WebSocket
@@ -109,8 +112,8 @@ const sendRawMessage = msg => {
else {
const DEV_SERVER_IP = process.env.DEV_SERVER_IP || '127.0.0.1';
const req = new XMLHttpRequest();
req.open('POST', `http://${DEV_SERVER_IP}:3001`);
req.timeout = 500;
req.open('POST', `http://${DEV_SERVER_IP}:3001`, true);
req.timeout = 250;
req.send(json);
}
}
@@ -119,7 +122,7 @@ const sendRawMessage = msg => {
export const sendLogEntry = (level, ns, ...args) => {
if (process.env.NODE_ENV !== 'production') {
try {
sendRawMessage({
sendMessage({
type: 'log',
payload: {
level,

View File

@@ -16,37 +16,108 @@ const DEBUG = process.argv.includes('--debug');
export { loadSourceMaps };
export const setupLink = () => {
logger.log('setting up');
const wss = setupWebSocketLink();
setupHttpLink();
return {
wss,
};
};
export const setupLink = () => new LinkServer();
export const broadcastMessage = (link, msg) => {
const { wss } = link;
const clients = [...wss.clients];
logger.log(`broadcasting ${msg.type} to ${clients.length} clients`);
for (let client of clients) {
const json = JSON.stringify(msg);
client.send(json);
class LinkServer {
constructor() {
logger.log('setting up');
this.wss = null;
this.setupWebSocketLink();
this.setupHttpLink();
}
};
// WebSocket-based client link
setupWebSocketLink() {
const port = 3000;
this.wss = new WebSocket.Server({ port });
this.wss.on('connection', ws => {
logger.log('client connected');
ws.on('message', json => {
const msg = deserializeObject(json);
this.handleLinkMessage(ws, msg);
});
ws.on('close', () => {
logger.log('client disconnected');
});
});
logger.log(`listening on port ${port} (WebSocket)`);
}
// One way HTTP-based client link for IE8
setupHttpLink() {
const port = 3001;
this.httpServer = http.createServer((req, res) => {
if (req.method === 'POST') {
let body = '';
req.on('data', chunk => {
body += chunk.toString();
});
req.on('end', () => {
const msg = deserializeObject(body);
this.handleLinkMessage(null, msg);
res.end();
});
return;
}
res.write('Hello');
res.end();
});
this.httpServer.listen(port);
logger.log(`listening on port ${port} (HTTP)`);
}
handleLinkMessage(ws, msg) {
const { type, payload } = msg;
if (type === 'log') {
const { level, ns, args } = payload;
// Skip debug messages
if (level <= 0 && !DEBUG) {
return;
}
directLog(ns, ...args.map(arg => {
if (typeof arg === 'object') {
return inspect(arg, {
depth: Infinity,
colors: true,
compact: 8,
});
}
return arg;
}));
return;
}
if (type === 'relay') {
for (let client of this.wss.clients) {
if (client === ws) {
continue;
}
this.sendMessage(client, msg);
}
return;
}
logger.log('unhandled message', msg);
}
sendMessage(ws, msg) {
ws.send(JSON.stringify(msg));
}
broadcastMessage(msg) {
const clients = [...this.wss.clients];
if (clients.length === 0) {
return;
}
logger.log(`broadcasting ${msg.type} to ${clients.length} clients`);
for (let client of clients) {
const json = JSON.stringify(msg);
client.send(json);
}
}
}
const deserializeObject = str => {
return JSON.parse(str, (key, value) => {
if (typeof value === 'object' && value !== null) {
if (value.__error__) {
if (!value.stack) {
return value.string;
}
return retrace(value.stack);
}
if (value.__number__) {
return parseFloat(value.__number__);
}
if (value.__undefined__) {
// NOTE: You should not rely on deserialized object's undefined,
// this is purely for inspection purposes.
@@ -54,80 +125,17 @@ const deserializeObject = str => {
[inspect.custom]: () => undefined,
};
}
if (value.__number__) {
return parseFloat(value.__number__);
}
if (value.__error__) {
if (!value.stack) {
return value.string;
}
return retrace(value.stack);
}
return value;
}
return value;
});
};
const handleLinkMessage = msg => {
const { type, payload } = msg;
if (type === 'log') {
const { level, ns, args } = payload;
// Skip debug messages
if (level <= 0 && !DEBUG) {
return;
}
directLog(ns, ...args.map(arg => {
if (typeof arg === 'object') {
return inspect(arg, {
depth: Infinity,
colors: true,
compact: 8,
});
}
return arg;
}));
return;
}
logger.log('unhandled message', msg);
};
// WebSocket-based client link
const setupWebSocketLink = () => {
const port = 3000;
const wss = new WebSocket.Server({ port });
wss.on('connection', ws => {
logger.log('client connected');
ws.on('message', json => {
const msg = deserializeObject(json);
handleLinkMessage(msg);
});
ws.on('close', () => {
logger.log('client disconnected');
});
});
logger.log(`listening on port ${port} (WebSocket)`);
return wss;
};
// One way HTTP-based client link for IE8
const setupHttpLink = () => {
const port = 3001;
const server = http.createServer((req, res) => {
if (req.method === 'POST') {
let body = '';
req.on('data', chunk => {
body += chunk.toString();
});
req.on('end', () => {
const msg = deserializeObject(body);
handleLinkMessage(msg);
res.end();
});
return;
}
res.write('Hello');
res.end();
});
server.listen(port);
logger.log(`listening on port ${port} (HTTP)`);
};

View File

@@ -1,9 +1,10 @@
{
"private": true,
"name": "tgui-dev-server",
"version": "3.0.0",
"version": "4.1.0",
"type": "module",
"dependencies": {
"axios": "^0.19.2",
"glob": "^7.1.4",
"source-map": "^0.7.3",
"stacktrace-parser": "^0.1.7",

View File

@@ -11,6 +11,7 @@ import { basename } from 'path';
import { promisify } from 'util';
import { resolveGlob, resolvePath } from './util.js';
import { regQuery } from './winreg.js';
import { DreamSeeker } from './dreamseeker.js';
const logger = createLogger('reloader');
@@ -43,7 +44,7 @@ export const findCacheRoot = async () => {
const paths = await resolveGlob(pattern);
if (paths.length > 0) {
cacheRoot = paths[0];
logger.log(`found cache at '${cacheRoot}'`);
onCacheRootFound(cacheRoot);
return cacheRoot;
}
}
@@ -58,13 +59,19 @@ export const findCacheRoot = async () => {
.replace(/\\$/, '')
.replace(/\\/g, '/')
+ '/cache';
logger.log(`found cache at '${cacheRoot}'`);
onCacheRootFound(cacheRoot);
return cacheRoot;
}
}
logger.log('found no cache directories');
};
const onCacheRootFound = cacheRoot => {
logger.log(`found cache at '${cacheRoot}'`);
// Plant dummy
fs.closeSync(fs.openSync(cacheRoot + '/dummy', 'w'));
};
export const reloadByondCache = async bundleDir => {
const cacheRoot = await findCacheRoot();
if (!cacheRoot) {
@@ -76,10 +83,16 @@ export const reloadByondCache = async bundleDir => {
logger.log('found no tmp folder in cache');
return;
}
const assets = await resolveGlob(bundleDir, './*.+(bundle|hot-update).*');
// Get dreamseeker instances
const pids = cacheDirs.map(cacheDir => (
parseInt(cacheDir.split('/cache/tmp').pop(), 10)
));
const dssPromise = DreamSeeker.getInstancesByPids(pids);
// Copy assets
const assets = await resolveGlob(bundleDir, './*.+(bundle|chunk|hot-update).*');
for (let cacheDir of cacheDirs) {
// Clear garbage
const garbage = await resolveGlob(cacheDir, './*.+(bundle|hot-update).*');
const garbage = await resolveGlob(cacheDir, './*.+(bundle|chunk|hot-update).*');
for (let file of garbage) {
await promisify(fs.unlink)(file);
}
@@ -90,4 +103,15 @@ export const reloadByondCache = async bundleDir => {
}
logger.log(`copied ${assets.length} files to '${cacheDir}'`);
}
// Notify dreamseeker
const dss = await dssPromise;
if (dss.length > 0) {
logger.log(`notifying dreamseeker`);
for (let dreamseeker of dss) {
dreamseeker.topic({
tgui: 1,
type: 'cacheReloaded',
});
}
}
};

View File

@@ -9,7 +9,7 @@ import fs from 'fs';
import { createRequire } from 'module';
import { promisify } from 'util';
import webpack from 'webpack';
import { broadcastMessage, loadSourceMaps, setupLink } from './link/server.js';
import { loadSourceMaps, setupLink } from './link/server.js';
import { reloadByondCache } from './reloader.js';
import { resolveGlob } from './util.js';
@@ -44,7 +44,7 @@ export const setupWebpack = async config => {
// Reload cache
await reloadByondCache(bundleDir);
// Notify all clients that update has happened
broadcastMessage(link, {
link.broadcastMessage({
type: 'hotUpdate',
});
});
@@ -55,6 +55,9 @@ export const setupWebpack = async config => {
logger.error('compilation error', err);
return;
}
logger.log(stats.toString(config.devServer.stats));
stats
.toString(config.devServer.stats)
.split('\n')
.forEach(line => logger.log(line));
});
};