@follenfang/fupload 0.0.12 → 0.0.16

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.
@@ -282,6 +282,31 @@ def _load_live_state() -> Optional[Dict[str, Any]]:
282
282
  return value
283
283
 
284
284
 
285
+ def _remove_session_state(path: Path, session_id: str, timeout: float = 5) -> bool:
286
+ deadline = time.monotonic() + timeout
287
+ while True:
288
+ try:
289
+ current = _read_json(path)
290
+ except FuploadError as exc:
291
+ if isinstance(exc.__cause__, FileNotFoundError):
292
+ return True
293
+ if not isinstance(exc.__cause__, OSError):
294
+ return False
295
+ else:
296
+ if current.get("session_id") != session_id:
297
+ return False
298
+ try:
299
+ path.unlink()
300
+ return True
301
+ except FileNotFoundError:
302
+ return True
303
+ except OSError:
304
+ pass
305
+ if time.monotonic() >= deadline:
306
+ return False
307
+ time.sleep(0.05)
308
+
309
+
285
310
  def doctor() -> Dict[str, Any]:
286
311
  dd_dir, signature = _dd_module().discover_dd_info()
287
312
  processes = running_dd_processes()
@@ -390,6 +415,10 @@ def start(confirm_close_gui: bool) -> Dict[str, Any]:
390
415
  "running": True,
391
416
  "reused": True,
392
417
  "login_count": active.get("login_count", 1),
418
+ "broker_count": active.get("broker_count", 1),
419
+ "sidecar_count": active.get("sidecar_count", 1),
420
+ "native_login_count": active.get("native_login_count", 1),
421
+ "credential_kind": active.get("credential_kind"),
393
422
  }
394
423
  processes = running_dd_processes()
395
424
  if processes and not confirm_close_gui:
@@ -427,6 +456,10 @@ def start(confirm_close_gui: bool) -> Dict[str, Any]:
427
456
  "running": True,
428
457
  "reused": False,
429
458
  "login_count": 1,
459
+ "broker_count": state.get("broker_count", 1),
460
+ "sidecar_count": state.get("sidecar_count", 1),
461
+ "native_login_count": state.get("native_login_count", 1),
462
+ "credential_kind": state.get("credential_kind"),
430
463
  "closed_gui_processes": len(processes),
431
464
  }
432
465
  if startup.exists():
@@ -471,15 +504,23 @@ def status(session_id: Optional[str] = None) -> Dict[str, Any]:
471
504
 
472
505
  def stop(session_id: str) -> Dict[str, Any]:
473
506
  data = _send({"session_id": session_id, "command": "stop"}, timeout=30)
474
- deadline = time.time() + 30
475
- while time.time() < deadline and _load_live_state():
507
+ deadline = time.monotonic() + 30
508
+ while True:
509
+ try:
510
+ state = _load_live_state()
511
+ except FuploadError as exc:
512
+ if exc.kind != "session_error" or not isinstance(exc.__cause__, OSError):
513
+ raise
514
+ else:
515
+ if not state:
516
+ break
517
+ if time.monotonic() >= deadline:
518
+ raise FuploadError(
519
+ "DD task session acknowledged stop but did not finish cleanup",
520
+ kind="session_stop_failed",
521
+ stage="session",
522
+ )
476
523
  time.sleep(0.05)
477
- if _load_live_state():
478
- raise FuploadError(
479
- "DD task session acknowledged stop but did not finish cleanup",
480
- kind="session_stop_failed",
481
- stage="session",
482
- )
483
524
  result = dict(data or {})
484
525
  result["cleanup_complete"] = True
485
526
  return result
@@ -542,6 +583,10 @@ def _serve(startup_id: str) -> int:
542
583
  sidecar = _dd_module().Sidecar().__enter__()
543
584
  state["dd_dir"] = str(sidecar.dd_dir)
544
585
  state["signature"] = sidecar.signature
586
+ state["credential_kind"] = sidecar.credential_kind
587
+ state["broker_count"] = 1
588
+ state["sidecar_count"] = 1
589
+ state["native_login_count"] = 1
545
590
  _atomic_json(state_path, state)
546
591
  try:
547
592
  startup_path.unlink()
@@ -570,6 +615,10 @@ def _serve(startup_id: str) -> int:
570
615
  "started_at": started_at,
571
616
  "last_activity": last_activity,
572
617
  "login_count": 1,
