accli 2.0.12__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.12 → accli-2.0.14}/PKG-INFO +1 -1
- {accli-2.0.12 → accli-2.0.14}/accli/AcceleratorTaskDispatcher.py +1 -1
- {accli-2.0.12 → accli-2.0.14}/accli/_version.py +1 -1
- {accli-2.0.12 → accli-2.0.14}/accli/cli.py +356 -94
- {accli-2.0.12 → accli-2.0.14}/accli/gui.py +33 -16
- {accli-2.0.12 → accli-2.0.14}/accli/nfs_watcher.py +5 -2
- {accli-2.0.12 → accli-2.0.14}/accli/setup-nfs.ps1 +14 -2
- {accli-2.0.12 → accli-2.0.14}/accli/token.py +7 -0
- {accli-2.0.12 → accli-2.0.14}/accli.egg-info/PKG-INFO +1 -1
- {accli-2.0.12 → accli-2.0.14}/accli.egg-info/SOURCES.txt +2 -1
- accli-2.0.14/tests/test_mount_lifecycle.py +139 -0
- {accli-2.0.12 → accli-2.0.14}/LICENSE +0 -0
- {accli-2.0.12 → accli-2.0.14}/README.md +0 -0
- {accli-2.0.12 → accli-2.0.14}/accli/AcceleratorJobProjectService.py +0 -0
- {accli-2.0.12 → accli-2.0.14}/accli/AcceleratorTerminalCliProjectService.py +0 -0
- {accli-2.0.12 → accli-2.0.14}/accli/CsvRegionalTimeseriesValidator.py +0 -0
- {accli-2.0.12 → accli-2.0.14}/accli/__init__.py +0 -0
- {accli-2.0.12 → accli-2.0.14}/accli/__main__.py +0 -0
- {accli-2.0.12 → accli-2.0.14}/accli/cleanup-nfs.ps1 +0 -0
- {accli-2.0.12 → accli-2.0.14}/accli/common.py +0 -0
- {accli-2.0.12 → accli-2.0.14}/accli/mount_downloader.py +0 -0
- {accli-2.0.12 → accli-2.0.14}/accli/refresh_explorer.ps1 +0 -0
- {accli-2.0.12 → accli-2.0.14}/accli.egg-info/dependency_links.txt +0 -0
- {accli-2.0.12 → accli-2.0.14}/accli.egg-info/entry_points.txt +0 -0
- {accli-2.0.12 → accli-2.0.14}/accli.egg-info/requires.txt +0 -0
- {accli-2.0.12 → accli-2.0.14}/accli.egg-info/top_level.txt +0 -0
- {accli-2.0.12 → accli-2.0.14}/pyproject.toml +0 -0
- {accli-2.0.12 → 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.")
|
|
@@ -672,6 +685,7 @@ def start_nfs_watcher(mount_point: str, interval: int):
|
|
|
672
685
|
proc = subprocess.Popen(
|
|
673
686
|
[executable, str(watcher_script), str(mount_point), str(interval)],
|
|
674
687
|
creationflags=creationflags,
|
|
688
|
+
stdin=subprocess.DEVNULL,
|
|
675
689
|
stdout=subprocess.DEVNULL,
|
|
676
690
|
stderr=subprocess.DEVNULL,
|
|
677
691
|
close_fds=True
|
|
@@ -722,6 +736,209 @@ def stop_nfs_watcher(mount_point: str):
|
|
|
722
736
|
pass
|
|
723
737
|
|
|
724
738
|
|
|
739
|
+
def cleanup_stuck_windows_mount_processes():
|
|
740
|
+
"""Terminate any orphaned mount.exe / umount.exe instances that block Windows NFS client."""
|
|
741
|
+
import subprocess
|
|
742
|
+
try:
|
|
743
|
+
subprocess.run(["taskkill", "/f", "/im", "mount.exe"], capture_output=True)
|
|
744
|
+
except Exception:
|
|
745
|
+
pass
|
|
746
|
+
try:
|
|
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
|
+
)
|
|
788
|
+
except Exception:
|
|
789
|
+
pass
|
|
790
|
+
|
|
791
|
+
|
|
792
|
+
def is_nfs_daemon_running() -> bool:
|
|
793
|
+
"""Check if hf-mount-nfs.exe is currently running."""
|
|
794
|
+
import subprocess
|
|
795
|
+
try:
|
|
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
|
|
798
|
+
except Exception:
|
|
799
|
+
return False
|
|
800
|
+
|
|
801
|
+
|
|
802
|
+
def is_nfs_kernel_driver_active() -> bool:
|
|
803
|
+
"""Check if the Windows Client for NFS kernel redirector driver (NfsRdr) is loaded and running."""
|
|
804
|
+
import subprocess
|
|
805
|
+
import shutil
|
|
806
|
+
if not shutil.which("mount.exe") and not os.path.isfile("C:\\Windows\\System32\\mount.exe"):
|
|
807
|
+
return False
|
|
808
|
+
try:
|
|
809
|
+
res = subprocess.run(["sc.exe", "query", "NfsRdr"], capture_output=True, text=True, errors="replace")
|
|
810
|
+
if res.stdout and "RUNNING" in res.stdout:
|
|
811
|
+
return True
|
|
812
|
+
# If stopped, try to start it
|
|
813
|
+
subprocess.run(["sc.exe", "start", "NfsRdr"], capture_output=True)
|
|
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
|
|
816
|
+
except Exception:
|
|
817
|
+
return False
|
|
818
|
+
|
|
819
|
+
|
|
820
|
+
def wait_for_nfs_portmapper(host: str, port: int, timeout: float) -> bool:
|
|
821
|
+
"""Poll until the NFS daemon's portmapper is accepting TCP connections and is stable."""
|
|
822
|
+
import socket
|
|
823
|
+
start = time.time()
|
|
824
|
+
consecutive_success = 0
|
|
825
|
+
while time.time() - start < timeout:
|
|
826
|
+
try:
|
|
827
|
+
with socket.create_connection((host, port), timeout=0.5):
|
|
828
|
+
consecutive_success += 1
|
|
829
|
+
if consecutive_success >= 2:
|
|
830
|
+
time.sleep(1.0)
|
|
831
|
+
return True
|
|
832
|
+
except (socket.error, OSError):
|
|
833
|
+
consecutive_success = 0
|
|
834
|
+
time.sleep(0.3)
|
|
835
|
+
return False
|
|
836
|
+
|
|
837
|
+
|
|
838
|
+
def ensure_nfs_daemon_stable(timeout: float) -> bool:
|
|
839
|
+
"""
|
|
840
|
+
Ensure the NFS daemon starts and survives any delayed UMNT packet flushed by Windows NFS client.
|
|
841
|
+
If the daemon exits due to UMNT, re-triggers the Task Scheduler task to start a fresh daemon.
|
|
842
|
+
"""
|
|
843
|
+
import subprocess
|
|
844
|
+
for attempt in range(3):
|
|
845
|
+
if not wait_for_nfs_portmapper("127.0.0.1", 111, timeout):
|
|
846
|
+
return False
|
|
847
|
+
|
|
848
|
+
# Windows NFS client kernel redirector flushes delayed UMNT within 7-8 seconds of portmapper binding
|
|
849
|
+
exited = False
|
|
850
|
+
for _ in range(16):
|
|
851
|
+
time.sleep(0.5)
|
|
852
|
+
if not is_nfs_daemon_running():
|
|
853
|
+
exited = True
|
|
854
|
+
break
|
|
855
|
+
|
|
856
|
+
if not exited:
|
|
857
|
+
# Daemon remained running and stable without being killed by delayed UMNT
|
|
858
|
+
return True
|
|
859
|
+
|
|
860
|
+
# Daemon consumed the queued UMNT packet and terminated cleanly. Re-trigger task!
|
|
861
|
+
time.sleep(1.0)
|
|
862
|
+
subprocess.run(["schtasks", "/run", "/tn", "accli-mount-nfs"], capture_output=True)
|
|
863
|
+
|
|
864
|
+
return wait_for_nfs_portmapper("127.0.0.1", 111, timeout)
|
|
865
|
+
|
|
866
|
+
|
|
867
|
+
def map_windows_network_drive(mount_point_abs, max_attempts: int) -> tuple[bool, str]:
|
|
868
|
+
"""
|
|
869
|
+
Attempt to map the NFS share to the requested drive letter using mount.exe.
|
|
870
|
+
Retries with cleanup if Windows NFS client returns a transient error.
|
|
871
|
+
"""
|
|
872
|
+
import subprocess
|
|
873
|
+
cleanup_stale_rdp_namespace_entries()
|
|
874
|
+
target_str = str(mount_point_abs).rstrip("\\/")
|
|
875
|
+
if re.match(r"^[a-zA-Z]$", target_str):
|
|
876
|
+
target_str += ":"
|
|
877
|
+
|
|
878
|
+
mount_cmd = [
|
|
879
|
+
"C:\\Windows\\System32\\mount.exe",
|
|
880
|
+
"-o", "mtype=soft",
|
|
881
|
+
"-o", "timeout=2",
|
|
882
|
+
"-o", "retry=2",
|
|
883
|
+
"-o", "rsize=32",
|
|
884
|
+
"-o", "wsize=32",
|
|
885
|
+
"-o", "anon",
|
|
886
|
+
"-o", "nolock",
|
|
887
|
+
"\\\\127.0.0.1\\!",
|
|
888
|
+
target_str
|
|
889
|
+
]
|
|
890
|
+
|
|
891
|
+
last_err = ""
|
|
892
|
+
drive_root = str(mount_point_abs).rstrip("\\/") + "\\"
|
|
893
|
+
for attempt in range(1, max_attempts + 1):
|
|
894
|
+
cleanup_stuck_windows_mount_processes()
|
|
895
|
+
|
|
896
|
+
# If daemon died before or between attempts, revive it
|
|
897
|
+
if not is_nfs_daemon_running():
|
|
898
|
+
subprocess.run(["schtasks", "/run", "/tn", "accli-mount-nfs"], capture_output=True)
|
|
899
|
+
wait_for_nfs_portmapper("127.0.0.1", 111, 10.0)
|
|
900
|
+
|
|
901
|
+
try:
|
|
902
|
+
res = subprocess.run(mount_cmd, capture_output=True, text=True, errors="replace", timeout=15)
|
|
903
|
+
if res.returncode == 0:
|
|
904
|
+
# Confirm drive is actually mounted and directory listing is populated and queryable
|
|
905
|
+
drive_ready = False
|
|
906
|
+
for _ in range(30):
|
|
907
|
+
try:
|
|
908
|
+
if os.path.exists(drive_root):
|
|
909
|
+
entries = os.listdir(drive_root)
|
|
910
|
+
if len(entries) > 0:
|
|
911
|
+
drive_ready = True
|
|
912
|
+
return True, (res.stdout or "").strip()
|
|
913
|
+
except OSError:
|
|
914
|
+
pass
|
|
915
|
+
time.sleep(0.5)
|
|
916
|
+
if not drive_ready:
|
|
917
|
+
last_err = f"mount.exe reported success but drive {drive_root} was empty or not accessible"
|
|
918
|
+
# Clean up the stale mount connection so next attempt can remount fresh
|
|
919
|
+
subprocess.run(["net", "use", target_str, "/delete", "/y"], capture_output=True)
|
|
920
|
+
else:
|
|
921
|
+
last_err = (res.stderr or res.stdout or "").strip()
|
|
922
|
+
if "85" in last_err or "already in use" in last_err.lower():
|
|
923
|
+
# Release stale network drive letter without unmounting the daemon UNC path
|
|
924
|
+
subprocess.run(["net", "use", target_str, "/delete", "/y"], capture_output=True)
|
|
925
|
+
time.sleep(1.0)
|
|
926
|
+
except subprocess.TimeoutExpired:
|
|
927
|
+
last_err = "mount.exe command timed out"
|
|
928
|
+
cleanup_stuck_windows_mount_processes()
|
|
929
|
+
except Exception as e:
|
|
930
|
+
last_err = str(e)
|
|
931
|
+
|
|
932
|
+
if attempt < max_attempts:
|
|
933
|
+
# Check if daemon needs recovery
|
|
934
|
+
if not is_nfs_daemon_running():
|
|
935
|
+
subprocess.run(["schtasks", "/run", "/tn", "accli-mount-nfs"], capture_output=True)
|
|
936
|
+
wait_for_nfs_portmapper("127.0.0.1", 111, 3.0)
|
|
937
|
+
time.sleep(1.0 + (attempt * 0.5))
|
|
938
|
+
|
|
939
|
+
return False, last_err
|
|
940
|
+
|
|
941
|
+
|
|
725
942
|
def enable_windows_nfs_features():
|
|
726
943
|
"""Automatically check and enable Windows Client for NFS features, registry key, and Task Scheduler gateway if needed."""
|
|
727
944
|
import subprocess
|
|
@@ -759,7 +976,17 @@ def enable_windows_nfs_features():
|
|
|
759
976
|
"$taskEnabled = $false\n"
|
|
760
977
|
"if (Get-Command Get-ScheduledTask -ErrorAction SilentlyContinue) {\n"
|
|
761
978
|
" $t = Get-ScheduledTask -TaskName 'accli-mount-nfs' -ErrorAction SilentlyContinue\n"
|
|
762
|
-
" 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"
|
|
763
990
|
"} else {\n"
|
|
764
991
|
" $query = schtasks /query /tn 'accli-mount-nfs' 2> $null\n"
|
|
765
992
|
" if ($LASTEXITCODE -eq 0) { $taskEnabled = $true }\n"
|
|
@@ -780,8 +1007,8 @@ def enable_windows_nfs_features():
|
|
|
780
1007
|
task_ok = False
|
|
781
1008
|
|
|
782
1009
|
try:
|
|
783
|
-
res = subprocess.run(check_cmd, capture_output=True, text=True, check=True)
|
|
784
|
-
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()
|
|
785
1012
|
for part in output.split(';'):
|
|
786
1013
|
if part.startswith("NFS:"):
|
|
787
1014
|
nfs_ok = (part.split(':')[1].lower() == 'true')
|
|
@@ -851,7 +1078,7 @@ def enable_windows_nfs_features():
|
|
|
851
1078
|
f"Start-Process powershell.exe -ArgumentList '-NoProfile -ExecutionPolicy Bypass -File \"{temp_file_path}\" {params_str}' -Verb RunAs -Wait"
|
|
852
1079
|
]
|
|
853
1080
|
|
|
854
|
-
result = subprocess.run(elevate_cmd, capture_output=True, text=True)
|
|
1081
|
+
result = subprocess.run(elevate_cmd, capture_output=True, text=True, errors="replace")
|
|
855
1082
|
if result.returncode != 0:
|
|
856
1083
|
print("[bold red][ERROR] Administrative configuration failed or UAC prompt was denied.[/bold red]")
|
|
857
1084
|
if result.stderr:
|
|
@@ -860,13 +1087,16 @@ def enable_windows_nfs_features():
|
|
|
860
1087
|
|
|
861
1088
|
print("[bold green][OK] Elevated administrative setup completed successfully![/bold green]")
|
|
862
1089
|
|
|
863
|
-
#
|
|
864
|
-
if
|
|
865
|
-
print("")
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
1090
|
+
# Verify if kernel driver is loaded; if active, restart is NOT required and we continue mounting
|
|
1091
|
+
if is_nfs_kernel_driver_active():
|
|
1092
|
+
print("[bold green][OK] Windows NFS kernel redirector is active. Continuing with mount...[/bold green]")
|
|
1093
|
+
return
|
|
1094
|
+
|
|
1095
|
+
print("")
|
|
1096
|
+
print("[bold yellow][WARNING] IMPORTANT: A Windows system restart is required for the new NFS Client features and registry policies to take effect.[/bold yellow]")
|
|
1097
|
+
print("[bold yellow]Please restart your computer, then run 'accli mount start' again to mount your drive passwordlessly![/bold yellow]")
|
|
1098
|
+
print("")
|
|
1099
|
+
raise typer.Exit(0)
|
|
870
1100
|
|
|
871
1101
|
except Exception as e:
|
|
872
1102
|
if isinstance(e, typer.Exit):
|
|
@@ -909,7 +1139,12 @@ def mount_start(
|
|
|
909
1139
|
try:
|
|
910
1140
|
token = get_token()
|
|
911
1141
|
server_url = get_server_url()
|
|
1142
|
+
if not token:
|
|
1143
|
+
print("[bold red]ERROR: No token found. Please run 'accli login' first.[/bold red]")
|
|
1144
|
+
raise typer.Exit(1)
|
|
912
1145
|
except Exception as e:
|
|
1146
|
+
if isinstance(e, typer.Exit):
|
|
1147
|
+
raise e
|
|
913
1148
|
print(f"[bold red]ERROR: Could not retrieve login details. Please run 'accli login' first. Details: {e}[/bold red]")
|
|
914
1149
|
raise typer.Exit(1)
|
|
915
1150
|
|
|
@@ -934,26 +1169,19 @@ def mount_start(
|
|
|
934
1169
|
|
|
935
1170
|
# Standardize mount_point and handle defaults / auto-selection
|
|
936
1171
|
if sys_name == "Windows":
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
pass
|
|
951
|
-
print(f"[bold red]ERROR: A mount daemon is already running{active_msg}.[/bold red]")
|
|
952
|
-
print("[yellow]Please run 'accli mount stop' to unmount before starting a new mount.[/yellow]")
|
|
953
|
-
raise typer.Exit(1)
|
|
954
|
-
except Exception as e:
|
|
955
|
-
if isinstance(e, typer.Exit):
|
|
956
|
-
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)
|
|
957
1185
|
|
|
958
1186
|
if mount_point is None:
|
|
959
1187
|
# Auto-detect available drive letter starting with W
|
|
@@ -965,10 +1193,10 @@ def mount_start(
|
|
|
965
1193
|
print(f"[bold red]ERROR: {e}[/bold red]")
|
|
966
1194
|
raise typer.Exit(1)
|
|
967
1195
|
else:
|
|
968
|
-
mount_point_str = str(mount_point)
|
|
969
|
-
if re.match(r"^[a-zA-Z]
|
|
1196
|
+
mount_point_str = str(mount_point).strip()
|
|
1197
|
+
if re.match(r"^[a-zA-Z]:?[\\/]*$", mount_point_str):
|
|
970
1198
|
drive_letter = mount_point_str[0].upper()
|
|
971
|
-
if
|
|
1199
|
+
if is_windows_drive_mapped(drive_letter):
|
|
972
1200
|
print(f"[yellow]Warning: Drive '{drive_letter}:' is already in use.[/yellow]")
|
|
973
1201
|
try:
|
|
974
1202
|
fallback_drive = find_available_windows_drive("W")
|
|
@@ -997,6 +1225,12 @@ def mount_start(
|
|
|
997
1225
|
env["ACC_ENDPOINT"] = server_url
|
|
998
1226
|
env["ACC_CAS_ENDPOINT"] = f"{server_url.rstrip('/')}/api/xet-cas"
|
|
999
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)
|
|
1000
1234
|
|
|
1001
1235
|
if sys_name == "Windows":
|
|
1002
1236
|
# Enable Windows NFS Client features if needed
|
|
@@ -1018,6 +1252,7 @@ def mount_start(
|
|
|
1018
1252
|
str(hf_mount_bin),
|
|
1019
1253
|
"--token-file", db_path,
|
|
1020
1254
|
"--hub-endpoint", server_url,
|
|
1255
|
+
"--cache-dir", str(cache_base / "hf-mount"),
|
|
1021
1256
|
mode,
|
|
1022
1257
|
project_slug,
|
|
1023
1258
|
str(mount_point_abs)
|
|
@@ -1049,7 +1284,7 @@ def mount_start(
|
|
|
1049
1284
|
has_task = False
|
|
1050
1285
|
if not is_admin:
|
|
1051
1286
|
try:
|
|
1052
|
-
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")
|
|
1053
1288
|
if task_check.returncode == 0:
|
|
1054
1289
|
has_task = True
|
|
1055
1290
|
except Exception:
|
|
@@ -1103,39 +1338,30 @@ def mount_start(
|
|
|
1103
1338
|
config_file = config_dir / "mount_config.json"
|
|
1104
1339
|
config_file.write_text(json.dumps(config_data, indent=2), encoding="utf-8")
|
|
1105
1340
|
|
|
1106
|
-
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")
|
|
1107
1342
|
if run_res.returncode == 0:
|
|
1108
1343
|
print("[cyan]Waiting for elevated NFS daemon to initialize...[/cyan]")
|
|
1109
|
-
|
|
1344
|
+
if not ensure_nfs_daemon_stable(15.0):
|
|
1345
|
+
print("[bold red][ERROR] Timed out waiting for NFS daemon to bind to 127.0.0.1:111.[/bold red]")
|
|
1346
|
+
raise typer.Exit(1)
|
|
1110
1347
|
|
|
1111
1348
|
# Map the network drive in the CURRENT user session (so it is visible in Explorer)
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
"-o", "mtype=hard",
|
|
1115
|
-
"-o", "timeout=10",
|
|
1116
|
-
"-o", "rsize=32",
|
|
1117
|
-
"-o", "wsize=32",
|
|
1118
|
-
"-o", "anon",
|
|
1119
|
-
"-o", "nolock",
|
|
1120
|
-
"\\\\127.0.0.1\\!",
|
|
1121
|
-
str(mount_point_abs)
|
|
1122
|
-
]
|
|
1123
|
-
mount_res = subprocess.run(mount_cmd, capture_output=True, text=True)
|
|
1124
|
-
if mount_res.returncode == 0:
|
|
1349
|
+
success, err_msg = map_windows_network_drive(mount_point_abs, 5)
|
|
1350
|
+
if success:
|
|
1125
1351
|
start_nfs_watcher(str(mount_point_abs), interval=30)
|
|
1126
1352
|
print(f"[bold green][OK] NFS mount successfully mapped at [white]{mount_point_abs}[/white]![/bold green]")
|
|
1127
1353
|
print("[cyan]Use 'umount' or 'accli mount stop' to unmount the drive.[/cyan]")
|
|
1128
1354
|
return
|
|
1129
1355
|
else:
|
|
1130
1356
|
print(f"[bold red][ERROR] NFS daemon started, but failed to map drive {mount_point_abs} in user session.[/bold red]")
|
|
1131
|
-
if
|
|
1132
|
-
print(f"[red]{
|
|
1357
|
+
if err_msg:
|
|
1358
|
+
print(f"[red]{err_msg}[/red]")
|
|
1133
1359
|
print("[yellow]Tip: Make sure Windows NFS client feature was fully enabled and the PC was restarted.[/yellow]")
|
|
1134
1360
|
raise typer.Exit(1)
|
|
1135
1361
|
else:
|
|
1136
1362
|
print("[bold red][ERROR] Failed to execute Scheduled Task 'accli-mount-nfs'.[/bold red]")
|
|
1137
|
-
if
|
|
1138
|
-
print(f"[red]{
|
|
1363
|
+
if run_res.stderr:
|
|
1364
|
+
print(f"[red]{run_res.stderr.strip()}[/red]")
|
|
1139
1365
|
print("[yellow]Falling back to UAC elevation...[/yellow]")
|
|
1140
1366
|
has_task = False
|
|
1141
1367
|
except Exception as e:
|
|
@@ -1199,7 +1425,7 @@ def mount_start(
|
|
|
1199
1425
|
ps_command
|
|
1200
1426
|
]
|
|
1201
1427
|
|
|
1202
|
-
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")
|
|
1203
1429
|
if elevate_res.returncode != 0:
|
|
1204
1430
|
print("[bold red][ERROR] Administrative elevation failed or was denied.[/bold red]")
|
|
1205
1431
|
print("[yellow]Tip: Please request your IT Administrator to run 'setup-nfs.ps1' to configure the passwordless Task Scheduler gateway.[/yellow]")
|
|
@@ -1208,29 +1434,20 @@ def mount_start(
|
|
|
1208
1434
|
raise typer.Exit(1)
|
|
1209
1435
|
|
|
1210
1436
|
print("[cyan]Waiting for elevated NFS daemon to initialize...[/cyan]")
|
|
1211
|
-
|
|
1437
|
+
if not wait_for_nfs_portmapper("127.0.0.1", 111, 15.0):
|
|
1438
|
+
print("[bold red][ERROR] Timed out waiting for NFS daemon to bind to 127.0.0.1:111.[/bold red]")
|
|
1439
|
+
raise typer.Exit(1)
|
|
1212
1440
|
|
|
1213
1441
|
# Map the network drive in the CURRENT user session (so it is visible in Explorer)
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
"-o", "mtype=hard",
|
|
1217
|
-
"-o", "timeout=10",
|
|
1218
|
-
"-o", "rsize=32",
|
|
1219
|
-
"-o", "wsize=32",
|
|
1220
|
-
"-o", "anon",
|
|
1221
|
-
"-o", "nolock",
|
|
1222
|
-
"\\\\127.0.0.1\\!",
|
|
1223
|
-
str(mount_point_abs)
|
|
1224
|
-
]
|
|
1225
|
-
mount_res = subprocess.run(mount_cmd, capture_output=True, text=True)
|
|
1226
|
-
if mount_res.returncode == 0:
|
|
1442
|
+
success, err_msg = map_windows_network_drive(mount_point_abs, 5)
|
|
1443
|
+
if success:
|
|
1227
1444
|
start_nfs_watcher(str(mount_point_abs), interval=30)
|
|
1228
1445
|
print(f"[bold green][OK] NFS mount successfully mapped at [white]{mount_point_abs}[/white]![/bold green]")
|
|
1229
1446
|
print("[cyan]Use 'umount' or 'accli mount stop' to unmount the drive.[/cyan]")
|
|
1230
1447
|
else:
|
|
1231
1448
|
print(f"[bold red][ERROR] NFS daemon spawned elevated, but failed to map drive {mount_point_abs} in user session.[/bold red]")
|
|
1232
|
-
if
|
|
1233
|
-
print(f"[red]{
|
|
1449
|
+
if err_msg:
|
|
1450
|
+
print(f"[red]{err_msg}[/red]")
|
|
1234
1451
|
raise typer.Exit(1)
|
|
1235
1452
|
except Exception as e:
|
|
1236
1453
|
if isinstance(e, typer.Exit):
|
|
@@ -1268,7 +1485,7 @@ def mount_start(
|
|
|
1268
1485
|
print(f"[bold cyan]Starting mount for project '[white]{project_slug}[/white]' at '[white]{mount_point_abs}[/white]'...[/bold cyan]")
|
|
1269
1486
|
import subprocess
|
|
1270
1487
|
try:
|
|
1271
|
-
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")
|
|
1272
1489
|
if result.returncode == 0:
|
|
1273
1490
|
print(f"[bold green][OK] Mount started successfully![/bold green]")
|
|
1274
1491
|
if result.stdout:
|
|
@@ -1349,10 +1566,17 @@ def mount_stop(
|
|
|
1349
1566
|
mount_point_abs = Path.home() / "accelerator" / "mnt" / project_slug
|
|
1350
1567
|
print(f"[cyan]No mount point specified. Selected default directory: [bold white]{mount_point_abs}[/bold white][/cyan]")
|
|
1351
1568
|
else:
|
|
1352
|
-
|
|
1569
|
+
mount_point_str = str(mount_point).strip()
|
|
1570
|
+
if sys_name == "Windows" and re.match(r"^[a-zA-Z]:?[\\/]*$", mount_point_str):
|
|
1571
|
+
mount_point_abs = Path(f"{mount_point_str[0].upper()}:")
|
|
1572
|
+
else:
|
|
1573
|
+
mount_point_abs = mount_point.resolve()
|
|
1353
1574
|
|
|
1354
1575
|
if sys_name == "Windows":
|
|
1355
|
-
|
|
1576
|
+
target_str = str(mount_point_abs).rstrip("\\/")
|
|
1577
|
+
if re.match(r"^[a-zA-Z]$", target_str):
|
|
1578
|
+
target_str += ":"
|
|
1579
|
+
print(f"[bold cyan]Stopping mount at '[white]{target_str}[/white]' on Windows...[/bold cyan]")
|
|
1356
1580
|
import subprocess
|
|
1357
1581
|
|
|
1358
1582
|
# Determine if current user is admin
|
|
@@ -1363,19 +1587,41 @@ def mount_stop(
|
|
|
1363
1587
|
is_admin = False
|
|
1364
1588
|
|
|
1365
1589
|
# 0. Stop background NFS watcher
|
|
1366
|
-
stop_nfs_watcher(
|
|
1590
|
+
stop_nfs_watcher(target_str)
|
|
1591
|
+
|
|
1592
|
+
# 1. Unmap the network drive in the user session using net use
|
|
1593
|
+
# (Avoids sending RPC UMNT packet via umount.exe which causes kernel NFS redirector to queue stale UMNTs)
|
|
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()
|
|
1367
1617
|
|
|
1368
|
-
|
|
1369
|
-
res = subprocess.run(["C:\\Windows\\System32\\umount.exe", "-f", str(mount_point_abs)], capture_output=True, text=True)
|
|
1370
|
-
if res.returncode != 0:
|
|
1371
|
-
# Fallback to force-releasing using net use
|
|
1372
|
-
subprocess.run(["net", "use", str(mount_point_abs), "/delete", "/y"], capture_output=True)
|
|
1618
|
+
cleanup_stuck_windows_mount_processes()
|
|
1373
1619
|
|
|
1374
1620
|
# 2. Terminate the background daemon
|
|
1375
1621
|
has_umount_task = False
|
|
1376
1622
|
if not is_admin:
|
|
1377
1623
|
try:
|
|
1378
|
-
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")
|
|
1379
1625
|
if task_check.returncode == 0:
|
|
1380
1626
|
has_umount_task = True
|
|
1381
1627
|
except Exception:
|
|
@@ -1387,9 +1633,20 @@ def mount_stop(
|
|
|
1387
1633
|
# Poll for up to 5 seconds to ensure hf-mount-nfs.exe has fully terminated
|
|
1388
1634
|
for _ in range(10):
|
|
1389
1635
|
time.sleep(0.5)
|
|
1390
|
-
|
|
1391
|
-
|
|
1636
|
+
if not is_nfs_daemon_running():
|
|
1637
|
+
break
|
|
1638
|
+
cleanup_stuck_windows_mount_processes()
|
|
1639
|
+
for _ in range(10):
|
|
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:
|
|
1392
1642
|
break
|
|
1643
|
+
time.sleep(0.5)
|
|
1644
|
+
|
|
1645
|
+
# Wait until drive letter is released from Windows namespace
|
|
1646
|
+
for _ in range(10):
|
|
1647
|
+
if not is_windows_drive_mapped(target_str):
|
|
1648
|
+
break
|
|
1649
|
+
time.sleep(0.3)
|
|
1393
1650
|
else:
|
|
1394
1651
|
# If admin or task scheduler gateway is not registered, stop/kill directly
|
|
1395
1652
|
if is_admin:
|
|
@@ -1423,11 +1680,12 @@ def mount_stop(
|
|
|
1423
1680
|
ps_command
|
|
1424
1681
|
], capture_output=True)
|
|
1425
1682
|
|
|
1426
|
-
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))):
|
|
1427
1684
|
print("[bold green][OK] Mount stopped successfully.[/bold green]")
|
|
1685
|
+
print("[cyan]Note: If mounting again, please allow a few seconds for the Windows NFS network cache to clear.[/cyan]")
|
|
1428
1686
|
else:
|
|
1429
1687
|
print("[yellow]Attempted to unmount. Please verify drive status manually using 'umount'.[/yellow]")
|
|
1430
|
-
if res.stderr:
|
|
1688
|
+
if res is not None and res.stderr:
|
|
1431
1689
|
print(f"[red]{res.stderr.strip()}[/red]")
|
|
1432
1690
|
return
|
|
1433
1691
|
|
|
@@ -1454,7 +1712,7 @@ def mount_stop(
|
|
|
1454
1712
|
|
|
1455
1713
|
import subprocess
|
|
1456
1714
|
try:
|
|
1457
|
-
result = subprocess.run(args, capture_output=True, text=True)
|
|
1715
|
+
result = subprocess.run(args, capture_output=True, text=True, errors="replace")
|
|
1458
1716
|
if result.returncode == 0:
|
|
1459
1717
|
print(f"[bold green][OK] Mount stopped successfully.[/bold green]")
|
|
1460
1718
|
else:
|
|
@@ -1484,13 +1742,17 @@ def mount_status(
|
|
|
1484
1742
|
print("[bold cyan]Active NFS mounts on Windows:[/bold cyan]")
|
|
1485
1743
|
try:
|
|
1486
1744
|
# Windows 'mount' command lists active NFS mounts
|
|
1487
|
-
result =
|
|
1488
|
-
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():
|
|
1489
1752
|
print(result.stdout)
|
|
1490
1753
|
else:
|
|
1491
1754
|
# Check tasklist for active daemon process
|
|
1492
|
-
|
|
1493
|
-
if "hf-mount-nfs.exe" in task_res.stdout:
|
|
1755
|
+
if is_nfs_daemon_running():
|
|
1494
1756
|
print("[cyan]hf-mount-nfs.exe process is running in the background.[/cyan]")
|
|
1495
1757
|
else:
|
|
1496
1758
|
print("[cyan]No running mounts found.[/cyan]")
|
|
@@ -1522,8 +1784,8 @@ def mount_status(
|
|
|
1522
1784
|
|
|
1523
1785
|
# 1. Query hf-mount status directly
|
|
1524
1786
|
try:
|
|
1525
|
-
result = subprocess.run(args, capture_output=True, text=True)
|
|
1526
|
-
if result.returncode == 0:
|
|
1787
|
+
result = subprocess.run(args, capture_output=True, text=True, errors="replace")
|
|
1788
|
+
if result.returncode == 0 and result.stdout:
|
|
1527
1789
|
stdout = result.stdout.strip()
|
|
1528
1790
|
if stdout:
|
|
1529
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
|
)
|
|
@@ -449,6 +450,9 @@ class AccliGuiApp(tk.Tk):
|
|
|
449
450
|
|
|
450
451
|
mount_btn = ttk.Button(btn_frame, text="Start Mount", command=self.action_mount_start)
|
|
451
452
|
mount_btn.pack(side=tk.LEFT, padx=5)
|
|
453
|
+
|
|
454
|
+
stop_form_btn = ttk.Button(btn_frame, text="Stop Mount", style="Stop.TButton", command=self.action_mount_stop)
|
|
455
|
+
stop_form_btn.pack(side=tk.LEFT, padx=5)
|
|
452
456
|
|
|
453
457
|
# Right Panel (List of mounts)
|
|
454
458
|
list_frame = ttk.LabelFrame(mounts_tab, text="Active Mounts", padding=10)
|
|
@@ -528,20 +532,33 @@ class AccliGuiApp(tk.Tk):
|
|
|
528
532
|
self.run_cli_async(args, on_done=on_done, elevate=True)
|
|
529
533
|
|
|
530
534
|
def action_mount_stop(self):
|
|
531
|
-
selected
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
535
|
+
# 1. Check if user selected an item from the active mounts list
|
|
536
|
+
mount_point = ""
|
|
537
|
+
selected_indices = self.mounts_list.curselection()
|
|
538
|
+
if selected_indices:
|
|
539
|
+
selected = self.mounts_list.get(selected_indices[0])
|
|
540
|
+
parts = selected.split()
|
|
541
|
+
if parts:
|
|
542
|
+
mount_point = parts[0]
|
|
543
|
+
elif self.mounts_list.size() > 0:
|
|
544
|
+
selected = self.mounts_list.get(tk.ACTIVE)
|
|
545
|
+
if selected:
|
|
546
|
+
parts = selected.split()
|
|
547
|
+
if parts:
|
|
548
|
+
mount_point = parts[0]
|
|
549
|
+
|
|
550
|
+
# 2. If nothing selected or list is empty, fallback to the Mount Point / Drive Letter input field
|
|
551
|
+
if not mount_point:
|
|
552
|
+
entered_point = self.mount_point_val.get().strip()
|
|
553
|
+
if entered_point:
|
|
554
|
+
mount_point = entered_point
|
|
555
|
+
|
|
556
|
+
args = ["mount", "stop"]
|
|
557
|
+
if mount_point:
|
|
558
|
+
args.append(mount_point)
|
|
559
|
+
target_desc = mount_point
|
|
560
|
+
else:
|
|
561
|
+
target_desc = "active mount"
|
|
545
562
|
|
|
546
563
|
self.status_bar_val.set("Stopping mount...")
|
|
547
564
|
self.notebook.select(self.log_tab_idx)
|
|
@@ -550,10 +567,10 @@ class AccliGuiApp(tk.Tk):
|
|
|
550
567
|
self.refresh_mounts_list()
|
|
551
568
|
if code == 0:
|
|
552
569
|
self.status_bar_val.set("Mount Stopped")
|
|
553
|
-
messagebox.showinfo("Success", f"Successfully stopped mount
|
|
570
|
+
messagebox.showinfo("Success", f"Successfully stopped mount ({target_desc}).")
|
|
554
571
|
else:
|
|
555
572
|
self.status_bar_val.set("Stop Failed")
|
|
556
|
-
messagebox.showerror("Error", f"Failed to stop mount
|
|
573
|
+
messagebox.showerror("Error", f"Failed to stop mount ({target_desc}). See logs for details.")
|
|
557
574
|
|
|
558
575
|
self.run_cli_async(args, on_done=on_done, elevate=True)
|
|
559
576
|
|
|
@@ -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
|
|
@@ -139,6 +139,13 @@ def exchange_refresh_token(project_slug: str) -> tuple[str, str, int]:
|
|
|
139
139
|
_cached_expires_at = expires_at
|
|
140
140
|
|
|
141
141
|
return cas_token, access_token, expires_at
|
|
142
|
+
except requests.exceptions.HTTPError as e:
|
|
143
|
+
if e.response is not None and e.response.status_code in (400, 401, 403):
|
|
144
|
+
print("[bold red]ERROR: Your session has expired or the token is invalid.[/bold red]")
|
|
145
|
+
print("[yellow]Please run 'accli login' to authenticate again.[/yellow]")
|
|
146
|
+
raise typer.Exit(1)
|
|
147
|
+
print(f"[bold red]ERROR: Failed to authenticate/exchange refresh token: {e}[/bold red]")
|
|
148
|
+
raise typer.Exit(1)
|
|
142
149
|
except Exception as e:
|
|
143
150
|
print(f"[bold red]ERROR: Failed to authenticate/exchange refresh token: {e}[/bold red]")
|
|
144
151
|
raise typer.Exit(1)
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Test for mounting lifecycle:
|
|
3
|
+
1. Ensure clean initial state (mount stop)
|
|
4
|
+
2. accli mount start <project_slug>
|
|
5
|
+
3. Capture directory structure (ensure not empty) & read any file
|
|
6
|
+
4. accli mount stop
|
|
7
|
+
5. accli mount start <project_slug> again
|
|
8
|
+
6. Capture directory structure (must not be empty) & read any file
|
|
9
|
+
"""
|
|
10
|
+
import os
|
|
11
|
+
import sys
|
|
12
|
+
import time
|
|
13
|
+
import platform
|
|
14
|
+
import subprocess
|
|
15
|
+
import unittest
|
|
16
|
+
from pathlib import Path
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class TestMountLifecycle(unittest.TestCase):
|
|
20
|
+
@classmethod
|
|
21
|
+
def setUpClass(cls):
|
|
22
|
+
cls.project_slug = os.environ.get("ACCLI_TEST_PROJECT", "test")
|
|
23
|
+
cls.mount_drive = os.environ.get("ACCLI_TEST_DRIVE", "W:")
|
|
24
|
+
cls.mount_arg = cls.mount_drive.rstrip("\\/")
|
|
25
|
+
if platform.system() == "Windows" and len(cls.mount_arg) == 1 and cls.mount_arg.isalpha():
|
|
26
|
+
cls.mount_arg += ":"
|
|
27
|
+
cls.mount_path = Path(f"{cls.mount_arg}\\") if platform.system() == "Windows" else Path.home() / "accelerator" / "mnt" / cls.project_slug
|
|
28
|
+
|
|
29
|
+
def run_cli(self, args: list, timeout: int):
|
|
30
|
+
cmd = [sys.executable, "-m", "accli"] + args
|
|
31
|
+
print(f"\n[TEST EXEC] {' '.join(cmd)}")
|
|
32
|
+
result = subprocess.run(
|
|
33
|
+
cmd,
|
|
34
|
+
stdin=subprocess.DEVNULL,
|
|
35
|
+
capture_output=True,
|
|
36
|
+
text=True,
|
|
37
|
+
errors="replace",
|
|
38
|
+
timeout=timeout
|
|
39
|
+
)
|
|
40
|
+
print(f"[EXIT CODE] {result.returncode}")
|
|
41
|
+
if result.stdout:
|
|
42
|
+
print(f"[STDOUT]\n{result.stdout.strip()}")
|
|
43
|
+
if result.stderr:
|
|
44
|
+
print(f"[STDERR]\n{result.stderr.strip()}")
|
|
45
|
+
return result
|
|
46
|
+
|
|
47
|
+
def get_structure(self, target_path: Path):
|
|
48
|
+
"""List files and folders at the mounted location and ensure it is not empty."""
|
|
49
|
+
entries = []
|
|
50
|
+
for _ in range(10):
|
|
51
|
+
try:
|
|
52
|
+
if target_path.exists():
|
|
53
|
+
entries = [p.name for p in target_path.iterdir()]
|
|
54
|
+
if entries:
|
|
55
|
+
break
|
|
56
|
+
except OSError:
|
|
57
|
+
pass
|
|
58
|
+
time.sleep(0.5)
|
|
59
|
+
|
|
60
|
+
self.assertTrue(
|
|
61
|
+
target_path.exists(),
|
|
62
|
+
f"Mount path '{target_path}' does not exist or drive is not mapped."
|
|
63
|
+
)
|
|
64
|
+
print(f"[MOUNT STRUCTURE at {target_path}]: {entries}")
|
|
65
|
+
return entries
|
|
66
|
+
|
|
67
|
+
def read_any_file(self, target_path: Path):
|
|
68
|
+
"""Locate any readable file under the mounted project path and read its content."""
|
|
69
|
+
search_dirs = [target_path / self.project_slug, target_path]
|
|
70
|
+
for sdir in search_dirs:
|
|
71
|
+
if not sdir.exists():
|
|
72
|
+
continue
|
|
73
|
+
for root, dirs, files in os.walk(sdir):
|
|
74
|
+
for file_name in files:
|
|
75
|
+
candidate = Path(root) / file_name
|
|
76
|
+
try:
|
|
77
|
+
print(f"[TEST READ FILE] Attempting to read: {candidate}")
|
|
78
|
+
with open(candidate, "rb") as fp:
|
|
79
|
+
data = fp.read(1024)
|
|
80
|
+
print(f"[TEST READ FILE] Successfully read {len(data)} bytes from '{candidate.name}'.")
|
|
81
|
+
return candidate, len(data)
|
|
82
|
+
except Exception as e:
|
|
83
|
+
print(f"[TEST READ FILE] Skipping unreadable candidate {candidate}: {e}")
|
|
84
|
+
|
|
85
|
+
self.fail(f"Could not read any file under mounted path '{target_path}'.")
|
|
86
|
+
|
|
87
|
+
def tearDown(self):
|
|
88
|
+
try:
|
|
89
|
+
self.run_cli(["mount", "stop", self.mount_arg], 30)
|
|
90
|
+
except Exception:
|
|
91
|
+
pass
|
|
92
|
+
|
|
93
|
+
def test_mount_start_stop_start_lifecycle(self):
|
|
94
|
+
# Step 0: Ensure clean initial state
|
|
95
|
+
print("\n--- Step 0: Ensure clean initial state ---")
|
|
96
|
+
self.run_cli(["mount", "stop", self.mount_arg], 30)
|
|
97
|
+
time.sleep(2)
|
|
98
|
+
|
|
99
|
+
# Step 1: First mount start
|
|
100
|
+
print(f"\n--- Step 1: Starting mount for '{self.project_slug}' at '{self.mount_arg}' ---")
|
|
101
|
+
start1 = self.run_cli(["mount", "start", self.project_slug, self.mount_arg], 60)
|
|
102
|
+
self.assertEqual(start1.returncode, 0, f"First mount start failed: {start1.stderr or start1.stdout}")
|
|
103
|
+
|
|
104
|
+
# Step 2: Check structure is not empty and try to read any file
|
|
105
|
+
print("\n--- Step 2: Checking structure & reading file after 1st mount ---")
|
|
106
|
+
structure1 = self.get_structure(self.mount_path)
|
|
107
|
+
self.assertGreater(
|
|
108
|
+
len(structure1), 0,
|
|
109
|
+
f"Expected structure to be not empty after 1st mount, got: {structure1}"
|
|
110
|
+
)
|
|
111
|
+
file1, bytes_read1 = self.read_any_file(self.mount_path)
|
|
112
|
+
self.assertIsNotNone(file1)
|
|
113
|
+
|
|
114
|
+
# Step 3: Mount stop (clean)
|
|
115
|
+
print(f"\n--- Step 3: Stopping mount at '{self.mount_arg}' ---")
|
|
116
|
+
stop = self.run_cli(["mount", "stop", self.mount_arg], 30)
|
|
117
|
+
self.assertEqual(stop.returncode, 0, f"Mount stop failed: {stop.stderr or stop.stdout}")
|
|
118
|
+
time.sleep(3)
|
|
119
|
+
|
|
120
|
+
# Step 4: Second mount start
|
|
121
|
+
print(f"\n--- Step 4: Starting mount for '{self.project_slug}' at '{self.mount_arg}' (2nd time) ---")
|
|
122
|
+
start2 = self.run_cli(["mount", "start", self.project_slug, self.mount_arg], 120)
|
|
123
|
+
self.assertEqual(start2.returncode, 0, f"Second mount start failed: {start2.stderr or start2.stdout}")
|
|
124
|
+
|
|
125
|
+
# Step 5: Check structure is not empty and try to read any file after clean & remount
|
|
126
|
+
print("\n--- Step 5: Checking structure & reading file after 2nd mount ---")
|
|
127
|
+
structure2 = self.get_structure(self.mount_path)
|
|
128
|
+
self.assertGreater(
|
|
129
|
+
len(structure2), 0,
|
|
130
|
+
f"Expected structure to be not empty after 2nd mount, got: {structure2}"
|
|
131
|
+
)
|
|
132
|
+
file2, bytes_read2 = self.read_any_file(self.mount_path)
|
|
133
|
+
self.assertIsNotNone(file2)
|
|
134
|
+
|
|
135
|
+
print("\n[SUCCESS] Mount start -> read -> stop -> start -> read lifecycle test completed successfully!")
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
if __name__ == "__main__":
|
|
139
|
+
unittest.main()
|
|
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
|