flashpod 0.2.3__tar.gz → 0.2.5__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {flashpod-0.2.3 → flashpod-0.2.5}/PKG-INFO +1 -1
- {flashpod-0.2.3 → flashpod-0.2.5}/flashpod/__init__.py +1 -1
- {flashpod-0.2.3 → flashpod-0.2.5}/flashpod/cli.py +19 -10
- {flashpod-0.2.3 → flashpod-0.2.5}/flashpod/ipod_flash.py +15 -4
- {flashpod-0.2.3 → flashpod-0.2.5}/flashpod/platform/macos.py +6 -2
- {flashpod-0.2.3 → flashpod-0.2.5}/flashpod/platform/windows.py +75 -18
- {flashpod-0.2.3 → flashpod-0.2.5}/flashpod.egg-info/PKG-INFO +1 -1
- {flashpod-0.2.3 → flashpod-0.2.5}/pyproject.toml +1 -1
- {flashpod-0.2.3 → flashpod-0.2.5}/LICENSE +0 -0
- {flashpod-0.2.3 → flashpod-0.2.5}/README.md +0 -0
- {flashpod-0.2.3 → flashpod-0.2.5}/flashpod/__main__.py +0 -0
- {flashpod-0.2.3 → flashpod-0.2.5}/flashpod/contrib/99-flashpod-firewire-ipod.rules +0 -0
- {flashpod-0.2.3 → flashpod-0.2.5}/flashpod/fat32.py +0 -0
- {flashpod-0.2.3 → flashpod-0.2.5}/flashpod/fatfs.py +0 -0
- {flashpod-0.2.3 → flashpod-0.2.5}/flashpod/firmware/firmware.json +0 -0
- {flashpod-0.2.3 → flashpod-0.2.5}/flashpod/itunesdb.py +0 -0
- {flashpod-0.2.3 → flashpod-0.2.5}/flashpod/platform/__init__.py +0 -0
- {flashpod-0.2.3 → flashpod-0.2.5}/flashpod/platform/base.py +0 -0
- {flashpod-0.2.3 → flashpod-0.2.5}/flashpod/platform/linux.py +0 -0
- {flashpod-0.2.3 → flashpod-0.2.5}/flashpod/resources.py +0 -0
- {flashpod-0.2.3 → flashpod-0.2.5}/flashpod.egg-info/SOURCES.txt +0 -0
- {flashpod-0.2.3 → flashpod-0.2.5}/flashpod.egg-info/dependency_links.txt +0 -0
- {flashpod-0.2.3 → flashpod-0.2.5}/flashpod.egg-info/entry_points.txt +0 -0
- {flashpod-0.2.3 → flashpod-0.2.5}/flashpod.egg-info/requires.txt +0 -0
- {flashpod-0.2.3 → flashpod-0.2.5}/flashpod.egg-info/top_level.txt +0 -0
- {flashpod-0.2.3 → flashpod-0.2.5}/setup.cfg +0 -0
|
@@ -354,8 +354,18 @@ def _sudo_reexec(extra):
|
|
|
354
354
|
"""Re-exec this same flashpod under sudo with ``extra`` args appended
|
|
355
355
|
(prompting for the password on a terminal). REPLACES the process and never
|
|
356
356
|
returns on success; returns only if it can't elevate (non-tty / no sudo)."""
|
|
357
|
-
if
|
|
358
|
-
return
|
|
357
|
+
if not sys.stdin.isatty():
|
|
358
|
+
return
|
|
359
|
+
if os.name == "nt":
|
|
360
|
+
# Windows 11 24H2+ ships sudo.exe. In "Inline" mode it elevates
|
|
361
|
+
# within the same console, preserving interactive prompts — unlike
|
|
362
|
+
# ShellExecute "runas" which opens a detached window. The
|
|
363
|
+
# environment is inherited, so no env/PYTHONPATH dance needed.
|
|
364
|
+
cmd = ["sudo"] + _self_cmd() + extra
|
|
365
|
+
try:
|
|
366
|
+
sys.exit(subprocess.call(cmd))
|
|
367
|
+
except OSError:
|
|
368
|
+
return # no sudo.exe — caller handles it
|
|
359
369
|
# sudo resets the environment, so the FLASHPOD_* tuning knobs the user set
|
|
360
370
|
# would be lost across elevation. Re-assert them in the child via `env`.
|
|
361
371
|
passthru = ["%s=%s" % (k, v) for k, v in sorted(os.environ.items())
|
|
@@ -2424,16 +2434,15 @@ def main():
|
|
|
2424
2434
|
return 0
|
|
2425
2435
|
plat = platform.current()
|
|
2426
2436
|
if not opts.dry_run and not plat.is_admin():
|
|
2427
|
-
# Writing to a disk needs
|
|
2428
|
-
#
|
|
2429
|
-
|
|
2430
|
-
|
|
2431
|
-
print("flashpod flash: writing to a disk needs
|
|
2432
|
-
"elevating via sudo...", file=sys.stderr)
|
|
2437
|
+
# Writing to a disk needs elevation — try sudo (prompts for
|
|
2438
|
+
# credentials or shows a UAC dialog) rather than just bailing.
|
|
2439
|
+
if sys.stdin.isatty():
|
|
2440
|
+
role = "Administrator" if os.name == "nt" else "root"
|
|
2441
|
+
print("flashpod flash: writing to a disk needs %s — "
|
|
2442
|
+
"elevating via sudo..." % role, file=sys.stderr)
|
|
2433
2443
|
_sudo_reexec(_cmd_args(opts)) # re-execs; returns only if sudo is missing
|
|
2434
2444
|
msg = "flashpod flash: " + plat.privilege_hint()
|
|
2435
|
-
|
|
2436
|
-
msg += "\n sudo " + " ".join(_self_cmd() + _cmd_args(opts))
|
|
2445
|
+
msg += "\n sudo " + " ".join(_self_cmd() + _cmd_args(opts))
|
|
2437
2446
|
print(msg, file=sys.stderr)
|
|
2438
2447
|
return 1
|
|
2439
2448
|
max_data_gb = None
|
|
@@ -402,7 +402,10 @@ def pick_device(scan, render, path_of, empty_msg):
|
|
|
402
402
|
|
|
403
403
|
``scan()`` returns the candidate list (called again on each refresh),
|
|
404
404
|
``render(items)`` prints the table, ``path_of(item)`` yields the device path
|
|
405
|
-
to return
|
|
405
|
+
to return — or ``None`` for an entry that exists but can't be written, such
|
|
406
|
+
as the empty slot of a multi-slot card reader. Rejecting it here is much
|
|
407
|
+
kinder than letting it through: with no media, every downstream call fails
|
|
408
|
+
obscurely ("could not determine size", access denied) and reads as a bug.
|
|
406
409
|
|
|
407
410
|
The loop accepts 'r' to re-scan. Without it, a reader plugged in — or a card
|
|
408
411
|
inserted — after the list was drawn stays invisible until you quit and start
|
|
@@ -430,7 +433,13 @@ def pick_device(scan, render, path_of, empty_msg):
|
|
|
430
433
|
render(items)
|
|
431
434
|
continue
|
|
432
435
|
if sel.isdigit() and int(sel) < len(items):
|
|
433
|
-
|
|
436
|
+
path = path_of(items[int(sel)])
|
|
437
|
+
if path:
|
|
438
|
+
return path
|
|
439
|
+
print(color(" that slot is empty — insert a card and press 'r' "
|
|
440
|
+
"to rescan, or pick another device", C_YEL),
|
|
441
|
+
file=sys.stderr)
|
|
442
|
+
continue
|
|
434
443
|
print(color(" invalid selection", C_RED), file=sys.stderr)
|
|
435
444
|
|
|
436
445
|
|
|
@@ -481,7 +490,7 @@ def _render_candidates(cands):
|
|
|
481
490
|
def choose_device():
|
|
482
491
|
return pick_device(
|
|
483
492
|
list_candidates, _render_candidates,
|
|
484
|
-
lambda d: "/dev/" + d["name"],
|
|
493
|
+
lambda d: ("/dev/" + d["name"]) if int(d.get("size") or 0) else None,
|
|
485
494
|
"No removable/USB disks found. Plug in the card and retry.")
|
|
486
495
|
|
|
487
496
|
def device_label(dev):
|
|
@@ -795,7 +804,9 @@ def flash(device=None, firmware=None,
|
|
|
795
804
|
|
|
796
805
|
total_sectors = plat.device_sectors(dev)
|
|
797
806
|
if total_sectors <= 0:
|
|
798
|
-
sys.exit(color("could not determine size of %s
|
|
807
|
+
sys.exit(color("could not determine size of %s — is a card inserted? "
|
|
808
|
+
"An empty slot in a multi-slot reader still appears as "
|
|
809
|
+
"a disk, but reports size 0." % dev, C_RED))
|
|
799
810
|
confirm(dev, total_sectors, assume_yes or dry_run, lba48, max_data_sectors)
|
|
800
811
|
plat.unmount_all(dev, dry_run)
|
|
801
812
|
write_layout(dev, fw, total_sectors, dry_run, do_format, lba48, max_data_sectors)
|
|
@@ -85,9 +85,13 @@ class MacOSPlatform(Platform):
|
|
|
85
85
|
(i, d, ipod_flash.fmt_size(size), name), file=sys.stderr)
|
|
86
86
|
print(file=sys.stderr)
|
|
87
87
|
|
|
88
|
+
def to_path(item):
|
|
89
|
+
d, info = item
|
|
90
|
+
size = int(info.get("TotalSize") or info.get("Size") or 0)
|
|
91
|
+
return ("/dev/" + d) if size else None # 0 = empty reader slot
|
|
92
|
+
|
|
88
93
|
return ipod_flash.pick_device(
|
|
89
|
-
self._external_disks, render,
|
|
90
|
-
lambda item: "/dev/" + item[0],
|
|
94
|
+
self._external_disks, render, to_path,
|
|
91
95
|
"No external disks found. Plug in the card reader and retry.")
|
|
92
96
|
|
|
93
97
|
def device_sectors(self, dev):
|
|
@@ -92,6 +92,9 @@ def _drive_number(dev):
|
|
|
92
92
|
class WindowsPlatform(Platform):
|
|
93
93
|
name = "windows"
|
|
94
94
|
|
|
95
|
+
def __init__(self):
|
|
96
|
+
self._held_locks = [] # [(handle_path, win_handle), ...]
|
|
97
|
+
|
|
95
98
|
# -- privilege --------------------------------------------------------
|
|
96
99
|
def is_admin(self):
|
|
97
100
|
try:
|
|
@@ -163,11 +166,22 @@ class WindowsPlatform(Platform):
|
|
|
163
166
|
print(" [%d] \\\\.\\PhysicalDrive%-3d %10s %s (%s)" %
|
|
164
167
|
(i, num, ipod_flash.fmt_size(size), name.strip(), bus),
|
|
165
168
|
file=sys.stderr)
|
|
169
|
+
if not size:
|
|
170
|
+
# A multi-slot reader shows one disk per slot; an empty one
|
|
171
|
+
# reports size 0 / "No Media". Say so, or it reads as a
|
|
172
|
+
# normal target and every later failure ("could not
|
|
173
|
+
# determine size", access denied) looks like a flashpod bug.
|
|
174
|
+
print(color(" no card inserted — empty reader slot",
|
|
175
|
+
ipod_flash.C_YEL), file=sys.stderr)
|
|
166
176
|
print(file=sys.stderr)
|
|
167
177
|
|
|
178
|
+
def to_path(d):
|
|
179
|
+
if not d[1]:
|
|
180
|
+
return None # empty slot: rejected by pick_device
|
|
181
|
+
return "\\\\.\\PhysicalDrive%d" % d[0]
|
|
182
|
+
|
|
168
183
|
return ipod_flash.pick_device(
|
|
169
|
-
self._removable_disks, render,
|
|
170
|
-
lambda d: "\\\\.\\PhysicalDrive%d" % d[0],
|
|
184
|
+
self._removable_disks, render, to_path,
|
|
171
185
|
"No removable disks found. Plug in the card and retry.")
|
|
172
186
|
|
|
173
187
|
def device_sectors(self, dev):
|
|
@@ -202,11 +216,29 @@ class WindowsPlatform(Platform):
|
|
|
202
216
|
num = _drive_number(dev)
|
|
203
217
|
except ValueError:
|
|
204
218
|
return []
|
|
219
|
+
# AccessPaths covers both drive letters (D:\) and volume GUID
|
|
220
|
+
# paths (\\?\Volume{...}\). The old DriveLetter-only filter missed
|
|
221
|
+
# letterless volumes, so their sectors were never locked and raw
|
|
222
|
+
# writes failed with access-denied / bad-file-descriptor.
|
|
205
223
|
txt = _powershell(
|
|
206
|
-
"Get-Partition -DiskNumber %d
|
|
207
|
-
"ForEach-Object { $_.
|
|
208
|
-
|
|
209
|
-
|
|
224
|
+
"Get-Partition -DiskNumber %d -ErrorAction SilentlyContinue"
|
|
225
|
+
" | ForEach-Object { foreach ($a in $_.AccessPaths) { $a } }"
|
|
226
|
+
% num)
|
|
227
|
+
seen = set()
|
|
228
|
+
results = []
|
|
229
|
+
for line in txt.splitlines():
|
|
230
|
+
p = line.strip()
|
|
231
|
+
if not p or p in seen:
|
|
232
|
+
continue
|
|
233
|
+
seen.add(p)
|
|
234
|
+
if len(p) >= 2 and p[1] == ':':
|
|
235
|
+
# Drive letter: D:\ -> ("D:", "D:\")
|
|
236
|
+
results.append((p[:2], p))
|
|
237
|
+
elif p.startswith('\\\\?\\'):
|
|
238
|
+
# Volume GUID: \\?\Volume{...}\ -> ("Volume{...}", path)
|
|
239
|
+
# _lock_volumes prepends \\.\ to build \\.\Volume{...}
|
|
240
|
+
results.append((p[4:].rstrip('\\'), p))
|
|
241
|
+
return results
|
|
210
242
|
|
|
211
243
|
def validate_target(self, dev, dry_run):
|
|
212
244
|
from .. import ipod_flash
|
|
@@ -221,20 +253,44 @@ class WindowsPlatform(Platform):
|
|
|
221
253
|
sys.exit(color("refusing: PhysicalDrive%d backs the running system." % num, red))
|
|
222
254
|
|
|
223
255
|
# -- mutation around the raw write ------------------------------------
|
|
224
|
-
def
|
|
225
|
-
|
|
226
|
-
|
|
256
|
+
def _lock_volumes(self, dev):
|
|
257
|
+
"""Lock and dismount every volume on *dev*, holding the handles so
|
|
258
|
+
Windows cannot remount between write_layout and format_data.
|
|
259
|
+
|
|
260
|
+
Safe to call more than once: volumes already locked are skipped, and
|
|
261
|
+
volumes that appeared since the previous call (e.g. after an MBR
|
|
262
|
+
rewrite) are picked up."""
|
|
263
|
+
already = {v for v, _h in self._held_locks}
|
|
227
264
|
for vol, _mp in self.device_mountpoints(dev):
|
|
265
|
+
handle_path = "\\\\.\\%s" % vol.rstrip("\\")
|
|
266
|
+
if handle_path in already:
|
|
267
|
+
continue
|
|
228
268
|
try:
|
|
229
|
-
h = self._open_handle(
|
|
269
|
+
h = self._open_handle(handle_path, write=True)
|
|
230
270
|
try:
|
|
231
271
|
self._ioctl(h, FSCTL_LOCK_VOLUME)
|
|
232
272
|
self._ioctl(h, FSCTL_DISMOUNT_VOLUME)
|
|
233
|
-
|
|
234
|
-
|
|
273
|
+
self._held_locks.append((handle_path, h))
|
|
274
|
+
except OSError:
|
|
275
|
+
_k32().CloseHandle(h)
|
|
235
276
|
except OSError:
|
|
236
277
|
pass
|
|
237
278
|
|
|
279
|
+
def _release_locks(self):
|
|
280
|
+
"""Close all held volume-lock handles."""
|
|
281
|
+
k = _k32()
|
|
282
|
+
for _vol, h in self._held_locks:
|
|
283
|
+
try:
|
|
284
|
+
k.CloseHandle(h)
|
|
285
|
+
except Exception: # noqa: BLE001
|
|
286
|
+
pass
|
|
287
|
+
self._held_locks = []
|
|
288
|
+
|
|
289
|
+
def unmount_all(self, dev, dry):
|
|
290
|
+
if dry:
|
|
291
|
+
return
|
|
292
|
+
self._lock_volumes(dev)
|
|
293
|
+
|
|
238
294
|
def wipe_signatures(self, dev, dry):
|
|
239
295
|
return # raw zeroing in write_layout handles this
|
|
240
296
|
|
|
@@ -259,6 +315,7 @@ class WindowsPlatform(Platform):
|
|
|
259
315
|
pass
|
|
260
316
|
|
|
261
317
|
def eject(self, dev, dry):
|
|
318
|
+
self._release_locks()
|
|
262
319
|
if dry:
|
|
263
320
|
return
|
|
264
321
|
try:
|
|
@@ -276,12 +333,12 @@ class WindowsPlatform(Platform):
|
|
|
276
333
|
import msvcrt
|
|
277
334
|
write = ("w" in mode or "+" in mode or "a" in mode)
|
|
278
335
|
if write:
|
|
279
|
-
#
|
|
280
|
-
#
|
|
281
|
-
#
|
|
282
|
-
#
|
|
283
|
-
#
|
|
284
|
-
self.
|
|
336
|
+
# Lock (and hold) every volume on this disk so Windows cannot
|
|
337
|
+
# remount between write_layout's MBR write and format_data's
|
|
338
|
+
# FAT32 write. Handles are kept alive in self._held_locks and
|
|
339
|
+
# released in eject(). _lock_volumes is incremental — safe to
|
|
340
|
+
# call on every open.
|
|
341
|
+
self._lock_volumes(dev)
|
|
285
342
|
h = self._open_handle(dev, write=write)
|
|
286
343
|
fd = msvcrt.open_osfhandle(h, os.O_BINARY)
|
|
287
344
|
return AlignedRawIO(os.fdopen(fd, mode, buffering=0))
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "flashpod"
|
|
7
|
-
version = "0.2.
|
|
7
|
+
version = "0.2.5"
|
|
8
8
|
description = "Command-line iPod sync + card-flashing tooling for early FireWire-era iPods, pure Python"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.6"
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|