618
+ "broker_count": state.get("broker_count", 1),
619
+ "sidecar_count": state.get("sidecar_count", 1),
620
+ "native_login_count": state.get("native_login_count", 1),
621
+ "credential_kind": state.get("credential_kind"),
573
622
  "dd_dir": state.get("dd_dir"),
574
623
  "signature": state.get("signature"),
575
624
  }
@@ -615,12 +664,7 @@ def _serve(startup_id: str) -> int:
615
664
  listener.close()
616
665
  if sidecar is not None:
617
666
  sidecar.__exit__(None, None, None)
618
- try:
619
- current = _read_json(state_path)
620
- if current.get("session_id") == session_id:
621
- state_path.unlink()
622
- except (FuploadError, OSError):
623
- pass
667
+ _remove_session_state(state_path, session_id)
624
668
 
625
669
 
626
670
  def main(argv: Optional[List[str]] = None) -> int:
@@ -551,10 +551,52 @@ def wait_until(qt, predicate, timeout):
551
551
  return bool(predicate())
552
552
 
553
553
 
554
+ def _enum_name(value):
555
+ name = getattr(value, "name", None)
556
+ return name if isinstance(name, str) else None
557
+
558
+
559
+ def _create_relogin_flow(
560
+ account, credential, controller, sdk, cgi, netconfig,
561
+ urs_flow_class, mobile_flow_class,
562
+ ):
563
+ method = _enum_name(getattr(account, "method", None))
564
+ credential_type = _enum_name(getattr(credential, "type", None))
565
+ modifier = _enum_name(getattr(credential, "modifier", None))
566
+
567
+ if (method, credential_type, modifier) == ("urs", "urs_token", "normal"):
568
+ return urs_flow_class(controller, sdk, credential.value, account.name)
569
+ if (
570
+ method == "mobile"
571
+ and credential_type == "urs_mobile_token"
572
+ and modifier in ("mobile_password", "mobile_uplink")
573
+ ):
574
+ return mobile_flow_class(
575
+ controller, sdk, cgi, netconfig, credential.value,
576
+ modifier == "mobile_password", account.name,
577
+ )
578
+ raise RuntimeError("DD persisted login state uses an unsupported credential combination")
579
+
580
+
581
+ def _credential_kind(account, credential):
582
+ method = _enum_name(getattr(account, "method", None))
583
+ credential_type = _enum_name(getattr(credential, "type", None))
584
+ modifier = _enum_name(getattr(credential, "modifier", None))
585
+ if (method, credential_type, modifier) == ("urs", "urs_token", "normal"):
586
+ return "email"
587
+ if (
588
+ method == "mobile"
589
+ and credential_type == "urs_mobile_token"
590
+ and modifier in ("mobile_password", "mobile_uplink")
591
+ ):
592
+ return "mobile"
593
+ raise RuntimeError("DD persisted login state uses an unsupported credential combination")
594
+
595
+
554
596
  def open_session(timeout=45):
555
597
  from cli_anything.ccvoicehub.core.container import ContainerManager
556
598
  from cli_anything.ccvoicehub.core.qt_runtime import QtRuntime
557
- from components.login.flow import MobileReLoginFlow
599
+ from components.login.flow import MobileReLoginFlow, UrsReLoginFlow
558
600
  import components.login.login_controller as login_controller_module
559
601
  import logger.cclogger as cclogger
560
602
 
@@ -562,7 +604,7 @@ def open_session(timeout=45):
562
604
  container = ContainerManager.get_container()
563
605
  storage = container.get_instance("AccountCredStorage")
564
606
  account = storage.getAutoAccount()
565
- credential = storage.getCred(account)
607
+ credential = storage.getCred(account) if account else None
566
608
  if not account or not credential:
567
609
  raise RuntimeError("DD has no persisted credential; sign in with the desktop client first")
568
610
  cclogger.renameLogAfterLogin = lambda *_args, **_kwargs: None
@@ -581,11 +623,12 @@ def open_session(timeout=45):
581
623
  state["jwt"] = True
582
624
 
583
625
  jwt_helper.sigJwtUpdated.connect(jwt_result)
