mirror of
https://github.com/nimbold/Firelink.git
synced 2026-10-25 13:10:50 +00:00
fix: harden runtime updates and shutdown handling
- Upgrade frontend and Tauri dependencies; use process-group kills for yt-dlp descendants. - Refresh verified Windows/Linux FFmpeg sources and add HLS byte-range coverage. - Fence Aria2 handoffs across shutdown, retry, seeding, recovery, and user controls; fix expired queue-hold wakeups. - Preserve Linux AppImage autostart for install paths containing '='. - Refs #44
This commit is contained in:
@@ -119,6 +119,10 @@ jobs:
|
||||
if: runner.os == 'Windows'
|
||||
working-directory: src-tauri
|
||||
run: cargo test --lib --no-run --target ${{ matrix.target }}
|
||||
- name: Test Windows yt-dlp process-group cancellation
|
||||
if: runner.os == 'Windows'
|
||||
working-directory: src-tauri
|
||||
run: cargo test --lib tests::shell_process_group_kills_descendant_after_launcher_exit --target ${{ matrix.target }} -- --nocapture
|
||||
- name: Run Windows headless production contracts
|
||||
if: runner.os == 'Windows'
|
||||
working-directory: src-tauri
|
||||
|
||||
@@ -13,9 +13,9 @@
|
||||
"sha256": "a0c3101b4158d1dfb7d6a78a7bf0f3de80c96bb423c152beec8beb22786f2238"
|
||||
},
|
||||
"ffmpeg": {
|
||||
"version": "9.0.2-24-gfd5d616c29",
|
||||
"url": "https://github.com/BtbN/FFmpeg-Builds/releases/download/autobuild-2026-10-09-14-16/ffmpeg-n9.0.2-24-gfd5d616c29-win64-gpl-9.0.zip",
|
||||
"sha256": "9486304d0d6be88eade8715180e0cb198d45e22d10cf66d9a412c45df9a87157"
|
||||
"version": "9.0.2-27-g7e93014c5b",
|
||||
"url": "https://github.com/BtbN/FFmpeg-Builds/releases/download/autobuild-2026-10-12-13-12/ffmpeg-n9.0.2-27-g7e93014c5b-win64-gpl-9.0.zip",
|
||||
"sha256": "c4da71f5a7593802cbd16419def340c37beeb90d20d04f7dd02f0d4d08adb9e2"
|
||||
},
|
||||
"aria2c": {
|
||||
"version": "1.37.0-firelink-native-dns-v1",
|
||||
@@ -45,9 +45,9 @@
|
||||
"sha256": "c6527f24f4b16031d3ae4fa9f658d5f11534c8d84ce7dc8502420280919c3490"
|
||||
},
|
||||
"ffmpeg": {
|
||||
"version": "9.0.2-24-gfd5d616c29",
|
||||
"url": "https://github.com/BtbN/FFmpeg-Builds/releases/download/autobuild-2026-10-09-14-16/ffmpeg-n9.0.2-24-gfd5d616c29-linux64-gpl-9.0.tar.xz",
|
||||
"sha256": "0f792547d955e8f13fd80dfafb41002236492ddc3961fbd0ec680c6a8609e868"
|
||||
"version": "9.0.2-27-g7e93014c5b",
|
||||
"url": "https://github.com/BtbN/FFmpeg-Builds/releases/download/autobuild-2026-10-12-13-12/ffmpeg-n9.0.2-27-g7e93014c5b-linux64-gpl-9.0.tar.xz",
|
||||
"sha256": "99ac3849c1910e8a2e0dc16da11bbacb2bd6e8c445b5d180e5540cb57871a8bf"
|
||||
},
|
||||
"aria2c": {
|
||||
"version": "1.37.0-firelink-native-dns-v1",
|
||||
|
||||
Generated
+121
-121
@@ -11,33 +11,33 @@
|
||||
"dependencies": {
|
||||
"@fontsource-variable/inter": "^5.3.0",
|
||||
"@fontsource-variable/noto-sans-hebrew": "^5.3.0",
|
||||
"@fontsource-variable/noto-sans-sc": "^5.3.0",
|
||||
"@fontsource-variable/noto-sans-sc": "^5.3.1",
|
||||
"@fontsource-variable/outfit": "^5.3.0",
|
||||
"@fontsource-variable/roboto": "^5.3.0",
|
||||
"@fontsource-variable/vazirmatn": "^5.3.0",
|
||||
"@tailwindcss/vite": "^4.3.3",
|
||||
"@tauri-apps/api": "^2.12.2",
|
||||
"@tauri-apps/plugin-clipboard-manager": "^2.4.1",
|
||||
"@tauri-apps/plugin-dialog": "^2.8.1",
|
||||
"@tauri-apps/plugin-log": "^2.10.0",
|
||||
"@tauri-apps/plugin-notification": "^2.5.1",
|
||||
"@tauri-apps/plugin-opener": "^2.7.0",
|
||||
"i18next": "^26.4.2",
|
||||
"lucide-react": "^1.54.0",
|
||||
"@tauri-apps/api": "^2.12.3",
|
||||
"@tauri-apps/plugin-clipboard-manager": "^2.4.2",
|
||||
"@tauri-apps/plugin-dialog": "^2.8.2",
|
||||
"@tauri-apps/plugin-log": "^2.10.1",
|
||||
"@tauri-apps/plugin-notification": "^2.6.0",
|
||||
"@tauri-apps/plugin-opener": "^2.7.1",
|
||||
"i18next": "^26.4.4",
|
||||
"lucide-react": "^1.56.0",
|
||||
"react": "^19.3.0",
|
||||
"react-dom": "^19.3.0",
|
||||
"react-i18next": "^17.0.16",
|
||||
"zustand": "^5.0.15"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tauri-apps/cli": "^2.12.1",
|
||||
"@tauri-apps/cli": "^2.12.2",
|
||||
"@types/react": "^19.3.0",
|
||||
"@types/react-dom": "^19.3.0",
|
||||
"@vitejs/plugin-react": "^6.1.2",
|
||||
"tailwindcss": "^4.3.3",
|
||||
"typescript": "^7.0.2",
|
||||
"vite": "^8.3.4",
|
||||
"vitest": "^5.0.3"
|
||||
"vitest": "^5.0.4"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=22.12"
|
||||
@@ -71,9 +71,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@fontsource-variable/noto-sans-sc": {
|
||||
"version": "5.3.0",
|
||||
"resolved": "https://registry.npmjs.org/@fontsource-variable/noto-sans-sc/-/noto-sans-sc-5.3.0.tgz",
|
||||
"integrity": "sha512-lNar1dF7Ik/lHNPo/7JWG0TolXY29LtsqYgMvEysooZ5bsO9uH4shJmRrwyJ3PjyTPljhpMJEK0jDuLSU4vJ1w==",
|
||||
"version": "5.3.1",
|
||||
"resolved": "https://registry.npmjs.org/@fontsource-variable/noto-sans-sc/-/noto-sans-sc-5.3.1.tgz",
|
||||
"integrity": "sha512-UB96JJ17HyD5ehEU7Em0hDdh7fvTMB4B4Hzmq5c4QaNwwSLqTrdM2S+coySeBqCkXwh0DxqXT0b0wMrmzorFpQ==",
|
||||
"license": "OFL-1.1",
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ayuhito"
|
||||
@@ -754,9 +754,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@tauri-apps/api": {
|
||||
"version": "2.12.2",
|
||||
"resolved": "https://registry.npmjs.org/@tauri-apps/api/-/api-2.12.2.tgz",
|
||||
"integrity": "sha512-cr0FYEsbSQIeEuIIoC8bwdiHW3YlziMYOZ0j3R7F6kkIHSVrrZnV6CSjv/VQvFoyuERGxFc8NMtLJDXi5wYOYg==",
|
||||
"version": "2.12.3",
|
||||
"resolved": "https://registry.npmjs.org/@tauri-apps/api/-/api-2.12.3.tgz",
|
||||
"integrity": "sha512-9s0PKw0p4r3kFq92jhpivYSp+cSht15o0gXpZxuQd0+dV9KCexEVkNaLELVoLdJWyoH4KlfQo5h3W2LIM8siJg==",
|
||||
"license": "Apache-2.0 OR MIT",
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
@@ -764,9 +764,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@tauri-apps/cli": {
|
||||
"version": "2.12.1",
|
||||
"resolved": "https://registry.npmjs.org/@tauri-apps/cli/-/cli-2.12.1.tgz",
|
||||
"integrity": "sha512-kEDEiGzG+yAc5FeLxtXpES/VN+F2C8H0r4gDVtfRLKxT9np9101d9REG/Kgo2lr0hKi0IpDsODiHnU3+naOmLg==",
|
||||
"version": "2.12.2",
|
||||
"resolved": "https://registry.npmjs.org/@tauri-apps/cli/-/cli-2.12.2.tgz",
|
||||
"integrity": "sha512-PMSppzAnlT+BvZtRKuidxjRSm2r5t8kDpJ/czmmQUa0AtDNTwrjLE8vApK3phCWGg7isfnYL46Dl1jB4ArI0hA==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0 OR MIT",
|
||||
"bin": {
|
||||
@@ -780,23 +780,23 @@
|
||||
"url": "https://opencollective.com/tauri"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@tauri-apps/cli-darwin-arm64": "2.12.1",
|
||||
"@tauri-apps/cli-darwin-x64": "2.12.1",
|
||||
"@tauri-apps/cli-linux-arm-gnueabihf": "2.12.1",
|
||||
"@tauri-apps/cli-linux-arm64-gnu": "2.12.1",
|
||||
"@tauri-apps/cli-linux-arm64-musl": "2.12.1",
|
||||
"@tauri-apps/cli-linux-riscv64-gnu": "2.12.1",
|
||||
"@tauri-apps/cli-linux-x64-gnu": "2.12.1",
|
||||
"@tauri-apps/cli-linux-x64-musl": "2.12.1",
|
||||
"@tauri-apps/cli-win32-arm64-msvc": "2.12.1",
|
||||
"@tauri-apps/cli-win32-ia32-msvc": "2.12.1",
|
||||
"@tauri-apps/cli-win32-x64-msvc": "2.12.1"
|
||||
"@tauri-apps/cli-darwin-arm64": "2.12.2",
|
||||
"@tauri-apps/cli-darwin-x64": "2.12.2",
|
||||
"@tauri-apps/cli-linux-arm-gnueabihf": "2.12.2",
|
||||
"@tauri-apps/cli-linux-arm64-gnu": "2.12.2",
|
||||
"@tauri-apps/cli-linux-arm64-musl": "2.12.2",
|
||||
"@tauri-apps/cli-linux-riscv64-gnu": "2.12.2",
|
||||
"@tauri-apps/cli-linux-x64-gnu": "2.12.2",
|
||||
"@tauri-apps/cli-linux-x64-musl": "2.12.2",
|
||||
"@tauri-apps/cli-win32-arm64-msvc": "2.12.2",
|
||||
"@tauri-apps/cli-win32-ia32-msvc": "2.12.2",
|
||||
"@tauri-apps/cli-win32-x64-msvc": "2.12.2"
|
||||
}
|
||||
},
|
||||
"node_modules/@tauri-apps/cli-darwin-arm64": {
|
||||
"version": "2.12.1",
|
||||
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-darwin-arm64/-/cli-darwin-arm64-2.12.1.tgz",
|
||||
"integrity": "sha512-8bMnbpJ2jKO/ufrOPL65EMHC3ptw7fzeraoQkwibyfGLqIl+UCWsC0152kmNbW6r9NvVQduXqAZF5mH1QgTWhw==",
|
||||
"version": "2.12.2",
|
||||
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-darwin-arm64/-/cli-darwin-arm64-2.12.2.tgz",
|
||||
"integrity": "sha512-Lk3zaBTfzNvA7670Al5PNgo2M/95uaDPe1yp+ILJeDOmol87XiYuK5E5+FUbK5e6U6OORx9qpZdOo2U+5tETtQ==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -811,9 +811,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@tauri-apps/cli-darwin-x64": {
|
||||
"version": "2.12.1",
|
||||
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-darwin-x64/-/cli-darwin-x64-2.12.1.tgz",
|
||||
"integrity": "sha512-G+2qte8GNqNlEZ9ftd21rbu/APVkl3r/nzNDI7LVxEoqJWVNe9GbEgQUMH1r4PHT20Z2UxR3KjCkltIb0e/Wjg==",
|
||||
"version": "2.12.2",
|
||||
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-darwin-x64/-/cli-darwin-x64-2.12.2.tgz",
|
||||
"integrity": "sha512-2CyYjJ833JB3sKRz4B0W/Tv4oOVC16ws1bxdSCWj1XvrmanWwvZi99e5tNIoCHwHhcOm0jh+sh0PCGQyls6Z+w==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -828,9 +828,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@tauri-apps/cli-linux-arm-gnueabihf": {
|
||||
"version": "2.12.1",
|
||||
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-arm-gnueabihf/-/cli-linux-arm-gnueabihf-2.12.1.tgz",
|
||||
"integrity": "sha512-5b3n4DaTW+A1Zntnh4Yn44VyLrlPEpB8kMOBRfWkzNnzolBk+AYhKIfKjCLmoC8Mob3ETodxAEOMkE2YPKEWzQ==",
|
||||
"version": "2.12.2",
|
||||
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-arm-gnueabihf/-/cli-linux-arm-gnueabihf-2.12.2.tgz",
|
||||
"integrity": "sha512-uvt2xbone4JftKarelssi86Kqnl7JiG1Smh5+Ae+nZmtM+JUhTGmqkcqfUPXRW8Ygwog92POZvd4oGDN9j8eXQ==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
@@ -845,9 +845,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@tauri-apps/cli-linux-arm64-gnu": {
|
||||
"version": "2.12.1",
|
||||
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-arm64-gnu/-/cli-linux-arm64-gnu-2.12.1.tgz",
|
||||
"integrity": "sha512-APArHhDu+8pXSCkCQmvaz1nD3d8WTmlCoSS/oG475TJ6AATBvyQOAmf5qELgsQ4s2+8MOsop0Zd699RKELF1Cw==",
|
||||
"version": "2.12.2",
|
||||
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-arm64-gnu/-/cli-linux-arm64-gnu-2.12.2.tgz",
|
||||
"integrity": "sha512-DZlDJ24u3tlocKOYqzR+b2M+Qr+5WFcoMBZ7EfdhHN5j3ZXwBBpms4G4gyNvPCrak5vMzmdZ65ecBXPJcC+5sA==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -865,9 +865,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@tauri-apps/cli-linux-arm64-musl": {
|
||||
"version": "2.12.1",
|
||||
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-arm64-musl/-/cli-linux-arm64-musl-2.12.1.tgz",
|
||||
"integrity": "sha512-JaCxcLoV0oGtllVkLr4UAHWG/wTfkEunnUqnsEsGYOp0VRxnUwHg63wFBD0jU2ul5eFfSLIDS4RF2gfzsSo5Jg==",
|
||||
"version": "2.12.2",
|
||||
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-arm64-musl/-/cli-linux-arm64-musl-2.12.2.tgz",
|
||||
"integrity": "sha512-pb+RTZUYH/p0gR4j4ecCCq+FNfz3W3eKuUQvVEBn7MQZx6DzfLO7FaYzt2NyDKWpdNN3lP10mdzOvw3R5P5aAQ==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -885,9 +885,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@tauri-apps/cli-linux-riscv64-gnu": {
|
||||
"version": "2.12.1",
|
||||
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-riscv64-gnu/-/cli-linux-riscv64-gnu-2.12.1.tgz",
|
||||
"integrity": "sha512-K5+VXM7+SGVDwZleA2+lMgvbTVuA5e1tkiVEawu1Yxj26ZyEfO8h8xTC1rBdRX2xgJDSwCk6vPUBCS6i+Z5slQ==",
|
||||
"version": "2.12.2",
|
||||
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-riscv64-gnu/-/cli-linux-riscv64-gnu-2.12.2.tgz",
|
||||
"integrity": "sha512-nidEvDUJyHYNbIv9MUk6KiEix8IXfRh4Wj2G2SA5g8nFmgbBCwazUNWYiG/LhNeNbfZpBMLerRBUtoQ5K0GidA==",
|
||||
"cpu": [
|
||||
"riscv64"
|
||||
],
|
||||
@@ -905,9 +905,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@tauri-apps/cli-linux-x64-gnu": {
|
||||
"version": "2.12.1",
|
||||
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-x64-gnu/-/cli-linux-x64-gnu-2.12.1.tgz",
|
||||
"integrity": "sha512-Z1QGPr49HJZMktu+Spu6Q1b/f9ANadXvqZZZt2645ua1x1ev6prOgp85eiGv2jRzohFEMLo6aJyaLjLUX5KIvA==",
|
||||
"version": "2.12.2",
|
||||
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-x64-gnu/-/cli-linux-x64-gnu-2.12.2.tgz",
|
||||
"integrity": "sha512-gkXyYn3w+Xfc8nz/DleEbkm1M7bBuU2cejqUXNmI98cA2P5kp+oPmS4ALTorb1FaNWjpsKoYZBWsbS9BXxYFkQ==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -925,9 +925,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@tauri-apps/cli-linux-x64-musl": {
|
||||
"version": "2.12.1",
|
||||
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-x64-musl/-/cli-linux-x64-musl-2.12.1.tgz",
|
||||
"integrity": "sha512-nHuEhRPOMSJ/8RRhtMRxB9B9lVr7xxpe4DapBz8x1GIau3vdzjHHJ1hAfmw+PyayPLZ6XoaVIHPgI17rX6ii8w==",
|
||||
"version": "2.12.2",
|
||||
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-x64-musl/-/cli-linux-x64-musl-2.12.2.tgz",
|
||||
"integrity": "sha512-MrjgoZ09s8Hsn99XvxSNvK8NQTZxi7XiyK3FxxytzfHgz2jb3qUT69CZtsokRkN/zdi9rCgrMtq+w9XNICjcfQ==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -945,9 +945,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@tauri-apps/cli-win32-arm64-msvc": {
|
||||
"version": "2.12.1",
|
||||
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-win32-arm64-msvc/-/cli-win32-arm64-msvc-2.12.1.tgz",
|
||||
"integrity": "sha512-I0oPk2uGh86R8dAs2X5OxSP+/GqSc8jh1Px3l8EqrocSNivsRHDTh2k3PVOqdv163dlV6PIVvmhZAbH/lPfZrA==",
|
||||
"version": "2.12.2",
|
||||
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-win32-arm64-msvc/-/cli-win32-arm64-msvc-2.12.2.tgz",
|
||||
"integrity": "sha512-BR+CYM7iF3lkfyTtVkwMQU/s9hwegLpj3UqDjaxKCPmixQCa1csBBPy3C75V+FhLtOjIXNna4Gt6Vm9oxbV+qA==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -962,9 +962,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@tauri-apps/cli-win32-ia32-msvc": {
|
||||
"version": "2.12.1",
|
||||
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-win32-ia32-msvc/-/cli-win32-ia32-msvc-2.12.1.tgz",
|
||||
"integrity": "sha512-baNZRBfepAJ5qNFRMwzgBk94xQmkcmTZrNDjZjnzrq2vAaTC3NAhallb/zo3hyoCGQlMS13sdovktrg46NZz6Q==",
|
||||
"version": "2.12.2",
|
||||
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-win32-ia32-msvc/-/cli-win32-ia32-msvc-2.12.2.tgz",
|
||||
"integrity": "sha512-zODJo9l3TbNBM+GrO/yPqGeELNMK5cjIc+QHh1WQc3l8tD+jDZGTQyYuAxViCsM6Uh5uWJ//RnCZG9LXrvsXww==",
|
||||
"cpu": [
|
||||
"ia32"
|
||||
],
|
||||
@@ -979,9 +979,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@tauri-apps/cli-win32-x64-msvc": {
|
||||
"version": "2.12.1",
|
||||
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-win32-x64-msvc/-/cli-win32-x64-msvc-2.12.1.tgz",
|
||||
"integrity": "sha512-dIFaKslrzcbesb+bBh+E1R9km241pHyj32w6D9FPtvQeYpRUFbiCLEa/2S+S0UnIytfxepp4P+X8IerGqnXeYg==",
|
||||
"version": "2.12.2",
|
||||
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-win32-x64-msvc/-/cli-win32-x64-msvc-2.12.2.tgz",
|
||||
"integrity": "sha512-cQXI2ZaZmwBDt0XATCqASVanYizxkEE3ceQVlVHXaQ51HzsQUWA04c9Nz7LrrY2lgAu3jFMyat+8Yz3Z2R2qBA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -996,45 +996,45 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@tauri-apps/plugin-clipboard-manager": {
|
||||
"version": "2.4.1",
|
||||
"resolved": "https://registry.npmjs.org/@tauri-apps/plugin-clipboard-manager/-/plugin-clipboard-manager-2.4.1.tgz",
|
||||
"integrity": "sha512-5Q9hWWnsE7UF1xABWHpRwqGiyORrP9o/rJift/7V8woP/kSVF21W+bP9G7ch8Qd7pneDWUMk6hkfpLpjcC/4Ag==",
|
||||
"version": "2.4.2",
|
||||
"resolved": "https://registry.npmjs.org/@tauri-apps/plugin-clipboard-manager/-/plugin-clipboard-manager-2.4.2.tgz",
|
||||
"integrity": "sha512-bpQ6ONK2jcCVraBVtazdQ2X7bRK6P/vdi+D4jFDOJ9IbqR4cC8D6K3YGHR+c+cgZf2HQOE7hBEkRlybEVnCNPg==",
|
||||
"license": "MIT OR Apache-2.0",
|
||||
"dependencies": {
|
||||
"@tauri-apps/api": "^2.12.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@tauri-apps/plugin-dialog": {
|
||||
"version": "2.8.1",
|
||||
"resolved": "https://registry.npmjs.org/@tauri-apps/plugin-dialog/-/plugin-dialog-2.8.1.tgz",
|
||||
"integrity": "sha512-/DtE3B62JgpYuWFL5ATXM6KOXUq7pca7cJWOgSqULhZsgyjLk0PWKek1dbRDvhNBW6flNsOfWphsKnb+738Dmw==",
|
||||
"version": "2.8.2",
|
||||
"resolved": "https://registry.npmjs.org/@tauri-apps/plugin-dialog/-/plugin-dialog-2.8.2.tgz",
|
||||
"integrity": "sha512-LFqp8OTx0bHOwhKRhMMDyRDlo/2IO5XxoOclpf8+yvjAfM2ytVdbB6tk63dBlicsR8pRw38maj/GxrU6cW8mXw==",
|
||||
"license": "MIT OR Apache-2.0",
|
||||
"dependencies": {
|
||||
"@tauri-apps/api": "^2.12.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@tauri-apps/plugin-log": {
|
||||
"version": "2.10.0",
|
||||
"resolved": "https://registry.npmjs.org/@tauri-apps/plugin-log/-/plugin-log-2.10.0.tgz",
|
||||
"integrity": "sha512-IHMspJx/fDA+8q8O6MTbrKffkemSDa1I5iXRhnLrLVnOd7xK4bAosyQbWcjnpPA7E+c6BMkpsE2kESHhkiBrDg==",
|
||||
"version": "2.10.1",
|
||||
"resolved": "https://registry.npmjs.org/@tauri-apps/plugin-log/-/plugin-log-2.10.1.tgz",
|
||||
"integrity": "sha512-QYgxDunUZP+Cd3ev+8ItOc3Q6uUFkEhRHdTolxUtlC/8SGsc5IyVjGgFclprkAx736HUCUYfh2vqUr3qwNfTTw==",
|
||||
"license": "MIT OR Apache-2.0",
|
||||
"dependencies": {
|
||||
"@tauri-apps/api": "^2.12.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@tauri-apps/plugin-notification": {
|
||||
"version": "2.5.1",
|
||||
"resolved": "https://registry.npmjs.org/@tauri-apps/plugin-notification/-/plugin-notification-2.5.1.tgz",
|
||||
"integrity": "sha512-i0nD48YsLov18siENevC3X7SmJpQE1cImq+JUsjrUPdu73J8cTIOyNIS8H0qxftFAc1k2w6s1E74DoFE02Jirw==",
|
||||
"version": "2.6.0",
|
||||
"resolved": "https://registry.npmjs.org/@tauri-apps/plugin-notification/-/plugin-notification-2.6.0.tgz",
|
||||
"integrity": "sha512-QvpsQ6L6uSGT/7AmIrEUrfOiqjP1S8HtBU1ieaX6bvOFXGjnoxbNMR9V+8PVhLqRdOntqevpUJ3c52S+1afsIw==",
|
||||
"license": "MIT OR Apache-2.0",
|
||||
"dependencies": {
|
||||
"@tauri-apps/api": "^2.12.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@tauri-apps/plugin-opener": {
|
||||
"version": "2.7.0",
|
||||
"resolved": "https://registry.npmjs.org/@tauri-apps/plugin-opener/-/plugin-opener-2.7.0.tgz",
|
||||
"integrity": "sha512-mBorYfVKh9Lt7s4ZFtKbMUTxBw+Y/sVmSlKCtkkoBgMTC/DIbt/kaPm67L/2djAUqDI1wbixy5OHHVmipjuGMw==",
|
||||
"version": "2.7.1",
|
||||
"resolved": "https://registry.npmjs.org/@tauri-apps/plugin-opener/-/plugin-opener-2.7.1.tgz",
|
||||
"integrity": "sha512-XkXdt30rontti9TII/gBBxFRXWfj8DiprFW7RgBFRBj1OhdfjBNVcFqSEWkkZ4hXafHnuEnB7F1sFsmu6uyywQ==",
|
||||
"license": "MIT OR Apache-2.0",
|
||||
"dependencies": {
|
||||
"@tauri-apps/api": "^2.12.0"
|
||||
@@ -1436,22 +1436,22 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@vitest/mocker": {
|
||||
"version": "5.0.3",
|
||||
"resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-5.0.3.tgz",
|
||||
"integrity": "sha512-T8sWAIbkSyAjkwTcaEc3Iu0o9A27X1/kdXrizhZkGuSKScRQtRzclfAMpOTcGdXCsqxeWlpGy3XjqaW8CpLORg==",
|
||||
"version": "5.0.4",
|
||||
"resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-5.0.4.tgz",
|
||||
"integrity": "sha512-tqAlrYWce4iUxhr3ncNohejtCTOcHeE8XWrSNZY3OhPoDn01yeGv+gnaBy64ATp7o6pb03eIbBAjzmibpc9vIQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@jridgewell/trace-mapping": "0.3.31",
|
||||
"@vitest/spy": "5.0.3",
|
||||
"@vitest/spy": "5.0.4",
|
||||
"estree-walker": "^3.0.3",
|
||||
"magic-string": "^1.2.3"
|
||||
"magic-string": "^1.4.3"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/vitest"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"msw": "^2.4.9",
|
||||
"msw": "^2.4.9 || ^3.0.0",
|
||||
"vite": "^6.0.0 || ^7.0.0 || ^8.0.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
@@ -1464,9 +1464,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@vitest/mocker/node_modules/magic-string": {
|
||||
"version": "1.4.2",
|
||||
"resolved": "https://registry.npmjs.org/magic-string/-/magic-string-1.4.2.tgz",
|
||||
"integrity": "sha512-vG+rjFRj1PqdIBozIxAGMjPlOhaVe+GXpbttY/iSK7rGcJRMlwNJO7dcUwmUqkymsFLJiNGI06t4D7Fr7yRC9g==",
|
||||
"version": "1.4.3",
|
||||
"resolved": "https://registry.npmjs.org/magic-string/-/magic-string-1.4.3.tgz",
|
||||
"integrity": "sha512-z12OxmaPGE0F4xlpGdjuAUckipLpQlTAuAmyGhNoD7ODpYUzvDfvtxUbjM/jwznyP178oju/KDdyi220wNJ0RQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
@@ -1474,9 +1474,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@vitest/spy": {
|
||||
"version": "5.0.3",
|
||||
"resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-5.0.3.tgz",
|
||||
"integrity": "sha512-XhFysQTB8AZ+P4gMi+Lpo99vg2AZi0qKpaB9yXQl37+CaMEAPO3iH/wGVnSyL5MPERiLezpqTVtrR6UZH5GCXg==",
|
||||
"version": "5.0.4",
|
||||
"resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-5.0.4.tgz",
|
||||
"integrity": "sha512-MZKk/gSkm2q+ICobMxCpH1mA+cCAWm8WV7UhxlqGjq/sJ3AGc1BqFOs5CRVXxH78wQtme9q/NAAgBquj4AcyMw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
@@ -1606,9 +1606,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/i18next": {
|
||||
"version": "26.4.2",
|
||||
"resolved": "https://registry.npmjs.org/i18next/-/i18next-26.4.2.tgz",
|
||||
"integrity": "sha512-RX+R0VLg13IbvRuJSxnqykUFS9vQZTl8wYpWPCIUDWVrSGjsQywB5Y+pjzrkboxGAuYfJZVH1InFTdgBdxq6ug==",
|
||||
"version": "26.4.4",
|
||||
"resolved": "https://registry.npmjs.org/i18next/-/i18next-26.4.4.tgz",
|
||||
"integrity": "sha512-O+rqmQUi54OwAgnZ4SAwO6aRrbXZt36VZXWt6zvq0FpC3vJbH46oQRs6u4Uwm1vf8ZI7UqOXQiJhQVTor7vRiQ==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "individual",
|
||||
@@ -1904,9 +1904,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/lucide-react": {
|
||||
"version": "1.54.0",
|
||||
"resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-1.54.0.tgz",
|
||||
"integrity": "sha512-kVmnVCBoLztTGe3S76xu8utwxijRLOinknNq9e8btZ0R2fm6WCgxY+weaLiA50odo/yi+apm/4B2A/LsFZhTrw==",
|
||||
"version": "1.56.0",
|
||||
"resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-1.56.0.tgz",
|
||||
"integrity": "sha512-b+hqj30/trHBma5r1zlKb6yLewQp6tUtnz9loc3xdrYerPmvLYhlEbyPVCJ8DOg2/blBf6lwPLznftFVACka6A==",
|
||||
"license": "ISC",
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
@@ -1946,9 +1946,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/obug": {
|
||||
"version": "2.2.1",
|
||||
"resolved": "https://registry.npmjs.org/obug/-/obug-2.2.1.tgz",
|
||||
"integrity": "sha512-XrsrhT5sybtKI6wakr2SPOlGZWWYbUXZ7a0jT8/QOeAPau+1X/bSegNe5YR75oJmEZQbKningirmGOEJCIk61Q==",
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/obug/-/obug-3.0.0.tgz",
|
||||
"integrity": "sha512-5vvB5+W7ePv+p3uqxi+RcW1XAzLW0/hxt3/4X4Lc4qHudzOhmBiBwOY6DRob4WnanAEGvNcLjF+KNOufrUoEQw==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
"https://github.com/sponsors/sxzz",
|
||||
@@ -2128,9 +2128,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/tinybench": {
|
||||
"version": "6.2.0",
|
||||
"resolved": "https://registry.npmjs.org/tinybench/-/tinybench-6.2.0.tgz",
|
||||
"integrity": "sha512-78U2TlB2CnVenajOFzf3BKSm0J6oz5L0NV7g32LCPccvYc0lbWvys4d3uUUCS2B1N8PAf2+aekR8i1KbC3HO7Q==",
|
||||
"version": "6.2.2",
|
||||
"resolved": "https://registry.npmjs.org/tinybench/-/tinybench-6.2.2.tgz",
|
||||
"integrity": "sha512-nIZ2N2dFpjBsg2hgitKu7mA+FvsIJuGJOwysRngmVRRmWgR/UyPjwJj8c/fT/dhm8EkHnF5EOogRf+0Z2qrimA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
@@ -2546,23 +2546,23 @@
|
||||
}
|
||||
},
|
||||
"node_modules/vitest": {
|
||||
"version": "5.0.3",
|
||||
"resolved": "https://registry.npmjs.org/vitest/-/vitest-5.0.3.tgz",
|
||||
"integrity": "sha512-xMw97S3rjdtj5dkVat7jCsqWBpvchs3RlpQctUqwJD0KkERk40vz2fJ77lDwW/Vzh/pk18eItYAzkodhSes3jQ==",
|
||||
"version": "5.0.4",
|
||||
"resolved": "https://registry.npmjs.org/vitest/-/vitest-5.0.4.tgz",
|
||||
"integrity": "sha512-z0WxZRV2Sg48wF0FSgMq3TFG8JS38/Qr96vZVZdX6+Em9raQTvnXQvdTFJe2m3rfyJnEHeHQ/snceIdi8D1pVg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/chai": "^5.2.2",
|
||||
"@vitest/mocker": "5.0.3",
|
||||
"chai": "^6.2.2",
|
||||
"@vitest/mocker": "5.0.4",
|
||||
"chai": "^6.3.0",
|
||||
"es-module-lexer": "^2.3.2",
|
||||
"expect-type": "^1.4.0",
|
||||
"magic-string": "^1.2.3",
|
||||
"obug": "^2.1.4",
|
||||
"magic-string": "^1.4.3",
|
||||
"obug": "^3.0.0",
|
||||
"picomatch": "^4.0.7",
|
||||
"std-env": "^4.2.0",
|
||||
"tinybench": "^6.1.4",
|
||||
"tinyexec": "^1.3.0",
|
||||
"std-env": "^4.3.0",
|
||||
"tinybench": "^6.2.2",
|
||||
"tinyexec": "^1.3.1",
|
||||
"tinyglobby": "^0.2.17",
|
||||
"why-is-node-running": "3.2.1"
|
||||
},
|
||||
@@ -2579,12 +2579,12 @@
|
||||
"@edge-runtime/vm": "*",
|
||||
"@opentelemetry/api": "^1.9.0",
|
||||
"@types/node": "^22.0.0 || >=24.0.0",
|
||||
"@vitest/browser-playwright": "5.0.3",
|
||||
"@vitest/browser-preview": "5.0.3",
|
||||
"@vitest/browser-playwright": "5.0.4",
|
||||
"@vitest/browser-preview": "5.0.4",
|
||||
"@vitest/browser-webdriverio": "^5.0.0-beta.5 || >=5.0.0",
|
||||
"@vitest/coverage-istanbul": "5.0.3",
|
||||
"@vitest/coverage-v8": "5.0.3",
|
||||
"@vitest/ui": "5.0.3",
|
||||
"@vitest/coverage-istanbul": "5.0.4",
|
||||
"@vitest/coverage-v8": "5.0.4",
|
||||
"@vitest/ui": "5.0.4",
|
||||
"happy-dom": "*",
|
||||
"jsdom": "*",
|
||||
"vite": "^6.4.0 || ^7.0.0 || ^8.0.0"
|
||||
@@ -2629,9 +2629,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/vitest/node_modules/magic-string": {
|
||||
"version": "1.4.2",
|
||||
"resolved": "https://registry.npmjs.org/magic-string/-/magic-string-1.4.2.tgz",
|
||||
"integrity": "sha512-vG+rjFRj1PqdIBozIxAGMjPlOhaVe+GXpbttY/iSK7rGcJRMlwNJO7dcUwmUqkymsFLJiNGI06t4D7Fr7yRC9g==",
|
||||
"version": "1.4.3",
|
||||
"resolved": "https://registry.npmjs.org/magic-string/-/magic-string-1.4.3.tgz",
|
||||
"integrity": "sha512-z12OxmaPGE0F4xlpGdjuAUckipLpQlTAuAmyGhNoD7ODpYUzvDfvtxUbjM/jwznyP178oju/KDdyi220wNJ0RQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
|
||||
+11
-11
@@ -47,32 +47,32 @@
|
||||
"dependencies": {
|
||||
"@fontsource-variable/inter": "^5.3.0",
|
||||
"@fontsource-variable/noto-sans-hebrew": "^5.3.0",
|
||||
"@fontsource-variable/noto-sans-sc": "^5.3.0",
|
||||
"@fontsource-variable/noto-sans-sc": "^5.3.1",
|
||||
"@fontsource-variable/outfit": "^5.3.0",
|
||||
"@fontsource-variable/roboto": "^5.3.0",
|
||||
"@fontsource-variable/vazirmatn": "^5.3.0",
|
||||
"@tailwindcss/vite": "^4.3.3",
|
||||
"@tauri-apps/api": "^2.12.2",
|
||||
"@tauri-apps/plugin-clipboard-manager": "^2.4.1",
|
||||
"@tauri-apps/plugin-dialog": "^2.8.1",
|
||||
"@tauri-apps/plugin-log": "^2.10.0",
|
||||
"@tauri-apps/plugin-notification": "^2.5.1",
|
||||
"@tauri-apps/plugin-opener": "^2.7.0",
|
||||
"i18next": "^26.4.2",
|
||||
"lucide-react": "^1.54.0",
|
||||
"@tauri-apps/api": "^2.12.3",
|
||||
"@tauri-apps/plugin-clipboard-manager": "^2.4.2",
|
||||
"@tauri-apps/plugin-dialog": "^2.8.2",
|
||||
"@tauri-apps/plugin-log": "^2.10.1",
|
||||
"@tauri-apps/plugin-notification": "^2.6.0",
|
||||
"@tauri-apps/plugin-opener": "^2.7.1",
|
||||
"i18next": "^26.4.4",
|
||||
"lucide-react": "^1.56.0",
|
||||
"react": "^19.3.0",
|
||||
"react-dom": "^19.3.0",
|
||||
"react-i18next": "^17.0.16",
|
||||
"zustand": "^5.0.15"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tauri-apps/cli": "^2.12.1",
|
||||
"@tauri-apps/cli": "^2.12.2",
|
||||
"@types/react": "^19.3.0",
|
||||
"@types/react-dom": "^19.3.0",
|
||||
"@vitejs/plugin-react": "^6.1.2",
|
||||
"tailwindcss": "^4.3.3",
|
||||
"typescript": "^7.0.2",
|
||||
"vite": "^8.3.4",
|
||||
"vitest": "^5.0.3"
|
||||
"vitest": "^5.0.4"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,7 +3,9 @@
|
||||
import fs from 'node:fs';
|
||||
import os from 'node:os';
|
||||
import path from 'node:path';
|
||||
import { execFileSync } from 'node:child_process';
|
||||
import { execFileSync, spawn } from 'node:child_process';
|
||||
import { createReadStream } from 'node:fs';
|
||||
import { createServer } from 'node:http';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { resolveTargetTriple } from './engine-workspace.js';
|
||||
|
||||
@@ -47,6 +49,121 @@ const run = (label, args) => {
|
||||
}
|
||||
};
|
||||
|
||||
const runAsync = (label, args) => new Promise((resolve, reject) => {
|
||||
const child = spawn(ffmpegPath, [
|
||||
'-hide_banner',
|
||||
'-nostdin',
|
||||
'-loglevel', 'error',
|
||||
'-nostats',
|
||||
'-y',
|
||||
...args,
|
||||
], {
|
||||
cwd: workspace,
|
||||
stdio: ['ignore', 'ignore', 'pipe'],
|
||||
windowsHide: true,
|
||||
});
|
||||
const stderr = [];
|
||||
let stderrBytes = 0;
|
||||
let timedOut = false;
|
||||
const timeout = setTimeout(() => {
|
||||
timedOut = true;
|
||||
child.kill('SIGKILL');
|
||||
}, 30_000);
|
||||
|
||||
child.stderr.on('data', chunk => {
|
||||
stderrBytes += chunk.length;
|
||||
if (stderrBytes <= 8 * 1024 * 1024) stderr.push(chunk);
|
||||
else child.kill('SIGKILL');
|
||||
});
|
||||
child.once('error', error => {
|
||||
clearTimeout(timeout);
|
||||
reject(new Error(`${label} could not start: ${error.message}`));
|
||||
});
|
||||
child.once('close', (code, signal) => {
|
||||
clearTimeout(timeout);
|
||||
if (code === 0) {
|
||||
resolve();
|
||||
return;
|
||||
}
|
||||
const output = Buffer.concat(stderr).toString('utf8').trim();
|
||||
const reason = timedOut ? 'timed out after 30 seconds' : signal || code || 'unknown error';
|
||||
reject(new Error(`${label} failed (${reason})${output ? `:\n${output}` : '.'}`));
|
||||
});
|
||||
});
|
||||
|
||||
const startByteRangeHttpServer = async (playlistName, streamName) => {
|
||||
const playlistBody = fs.readFileSync(path.join(workspace, playlistName));
|
||||
const streamPath = path.join(workspace, streamName);
|
||||
const server = createServer((request, response) => {
|
||||
const requestPath = new URL(request.url ?? '/', 'http://127.0.0.1').pathname;
|
||||
if (requestPath === `/${playlistName}`) {
|
||||
response.writeHead(200, {
|
||||
'content-type': 'application/vnd.apple.mpegurl',
|
||||
'content-length': playlistBody.length,
|
||||
connection: 'close',
|
||||
});
|
||||
response.end(playlistBody);
|
||||
return;
|
||||
}
|
||||
if (requestPath !== `/${streamName}`) {
|
||||
response.writeHead(404, { connection: 'close' });
|
||||
response.end();
|
||||
return;
|
||||
}
|
||||
|
||||
const size = fs.statSync(streamPath).size;
|
||||
const rangeHeader = request.headers.range;
|
||||
if (rangeHeader === undefined) {
|
||||
response.writeHead(200, {
|
||||
'accept-ranges': 'bytes',
|
||||
'content-length': size,
|
||||
connection: 'close',
|
||||
});
|
||||
createReadStream(streamPath).pipe(response);
|
||||
return;
|
||||
}
|
||||
|
||||
const match = /^bytes=(\d*)-(\d*)$/.exec(rangeHeader);
|
||||
if (!match || (!match[1] && !match[2])) {
|
||||
response.writeHead(416, { 'content-range': `bytes */${size}`, connection: 'close' });
|
||||
response.end();
|
||||
return;
|
||||
}
|
||||
let start = match[1] ? Number(match[1]) : Math.max(0, size - Number(match[2]));
|
||||
const end = match[1]
|
||||
? (match[2] ? Math.min(Number(match[2]), size - 1) : size - 1)
|
||||
: size - 1;
|
||||
if (!Number.isSafeInteger(start) || !Number.isSafeInteger(end) || start >= size || end < start) {
|
||||
response.writeHead(416, { 'content-range': `bytes */${size}`, connection: 'close' });
|
||||
response.end();
|
||||
return;
|
||||
}
|
||||
|
||||
response.writeHead(206, {
|
||||
'accept-ranges': 'bytes',
|
||||
'content-range': `bytes ${start}-${end}/${size}`,
|
||||
'content-length': end - start + 1,
|
||||
connection: 'close',
|
||||
});
|
||||
createReadStream(streamPath, { start, end }).pipe(response);
|
||||
});
|
||||
|
||||
await new Promise((resolve, reject) => {
|
||||
server.once('error', reject);
|
||||
server.listen(0, '127.0.0.1', resolve);
|
||||
});
|
||||
const address = server.address();
|
||||
if (!address || typeof address === 'string') {
|
||||
await new Promise(resolve => server.close(resolve));
|
||||
throw new Error('The local byte-range server did not bind an IP address.');
|
||||
}
|
||||
|
||||
return {
|
||||
url: `http://127.0.0.1:${address.port}/${playlistName}`,
|
||||
close: () => new Promise(resolve => server.close(resolve)),
|
||||
};
|
||||
};
|
||||
|
||||
const assertDecodesAudioAndVideo = (label, input) => {
|
||||
run(`${label} audio/video stream check`, [
|
||||
'-i', input,
|
||||
@@ -165,6 +282,41 @@ try {
|
||||
assertNonEmpty('HLS input remux', hlsRemux);
|
||||
assertDecodesAudioAndVideo('HLS remux', hlsRemux);
|
||||
|
||||
const byteRangePlaylist = 'stream-byterange.m3u8';
|
||||
const byteRangeStream = 'stream-byterange.ts';
|
||||
run('HLS byte-range packaging', [
|
||||
'-i', source,
|
||||
'-map', '0:v:0',
|
||||
'-map', '0:a:0',
|
||||
'-c', 'copy',
|
||||
'-f', 'hls',
|
||||
'-hls_time', '1',
|
||||
'-hls_playlist_type', 'vod',
|
||||
'-hls_flags', 'single_file',
|
||||
'-hls_segment_filename', byteRangeStream,
|
||||
byteRangePlaylist,
|
||||
]);
|
||||
assertNonEmpty('HLS byte-range packaging', byteRangePlaylist);
|
||||
if (!fs.readFileSync(path.join(workspace, byteRangePlaylist), 'utf8').includes('#EXT-X-BYTERANGE:')) {
|
||||
throw new Error('HLS byte-range packaging produced no byte-range segments.');
|
||||
}
|
||||
|
||||
const byteRangeRemux = 'hls-byterange-remux.mkv';
|
||||
const byteRangeServer = await startByteRangeHttpServer(byteRangePlaylist, byteRangeStream);
|
||||
try {
|
||||
await runAsync('HLS byte-range input remux', [
|
||||
'-i', byteRangeServer.url,
|
||||
'-map', '0:v:0',
|
||||
'-map', '0:a:0',
|
||||
'-c', 'copy',
|
||||
byteRangeRemux,
|
||||
]);
|
||||
} finally {
|
||||
await byteRangeServer.close();
|
||||
}
|
||||
assertNonEmpty('HLS byte-range input remux', byteRangeRemux);
|
||||
assertDecodesAudioAndVideo('HLS byte-range remux', byteRangeRemux);
|
||||
|
||||
const dashManifest = 'stream.mpd';
|
||||
run('DASH packaging', [
|
||||
'-i', source,
|
||||
@@ -193,7 +345,7 @@ try {
|
||||
assertNonEmpty('DASH input remux', dashRemux);
|
||||
assertDecodesAudioAndVideo('DASH remux', dashRemux);
|
||||
|
||||
console.log(`FFmpeg media smoke passed for ${target} (Vorbis encode/remux, split/merge, HLS, and DASH).`);
|
||||
console.log(`FFmpeg media smoke passed for ${target} (Vorbis encode/remux, split/merge, HLS byte ranges, HLS, and DASH).`);
|
||||
} finally {
|
||||
fs.rmSync(workspace, { recursive: true, force: true, maxRetries: 5, retryDelay: 100 });
|
||||
}
|
||||
|
||||
Generated
+90
-109
@@ -200,7 +200,7 @@ checksum = "4f8abc12baad266b1c8cec146854c195b5864b4221d4b2ca7296a7ae82d9e451"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 3.0.6",
|
||||
"syn 3.0.9",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -235,7 +235,7 @@ checksum = "82f6aeea286b8eb4dd3431a1be1b59d290ace00f5bfd8e2a159bc2a05e2c1667"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 3.0.6",
|
||||
"syn 3.0.9",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -578,9 +578,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "cc"
|
||||
version = "1.6.0"
|
||||
version = "1.8.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f74872d07caf508b30a21f6836e7d7016a2eaf7d9ff4f48deaa58cd8a0407630"
|
||||
checksum = "6651c9ed80effdc7db0ff72512157f901af5e3549e341e24b1dd4887d836d838"
|
||||
dependencies = [
|
||||
"find-msvc-tools",
|
||||
"shlex",
|
||||
@@ -869,7 +869,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d045de693cb712d0b22c6a64be5b953f67b3ce00ab5ad3dd5d8b441886ab8e1a"
|
||||
dependencies = [
|
||||
"quote",
|
||||
"syn 3.0.6",
|
||||
"syn 2.0.119",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -931,7 +931,7 @@ dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"strsim",
|
||||
"syn 3.0.6",
|
||||
"syn 3.0.9",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -953,7 +953,7 @@ checksum = "2ac7135c3ef02b2f7833bbeb1be5ba7f966dcde8a87c6b87f65a778d71a02785"
|
||||
dependencies = [
|
||||
"darling_core 0.24.1",
|
||||
"quote",
|
||||
"syn 3.0.6",
|
||||
"syn 3.0.9",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1006,9 +1006,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "deranged"
|
||||
version = "0.5.8"
|
||||
version = "0.5.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c"
|
||||
checksum = "9e9de72ce2ad1f90dc62fa25f0f430ef85eb4b0d8fa0be4f30373bc40a21d28e"
|
||||
dependencies = [
|
||||
"serde_core",
|
||||
]
|
||||
@@ -1114,7 +1114,7 @@ dependencies = [
|
||||
"libc",
|
||||
"option-ext",
|
||||
"redox_users",
|
||||
"windows-sys 0.61.2",
|
||||
"windows-sys 0.60.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1137,7 +1137,7 @@ checksum = "c6232dd377dcc64799954cbd3a9bb882e9cdc1308ccd87b1c098f1fb2eaf82a8"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 3.0.6",
|
||||
"syn 3.0.9",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1324,7 +1324,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"windows-sys 0.61.2",
|
||||
"windows-sys 0.52.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1438,7 +1438,7 @@ dependencies = [
|
||||
"serde",
|
||||
"serde_json",
|
||||
"sha1",
|
||||
"sha2 0.11.0",
|
||||
"sha2 0.11.1",
|
||||
"smappservice-rs",
|
||||
"sysinfo",
|
||||
"system_shutdown",
|
||||
@@ -1520,7 +1520,7 @@ checksum = "ea5190182e6915eb873ddbc16e23b711b6eb1f9c00a0d0a3a91b5f6228475225"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 3.0.6",
|
||||
"syn 3.0.9",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1591,7 +1591,7 @@ checksum = "9fb9654ba8355388abeb8dcb4fc62f511300867002afc858860463bdd9fe0c44"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 3.0.6",
|
||||
"syn 3.0.9",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -2590,9 +2590,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "keyring-core"
|
||||
version = "1.0.0"
|
||||
version = "1.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fb1e621458ca9c51aa110bd0339d4751a056b9576bf1253aee1aa560dda0fc9d"
|
||||
checksum = "29fbf4b551298c67925f38cf341a1ada797a75f1837bdf8ae1fc8e2ebc6efb59"
|
||||
dependencies = [
|
||||
"log",
|
||||
]
|
||||
@@ -2804,7 +2804,7 @@ dependencies = [
|
||||
"png 0.18.1",
|
||||
"serde",
|
||||
"thiserror 2.0.21",
|
||||
"windows-sys 0.61.2",
|
||||
"windows-sys 0.60.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -3338,7 +3338,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7d8fae84b431384b68627d0f9b3b1245fcf9f46f6c0e3dc902e9dce64edd1967"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"windows-sys 0.61.2",
|
||||
"windows-sys 0.45.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -3787,7 +3787,7 @@ checksum = "92ecd8964f8453721699a1ed72037b0db49ce2f5a5138486ee89bed6f67cdf3a"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 3.0.6",
|
||||
"syn 3.0.9",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -3956,7 +3956,7 @@ dependencies = [
|
||||
"errno",
|
||||
"libc",
|
||||
"linux-raw-sys",
|
||||
"windows-sys 0.61.2",
|
||||
"windows-sys 0.52.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -3975,9 +3975,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "rustls-native-certs"
|
||||
version = "0.8.4"
|
||||
version = "0.8.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "dab5152771c58876a2146916e53e35057e1a4dfa2b9df0f0305b07f611fdea4d"
|
||||
checksum = "4a073f5dc7469f984c52ad2752b63b0807745133b6de880b7b64c1ac4c48aec4"
|
||||
dependencies = [
|
||||
"openssl-probe",
|
||||
"rustls-pki-types",
|
||||
@@ -4012,7 +4012,7 @@ dependencies = [
|
||||
"security-framework",
|
||||
"security-framework-sys",
|
||||
"webpki-root-certs",
|
||||
"windows-sys 0.61.2",
|
||||
"windows-sys 0.52.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -4091,9 +4091,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "schemars"
|
||||
version = "1.2.2"
|
||||
version = "1.2.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "687274d293b6cdc6e73e0fee520bf2049650090d7164f87672d212a3c530cf4a"
|
||||
checksum = "ab508826f74a77ca9d5aba6ff19b522583ee3eaf28a19384ff3d0e5835fadf6e"
|
||||
dependencies = [
|
||||
"dyn-clone",
|
||||
"ref-cast",
|
||||
@@ -4134,7 +4134,7 @@ dependencies = [
|
||||
"num",
|
||||
"once_cell",
|
||||
"serde",
|
||||
"sha2 0.11.0",
|
||||
"sha2 0.11.1",
|
||||
"zbus",
|
||||
]
|
||||
|
||||
@@ -4229,7 +4229,7 @@ checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 3.0.6",
|
||||
"syn 3.0.9",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -4245,9 +4245,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "serde_json"
|
||||
version = "1.0.151"
|
||||
version = "1.0.154"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c841b55ecdae098c80dcae9cf767f6f8a0c2cdb3416bbef72181df4d0fe73f14"
|
||||
checksum = "e7e9cc8b1b85264074fbcc02a88680c4096b1e47df8f739dceb03bf482f04bd6"
|
||||
dependencies = [
|
||||
"itoa",
|
||||
"memchr",
|
||||
@@ -4275,7 +4275,7 @@ checksum = "8d3b1629de253c70a0508c3899572da79ca359fdab27c7920ff00406df418906"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 3.0.6",
|
||||
"syn 3.0.9",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -4322,7 +4322,7 @@ dependencies = [
|
||||
"indexmap 2.14.2",
|
||||
"jiff",
|
||||
"schemars 0.9.0",
|
||||
"schemars 1.2.2",
|
||||
"schemars 1.2.3",
|
||||
"serde_core",
|
||||
"serde_json",
|
||||
"serde_with_macros",
|
||||
@@ -4338,7 +4338,7 @@ dependencies = [
|
||||
"darling 0.24.1",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 3.0.6",
|
||||
"syn 3.0.9",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -4396,9 +4396,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "sha2"
|
||||
version = "0.11.0"
|
||||
version = "0.11.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "446ba717509524cb3f22f17ecc096f10f4822d76ab5c0b9822c5f9c284e825f4"
|
||||
checksum = "47d7069beb7d6ac7b9acd1039986e73443f24234f41074da099d6f994ac9ad19"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"cpufeatures 0.3.1",
|
||||
@@ -4412,7 +4412,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "607549934f6cc26b89cfecfdc46fa90f1e5d1536a68349b0c3a4f9d1c0d37959"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"sigchld",
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
@@ -4422,27 +4421,6 @@ version = "2.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba"
|
||||
|
||||
[[package]]
|
||||
name = "sigchld"
|
||||
version = "0.2.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "24f2b37f04360cd465089b87a9c3869c08220a2f3458463f0adf8badf5e77f2c"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"os_pipe",
|
||||
"signal-hook",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "signal-hook"
|
||||
version = "0.4.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b29ffa27d3d59a049bb328258de100f8012848d1b08d444e08bbc02ac468435e"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"signal-hook-registry",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "signal-hook-registry"
|
||||
version = "1.4.8"
|
||||
@@ -4489,9 +4467,9 @@ checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5"
|
||||
|
||||
[[package]]
|
||||
name = "smallvec"
|
||||
version = "1.16.2"
|
||||
version = "1.16.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f9395f0f0eee849a9b707b2f06bb92a6a422090e2123bb2ef8e87a0e61892a8e"
|
||||
checksum = "5b3dc8af474f516a851ff4bd12db780f948b9250ad37211e4eec0bccea54e01b"
|
||||
|
||||
[[package]]
|
||||
name = "smappservice-rs"
|
||||
@@ -4512,7 +4490,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c3d1e2c7f27f8d4cb10542a02c49005dbd6e93095799d6f3be745fae9f8fedd4"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"windows-sys 0.61.2",
|
||||
"windows-sys 0.60.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -4619,9 +4597,9 @@ checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292"
|
||||
|
||||
[[package]]
|
||||
name = "swift-rs"
|
||||
version = "1.0.8"
|
||||
version = "1.0.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e45c444e496845d3f2a351146bff59aae4975b2280238df1dfaa0c7d1846f38e"
|
||||
checksum = "cefd87076fd385308ee4aff597256c902f1fbb4d031de1558efc12f8ffefadd9"
|
||||
dependencies = [
|
||||
"base64 0.21.7",
|
||||
"serde",
|
||||
@@ -4651,9 +4629,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "syn"
|
||||
version = "3.0.6"
|
||||
version = "3.0.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8593e8e72159ed2257d083c7a454a85cbf854f37a0966d8d483aff8c8a3ebcee"
|
||||
checksum = "d78c8dee4c7bf0e14673097256fed6142ce9d3b85a408189d07482442145823b"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
@@ -4677,7 +4655,7 @@ checksum = "901704edd0dfe137f1987838ee4f259e4e063c31371bdb423f7ae38ec6f77f02"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 3.0.6",
|
||||
"syn 3.0.9",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -4777,9 +4755,9 @@ checksum = "61c41af27dd6d1e27b1b16b489db798443478cef1f06a660c96db617ba5de3b1"
|
||||
|
||||
[[package]]
|
||||
name = "tauri"
|
||||
version = "2.12.2"
|
||||
version = "2.12.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d061d042ccd2df9c9f1508c1e63d835278c0a4c8801eb41764080e51f2eaaf34"
|
||||
checksum = "c404ecf956ff241bf6b39dd688614724e1bf1f63255abb4fc46d1c40371672ea"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"bytes",
|
||||
@@ -4829,9 +4807,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "tauri-build"
|
||||
version = "2.7.1"
|
||||
version = "2.7.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "59563ca5b331fd97f27ee9672ec8d36d33bb3c9e2c9f6710a1a5687256052e45"
|
||||
checksum = "2eea0b6302f82c574b21af6fc6ae6e86183b9280853c22621d0abf0936911ffc"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"cargo_toml",
|
||||
@@ -4850,11 +4828,11 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "tauri-codegen"
|
||||
version = "2.7.1"
|
||||
version = "2.7.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e50213e463086ecc283d7e9f51c30cc0fd304ad457425158018be26dbf0d0a80"
|
||||
checksum = "02f468f9a9bcd404f57e5c191d91024e3de953d4b610f78c2907e7110615cb1c"
|
||||
dependencies = [
|
||||
"base64 0.22.1",
|
||||
"base64 0.23.1",
|
||||
"brotli",
|
||||
"ico",
|
||||
"json-patch",
|
||||
@@ -4877,9 +4855,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "tauri-macros"
|
||||
version = "2.7.1"
|
||||
version = "2.7.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8129fb43bb6e0d6b73ad3315cd172beaab17cd28899c594b1b32c80edbbfd99c"
|
||||
checksum = "131bffec939b20642380317f8403a33c8888489925e14bb965ceaff0d949b1cb"
|
||||
dependencies = [
|
||||
"heck 0.5.0",
|
||||
"proc-macro2",
|
||||
@@ -4907,9 +4885,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "tauri-plugin-clipboard-manager"
|
||||
version = "2.4.1"
|
||||
version = "2.4.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "333d3b6beeb1c2c7fc040233d4ea7b6c03a9f8f039592b7400833c43fb5b568a"
|
||||
checksum = "640d0789c9db02265a800fded60520df5a3baa4a1b5f40715b83d58842c24fcb"
|
||||
dependencies = [
|
||||
"arboard",
|
||||
"log",
|
||||
@@ -4943,9 +4921,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "tauri-plugin-dialog"
|
||||
version = "2.8.1"
|
||||
version = "2.8.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "daf9a5c92e39bdd84f22f6be9230e39d0c4e140345f71ffe6058dca4f71127cf"
|
||||
checksum = "3dca325d6e66e9170f2e1b9631722e4e544bcbc51781575d266abdaba697bd51"
|
||||
dependencies = [
|
||||
"log",
|
||||
"raw-window-handle",
|
||||
@@ -4961,9 +4939,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "tauri-plugin-fs"
|
||||
version = "2.6.0"
|
||||
version = "2.7.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "466eb32466fe5021b6728b06862f9ff14a9f7e1af890047654e03a96d7c0e1bb"
|
||||
checksum = "213fae2d85d4b76b06de5915d1862db9fa74541171f9d2c15da6ac9bd6e788c6"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"dunce",
|
||||
@@ -4985,9 +4963,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "tauri-plugin-log"
|
||||
version = "2.10.0"
|
||||
version = "2.10.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "850d1e8f27d96203b7e6e6da1a2bf034c10f465b64a17c97e06940c9006385bf"
|
||||
checksum = "d83980f6765feb86cf2224959246be28b0488e14a23204a719f7f9deecfc64c9"
|
||||
dependencies = [
|
||||
"android_logger",
|
||||
"fern",
|
||||
@@ -5006,9 +4984,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "tauri-plugin-notification"
|
||||
version = "2.5.1"
|
||||
version = "2.6.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "533770889abb60a0407d5a32096182f0e6463f5a3dfaa675be94580aa14070e1"
|
||||
checksum = "8cf3cce3ea492b3a8a7f431a4e5dd5d31dc41b2a4a243faa660cbe8095a577bd"
|
||||
dependencies = [
|
||||
"log",
|
||||
"notify-rust",
|
||||
@@ -5021,13 +4999,14 @@ dependencies = [
|
||||
"thiserror 2.0.21",
|
||||
"time",
|
||||
"url",
|
||||
"zbus",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tauri-plugin-opener"
|
||||
version = "2.7.0"
|
||||
version = "2.7.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d39404b2572927e0d3ed30a7ca226037f7508d22e79c96ee6ef0d42b6a4b3345"
|
||||
checksum = "85dead674ee41937d042ee855d785f07ee67206f19402200c12b9255964d5ac8"
|
||||
dependencies = [
|
||||
"dunce",
|
||||
"glob",
|
||||
@@ -5047,11 +5026,12 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "tauri-plugin-shell"
|
||||
version = "2.4.1"
|
||||
version = "2.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9bd8f131731087b2608ad604969af43b06c5411d094d635a81fc8c852dda5634"
|
||||
checksum = "8567d42b2c7264355daa1df415ec77f52aa42e885924c86a969be2e920e05521"
|
||||
dependencies = [
|
||||
"encoding_rs",
|
||||
"libc",
|
||||
"log",
|
||||
"open",
|
||||
"os_pipe",
|
||||
@@ -5064,13 +5044,14 @@ dependencies = [
|
||||
"tauri-plugin",
|
||||
"thiserror 2.0.21",
|
||||
"tokio",
|
||||
"windows-sys 0.60.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tauri-plugin-single-instance"
|
||||
version = "2.5.2"
|
||||
version = "2.5.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ee2c8fe2d6b75caed0153f91eda30ea1dea3bfb6710d5adb80456d2c035a569d"
|
||||
checksum = "3c142ed88deee222bd2d979269d35c73b6c1c0f6ebd5b79b4ff80066fcad6af1"
|
||||
dependencies = [
|
||||
"serde",
|
||||
"serde_json",
|
||||
@@ -5204,7 +5185,7 @@ dependencies = [
|
||||
"getrandom 0.4.3",
|
||||
"once_cell",
|
||||
"rustix",
|
||||
"windows-sys 0.61.2",
|
||||
"windows-sys 0.52.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -5262,7 +5243,7 @@ checksum = "fe5197923287db20a58125f0bc85c062f7f2c892de97b18c356f9efb14b28524"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 3.0.6",
|
||||
"syn 3.0.9",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -5360,7 +5341,7 @@ checksum = "78773a2a397f451582ce068015985c33193cf6dea8b74d2a639fe457b2f07b0e"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 3.0.6",
|
||||
"syn 3.0.9",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -5626,7 +5607,7 @@ dependencies = [
|
||||
"png 0.18.1",
|
||||
"serde",
|
||||
"thiserror 2.0.21",
|
||||
"windows-sys 0.61.2",
|
||||
"windows-sys 0.60.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -5705,14 +5686,14 @@ checksum = "f2f6fb2847f6742cd76af783a2a2c49e9375d0a111c7bef6f71cd9e738c72d6e"
|
||||
dependencies = [
|
||||
"memoffset",
|
||||
"tempfile",
|
||||
"windows-sys 0.61.2",
|
||||
"windows-sys 0.60.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "unicode-ident"
|
||||
version = "1.0.26"
|
||||
version = "1.0.27"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d245f478577f809a851594d02313b640fb437e0bb33866753cff937863096954"
|
||||
checksum = "a2c754d6c33795a1c324727428e5a7dedb5b06195f9890bdbcba760d3e246563"
|
||||
|
||||
[[package]]
|
||||
name = "unicode-normalization"
|
||||
@@ -5774,9 +5755,9 @@ checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be"
|
||||
|
||||
[[package]]
|
||||
name = "uuid"
|
||||
version = "1.27.0"
|
||||
version = "1.28.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "97277d36b9c3ace13e58fa6e753f8b0bbbf302a18dd193240d70f9e29681059a"
|
||||
checksum = "7cc1186384beb7dd8eedea376413fd654937285ea6c9cfbb928dc3043ea4b606"
|
||||
dependencies = [
|
||||
"getrandom 0.4.3",
|
||||
"js-sys",
|
||||
@@ -5899,7 +5880,7 @@ dependencies = [
|
||||
"bumpalo",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 3.0.6",
|
||||
"syn 3.0.9",
|
||||
"wasm-bindgen-shared",
|
||||
]
|
||||
|
||||
@@ -6142,7 +6123,7 @@ version = "0.1.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22"
|
||||
dependencies = [
|
||||
"windows-sys 0.61.2",
|
||||
"windows-sys 0.52.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -6163,7 +6144,7 @@ dependencies = [
|
||||
"objc2-foundation",
|
||||
"objc2-quartz-core",
|
||||
"raw-window-handle",
|
||||
"windows-sys 0.61.2",
|
||||
"windows-sys 0.60.2",
|
||||
"windows-version",
|
||||
]
|
||||
|
||||
@@ -6557,7 +6538,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7d6f32a0ff4a9f6f01231eb2059cc85479330739333e0e58cadf03b6af2cca10"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"windows-sys 0.61.2",
|
||||
"windows-sys 0.60.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -6691,7 +6672,7 @@ checksum = "ec8ebde2db3681e8c9980cc27822030e68752690ddfa9473e739aeb4dbde6d71"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 3.0.6",
|
||||
"syn 3.0.9",
|
||||
"synstructure",
|
||||
]
|
||||
|
||||
@@ -6750,7 +6731,7 @@ dependencies = [
|
||||
"proc-macro-crate 3.5.0",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 3.0.6",
|
||||
"syn 3.0.9",
|
||||
"zbus_names",
|
||||
"zvariant",
|
||||
"zvariant_utils",
|
||||
@@ -6813,7 +6794,7 @@ checksum = "f75b4683f6c7f45248d4d64056a24298c6281e0993356d7d1b4a1a962ef10d4a"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 3.0.6",
|
||||
"syn 3.0.9",
|
||||
"synstructure",
|
||||
]
|
||||
|
||||
@@ -6853,7 +6834,7 @@ checksum = "34df6fc39dbd26ddc9c10e6a2984476e13acce22e64e4487636ef494369225da"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 3.0.6",
|
||||
"syn 3.0.9",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -6907,7 +6888,7 @@ dependencies = [
|
||||
"proc-macro-crate 3.5.0",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 3.0.6",
|
||||
"syn 3.0.9",
|
||||
"zvariant_utils",
|
||||
]
|
||||
|
||||
@@ -6920,6 +6901,6 @@ dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"serde",
|
||||
"syn 3.0.6",
|
||||
"syn 3.0.9",
|
||||
"winnow 1.0.4",
|
||||
]
|
||||
|
||||
+10
-10
@@ -20,13 +20,13 @@ name = "firelink_lib"
|
||||
crate-type = ["staticlib", "cdylib", "rlib"]
|
||||
|
||||
[build-dependencies]
|
||||
tauri-build = { version = "2.7.1", features = [] }
|
||||
tauri-build = { version = "2.7.2", features = [] }
|
||||
|
||||
[dependencies]
|
||||
tauri = { version = "2.12.2", features = ["tray-icon", "image-png", "test"] }
|
||||
tauri-plugin-opener = "2.7.0"
|
||||
tauri-plugin-dialog = "2.8.1"
|
||||
tauri-plugin-shell = "2.4.1"
|
||||
tauri = { version = "2.12.3", features = ["tray-icon", "image-png", "test"] }
|
||||
tauri-plugin-opener = "2.7.1"
|
||||
tauri-plugin-dialog = "2.8.2"
|
||||
tauri-plugin-shell = "2.5.0"
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
serde_json = "1"
|
||||
tokio = { version = "1", features = ["fs", "process", "io-util", "rt", "rt-multi-thread", "macros", "sync", "time"] }
|
||||
@@ -35,15 +35,15 @@ reqwest = { version = "0.13.5", default-features = false, features = ["rustls-no
|
||||
rustls = { version = "0.23.45", default-features = false, features = ["ring", "std", "tls12"] }
|
||||
uuid = { version = "1", features = ["v4"] }
|
||||
ts-rs = { version = "12", features = ["serde-compat", "uuid-impl"] }
|
||||
tauri-plugin-notification = "2.5.1"
|
||||
tauri-plugin-clipboard-manager = "2.4.1"
|
||||
tauri-plugin-notification = "2.6.0"
|
||||
tauri-plugin-clipboard-manager = "2.4.2"
|
||||
sysinfo = "0.39.6"
|
||||
hmac = "0.13"
|
||||
sha2 = "0.11"
|
||||
sha1 = "0.11"
|
||||
base64 = { version = "0.23.1", default-features = false, features = ["std"] }
|
||||
tauri-plugin-deep-link = "2.6.1"
|
||||
tauri-plugin-single-instance = { version = "2.5.2", features = ["deep-link"] }
|
||||
tauri-plugin-single-instance = { version = "2.5.3", features = ["deep-link"] }
|
||||
tempfile = "3"
|
||||
thiserror = "2.0.21"
|
||||
axum = "0.8.9"
|
||||
@@ -57,10 +57,10 @@ chrono = "0.4.45"
|
||||
url = "2"
|
||||
rusqlite = { version = "0.40.2", features = ["bundled"] }
|
||||
log = "0.4.34"
|
||||
tauri-plugin-log = "2.10.0"
|
||||
tauri-plugin-log = "2.10.1"
|
||||
trash = "5"
|
||||
async-trait = "0.1"
|
||||
keyring-core = "1.0.0"
|
||||
keyring-core = "1.0.1"
|
||||
auto-launch = "0.6.0"
|
||||
[target.'cfg(target_os = "macos")'.dependencies]
|
||||
apple-native-keyring-store = { version = "1.0.2", features = ["keychain"] }
|
||||
|
||||
+228
-32
@@ -2758,17 +2758,27 @@ struct ShellCommandOutput {
|
||||
stderr: Vec<u8>,
|
||||
}
|
||||
|
||||
fn terminate_shell_process_tree(child: tauri_plugin_shell::process::CommandChild) {
|
||||
crate::process::kill_process_tree(child.pid());
|
||||
// The process-tree helper may already have signalled the root. The
|
||||
// direct kill is still needed when the root was not visible in the
|
||||
// snapshot, but its result is cleanup telemetry, not the operation's
|
||||
// primary error.
|
||||
fn terminate_shell_process_group(child: tauri_plugin_shell::process::CommandChild) {
|
||||
// Firelink enables process-group ownership on every yt-dlp command. This
|
||||
// reaches PyInstaller workers even after the wrapper exits, unlike a PID
|
||||
// tree snapshot taken during cancellation.
|
||||
if let Err(error) = child.kill() {
|
||||
log::warn!("failed to signal cancelled media process: {error}");
|
||||
}
|
||||
}
|
||||
|
||||
fn ytdlp_shell_command<R: tauri::Runtime>(
|
||||
app_handle: &tauri::AppHandle<R>,
|
||||
executable: impl AsRef<std::ffi::OsStr>,
|
||||
) -> tauri_plugin_shell::process::Command {
|
||||
use tauri_plugin_shell::ShellExt;
|
||||
|
||||
app_handle
|
||||
.shell()
|
||||
.command(executable)
|
||||
.set_process_group(true)
|
||||
}
|
||||
|
||||
async fn receive_media_termination_after_cancel(
|
||||
rx: &mut tauri::async_runtime::Receiver<tauri_plugin_shell::process::CommandEvent>,
|
||||
stdout_buffer: &mut String,
|
||||
@@ -2944,15 +2954,15 @@ async fn shell_command_output_with_timeout(
|
||||
match tokio::time::timeout(timeout, collect_output).await {
|
||||
Ok(Ok(output)) if output.status_code.is_some() => Ok(output),
|
||||
Ok(Ok(_)) => {
|
||||
terminate_shell_process_tree(child);
|
||||
terminate_shell_process_group(child);
|
||||
Err(format!("{operation} ended without a process exit status"))
|
||||
}
|
||||
Ok(Err(error)) => {
|
||||
terminate_shell_process_tree(child);
|
||||
terminate_shell_process_group(child);
|
||||
Err(error)
|
||||
}
|
||||
Err(_) => {
|
||||
terminate_shell_process_tree(child);
|
||||
terminate_shell_process_group(child);
|
||||
Err(format!(
|
||||
"{operation} timed out after {}s",
|
||||
timeout.as_secs()
|
||||
@@ -3509,11 +3519,11 @@ async fn fetch_media_playlist_metadata_uncached(
|
||||
let deno_runtime = format!("deno:{}", deno_path.to_string_lossy());
|
||||
let trusted_path = crate::platform::trusted_system_path()?;
|
||||
|
||||
use tauri_plugin_shell::ShellExt;
|
||||
let (ytdlp_path, _) = resolve_metadata_ytdlp_path(&app_handle)?;
|
||||
let mut cmd = app_handle
|
||||
.shell()
|
||||
.command(ytdlp_path.to_string_lossy().to_string());
|
||||
let mut cmd = ytdlp_shell_command(
|
||||
&app_handle,
|
||||
ytdlp_path.to_string_lossy().to_string(),
|
||||
);
|
||||
cmd = cmd
|
||||
.env("PATH", trusted_path)
|
||||
.arg("--ffmpeg-location")
|
||||
@@ -3624,11 +3634,11 @@ async fn fetch_media_metadata_uncached(
|
||||
let deno_runtime = format!("deno:{}", deno_path.to_string_lossy());
|
||||
let trusted_path = crate::platform::trusted_system_path()?;
|
||||
|
||||
use tauri_plugin_shell::ShellExt;
|
||||
let (ytdlp_path, _) = resolve_metadata_ytdlp_path(&app_handle)?;
|
||||
let mut cmd = app_handle
|
||||
.shell()
|
||||
.command(ytdlp_path.to_string_lossy().to_string());
|
||||
let mut cmd = ytdlp_shell_command(
|
||||
&app_handle,
|
||||
ytdlp_path.to_string_lossy().to_string(),
|
||||
);
|
||||
cmd = cmd
|
||||
.env("PATH", trusted_path)
|
||||
.arg("--ffmpeg-location")
|
||||
@@ -4971,7 +4981,6 @@ mod login_start;
|
||||
pub use platform::atomic_write_replace;
|
||||
mod properties_window;
|
||||
pub mod queue;
|
||||
pub mod process;
|
||||
pub mod retry;
|
||||
mod torrent_probe;
|
||||
pub mod torrent;
|
||||
@@ -6528,8 +6537,6 @@ pub(crate) async fn start_media_download_internal(
|
||||
1.0
|
||||
};
|
||||
|
||||
use tauri_plugin_shell::ShellExt;
|
||||
|
||||
let mut config_file = tempfile::Builder::new()
|
||||
.prefix("ytdlp-")
|
||||
.suffix(".conf")
|
||||
@@ -6602,7 +6609,7 @@ pub(crate) async fn start_media_download_internal(
|
||||
}
|
||||
let mut processing_started = false;
|
||||
let ytdlp_path = resolve_bundled_binary_path(&app_handle, "yt-dlp")?;
|
||||
let mut cmd = app_handle.shell().command(&ytdlp_path);
|
||||
let mut cmd = ytdlp_shell_command(&app_handle, &ytdlp_path);
|
||||
for arg in media_progress_args() {
|
||||
cmd = cmd.arg(arg);
|
||||
}
|
||||
@@ -6717,7 +6724,7 @@ pub(crate) async fn start_media_download_internal(
|
||||
Duration::from_secs(1),
|
||||
|| {
|
||||
if let Some(child) = child.take() {
|
||||
terminate_shell_process_tree(child);
|
||||
terminate_shell_process_group(child);
|
||||
}
|
||||
},
|
||||
)
|
||||
@@ -7003,6 +7010,9 @@ async fn pause_download(
|
||||
properties_window::ensure_main_window(&caller)?;
|
||||
log::info!("pause_download called for id: {}", id);
|
||||
|
||||
let Some(_aria2_handoff_guard) = state.queue_manager.try_begin_aria2_handoff() else {
|
||||
return Err("Firelink is preparing to exit".to_string());
|
||||
};
|
||||
let mut control_guard = state.queue_manager.acquire_aria2_control(&id).await;
|
||||
removal_jobs::ensure_not_removing(&app_handle, &id)?;
|
||||
let active_kind = state.queue_manager.active_kind(&id).await;
|
||||
@@ -7330,6 +7340,9 @@ async fn resume_download(
|
||||
if queue_id.is_empty() {
|
||||
return Err("Queue id cannot be empty".to_string());
|
||||
}
|
||||
let Some(aria2_handoff_guard) = state.queue_manager.try_begin_aria2_handoff() else {
|
||||
return Err("Firelink is preparing to exit".to_string());
|
||||
};
|
||||
let control_guard = state.queue_manager.acquire_aria2_control(&id).await;
|
||||
removal_jobs::ensure_not_removing(&app_handle, &id)?;
|
||||
let Some(gid) = state.queue_manager.aria2_gid_for_download(&id) else {
|
||||
@@ -7423,8 +7436,10 @@ async fn resume_download(
|
||||
let gid_clone = gid.clone();
|
||||
|
||||
let app_handle_clone = app_handle.clone();
|
||||
let handoff_guard = aria2_handoff_guard;
|
||||
drop(control_guard);
|
||||
tauri::async_runtime::spawn(async move {
|
||||
let _handoff_guard = handoff_guard;
|
||||
let had_permit = queue_manager.has_active_permit(&id_clone).await;
|
||||
let permit_candidate = if had_permit {
|
||||
None
|
||||
@@ -7777,7 +7792,9 @@ async fn resume_download(
|
||||
let queue_id_clone = queue_id.clone();
|
||||
let app_handle_clone = app_handle.clone();
|
||||
let status_for_log = status.to_string();
|
||||
let handoff_guard = aria2_handoff_guard;
|
||||
tauri::async_runtime::spawn(async move {
|
||||
let _handoff_guard = handoff_guard;
|
||||
let had_permit = queue_manager.has_active_permit(&id_clone).await;
|
||||
let permit_candidate = if had_permit {
|
||||
None
|
||||
@@ -7909,6 +7926,9 @@ async fn remove_download(
|
||||
) -> Result<(), String> {
|
||||
properties_window::ensure_main_window(&caller)?;
|
||||
removal_jobs::ensure_not_removing(&app_handle, &id)?;
|
||||
let Some(_handoff_guard) = state.queue_manager.try_begin_aria2_handoff() else {
|
||||
return Err("Firelink is preparing to exit".to_string());
|
||||
};
|
||||
remove_download_inner(app_handle, state, id, delete_assets, preserve_resumable,
|
||||
asset_removal_policy, expected_lifecycle_generation, false).await
|
||||
}
|
||||
@@ -9857,6 +9877,9 @@ async fn detach_download_for_reconfigure(
|
||||
) -> Result<(), String> {
|
||||
properties_window::ensure_main_window(&caller)?;
|
||||
log::info!("detach_download_for_reconfigure called for id: {}", id);
|
||||
let Some(_handoff_guard) = state.queue_manager.try_begin_aria2_handoff() else {
|
||||
return Err("Firelink is preparing to exit".to_string());
|
||||
};
|
||||
let mut control_guard = state.queue_manager.acquire_aria2_control(&id).await;
|
||||
removal_jobs::ensure_not_removing(&app_handle, &id)?;
|
||||
detach_download_for_reconfigure_locked(
|
||||
@@ -12878,6 +12901,9 @@ async fn set_torrent_file_selection(
|
||||
selected_indices: Option<Vec<u32>>,
|
||||
) -> Result<crate::ipc::TorrentFileSelectionSnapshot, String> {
|
||||
properties_window::ensure_properties_or_main(&caller, &properties, &id)?;
|
||||
let Some(_handoff_guard) = state.queue_manager.try_begin_aria2_handoff() else {
|
||||
return Err("Firelink is preparing to exit".to_string());
|
||||
};
|
||||
let mut control_guard = state.queue_manager.acquire_aria2_control(&id).await;
|
||||
removal_jobs::ensure_not_removing(&app_handle, &id)?;
|
||||
let item = load_persisted_torrent_item(database.inner(), &id)?;
|
||||
@@ -14235,6 +14261,9 @@ async fn verify_torrent_data(
|
||||
id: String,
|
||||
) -> Result<(), String> {
|
||||
properties_window::ensure_properties_or_main(&caller, &properties, &id)?;
|
||||
let Some(_handoff_guard) = state.queue_manager.try_begin_aria2_handoff() else {
|
||||
return Err("Firelink is preparing to exit".to_string());
|
||||
};
|
||||
// Verification replaces the current Aria2 lifecycle. Serialize that
|
||||
// replacement with pause/resume/remove so a paused GID cannot reject the
|
||||
// maintenance enqueue as a duplicate task or race it with a late event.
|
||||
@@ -14592,6 +14621,9 @@ async fn set_torrent_web_seeds(
|
||||
seeds: Vec<crate::ipc::TorrentWebSeed>,
|
||||
) -> Result<Vec<crate::ipc::TorrentWebSeed>, String> {
|
||||
properties_window::ensure_main_window(&caller)?;
|
||||
let Some(_handoff_guard) = state.queue_manager.try_begin_aria2_handoff() else {
|
||||
return Err("Firelink is preparing to exit".to_string());
|
||||
};
|
||||
let control_guard = state.queue_manager.acquire_aria2_control(&id).await;
|
||||
removal_jobs::ensure_not_removing(&state.queue_manager.app_handle(), &id)?;
|
||||
let active = state.queue_manager.is_registered(&id).await
|
||||
@@ -14863,6 +14895,9 @@ async fn set_torrent_max_open_files(
|
||||
properties_window::ensure_main_window(&caller)?;
|
||||
let max_open_files = queue::normalize_torrent_max_open_files(max_open_files)?;
|
||||
let guard = caller.app_handle().state::<Aria2DaemonGuard>();
|
||||
let Some(_handoff_guard) = state.queue_manager.try_begin_aria2_handoff() else {
|
||||
return Err("Firelink is preparing to exit".to_string());
|
||||
};
|
||||
let _restart_guard = guard.restart_lock.lock().await;
|
||||
rpc_call(
|
||||
state.aria2_port.load(std::sync::atomic::Ordering::Relaxed),
|
||||
@@ -14887,6 +14922,9 @@ async fn set_torrent_overall_upload_limit(
|
||||
let normalized_limit = normalize_torrent_overall_upload_limit(limit.as_deref())?;
|
||||
let limit_str = normalized_limit.as_deref().unwrap_or("0");
|
||||
let guard = caller.app_handle().state::<Aria2DaemonGuard>();
|
||||
let Some(_handoff_guard) = state.queue_manager.try_begin_aria2_handoff() else {
|
||||
return Err("Firelink is preparing to exit".to_string());
|
||||
};
|
||||
let _restart_guard = guard.restart_lock.lock().await;
|
||||
rpc_call(
|
||||
state.aria2_port.load(std::sync::atomic::Ordering::Relaxed),
|
||||
@@ -14917,6 +14955,9 @@ async fn set_global_speed_limit(
|
||||
};
|
||||
let limit_str = normalized_limit.clone().unwrap_or_else(|| "0".to_string());
|
||||
let guard = caller.app_handle().state::<Aria2DaemonGuard>();
|
||||
let Some(_handoff_guard) = state.queue_manager.try_begin_aria2_handoff() else {
|
||||
return Err("Firelink is preparing to exit".to_string());
|
||||
};
|
||||
let _restart_guard = guard.restart_lock.lock().await;
|
||||
rpc_call(
|
||||
state.aria2_port.load(std::sync::atomic::Ordering::Relaxed),
|
||||
@@ -16830,6 +16871,152 @@ mod tests {
|
||||
std::process::exit(0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn shell_process_group_descendant_fixture() {
|
||||
match std::env::var("FIRELINK_SHELL_PROCESS_GROUP_FIXTURE").as_deref() {
|
||||
Ok("launcher") => {
|
||||
let executable = std::env::current_exe().expect("test executable path");
|
||||
let descendant = std::process::Command::new(executable)
|
||||
.args([
|
||||
"--exact",
|
||||
"tests::shell_process_group_descendant_fixture",
|
||||
"--nocapture",
|
||||
])
|
||||
.env("FIRELINK_SHELL_PROCESS_GROUP_FIXTURE", "descendant")
|
||||
.spawn()
|
||||
.expect("spawn long-running descendant");
|
||||
|
||||
use std::io::Write;
|
||||
let mut stdout = std::io::stdout().lock();
|
||||
writeln!(stdout, "FIRELINK_DESCENDANT_PID={}", descendant.id())
|
||||
.expect("write descendant PID");
|
||||
stdout.flush().expect("flush descendant PID");
|
||||
}
|
||||
Ok("descendant") => loop {
|
||||
std::thread::park_timeout(std::time::Duration::from_secs(60));
|
||||
},
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
fn process_is_running(pid: u32) -> bool {
|
||||
let mut system = sysinfo::System::new();
|
||||
system.refresh_processes(sysinfo::ProcessesToUpdate::All, true);
|
||||
system.process(sysinfo::Pid::from_u32(pid)).is_some()
|
||||
}
|
||||
|
||||
async fn wait_for_process_exit(pid: u32, timeout: std::time::Duration) -> bool {
|
||||
let deadline = std::time::Instant::now() + timeout;
|
||||
loop {
|
||||
if !process_is_running(pid) {
|
||||
return true;
|
||||
}
|
||||
if std::time::Instant::now() >= deadline {
|
||||
return false;
|
||||
}
|
||||
tokio::time::sleep(std::time::Duration::from_millis(10)).await;
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn shell_process_group_kills_descendant_after_launcher_exit() {
|
||||
let app = tauri::test::mock_builder()
|
||||
.plugin(tauri_plugin_shell::init())
|
||||
.build(tauri::test::mock_context(tauri::test::noop_assets()))
|
||||
.expect("mock app");
|
||||
let executable = std::env::current_exe().expect("test executable path");
|
||||
let (mut events, child) = super::ytdlp_shell_command(app.handle(), executable)
|
||||
.args([
|
||||
"--exact",
|
||||
"tests::shell_process_group_descendant_fixture",
|
||||
"--nocapture",
|
||||
])
|
||||
.env("FIRELINK_SHELL_PROCESS_GROUP_FIXTURE", "launcher")
|
||||
.spawn()
|
||||
.expect("spawn launcher with process-group ownership");
|
||||
let launcher_pid = child.pid();
|
||||
let mut output = String::new();
|
||||
let descendant_pid = tokio::time::timeout(std::time::Duration::from_secs(10), async {
|
||||
loop {
|
||||
match events.recv().await {
|
||||
Some(tauri_plugin_shell::process::CommandEvent::Stdout(bytes)) => {
|
||||
output.push_str(&String::from_utf8_lossy(&bytes));
|
||||
if let Some(pid) = output
|
||||
.split("FIRELINK_DESCENDANT_PID=")
|
||||
.nth(1)
|
||||
.and_then(|value| value.split_whitespace().next())
|
||||
.and_then(|value| value.parse::<u32>().ok())
|
||||
{
|
||||
return Ok(pid);
|
||||
}
|
||||
}
|
||||
Some(tauri_plugin_shell::process::CommandEvent::Terminated(_)) => {
|
||||
return Err("launcher exited before reporting its descendant".to_string());
|
||||
}
|
||||
Some(tauri_plugin_shell::process::CommandEvent::Error(error)) => {
|
||||
return Err(format!("launcher process error: {error}"));
|
||||
}
|
||||
None => {
|
||||
return Err(
|
||||
"launcher event stream closed before reporting its descendant"
|
||||
.to_string(),
|
||||
);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
})
|
||||
.await;
|
||||
let descendant_pid = match descendant_pid {
|
||||
Ok(Ok(pid)) => pid,
|
||||
Ok(Err(error)) => {
|
||||
super::terminate_shell_process_group(child);
|
||||
panic!("{error}");
|
||||
}
|
||||
Err(_) => {
|
||||
super::terminate_shell_process_group(child);
|
||||
panic!("launcher did not report its descendant within 10 seconds");
|
||||
}
|
||||
};
|
||||
|
||||
if !wait_for_process_exit(launcher_pid, std::time::Duration::from_secs(10)).await {
|
||||
super::terminate_shell_process_group(child);
|
||||
panic!("launcher did not exit after spawning its descendant");
|
||||
}
|
||||
if !process_is_running(descendant_pid) {
|
||||
super::terminate_shell_process_group(child);
|
||||
panic!("descendant exited before its process group was terminated");
|
||||
}
|
||||
|
||||
super::terminate_shell_process_group(child);
|
||||
let terminated = tokio::time::timeout(std::time::Duration::from_secs(10), async {
|
||||
loop {
|
||||
match events.recv().await {
|
||||
Some(tauri_plugin_shell::process::CommandEvent::Terminated(payload)) => {
|
||||
return Ok(payload);
|
||||
}
|
||||
Some(tauri_plugin_shell::process::CommandEvent::Error(error)) => {
|
||||
return Err(format!("process-group termination error: {error}"));
|
||||
}
|
||||
None => {
|
||||
return Err(
|
||||
"process-group event stream closed before termination".to_string()
|
||||
);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
})
|
||||
.await
|
||||
.expect("process group should terminate within 10 seconds")
|
||||
.expect("process group should report termination");
|
||||
assert_eq!(terminated.code, Some(0), "launcher exit status");
|
||||
assert!(
|
||||
wait_for_process_exit(descendant_pid, std::time::Duration::from_secs(10)).await,
|
||||
"descendant should exit when its process group is killed"
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn shell_fast_exit_preserves_final_output_and_reported_path() {
|
||||
use tauri_plugin_shell::ShellExt;
|
||||
@@ -16845,7 +17032,7 @@ mod tests {
|
||||
|
||||
// Repeated fast exits exercise the reader/termination race without
|
||||
// timing sleeps. The media consumer intentionally stops at Terminated,
|
||||
// so this asserts the ordering guarantee supplied by shell 2.4.1.
|
||||
// so this asserts the ordering guarantee supplied by shell 2.5.0.
|
||||
for _ in 0..8 {
|
||||
let (mut events, _child) = app
|
||||
.shell()
|
||||
@@ -16859,6 +17046,7 @@ mod tests {
|
||||
"FIRELINK_SHELL_TEST_OUTPUT_PATH",
|
||||
output_path.as_os_str(),
|
||||
)
|
||||
.set_process_group(true)
|
||||
.spawn()
|
||||
.expect("spawn fast-exiting shell child");
|
||||
|
||||
@@ -24755,6 +24943,10 @@ pub fn run() {
|
||||
if !guard.exit_allowed() {
|
||||
api.prevent_exit();
|
||||
if guard.begin_shutdown() {
|
||||
app_handle
|
||||
.state::<AppState>()
|
||||
.queue_manager
|
||||
.close_aria2_handoffs_for_shutdown();
|
||||
let app = app_handle.clone();
|
||||
tauri::async_runtime::spawn(async move {
|
||||
let exit_started = Instant::now();
|
||||
@@ -24841,25 +25033,26 @@ pub fn run() {
|
||||
);
|
||||
return;
|
||||
}
|
||||
// Stop any addUri handoff admitted just before the
|
||||
// shutdown fence. If one cannot settle promptly,
|
||||
// reject this exit while the existing daemon is
|
||||
// still available to finish the transfer.
|
||||
let dispatch_drain = tokio::time::timeout(
|
||||
// Drain Aria2 mutations admitted before the
|
||||
// shutdown fence. This includes dispatch, resume,
|
||||
// retry, seed, recovery, and user control work. If
|
||||
// any cannot settle promptly, reject this exit
|
||||
// while the existing daemon is still available.
|
||||
let handoff_drain = tokio::time::timeout(
|
||||
Duration::from_secs(15),
|
||||
app.state::<AppState>()
|
||||
.queue_manager
|
||||
.wait_for_dispatch_handoffs(),
|
||||
.wait_for_aria2_handoffs(),
|
||||
)
|
||||
.await;
|
||||
if !matches!(dispatch_drain, Ok(true)) {
|
||||
if !matches!(handoff_drain, Ok(true)) {
|
||||
log::warn!(
|
||||
"app_shutdown_phase=aria2_dispatch_drain outcome=timeout-or-incomplete"
|
||||
"app_shutdown_phase=aria2_handoff_drain outcome=timeout-or-incomplete"
|
||||
);
|
||||
reject_app_exit_request(
|
||||
&app,
|
||||
request,
|
||||
"aria2-dispatch-drain-failed",
|
||||
"aria2-handoff-drain-failed",
|
||||
&[],
|
||||
)
|
||||
.await;
|
||||
@@ -25010,6 +25203,9 @@ pub fn run() {
|
||||
app.state::<Aria2DaemonGuard>().clear_shutdown_journal_failure();
|
||||
}
|
||||
app.state::<Aria2DaemonGuard>().allow_exit();
|
||||
app.state::<AppState>()
|
||||
.queue_manager
|
||||
.notify_aria2_exit_allowed();
|
||||
let _ = extension_server_shutdown.send(true);
|
||||
log::info!(
|
||||
"app_shutdown_phase=total elapsed_ms={}",
|
||||
|
||||
@@ -217,18 +217,12 @@ fn launch_path(path: String) -> String {
|
||||
}
|
||||
|
||||
#[cfg(any(target_os = "linux", test))]
|
||||
fn encode_linux_exec_path(path: &str) -> Result<String, String> {
|
||||
fn encode_linux_exec_argument(path: &str) -> Result<String, String> {
|
||||
if path.chars().any(char::is_control) {
|
||||
return Err(
|
||||
"Linux executable path contains an unsupported control character".to_string(),
|
||||
);
|
||||
}
|
||||
if path.contains('=') {
|
||||
return Err(
|
||||
"Linux executable path contains '=' which Desktop Entry Exec does not support"
|
||||
.to_string(),
|
||||
);
|
||||
}
|
||||
|
||||
// First encode the argument according to the Exec quoting rules. Percent
|
||||
// signs are doubled so the launcher does not treat them as field codes.
|
||||
@@ -262,6 +256,18 @@ fn encode_linux_exec_path(path: &str) -> Result<String, String> {
|
||||
Ok(encoded)
|
||||
}
|
||||
|
||||
#[cfg(any(target_os = "linux", test))]
|
||||
fn encode_linux_exec_path(path: &str) -> Result<String, String> {
|
||||
// Desktop Entry Exec forbids '=' in its executable token. Keep that token
|
||||
// fixed at env, then pass the AppImage path as a quoted argument so valid
|
||||
// install paths such as "Work=Home" remain launchable. The absolute path
|
||||
// also prevents env from interpreting it as an environment assignment.
|
||||
Ok(format!(
|
||||
"\"/usr/bin/env\" -- {}",
|
||||
encode_linux_exec_argument(path)?
|
||||
))
|
||||
}
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
fn launch_path(path: String) -> Result<String, String> {
|
||||
encode_linux_exec_path(&path)
|
||||
@@ -546,7 +552,16 @@ mod tests {
|
||||
|
||||
assert_eq!(
|
||||
encode_linux_exec_path(path).unwrap(),
|
||||
r#""/opt/Firelink app/quoted\\"/back\\\\slash/\\$cost/\\`tick\\`/100%%""#
|
||||
r#""/usr/bin/env" -- "/opt/Firelink app/quoted\\"/back\\\\slash/\\$cost/\\`tick\\`/100%%""#
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn passes_equals_in_linux_appimage_path_as_a_launcher_argument() {
|
||||
assert_eq!(
|
||||
encode_linux_exec_path("/home/user/Apps/Work=Home/Firelink AppImage%2.AppImage")
|
||||
.unwrap(),
|
||||
r#""/usr/bin/env" -- "/home/user/Apps/Work=Home/Firelink AppImage%%2.AppImage""#
|
||||
);
|
||||
}
|
||||
|
||||
@@ -558,8 +573,4 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rejects_equals_in_linux_executable_path() {
|
||||
assert!(encode_linux_exec_path("/opt/Fire=link").is_err());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,24 +0,0 @@
|
||||
use sysinfo::System;
|
||||
|
||||
pub fn kill_process_tree(pid: u32) {
|
||||
let mut sys = System::new();
|
||||
sys.refresh_processes(sysinfo::ProcessesToUpdate::All, true);
|
||||
|
||||
let mut to_kill = vec![sysinfo::Pid::from_u32(pid)];
|
||||
let mut i = 0;
|
||||
while i < to_kill.len() {
|
||||
let current_pid = to_kill[i];
|
||||
for (p, process) in sys.processes() {
|
||||
if process.parent() == Some(current_pid) && !to_kill.contains(p) {
|
||||
to_kill.push(*p);
|
||||
}
|
||||
}
|
||||
i += 1;
|
||||
}
|
||||
|
||||
for p in to_kill.into_iter().rev() {
|
||||
if let Some(process) = sys.process(p) {
|
||||
process.kill();
|
||||
}
|
||||
}
|
||||
}
|
||||
+324
-43
@@ -1115,19 +1115,31 @@ struct PendingOrderEntry {
|
||||
ordinal: u64,
|
||||
}
|
||||
|
||||
struct DispatchHandoffGuard {
|
||||
pub(crate) struct Aria2HandoffGuard {
|
||||
count: Arc<AtomicUsize>,
|
||||
notify: Arc<Notify>,
|
||||
}
|
||||
|
||||
impl DispatchHandoffGuard {
|
||||
fn new(count: Arc<AtomicUsize>, notify: Arc<Notify>) -> Self {
|
||||
impl Aria2HandoffGuard {
|
||||
fn try_new(
|
||||
accepting: Arc<StdMutex<bool>>,
|
||||
count: Arc<AtomicUsize>,
|
||||
notify: Arc<Notify>,
|
||||
) -> Option<Self> {
|
||||
let is_accepting = accepting.lock().unwrap_or_else(|error| error.into_inner());
|
||||
if !*is_accepting {
|
||||
return None;
|
||||
}
|
||||
count.fetch_add(1, Ordering::AcqRel);
|
||||
Self { count, notify }
|
||||
drop(is_accepting);
|
||||
Some(Self {
|
||||
count,
|
||||
notify,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for DispatchHandoffGuard {
|
||||
impl Drop for Aria2HandoffGuard {
|
||||
fn drop(&mut self) {
|
||||
self.count.fetch_sub(1, Ordering::AcqRel);
|
||||
self.notify.notify_waiters();
|
||||
@@ -1342,10 +1354,12 @@ pub struct QueueManager<R: tauri::Runtime = tauri::Wry> {
|
||||
/// dispatch has not yet been accepted by the sidecar.
|
||||
pending_order_ordinals: Mutex<HashMap<(String, u64), PendingOrderEntry>>,
|
||||
next_pending_order_ordinal: Mutex<HashMap<String, u64>>,
|
||||
/// Selection-to-dispatch handoffs that shutdown must wait for before
|
||||
/// finalizing Aria2 exit.
|
||||
dispatch_handoff_count: Arc<AtomicUsize>,
|
||||
dispatch_handoff_notify: Arc<Notify>,
|
||||
/// Aria2 operations admitted before shutdown that must finish or cancel
|
||||
/// before the daemon can stop. The gate and count make close/admission a
|
||||
/// single transition, including resume workers waiting for a permit.
|
||||
aria2_handoff_accepting: Arc<StdMutex<bool>>,
|
||||
aria2_handoff_count: Arc<AtomicUsize>,
|
||||
aria2_handoff_notify: Arc<Notify>,
|
||||
/// Serializes queue-slot selection with global permit acquisition and
|
||||
/// ownership transitions.
|
||||
admission_gate: Mutex<()>,
|
||||
@@ -1484,8 +1498,9 @@ impl<R: tauri::Runtime> QueueManager<R> {
|
||||
queue_permit_ownership: Mutex::new(HashMap::new()),
|
||||
pending_order_ordinals: Mutex::new(HashMap::new()),
|
||||
next_pending_order_ordinal: Mutex::new(HashMap::new()),
|
||||
dispatch_handoff_count: Arc::new(AtomicUsize::new(0)),
|
||||
dispatch_handoff_notify: Arc::new(Notify::new()),
|
||||
aria2_handoff_accepting: Arc::new(StdMutex::new(true)),
|
||||
aria2_handoff_count: Arc::new(AtomicUsize::new(0)),
|
||||
aria2_handoff_notify: Arc::new(Notify::new()),
|
||||
admission_gate: Mutex::new(()),
|
||||
aria2_dispatch_hold: AtomicBool::new(false),
|
||||
system_action_pending: AtomicBool::new(false),
|
||||
@@ -1790,6 +1805,69 @@ impl<R: tauri::Runtime> QueueManager<R> {
|
||||
self.notify.notify_waiters();
|
||||
}
|
||||
|
||||
fn aria2_handoff_admission_closed(&self) -> bool {
|
||||
self.aria2_handoff_accepting
|
||||
.lock()
|
||||
.map(|accepting| !*accepting)
|
||||
.unwrap_or(true)
|
||||
}
|
||||
|
||||
fn aria2_shutdown_requested(&self) -> bool {
|
||||
self.aria2_handoff_admission_closed()
|
||||
|| self
|
||||
.app_handle
|
||||
.try_state::<crate::Aria2DaemonGuard>()
|
||||
.is_some_and(|guard| guard.is_shutting_down())
|
||||
}
|
||||
|
||||
pub(crate) fn try_begin_aria2_handoff(&self) -> Option<Aria2HandoffGuard> {
|
||||
if self
|
||||
.app_handle
|
||||
.try_state::<crate::Aria2DaemonGuard>()
|
||||
.is_some_and(|guard| guard.is_shutting_down())
|
||||
{
|
||||
return None;
|
||||
}
|
||||
Aria2HandoffGuard::try_new(
|
||||
Arc::clone(&self.aria2_handoff_accepting),
|
||||
Arc::clone(&self.aria2_handoff_count),
|
||||
Arc::clone(&self.aria2_handoff_notify),
|
||||
)
|
||||
}
|
||||
|
||||
pub(crate) fn close_aria2_handoffs_for_shutdown(&self) {
|
||||
*self
|
||||
.aria2_handoff_accepting
|
||||
.lock()
|
||||
.unwrap_or_else(|error| error.into_inner()) = false;
|
||||
self.aria2_handoff_notify.notify_waiters();
|
||||
self.notify.notify_waiters();
|
||||
}
|
||||
|
||||
pub(crate) async fn wait_for_aria2_handoff(&self) -> Option<Aria2HandoffGuard> {
|
||||
loop {
|
||||
let notified = self.notify.notified();
|
||||
tokio::pin!(notified);
|
||||
notified.as_mut().enable();
|
||||
if let Some(handoff) = self.try_begin_aria2_handoff() {
|
||||
return Some(handoff);
|
||||
}
|
||||
if self
|
||||
.app_handle
|
||||
.try_state::<crate::Aria2DaemonGuard>()
|
||||
.is_some_and(|guard| guard.exit_allowed())
|
||||
{
|
||||
return None;
|
||||
}
|
||||
notified.await;
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn notify_aria2_exit_allowed(&self) {
|
||||
self.aria2_handoff_notify.notify_waiters();
|
||||
self.notify.notify_waiters();
|
||||
}
|
||||
|
||||
async fn record_seed_started(&self, id: &str) {
|
||||
let remaining_minutes = self
|
||||
.aria2_payloads
|
||||
@@ -1924,22 +2002,24 @@ impl<R: tauri::Runtime> QueueManager<R> {
|
||||
}
|
||||
|
||||
async fn try_start_waiting_seed(self: &Arc<Self>) -> bool {
|
||||
if self.app_handle.try_state::<crate::Aria2DaemonGuard>()
|
||||
.is_some_and(|guard| guard.is_shutting_down())
|
||||
{
|
||||
let Some(handoff_guard) = self.try_begin_aria2_handoff() else {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
let Some(waiter) = self.reserve_seed_waiter() else {
|
||||
return false;
|
||||
};
|
||||
let manager = Arc::clone(self);
|
||||
tauri::async_runtime::spawn(async move {
|
||||
manager.resume_waiting_seed(waiter).await;
|
||||
manager.resume_waiting_seed(waiter, handoff_guard).await;
|
||||
});
|
||||
true
|
||||
}
|
||||
|
||||
async fn resume_waiting_seed(self: Arc<Self>, waiter: SeedWaiter) {
|
||||
async fn resume_waiting_seed(
|
||||
self: Arc<Self>,
|
||||
waiter: SeedWaiter,
|
||||
_handoff_guard: Aria2HandoffGuard,
|
||||
) {
|
||||
let id = waiter.id.clone();
|
||||
let Some(gid) = self.aria2_gid_for_download(&id) else {
|
||||
self.abandon_seed_start(&id);
|
||||
@@ -1965,6 +2045,23 @@ impl<R: tauri::Runtime> QueueManager<R> {
|
||||
)
|
||||
.await
|
||||
else {
|
||||
if self.aria2_shutdown_requested() {
|
||||
let _control_guard = self.acquire_aria2_control(&id).await;
|
||||
let is_removing = self.removal_requested(&id);
|
||||
let still_waiting = self
|
||||
.is_registered_generation_or_legacy(&id, waiter.lifecycle_generation)
|
||||
.await
|
||||
&& self.seed_waiting(&id)
|
||||
&& matches!(self.active_kind(&id).await, Some(TaskKind::Aria2))
|
||||
&& !is_removing;
|
||||
self.abandon_seed_start(&id);
|
||||
if still_waiting {
|
||||
self.add_seed_waiter(waiter);
|
||||
} else if is_removing {
|
||||
self.release_seed_tracking(&id);
|
||||
}
|
||||
return;
|
||||
}
|
||||
self.abandon_seed_start(&id);
|
||||
return;
|
||||
};
|
||||
@@ -2113,17 +2210,17 @@ impl<R: tauri::Runtime> QueueManager<R> {
|
||||
continue;
|
||||
}
|
||||
|
||||
let Some(_handoff_guard) = self.wait_for_aria2_handoff().await else {
|
||||
return;
|
||||
};
|
||||
|
||||
let terminal_work = match self.enter_shutdown_terminal_work().await {
|
||||
Ok(terminal_work) => terminal_work,
|
||||
Err(()) => return,
|
||||
};
|
||||
|
||||
let _control_guard = self.acquire_aria2_control(&waiter.id).await;
|
||||
if self
|
||||
.app_handle
|
||||
.try_state::<crate::Aria2DaemonGuard>()
|
||||
.is_some_and(|guard| guard.is_shutting_down())
|
||||
{
|
||||
if self.aria2_shutdown_requested() {
|
||||
drop(_control_guard);
|
||||
drop(terminal_work);
|
||||
continue;
|
||||
@@ -2770,11 +2867,12 @@ impl<R: tauri::Runtime> QueueManager<R> {
|
||||
|
||||
async fn next_queue_dispatch_hold_delay(&self) -> Option<Duration> {
|
||||
let now = Instant::now();
|
||||
let mut holds = self.queue_dispatch_holds.lock().await;
|
||||
holds.retain(|_, queue_holds| {
|
||||
queue_holds.retain(|_, expires_at| *expires_at > now);
|
||||
!queue_holds.is_empty()
|
||||
});
|
||||
// Do not prune expired leases here. `try_admit_next_task` already
|
||||
// prunes them while checking eligibility. A lease can expire between
|
||||
// that check and this deadline calculation; retaining its deadline
|
||||
// yields a zero-duration wake so the dispatcher retries admission
|
||||
// instead of waiting forever with no notification pending.
|
||||
let holds = self.queue_dispatch_holds.lock().await;
|
||||
holds
|
||||
.values()
|
||||
.flat_map(HashMap::values)
|
||||
@@ -3249,6 +3347,9 @@ impl<R: tauri::Runtime> QueueManager<R> {
|
||||
),
|
||||
String,
|
||||
> {
|
||||
let Some(_handoff_guard) = self.try_begin_aria2_handoff() else {
|
||||
return Err("Firelink is preparing to exit".to_string());
|
||||
};
|
||||
let control_guard = self.acquire_aria2_control(id).await;
|
||||
self.set_aria2_torrent_web_seeds_locked(id, seeds, &control_guard)
|
||||
.await
|
||||
@@ -3542,6 +3643,9 @@ impl<R: tauri::Runtime> QueueManager<R> {
|
||||
.ok_or_else(|| "invalid download speed limit".to_string())?,
|
||||
),
|
||||
};
|
||||
let Some(_handoff_guard) = self.try_begin_aria2_handoff() else {
|
||||
return Err("Firelink is preparing to exit".to_string());
|
||||
};
|
||||
let _control_guard = self.acquire_aria2_control(id).await;
|
||||
|
||||
if !self.is_registered(id).await
|
||||
@@ -3603,6 +3707,9 @@ impl<R: tauri::Runtime> QueueManager<R> {
|
||||
.ok_or_else(|| "invalid torrent upload limit".to_string())?,
|
||||
),
|
||||
};
|
||||
let Some(_handoff_guard) = self.try_begin_aria2_handoff() else {
|
||||
return Err("Firelink is preparing to exit".to_string());
|
||||
};
|
||||
let _control_guard = self.acquire_aria2_control(id).await;
|
||||
|
||||
if !self.is_registered(id).await
|
||||
@@ -3674,6 +3781,9 @@ impl<R: tauri::Runtime> QueueManager<R> {
|
||||
let rpc_peer_speed_limit = normalized_peer_speed_limit
|
||||
.as_deref()
|
||||
.unwrap_or(ARIA2_DEFAULT_TORRENT_PEER_SPEED_LIMIT);
|
||||
let Some(_handoff_guard) = self.try_begin_aria2_handoff() else {
|
||||
return Err("Firelink is preparing to exit".to_string());
|
||||
};
|
||||
let _control_guard = self.acquire_aria2_control(id).await;
|
||||
|
||||
if !self.is_registered(id).await
|
||||
@@ -4079,7 +4189,8 @@ impl<R: tauri::Runtime> QueueManager<R> {
|
||||
control_epoch: u64,
|
||||
) -> Option<OwnedSemaphorePermit> {
|
||||
loop {
|
||||
if !self
|
||||
if self.aria2_shutdown_requested()
|
||||
|| !self
|
||||
.is_registered_generation_or_legacy(id, lifecycle_generation)
|
||||
.await
|
||||
|| self.is_aria2_retry_cancelled(id).await
|
||||
@@ -4094,7 +4205,8 @@ impl<R: tauri::Runtime> QueueManager<R> {
|
||||
.try_reserve_queue_slot(id, queue_id, lifecycle_generation)
|
||||
.await
|
||||
{
|
||||
if self
|
||||
if !self.aria2_shutdown_requested()
|
||||
&& self
|
||||
.is_registered_generation_or_legacy(id, lifecycle_generation)
|
||||
.await
|
||||
&& !self.is_aria2_retry_cancelled(id).await
|
||||
@@ -4136,7 +4248,9 @@ impl<R: tauri::Runtime> QueueManager<R> {
|
||||
lifecycle_generation: u64,
|
||||
) -> Option<OwnedSemaphorePermit> {
|
||||
let _admission_gate = self.admission_gate.lock().await;
|
||||
if self.system_action_pending.load(Ordering::Acquire) {
|
||||
if self.aria2_shutdown_requested()
|
||||
|| self.system_action_pending.load(Ordering::Acquire)
|
||||
{
|
||||
return None;
|
||||
}
|
||||
let mut ownership = self.queue_permit_ownership.lock().await;
|
||||
@@ -4361,7 +4475,7 @@ impl<R: tauri::Runtime> QueueManager<R> {
|
||||
usize,
|
||||
Option<String>,
|
||||
u64,
|
||||
DispatchHandoffGuard,
|
||||
Aria2HandoffGuard,
|
||||
)> {
|
||||
let _admission_gate = self.admission_gate.lock().await;
|
||||
if self.app_handle.try_state::<crate::Aria2DaemonGuard>()
|
||||
@@ -4425,6 +4539,9 @@ impl<R: tauri::Runtime> QueueManager<R> {
|
||||
drop(queue_limits);
|
||||
drop(ownership);
|
||||
|
||||
let Some(handoff_guard) = self.try_begin_aria2_handoff() else {
|
||||
return None;
|
||||
};
|
||||
let permit = self.try_acquire_permit_after_retirement()?;
|
||||
let previous_cursor = cursor;
|
||||
let task = pending.remove(task_index)?;
|
||||
@@ -4438,10 +4555,6 @@ impl<R: tauri::Runtime> QueueManager<R> {
|
||||
);
|
||||
*self.dispatch_cursor.lock().await = Some(selected_queue);
|
||||
let pending_ordinal = self.ensure_pending_order_ordinal(&task).await;
|
||||
let handoff_guard = DispatchHandoffGuard::new(
|
||||
Arc::clone(&self.dispatch_handoff_count),
|
||||
Arc::clone(&self.dispatch_handoff_notify),
|
||||
);
|
||||
Some((
|
||||
permit,
|
||||
task,
|
||||
@@ -4504,6 +4617,23 @@ impl<R: tauri::Runtime> QueueManager<R> {
|
||||
permit: OwnedSemaphorePermit,
|
||||
) -> bool {
|
||||
let _admission_gate = self.admission_gate.lock().await;
|
||||
if self.aria2_shutdown_requested() {
|
||||
let remove_reservation = self
|
||||
.queue_permit_ownership
|
||||
.lock()
|
||||
.await
|
||||
.get(id)
|
||||
.is_some_and(|entry| {
|
||||
entry.queue_id == queue_id
|
||||
&& entry.lifecycle_generation == lifecycle_generation
|
||||
&& !entry.active
|
||||
});
|
||||
if remove_reservation {
|
||||
self.queue_permit_ownership.lock().await.remove(id);
|
||||
self.notify.notify_waiters();
|
||||
}
|
||||
return false;
|
||||
}
|
||||
let registered_generation = self
|
||||
.registered_lifecycle_generations
|
||||
.lock()
|
||||
@@ -5036,17 +5166,22 @@ impl<R: tauri::Runtime> QueueManager<R> {
|
||||
}
|
||||
|
||||
pub(crate) fn wake_after_exit_rejected(&self) {
|
||||
*self
|
||||
.aria2_handoff_accepting
|
||||
.lock()
|
||||
.unwrap_or_else(|error| error.into_inner()) = true;
|
||||
self.aria2_handoff_notify.notify_waiters();
|
||||
self.notify.notify_waiters();
|
||||
}
|
||||
|
||||
pub(crate) async fn wait_for_dispatch_handoffs(&self) -> bool {
|
||||
pub(crate) async fn wait_for_aria2_handoffs(&self) -> bool {
|
||||
loop {
|
||||
let notified = self.dispatch_handoff_notify.notified();
|
||||
let notified = self.aria2_handoff_notify.notified();
|
||||
tokio::pin!(notified);
|
||||
notified.as_mut().enable();
|
||||
|
||||
let admission_gate = self.admission_gate.lock().await;
|
||||
if self.dispatch_handoff_count.load(Ordering::Acquire) == 0 {
|
||||
if self.aria2_handoff_count.load(Ordering::Acquire) == 0 {
|
||||
return true;
|
||||
}
|
||||
drop(admission_gate);
|
||||
@@ -5061,7 +5196,7 @@ impl<R: tauri::Runtime> QueueManager<R> {
|
||||
pending_index: usize,
|
||||
previous_cursor: Option<String>,
|
||||
pending_ordinal: u64,
|
||||
_handoff_guard: DispatchHandoffGuard,
|
||||
_handoff_guard: Aria2HandoffGuard,
|
||||
) {
|
||||
let id = task.id.clone();
|
||||
let lifecycle_generation = task.lifecycle_generation;
|
||||
@@ -6029,6 +6164,9 @@ impl<R: tauri::Runtime> QueueManager<R> {
|
||||
}
|
||||
|
||||
async fn remove_aria2_magnet_parent_result(&self, gid: &str) {
|
||||
let Some(_handoff_guard) = self.wait_for_aria2_handoff().await else {
|
||||
return;
|
||||
};
|
||||
let Some(state) = self.app_handle.try_state::<crate::AppState>() else {
|
||||
return;
|
||||
};
|
||||
@@ -6602,8 +6740,12 @@ impl<R: tauri::Runtime> QueueManager<R> {
|
||||
gid: &str,
|
||||
observed_epoch: u64,
|
||||
) -> Result<(), String> {
|
||||
let Some(_handoff_guard) = self.try_begin_aria2_handoff() else {
|
||||
return Ok(());
|
||||
};
|
||||
let _control_guard = self.acquire_aria2_control(id).await;
|
||||
if self.aria2_gid_for_download(id).as_deref() != Some(gid)
|
||||
if self.aria2_shutdown_requested()
|
||||
|| self.aria2_gid_for_download(id).as_deref() != Some(gid)
|
||||
|| !self.is_registered(id).await
|
||||
|| !self.has_active_permit(id).await
|
||||
|| self.is_aria2_retry_cancelled(id).await
|
||||
@@ -7028,6 +7170,12 @@ impl<R: tauri::Runtime> QueueManager<R> {
|
||||
return;
|
||||
}
|
||||
|
||||
let Some(_handoff_guard) = this.wait_for_aria2_handoff().await else {
|
||||
this.finish_aria2_retry(&id_for_task, &retry_gid, retry_epoch)
|
||||
.await;
|
||||
return;
|
||||
};
|
||||
|
||||
// Serialize the payload snapshot and addUri with live speed
|
||||
// changes. Without this guard, a speed update could change the
|
||||
// old GID and payload just before this worker re-added a new GID
|
||||
@@ -10681,6 +10829,40 @@ mod tests {
|
||||
.contains("queue"));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn hold_expiring_after_dispatch_eligibility_check_wakes_immediately() {
|
||||
let app = tauri::test::mock_builder()
|
||||
.build(tauri::test::mock_context(tauri::test::noop_assets()))
|
||||
.unwrap();
|
||||
let manager = QueueManager::test_new(app.handle().clone(), 1, Arc::new(TestSpawner));
|
||||
|
||||
let token = manager.begin_queue_dispatch_hold("queue").await.unwrap();
|
||||
assert!(manager
|
||||
.active_queue_dispatch_holds()
|
||||
.await
|
||||
.contains("queue"));
|
||||
|
||||
// Model expiry after admission observed the live hold but before the
|
||||
// dispatcher computes how long it should wait for the next lease.
|
||||
manager
|
||||
.queue_dispatch_holds
|
||||
.lock()
|
||||
.await
|
||||
.get_mut("queue")
|
||||
.unwrap()
|
||||
.insert(token, Instant::now() - Duration::from_millis(1));
|
||||
|
||||
assert_eq!(
|
||||
manager.next_queue_dispatch_hold_delay().await,
|
||||
Some(Duration::ZERO),
|
||||
"an expired deadline must trigger another admission pass"
|
||||
);
|
||||
assert!(!manager
|
||||
.active_queue_dispatch_holds()
|
||||
.await
|
||||
.contains("queue"));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn explicit_allocation_telemetry_ends_without_payload_and_rejects_old_epoch() {
|
||||
let app = tauri::test::mock_builder()
|
||||
@@ -10885,6 +11067,7 @@ mod tests {
|
||||
.await
|
||||
.expect("task is admitted before shutdown starts");
|
||||
assert!(guard.begin_shutdown());
|
||||
manager.close_aria2_handoffs_for_shutdown();
|
||||
Arc::clone(&manager)
|
||||
.dispatch_one(
|
||||
permit,
|
||||
@@ -10957,13 +11140,14 @@ mod tests {
|
||||
.await
|
||||
.expect("dispatch should reach the gated addUri call");
|
||||
assert!(guard.begin_shutdown());
|
||||
manager.close_aria2_handoffs_for_shutdown();
|
||||
|
||||
let waiting_manager = Arc::clone(&manager);
|
||||
let shutdown_waiter = tokio::spawn(async move {
|
||||
waiting_manager.wait_for_dispatch_handoffs().await;
|
||||
waiting_manager.wait_for_aria2_handoffs().await;
|
||||
});
|
||||
tokio::task::yield_now().await;
|
||||
assert_eq!(manager.dispatch_handoff_count.load(Ordering::Acquire), 1);
|
||||
assert_eq!(manager.aria2_handoff_count.load(Ordering::Acquire), 1);
|
||||
assert!(!shutdown_waiter.is_finished(), "shutdown must wait for addUri");
|
||||
|
||||
spawner.release_add_uri.notify_one();
|
||||
@@ -10976,12 +11160,109 @@ mod tests {
|
||||
.expect("shutdown handoff wait should drain")
|
||||
.expect("shutdown waiter should not panic");
|
||||
|
||||
assert_eq!(manager.dispatch_handoff_count.load(Ordering::Acquire), 0);
|
||||
assert_eq!(manager.aria2_handoff_count.load(Ordering::Acquire), 0);
|
||||
assert_eq!(manager.pending_order(None).await, ["gated-shutdown"]);
|
||||
assert_eq!(manager.registered_lifecycle_generation("gated-shutdown").await, Some(13));
|
||||
assert_eq!(manager.available_permits(), 1);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn shutdown_closes_admission_and_wakes_resume_permit_waiters() {
|
||||
let app = tauri::test::mock_builder()
|
||||
.manage(crate::Aria2DaemonGuard::new())
|
||||
.build(tauri::test::mock_context(tauri::test::noop_assets()))
|
||||
.expect("mock app");
|
||||
let guard = app.handle().state::<crate::Aria2DaemonGuard>();
|
||||
let manager = Arc::new(QueueManager::test_new(
|
||||
app.handle().clone(),
|
||||
1,
|
||||
Arc::new(TestSpawner),
|
||||
));
|
||||
enqueue_test_task(&manager, "permit-owner", "occupied", TaskKind::Aria2, 1).await;
|
||||
let (permit, task, _, _, _, handoff_guard) = manager
|
||||
.try_admit_next_task()
|
||||
.await
|
||||
.expect("active transfer should take the only permit");
|
||||
assert!(manager
|
||||
.park_aria2_permit_if_missing_for_queue(
|
||||
&task.id,
|
||||
&task.queue_id,
|
||||
task.lifecycle_generation,
|
||||
permit,
|
||||
)
|
||||
.await);
|
||||
drop(handoff_guard);
|
||||
manager
|
||||
.reserve_enqueue_generation("resume-waiter", 2)
|
||||
.await
|
||||
.expect("resume lifecycle registration");
|
||||
|
||||
let resume_handoff = manager
|
||||
.try_begin_aria2_handoff()
|
||||
.expect("resume is accepted before shutdown");
|
||||
let (started_tx, started_rx) = tokio::sync::oneshot::channel();
|
||||
let resume_manager = Arc::clone(&manager);
|
||||
let resume_worker = tokio::spawn(async move {
|
||||
let _handoff = resume_handoff;
|
||||
let _ = started_tx.send(());
|
||||
let candidate = resume_manager
|
||||
.acquire_aria2_permit_candidate_for_queue(
|
||||
"resume-waiter",
|
||||
"resume-queue",
|
||||
2,
|
||||
0,
|
||||
)
|
||||
.await;
|
||||
if let Some(permit) = candidate {
|
||||
resume_manager
|
||||
.park_aria2_permit_if_missing_for_queue(
|
||||
"resume-waiter",
|
||||
"resume-queue",
|
||||
2,
|
||||
permit,
|
||||
)
|
||||
.await
|
||||
} else {
|
||||
false
|
||||
}
|
||||
});
|
||||
started_rx.await.expect("resume worker started");
|
||||
tokio::task::yield_now().await;
|
||||
|
||||
assert!(guard.begin_shutdown());
|
||||
manager.close_aria2_handoffs_for_shutdown();
|
||||
let waiting_manager = Arc::clone(&manager);
|
||||
let shutdown_drain = tokio::spawn(async move {
|
||||
waiting_manager.wait_for_aria2_handoffs().await
|
||||
});
|
||||
manager.release_permit("permit-owner").await;
|
||||
|
||||
let acquired = tokio::time::timeout(Duration::from_secs(1), resume_worker)
|
||||
.await
|
||||
.expect("shutdown notification should release the waiting resume")
|
||||
.expect("resume worker should not panic");
|
||||
assert!(!acquired, "a closing handoff gate must reject the permit");
|
||||
assert!(!manager.has_active_permit("resume-waiter").await);
|
||||
assert!(!manager
|
||||
.queue_permit_ownership
|
||||
.lock()
|
||||
.await
|
||||
.contains_key("resume-waiter"));
|
||||
assert!(tokio::time::timeout(Duration::from_secs(1), shutdown_drain)
|
||||
.await
|
||||
.expect("shutdown should drain the canceled resume")
|
||||
.expect("shutdown waiter should not panic"));
|
||||
assert_eq!(manager.aria2_handoff_count.load(Ordering::Acquire), 0);
|
||||
assert!(manager.try_begin_aria2_handoff().is_none());
|
||||
|
||||
assert!(guard.cancel_shutdown());
|
||||
manager.wake_after_exit_rejected();
|
||||
let reopened = manager
|
||||
.try_begin_aria2_handoff()
|
||||
.expect("rejected shutdown should reopen handoff admission");
|
||||
drop(reopened);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn unavailable_add_uri_requeues_selected_task_without_terminal_failure() {
|
||||
use tauri::Listener;
|
||||
|
||||
Reference in New Issue
Block a user