Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions native/.gitattributes
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# Go source is LF by convention (gofmt/goimports emit LF). Pin it so Windows
# checkouts don't introduce CRLF, which would trip gofmt in golangci-lint.
*.go text eol=lf
go.mod text eol=lf
go.sum text eol=lf
4 changes: 4 additions & 0 deletions native/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
# build outputs
*.exe
# runtime log written by rollback.exe/migration.exe
*.log
25 changes: 25 additions & 0 deletions native/.golangci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
# Lint config for the native upgrade executables (migration.exe / rollback.exe).
# Mirrors the mosip/esignet develop-go service config for consistency across the
# org's Go code; only the goimports local-prefix differs (our module path).
#
# Not required to build/test - go vet (in build.ps1) is the essential gate. This
# adds the stricter golangci-lint pass used in CI. Run locally with:
# golangci-lint run (https://golangci-lint.run)
version: "2"

linters:
default: standard
enable:
- errorlint
- gocritic
- misspell
- revive

formatters:
enable:
- gofmt
- goimports
settings:
goimports:
local-prefixes:
- github.com/mosip/registration-client/native
77 changes: 77 additions & 0 deletions native/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
# Native migration tooling (Go) — `migration.exe` / `rollback.exe`

Native Windows executables for the Java 11 → 21 JRE migration (the upgrade-revamp
feature). They perform the JRE swap **outside the JVM**, because the swap
deletes/replaces the very `jre/` a running JVM would hold open (Windows locks
in-use files). `_launcher.jar` starts them and exits the JVM first.

| Output | Role | Status |
|--------|------|--------|
| `rollback.exe` | Restore the pre-migration state after a failed/aborted migration | **Logic complete** (`cmd/rollback`, 4 tests) |
| `migration.exe` | Perform the Java 11 → 21 JRE swap | **Logic complete** (`cmd/migration`, 6 tests; auto-invokes `rollback.exe` on failure) |

## Why Go
- Single **static `.exe`, no runtime dependency** (the hard requirement — these run when the JRE is mid-swap/absent).
Comment thread
GOKULRAJ136 marked this conversation as resolved.
- File ops are stdlib (`os`, `path/filepath`); the operator dialog is a one-line `user32!MessageBoxW` syscall — no GUI toolkit.
- Cross-compiles to Windows from any OS; small, self-contained binary.
- (Comparison candidate vs GraalVM `native-image`, which keeps the language Java but adds the GraalVM + MSVC toolchain. Same logic either way.)

## Layout
Two thin `cmd/` entrypoints build the two exes; **both migration and rollback
logic live together in a single file**, `internal/upgrade/upgrade.go`.
```
native/
go.mod
cmd/
rollback/ rollback.exe - thin main(): dialog/exit/log -> upgrade.Rollback
main.go
migration/ migration.exe - thin main(): dialog/exit/log + invokeRollback -> upgrade.Migrate
main.go
internal/
upgrade/ Migrate(base) + Rollback(base) - BOTH flows in upgrade.go
upgrade.go
migrate_test.go / rollback_test.go / helpers_test.go (6 + 4 tests)
appenv/ resolve app root from the exe location (not CWD)
fsops/ idempotent move/remove/clean/restore (+ CopyFile, Unzip)
jre/ detect JRE feature version from <jre>/release
ui/ MessageBoxW (Windows) + stdout stub (dev)
```

## `migration.exe` behaviour (design step 4)
Idempotent/resumable. Against the app root:
1. extract `.artifacts/jre21.zip → jre21_temp/` (only if missing — done first so a failed extract never destroys `jre/`)
2. back up `jre/ → jre11/` (once, only when on JRE 11)
3. promote `jre21_temp/ → jre/` (delete old `jre/`, rename)
4. reset `lib/` to just `_launcher.jar` (new jars arrive from `.TEMP/` via `run.bat` on restart)
5. restore `run.bat` from `.artifacts/`
6. success dialog — **on any failure, auto-invoke `rollback.exe` and exit non-zero** (design Scenarios 1 & 5)

## `rollback.exe` behaviour (design step 4a)
Idempotent, safe to re-run. Against the app root:
1. `jre11/ → jre/` (skip if no backup — never deletes `jre/` without a replacement)
2. remove `jre21_temp/`
3. `run.bat_jre11 → run.bat` (skip if no backup)
4. empty `.TEMP/`, remove `.artifacts/` (re-downloaded on retry — design P8 / N12 / Scenario 5)
5. completion dialog
6. **never touches `./MANIFEST.MF`** (preserves the version mismatch for retry) — asserted via a before/after fingerprint

