diff --git a/packaging/windows/OVMS_WINDOWS_INSTALLER_MANAGER_PLAN.md b/packaging/windows/OVMS_WINDOWS_INSTALLER_MANAGER_PLAN.md new file mode 100644 index 0000000000..8a0980cd10 --- /dev/null +++ b/packaging/windows/OVMS_WINDOWS_INSTALLER_MANAGER_PLAN.md @@ -0,0 +1,1277 @@ +# OVMS Windows Installer and Manager Plan + +## Goal + +Create a Windows installer and GUI manager that make OpenVINO Model Server +simple to install, configure, run, and maintain on Windows. + +The user-facing experience should feel closer to a local AI desktop server app: + +- Install OVMS with the required OpenVINO runtime dependencies. +- Include GenAI/tokenizer support for LLM use cases when selected. +- Optionally include Python-enabled OVMS support. +- Configure server settings during installation. +- Provide a GUI and taskbar tray icon for later management. +- Support start-at-login and Windows Service modes. +- Allow repair, upgrade, and optional feature installation later. + +The first implementation should live in the `model_server` repository because +the primary product being installed and managed is `ovms.exe`. OpenVINO Runtime +and OpenVINO GenAI are dependencies consumed through the OVMS Windows package. + +## Non-Goals + +- Do not turn `ovms.exe` itself into a GUI application. +- Do not install unrelated OpenVINO SDK/toolkit components unless they are + required by the OVMS package. +- Do not mix arbitrary OpenVINO, GenAI, and OVMS versions. The installer and + manager should keep the installed stack version-matched. +- Do not replace the portable `ovms.zip` package. The installer should exist + alongside it. + +## Recommended Repository Layout + +```text +model_server/ + packaging/ + windows/ + OVMS_WINDOWS_INSTALLER_MANAGER_PLAN.md + README.md + installer/ + README.md + build_installer.ps1 + OVMSInstaller.iss + assets/ + uninstall/ + manager/ + README.md + build_manager.ps1 + OVMS.Manager.sln + src/ + OVMS.Manager/ + OVMS.Manager.Core/ + assets/ + scripts/ + configure-ovms.ps1 + install-service.ps1 + uninstall-service.ps1 + uninstall-ovms.ps1 + repair-package.ps1 + validate-install.ps1 + schemas/ + ovms-manager-settings.schema.json + install-options.schema.json +``` + +Existing root-level packaging scripts should remain in place: + +```text +windows_build.bat +windows_create_package.bat +install_ovms_service.bat +setupvars.bat +setupvars.ps1 +``` + +The Windows installer should consume the existing package output: + +```text +dist/windows/ovms/ +dist/windows/ovms.zip +``` + +Final release artifacts should include: + +```text +dist/windows/ovms.zip +dist/windows/OVMS-Setup.exe +``` + +## Product Architecture + +Keep the server headless and add a Windows UX layer around it. + +```text +OVMS-Setup.exe + installs files + writes first-time config + optionally registers the Windows Service + installs OVMS Manager + +OVMS Manager.exe + owns the GUI and tray icon + controls server startup/shutdown + manages settings, logs, models, and feature repair + +ovms.exe + remains the model server process +``` + +## Installer Technology + +Start with Inno Setup. + +Reasons: + +- Fast path to a polished single-file installer. +- Supports component selection, custom wizard pages, Start Menu shortcuts, + uninstall hooks, and scripted service commands. +- Easier to iterate than WiX/MSI for the first product version. + +WiX/MSI can be considered later if enterprise/GPO deployment becomes a firm +requirement. + +Inno Setup is a build-time dependency only. The generated installer must be a +normal Windows installer that does not require users to install Inno Setup, +Python, .NET, OpenVINO, GenAI, or other runtime packages separately. + +Runtime dependency policy: + +- OVMS, OpenVINO runtime DLLs, OpenVINO GenAI DLLs, tokenizers, and bundled + Python come from the packaged `dist/windows/ovms` payload. +- The tray app must be published self-contained and included in the installer + payload. +- The installer must not download required runtime dependencies during install. +- PowerShell may be used for install-time configuration because it is part of + supported Windows installations, but it must not fetch external packages. + +## Install Locations + +Program files: + +```text +C:\Program Files\OVMS\ +``` + +Persistent data: + +```text +C:\ProgramData\OVMS\ + settings.json + models\ + config.json + logs\ + ovms_server.log + packages\ + downloads\ +``` + +Per-user startup, when using login mode: + +```text +HKCU\Software\Microsoft\Windows\CurrentVersion\Run +``` + +Service configuration, when using service mode: + +```text +HKLM\SYSTEM\CurrentControlSet\Services\ovms +``` + +## Privilege Model + +The installer and Manager must support a clear privilege boundary. + +### Machine-Wide Install + +Default for the first release: + +```text +C:\Program Files\OVMS +C:\ProgramData\OVMS +HKLM service registration +Machine PATH, if selected +``` + +Machine-wide install requires elevation during setup. + +Admin-only actions: + +- Installing to `C:\Program Files\OVMS`. +- Writing machine-wide PATH entries. +- Registering or deleting the `ovms` Windows Service. +- Changing service start type. +- Writing service command-line configuration. +- Opening firewall rules, if added later. + +### Per-User Actions + +These should not require elevation: + +- Starting Manager at user login through HKCU Run. +- Starting or stopping a Manager-owned `ovms.exe` process. +- Editing user-writable settings where permissions allow it. +- Opening logs, model folders, and diagnostics. + +### Manager Elevation Behavior + +Manager should run unelevated by default. When the user performs an admin-only +action, Manager should prompt for elevation for that action only. + +Manager should not require elevation just to show the tray icon, read status, or +start a user-owned OVMS process. + +### Future Per-User Install + +A per-user install may be added later: + +```text +%LOCALAPPDATA%\Programs\OVMS +%LOCALAPPDATA%\OVMS +HKCU startup only +No Windows Service support unless elevated later +``` + +This should be treated as a separate install mode, not an accidental fallback. + +## Installer Wizard + +### Component Selection + +Suggested defaults: + +```text +[x] OpenVINO Model Server +[x] GenAI / LLM support +[x] Python support +[x] OVMS Manager app +[x] Tray icon +[ ] Windows Service +[ ] Start at boot +[x] Add OVMS to PATH +``` + +The UI should present OpenVINO and GenAI as required runtime capabilities of +the selected OVMS package, not as unrelated SDK installs. + +### Existing Install Detection + +Before showing the normal install flow, the installer must check whether OVMS is +already installed. + +Detection sources: + +- Apps & Features uninstall registry entry for OpenVINO Model Server. +- Existing install marker under `C:\Program Files\OVMS`. +- Existing Manager settings at `C:\ProgramData\OVMS\settings.json`. +- Existing `ovms` Windows Service registration. +- Existing PATH entries pointing to an OVMS install directory. +- Running `OVMS Manager.exe` process. +- Running `ovms.exe` process from a known OVMS install directory. + +Install marker: + +```text +C:\ProgramData\OVMS\install.json +``` + +Suggested content: + +```json +{ + "productName": "OpenVINO Model Server", + "installDir": "C:\\Program Files\\OVMS", + "dataDir": "C:\\ProgramData\\OVMS", + "version": "2026.2.1", + "packageVariant": "python_on", + "installedAtUtc": "2026-06-30T15:00:00Z", + "installerVersion": "1.0.0" +} +``` + +If an existing install is detected, the installer should offer: + +```text +Repair existing install +Upgrade existing install +Modify installed features +Uninstall +Cancel +``` + +Rules: + +- Do not silently install over an existing OVMS directory. +- Stop Manager and OVMS before repair, upgrade, modify, or uninstall. +- Preserve `C:\ProgramData\OVMS` by default. +- If multiple possible installs are detected, show the paths and require the user + to choose or cancel. +- If only a running `ovms.exe` is found outside the managed install directory, + warn about the conflict but do not treat it as the managed install. + +### Server Configuration + +Collect: + +```text +REST port: 8000 +gRPC port: 9000 +Bind address: 127.0.0.1 +Model repository: C:\ProgramData\OVMS\models +Log level: INFO +Log path: C:\ProgramData\OVMS\logs\ovms_server.log +``` + +### Runtime Mode + +Offer: + +```text +Run when I sign in with tray icon +Run as Windows Service +Manual only +``` + +Recommended default: + +```text +Run when I sign in with tray icon +``` + +Advanced option: + +```text +Run as Windows Service and optionally start at boot +``` + +### Optional Initial Model + +Offer: + +```text +None +Add local model +Pull model after install +``` + +The MVP can start with `None` only and add model workflows in a later phase. + +### Finish Page + +Offer: + +```text +[x] Launch OVMS Manager +[ ] Start server now +``` + +## Manager Application + +Use .NET WPF or WinUI 3. WPF is the recommended first choice because it is +pragmatic, native-feeling, and works well with tray icons, Windows services, +process control, and file-based settings. + +Suggested projects: + +```text +OVMS.Manager + WPF UI and tray integration + +OVMS.Manager.Core + service/process/config/log/package logic +``` + +### Dashboard + +Show: + +- Server running/stopped state. +- Runtime mode: process, service, or manual. +- Active REST and gRPC ports. +- Bind address. +- Installed package variant. +- Served model count. +- Latest health check result. + +Health check: + +```text +GET http://127.0.0.1:/v3/models +``` + +### Server Controls + +Support: + +- Start server. +- Stop server. +- Restart server. +- Reload status. +- Open logs. +- Open model repository. + +In user-login mode, Manager starts and stops `ovms.exe` directly. + +In service mode, Manager controls the `ovms` Windows Service. + +### Settings + +Support editing: + +- REST port. +- gRPC port. +- Bind address. +- Log level. +- Log path. +- Model repository path. +- Config path. +- Target device defaults, if added later. +- Startup mode. +- Show tray icon. + +Settings changes that affect the process command line should prompt for or +perform a restart. + +### Startup + +Support: + +- Start Manager at user login. +- Show tray icon. +- Start OVMS when Manager starts. +- Register as Windows Service. +- Configure service start type: manual or automatic. + +### Models + +Support: + +- List configured models from `config.json`. +- Add local model. +- Remove model from config. +- Open model folder. +- Refresh/reload config. +- Pull supported model, in a later phase. + +### Features + +Detect: + +- `ovms.exe` exists. +- `openvino_genai.dll` exists. +- `openvino_tokenizers.dll` exists. +- `python\python.exe` exists, for Python-enabled package. +- `ovms.exe --version` succeeds. + +Show: + +```text +GenAI support: Installed / Missing +Python support: Installed / Missing +Package variant: python_on / python_off +``` + +If GenAI or Python support is missing, offer repair or upgrade to a matching +package variant rather than mixing arbitrary DLL versions. + +### Updates + +Support: + +- Check for updates manually from Manager. +- Optionally check for updates periodically. +- Show installed version, latest available version, package variant, and release + notes link. +- Download updates only after user confirmation. +- Reuse the staged upgrade and rollback flow. + +The first release should not silently auto-install updates. + +### Logs + +Support: + +- Tail `ovms_server.log`. +- Open log folder. +- Copy diagnostics summary. +- Show recent service events, if practical. + +### Advanced + +Support: + +- View effective server command line. +- Validate PATH and environment variables. +- Reset config. +- Repair package. +- Check for updates. +- Export diagnostics bundle. + +## Tray Icon + +The tray icon belongs to `OVMS Manager.exe`, not `ovms.exe`. + +Left-click behavior: + +- Open or focus the OVMS Manager window. +- If the Manager window is minimized or hidden, restore it. +- If the Manager window is already open, bring it to the foreground. + +Context menu: + +```text +Open OVMS Manager +Start Server +Stop Server +``` + +Keep the first version intentionally small. Additional items such as restart, +logs, diagnostics, model folder, settings, and quit can be added later from the +Manager UI if needed. + +Notifications: + +- Server started. +- Server stopped. +- Server failed to start. +- Port already in use. +- Model config changed. + +## Configuration Files + +Manager settings: + +```text +C:\ProgramData\OVMS\settings.json +``` + +Initial content: + +```json +{ + "installDir": "C:\\Program Files\\OVMS", + "dataDir": "C:\\ProgramData\\OVMS", + "modelRepositoryPath": "C:\\ProgramData\\OVMS\\models", + "configPath": "C:\\ProgramData\\OVMS\\models\\config.json", + "logPath": "C:\\ProgramData\\OVMS\\logs\\ovms_server.log", + "restPort": 8000, + "grpcPort": 9000, + "bindAddress": "127.0.0.1", + "logLevel": "INFO", + "runMode": "user-login", + "startAtLogin": true, + "showTrayIcon": true, + "serviceAutoStart": false, + "packageVariant": "python_on" +} +``` + +OVMS config: + +```text +C:\ProgramData\OVMS\models\config.json +``` + +Initial content: + +```json +{ + "model_config_list": [] +} +``` + +## Startup Modes + +### User-Login Mode + +Behavior: + +- Manager starts at user login. +- Manager owns the tray icon. +- Manager starts `ovms.exe` hidden. +- Server runs only after user login. + +This is the best Ollama-like default. + +### Service Mode + +Behavior: + +- `ovms` Windows Service owns the server process. +- Service can start before user login. +- Manager controls service state. +- Tray icon is still provided by Manager after login. + +This is best for machine/server reliability. + +### Manual Mode + +Behavior: + +- Nothing starts automatically. +- User launches Manager and starts OVMS manually. + +This is best for development and low-impact installs. + +## Runtime Ownership + +Exactly one runtime owner should control OVMS at a time. + +Runtime owners: + +```text +manager-process +windows-service +manual +none +``` + +Manager must avoid starting a duplicate server when: + +- The `ovms` Windows Service is running. +- A Manager-owned `ovms.exe` process is already running. +- Another process is already listening on the configured REST or gRPC port. +- A lock file indicates another Manager instance is controlling the server. + +State file: + +```text +C:\ProgramData\OVMS\runtime.json +``` + +Suggested content: + +```json +{ + "owner": "manager-process", + "pid": 12345, + "serviceName": "ovms", + "restPort": 8000, + "grpcPort": 9000, + "startedAtUtc": "2026-06-30T15:00:00Z" +} +``` + +Lock file: + +```text +C:\ProgramData\OVMS\ovms-manager.lock +``` + +Ownership rules: + +- User-login mode may start only a Manager-owned process. +- Service mode may start only the `ovms` Windows Service. +- Manual mode never auto-starts OVMS. +- Switching from process mode to service mode must stop the process first. +- Switching from service mode to process mode must stop the service first. +- Manager should show the current owner and block conflicting start actions with + a clear explanation. + +Port preflight: + +- Before starting OVMS, Manager must check configured REST and gRPC ports. +- If a port is occupied by another process, Manager should show the process id + and executable name when available. +- Manager should not blindly kill unrelated processes. + +## Feature and Package Strategy + +Use the OVMS Windows package as the unit of installation. + +Recommended variants: + +```text +Minimal install: OVMS python_off package +LLM/GenAI install: OVMS python_on package +``` + +Do not independently install unmatched OpenVINO Runtime, GenAI, and tokenizer +DLLs. The Manager should install, repair, or upgrade a matching OVMS package +variant. + +This avoids version mismatch issues between: + +- `ovms.exe` +- OpenVINO runtime DLLs +- OpenVINO GenAI DLLs +- OpenVINO tokenizers DLLs +- Python support files + +## Upgrade and Repair Strategy + +Upgrade and repair must be staged and recoverable. + +Package source: + +- Installer builds from local `dist/windows/ovms`. +- Manager repair/upgrade uses a matching OVMS release package or an installer + cache under `C:\ProgramData\OVMS\packages`. + +Verification before use: + +- Validate package version metadata. +- Validate expected files are present. +- Validate checksums or signatures when packages are downloaded. +- Run `ovms.exe --version` from the staged package before swapping. + +Staging path: + +```text +C:\ProgramData\OVMS\packages\staging\\ +``` + +Backup path: + +```text +C:\ProgramData\OVMS\packages\backup\\ +``` + +Upgrade steps: + +1. Stop Manager-owned OVMS process or `ovms` service. +2. Download or locate the matching package. +3. Verify checksum/signature and required files. +4. Extract to staging. +5. Run staged `ovms.exe --version`. +6. Backup current install files. +7. Swap staged files into `C:\Program Files\OVMS`. +8. Preserve `C:\ProgramData\OVMS` settings, models, and logs. +9. Restart OVMS if it was running before upgrade. +10. Run health check. +11. Roll back from backup if version or health validation fails. + +Repair steps: + +- Re-verify installed files. +- Restore missing files from package cache when possible. +- Re-run `ovms.exe --version`. +- Re-register service only if the selected runtime mode requires it. + +The Manager should never mix individual DLLs from unrelated package versions. + +## Update Checking + +Update checking should be explicit, version-aware, and safe. + +Default behavior: + +- Manual update checks are supported. +- Periodic update checks are opt-in. +- Updates are downloaded and installed only after user confirmation. + +Update source: + +- Official OVMS release metadata. +- For development builds, an explicitly configured package feed may be used. + +The Manager should compare: + +- Installed OVMS version. +- Installed package variant, such as `python_on` or `python_off`. +- Installed Manager version. +- Latest compatible OVMS package. +- Latest compatible Manager package. + +Update states: + +```text +Up to date +Update available +Update downloaded +Update failed +Update requires restart +Cannot check for updates +``` + +Update flow: + +1. User clicks "Check for updates". +2. Manager fetches release metadata. +3. Manager shows latest compatible version and release notes link. +4. User confirms download. +5. Manager downloads package to `C:\ProgramData\OVMS\packages`. +6. Manager verifies checksum/signature. +7. Manager stages package. +8. Manager stops OVMS if needed. +9. Manager applies upgrade using the staged upgrade flow. +10. Manager restarts OVMS if it was running before update. +11. Manager reports success or rollback result. + +The update checker must never install a package whose version or variant cannot +be matched to the installed stack. + +## Configuration Safety + +All configuration writes must be validated and recoverable. + +Files controlled by Manager: + +```text +C:\ProgramData\OVMS\settings.json +C:\ProgramData\OVMS\models\config.json +C:\ProgramData\OVMS\runtime.json +``` + +Rules: + +- Validate JSON before saving. +- Validate against schema where a schema exists. +- Write to a temporary file first. +- Atomically replace the destination file. +- Keep a `.bak` copy of the previous valid version. +- Preserve comments only if a comment-preserving parser is adopted; otherwise + treat files as strict JSON. +- If config is malformed on startup, show the error and offer restore from + backup. + +Suggested backup names: + +```text +settings.json.bak +config.json.bak +runtime.json.bak +``` + +Restart behavior: + +- Settings that alter the OVMS command line require restart. +- Settings that only affect Manager UI should apply immediately. +- Manager must validate config before restarting OVMS. + +## Security and Network Exposure + +Default bind address must be local-only: + +```text +127.0.0.1 +``` + +Binding to these addresses is security-sensitive: + +```text +0.0.0.0 +:: +LAN adapter IPs +``` + +If the user selects a non-local bind address, Manager should: + +- Show an explicit warning that the model server may be reachable from other + devices. +- Require confirmation. +- Show the active URLs after restart. +- Avoid adding firewall rules unless the user explicitly opts in. + +If firewall rule management is added later, it must be treated as an admin-only +operation and must be removed during uninstall if created by the installer or +Manager. + +## Model Download and Trust + +Model download can be added after the MVP, but the plan must account for trust +and storage rules. + +Model download requirements: + +- Show source, model id, license link, and estimated size before download. +- Support cancellation. +- Support resume when practical. +- Check free disk space before download. +- Use a cache under `C:\ProgramData\OVMS\downloads` by default. +- Support Hugging Face authentication for gated models if needed. +- Do not auto-accept model licenses on behalf of the user. +- Record downloaded model metadata. + +Metadata path: + +```text +C:\ProgramData\OVMS\models\model-metadata.json +``` + +Suggested metadata: + +```json +{ + "models": [ + { + "name": "OpenVINO/Qwen3-8B-int4-ov", + "source": "huggingface", + "path": "C:\\ProgramData\\OVMS\\models\\OpenVINO\\Qwen3-8B-int4-ov", + "downloadedAtUtc": "2026-06-30T15:00:00Z", + "licenseAccepted": false + } + ] +} +``` + +## Installer Responsibilities + +Install: + +- Copy `dist/windows/ovms` to install directory. +- Copy `OVMS Manager.exe` and supporting files. +- Create `C:\ProgramData\OVMS`. +- Create `install.json`. +- Create default `settings.json`. +- Create default `models\config.json`. +- Add PATH entries if selected. +- Register service if selected. +- Add login startup entry if selected. +- Create Start Menu shortcuts. +- Launch Manager if selected. + +Uninstall: + +- Stop Manager-controlled process if running. +- Stop service if installed. +- Delete service if installed. +- Remove login startup entry. +- Remove PATH entries created by installer. +- Remove program files. +- Ask whether to preserve models, logs, and settings under `C:\ProgramData\OVMS`. + +## Manager Responsibilities + +- Read and write `settings.json`. +- Read `install.json` and report installed version/variant. +- Read and update OVMS `config.json`. +- Build the effective `ovms.exe` command line. +- Start and stop `ovms.exe` in user-login/manual modes. +- Start and stop the `ovms` Windows Service in service mode. +- Update service command line when settings change. +- Run health checks. +- Show logs. +- Detect package capabilities. +- Repair or upgrade the installed package. +- Check for updates. +- Manage startup mode. +- Enforce runtime ownership rules. +- Validate ports before starting OVMS. +- Validate and atomically write configuration. +- Export diagnostics bundles. + +## Build Pipeline + +Initial local pipeline: + +```powershell +.\windows_build.bat --with_python +.\windows_create_package.bat opt --with_python +.\packaging\windows\manager\build_manager.ps1 +.\packaging\windows\installer\build_installer.ps1 +``` + +Expected outputs: + +```text +dist/windows/ovms.zip +dist/windows/OVMS-Setup.exe +``` + +The installer build should fail clearly if `dist/windows/ovms/ovms.exe` is +missing. + +## Uninstaller + +Uninstall support is a first-class deliverable. The Windows installer must +register a standard uninstaller in Windows Apps & Features and provide clean +scripted cleanup for files, service state, startup entries, and environment +changes. + +Uninstaller entry: + +```text +Settings > Apps > Installed apps > OpenVINO Model Server +``` + +Uninstaller responsibilities: + +- Stop `OVMS Manager.exe` if it is running. +- Stop any Manager-launched `ovms.exe` process. +- Stop the `ovms` Windows Service if installed. +- Delete the `ovms` Windows Service if installed. +- Remove login startup registry entries created by the installer. +- Remove PATH entries created by the installer. +- Remove Start Menu and desktop shortcuts. +- Remove installed program files under `C:\Program Files\OVMS`. +- Ask whether to preserve user data under `C:\ProgramData\OVMS`. +- Preserve models, logs, and settings by default unless the user selects full + removal. + +User data removal options: + +```text +Preserve models, logs, and settings +Remove logs and settings but keep models +Remove everything, including models +``` + +The uninstaller should avoid deleting arbitrary user-selected directories unless +they are inside `C:\ProgramData\OVMS` or were explicitly created by the +installer. If the user configured an external model repository, uninstall should +preserve it by default and show the path before offering deletion. + +## Diagnostics + +Diagnostics should be available before release hardening. The Manager should +include a "Copy diagnostics" or "Export diagnostics bundle" action. + +Diagnostics bundle contents: + +- Manager version. +- OVMS version from `ovms.exe --version`. +- Installed package variant. +- Install directory. +- Data directory. +- Effective command line with secrets redacted. +- `settings.json`. +- OVMS `config.json`. +- `runtime.json`. +- Service state from `sc query ovms`, when available. +- Service configuration from `sc qc ovms`, when available. +- Port ownership for configured REST and gRPC ports. +- Last 500 lines of `ovms_server.log`, if present. +- Recent Manager log entries. +- Latest health check result. + +Diagnostics should be written to: + +```text +C:\ProgramData\OVMS\diagnostics\ovms-diagnostics-.zip +``` + +Sensitive values, tokens, and credentials must be redacted. + +## Test Matrix + +The Windows installer and Manager should have explicit install-time and +runtime smoke tests. + +Installer tests: + +- Detect existing install and show repair/upgrade/modify/uninstall choices. +- Clean machine-wide install. +- Install with PATH enabled. +- Install without PATH. +- Install with Manager startup enabled. +- Install with service mode enabled. +- Install to a path containing spaces. +- Install when `C:\ProgramData\OVMS` already exists. +- Install over a previous version. +- Detect existing unmanaged `ovms.exe` and warn without modifying it. + +Manager tests: + +- Start/stop/restart in user-login mode. +- Start/stop/restart in service mode. +- Switch from process mode to service mode. +- Switch from service mode to process mode. +- Block duplicate start when port is occupied. +- Report owner when another OVMS process is running. +- Persist settings changes. +- Restore malformed config from backup. +- Export diagnostics bundle. + +Uninstaller tests: + +- Uninstall after process-mode install. +- Uninstall after service-mode install. +- Preserve models/logs/settings. +- Remove logs/settings but keep models. +- Full removal. +- External model repository is preserved by default. +- PATH and startup entries are removed. +- Service is stopped and deleted. + +Upgrade/repair tests: + +- Repair missing DLL. +- Repair missing Manager file. +- Upgrade python_off to python_on. +- Failed upgrade rolls back. +- Upgrade preserves settings and model repository. +- Update checker reports up-to-date state. +- Update checker reports update-available state. +- Update checker handles offline/network failure. + +Security tests: + +- Default bind is `127.0.0.1`. +- Non-local bind shows warning. +- Firewall changes require explicit opt-in if implemented. + +## Implementation Phases + +### Phase 1: Foundation + +- Add folder layout. +- Add README files. +- Add settings schema. +- Add install-options schema. +- Add install marker schema. +- Add runtime state schema. +- Add validation script. +- Document service/process/startup behavior. +- Document privilege model. +- Document runtime ownership rules. +- Document config safety rules. + +Acceptance criteria: + +- `packaging/windows` documents the intended architecture. +- `validate-install.ps1` can inspect an unpacked OVMS folder and report missing + required files. +- The plan defines admin-only actions and non-admin Manager actions. +- The plan defines duplicate-start prevention. + +### Phase 2: Installer MVP + +- Add Inno Setup script. +- Add existing-install detection. +- Install existing `dist/windows/ovms` package. +- Write `settings.json`. +- Write empty OVMS `config.json`. +- Create Start Menu shortcuts. +- Support Add to PATH. +- Support optional service registration. +- Register a standard Apps & Features uninstaller. +- Support clean uninstall with preserve/remove data options. +- Implement elevated machine-wide install. +- Fail clearly when required elevation is unavailable. + +Acceptance criteria: + +- Fresh install succeeds on Windows. +- Existing install shows repair/upgrade/modify/uninstall choices. +- `ovms.exe --version` works after install. +- User can start OVMS manually from installed files. +- Uninstall removes program files, service registration, startup entries, PATH + changes, and shortcuts. +- Uninstall preserves or removes data according to user choice. +- Installer-created PATH and startup entries are tracked for cleanup. + +### Phase 3: Manager MVP + +- Add WPF Manager shell. +- Add tray icon. +- Add dashboard. +- Add start/stop/restart controls. +- Add settings page. +- Add logs page. +- Add start-at-login toggle. +- Add runtime ownership detection. +- Add port preflight. +- Add minimal diagnostics export. + +Acceptance criteria: + +- Manager starts from Start Menu. +- Tray menu works. +- Manager can start and stop `ovms.exe`. +- Health check reflects the running server state. +- Settings changes persist. +- Manager blocks duplicate starts and reports port conflicts. +- Diagnostics bundle can be exported. + +### Phase 4: Service Mode + +- Manager can register/unregister service. +- Manager can start/stop/restart service. +- Manager can set service start type. +- Installer and Manager use compatible service configuration. +- Switching runtime modes stops the previous owner first. + +Acceptance criteria: + +- `sc query ovms` reflects selected state. +- Service mode starts OVMS with configured port, bind address, config path, and + log path. +- Manager reports service errors clearly. +- Manager does not allow process mode and service mode to run OVMS + simultaneously. + +### Phase 5: Model Management + +- List configured models. +- Add local model to `config.json`. +- Remove model from `config.json`. +- Open model repository. +- Refresh health/model list. +- Validate config before writing. +- Backup config before writing. + +Acceptance criteria: + +- Adding a local model updates `config.json`. +- Removing a model updates `config.json`. +- Manager shows served models when OVMS is running. +- Malformed config can be restored from backup. + +### Phase 6: Feature Management + +- Detect GenAI support. +- Detect Python support. +- Detect package variant. +- Offer repair install. +- Offer upgrade from python_off to python_on using a matching package. +- Add manual update checking. +- Stage package upgrades. +- Verify package before swap. +- Roll back failed upgrades. + +Acceptance criteria: + +- Manager reports installed capabilities accurately. +- Repair verifies required files after completion. +- Upgrade preserves settings and model repository. +- Failed upgrade restores the previous working package. +- Update checker reports current state accurately. + +### Phase 7: Release Hardening + +- Add code signing path. +- Add installer versioning. +- Add upgrade behavior. +- Add modify/repair behavior for existing installs. +- Add update checking and package-feed compatibility rules. +- Add diagnostics bundle export. +- Add CI packaging job. +- Add documentation for support and troubleshooting. +- Add automated installer, Manager, service, uninstall, and upgrade smoke tests. + +Acceptance criteria: + +- Signed installer builds reproducibly. +- Installing over a previous version upgrades cleanly. +- Diagnostics bundle contains settings, logs, version, service state, and health + check output. +- Test matrix runs successfully on a clean Windows worker. + +## Open Questions + +- Should the default REST port be 8000, or should it preserve the current local + AgentTools convention when used there? +- Should the installer default to user-login mode or service mode for non-admin + installs? +- Should the first version support model download during install, or should that + be Manager-only after install? +- Should we ship both python_on and python_off installers, or one installer that + can download the other package variant? +- Should Manager require admin elevation only for service/PATH changes, or run + elevated at startup? +- Should the first release support per-user install, or only elevated + machine-wide install? +- What package signature/hash source should Manager trust for repair and + upgrade downloads? +- Should periodic update checks be opt-in during install or only configurable + later in Manager? +- What registry keys should be authoritative for installed-version detection? +- Should non-local bind addresses be allowed in the installer, or only in + Manager advanced settings? +- How should Manager store Hugging Face tokens if gated model downloads are + supported? + +## First Concrete Tasks + +1. Add `packaging/windows/README.md` summarizing the installer and manager + ownership. +2. Add schemas for `settings.json`, install options, and install marker. +3. Add existing-install detection rules. +4. Add runtime state schema and duplicate-start ownership rules. +5. Add `validate-install.ps1` for unpacked `dist/windows/ovms`. +6. Add an Inno Setup MVP that installs the existing package. +7. Add explicit uninstaller scripts and Apps & Features registration. +8. Add a minimal WPF Manager with tray icon and start/stop controls. +9. Add diagnostics export and port preflight. +10. Add manual update checker design and package metadata contract. diff --git a/packaging/windows/README.md b/packaging/windows/README.md new file mode 100644 index 0000000000..059ea019ad --- /dev/null +++ b/packaging/windows/README.md @@ -0,0 +1,43 @@ +# OVMS Windows Packaging + +This folder contains the Windows installer, uninstaller, server-control scripts, +and lightweight tray app for OpenVINO Model Server. + +The first implementation intentionally keeps the full configuration UI out of +scope. It provides: + +- An Inno Setup installer that consumes `dist/windows/ovms`. +- A standard Windows uninstaller. +- First-run settings and install markers under `C:\ProgramData\OVMS`. +- A minimal tray app with: + - Open OVMS Manager + - Start Server + - Stop Server + +The installed application should behave like a regular Windows application. It +must not require the user to install Python, .NET, OpenVINO, GenAI, or other +runtime dependencies separately. Those files are bundled in the installer +payload: + +- OpenVINO and GenAI DLLs come from `dist/windows/ovms`. +- Bundled Python support comes from the `python_on` OVMS package. +- The tray app is published self-contained so it does not require a separate + .NET Desktop Runtime install. +- Inno Setup is a build-time tool only; users do not need it installed. + +The full Manager UI is planned as a later phase. + +## Build Order + +```powershell +.\windows_build.bat --with_python +.\windows_create_package.bat opt --with_python +.\packaging\windows\manager\build_manager.ps1 +.\packaging\windows\installer\build_installer.ps1 +``` + +Expected output: + +```text +dist/windows/OVMS-Setup.exe +``` diff --git a/packaging/windows/installer/OVMSInstaller.iss b/packaging/windows/installer/OVMSInstaller.iss new file mode 100644 index 0000000000..97c1d88346 --- /dev/null +++ b/packaging/windows/installer/OVMSInstaller.iss @@ -0,0 +1,315 @@ +#define AppName "OpenVINO Model Server" +#define AppPublisher "OpenVINO" +#define AppExeName "OVMS.Manager.exe" +#ifndef SourceRoot +#define SourceRoot "..\..\.." +#endif +#ifndef OvmsSourceDir +#define OvmsSourceDir SourceRoot + "\dist\windows\ovms" +#endif +#ifndef ManagerPublishDir +#define ManagerPublishDir SourceRoot + "\packaging\windows\manager\artifacts\publish" +#endif +#ifndef OutputDir +#define OutputDir SourceRoot + "\dist\windows" +#endif + +[Setup] +AppId={{9F35B167-C72A-4E1D-93E9-448CE4AE5270} +AppName={#AppName} +AppVersion=1.0.0 +AppPublisher={#AppPublisher} +DefaultDirName={localappdata}\Programs\OVMS +DefaultGroupName=OpenVINO Model Server +DisableProgramGroupPage=yes +OutputDir={#OutputDir} +OutputBaseFilename=OVMS-Setup +Compression=lzma +SolidCompression=yes +ArchitecturesAllowed=x64compatible +ArchitecturesInstallIn64BitMode=x64compatible +PrivilegesRequired=lowest +UninstallDisplayName={#AppName} +UninstallDisplayIcon={app}\OVMS.Manager.exe +CloseApplications=yes +RestartApplications=no +WizardStyle=modern +WizardSizePercent=120 +WizardImageFile=assets\ovms-welcome.bmp +WizardSmallImageFile=assets\ovms-small.bmp +WizardImageStretch=yes +DisableWelcomePage=no +ShowLanguageDialog=no +DisableReadyPage=no + +[Languages] +Name: "english"; MessagesFile: "compiler:Default.isl" + +[Tasks] +Name: "path"; Description: "Add OVMS to PATH"; GroupDescription: "Optional configuration:"; Flags: checkedonce +Name: "startup"; Description: "Start tray app when I sign in"; GroupDescription: "Optional configuration:"; Flags: checkedonce + +[Files] +Source: "{#OvmsSourceDir}\*"; DestDir: "{app}"; Flags: ignoreversion recursesubdirs createallsubdirs +Source: "{#ManagerPublishDir}\*"; DestDir: "{app}"; Flags: ignoreversion recursesubdirs createallsubdirs +Source: "{#SourceRoot}\packaging\windows\scripts\*.ps1"; DestDir: "{app}\scripts"; Flags: ignoreversion +Source: "{#SourceRoot}\packaging\windows\settings\settings.template.json"; DestDir: "{app}\templates"; Flags: ignoreversion +Source: "{#OvmsSourceDir}\*"; DestDir: "{localappdata}\OVMS\packages\source"; Flags: ignoreversion recursesubdirs createallsubdirs + +[Icons] +Name: "{group}\Open OVMS Manager"; Filename: "{app}\OVMS.Manager.exe" +Name: "{group}\Start OVMS Server"; Filename: "powershell.exe"; Parameters: "-NoProfile -ExecutionPolicy Bypass -File ""{app}\scripts\start-ovms.ps1"" -DataDir ""{localappdata}\OVMS""" +Name: "{group}\Stop OVMS Server"; Filename: "powershell.exe"; Parameters: "-NoProfile -ExecutionPolicy Bypass -File ""{app}\scripts\stop-ovms.ps1"" -DataDir ""{localappdata}\OVMS""" +Name: "{group}\Repair OVMS Server"; Filename: "powershell.exe"; Parameters: "-NoProfile -ExecutionPolicy Bypass -File ""{app}\scripts\repair-package.ps1"" -InstallDir ""{app}"" -DataDir ""{localappdata}\OVMS""" +Name: "{group}\Uninstall OpenVINO Model Server"; Filename: "{uninstallexe}" + +[Registry] +Root: HKCU; Subkey: "Software\OpenVINO\OVMS"; ValueType: string; ValueName: "InstallDir"; ValueData: "{app}"; Flags: uninsdeletekey +Root: HKCU; Subkey: "Software\OpenVINO\OVMS"; ValueType: string; ValueName: "DataDir"; ValueData: "{localappdata}\OVMS"; Flags: uninsdeletekey +Root: HKCU; Subkey: "Software\OpenVINO\OVMS"; ValueType: string; ValueName: "Version"; ValueData: "1.0.0"; Flags: uninsdeletekey +Root: HKCU; Subkey: "Software\Microsoft\Windows\CurrentVersion\Run"; ValueType: string; ValueName: "OVMS Manager"; ValueData: """{app}\OVMS.Manager.exe"""; Tasks: startup; Flags: uninsdeletevalue + +[Run] +Filename: "powershell.exe"; Parameters: "-NoProfile -ExecutionPolicy Bypass -File ""{app}\scripts\configure-ovms.ps1"" -InstallDir ""{app}"" -DataDir ""{localappdata}\OVMS"" -PackageVariant ""python_on"""; Flags: runhidden waituntilterminated +Filename: "powershell.exe"; Parameters: "-NoProfile -ExecutionPolicy Bypass -File ""{app}\scripts\set-path.ps1"" -InstallDir ""{app}"" -Action Add"; Tasks: path; Flags: runhidden waituntilterminated +Filename: "{app}\OVMS.Manager.exe"; Description: "Launch OVMS Manager"; Flags: nowait postinstall skipifsilent + +[UninstallRun] +Filename: "powershell.exe"; Parameters: "-NoProfile -ExecutionPolicy Bypass -File ""{app}\scripts\uninstall-ovms.ps1"" -InstallDir ""{app}"" -DataDir ""{localappdata}\OVMS"" -DataMode ""{code:GetDataMode}"""; Flags: runhidden waituntilterminated; RunOnceId: "OVMSCleanup" + +[Code] +var + DataModeChoice: String; + // Existing-install detection state, populated in InitializeSetup and + // consumed by InitializeWizard/NextButtonClick to drive the custom page. + ExistingInstallDetected: Boolean; + ExistingInstallDir: String; + ExistingUninstallString: String; + ExistingInstallPage: TWizardPage; + RbRepair: TNewRadioButton; + RbUninstall: TNewRadioButton; + // When True, the "Exit Setup?" confirmation is suppressed so Setup can + // close cleanly after handing off to the existing uninstaller. + SuppressCancelConfirm: Boolean; + +function GetDataMode(Param: String): String; +begin + Result := DataModeChoice; +end; +function QueryExistingManagedInstall(var ExistingDir: String; var UninstallString: String): Boolean; +begin + Result := False; + ExistingDir := ''; + UninstallString := ''; + + if RegQueryStringValue(HKCU, 'Software\OpenVINO\OVMS', 'InstallDir', ExistingDir) then + begin + RegQueryStringValue(HKCU, 'Software\Microsoft\Windows\CurrentVersion\Uninstall\{9F35B167-C72A-4E1D-93E9-448CE4AE5270}_is1', 'UninstallString', UninstallString); + Result := True; + exit; + end; + + if RegQueryStringValue(HKCU, 'Software\Microsoft\Windows\CurrentVersion\Uninstall\{9F35B167-C72A-4E1D-93E9-448CE4AE5270}_is1', 'InstallLocation', ExistingDir) then + begin + RegQueryStringValue(HKCU, 'Software\Microsoft\Windows\CurrentVersion\Uninstall\{9F35B167-C72A-4E1D-93E9-448CE4AE5270}_is1', 'UninstallString', UninstallString); + Result := True; + exit; + end; + + if DirExists(ExpandConstant('{localappdata}\Programs\OVMS')) then + begin + ExistingDir := ExpandConstant('{localappdata}\Programs\OVMS'); + RegQueryStringValue(HKCU, 'Software\Microsoft\Windows\CurrentVersion\Uninstall\{9F35B167-C72A-4E1D-93E9-448CE4AE5270}_is1', 'UninstallString', UninstallString); + Result := True; + end; +end; + +function HasRunningOvmsProcess(): Boolean; +var + ResultCode: Integer; + MarkerPath: String; +begin + MarkerPath := ExpandConstant('{tmp}\ovms-running.txt'); + DeleteFile(MarkerPath); + Exec('powershell.exe', + '-NoProfile -ExecutionPolicy Bypass -Command "if (Get-Process ovms -ErrorAction SilentlyContinue) { ''''running'''' | Set-Content -Path ''''' + MarkerPath + ''''' }"', + '', SW_HIDE, ewWaitUntilTerminated, ResultCode); + Result := FileExists(MarkerPath); + DeleteFile(MarkerPath); +end; + +function RunExistingUninstaller(UninstallString: String): Boolean; +var + ResultCode: Integer; +begin + Result := False; + if UninstallString = '' then + begin + MsgBox('An existing OpenVINO Model Server install was detected, but its uninstaller could not be found.', mbError, MB_OK); + exit; + end; + + Exec('cmd.exe', '/C ' + UninstallString, '', SW_SHOWNORMAL, ewWaitUntilTerminated, ResultCode); + Result := ResultCode = 0; +end; + +procedure StopRunningManager(); +var + ResultCode: Integer; +begin + Exec('taskkill.exe', '/IM OVMS.Manager.exe /F', '', SW_HIDE, ewWaitUntilTerminated, ResultCode); +end; + +function InitializeSetup(): Boolean; +begin + Result := True; + StopRunningManager(); + + // Existing-install detection: just stash the result in globals. The actual + // repair-vs-uninstall choice is now presented as a custom wizard page + // (see InitializeWizard/NextButtonClick below) instead of a MsgBox here. + // For silent installs there is no UI at all, so default to repair/upgrade + // (i.e. just proceed) -- ExistingInstallDetected is still set, but since + // the custom page is never shown/navigated in silent mode, RbRepair's + // default behavior (proceed) is effectively what happens. + ExistingInstallDetected := QueryExistingManagedInstall(ExistingInstallDir, ExistingUninstallString); + + if HasRunningOvmsProcess() then + begin + if WizardSilent() then + begin + Result := True; + exit; + end; + + if MsgBox( + 'A running ovms.exe process was detected, but it does not appear to be managed by this installer.' + #13#10#13#10 + + 'Setup will not repair or uninstall unmanaged portable OVMS processes. Stop the running server before installing if it uses the same ports.' + #13#10#13#10 + + 'Continue setup?', + mbConfirmation, MB_YESNO) = IDNO then + begin + Result := False; + end; + end; +end; + +procedure InitializeWizard(); +var + Lbl: TNewStaticText; +begin + if not ExistingInstallDetected then + exit; + + ExistingInstallPage := CreateCustomPage(wpWelcome, + 'Existing Installation Found', + 'OpenVINO Model Server is already installed on this computer.'); + + Lbl := TNewStaticText.Create(ExistingInstallPage); + Lbl.Parent := ExistingInstallPage.Surface; + Lbl.Left := 0; + Lbl.Top := 0; + Lbl.Width := ExistingInstallPage.SurfaceWidth; + Lbl.AutoSize := False; + Lbl.WordWrap := True; + Lbl.Caption := + 'An existing install was found at:' + #13#10 + + ExistingInstallDir + #13#10#13#10 + + 'Choose how to continue:'; + Lbl.Height := ScaleY(60); + + RbRepair := TNewRadioButton.Create(ExistingInstallPage); + RbRepair.Parent := ExistingInstallPage.Surface; + RbRepair.Left := 0; + RbRepair.Top := Lbl.Top + Lbl.Height + ScaleY(8); + RbRepair.Width := ExistingInstallPage.SurfaceWidth; + RbRepair.Height := ScaleY(17); + RbRepair.Caption := 'Repair or upgrade the existing installation (recommended)'; + RbRepair.Checked := True; + + RbUninstall := TNewRadioButton.Create(ExistingInstallPage); + RbUninstall.Parent := ExistingInstallPage.Surface; + RbUninstall.Left := 0; + RbUninstall.Top := RbRepair.Top + RbRepair.Height + ScaleY(8); + RbUninstall.Width := ExistingInstallPage.SurfaceWidth; + RbUninstall.Height := ScaleY(17); + RbUninstall.Caption := 'Uninstall the existing installation, then exit'; +end; + +function NextButtonClick(CurPageID: Integer): Boolean; +begin + Result := True; + + if (ExistingInstallPage <> nil) and (CurPageID = ExistingInstallPage.ID) then + begin + if RbUninstall.Checked then + begin + RunExistingUninstaller(ExistingUninstallString); + // Hand-off to the existing uninstaller is done; close Setup cleanly. + // SuppressCancelConfirm tells CancelButtonClick to skip the normal + // "Setup is not complete. Exit Setup?" confirmation, so the user is + // not nagged after deliberately choosing to uninstall and exit. + MsgBox('The existing OpenVINO Model Server installation has been removed.' + #13#10#13#10 + + 'Setup will now close. Run it again to install a fresh copy.', + mbInformation, MB_OK); + SuppressCancelConfirm := True; + WizardForm.Close; + Result := False; + exit; + end; + + // RbRepair.Checked (default): proceed with repair/upgrade as normal. + Result := True; + end; +end; + +procedure CancelButtonClick(CurPageID: Integer; var Cancel, Confirm: Boolean); +begin + // After the "Uninstall existing, then exit" path triggers WizardForm.Close, + // skip the "Setup is not complete" confirmation prompt. + if SuppressCancelConfirm then + Confirm := False; +end; + +function InitializeUninstall(): Boolean; +var + DataDir: String; + KeepEverything: Integer; + RemoveModelsToo: Integer; +begin + Result := True; + DataDir := ExpandConstant('{localappdata}\OVMS'); + DataModeChoice := 'PreserveAll'; + + if UninstallSilent() then + begin + exit; + end; + + KeepEverything := MsgBox( + 'Keep your downloaded models and data under:' + #13#10 + + DataDir + #13#10#13#10 + + 'Yes = keep everything (models, settings, and logs)' + #13#10 + + 'No = choose what to remove', + mbConfirmation, MB_YESNO); + + if KeepEverything = IDYES then + begin + DataModeChoice := 'PreserveAll'; + exit; + end; + + RemoveModelsToo := MsgBox( + 'Also delete your downloaded MODELS?' + #13#10#13#10 + + 'Yes = remove everything, including models' + #13#10 + + 'No = keep models, but remove settings and logs', + mbConfirmation, MB_YESNO); + + if RemoveModelsToo = IDYES then + begin + DataModeChoice := 'RemoveAll'; + end + else + begin + DataModeChoice := 'RemoveSettingsKeepModels'; + end; +end; diff --git a/packaging/windows/installer/README.md b/packaging/windows/installer/README.md new file mode 100644 index 0000000000..8caadaa96f --- /dev/null +++ b/packaging/windows/installer/README.md @@ -0,0 +1,29 @@ +# OVMS Windows Installer + +The installer is built with Inno Setup from `OVMSInstaller.iss`. + +It expects the OVMS portable package to already exist at: + +```text +dist/windows/ovms +``` + +The installer adds: + +- OVMS files under `C:\Program Files\OVMS`. +- Data folders under `C:\ProgramData\OVMS`. +- Default settings and install marker files. +- Start Menu shortcuts. +- Optional PATH entry. +- Optional start-at-login entry for the tray app. +- Optional Windows Service registration. +- A standard Apps & Features uninstaller. + +The installer must be fully offline once built. It should not download Python, +.NET, OpenVINO, GenAI, or model-server dependencies during installation. The +OVMS package and self-contained tray app must already be present in the +installer payload. + +The uninstaller stops managed OVMS processes, removes optional service/startup +configuration, removes installer-created PATH entries, and removes installed +program files. Data under `C:\ProgramData\OVMS` is preserved by default. diff --git a/packaging/windows/installer/assets/ovms-small.bmp b/packaging/windows/installer/assets/ovms-small.bmp new file mode 100644 index 0000000000..134821c273 Binary files /dev/null and b/packaging/windows/installer/assets/ovms-small.bmp differ diff --git a/packaging/windows/installer/assets/ovms-welcome.bmp b/packaging/windows/installer/assets/ovms-welcome.bmp new file mode 100644 index 0000000000..b32e7c5dd1 Binary files /dev/null and b/packaging/windows/installer/assets/ovms-welcome.bmp differ diff --git a/packaging/windows/installer/build_installer.ps1 b/packaging/windows/installer/build_installer.ps1 new file mode 100644 index 0000000000..868738a173 --- /dev/null +++ b/packaging/windows/installer/build_installer.ps1 @@ -0,0 +1,59 @@ +param( + [string]$Configuration = "Release", + [string]$SourceRoot = (Resolve-Path (Join-Path $PSScriptRoot "..\..\..")).Path, + [string]$OvmsSourceDir = "", + [string]$OutputDir = "", + [string]$InnoSetupPath = "" +) + +$ErrorActionPreference = "Stop" + +$ovmsDir = if ($OvmsSourceDir) { (Resolve-Path -LiteralPath $OvmsSourceDir).Path } else { Join-Path $SourceRoot "dist\windows\ovms" } +$managerPublishDir = Join-Path $SourceRoot "packaging\windows\manager\artifacts\publish" +$installerScript = Join-Path $PSScriptRoot "OVMSInstaller.iss" +$outputDir = if ($OutputDir) { $OutputDir } else { Join-Path $SourceRoot "dist\windows" } + +if (-not (Test-Path (Join-Path $ovmsDir "ovms.exe"))) { + throw "Missing OVMS package at $ovmsDir. Run windows_create_package.bat first." +} + +if (-not (Test-Path (Join-Path $managerPublishDir "OVMS.Manager.exe"))) { + & (Join-Path $SourceRoot "packaging\windows\manager\build_manager.ps1") -Configuration $Configuration +} + +if (-not $InnoSetupPath) { + $cmd = Get-Command ISCC.exe -ErrorAction SilentlyContinue + if ($cmd) { + $InnoSetupPath = $cmd.Source + } else { + $candidates = @( + "${env:ProgramFiles(x86)}\Inno Setup 6\ISCC.exe", + "$env:ProgramFiles\Inno Setup 6\ISCC.exe" + ) + foreach ($candidate in $candidates) { + if (Test-Path $candidate) { + $InnoSetupPath = $candidate + break + } + } + } +} + +if (-not $InnoSetupPath -or -not (Test-Path $InnoSetupPath)) { + throw "Inno Setup compiler ISCC.exe was not found. Install Inno Setup 6 or pass -InnoSetupPath." +} + +New-Item -ItemType Directory -Force -Path $outputDir | Out-Null + +& $InnoSetupPath ` + "/DSourceRoot=$SourceRoot" ` + "/DOvmsSourceDir=$ovmsDir" ` + "/DManagerPublishDir=$managerPublishDir" ` + "/DOutputDir=$outputDir" ` + $installerScript + +if ($LASTEXITCODE -ne 0) { + throw "Inno Setup failed with exit code $LASTEXITCODE." +} + +Write-Host "[INFO] Installer created at $(Join-Path $outputDir 'OVMS-Setup.exe')" diff --git a/packaging/windows/manager/.gitignore b/packaging/windows/manager/.gitignore new file mode 100644 index 0000000000..b8c89995ae --- /dev/null +++ b/packaging/windows/manager/.gitignore @@ -0,0 +1,3 @@ +artifacts/ +src/**/bin/ +src/**/obj/ diff --git a/packaging/windows/manager/build_manager.ps1 b/packaging/windows/manager/build_manager.ps1 new file mode 100644 index 0000000000..2b145d5351 --- /dev/null +++ b/packaging/windows/manager/build_manager.ps1 @@ -0,0 +1,22 @@ +param( + [string]$Configuration = "Release" +) + +$ErrorActionPreference = "Stop" + +$project = Join-Path $PSScriptRoot "src\OVMS.Manager\OVMS.Manager.csproj" +$output = Join-Path $PSScriptRoot "artifacts\publish" + +dotnet publish $project ` + -c $Configuration ` + -r win-x64 ` + --self-contained true ` + -p:PublishSingleFile=true ` + -p:IncludeNativeLibrariesForSelfExtract=true ` + -p:EnableCompressionInSingleFile=true ` + -o $output +if ($LASTEXITCODE -ne 0) { + throw "dotnet publish failed." +} + +Write-Host "[INFO] Manager published to $output" diff --git a/packaging/windows/manager/src/OVMS.Manager/Assets/ovms-manager.ico b/packaging/windows/manager/src/OVMS.Manager/Assets/ovms-manager.ico new file mode 100644 index 0000000000..c31506f868 Binary files /dev/null and b/packaging/windows/manager/src/OVMS.Manager/Assets/ovms-manager.ico differ diff --git a/packaging/windows/manager/src/OVMS.Manager/OVMS.Manager.csproj b/packaging/windows/manager/src/OVMS.Manager/OVMS.Manager.csproj new file mode 100644 index 0000000000..b8b0a94ba1 --- /dev/null +++ b/packaging/windows/manager/src/OVMS.Manager/OVMS.Manager.csproj @@ -0,0 +1,12 @@ + + + WinExe + net8.0-windows + true + enable + enable + OVMS.Manager + OVMS.Manager + Assets\ovms-manager.ico + + diff --git a/packaging/windows/manager/src/OVMS.Manager/Program.cs b/packaging/windows/manager/src/OVMS.Manager/Program.cs new file mode 100644 index 0000000000..e650726200 --- /dev/null +++ b/packaging/windows/manager/src/OVMS.Manager/Program.cs @@ -0,0 +1,212 @@ +using System.Diagnostics; +using System.Drawing; +using System.Text.Json; +using System.Windows.Forms; + +namespace OVMS.Manager; + +internal static class Program +{ + [STAThread] + private static void Main() + { + ApplicationConfiguration.Initialize(); + using var app = new TrayApplicationContext(); + Application.Run(app); + } +} + +internal sealed class TrayApplicationContext : ApplicationContext +{ + private readonly NotifyIcon notifyIcon; + private readonly ToolStripMenuItem startItem; + private readonly ToolStripMenuItem stopItem; + private readonly OvmsController controller; + + public TrayApplicationContext() + { + controller = new OvmsController(); + + startItem = new ToolStripMenuItem("Start Server", null, (_, _) => RunAction("Start Server", controller.Start)); + stopItem = new ToolStripMenuItem("Stop Server", null, (_, _) => RunAction("Stop Server", controller.Stop)); + + var openItem = new ToolStripMenuItem("Open OVMS Manager", null, (_, _) => OpenManager()); + var menu = new ContextMenuStrip(); + menu.Items.Add(openItem); + menu.Items.Add(startItem); + menu.Items.Add(stopItem); + + notifyIcon = new NotifyIcon + { + Icon = Icon.ExtractAssociatedIcon(Application.ExecutablePath) ?? SystemIcons.Application, + Text = "OpenVINO Model Server", + ContextMenuStrip = menu, + Visible = true + }; + notifyIcon.MouseClick += (_, args) => + { + if (args.Button == MouseButtons.Left) + { + OpenManager(); + } + }; + } + + protected override void Dispose(bool disposing) + { + if (disposing) + { + notifyIcon.Visible = false; + notifyIcon.Dispose(); + controller.Dispose(); + } + base.Dispose(disposing); + } + + private void OpenManager() + { + var status = controller.GetStatus(); + MessageBox.Show(status, "OpenVINO Model Server", MessageBoxButtons.OK, MessageBoxIcon.Information); + } + + private void RunAction(string title, Action action) + { + try + { + startItem.Enabled = false; + stopItem.Enabled = false; + action(); + notifyIcon.ShowBalloonTip(3000, "OpenVINO Model Server", $"{title} completed.", ToolTipIcon.Info); + } + catch (Exception ex) + { + MessageBox.Show(ex.Message, title, MessageBoxButtons.OK, MessageBoxIcon.Error); + } + finally + { + startItem.Enabled = true; + stopItem.Enabled = true; + } + } +} + +internal sealed class OvmsController : IDisposable +{ + private readonly string dataDir = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), "OVMS"); + private readonly string appDir = AppContext.BaseDirectory; + + public string GetStatus() + { + var settings = LoadSettings(); + var runtimePath = Path.Combine(dataDir, "runtime.json"); + var status = "Stopped"; + + if (File.Exists(runtimePath)) + { + try + { + using var doc = JsonDocument.Parse(File.ReadAllText(runtimePath)); + if (doc.RootElement.TryGetProperty("pid", out var pidElement) && pidElement.TryGetInt32(out var pid)) + { + var process = Process.GetProcessById(pid); + if (string.Equals(process.ProcessName, "ovms", StringComparison.OrdinalIgnoreCase)) + { + status = $"Running, pid {pid}"; + } + } + } + catch + { + status = "Runtime state is stale or unreadable"; + } + } + + return string.Join(Environment.NewLine, new[] + { + $"Status: {status}", + $"REST: http://{settings.BindAddress}:{settings.RestPort}", + $"Install: {settings.InstallDir}", + $"Config: {settings.ConfigPath}", + $"Logs: {settings.LogPath}" + }); + } + + public void Start() + { + RunPowerShell("start-ovms.ps1"); + } + + public void Stop() + { + RunPowerShell("stop-ovms.ps1"); + } + + public void Dispose() + { + } + + private OvmsSettings LoadSettings() + { + var settingsPath = Path.Combine(dataDir, "settings.json"); + if (!File.Exists(settingsPath)) + { + return new OvmsSettings + { + InstallDir = appDir.TrimEnd(Path.DirectorySeparatorChar), + DataDir = dataDir, + ConfigPath = Path.Combine(dataDir, "models", "config.json"), + LogPath = Path.Combine(dataDir, "logs", "ovms_server.log"), + BindAddress = "127.0.0.1", + RestPort = 8000, + GrpcPort = 0 + }; + } + + var settings = JsonSerializer.Deserialize(File.ReadAllText(settingsPath), new JsonSerializerOptions + { + PropertyNameCaseInsensitive = true + }); + + return settings ?? throw new InvalidOperationException($"Could not parse settings: {settingsPath}"); + } + + private void RunPowerShell(string scriptName) + { + var scriptPath = Path.Combine(appDir, "scripts", scriptName); + if (!File.Exists(scriptPath)) + { + throw new FileNotFoundException("Missing control script.", scriptPath); + } + + var startInfo = new ProcessStartInfo + { + FileName = "powershell.exe", + Arguments = $"-NoProfile -ExecutionPolicy Bypass -File \"{scriptPath}\" -DataDir \"{dataDir}\"", + CreateNoWindow = true, + UseShellExecute = false, + RedirectStandardError = true, + RedirectStandardOutput = true + }; + + using var process = Process.Start(startInfo) ?? throw new InvalidOperationException("Failed to start PowerShell."); + var stdout = process.StandardOutput.ReadToEnd(); + var stderr = process.StandardError.ReadToEnd(); + process.WaitForExit(); + + if (process.ExitCode != 0) + { + throw new InvalidOperationException(string.IsNullOrWhiteSpace(stderr) ? stdout : stderr); + } + } +} + +internal sealed class OvmsSettings +{ + public string InstallDir { get; set; } = ""; + public string DataDir { get; set; } = ""; + public string ConfigPath { get; set; } = ""; + public string LogPath { get; set; } = ""; + public string BindAddress { get; set; } = "127.0.0.1"; + public int RestPort { get; set; } = 8000; + public int GrpcPort { get; set; } = 9000; +} diff --git a/packaging/windows/scripts/configure-ovms.ps1 b/packaging/windows/scripts/configure-ovms.ps1 new file mode 100644 index 0000000000..b12f7305d6 --- /dev/null +++ b/packaging/windows/scripts/configure-ovms.ps1 @@ -0,0 +1,75 @@ +param( + [Parameter(Mandatory = $true)][string]$InstallDir, + [Parameter(Mandatory = $true)][string]$DataDir, + [string]$PackageVariant = "python_on", + [int]$RestPort = 8000, + [int]$GrpcPort = 0, + [string]$BindAddress = "127.0.0.1", + [string]$LogLevel = "INFO" +) + +$ErrorActionPreference = "Stop" + +$modelsDir = Join-Path $DataDir "models" +$logsDir = Join-Path $DataDir "logs" +$configPath = Join-Path $modelsDir "config.json" +$settingsPath = Join-Path $DataDir "settings.json" +$installMarkerPath = Join-Path $DataDir "install.json" +$logPath = Join-Path $logsDir "ovms_server.log" +$utf8NoBom = New-Object System.Text.UTF8Encoding($false) + +New-Item -ItemType Directory -Force -Path $DataDir, $modelsDir, $logsDir, (Join-Path $DataDir "packages"), (Join-Path $DataDir "downloads"), (Join-Path $DataDir "diagnostics") | Out-Null + +if (-not (Test-Path $configPath)) { + [System.IO.File]::WriteAllText($configPath, '{"model_config_list":[]}', $utf8NoBom) +} + +$settings = [ordered]@{ + installDir = $InstallDir + dataDir = $DataDir + modelRepositoryPath = $modelsDir + configPath = $configPath + logPath = $logPath + restPort = $RestPort + grpcPort = $GrpcPort + bindAddress = $BindAddress + logLevel = $LogLevel + runMode = "user-login" + startAtLogin = $true + showTrayIcon = $true + serviceAutoStart = $false + packageVariant = $PackageVariant +} + +$tmpSettings = "$settingsPath.tmp" +[System.IO.File]::WriteAllText($tmpSettings, ($settings | ConvertTo-Json -Depth 10), $utf8NoBom) +Move-Item -Force -Path $tmpSettings -Destination $settingsPath + +$version = "unknown" +$ovmsExe = Join-Path $InstallDir "ovms.exe" +if (Test-Path $ovmsExe) { + try { + $versionOutput = & $ovmsExe --version 2>$null + if ($versionOutput) { + $version = ($versionOutput | Select-Object -First 1).ToString() + } + } catch { + $version = "unknown" + } +} + +$installMarker = [ordered]@{ + productName = "OpenVINO Model Server" + installDir = $InstallDir + dataDir = $DataDir + version = $version + packageVariant = $PackageVariant + installedAtUtc = (Get-Date).ToUniversalTime().ToString("o") + installerVersion = "1.0.0" +} + +$tmpInstall = "$installMarkerPath.tmp" +[System.IO.File]::WriteAllText($tmpInstall, ($installMarker | ConvertTo-Json -Depth 10), $utf8NoBom) +Move-Item -Force -Path $tmpInstall -Destination $installMarkerPath + +Write-Host "[INFO] OVMS configured at $DataDir" diff --git a/packaging/windows/scripts/install-service.ps1 b/packaging/windows/scripts/install-service.ps1 new file mode 100644 index 0000000000..74985ac9d4 --- /dev/null +++ b/packaging/windows/scripts/install-service.ps1 @@ -0,0 +1,46 @@ +param( + [Parameter(Mandatory = $true)][string]$InstallDir, + [Parameter(Mandatory = $true)][string]$DataDir +) + +$ErrorActionPreference = "Stop" + +$settingsPath = Join-Path $DataDir "settings.json" +if (-not (Test-Path $settingsPath)) { + throw "Settings file not found: $settingsPath" +} + +$settings = Get-Content $settingsPath -Raw | ConvertFrom-Json +$ovmsExe = Join-Path $InstallDir "ovms.exe" +if (-not (Test-Path $ovmsExe)) { + throw "ovms.exe not found: $ovmsExe" +} + +. (Join-Path $PSScriptRoot "ovms-env.ps1") -InstallDir $InstallDir -PersistMachine + +$binPath = "`"$ovmsExe`" --rest_port $($settings.restPort) --rest_bind_address `"$($settings.bindAddress)`" --config_path `"$($settings.configPath)`" --log_level $($settings.logLevel) --log_path `"$($settings.logPath)`"" +if ($settings.grpcPort -and [int]$settings.grpcPort -gt 0) { + $binPath += " --port $($settings.grpcPort)" +} + +$existing = Get-Service ovms -ErrorAction SilentlyContinue +if ($existing) { + & sc.exe config ovms binPath= $binPath DisplayName= "OpenVINO Model Server" | Out-Host +} else { + & sc.exe create ovms binPath= $binPath DisplayName= "OpenVINO Model Server" start= demand | Out-Host +} +if ($LASTEXITCODE -ne 0) { + throw "Failed to create or update ovms service." +} + +try { + & $ovmsExe install | Out-Host +} catch { + Write-Warning "ovms.exe install returned an error: $_" +} + +$settings.runMode = "service" +$utf8NoBom = New-Object System.Text.UTF8Encoding($false) +[System.IO.File]::WriteAllText($settingsPath, ($settings | ConvertTo-Json -Depth 10), $utf8NoBom) + +Write-Host "[INFO] OVMS service installed." diff --git a/packaging/windows/scripts/ovms-env.ps1 b/packaging/windows/scripts/ovms-env.ps1 new file mode 100644 index 0000000000..561f547cc6 --- /dev/null +++ b/packaging/windows/scripts/ovms-env.ps1 @@ -0,0 +1,33 @@ +param( + [Parameter(Mandatory = $true)][string]$InstallDir, + [switch]$PersistMachine +) + +$ErrorActionPreference = "Stop" + +$installDirFull = (Resolve-Path -LiteralPath $InstallDir).Path.TrimEnd("\") +$pythonHome = Join-Path $installDirFull "python" +$pythonScripts = Join-Path $pythonHome "Scripts" +$espeakData = Join-Path $installDirFull "espeak-ng-data" + +$env:OVMS_DIR = $installDirFull +if (Test-Path -LiteralPath $pythonHome) { + $env:PYTHONHOME = $pythonHome + $env:PATH = (($installDirFull, $pythonHome, $pythonScripts, $env:PATH) -join ";") +} else { + $env:PATH = (($installDirFull, $env:PATH) -join ";") +} + +if (Test-Path -LiteralPath $espeakData) { + $env:ESPEAK_DATA_PATH = $espeakData +} + +if ($PersistMachine) { + [Environment]::SetEnvironmentVariable("OVMS_DIR", $installDirFull, "Machine") + if (Test-Path -LiteralPath $pythonHome) { + [Environment]::SetEnvironmentVariable("PYTHONHOME", $pythonHome, "Machine") + } + if (Test-Path -LiteralPath $espeakData) { + [Environment]::SetEnvironmentVariable("ESPEAK_DATA_PATH", $espeakData, "Machine") + } +} diff --git a/packaging/windows/scripts/repair-package.ps1 b/packaging/windows/scripts/repair-package.ps1 new file mode 100644 index 0000000000..66a36f13ad --- /dev/null +++ b/packaging/windows/scripts/repair-package.ps1 @@ -0,0 +1,91 @@ +param( + [Parameter(Mandatory = $true)][string]$InstallDir, + [Parameter(Mandatory = $true)][string]$DataDir, + [string]$PackageSource = "" +) + +$ErrorActionPreference = "Stop" + +$packageCacheDir = Join-Path $DataDir "packages\source" + +$resolvedSource = "" +if ($PackageSource -and (Test-Path (Join-Path $PackageSource "ovms.exe"))) { + $resolvedSource = $PackageSource +} elseif (Test-Path (Join-Path $packageCacheDir "ovms.exe")) { + $resolvedSource = $packageCacheDir +} + +if (-not $resolvedSource) { + Write-Warning "No package source available to restore missing files from. Falling back to self-verify only." + Write-Warning "Repair cannot restore missing files without a valid package source (checked: '$PackageSource', '$packageCacheDir')." +} + +$required = @( + "ovms.exe", + "setupvars.bat", + "setupvars.ps1", + "install_ovms_service.bat" +) + +$repairedFiles = @() +foreach ($file in $required) { + $destPath = Join-Path $InstallDir $file + if (-not (Test-Path $destPath)) { + if ($resolvedSource) { + $srcPath = Join-Path $resolvedSource $file + if (Test-Path $srcPath) { + $destDir = Split-Path -Parent $destPath + if (-not (Test-Path $destDir)) { + New-Item -ItemType Directory -Force -Path $destDir | Out-Null + } + Copy-Item -Path $srcPath -Destination $destPath -Force + $repairedFiles += $file + Write-Host "[INFO] Restored missing file: $file" + } else { + Write-Warning "Missing file '$file' could not be restored: not found in package source." + } + } else { + Write-Warning "Missing file '$file' could not be restored: no package source available." + } + } +} + +$genAiInstalled = (Test-Path (Join-Path $InstallDir "openvino_genai.dll")) -and (Test-Path (Join-Path $InstallDir "openvino_tokenizers.dll")) +$pythonInstalled = Test-Path (Join-Path $InstallDir "python\python.exe") + +Write-Host "[INFO] GenAI support: $(if ($genAiInstalled) { 'Installed' } else { 'Missing' })" +Write-Host "[INFO] Python support: $(if ($pythonInstalled) { 'Installed' } else { 'Missing' })" + +$ovmsExe = Join-Path $InstallDir "ovms.exe" +if (-not (Test-Path $ovmsExe)) { + throw "ovms.exe is missing from $InstallDir and could not be restored. Repair cannot continue." +} + +. (Join-Path $PSScriptRoot "ovms-env.ps1") -InstallDir $InstallDir + +try { + $versionOutput = & $ovmsExe --version 2>$null + if ($LASTEXITCODE -ne 0) { + throw "ovms.exe --version exited with code $LASTEXITCODE." + } +} catch { + throw "ovms.exe --version failed during repair: $_" +} + +$settingsPath = Join-Path $DataDir "settings.json" +$configPath = Join-Path $DataDir "models\config.json" + +if ((-not (Test-Path $settingsPath)) -or (-not (Test-Path $configPath))) { + Write-Host "[INFO] Regenerating missing configuration via configure-ovms.ps1." + & (Join-Path $PSScriptRoot "configure-ovms.ps1") -InstallDir $InstallDir -DataDir $DataDir +} + +Write-Host "" +if ($repairedFiles.Count -gt 0) { + Write-Host "[INFO] Repair restored $($repairedFiles.Count) file(s): $($repairedFiles -join ', ')" +} else { + Write-Host "[INFO] No required files were missing; no file repair was needed." +} +Write-Host "[INFO] GenAI support: $(if ($genAiInstalled) { 'Installed' } else { 'Missing' })" +Write-Host "[INFO] Python support: $(if ($pythonInstalled) { 'Installed' } else { 'Missing' })" +Write-Host "[INFO] Repair complete." diff --git a/packaging/windows/scripts/set-path.ps1 b/packaging/windows/scripts/set-path.ps1 new file mode 100644 index 0000000000..eae7760340 --- /dev/null +++ b/packaging/windows/scripts/set-path.ps1 @@ -0,0 +1,44 @@ +param( + [Parameter(Mandatory = $true)][string]$InstallDir, + [ValidateSet("Add", "Remove")][string]$Action +) + +$ErrorActionPreference = "Stop" + +$installDirFull = (Resolve-Path -LiteralPath $InstallDir).Path.TrimEnd("\") +$pythonHome = Join-Path $installDirFull "python" +$pythonScripts = Join-Path $pythonHome "Scripts" +$required = @($installDirFull) +if (Test-Path -LiteralPath $pythonHome) { + $required += $pythonHome + $required += $pythonScripts +} + +$target = "User" +$current = [Environment]::GetEnvironmentVariable("Path", $target) +$parts = @() +if (-not [string]::IsNullOrWhiteSpace($current)) { + $parts = @($current -split ";" | Where-Object { -not [string]::IsNullOrWhiteSpace($_) }) +} + +if ($Action -eq "Add") { + foreach ($req in $required) { + $exists = $false + foreach ($part in $parts) { + if ($part.TrimEnd("\") -ieq $req.TrimEnd("\")) { + $exists = $true + break + } + } + if (-not $exists) { + $parts = @($req) + $parts + } + } +} else { + $parts = @($parts | Where-Object { + $candidate = $_.TrimEnd("\") + -not ($required | Where-Object { $_.TrimEnd("\") -ieq $candidate }) + }) +} + +[Environment]::SetEnvironmentVariable("Path", ($parts -join ";"), $target) diff --git a/packaging/windows/scripts/start-ovms.ps1 b/packaging/windows/scripts/start-ovms.ps1 new file mode 100644 index 0000000000..9a8df6770e --- /dev/null +++ b/packaging/windows/scripts/start-ovms.ps1 @@ -0,0 +1,86 @@ +param( + [string]$DataDir = "$env:LOCALAPPDATA\OVMS" +) + +$ErrorActionPreference = "Stop" + +$settingsPath = Join-Path $DataDir "settings.json" +if (-not (Test-Path $settingsPath)) { + throw "Settings file not found: $settingsPath" +} + +$settings = Get-Content $settingsPath -Raw | ConvertFrom-Json +$runtimePath = Join-Path $DataDir "runtime.json" +$utf8NoBom = New-Object System.Text.UTF8Encoding($false) + +$service = Get-Service ovms -ErrorAction SilentlyContinue +if ($service -and $settings.runMode -eq "service") { + if ($service.Status -ne "Running") { + Start-Service ovms + } + Write-Host "[INFO] OVMS service is running." + exit 0 +} + +$restPort = [int]$settings.restPort +$grpcPort = [int]$settings.grpcPort +$ports = @($restPort, $grpcPort) | Where-Object { $_ -gt 0 } | Select-Object -Unique +foreach ($port in $ports) { + $listeners = Get-NetTCPConnection -LocalPort $port -State Listen -ErrorAction SilentlyContinue + foreach ($listener in $listeners) { + if ($listener.OwningProcess -ne $PID) { + throw "Port $port is already in use by process id $($listener.OwningProcess)." + } + } +} + +if (Test-Path $runtimePath) { + try { + $runtime = Get-Content $runtimePath -Raw | ConvertFrom-Json + if ($runtime.pid) { + $existing = Get-Process -Id ([int]$runtime.pid) -ErrorAction SilentlyContinue + if ($existing -and $existing.ProcessName -ieq "ovms") { + Write-Host "[INFO] OVMS is already running with pid $($runtime.pid)." + exit 0 + } + } + } catch { + Write-Warning "Ignoring invalid runtime state: $runtimePath" + } +} + +$ovmsExe = Join-Path $settings.installDir "ovms.exe" +if (-not (Test-Path $ovmsExe)) { + throw "ovms.exe not found: $ovmsExe" +} + +. (Join-Path $PSScriptRoot "ovms-env.ps1") -InstallDir $settings.installDir + +$args = @( + "--rest_port", $settings.restPort, + "--rest_bind_address", $settings.bindAddress, + "--config_path", $settings.configPath, + "--log_level", $settings.logLevel, + "--log_path", $settings.logPath +) + +if ($settings.grpcPort -and [int]$settings.grpcPort -gt 0) { + $args += @("--port", $settings.grpcPort) +} + +$process = Start-Process -FilePath $ovmsExe -ArgumentList $args -WindowStyle Hidden -PassThru + +$runtimeState = [ordered]@{ + owner = "manager-process" + pid = $process.Id + serviceName = "ovms" + restPort = [int]$settings.restPort + grpcPort = [int]$settings.grpcPort + startedAtUtc = (Get-Date).ToUniversalTime().ToString("o") +} + +$tmpRuntime = "$runtimePath.tmp" +[System.IO.File]::WriteAllText($tmpRuntime, ($runtimeState | ConvertTo-Json -Depth 10), $utf8NoBom) +Move-Item -Force -Path $tmpRuntime -Destination $runtimePath + +Write-Host "[INFO] Started OVMS with pid $($process.Id)." diff --git a/packaging/windows/scripts/stop-ovms.ps1 b/packaging/windows/scripts/stop-ovms.ps1 new file mode 100644 index 0000000000..061fa9940b --- /dev/null +++ b/packaging/windows/scripts/stop-ovms.ps1 @@ -0,0 +1,38 @@ +param( + [string]$DataDir = "$env:LOCALAPPDATA\OVMS" +) + +$ErrorActionPreference = "Stop" + +$settingsPath = Join-Path $DataDir "settings.json" +$runtimePath = Join-Path $DataDir "runtime.json" + +if (Test-Path $settingsPath) { + $settings = Get-Content $settingsPath -Raw | ConvertFrom-Json + if ($settings.runMode -eq "service") { + $service = Get-Service ovms -ErrorAction SilentlyContinue + if ($service -and $service.Status -ne "Stopped") { + Stop-Service ovms -ErrorAction Stop + Write-Host "[INFO] Stopped OVMS service." + exit 0 + } + } +} + +if (Test-Path $runtimePath) { + try { + $runtime = Get-Content $runtimePath -Raw | ConvertFrom-Json + if ($runtime.pid) { + $process = Get-Process -Id ([int]$runtime.pid) -ErrorAction SilentlyContinue + if ($process -and $process.ProcessName -ieq "ovms") { + Stop-Process -Id $process.Id -Force + Write-Host "[INFO] Stopped OVMS process $($process.Id)." + } + } + } finally { + Remove-Item -Force -Path $runtimePath -ErrorAction SilentlyContinue + } +} else { + Write-Host "[INFO] No OVMS runtime state found." +} + diff --git a/packaging/windows/scripts/uninstall-ovms.ps1 b/packaging/windows/scripts/uninstall-ovms.ps1 new file mode 100644 index 0000000000..b051b0c2d4 --- /dev/null +++ b/packaging/windows/scripts/uninstall-ovms.ps1 @@ -0,0 +1,71 @@ +param( + [Parameter(Mandatory = $true)][string]$InstallDir, + [Parameter(Mandatory = $true)][string]$DataDir, + [ValidateSet("PreserveAll", "RemoveSettingsKeepModels", "RemoveAll")][string]$DataMode = "PreserveAll" +) + +$ErrorActionPreference = "Stop" + +Get-Process -Name "OVMS.Manager" -ErrorAction SilentlyContinue | Stop-Process -Force -ErrorAction SilentlyContinue + +$runtimePath = Join-Path $DataDir "runtime.json" +if (Test-Path $runtimePath) { + try { + $runtime = Get-Content $runtimePath -Raw | ConvertFrom-Json + if ($runtime.pid) { + $process = Get-Process -Id ([int]$runtime.pid) -ErrorAction SilentlyContinue + if ($process -and $process.ProcessName -ieq "ovms") { + Stop-Process -Id $process.Id -Force -ErrorAction SilentlyContinue + } + } + } catch { + Write-Warning "Could not read runtime state: $_" + } + Remove-Item -Force -Path $runtimePath -ErrorAction SilentlyContinue +} + +& (Join-Path $PSScriptRoot "uninstall-service.ps1") +& (Join-Path $PSScriptRoot "set-path.ps1") -InstallDir $InstallDir -Action Remove +[Environment]::SetEnvironmentVariable("OVMS_DIR", $null, "User") +[Environment]::SetEnvironmentVariable("PYTHONHOME", $null, "User") +[Environment]::SetEnvironmentVariable("ESPEAK_DATA_PATH", $null, "User") + +Remove-Item -Path "HKCU:\Software\Microsoft\Windows\CurrentVersion\Run" -Name "OVMS Manager" -ErrorAction SilentlyContinue + +switch ($DataMode) { + "PreserveAll" { + # Keep everything under $DataDir. + } + "RemoveSettingsKeepModels" { + Remove-Item -Force -Path (Join-Path $DataDir "settings.json") -ErrorAction SilentlyContinue + Remove-Item -Force -Path (Join-Path $DataDir "install.json") -ErrorAction SilentlyContinue + Remove-Item -Force -Path (Join-Path $DataDir "runtime.json") -ErrorAction SilentlyContinue + + $logsDir = Join-Path $DataDir "logs" + if (Test-Path $logsDir) { + Remove-Item -LiteralPath $logsDir -Recurse -Force -ErrorAction SilentlyContinue + } + + $packagesDir = Join-Path $DataDir "packages" + if (Test-Path $packagesDir) { + Remove-Item -LiteralPath $packagesDir -Recurse -Force -ErrorAction SilentlyContinue + } + + $downloadsDir = Join-Path $DataDir "downloads" + if (Test-Path $downloadsDir) { + Remove-Item -LiteralPath $downloadsDir -Recurse -Force -ErrorAction SilentlyContinue + } + + $diagnosticsDir = Join-Path $DataDir "diagnostics" + if (Test-Path $diagnosticsDir) { + Remove-Item -LiteralPath $diagnosticsDir -Recurse -Force -ErrorAction SilentlyContinue + } + } + "RemoveAll" { + if (Test-Path $DataDir) { + Remove-Item -LiteralPath $DataDir -Recurse -Force + } + } +} + +Write-Host "[INFO] OVMS uninstall cleanup complete." diff --git a/packaging/windows/scripts/uninstall-service.ps1 b/packaging/windows/scripts/uninstall-service.ps1 new file mode 100644 index 0000000000..ed6f0b8c61 --- /dev/null +++ b/packaging/windows/scripts/uninstall-service.ps1 @@ -0,0 +1,13 @@ +$ErrorActionPreference = "Stop" + +$service = Get-Service ovms -ErrorAction SilentlyContinue +if ($service) { + if ($service.Status -ne "Stopped") { + Stop-Service ovms -ErrorAction SilentlyContinue + } + & sc.exe delete ovms | Out-Host + if ($LASTEXITCODE -ne 0) { + throw "Failed to delete ovms service." + } +} + diff --git a/packaging/windows/scripts/validate-install.ps1 b/packaging/windows/scripts/validate-install.ps1 new file mode 100644 index 0000000000..2e472905b9 --- /dev/null +++ b/packaging/windows/scripts/validate-install.ps1 @@ -0,0 +1,36 @@ +param( + [string]$InstallDir +) + +$ErrorActionPreference = "Stop" + +if (-not $InstallDir) { + $sourceRoot = (Resolve-Path (Join-Path $PSScriptRoot "..\..\..")).Path + $InstallDir = Join-Path $sourceRoot "dist\windows\ovms" +} + +$required = @( + "ovms.exe", + "setupvars.bat", + "setupvars.ps1", + "install_ovms_service.bat" +) + +$missing = @() +foreach ($file in $required) { + $path = Join-Path $InstallDir $file + if (-not (Test-Path $path)) { + $missing += $file + } +} + +if ($missing.Count -gt 0) { + throw "Missing required files in ${InstallDir}: $($missing -join ', ')" +} + +& (Join-Path $InstallDir "ovms.exe") --version +if ($LASTEXITCODE -ne 0) { + throw "ovms.exe --version failed." +} + +Write-Host "[INFO] Install folder validation passed: $InstallDir" diff --git a/packaging/windows/settings/settings.template.json b/packaging/windows/settings/settings.template.json new file mode 100644 index 0000000000..3f5734aa38 --- /dev/null +++ b/packaging/windows/settings/settings.template.json @@ -0,0 +1,16 @@ +{ + "installDir": "", + "dataDir": "", + "modelRepositoryPath": "", + "configPath": "", + "logPath": "", + "restPort": 8000, + "grpcPort": 0, + "bindAddress": "127.0.0.1", + "logLevel": "INFO", + "runMode": "user-login", + "startAtLogin": true, + "showTrayIcon": true, + "serviceAutoStart": false, + "packageVariant": "python_on" +}