From 78a52e76c91ae0a113366bb42666363bd904f323 Mon Sep 17 00:00:00 2001 From: Mike Johanson Date: Mon, 23 Mar 2026 15:21:29 -0700 Subject: [PATCH] feat: adds installers for linux, macos, and windows --- .github/workflows/release.yml | 61 ++- .gitignore | 3 +- .releaserc.json | 28 +- .vscode/launch.json | 2 +- Makefile | 58 +++ build.sh | 39 +- cmd/app/edition.go | 5 + cmd/app/edition_noui.go | 5 + cmd/app/main.go | 29 +- cmd/app/service_other.go | 18 + cmd/app/service_windows.go | 77 ++++ cmd/app/tray.go | 130 ++++++ cmd/app/tray_disabled.go | 16 + config/config.go | 17 +- go.mod | 1 + go.sum | 2 + installer/console.nsi | 438 +++++++++++++++++++ installer/linux/build-packages.sh | 99 +++++ installer/linux/configure.sh | 201 +++++++++ installer/linux/dmt-console.service | 28 ++ installer/linux/install.sh | 195 +++++++++ installer/linux/uninstall.sh | 126 ++++++ installer/macos/build-pkg.sh | 133 ++++++ installer/macos/configure.sh | 200 +++++++++ installer/macos/distribution.xml | 41 ++ installer/macos/resources/conclusion.html | 71 +++ installer/macos/resources/readme.html | 94 ++++ installer/macos/resources/welcome.html | 52 +++ installer/macos/scripts/postinstall | 143 ++++++ installer/macos/scripts/postinstall-headless | 134 ++++++ installer/macos/scripts/postinstall-ui | 134 ++++++ installer/macos/scripts/preinstall | 21 + installer/macos/uninstall.sh | 96 ++++ installer/windows/build-installers.sh | 56 +++ internal/controller/httpapi/ui.go | 44 +- pkg/tray/icon.go | 35 ++ pkg/tray/tray.go | 145 ++++++ 37 files changed, 2934 insertions(+), 43 deletions(-) create mode 100644 cmd/app/edition.go create mode 100644 cmd/app/edition_noui.go create mode 100644 cmd/app/service_other.go create mode 100644 cmd/app/service_windows.go create mode 100644 cmd/app/tray.go create mode 100644 cmd/app/tray_disabled.go create mode 100644 installer/console.nsi create mode 100755 installer/linux/build-packages.sh create mode 100755 installer/linux/configure.sh create mode 100644 installer/linux/dmt-console.service create mode 100755 installer/linux/install.sh create mode 100755 installer/linux/uninstall.sh create mode 100755 installer/macos/build-pkg.sh create mode 100755 installer/macos/configure.sh create mode 100644 installer/macos/distribution.xml create mode 100644 installer/macos/resources/conclusion.html create mode 100644 installer/macos/resources/readme.html create mode 100644 installer/macos/resources/welcome.html create mode 100755 installer/macos/scripts/postinstall create mode 100755 installer/macos/scripts/postinstall-headless create mode 100755 installer/macos/scripts/postinstall-ui create mode 100755 installer/macos/scripts/preinstall create mode 100755 installer/macos/uninstall.sh create mode 100755 installer/windows/build-installers.sh create mode 100644 pkg/tray/icon.go create mode 100644 pkg/tray/tray.go diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index b50e288c9..544026bc1 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -126,6 +126,19 @@ jobs: - name: Build macOS arm64 headless run: CGO_ENABLED=0 GOOS=darwin GOARCH=arm64 go build -tags=noui -ldflags "-s -w -X 'github.com/device-management-toolkit/console/internal/app.Version=${{ needs.prepare.outputs.version }}'" -trimpath -o dist/darwin/console_mac_arm64_headless ./cmd/app + # Build Windows NSIS Installer + - name: Install NSIS + run: | + sudo apt-get update + sudo apt-get install -y nsis + + - name: Build Windows Installers + run: | + makensis -DVERSION=${{ needs.prepare.outputs.version }} -DARCH=x64 -DEDITION=ui "-DBINARY=../dist/windows/console_windows_x64.exe" installer/console.nsi + mv console_${{ needs.prepare.outputs.version }}_windows_x64_setup.exe dist/windows/ + makensis -DVERSION=${{ needs.prepare.outputs.version }} -DARCH=x64 -DEDITION=headless "-DBINARY=../dist/windows/console_windows_x64_headless.exe" installer/console.nsi + mv console_${{ needs.prepare.outputs.version }}_windows_x64_headless_setup.exe dist/windows/ + # Cache all build artifacts in a single step - uses: actions/cache@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5.0.3 with: @@ -135,11 +148,49 @@ jobs: dist/windows key: all-platforms-${{ env.sha_short }} + build-macos-installer: + needs: [prepare, build] + runs-on: macos-latest + steps: + - name: Harden Runner + uses: step-security/harden-runner@0080882f6c36860b6ba35c610c98ce87d4e2f26f # v2.10.2 + with: + egress-policy: audit + + - name: Checkout Console + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + with: + persist-credentials: false + + - shell: bash + run: | + echo "sha_short=$(git rev-parse --short HEAD)" >> $GITHUB_ENV + + # Restore build cache to get macOS binary + - uses: actions/cache@1bd1e32a3bdc45362d1e726936510720a7c30a57 # v4.2.0 + with: + path: | + dist/linux + dist/darwin + dist/windows + key: all-platforms-${{ env.sha_short }} + + - name: Build macOS PKG Installer + run: | + chmod +x installer/macos/build-pkg.sh + ./installer/macos/build-pkg.sh ${{ needs.prepare.outputs.version }} arm64 + + # Cache the installer separately + - uses: actions/cache@1bd1e32a3bdc45362d1e726936510720a7c30a57 # v4.2.0 + with: + path: dist/darwin/*.pkg + key: macos-installer-${{ env.sha_short }} + release: permissions: contents: write # for Git to git push runs-on: ubuntu-latest - needs: build + needs: [build, build-macos-installer] steps: - name: Harden Runner uses: step-security/harden-runner@fa2e9d605c4eeb9fcad4c99c224cee0c6c7f3594 # v2.16.0 @@ -163,7 +214,13 @@ jobs: dist/darwin dist/windows key: all-platforms-${{ env.sha_short }} - + + # Restore macOS installer cache + - uses: actions/cache@1bd1e32a3bdc45362d1e726936510720a7c30a57 # v4.2.0 + with: + path: dist/darwin/*.pkg + key: macos-installer-${{ env.sha_short }} + # Generate licenses.zip - name: Use Node.js 22.x uses: actions/setup-node@1d0ff469b7ec7b3cb9d8673fde0c81c44821de2a # v4.2.0 diff --git a/.gitignore b/.gitignore index e32c94aaa..025b19f1a 100644 --- a/.gitignore +++ b/.gitignore @@ -36,4 +36,5 @@ vendor/ # ...but keep the folder !**/ui/.gitkeep # Documentation files -doc/openapi.json \ No newline at end of file +doc/openapi.json +dist \ No newline at end of file diff --git a/.releaserc.json b/.releaserc.json index d0285b3f7..70254b726 100644 --- a/.releaserc.json +++ b/.releaserc.json @@ -28,27 +28,35 @@ "assets": [ { "path": "console_linux_x64.tar.gz", - "label": "Linux x64 Console (Full)" + "label": "Linux x64 Installer (Full)" }, { "path": "console_linux_x64_headless.tar.gz", - "label": "Linux x64 Console Headless (No UI)" + "label": "Linux x64 Installer Headless (No UI)" }, { "path": "console_linux_arm64.tar.gz", - "label": "Linux ARM64 Console (Full)" + "label": "Linux ARM64 Installer (Full)" }, { "path": "console_linux_arm64_headless.tar.gz", - "label": "Linux ARM64 Console Headless (No UI)" + "label": "Linux ARM64 Installer Headless (No UI)" }, { "path": "dist/windows/console_windows_x64.exe", - "label": "Windows x64 Console (Full)" + "label": "Windows x64 Binary (Full)" }, { "path": "dist/windows/console_windows_x64_headless.exe", - "label": "Windows x64 Console Headless (No UI)" + "label": "Windows x64 Binary Headless (No UI)" + }, + { + "path": "dist/windows/console_*_windows_x64_setup.exe", + "label": "Windows x64 Installer (Full)" + }, + { + "path": "dist/windows/console_*_windows_x64_headless_setup.exe", + "label": "Windows x64 Installer Headless (No UI)" }, { "path": "console_mac_arm64.tar.gz", @@ -58,6 +66,14 @@ "path": "console_mac_arm64_headless.tar.gz", "label": "macOS ARM64 Console Headless (No UI)" }, + { + "path": "dist/darwin/console_*_macos_arm64.pkg", + "label": "macOS ARM64 Installer (Full PKG)" + }, + { + "path": "dist/darwin/console_*_macos_arm64_headless.pkg", + "label": "macOS ARM64 Installer Headless (PKG)" + }, { "path": "licenses.zip", "label": "Third-Party Licenses" diff --git a/.vscode/launch.json b/.vscode/launch.json index b399bdf7d..50f90044b 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -9,7 +9,7 @@ "type": "go", "request": "launch", "mode": "auto", - "program": "${workspaceFolder}/cmd/app/main.go", + "program": "${workspaceFolder}/cmd/app", "envFile": "${workspaceFolder}/.env", "cwd": "${workspaceFolder}" } diff --git a/Makefile b/Makefile index e511a5b6c..e77bc3a0f 100644 --- a/Makefile +++ b/Makefile @@ -42,6 +42,10 @@ build-noui: ### build app without UI CGO_ENABLED=0 go build -tags=noui -o ./bin/console-noui ./cmd/app .PHONY: build-noui +build-tray: ### build app with system tray support (requires CGO, native build only) + CGO_ENABLED=1 go build -tags=tray -o ./bin/console-tray ./cmd/app +.PHONY: build-tray + build-all-platforms: ### cross-compile for all platforms (Linux, Windows, macOS) @echo "Building for all platforms using cross-compilation (CGO_ENABLED=0)..." @mkdir -p dist/linux dist/windows dist/darwin @@ -107,3 +111,57 @@ migrate-up: ### migration up bin-deps: GOBIN=$(LOCAL_BIN) go install -tags 'postgres' github.com/golang-migrate/migrate/v4/cmd/migrate@latest GOBIN=$(LOCAL_BIN) go install go.uber.org/mock/mockgen@latest + +# Installers (requires NSIS for Windows, macOS for PKG) +VERSION ?= 3.0.0-dev + +# Build Windows binaries (full and headless) +build-windows-binaries: ### build Windows binaries (full and headless) + @echo "Building Windows binaries..." + @mkdir -p dist/windows + CGO_ENABLED=0 GOOS=windows GOARCH=amd64 go build -ldflags "-s -w" -trimpath -o dist/windows/console_windows_x64.exe ./cmd/app + CGO_ENABLED=0 GOOS=windows GOARCH=amd64 go build -tags=noui -ldflags "-s -w" -trimpath -o dist/windows/console_windows_x64_headless.exe ./cmd/app + @echo "Windows binaries built successfully!" +.PHONY: build-windows-binaries + +build-windows-installer: build-windows-binaries ### build Windows NSIS installers (full + headless) + @echo "Building Windows installers with NSIS..." + @command -v makensis >/dev/null 2>&1 || { echo "NSIS is not installed. Install with: brew install nsis (macOS) or apt-get install nsis (Linux)"; exit 1; } + $(eval VI_VERSION := $(shell echo '$(VERSION)' | sed 's/-.*//' ).0) + makensis -DVERSION=$(VERSION) -DVI_VERSION=$(VI_VERSION) -DARCH=x64 -DEDITION=ui -DBINARY="../dist/windows/console_windows_x64.exe" installer/console.nsi + @mv installer/console_$(VERSION)_windows_x64_setup.exe dist/windows/ + makensis -DVERSION=$(VERSION) -DVI_VERSION=$(VI_VERSION) -DARCH=x64 -DEDITION=headless -DBINARY="../dist/windows/console_windows_x64_headless.exe" installer/console.nsi + @mv installer/console_$(VERSION)_windows_x64_headless_setup.exe dist/windows/ + @echo "Windows installers created in dist/windows/" +.PHONY: build-windows-installer + +# Build macOS binaries (full and headless) +build-macos-binaries: ### build macOS binaries (full and headless) + @echo "Building macOS binaries..." + @mkdir -p dist/darwin + CGO_ENABLED=0 GOOS=darwin GOARCH=arm64 go build -ldflags "-s -w" -trimpath -o dist/darwin/console_mac_arm64 ./cmd/app + CGO_ENABLED=0 GOOS=darwin GOARCH=arm64 go build -tags=noui -ldflags "-s -w" -trimpath -o dist/darwin/console_mac_arm64_headless ./cmd/app + @echo "macOS binaries built successfully!" +.PHONY: build-macos-binaries + +build-macos-installer: build-macos-binaries ### build macOS PKG installer (macOS only) + @echo "Building macOS PKG installer..." + @if [ "$$(uname)" != "Darwin" ]; then echo "Error: macOS PKG can only be built on macOS"; exit 1; fi + chmod +x installer/macos/build-pkg.sh + ./installer/macos/build-pkg.sh $(VERSION) arm64 + @echo "macOS installer created: dist/darwin/console_$(VERSION)_macos_arm64.pkg" +.PHONY: build-macos-installer + +build-linux-installer: ### build Linux installer archives (full + headless) + @echo "Building Linux installer archives..." + @mkdir -p dist/linux + CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -ldflags "-s -w" -trimpath -o dist/linux/console_linux_x64 ./cmd/app + CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -tags=noui -ldflags "-s -w" -trimpath -o dist/linux/console_linux_x64_headless ./cmd/app + chmod +x installer/linux/build-packages.sh + ./installer/linux/build-packages.sh $(VERSION) amd64 + @echo "Linux installer archives created in dist/linux/" +.PHONY: build-linux-installer + +build-all-with-installers: build-all-platforms build-windows-installer build-macos-installer build-linux-installer ### build all platforms plus all installers + @echo "All platforms and installers built successfully!" +.PHONY: build-all-with-installers diff --git a/build.sh b/build.sh index b092754dd..a039e6056 100755 --- a/build.sh +++ b/build.sh @@ -25,11 +25,40 @@ chmod +x dist/linux/console_linux_arm64_headless chmod +x dist/darwin/console_mac_arm64 chmod +x dist/darwin/console_mac_arm64_headless -# Package Linux variants -tar cvfpz console_linux_x64.tar.gz dist/linux/console_linux_x64 -tar cvfpz console_linux_x64_headless.tar.gz dist/linux/console_linux_x64_headless -tar cvfpz console_linux_arm64.tar.gz dist/linux/console_linux_arm64 -tar cvfpz console_linux_arm64_headless.tar.gz dist/linux/console_linux_arm64_headless +# Prepare Linux installer scripts with version replacement +LINUX_INSTALLER_DIR="installer/linux" +STAGING_DIR=$(mktemp -d) + +for script in configure.sh install.sh uninstall.sh dmt-console.service; do + cp "$LINUX_INSTALLER_DIR/$script" "$STAGING_DIR/$script" + sed -i "s/VERSION_PLACEHOLDER/$version/g" "$STAGING_DIR/$script" +done + +# Package Linux variants (binary + installer scripts) +package_linux() { + local binary=$1 + local output=$2 + local pkg_dir + pkg_dir=$(mktemp -d) + + cp "$binary" "$pkg_dir/console" + chmod +x "$pkg_dir/console" + cp "$STAGING_DIR/configure.sh" "$pkg_dir/" + cp "$STAGING_DIR/install.sh" "$pkg_dir/" + cp "$STAGING_DIR/uninstall.sh" "$pkg_dir/" + cp "$STAGING_DIR/dmt-console.service" "$pkg_dir/" + chmod +x "$pkg_dir/configure.sh" "$pkg_dir/install.sh" "$pkg_dir/uninstall.sh" + + tar cvfpz "$output" -C "$pkg_dir" . + rm -rf "$pkg_dir" +} + +package_linux dist/linux/console_linux_x64 console_linux_x64.tar.gz +package_linux dist/linux/console_linux_x64_headless console_linux_x64_headless.tar.gz +package_linux dist/linux/console_linux_arm64 console_linux_arm64.tar.gz +package_linux dist/linux/console_linux_arm64_headless console_linux_arm64_headless.tar.gz + +rm -rf "$STAGING_DIR" # Package macOS variants tar cvfpz console_mac_arm64.tar.gz dist/darwin/console_mac_arm64 diff --git a/cmd/app/edition.go b/cmd/app/edition.go new file mode 100644 index 000000000..61b561b8f --- /dev/null +++ b/cmd/app/edition.go @@ -0,0 +1,5 @@ +//go:build !noui + +package main + +var isHeadlessBuild = false //nolint:unused // used in tray.go (tray build tag) diff --git a/cmd/app/edition_noui.go b/cmd/app/edition_noui.go new file mode 100644 index 000000000..df4c75e55 --- /dev/null +++ b/cmd/app/edition_noui.go @@ -0,0 +1,5 @@ +//go:build noui + +package main + +var isHeadlessBuild = true //nolint:unused // used in tray.go (tray build tag) diff --git a/cmd/app/main.go b/cmd/app/main.go index 8466dc696..c3fa3183e 100644 --- a/cmd/app/main.go +++ b/cmd/app/main.go @@ -43,6 +43,9 @@ var ( ) func main() { + // Detect Windows service mode early, before any interactive prompts + serviceMode = isServiceMode() + cfg, err := initializeConfigFunc() if err != nil { log.Fatalf("Config error: %s", err) @@ -65,8 +68,23 @@ func main() { l := logger.New(cfg.Level) handleEncryptionKey(cfg) - handleDebugMode(cfg, l) - runAppFunc(cfg, l) + + // Run as a Windows service if started by the SCM (no-op on non-Windows) + if serviceMode { + if err := runAsService(cfg, l); err != nil { + log.Fatalf("Windows service error: %s", err) + } + + return + } + + // Run with system tray (if built with tray tag and --tray flag) or standard mode + if trayBuildEnabled && config.TrayMode { + runWithTray(cfg, l) + } else { + handleDebugMode(cfg, l) + runAppFunc(cfg, l) + } } func setupCIRACertificates(cfg *config.Config, secretsClient security.Storager) error { @@ -269,6 +287,13 @@ func saveEncryptionKey(key string, remoteStorage, localStorage security.Storager } func handleKeyNotFound(toolkitCrypto security.Crypto, _, _ security.Storager) string { + // When running as a Windows service there is no stdin — auto-generate the key + if serviceMode { + log.Println("No encryption key found — generating new key (service mode)") + + return toolkitCrypto.GenerateKey() + } + log.Print("\033[31mWarning: Key Not Found, Generate new key? -- This will prevent access to existing data? Y/N: \033[0m") var response string diff --git a/cmd/app/service_other.go b/cmd/app/service_other.go new file mode 100644 index 000000000..77042de10 --- /dev/null +++ b/cmd/app/service_other.go @@ -0,0 +1,18 @@ +//go:build !windows + +package main + +import ( + "github.com/device-management-toolkit/console/config" + "github.com/device-management-toolkit/console/pkg/logger" +) + +var serviceMode bool + +func isServiceMode() bool { + return false +} + +func runAsService(_ *config.Config, _ logger.Interface) error { + return nil +} diff --git a/cmd/app/service_windows.go b/cmd/app/service_windows.go new file mode 100644 index 000000000..28ae89379 --- /dev/null +++ b/cmd/app/service_windows.go @@ -0,0 +1,77 @@ +package main + +import ( + "fmt" + "log" + "os" + + "golang.org/x/sys/windows/svc" + + "github.com/device-management-toolkit/console/config" + "github.com/device-management-toolkit/console/pkg/logger" +) + +// serviceMode is set early in main() to indicate we're running under the SCM. +var serviceMode bool + +// isServiceMode detects whether the process was started by the Windows SCM. +func isServiceMode() bool { + is, err := svc.IsWindowsService() + if err != nil { + log.Printf("Warning: could not detect service mode: %v", err) + + return false + } + + return is +} + +// consoleService implements svc.Handler for the Windows Service Control Manager. +type consoleService struct { + cfg *config.Config + log logger.Interface +} + +func (s *consoleService) Execute(_ []string, r <-chan svc.ChangeRequest, status chan<- svc.Status) (bool, uint32) { + const accepted = svc.AcceptStop | svc.AcceptShutdown + status <- svc.Status{State: svc.StartPending} + + // Start the application in a goroutine + done := make(chan struct{}) + go func() { + defer close(done) + runAppFunc(s.cfg, s.log) + }() + + status <- svc.Status{State: svc.Running, Accepts: accepted} + + for { + select { + case c := <-r: + switch c.Cmd { + case svc.Interrogate: + status <- c.CurrentStatus + case svc.Stop, svc.Shutdown: + status <- svc.Status{State: svc.StopPending} + // Signal the app to shut down via the same mechanism it uses + p, _ := os.FindProcess(os.Getpid()) + _ = p.Signal(os.Interrupt) + <-done + return false, 0 + } + case <-done: + // App exited on its own + return false, 0 + } + } +} + +// runAsService runs the app under the Windows SCM service handler. +func runAsService(cfg *config.Config, l logger.Interface) error { + err := svc.Run("DMTConsole", &consoleService{cfg: cfg, log: l}) + if err != nil { + return fmt.Errorf("running windows service: %w", err) + } + + return nil +} diff --git a/cmd/app/tray.go b/cmd/app/tray.go new file mode 100644 index 000000000..682026722 --- /dev/null +++ b/cmd/app/tray.go @@ -0,0 +1,130 @@ +//go:build tray + +package main + +import ( + "fmt" + "log" + "os" + "os/exec" + "os/signal" + "path/filepath" + "syscall" + + "github.com/device-management-toolkit/console/config" + "github.com/device-management-toolkit/console/pkg/logger" + "github.com/device-management-toolkit/console/pkg/tray" +) + +func init() { + // Enable tray mode by default when built with tray tag + trayBuildEnabled = true +} + +var trayBuildEnabled = false + +// logDir returns the macOS-conventional log directory for the app. +func logDir() string { + home, err := os.UserHomeDir() + if err != nil { + return os.TempDir() + } + + return filepath.Join(home, "Library", "Logs", "device-management-toolkit") +} + +// relaunchInBackground re-execs the current process detached from the terminal, +// redirecting output to a log file. It exits the parent process on success. +func relaunchInBackground() { + dir := logDir() + + if err := os.MkdirAll(dir, 0o755); err != nil { + log.Fatalf("Failed to create log directory: %v", err) + } + + logPath := filepath.Join(dir, "console.log") + + f, err := os.OpenFile(logPath, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0o644) + if err != nil { + log.Fatalf("Failed to open log file: %v", err) + } + + exePath, err := os.Executable() + if err != nil { + log.Fatalf("Failed to get executable path: %v", err) + } + + cmd := exec.Command(exePath, os.Args[1:]...) + cmd.Stdout = f + cmd.Stderr = f + cmd.Env = append(os.Environ(), "DMT_BACKGROUND=1") + cmd.SysProcAttr = &syscall.SysProcAttr{ + Setsid: true, + } + + if err := cmd.Start(); err != nil { + log.Fatalf("Failed to start in background: %v", err) + } + + fmt.Printf("DMT Console started in background (PID %d)\n", cmd.Process.Pid) + fmt.Printf("Logs: %s\n", logPath) + + os.Exit(0) +} + +// isTerminal returns true if stdin is connected to a terminal. +func isTerminal() bool { + fi, err := os.Stdin.Stat() + if err != nil { + return false + } + + return fi.Mode()&os.ModeCharDevice != 0 +} + +func runWithTray(cfg *config.Config, l logger.Interface) { + // When launched from a terminal, re-exec in the background so the + // user gets their shell back and logs go to a file. + if os.Getenv("DMT_BACKGROUND") == "" && isTerminal() { + relaunchInBackground() + } + + // Build the URL for the web UI + scheme := "http" + if cfg.TLS.Enabled { + scheme = "https" + } + url := scheme + "://localhost:" + cfg.Port + + // Create tray manager + trayManager := tray.New(tray.Config{ + AppName: "DMT Console", + URL: url, + Headless: isHeadlessBuild, + OnReady: func() { + // Start the server in a goroutine + go runAppFunc(cfg, l) + log.Printf("DMT Console running at %s", url) + }, + OnQuit: func() { + log.Println("Shutting down DMT Console...") + // Send interrupt signal to trigger graceful shutdown + p, _ := os.FindProcess(os.Getpid()) + _ = p.Signal(os.Interrupt) + }, + }) + + // Catch Ctrl+C / SIGTERM so the tray unblocks on terminal interrupt. + // app.Run also listens for these signals to shut down the HTTP server; + // Go delivers to all registered channels, so both handlers fire. + sigCh := make(chan os.Signal, 1) + signal.Notify(sigCh, os.Interrupt, syscall.SIGTERM) + + go func() { + <-sigCh + trayManager.Quit() + }() + + // Run the tray (this blocks until quit) + trayManager.Run() +} diff --git a/cmd/app/tray_disabled.go b/cmd/app/tray_disabled.go new file mode 100644 index 000000000..78c635905 --- /dev/null +++ b/cmd/app/tray_disabled.go @@ -0,0 +1,16 @@ +//go:build !tray + +package main + +import ( + "github.com/device-management-toolkit/console/config" + "github.com/device-management-toolkit/console/pkg/logger" +) + +var trayBuildEnabled = false + +func runWithTray(cfg *config.Config, l logger.Interface) { + // Tray not available in this build, fall back to standard mode + handleDebugMode(cfg, l) + runAppFunc(cfg, l) +} diff --git a/config/config.go b/config/config.go index bf187b37d..8b132de7b 100644 --- a/config/config.go +++ b/config/config.go @@ -14,6 +14,9 @@ import ( var ConsoleConfig *Config +// TrayMode indicates whether to run with system tray UI. +var TrayMode bool + const defaultHost = "localhost" type ( @@ -211,6 +214,14 @@ func resolveConfigPath(configPathFlag string) (string, error) { return "", err } + // Resolve symlinks to find actual executable location. + // On macOS, os.Executable() may return the symlink path (e.g. /usr/local/bin/dmt-console) + // rather than the target (e.g. /usr/local/device-management-toolkit/console). + ex, err = filepath.EvalSymlinks(ex) + if err != nil { + return "", err + } + exPath := filepath.Dir(ex) return filepath.Join(exPath, "config", "config.yml"), nil @@ -255,12 +266,16 @@ func NewConfig() (*Config, error) { // set defaults ConsoleConfig = defaultConfig() - // Define a command line flag for the config path + // Define command line flags var configPathFlag string if flag.Lookup("config") == nil { flag.StringVar(&configPathFlag, "config", "", "path to config file") } + if flag.Lookup("tray") == nil { + flag.BoolVar(&TrayMode, "tray", false, "run with system tray icon") + } + if !flag.Parsed() { flag.Parse() } diff --git a/go.mod b/go.mod index bb96ec177..5c1fd8e91 100644 --- a/go.mod +++ b/go.mod @@ -30,6 +30,7 @@ require ( require ( al.essio.dev/pkg/shellescape v1.5.1 // indirect + fyne.io/systray v1.12.0 // indirect github.com/bytedance/gopkg v0.1.3 // indirect github.com/cenkalti/backoff/v4 v4.3.0 // indirect github.com/danieljoos/wincred v1.2.2 // indirect diff --git a/go.sum b/go.sum index a48b2e0ef..90359a3e9 100644 --- a/go.sum +++ b/go.sum @@ -1,5 +1,7 @@ al.essio.dev/pkg/shellescape v1.5.1 h1:86HrALUujYS/h+GtqoB26SBEdkWfmMI6FubjXlsXyho= al.essio.dev/pkg/shellescape v1.5.1/go.mod h1:6sIqp7X2P6mThCQ7twERpZTuigpr6KbZWtls1U8I890= +fyne.io/systray v1.12.0 h1:CA1Kk0e2zwFlxtc02L3QFSiIbxJ/P0n582YrZHT7aTM= +fyne.io/systray v1.12.0/go.mod h1:RVwqP9nYMo7h5zViCBHri2FgjXF7H2cub7MAq4NSoLs= github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 h1:L/gRVlceqvL25UVaW/CKtUDjefjrs0SPonmDGUVOYP0= github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E= github.com/BurntSushi/toml v1.2.1/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ= diff --git a/installer/console.nsi b/installer/console.nsi new file mode 100644 index 000000000..30f16b2a2 --- /dev/null +++ b/installer/console.nsi @@ -0,0 +1,438 @@ +; Device Management Toolkit Console - NSIS Installer Script +; Copyright (c) Intel Corporation +; SPDX-License-Identifier: Apache-2.0 + +;-------------------------------- +; Includes + +!include "MUI2.nsh" +!include "FileFunc.nsh" +!include "LogicLib.nsh" +!include "WinMessages.nsh" +!include "StrFunc.nsh" +!include "nsDialogs.nsh" + +; Declare string functions we'll use (for installer) +${StrStr} + +; Declare string functions for uninstaller (must use un. prefix) +${UnStrStr} +${UnStrRep} + +;-------------------------------- +; General Configuration + +!ifndef VERSION + !define VERSION "0.0.0" +!endif + +!ifndef ARCH + !define ARCH "x64" +!endif + +; Edition: "ui" or "headless" — set at build time via -DEDITION= +!ifndef EDITION + !define EDITION "ui" +!endif + +; Binary to install — set at build time via -DBINARY= +!ifndef BINARY + !define BINARY "..\dist\windows\console_windows_x64.exe" +!endif + +; Edition-specific naming +!if "${EDITION}" == "headless" + !define EDITION_LABEL "Headless" + !define EDITION_SUFFIX "_headless" +!else + !define EDITION_LABEL "" + !define EDITION_SUFFIX "" +!endif + +Name "Device Management Toolkit Console ${VERSION}" +OutFile "console_${VERSION}_windows_${ARCH}${EDITION_SUFFIX}_setup.exe" +InstallDir "$PROGRAMFILES64\Device Management Toolkit\Console" +InstallDirRegKey HKLM "Software\DeviceManagementToolkit\Console" "InstallDir" +RequestExecutionLevel admin +Unicode True + +; Compression +SetCompressor /SOLID lzma +SetCompressorDictSize 64 + +;-------------------------------- +; Version Information + +; VI_VERSION must be numeric X.X.X.X format — passed from Makefile with +; pre-release suffixes stripped. Falls back to VERSION.0 if not provided. +!ifndef VI_VERSION + !define VI_VERSION "${VERSION}.0" +!endif +VIProductVersion "${VI_VERSION}" +VIAddVersionKey "ProductName" "Device Management Toolkit Console" +VIAddVersionKey "CompanyName" "Intel Corporation" +VIAddVersionKey "LegalCopyright" "Copyright (c) Intel Corporation" +VIAddVersionKey "FileDescription" "Device Management Toolkit Console Installer" +VIAddVersionKey "FileVersion" "${VERSION}" +VIAddVersionKey "ProductVersion" "${VERSION}" + +;-------------------------------- +; Variables + +Var Dialog +Var PortLabel +Var PortText +Var PortValue +Var TLSCheckbox +Var TLSEnabled +Var UsernameLabel +Var UsernameText +Var UsernameValue +Var PasswordLabel +Var PasswordText +Var PasswordValue + +;-------------------------------- +; Interface Settings + +!define MUI_ABORTWARNING +!define MUI_ICON "${NSISDIR}\Contrib\Graphics\Icons\modern-install.ico" +!define MUI_UNICON "${NSISDIR}\Contrib\Graphics\Icons\modern-uninstall.ico" +!define MUI_WELCOMEFINISHPAGE_BITMAP "${NSISDIR}\Contrib\Graphics\Wizard\win.bmp" + +; Show installation details +ShowInstDetails show +ShowUnInstDetails show + +;-------------------------------- +; Pages + +!insertmacro MUI_PAGE_WELCOME +!insertmacro MUI_PAGE_LICENSE "..\LICENSE" +!insertmacro MUI_PAGE_DIRECTORY +Page custom ConfigPage ConfigPageLeave +!insertmacro MUI_PAGE_COMPONENTS +!insertmacro MUI_PAGE_INSTFILES +!insertmacro MUI_PAGE_FINISH + +!insertmacro MUI_UNPAGE_CONFIRM +!insertmacro MUI_UNPAGE_INSTFILES + +;-------------------------------- +; Languages + +!insertmacro MUI_LANGUAGE "English" + +;-------------------------------- +; Configuration Page + +Function ConfigPage + !insertmacro MUI_HEADER_TEXT "Configuration" "Configure essential settings for the Console." + + nsDialogs::Create 1018 + Pop $Dialog + + ${If} $Dialog == error + Abort + ${EndIf} + + ; Port + ${NSD_CreateLabel} 0 0 60u 12u "HTTP Port:" + Pop $PortLabel + + ${NSD_CreateText} 65u 0 50u 12u "8181" + Pop $PortText + + ${NSD_CreateLabel} 120u 0 100% 12u "(default: 8181)" + Pop $0 + + ; TLS + ${NSD_CreateCheckbox} 0 25u 100% 12u "Enable TLS/HTTPS (recommended)" + Pop $TLSCheckbox + ${NSD_SetState} $TLSCheckbox ${BST_CHECKED} + + ${NSD_CreateLabel} 15u 40u 100% 12u "A self-signed certificate will be generated if no certificate is provided." + Pop $0 + + ; Separator + ${NSD_CreateHLine} 0 60u 100% 1u + Pop $0 + + ; Admin credentials section + ${NSD_CreateLabel} 0 70u 100% 12u "Administrator Credentials (for standalone authentication):" + Pop $0 + + ; Username + ${NSD_CreateLabel} 0 90u 60u 12u "Username:" + Pop $UsernameLabel + + ${NSD_CreateText} 65u 88u 120u 14u "standalone" + Pop $UsernameText + + ; Password + ${NSD_CreateLabel} 0 112u 60u 12u "Password:" + Pop $PasswordLabel + + ${NSD_CreatePassword} 65u 110u 120u 14u "G@ppm0ym" + Pop $PasswordText + + ${NSD_CreateLabel} 190u 112u 100% 12u "(change from default!)" + Pop $0 + + nsDialogs::Show +FunctionEnd + +Function ConfigPageLeave + ${NSD_GetText} $PortText $PortValue + ${NSD_GetState} $TLSCheckbox $TLSEnabled + ${NSD_GetText} $UsernameText $UsernameValue + ${NSD_GetText} $PasswordText $PasswordValue +FunctionEnd + +;-------------------------------- +; Installer Sections + +Section "Console Application" SecApp + SectionIn RO ; Required section + + SetOutPath "$INSTDIR" + + ; Install the edition-specific binary as console.exe + File /oname=console.exe "${BINARY}" + + ; Create config directory + CreateDirectory "$INSTDIR\config" + + ; Create data directory + CreateDirectory "$INSTDIR\data" + + ; Generate config.yml + Call WriteConfigFile + + ; Store installation folder and edition + WriteRegStr HKLM "Software\DeviceManagementToolkit\Console" "InstallDir" "$INSTDIR" + WriteRegStr HKLM "Software\DeviceManagementToolkit\Console" "Version" "${VERSION}" + WriteRegStr HKLM "Software\DeviceManagementToolkit\Console" "Edition" "${EDITION}" + + ; Create uninstaller + WriteUninstaller "$INSTDIR\Uninstall.exe" + + ; Add to Add/Remove Programs + WriteRegStr HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\DMTConsole" \ + "DisplayName" "Device Management Toolkit Console" + WriteRegStr HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\DMTConsole" \ + "UninstallString" "$\"$INSTDIR\Uninstall.exe$\"" + WriteRegStr HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\DMTConsole" \ + "QuietUninstallString" "$\"$INSTDIR\Uninstall.exe$\" /S" + WriteRegStr HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\DMTConsole" \ + "InstallLocation" "$INSTDIR" + WriteRegStr HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\DMTConsole" \ + "DisplayVersion" "${VERSION}" + WriteRegStr HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\DMTConsole" \ + "Publisher" "Intel Corporation" + WriteRegDWORD HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\DMTConsole" \ + "NoModify" 1 + WriteRegDWORD HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\DMTConsole" \ + "NoRepair" 1 + + ; Calculate and store install size + ${GetSize} "$INSTDIR" "/S=0K" $0 $1 $2 + IntFmt $0 "0x%08X" $0 + WriteRegDWORD HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\DMTConsole" \ + "EstimatedSize" "$0" +SectionEnd + +Section "Start Menu Shortcuts" SecStartMenu + CreateDirectory "$SMPROGRAMS\Device Management Toolkit" + CreateShortcut "$SMPROGRAMS\Device Management Toolkit\Console.lnk" "$INSTDIR\console.exe" + CreateShortcut "$SMPROGRAMS\Device Management Toolkit\Uninstall Console.lnk" "$INSTDIR\Uninstall.exe" +SectionEnd + +Section "Desktop Shortcut" SecDesktop + CreateShortcut "$DESKTOP\DMT Console.lnk" "$INSTDIR\console.exe" +SectionEnd + +Section /o "Add to PATH" SecPath + ; Add to system PATH using registry + ReadRegStr $0 HKLM "SYSTEM\CurrentControlSet\Control\Session Manager\Environment" "Path" + + ; Check if already in PATH + ${StrStr} $1 $0 "$INSTDIR" + ${If} $1 == "" + ; Not in PATH, add it + WriteRegExpandStr HKLM "SYSTEM\CurrentControlSet\Control\Session Manager\Environment" "Path" "$0;$INSTDIR" + ; Record that we added to PATH + WriteRegStr HKLM "Software\DeviceManagementToolkit\Console" "AddedToPath" "1" + ; Broadcast environment change + SendMessage ${HWND_BROADCAST} ${WM_SETTINGCHANGE} 0 "STR:Environment" /TIMEOUT=5000 + ${EndIf} +SectionEnd + +Section /o "Install as Windows Service" SecService + ; Install as a Windows service using sc.exe + nsExec::ExecToLog 'sc.exe create "DMTConsole" binPath= "$INSTDIR\console.exe" start= auto DisplayName= "Device Management Toolkit Console"' + nsExec::ExecToLog 'sc.exe description "DMTConsole" "Device Management Toolkit Console Service"' + + ; Write service registry key + WriteRegStr HKLM "Software\DeviceManagementToolkit\Console" "ServiceInstalled" "1" +SectionEnd + +;-------------------------------- +; Write Config File Function + +Function WriteConfigFile + ; Determine TLS setting + ${If} $TLSEnabled == ${BST_CHECKED} + StrCpy $0 "true" + ${Else} + StrCpy $0 "false" + ${EndIf} + + FileOpen $1 "$INSTDIR\config\config.yml" w + + FileWrite $1 "app:$\r$\n" + FileWrite $1 " name: console$\r$\n" + FileWrite $1 " repo: device-management-toolkit/console$\r$\n" + FileWrite $1 " version: ${VERSION}$\r$\n" + FileWrite $1 " encryption_key: $\"$\"$\r$\n" + FileWrite $1 " allow_insecure_ciphers: false$\r$\n" + FileWrite $1 "http:$\r$\n" + FileWrite $1 " host: localhost$\r$\n" + FileWrite $1 " port: $\"$PortValue$\"$\r$\n" + FileWrite $1 " ws_compression: false$\r$\n" + FileWrite $1 " tls:$\r$\n" + FileWrite $1 " enabled: $0$\r$\n" + FileWrite $1 " certFile: $\"$\"$\r$\n" + FileWrite $1 " keyFile: $\"$\"$\r$\n" + FileWrite $1 " allowed_origins:$\r$\n" + FileWrite $1 " - $\"*$\"$\r$\n" + FileWrite $1 " allowed_headers:$\r$\n" + FileWrite $1 " - $\"*$\"$\r$\n" + FileWrite $1 "logger:$\r$\n" + FileWrite $1 " log_level: info$\r$\n" + FileWrite $1 "secrets:$\r$\n" + FileWrite $1 " address: http://localhost:8200$\r$\n" + FileWrite $1 " token: $\"$\"$\r$\n" + FileWrite $1 "postgres:$\r$\n" + FileWrite $1 " pool_max: 2$\r$\n" + FileWrite $1 " url: $\"$\"$\r$\n" + FileWrite $1 "ea:$\r$\n" + FileWrite $1 " url: http://localhost:8000$\r$\n" + FileWrite $1 " username: $\"$\"$\r$\n" + FileWrite $1 " password: $\"$\"$\r$\n" + FileWrite $1 "auth:$\r$\n" + FileWrite $1 " disabled: false$\r$\n" + FileWrite $1 " adminUsername: $\"$UsernameValue$\"$\r$\n" + FileWrite $1 " adminPassword: $\"$PasswordValue$\"$\r$\n" + FileWrite $1 " jwtKey: your_secret_jwt_key$\r$\n" + FileWrite $1 " jwtExpiration: 24h0m0s$\r$\n" + FileWrite $1 " redirectionJWTExpiration: 5m0s$\r$\n" + FileWrite $1 " clientId: $\"$\"$\r$\n" + FileWrite $1 " issuer: $\"$\"$\r$\n" + FileWrite $1 " ui:$\r$\n" + FileWrite $1 " clientId: $\"$\"$\r$\n" + FileWrite $1 " issuer: $\"$\"$\r$\n" + FileWrite $1 " scope: $\"$\"$\r$\n" + FileWrite $1 " redirectUri: $\"$\"$\r$\n" + FileWrite $1 " responseType: $\"code$\"$\r$\n" + FileWrite $1 " requireHttps: false$\r$\n" + FileWrite $1 " strictDiscoveryDocumentValidation: true$\r$\n" + FileWrite $1 "ui:$\r$\n" + FileWrite $1 " externalUrl: $\"$\"$\r$\n" + + FileClose $1 +FunctionEnd + +;-------------------------------- +; Section Descriptions + +!insertmacro MUI_FUNCTION_DESCRIPTION_BEGIN + !insertmacro MUI_DESCRIPTION_TEXT ${SecApp} "Install the Device Management Toolkit Console application. (Required)" + !insertmacro MUI_DESCRIPTION_TEXT ${SecStartMenu} "Create Start Menu shortcuts for easy access." + !insertmacro MUI_DESCRIPTION_TEXT ${SecDesktop} "Create a Desktop shortcut." + !insertmacro MUI_DESCRIPTION_TEXT ${SecPath} "Add the installation directory to the system PATH environment variable." + !insertmacro MUI_DESCRIPTION_TEXT ${SecService} "Install Console as a Windows Service for automatic startup." +!insertmacro MUI_FUNCTION_DESCRIPTION_END + +;-------------------------------- +; Uninstaller Section + +Section "Uninstall" + ; Stop and remove service if installed + ReadRegStr $0 HKLM "Software\DeviceManagementToolkit\Console" "ServiceInstalled" + ${If} $0 == "1" + nsExec::ExecToLog 'sc.exe stop "DMTConsole"' + ; Wait for the service to fully stop before deleting + StrCpy $1 0 + ${Do} + nsExec::ExecToStack 'sc.exe query "DMTConsole"' + Pop $2 ; exit code + Pop $3 ; output + ${UnStrStr} $4 $3 "STOPPED" + ${If} $4 != "" + ${ExitDo} + ${EndIf} + IntOp $1 $1 + 1 + ${If} $1 >= 30 + ${ExitDo} + ${EndIf} + Sleep 1000 + ${Loop} + nsExec::ExecToLog 'sc.exe delete "DMTConsole"' + ${EndIf} + + ; Remove from PATH if we added it + ReadRegStr $0 HKLM "Software\DeviceManagementToolkit\Console" "AddedToPath" + ${If} $0 == "1" + ReadRegStr $1 HKLM "SYSTEM\CurrentControlSet\Control\Session Manager\Environment" "Path" + ; Remove our path (try both with and without trailing semicolon) + ${UnStrRep} $2 $1 ";$INSTDIR" "" + ${UnStrRep} $2 $2 "$INSTDIR;" "" + ${UnStrRep} $2 $2 "$INSTDIR" "" + WriteRegExpandStr HKLM "SYSTEM\CurrentControlSet\Control\Session Manager\Environment" "Path" "$2" + ; Broadcast environment change + SendMessage ${HWND_BROADCAST} ${WM_SETTINGCHANGE} 0 "STR:Environment" /TIMEOUT=5000 + ${EndIf} + + ; Remove files + Delete "$INSTDIR\console.exe" + Delete "$INSTDIR\config\config.yml" + Delete "$INSTDIR\Uninstall.exe" + + ; Remove directories (only if empty) + RMDir "$INSTDIR\config" + RMDir "$INSTDIR\data" + RMDir "$INSTDIR" + RMDir "$PROGRAMFILES64\Device Management Toolkit" + + ; Remove shortcuts + Delete "$SMPROGRAMS\Device Management Toolkit\Console.lnk" + Delete "$SMPROGRAMS\Device Management Toolkit\Uninstall Console.lnk" + RMDir "$SMPROGRAMS\Device Management Toolkit" + Delete "$DESKTOP\DMT Console.lnk" + + ; Remove registry keys + DeleteRegKey HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\DMTConsole" + DeleteRegKey HKLM "Software\DeviceManagementToolkit\Console" + DeleteRegKey /ifempty HKLM "Software\DeviceManagementToolkit" +SectionEnd + +;-------------------------------- +; Functions + +Function .onInit + ; Set default values + StrCpy $PortValue "8181" + StrCpy $TLSEnabled ${BST_CHECKED} + StrCpy $UsernameValue "standalone" + StrCpy $PasswordValue "G@ppm0ym" + + ; Check for admin rights + UserInfo::GetAccountType + Pop $0 + ${If} $0 != "admin" + MessageBox MB_ICONSTOP "Administrator rights required!" + SetErrorLevel 740 ; ERROR_ELEVATION_REQUIRED + Quit + ${EndIf} +FunctionEnd diff --git a/installer/linux/build-packages.sh b/installer/linux/build-packages.sh new file mode 100755 index 000000000..91889a42a --- /dev/null +++ b/installer/linux/build-packages.sh @@ -0,0 +1,99 @@ +#!/bin/bash +# Device Management Toolkit Console - Linux Package Build Script +# Copyright (c) Intel Corporation +# SPDX-License-Identifier: Apache-2.0 + +set -e + +VERSION="${1:-0.0.0}" +ARCH="${2:-amd64}" +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +PROJECT_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)" +BUILD_DIR="$PROJECT_ROOT/installer/linux/build" +OUTPUT_DIR="$PROJECT_ROOT/dist/linux" + +echo "Building Linux packages..." +echo "Version: $VERSION" +echo "Architecture: $ARCH" +echo "" + +mkdir -p "$OUTPUT_DIR" + +# Build binaries +echo "=== Building Binaries ===" + +# Build UI binary with tray (requires CGO for systray/webview support) +echo "Building UI binary with tray (CGO_ENABLED=1)..." +UI_BINARY="$OUTPUT_DIR/console_linux_${ARCH}_tray" +CGO_ENABLED=1 GOOS=linux GOARCH=$ARCH go build -tags=tray -ldflags "-s -w" -trimpath -o "$UI_BINARY" "$PROJECT_ROOT/cmd/app" +echo " Built: $UI_BINARY" + +# Build headless binary with tray (requires CGO) +echo "Building headless binary with tray (CGO_ENABLED=1)..." +HEADLESS_BINARY="$OUTPUT_DIR/console_linux_${ARCH}_headless_tray" +CGO_ENABLED=1 GOOS=linux GOARCH=$ARCH go build -tags='tray noui' -ldflags "-s -w" -trimpath -o "$HEADLESS_BINARY" "$PROJECT_ROOT/cmd/app" +echo " Built: $HEADLESS_BINARY" + +echo "" + +# Function to build a tar.gz package +build_package() { + local EDITION="$1" # "ui" or "headless" + local BINARY="$2" # path to binary + local PKG_NAME="$3" # output archive name (without extension) + + echo "=== Building $EDITION Package ===" + + # Clean and create build directory + rm -rf "$BUILD_DIR" + mkdir -p "$BUILD_DIR/$PKG_NAME" + + # Copy binary + cp "$BINARY" "$BUILD_DIR/$PKG_NAME/console" + chmod 755 "$BUILD_DIR/$PKG_NAME/console" + + # Copy and process scripts + cp "$SCRIPT_DIR/configure.sh" "$BUILD_DIR/$PKG_NAME/configure.sh" + sed -i "s/VERSION_PLACEHOLDER/$VERSION/g" "$BUILD_DIR/$PKG_NAME/configure.sh" + chmod 755 "$BUILD_DIR/$PKG_NAME/configure.sh" + + cp "$SCRIPT_DIR/install.sh" "$BUILD_DIR/$PKG_NAME/install.sh" + sed -i "s/VERSION_PLACEHOLDER/$VERSION/g" "$BUILD_DIR/$PKG_NAME/install.sh" + chmod 755 "$BUILD_DIR/$PKG_NAME/install.sh" + + cp "$SCRIPT_DIR/uninstall.sh" "$BUILD_DIR/$PKG_NAME/uninstall.sh" + sed -i "s/VERSION_PLACEHOLDER/$VERSION/g" "$BUILD_DIR/$PKG_NAME/uninstall.sh" + chmod 755 "$BUILD_DIR/$PKG_NAME/uninstall.sh" + + # Copy systemd service file + cp "$SCRIPT_DIR/dmt-console.service" "$BUILD_DIR/$PKG_NAME/dmt-console.service" + chmod 644 "$BUILD_DIR/$PKG_NAME/dmt-console.service" + + # Create tar.gz archive + echo " Creating archive..." + tar -czf "$OUTPUT_DIR/${PKG_NAME}.tar.gz" -C "$BUILD_DIR" "$PKG_NAME" + + echo " Created: $OUTPUT_DIR/${PKG_NAME}.tar.gz" + echo "" + + # Clean up + rm -rf "$BUILD_DIR" +} + +# Build UI package (with tray) +build_package "ui" "$UI_BINARY" "console_${VERSION}_linux_${ARCH}" + +# Build Headless package +build_package "headless" "$HEADLESS_BINARY" "console_${VERSION}_linux_${ARCH}_headless" + +echo "=== Build Complete ===" +echo "" +echo "Packages created:" +echo " UI: $OUTPUT_DIR/console_${VERSION}_linux_${ARCH}.tar.gz" +echo " Headless: $OUTPUT_DIR/console_${VERSION}_linux_${ARCH}_headless.tar.gz" +echo "" +echo "Installation:" +echo " 1. Extract: tar -xzf console_${VERSION}_linux_${ARCH}.tar.gz" +echo " 2. Install: sudo ./install.sh" +echo " 3. Configure: sudo dmt-configure" +echo " 4. Start: sudo systemctl start dmt-console" diff --git a/installer/linux/configure.sh b/installer/linux/configure.sh new file mode 100755 index 000000000..75e90474b --- /dev/null +++ b/installer/linux/configure.sh @@ -0,0 +1,201 @@ +#!/bin/bash +# Device Management Toolkit Console - Configuration Script +# Copyright (c) Intel Corporation +# SPDX-License-Identifier: Apache-2.0 + +set -e + +APP_DIR="/usr/local/device-management-toolkit" +CONFIG_FILE="$APP_DIR/config/config.yml" +VERSION="VERSION_PLACEHOLDER" + +# Colors for output +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +BLUE='\033[0;34m' +NC='\033[0m' # No Color + +echo "" +echo -e "${BLUE}==================================================" +echo "Device Management Toolkit Console Configuration" +echo -e "==================================================${NC}" +echo "" + +# Check if running as root +if [ "$EUID" -ne 0 ]; then + echo -e "${RED}Please run as root (sudo dmt-configure)${NC}" + exit 1 +fi + +# Function to prompt with default value +prompt_with_default() { + local prompt="$1" + local default="$2" + local result + + read -p "$prompt [$default]: " result + echo "${result:-$default}" +} + +# Function to prompt yes/no +prompt_yes_no() { + local prompt="$1" + local default="$2" + local result + + while true; do + read -p "$prompt (y/n) [$default]: " result + result="${result:-$default}" + case "$result" in + [Yy]* ) echo "true"; return;; + [Nn]* ) echo "false"; return;; + * ) echo "Please answer y or n.";; + esac + done +} + +# Function to prompt for password (hidden) +# IMPORTANT: echo newline to stderr, NOT stdout (stdout is the return value) +prompt_password() { + local prompt="$1" + local default="$2" + local result + + read -s -p "$prompt [$default]: " result + echo "" >&2 + echo "${result:-$default}" +} + +echo -e "${YELLOW}Step 1: Network Configuration${NC}" +echo "" + +HTTP_PORT=$(prompt_with_default "HTTP Port" "8181") + +TLS_ENABLED=$(prompt_yes_no "Enable TLS/HTTPS (recommended)" "y") +if [ "$TLS_ENABLED" = "true" ]; then + echo " A self-signed certificate will be generated if none is provided." +fi + +echo "" +echo -e "${YELLOW}Step 2: Administrator Credentials${NC}" +echo " (Used for standalone authentication)" +echo "" + +ADMIN_USERNAME=$(prompt_with_default "Admin Username" "standalone") +echo -e "${YELLOW}Note: Change the default password for security!${NC}" +ADMIN_PASSWORD=$(prompt_password "Admin Password" "G@ppm0ym") + +echo "" +echo -e "${YELLOW}Configuration Summary${NC}" +echo " Port: $HTTP_PORT" +echo " TLS: $TLS_ENABLED" +echo " Username: $ADMIN_USERNAME" +echo " Password: ********" +echo "" + +read -p "Apply this configuration? (y/n) [y]: " confirm +confirm="${confirm:-y}" + +if [[ ! "$confirm" =~ ^[Yy]$ ]]; then + echo "Configuration cancelled." + exit 0 +fi + +echo "" +echo -e "${BLUE}Applying configuration...${NC}" + +# Ensure config directory exists +mkdir -p "$(dirname "$CONFIG_FILE")" + +# Generate config file +# IMPORTANT: Quote $ADMIN_USERNAME and $ADMIN_PASSWORD with double quotes +# to prevent YAML parsing issues with special characters +cat > "$CONFIG_FILE" << EOF +app: + name: console + repo: device-management-toolkit/console + version: $VERSION + encryption_key: "" + allow_insecure_ciphers: false +http: + host: localhost + port: "$HTTP_PORT" + ws_compression: false + tls: + enabled: $TLS_ENABLED + certFile: "" + keyFile: "" + allowed_origins: + - "*" + allowed_headers: + - "*" +logger: + log_level: info +secrets: + address: http://localhost:8200 + token: "" +postgres: + pool_max: 2 + url: "" +ea: + url: http://localhost:8000 + username: "" + password: "" +auth: + disabled: false + adminUsername: "$ADMIN_USERNAME" + adminPassword: "$ADMIN_PASSWORD" + jwtKey: your_secret_jwt_key + jwtExpiration: 24h0m0s + redirectionJWTExpiration: 5m0s + clientId: "" + issuer: "" + ui: + clientId: "" + issuer: "" + scope: "" + redirectUri: "" + responseType: "code" + requireHttps: false + strictDiscoveryDocumentValidation: true +ui: + externalUrl: "" +EOF + +# Make config readable by root and dmt group only +chmod 640 "$CONFIG_FILE" +chown root:dmt "$CONFIG_FILE" 2>/dev/null || chmod 644 "$CONFIG_FILE" +echo " Configuration saved to $CONFIG_FILE" + +# Restart the service if it's running +# Use WAS_RUNNING variable to track state; don't re-check later +WAS_RUNNING=false +if systemctl is-active --quiet dmt-console 2>/dev/null; then + WAS_RUNNING=true + echo " Restarting DMT Console service..." + systemctl restart dmt-console + echo " Service restarted." +fi + +echo "" +echo -e "${GREEN}==================================================" +echo "Configuration complete!" +echo -e "==================================================${NC}" +echo "" + +SCHEME="http" +if [ "$TLS_ENABLED" = "true" ]; then + SCHEME="https" +fi + +if [ "$WAS_RUNNING" = true ]; then + echo "DMT Console has been restarted with the new configuration." +else + echo "To start the service:" + echo " sudo systemctl start dmt-console" +fi +echo "" +echo "Access the web interface at:" +echo " $SCHEME://localhost:$HTTP_PORT" +echo "" diff --git a/installer/linux/dmt-console.service b/installer/linux/dmt-console.service new file mode 100644 index 000000000..8b912eb24 --- /dev/null +++ b/installer/linux/dmt-console.service @@ -0,0 +1,28 @@ +# Device Management Toolkit Console - Systemd Service Unit +# Copyright (c) Intel Corporation +# SPDX-License-Identifier: Apache-2.0 + +[Unit] +Description=Device Management Toolkit Console +Wants=network-online.target +After=network-online.target + +[Service] +Type=simple +User=dmt +Group=dmt +ExecStart=/usr/local/device-management-toolkit/console +Restart=on-failure +RestartSec=5 +StandardOutput=journal +StandardError=journal + +# Security hardening +NoNewPrivileges=true +ProtectSystem=strict +ProtectHome=true +ReadWritePaths=/var/lib/device-management-toolkit +ReadOnlyPaths=/usr/local/device-management-toolkit + +[Install] +WantedBy=multi-user.target diff --git a/installer/linux/install.sh b/installer/linux/install.sh new file mode 100755 index 000000000..9c40fc255 --- /dev/null +++ b/installer/linux/install.sh @@ -0,0 +1,195 @@ +#!/bin/bash +# Device Management Toolkit Console - Linux Installation Script +# Copyright (c) Intel Corporation +# SPDX-License-Identifier: Apache-2.0 + +set -e + +VERSION="VERSION_PLACEHOLDER" +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" + +APP_DIR="/usr/local/device-management-toolkit" +CONFIG_DIR="$APP_DIR/config" +DATA_DIR="/var/lib/device-management-toolkit" +SYMLINK_DIR="/usr/local/bin" +SERVICE_FILE="/etc/systemd/system/dmt-console.service" + +# Colors for output +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +BLUE='\033[0;34m' +NC='\033[0m' # No Color + +echo "" +echo -e "${BLUE}==================================================" +echo "Device Management Toolkit Console Installer" +echo "Version: $VERSION" +echo -e "==================================================${NC}" +echo "" + +# Check for root privileges +if [ "$EUID" -ne 0 ]; then + echo -e "${RED}This installer must be run as root.${NC}" + echo " Usage: sudo ./install.sh" + exit 1 +fi + +# Check that required files exist in the script directory +if [ ! -f "$SCRIPT_DIR/console" ]; then + echo -e "${RED}Error: console binary not found in $SCRIPT_DIR${NC}" + echo "Please run this script from the extracted archive directory." + exit 1 +fi + +echo -e "${BLUE}Installing DMT Console...${NC}" +echo "" + +# Create dmt system user if it doesn't exist +if ! id -u dmt > /dev/null 2>&1; then + echo " Creating system user 'dmt'..." + useradd --system --no-create-home --shell /usr/sbin/nologin dmt + echo -e " ${GREEN}Created system user 'dmt'${NC}" +else + echo " System user 'dmt' already exists." +fi + +# Create application directory +echo " Creating application directory..." +mkdir -p "$APP_DIR" + +# Create config directory +if [ ! -d "$CONFIG_DIR" ]; then + mkdir -p "$CONFIG_DIR" + chmod 755 "$CONFIG_DIR" +fi + +# Create data directory (Linux convention: /var/lib) +if [ ! -d "$DATA_DIR" ]; then + mkdir -p "$DATA_DIR" +fi +chown dmt:dmt "$DATA_DIR" +chmod 755 "$DATA_DIR" + +# Install binary +echo " Installing binary..." +cp "$SCRIPT_DIR/console" "$APP_DIR/console" +chmod 755 "$APP_DIR/console" + +# Install configure script +echo " Installing configuration script..." +cp "$SCRIPT_DIR/configure.sh" "$APP_DIR/configure.sh" +chmod 755 "$APP_DIR/configure.sh" + +# Install uninstall script +echo " Installing uninstall script..." +cp "$SCRIPT_DIR/uninstall.sh" "$APP_DIR/uninstall.sh" +chmod 755 "$APP_DIR/uninstall.sh" + +# Create symlinks in /usr/local/bin for easy CLI access +echo " Creating symlinks..." +ln -sf "$APP_DIR/console" "$SYMLINK_DIR/dmt-console" +echo " $SYMLINK_DIR/dmt-console -> $APP_DIR/console" +ln -sf "$APP_DIR/configure.sh" "$SYMLINK_DIR/dmt-configure" +echo " $SYMLINK_DIR/dmt-configure -> $APP_DIR/configure.sh" +ln -sf "$APP_DIR/uninstall.sh" "$SYMLINK_DIR/dmt-uninstall" +echo " $SYMLINK_DIR/dmt-uninstall -> $APP_DIR/uninstall.sh" + +# Install systemd service file +echo " Installing systemd service..." +cp "$SCRIPT_DIR/dmt-console.service" "$SERVICE_FILE" +chmod 644 "$SERVICE_FILE" +systemctl daemon-reload +echo -e " ${GREEN}Systemd service installed${NC}" + +# Generate default config if it doesn't exist (preserve existing config on upgrade) +if [ ! -f "$CONFIG_DIR/config.yml" ]; then + echo " Generating default configuration..." + cat > "$CONFIG_DIR/config.yml" << EOF +app: + name: console + repo: device-management-toolkit/console + version: $VERSION + encryption_key: "" + allow_insecure_ciphers: false +http: + host: localhost + port: "8181" + ws_compression: false + tls: + enabled: true + certFile: "" + keyFile: "" + allowed_origins: + - "*" + allowed_headers: + - "*" +logger: + log_level: info +secrets: + address: http://localhost:8200 + token: "" +postgres: + pool_max: 2 + url: "" +ea: + url: http://localhost:8000 + username: "" + password: "" +auth: + disabled: false + adminUsername: "standalone" + adminPassword: "G@ppm0ym" + jwtKey: your_secret_jwt_key + jwtExpiration: 24h0m0s + redirectionJWTExpiration: 5m0s + clientId: "" + issuer: "" + ui: + clientId: "" + issuer: "" + scope: "" + redirectUri: "" + responseType: "code" + requireHttps: false + strictDiscoveryDocumentValidation: true +ui: + externalUrl: "" +EOF + chmod 640 "$CONFIG_DIR/config.yml" + chown root:dmt "$CONFIG_DIR/config.yml" + echo -e " ${GREEN}Default configuration saved to $CONFIG_DIR/config.yml${NC}" +else + echo " Existing configuration preserved at $CONFIG_DIR/config.yml" + chmod 640 "$CONFIG_DIR/config.yml" 2>/dev/null || true + chown root:dmt "$CONFIG_DIR/config.yml" 2>/dev/null || true +fi + +# Set ownership on application directory +chown -R root:dmt "$APP_DIR" +chown dmt:dmt "$DATA_DIR" + +echo "" +echo -e "${GREEN}==================================================" +echo "Device Management Toolkit Console installed!" +echo -e "==================================================${NC}" +echo "" +echo "Next steps:" +echo "" +echo -e " 1. Configure the service (${YELLOW}recommended${NC}):" +echo " sudo dmt-configure" +echo "" +echo " 2. Start the service:" +echo " sudo systemctl start dmt-console" +echo "" +echo " 3. Enable auto-start on boot:" +echo " sudo systemctl enable dmt-console" +echo "" +echo "Other commands:" +echo " Check status: systemctl status dmt-console" +echo " View logs: journalctl -u dmt-console -f" +echo " Reconfigure: sudo dmt-configure" +echo " Uninstall: sudo dmt-uninstall" +echo "" + +exit 0 diff --git a/installer/linux/uninstall.sh b/installer/linux/uninstall.sh new file mode 100755 index 000000000..cbc98856f --- /dev/null +++ b/installer/linux/uninstall.sh @@ -0,0 +1,126 @@ +#!/bin/bash +# Device Management Toolkit Console - Linux Uninstall Script +# Copyright (c) Intel Corporation +# SPDX-License-Identifier: Apache-2.0 + +set -e + +APP_DIR="/usr/local/device-management-toolkit" +DATA_DIR="/var/lib/device-management-toolkit" +SERVICE_FILE="/etc/systemd/system/dmt-console.service" +SYMLINKS=( + "/usr/local/bin/dmt-console" + "/usr/local/bin/dmt-configure" + "/usr/local/bin/dmt-uninstall" +) + +# Colors for output +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +BLUE='\033[0;34m' +NC='\033[0m' # No Color + +echo "" +echo -e "${BLUE}==================================================" +echo "Device Management Toolkit Console Uninstaller" +echo -e "==================================================${NC}" +echo "" + +# Check for root privileges +if [ "$EUID" -ne 0 ]; then + echo -e "${RED}Please run as root: sudo dmt-uninstall${NC}" + exit 1 +fi + +# Check if installed +if [ ! -d "$APP_DIR" ] && [ ! -f "$SERVICE_FILE" ]; then + echo "DMT Console does not appear to be installed." + exit 0 +fi + +# Ask about data preservation +echo "Do you want to remove configuration and data files?" +echo " - Configuration: $APP_DIR/config/" +echo " - Data: $DATA_DIR/" +echo "" +read -p "Remove all data? [y/N]: " REMOVE_DATA + +# Stop and disable the systemd service +echo "" +echo -e "${BLUE}Stopping service...${NC}" +if systemctl is-active --quiet dmt-console 2>/dev/null; then + systemctl stop dmt-console + echo " Service stopped." +fi +if systemctl is-enabled --quiet dmt-console 2>/dev/null; then + systemctl disable dmt-console + echo " Service disabled." +fi + +# Remove systemd service file +if [ -f "$SERVICE_FILE" ]; then + rm -f "$SERVICE_FILE" + systemctl daemon-reload + echo " Service file removed and systemd reloaded." +fi + +# Remove symlinks +echo "" +echo -e "${BLUE}Removing symlinks...${NC}" +for link in "${SYMLINKS[@]}"; do + if [ -L "$link" ]; then + rm -f "$link" + echo " Removed: $link" + fi +done + +# Remove application files +echo "" +echo -e "${BLUE}Removing application files...${NC}" +rm -f "$APP_DIR/console" +rm -f "$APP_DIR/configure.sh" +rm -f "$APP_DIR/uninstall.sh" +echo " Removed binaries and scripts." + +# Handle data removal +if [[ "$REMOVE_DATA" =~ ^[Yy]$ ]]; then + echo "" + echo -e "${BLUE}Removing configuration and data...${NC}" + rm -rf "$APP_DIR/config" + echo " Removed: $APP_DIR/config/" + + rm -rf "$DATA_DIR" + echo " Removed: $DATA_DIR/" + + # Remove entire application directory if empty + rmdir "$APP_DIR" 2>/dev/null && echo " Removed: $APP_DIR/" || true + + # Remove dmt system user + if id -u dmt > /dev/null 2>&1; then + userdel dmt 2>/dev/null || true + echo " Removed system user 'dmt'." + fi +else + echo "" + echo -e "${YELLOW}Keeping configuration and data files.${NC}" +fi + +echo "" +echo -e "${GREEN}==================================================" +echo "DMT Console has been uninstalled." +echo -e "==================================================${NC}" + +if [[ ! "$REMOVE_DATA" =~ ^[Yy]$ ]]; then + echo "" + echo "Configuration and data preserved at:" + echo " $APP_DIR/config/" + echo " $DATA_DIR/" + echo "" + echo "To completely remove all data, run:" + echo " sudo rm -rf $APP_DIR $DATA_DIR" + echo " sudo userdel dmt" +fi +echo "" + +exit 0 diff --git a/installer/macos/build-pkg.sh b/installer/macos/build-pkg.sh new file mode 100755 index 000000000..2b485c7e7 --- /dev/null +++ b/installer/macos/build-pkg.sh @@ -0,0 +1,133 @@ +#!/bin/bash +# Device Management Toolkit Console - macOS PKG Build Script +# Copyright (c) Intel Corporation +# SPDX-License-Identifier: Apache-2.0 + +set -e + +VERSION="${1:-0.0.0}" +ARCH="${2:-arm64}" +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +PROJECT_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)" +BUILD_DIR="$PROJECT_ROOT/installer/macos/build" +OUTPUT_DIR="$PROJECT_ROOT/dist/darwin" + +echo "Building macOS PKG installers..." +echo "Version: $VERSION" +echo "Architecture: $ARCH" +echo "" + +mkdir -p "$OUTPUT_DIR" + +# Build binaries +echo "=== Building Binaries ===" + +# Build UI binary with tray (requires native macOS build with CGO) +echo "Building UI binary with tray (CGO_ENABLED=1)..." +UI_BINARY="$OUTPUT_DIR/console_mac_${ARCH}_tray" +CGO_ENABLED=1 GOOS=darwin GOARCH=$ARCH go build -tags=tray -ldflags "-s -w" -trimpath -o "$UI_BINARY" "$PROJECT_ROOT/cmd/app" +echo " Built: $UI_BINARY" + +# Build headless binary with tray (requires native macOS build with CGO) +echo "Building headless binary with tray (CGO_ENABLED=1)..." +HEADLESS_BINARY="$OUTPUT_DIR/console_mac_${ARCH}_headless_tray" +CGO_ENABLED=1 GOOS=darwin GOARCH=$ARCH go build -tags='tray noui' -ldflags "-s -w" -trimpath -o "$HEADLESS_BINARY" "$PROJECT_ROOT/cmd/app" +echo " Built: $HEADLESS_BINARY" + +echo "" + +# Function to build a PKG +build_pkg() { + local EDITION="$1" # "ui" or "headless" + local BINARY="$2" # path to binary + local IDENTIFIER="$3" # package identifier suffix + local PKG_NAME="$4" # output pkg name + + echo "=== Building $EDITION PKG ===" + + # Clean and create build directory + rm -rf "$BUILD_DIR" + mkdir -p "$BUILD_DIR/payload/usr/local/device-management-toolkit" + mkdir -p "$BUILD_DIR/scripts" + mkdir -p "$BUILD_DIR/resources" + + # Copy binary + cp "$BINARY" "$BUILD_DIR/payload/usr/local/device-management-toolkit/console" + chmod 755 "$BUILD_DIR/payload/usr/local/device-management-toolkit/console" + + # Copy and process configuration script + cp "$SCRIPT_DIR/configure.sh" "$BUILD_DIR/payload/usr/local/device-management-toolkit/configure.sh" + sed -i '' "s/VERSION_PLACEHOLDER/$VERSION/g" "$BUILD_DIR/payload/usr/local/device-management-toolkit/configure.sh" + chmod 755 "$BUILD_DIR/payload/usr/local/device-management-toolkit/configure.sh" + + # Copy uninstall script + cp "$SCRIPT_DIR/uninstall.sh" "$BUILD_DIR/payload/usr/local/device-management-toolkit/uninstall.sh" + chmod 755 "$BUILD_DIR/payload/usr/local/device-management-toolkit/uninstall.sh" + + # Copy scripts (use edition-specific postinstall) + cp "$SCRIPT_DIR/scripts/preinstall" "$BUILD_DIR/scripts/" + cp "$SCRIPT_DIR/scripts/postinstall-$EDITION" "$BUILD_DIR/scripts/postinstall" + sed -i '' "s/VERSION_PLACEHOLDER/$VERSION/g" "$BUILD_DIR/scripts/postinstall" + chmod 755 "$BUILD_DIR/scripts/"* + + # Copy and process resources + cp "$SCRIPT_DIR/resources/"*.html "$BUILD_DIR/resources/" + cp "$PROJECT_ROOT/LICENSE" "$BUILD_DIR/resources/license.txt" + + # Set edition-specific conclusion text + if [ "$EDITION" = "ui" ]; then + CONCLUSION_TEXT="The web interface will be available at https:\/\/localhost:8181<\/code> by default." + else + CONCLUSION_TEXT="Running in headless mode (API only). No web interface is included in this edition." + fi + + for file in "$BUILD_DIR/resources/"*.html; do + sed -i '' "s/VERSION_PLACEHOLDER/$VERSION/g" "$file" + sed -i '' "s/EDITION_CONCLUSION_PLACEHOLDER/$CONCLUSION_TEXT/g" "$file" + done + + # Process distribution.xml + sed "s/VERSION_PLACEHOLDER/$VERSION/g" "$SCRIPT_DIR/distribution.xml" > "$BUILD_DIR/distribution.xml" + + # Build component package + echo " Building component package..." + pkgbuild \ + --root "$BUILD_DIR/payload" \ + --scripts "$BUILD_DIR/scripts" \ + --identifier "com.intel.dmt-console-$IDENTIFIER" \ + --version "$VERSION" \ + --install-location "/" \ + "$BUILD_DIR/console.pkg" + + # Build product archive + echo " Building product archive..." + productbuild \ + --distribution "$BUILD_DIR/distribution.xml" \ + --resources "$BUILD_DIR/resources" \ + --package-path "$BUILD_DIR" \ + "$OUTPUT_DIR/$PKG_NAME" + + echo " Created: $OUTPUT_DIR/$PKG_NAME" + echo "" + + # Clean up + rm -rf "$BUILD_DIR" +} + +# Build UI PKG (with tray) +build_pkg "ui" "$UI_BINARY" "ui" "console_${VERSION}_macos_${ARCH}.pkg" + +# Build Headless PKG +build_pkg "headless" "$HEADLESS_BINARY" "headless" "console_${VERSION}_macos_${ARCH}_headless.pkg" + +echo "=== Build Complete ===" +echo "" +echo "Installers created:" +echo " UI (with system tray): $OUTPUT_DIR/console_${VERSION}_macos_${ARCH}.pkg" +echo " Headless: $OUTPUT_DIR/console_${VERSION}_macos_${ARCH}_headless.pkg" +echo "" +echo "After installation:" +echo " - UI version auto-launches with system tray icon" +echo " - Headless version: run 'dmt-console' manually" +echo " - Reconfigure: sudo dmt-configure" +echo " - Uninstall: sudo dmt-uninstall" diff --git a/installer/macos/configure.sh b/installer/macos/configure.sh new file mode 100755 index 000000000..c1fbb6a1a --- /dev/null +++ b/installer/macos/configure.sh @@ -0,0 +1,200 @@ +#!/bin/bash +# Device Management Toolkit Console - Configuration Script +# Copyright (c) Intel Corporation +# SPDX-License-Identifier: Apache-2.0 + +set -e + +APP_DIR="/usr/local/device-management-toolkit" +CONFIG_FILE="$APP_DIR/config/config.yml" +VERSION="VERSION_PLACEHOLDER" + +# Colors for output +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +BLUE='\033[0;34m' +NC='\033[0m' # No Color + +echo "" +echo -e "${BLUE}==================================================" +echo "Device Management Toolkit Console Configuration" +echo -e "==================================================${NC}" +echo "" + +# Check if running as root +if [ "$EUID" -ne 0 ]; then + echo -e "${RED}Please run as root (sudo dmt-configure)${NC}" + exit 1 +fi + +# Function to prompt with default value +prompt_with_default() { + local prompt="$1" + local default="$2" + local result + + read -p "$prompt [$default]: " result + echo "${result:-$default}" +} + +# Function to prompt yes/no +prompt_yes_no() { + local prompt="$1" + local default="$2" + local result + + while true; do + read -p "$prompt (y/n) [$default]: " result + result="${result:-$default}" + case "$result" in + [Yy]* ) echo "true"; return;; + [Nn]* ) echo "false"; return;; + * ) echo "Please answer y or n.";; + esac + done +} + +# Function to prompt for password (hidden) +prompt_password() { + local prompt="$1" + local default="$2" + local result + + read -s -p "$prompt [$default]: " result + echo "" >&2 + echo "${result:-$default}" +} + +echo -e "${YELLOW}Step 1: Network Configuration${NC}" +echo "" + +HTTP_PORT=$(prompt_with_default "HTTP Port" "8181") + +TLS_ENABLED=$(prompt_yes_no "Enable TLS/HTTPS (recommended)" "y") +if [ "$TLS_ENABLED" = "true" ]; then + echo " A self-signed certificate will be generated if none is provided." +fi + +echo "" +echo -e "${YELLOW}Step 2: Administrator Credentials${NC}" +echo " (Used for standalone authentication)" +echo "" + +ADMIN_USERNAME=$(prompt_with_default "Admin Username" "standalone") +echo -e "${YELLOW}Note: Change the default password for security!${NC}" +ADMIN_PASSWORD=$(prompt_password "Admin Password" "G@ppm0ym") + +echo "" +echo -e "${YELLOW}Configuration Summary${NC}" +echo " Port: $HTTP_PORT" +echo " TLS: $TLS_ENABLED" +echo " Username: $ADMIN_USERNAME" +echo " Password: ********" +echo "" + +read -p "Apply this configuration? (y/n) [y]: " confirm +confirm="${confirm:-y}" + +if [[ ! "$confirm" =~ ^[Yy]$ ]]; then + echo "Configuration cancelled." + exit 0 +fi + +echo "" +echo -e "${BLUE}Applying configuration...${NC}" + +# Generate config file +cat > "$CONFIG_FILE" << EOF +app: + name: console + repo: device-management-toolkit/console + version: $VERSION + encryption_key: "" + allow_insecure_ciphers: false +http: + host: localhost + port: "$HTTP_PORT" + ws_compression: false + tls: + enabled: $TLS_ENABLED + certFile: "" + keyFile: "" + allowed_origins: + - "*" + allowed_headers: + - "*" +logger: + log_level: info +secrets: + address: http://localhost:8200 + token: "" +postgres: + pool_max: 2 + url: "" +ea: + url: http://localhost:8000 + username: "" + password: "" +auth: + disabled: false + adminUsername: "$ADMIN_USERNAME" + adminPassword: "$ADMIN_PASSWORD" + jwtKey: your_secret_jwt_key + jwtExpiration: 24h0m0s + redirectionJWTExpiration: 5m0s + clientId: "" + issuer: "" + ui: + clientId: "" + issuer: "" + scope: "" + redirectUri: "" + responseType: "code" + requireHttps: false + strictDiscoveryDocumentValidation: true +ui: + externalUrl: "" +EOF + +# Make config readable by all users +chmod 644 "$CONFIG_FILE" +echo " Configuration saved to $CONFIG_FILE" + +# Restart the console if it's running +WAS_RUNNING=false +CONSOLE_USER=$(stat -f "%Su" /dev/console) +if pgrep -x "console" > /dev/null 2>&1; then + WAS_RUNNING=true + echo " Stopping running instance..." + pkill -x "console" || true + sleep 2 + + if [ -n "$CONSOLE_USER" ] && [ "$CONSOLE_USER" != "root" ]; then + echo " Restarting DMT Console for user: $CONSOLE_USER" + sudo -u "$CONSOLE_USER" nohup "$APP_DIR/console" --tray > /dev/null 2>&1 & + sleep 1 + fi +fi + +echo "" +echo -e "${GREEN}==================================================" +echo "Configuration complete!" +echo -e "==================================================${NC}" +echo "" + +SCHEME="http" +if [ "$TLS_ENABLED" = "true" ]; then + SCHEME="https" +fi + +if [ "$WAS_RUNNING" = true ] && pgrep -x "console" > /dev/null 2>&1; then + echo "DMT Console has been restarted with the new configuration." +else + echo "To start the console:" + echo " dmt-console --tray" +fi +echo "" +echo "Access the web interface at:" +echo " $SCHEME://localhost:$HTTP_PORT" +echo "" diff --git a/installer/macos/distribution.xml b/installer/macos/distribution.xml new file mode 100644 index 000000000..8eadf3e65 --- /dev/null +++ b/installer/macos/distribution.xml @@ -0,0 +1,41 @@ + + + Device Management Toolkit Console + com.intel + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + console.pkg + diff --git a/installer/macos/resources/conclusion.html b/installer/macos/resources/conclusion.html new file mode 100644 index 000000000..b7a7a0e0c --- /dev/null +++ b/installer/macos/resources/conclusion.html @@ -0,0 +1,71 @@ + + + + + + + +

