accli 2.0.3__tar.gz → 2.0.4__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.3 → accli-2.0.4}/PKG-INFO +1 -1
- {accli-2.0.3 → accli-2.0.4}/accli/_version.py +1 -1
- {accli-2.0.3 → accli-2.0.4}/accli/cli.py +76 -0
- accli-2.0.4/accli/refresh_explorer.ps1 +12 -0
- accli-2.0.4/accli/windows_refresher.py +159 -0
- {accli-2.0.3 → accli-2.0.4}/accli.egg-info/PKG-INFO +1 -1
- {accli-2.0.3 → accli-2.0.4}/accli.egg-info/SOURCES.txt +2 -0
- {accli-2.0.3 → accli-2.0.4}/LICENSE +0 -0
- {accli-2.0.3 → accli-2.0.4}/README.md +0 -0
- {accli-2.0.3 → accli-2.0.4}/accli/AcceleratorJobProjectService.py +0 -0
- {accli-2.0.3 → accli-2.0.4}/accli/AcceleratorTaskDispatcher.py +0 -0
- {accli-2.0.3 → accli-2.0.4}/accli/AcceleratorTerminalCliProjectService.py +0 -0
- {accli-2.0.3 → accli-2.0.4}/accli/CsvRegionalTimeseriesValidator.py +0 -0
- {accli-2.0.3 → accli-2.0.4}/accli/__init__.py +0 -0
- {accli-2.0.3 → accli-2.0.4}/accli/__main__.py +0 -0
- {accli-2.0.3 → accli-2.0.4}/accli/cleanup-nfs.ps1 +0 -0
- {accli-2.0.3 → accli-2.0.4}/accli/common.py +0 -0
- {accli-2.0.3 → accli-2.0.4}/accli/gui.py +0 -0
- {accli-2.0.3 → accli-2.0.4}/accli/mount_downloader.py +0 -0
- {accli-2.0.3 → accli-2.0.4}/accli/setup-nfs.ps1 +0 -0
- {accli-2.0.3 → accli-2.0.4}/accli/token.py +0 -0
- {accli-2.0.3 → accli-2.0.4}/accli.egg-info/dependency_links.txt +0 -0
- {accli-2.0.3 → accli-2.0.4}/accli.egg-info/entry_points.txt +0 -0
- {accli-2.0.3 → accli-2.0.4}/accli.egg-info/requires.txt +0 -0
- {accli-2.0.3 → accli-2.0.4}/accli.egg-info/top_level.txt +0 -0
- {accli-2.0.3 → accli-2.0.4}/pyproject.toml +0 -0
- {accli-2.0.3 → accli-2.0.4}/setup.cfg +0 -0
|
@@ -604,6 +604,76 @@ def find_available_windows_drive(preferred: str = "W") -> str:
|
|
|
604
604
|
raise RuntimeError("No available Windows drive letters found.")
|
|
605
605
|
|
|
606
606
|
|
|
607
|
+
def start_windows_explorer_refresher(mount_point: str, interval: int):
|
|
608
|
+
"""Starts a lightweight background watcher in the user session that notifies Explorer when remote changes occur."""
|
|
609
|
+
import sys
|
|
610
|
+
import subprocess
|
|
611
|
+
from pathlib import Path
|
|
612
|
+
|
|
613
|
+
refresher_script = Path(__file__).parent / "windows_refresher.py"
|
|
614
|
+
if not refresher_script.is_file():
|
|
615
|
+
return
|
|
616
|
+
|
|
617
|
+
# Locate Python interpreter (prefer pythonw to run completely windowless if available)
|
|
618
|
+
py_exec = sys.executable
|
|
619
|
+
parent_dir = Path(py_exec).parent
|
|
620
|
+
pythonw = parent_dir / "pythonw.exe"
|
|
621
|
+
if pythonw.is_file():
|
|
622
|
+
executable = str(pythonw)
|
|
623
|
+
else:
|
|
624
|
+
executable = py_exec
|
|
625
|
+
|
|
626
|
+
try:
|
|
627
|
+
# DETACHED_PROCESS (0x00000008) | CREATE_NEW_PROCESS_GROUP (0x00000200) | CREATE_NO_WINDOW (0x08000000)
|
|
628
|
+
creationflags = 0x08000008 | 0x00000200
|
|
629
|
+
proc = subprocess.Popen(
|
|
630
|
+
[executable, str(refresher_script), str(mount_point), str(interval)],
|
|
631
|
+
creationflags=creationflags,
|
|
632
|
+
stdout=subprocess.DEVNULL,
|
|
633
|
+
stderr=subprocess.DEVNULL,
|
|
634
|
+
close_fds=True
|
|
635
|
+
)
|
|
636
|
+
pid_file = Path("C:/ProgramData/accli/refresher.pid")
|
|
637
|
+
try:
|
|
638
|
+
pid_file.parent.mkdir(parents=True, exist_ok=True)
|
|
639
|
+
pid_file.write_text(str(proc.pid), encoding="utf-8")
|
|
640
|
+
except Exception:
|
|
641
|
+
pass
|
|
642
|
+
except Exception:
|
|
643
|
+
pass
|
|
644
|
+
|
|
645
|
+
|
|
646
|
+
def stop_windows_explorer_refresher():
|
|
647
|
+
"""Stops any active background Explorer watcher process."""
|
|
648
|
+
import subprocess
|
|
649
|
+
from pathlib import Path
|
|
650
|
+
|
|
651
|
+
pid_file = Path("C:/ProgramData/accli/refresher.pid")
|
|
652
|
+
if pid_file.is_file():
|
|
653
|
+
try:
|
|
654
|
+
pid_str = pid_file.read_text(encoding="utf-8").strip()
|
|
655
|
+
if pid_str:
|
|
656
|
+
subprocess.run(["taskkill", "/F", "/PID", pid_str], capture_output=True)
|
|
657
|
+
except Exception:
|
|
658
|
+
pass
|
|
659
|
+
try:
|
|
660
|
+
pid_file.unlink()
|
|
661
|
+
except Exception:
|
|
662
|
+
pass
|
|
663
|
+
|
|
664
|
+
# Also clean up any lingering windows_refresher.py processes
|
|
665
|
+
try:
|
|
666
|
+
cleanup_cmd = [
|
|
667
|
+
"powershell.exe", "-NoProfile", "-NonInteractive", "-ExecutionPolicy", "Bypass", "-Command",
|
|
668
|
+
"Get-CimInstance Win32_Process -Filter \"name='python.exe' or name='pythonw.exe'\" | "
|
|
669
|
+
"Where-Object { $_.CommandLine -like '*windows_refresher.py*' } | "
|
|
670
|
+
"ForEach-Object { Stop-Process -Id $_.ProcessId -Force -ErrorAction SilentlyContinue }"
|
|
671
|
+
]
|
|
672
|
+
subprocess.run(cleanup_cmd, capture_output=True)
|
|
673
|
+
except Exception:
|
|
674
|
+
pass
|
|
675
|
+
|
|
676
|
+
|
|
607
677
|
def enable_windows_nfs_features():
|
|
608
678
|
"""Automatically check and enable Windows Client for NFS features, registry key, and Task Scheduler gateway if needed."""
|
|
609
679
|
import subprocess
|
|
@@ -996,6 +1066,7 @@ def mount_start(
|
|
|
996
1066
|
]
|
|
997
1067
|
mount_res = subprocess.run(mount_cmd, capture_output=True, text=True)
|
|
998
1068
|
if mount_res.returncode == 0:
|
|
1069
|
+
start_windows_explorer_refresher(str(mount_point_abs), interval=30)
|
|
999
1070
|
print(f"[bold green][OK] NFS mount successfully mapped at [white]{mount_point_abs}[/white]![/bold green]")
|
|
1000
1071
|
print("[cyan]Use 'umount' or 'accli mount stop' to unmount the drive.[/cyan]")
|
|
1001
1072
|
return
|
|
@@ -1049,6 +1120,7 @@ def mount_start(
|
|
|
1049
1120
|
print(f"[yellow]Please check the log file at {log_file_path} for more details.[/yellow]")
|
|
1050
1121
|
raise typer.Exit(1)
|
|
1051
1122
|
|
|
1123
|
+
start_windows_explorer_refresher(str(mount_point_abs), interval=30)
|
|
1052
1124
|
print(f"[bold green][OK] NFS mount process spawned successfully (PID: {process.pid}).[/bold green]")
|
|
1053
1125
|
print(f"[cyan]Use 'umount' or 'accli mount stop' to unmount the drive.[/cyan]")
|
|
1054
1126
|
else:
|
|
@@ -1096,6 +1168,7 @@ def mount_start(
|
|
|
1096
1168
|
]
|
|
1097
1169
|
mount_res = subprocess.run(mount_cmd, capture_output=True, text=True)
|
|
1098
1170
|
if mount_res.returncode == 0:
|
|
1171
|
+
start_windows_explorer_refresher(str(mount_point_abs), interval=30)
|
|
1099
1172
|
print(f"[bold green][OK] NFS mount successfully mapped at [white]{mount_point_abs}[/white]![/bold green]")
|
|
1100
1173
|
print("[cyan]Use 'umount' or 'accli mount stop' to unmount the drive.[/cyan]")
|
|
1101
1174
|
else:
|
|
@@ -1233,6 +1306,9 @@ def mount_stop(
|
|
|
1233
1306
|
except Exception:
|
|
1234
1307
|
is_admin = False
|
|
1235
1308
|
|
|
1309
|
+
# 0. Stop Explorer background refresher
|
|
1310
|
+
stop_windows_explorer_refresher()
|
|
1311
|
+
|
|
1236
1312
|
# 1. Unmap the network drive in the user session
|
|
1237
1313
|
res = subprocess.run(["C:\\Windows\\System32\\umount.exe", "-f", str(mount_point_abs)], capture_output=True, text=True)
|
|
1238
1314
|
if res.returncode != 0:
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
$ErrorActionPreference = "SilentlyContinue"
|
|
2
|
+
$mountPoint = $args[0]
|
|
3
|
+
if (-not $mountPoint) { $mountPoint = "W:" }
|
|
4
|
+
$mountClean = $mountPoint.TrimEnd("\/")
|
|
5
|
+
|
|
6
|
+
$shell = New-Object -ComObject Shell.Application
|
|
7
|
+
foreach ($win in $shell.Windows()) {
|
|
8
|
+
$url = $win.LocationURL
|
|
9
|
+
if ($url -and ($url -like "*$mountClean*" -or $win.Path -like "*$mountClean*")) {
|
|
10
|
+
$win.Refresh()
|
|
11
|
+
}
|
|
12
|
+
}
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import sys
|
|
3
|
+
import time
|
|
4
|
+
import ctypes
|
|
5
|
+
import subprocess
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from datetime import datetime
|
|
8
|
+
|
|
9
|
+
# Windows Shell Notification constants
|
|
10
|
+
SHCNE_UPDATEDIR = 0x00001000
|
|
11
|
+
SHCNF_PATHW = 0x0005
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def log(msg: str):
|
|
15
|
+
timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
|
16
|
+
print(f"[{timestamp}] [Watcher] {msg}", flush=True)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def notify_explorer(path_str: str):
|
|
20
|
+
"""Notifies Windows Shell / Explorer to refresh views for path_str."""
|
|
21
|
+
log(f"Broadcasting SHChangeNotify on '{path_str}'...")
|
|
22
|
+
try:
|
|
23
|
+
ctypes.windll.shell32.SHChangeNotify(
|
|
24
|
+
SHCNE_UPDATEDIR,
|
|
25
|
+
SHCNF_PATHW,
|
|
26
|
+
path_str,
|
|
27
|
+
None
|
|
28
|
+
)
|
|
29
|
+
except Exception as e:
|
|
30
|
+
log(f"SHChangeNotify error: {e}")
|
|
31
|
+
|
|
32
|
+
try:
|
|
33
|
+
script_path = Path(__file__).parent / "refresh_explorer.ps1"
|
|
34
|
+
if script_path.is_file():
|
|
35
|
+
log(f"Executing COM window refresh for '{path_str}'...")
|
|
36
|
+
res = subprocess.run(
|
|
37
|
+
[
|
|
38
|
+
"powershell.exe", "-NoProfile", "-NonInteractive", "-ExecutionPolicy", "Bypass",
|
|
39
|
+
"-File", str(script_path), path_str
|
|
40
|
+
],
|
|
41
|
+
capture_output=True,
|
|
42
|
+
text=True,
|
|
43
|
+
creationflags=0x08000000 # CREATE_NO_WINDOW
|
|
44
|
+
)
|
|
45
|
+
if res.stdout and res.stdout.strip():
|
|
46
|
+
log(f"COM output: {res.stdout.strip()}")
|
|
47
|
+
if res.stderr and res.stderr.strip():
|
|
48
|
+
log(f"COM error: {res.stderr.strip()}")
|
|
49
|
+
log("Explorer windows refreshed.")
|
|
50
|
+
except Exception as e:
|
|
51
|
+
log(f"COM refresh error: {e}")
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def scan_directory_state(root_path: Path):
|
|
55
|
+
"""
|
|
56
|
+
Fast snapshot of directory state: mapping of relative_path -> (size, mtime_ns).
|
|
57
|
+
Returns None if root_path is not accessible or unmounted.
|
|
58
|
+
"""
|
|
59
|
+
state = {}
|
|
60
|
+
try:
|
|
61
|
+
if not root_path.exists():
|
|
62
|
+
return None
|
|
63
|
+
|
|
64
|
+
for root, dirs, files in os.walk(root_path):
|
|
65
|
+
for name in files:
|
|
66
|
+
full_path = os.path.join(root, name)
|
|
67
|
+
try:
|
|
68
|
+
st = os.stat(full_path)
|
|
69
|
+
rel = os.path.relpath(full_path, root_path)
|
|
70
|
+
state[rel] = (st.st_size, st.st_mtime_ns)
|
|
71
|
+
except (OSError, PermissionError):
|
|
72
|
+
continue
|
|
73
|
+
for name in dirs:
|
|
74
|
+
full_path = os.path.join(root, name)
|
|
75
|
+
try:
|
|
76
|
+
st = os.stat(full_path)
|
|
77
|
+
rel = os.path.relpath(full_path, root_path)
|
|
78
|
+
state[rel] = (-1, st.st_mtime_ns)
|
|
79
|
+
except (OSError, PermissionError):
|
|
80
|
+
continue
|
|
81
|
+
return state
|
|
82
|
+
except Exception:
|
|
83
|
+
return None
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def describe_changes(old_state, new_state):
|
|
87
|
+
old_keys = set(old_state.keys())
|
|
88
|
+
new_keys = set(new_state.keys())
|
|
89
|
+
added = new_keys - old_keys
|
|
90
|
+
removed = old_keys - new_keys
|
|
91
|
+
modified = {k for k in (old_keys & new_keys) if old_state[k] != new_state[k]}
|
|
92
|
+
|
|
93
|
+
parts = []
|
|
94
|
+
if added:
|
|
95
|
+
parts.append(f"Added: {list(added)[:5]}")
|
|
96
|
+
if removed:
|
|
97
|
+
parts.append(f"Removed: {list(removed)[:5]}")
|
|
98
|
+
if modified:
|
|
99
|
+
parts.append(f"Modified: {list(modified)[:5]}")
|
|
100
|
+
return "; ".join(parts)
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def run_watcher(mount_point: str, interval: float):
|
|
104
|
+
"""
|
|
105
|
+
Periodically inspects mount_point for remote additions/deletions/modifications.
|
|
106
|
+
Triggers Windows Shell & open Explorer window refreshes whenever changes are detected.
|
|
107
|
+
"""
|
|
108
|
+
drive_root = mount_point.rstrip("\\/") + "\\"
|
|
109
|
+
root_path = Path(drive_root)
|
|
110
|
+
|
|
111
|
+
log(f"Starting watcher on '{drive_root}' (poll interval: {interval}s)...")
|
|
112
|
+
|
|
113
|
+
# Baseline snapshot
|
|
114
|
+
try:
|
|
115
|
+
last_state = scan_directory_state(root_path)
|
|
116
|
+
except Exception as e:
|
|
117
|
+
last_state = None
|
|
118
|
+
log(f"Initial scan error: {e}")
|
|
119
|
+
|
|
120
|
+
if last_state is not None:
|
|
121
|
+
log(f"Initial baseline snapshot indexed {len(last_state)} items on '{drive_root}'.")
|
|
122
|
+
else:
|
|
123
|
+
log(f"Warning: Could not read '{drive_root}'. Waiting for drive to become available...")
|
|
124
|
+
|
|
125
|
+
while True:
|
|
126
|
+
try:
|
|
127
|
+
time.sleep(interval)
|
|
128
|
+
except (KeyboardInterrupt, SystemExit):
|
|
129
|
+
log("Watcher stopped.")
|
|
130
|
+
break
|
|
131
|
+
except Exception as e:
|
|
132
|
+
log(f"Sleep interrupted: {e}")
|
|
133
|
+
continue
|
|
134
|
+
|
|
135
|
+
try:
|
|
136
|
+
current_state = scan_directory_state(root_path)
|
|
137
|
+
if current_state is None:
|
|
138
|
+
continue
|
|
139
|
+
|
|
140
|
+
if last_state is not None:
|
|
141
|
+
if current_state != last_state:
|
|
142
|
+
diff_summary = describe_changes(last_state, current_state)
|
|
143
|
+
log(f"Change detected on '{drive_root}' -> {diff_summary}")
|
|
144
|
+
notify_explorer(drive_root)
|
|
145
|
+
last_state = current_state
|
|
146
|
+
else:
|
|
147
|
+
last_state = current_state
|
|
148
|
+
log(f"Baseline established: {len(last_state)} items found.")
|
|
149
|
+
except Exception as e:
|
|
150
|
+
log(f"Loop error: {e}")
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
if __name__ == "__main__":
|
|
154
|
+
if len(sys.argv) < 3:
|
|
155
|
+
print("Usage: python windows_refresher.py <mount_point> <interval_seconds>")
|
|
156
|
+
sys.exit(1)
|
|
157
|
+
mp = sys.argv[1]
|
|
158
|
+
poll_int = float(sys.argv[2])
|
|
159
|
+
run_watcher(mp, poll_int)
|
|
@@ -13,8 +13,10 @@ accli/cli.py
|
|
|
13
13
|
accli/common.py
|
|
14
14
|
accli/gui.py
|
|
15
15
|
accli/mount_downloader.py
|
|
16
|
+
accli/refresh_explorer.ps1
|
|
16
17
|
accli/setup-nfs.ps1
|
|
17
18
|
accli/token.py
|
|
19
|
+
accli/windows_refresher.py
|
|
18
20
|
accli.egg-info/PKG-INFO
|
|
19
21
|
accli.egg-info/SOURCES.txt
|
|
20
22
|
accli.egg-info/dependency_links.txt
|
|
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
|
|
File without changes
|