Skip to content

Multi-threaded downloads & parallel SFTP OBB transfers with live speeds - #209

Open
xAstroBoy wants to merge 6 commits into
nerdunit:masterfrom
xAstroBoy:feature/multithreaded-transfers
Open

Multi-threaded downloads & parallel SFTP OBB transfers with live speeds#209
xAstroBoy wants to merge 6 commits into
nerdunit:masterfrom
xAstroBoy:feature/multithreaded-transfers

Conversation

@xAstroBoy

Copy link
Copy Markdown

Overview

Overhauls how large game/OBB data moves in and out of the app: downloads and SFTP OBB uploads are concurrent, every transfer is shown live in an integrated strip, and the whole path is hardened so it can't softlock, silently drop files, or report a bogus failure. Tested end to end on a wireless Quest 3 (metadata sync, multi-part download, multi-GB OBB set over SFTP, install, reconnect across restarts).


Why SFTP instead of adb push?

For multi-GB OBBs, a direct SFTP session to an on-device sshd (rooted headsets) is several times faster than adb push, and the difference grows with size:

  • adb push is throttled by the adb sync protocol. It streams a single channel in small chunks, each acknowledged, multiplexed through the adb daemon on both ends. Over wireless adb especially, that overhead caps throughput far below the link's real capacity.
  • SFTP streams raw over TCP to sshd with large windows and far less per-chunk handshaking, so it saturates the Wi-Fi/USB link instead of the protocol.
  • SFTP parallelizes; adb's sync channel does not. A single adb server serializes sync operations, so you can't meaningfully push multiple files at once. SFTP lets us open several independent connections and run a bounded concurrent queue.

Measured here: ~62.8 MB/s for a 9.5 GB OBB over 3 SFTP connections; adb push over wireless is a fraction of that.

Trade-off / fallback: SFTP needs root + a reachable on-device sshd. Detection is automatic and everything degrades gracefully — no root, no server, or a total SFTP failure falls straight back to adb push.


Multi-threaded downloads

ChunkedDownloader fetches a game's split archive (.7z.001, .7z.002, …) over a local rclone serve http proxy. Concurrency was hardcoded to Min(4, parts) and gated behind SingleThreadMode, which defaulted on — so downloads were single-threaded out of the box.

  • Concurrency is now a DownloadThreads setting (default 6), clamped to the part count.
  • SingleThreadMode defaults to off; turning it on still forces a single stream.
  • Public-mirror note: its proxy runs --tpslimit 1.0 --tpslimit-burst 3, so rclone throttles opening new files; extra threads still overlap in-flight streams but won't scale linearly there. Private mirrors and SFTP have no such cap.

SFTP OBB transfers — concurrent, bounded, and self-healing

  • Bounded work-stealing queue. SftpThreads workers (default 3) each hold one connection and pull the next file from a shared queue — at most N files move at once regardless of OBB count (the FileZilla model). Gentle on the device sshd/FUSE storage.
  • Never hangs. Each upload uses a 60s OperationTimeout, so a wedged channel throws instead of blocking forever. Previously a single stuck file could freeze an entire multi-hundred-file OBB upload.
  • Auto-retry ×3 on a fresh reconnection with backoff.
  • Post-upload verification: after each file the remote path is stat'd for exact size; a file that isn't there or is truncated is a failure and is retried, never reported as done.
  • Failure isolation: one bad file no longer aborts the batch; it's left Failed with a per-file Retry (SFTP.RetryUpload) instead of re-pushing the whole set. Only a total failure falls back to adb push.
  • Perms: best-effort chmod -R 0777 + restorecon so the game can read files an sshd wrote as root.

Integrated transfer strip

Replaces the earlier popup with a themed, collapsible strip that overlays the bottom of the games list only while files move, then hides. One row per file (File · Size · live progress bar · Speed · Status). Successful rows auto-clear; failed rows stay with a right-click Retry / Remove / Clear finished menu. Fed by both SFTP and adb push.

Install verification

AdvancedSharpAdbClient throws "An unknown error occurred." whenever it can't parse pm install's result — which happens even when the APK installed fine. On an install error the app now verifies against the device (APK package + version code via aapt, matched against the installed version) and reports success when it really landed. A failed upgrade still fails.


Reliability fixes (reported as "freezes")

Symptom Root cause Fix
Window locks for tens of seconds after connect UpdateQuestInfoPanel ran adb shell getprop/df on the UI thread Moved off the UI thread
Hangs indefinitely mid-operation RunAdbCommandToString had no timeout; Android df can wedge on a bad mount Safety timeout on shell commands; transfers/backup/restore/install exempt; async reads also fix a latent ReadToEnd deadlock
Wireless device drops every launch Startup hard-killed every adb.exe, tearing down the running server + its adb connect session Reuse the running server (start-server) instead of killing it
Looks frozen but idle A modal dialog opened behind another window with ShowInTaskbar = false Dialogs are TopMost, shown in taskbar, brought to front

Build

build.cmd's vswhere now passes -products *, so it finds MSBuild in a VS BuildTools install, not just full editions.

New settings

Setting Default Purpose
DownloadThreads 6 Parallel download parts
SftpThreads 3 Concurrent SFTP OBB uploads
SingleThreadMode now false Master switch; forces single-stream everywhere when on

Missing settings fall back to these defaults, so existing settings.json files need no migration. No new NuGet dependencies.


Supersedes #208 — resubmitted after an account migration; branch and commits are identical.

