greybel-js
Version:
Transpiler/Interpreter for GreyScript. (GreyHack)
266 lines • 10.6 kB
JavaScript
import { input } from '@inquirer/prompts';
import { ModifierType } from 'another-ansi';
import fs from 'fs';
import GreyHackMessageHookClientPkg from 'greyhack-message-hook-client';
import pathUtils from 'path';
import { configurationManager } from '../../helper/configuration-manager.js';
import { GlobalFileSystemManager } from '../../helper/fs.js';
import { logger } from '../../helper/logger.js';
import { randomString } from '../../helper/random-string.js';
import { VersionManager } from '../../helper/version-manager.js';
import { transformInternalKeyEventToKeyEvent } from '../key-event.js';
import { ansiProvider, Terminal, useColor } from '../output.js';
import { ClientMessageType } from '../types.js';
const { ContextAgent } = GreyHackMessageHookClientPkg;
async function resolveFileExtension(path, allowedFileExtension) {
return await GlobalFileSystemManager.findExistingPath(path, ...allowedFileExtension.map((ext) => `${path}.${ext}`));
}
function sessionExitHandler(options, exitCode) {
InGameSession.singleton?.stop();
}
process.on('exit', sessionExitHandler);
process.on('SIGINT', sessionExitHandler);
process.on('SIGUSR1', sessionExitHandler);
process.on('SIGUSR2', sessionExitHandler);
process.on('SIGTERM', sessionExitHandler);
process.on('uncaughtException', sessionExitHandler);
class InGameSession {
static singleton = null;
target;
debugMode;
envMapper;
instance;
terminal;
internalFileMap;
temporaryPath;
agent;
running = false;
basePath = null;
programName;
crashed = false;
constructor({ target, envMapper, debugMode = false, port = 7777, programName = 'myprogram' }) {
this.target = pathUtils.resolve(target);
this.debugMode = debugMode;
this.envMapper = envMapper;
this.programName = programName;
this.basePath = process.cwd();
this.agent = new ContextAgent({
warn: () => { },
error: () => { },
info: () => { },
debug: () => { }
}, port);
this.instance = null;
this.terminal = new Terminal();
this.internalFileMap = {};
this.temporaryPath = 'temp-' + randomString(10);
InGameSession.singleton = this;
}
async prepare() {
const healthcheck = await VersionManager.performHealthCheck(this.agent);
if (!healthcheck.isSingleplayer) {
logger.error('Can only start in-game debug session with singleplayer running!');
process.exit(1);
}
const resolvedPath = pathUtils.join(this.basePath, this.temporaryPath);
fs.mkdirSync(resolvedPath);
}
async run(params = []) {
const content = fs.readFileSync(this.target, 'utf8');
const { value } = await this.agent.createContext(`params=[${params
.map((it) => `"${it.replace(/"/g, '""')}"`)
.join(',')}];` + content, this.target, this.basePath, this.programName, this.debugMode, [], this.envMapper.toMap(true));
this.running = true;
this.instance = value;
this.registerMessageHandler();
await this.waitForFinished();
return !this.crashed;
}
async verifyFilepath(path) {
if (this.internalFileMap[path]) {
const resolvedPath = pathUtils.join(this.basePath, this.temporaryPath, `${path}.src`);
const content = this.internalFileMap[path];
fs.writeFileSync(resolvedPath, content);
return {
resolvedPath,
originalPath: path
};
}
const resolvedPath = await resolveFileExtension(path, configurationManager.get('fileExtensions'));
return {
resolvedPath: resolvedPath ?? path,
originalPath: path
};
}
async createFilepathMap(paths) {
const resolvedPaths = await Promise.all(paths.map(async (it) => this.verifyFilepath(it)));
return resolvedPaths.reduce((result, { resolvedPath, originalPath }) => {
result[originalPath] = resolvedPath;
return result;
}, {});
}
async parseContextBreakpoint(contextBreakpoint) {
const mappedPaths = await this.createFilepathMap([
contextBreakpoint.filepath,
...contextBreakpoint.stacktrace.map((it) => it.filepath)
]);
return {
contextID: contextBreakpoint.contextID,
filepath: mappedPaths[contextBreakpoint.filepath],
source: contextBreakpoint.source,
line: contextBreakpoint.line,
variables: contextBreakpoint.variables,
stacktrace: contextBreakpoint.stacktrace.map((it) => {
return {
filepath: mappedPaths[it.filepath],
lineNum: it.lineNum,
name: it.name,
isExternal: it.isExternal
};
})
};
}
registerMessageHandler() {
this.instance.on('receive', async (id, response) => {
switch (id) {
case ClientMessageType.FinishedContextRpc: {
this.crashed = response.failed;
this.stop();
break;
}
case ClientMessageType.ContextBreakpointRpc: {
logger.info(useColor('cyan', ansiProvider.modify(ModifierType.Bold, `REPL - Console`)));
logger.info(useColor('cyan', `You can execute code in the current context.`));
logger.info(``);
logger.info(useColor('cyan', `Press "next" or "exit" to either move to the next line or continue execution.`));
const lastBreakpoint = await this.parseContextBreakpoint(response);
const line = await input({
message: useColor('cyan', `${ansiProvider.modify(ModifierType.Bold, `[${lastBreakpoint.filepath}:${lastBreakpoint.line}]`)} >`)
});
if (line === 'next') {
await this.goToNextLine();
}
else if (line === 'exit') {
await this.setDebugMode(false);
}
break;
}
case ClientMessageType.DecipherTimeClientRpc: {
this.terminal.print('Deciphering...');
await this.endDecipher();
break;
}
case ClientMessageType.SendFileSizeClientRpc: {
this.terminal.print('Transfer...');
await this.endDownload();
break;
}
case ClientMessageType.ContextLoadFileRpc: {
await this.resolveFile(response.filepath);
break;
}
case ClientMessageType.ContextOpenInternalFileRpc: {
this.internalFileMap[response.filepath] = response.source
.replace(/\\n/g, '\n')
.replace(/\\t/g, '\t');
break;
}
case ClientMessageType.PrintSentClientRpc: {
this.terminal.print(response.output, response.replaceText);
break;
}
case ClientMessageType.ClearScreenClientRpc: {
this.terminal.clear();
break;
}
case ClientMessageType.InputSentClientRpc: {
if (response.anyKey) {
const key = await this.terminal.waitForKeyPress(response.output, () => this.stop());
this.instance.sendInput(transformInternalKeyEventToKeyEvent(key));
break;
}
const input = await this.terminal.waitForInput(response.isPassword, response.output);
this.instance.sendInput(input);
break;
}
case ClientMessageType.ContextUnexpectedErrorRpc: {
this.crashed = true;
await this.stop();
break;
}
}
});
}
async waitForFinished() {
return new Promise((resolve) => {
const interval = setInterval(() => {
if (!this.running) {
clearInterval(interval);
resolve();
}
}, 10);
});
}
async setDebugMode(debugMode) {
if (this.instance == null)
return;
await this.instance.setDebugMode(debugMode);
}
async endDecipher() {
if (this.instance == null)
return;
await this.instance.endDecipher();
}
async endDownload() {
if (this.instance == null)
return;
await this.instance.endDownload();
}
async goToNextLine() {
if (this.instance == null)
return;
await this.instance.goToNextLine();
}
async injectCode(context) {
if (this.instance == null)
return;
await this.instance.injectCode(context);
}
async resolveFile(path) {
if (this.instance == null)
return;
const resolvedPath = await resolveFileExtension(path, configurationManager.get('fileExtensions'));
if (resolvedPath == null) {
await this.instance.resolvedFile(path, null);
return;
}
const content = fs.readFileSync(resolvedPath, 'utf8');
await this.instance.resolvedFile(resolvedPath, content);
}
async stop() {
if (this.instance) {
const agent = this.agent;
const instance = this.instance;
const tempFolderPath = pathUtils.join(this.basePath, this.temporaryPath);
this.instance = null;
this.agent = null;
await instance.dispose().catch(console.warn);
agent.dispose().catch(console.warn);
try {
fs.rmSync(tempFolderPath, {
recursive: true,
force: true
});
}
catch (err) {
logger.warn(`Failed to delete temporary folder: ${tempFolderPath}`);
}
if (InGameSession.singleton === this) {
InGameSession.singleton = null;
}
this.running = false;
}
}
}
export { InGameSession };
//# sourceMappingURL=session.js.map