## Build + test (one step)
```powershell
./build.ps1 # go vet + go test, then build both .exe into ./dist
./build.ps1 -TestOnly # vet + test only
```

Or by hand:
```bash
go vet ./...
go test ./... # runs on any OS (ui has a non-Windows stub)

# build ON or FOR Windows; -H=windowsgui = no console window
go build -ldflags "-H=windowsgui" -o dist/rollback.exe ./cmd/rollback
go build -ldflags "-H=windowsgui" -o dist/migration.exe ./cmd/migration

# cross-compile from Linux/macOS:
GOOS=windows GOARCH=amd64 go build -ldflags "-H=windowsgui" -o dist/rollback.exe ./cmd/rollback
GOOS=windows GOARCH=amd64 go build -ldflags "-H=windowsgui" -o dist/migration.exe ./cmd/migration
```

50 changes: 50 additions & 0 deletions native/build.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
# Build + verify the T3/T4 native upgrade executables (migration.exe / rollback.exe).
#
# ./build.ps1 # vet + test, then build both .exe into ./dist
# ./build.ps1 -TestOnly # vet + test only (no .exe produced)
#
# Requires the Go toolchain on PATH (https://go.dev/dl/ or: winget install GoLang.Go).
# The .exe still needs Authenticode signing before release (see README.md).
[CmdletBinding()]
param(
[switch]$TestOnly
)

$ErrorActionPreference = 'Stop'
Set-Location -Path $PSScriptRoot

if (-not (Get-Command go -ErrorAction SilentlyContinue)) {
throw "Go toolchain not found on PATH. Install it (winget install GoLang.Go) and re-run."
}
Write-Host ("Using {0}" -f (go version))

Write-Host "`n== go vet ==" -ForegroundColor Cyan
go vet ./...
if ($LASTEXITCODE -ne 0) { throw "go vet failed" }

Write-Host "`n== go test ==" -ForegroundColor Cyan
go test ./...
if ($LASTEXITCODE -ne 0) { throw "go test failed" }

if ($TestOnly) {
Write-Host "`nTests passed (TestOnly: no binaries built)." -ForegroundColor Green
return
}

$dist = Join-Path $PSScriptRoot 'dist'
New-Item -ItemType Directory -Force -Path $dist | Out-Null

# -H=windowsgui => no console window pops up when run.bat / _launcher.jar launches it.
$ldflags = '-H=windowsgui'
$env:GOOS = 'windows'
$env:GOARCH = 'amd64'

foreach ($cmd in @('rollback', 'migration')) {
$out = Join-Path $dist "$cmd.exe"
Write-Host ("`n== build {0}.exe ==" -f $cmd) -ForegroundColor Cyan
go build -ldflags $ldflags -o $out "./cmd/$cmd"
if ($LASTEXITCODE -ne 0) { throw "build $cmd failed" }
Write-Host (" -> {0}" -f $out)
}

Write-Host "`nDone. Sign the binaries in ./dist before release (see README.md)." -ForegroundColor Green
77 changes: 77 additions & 0 deletions native/cmd/migration/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at https://mozilla.org/MPL/2.0/.

// Command migration is the native executable: migration.exe.
//
// Thin entrypoint over the shared upgrade logic. It performs the Java 11 -> 21
// JRE swap from outside the JVM (the swap deletes the very jre/ a JVM would hold
// open). It is launched by _launcher.jar (which then exits the JVM) and is
// idempotent/resumable. On failure it auto-invokes rollback.exe (T4).
//
// The migration + rollback logic both live in ../../internal/upgrade.
package main

import (
"fmt"
"log"
"os"
"os/exec"
"path/filepath"

"github.com/mosip/registration-client/native/internal/appenv"
"github.com/mosip/registration-client/native/internal/fsops"
"github.com/mosip/registration-client/native/internal/ui"
"github.com/mosip/registration-client/native/internal/upgrade"
)

const (
dialogTitle = "Registration Client - JRE Migration"
successMessage = "JRE migration complete. Please start the application using run.bat."
)

func main() { os.Exit(run()) }

