Release v1.4.13 with global stall watchdog and freeze recovery
This commit is contained in:
parent
0f85cd4c8d
commit
6d8ead8598
4
package-lock.json
generated
4
package-lock.json
generated
@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "real-debrid-downloader",
|
"name": "real-debrid-downloader",
|
||||||
"version": "1.4.12",
|
"version": "1.4.13",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "real-debrid-downloader",
|
"name": "real-debrid-downloader",
|
||||||
"version": "1.4.12",
|
"version": "1.4.13",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"adm-zip": "^0.5.16",
|
"adm-zip": "^0.5.16",
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "real-debrid-downloader",
|
"name": "real-debrid-downloader",
|
||||||
"version": "1.4.12",
|
"version": "1.4.13",
|
||||||
"description": "Real-Debrid Downloader Desktop (Electron + React + TypeScript)",
|
"description": "Real-Debrid Downloader Desktop (Electron + React + TypeScript)",
|
||||||
"main": "build/main/main/main.js",
|
"main": "build/main/main/main.js",
|
||||||
"author": "Sucukdeluxe",
|
"author": "Sucukdeluxe",
|
||||||
|
|||||||
@ -40,6 +40,8 @@ const DEFAULT_DOWNLOAD_STALL_TIMEOUT_MS = 30000;
|
|||||||
|
|
||||||
const DEFAULT_DOWNLOAD_CONNECT_TIMEOUT_MS = 25000;
|
const DEFAULT_DOWNLOAD_CONNECT_TIMEOUT_MS = 25000;
|
||||||
|
|
||||||
|
const DEFAULT_GLOBAL_STALL_WATCHDOG_TIMEOUT_MS = 90000;
|
||||||
|
|
||||||
function getDownloadStallTimeoutMs(): number {
|
function getDownloadStallTimeoutMs(): number {
|
||||||
const fromEnv = Number(process.env.RD_STALL_TIMEOUT_MS ?? NaN);
|
const fromEnv = Number(process.env.RD_STALL_TIMEOUT_MS ?? NaN);
|
||||||
if (Number.isFinite(fromEnv) && fromEnv >= 2000 && fromEnv <= 600000) {
|
if (Number.isFinite(fromEnv) && fromEnv >= 2000 && fromEnv <= 600000) {
|
||||||
@ -56,6 +58,19 @@ function getDownloadConnectTimeoutMs(): number {
|
|||||||
return DEFAULT_DOWNLOAD_CONNECT_TIMEOUT_MS;
|
return DEFAULT_DOWNLOAD_CONNECT_TIMEOUT_MS;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getGlobalStallWatchdogTimeoutMs(): number {
|
||||||
|
const fromEnv = Number(process.env.RD_GLOBAL_STALL_TIMEOUT_MS ?? NaN);
|
||||||
|
if (Number.isFinite(fromEnv)) {
|
||||||
|
if (fromEnv <= 0) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
if (fromEnv >= 2000 && fromEnv <= 600000) {
|
||||||
|
return Math.floor(fromEnv);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return DEFAULT_GLOBAL_STALL_WATCHDOG_TIMEOUT_MS;
|
||||||
|
}
|
||||||
|
|
||||||
type DownloadManagerOptions = {
|
type DownloadManagerOptions = {
|
||||||
megaWebUnrestrict?: MegaWebUnrestrictor;
|
megaWebUnrestrict?: MegaWebUnrestrictor;
|
||||||
};
|
};
|
||||||
@ -213,6 +228,10 @@ export class DownloadManager extends EventEmitter {
|
|||||||
|
|
||||||
private lastReconnectMarkAt = 0;
|
private lastReconnectMarkAt = 0;
|
||||||
|
|
||||||
|
private lastGlobalProgressBytes = 0;
|
||||||
|
|
||||||
|
private lastGlobalProgressAt = 0;
|
||||||
|
|
||||||
public constructor(settings: AppSettings, session: SessionState, storagePaths: StoragePaths, options: DownloadManagerOptions = {}) {
|
public constructor(settings: AppSettings, session: SessionState, storagePaths: StoragePaths, options: DownloadManagerOptions = {}) {
|
||||||
super();
|
super();
|
||||||
this.settings = settings;
|
this.settings = settings;
|
||||||
@ -1044,6 +1063,8 @@ export class DownloadManager extends EventEmitter {
|
|||||||
this.session.reconnectReason = "";
|
this.session.reconnectReason = "";
|
||||||
this.speedEvents = [];
|
this.speedEvents = [];
|
||||||
this.speedBytesLastWindow = 0;
|
this.speedBytesLastWindow = 0;
|
||||||
|
this.lastGlobalProgressBytes = 0;
|
||||||
|
this.lastGlobalProgressAt = nowMs();
|
||||||
this.summary = null;
|
this.summary = null;
|
||||||
this.persistSoon();
|
this.persistSoon();
|
||||||
this.emitState(true);
|
this.emitState(true);
|
||||||
@ -1064,6 +1085,8 @@ export class DownloadManager extends EventEmitter {
|
|||||||
this.lastReconnectMarkAt = 0;
|
this.lastReconnectMarkAt = 0;
|
||||||
this.speedEvents = [];
|
this.speedEvents = [];
|
||||||
this.speedBytesLastWindow = 0;
|
this.speedBytesLastWindow = 0;
|
||||||
|
this.lastGlobalProgressBytes = 0;
|
||||||
|
this.lastGlobalProgressAt = nowMs();
|
||||||
this.summary = null;
|
this.summary = null;
|
||||||
this.persistSoon();
|
this.persistSoon();
|
||||||
this.emitState(true);
|
this.emitState(true);
|
||||||
@ -1075,6 +1098,8 @@ export class DownloadManager extends EventEmitter {
|
|||||||
this.session.paused = false;
|
this.session.paused = false;
|
||||||
this.session.reconnectUntil = 0;
|
this.session.reconnectUntil = 0;
|
||||||
this.session.reconnectReason = "";
|
this.session.reconnectReason = "";
|
||||||
|
this.lastGlobalProgressBytes = this.session.totalDownloadedBytes;
|
||||||
|
this.lastGlobalProgressAt = nowMs();
|
||||||
this.abortPostProcessing("stop");
|
this.abortPostProcessing("stop");
|
||||||
for (const active of this.activeTasks.values()) {
|
for (const active of this.activeTasks.values()) {
|
||||||
active.abortReason = "stop";
|
active.abortReason = "stop";
|
||||||
@ -1090,6 +1115,8 @@ export class DownloadManager extends EventEmitter {
|
|||||||
this.session.paused = false;
|
this.session.paused = false;
|
||||||
this.session.reconnectUntil = 0;
|
this.session.reconnectUntil = 0;
|
||||||
this.session.reconnectReason = "";
|
this.session.reconnectReason = "";
|
||||||
|
this.lastGlobalProgressBytes = this.session.totalDownloadedBytes;
|
||||||
|
this.lastGlobalProgressAt = nowMs();
|
||||||
this.abortPostProcessing("shutdown");
|
this.abortPostProcessing("shutdown");
|
||||||
|
|
||||||
let requeuedItems = 0;
|
let requeuedItems = 0;
|
||||||
@ -1592,6 +1619,8 @@ export class DownloadManager extends EventEmitter {
|
|||||||
this.startItem(next.packageId, next.itemId);
|
this.startItem(next.packageId, next.itemId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.runGlobalStallWatchdog(now);
|
||||||
|
|
||||||
if (this.activeTasks.size === 0 && !this.hasQueuedItems() && this.packagePostProcessTasks.size === 0) {
|
if (this.activeTasks.size === 0 && !this.hasQueuedItems() && this.packagePostProcessTasks.size === 0) {
|
||||||
this.finishRun();
|
this.finishRun();
|
||||||
break;
|
break;
|
||||||
@ -1611,6 +1640,48 @@ export class DownloadManager extends EventEmitter {
|
|||||||
return this.session.reconnectUntil > nowMs();
|
return this.session.reconnectUntil > nowMs();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private runGlobalStallWatchdog(now: number): void {
|
||||||
|
const timeoutMs = getGlobalStallWatchdogTimeoutMs();
|
||||||
|
if (timeoutMs <= 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!this.session.running || this.session.paused || this.reconnectActive()) {
|
||||||
|
this.lastGlobalProgressBytes = this.session.totalDownloadedBytes;
|
||||||
|
this.lastGlobalProgressAt = now;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.session.totalDownloadedBytes !== this.lastGlobalProgressBytes) {
|
||||||
|
this.lastGlobalProgressBytes = this.session.totalDownloadedBytes;
|
||||||
|
this.lastGlobalProgressAt = now;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (now - this.lastGlobalProgressAt < timeoutMs) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const stalled = Array.from(this.activeTasks.values()).filter((active) => {
|
||||||
|
if (active.abortController.signal.aborted) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const item = this.session.items[active.itemId];
|
||||||
|
return Boolean(item && item.status === "downloading");
|
||||||
|
});
|
||||||
|
if (stalled.length === 0) {
|
||||||
|
this.lastGlobalProgressAt = now;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.warn(`Globaler Download-Stall erkannt (${Math.floor((now - this.lastGlobalProgressAt) / 1000)}s ohne Fortschritt), ${stalled.length} Task(s) neu starten`);
|
||||||
|
for (const active of stalled) {
|
||||||
|
active.abortReason = "stall";
|
||||||
|
active.abortController.abort("stall");
|
||||||
|
}
|
||||||
|
this.lastGlobalProgressAt = now;
|
||||||
|
}
|
||||||
|
|
||||||
private requestReconnect(reason: string): void {
|
private requestReconnect(reason: string): void {
|
||||||
if (!this.settings.autoReconnect) {
|
if (!this.settings.autoReconnect) {
|
||||||
return;
|
return;
|
||||||
@ -2757,6 +2828,8 @@ export class DownloadManager extends EventEmitter {
|
|||||||
this.runCompletedPackages.clear();
|
this.runCompletedPackages.clear();
|
||||||
this.reservedTargetPaths.clear();
|
this.reservedTargetPaths.clear();
|
||||||
this.claimedTargetPathByItem.clear();
|
this.claimedTargetPathByItem.clear();
|
||||||
|
this.lastGlobalProgressBytes = this.session.totalDownloadedBytes;
|
||||||
|
this.lastGlobalProgressAt = nowMs();
|
||||||
this.persistNow();
|
this.persistNow();
|
||||||
this.emitState();
|
this.emitState();
|
||||||
}
|
}
|
||||||
|
|||||||
@ -622,6 +622,110 @@ describe("download manager", () => {
|
|||||||
}
|
}
|
||||||
}, 35000);
|
}, 35000);
|
||||||
|
|
||||||
|
it("recovers via global watchdog when stream hangs without reader timeout", async () => {
|
||||||
|
const root = fs.mkdtempSync(path.join(os.tmpdir(), "rd-dm-"));
|
||||||
|
tempDirs.push(root);
|
||||||
|
const binary = Buffer.alloc(240 * 1024, 31);
|
||||||
|
const previousStallTimeout = process.env.RD_STALL_TIMEOUT_MS;
|
||||||
|
const previousConnectTimeout = process.env.RD_CONNECT_TIMEOUT_MS;
|
||||||
|
const previousGlobalWatchdog = process.env.RD_GLOBAL_STALL_TIMEOUT_MS;
|
||||||
|
process.env.RD_STALL_TIMEOUT_MS = "120000";
|
||||||
|
process.env.RD_CONNECT_TIMEOUT_MS = "120000";
|
||||||
|
process.env.RD_GLOBAL_STALL_TIMEOUT_MS = "2500";
|
||||||
|
let directCalls = 0;
|
||||||
|
|
||||||
|
const server = http.createServer((req, res) => {
|
||||||
|
if ((req.url || "") !== "/watchdog-stall") {
|
||||||
|
res.statusCode = 404;
|
||||||
|
res.end("not-found");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
directCalls += 1;
|
||||||
|
if (directCalls === 1) {
|
||||||
|
const firstChunk = Math.floor(binary.length / 3);
|
||||||
|
res.statusCode = 200;
|
||||||
|
res.setHeader("Accept-Ranges", "bytes");
|
||||||
|
res.setHeader("Content-Length", String(binary.length));
|
||||||
|
res.write(binary.subarray(0, firstChunk));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
res.statusCode = 200;
|
||||||
|
res.setHeader("Accept-Ranges", "bytes");
|
||||||
|
res.setHeader("Content-Length", String(binary.length));
|
||||||
|
res.end(binary);
|
||||||
|
});
|
||||||
|
|
||||||
|
server.listen(0, "127.0.0.1");
|
||||||
|
await once(server, "listening");
|
||||||
|
|
||||||
|
const address = server.address();
|
||||||
|
if (!address || typeof address === "string") {
|
||||||
|
throw new Error("server address unavailable");
|
||||||
|
}
|
||||||
|
const directUrl = `http://127.0.0.1:${address.port}/watchdog-stall`;
|
||||||
|
|
||||||
|
globalThis.fetch = async (input: RequestInfo | URL, init?: RequestInit): Promise<Response> => {
|
||||||
|
const url = typeof input === "string" ? input : input instanceof URL ? input.toString() : input.url;
|
||||||
|
if (url.includes("/unrestrict/link")) {
|
||||||
|
return new Response(
|
||||||
|
JSON.stringify({
|
||||||
|
download: directUrl,
|
||||||
|
filename: "watchdog-stall.bin",
|
||||||
|
filesize: binary.length
|
||||||
|
}),
|
||||||
|
{
|
||||||
|
status: 200,
|
||||||
|
headers: { "Content-Type": "application/json" }
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return originalFetch(input, init);
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
const manager = new DownloadManager(
|
||||||
|
{
|
||||||
|
...defaultSettings(),
|
||||||
|
token: "rd-token",
|
||||||
|
outputDir: path.join(root, "downloads"),
|
||||||
|
extractDir: path.join(root, "extract"),
|
||||||
|
autoExtract: false,
|
||||||
|
autoReconnect: false
|
||||||
|
},
|
||||||
|
emptySession(),
|
||||||
|
createStoragePaths(path.join(root, "state"))
|
||||||
|
);
|
||||||
|
|
||||||
|
manager.addPackages([{ name: "watchdog-stall", links: ["https://dummy/watchdog-stall"] }]);
|
||||||
|
manager.start();
|
||||||
|
await waitFor(() => !manager.getSnapshot().session.running, 30000);
|
||||||
|
|
||||||
|
const item = Object.values(manager.getSnapshot().session.items)[0];
|
||||||
|
expect(item?.status).toBe("completed");
|
||||||
|
expect(directCalls).toBeGreaterThan(1);
|
||||||
|
} finally {
|
||||||
|
if (previousStallTimeout === undefined) {
|
||||||
|
delete process.env.RD_STALL_TIMEOUT_MS;
|
||||||
|
} else {
|
||||||
|
process.env.RD_STALL_TIMEOUT_MS = previousStallTimeout;
|
||||||
|
}
|
||||||
|
if (previousConnectTimeout === undefined) {
|
||||||
|
delete process.env.RD_CONNECT_TIMEOUT_MS;
|
||||||
|
} else {
|
||||||
|
process.env.RD_CONNECT_TIMEOUT_MS = previousConnectTimeout;
|
||||||
|
}
|
||||||
|
if (previousGlobalWatchdog === undefined) {
|
||||||
|
delete process.env.RD_GLOBAL_STALL_TIMEOUT_MS;
|
||||||
|
} else {
|
||||||
|
process.env.RD_GLOBAL_STALL_TIMEOUT_MS = previousGlobalWatchdog;
|
||||||
|
}
|
||||||
|
server.close();
|
||||||
|
await once(server, "close");
|
||||||
|
}
|
||||||
|
}, 35000);
|
||||||
|
|
||||||
it("uses content-disposition filename when provider filename is opaque", async () => {
|
it("uses content-disposition filename when provider filename is opaque", async () => {
|
||||||
const root = fs.mkdtempSync(path.join(os.tmpdir(), "rd-dm-"));
|
const root = fs.mkdtempSync(path.join(os.tmpdir(), "rd-dm-"));
|
||||||
tempDirs.push(root);
|
tempDirs.push(root);
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user