Add "Sideload Magisk Module" button (push .zip to a running instance)#36
Conversation
BlueStacks' in-app file picker hands Magisk/Kitsune a Windows-style URI its module installer rejects with "Invalid Uri", so modules can't be flashed from the picker. This adds a one-click path around it: - adb_handler.py: locate BlueStacks' bundled HD-Adb.exe, read the target instance's adb_port from bluestacks.conf, connect, and push the .zip to /sdcard/Download/ (falls back to the sole attached device if the port is unknown). Clear errors when nothing is reachable. - main.py: "Sideload Magisk Module (.zip)" button. Requires exactly one (running) instance selected; opens a file dialog; runs on the worker thread like every other op; shows the flash instructions in a dialog on success. The user then flashes it from Magisk's own storage picker, which reads guest storage fine. This is the only action that needs the instance running.
There was a problem hiding this comment.
Code Review
This pull request introduces a new "Sideload Magisk Module" feature, allowing users to push module .zip files directly into a running BlueStacks instance's Download folder via ADB to bypass "Invalid Uri" errors. This includes the addition of adb_handler.py for ADB operations and UI updates in main.py to support the sideload action. The code review feedback suggests two key improvements in adb_handler.py: catching Exception instead of OSError when reading bluestacks.conf to prevent crashes from UnicodeDecodeError, and dynamically parsing the adb devices output instead of using a hardcoded slice index to handle potential daemon startup messages robustly.
Important
The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.
| m = key.match(line.strip()) | ||
| if m: | ||
| return int(m.group(1)) | ||
| except OSError: |
There was a problem hiding this comment.
When reading bluestacks.conf, open(..., encoding="utf-8") or iterating over the file can raise a UnicodeDecodeError if the file contains invalid UTF-8 bytes (e.g., due to corruption or a different encoding used by certain BlueStacks versions). Since UnicodeDecodeError inherits from ValueError (not OSError), it will bypass the except OSError: block and cause the application to crash.
We should catch Exception to ensure the application handles any read or decoding failures gracefully.
| except OSError: | |
| except Exception: |
| for line in stdout.splitlines()[1:]: # skip "List of devices attached" | ||
| parts = line.split() | ||
| if len(parts) >= 2 and parts[1] == "device": | ||
| serials.append(parts[0]) |
There was a problem hiding this comment.
Relying on a hardcoded slice index [1:] to skip the header line is fragile. If ADB outputs any daemon startup messages, warnings, or other diagnostic lines before "List of devices attached", the slice index will be incorrect, potentially skipping actual devices or attempting to parse daemon messages as device serials.
Instead, we should dynamically find the "List of devices attached" header line and only parse the lines following it.
| for line in stdout.splitlines()[1:]: # skip "List of devices attached" | |
| parts = line.split() | |
| if len(parts) >= 2 and parts[1] == "device": | |
| serials.append(parts[0]) | |
| lines = stdout.splitlines() | |
| try: | |
| header_idx = next(i for i, line in enumerate(lines) if "List of devices attached" in line) | |
| device_lines = lines[header_idx + 1:] | |
| except StopIteration: | |
| device_lines = lines | |
| for line in device_lines: | |
| parts = line.split() | |
| if len(parts) >= 2 and parts[1] == "device": | |
| serials.append(parts[0]) |
The button now flashes the module over an ADB root shell (magisk --install-module) -- the same command we run by hand -- so it's a one-click install, not a push-then-flash-yourself. Restart the instance to activate. If the root shell / Magisk isn't reachable, it falls back to leaving the zip in the instance's Download folder with instructions. - adb_handler: extract _resolve_serial(); replace push_module() with install_module() (push to /data/local/tmp -> magisk --install-module -> cleanup, with the Download fallback on failure). - main.py: rename to "Install Magisk Module", call install_module, and report install + restart in the success dialog.
When magisk --install-module is rejected over adb (the guest hasn't granted root to the shell), the fallback message now tells the user to set Superuser access -> "Apps and ADB" and retry, in addition to the manual-flash path.
Problem
BlueStacks' in-app file picker hands Magisk/Kitsune a Windows-style URI that its module installer rejects with "Invalid Uri", so a module
.zipon the Windows side can't be flashed from the picker.Fix
A one-click "Sideload Magisk Module (.zip)" button that pushes the chosen module into a running instance's
/sdcard/Download/over BlueStacks' bundled ADB. The user then flashes it from Magisk/Kitsune's own storage picker (which reads guest storage fine): Modules → Install from storage → Download.adb_handler.py(new): findsHD-Adb.exein the install dir, reads the target instance'sadb_portfrombluestacks.conf, connects, and pushes to/sdcard/Download/. Falls back to the sole attached device if the port isn't recorded yet. Surfaces clear errors ("start the instance…", "multiple instances running…", push failures).main.py: the button requires exactly one selected (running) instance, opens a file dialog, runs on the existingQThreadworker like every other op, and shows the flash instructions in a dialog on success. Disabled while busy.This is the only action in the app that needs the instance running (its ADB port must be open); everything else operates on shut-down disks.
Testing
14 offline unit tests for
adb_handler(exe discovery, port parsing incl. no-substring-false-match,adb devicesstate parsing, push happy-path, missing file, no device reachable, push failure, and port-unknown → sole-device fallback) — all pass, using an injected runner so no real ADB/BlueStacks is needed. GUI builds headlessly; button is wired into the busy-state handling.py_compileclean; no new lint. The live push against a running instance is left for on-device verification.