shiveshnavin commited on
Commit
7f36d25
·
1 Parent(s): 00d8f4e

Add process killer

Browse files
Files changed (2) hide show
  1. renderer.js +17 -131
  2. utils/ProcessKiller.js +271 -0
renderer.js CHANGED
@@ -8,6 +8,7 @@ import { platform } from 'os';
8
  import { renderSSR } from './ssr.js';
9
  import path from 'path';
10
  import { renderProxy } from './proxy-renderer.js';
 
11
  const { UnzipFiles, Utils, ZipFiles } = pkg;
12
 
13
  const __filename = fileURLToPath(import.meta.url);
@@ -109,98 +110,7 @@ export async function doRender(
109
 
110
  const childProcess = spawn('npx', remotionArgs, spawnOptions);
111
  let isProcessKilled = false;
112
- let processTreePids = new Set(); // Track all process PIDs for cleanup
113
-
114
- // Function to find and kill all child processes
115
- const killProcessTree = async (pid, signal = 'SIGTERM') => {
116
- try {
117
- if (platform() === 'win32') {
118
- // On Windows, use taskkill to kill the entire process tree
119
- const { exec } = await import('child_process');
120
- return new Promise((resolve) => {
121
- exec(`taskkill /pid ${pid} /t /f`, (error) => {
122
- if (error && !error.message.includes('not found')) {
123
- console.error('Failed to kill Windows process tree:', error.message);
124
- } else {
125
- console.log(`Windows process tree ${pid} killed`);
126
- }
127
- resolve();
128
- });
129
- });
130
- } else {
131
- // On Unix systems, kill the process group
132
- try {
133
- process.kill(-pid, signal);
134
- console.log(`Unix process group ${pid} killed with ${signal}`);
135
- } catch (e) {
136
- if (e.code !== 'ESRCH') {
137
- // Try individual process kill as fallback
138
- try {
139
- process.kill(pid, signal);
140
- console.log(`Unix process ${pid} killed with ${signal}`);
141
- } catch (e2) {
142
- if (e2.code !== 'ESRCH') {
143
- console.error('Failed to kill Unix process:', e2.message);
144
- }
145
- }
146
- }
147
- }
148
- }
149
- } catch (e) {
150
- if (e.code !== 'ESRCH') {
151
- console.error('Failed to kill process tree:', e.message);
152
- }
153
- }
154
- };
155
-
156
- // Additional cleanup function to kill any remaining remotion processes
157
- const killRemainingRemotionProcesses = async () => {
158
- try {
159
- if (platform() === 'win32') {
160
- // On Windows, find and kill remotion processes
161
- const { exec } = await import('child_process');
162
- exec('tasklist /fi "imagename eq node.exe" /fo csv | findstr remotion', (error, stdout) => {
163
- if (!error && stdout) {
164
- console.log('Found remaining remotion processes, attempting cleanup...');
165
- exec('taskkill /f /im node.exe /fi "windowtitle eq *remotion*"', (killError) => {
166
- if (!killError) {
167
- console.log('Cleaned up remaining remotion processes');
168
- }
169
- });
170
- }
171
- });
172
- } else {
173
- // On Unix systems, use pkill to find remotion processes
174
- const { exec } = await import('child_process');
175
- exec('pkill -f "remotion.*render"', (error) => {
176
- if (!error) {
177
- console.log('Cleaned up remaining remotion processes');
178
- }
179
- });
180
- }
181
- } catch (e) {
182
- // Ignore cleanup errors
183
- console.log('Cleanup attempt completed');
184
- }
185
- };
186
-
187
- // Function to verify if process is actually terminated
188
- const verifyProcessTerminated = async (pid) => {
189
- try {
190
- if (platform() === 'win32') {
191
- const { execSync } = await import('child_process');
192
- const result = execSync(`tasklist /fi "pid eq ${pid}"`, { encoding: 'utf8' });
193
- return !result.includes(pid.toString());
194
- } else {
195
- // On Unix, sending signal 0 checks if process exists
196
- process.kill(pid, 0);
197
- return false; // If no error thrown, process still exists
198
- }
199
- } catch (e) {
200
- // If error thrown, process doesn't exist
201
- return true;
202
- }
203
- };
204
 
205
  if (controller && controller.stop) {
206
  controller.stop = async () => {
@@ -212,42 +122,18 @@ export async function doRender(
212
 
213
  isProcessKilled = true;
214
 
215
- if (!childProcess.pid) {
216
- console.log('No PID available');
217
- return;
218
- }
219
-
220
- try {
221
- console.log(`Attempting to kill process tree with PID: ${childProcess.pid}`);
222
-
223
- // Kill the entire process tree
224
- await killProcessTree(childProcess.pid, 'SIGTERM');
225
 
226
- // Give it a moment to terminate gracefully
227
- setTimeout(async () => {
228
- const isTerminated = await verifyProcessTerminated(childProcess.pid);
229
- if (!childProcess.killed && !isTerminated) {
230
- console.log('Process still running, trying SIGKILL');
231
- await killProcessTree(childProcess.pid, 'SIGKILL');
232
-
233
- // Final verification and cleanup attempt
234
- setTimeout(async () => {
235
- const isFinallyTerminated = await verifyProcessTerminated(childProcess.pid);
236
- if (!isFinallyTerminated) {
237
- console.log('Process still exists after SIGKILL, attempting additional cleanup');
238
- await killRemainingRemotionProcesses();
239
- } else {
240
- console.log('Process successfully terminated');
241
- }
242
- }, 1000);
243
- } else {
244
- console.log('Process terminated successfully with SIGTERM');
245
- }
246
- }, 2000); console.log('Process termination initiated');
247
- } catch (e) {
248
- console.error('Failed to stop process:', e.message);
249
- // Try cleanup as fallback
250
- await killRemainingRemotionProcesses();
251
  }
252
  }
253
  }
@@ -255,8 +141,8 @@ export async function doRender(
255
  console.log('Starting video render. ' + cmd);
256
  console.log(`Spawned process with PID: ${childProcess.pid}`);
257
 
258
- // Store the process for cleanup
259
- processTreePids.add(childProcess.pid);
260
 
261
  let updateCounter = 0;
262
  childProcess.stdout.on('data', (data) => {
@@ -279,7 +165,7 @@ export async function doRender(
279
  console.log(`Render process closed with code: ${code}, signal: ${signal}`);
280
 
281
  // Clean up tracked PIDs
282
- processTreePids.clear();
283
 
284
  sendToObserver(jobId, code === 0 ? 'completed' : 'failed');
285
  if (code === 0) {
@@ -296,7 +182,7 @@ export async function doRender(
296
 
297
  childProcess.on('error', (error) => {
298
  isProcessKilled = true; // Mark process as terminated
299
- processTreePids.clear();
300
  console.error('Child process error:', error);
301
  sendToObserver(jobId, 'failed');
302
  reject(error);
 
8
  import { renderSSR } from './ssr.js';
9
  import path from 'path';
10
  import { renderProxy } from './proxy-renderer.js';
11
+ import { ProcessKiller } from './utils/ProcessKiller.js';
12
  const { UnzipFiles, Utils, ZipFiles } = pkg;
13
 
14
  const __filename = fileURLToPath(import.meta.url);
 
110
 
111
  const childProcess = spawn('npx', remotionArgs, spawnOptions);
112
  let isProcessKilled = false;
113
+ const processKiller = new ProcessKiller();
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
114
 
115
  if (controller && controller.stop) {
116
  controller.stop = async () => {
 
122
 
123
  isProcessKilled = true;
124
 
125
+ // Use the ProcessKiller utility for comprehensive termination
126
+ const success = await processKiller.terminateProcess(childProcess.pid, {
127
+ gracefulTimeout: 2000,
128
+ forceTimeout: 1000,
129
+ processPattern: "remotion.*render",
130
+ onProgress: (message) => console.log(message)
131
+ });
 
 
 
132
 
133
+ if (success) {
134
+ console.log('Process termination completed');
135
+ } else {
136
+ console.log('Process termination attempted (may have already been terminated)');
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
137
  }
138
  }
139
  }
 
141
  console.log('Starting video render. ' + cmd);
142
  console.log(`Spawned process with PID: ${childProcess.pid}`);
143
 
144
+ // Track the process for cleanup
145
+ processKiller.trackPid(childProcess.pid);
146
 
147
  let updateCounter = 0;
148
  childProcess.stdout.on('data', (data) => {
 
165
  console.log(`Render process closed with code: ${code}, signal: ${signal}`);
166
 
167
  // Clean up tracked PIDs
168
+ processKiller.clearTrackedPids();
169
 
170
  sendToObserver(jobId, code === 0 ? 'completed' : 'failed');
171
  if (code === 0) {
 
182
 
183
  childProcess.on('error', (error) => {
184
  isProcessKilled = true; // Mark process as terminated
185
+ processKiller.clearTrackedPids();
186
  console.error('Child process error:', error);
187
  sendToObserver(jobId, 'failed');
188
  reject(error);
utils/ProcessKiller.js ADDED
@@ -0,0 +1,271 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { exec, execSync } from 'child_process';
2
+ import { platform } from 'os';
3
+
4
+ /**
5
+ * Enhanced process killer utility for handling stubborn processes
6
+ * Handles cross-platform process tree termination with multiple fallback strategies
7
+ */
8
+ export class ProcessKiller {
9
+ constructor() {
10
+ this.isWindows = platform() === 'win32';
11
+ this.trackedPids = new Set();
12
+ }
13
+
14
+ /**
15
+ * Track a PID for cleanup
16
+ * @param {number} pid - Process ID to track
17
+ */
18
+ trackPid(pid) {
19
+ if (pid) {
20
+ this.trackedPids.add(pid);
21
+ }
22
+ }
23
+
24
+ /**
25
+ * Kill a process tree with the specified signal
26
+ * @param {number} pid - Process ID to kill
27
+ * @param {string} signal - Signal to send (SIGTERM, SIGKILL, etc.)
28
+ * @returns {Promise<boolean>} - True if successful
29
+ */
30
+ async killProcessTree(pid, signal = 'SIGTERM') {
31
+ try {
32
+ if (this.isWindows) {
33
+ return await this._killWindowsProcessTree(pid);
34
+ } else {
35
+ return await this._killUnixProcessTree(pid, signal);
36
+ }
37
+ } catch (e) {
38
+ if (e.code !== 'ESRCH') {
39
+ console.error('Failed to kill process tree:', e.message);
40
+ }
41
+ return false;
42
+ }
43
+ }
44
+
45
+ /**
46
+ * Windows-specific process tree killing
47
+ * @param {number} pid - Process ID
48
+ * @returns {Promise<boolean>}
49
+ */
50
+ async _killWindowsProcessTree(pid) {
51
+ return new Promise((resolve) => {
52
+ exec(`taskkill /pid ${pid} /t /f`, (error) => {
53
+ if (error && !error.message.includes('not found')) {
54
+ console.error('Failed to kill Windows process tree:', error.message);
55
+ resolve(false);
56
+ } else {
57
+ console.log(`Windows process tree ${pid} killed`);
58
+ resolve(true);
59
+ }
60
+ });
61
+ });
62
+ }
63
+
64
+ /**
65
+ * Unix-specific process tree killing
66
+ * @param {number} pid - Process ID
67
+ * @param {string} signal - Signal to send
68
+ * @returns {Promise<boolean>}
69
+ */
70
+ async _killUnixProcessTree(pid, signal) {
71
+ try {
72
+ // Try to kill the process group first
73
+ process.kill(-pid, signal);
74
+ console.log(`Unix process group ${pid} killed with ${signal}`);
75
+ return true;
76
+ } catch (e) {
77
+ if (e.code !== 'ESRCH') {
78
+ // Try individual process kill as fallback
79
+ try {
80
+ process.kill(pid, signal);
81
+ console.log(`Unix process ${pid} killed with ${signal}`);
82
+ return true;
83
+ } catch (e2) {
84
+ if (e2.code !== 'ESRCH') {
85
+ console.error('Failed to kill Unix process:', e2.message);
86
+ }
87
+ return false;
88
+ }
89
+ }
90
+ return false;
91
+ }
92
+ }
93
+
94
+ /**
95
+ * Verify if a process is actually terminated
96
+ * @param {number} pid - Process ID to check
97
+ * @returns {Promise<boolean>} - True if terminated
98
+ */
99
+ async verifyProcessTerminated(pid) {
100
+ try {
101
+ if (this.isWindows) {
102
+ const result = execSync(`tasklist /fi "pid eq ${pid}"`, { encoding: 'utf8' });
103
+ return !result.includes(pid.toString());
104
+ } else {
105
+ // On Unix, sending signal 0 checks if process exists
106
+ process.kill(pid, 0);
107
+ return false; // If no error thrown, process still exists
108
+ }
109
+ } catch (e) {
110
+ // If error thrown, process doesn't exist
111
+ return true;
112
+ }
113
+ }
114
+
115
+ /**
116
+ * Kill any remaining processes matching a pattern
117
+ * @param {string} processPattern - Pattern to match (e.g., "remotion.*render")
118
+ * @returns {Promise<void>}
119
+ */
120
+ async killRemainingProcesses(processPattern = "remotion.*render") {
121
+ try {
122
+ if (this.isWindows) {
123
+ await this._killRemainingWindowsProcesses(processPattern);
124
+ } else {
125
+ await this._killRemainingUnixProcesses(processPattern);
126
+ }
127
+ } catch (e) {
128
+ console.log('Cleanup attempt completed');
129
+ }
130
+ }
131
+
132
+ /**
133
+ * Windows-specific remaining process cleanup
134
+ * @param {string} pattern - Process pattern
135
+ */
136
+ async _killRemainingWindowsProcesses(pattern) {
137
+ return new Promise((resolve) => {
138
+ exec('tasklist /fi "imagename eq node.exe" /fo csv | findstr remotion', (error, stdout) => {
139
+ if (!error && stdout) {
140
+ console.log('Found remaining remotion processes, attempting cleanup...');
141
+ exec('taskkill /f /im node.exe /fi "windowtitle eq *remotion*"', (killError) => {
142
+ if (!killError) {
143
+ console.log('Cleaned up remaining remotion processes');
144
+ }
145
+ resolve();
146
+ });
147
+ } else {
148
+ resolve();
149
+ }
150
+ });
151
+ });
152
+ }
153
+
154
+ /**
155
+ * Unix-specific remaining process cleanup
156
+ * @param {string} pattern - Process pattern
157
+ */
158
+ async _killRemainingUnixProcesses(pattern) {
159
+ return new Promise((resolve) => {
160
+ exec(`pkill -f "${pattern}"`, (error) => {
161
+ if (!error) {
162
+ console.log('Cleaned up remaining remotion processes');
163
+ }
164
+ resolve();
165
+ });
166
+ });
167
+ }
168
+
169
+ /**
170
+ * Comprehensive process termination with multiple strategies
171
+ * @param {number} pid - Process ID to terminate
172
+ * @param {Object} options - Termination options
173
+ * @returns {Promise<boolean>} - True if successfully terminated
174
+ */
175
+ async terminateProcess(pid, options = {}) {
176
+ const {
177
+ gracefulTimeout = 2000,
178
+ forceTimeout = 1000,
179
+ processPattern = "remotion.*render",
180
+ onProgress = () => { }
181
+ } = options;
182
+
183
+ if (!pid) {
184
+ onProgress('No PID available');
185
+ return false;
186
+ }
187
+
188
+ try {
189
+ onProgress(`Attempting to kill process tree with PID: ${pid}`);
190
+
191
+ // Stage 1: Graceful termination
192
+ await this.killProcessTree(pid, 'SIGTERM');
193
+
194
+ // Wait and verify
195
+ await new Promise(resolve => setTimeout(resolve, gracefulTimeout));
196
+
197
+ const isTerminated = await this.verifyProcessTerminated(pid);
198
+ if (isTerminated) {
199
+ onProgress('Process terminated successfully with SIGTERM');
200
+ return true;
201
+ }
202
+
203
+ // Stage 2: Force termination
204
+ onProgress('Process still running, trying SIGKILL');
205
+ await this.killProcessTree(pid, 'SIGKILL');
206
+
207
+ // Final verification
208
+ await new Promise(resolve => setTimeout(resolve, forceTimeout));
209
+
210
+ const isFinallyTerminated = await this.verifyProcessTerminated(pid);
211
+ if (isFinallyTerminated) {
212
+ onProgress('Process successfully terminated');
213
+ return true;
214
+ }
215
+
216
+ // Stage 3: Cleanup remaining processes
217
+ onProgress('Process still exists after SIGKILL, attempting additional cleanup');
218
+ await this.killRemainingProcesses(processPattern);
219
+
220
+ return true; // Assume success after cleanup attempt
221
+
222
+ } catch (e) {
223
+ console.error('Failed to terminate process:', e.message);
224
+ // Final fallback cleanup
225
+ await this.killRemainingProcesses(processPattern);
226
+ return false;
227
+ }
228
+ }
229
+
230
+ /**
231
+ * Clean up all tracked PIDs
232
+ */
233
+ clearTrackedPids() {
234
+ this.trackedPids.clear();
235
+ }
236
+
237
+ /**
238
+ * Get all tracked PIDs
239
+ * @returns {Set<number>}
240
+ */
241
+ getTrackedPids() {
242
+ return new Set(this.trackedPids);
243
+ }
244
+ }
245
+
246
+ /**
247
+ * Default export - singleton instance
248
+ */
249
+ export default new ProcessKiller();
250
+
251
+ /**
252
+ * Convenience function for quick process termination
253
+ * @param {number} pid - Process ID
254
+ * @param {Object} options - Termination options
255
+ * @returns {Promise<boolean>}
256
+ */
257
+ export async function terminateProcess(pid, options = {}) {
258
+ const killer = new ProcessKiller();
259
+ return await killer.terminateProcess(pid, options);
260
+ }
261
+
262
+ /**
263
+ * Convenience function for process tree killing
264
+ * @param {number} pid - Process ID
265
+ * @param {string} signal - Signal to send
266
+ * @returns {Promise<boolean>}
267
+ */
268
+ export async function killProcessTree(pid, signal = 'SIGTERM') {
269
+ const killer = new ProcessKiller();
270
+ return await killer.killProcessTree(pid, signal);
271
+ }