584
- flow = MobileReLoginFlow(
585
- controller, container.get_instance("UrsSDK"), container.get_instance("CgiHelper"),
586
- container.get_instance("NetConfig"), credential.value,
587
- credential.modifier.name == "mobile_password", account.name,
626
+ flow = _create_relogin_flow(
627
+ account, credential, controller,
628
+ container.get_instance("UrsSDK"), container.get_instance("CgiHelper"),
629
+ container.get_instance("NetConfig"), UrsReLoginFlow, MobileReLoginFlow,
588
630
  )
631
+ flow._fupload_credential_kind = _credential_kind(account, credential)
589
632
  flow.sigResult.connect(login_result)
590
633
  flow.start()
591
634
  if not wait_until(qt, lambda: state["done"], timeout) or not state["ok"]:
@@ -610,6 +653,12 @@ def api_result(payload):
610
653
  return payload.get("result") if isinstance(payload, dict) else None
611
654
 
612
655
 
656
+ def _retryable_get_signature_rejection(payload):
657
+ if not isinstance(payload, dict) or payload.get("code") != 409:
658
+ return False
659
+ return str(payload.get("msg") or payload.get("message") or "").strip() == "签名无效"
660
+
661
+
613
662
  def _native_json(value):
614
663
  to_json = getattr(value, "toJson", None)
615
664
  if callable(to_json):
@@ -673,7 +722,13 @@ def run_command(session, command):
673
722
  method = command.get("method", "GET").upper()
674
723
  path = command["path"]
675
724
  params = command.get("payload") or {}
676
- response_payload = client.get(path, params) if method == "GET" else client.post(path, params)
725
+ if method == "GET":
726
+ response_payload = client.get(path, params)
727
+ if _retryable_get_signature_rejection(response_payload):
728
+ client._fupload_last_response_error = None
729
+ response_payload = client.get(path, params)
730
+ else:
731
+ response_payload = client.post(path, params)
677
732
  if isinstance(response_payload, dict) and response_payload.get("code") not in (None, 0):
678
733
  rejected_payload = response_payload
679
734
  response_probe = getattr(client, "_fupload_last_response_error", None)
@@ -831,7 +886,11 @@ def main():
831
886
  session = None
832
887
  try:
833
888
  session = open_session()
834
- output({"ready": True, "device_state_created": created})
889
+ output({
890
+ "ready": True,
891
+ "device_state_created": created,
892
+ "credential_kind": getattr(session[2], "_fupload_credential_kind", None),
893
+ })
835
894
  for line in sys.stdin:
836
895
  if not line.strip():
837
896
  continue
@@ -2,6 +2,8 @@
2
2
 
3
3
  from __future__ import annotations
4
4
 
5
+ import base64
6
+ import binascii
5
7
  import hashlib
6
8
  import json
7
9
  import sys
@@ -22,8 +24,10 @@ _SENSITIVE_KEYS = {
22
24
  }
23
25
  _RAW_CONTENT_KEYS = {
24
26
  "content", "wa_str", "t_wa_str", "raw_wtf", "wtf_zip", "download_url",
25
- "import_string",
27
+ "import_string", "content_text", "contenttext", "code_text", "codetext",
28
+ "description", "changelog", "license_content", "licensecontent",
26
29
  }
30
+ _BASE64_CONTENT_KEYS = {"base64", "logo_base64", "screenshot_base64s"}
27
31
 
28
32
 
29
33
  class _DuplicateKey(ValueError):
@@ -89,7 +93,9 @@ def sanitize_output(value: Any) -> Any:
89
93
  result: Dict[str, Any] = {}
90
94
  for key, item in value.items():
91
95
  normalized = str(key).replace("-", "_").lower()
92
- if normalized in {"token_present", "token_decrypted", "token_nonempty", "api_ready"} and isinstance(item, bool):
96
+ if normalized == "credential_kind" and item in {"email", "mobile"}:
97
+ result[key] = item
98
+ elif normalized in {"token_present", "token_decrypted", "token_nonempty", "api_ready"} and isinstance(item, bool):
93
99
  result[key] = item
94
100
  elif normalized in _SENSITIVE_KEYS or any(
95
101
  marker in normalized
@@ -102,6 +108,18 @@ def sanitize_output(value: Any) -> Any:
102
108
  "bytes": len(text),
103
109
  "sha256": hashlib.sha256(text).hexdigest(),
104
110
  }
111
+ elif normalized in _BASE64_CONTENT_KEYS and isinstance(item, str):
112
+ try:
113
+ content = base64.b64decode(item, validate=True)
114
+ encoding = "base64"
115
+ except (binascii.Error, ValueError):
116
+ content = item.encode("utf-8")
117
+ encoding = "utf-8"
118
+ result[str(key) + "_summary"] = {
119
+ "bytes": len(content),
120
+ "sha256": hashlib.sha256(content).hexdigest(),
121
+ "source_encoding": encoding,
122
+ }
105
123
  else:
106
124
  result[key] = sanitize_output(item)
107
125
  return result