// run performs the migration and returns the process exit code. Keeping os.Exit
// in main (not here) lets the deferred log-file close actually run.
func run() int {
base, err := appenv.AppRoot()
if err != nil {
ui.ShowError(dialogTitle, "Could not determine the application directory: "+err.Error())
return 1
}

if f, e := os.OpenFile(filepath.Join(base, "migration.log"),
os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0o644); e == nil {
defer func() { _ = f.Close() }()
log.SetOutput(f)
}
log.Printf("migration started, base=%s", base)

if err := upgrade.Migrate(base); err != nil {
log.Printf("migration FAILED: %v", err)
// Design step 4 / Scenarios 1 & 5: on failure, auto-invoke rollback.exe,
// which restores the previous state and shows its own dialog.
if rbErr := invokeRollback(base); rbErr != nil {
log.Printf("could not launch rollback.exe: %v", rbErr)
ui.ShowError(dialogTitle,
"Migration failed: "+err.Error()+"\n\nRollback could not be started: "+rbErr.Error())
}
return 1
}

log.Printf("migration completed successfully")
ui.ShowInfo(dialogTitle, successMessage)
return 0
}

// invokeRollback fire-and-forget launches rollback.exe from the app root.
func invokeRollback(base string) error {
rb := filepath.Join(base, "rollback.exe")
if !fsops.Exists(rb) {
return fmt.Errorf("rollback.exe not found at %s", rb)
}
log.Printf("auto-invoking rollback.exe")
return exec.Command(rb).Start()
}

62 changes: 62 additions & 0 deletions native/cmd/rollback/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at https://mozilla.org/MPL/2.0/.

// Command rollback is the native executable: rollback.exe.
//
// Thin entrypoint over the shared upgrade logic. It restores the Registration
// Client to its pre-migration state after a failed or aborted Java 11 -> 21
// migration. It is invoked by migration.exe on failure, by _launcher.jar on an
// inconsistent-JRE startup, or manually by an operator. It is fully idempotent
// and never touches ./MANIFEST.MF, so the version mismatch is preserved and the
// upgrade can be retried later.
//
// The migration + rollback logic both live in ../../internal/upgrade.
package main

import (
"log"
"os"
"path/filepath"

"github.com/mosip/registration-client/native/internal/appenv"
"github.com/mosip/registration-client/native/internal/ui"
"github.com/mosip/registration-client/native/internal/upgrade"
)

const (
dialogTitle = "Registration Client - Rollback"
successMessage = "Rollback complete. Application restored to previous state. Please start using run.bat."
)

func main() { os.Exit(run()) }

// run performs the rollback and returns the process exit code. Keeping os.Exit
// in main (not here) lets the deferred log-file close actually run.
func run() int {
base, err := appenv.AppRoot()
if err != nil {
ui.ShowError(dialogTitle, "Could not determine the application directory: "+err.Error())
return 1
}

// Best-effort file log in the app root (the windowsgui build has no console).
if f, e := os.OpenFile(filepath.Join(base, "rollback.log"),
os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0o644); e == nil {
defer func() { _ = f.Close() }()
log.SetOutput(f)
}
log.Printf("rollback started, base=%s", base)

if err := upgrade.Rollback(base); err != nil {
log.Printf("rollback FAILED: %v", err)
ui.ShowError(dialogTitle,
"Rollback failed: "+err.Error()+"\n\nYou can retry by running rollback.exe again.")
return 1
}

log.Printf("rollback completed successfully")
ui.ShowInfo(dialogTitle, successMessage)
return 0
}

3 changes: 3 additions & 0 deletions native/go.mod
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
module github.com/mosip/registration-client/native

go 1.21
29 changes: 29 additions & 0 deletions native/internal/appenv/appenv.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at https://mozilla.org/MPL/2.0/.

// Package appenv resolves the Registration Client application root directory.
//
// The migration/rollback executables live in the app root (next to run.bat,
// jre/, lib/, ./MANIFEST.MF). They may be launched by run.bat, by migration.exe,
// or manually by an operator, so the working directory is not reliable. We resolve
// the app root from the executable's own location instead.
package appenv

import (
"os"
"path/filepath"
)

// AppRoot returns the directory containing the running executable, with symlinks
// resolved. This is the Registration Client application root.
func AppRoot() (string, error) {
exe, err := os.Executable()
if err != nil {
return "", err
}
if resolved, err := filepath.EvalSymlinks(exe); err == nil {
exe = resolved
}
return filepath.Dir(exe), nil
}
Loading
Loading