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.
- {accli-2.0.13 → accli-2.0.14}/PKG-INFO +1 -1
- {accli-2.0.13 → accli-2.0.14}/accli/AcceleratorTaskDispatcher.py +1 -1
- {accli-2.0.13 → accli-2.0.14}/accli/_version.py +1 -1
- {accli-2.0.13 → accli-2.0.14}/accli/cli.py +149 -62
- {accli-2.0.13 → accli-2.0.14}/accli/gui.py +1 -0
- {accli-2.0.13 → accli-2.0.14}/accli/nfs_watcher.py +5 -2
- {accli-2.0.13 → accli-2.0.14}/accli/setup-nfs.ps1 +14 -2
- {accli-2.0.13 → accli-2.0.14}/accli.egg-info/PKG-INFO +1 -1
- {accli-2.0.13 → accli-2.0.14}/tests/test_mount_lifecycle.py +1 -0
- {accli-2.0.13 → accli-2.0.14}/LICENSE +0 -0
- {accli-2.0.13 → accli-2.0.14}/README.md +0 -0
- {accli-2.0.13 → accli-2.0.14}/accli/AcceleratorJobProjectService.py +0 -0
- {accli-2.0.13 → accli-2.0.14}/accli/AcceleratorTerminalCliProjectService.py +0 -0
- {accli-2.0.13 → accli-2.0.14}/accli/CsvRegionalTimeseriesValidator.py +0 -0
- {accli-2.0.13 → accli-2.0.14}/accli/__init__.py +0 -0
- {accli-2.0.13 → accli-2.0.14}/accli/__main__.py +0 -0
- {accli-2.0.13 → accli-2.0.14}/accli/cleanup-nfs.ps1 +0 -0
- {accli-2.0.13 → accli-2.0.14}/accli/common.py +0 -0
- {accli-2.0.13 → accli-2.0.14}/accli/mount_downloader.py +0 -0
- {accli-2.0.13 → accli-2.0.14}/accli/refresh_explorer.ps1 +0 -0
- {accli-2.0.13 → accli-2.0.14}/accli/token.py +0 -0
- {accli-2.0.13 → accli-2.0.14}/accli.egg-info/SOURCES.txt +0 -0
- {accli-2.0.13 → accli-2.0.14}/accli.egg-info/dependency_links.txt +0 -0
- {accli-2.0.13 → accli-2.0.14}/accli.egg-info/entry_points.txt +0 -0
- {accli-2.0.13 → accli-2.0.14}/accli.egg-info/requires.txt +0 -0
- {accli-2.0.13 → accli-2.0.14}/accli.egg-info/top_level.txt +0 -0
- {accli-2.0.13 → accli-2.0.14}/pyproject.toml +0 -0
- {accli-2.0.13 → accli-2.0.14}/setup.cfg +0 -0
|
@@ -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 =
|
|
199
|
+
free_node: bool = True
|
|
200
200
|
|
|
201
201
|
children: List['JobDispatchModel'] = []
|
|
202
202
|
callback: Optional['JobDispatchModel'] = None
|
|
@@ -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
|
|
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
|
|
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=
|
|
827
|
-
"-o", "timeout=
|
|
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) {
|
|
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
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
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
|
|
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
|
|
1299
|
-
print(f"[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
|
-
|
|
1530
|
-
|
|
1531
|
-
|
|
1532
|
-
|
|
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
|
-
|
|
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
|
|
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 =
|
|
1663
|
-
if
|
|
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
|
-
|
|
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():
|
|
@@ -17,7 +17,7 @@ WAIT_TIMEOUT = 0x00000102
|
|
|
17
17
|
ERROR_ALREADY_EXISTS = 183
|
|
18
18
|
EVENT_MODIFY_STATE = 0x0002
|
|
19
19
|
|
|
20
|
-
kernel32 = ctypes.
|
|
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
|
|
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
|
|
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
|
|
File without changes
|