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.
- {accli-2.0.0b3 → accli-2.0.0rc1}/PKG-INFO +1 -1
- {accli-2.0.0b3 → accli-2.0.0rc1}/accli/AcceleratorTaskDispatcher.py +2 -2
- {accli-2.0.0b3 → accli-2.0.0rc1}/accli/CsvRegionalTimeseriesValidator.py +2 -2
- {accli-2.0.0b3 → accli-2.0.0rc1}/accli/_version.py +1 -1
- accli-2.0.0rc1/accli/cleanup-nfs.ps1 +57 -0
- {accli-2.0.0b3 → accli-2.0.0rc1}/accli/cli.py +95 -27
- accli-2.0.0rc1/accli/gui.py +623 -0
- {accli-2.0.0b3 → accli-2.0.0rc1}/accli/mount_downloader.py +3 -3
- accli-2.0.0rc1/accli/setup-nfs.ps1 +262 -0
- {accli-2.0.0b3 → accli-2.0.0rc1}/accli.egg-info/PKG-INFO +1 -1
- {accli-2.0.0b3 → accli-2.0.0rc1}/accli.egg-info/SOURCES.txt +3 -0
- {accli-2.0.0b3 → accli-2.0.0rc1}/accli.egg-info/entry_points.txt +1 -0
- {accli-2.0.0b3 → accli-2.0.0rc1}/pyproject.toml +4 -0
- {accli-2.0.0b3 → accli-2.0.0rc1}/LICENSE +0 -0
- {accli-2.0.0b3 → accli-2.0.0rc1}/README.md +0 -0
- {accli-2.0.0b3 → accli-2.0.0rc1}/accli/AcceleratorJobProjectService.py +0 -0
- {accli-2.0.0b3 → accli-2.0.0rc1}/accli/AcceleratorTerminalCliProjectService.py +0 -0
- {accli-2.0.0b3 → accli-2.0.0rc1}/accli/__init__.py +0 -0
- {accli-2.0.0b3 → accli-2.0.0rc1}/accli/__main__.py +0 -0
- {accli-2.0.0b3 → accli-2.0.0rc1}/accli/common.py +0 -0
- {accli-2.0.0b3 → accli-2.0.0rc1}/accli/token.py +0 -0
- {accli-2.0.0b3 → accli-2.0.0rc1}/accli.egg-info/dependency_links.txt +0 -0
- {accli-2.0.0b3 → accli-2.0.0rc1}/accli.egg-info/requires.txt +0 -0
- {accli-2.0.0b3 → accli-2.0.0rc1}/accli.egg-info/top_level.txt +0 -0
- {accli-2.0.0b3 → accli-2.0.0rc1}/setup.cfg +0 -0
|
@@ -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)
|
|
@@ -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
|
-
#
|
|
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
|
-
#
|
|
155
|
+
# Temporarily switch to the workflow file's directory
|
|
156
156
|
with pushd(workflow_dir):
|
|
157
|
-
#
|
|
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]
|
|
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]
|
|
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
|
-
|
|
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
|
-
#
|
|
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
|
-
|
|
1143
|
-
|
|
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
|
-
|
|
1152
|
-
|
|
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]
|
|
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]
|
|
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
|
|
@@ -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
|
|
@@ -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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|