SFTP fast transfers (rooted headsets running an SSH server):
- SFTP.cs: detects root (su -> uid=0), probes for an SSH server on the
  headset (ports 22/8022/2222), authenticates via key or password, and
  verifies the OBB dir is visible before use. Uploads OBBs over SFTP with
  the same progress/ETA UI as adb push, and transparently falls back to
  adb push when SFTP is unavailable or fails.
- Uses /storage/emulated/0/Android/obb (canonical) with /sdcard fallback.
- Titlebar shows Root and SFTP status after connect.
- SftpCredentialsForm: prompts for username + password and/or key file
  when a server is found but stored credentials do not authenticate.
- SFTP settings persisted in SettingsManager (host/port/user/pass/key).

ADB device detection:
- GetDevicesResilient() retries across an adb daemon restart so a
  version-mismatch server bounce (e.g. a different-version adb in PATH
  killing/restarting the server) no longer flashes "No Device Connected".

Measured on wireless (Quest 3, same link): SFTP ~50 MB/s vs adb push
~28 MB/s for a 200 MB payload, verified byte-identical (md5).
Downloads:
- ChunkedDownloader concurrency is now configurable via a new
  DownloadThreads setting (default 6) instead of a hardcoded cap of 4,
  and SingleThreadMode now defaults to off so multithreading is on by
  default. SingleThreadMode still forces a single stream when enabled.

SFTP OBB transfers:
- CopyObbCore now uploads multiple OBB files in parallel, one SFTP
  connection per worker (SSH.NET clients are not safe to share). Remote
  directories are pre-created once up front to avoid races, and files are
  distributed across workers greedily by size for balanced connections.

Transfer speed display:
- OBB copies now report live throughput (MB/s) in the status line for
  both the SFTP fast path and the adb push fallback, matching downloads.

Reliability:
- UpdateQuestInfoPanel ran its adb shell calls (getprop/df) on the UI
  thread; a slow or mid-restart adb daemon would freeze the whole window.
  Those calls now run off the UI thread.

Build:
- build.cmd's vswhere lookup adds -products * so it also finds MSBuild in
  a Visual Studio BuildTools install, not just full VS editions.
…s, non-hiding dialogs

ADB:
- RunAdbCommandToString now applies a safety timeout to shell commands so a
  wedged device command (e.g. Android's `df` blocking on an unresponsive mount)
  can no longer hang the app indefinitely. Large transfers and on-device
  backup/restore/install prompts are exempt so they are never force-killed.
  Timed reads use async stdout/stderr, which also removes a latent
  sequential-ReadToEnd deadlock. A timeoutMs override was added.

Startup:
- Stop hard-killing every adb.exe on launch. That tore down a running adb
  server and dropped any active wireless (adb connect) session, forcing a
  reconnect that often failed silently. start-server reuses a compatible
  running server, preserving the connected device.

SFTP OBB:
- After a transfer, best-effort chmod -R 0777 + restorecon on the OBB folder
  so the game can read files an sshd wrote as root (no-op on FUSE storage,
  needed where the sshd writes through to a real filesystem).

UI:
- Message boxes are now TopMost, shown in the taskbar, and brought to front on
  open, so a prompt can never hide behind another window/monitor and leave the
  disabled main window looking frozen while it waits for an answer.
- Add a FileZilla-style Transfer window (TransferWindow / TransferQueue): a
  non-modal popup that appears when a transfer starts and shows a row per file
  with a live progress bar, speed and status (Queued/Active/Done/Failed). It
  hides on close and re-shows on new activity, and has a "Clear finished"
  button. Both SFTP uploads and adb pushes feed into it.

- SFTP OBB upload is now a bounded work-stealing queue instead of firing every
  file at once: N workers (default 3, new SftpThreads setting) each hold one
  connection and pull the next file from a shared queue, so at most N files
  transfer at any moment regardless of OBB file count. Gentler on the on-device
  sshd/FUSE storage. SingleThreadMode still forces a single stream.
Transfer UI:
- Replace the popup transfer window with an integrated, collapsible strip that
  overlays the bottom of the games list only while files are moving, themed to
  match the list, and hides when idle. Successful rows auto-clear; failed rows
  stay with a right-click Retry / Remove / Clear finished menu.

Never hang (the softlock fix):
- Each SFTP upload now uses a 60s OperationTimeout, so a wedged channel throws
  instead of blocking UploadFile forever. A single stuck file could previously
  freeze an entire multi-hundred-file OBB upload; it can't anymore.

Auto-retry + verify:
- Each file is retried up to 3x on a fresh reconnection with backoff.
- After every upload the remote path is stat'd and checked for exact size; a
  file that isn't actually there (or is truncated) is treated as a failure and
  retried, never reported as done.

Failure isolation:
- One bad file no longer aborts the batch. Files that still fail after retries
  are left Failed in the strip with a per-file Retry action (SFTP.RetryUpload),
  instead of re-pushing the whole set. Only a total failure falls back to adb push.
AdvancedSharpAdbClient throws "An unknown error occurred." whenever it cannot
parse pm install's result, which frequently happens even when the install
actually succeeded (a connection blip right after install, unusual pm output).
Rookie treated that as a hard failure.

Now, on an install exception, VerifyInstalled reads the APK's package name and
version code with aapt, confirms the package is present on the device, and
requires the installed version code to match before reporting success. A real
failed upgrade (old version still present) still fails, so it is not a blind pass.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant