accli 2.0.0b3__tar.gz → 2.0.0rc1__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 (25) hide show
  1. {accli-2.0.0b3 → accli-2.0.0rc1}/PKG-INFO +1 -1
  2. {accli-2.0.0b3 → accli-2.0.0rc1}/accli/AcceleratorTaskDispatcher.py +2 -2
  3. {accli-2.0.0b3 → accli-2.0.0rc1}/accli/CsvRegionalTimeseriesValidator.py +2 -2
  4. {accli-2.0.0b3 → accli-2.0.0rc1}/accli/_version.py +1 -1
  5. accli-2.0.0rc1/accli/cleanup-nfs.ps1 +57 -0
  6. {accli-2.0.0b3 → accli-2.0.0rc1}/accli/cli.py +95 -27
  7. accli-2.0.0rc1/accli/gui.py +623 -0
  8. {accli-2.0.0b3 → accli-2.0.0rc1}/accli/mount_downloader.py +3 -3
  9. accli-2.0.0rc1/accli/setup-nfs.ps1 +262 -0
  10. {accli-2.0.0b3 → accli-2.0.0rc1}/accli.egg-info/PKG-INFO +1 -1
  11. {accli-2.0.0b3 → accli-2.0.0rc1}/accli.egg-info/SOURCES.txt +3 -0
  12. {accli-2.0.0b3 → accli-2.0.0rc1}/accli.egg-info/entry_points.txt +1 -0
  13. {accli-2.0.0b3 → accli-2.0.0rc1}/pyproject.toml +4 -0
  14. {accli-2.0.0b3 → accli-2.0.0rc1}/LICENSE +0 -0
  15. {accli-2.0.0b3 → accli-2.0.0rc1}/README.md +0 -0
  16. {accli-2.0.0b3 → accli-2.0.0rc1}/accli/AcceleratorJobProjectService.py +0 -0
  17. {accli-2.0.0b3 → accli-2.0.0rc1}/accli/AcceleratorTerminalCliProjectService.py +0 -0
  18. {accli-2.0.0b3 → accli-2.0.0rc1}/accli/__init__.py +0 -0
  19. {accli-2.0.0b3 → accli-2.0.0rc1}/accli/__main__.py +0 -0
  20. {accli-2.0.0b3 → accli-2.0.0rc1}/accli/common.py +0 -0
  21. {accli-2.0.0b3 → accli-2.0.0rc1}/accli/token.py +0 -0
  22. {accli-2.0.0b3 → accli-2.0.0rc1}/accli.egg-info/dependency_links.txt +0 -0
  23. {accli-2.0.0b3 → accli-2.0.0rc1}/accli.egg-info/requires.txt +0 -0
  24. {accli-2.0.0b3 → accli-2.0.0rc1}/accli.egg-info/top_level.txt +0 -0
  25. {accli-2.0.0b3 → accli-2.0.0rc1}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: accli
3
- Version: 2.0.0b3
3
+ Version: 2.0.0rc1
4
4
  Summary: IIASA Accelerator Client
5
5
  Author-email: Wrufesh S <wrufesh@gmail.com>
6
6
  License: The MIT License (MIT)
@@ -139,10 +139,10 @@ class JobDispatchModel(BaseModel):
139
139
  class WKubeTaskMeta(BaseModel):
140
140
  required_cores: float
141
141
  required_ram: float
142
- required_storage_local: float
142
+ required_storage_local: Optional[float] = None
143
143
 
144
144
  # is ignored if it is a callback and child of non-free node jobs
145
- required_storage_workflow: float
145
+ required_storage_workflow: Optional[float] = None
146
146
 
147
147
  job_secrets: Optional[dict] = {}
148
148
 
@@ -205,8 +205,8 @@ class CsvRegionalTimeseriesValidator:
205
205
  for key in self.errors:
206
206
  print(f"[bold red]Invalid data:[/bold red] {self.errors[key]}")
207
207
  print(f"[bold red]Error:[/bold red] {key}")
208
- print("[bold red]Data is not valid against selected template. ⚠️⚠️")
208
+ print("[bold red]Data is not valid against selected template.[/bold red]")
209
209
  typer.Exit(1)
210
210
  else:
211
- print("[bold green]Data validated against selected template.[/bold green] 🎉🎉")
211
+ print("[bold green]Data validated against selected template.[/bold green]")
212
212
  typer.Exit(0)