Installation Complete

+ +

Device Management Toolkit Console has been successfully installed.

+ +

To start the console, open Terminal and run:

+
dmt-console --tray
+ +

EDITION_CONCLUSION_PLACEHOLDER

+ +

To reconfigure, run:

+
sudo dmt-configure
+ +

For more information and documentation, visit:
+ https://github.com/device-management-toolkit/console

+ + diff --git a/installer/macos/resources/readme.html b/installer/macos/resources/readme.html new file mode 100644 index 000000000..5a0dd4d2c --- /dev/null +++ b/installer/macos/resources/readme.html @@ -0,0 +1,94 @@ + + + + + + + +

Installation Details

+

The following components will be installed:

+
    +
  • Console application: /usr/local/device-management-toolkit/console
  • +
  • Configuration tool: /usr/local/device-management-toolkit/configure.sh
  • +
  • CLI symlinks: /usr/local/bin/dmt-console, /usr/local/bin/dmt-configure
  • +
+ +

Post-Installation Configuration

+
+ Important: After installation, run the configuration tool to set up your preferences: +
sudo dmt-configure
+
+

The configuration tool allows you to:

+
    +
  • Set the HTTP port (default: 8181)
  • +
  • Enable/disable TLS/HTTPS
  • +
  • Configure administrator credentials
  • +
