Skip to content

Commit 43c3a40

Browse files
committed
sentinel: Support selfpatching
1 parent 5b166a7 commit 43c3a40

11 files changed

Lines changed: 86 additions & 52 deletions

File tree

src/discord/loaders/commands.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import { promises as fs } from 'fs';
44
import { DiscCommands } from '@/cache';
55
import { resetCache } from '@/cache/reset';
66
import { clientId, token } from '@/config/discord';
7-
import { cachebuster } from '@/utils/cachebuster';
7+
import { cachebust } from '@/utils/cachebust';
88
import { fsPath } from '@/utils/fsPath';
99
import { log } from '@/utils/logger';
1010

@@ -66,7 +66,7 @@ export async function loadCommands(): Promise<void> {
6666

6767
export function unloadCommands(): void {
6868
// Delete require cache for commands
69-
Object.values(DiscCommands).forEach(({ path }) => cachebuster(path));
69+
Object.values(DiscCommands).forEach(({ path }) => cachebust(path));
7070
resetCache('DiscCommands');
7171
}
7272

src/eval.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,10 @@ import _path from 'path';
33
import { inspect } from 'util';
44

55
import * as _cache from '@/cache';
6+
import _Sentinel from '@/sentinel';
67
import * as _Tools from '@/tools';
78
import { ansiToHtml } from '@/utils/ansiToHtml';
8-
import { cachebuster as _cachebuster } from '@/utils/cachebuster';
9+
import { cachebust as _cachebuster } from '@/utils/cachebust';
910
import { $ as _$ } from '@/utils/child_process';
1011
import { fsPath as _fsPath } from '@/utils/fsPath';
1112
import { log as _log } from '@/utils/logger';
@@ -23,9 +24,10 @@ const log = _log;
2324
const path = _path;
2425
const Tools = _Tools;
2526
const $ = _$;
27+
const Sentinel = _Sentinel;
2628

2729
// Storing in context for eval()
28-
const _evalContext = [cache, cachebuster, fs, fsSync, fsPath, log, path, Tools, $];
30+
const _evalContext = [cache, cachebuster, fs, fsSync, fsPath, log, path, Tools, $, Sentinel];
2931

3032
export type EvalModes = 'COLOR_OUTPUT' | 'FULL_OUTPUT' | 'ABBR_OUTPUT' | 'NO_OUTPUT';
3133
export type EvalOutput = {

src/ps/loaders/commands.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import path from 'path';
44
import { PSAliases, PSCommands } from '@/cache';
55
import { resetCache } from '@/cache/reset';
66
import getSecretCommands from '@/secrets/ps';
7-
import { cachebuster } from '@/utils/cachebuster';
7+
import { cachebust } from '@/utils/cachebust';
88
import { fsPath } from '@/utils/fsPath';
99

1010
import type { PSCommand } from '@/types/chat';
@@ -67,7 +67,7 @@ export async function loadCommands(): Promise<void> {
6767

6868
export function unloadCommands(): void {
6969
// Delete cached commands
70-
Object.values(PSCommands).forEach(({ path }) => cachebuster(path));
70+
Object.values(PSCommands).forEach(({ path }) => cachebust(path));
7171
// Delete command data and aliases
7272
resetCache('PSCommands', 'PSAliases');
7373
}
Lines changed: 4 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,12 @@
1-
import { watch } from 'chokidar';
2-
import EventEmitter from 'events';
1+
import { type FSWatcher, watch } from 'chokidar';
32

43
import { registers } from '@/sentinel/registers';
54
import { debounce } from '@/utils/debounce';
65
import { fsPath } from '@/utils/fsPath';
76

8-
import type { EmitterEvents, Listener, Register } from '@/sentinel/types';
9-
import type { FSWatcher } from 'chokidar';
10-
11-
class Emitter extends EventEmitter {
12-
emit<K extends keyof EmitterEvents>(event: K, ...args: EmitterEvents[K]): boolean {
13-
return super.emit(event, ...args);
14-
}
15-
on<K extends keyof EmitterEvents>(event: K, listener: (...args: EmitterEvents[K]) => void): this {
16-
return super.on(event, listener);
17-
}
18-
}
19-
20-
export default function createSentinel(): { emitter: Emitter; sentinel: FSWatcher } {
21-
const emitter = new Emitter();
7+
import type { Emitter, Listener, Register } from '@/sentinel/types';
228

9+
export function create(emitter: Emitter): FSWatcher {
2310
const listeners: Listener[] = registers.list
2411
.map(
2512
// Add debouncing
@@ -52,5 +39,5 @@ export default function createSentinel(): { emitter: Emitter; sentinel: FSWatche
5239
sentinel.on('all', async (event, filepath) => {
5340
listeners.find(({ pattern }) => pattern.test(filepath))?.reload(filepath);
5441
});
55-
return { emitter, sentinel };
42+
return sentinel;
5643
}

src/sentinel/hotpatch.ts

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,18 @@
11
import { promises as fs } from 'fs';
22
import path from 'path';
3-
import { update } from 'ps-client/tools';
3+
import { update as updatePSData } from 'ps-client/tools';
44

55
import { registers } from '@/sentinel/registers';
6+
import { cachebust, cachebustDir } from '@/utils/cachebust';
67
import { $ } from '@/utils/child_process';
78
import { fsPath } from '@/utils/fsPath';
89
import { errorLog, log } from '@/utils/logger';
910

11+
import type { Sentinel } from '@/sentinel/types';
12+
1013
export type HotpatchType = 'code' | 'data' | string;
1114

12-
export async function hotpatch(hotpatchType: HotpatchType, by: string | symbol): Promise<void> {
15+
export async function hotpatch(this: Sentinel, hotpatchType: HotpatchType, by: string | symbol): Promise<void> {
1316
if (!hotpatchType) throw new TypeError('Missing hotpatchType');
1417
try {
1518
// Hardcoded variants
@@ -19,10 +22,18 @@ export async function hotpatch(hotpatchType: HotpatchType, by: string | symbol):
1922
break;
2023
}
2124
case 'data': {
22-
await update();
25+
await updatePSData();
2326
// TODO: cachebust
2427
break;
2528
}
29+
case 'sentinel': {
30+
cachebust('@/sentinel/create');
31+
await cachebustDir(fsPath('sentinel', 'registers'));
32+
const newSentinel = await import('@/sentinel/create');
33+
this.sentinel.close();
34+
this.sentinel = newSentinel.create(this.emitter);
35+
break;
36+
}
2637
default:
2738
const register = registers.list.find(register => register.label === hotpatchType);
2839
if (register) {

src/sentinel/index.ts

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,14 @@
1-
import createSentinel from '@/sentinel/sentinel';
1+
import EventEmitter from 'events';
2+
3+
import { create } from '@/sentinel/create';
4+
import { hotpatch } from '@/sentinel/hotpatch';
5+
import { Sentinel } from '@/sentinel/types';
26
import { log } from '@/utils/logger';
3-
const { emitter, sentinel } = createSentinel();
7+
8+
import type { Emitter } from '@/sentinel/types';
9+
10+
const emitter = new EventEmitter() as Emitter;
11+
const sentinel = create(emitter);
412

513
emitter.on('complete', (label, files) => {
614
log(`Reloaded ${label} with ${files.join(', ')}`);
@@ -10,4 +18,6 @@ emitter.on('error', (err, label, files) => {
1018
log(`Ran into an error while reloading ${label} for ${files.join(', ')}`, err);
1119
});
1220

13-
export { emitter, sentinel };
21+
const Sentinel: Sentinel = { hotpatch, sentinel, emitter };
22+
23+
export default Sentinel;

src/sentinel/registers/ps.ts

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { promises as fs } from 'fs';
33
import { Games } from '@/ps/games';
44
import { reloadCommands } from '@/ps/loaders/commands';
55
import { LivePS } from '@/sentinel/live';
6-
import { cachebuster } from '@/utils/cachebuster';
6+
import { cachebust } from '@/utils/cachebust';
77
import { fsPath } from '@/utils/fsPath';
88

99
import type { GamesList, Meta } from '@/ps/games/common';
@@ -23,7 +23,7 @@ export const PS_REGISTERS: Register[] = [
2323
label: 'commands',
2424
pattern: /\/ps\/commands\//,
2525
reload: async filepaths => {
26-
filepaths.forEach(cachebuster);
26+
filepaths.forEach(cachebust);
2727
return reloadCommands();
2828
},
2929
},
@@ -32,15 +32,15 @@ export const PS_REGISTERS: Register[] = [
3232
label: 'games',
3333
pattern: /\/ps\/games\//,
3434
reload: async () => {
35-
['common', 'game', 'index', 'render'].forEach(file => cachebuster(`@/ps/games/${file}`));
35+
['common', 'game', 'index', 'render'].forEach(file => cachebust(`@/ps/games/${file}`));
3636
const games = await fs.readdir(fsPath('ps', 'games'), { withFileTypes: true });
3737
await Promise.all(
3838
games
3939
.filter(game => game.isDirectory())
4040
.map(async game => {
4141
const gameDir = game.name as GamesList;
4242
const files = await fs.readdir(fsPath('ps', 'games', gameDir));
43-
files.forEach(file => cachebuster(fsPath('ps', 'games', gameDir, file)));
43+
files.forEach(file => cachebust(fsPath('ps', 'games', gameDir, file)));
4444

4545
const gameImport = await import(`@/ps/games/${gameDir}`);
4646
const { meta }: { meta: Meta } = gameImport;
@@ -51,7 +51,7 @@ export const PS_REGISTERS: Register[] = [
5151
);
5252

5353
const gameCommands = await fs.readdir(fsPath('ps', 'commands', 'games'));
54-
gameCommands.forEach(commandFile => cachebuster(fsPath('ps', 'commands', 'games', commandFile)));
54+
gameCommands.forEach(commandFile => cachebust(fsPath('ps', 'commands', 'games', commandFile)));
5555
await reloadCommands();
5656
},
5757
},
@@ -63,17 +63,17 @@ export const PS_REGISTERS: Register[] = [
6363
await Promise.all(
6464
(<const>['parse', 'permissions', 'spoof']).map(async file => {
6565
const importPath = `@/ps/handlers/commands/${file}`;
66-
cachebuster(importPath);
66+
cachebust(importPath);
6767
const hotHandler = await import(importPath);
6868
LivePS.commands[file] = hotHandler[file];
6969
})
7070
);
7171

72-
cachebuster('@/ps/handlers/commands/customPerms');
72+
cachebust('@/ps/handlers/commands/customPerms');
7373
const { GROUPED_PERMS: newGroupedPerms } = await import('@/ps/handlers/commands/customPerms');
7474
LivePS.commands.GROUPED_PERMS = newGroupedPerms;
7575

76-
cachebuster('@/ps/handlers/commands');
76+
cachebust('@/ps/handlers/commands');
7777
const { commandHandler } = await import('@/ps/handlers/commands');
7878
LivePS.commands.commandHandler = commandHandler;
7979
},
@@ -84,7 +84,7 @@ export const PS_REGISTERS: Register[] = [
8484
label,
8585
pattern: new RegExp(`\\/ps\\/handlers\\/${handlerData.fileName}`),
8686
reload: async () => {
87-
cachebuster(handlerData.importPath);
87+
cachebust(handlerData.importPath);
8888
const hotHandler = await import(handlerData.importPath);
8989
handlerData.imports.forEach(namedImport => (LivePS[namedImport] = hotHandler[namedImport]));
9090
},

src/sentinel/types.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,19 @@
1+
import type { hotpatch } from '@/sentinel/hotpatch';
2+
import type { FSWatcher } from 'chokidar';
3+
import type EventEmitter from 'events';
4+
15
export interface EmitterEvents {
26
trigger: [label: string, file: string];
37
start: [label: string, files: string[]];
48
complete: [label: string, files: string[]];
59
error: [error: Error, label: string, files: string[]];
610
}
711

12+
export interface Emitter extends EventEmitter {
13+
emit<K extends keyof EmitterEvents>(event: K, ...args: EmitterEvents[K]): boolean;
14+
on<K extends keyof EmitterEvents>(event: K, listener: (...args: EmitterEvents[K]) => void): this;
15+
}
16+
export type Sentinel = { emitter: Emitter; sentinel: FSWatcher; hotpatch: typeof hotpatch };
17+
818
export type Register = { label: string; pattern: RegExp; reload: (filepaths: string[]) => Promise<void> | void; debounce?: number };
919
export type Listener = { label: string; pattern: RegExp; reload: (filepaths: string) => Promise<void> | void };

src/utils/cachebust.ts

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
import { promises as fs } from 'fs';
2+
import path from 'path';
3+
4+
import { emptyObject } from '@/utils/emptyObject';
5+
6+
export function cachebust(_filepath: string): boolean {
7+
const filepath = _filepath.startsWith('/') ? _filepath : require.resolve(_filepath);
8+
const cache = require.cache[filepath];
9+
if (!cache) return false;
10+
emptyObject(cache.exports);
11+
cache.children.length = 0;
12+
emptyObject(cache);
13+
delete require.cache[filepath];
14+
return true;
15+
}
16+
17+
export async function cachebustDir(_dir: string): Promise<boolean> {
18+
const entries = await fs.readdir(_dir, { withFileTypes: true, recursive: true });
19+
const files = entries.filter(entry => entry.isFile());
20+
if (files.length === 0) return false;
21+
22+
files.forEach(file => {
23+
cachebust(path.join(file.parentPath, file.name));
24+
});
25+
return true;
26+
}

src/utils/cachebuster.ts

Lines changed: 0 additions & 12 deletions
This file was deleted.

0 commit comments

Comments
 (0)