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.
Files changed (26) hide show
  1. {flashpod-0.2.3 → flashpod-0.2.5}/PKG-INFO +1 -1
  2. {flashpod-0.2.3 → flashpod-0.2.5}/flashpod/__init__.py +1 -1
  3. {flashpod-0.2.3 → flashpod-0.2.5}/flashpod/cli.py +19 -10
  4. {flashpod-0.2.3 → flashpod-0.2.5}/flashpod/ipod_flash.py +15 -4
  5. {flashpod-0.2.3 → flashpod-0.2.5}/flashpod/platform/macos.py +6 -2
  6. {flashpod-0.2.3 → flashpod-0.2.5}/flashpod/platform/windows.py +75 -18
  7. {flashpod-0.2.3 → flashpod-0.2.5}/flashpod.egg-info/PKG-INFO +1 -1
  8. {flashpod-0.2.3 → flashpod-0.2.5}/pyproject.toml +1 -1
  9. {flashpod-0.2.3 → flashpod-0.2.5}/LICENSE +0 -0
  10. {flashpod-0.2.3 → flashpod-0.2.5}/README.md +0 -0
  11. {flashpod-0.2.3 → flashpod-0.2.5}/flashpod/__main__.py +0 -0
  12. {flashpod-0.2.3 → flashpod-0.2.5}/flashpod/contrib/99-flashpod-firewire-ipod.rules +0 -0
  13. {flashpod-0.2.3 → flashpod-0.2.5}/flashpod/fat32.py +0 -0
  14. {flashpod-0.2.3 → flashpod-0.2.5}/flashpod/fatfs.py +0 -0
  15. {flashpod-0.2.3 → flashpod-0.2.5}/flashpod/firmware/firmware.json +0 -0
  16. {flashpod-0.2.3 → flashpod-0.2.5}/flashpod/itunesdb.py +0 -0
  17. {flashpod-0.2.3 → flashpod-0.2.5}/flashpod/platform/__init__.py +0 -0
  18. {flashpod-0.2.3 → flashpod-0.2.5}/flashpod/platform/base.py +0 -0
  19. {flashpod-0.2.3 → flashpod-0.2.5}/flashpod/platform/linux.py +0 -0
  20. {flashpod-0.2.3 → flashpod-0.2.5}/flashpod/resources.py +0 -0
  21. {flashpod-0.2.3 → flashpod-0.2.5}/flashpod.egg-info/SOURCES.txt +0 -0
  22. {flashpod-0.2.3 → flashpod-0.2.5}/flashpod.egg-info/dependency_links.txt +0 -0
  23. {flashpod-0.2.3 → flashpod-0.2.5}/flashpod.egg-info/entry_points.txt +0 -0
  24. {flashpod-0.2.3 → flashpod-0.2.5}/flashpod.egg-info/requires.txt +0 -0
  25. {flashpod-0.2.3 → flashpod-0.2.5}/flashpod.egg-info/top_level.txt +0 -0
  26. {flashpod-0.2.3 → flashpod-0.2.5}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: flashpod
3
- Version: 0.2.3
3
+ Version: 0.2.5
4
4
  Summary: Command-line iPod sync + card-flashing tooling for early FireWire-era iPods, pure Python
5
5
  Author: David Barnhart
6
6
  License-Expression: MIT
@@ -4,4 +4,4 @@
4
4
  Pure Python, no libgpod. See the README for usage.
5
5
  """
6
6
 
7
- __version__ = "0.2.0"
7
+ __version__ = "0.2.5"
@@ -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 os.name == "nt" or not sys.stdin.isatty():
358
- return # can't prompt — caller handles it
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 rootelevate via sudo (prompts for the
2428
- # password on a terminal) rather than just bailing, like the raw
2429
- # data commands do.
2430
- if os.name != "nt" and sys.stdin.isatty():
2431
- print("flashpod flash: writing to a disk needs root — "
2432
- "elevating via sudo...", file=sys.stderr)
2437
+ # Writing to a disk needs elevationtry 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
- if os.name != "nt": # offer the exact sudo rerun on POSIX
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
- return path_of(items[int(sel)])
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" % dev, C_RED))
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 | Where-Object DriveLetter | "
207
- "ForEach-Object { $_.DriveLetter }" % num)
208
- return [("%s:" % c.strip(), "%s:\\" % c.strip())
209
- for c in txt.splitlines() if c.strip()]
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 unmount_all(self, dev, dry):
225
- if dry:
226
- return
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("\\\\.\\%s" % vol.rstrip("\\"), write=True)
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
- finally:
234
- ctypes.windll.kernel32.CloseHandle(h)
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
- # FSCTL_LOCK_VOLUME only holds while the locking handle is open, so
280
- # the dismount done before write_layout is already undone by now:
281
- # writing the new MBR makes Windows rescan and re-mount the data
282
- # partition, and it then refuses writes into those sectors. Dismount
283
- # again immediately before each write open.
284
- self.unmount_all(dev, False)
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))
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: flashpod
3
- Version: 0.2.3
3
+ Version: 0.2.5
4
4
  Summary: Command-line iPod sync + card-flashing tooling for early FireWire-era iPods, pure Python
5
5
  Author: David Barnhart
6
6
  License-Expression: MIT
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "flashpod"
7
- version = "0.2.3"
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