accli 2.0.13__tar.gz → 2.0.14__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 (28) hide show
  1. {accli-2.0.13 → accli-2.0.14}/PKG-INFO +1 -1
  2. {accli-2.0.13 → accli-2.0.14}/accli/AcceleratorTaskDispatcher.py +1 -1
  3. {accli-2.0.13 → accli-2.0.14}/accli/_version.py +1 -1
  4. {accli-2.0.13 → accli-2.0.14}/accli/cli.py +149 -62
  5. {accli-2.0.13 → accli-2.0.14}/accli/gui.py +1 -0
  6. {accli-2.0.13 → accli-2.0.14}/accli/nfs_watcher.py +5 -2
  7. {accli-2.0.13 → accli-2.0.14}/accli/setup-nfs.ps1 +14 -2
  8. {accli-2.0.13 → accli-2.0.14}/accli.egg-info/PKG-INFO +1 -1
  9. {accli-2.0.13 → accli-2.0.14}/tests/test_mount_lifecycle.py +1 -0
  10. {accli-2.0.13 → accli-2.0.14}/LICENSE +0 -0
  11. {accli-2.0.13 → accli-2.0.14}/README.md +0 -0
  12. {accli-2.0.13 → accli-2.0.14}/accli/AcceleratorJobProjectService.py +0 -0
  13. {accli-2.0.13 → accli-2.0.14}/accli/AcceleratorTerminalCliProjectService.py +0 -0
  14. {accli-2.0.13 → accli-2.0.14}/accli/CsvRegionalTimeseriesValidator.py +0 -0
  15. {accli-2.0.13 → accli-2.0.14}/accli/__init__.py +0 -0
  16. {accli-2.0.13 → accli-2.0.14}/accli/__main__.py +0 -0
  17. {accli-2.0.13 → accli-2.0.14}/accli/cleanup-nfs.ps1 +0 -0
  18. {accli-2.0.13 → accli-2.0.14}/accli/common.py +0 -0
  19. {accli-2.0.13 → accli-2.0.14}/accli/mount_downloader.py +0 -0
  20. {accli-2.0.13 → accli-2.0.14}/accli/refresh_explorer.ps1 +0 -0
  21. {accli-2.0.13 → accli-2.0.14}/accli/token.py +0 -0
  22. {accli-2.0.13 → accli-2.0.14}/accli.egg-info/SOURCES.txt +0 -0
  23. {accli-2.0.13 → accli-2.0.14}/accli.egg-info/dependency_links.txt +0 -0
  24. {accli-2.0.13 → accli-2.0.14}/accli.egg-info/entry_points.txt +0 -0
  25. {accli-2.0.13 → accli-2.0.14}/accli.egg-info/requires.txt +0 -0
  26. {accli-2.0.13 → accli-2.0.14}/accli.egg-info/top_level.txt +0 -0
  27. {accli-2.0.13 → accli-2.0.14}/pyproject.toml +0 -0
  28. {accli-2.0.13 → accli-2.0.14}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: accli
3
- Version: 2.0.13
3
+ Version: 2.0.14
4
4
  Summary: IIASA Accelerator Client
5
5
  Author-email: Wrufesh S <wrufesh@gmail.com>
6
6
  License: The MIT License (MIT)
@@ -196,7 +196,7 @@ class JobDispatchModel(BaseModel):
196
196
  node_id: Optional[str] = None
197
197
 
198
198
  ignore_duplicate_job: bool = False
199
- free_node: bool = False # Only applies to immediate children jobs
199
+ free_node: bool = True
200
200
 
201
201
  children: List['JobDispatchModel'] = []
202
202
  callback: Optional['JobDispatchModel'] = None
@@ -1,3 +1,3 @@
1
1
  # Please strictly put double quote to use this info for git tag
2
2
  # Read a developer guide for git tag command with regex
