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.
Files changed (27) hide show
  1. {accli-2.0.3 → accli-2.0.4}/PKG-INFO +1 -1
  2. {accli-2.0.3 → accli-2.0.4}/accli/_version.py +1 -1
  3. {accli-2.0.3 → accli-2.0.4}/accli/cli.py +76 -0
  4. accli-2.0.4/accli/refresh_explorer.ps1 +12 -0
  5. accli-2.0.4/accli/windows_refresher.py +159 -0
  6. {accli-2.0.3 → accli-2.0.4}/accli.egg-info/PKG-INFO +1 -1
  7. {accli-2.0.3 → accli-2.0.4}/accli.egg-info/SOURCES.txt +2 -0
  8. {accli-2.0.3 → accli-2.0.4}/LICENSE +0 -0
  9. {accli-2.0.3 → accli-2.0.4}/README.md +0 -0
  10. {accli-2.0.3 → accli-2.0.4}/accli/AcceleratorJobProjectService.py +0 -0
  11. {accli-2.0.3 → accli-2.0.4}/accli/AcceleratorTaskDispatcher.py +0 -0
  12. {accli-2.0.3 → accli-2.0.4}/accli/AcceleratorTerminalCliProjectService.py +0 -0
  13. {accli-2.0.3 → accli-2.0.4}/accli/CsvRegionalTimeseriesValidator.py +0 -0
  14. {accli-2.0.3 → accli-2.0.4}/accli/__init__.py +0 -0
  15. {accli-2.0.3 → accli-2.0.4}/accli/__main__.py +0 -0
  16. {accli-2.0.3 → accli-2.0.4}/accli/cleanup-nfs.ps1 +0 -0
  17. {accli-2.0.3 → accli-2.0.4}/accli/common.py +0 -0
  18. {accli-2.0.3 → accli-2.0.4}/accli/gui.py +0 -0
  19. {accli-2.0.3 → accli-2.0.4}/accli/mount_downloader.py +0 -0
  20. {accli-2.0.3 → accli-2.0.4}/accli/setup-nfs.ps1 +0 -0
  21. {accli-2.0.3 → accli-2.0.4}/accli/token.py +0 -0
  22. {accli-2.0.3 → accli-2.0.4}/accli.egg-info/dependency_links.txt +0 -0
  23. {accli-2.0.3 → accli-2.0.4}/accli.egg-info/entry_points.txt +0 -0
  24. {accli-2.0.3 → accli-2.0.4}/accli.egg-info/requires.txt +0 -0
  25. {accli-2.0.3 → accli-2.0.4}/accli.egg-info/top_level.txt +0 -0
  26. {accli-2.0.3 → accli-2.0.4}/pyproject.toml +0 -0
  27. {accli-2.0.3 → accli-2.0.4}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: accli
3
- Version: 2.0.3
3
+ Version: 2.0.4
4
4
  Summary: IIASA Accelerator Client
5
5
  Author-email: Wrufesh S <wrufesh@gmail.com>
6
6
  License: The MIT License (MIT)
@@ -1,3 +1,3 @@
1
1
  # Please strictly put double quote to use this info for git tag
2
2
  # Read a developer guide for git tag command with regex
3
- VERSION = "v2.0.3"
3
+ VERSION = "v2.0.4"
@@ -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)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: accli
3
- Version: 2.0.3
3
+ Version: 2.0.4
4
4
  Summary: IIASA Accelerator Client
5
5
  Author-email: Wrufesh S <wrufesh@gmail.com>
6
6
  License: The MIT License (MIT)
@@ -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