mirror of
https://github.com/SPLURT-Station/S.P.L.U.R.T-Station-13.git
synced 2025-12-11 10:22:13 +00:00
tguichat and tgui4.1?
This commit is contained in:
87
tgui/packages/tgui-dev-server/dreamseeker.js
Normal file
87
tgui/packages/tgui-dev-server/dreamseeker.js
Normal 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;
|
||||
@@ -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) {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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)`);
|
||||
};
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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',
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -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));
|
||||
});
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user