-
Notifications
You must be signed in to change notification settings - Fork 1
Expand file tree
/
Copy pathplugin-process.ts
More file actions
194 lines (159 loc) · 6.6 KB
/
Copy pathplugin-process.ts
File metadata and controls
194 lines (159 loc) · 6.6 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
import {
ApplyNoteRequestData,
ApplyNoteRequestDataSchema,
CommandRequestData,
CommandRequestDataSchema,
CommandRequestResponseData,
IpcMessageV2,
IpcMessageV2Schema,
MessageCmd,
PressKeyToContinueRequestData,
PressKeyToContinueRequestDataSchema,
} from '@codifycli/schemas';
import { ChildProcess, fork } from 'node:child_process';
import { createRequire } from 'node:module';
import { Event, ctx } from '../events/context.js';
import { ajv } from '../utils/ajv.js';
import { sendIpcMessageForResult } from './message-sender.js';
import { PluginMessage } from './plugin-message.js';
export const ipcMessageValidator = ajv.compile(IpcMessageV2Schema);
export const commandRequestValidator = ajv.compile(CommandRequestDataSchema);
export const pressKeyToContinueRequestValidator = ajv.compile(PressKeyToContinueRequestDataSchema);
export const applyNoteRequestValidator = ajv.compile(ApplyNoteRequestDataSchema);
const DEFAULT_NODE_MODULES_DIR = '/usr/local/lib/codify/node_modules/'
// Find the location of the node_modules of the CLI itself. Plugins depend on a shared instance of node-pty to work (using NODE_PATH)
// ex: /Users/kevinwang/Projects/codify/node_modules/@homebridge/node-pty-prebuilt-multiarch/lib/index.js
const require = createRequire(import.meta.url);
const nodeModulesDir = require.resolve('@homebridge/node-pty-prebuilt-multiarch')
?.split('@homebridge/node-pty-prebuilt-multiarch')
?.at(0)
?? DEFAULT_NODE_MODULES_DIR;
export function returnMessageCmd(cmd: string) {
return `${cmd}_Response`;
}
export class PluginProcess {
process: ChildProcess;
static async start(pluginPath: string, name: string, secureMode: boolean): Promise<PluginProcess> {
const isTypescript = pluginPath.endsWith('.ts');
const isTsxInstalled = PluginProcess.isTsxInstalled();
if (isTypescript && !isTsxInstalled) {
throw new Error('Typescript plugins are only allowed for dev mode. TS plugins are not allowed for production');
}
ctx.log(`Starting plugin ${name}`);
const _process = fork(
pluginPath,
[],
{
detached: secureMode,
env: { ...process.env, DEBUG_COLORS: '1', FORCE_COLOR: '1', NODE_PATH: nodeModulesDir },
silent: true,
...(isTypescript && { execArgv: ['--import', 'tsx'] }),
},
);
// Note: stdin is not hooked up on purpose for security purposes. Interactive commands + sudo will have to run through the parent process.
_process.stdout!.on('data', (message) => ctx.pluginStdout(name, message.toString('utf8')));
_process.stderr!.on('data', (message) => ctx.pluginStderr(name, message.toString('utf8')));
_process.on('exit', (code) => {
if (code && code !== 0) {
throw new Error(`Plugin ${this.name} exited with code ${code}`);
}
})
PluginProcess.handleRequests(_process, name);
return new PluginProcess(_process);
}
constructor(process: ChildProcess) {
this.process = process;
}
private static handleRequests(process: ChildProcess, pluginName: string) {
// Listen for incoming sudo incoming sudo requests
process.on('message', (message) => {
if (!PluginProcess.isIpcMessage(message)) {
throw new Error(`Invalid message from plugin. ${JSON.stringify(message, null, 2)}`);
}
if (message.cmd === MessageCmd.COMMAND_REQUEST) {
const { data, requestId } = message;
if (!commandRequestValidator(data)) {
throw new Error(`Invalid command request from plugin ${pluginName}. ${JSON.stringify(commandRequestValidator.errors, null, 2)}`);
}
// Send out command completed
ctx.once(Event.COMMAND_REQUEST_GRANTED, (_pluginName, data) => {
if (_pluginName === pluginName) {
process.send({
cmd: returnMessageCmd(MessageCmd.COMMAND_REQUEST),
requestId,
data
})
}
})
return ctx.commandRequested(pluginName, data as unknown as CommandRequestData);
}
if (message.cmd === MessageCmd.PRESS_KEY_TO_CONTINUE_REQUEST) {
const { data, requestId } = message;
if (!pressKeyToContinueRequestValidator(data)) {
throw new Error(`Invalid press key to continue request from plugin ${pluginName}. ${JSON.stringify(pressKeyToContinueRequestValidator.errors, null, 2)}`);
}
ctx.once(Event.PRESS_KEY_TO_CONTINUE_COMPLETED, (_pluginName) => {
if (_pluginName === pluginName) {
process.send({
cmd: returnMessageCmd(MessageCmd.PRESS_KEY_TO_CONTINUE_REQUEST),
requestId,
data: {},
})
}
})
return ctx.pressToContinueRequested(pluginName, data as unknown as PressKeyToContinueRequestData);
}
if (message.cmd === MessageCmd.APPLY_NOTE_REQUEST) {
const { data, requestId } = message;
if (!applyNoteRequestValidator(data)) {
throw new Error(`Invalid apply note request from plugin ${pluginName}. ${JSON.stringify(applyNoteRequestValidator.errors, null, 2)}`);
}
process.send({
cmd: returnMessageCmd(MessageCmd.APPLY_NOTE_REQUEST),
requestId,
data: {},
});
return ctx.applyNoteRequested(pluginName, data as unknown as ApplyNoteRequestData);
}
if (message.cmd === MessageCmd.CODIFY_CREDENTIALS_REQUEST) {
if (pluginName !== 'default') {
throw new Error(`Only the default Codify plugin is able to request Codify credentials. ${pluginName}`);
}
const { requestId } = message;
// Send out credentials granted events
ctx.once(Event.CODIFY_LOGIN_CREDENTIALS_COMPLETED, (_pluginName, credentials) => {
if (_pluginName === pluginName) {
process.send({
cmd: returnMessageCmd(MessageCmd.CODIFY_CREDENTIALS_REQUEST),
requestId,
data: credentials,
})
}
})
return ctx.codifyLoginRequested(pluginName);
}
})
}
sendMessage(message: IpcMessageV2): void {
this.process.send(message);
}
// Tsx is only installed for dev builds. Only allow typescript plugins for testing.
private static isTsxInstalled(): boolean {
try {
require.resolve('tsx');
} catch {
return false;
}
return true;
}
sendMessageForResult(cmd: string, data: unknown): Promise<PluginMessage> {
const message = PluginMessage.create(cmd, data);
return sendIpcMessageForResult(message, this.process);
}
private static isIpcMessage(message: unknown): message is IpcMessageV2 {
return ipcMessageValidator(message);
}
kill() {
this.process.kill(9)
}
}