3
- VERSION = "v2.0.13"
3
+ VERSION = "v2.0.14"
@@ -589,17 +589,30 @@ mount_app = typer.Typer(
589
589
  app.add_typer(mount_app, name="mount")
590
590
 
591
591
 
592
+ def is_windows_drive_mapped(drive: str) -> bool:
593
+ """Non-blocking check if a Windows drive letter is currently mapped using Win32 GetLogicalDrives()."""
594
+ import ctypes
595
+ letter = drive.strip().rstrip(":\\/").upper()
596
+ if len(letter) != 1 or not letter.isalpha():
597
+ return os.path.exists(drive)
598
+ try:
599
+ bitmask = ctypes.windll.kernel32.GetLogicalDrives()
600
+ return bool(bitmask & (1 << (ord(letter) - ord("A"))))
601
+ except Exception:
602
+ return os.path.exists(f"{letter}:\\")
603
+
604
+
592
605
  def find_available_windows_drive(preferred: str) -> str:
593
606
  """Finds an available Windows drive letter, starting with preferred, then checking others."""
594
607
  import os
595
608
  preferred = preferred.upper()
596
- if not os.path.exists(f"{preferred}:\\"):
609
+ if not is_windows_drive_mapped(preferred):
597
610
  return f"{preferred}:"
598
611
 
599
612
  # Search order: subsequent letters first, then previous descending to D
600
613
  search_order = ["X", "Y", "Z"] + [chr(x) for x in range(ord("V"), ord("D") - 1, -1)]
601
614
  for letter in search_order:
602
- if not os.path.exists(f"{letter}:\\"):
615
+ if not is_windows_drive_mapped(letter):
603
616
  return f"{letter}:"
604
617
 
605
618
  raise RuntimeError("No available Windows drive letters found.")
@@ -732,6 +745,46 @@ def cleanup_stuck_windows_mount_processes():
732
745
  pass
733
746
  try:
734
747
  subprocess.run(["taskkill", "/f", "/im", "umount.exe"], capture_output=True)
748
+ subprocess.run(["taskkill", "/f", "/im", "net.exe"], capture_output=True)
749
+ except Exception:
750
+ pass
751
+
752
+
753
+ def cleanup_stale_rdp_namespace_entries():
754
+ """
755
+ Remove disconnected/stale RDP redirected drive entries (\\\\tsclient\\<drive>) from the user's
756
+ Explorer MyComputer Namespace registry key.
757
+ When Windows Client for NFS (NfsClnt) is active, any dead \\\\tsclient\\<drive> shortcut in
758
+ 'This PC' fails in RDPNP and falls through Windows MUP to NfsClnt, which attempts to resolve
759
+ 'tsclient' via DNS/NetBIOS/RPC and freezes new Explorer 'This PC' views for 21+ seconds.
760
+ """
761
+ import subprocess
762
+ ps_script = (
763
+ "$activeShares = [System.Collections.Generic.HashSet[string]]::new([System.StringComparer]::OrdinalIgnoreCase)\n"
764
+ "net use 2>$null | ForEach-Object {\n"
765
+ " if ($_ -match '(\\\\\\\\tsclient\\\\[a-zA-Z0-9_$]+)') {\n"
766
+ " [void]$activeShares.Add($matches[1])\n"
767
+ " }\n"
768
+ "}\n"
769
+ "Get-ChildItem 'HKCU:\\Software\\Microsoft\\Windows\\CurrentVersion\\Explorer\\SessionInfo\\*\\MyComputer\\Namespace\\*' -ErrorAction SilentlyContinue | ForEach-Object {\n"
770
+ " $clsid = $_.PSChildName\n"
771
+ " $bag = Get-ItemProperty \"HKCU:\\Software\\Classes\\CLSID\\$clsid\\Instance\\InitPropertyBag\" -ErrorAction SilentlyContinue\n"
772
+ " if ($bag -and $bag.Target -and $bag.Target -match '^\\\\\\\\tsclient\\\\') {\n"
773
+ " if (-not $activeShares.Contains($bag.Target)) {\n"
774
+ " Remove-Item -LiteralPath $_.PSPath -Recurse -Force -ErrorAction SilentlyContinue\n"
775
+ " }\n"
776
+ " }\n"
777
+ "}\n"
778
+ )
779
+ try:
780
+ subprocess.run(
781
+ ["powershell.exe", "-NoProfile", "-NonInteractive", "-ExecutionPolicy", "Bypass", "-Command", ps_script],
782
+ capture_output=True,
783
+ text=True,
784
+ errors="replace",
785
+ timeout=5,
786
+ creationflags=0x08000000 # CREATE_NO_WINDOW
787
+ )
735
788
  except Exception:
736
789
  pass
737
790
 
@@ -740,8 +793,8 @@ def is_nfs_daemon_running() -> bool:
740
793
  """Check if hf-mount-nfs.exe is currently running."""
741
794
  import subprocess
742
795
  try:
743
- res = subprocess.run(["tasklist", "/FI", "IMAGENAME eq hf-mount-nfs.exe"], capture_output=True, text=True)
744
- return "hf-mount-nfs.exe" in res.stdout
796
+ res = subprocess.run(["tasklist", "/FI", "IMAGENAME eq hf-mount-nfs.exe"], capture_output=True, text=True, errors="replace")
797
+ return res.stdout is not None and "hf-mount-nfs.exe" in res.stdout
745
798
  except Exception:
746
799
  return False
747
800
 
@@ -753,13 +806,13 @@ def is_nfs_kernel_driver_active() -> bool:
753
806
  if not shutil.which("mount.exe") and not os.path.isfile("C:\\Windows\\System32\\mount.exe"):
754
807
  return False
755
808
  try:
756
- res = subprocess.run(["sc.exe", "query", "NfsRdr"], capture_output=True, text=True)
757
- if "RUNNING" in res.stdout:
809
+ res = subprocess.run(["sc.exe", "query", "NfsRdr"], capture_output=True, text=True, errors="replace")
810
+ if res.stdout and "RUNNING" in res.stdout:
758
811
  return True
759
812
  # If stopped, try to start it
760
813
  subprocess.run(["sc.exe", "start", "NfsRdr"], capture_output=True)
761
- check = subprocess.run(["sc.exe", "query", "NfsRdr"], capture_output=True, text=True)
762
- return "RUNNING" in check.stdout
814
+ check = subprocess.run(["sc.exe", "query", "NfsRdr"], capture_output=True, text=True, errors="replace")
815
+ return check.stdout is not None and "RUNNING" in check.stdout
763
816
  except Exception:
764
817
  return False
765
818
 
@@ -817,14 +870,16 @@ def map_windows_network_drive(mount_point_abs, max_attempts: int) -> tuple[bool,
817
870
  Retries with cleanup if Windows NFS client returns a transient error.
818
871
  """
819
872
  import subprocess
873
+ cleanup_stale_rdp_namespace_entries()
820
874
  target_str = str(mount_point_abs).rstrip("\\/")
821
875
  if re.match(r"^[a-zA-Z]$", target_str):
822
876
  target_str += ":"
823
877
 
824
878
  mount_cmd = [
825
879
  "C:\\Windows\\System32\\mount.exe",
826
- "-o", "mtype=hard",
827
- "-o", "timeout=10",
880
+ "-o", "mtype=soft",
881
+ "-o", "timeout=2",
882
+ "-o", "retry=2",
828
883
  "-o", "rsize=32",
829
884
  "-o", "wsize=32",
830
885
  "-o", "anon",
@@ -844,7 +899,7 @@ def map_windows_network_drive(mount_point_abs, max_attempts: int) -> tuple[bool,
844
899
  wait_for_nfs_portmapper("127.0.0.1", 111, 10.0)
845
900
 
846
901
  try:
847
- res = subprocess.run(mount_cmd, capture_output=True, text=True, timeout=15)
902
+ res = subprocess.run(mount_cmd, capture_output=True, text=True, errors="replace", timeout=15)
848
903
  if res.returncode == 0:
849
904
  # Confirm drive is actually mounted and directory listing is populated and queryable
850
905
  drive_ready = False
@@ -854,7 +909,7 @@ def map_windows_network_drive(mount_point_abs, max_attempts: int) -> tuple[bool,
854
909
  entries = os.listdir(drive_root)
855
910
  if len(entries) > 0:
856
911
  drive_ready = True
857
- return True, res.stdout.strip()
912
+ return True, (res.stdout or "").strip()
858
913
  except OSError:
859
914
  pass
860
915
  time.sleep(0.5)
@@ -921,7 +976,17 @@ def enable_windows_nfs_features():
921
976
  "$taskEnabled = $false\n"
922
977
  "if (Get-Command Get-ScheduledTask -ErrorAction SilentlyContinue) {\n"
923
978
  " $t = Get-ScheduledTask -TaskName 'accli-mount-nfs' -ErrorAction SilentlyContinue\n"
924
- " if ($t) { $taskEnabled = $true }\n"
979
+ " if ($t) {\n"
980
+ " $taskEnabled = $true\n"
981
+ " if ($t.Settings.ExecutionTimeLimit -ne 'PT0S' -and $t.Settings.ExecutionTimeLimit -ne 'P0D' -and -not [string]::IsNullOrEmpty($t.Settings.ExecutionTimeLimit)) {\n"
982
+ " try {\n"
983
+ " $s = New-ScheduledTaskSettingsSet -AllowStartIfOnBatteries -DontStopIfGoingOnBatteries -ExecutionTimeLimit ([TimeSpan]::Zero)\n"
984
+ " Set-ScheduledTask -TaskName 'accli-mount-nfs' -Settings $s -ErrorAction Stop | Out-Null\n"
985
+ " } catch {\n"
986
+ " $taskEnabled = $false\n"
987
+ " }\n"
988
+ " }\n"
989
+ " }\n"
925
990
  "} else {\n"
926
991
  " $query = schtasks /query /tn 'accli-mount-nfs' 2> $null\n"
927
992
  " if ($LASTEXITCODE -eq 0) { $taskEnabled = $true }\n"
@@ -942,8 +1007,8 @@ def enable_windows_nfs_features():
942
1007
  task_ok = False
943
1008
 
944
1009
  try:
945
- res = subprocess.run(check_cmd, capture_output=True, text=True, check=True)
946
- output = res.stdout.strip()
1010
+ res = subprocess.run(check_cmd, capture_output=True, text=True, errors="replace", check=True)
1011
+ output = (res.stdout or "").strip()
947
1012
  for part in output.split(';'):
948
1013
  if part.startswith("NFS:"):
949
1014
  nfs_ok = (part.split(':')[1].lower() == 'true')
@@ -1013,7 +1078,7 @@ def enable_windows_nfs_features():
1013
1078
  f"Start-Process powershell.exe -ArgumentList '-NoProfile -ExecutionPolicy Bypass -File \"{temp_file_path}\" {params_str}' -Verb RunAs -Wait"
1014
1079
  ]
1015
1080
 
1016
- result = subprocess.run(elevate_cmd, capture_output=True, text=True)
1081
+ result = subprocess.run(elevate_cmd, capture_output=True, text=True, errors="replace")
1017
1082
  if result.returncode != 0:
1018
1083
  print("[bold red][ERROR] Administrative configuration failed or UAC prompt was denied.[/bold red]")
1019
1084
  if result.stderr:
@@ -1104,26 +1169,19 @@ def mount_start(
1104
1169
 
1105
1170
  # Standardize mount_point and handle defaults / auto-selection
1106
1171
  if sys_name == "Windows":
1107
- # Check if the daemon is already running to prevent double mount
1108
- import subprocess
1109
- try:
1110
- task_check = subprocess.run(["tasklist", "/FI", "IMAGENAME eq hf-mount-nfs.exe"], capture_output=True, text=True)
1111
- if "hf-mount-nfs.exe" in task_check.stdout:
1112
- config_file = Path("C:/ProgramData/accli/mount_config.json")
1113
- active_msg = ""
1114
- if config_file.is_file():
1115
- try:
1116
- import json
1117
- cfg = json.loads(config_file.read_text(encoding="utf-8"))
1118
- active_msg = f" for project '{cfg.get('project_slug')}' at '{cfg.get('mount_point')}'"
1119
- except Exception:
1120
- pass
1121
- print(f"[bold red]ERROR: A mount daemon is already running{active_msg}.[/bold red]")
1122
- print("[yellow]Please run 'accli mount stop' to unmount before starting a new mount.[/yellow]")
1123
- raise typer.Exit(1)
1124
- except Exception as e:
1125
- if isinstance(e, typer.Exit):
1126
- raise e
1172
+ if is_nfs_daemon_running():
1173
+ config_file = Path("C:/ProgramData/accli/mount_config.json")
1174
+ active_msg = ""
1175
+ if config_file.is_file():
1176
+ try:
1177
+ import json
1178
+ cfg = json.loads(config_file.read_text(encoding="utf-8"))
1179
+ active_msg = f" for project '{cfg.get('project_slug')}' at '{cfg.get('mount_point')}'"
1180
+ except Exception:
1181
+ pass
1182
+ print(f"[bold red]ERROR: A mount daemon is already running{active_msg}.[/bold red]")
1183
+ print("[yellow]Please run 'accli mount stop' to unmount before starting a new mount.[/yellow]")
1184
+ raise typer.Exit(1)
1127
1185
 
1128
1186
  if mount_point is None:
1129
1187
  # Auto-detect available drive letter starting with W
@@ -1138,7 +1196,7 @@ def mount_start(
1138
1196
  mount_point_str = str(mount_point).strip()
1139
1197
  if re.match(r"^[a-zA-Z]:?[\\/]*$", mount_point_str):
1140
1198
  drive_letter = mount_point_str[0].upper()
1141
- if os.path.exists(f"{drive_letter}:\\"):
1199
+ if is_windows_drive_mapped(drive_letter):
1142
1200
  print(f"[yellow]Warning: Drive '{drive_letter}:' is already in use.[/yellow]")
1143
1201
  try:
1144
1202
  fallback_drive = find_available_windows_drive("W")
@@ -1167,6 +1225,12 @@ def mount_start(
1167
1225
  env["ACC_ENDPOINT"] = server_url
1168
1226
  env["ACC_CAS_ENDPOINT"] = f"{server_url.rstrip('/')}/api/xet-cas"
1169
1227
  env["ACC_TOKEN"] = token
1228
+ cache_base = Path("C:/ProgramData/accli/cache") if sys_name == "Windows" else Path.home() / ".accli" / "cache"
1229
+ (cache_base / "xet").mkdir(parents=True, exist_ok=True)
1230
+ (cache_base / "hf-mount").mkdir(parents=True, exist_ok=True)
1231
+ env["HF_XET_CACHE"] = str(cache_base / "xet")
1232
+ env["HF_HOME"] = str(cache_base)
1233
+ env["XDG_CACHE_HOME"] = str(cache_base)
1170
1234
 
1171
1235
  if sys_name == "Windows":
1172
1236
  # Enable Windows NFS Client features if needed
@@ -1188,6 +1252,7 @@ def mount_start(
1188
1252
  str(hf_mount_bin),
1189
1253
  "--token-file", db_path,
1190
1254
  "--hub-endpoint", server_url,
1255
+ "--cache-dir", str(cache_base / "hf-mount"),
1191
1256
  mode,
1192
1257
  project_slug,
1193
1258
  str(mount_point_abs)
@@ -1219,7 +1284,7 @@ def mount_start(
1219
1284
  has_task = False
1220
1285
  if not is_admin:
1221
1286
  try:
1222
- task_check = subprocess.run(["schtasks", "/query", "/tn", "accli-mount-nfs"], capture_output=True, text=True)
1287
+ task_check = subprocess.run(["schtasks", "/query", "/tn", "accli-mount-nfs"], capture_output=True, text=True, errors="replace")
1223
1288
  if task_check.returncode == 0:
1224
1289
  has_task = True
1225
1290
  except Exception:
@@ -1273,7 +1338,7 @@ def mount_start(
1273
1338
  config_file = config_dir / "mount_config.json"
1274
1339
  config_file.write_text(json.dumps(config_data, indent=2), encoding="utf-8")
1275
1340
 
1276
- run_res = subprocess.run(["schtasks", "/run", "/tn", "accli-mount-nfs"], capture_output=True, text=True)
1341
+ run_res = subprocess.run(["schtasks", "/run", "/tn", "accli-mount-nfs"], capture_output=True, text=True, errors="replace")
1277
1342
  if run_res.returncode == 0:
1278
1343
  print("[cyan]Waiting for elevated NFS daemon to initialize...[/cyan]")
1279
1344
  if not ensure_nfs_daemon_stable(15.0):
@@ -1295,8 +1360,8 @@ def mount_start(
1295
1360
  raise typer.Exit(1)
1296
1361
  else:
1297
1362
  print("[bold red][ERROR] Failed to execute Scheduled Task 'accli-mount-nfs'.[/bold red]")
1298
- if trigger_res.stderr:
1299
- print(f"[red]{trigger_res.stderr.strip()}[/red]")
1363
+ if run_res.stderr:
1364
+ print(f"[red]{run_res.stderr.strip()}[/red]")
1300
1365
  print("[yellow]Falling back to UAC elevation...[/yellow]")
1301
1366
  has_task = False
1302
1367
  except Exception as e:
@@ -1360,7 +1425,7 @@ def mount_start(
1360
1425
  ps_command
1361
1426
  ]
1362
1427
 
1363
- elevate_res = subprocess.run(ps_args, capture_output=True, text=True)
1428
+ elevate_res = subprocess.run(ps_args, capture_output=True, text=True, errors="replace")
1364
1429
  if elevate_res.returncode != 0:
1365
1430
  print("[bold red][ERROR] Administrative elevation failed or was denied.[/bold red]")
1366
1431
  print("[yellow]Tip: Please request your IT Administrator to run 'setup-nfs.ps1' to configure the passwordless Task Scheduler gateway.[/yellow]")
@@ -1420,7 +1485,7 @@ def mount_start(
1420
1485
  print(f"[bold cyan]Starting mount for project '[white]{project_slug}[/white]' at '[white]{mount_point_abs}[/white]'...[/bold cyan]")
1421
1486
  import subprocess
1422
1487
  try:
1423
- result = subprocess.run(args, env=env, capture_output=True, text=True)
1488
+ result = subprocess.run(args, env=env, capture_output=True, text=True, errors="replace")
1424
1489
  if result.returncode == 0:
1425
1490
  print(f"[bold green][OK] Mount started successfully![/bold green]")
1426
1491
  if result.stdout:
@@ -1526,10 +1591,29 @@ def mount_stop(
1526
1591
 
1527
1592
  # 1. Unmap the network drive in the user session using net use
1528
1593
  # (Avoids sending RPC UMNT packet via umount.exe which causes kernel NFS redirector to queue stale UMNTs)
1529
- res = subprocess.run(["net", "use", target_str, "/delete", "/y"], capture_output=True, text=True)
1530
- if res.returncode != 0:
1531
- res = subprocess.run(["C:\\Windows\\System32\\umount.exe", "-f", target_str], capture_output=True, text=True)
1532
- subprocess.run(["net", "use", "\\\\127.0.0.1\\!", "/delete", "/y"], capture_output=True)
1594
+ # If the drive is mapped in Windows but the local NFS daemon has died (e.g., 72h task timeout),
1595
+ # Windows NFS kernel redirector (NfsRdr) with mtype=hard will block 'net use /delete' indefinitely
1596
+ # waiting for 127.0.0.1:111. Briefly revive the daemon so the kernel can complete the unmount cleanly.
1597
+ if is_windows_drive_mapped(target_str) and not is_nfs_daemon_running():
1598
+ try:
1599
+ subprocess.run(["schtasks", "/run", "/tn", "accli-mount-nfs"], capture_output=True, timeout=5)
1600
+ wait_for_nfs_portmapper("127.0.0.1", 111, 5.0)
1601
+ except Exception:
1602
+ pass
1603
+ res = None
1604
+ try:
1605
+ res = subprocess.run(["net", "use", target_str, "/delete", "/y"], capture_output=True, text=True, errors="replace", timeout=10)
1606
+ except subprocess.TimeoutExpired:
1607
+ cleanup_stuck_windows_mount_processes()
1608
+ if res is not None and res.returncode != 0:
1609
+ try:
1610
+ res = subprocess.run(["C:\\Windows\\System32\\umount.exe", "-f", target_str], capture_output=True, text=True, errors="replace", timeout=10)
1611
+ except subprocess.TimeoutExpired:
1612
+ cleanup_stuck_windows_mount_processes()
1613
+ try:
1614
+ subprocess.run(["net", "use", "\\\\127.0.0.1\\!", "/delete", "/y"], capture_output=True, timeout=5)
1615
+ except subprocess.TimeoutExpired:
1616
+ cleanup_stuck_windows_mount_processes()
1533
1617
 
1534
1618
  cleanup_stuck_windows_mount_processes()
1535
1619
 
@@ -1537,7 +1621,7 @@ def mount_stop(
1537
1621
  has_umount_task = False
1538
1622
  if not is_admin:
1539
1623
  try:
1540
- task_check = subprocess.run(["schtasks", "/query", "/tn", "accli-umount-nfs"], capture_output=True, text=True)
1624
+ task_check = subprocess.run(["schtasks", "/query", "/tn", "accli-umount-nfs"], capture_output=True, text=True, errors="replace")
1541
1625
  if task_check.returncode == 0:
1542
1626
  has_umount_task = True
1543
1627
  except Exception:
@@ -1549,19 +1633,18 @@ def mount_stop(
1549
1633
  # Poll for up to 5 seconds to ensure hf-mount-nfs.exe has fully terminated
1550
1634
  for _ in range(10):
1551
1635
  time.sleep(0.5)
1552
- task_check = subprocess.run(["tasklist", "/FI", "IMAGENAME eq hf-mount-nfs.exe"], capture_output=True, text=True)
1553
- if "hf-mount-nfs.exe" not in task_check.stdout:
1636
+ if not is_nfs_daemon_running():
1554
1637
  break
1555
1638
  cleanup_stuck_windows_mount_processes()
1556
1639
  for _ in range(10):
1557
- sched_check = subprocess.run(["schtasks", "/query", "/tn", "accli-mount-nfs"], capture_output=True, text=True)
1558
- if "Running" not in sched_check.stdout:
1640
+ sched_check = subprocess.run(["schtasks", "/query", "/tn", "accli-mount-nfs"], capture_output=True, text=True, errors="replace")
1641
+ if not sched_check.stdout or "Running" not in sched_check.stdout:
1559
1642
  break
1560
1643
  time.sleep(0.5)
1561
1644
 
1562
1645
  # Wait until drive letter is released from Windows namespace
1563
1646
  for _ in range(10):
1564
- if not os.path.exists(f"{target_str}\\"):
1647
+ if not is_windows_drive_mapped(target_str):
1565
1648
  break
1566
1649
  time.sleep(0.3)
1567
1650
  else:
@@ -1597,12 +1680,12 @@ def mount_stop(
1597
1680
  ps_command
1598
1681
  ], capture_output=True)
1599
1682
 
1600
- if res.returncode == 0 or "successfully" in res.stdout or "successfully" in res.stderr:
1683
+ if not is_windows_drive_mapped(target_str) or (res is not None and (res.returncode == 0 or (res.stdout and "successfully" in res.stdout) or (res.stderr and "successfully" in res.stderr))):
1601
1684
  print("[bold green][OK] Mount stopped successfully.[/bold green]")
1602
1685
  print("[cyan]Note: If mounting again, please allow a few seconds for the Windows NFS network cache to clear.[/cyan]")
1603
1686
  else:
1604
1687
  print("[yellow]Attempted to unmount. Please verify drive status manually using 'umount'.[/yellow]")
1605
- if res.stderr:
1688
+ if res is not None and res.stderr:
1606
1689
  print(f"[red]{res.stderr.strip()}[/red]")
1607
1690
  return
1608
1691
 
@@ -1629,7 +1712,7 @@ def mount_stop(
1629
1712
 
1630
1713
  import subprocess
1631
1714
  try:
1632
- result = subprocess.run(args, capture_output=True, text=True)
1715
+ result = subprocess.run(args, capture_output=True, text=True, errors="replace")
1633
1716
  if result.returncode == 0:
1634
1717
  print(f"[bold green][OK] Mount stopped successfully.[/bold green]")
1635
1718
  else:
@@ -1659,13 +1742,17 @@ def mount_status(
1659
1742
  print("[bold cyan]Active NFS mounts on Windows:[/bold cyan]")
1660
1743
  try:
1661
1744
  # Windows 'mount' command lists active NFS mounts
1662
- result = subprocess.run(["mount"], capture_output=True, text=True)
1663
- if result.returncode == 0 and result.stdout.strip():
1745
+ result = None
1746
+ if is_nfs_daemon_running():
1747
+ try:
1748
+ result = subprocess.run(["mount"], capture_output=True, text=True, errors="replace", timeout=5)
1749
+ except subprocess.TimeoutExpired:
1750
+ cleanup_stuck_windows_mount_processes()
1751
+ if result is not None and result.returncode == 0 and result.stdout and result.stdout.strip():
1664
1752
  print(result.stdout)
1665
1753
  else:
1666
1754
  # Check tasklist for active daemon process
1667
- task_res = subprocess.run(["tasklist", "/FI", "IMAGENAME eq hf-mount-nfs.exe"], capture_output=True, text=True)
1668
- if "hf-mount-nfs.exe" in task_res.stdout:
1755
+ if is_nfs_daemon_running():
1669
1756
  print("[cyan]hf-mount-nfs.exe process is running in the background.[/cyan]")
1670
1757
  else:
1671
1758
  print("[cyan]No running mounts found.[/cyan]")
@@ -1697,8 +1784,8 @@ def mount_status(
1697
1784
 
1698
1785
  # 1. Query hf-mount status directly
1699
1786
  try:
1700
- result = subprocess.run(args, capture_output=True, text=True)
1701
- if result.returncode == 0:
1787
+ result = subprocess.run(args, capture_output=True, text=True, errors="replace")
1788
+ if result.returncode == 0 and result.stdout:
1702
1789
  stdout = result.stdout.strip()
1703
1790
  if stdout:
1704
1791
  for line in stdout.splitlines():
@@ -150,6 +150,7 @@ class AccliGuiApp(tk.Tk):
150
150
  stdout=subprocess.PIPE,
151
151
  stderr=subprocess.STDOUT,
152
152
  text=True,
153
+ errors="replace",
153
154
  bufsize=1,
154
155
  startupinfo=startupinfo
155
156
  )
@@ -17,7 +17,7 @@ WAIT_TIMEOUT = 0x00000102
17
17
  ERROR_ALREADY_EXISTS = 183
18
18
  EVENT_MODIFY_STATE = 0x0002
19
19
 
20
- kernel32 = ctypes.windll.kernel32
20
+ kernel32 = ctypes.WinDLL("kernel32", use_last_error=True)
21
21
 
22
22
 
23
23
  def log(msg: str):
@@ -52,6 +52,7 @@ def get_open_explorer_paths(script_path: Path, mount_point: str) -> list[str]:
52
52
  ],
53
53
  capture_output=True,
54
54
  text=True,
55
+ errors="replace",
55
56
  creationflags=0x08000000 # CREATE_NO_WINDOW
56
57
  )
57
58
  if res.returncode == 0 and res.stdout:
@@ -83,6 +84,7 @@ def notify_explorer(script_path: Path, mount_point: str, path_str: str):
83
84
  ],
84
85
  capture_output=True,
85
86
  text=True,
87
+ errors="replace",
86
88
  creationflags=0x08000000 # CREATE_NO_WINDOW
87
89
  )
88
90
  except Exception as e:
@@ -142,8 +144,9 @@ def run_watcher(mount_point: str, interval: float):
142
144
  mutex_name, stop_event_name = get_sync_names(mount_point)
143
145
 
144
146
  # 1. Single-instance protection via Named Mutex
147
+ ctypes.set_last_error(0)
145
148
  mutex = kernel32.CreateMutexW(None, True, mutex_name)
146
- if kernel32.GetLastError() == ERROR_ALREADY_EXISTS:
149
+ if ctypes.get_last_error() == ERROR_ALREADY_EXISTS:
147
150
  log(f"Another watcher is already active for {drive_root} ({mutex_name}). Exiting.")
148
151
  sys.exit(0)
149
152
 
@@ -159,9 +159,21 @@ try {
159
159
  }
160
160
  } catch {}
161
161
 
162
+ # Force unified, accessible cache under C:\ProgramData\accli\cache
163
+ $cacheDir = "C:\ProgramData\accli\cache"
164
+ $hfXetCache = "$cacheDir\xet"
165
+ $hfMountCache = "$cacheDir\hf-mount"
166
+ New-Item -ItemType Directory -Path $hfXetCache -Force | Out-Null
167
+ New-Item -ItemType Directory -Path $hfMountCache -Force | Out-Null
168
+
169
+ $env:HF_XET_CACHE = $hfXetCache
170
+ $env:HF_HOME = $cacheDir
171
+ $env:XDG_CACHE_HOME = $cacheDir
172
+
162
173
  $mountArgs = @(
163
174
  "--token-file", $cfg.db_path,
164
175
  "--hub-endpoint", $cfg.server_url,
176
+ "--cache-dir", $hfMountCache,
165
177
  $cfg.mode,
166
178
  $cfg.project_slug,
167
179
  $cfg.mount_point
@@ -224,7 +236,7 @@ if (Get-Command Register-ScheduledTask -ErrorAction SilentlyContinue) {
224
236
  try {
225
237
  $action = New-ScheduledTaskAction -Execute $psActionPath -Argument $psActionArgs
226
238
  $principal = New-ScheduledTaskPrincipal -UserId "SYSTEM" -RunLevel Highest
227
- $settings = New-ScheduledTaskSettingsSet -AllowStartIfOnBatteries -DontStopIfGoingOnBatteries
239
+ $settings = New-ScheduledTaskSettingsSet -AllowStartIfOnBatteries -DontStopIfGoingOnBatteries -ExecutionTimeLimit ([TimeSpan]::Zero)
228
240
 
229
241
  # Unregister task if already exists to update
230
242
  Unregister-ScheduledTask -TaskName $taskName -Confirm:$false -ErrorAction SilentlyContinue | Out-Null
@@ -253,7 +265,7 @@ if (Get-Command Register-ScheduledTask -ErrorAction SilentlyContinue) {
253
265
  try {
254
266
  $action = New-ScheduledTaskAction -Execute $psActionPath -Argument $psUmountActionArgs
255
267
  $principal = New-ScheduledTaskPrincipal -UserId "SYSTEM" -RunLevel Highest
256
- $settings = New-ScheduledTaskSettingsSet -AllowStartIfOnBatteries -DontStopIfGoingOnBatteries
268
+ $settings = New-ScheduledTaskSettingsSet -AllowStartIfOnBatteries -DontStopIfGoingOnBatteries -ExecutionTimeLimit ([TimeSpan]::Zero)
257
269
 
258
270
  Unregister-ScheduledTask -TaskName $umountTaskName -Confirm:$false -ErrorAction SilentlyContinue | Out-Null
259
271
  Register-ScheduledTask -TaskName $umountTaskName -Action $action -Principal $principal -Settings $settings -Force | Out-Null
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: accli
3
- Version: 2.0.13
3
+ Version: 2.0.14
4
4
  Summary: IIASA Accelerator Client
5
5
  Author-email: Wrufesh S <wrufesh@gmail.com>
6
6
  License: The MIT License (MIT)
@@ -34,6 +34,7 @@ class TestMountLifecycle(unittest.TestCase):
34
34
  stdin=subprocess.DEVNULL,
35
35
  capture_output=True,
36
36
  text=True,
37
+ errors="replace",
37
38
  timeout=timeout
38
39
  )
39
40
  print(f"[EXIT CODE] {result.returncode}")
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