app-manager-edge-worker 1.0.8 → 1.0.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/worker.js +16 -16
- package/package.json +46 -46
- package/src/banner.js +80 -80
- package/src/client.js +300 -300
- package/src/config.js +1 -1
- package/src/handlers/mailHandler.js +74 -74
- package/src/handlers/previewHandler.js +647 -557
- package/src/handlers/scrapeHandler.js +887 -887
- package/src/handlers/transformUtils.js +340 -340
package/src/client.js
CHANGED
|
@@ -1,300 +1,300 @@
|
|
|
1
|
-
import { io } from "socket.io-client";
|
|
2
|
-
import os from "os";
|
|
3
|
-
import path from "path";
|
|
4
|
-
import axios from "axios";
|
|
5
|
-
import { execSync } from "child_process";
|
|
6
|
-
import localBrowserManager from "./browser.js";
|
|
7
|
-
import { handleChromiumScrape, handleHttpScrape } from "./handlers/scrapeHandler.js";
|
|
8
|
-
import { handlePreviewPage, handleInspectElement } from "./handlers/previewHandler.js";
|
|
9
|
-
import { handleSendEmail } from "./handlers/mailHandler.js";
|
|
10
|
-
import { handleAiSmartRepair } from "./handlers/aiRepairHandler.js";
|
|
11
|
-
import { printBanner, logInfo, logSuccess, logWarn, logError, logJob } from "./banner.js";
|
|
12
|
-
|
|
13
|
-
export async function startWorkerClient(config) {
|
|
14
|
-
const { workerKey, serverUrl, concurrency, workerName, keepAliveMinutes = 7 } = config;
|
|
15
|
-
const isTermux = localBrowserManager.isTermux();
|
|
16
|
-
|
|
17
|
-
printBanner({
|
|
18
|
-
workerName,
|
|
19
|
-
serverUrl,
|
|
20
|
-
concurrency,
|
|
21
|
-
platform: `${os.platform()} ${os.arch()}`,
|
|
22
|
-
isTermux,
|
|
23
|
-
keepAliveMinutes,
|
|
24
|
-
});
|
|
25
|
-
|
|
26
|
-
if (isTermux) {
|
|
27
|
-
try {
|
|
28
|
-
execSync("termux-wake-lock", { stdio: "ignore" });
|
|
29
|
-
logSuccess("[Android Termux] Wake lock acquired to keep CPU awake in background.");
|
|
30
|
-
} catch {
|
|
31
|
-
// termux-tools might not be installed yet
|
|
32
|
-
}
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
logInfo("Checking Chromium browser availability...");
|
|
36
|
-
let chromiumExecutable = null;
|
|
37
|
-
try {
|
|
38
|
-
chromiumExecutable = await localBrowserManager.ensureChromiumExecutable();
|
|
39
|
-
logSuccess(`Chromium ready: ${chromiumExecutable}`);
|
|
40
|
-
} catch (browserErr) {
|
|
41
|
-
logWarn(`Chromium initialization warning: ${browserErr.message}`);
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
logInfo("Connecting to Apps Manager cluster...");
|
|
45
|
-
|
|
46
|
-
const socket = io(`${serverUrl}/workers`, {
|
|
47
|
-
transports: ["websocket", "polling"],
|
|
48
|
-
reconnection: true,
|
|
49
|
-
reconnectionAttempts: Infinity,
|
|
50
|
-
reconnectionDelay: 1000,
|
|
51
|
-
reconnectionDelayMax: 5000,
|
|
52
|
-
auth: {
|
|
53
|
-
workerKey,
|
|
54
|
-
metadata: {
|
|
55
|
-
hostname: workerName || os.hostname(),
|
|
56
|
-
platform: os.platform(),
|
|
57
|
-
arch: os.arch(),
|
|
58
|
-
osVersion: os.release(),
|
|
59
|
-
nodeVersion: process.version,
|
|
60
|
-
chromiumVersion: chromiumExecutable ? path.basename(chromiumExecutable) : "",
|
|
61
|
-
chromiumPath: chromiumExecutable || "",
|
|
62
|
-
isTermux,
|
|
63
|
-
},
|
|
64
|
-
},
|
|
65
|
-
});
|
|
66
|
-
|
|
67
|
-
const CHUNK_SIZE = 256 * 1024; // 256 KB chunks to prevent any WebSocket frame buffer overflow
|
|
68
|
-
|
|
69
|
-
const sendJobResult = (payload) => {
|
|
70
|
-
let payloadStr = "";
|
|
71
|
-
try {
|
|
72
|
-
payloadStr = JSON.stringify(payload);
|
|
73
|
-
} catch {
|
|
74
|
-
socket.emit("job:result", payload);
|
|
75
|
-
return;
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
// Small payload: send directly
|
|
79
|
-
if (payloadStr.length <= CHUNK_SIZE) {
|
|
80
|
-
socket.emit("job:result", payload);
|
|
81
|
-
return;
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
// Large payload (e.g. screenshot or large scrape): stream in chunks
|
|
85
|
-
const totalChunks = Math.ceil(payloadStr.length / CHUNK_SIZE);
|
|
86
|
-
const jobId = payload.jobId;
|
|
87
|
-
|
|
88
|
-
socket.emit("job:result:stream:start", {
|
|
89
|
-
jobId,
|
|
90
|
-
totalChunks,
|
|
91
|
-
totalLength: payloadStr.length,
|
|
92
|
-
});
|
|
93
|
-
|
|
94
|
-
for (let i = 0; i < totalChunks; i++) {
|
|
95
|
-
const chunk = payloadStr.slice(i * CHUNK_SIZE, (i + 1) * CHUNK_SIZE);
|
|
96
|
-
socket.emit("job:result:stream:chunk", {
|
|
97
|
-
jobId,
|
|
98
|
-
index: i,
|
|
99
|
-
chunk,
|
|
100
|
-
});
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
socket.emit("job:result:stream:end", { jobId });
|
|
104
|
-
};
|
|
105
|
-
|
|
106
|
-
let activeJobs = 0;
|
|
107
|
-
let activeConcurrency = concurrency;
|
|
108
|
-
|
|
109
|
-
socket.on("connect", () => {
|
|
110
|
-
logSuccess(`Connected to cluster! (Socket ID: ${socket.id})`);
|
|
111
|
-
logInfo(`Ready to accept scraping and visual preview jobs (Concurrency: ${activeConcurrency}).\n`);
|
|
112
|
-
});
|
|
113
|
-
|
|
114
|
-
socket.on("worker:registered", (data) => {
|
|
115
|
-
if (data?.concurrencyLimit && typeof data.concurrencyLimit === "number") {
|
|
116
|
-
activeConcurrency = data.concurrencyLimit;
|
|
117
|
-
logInfo(`[Cluster Config] Worker registered as '${data.name || data.workerId}'. Active concurrency: ${activeConcurrency} concurrent tabs.`);
|
|
118
|
-
}
|
|
119
|
-
});
|
|
120
|
-
|
|
121
|
-
socket.on("connect_error", (err) => {
|
|
122
|
-
if (err.message && err.message.includes("Authentication failed")) {
|
|
123
|
-
logError(`Authentication Failed: ${err.message}`);
|
|
124
|
-
logWarn("Please verify your worker key or run with '--reset' to enter a new key.");
|
|
125
|
-
} else {
|
|
126
|
-
logWarn(`Connection failed (${err.message}). Retrying in a few seconds...`);
|
|
127
|
-
}
|
|
128
|
-
});
|
|
129
|
-
|
|
130
|
-
socket.on("disconnect", (reason) => {
|
|
131
|
-
logWarn(`Disconnected from cluster: ${reason}`);
|
|
132
|
-
});
|
|
133
|
-
|
|
134
|
-
// Handle incoming job dispatch from server
|
|
135
|
-
socket.on("job:dispatch", async (job) => {
|
|
136
|
-
const jobId = job.jobId;
|
|
137
|
-
const jobType = job.jobType || job.type;
|
|
138
|
-
const payload = job.payload || {};
|
|
139
|
-
const targetUrl = jobType === "SEND_EMAIL"
|
|
140
|
-
? (payload.mailOptions?.to ? `to: ${payload.mailOptions.to}` : "email")
|
|
141
|
-
: (payload.url || "(no url)");
|
|
142
|
-
|
|
143
|
-
logJob(jobType, targetUrl);
|
|
144
|
-
activeJobs++;
|
|
145
|
-
|
|
146
|
-
const startTime = Date.now();
|
|
147
|
-
|
|
148
|
-
try {
|
|
149
|
-
let resultData = null;
|
|
150
|
-
|
|
151
|
-
if (jobType === "SCRAPE_CHROMIUM") {
|
|
152
|
-
resultData = await handleChromiumScrape(payload);
|
|
153
|
-
} else if (jobType === "SCRAPE_HTTP") {
|
|
154
|
-
resultData = await handleHttpScrape(payload);
|
|
155
|
-
} else if (jobType === "PREVIEW_PAGE") {
|
|
156
|
-
resultData = await handlePreviewPage(payload);
|
|
157
|
-
} else if (jobType === "INSPECT_ELEMENT") {
|
|
158
|
-
resultData = await handleInspectElement(payload);
|
|
159
|
-
} else if (jobType === "SEND_EMAIL") {
|
|
160
|
-
resultData = await handleSendEmail(payload);
|
|
161
|
-
} else if (jobType === "AI_SMART_REPAIR") {
|
|
162
|
-
resultData = await handleAiSmartRepair(payload, config);
|
|
163
|
-
} else {
|
|
164
|
-
throw new Error(`Unsupported job type: ${jobType}`);
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
const durationMs = Date.now() - startTime;
|
|
168
|
-
logSuccess(`Completed ${jobType} for ${targetUrl} in ${durationMs}ms`);
|
|
169
|
-
|
|
170
|
-
sendJobResult({
|
|
171
|
-
jobId,
|
|
172
|
-
success: true,
|
|
173
|
-
data: resultData,
|
|
174
|
-
durationMs,
|
|
175
|
-
});
|
|
176
|
-
} catch (err) {
|
|
177
|
-
const durationMs = Date.now() - startTime;
|
|
178
|
-
logError(`Failed ${jobType} for ${targetUrl}: ${err.message}`);
|
|
179
|
-
|
|
180
|
-
sendJobResult({
|
|
181
|
-
jobId,
|
|
182
|
-
success: false,
|
|
183
|
-
error: {
|
|
184
|
-
message: err.message,
|
|
185
|
-
code: err.code || "JOB_EXECUTION_ERROR",
|
|
186
|
-
},
|
|
187
|
-
durationMs,
|
|
188
|
-
});
|
|
189
|
-
} finally {
|
|
190
|
-
activeJobs = Math.max(0, activeJobs - 1);
|
|
191
|
-
}
|
|
192
|
-
});
|
|
193
|
-
|
|
194
|
-
// Handle cluster heartbeat ping
|
|
195
|
-
socket.on("ping", () => {
|
|
196
|
-
socket.emit("pong", {
|
|
197
|
-
activeJobs,
|
|
198
|
-
memory: process.memoryUsage(),
|
|
199
|
-
uptime: process.uptime(),
|
|
200
|
-
});
|
|
201
|
-
});
|
|
202
|
-
|
|
203
|
-
// Render Anti-Sleep Keep-Alive Ping
|
|
204
|
-
let keepAliveTimer = null;
|
|
205
|
-
let initialKeepAliveTimer = null;
|
|
206
|
-
|
|
207
|
-
const clearKeepAliveTimers = () => {
|
|
208
|
-
if (keepAliveTimer) {
|
|
209
|
-
clearInterval(keepAliveTimer);
|
|
210
|
-
keepAliveTimer = null;
|
|
211
|
-
}
|
|
212
|
-
if (initialKeepAliveTimer) {
|
|
213
|
-
clearTimeout(initialKeepAliveTimer);
|
|
214
|
-
initialKeepAliveTimer = null;
|
|
215
|
-
}
|
|
216
|
-
};
|
|
217
|
-
|
|
218
|
-
const pingKeepAlive = async () => {
|
|
219
|
-
try {
|
|
220
|
-
// Try the dedicated keep-alive endpoint first
|
|
221
|
-
const endpoint = `${serverUrl}/api/workers/keep-alive`;
|
|
222
|
-
const res = await axios.get(endpoint, {
|
|
223
|
-
timeout: 15000,
|
|
224
|
-
headers: {
|
|
225
|
-
"x-worker-key": workerKey || "",
|
|
226
|
-
"user-agent": "AppsManagerEdgeWorker/1.0.4",
|
|
227
|
-
},
|
|
228
|
-
});
|
|
229
|
-
logInfo(`[Keep-Alive] Server awake ping OK (${res.status}) - Render instance active`);
|
|
230
|
-
} catch (err) {
|
|
231
|
-
// Fallback to /health
|
|
232
|
-
try {
|
|
233
|
-
const fallbackUrl = `${serverUrl}/health`;
|
|
234
|
-
await axios.get(fallbackUrl, {
|
|
235
|
-
timeout: 10000,
|
|
236
|
-
headers: {
|
|
237
|
-
"user-agent": "AppsManagerEdgeWorker/1.0.4",
|
|
238
|
-
},
|
|
239
|
-
});
|
|
240
|
-
logInfo(`[Keep-Alive] Fallback health ping OK - Render instance active`);
|
|
241
|
-
} catch (fallbackErr) {
|
|
242
|
-
logWarn(`[Keep-Alive] Server awake ping warning (${err.message}) - will retry next cycle`);
|
|
243
|
-
}
|
|
244
|
-
}
|
|
245
|
-
};
|
|
246
|
-
|
|
247
|
-
const startKeepAlive = () => {
|
|
248
|
-
if (keepAliveMinutes <= 0) return;
|
|
249
|
-
clearKeepAliveTimers();
|
|
250
|
-
|
|
251
|
-
// Send initial ping 15s after startup to warm up/verify
|
|
252
|
-
initialKeepAliveTimer = setTimeout(pingKeepAlive, 15000);
|
|
253
|
-
if (initialKeepAliveTimer.unref) initialKeepAliveTimer.unref();
|
|
254
|
-
|
|
255
|
-
const intervalMs = Math.max(1, keepAliveMinutes) * 60 * 1000;
|
|
256
|
-
keepAliveTimer = setInterval(pingKeepAlive, intervalMs);
|
|
257
|
-
if (keepAliveTimer.unref) keepAliveTimer.unref();
|
|
258
|
-
};
|
|
259
|
-
|
|
260
|
-
if (keepAliveMinutes > 0) {
|
|
261
|
-
startKeepAlive();
|
|
262
|
-
}
|
|
263
|
-
|
|
264
|
-
// Clean up keep-alive timers on socket disconnect
|
|
265
|
-
socket.on("disconnect", (reason) => {
|
|
266
|
-
logWarn(`Disconnected from cluster: ${reason}`);
|
|
267
|
-
clearKeepAliveTimers();
|
|
268
|
-
});
|
|
269
|
-
|
|
270
|
-
socket.on("connect", () => {
|
|
271
|
-
if (keepAliveMinutes > 0 && !keepAliveTimer) {
|
|
272
|
-
startKeepAlive();
|
|
273
|
-
}
|
|
274
|
-
});
|
|
275
|
-
|
|
276
|
-
// Provide programmatic close for automated test harnesses
|
|
277
|
-
socket.closeClient = async () => {
|
|
278
|
-
clearKeepAliveTimers();
|
|
279
|
-
if (isTermux) {
|
|
280
|
-
try {
|
|
281
|
-
execSync("termux-wake-unlock", { stdio: "ignore" });
|
|
282
|
-
} catch {}
|
|
283
|
-
}
|
|
284
|
-
socket.disconnect();
|
|
285
|
-
await localBrowserManager.close();
|
|
286
|
-
};
|
|
287
|
-
|
|
288
|
-
// Graceful shutdown handling
|
|
289
|
-
const shutdown = async () => {
|
|
290
|
-
logInfo("\nGracefully shutting down edge worker...");
|
|
291
|
-
await socket.closeClient();
|
|
292
|
-
logSuccess("Edge worker stopped cleanly.");
|
|
293
|
-
process.exit(0);
|
|
294
|
-
};
|
|
295
|
-
|
|
296
|
-
process.on("SIGINT", shutdown);
|
|
297
|
-
process.on("SIGTERM", shutdown);
|
|
298
|
-
|
|
299
|
-
return socket;
|
|
300
|
-
}
|
|
1
|
+
import { io } from "socket.io-client";
|
|
2
|
+
import os from "os";
|
|
3
|
+
import path from "path";
|
|
4
|
+
import axios from "axios";
|
|
5
|
+
import { execSync } from "child_process";
|
|
6
|
+
import localBrowserManager from "./browser.js";
|
|
7
|
+
import { handleChromiumScrape, handleHttpScrape } from "./handlers/scrapeHandler.js";
|
|
8
|
+
import { handlePreviewPage, handleInspectElement } from "./handlers/previewHandler.js";
|
|
9
|
+
import { handleSendEmail } from "./handlers/mailHandler.js";
|
|
10
|
+
import { handleAiSmartRepair } from "./handlers/aiRepairHandler.js";
|
|
11
|
+
import { printBanner, logInfo, logSuccess, logWarn, logError, logJob } from "./banner.js";
|
|
12
|
+
|
|
13
|
+
export async function startWorkerClient(config) {
|
|
14
|
+
const { workerKey, serverUrl, concurrency, workerName, keepAliveMinutes = 7 } = config;
|
|
15
|
+
const isTermux = localBrowserManager.isTermux();
|
|
16
|
+
|
|
17
|
+
printBanner({
|
|
18
|
+
workerName,
|
|
19
|
+
serverUrl,
|
|
20
|
+
concurrency,
|
|
21
|
+
platform: `${os.platform()} ${os.arch()}`,
|
|
22
|
+
isTermux,
|
|
23
|
+
keepAliveMinutes,
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
if (isTermux) {
|
|
27
|
+
try {
|
|
28
|
+
execSync("termux-wake-lock", { stdio: "ignore" });
|
|
29
|
+
logSuccess("[Android Termux] Wake lock acquired to keep CPU awake in background.");
|
|
30
|
+
} catch {
|
|
31
|
+
// termux-tools might not be installed yet
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
logInfo("Checking Chromium browser availability...");
|
|
36
|
+
let chromiumExecutable = null;
|
|
37
|
+
try {
|
|
38
|
+
chromiumExecutable = await localBrowserManager.ensureChromiumExecutable();
|
|
39
|
+
logSuccess(`Chromium ready: ${chromiumExecutable}`);
|
|
40
|
+
} catch (browserErr) {
|
|
41
|
+
logWarn(`Chromium initialization warning: ${browserErr.message}`);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
logInfo("Connecting to Apps Manager cluster...");
|
|
45
|
+
|
|
46
|
+
const socket = io(`${serverUrl}/workers`, {
|
|
47
|
+
transports: ["websocket", "polling"],
|
|
48
|
+
reconnection: true,
|
|
49
|
+
reconnectionAttempts: Infinity,
|
|
50
|
+
reconnectionDelay: 1000,
|
|
51
|
+
reconnectionDelayMax: 5000,
|
|
52
|
+
auth: {
|
|
53
|
+
workerKey,
|
|
54
|
+
metadata: {
|
|
55
|
+
hostname: workerName || os.hostname(),
|
|
56
|
+
platform: os.platform(),
|
|
57
|
+
arch: os.arch(),
|
|
58
|
+
osVersion: os.release(),
|
|
59
|
+
nodeVersion: process.version,
|
|
60
|
+
chromiumVersion: chromiumExecutable ? path.basename(chromiumExecutable) : "",
|
|
61
|
+
chromiumPath: chromiumExecutable || "",
|
|
62
|
+
isTermux,
|
|
63
|
+
},
|
|
64
|
+
},
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
const CHUNK_SIZE = 256 * 1024; // 256 KB chunks to prevent any WebSocket frame buffer overflow
|
|
68
|
+
|
|
69
|
+
const sendJobResult = (payload) => {
|
|
70
|
+
let payloadStr = "";
|
|
71
|
+
try {
|
|
72
|
+
payloadStr = JSON.stringify(payload);
|
|
73
|
+
} catch {
|
|
74
|
+
socket.emit("job:result", payload);
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Small payload: send directly
|
|
79
|
+
if (payloadStr.length <= CHUNK_SIZE) {
|
|
80
|
+
socket.emit("job:result", payload);
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Large payload (e.g. screenshot or large scrape): stream in chunks
|
|
85
|
+
const totalChunks = Math.ceil(payloadStr.length / CHUNK_SIZE);
|
|
86
|
+
const jobId = payload.jobId;
|
|
87
|
+
|
|
88
|
+
socket.emit("job:result:stream:start", {
|
|
89
|
+
jobId,
|
|
90
|
+
totalChunks,
|
|
91
|
+
totalLength: payloadStr.length,
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
for (let i = 0; i < totalChunks; i++) {
|
|
95
|
+
const chunk = payloadStr.slice(i * CHUNK_SIZE, (i + 1) * CHUNK_SIZE);
|
|
96
|
+
socket.emit("job:result:stream:chunk", {
|
|
97
|
+
jobId,
|
|
98
|
+
index: i,
|
|
99
|
+
chunk,
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
socket.emit("job:result:stream:end", { jobId });
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
let activeJobs = 0;
|
|
107
|
+
let activeConcurrency = concurrency;
|
|
108
|
+
|
|
109
|
+
socket.on("connect", () => {
|
|
110
|
+
logSuccess(`Connected to cluster! (Socket ID: ${socket.id})`);
|
|
111
|
+
logInfo(`Ready to accept scraping and visual preview jobs (Concurrency: ${activeConcurrency}).\n`);
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
socket.on("worker:registered", (data) => {
|
|
115
|
+
if (data?.concurrencyLimit && typeof data.concurrencyLimit === "number") {
|
|
116
|
+
activeConcurrency = data.concurrencyLimit;
|
|
117
|
+
logInfo(`[Cluster Config] Worker registered as '${data.name || data.workerId}'. Active concurrency: ${activeConcurrency} concurrent tabs.`);
|
|
118
|
+
}
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
socket.on("connect_error", (err) => {
|
|
122
|
+
if (err.message && err.message.includes("Authentication failed")) {
|
|
123
|
+
logError(`Authentication Failed: ${err.message}`);
|
|
124
|
+
logWarn("Please verify your worker key or run with '--reset' to enter a new key.");
|
|
125
|
+
} else {
|
|
126
|
+
logWarn(`Connection failed (${err.message}). Retrying in a few seconds...`);
|
|
127
|
+
}
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
socket.on("disconnect", (reason) => {
|
|
131
|
+
logWarn(`Disconnected from cluster: ${reason}`);
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
// Handle incoming job dispatch from server
|
|
135
|
+
socket.on("job:dispatch", async (job) => {
|
|
136
|
+
const jobId = job.jobId;
|
|
137
|
+
const jobType = job.jobType || job.type;
|
|
138
|
+
const payload = job.payload || {};
|
|
139
|
+
const targetUrl = jobType === "SEND_EMAIL"
|
|
140
|
+
? (payload.mailOptions?.to ? `to: ${payload.mailOptions.to}` : "email")
|
|
141
|
+
: (payload.url || "(no url)");
|
|
142
|
+
|
|
143
|
+
logJob(jobType, targetUrl);
|
|
144
|
+
activeJobs++;
|
|
145
|
+
|
|
146
|
+
const startTime = Date.now();
|
|
147
|
+
|
|
148
|
+
try {
|
|
149
|
+
let resultData = null;
|
|
150
|
+
|
|
151
|
+
if (jobType === "SCRAPE_CHROMIUM") {
|
|
152
|
+
resultData = await handleChromiumScrape(payload);
|
|
153
|
+
} else if (jobType === "SCRAPE_HTTP") {
|
|
154
|
+
resultData = await handleHttpScrape(payload);
|
|
155
|
+
} else if (jobType === "PREVIEW_PAGE") {
|
|
156
|
+
resultData = await handlePreviewPage(payload);
|
|
157
|
+
} else if (jobType === "INSPECT_ELEMENT") {
|
|
158
|
+
resultData = await handleInspectElement(payload);
|
|
159
|
+
} else if (jobType === "SEND_EMAIL") {
|
|
160
|
+
resultData = await handleSendEmail(payload);
|
|
161
|
+
} else if (jobType === "AI_SMART_REPAIR") {
|
|
162
|
+
resultData = await handleAiSmartRepair(payload, config);
|
|
163
|
+
} else {
|
|
164
|
+
throw new Error(`Unsupported job type: ${jobType}`);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
const durationMs = Date.now() - startTime;
|
|
168
|
+
logSuccess(`Completed ${jobType} for ${targetUrl} in ${durationMs}ms`);
|
|
169
|
+
|
|
170
|
+
sendJobResult({
|
|
171
|
+
jobId,
|
|
172
|
+
success: true,
|
|
173
|
+
data: resultData,
|
|
174
|
+
durationMs,
|
|
175
|
+
});
|
|
176
|
+
} catch (err) {
|
|
177
|
+
const durationMs = Date.now() - startTime;
|
|
178
|
+
logError(`Failed ${jobType} for ${targetUrl}: ${err.message}`);
|
|
179
|
+
|
|
180
|
+
sendJobResult({
|
|
181
|
+
jobId,
|
|
182
|
+
success: false,
|
|
183
|
+
error: {
|
|
184
|
+
message: err.message,
|
|
185
|
+
code: err.code || "JOB_EXECUTION_ERROR",
|
|
186
|
+
},
|
|
187
|
+
durationMs,
|
|
188
|
+
});
|
|
189
|
+
} finally {
|
|
190
|
+
activeJobs = Math.max(0, activeJobs - 1);
|
|
191
|
+
}
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
// Handle cluster heartbeat ping
|
|
195
|
+
socket.on("ping", () => {
|
|
196
|
+
socket.emit("pong", {
|
|
197
|
+
activeJobs,
|
|
198
|
+
memory: process.memoryUsage(),
|
|
199
|
+
uptime: process.uptime(),
|
|
200
|
+
});
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
// Render Anti-Sleep Keep-Alive Ping
|
|
204
|
+
let keepAliveTimer = null;
|
|
205
|
+
let initialKeepAliveTimer = null;
|
|
206
|
+
|
|
207
|
+
const clearKeepAliveTimers = () => {
|
|
208
|
+
if (keepAliveTimer) {
|
|
209
|
+
clearInterval(keepAliveTimer);
|
|
210
|
+
keepAliveTimer = null;
|
|
211
|
+
}
|
|
212
|
+
if (initialKeepAliveTimer) {
|
|
213
|
+
clearTimeout(initialKeepAliveTimer);
|
|
214
|
+
initialKeepAliveTimer = null;
|
|
215
|
+
}
|
|
216
|
+
};
|
|
217
|
+
|
|
218
|
+
const pingKeepAlive = async () => {
|
|
219
|
+
try {
|
|
220
|
+
// Try the dedicated keep-alive endpoint first
|
|
221
|
+
const endpoint = `${serverUrl}/api/workers/keep-alive`;
|
|
222
|
+
const res = await axios.get(endpoint, {
|
|
223
|
+
timeout: 15000,
|
|
224
|
+
headers: {
|
|
225
|
+
"x-worker-key": workerKey || "",
|
|
226
|
+
"user-agent": "AppsManagerEdgeWorker/1.0.4",
|
|
227
|
+
},
|
|
228
|
+
});
|
|
229
|
+
logInfo(`[Keep-Alive] Server awake ping OK (${res.status}) - Render instance active`);
|
|
230
|
+
} catch (err) {
|
|
231
|
+
// Fallback to /health
|
|
232
|
+
try {
|
|
233
|
+
const fallbackUrl = `${serverUrl}/health`;
|
|
234
|
+
await axios.get(fallbackUrl, {
|
|
235
|
+
timeout: 10000,
|
|
236
|
+
headers: {
|
|
237
|
+
"user-agent": "AppsManagerEdgeWorker/1.0.4",
|
|
238
|
+
},
|
|
239
|
+
});
|
|
240
|
+
logInfo(`[Keep-Alive] Fallback health ping OK - Render instance active`);
|
|
241
|
+
} catch (fallbackErr) {
|
|
242
|
+
logWarn(`[Keep-Alive] Server awake ping warning (${err.message}) - will retry next cycle`);
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
};
|
|
246
|
+
|
|
247
|
+
const startKeepAlive = () => {
|
|
248
|
+
if (keepAliveMinutes <= 0) return;
|
|
249
|
+
clearKeepAliveTimers();
|
|
250
|
+
|
|
251
|
+
// Send initial ping 15s after startup to warm up/verify
|
|
252
|
+
initialKeepAliveTimer = setTimeout(pingKeepAlive, 15000);
|
|
253
|
+
if (initialKeepAliveTimer.unref) initialKeepAliveTimer.unref();
|
|
254
|
+
|
|
255
|
+
const intervalMs = Math.max(1, keepAliveMinutes) * 60 * 1000;
|
|
256
|
+
keepAliveTimer = setInterval(pingKeepAlive, intervalMs);
|
|
257
|
+
if (keepAliveTimer.unref) keepAliveTimer.unref();
|
|
258
|
+
};
|
|
259
|
+
|
|
260
|
+
if (keepAliveMinutes > 0) {
|
|
261
|
+
startKeepAlive();
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
// Clean up keep-alive timers on socket disconnect
|
|
265
|
+
socket.on("disconnect", (reason) => {
|
|
266
|
+
logWarn(`Disconnected from cluster: ${reason}`);
|
|
267
|
+
clearKeepAliveTimers();
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
socket.on("connect", () => {
|
|
271
|
+
if (keepAliveMinutes > 0 && !keepAliveTimer) {
|
|
272
|
+
startKeepAlive();
|
|
273
|
+
}
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
// Provide programmatic close for automated test harnesses
|
|
277
|
+
socket.closeClient = async () => {
|
|
278
|
+
clearKeepAliveTimers();
|
|
279
|
+
if (isTermux) {
|
|
280
|
+
try {
|
|
281
|
+
execSync("termux-wake-unlock", { stdio: "ignore" });
|
|
282
|
+
} catch {}
|
|
283
|
+
}
|
|
284
|
+
socket.disconnect();
|
|
285
|
+
await localBrowserManager.close();
|
|
286
|
+
};
|
|
287
|
+
|
|
288
|
+
// Graceful shutdown handling
|
|
289
|
+
const shutdown = async () => {
|
|
290
|
+
logInfo("\nGracefully shutting down edge worker...");
|
|
291
|
+
await socket.closeClient();
|
|
292
|
+
logSuccess("Edge worker stopped cleanly.");
|
|
293
|
+
process.exit(0);
|
|
294
|
+
};
|
|
295
|
+
|
|
296
|
+
process.on("SIGINT", shutdown);
|
|
297
|
+
process.on("SIGTERM", shutdown);
|
|
298
|
+
|
|
299
|
+
return socket;
|
|
300
|
+
}
|
package/src/config.js
CHANGED
|
@@ -111,7 +111,7 @@ export async function resolveWorkerConfig() {
|
|
|
111
111
|
const cli = parseCliArgs();
|
|
112
112
|
|
|
113
113
|
if (cli.help) {
|
|
114
|
-
let pkgVer = "1.0.
|
|
114
|
+
let pkgVer = "1.0.9";
|
|
115
115
|
try {
|
|
116
116
|
const pkg = JSON.parse(fs.readFileSync(new URL("../package.json", import.meta.url), "utf-8"));
|
|
117
117
|
if (pkg?.version) pkgVer = pkg.version;
|