From e6d7198e2830dad297b7969343deb5abfc71a183 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 6 Oct 2026 13:51:32 +0000 Subject: [PATCH] Add Windows support (SDK 0.3.0, platforms manifest, cross-platform bridge) Co-Authored-By: Claude Sonnet 5.5 Claude-Session: https://claude.ai/code/session_016Xq7cCtgmRF34jJo3GAqfC --- .github/workflows/ci.yml | 23 ++- .github/workflows/release.yml | 2 +- CHANGELOG.md | 7 + README.md | 14 +- db-bridge.py | 268 +++++++++++++++++++++++++++------- package-lock.json | 11 +- package.json | 4 +- plugin/manifest.json | 40 ++++- src/api.ts | 4 +- src/views/InstancesView.tsx | 28 +++- 10 files changed, 326 insertions(+), 75 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 125f6a3..d36ebd4 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -11,7 +11,7 @@ permissions: contents: read env: - ERVISIO_REF: v0.5.0 + ERVISIO_REF: v0.6.2 jobs: build: @@ -58,3 +58,24 @@ jobs: name: plugin path: dist/*.tar.gz* retention-days: 7 + + bridge-windows: + # The bridge has to start and run its system-independent actions on Windows too. + runs-on: windows-2022 + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 1 + - uses: actions/setup-python@v5 + with: + python-version: '3.12' + - name: Compile + run: python -m py_compile db-bridge.py + - name: Smoke test (detect, create-sqlite, query) + shell: bash + run: | + python db-bridge.py detect | python -c "import json,sys; d=json.load(sys.stdin); assert 'instances' in d" + db="$RUNNER_TEMP/smoke.sqlite" + python db-bridge.py create-sqlite "$(printf '{"path":"%s"}' "$(cygpath -m "$db")" | base64 -w0)" | python -c "import json,sys; assert json.load(sys.stdin)['ok']" + inst="$(printf '{"instance":{"engine":"sqlite","path":"%s"},"sql":"SELECT 1 AS n"}' "$(cygpath -m "$db")" | base64 -w0)" + python db-bridge.py query "$inst" | python -c "import json,sys; d=json.load(sys.stdin); assert d['rows'][0]['n']==1, d" diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 47854cd..47c5c47 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -17,7 +17,7 @@ permissions: contents: read env: - ERVISIO_REF: v0.5.0 + ERVISIO_REF: v0.6.2 jobs: release: diff --git a/CHANGELOG.md b/CHANGELOG.md index 5a51d07..4e73d92 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,13 @@ All notable changes to the Ervisio Database Plugin will be documented in this file. +## 1.2.0 + +* Windows support (needs Ervisio 0.6.2 and `@ervisio/plugin-sdk` 0.3.0): the manifest declares `platforms: ["linux", "windows"]` and a separate `db-bridge` command for Windows (`python.exe`). +* Windows engines: Docker Desktop containers, native MariaDB/MySQL/PostgreSQL detected and controlled as Windows services (`net.exe`), installed with `winget`, SQLite discovery in the user profile and `%ProgramData%`. +* The bridge no longer depends on `sudo` (skipped when root or on Windows), on `tail`, or on the console code page. +* CI validates the manifest with Ervisio 0.6.2 and smoke-tests the bridge on a Windows runner. + ## 1.1.0 * HeidiSQL-grade database workbench for MySQL, MariaDB, PostgreSQL, and SQLite. diff --git a/README.md b/README.md index 549a49e..f2978dc 100644 --- a/README.md +++ b/README.md @@ -45,12 +45,22 @@ The dialog lists the permissions below; updates appear in **Plugins › Updates* * Toggle the slow query log on/off dynamically with custom duration thresholds. * Real-time viewing of recent slow log entries. +## Windows + +Needs Ervisio 0.6.2 or later on Windows and Python 3.10+ (`python.exe` on the `PATH` of the account that runs the daemon). +The plugin lives in `%ProgramData%\Ervisio\data\plugins\database`. On Windows: + +* **Docker**: Docker Desktop or Docker Engine (`docker.exe` on the `PATH`). +* **Native**: MariaDB, MySQL and PostgreSQL registered as Windows services are detected and started, stopped and restarted from the card view; installation uses `winget`. +* **Administrator rights**: operations that install or control services need the administrator unlock in Ervisio (members of `docker-users` run Docker commands as themselves). +* The Python packages `pymysql` and `psycopg2` must be installed for that Python (`pip install pymysql psycopg2-binary`); SQLite needs nothing. + ## Permissions The manifest is [plugin/manifest.json](plugin/manifest.json). In short: -* **Commands**, all `admin` with `adminUnlessGroup: docker`: - * `db-bridge` (`python3 /var/lib/ervisio/plugins/database/db-bridge.py {0} {1}`): +* **Commands**, all `admin` with `adminUnlessGroup: docker` (`docker-users` on Windows): + * `db-bridge` (`python3 /var/lib/ervisio/plugins/database/db-bridge.py {0} {1}` on Linux, `python.exe C:\ProgramData\Ervisio\data\plugins\database\db-bridge.py {0} {1}` on Windows): Executes database administration operations, schema introspection, container management, and query dispatching. * **Folders**: * `~/.config/ervisio/plugins/database` (created on first use; saved connections, preferences). diff --git a/db-bridge.py b/db-bridge.py index bc9481d..c42d4e2 100644 --- a/db-bridge.py +++ b/db-bridge.py @@ -15,6 +15,12 @@ import re +IS_WINDOWS = os.name == "nt" + +# JSON goes out as UTF-8 on every system (the Windows console code page would mangle it). +if hasattr(sys.stdout, "reconfigure"): + sys.stdout.reconfigure(encoding="utf-8") + IDENT_RE = re.compile(r"^[A-Za-z0-9_$]{1,64}$") def check_ident(name: str, what: str = "identifier"): @@ -46,13 +52,46 @@ def send_error(msg: str, code: str = "error"): def run_cmd(cmd: List[str], timeout: int = 300) -> str: try: - p = subprocess.run(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True, timeout=timeout) + extra: Dict[str, Any] = {} + if IS_WINDOWS: + extra["creationflags"] = 0x08000000 # CREATE_NO_WINDOW + p = subprocess.run(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True, + encoding="utf-8", errors="replace", timeout=timeout, **extra) if p.returncode != 0: raise RuntimeError(p.stderr.strip() or f"Command failed with exit code {p.returncode}") return p.stdout except Exception as e: raise RuntimeError(f"Execution error: {e}") +def privileged(cmd: List[str]) -> List[str]: + """Linux: prefix with sudo unless already root. Windows: the manifest runs the bridge elevated, no prefix.""" + if IS_WINDOWS or (hasattr(os, "geteuid") and os.geteuid() == 0): + return cmd + return ["sudo"] + cmd + +def tail_file(path: str, lines: int = 100) -> str: + """Last lines of a text file (no external `tail`, which Windows does not have).""" + with open(path, "rb") as fh: + fh.seek(0, os.SEEK_END) + size = fh.tell() + fh.seek(max(0, size - 256 * 1024)) + data = fh.read() + return "\n".join(data.decode("utf-8", "replace").splitlines()[-lines:]) + +SERVICE_RE = re.compile(r"^[A-Za-z0-9_.$-]{1,80}$") + +def windows_database_services() -> List[Dict[str, str]]: + """MariaDB, MySQL and PostgreSQL services registered with the Windows service manager.""" + script = ( + "Get-Service | Where-Object { $_.Name -match '^(mariadb|mysql|postgresql)' } | " + "Select-Object Name, @{n='State';e={$_.Status.ToString()}} | ConvertTo-Json -Compress" + ) + out = run_cmd(["powershell.exe", "-NoProfile", "-NonInteractive", "-Command", script], timeout=60).strip() + if not out: + return [] + data = json.loads(out) + return data if isinstance(data, list) else [data] + # Database connection helpers def get_mysql_conn(instance: Dict[str, Any], database: Optional[str] = None): import pymysql @@ -63,7 +102,7 @@ def get_mysql_conn(instance: Dict[str, Any], database: Optional[str] = None): unix_socket = instance.get("unix_socket") # If unix socket exists and host is localhost/127.0.0.1, try unix socket - if not unix_socket and (host in ("localhost", "127.0.0.1")): + if not unix_socket and not IS_WINDOWS and (host in ("localhost", "127.0.0.1")): for sock in ["/var/run/mysqld/mysqld.sock", "/var/run/mysql/mysql.sock", "/tmp/mysql.sock"]: if os.path.exists(sock): unix_socket = sock @@ -114,58 +153,130 @@ def get_sqlite_conn(path: str): return conn # Actions +WIN_SKIP_DIRS = {"appdata", "node_modules", ".git", "windows", "docker", "$recycle.bin"} + +def find_sqlite_windows(max_per_root: int = 15, max_depth: int = 4) -> List[Dict[str, Any]]: + """Shallow walk of the user profile and %ProgramData%, skipping AppData and other noisy folders.""" + found: List[Dict[str, Any]] = [] + roots = [os.path.expanduser("~"), os.environ.get("ProgramData", r"C:\ProgramData")] + for root in roots: + if not os.path.isdir(root): + continue + count = 0 + base_depth = root.rstrip("\\/").count(os.sep) + for dirpath, dirnames, filenames in os.walk(root): + if dirpath.rstrip("\\/").count(os.sep) - base_depth >= max_depth: + dirnames[:] = [] + dirnames[:] = [d for d in dirnames if d.lower() not in WIN_SKIP_DIRS] + for fn in filenames: + if not fn.lower().endswith((".db", ".sqlite", ".sqlite3")): + continue + f = os.path.join(dirpath, fn) + try: + size = os.path.getsize(f) + except OSError: + continue + found.append({ + "id": f"sqlite-{base64.b64encode(f.encode()).decode()[:16]}", + "name": fn, + "path": f, + "type": "sqlite", + "engine": "sqlite", + "mode": "file", + "status": "ready", + "size": size + }) + count += 1 + if count >= max_per_root: + break + if count >= max_per_root: + break + return found + def action_detect() -> Dict[str, Any]: instances = [] - # 1. Native systemd services - try: - units_out = run_cmd(["systemctl", "list-units", "--type=service", "--state=all", "--no-pager", "--no-legend"]) - for line in units_out.splitlines(): - parts = line.split() - if not parts: - continue - unit = parts[0] - if "mariadb" in unit: - is_active = "running" in line - instances.append({ - "id": "native-mariadb", - "name": "MariaDB (Native Systemd)", - "type": "mariadb", - "engine": "mysql", - "mode": "native", - "host": "127.0.0.1", - "port": 3306, - "status": "running" if is_active else "stopped", - "version": "native" - }) - elif "mysql" in unit and "native-mariadb" not in [x["id"] for x in instances]: - is_active = "running" in line - instances.append({ - "id": "native-mysql", - "name": "MySQL (Native Systemd)", - "type": "mysql", - "engine": "mysql", - "mode": "native", - "host": "127.0.0.1", - "port": 3306, - "status": "running" if is_active else "stopped", - "version": "native" - }) - elif "postgres" in unit: - is_active = "running" in line + # 1. Native services (systemd on Linux, the service manager on Windows) + if IS_WINDOWS: + try: + seen = set() + for svc in windows_database_services(): + name = svc.get("Name", "") + low = name.lower() + if low.startswith("mariadb"): + db_type, engine, port, label = "mariadb", "mysql", 3306, "MariaDB" + elif low.startswith("mysql"): + db_type, engine, port, label = "mysql", "mysql", 3306, "MySQL" + elif low.startswith("postgresql"): + db_type, engine, port, label = "postgres", "postgres", 5432, "PostgreSQL" + else: + continue + if db_type in seen: + continue + seen.add(db_type) instances.append({ - "id": "native-postgres", - "name": "PostgreSQL (Native Systemd)", - "type": "postgres", - "engine": "postgres", + "id": f"native-{db_type}", + "name": f"{label} (Windows service)", + "type": db_type, + "engine": engine, "mode": "native", + "service": name, "host": "127.0.0.1", - "port": 5432, - "status": "running" if is_active else "stopped", + "port": port, + "status": "running" if svc.get("State") == "Running" else "stopped", "version": "native" }) - except Exception: - pass + except Exception: + pass + else: + try: + units_out = run_cmd(["systemctl", "list-units", "--type=service", "--state=all", "--no-pager", "--no-legend"]) + for line in units_out.splitlines(): + parts = line.split() + if not parts: + continue + unit = parts[0] + if "mariadb" in unit: + is_active = "running" in line + instances.append({ + "id": "native-mariadb", + "name": "MariaDB (Native Systemd)", + "type": "mariadb", + "engine": "mysql", + "mode": "native", + "host": "127.0.0.1", + "port": 3306, + "status": "running" if is_active else "stopped", + "version": "native" + }) + elif "mysql" in unit and "native-mariadb" not in [x["id"] for x in instances]: + is_active = "running" in line + instances.append({ + "id": "native-mysql", + "name": "MySQL (Native Systemd)", + "type": "mysql", + "engine": "mysql", + "mode": "native", + "host": "127.0.0.1", + "port": 3306, + "status": "running" if is_active else "stopped", + "version": "native" + }) + elif "postgres" in unit: + is_active = "running" in line + instances.append({ + "id": "native-postgres", + "name": "PostgreSQL (Native Systemd)", + "type": "postgres", + "engine": "postgres", + "mode": "native", + "host": "127.0.0.1", + "port": 5432, + "status": "running" if is_active else "stopped", + "version": "native" + }) + except Exception: + pass # 2. Docker database containers try: @@ -219,9 +330,11 @@ def action_detect() -> Dict[str, Any]: except Exception: pass - # 3. Known SQLite files in /home/ubuntu and /var/lib + # 3. Known SQLite files (/home/ubuntu and /var/lib; the profile and ProgramData on Windows) sqlite_files = [] - for root_dir in ["/home/ubuntu", "/var/lib"]: + if IS_WINDOWS: + sqlite_files = find_sqlite_windows() + for root_dir in ([] if IS_WINDOWS else ["/home/ubuntu", "/var/lib"]): try: for ext in ["*.db", "*.sqlite", "*.sqlite3"]: for f in glob.glob(os.path.join(root_dir, "**", ext), recursive=True)[:15]: @@ -277,7 +390,32 @@ def action_install_docker(params: Dict[str, Any]) -> Dict[str, Any]: cid = run_cmd(cmd).strip() return {"ok": True, "container_id": cid, "name": name, "port": port} +WINGET_IDS = { + "mariadb": "MariaDB.Server", + "mysql": "Oracle.MySQL", + "postgres": "PostgreSQL.PostgreSQL.17", + "sqlite": "SQLite.SQLite", +} + +def action_install_winget(params: Dict[str, Any]) -> Dict[str, Any]: + engine = params.get("engine", "mariadb") + pkg = WINGET_IDS.get(engine) + if not pkg: + raise RuntimeError(f"Unsupported engine: {engine}") + cmd = ["winget.exe", "install", "--id", pkg, "-e", "--silent", + "--accept-package-agreements", "--accept-source-agreements"] + if engine == "postgres": + password = str(params.get("password", "")).strip() + if password: + if '"' in password: + raise RuntimeError("The password cannot contain double quotes") + cmd += ["--override", f'--mode unattended --superpassword "{password}"'] + run_cmd(cmd, timeout=1200) + return {"ok": True, "engine": engine, "package": pkg} + def action_install_apt(params: Dict[str, Any]) -> Dict[str, Any]: + if IS_WINDOWS: + return action_install_winget(params) engine = params.get("engine", "mariadb") # mariadb, mysql, postgres, sqlite pkg_map = { "mariadb": "mariadb-server", @@ -286,18 +424,19 @@ def action_install_apt(params: Dict[str, Any]) -> Dict[str, Any]: "sqlite": "sqlite3" } pkg = pkg_map.get(engine, "mariadb-server") - run_cmd(["sudo", "apt-get", "update"], timeout=120) - run_cmd(["sudo", "DEBIAN_FRONTEND=noninteractive", "apt-get", "install", "-y", pkg], timeout=300) + run_cmd(privileged(["apt-get", "update"]), timeout=120) + run_cmd(privileged(["env", "DEBIAN_FRONTEND=noninteractive", "apt-get", "install", "-y", pkg]), timeout=300) if engine in ("mariadb", "mysql"): - run_cmd(["sudo", "systemctl", "enable", "--now", engine]) + run_cmd(privileged(["systemctl", "enable", "--now", engine])) elif engine == "postgres": - run_cmd(["sudo", "systemctl", "enable", "--now", "postgresql"]) + run_cmd(privileged(["systemctl", "enable", "--now", "postgresql"])) return {"ok": True, "engine": engine, "package": pkg} def action_create_sqlite(params: Dict[str, Any]) -> Dict[str, Any]: path = params.get("path", "").strip() if not path: raise RuntimeError("File path is required for SQLite database") + path = os.path.expanduser(path) if not path.endswith(".sqlite") and not path.endswith(".db"): path = path + ".sqlite" dir_name = os.path.dirname(os.path.abspath(path)) @@ -324,10 +463,23 @@ def action_manage_instance(params: Dict[str, Any]) -> Dict[str, Any]: run_cmd(["docker", "restart", target]) elif op == "remove": run_cmd(["docker", "rm", "-f", target]) + elif mode == "native" and IS_WINDOWS: + if not SERVICE_RE.match(target or ""): + raise RuntimeError(f"Invalid service name: {target!r}") + if op == "start": + run_cmd(["net.exe", "start", target], timeout=180) + elif op == "stop": + run_cmd(["net.exe", "stop", target], timeout=180) + elif op == "restart": + try: + run_cmd(["net.exe", "stop", target], timeout=180) + except RuntimeError: + pass # already stopped + run_cmd(["net.exe", "start", target], timeout=180) elif mode == "native": svc = "mariadb" if "mariadb" in target else ("mysql" if "mysql" in target else "postgresql") if op in ("start", "stop", "restart"): - run_cmd(["sudo", "systemctl", op, svc]) + run_cmd(privileged(["systemctl", op, svc])) return {"ok": True, "op": op, "target": target} def action_list_databases(instance: Dict[str, Any]) -> Dict[str, Any]: @@ -1108,7 +1260,7 @@ def action_get_logs(instance: Dict[str, Any]) -> Dict[str, Any]: # Try to read last 50 lines of slow log if file exists fpath = status.get("log_file") if fpath and os.path.exists(fpath): - log_content = run_cmd(["tail", "-n", "100", fpath]) + log_content = tail_file(fpath, 100) finally: conn.close() return {"status": status, "content": log_content} @@ -1144,7 +1296,11 @@ def read_payload_file(name: str) -> str: homes.insert(0, pwd.getpwnam(sudo_user).pw_dir) except Exception: pass - homes += sorted(glob.glob("/home/*")) + ["/root"] + if IS_WINDOWS: + users = os.path.join(os.environ.get("SystemDrive", "C:") + os.sep, "Users", "*") + homes += sorted(glob.glob(users)) + else: + homes += sorted(glob.glob("/home/*")) + ["/root"] for home in homes: path = os.path.join(home, PAYLOAD_SUBDIR, name + ".json") if os.path.isfile(path): @@ -1185,6 +1341,8 @@ def main(): try: if action == "detect": send_json(action_detect()) + elif action == "install-winget": + send_json(action_install_winget(params)) elif action == "install-docker": send_json(action_install_docker(params)) elif action == "install-apt": diff --git a/package-lock.json b/package-lock.json index 6e9608f..46cefad 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,13 @@ { "name": "ervisio-plugin-database", - "version": "1.0.0", + "version": "1.2.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "ervisio-plugin-database", - "version": "1.0.0", + "version": "1.2.0", + "license": "MIT", "dependencies": { "@codemirror/autocomplete": "^6.20.3", "@codemirror/commands": "^6.11.1", @@ -18,7 +19,7 @@ "codemirror": "^6.0.2" }, "devDependencies": { - "@ervisio/plugin-sdk": "github:Ervisio/plugin-sdk#v0.2.0", + "@ervisio/plugin-sdk": "github:Ervisio/plugin-sdk#v0.3.0", "@types/react": "^18.3.18", "typescript": "^5.7.3", "vite": "^8.3.2" @@ -120,8 +121,8 @@ } }, "node_modules/@ervisio/plugin-sdk": { - "version": "0.2.0", - "resolved": "git+ssh://git@github.com/Ervisio/plugin-sdk.git#436ec3cd83392d6aa16a4be79c87ae4f8fa3541f", + "version": "0.3.0", + "resolved": "git+ssh://git@github.com/Ervisio/plugin-sdk.git#0614ee7d0a17ac4094b80c141e8ad31f1573309e", "dev": true, "license": "MIT", "bin": { diff --git a/package.json b/package.json index eab59fb..3809609 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "ervisio-plugin-database", "private": true, - "version": "1.1.0", + "version": "1.2.0", "type": "module", "license": "MIT", "repository": { @@ -15,7 +15,7 @@ "typecheck": "tsc --noEmit" }, "devDependencies": { - "@ervisio/plugin-sdk": "github:Ervisio/plugin-sdk#v0.2.0", + "@ervisio/plugin-sdk": "github:Ervisio/plugin-sdk#v0.3.0", "@types/react": "^18.3.18", "typescript": "^5.7.3", "vite": "^8.3.2" diff --git a/plugin/manifest.json b/plugin/manifest.json index c9d64f2..4f5d27a 100644 --- a/plugin/manifest.json +++ b/plugin/manifest.json @@ -1,12 +1,17 @@ { "id": "database", "name": "Databases", - "version": "1.1.0", + "version": "1.2.0", "author": "Ervisio Community", "description": "HeidiSQL-style database manager for MySQL, MariaDB, PostgreSQL and SQLite: schema tree, editable data grid with live search and filters, SQL editor with autocomplete, users and slow query log.", "homepage": "https://github.com/Ervisio", "icon": "database", "entry": "index.js", + "minCore": "0.6.2", + "platforms": [ + "linux", + "windows" + ], "contributes": { "pages": [ { @@ -46,6 +51,36 @@ "allowDash": true, "maxLen": 4096 } + ], + "platforms": [ + "linux" + ] + }, + { + "name": "db-bridge", + "description": "Ervisio Database Engine Bridge", + "admin": true, + "adminUnlessGroup": "docker-users", + "timeoutSec": 120, + "argv": [ + "python.exe", + "C:\\ProgramData\\Ervisio\\data\\plugins\\database\\db-bridge.py", + "{0}", + "{1}" + ], + "args": [ + { + "pattern": "[a-zA-Z0-9_-]+", + "maxLen": 64 + }, + { + "pattern": ".*", + "allowDash": true, + "maxLen": 4096 + } + ], + "platforms": [ + "windows" ] } ], @@ -62,7 +97,8 @@ "groups": [ "wheel", "sudo", - "docker" + "docker", + "docker-users" ] }, "files": { diff --git a/src/api.ts b/src/api.ts index 867731f..171aa9c 100644 --- a/src/api.ts +++ b/src/api.ts @@ -11,6 +11,8 @@ export interface DbInstance { host?: string; port?: number; container?: string; + /** Native service name (Windows service manager). */ + service?: string; path?: string; status: 'running' | 'stopped' | 'ready'; version?: string; @@ -206,7 +208,7 @@ export const dbApi = { detect: () => callBridge('detect'), installDocker: (p: { engine: string; version: string; name: string; password: string; port: number }) => callBridge<{ ok: boolean; container_id: string }>('install-docker', p), - installApt: (p: { engine: string }) => + installApt: (p: { engine: string; password?: string }) => callBridge<{ ok: boolean; package: string }>('install-apt', p), createSqlite: (p: { path: string }) => callBridge<{ ok: boolean; path: string }>('create-sqlite', p), diff --git a/src/views/InstancesView.tsx b/src/views/InstancesView.tsx index 0460048..6de54b3 100644 --- a/src/views/InstancesView.tsx +++ b/src/views/InstancesView.tsx @@ -35,7 +35,7 @@ export function InstancesView({ search }: { search: string }) { const manage = async (inst: DbInstance, op: 'start' | 'stop' | 'restart' | 'remove') => { setBusyId(inst.id); try { - await dbApi.manageInstance({ op, mode: inst.mode, target: inst.container || inst.id }); + await dbApi.manageInstance({ op, mode: inst.mode, target: inst.service || inst.container || inst.id }); toast.ok(t('Done: {op}', { op }), inst.name); invalidateInstance(inst, 'all'); await detectCache.get([], true); @@ -187,6 +187,7 @@ function randomPassword(): string { } function InstallDialog({ open, onClose, versions }: { open: boolean; onClose(): void; versions: Record }) { + const isWindows = getSdk().platform === 'windows'; const [mode, setMode] = useState<'docker' | 'apt' | 'sqlite'>('docker'); const [engine, setEngine] = useState('mysql'); const [version, setVersion] = useState(''); @@ -194,7 +195,7 @@ function InstallDialog({ open, onClose, versions }: { open: boolean; onClose(): const [password, setPassword] = useState(() => randomPassword()); const [port, setPort] = useState('3306'); const [aptEngine, setAptEngine] = useState('mariadb'); - const [sqlitePath, setSqlitePath] = useState('/home/ubuntu/database.sqlite'); + const [sqlitePath, setSqlitePath] = useState(isWindows ? 'C:\\ProgramData\\Ervisio\\data\\database.sqlite' : '/home/ubuntu/database.sqlite'); const [busy, setBusy] = useState(false); const vlist = versions[engine] ?? ['latest']; @@ -207,7 +208,7 @@ function InstallDialog({ open, onClose, versions }: { open: boolean; onClose(): await dbApi.installDocker({ engine, version: ver, name, password, port: parseInt(port, 10) || 3306 }); toast.ok(t('Engine deployed'), t('{name} is running in Docker', { name })); } else if (mode === 'apt') { - await dbApi.installApt({ engine: aptEngine }); + await dbApi.installApt(isWindows && aptEngine === 'postgres' ? { engine: aptEngine, password } : { engine: aptEngine }); toast.ok(t('Native service installed'), aptEngine); } else { const res = await dbApi.createSqlite({ path: sqlitePath }); @@ -237,7 +238,7 @@ function InstallDialog({ open, onClose, versions }: { open: boolean; onClose(): aria-label={t('Deploy mode')} value={mode} onChange={(v) => setMode(v as typeof mode)} - options={[{ value: 'docker', label: 'Docker' }, { value: 'apt', label: t('Native (APT)') }, { value: 'sqlite', label: 'SQLite' }]} + options={[{ value: 'docker', label: 'Docker' }, { value: 'apt', label: isWindows ? t('Native (winget)') : t('Native (APT)') }, { value: 'sqlite', label: 'SQLite' }]} /> {mode === 'docker' && ( <> @@ -275,14 +276,29 @@ function InstallDialog({ open, onClose, versions }: { open: boolean; onClose(): label={t('Package')} value={aptEngine} onChange={setAptEngine} - options={[ + options={isWindows ? [ + { value: 'mariadb', label: 'MariaDB Server (MariaDB.Server)' }, + { value: 'mysql', label: 'MySQL Server (Oracle.MySQL)' }, + { value: 'postgres', label: 'PostgreSQL (PostgreSQL.PostgreSQL.17)' }, + { value: 'sqlite', label: 'SQLite (SQLite.SQLite)' }, + ] : [ { value: 'mariadb', label: 'MariaDB Server (mariadb-server)' }, { value: 'mysql', label: 'MySQL Server (mysql-server)' }, { value: 'postgres', label: 'PostgreSQL (postgresql)' }, { value: 'sqlite', label: 'SQLite3 tools (sqlite3)' }, ]} /> -
{t('Runs apt-get install in the background, then enables and starts the systemd service.')}
+ {isWindows && aptEngine === 'postgres' && ( + setPassword(e.target.value)} + mono + hint={t('Set as the postgres password by the installer. Copy it now.')} + end={ setPassword(randomPassword())} />} + /> + )} +
{isWindows ? t('Runs winget install in the background; the engine is registered as a Windows service.') : t('Runs apt-get install in the background, then enables and starts the systemd service.')}
)} {mode === 'sqlite' && (