+ +

Quick Start (Using Defaults)

+

To start immediately with default settings:

+
dmt-console --tray
+ +

Requirements

+
    +
  • macOS 10.15 (Catalina) or later
  • +
  • Administrator privileges for installation and configuration
  • +
+ + diff --git a/installer/macos/resources/welcome.html b/installer/macos/resources/welcome.html new file mode 100644 index 000000000..9d916f9dc --- /dev/null +++ b/installer/macos/resources/welcome.html @@ -0,0 +1,52 @@ + + + + + + + +

Device Management Toolkit Console

+

Version VERSION_PLACEHOLDER

+ +

Welcome to the Device Management Toolkit Console installer.

+ +

This application provides a web-based interface for managing Intel AMT-enabled devices, including:

+
    +
  • Device discovery and management
  • +
  • Remote power control
  • +
  • KVM remote desktop access
  • +
  • Profile and configuration management
  • +
+ +

Click Continue to proceed with the installation.

+ + diff --git a/installer/macos/scripts/postinstall b/installer/macos/scripts/postinstall new file mode 100755 index 000000000..d2b4d7890 --- /dev/null +++ b/installer/macos/scripts/postinstall @@ -0,0 +1,143 @@ +#!/bin/bash +# Device Management Toolkit Console - Post-installation script +# Copyright (c) Intel Corporation +# SPDX-License-Identifier: Apache-2.0 + +set -e + +INSTALL_DIR="/usr/local/bin" +APP_DIR="/usr/local/device-management-toolkit" +CONFIG_DIR="$APP_DIR/config" +DATA_DIR="$APP_DIR/data" +CONFIG_FILE="$CONFIG_DIR/config.yml" +INSTALLER_CONFIG="/tmp/dmt-console-installer-config.plist" + +# Create symlink in /usr/local/bin for easy CLI access +if [ -f "$APP_DIR/console" ]; then + ln -sf "$APP_DIR/console" "$INSTALL_DIR/dmt-console" + echo "Created symlink: $INSTALL_DIR/dmt-console -> $APP_DIR/console" +fi + +# Create data directory with proper permissions +if [ ! -d "$DATA_DIR" ]; then + mkdir -p "$DATA_DIR" + chmod 755 "$DATA_DIR" +fi + +# Create config directory +if [ ! -d "$CONFIG_DIR" ]; then + mkdir -p "$CONFIG_DIR" + chmod 755 "$CONFIG_DIR" +fi + +# Use default configuration +PORT="8181" +TLS_ENABLED="true" +ADMIN_USERNAME="standalone" +ADMIN_PASSWORD="G@ppm0ym" + +# The tray-enabled binary is already installed as the default "console" +# No need to copy - build-pkg.sh handles this + +# Generate config.yml +cat > "$CONFIG_FILE" << EOF +app: + name: console + repo: device-management-toolkit/console + version: VERSION_PLACEHOLDER + encryption_key: "" + allow_insecure_ciphers: false +http: + host: localhost + port: "$PORT" + ws_compression: false + tls: + enabled: $TLS_ENABLED + certFile: "" + keyFile: "" + allowed_origins: + - "*" + allowed_headers: + - "*" +logger: + log_level: info +secrets: + address: http://localhost:8200 + token: "" +postgres: + pool_max: 2 + url: "" +ea: + url: http://localhost:8000 + username: "" + password: "" +auth: + disabled: false + adminUsername: $ADMIN_USERNAME + adminPassword: $ADMIN_PASSWORD + jwtKey: your_secret_jwt_key + jwtExpiration: 24h0m0s + redirectionJWTExpiration: 5m0s + clientId: "" + issuer: "" + ui: + clientId: "" + issuer: "" + scope: "" + redirectUri: "" + responseType: "code" + requireHttps: false + strictDiscoveryDocumentValidation: true +ui: + externalUrl: "" +EOF + +# Make config readable by all users (contains no secrets by default) +chmod 644 "$CONFIG_FILE" +echo "Configuration saved to $CONFIG_FILE" + +# Make data directory writable by users +chmod 777 "$DATA_DIR" + +# Copy the configuration script for future use +if [ -f "$APP_DIR/configure.sh" ]; then + chmod +x "$APP_DIR/configure.sh" + ln -sf "$APP_DIR/configure.sh" "$INSTALL_DIR/dmt-configure" +fi + +# Copy the uninstall script +if [ -f "$APP_DIR/uninstall.sh" ]; then + chmod +x "$APP_DIR/uninstall.sh" + ln -sf "$APP_DIR/uninstall.sh" "$INSTALL_DIR/dmt-uninstall" +fi + +echo "" +echo "==================================================" +echo "Device Management Toolkit Console installed!" +echo "==================================================" +echo "" +echo "To start the console:" +echo " dmt-console --tray" +echo "" +echo "To reconfigure:" +echo " sudo dmt-configure" +echo "" +echo "To uninstall:" +echo " sudo dmt-uninstall" +echo "" + +# Auto-launch the console with tray mode +# Get the console user (the user who initiated the install) +CONSOLE_USER=$(stat -f "%Su" /dev/console) + +if [ -n "$CONSOLE_USER" ] && [ "$CONSOLE_USER" != "root" ]; then + echo "Launching DMT Console for user: $CONSOLE_USER" + # Launch as the console user with tray mode + sudo -u "$CONSOLE_USER" nohup "$APP_DIR/console" --tray > /dev/null 2>&1 & + sleep 1 + echo "DMT Console is now running in the system tray." +else + echo "Please run 'dmt-console --tray' to start the console." +fi + +exit 0 diff --git a/installer/macos/scripts/postinstall-headless b/installer/macos/scripts/postinstall-headless new file mode 100755 index 000000000..7ce435f59 --- /dev/null +++ b/installer/macos/scripts/postinstall-headless @@ -0,0 +1,134 @@ +#!/bin/bash +# Device Management Toolkit Console - Post-installation script (Headless Edition) +# Copyright (c) Intel Corporation +# SPDX-License-Identifier: Apache-2.0 + +set -e + +INSTALL_DIR="/usr/local/bin" +APP_DIR="/usr/local/device-management-toolkit" +CONFIG_DIR="$APP_DIR/config" +DATA_DIR="$APP_DIR/data" +CONFIG_FILE="$CONFIG_DIR/config.yml" + +# Get the console user (the user who initiated the install) +CONSOLE_USER=$(stat -f "%Su" /dev/console) + +# Create symlink in /usr/local/bin for easy CLI access +if [ -f "$APP_DIR/console" ]; then + ln -sf "$APP_DIR/console" "$INSTALL_DIR/dmt-console" + echo "Created symlink: $INSTALL_DIR/dmt-console -> $APP_DIR/console" +fi + +# Create data directory with proper permissions +if [ ! -d "$DATA_DIR" ]; then + mkdir -p "$DATA_DIR" +fi +chmod 777 "$DATA_DIR" + +# Create config directory +if [ ! -d "$CONFIG_DIR" ]; then + mkdir -p "$CONFIG_DIR" + chmod 755 "$CONFIG_DIR" +fi + +# Only create config if it doesn't exist (preserve existing config on upgrade) +if [ ! -f "$CONFIG_FILE" ]; then + # Generate config.yml with defaults + cat > "$CONFIG_FILE" << EOF +app: + name: console + repo: device-management-toolkit/console + version: VERSION_PLACEHOLDER + encryption_key: "" + allow_insecure_ciphers: false +http: + host: localhost + port: "8181" + ws_compression: false + tls: + enabled: true + certFile: "" + keyFile: "" + allowed_origins: + - "*" + allowed_headers: + - "*" +logger: + log_level: info +secrets: + address: http://localhost:8200 + token: "" +postgres: + pool_max: 2 + url: "" +ea: + url: http://localhost:8000 + username: "" + password: "" +auth: + disabled: false + adminUsername: standalone + adminPassword: G@ppm0ym + jwtKey: your_secret_jwt_key + jwtExpiration: 24h0m0s + redirectionJWTExpiration: 5m0s + clientId: "" + issuer: "" + ui: + clientId: "" + issuer: "" + scope: "" + redirectUri: "" + responseType: "code" + requireHttps: false + strictDiscoveryDocumentValidation: true +ui: + externalUrl: "" +EOF + + chmod 644 "$CONFIG_FILE" + chown "$CONSOLE_USER" "$CONFIG_FILE" 2>/dev/null || true + echo "Configuration saved to $CONFIG_FILE" +else + echo "Existing configuration preserved at $CONFIG_FILE" + # Ensure existing config is readable + chmod 644 "$CONFIG_FILE" 2>/dev/null || true +fi + +# Copy the configuration script for future use +if [ -f "$APP_DIR/configure.sh" ]; then + chmod +x "$APP_DIR/configure.sh" + ln -sf "$APP_DIR/configure.sh" "$INSTALL_DIR/dmt-configure" +fi + +# Copy the uninstall script +if [ -f "$APP_DIR/uninstall.sh" ]; then + chmod +x "$APP_DIR/uninstall.sh" + ln -sf "$APP_DIR/uninstall.sh" "$INSTALL_DIR/dmt-uninstall" +fi + +echo "" +echo "=======================================================" +echo "Device Management Toolkit Console (Headless) installed!" +echo "=======================================================" +echo "" +echo "To reconfigure:" +echo " sudo dmt-configure" +echo "" +echo "To uninstall:" +echo " sudo dmt-uninstall" +echo "" + +# Auto-launch the console with tray mode +if [ -n "$CONSOLE_USER" ] && [ "$CONSOLE_USER" != "root" ]; then + echo "Launching DMT Console for user: $CONSOLE_USER" + # Launch as the console user with tray mode + sudo -u "$CONSOLE_USER" nohup "$APP_DIR/console" --tray > /dev/null 2>&1 & + sleep 1 + echo "DMT Console is now running in the system tray." +else + echo "Please run 'dmt-console --tray' to start the console." +fi + +exit 0 diff --git a/installer/macos/scripts/postinstall-ui b/installer/macos/scripts/postinstall-ui new file mode 100755 index 000000000..ef7392778 --- /dev/null +++ b/installer/macos/scripts/postinstall-ui @@ -0,0 +1,134 @@ +#!/bin/bash +# Device Management Toolkit Console - Post-installation script (UI Edition) +# Copyright (c) Intel Corporation +# SPDX-License-Identifier: Apache-2.0 + +set -e + +INSTALL_DIR="/usr/local/bin" +APP_DIR="/usr/local/device-management-toolkit" +CONFIG_DIR="$APP_DIR/config" +DATA_DIR="$APP_DIR/data" +CONFIG_FILE="$CONFIG_DIR/config.yml" + +# Get the console user (the user who initiated the install) +CONSOLE_USER=$(stat -f "%Su" /dev/console) + +# Create symlink in /usr/local/bin for easy CLI access +if [ -f "$APP_DIR/console" ]; then + ln -sf "$APP_DIR/console" "$INSTALL_DIR/dmt-console" + echo "Created symlink: $INSTALL_DIR/dmt-console -> $APP_DIR/console" +fi + +# Create data directory with proper permissions +if [ ! -d "$DATA_DIR" ]; then + mkdir -p "$DATA_DIR" +fi +chmod 777 "$DATA_DIR" + +# Create config directory +if [ ! -d "$CONFIG_DIR" ]; then + mkdir -p "$CONFIG_DIR" + chmod 755 "$CONFIG_DIR" +fi + +# Only create config if it doesn't exist (preserve existing config on upgrade) +if [ ! -f "$CONFIG_FILE" ]; then + # Generate config.yml with defaults + cat > "$CONFIG_FILE" << EOF +app: + name: console + repo: device-management-toolkit/console + version: VERSION_PLACEHOLDER + encryption_key: "" + allow_insecure_ciphers: false +http: + host: localhost + port: "8181" + ws_compression: false + tls: + enabled: true + certFile: "" + keyFile: "" + allowed_origins: + - "*" + allowed_headers: + - "*" +logger: + log_level: info +secrets: + address: http://localhost:8200 + token: "" +postgres: + pool_max: 2 + url: "" +ea: + url: http://localhost:8000 + username: "" + password: "" +auth: + disabled: false + adminUsername: standalone + adminPassword: G@ppm0ym + jwtKey: your_secret_jwt_key + jwtExpiration: 24h0m0s + redirectionJWTExpiration: 5m0s + clientId: "" + issuer: "" + ui: + clientId: "" + issuer: "" + scope: "" + redirectUri: "" + responseType: "code" + requireHttps: false + strictDiscoveryDocumentValidation: true +ui: + externalUrl: "" +EOF + + chmod 644 "$CONFIG_FILE" + chown "$CONSOLE_USER" "$CONFIG_FILE" 2>/dev/null || true + echo "Configuration saved to $CONFIG_FILE" +else + echo "Existing configuration preserved at $CONFIG_FILE" + # Ensure existing config is readable + chmod 644 "$CONFIG_FILE" 2>/dev/null || true +fi + +# Copy the configuration script for future use +if [ -f "$APP_DIR/configure.sh" ]; then + chmod +x "$APP_DIR/configure.sh" + ln -sf "$APP_DIR/configure.sh" "$INSTALL_DIR/dmt-configure" +fi + +# Copy the uninstall script +if [ -f "$APP_DIR/uninstall.sh" ]; then + chmod +x "$APP_DIR/uninstall.sh" + ln -sf "$APP_DIR/uninstall.sh" "$INSTALL_DIR/dmt-uninstall" +fi + +echo "" +echo "==================================================" +echo "Device Management Toolkit Console (UI) installed!" +echo "==================================================" +echo "" +echo "To reconfigure:" +echo " sudo dmt-configure" +echo "" +echo "To uninstall:" +echo " sudo dmt-uninstall" +echo "" + +# Auto-launch the console with tray mode +if [ -n "$CONSOLE_USER" ] && [ "$CONSOLE_USER" != "root" ]; then + echo "Launching DMT Console for user: $CONSOLE_USER" + # Launch as the console user with tray mode + sudo -u "$CONSOLE_USER" nohup "$APP_DIR/console" --tray > /dev/null 2>&1 & + sleep 1 + echo "DMT Console is now running in the system tray." +else + echo "Please run 'dmt-console --tray' to start the console." +fi + +exit 0 diff --git a/installer/macos/scripts/preinstall b/installer/macos/scripts/preinstall new file mode 100755 index 000000000..2a46bf012 --- /dev/null +++ b/installer/macos/scripts/preinstall @@ -0,0 +1,21 @@ +#!/bin/bash +# Device Management Toolkit Console - Pre-installation script +# Copyright (c) Intel Corporation +# SPDX-License-Identifier: Apache-2.0 + +set -e + +# Stop the service if it's running as a LaunchDaemon +PLIST_PATH="/Library/LaunchDaemons/com.intel.dmt-console.plist" +if [ -f "$PLIST_PATH" ]; then + echo "Stopping existing DMT Console service..." + launchctl unload "$PLIST_PATH" 2>/dev/null || true +fi + +# Remove old symlink if it exists +if [ -L "/usr/local/bin/dmt-console" ]; then + rm -f "/usr/local/bin/dmt-console" +fi + +echo "Pre-installation cleanup completed." +exit 0 diff --git a/installer/macos/uninstall.sh b/installer/macos/uninstall.sh new file mode 100755 index 000000000..253f38098 --- /dev/null +++ b/installer/macos/uninstall.sh @@ -0,0 +1,96 @@ +#!/bin/bash +# Device Management Toolkit Console - Uninstall Script +# Copyright (c) Intel Corporation +# SPDX-License-Identifier: Apache-2.0 + +set -e + +APP_DIR="/usr/local/device-management-toolkit" +SYMLINKS=( + "/usr/local/bin/dmt-console" + "/usr/local/bin/dmt-configure" + "/usr/local/bin/dmt-uninstall" +) + +# Check for root privileges +if [ "$EUID" -ne 0 ]; then + echo "Please run as root: sudo dmt-uninstall" + exit 1 +fi + +echo "Device Management Toolkit Console Uninstaller" +echo "==============================================" +echo "" + +# Check if installed +if [ ! -d "$APP_DIR" ]; then + echo "DMT Console does not appear to be installed." + exit 0 +fi + +# Ask about data preservation +echo "Do you want to remove configuration and data files?" +echo " - Configuration: $APP_DIR/config/" +echo " - Data: $APP_DIR/data/" +echo "" +read -p "Remove all data? [y/N]: " REMOVE_DATA + +# Stop any running instances +echo "" +echo "Stopping any running instances..." +pkill -f "$APP_DIR/console" 2>/dev/null || true + +# Remove symlinks +echo "Removing symlinks..." +for link in "${SYMLINKS[@]}"; do + if [ -L "$link" ]; then + rm -f "$link" + echo " Removed: $link" + fi +done + +# Remove application files +echo "Removing application files..." +rm -f "$APP_DIR/console" +rm -f "$APP_DIR/console_full" +rm -f "$APP_DIR/console_headless" +rm -f "$APP_DIR/configure.sh" +rm -f "$APP_DIR/uninstall.sh" + +# Handle data removal +if [[ "$REMOVE_DATA" =~ ^[Yy]$ ]]; then + echo "Removing configuration and data..." + rm -rf "$APP_DIR/config" + rm -rf "$APP_DIR/data" + + # Remove entire directory if empty + rmdir "$APP_DIR" 2>/dev/null || true + + # Also remove user-specific data + USER_DATA_DIR="$HOME/Library/Application Support/device-management-toolkit" + if [ -d "$USER_DATA_DIR" ]; then + echo "Removing user data directory..." + rm -rf "$USER_DATA_DIR" + fi +else + echo "Keeping configuration and data files at: $APP_DIR/" +fi + +# Forget the package receipt (allows clean reinstall) +echo "Removing package receipt..." +pkgutil --forget com.intel.dmt-console 2>/dev/null || true + +echo "" +echo "==============================================" +echo "DMT Console has been uninstalled." +if [[ ! "$REMOVE_DATA" =~ ^[Yy]$ ]]; then + echo "" + echo "Configuration and data preserved at:" + echo " $APP_DIR/" + echo "" + echo "To completely remove, run:" + echo " sudo rm -rf $APP_DIR" +fi +echo "==============================================" + +exit 0 diff --git a/installer/windows/build-installers.sh b/installer/windows/build-installers.sh new file mode 100755 index 000000000..4aee74fbf --- /dev/null +++ b/installer/windows/build-installers.sh @@ -0,0 +1,56 @@ +#!/bin/bash +# Device Management Toolkit Console - Windows Installer Build Script +# Copyright (c) Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# +# Builds two NSIS installers: one for UI edition, one for headless. +# Requires: NSIS (makensis) installed and on PATH. + +set -e + +VERSION="${1:-0.0.0}" +ARCH="${2:-x64}" +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +PROJECT_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)" +NSI_FILE="$PROJECT_ROOT/installer/console.nsi" +OUTPUT_DIR="$PROJECT_ROOT/dist/windows" + +echo "Building Windows NSIS installers..." +echo "Version: $VERSION" +echo "Architecture: $ARCH" +echo "" + +mkdir -p "$OUTPUT_DIR" + +# Build binaries +echo "=== Building Binaries ===" + +echo "Building UI binary..." +UI_BINARY="$OUTPUT_DIR/console_windows_${ARCH}.exe" +CGO_ENABLED=1 GOOS=windows GOARCH=amd64 go build -tags=tray -ldflags "-s -w" -trimpath -o "$UI_BINARY" "$PROJECT_ROOT/cmd/app" +echo " Built: $UI_BINARY" + +echo "Building headless binary..." +HEADLESS_BINARY="$OUTPUT_DIR/console_windows_${ARCH}_headless.exe" +CGO_ENABLED=1 GOOS=windows GOARCH=amd64 go build -tags='tray noui' -ldflags "-s -w" -trimpath -o "$HEADLESS_BINARY" "$PROJECT_ROOT/cmd/app" +echo " Built: $HEADLESS_BINARY" + +echo "" + +# Build NSIS installers +echo "=== Building NSIS Installers ===" + +echo "Building UI installer..." +makensis -DVERSION="$VERSION" -DARCH="$ARCH" -DEDITION=ui -DBINARY="$UI_BINARY" "$NSI_FILE" +echo " Created: console_${VERSION}_windows_${ARCH}_setup.exe" + +echo "Building headless installer..." +makensis -DVERSION="$VERSION" -DARCH="$ARCH" -DEDITION=headless -DBINARY="$HEADLESS_BINARY" "$NSI_FILE" +echo " Created: console_${VERSION}_windows_${ARCH}_headless_setup.exe" + +echo "" +echo "=== Build Complete ===" +echo "" +echo "Installers created:" +echo " UI: console_${VERSION}_windows_${ARCH}_setup.exe" +echo " Headless: console_${VERSION}_windows_${ARCH}_headless_setup.exe" diff --git a/internal/controller/httpapi/ui.go b/internal/controller/httpapi/ui.go index cc7546e4f..5e4f2be84 100644 --- a/internal/controller/httpapi/ui.go +++ b/internal/controller/httpapi/ui.go @@ -35,36 +35,30 @@ func setupUIRoutes(handler *gin.Engine, l logger.Interface, cfg *config.Config) l.Fatal(err) } - handler.StaticFileFS("/", "./", http.FS(staticFiles)) // Serve static files from "/" route + // Serve index.html at root + handler.StaticFileFS("/", "./", http.FS(staticFiles)) + // main.js needs config injection, so it's handled specially modifiedMainJS := injectConfigToMainJS(l, cfg) handler.StaticFile("/main.js", modifiedMainJS) - handler.StaticFileFS("/polyfills.js", "./polyfills.js", http.FS(staticFiles)) - handler.StaticFileFS("/media/kJEhBvYX7BgnkSrUwT8OhrdQw4oELdPIeeII9v6oFsI.woff2", "./media/kJEhBvYX7BgnkSrUwT8OhrdQw4oELdPIeeII9v6oFsI.woff2", http.FS(staticFiles)) - handler.StaticFileFS("/runtime.js", "./runtime.js", http.FS(staticFiles)) - handler.StaticFileFS("/styles.css", "./styles.css", http.FS(staticFiles)) - handler.StaticFileFS("/vendor.js", "./vendor.js", http.FS(staticFiles)) - handler.StaticFileFS("/favicon.ico", "./favicon.ico", http.FS(staticFiles)) - handler.StaticFileFS("/assets/logo.png", "./assets/logo.png", http.FS(staticFiles)) - handler.StaticFileFS("/assets/monaco/min/vs/loader.js", "./assets/monaco/min/vs/loader.js", http.FS(staticFiles)) - handler.StaticFileFS("/assets/monaco/min/vs/editor/editor.main.js", "./assets/monaco/min/vs/editor/editor.main.js", http.FS(staticFiles)) - handler.StaticFileFS("/assets/monaco/min/vs/editor/editor.main.css", "./assets/monaco/min/vs/editor/editor.main.css", http.FS(staticFiles)) - handler.StaticFileFS("/assets/monaco/min/vs/editor/editor.main.nls.js", "./assets/monaco/min/vs/editor/editor.main.nls.js", http.FS(staticFiles)) - handler.StaticFileFS("/assets/monaco/min/vs/base/worker/workerMain.js", "./assets/monaco/min/vs/base/worker/workerMain.js", http.FS(staticFiles)) - handler.StaticFileFS("/assets/monaco/min/vs/base/common/worker/simpleWorker.nls.js", "./assets/monaco/min/vs/base/common/worker/simpleWorker.nls.js", http.FS(staticFiles)) - handler.StaticFileFS("/assets/monaco/min/vs/base/browser/ui/codicons/codicon/codicon.ttf", "./assets/monaco/min/vs/base/browser/ui/codicons/codicon/codicon.ttf", http.FS(staticFiles)) - handler.StaticFileFS("/assets/monaco/min/vs/basic-languages/xml/xml.js", "./assets/monaco/min/vs/basic-languages/xml/xml.js", http.FS(staticFiles)) - - langs := []string{"en", "fr", "de", "ar", "es", "fi", "he", "it", "ja", "nl", "ru", "sv"} - for _, lang := range langs { - relativePath := "/assets/i18n/" + lang + ".json" - filePath := "." + relativePath - handler.StaticFileFS(relativePath, filePath, http.FS(staticFiles)) - } - - // Setup default NoRoute handler for SPA + // Serve all other static files dynamically via NoRoute handler + // This handles chunk files, assets, and any other embedded files handler.NoRoute(func(c *gin.Context) { + path := strings.TrimPrefix(c.Request.URL.Path, "/") + if path == "" { + path = "." + } + + // Try to serve the actual file if it exists + if file, err := staticFiles.Open(path); err == nil { + file.Close() + c.FileFromFS(path, http.FS(staticFiles)) + + return + } + + // Fallback to index.html for SPA routing c.FileFromFS("./", http.FS(staticFiles)) }) } diff --git a/pkg/tray/icon.go b/pkg/tray/icon.go new file mode 100644 index 000000000..1d547e28d --- /dev/null +++ b/pkg/tray/icon.go @@ -0,0 +1,35 @@ +package tray + +// iconData contains a simple 22x22 PNG tray icon +// This is a blue square with "C" for Console - replace with proper branded icon +// Generated as a simple placeholder - works on all platforms. +var iconData = []byte{ + 0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a, 0x00, 0x00, 0x00, 0x0d, + 0x49, 0x48, 0x44, 0x52, 0x00, 0x00, 0x00, 0x16, 0x00, 0x00, 0x00, 0x16, + 0x08, 0x06, 0x00, 0x00, 0x00, 0xc4, 0xb4, 0x6c, 0x3b, 0x00, 0x00, 0x00, + 0x09, 0x70, 0x48, 0x59, 0x73, 0x00, 0x00, 0x0b, 0x13, 0x00, 0x00, 0x0b, + 0x13, 0x01, 0x00, 0x9a, 0x9c, 0x18, 0x00, 0x00, 0x01, 0x1d, 0x49, 0x44, + 0x41, 0x54, 0x38, 0xcb, 0xed, 0x94, 0xb1, 0x4a, 0xc4, 0x40, 0x10, 0x86, + 0xbf, 0xd9, 0xbd, 0x84, 0x5c, 0x0e, 0x0e, 0xae, 0xb0, 0xb0, 0xb2, 0xf5, + 0x0d, 0x7c, 0x00, 0x4b, 0x1b, 0x5b, 0xab, 0xf0, 0x01, 0x6c, 0x6c, 0xb4, + 0xf1, 0x01, 0x2c, 0x7c, 0x80, 0xb6, 0xb6, 0x96, 0x16, 0x5a, 0x59, 0x58, + 0x68, 0x61, 0x63, 0x6f, 0x21, 0x1c, 0x97, 0xcd, 0x66, 0x32, 0x16, 0x77, + 0x70, 0x89, 0xa0, 0x20, 0x58, 0x38, 0xf0, 0xc3, 0x30, 0x33, 0xcc, 0xfc, + 0x33, 0xf3, 0x0f, 0x1c, 0xe0, 0x60, 0x4b, 0x2d, 0x15, 0x50, 0x02, 0xdf, + 0x01, 0xf0, 0x62, 0xdf, 0x93, 0x39, 0x60, 0x0f, 0x78, 0xce, 0x3d, 0xbf, + 0x00, 0xba, 0x53, 0x9b, 0x6e, 0x00, 0xf7, 0xc0, 0x33, 0xb0, 0x06, 0x9c, + 0x02, 0x4b, 0x40, 0x0d, 0xb8, 0x02, 0x9e, 0x80, 0x35, 0xa0, 0x01, 0x2e, + 0x81, 0x17, 0xe0, 0x0a, 0x78, 0x04, 0xd6, 0x81, 0x2a, 0x70, 0x01, 0x3c, + 0x02, 0xab, 0x40, 0x13, 0x38, 0x07, 0x9e, 0x81, 0x55, 0xa0, 0x0d, 0x5c, + 0x00, 0xcf, 0xc0, 0x0a, 0xd0, 0x04, 0x4e, 0x81, 0x67, 0x60, 0x09, 0x68, + 0x00, 0x17, 0xc0, 0x53, 0xb0, 0x0a, 0x34, 0x81, 0x33, 0xe0, 0x29, 0x58, + 0x01, 0xda, 0xc0, 0x39, 0xf0, 0x0c, 0x2c, 0x03, 0x4d, 0xe0, 0x14, 0x78, + 0x0a, 0x96, 0x81, 0x06, 0x70, 0x0e, 0x3c, 0x03, 0xcb, 0x40, 0x0b, 0x38, + 0x05, 0x9e, 0x82, 0x25, 0xa0, 0x0e, 0x9c, 0x01, 0x4f, 0xc1, 0x32, 0xd0, + 0x00, 0xce, 0x81, 0xa7, 0x60, 0x05, 0xa8, 0x03, 0xa7, 0xc0, 0x13, 0xb0, + 0x04, 0xd4, 0x81, 0x33, 0xe0, 0x09, 0x58, 0x06, 0x1a, 0xc0, 0x19, 0xf0, + 0x14, 0xac, 0x00, 0x75, 0xe0, 0x0c, 0x78, 0x02, 0x96, 0x81, 0x3a, 0x70, + 0x06, 0x3c, 0x05, 0x2b, 0x40, 0x1d, 0x38, 0x07, 0xfe, 0xfb, 0x80, 0x1f, + 0xb0, 0xfc, 0x3e, 0x19, 0x71, 0x37, 0x6b, 0xd6, 0x00, 0x00, 0x00, 0x00, + 0x49, 0x45, 0x4e, 0x44, 0xae, 0x42, 0x60, 0x82, +} diff --git a/pkg/tray/tray.go b/pkg/tray/tray.go new file mode 100644 index 000000000..26989923a --- /dev/null +++ b/pkg/tray/tray.go @@ -0,0 +1,145 @@ +package tray + +import ( + "context" + "os/exec" + "runtime" + + "fyne.io/systray" +) + +// Config holds the configuration for the system tray. +type Config struct { + AppName string + URL string + Headless bool + OnQuit func() + OnReady func() +} + +// Manager handles the system tray lifecycle. +type Manager struct { + config Config +} + +// New creates a new tray manager. +func New(cfg Config) *Manager { + return &Manager{config: cfg} +} + +// Run starts the system tray - this blocks until quit. +func (m *Manager) Run() { + systray.Run(m.onReady, m.onExit) +} + +// Quit exits the system tray. +func (m *Manager) Quit() { + systray.Quit() +} + +func (m *Manager) onReady() { + systray.SetIcon(getIcon()) + systray.SetTitle(m.config.AppName) + systray.SetTooltip(m.config.AppName) + + if m.config.Headless { + m.onReadyHeadless() + } else { + m.onReadyFull() + } +} + +func (m *Manager) onReadyFull() { + // Menu items + mOpen := systray.AddMenuItem("Open "+m.config.AppName, "Open the web interface") + + systray.AddSeparator() + + mStatus := systray.AddMenuItem("Running on "+m.config.URL, "Server status") + mStatus.Disable() + systray.AddSeparator() + + mQuit := systray.AddMenuItem("Quit", "Stop the server and exit") + + // Call the onReady callback if provided + if m.config.OnReady != nil { + m.config.OnReady() + } + + // Handle menu clicks + go func() { + for { + select { + case <-mOpen.ClickedCh: + _ = openBrowser(m.config.URL) + case <-mQuit.ClickedCh: + if m.config.OnQuit != nil { + m.config.OnQuit() + } + + systray.Quit() + + return + } + } + }() +} + +func (m *Manager) onReadyHeadless() { + mMode := systray.AddMenuItem("Running in headless mode", "No web UI available") + mMode.Disable() + systray.AddSeparator() + + mStatus := systray.AddMenuItem("API on "+m.config.URL, "Server status") + mStatus.Disable() + systray.AddSeparator() + + mQuit := systray.AddMenuItem("Quit", "Stop the server and exit") + + // Call the onReady callback if provided + if m.config.OnReady != nil { + m.config.OnReady() + } + + // Handle menu clicks + go func() { + <-mQuit.ClickedCh + + if m.config.OnQuit != nil { + m.config.OnQuit() + } + + systray.Quit() + }() +} + +func (m *Manager) onExit() { + // Cleanup if needed +} + +// openBrowser opens the default browser to the given URL. +func openBrowser(url string) error { + var ( + cmd string + args []string + ) + + switch runtime.GOOS { + case "darwin": + cmd = "open" + args = []string{url} + case "windows": + cmd = "cmd" + args = []string{"/c", "start", url} + default: + cmd = "xdg-open" + args = []string{url} + } + + return exec.CommandContext(context.Background(), cmd, args...).Start() +} + +// getIcon returns the icon bytes for the system tray. +func getIcon() []byte { + return iconData +}