Switch GST operator agent to custom protocol launch
This commit is contained in:
@@ -1,27 +1,25 @@
|
||||
ARRR GST Operator Agent 1.0.1
|
||||
ARRR GST Operator Agent 1.1.0
|
||||
|
||||
Purpose
|
||||
- Runs only on the GST operator's Windows workstation.
|
||||
- Listens only on 127.0.0.1:8791.
|
||||
- Exposes a trusted local HTTPS endpoint at https://localhost:8791.
|
||||
- Opens visible Chrome/Edge for GST Portal authentication and return download.
|
||||
- Autofills the selected Credential Vault username/password supplied through the ERP short-lived job token.
|
||||
- CAPTCHA/OTP remains interactive with the operator.
|
||||
- Does not run Tally and does not hold the client storage.
|
||||
- Completed GST packages continue through the ERP to the already configured Local Storage Agent.
|
||||
-------
|
||||
Runs only on the GST operator workstation. It opens visible Chrome/Edge, redeems a short-lived GST job token from ARRR ERP, fills the selected Credential Vault username/password, waits for manual CAPTCHA/OTP, downloads selected GST return data, and uploads the package back to ERP for transfer to the configured Storage Agent.
|
||||
|
||||
Install
|
||||
1. Extract the downloaded ZIP.
|
||||
2. Run PowerShell in the extracted ARRR_GST_Operator_Agent folder:
|
||||
powershell.exe -NoProfile -ExecutionPolicy Bypass -File .\install_gst_operator_agent.ps1
|
||||
3. The installer creates a localhost TLS certificate and trusts it only for the current Windows user.
|
||||
4. Verify https://localhost:8791/api/status.
|
||||
5. Refresh the ERP GST Return Reconciliation page.
|
||||
Architecture
|
||||
------------
|
||||
ARRR ERP starts the agent through the Windows custom URL protocol arrrgst://. No browser-to-localhost HTTP/HTTPS request is used. No localhost TLS certificate, local web server, Tally access, or client storage access is required on the operator workstation.
|
||||
|
||||
Upgrade from 1.0.0
|
||||
- Download the current agent ZIP from ERP and run install_gst_operator_agent.ps1 again.
|
||||
- The installer stops the old HTTP listener, replaces the runtime, installs the trusted localhost certificate and starts v1.0.1 over HTTPS.
|
||||
Installation
|
||||
------------
|
||||
Run install_gst_operator_agent.ps1 as the Windows user who will operate GST downloads. Administrator rights are not required. Python 3.11+ and installed Chrome or Edge are required.
|
||||
|
||||
Browser prompt
|
||||
--------------
|
||||
The first time ARRR ERP opens arrrgst://, Chrome/Edge may ask whether to open ARRR GST Operator Agent. Choose Open/Allow.
|
||||
|
||||
Security
|
||||
--------
|
||||
The custom URL contains only a short-lived signed job token. GST username/password are redeemed by the local agent directly from ARRR ERP and are never inserted in ERP page HTML or in the custom URL. CAPTCHA/OTP remains manual.
|
||||
|
||||
Uninstall
|
||||
- Run uninstall_gst_operator_agent.ps1 from the installed/downloaded package.
|
||||
- The installer-created certificate is also removed from the current user's trusted root store.
|
||||
---------
|
||||
Run uninstall_gst_operator_agent.ps1. It removes the protocol registration and also cleans legacy v1.0.x localhost listener/startup/certificate state.
|
||||
|
||||
@@ -1,59 +0,0 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import ipaddress
|
||||
from datetime import datetime, timedelta, timezone
|
||||
from pathlib import Path
|
||||
|
||||
from cryptography import x509
|
||||
from cryptography.hazmat.primitives import hashes, serialization
|
||||
from cryptography.hazmat.primitives.asymmetric import rsa
|
||||
from cryptography.x509.oid import NameOID
|
||||
|
||||
ROOT = Path(__file__).resolve().parent
|
||||
CERT_PEM = ROOT / "localhost-cert.pem"
|
||||
KEY_PEM = ROOT / "localhost-key.pem"
|
||||
CERT_DER = ROOT / "localhost-cert.cer"
|
||||
|
||||
|
||||
def main() -> None:
|
||||
key = rsa.generate_private_key(public_exponent=65537, key_size=2048)
|
||||
name = x509.Name([
|
||||
x509.NameAttribute(NameOID.COMMON_NAME, "ARRR GST Operator Agent localhost"),
|
||||
x509.NameAttribute(NameOID.ORGANIZATION_NAME, "ARRR & Associates"),
|
||||
])
|
||||
now = datetime.now(timezone.utc)
|
||||
cert = (
|
||||
x509.CertificateBuilder()
|
||||
.subject_name(name)
|
||||
.issuer_name(name)
|
||||
.public_key(key.public_key())
|
||||
.serial_number(x509.random_serial_number())
|
||||
.not_valid_before(now - timedelta(days=1))
|
||||
.not_valid_after(now + timedelta(days=825))
|
||||
.add_extension(
|
||||
x509.SubjectAlternativeName([
|
||||
x509.DNSName("localhost"),
|
||||
x509.IPAddress(ipaddress.ip_address("127.0.0.1")),
|
||||
]),
|
||||
critical=False,
|
||||
)
|
||||
.add_extension(x509.BasicConstraints(ca=False, path_length=None), critical=True)
|
||||
.add_extension(
|
||||
x509.ExtendedKeyUsage([x509.oid.ExtendedKeyUsageOID.SERVER_AUTH]),
|
||||
critical=False,
|
||||
)
|
||||
.sign(key, hashes.SHA256())
|
||||
)
|
||||
KEY_PEM.write_bytes(
|
||||
key.private_bytes(
|
||||
encoding=serialization.Encoding.PEM,
|
||||
format=serialization.PrivateFormat.TraditionalOpenSSL,
|
||||
encryption_algorithm=serialization.NoEncryption(),
|
||||
)
|
||||
)
|
||||
CERT_PEM.write_bytes(cert.public_bytes(serialization.Encoding.PEM))
|
||||
CERT_DER.write_bytes(cert.public_bytes(serialization.Encoding.DER))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -3,19 +3,17 @@ from __future__ import annotations
|
||||
import json
|
||||
import re
|
||||
import shutil
|
||||
import ssl
|
||||
import threading
|
||||
import time
|
||||
import zipfile
|
||||
from datetime import datetime, timezone
|
||||
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
|
||||
from pathlib import Path
|
||||
from urllib.parse import parse_qs, urlsplit
|
||||
import sys
|
||||
import traceback
|
||||
|
||||
import requests
|
||||
|
||||
VERSION = "1.0.1"
|
||||
PORT = 8791
|
||||
VERSION = "1.1.0"
|
||||
ROOT = Path(__file__).resolve().parent
|
||||
DATA = ROOT / "data"
|
||||
CONFIG_PATH = ROOT / "config.json"
|
||||
@@ -26,7 +24,6 @@ GSTR1_DOWNLOAD_URL = "https://return.gst.gov.in/returns/auth/api/offline/downloa
|
||||
GSTR2B_URL = "https://gstr2b.gst.gov.in/gstr2b/auth/api/gstr2b/getjson?rtnprd={period}"
|
||||
GSTR3B_SUMMARY_URL = "https://return.gst.gov.in/returns/auth/api/gstr3b/summary?rtn_prd={period}"
|
||||
GSTR3B_URL = "https://return.gst.gov.in/returns/auth/api/gstr3b/taxpayble?rtn_prd={period}"
|
||||
THREADS: dict[str, threading.Thread] = {}
|
||||
|
||||
|
||||
def now() -> str:
|
||||
@@ -37,7 +34,7 @@ def read_config() -> dict:
|
||||
try:
|
||||
return json.loads(CONFIG_PATH.read_text(encoding="utf-8"))
|
||||
except Exception:
|
||||
return {"erp_base_url": "https://office.arrrassociates.com", "port": PORT, "certfile": str(ROOT / "localhost-cert.pem"), "keyfile": str(ROOT / "localhost-key.pem")}
|
||||
return {"erp_base_url": "https://office.arrrassociates.com", "protocol": "arrrgst"}
|
||||
|
||||
|
||||
def safe(value: str, fallback: str = "item") -> str:
|
||||
@@ -275,12 +272,40 @@ def browser_worker(payload: dict, token: str, path: Path) -> None:
|
||||
pass
|
||||
|
||||
|
||||
def start_job(token: str) -> dict:
|
||||
def _log(message: str) -> None:
|
||||
try:
|
||||
DATA.mkdir(parents=True, exist_ok=True)
|
||||
with (DATA / "operator_agent.log").open("a", encoding="utf-8") as handle:
|
||||
handle.write(f"[{now()}] {message}\n")
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
|
||||
def _show_error(message: str) -> None:
|
||||
_log("ERROR: " + message)
|
||||
try:
|
||||
import tkinter as tk
|
||||
from tkinter import messagebox
|
||||
root = tk.Tk()
|
||||
root.withdraw()
|
||||
messagebox.showerror("ARRR GST Operator Agent", message)
|
||||
root.destroy()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
|
||||
def redeem_job(token: str) -> dict:
|
||||
cfg = read_config()
|
||||
erp = str(cfg.get("erp_base_url") or "").rstrip("/")
|
||||
if not token:
|
||||
raise ValueError("GST operator token is required.")
|
||||
response = requests.post(erp + "/tools/accounting/gst-reconciliation/operator/redeem", json={"token": token}, timeout=30)
|
||||
if not erp.startswith("https://"):
|
||||
raise RuntimeError("ERP base URL must use HTTPS.")
|
||||
response = requests.post(
|
||||
erp + "/tools/accounting/gst-reconciliation/operator/redeem",
|
||||
json={"token": token},
|
||||
timeout=30,
|
||||
)
|
||||
try:
|
||||
body = response.json()
|
||||
except Exception:
|
||||
@@ -291,88 +316,75 @@ def start_job(token: str) -> dict:
|
||||
job_id = str(payload.get("jti") or "")
|
||||
if not job_id:
|
||||
raise RuntimeError("ERP did not return a GST job id.")
|
||||
return payload
|
||||
|
||||
|
||||
def run_job_foreground(token: str) -> None:
|
||||
payload = redeem_job(token)
|
||||
job_id = str(payload.get("jti") or "")
|
||||
path = job_path(job_id)
|
||||
current = read_json(path)
|
||||
if current.get("status") in {"queued", "running"}:
|
||||
return current
|
||||
initial = {
|
||||
"status": "queued", "percent": 1, "stage": "Queued", "message": "GST browser job queued.",
|
||||
"job_id": job_id, "periods": payload.get("periods") or [], "return_types": payload.get("return_types") or [],
|
||||
"status": "queued", "percent": 1, "stage": "Queued",
|
||||
"message": "GST browser job launched from ARRR ERP.",
|
||||
"job_id": job_id, "periods": payload.get("periods") or [],
|
||||
"return_types": payload.get("return_types") or [],
|
||||
"started_at_utc": now(), "updated_at_utc": now(),
|
||||
}
|
||||
write_json(path, initial)
|
||||
thread = threading.Thread(target=browser_worker, args=(payload, token, path), daemon=True, name=f"gst-{job_id}")
|
||||
THREADS[job_id] = thread
|
||||
thread.start()
|
||||
return initial
|
||||
_log(f"Starting GST job {job_id}")
|
||||
browser_worker(payload, token, path)
|
||||
final = read_json(path)
|
||||
if final.get("status") == "failed":
|
||||
_show_error(str(final.get("error") or final.get("message") or "GST download failed."))
|
||||
else:
|
||||
_log(f"Completed GST job {job_id}")
|
||||
|
||||
|
||||
class Handler(BaseHTTPRequestHandler):
|
||||
def log_message(self, fmt, *args):
|
||||
return
|
||||
def handle_protocol_url(raw_url: str) -> None:
|
||||
parsed = urlsplit(str(raw_url or "").strip())
|
||||
if parsed.scheme.lower() != "arrrgst":
|
||||
raise ValueError("Invalid ARRR GST protocol URL.")
|
||||
action = (parsed.netloc or parsed.path.lstrip("/") or "").lower()
|
||||
if action != "start":
|
||||
raise ValueError("Unsupported ARRR GST action.")
|
||||
token = str((parse_qs(parsed.query).get("token") or [""])[0]).strip()
|
||||
if not token:
|
||||
raise ValueError("GST operator token is missing from the launch request.")
|
||||
run_job_foreground(token)
|
||||
|
||||
def cors(self):
|
||||
cfg = read_config()
|
||||
origin = self.headers.get("Origin") or ""
|
||||
allowed = str(cfg.get("erp_base_url") or "").rstrip("/")
|
||||
if origin.rstrip("/") == allowed:
|
||||
self.send_header("Access-Control-Allow-Origin", origin)
|
||||
self.send_header("Vary", "Origin")
|
||||
if (self.headers.get("Access-Control-Request-Private-Network") or "").lower() == "true":
|
||||
self.send_header("Access-Control-Allow-Private-Network", "true")
|
||||
|
||||
def reply(self, payload, status=200):
|
||||
data = json.dumps(payload, ensure_ascii=False, default=str).encode("utf-8")
|
||||
self.send_response(status)
|
||||
self.send_header("Content-Type", "application/json; charset=utf-8")
|
||||
self.send_header("Cache-Control", "no-store")
|
||||
self.cors()
|
||||
self.send_header("Content-Length", str(len(data)))
|
||||
self.end_headers()
|
||||
self.wfile.write(data)
|
||||
|
||||
def do_OPTIONS(self):
|
||||
self.send_response(204)
|
||||
self.cors()
|
||||
self.send_header("Access-Control-Allow-Methods", "GET, POST, OPTIONS")
|
||||
self.send_header("Access-Control-Allow-Headers", "Content-Type")
|
||||
self.end_headers()
|
||||
|
||||
def do_GET(self):
|
||||
path = urlsplit(self.path)
|
||||
if path.path == "/api/status":
|
||||
return self.reply({"ok": True, "name": "ARRR GST Operator Agent", "version": VERSION, "port": PORT, "transport": "https", "host": "localhost"})
|
||||
if path.path == "/api/gst/status":
|
||||
job_id = str((parse_qs(path.query).get("job_id") or [""])[0])
|
||||
return self.reply({"ok": True, "job": read_json(job_path(job_id))})
|
||||
return self.reply({"ok": False, "error": "Not found"}, 404)
|
||||
|
||||
def do_POST(self):
|
||||
try:
|
||||
if urlsplit(self.path).path != "/api/gst/start":
|
||||
return self.reply({"ok": False, "error": "Not found"}, 404)
|
||||
length = int(self.headers.get("Content-Length") or 0)
|
||||
body = json.loads((self.rfile.read(length) if length else b"{}").decode("utf-8"))
|
||||
job = start_job(str(body.get("token") or ""))
|
||||
return self.reply({"ok": True, "job": job})
|
||||
except Exception as exc:
|
||||
return self.reply({"ok": False, "error": str(exc)}, 400)
|
||||
def self_test() -> int:
|
||||
cfg = read_config()
|
||||
erp = str(cfg.get("erp_base_url") or "")
|
||||
if not erp.startswith("https://"):
|
||||
print("ERROR: ERP base URL must use HTTPS.")
|
||||
return 1
|
||||
try:
|
||||
import playwright # noqa: F401
|
||||
except Exception as exc:
|
||||
print(f"ERROR: Playwright is unavailable: {exc}")
|
||||
return 1
|
||||
print(f"ARRR GST Operator Agent {VERSION} ready")
|
||||
print(f"ERP: {erp}")
|
||||
print("Protocol: arrrgst://start?token=<short-lived-token>")
|
||||
return 0
|
||||
|
||||
|
||||
def main():
|
||||
DATA.mkdir(parents=True, exist_ok=True)
|
||||
cfg = read_config()
|
||||
port = int(cfg.get("port") or PORT)
|
||||
certfile = Path(str(cfg.get("certfile") or (ROOT / "localhost-cert.pem")))
|
||||
keyfile = Path(str(cfg.get("keyfile") or (ROOT / "localhost-key.pem")))
|
||||
if not certfile.is_file() or not keyfile.is_file():
|
||||
raise RuntimeError("GST Operator Agent localhost TLS certificate is missing. Re-run the agent installer.")
|
||||
server = ThreadingHTTPServer(("127.0.0.1", port), Handler)
|
||||
context = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER)
|
||||
context.minimum_version = ssl.TLSVersion.TLSv1_2
|
||||
context.load_cert_chain(certfile=str(certfile), keyfile=str(keyfile))
|
||||
server.socket = context.wrap_socket(server.socket, server_side=True)
|
||||
server.serve_forever()
|
||||
if len(sys.argv) >= 2 and sys.argv[1] == "--self-test":
|
||||
raise SystemExit(self_test())
|
||||
if len(sys.argv) >= 2 and str(sys.argv[1]).lower().startswith("arrrgst:"):
|
||||
try:
|
||||
handle_protocol_url(sys.argv[1])
|
||||
except Exception as exc:
|
||||
_log(traceback.format_exc())
|
||||
_show_error(str(exc))
|
||||
raise SystemExit(1)
|
||||
return
|
||||
print(f"ARRR GST Operator Agent {VERSION}")
|
||||
print("This agent is launched by the arrrgst:// Windows protocol from ARRR ERP.")
|
||||
print("Run with --self-test to validate this installation.")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
@@ -7,10 +7,25 @@ $InstallRoot = Join-Path $env:LOCALAPPDATA "ARRR\GSTOperatorAgent"
|
||||
$Venv = Join-Path $InstallRoot ".venv"
|
||||
$Startup = Join-Path $env:APPDATA "Microsoft\Windows\Start Menu\Programs\Startup\ARRR GST Operator Agent.cmd"
|
||||
$ThumbprintFile = Join-Path $InstallRoot "cert_thumbprint.txt"
|
||||
$ProtocolKey = "HKCU:\Software\Classes\arrrgst"
|
||||
|
||||
Write-Host "ARRR GST Operator Agent 1.0.1" -ForegroundColor Cyan
|
||||
Write-Host "ARRR GST Operator Agent 1.1.0 - clean installation" -ForegroundColor Cyan
|
||||
Write-Host "Install root: $InstallRoot"
|
||||
|
||||
# Clean legacy v1.0.x listener/startup/certificate/protocol state first.
|
||||
try {
|
||||
Get-NetTCPConnection -LocalPort 8791 -State Listen -ErrorAction SilentlyContinue | ForEach-Object {
|
||||
if ($_.OwningProcess) { Stop-Process -Id $_.OwningProcess -Force -ErrorAction SilentlyContinue }
|
||||
}
|
||||
} catch {}
|
||||
if (Test-Path $ThumbprintFile) {
|
||||
$Thumbprint = (Get-Content $ThumbprintFile -Raw -ErrorAction SilentlyContinue).Trim()
|
||||
if ($Thumbprint) { Remove-Item "Cert:\CurrentUser\Root\$Thumbprint" -Force -ErrorAction SilentlyContinue }
|
||||
}
|
||||
Remove-Item $Startup -Force -ErrorAction SilentlyContinue
|
||||
Remove-Item $ProtocolKey -Recurse -Force -ErrorAction SilentlyContinue
|
||||
Remove-Item $InstallRoot -Recurse -Force -ErrorAction SilentlyContinue
|
||||
|
||||
$Python = $null
|
||||
if (Get-Command py -ErrorAction SilentlyContinue) { $Python = @("py", "-3") }
|
||||
elseif (Get-Command python -ErrorAction SilentlyContinue) { $Python = @("python") }
|
||||
@@ -19,7 +34,6 @@ else { throw "Python 3 is required on this workstation. Install Python 3.11+ and
|
||||
New-Item -ItemType Directory -Path $InstallRoot -Force | Out-Null
|
||||
$RuntimeFiles = @(
|
||||
"gst_operator_agent.py",
|
||||
"generate_localhost_cert.py",
|
||||
"requirements.txt",
|
||||
"README.txt",
|
||||
"uninstall_gst_operator_agent.ps1"
|
||||
@@ -32,58 +46,39 @@ foreach ($Name in $RuntimeFiles) {
|
||||
|
||||
$config = @{
|
||||
erp_base_url = $ErpBaseUrl.TrimEnd('/')
|
||||
port = 8791
|
||||
certfile = (Join-Path $InstallRoot "localhost-cert.pem")
|
||||
keyfile = (Join-Path $InstallRoot "localhost-key.pem")
|
||||
protocol = "arrrgst"
|
||||
} | ConvertTo-Json
|
||||
[System.IO.File]::WriteAllText((Join-Path $InstallRoot "config.json"), $config, [System.Text.UTF8Encoding]::new($false))
|
||||
|
||||
if (-not (Test-Path (Join-Path $Venv "Scripts\python.exe"))) {
|
||||
if ($Python.Count -eq 2) { & $Python[0] $Python[1] -m venv $Venv }
|
||||
else { & $Python[0] -m venv $Venv }
|
||||
if ($LASTEXITCODE -ne 0) { throw "Could not create GST Operator Agent virtual environment." }
|
||||
}
|
||||
if ($Python.Count -eq 2) { & $Python[0] $Python[1] -m venv $Venv }
|
||||
else { & $Python[0] -m venv $Venv }
|
||||
if ($LASTEXITCODE -ne 0) { throw "Could not create GST Operator Agent virtual environment." }
|
||||
|
||||
$VenvPython = Join-Path $Venv "Scripts\python.exe"
|
||||
$Pythonw = Join-Path $Venv "Scripts\pythonw.exe"
|
||||
& $VenvPython -m pip install --disable-pip-version-check --upgrade pip
|
||||
if ($LASTEXITCODE -ne 0) { throw "pip upgrade failed." }
|
||||
& $VenvPython -m pip install --disable-pip-version-check -r (Join-Path $InstallRoot "requirements.txt")
|
||||
if ($LASTEXITCODE -ne 0) { throw "GST Operator Agent dependencies could not be installed." }
|
||||
|
||||
# Remove only the certificate previously installed by this agent instance.
|
||||
if (Test-Path $ThumbprintFile) {
|
||||
$OldThumbprint = (Get-Content $ThumbprintFile -Raw -ErrorAction SilentlyContinue).Trim()
|
||||
if ($OldThumbprint) {
|
||||
Remove-Item "Cert:\CurrentUser\Root\$OldThumbprint" -Force -ErrorAction SilentlyContinue
|
||||
}
|
||||
}
|
||||
# Register per-user custom protocol. No administrator rights, localhost server, TLS certificate, or startup task is required.
|
||||
New-Item -Path $ProtocolKey -Force | Out-Null
|
||||
Set-ItemProperty -Path $ProtocolKey -Name "(Default)" -Value "URL:ARRR GST Operator Agent"
|
||||
New-ItemProperty -Path $ProtocolKey -Name "URL Protocol" -Value "" -PropertyType String -Force | Out-Null
|
||||
New-Item -Path "$ProtocolKey\DefaultIcon" -Force | Out-Null
|
||||
Set-ItemProperty -Path "$ProtocolKey\DefaultIcon" -Name "(Default)" -Value "`"$Pythonw`",0"
|
||||
New-Item -Path "$ProtocolKey\shell\open\command" -Force | Out-Null
|
||||
$Command = "`"$Pythonw`" `"$InstallRoot\gst_operator_agent.py`" `"%1`""
|
||||
Set-ItemProperty -Path "$ProtocolKey\shell\open\command" -Name "(Default)" -Value $Command
|
||||
|
||||
# Create a localhost-only TLS certificate and trust it for the current Windows user.
|
||||
& $VenvPython (Join-Path $InstallRoot "generate_localhost_cert.py")
|
||||
if ($LASTEXITCODE -ne 0) { throw "Could not create the GST Operator Agent localhost certificate." }
|
||||
$CertFile = Join-Path $InstallRoot "localhost-cert.cer"
|
||||
$ImportedCert = Import-Certificate -FilePath $CertFile -CertStoreLocation "Cert:\CurrentUser\Root"
|
||||
if (-not $ImportedCert -or -not $ImportedCert.Thumbprint) { throw "Could not trust the GST Operator Agent localhost certificate." }
|
||||
[System.IO.File]::WriteAllText($ThumbprintFile, $ImportedCert.Thumbprint, [System.Text.ASCIIEncoding]::new())
|
||||
& $VenvPython (Join-Path $InstallRoot "gst_operator_agent.py") --self-test
|
||||
if ($LASTEXITCODE -ne 0) { throw "GST Operator Agent self-test failed." }
|
||||
|
||||
$Pythonw = Join-Path $Venv "Scripts\pythonw.exe"
|
||||
$Cmd = "@echo off`r`nstart `"`" `"$Pythonw`" `"$InstallRoot\gst_operator_agent.py`"`r`n"
|
||||
[System.IO.File]::WriteAllText($Startup, $Cmd, [System.Text.ASCIIEncoding]::new())
|
||||
$Registered = (Get-ItemProperty "$ProtocolKey\shell\open\command" -ErrorAction Stop)."(default)"
|
||||
if (-not $Registered) { throw "ARRR GST custom protocol registration failed." }
|
||||
|
||||
try {
|
||||
$Connections = Get-NetTCPConnection -LocalPort 8791 -State Listen -ErrorAction SilentlyContinue
|
||||
foreach ($Connection in $Connections) {
|
||||
if ($Connection.OwningProcess) { Stop-Process -Id $Connection.OwningProcess -Force -ErrorAction SilentlyContinue }
|
||||
}
|
||||
} catch {}
|
||||
|
||||
Start-Process -FilePath $Pythonw -ArgumentList @((Join-Path $InstallRoot "gst_operator_agent.py")) -WorkingDirectory $InstallRoot
|
||||
Start-Sleep -Seconds 3
|
||||
try {
|
||||
$Status = Invoke-RestMethod -Uri "https://localhost:8791/api/status" -TimeoutSec 8
|
||||
Write-Host "Agent status: $($Status.name) v$($Status.version) - Online via HTTPS" -ForegroundColor Green
|
||||
} catch {
|
||||
throw "GST Operator Agent was installed but did not answer on https://localhost:8791. $($_.Exception.Message)"
|
||||
}
|
||||
Write-Host "Installed successfully. It will start automatically when this Windows user logs in." -ForegroundColor Green
|
||||
Write-Host "Local status URL: https://localhost:8791/api/status" -ForegroundColor Cyan
|
||||
Write-Host ""
|
||||
Write-Host "Installed successfully." -ForegroundColor Green
|
||||
Write-Host "Protocol registered: arrrgst://" -ForegroundColor Green
|
||||
Write-Host "No localhost web server or trusted localhost certificate is used." -ForegroundColor DarkGray
|
||||
Write-Host "Chrome/Edge may ask once whether ARRR GST Operator Agent can be opened. Choose Open/Allow." -ForegroundColor Yellow
|
||||
|
||||
@@ -2,19 +2,18 @@ $ErrorActionPreference = "Stop"
|
||||
$InstallRoot = Join-Path $env:LOCALAPPDATA "ARRR\GSTOperatorAgent"
|
||||
$Startup = Join-Path $env:APPDATA "Microsoft\Windows\Start Menu\Programs\Startup\ARRR GST Operator Agent.cmd"
|
||||
$ThumbprintFile = Join-Path $InstallRoot "cert_thumbprint.txt"
|
||||
$ProtocolKey = "HKCU:\Software\Classes\arrrgst"
|
||||
|
||||
try {
|
||||
Get-NetTCPConnection -LocalPort 8791 -State Listen -ErrorAction SilentlyContinue | ForEach-Object {
|
||||
if ($_.OwningProcess) { Stop-Process -Id $_.OwningProcess -Force -ErrorAction SilentlyContinue }
|
||||
}
|
||||
} catch {}
|
||||
|
||||
if (Test-Path $ThumbprintFile) {
|
||||
$Thumbprint = (Get-Content $ThumbprintFile -Raw -ErrorAction SilentlyContinue).Trim()
|
||||
if ($Thumbprint) {
|
||||
Remove-Item "Cert:\CurrentUser\Root\$Thumbprint" -Force -ErrorAction SilentlyContinue
|
||||
}
|
||||
if ($Thumbprint) { Remove-Item "Cert:\CurrentUser\Root\$Thumbprint" -Force -ErrorAction SilentlyContinue }
|
||||
}
|
||||
Remove-Item $Startup -Force -ErrorAction SilentlyContinue
|
||||
Remove-Item $ProtocolKey -Recurse -Force -ErrorAction SilentlyContinue
|
||||
Remove-Item $InstallRoot -Recurse -Force -ErrorAction SilentlyContinue
|
||||
Write-Host "ARRR GST Operator Agent removed." -ForegroundColor Green
|
||||
Write-Host "ARRR GST Operator Agent removed, including legacy localhost certificate/startup state." -ForegroundColor Green
|
||||
|
||||
Reference in New Issue
Block a user