@@ -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.0-beta.3"
3
+ VERSION = "v2.0.0-c1"
@@ -0,0 +1,57 @@
1
+ <#
2
+ .SYNOPSIS
3
+ Reverts all Windows NFS Client and Task Scheduler changes made by setup-nfs.ps1.
4
+ .DESCRIPTION
5
+ This script removes the scheduled task, registry keys, ProgramData configs, and disables
6
+ the native Windows NFS Client features. Useful for testing first-time accli setups.
7
+ .NOTES
8
+ Must be run in an elevated (Administrator) PowerShell window.
9
+ .EXAMPLE
10
+ # Open an Administrator PowerShell window and execute:
11
+ powershell -ExecutionPolicy Bypass -File .\accli\cleanup-nfs.ps1
12
+ #>
13
+
14
+ # Ensure running as Administrator
15
+ $isAdmin = ([Security.Principal.WindowsPrincipal][Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)
16
+ if (-not $isAdmin) {
17
+ Write-Error "This script must be run as Administrator."
18
+ Write-Host "Please open PowerShell as Administrator and run:" -ForegroundColor Yellow
19
+ Write-Host " powershell -ExecutionPolicy Bypass -File `"$PSCommandPath`"" -ForegroundColor Yellow
20
+ exit 1
21
+ }
22
+
23
+ Write-Host "=== Reverting accli Windows NFS Client Configuration ===" -ForegroundColor Cyan
24
+
25
+ # 1. Unregister Scheduled Tasks
26
+ Write-Host "Removing Scheduled Tasks..." -ForegroundColor Yellow
27
+ if (Get-Command Unregister-ScheduledTask -ErrorAction SilentlyContinue) {
28
+ Unregister-ScheduledTask -TaskName "accli-mount-nfs" -Confirm:$false -ErrorAction SilentlyContinue | Out-Null
29
+ Unregister-ScheduledTask -TaskName "accli-umount-nfs" -Confirm:$false -ErrorAction SilentlyContinue | Out-Null
30
+ } else {
31
+ schtasks /delete /tn "accli-mount-nfs" /f 2> $null | Out-Null
32
+ schtasks /delete /tn "accli-umount-nfs" /f 2> $null | Out-Null
33
+ }
34
+ Write-Host "[OK] Scheduled Tasks removed." -ForegroundColor Green
35
+
36
+ # 2. Delete Registry Policy Keys
37
+ Write-Host "Removing registry policies (EnableLinkedConnections, AnonymousUid, AnonymousGid)..." -ForegroundColor Yellow
38
+ Remove-ItemProperty -Path "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Policies\System" -Name "EnableLinkedConnections" -ErrorAction SilentlyContinue | Out-Null
39
+ Remove-ItemProperty -Path "HKLM:\SOFTWARE\Microsoft\ClientForNFS\CurrentVersion\Default" -Name "AnonymousUid" -ErrorAction SilentlyContinue | Out-Null
40
+ Remove-ItemProperty -Path "HKLM:\SOFTWARE\Microsoft\ClientForNFS\CurrentVersion\Default" -Name "AnonymousGid" -ErrorAction SilentlyContinue | Out-Null
41
+ Write-Host "[OK] Registry policies removed." -ForegroundColor Green
42
+
43
+ # 3. Delete Config Folder & Helpers
44
+ Write-Host "Deleting ProgramData configurations..." -ForegroundColor Yellow
45
+ Remove-Item -Path "C:\ProgramData\accli" -Recurse -Force -ErrorAction SilentlyContinue | Out-Null
46
+ Write-Host "[OK] ProgramData folder removed." -ForegroundColor Green
47
+
48
+ # 4. Disable Windows NFS Client features
49
+ Write-Host "Disabling Windows Client for NFS features..." -ForegroundColor Yellow
50
+ if (Get-Command Disable-WindowsOptionalFeature -ErrorAction SilentlyContinue) {
51
+ Disable-WindowsOptionalFeature -Online -FeatureName ServicesForNFS-ClientOnly,ClientForNFS-Infrastructure -NoRestart | Out-Null
52
+ }
53
+ Write-Host "[OK] NFS Client features disabled." -ForegroundColor Green
54
+
55
+ Write-Host ""
56
+ Write-Host "Reversion completed successfully!" -ForegroundColor Green
57
+ Write-Host "⚠ Note: A Windows sign-out/sign-in or system restart is required for changes to take effect." -ForegroundColor Yellow
@@ -141,7 +141,7 @@ def dispatch(
141
141
  verify_cert=(not ACCLI_DEBUG)
142
142
  )
143
143
 
144
- # ✅ Resolve the workflow file path properly
144
+ # Resolve the workflow file path properly
145
145
  if os.path.isabs(workflow_filename):
146
146
  workflow_filepath = workflow_filename
147
147
  else:
@@ -152,9 +152,9 @@ def dispatch(
152
152
 
153
153
  workflow_dir = os.path.dirname(workflow_filepath)
154
154
 
155
- # ✅ Temporarily switch to the workflow file's directory
155
+ # Temporarily switch to the workflow file's directory
156
156
  with pushd(workflow_dir):
157
- # ✅ Import the module dynamically
157
+ # Import the module dynamically
158
158
  spec = importlib.util.spec_from_file_location("workflow", workflow_filepath)
159
159
  module = importlib.util.module_from_spec(spec)
160
160
  spec.loader.exec_module(module)
@@ -400,7 +400,7 @@ def copy(
400
400
  progress_updater=None,
401
401
  request_headers=None
402
402
  )
403
- print("[bold green]✔ Download completed successfully![/bold green]")
403
+ print("[bold green][OK] Download completed successfully![/bold green]")
404
404
  except Exception as e:
405
405
  print(f"[bold red]ERROR: Download failed: {e}[/bold red]")
406
406
  raise typer.Exit(1)
@@ -500,7 +500,7 @@ def copy(
500
500
  verify=(not ACCLI_DEBUG)
501
501
  )
502
502
  response.raise_for_status()
503
- print("[bold green]✔ Upload and bulk metadata registration completed successfully![/bold green]")
503
+ print("[bold green][OK] Upload and bulk metadata registration completed successfully![/bold green]")
504
504
  except requests.exceptions.HTTPError as e:
505
505
  detail = None
506
506
  try:
@@ -710,6 +710,7 @@ def mount_start(
710
710
  from . import mount_downloader
711
711
  import platform
712
712
  import re
713
+ import json
713
714
 
714
715
  sys_name = platform.system()
715
716
 
@@ -738,6 +739,27 @@ def mount_start(
738
739
 
739
740
  # Standardize mount_point and handle defaults / auto-selection
740
741
  if sys_name == "Windows":
742
+ # Check if the daemon is already running to prevent double mount
743
+ import subprocess
744
+ try:
745
+ task_check = subprocess.run(["tasklist", "/FI", "IMAGENAME eq hf-mount-nfs.exe"], capture_output=True, text=True)
746
+ if "hf-mount-nfs.exe" in task_check.stdout:
747
+ config_file = Path("C:/ProgramData/accli/mount_config.json")
748
+ active_msg = ""
749
+ if config_file.is_file():
750
+ try:
751
+ import json
752
+ cfg = json.loads(config_file.read_text(encoding="utf-8"))
753
+ active_msg = f" for project '{cfg.get('project_slug')}' at '{cfg.get('mount_point')}'"
754
+ except Exception:
755
+ pass
756
+ print(f"[bold red]ERROR: A mount daemon is already running{active_msg}.[/bold red]")
757
+ print("[yellow]Please run 'accli mount stop' to unmount before starting a new mount.[/yellow]")
758
+ raise typer.Exit(1)
759
+ except Exception as e:
760
+ if isinstance(e, typer.Exit):
761
+ raise e
762
+
741
763
  if mount_point is None:
742
764
  # Auto-detect available drive letter starting with W
743
765
  try:
@@ -917,6 +939,8 @@ def mount_start(
917
939
  print("[yellow]Falling back to UAC elevation...[/yellow]")
918
940
  has_task = False
919
941
  except Exception as e:
942
+ if isinstance(e, typer.Exit) or type(e).__name__ == "Exit":
943
+ raise e
920
944
  print(f"[bold red]ERROR: Task Scheduler trigger failed: {e}[/bold red]")
921
945
  print("[yellow]Falling back to UAC elevation...[/yellow]")
922
946
  has_task = False
@@ -1108,8 +1132,9 @@ def mount_stop(
1108
1132
  print(f"[bold red]ERROR: You requested to stop '{mount_point_abs}', but the active daemon is mounted at '{active_mount}'.[/bold red]")
1109
1133
  print(f"[yellow]To stop the server, please run: accli mount stop {active_mount}[/yellow]")
1110
1134
  raise typer.Exit(1)
1111
- except Exception:
1112
- pass
1135
+ except Exception as e:
1136
+ if isinstance(e, typer.Exit) or type(e).__name__ == "Exit":
1137
+ raise e
1113
1138
  else:
1114
1139
  if mount_point is None:
1115
1140
  try:
@@ -1128,28 +1153,71 @@ def mount_stop(
1128
1153
  print(f"[bold cyan]Stopping mount at '[white]{mount_point_abs}[/white]' on Windows...[/bold cyan]")
1129
1154
  import subprocess
1130
1155
 
1131
- # 1. Run Windows standard umount command
1132
- res = subprocess.run(["C:\\Windows\\System32\\umount.exe", "-f", str(mount_point_abs)], capture_output=True, text=True)
1133
-
1134
- # 2. Stop the scheduled task (which terminates the elevated daemon background process tree)
1135
- try:
1136
- subprocess.run(["schtasks", "/end", "/tn", "accli-mount-nfs"], capture_output=True)
1137
- except Exception:
1138
- pass
1139
-
1140
- # 2. Selective Kill: Target and terminate only the specific process using the target mount point using a PowerShell command-line filter
1156
+ # Determine if current user is admin
1141
1157
  try:
1142
- escaped_mount_point = str(mount_point_abs).replace("'", "''")
1143
- kill_cmd = [
1144
- "powershell.exe", "-NoProfile", "-NonInteractive", "-ExecutionPolicy", "Bypass", "-Command",
1145
- f"Get-CimInstance Win32_Process -Filter \"name='hf-mount-nfs.exe'\" | "
1146
- f"Where-Object {{ $_.CommandLine -like '*{escaped_mount_point}*' }} | "
1147
- f"ForEach-Object {{ Stop-Process -Id $_.ProcessId -Force }}"
1148
- ]
1149
- subprocess.run(kill_cmd, capture_output=True)
1158
+ import ctypes
1159
+ is_admin = ctypes.windll.shell32.IsUserAnAdmin() != 0
1150
1160
  except Exception:
1151
- # Fallback to killing all instances if selective filter fails
1152
- subprocess.run(["taskkill", "/f", "/im", "hf-mount-nfs.exe"], capture_output=True)
1161
+ is_admin = False
1162
+
1163
+ # 1. Unmap the network drive in the user session
1164
+ res = subprocess.run(["C:\\Windows\\System32\\umount.exe", "-f", str(mount_point_abs)], capture_output=True, text=True)
1165
+ if res.returncode != 0:
1166
+ # Fallback to force-releasing using net use
1167
+ subprocess.run(["net", "use", str(mount_point_abs), "/delete", "/y"], capture_output=True)
1168
+
1169
+ # 2. Terminate the background daemon
1170
+ has_umount_task = False
1171
+ if not is_admin:
1172
+ try:
1173
+ task_check = subprocess.run(["schtasks", "/query", "/tn", "accli-umount-nfs"], capture_output=True, text=True)
1174
+ if task_check.returncode == 0:
1175
+ has_umount_task = True
1176
+ except Exception:
1177
+ pass
1178
+
1179
+ if has_umount_task:
1180
+ print("[cyan]Triggering elevated NFS daemon termination via Task Scheduler...[/cyan]")
1181
+ subprocess.run(["schtasks", "/run", "/tn", "accli-umount-nfs"], capture_output=True)
1182
+ # Poll for up to 5 seconds to ensure hf-mount-nfs.exe has fully terminated
1183
+ import time
1184
+ for _ in range(10):
1185
+ time.sleep(0.5)
1186
+ task_check = subprocess.run(["tasklist", "/FI", "IMAGENAME eq hf-mount-nfs.exe"], capture_output=True, text=True)
1187
+ if "hf-mount-nfs.exe" not in task_check.stdout:
1188
+ break
1189
+ else:
1190
+ # If admin or task scheduler gateway is not registered, stop/kill directly
1191
+ if is_admin:
1192
+ try:
1193
+ subprocess.run(["schtasks", "/end", "/tn", "accli-mount-nfs"], capture_output=True)
1194
+ except Exception:
1195
+ pass
1196
+ try:
1197
+ escaped_mount_point = str(mount_point_abs).replace("'", "''")
1198
+ kill_cmd = [
1199
+ "powershell.exe", "-NoProfile", "-NonInteractive", "-ExecutionPolicy", "Bypass", "-Command",
1200
+ f"Get-CimInstance Win32_Process -Filter \"name='hf-mount-nfs.exe'\" | "
1201
+ f"Where-Object {{ $_.CommandLine -like '*{escaped_mount_point}*' }} | "
1202
+ f"ForEach-Object {{ Stop-Process -Id $_.ProcessId -Force }}"
1203
+ ]
1204
+ subprocess.run(kill_cmd, capture_output=True)
1205
+ except Exception:
1206
+ # Fallback to killing all instances if selective filter fails
1207
+ subprocess.run(["taskkill", "/f", "/im", "hf-mount-nfs.exe"], capture_output=True)
1208
+ else:
1209
+ # Standard user with no task registered - need UAC to end task/kill process
1210
+ print("[yellow]Notice: Task Scheduler gateway is not active. Requesting UAC elevation to terminate daemon...[/yellow]")
1211
+ ps_command = (
1212
+ "Start-Process powershell.exe -ArgumentList '-NoProfile -ExecutionPolicy Bypass -Command "
1213
+ "\"Stop-ScheduledTask -TaskName accli-mount-nfs -ErrorAction SilentlyContinue; "
1214
+ "Get-CimInstance Win32_Process -Filter \\\"name=''hf-mount-nfs.exe''\\\" | Stop-Process -Force -ErrorAction SilentlyContinue\"' "
1215
+ "-Verb RunAs -Wait"
1216
+ )
1217
+ subprocess.run([
1218
+ "powershell.exe", "-NoProfile", "-NonInteractive", "-ExecutionPolicy", "Bypass", "-Command",
1219
+ ps_command
1220
+ ], capture_output=True)
1153
1221
 
1154
1222
  if res.returncode == 0 or "successfully" in res.stdout or "successfully" in res.stderr:
1155
1223
  print("[bold green][OK] Mount stopped successfully.[/bold green]")
@@ -0,0 +1,623 @@
1
+ import os
2
+ import sys
3
+ import json
4
+ import time
5
+ import platform
6
+ import threading
7
+ import subprocess
8
+ from pathlib import Path
9
+
10
+ def setup_tcl_tk_env():
11
+ base = sys.base_prefix
12
+ for sub in ["tcl", "lib", "."]:
13
+ search_dir = os.path.join(base, sub)
14
+ if not os.path.isdir(search_dir):
15
+ continue
16
+ try:
17
+ for item in os.listdir(search_dir):
18
+ item_path = os.path.join(search_dir, item)
19
+ if os.path.isdir(item_path):
20
+ if item.startswith("tcl") and not os.environ.get("TCL_LIBRARY"):
21
+ if os.path.isfile(os.path.join(item_path, "init.tcl")):
22
+ os.environ["TCL_LIBRARY"] = item_path
23
+ elif item.startswith("tk") and not os.environ.get("TK_LIBRARY"):
24
+ if os.path.isfile(os.path.join(item_path, "tk.tcl")):
25
+ os.environ["TK_LIBRARY"] = item_path
26
+ except Exception:
27
+ pass
28
+
29
+ setup_tcl_tk_env()
30
+
31
+ import tkinter as tk
32
+ from tkinter import ttk, messagebox, filedialog
33
+
34
+ # Ensure PyInstaller static analyzer traces and bundles all CLI dependencies
35
+ import accli.cli
36
+
37
+ class AccliGuiApp(tk.Tk):
38
+ def __init__(self):
39
+ super().__init__()
40
+ self.title("IIASA Accelerator Client (accli)")
41
+ self.geometry("800x600")
42
+ self.minsize(700, 500)
43
+
44
+ # Apply dark modern theme style
45
+ self.style = ttk.Style()
46
+ self.configure_styles()
47
+
48
+ # Main layout
49
+ self.create_widgets()
50
+
51
+ # Start state check poll
52
+ self.refresh_login_status()
53
+ self.refresh_mounts_list()
54
+
55
+ def configure_styles(self):
56
+ self.configure(bg="#1e1e1e")
57
+ self.style.theme_use("clam")
58
+
59
+ # Style Definitions
60
+ self.style.configure(".", background="#1e1e1e", foreground="#ffffff", font=("Segoe UI", 10))
61
+ self.style.configure("TFrame", background="#1e1e1e")
62
+ self.style.configure("TLabel", background="#1e1e1e", foreground="#ffffff")
63
+ self.style.configure("Header.TLabel", font=("Segoe UI", 14, "bold"), foreground="#007acc")
64
+
65
+ # Notebook Style
66
+ self.style.configure("TNotebook", background="#252526", borderwidth=0)
67
+ self.style.configure("TNotebook.Tab", background="#2d2d2d", foreground="#888888", padding=[15, 5])
68
+ self.style.map("TNotebook.Tab",
69
+ background=[("selected", "#252526"), ("active", "#333333")],
70
+ foreground=[("selected", "#ffffff"), ("active", "#ffffff")]
71
+ )
72
+
73
+ # Buttons Style
74
+ self.style.configure("TButton", background="#007acc", foreground="#ffffff", borderwidth=0, padding=[10, 5])
75
+ self.style.map("TButton",
76
+ background=[("active", "#0098ff"), ("disabled", "#555555")],
77
+ foreground=[("disabled", "#aaaaaa")]
78
+ )
79
+ self.style.configure("Stop.TButton", background="#e51400")
80
+ self.style.map("Stop.TButton",
81
+ background=[("active", "#ff3333"), ("disabled", "#555555")]
82
+ )
83
+ self.style.configure("Refresh.TButton", background="#333333")
84
+ self.style.map("Refresh.TButton", background=[("active", "#444444")])
85
+
86
+ # Entry fields
87
+ self.style.configure("TEntry", fieldbackground="#2d2d2d", foreground="#ffffff", borderwidth=0)
88
+
89
+ # Checkboxes
90
+ self.style.configure("TCheckbutton", background="#1e1e1e", foreground="#ffffff")
91
+ self.style.map("TCheckbutton", background=[("active", "#1e1e1e")])
92
+
93
+ def create_widgets(self):
94
+ # Top banner
95
+ banner_frame = ttk.Frame(self, padding=10)
96
+ banner_frame.pack(fill=tk.X)
97
+
98
+ title_label = ttk.Label(banner_frame, text="IIASA ACCELERATOR CLIENT", style="Header.TLabel")
99
+ title_label.pack(side=tk.LEFT)
100
+
101
+ self.status_bar_val = tk.StringVar(value="Checking state...")
102
+ status_lbl = ttk.Label(banner_frame, textvariable=self.status_bar_val, font=("Segoe UI", 9, "italic"), foreground="#4ec9b0")
103
+ status_lbl.pack(side=tk.RIGHT)
104
+
105
+ # Tabs
106
+ self.notebook = ttk.Notebook(self)
107
+ self.notebook.pack(fill=tk.BOTH, expand=True, padx=10, pady=(0, 10))
108
+
109
+ # Build Tabs
110
+ self.build_mounts_tab()
111
+ self.build_copy_tab()
112
+ self.build_dispatch_tab()
113
+ self.build_auth_tab()
114
+ self.build_logs_tab()
115
+
116
+ def run_cli_async(self, args, on_done=None, log_to_viewer=True):
117
+ """Runs the CLI binary in a background thread to prevent UI freezing."""
118
+ def worker():
119
+ # Determine how to invoke accli
120
+ if getattr(sys, "frozen", False):
121
+ # Executable mode (PyInstaller package)
122
+ cmd = [sys.executable] + args
123
+ else:
124
+ # Script mode
125
+ cmd = [sys.executable, "-m", "accli.cli"] + args
126
+
127
+ try:
128
+ if log_to_viewer:
129
+ self.append_log(f"Executing: {' '.join(cmd)}\n")
130
+
131
+ # Start process with hidden console window on Windows
132
+ startupinfo = None
133
+ if platform.system() == "Windows":
134
+ startupinfo = subprocess.STARTUPINFO()
135
+ startupinfo.dwFlags |= subprocess.STARTF_USESHOWWINDOW
136
+
137
+ process = subprocess.Popen(
138
+ cmd,
139
+ stdout=subprocess.PIPE,
140
+ stderr=subprocess.STDOUT,
141
+ text=True,
142
+ bufsize=1,
143
+ startupinfo=startupinfo
144
+ )
145
+
146
+ output_lines = []
147
+ while True:
148
+ line = process.stdout.readline()
149
+ if not line:
150
+ break
151
+ output_lines.append(line)
152
+ if log_to_viewer:
153
+ self.append_log(line)
154
+
155
+ process.wait()
156
+
157
+ if on_done:
158
+ self.after(0, lambda: on_done(process.returncode, "".join(output_lines)))
159
+ except Exception as e:
160
+ self.append_log(f"ERROR executing command: {e}\n")
161
+ if on_done:
162
+ self.after(0, lambda: on_done(-1, str(e)))
163
+
164
+ threading.Thread(target=worker, daemon=True).start()
165
+
166
+ def append_log(self, text):
167
+ self.after(0, lambda: self._safe_append_log(text))
168
+
169
+ def _safe_append_log(self, text):
170
+ self.log_txt.configure(state=tk.NORMAL)
171
+ self.log_txt.insert(tk.END, text)
172
+ self.log_txt.see(tk.END)
173
+ self.log_txt.configure(state=tk.DISABLED)
174
+
175
+ # ------------------ AUTH TAB ------------------
176
+ def build_auth_tab(self):
177
+ auth_tab = ttk.Frame(self.notebook, padding=20)
178
+ self.notebook.add(auth_tab, text="Connection / Login")
179
+
180
+ # Login Details Card
181
+ card = ttk.LabelFrame(auth_tab, text="Login Credentials", padding=15)
182
+ card.pack(fill=tk.X, pady=10)
183
+
184
+ ttk.Label(card, text="Server URL:").grid(row=0, column=0, sticky=tk.W, pady=5)
185
+ self.server_url_val = tk.StringVar(value="https://accelerator.iiasa.ac.at")
186
+ self.server_entry = ttk.Entry(card, textvariable=self.server_url_val, width=40)
187
+ self.server_entry.grid(row=0, column=1, sticky=tk.W, padx=10, pady=5)
188
+
189
+ self.login_btn = ttk.Button(card, text="Authenticate / Login", command=self.action_login)
190
+ self.login_btn.grid(row=0, column=2, padx=10, pady=5)
191
+
192
+ # Connection status details
193
+ self.conn_details_txt = tk.Text(auth_tab, height=12, bg="#2d2d2d", fg="#ffffff", insertbackground="white", font=("Courier New", 10), state=tk.DISABLED)
194
+ self.conn_details_txt.pack(fill=tk.BOTH, expand=True, pady=10)
195
+
196
+ def refresh_login_status(self):
197
+ from accli.token import get_db_path
198
+ from tinydb import TinyDB
199
+ db_path = get_db_path()
200
+ logged_in = False
201
+ server_url = "https://accelerator.iiasa.ac.at"
202
+
203
+ if os.path.exists(db_path):
204
+ try:
205
+ db = TinyDB(db_path)
206
+ for item in db:
207
+ if item.get('token'):
208
+ logged_in = True
209
+ if item.get('server_url'):
210
+ server_url = item.get('server_url')
211
+ except Exception:
212
+ pass
213
+
214
+ self.is_logged_in_state = logged_in
215
+ if logged_in:
216
+ self.login_btn.configure(text="Logout", style="Stop.TButton")
217
+ self.server_entry.configure(state=tk.DISABLED)
218
+ self.server_url_val.set(server_url)
219
+ self.status_bar_val.set("Session Status: Connected")
220
+ else:
221
+ self.login_btn.configure(text="Authenticate / Login", style="TButton")
222
+ self.server_entry.configure(state=tk.NORMAL)
223
+ self.status_bar_val.set("Session Status: Disconnected")
224
+
225
+ def on_check_done(code, output):
226
+ self.conn_details_txt.configure(state=tk.NORMAL)
227
+ self.conn_details_txt.delete("1.0", tk.END)
228
+ self.conn_details_txt.insert(tk.END, output)
229
+ self.conn_details_txt.configure(state=tk.DISABLED)
230
+
231
+ self.run_cli_async(["status"], on_done=on_check_done, log_to_viewer=False)
232
+
233
+ def action_login(self):
234
+ if hasattr(self, "is_logged_in_state") and self.is_logged_in_state:
235
+ from accli.token import get_db_path
236
+ db_path = get_db_path()
237
+ try:
238
+ if os.path.exists(db_path):
239
+ os.remove(db_path)
240
+ self.append_log("Logged out successfully.\n")
241
+ messagebox.showinfo("Success", "Logged out successfully.")
242
+ except Exception as e:
243
+ self.append_log(f"Error during logout: {e}\n")
244
+ self.refresh_login_status()
245
+ return
246
+
247
+ url = self.server_url_val.get().strip()
248
+ if not url:
249
+ messagebox.showerror("Error", "Please provide a valid Server URL.")
250
+ return
251
+
252
+ self.status_bar_val.set("Authenticating...")
253
+
254
+ import webbrowser
255
+ from tkinter import simpledialog
256
+ import requests
257
+ from accli.token import save_token_details
258
+
259
+ webcli_url = url
260
+ auth_url = f"{webcli_url.rstrip('/')}/acli-auth-code"
261
+
262
+ self.append_log(f"Opening browser for authentication at: {auth_url}\n")
263
+ try:
264
+ webbrowser.open(auth_url)
265
+ except Exception as e:
266
+ self.append_log(f"Warning: Could not open browser automatically: {e}\n")
267
+
268
+ auth_code = simpledialog.askstring(
269
+ "Authorization Code",
270
+ f"Please authenticate in your browser at:\n{auth_url}\n\nThen enter the authorization code here:"
271
+ )
272
+
273
+ if not auth_code:
274
+ self.status_bar_val.set("Session Status: Disconnected")
275
+ return
276
+
277
+ def perform_auth():
278
+ try:
279
+ self.append_log("Exchanging authorization code for token...\n")
280
+ token_endpoint = f"{url.rstrip('/')}/api/v1/oauth/device/token/"
281
+ response = requests.post(
282
+ token_endpoint,
283
+ json={"device_authorization_code": auth_code.strip()},
284
+ verify=True
285
+ )
286
+
287
+ if response.status_code == 400:
288
+ detail = response.json().get("detail", "Unknown error")
289
+ self.append_log(f"Authentication Error: {detail}\n")
290
+ self.after(0, lambda: messagebox.showerror("Auth Error", f"Failed to log in: {detail}"))
291
+ self.after(0, self.refresh_login_status)
292
+ return
293
+
294
+ response.raise_for_status()
295
+ data = response.json()
296
+
297
+ save_token_details(data, url, webcli_url)
298
+ self.append_log("Authentication successful! Token saved.\n")
299
+ self.after(0, lambda: messagebox.showinfo("Success", "Successfully logged in!"))
300
+ self.after(0, self.refresh_login_status)
301
+ except Exception as e:
302
+ self.append_log(f"Authentication Failed: {e}\n")
303
+ self.after(0, lambda: messagebox.showerror("Error", f"Authentication failed: {e}"))
304
+ self.after(0, self.refresh_login_status)
305
+
306
+ threading.Thread(target=perform_auth, daemon=True).start()
307
+
308
+ # ------------------ DISPATCH TAB ------------------
309
+ def build_dispatch_tab(self):
310
+ dispatch_tab = ttk.Frame(self.notebook, padding=15)
311
+ self.notebook.add(dispatch_tab, text="Task Dispatcher")
312
+
313
+ card = ttk.LabelFrame(dispatch_tab, text="Workflow Configuration", padding=15)
314
+ card.pack(fill=tk.X, pady=10)
315
+
316
+ # Project Slug
317
+ ttk.Label(card, text="Project Slug:").grid(row=0, column=0, sticky=tk.W, pady=5)
318
+ self.disp_slug_val = tk.StringVar()
319
+ ttk.Entry(card, textvariable=self.disp_slug_val, width=30).grid(row=0, column=1, sticky=tk.W, padx=10, pady=5)
320
+
321
+ # Root Task Variable
322
+ ttk.Label(card, text="Root Task Variable:").grid(row=1, column=0, sticky=tk.W, pady=5)
323
+ self.disp_root_val = tk.StringVar()
324
+ ttk.Entry(card, textvariable=self.disp_root_val, width=30).grid(row=1, column=1, sticky=tk.W, padx=10, pady=5)
325
+
326
+ # Workflow File Path
327
+ ttk.Label(card, text="Workflow Python File (-f):").grid(row=2, column=0, sticky=tk.W, pady=5)
328
+ file_frame = ttk.Frame(card)
329
+ file_frame.grid(row=2, column=1, sticky=tk.W, padx=10, pady=5)
330
+
331
+ self.disp_file_val = tk.StringVar(value="wkube.py")
332
+ ttk.Entry(file_frame, textvariable=self.disp_file_val, width=30).pack(side=tk.LEFT)
333
+ ttk.Button(file_frame, text="Browse...", command=self.browse_workflow_file).pack(side=tk.LEFT, padx=5)
334
+
335
+ # Dispatch Button
336
+ btn_frame = ttk.Frame(dispatch_tab)
337
+ btn_frame.pack(fill=tk.X, pady=15)
338
+
339
+ dispatch_btn = ttk.Button(btn_frame, text="Dispatch Task", command=self.action_dispatch)
340
+ dispatch_btn.pack(anchor=tk.CENTER)
341
+
342
+ # Try to pre-populate project slug from config
343
+ try:
344
+ from accli.token import get_project_slug
345
+ slug = get_project_slug()
346
+ if slug:
347
+ self.disp_slug_val.set(slug)
348
+ except Exception:
349
+ pass
350
+
351
+ def browse_workflow_file(self):
352
+ file_path = filedialog.askopenfilename(filetypes=[("Python Files", "*.py")])
353
+ if file_path:
354
+ self.disp_file_val.set(file_path)
355
+
356
+ def action_dispatch(self):
357
+ slug = self.disp_slug_val.get().strip()
358
+ root_var = self.disp_root_val.get().strip()
359
+ wf_file = self.disp_file_val.get().strip()
360
+
361
+ if not slug or not root_var or not wf_file:
362
+ messagebox.showerror("Error", "Please fill in all Dispatch fields.")
363
+ return
364
+
365
+ args = ["dispatch", slug, root_var, "-f", wf_file]
366
+
367
+ self.status_bar_val.set("Dispatching workflow...")
368
+ self.notebook.select(self.log_tab_idx)
369
+
370
+ def on_done(code, output):
371
+ if code == 0:
372
+ self.status_bar_val.set("Workflow Dispatched")
373
+ messagebox.showinfo("Success", "Workflow task successfully dispatched!")
374
+ else:
375
+ self.status_bar_val.set("Dispatch Failed")
376
+ messagebox.showerror("Error", "Failed to dispatch workflow task. See logs for details.")
377
+
378
+ self.run_cli_async(args, on_done=on_done)
379
+
380
+ # ------------------ MOUNTS TAB ------------------
381
+ def build_mounts_tab(self):
382
+ mounts_tab = ttk.Frame(self.notebook, padding=15)
383
+ self.notebook.add(mounts_tab, text="Mount Manager")
384
+
385
+ # Left Panel (Input Form)
386
+ form_frame = ttk.LabelFrame(mounts_tab, text="Mount Configuration", padding=15)
387
+ form_frame.pack(side=tk.LEFT, fill=tk.BOTH, expand=True, padx=(0, 10))
388
+
389
+ ttk.Label(form_frame, text="Project Slug / Repository ID:").grid(row=0, column=0, sticky=tk.W, pady=5)
390
+ self.proj_slug_val = tk.StringVar()
391
+ ttk.Entry(form_frame, textvariable=self.proj_slug_val, width=25).grid(row=0, column=1, sticky=tk.W, padx=10, pady=5)
392
+
393
+ ttk.Label(form_frame, text="Mount Point / Drive Letter:").grid(row=1, column=0, sticky=tk.W, pady=5)
394
+ self.mount_point_val = tk.StringVar(value="W:" if platform.system() == "Windows" else "")
395
+ ttk.Entry(form_frame, textvariable=self.mount_point_val, width=25).grid(row=1, column=1, sticky=tk.W, padx=10, pady=5)
396
+
397
+ # Options
398
+ ttk.Label(form_frame, text="Options:").grid(row=2, column=0, sticky=tk.NW, pady=5)
399
+ options_frame = ttk.Frame(form_frame)
400
+ options_frame.grid(row=2, column=1, sticky=tk.W, padx=10, pady=5)
401
+
402
+ self.opt_overlay = tk.BooleanVar(value=False)
403
+ ttk.Checkbutton(options_frame, text="Enable Overlay (local writes)", variable=self.opt_overlay).pack(anchor=tk.W, pady=2)
404
+
405
+ self.opt_readonly = tk.BooleanVar(value=False)
406
+ ttk.Checkbutton(options_frame, text="Force Read-Only", variable=self.opt_readonly).pack(anchor=tk.W, pady=2)
407
+
408
+ self.opt_fuse = tk.BooleanVar(value=False)
409
+ if platform.system() != "Windows":
410
+ ttk.Checkbutton(options_frame, text="Use FUSE backend", variable=self.opt_fuse).pack(anchor=tk.W, pady=2)
411
+
412
+ # Mount / Unmount buttons
413
+ btn_frame = ttk.Frame(form_frame)
414
+ btn_frame.grid(row=3, column=0, columnspan=2, pady=15, sticky=tk.W)
415
+
416
+ mount_btn = ttk.Button(btn_frame, text="Start Mount", command=self.action_mount_start)
417
+ mount_btn.pack(side=tk.LEFT, padx=5)
418
+
419
+ # Right Panel (List of mounts)
420
+ list_frame = ttk.LabelFrame(mounts_tab, text="Active Mounts", padding=10)
421
+ list_frame.pack(side=tk.RIGHT, fill=tk.BOTH, expand=True)
422
+
423
+ list_btn_frame = ttk.Frame(list_frame)
424
+ list_btn_frame.pack(fill=tk.X, pady=5)
425
+
426
+ refresh_btn = ttk.Button(list_btn_frame, text="Refresh", style="Refresh.TButton", command=self.refresh_mounts_list)
427
+ refresh_btn.pack(side=tk.LEFT, padx=2)
428
+
429
+ stop_btn = ttk.Button(list_btn_frame, text="Stop Selected", style="Stop.TButton", command=self.action_mount_stop)
430
+ stop_btn.pack(side=tk.RIGHT, padx=2)
431
+
432
+ self.mounts_list = tk.Listbox(list_frame, bg="#2d2d2d", fg="#ffffff", selectbackground="#007acc", font=("Segoe UI", 10))
433
+ self.mounts_list.pack(fill=tk.BOTH, expand=True)
434
+
435
+ def refresh_mounts_list(self):
436
+ def on_done(code, output):
437
+ self.mounts_list.delete(0, tk.END)
438
+ # Filter and parse 'accli mount status' output
439
+ lines = output.splitlines()
440
+ for line in lines:
441
+ cleaned = line.strip()
442
+ if not cleaned:
443
+ continue
444
+
445
+ # Only insert actual mount entries, skip headers and info logs
446
+ if platform.system() == "Windows":
447
+ # Look for lines starting with drive letter (e.g. W:)
448
+ if len(cleaned) >= 2 and cleaned[0].isalpha() and cleaned[1] == ":":
449
+ self.mounts_list.insert(tk.END, cleaned)
450
+ else:
451
+ # Look for lines starting with absolute/home path (e.g. / or ~)
452
+ if cleaned.startswith("/") or cleaned.startswith("~"):
453
+ self.mounts_list.insert(tk.END, cleaned)
454
+
455
+ self.run_cli_async(["mount", "status"], on_done=on_done, log_to_viewer=False)
456
+
457
+ def action_mount_start(self):
458
+ slug = self.proj_slug_val.get().strip()
459
+ point = self.mount_point_val.get().strip()
460
+ if not slug:
461
+ messagebox.showerror("Error", "Please enter a valid Project Slug.")
462
+ return
463
+
464
+ args = ["mount", "start", slug]
465
+ if point:
466
+ args.append(point)
467
+ if self.opt_overlay.get():
468
+ args.append("--overlay")
469
+ if self.opt_readonly.get():
470
+ args.append("--read-only")
471
+ if platform.system() != "Windows" and self.opt_fuse.get():
472
+ args.append("--fuse")
473
+
474
+ self.status_bar_val.set("Mounting project...")
475
+ self.notebook.select(self.log_tab_idx)
476
+
477
+ def on_done(code, output):
478
+ self.refresh_mounts_list()
479
+ if code == 0:
480
+ if "system restart is required" in output.lower() or "restart your computer" in output.lower():
481
+ self.status_bar_val.set("Restart Required")
482
+ messagebox.showwarning(
483
+ "System Restart Required",
484
+ "A Windows system restart is required for the new NFS Client features and registry policies to take effect.\n\n"
485
+ "Please restart your computer, then run the mount again."
486
+ )
487
+ else:
488
+ self.status_bar_val.set("Mount Active")
489
+ messagebox.showinfo("Success", f"Successfully mounted project '{slug}'!")
490
+ else:
491
+ self.status_bar_val.set("Mount Failed")
492
+ messagebox.showerror("Error", f"Failed to mount project '{slug}'. See logs for details.")
493
+
494
+ self.run_cli_async(args, on_done=on_done)
495
+
496
+ def action_mount_stop(self):
497
+ selected = self.mounts_list.get(tk.ACTIVE)
498
+ if not selected:
499
+ messagebox.showwarning("Warning", "Please select a mount to stop from the list.")
500
+ return
501
+
502
+ # Parse the mount point from the status line
503
+ # E.g. "W: \\\\127.0.0.1\\!" -> "W:"
504
+ # Or parse folder paths
505
+ parts = selected.split()
506
+ if not parts:
507
+ return
508
+
509
+ mount_point = parts[0]
510
+ args = ["mount", "stop", mount_point]
511
+
512
+ self.status_bar_val.set("Stopping mount...")
513
+ self.notebook.select(self.log_tab_idx)
514
+
515
+ def on_done(code, output):
516
+ self.refresh_mounts_list()
517
+ if code == 0:
518
+ self.status_bar_val.set("Mount Stopped")
519
+ messagebox.showinfo("Success", f"Successfully stopped mount at {mount_point}.")
520
+ else:
521
+ self.status_bar_val.set("Stop Failed")
522
+ messagebox.showerror("Error", f"Failed to stop mount at {mount_point}.")
523
+
524
+ self.run_cli_async(args, on_done=on_done)
525
+
526
+ # ------------------ COPY TAB ------------------
527
+ def build_copy_tab(self):
528
+ copy_tab = ttk.Frame(self.notebook, padding=15)
529
+ self.notebook.add(copy_tab, text="File Transfer")
530
+
531
+ # Source Group
532
+ src_frame = ttk.LabelFrame(copy_tab, text="Source Path", padding=10)
533
+ src_frame.pack(fill=tk.X, pady=5)
534
+ self.src_val = tk.StringVar()
535
+ ttk.Entry(src_frame, textvariable=self.src_val).pack(side=tk.LEFT, fill=tk.X, expand=True, padx=5)
536
+ ttk.Button(src_frame, text="Browse...", command=lambda: self.browse_path(self.src_val)).pack(side=tk.RIGHT)
537
+
538
+ # Swap button
539
+ swap_frame = ttk.Frame(copy_tab)
540
+ swap_frame.pack(fill=tk.X)
541
+ ttk.Button(swap_frame, text="<-> Swap Paths", style="Refresh.TButton", command=self.swap_paths).pack(pady=2)
542
+
543
+ # Destination Group
544
+ dest_frame = ttk.LabelFrame(copy_tab, text="Destination Path", padding=10)
545
+ dest_frame.pack(fill=tk.X, pady=5)
546
+ self.dest_val = tk.StringVar()
547
+ ttk.Entry(dest_frame, textvariable=self.dest_val).pack(side=tk.LEFT, fill=tk.X, expand=True, padx=5)
548
+ ttk.Button(dest_frame, text="Browse...", command=lambda: self.browse_path(self.dest_val)).pack(side=tk.RIGHT)
549
+
550
+ # Copy Action
551
+ btn_frame = ttk.Frame(copy_tab)
552
+ btn_frame.pack(fill=tk.X, pady=15)
553
+ ttk.Button(btn_frame, text="Start Transfer (Copy)", command=self.action_copy).pack(anchor=tk.CENTER)
554
+
555
+ # Tip
556
+ tip_lbl = ttk.Label(copy_tab, text="Tip: Remote paths must be structured as 'acc://[project-slug]/[file-or-folder]'.\nLocal paths can be chosen using the Browse button.", font=("Segoe UI", 9, "italic"), justify=tk.CENTER)
557
+ tip_lbl.pack(pady=10)
558
+
559
+ def browse_path(self, var_target):
560
+ # Open file dialog
561
+ path = filedialog.askopenfilename() or filedialog.askdirectory()
562
+ if path:
563
+ var_target.set(path)
564
+
565
+ def swap_paths(self):
566
+ s = self.src_val.get()
567
+ d = self.dest_val.get()
568
+ self.src_val.set(d)
569
+ self.dest_val.set(s)
570
+
571
+ def action_copy(self):
572
+ src = self.src_val.get().strip()
573
+ dest = self.dest_val.get().strip()
574
+ if not src or not dest:
575
+ messagebox.showerror("Error", "Please fill in both Source and Destination paths.")
576
+ return
577
+
578
+ args = ["copy", src, dest]
579
+
580
+ self.status_bar_val.set("Transferring files...")
581
+ self.notebook.select(self.log_tab_idx)
582
+
583
+ def on_done(code, output):
584
+ if code == 0:
585
+ self.status_bar_val.set("Transfer Completed")
586
+ messagebox.showinfo("Success", "File transfer finished successfully!")
587
+ else:
588
+ self.status_bar_val.set("Transfer Failed")
589
+ messagebox.showerror("Error", "File transfer failed. See logs for details.")
590
+
591
+ self.run_cli_async(args, on_done=on_done)
592
+
593
+ # ------------------ LOGS TAB ------------------
594
+ def build_logs_tab(self):
595
+ logs_tab = ttk.Frame(self.notebook, padding=10)
596
+ self.notebook.add(logs_tab, text="Log Console")
597
+ self.log_tab_idx = self.notebook.index(logs_tab)
598
+
599
+ # Log buttons
600
+ ctrl_frame = ttk.Frame(logs_tab)
601
+ ctrl_frame.pack(fill=tk.X, pady=(0, 5))
602
+
603
+ clear_btn = ttk.Button(ctrl_frame, text="Clear Console", style="Refresh.TButton", command=self.clear_logs)
604
+ clear_btn.pack(side=tk.LEFT)
605
+
606
+ self.log_txt = tk.Text(logs_tab, bg="#1e1e1e", fg="#d4d4d4", insertbackground="white", font=("Courier New", 10), state=tk.DISABLED)
607
+ self.log_txt.pack(fill=tk.BOTH, expand=True)
608
+
609
+ def clear_logs(self):
610
+ self.log_txt.configure(state=tk.NORMAL)
611
+ self.log_txt.delete("1.0", tk.END)
612
+ self.log_txt.configure(state=tk.DISABLED)
613
+
614
+ def main():
615
+ app = AccliGuiApp()
616
+ app.mainloop()
617
+
618
+ if __name__ == "__main__":
619
+ if len(sys.argv) > 1:
620
+ from accli.cli import app as cli_app
621
+ cli_app()
622
+ else:
623
+ main()
@@ -9,7 +9,7 @@ import typer
9
9
  from rich import print
10
10
  from rich.progress import Progress, TextColumn, BarColumn, TaskProgressColumn, TimeElapsedColumn
11
11
 
12
- DEFAULT_VERSION = "v0.6.5-acc-pr140"
12
+ DEFAULT_VERSION = "v0.6.5-acc-pr140-p4"
13
13
  BINARY_DIR = Path.home() / ".accli" / "bin"
14
14
 
15
15
  # Regex to safely validate version strings to prevent URL manipulation or directory traversal
@@ -140,12 +140,12 @@ def ensure_binaries(version: str = DEFAULT_VERSION, use_fuse: bool = False):
140
140
  if sys_name != "Windows":
141
141
  current_stat = dest_path.stat().st_mode
142
142
  dest_path.chmod(current_stat | stat.S_IXUSR | stat.S_IXGRP | stat.S_IXOTH)
143
- print(f"[bold green]✔ Successfully cached {bin_name}[/bold green]")
143
+ print(f"[bold green][OK] Successfully cached {bin_name}[/bold green]")
144
144
 
145
145
  except Exception as e:
146
146
  if part_path.exists():
147
147
  part_path.unlink()
148
- print(f"[bold red]✖ Failed to download {bin_name}: {e}[/bold red]")
148
+ print(f"[bold red][ERROR] Failed to download {bin_name}: {e}[/bold red]")
149
149
  raise typer.Exit(1)
150
150
 
151
151
  # After all downloads successfully complete, update the cached version file
@@ -0,0 +1,262 @@
1
+ <#
2
+ .SYNOPSIS
3
+ One-time administrative configuration script to enable Windows Client for NFS and register the Task Scheduler Mount Gateway.
4
+ .DESCRIPTION
5
+ This script performs the administrative setup required to allow standard users to mount
6
+ Accelerator project spaces on Windows over NFS with ZERO UAC prompts.
7
+ It can be run silently by IT Administrators using Microsoft Intune, Group Policy (GPO), or SCCM.
8
+ .PARAMETER NfsAlreadyEnabled
9
+ Bypasses enabling NFS Optional features if already enabled.
10
+ .PARAMETER RegAlreadyEnabled
11
+ Bypasses EnableLinkedConnections registry policy creation if already enabled.
12
+ .NOTES
13
+ This script is static and fully auditable by Security and Network Administrators.
14
+ #>
15
+
16
+ param (
17
+ [switch]$NfsAlreadyEnabled,
18
+ [switch]$RegAlreadyEnabled
19
+ )
20
+
21
+ # 1. Enable Windows Client for NFS Optional Features
22
+ if (-not $NfsAlreadyEnabled) {
23
+ Write-Host "Enabling Windows Client for NFS features..." -ForegroundColor Cyan
24
+
25
+ if (Get-Command Enable-WindowsOptionalFeature -ErrorAction SilentlyContinue) {
26
+ # Split into two separate calls to bypass Windows 11 comma-separated feature parser bug
27
+ Enable-WindowsOptionalFeature -Online -FeatureName ServicesForNFS-ClientOnly -All -NoRestart | Out-Null
28
+ Enable-WindowsOptionalFeature -Online -FeatureName ClientForNFS-Infrastructure -All -NoRestart | Out-Null
29
+ } elseif (Get-Command Install-WindowsFeature -ErrorAction SilentlyContinue) {
30
+ # Fallback for Windows Server OS
31
+ Install-WindowsFeature -Name NFS-Client | Out-Null
32
+ }
33
+ Write-Host "[OK] Enabled Client for NFS features." -ForegroundColor Green
34
+ } else {
35
+ Write-Host "[OK] Client for NFS features already enabled." -ForegroundColor Green
36
+ }
37
+
38
+ # 2. Add Registry value for Linked Connections
39
+ # Allows mapped network drives to be shared between elevated and standard sessions under the same user log-in.
40
+ if (-not $RegAlreadyEnabled) {
41
+ Write-Host "Configuring EnableLinkedConnections registry policy..." -ForegroundColor Cyan
42
+
43
+ $regPath = "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Policies\System"
44
+ if (-not (Test-Path $regPath)) {
45
+ New-Item -Path $regPath -Force | Out-Null
46
+ }
47
+ New-ItemProperty -Path $regPath -Name "EnableLinkedConnections" -Value 1 -PropertyType DWORD -Force | Out-Null
48
+ Write-Host "[OK] Registry value configured." -ForegroundColor Green
49
+ } else {
50
+ Write-Host "[OK] Registry value already configured." -ForegroundColor Green
51
+ }
52
+
53
+ # 2.5 Configure Anonymous UID/GID to match hf-mount-nfs (UID 0)
54
+ if (-not $RegAlreadyEnabled) {
55
+ Write-Host "Configuring Anonymous UID/GID for NFS Client..." -ForegroundColor Cyan
56
+ $clientForNfsPath = "HKLM:\SOFTWARE\Microsoft\ClientForNFS\CurrentVersion\Default"
57
+ if (-not (Test-Path $clientForNfsPath)) {
58
+ New-Item -Path $clientForNfsPath -Force | Out-Null
59
+ }
60
+ New-ItemProperty -Path $clientForNfsPath -Name "AnonymousUid" -Value 0 -PropertyType DWORD -Force | Out-Null
61
+ New-ItemProperty -Path $clientForNfsPath -Name "AnonymousGid" -Value 0 -PropertyType DWORD -Force | Out-Null
62
+ Write-Host "[OK] Anonymous UID/GID configured." -ForegroundColor Green
63
+ } else {
64
+ Write-Host "[OK] Anonymous UID/GID registry values already configured (skipped)." -ForegroundColor Green
65
+ }
66
+
67
+ # 3. Create public ProgramData folder and configure permissions for Authenticated Users
68
+ Write-Host "Initializing public accli configuration directory..." -ForegroundColor Cyan
69
+ $configDir = "C:\ProgramData\accli"
70
+ if (-not (Test-Path $configDir)) {
71
+ New-Item -ItemType Directory -Path $configDir -Force | Out-Null
72
+ }
73
+
74
+ try {
75
+ $acl = Get-Acl $configDir
76
+ # Grant Modify (Read, Write, Execute, Delete) to all Authenticated Users so non-admin users can write the mount_config.json
77
+ $ar = New-Object System.Security.AccessControl.FileSystemAccessRule("Authenticated Users", "Modify", "ContainerInherit,ObjectInherit", "None", "Allow")
78
+ $acl.SetAccessRule($ar)
79
+ Set-Acl $configDir $acl
80
+ Write-Host "[OK] Directory permissions configured." -ForegroundColor Green
81
+ } catch {
82
+ Write-Warning "Could not configure ProgramData permissions. Standalone non-admin users might not be able to write configuration."
83
+ }
84
+
85
+ # 4. Write static run-mount-helper.ps1 static helper
86
+ Write-Host "Registering run-mount-helper script..." -ForegroundColor Cyan
87
+ $helperScript = @'
88
+ $configPath = "C:\ProgramData\accli\mount_config.json"
89
+ if (-not (Test-Path $configPath)) {
90
+ Write-Error "Config file not found at $configPath."
91
+ exit 1
92
+ }
93
+
94
+ try {
95
+ $cfg = Get-Content $configPath -Raw | ConvertFrom-Json
96
+ } catch {
97
+ Write-Error "Failed to parse $configPath."
98
+ exit 1
99
+ }
100
+
101
+ # Construct environment
102
+ $env:PATH = "C:\Windows\System32\downlevel;" + $env:PATH
103
+ $env:ACCELERATOR_MOUNT = "1"
104
+ $env:ACC_ENDPOINT = $cfg.server_url
105
+ $env:ACC_CAS_ENDPOINT = "$($cfg.server_url.TrimEnd('/'))/api/xet-cas"
106
+ $env:ACC_TOKEN = $cfg.token
107
+ if ($cfg.skip_auto_mount) {
108
+ $env:HF_MOUNT_SKIP_AUTO_MOUNT = "1"
109
+ }
110
+
111
+ # Construct arguments
112
+ $mountArgs = @(
113
+ "--token-file", $cfg.db_path,
114
+ "--hub-endpoint", $cfg.server_url,
115
+ $cfg.mode,
116
+ $cfg.project_slug,
117
+ $cfg.mount_point
118
+ )
119
+
120
+ if ($cfg.overlay) { $mountArgs += "--overlay" }
121
+ if ($cfg.read_only) { $mountArgs += "--read-only" }
122
+
123
+ # Prepare background logger
124
+ $logFile = "C:\ProgramData\accli\mount.log"
125
+ $errFile = "C:\ProgramData\accli\mount_err.log"
126
+ try {
127
+ # Run the background daemon process elevated (since task runs as SYSTEM)
128
+ & $cfg.hf_mount_bin @mountArgs > $logFile 2> $errFile
129
+ if ($LASTEXITCODE -ne 0 -and $LASTEXITCODE -ne $null) {
130
+ Add-Content -Path $errFile -Value "Process exited with code `$LASTEXITCODE"
131
+ exit `$LASTEXITCODE
132
+ }
133
+ } catch {
134
+ Write-Error "Failed to execute hf-mount-nfs.exe."
135
+ exit 1
136
+ }
137
+ '@
138
+
139
+ $helperScriptPath = "C:\ProgramData\accli\run-mount-helper.ps1"
140
+ Set-Content -Path $helperScriptPath -Value $helperScript -Encoding UTF8
141
+ Write-Host "[OK] Mount helper script registered." -ForegroundColor Green
142
+
143
+ # 4.5 Write static run-umount-helper.ps1 static helper
144
+ Write-Host "Registering run-umount-helper script..." -ForegroundColor Cyan
145
+ $helperUmountScript = @'
146
+ try {
147
+ if (Get-Command Stop-ScheduledTask -ErrorAction SilentlyContinue) {
148
+ Stop-ScheduledTask -TaskName "accli-mount-nfs" -ErrorAction SilentlyContinue | Out-Null
149
+ } else {
150
+ schtasks /end /tn "accli-mount-nfs" 2> $null | Out-Null
151
+ }
152
+ } catch {}
153
+
154
+ try {
155
+ Get-CimInstance Win32_Process -Filter "name='hf-mount-nfs.exe'" | ForEach-Object {
156
+ Stop-Process -Id $_.ProcessId -Force -ErrorAction SilentlyContinue | Out-Null
157
+ }
158
+ } catch {}
159
+ '@
160
+
161
+ $helperUmountScriptPath = "C:\ProgramData\accli\run-umount-helper.ps1"
162
+ Set-Content -Path $helperUmountScriptPath -Value $helperUmountScript -Encoding UTF8
163
+ Write-Host "[OK] Umount helper script registered." -ForegroundColor Green
164
+
165
+ # 5. Register the Scheduled Task (accli-mount-nfs)
166
+ Write-Host "Registering accli-mount-nfs Scheduled Task..." -ForegroundColor Cyan
167
+ $taskName = "accli-mount-nfs"
168
+
169
+ # Helper block executing PowerShell in Task Action
170
+ $psActionPath = "powershell.exe"
171
+ $psActionArgs = "-NoProfile -ExecutionPolicy Bypass -File `"$helperScriptPath`""
172
+
173
+ if (Get-Command Register-ScheduledTask -ErrorAction SilentlyContinue) {
174
+ try {
175
+ $action = New-ScheduledTaskAction -Execute $psActionPath -Argument $psActionArgs
176
+ $principal = New-ScheduledTaskPrincipal -UserId "SYSTEM" -RunLevel Highest
177
+ $settings = New-ScheduledTaskSettingsSet -AllowStartIfOnBatteries -DontStopIfGoingOnBatteries
178
+
179
+ # Unregister task if already exists to update
180
+ Unregister-ScheduledTask -TaskName $taskName -Confirm:$false -ErrorAction SilentlyContinue | Out-Null
181
+ Register-ScheduledTask -TaskName $taskName -Action $action -Principal $principal -Settings $settings -Force | Out-Null
182
+
183
+ Write-Host "[OK] Scheduled Task registered successfully." -ForegroundColor Green
184
+ } catch {
185
+ # Fallback to schtasks.exe if cmdlet fails
186
+ schtasks /delete /tn $taskName /f 2> $null | Out-Null
187
+ schtasks /create /tn $taskName /tr "$psActionPath $psActionArgs" /sc ONCE /sd "2026/01/01" /st "00:00" /ru "SYSTEM" /rl "HIGHEST" /f | Out-Null
188
+ Write-Host "[OK] Scheduled Task registered successfully." -ForegroundColor Green
189
+ }
190
+ } else {
191
+ # Legacy fallback
192
+ schtasks /delete /tn $taskName /f 2> $null | Out-Null
193
+ schtasks /create /tn $taskName /tr "$psActionPath $psActionArgs" /sc ONCE /sd "2026/01/01" /st "00:00" /ru "SYSTEM" /rl "HIGHEST" /f | Out-Null
194
+ Write-Host "[OK] Scheduled Task registered successfully." -ForegroundColor Green
195
+ }
196
+
197
+ # 5.5 Register the Scheduled Task (accli-umount-nfs)
198
+ Write-Host "Registering accli-umount-nfs Scheduled Task..." -ForegroundColor Cyan
199
+ $umountTaskName = "accli-umount-nfs"
200
+ $psUmountActionArgs = "-NoProfile -ExecutionPolicy Bypass -File `"$helperUmountScriptPath`""
201
+
202
+ if (Get-Command Register-ScheduledTask -ErrorAction SilentlyContinue) {
203
+ try {
204
+ $action = New-ScheduledTaskAction -Execute $psActionPath -Argument $psUmountActionArgs
205
+ $principal = New-ScheduledTaskPrincipal -UserId "SYSTEM" -RunLevel Highest
206
+ $settings = New-ScheduledTaskSettingsSet -AllowStartIfOnBatteries -DontStopIfGoingOnBatteries
207
+
208
+ Unregister-ScheduledTask -TaskName $umountTaskName -Confirm:$false -ErrorAction SilentlyContinue | Out-Null
209
+ Register-ScheduledTask -TaskName $umountTaskName -Action $action -Principal $principal -Settings $settings -Force | Out-Null
210
+
211
+ Write-Host "[OK] Umount Scheduled Task registered successfully." -ForegroundColor Green
212
+ } catch {
213
+ schtasks /delete /tn $umountTaskName /f 2> $null | Out-Null
214
+ schtasks /create /tn $umountTaskName /tr "$psActionPath $psUmountActionArgs" /sc ONCE /sd "2026/01/01" /st "00:00" /ru "SYSTEM" /rl "HIGHEST" /f | Out-Null
215
+ Write-Host "[OK] Umount Scheduled Task registered successfully." -ForegroundColor Green
216
+ }
217
+ } else {
218
+ schtasks /delete /tn $umountTaskName /f 2> $null | Out-Null
219
+ schtasks /create /tn $umountTaskName /tr "$psActionPath $psUmountActionArgs" /sc ONCE /sd "2026/01/01" /st "00:00" /ru "SYSTEM" /rl "HIGHEST" /f | Out-Null
220
+ Write-Host "[OK] Umount Scheduled Task registered successfully." -ForegroundColor Green
221
+ }
222
+
223
+ # Grant Authenticated Users permission to read, query, and run the task files
224
+ foreach ($tName in @("accli-mount-nfs", "accli-umount-nfs")) {
225
+ $taskFile = "C:\Windows\System32\Tasks\$tName"
226
+ if (Test-Path $taskFile) {
227
+ try {
228
+ $acl = Get-Acl $taskFile
229
+ $rule = New-Object System.Security.AccessControl.FileSystemAccessRule("Authenticated Users", "ReadAndExecute", "Allow")
230
+ $acl.AddAccessRule($rule)
231
+ Set-Acl $taskFile $acl
232
+ Write-Host "[OK] Task file permissions configured for $tName." -ForegroundColor Green
233
+ } catch {
234
+ Write-Warning "Could not configure task file permissions for $tName."
235
+ }
236
+ }
237
+ }
238
+
239
+ # Grant Authenticated Users permission to query and trigger the Task Scheduler Service task objects themselves via COM SDDL
240
+ try {
241
+ $Scheduler = New-Object -ComObject "Schedule.Service"
242
+ $Scheduler.Connect()
243
+
244
+ foreach ($tName in @("accli-mount-nfs", "accli-umount-nfs")) {
245
+ $TaskObject = $Scheduler.GetFolder("\").GetTask($tName)
246
+ $SDDL = $TaskObject.GetSecurityDescriptor(0xF)
247
+
248
+ # (A;;GRGX;;;AU) grants Read (GR) and Execute (GX) to Authenticated Users (AU)
249
+ if ($SDDL -notmatch 'A;;0x1200a9;;;AU' -and $SDDL -notmatch 'A;;GRGX;;;AU') {
250
+ $SDDL = $SDDL + '(A;;GRGX;;;AU)'
251
+ $TaskObject.SetSecurityDescriptor($SDDL, 0)
252
+ Write-Host "[OK] Task Scheduler COM Service permissions configured for $tName." -ForegroundColor Green
253
+ } else {
254
+ Write-Host "[OK] Task Scheduler COM Service permissions already configured for $tName." -ForegroundColor Green
255
+ }
256
+ }
257
+ } catch {
258
+ Write-Warning "Could not configure Task Scheduler COM permissions: $_"
259
+ }
260
+
261
+ Write-Host "Windows NFS Client Task Scheduler Gateway Setup Completed Successfully!" -ForegroundColor Green
262
+ Write-Host "⚠ Note: A Windows sign-out/sign-in or system restart is required for changes to take effect." -ForegroundColor Yellow
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: accli
3
- Version: 2.0.0b3
3
+ Version: 2.0.0rc1
4
4
  Summary: IIASA Accelerator Client
5
5
  Author-email: Wrufesh S <wrufesh@gmail.com>
6
6
  License: The MIT License (MIT)
@@ -8,9 +8,12 @@ accli/CsvRegionalTimeseriesValidator.py
8
8
  accli/__init__.py
9
9
  accli/__main__.py
10
10
  accli/_version.py
11
+ accli/cleanup-nfs.ps1
11
12
  accli/cli.py
12
13
  accli/common.py
14
+ accli/gui.py
13
15
  accli/mount_downloader.py
16
+ accli/setup-nfs.ps1
14
17
  accli/token.py
15
18
  accli.egg-info/PKG-INFO
16
19
  accli.egg-info/SOURCES.txt
@@ -1,2 +1,3 @@
1
1
  [console_scripts]
2
2
  accli = accli.cli:app
3
+ accli-gui = accli.gui:main
@@ -31,6 +31,9 @@ classifiers = [
31
31
  [tool.setuptools]
32
32
  packages = ['accli']
33
33
 
34
+ [tool.setuptools.package-data]
35
+ accli = ["*.ps1"]
36
+
34
37
  [tool.setuptools.dynamic]
35
38
  version = {attr="accli._version.VERSION"}
36
39
 
@@ -40,3 +43,4 @@ version = {attr="accli._version.VERSION"}
40
43
 
41
44
  [project.scripts]
42
45
  accli = "accli.cli:app"
46
+ accli-gui = "accli.gui:main"
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes