Release 3.8.2.4: unify Windows/macOS GUI on glaze HTTP host.
Auto-bump build number on each compile; ship versioned Windows artifacts. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -6,8 +6,8 @@
|
||||
|
||||
| OS | Артефакты | UI |
|
||||
|----|----------|----|
|
||||
| Windows amd64 | `Navis.exe` | GUI (WebView2) |
|
||||
| macOS universal | `Navis.dmg` · `Navis.app.zip` | **GUI** (как Windows) |
|
||||
| Windows amd64 | `Navis.exe` | GUI (glaze → WebView2 + HTTP UI) |
|
||||
| macOS universal | `Navis.dmg` · `Navis.app.zip` | GUI (glaze → WKWebView + HTTP UI) |
|
||||
| macOS arm64 / amd64 | `Navis` · DMG · `Navis-cli` | GUI + CLI |
|
||||
| Android | `Navis.apk` | GUI (VpnService) |
|
||||
|
||||
@@ -192,10 +192,14 @@ cd ~/Navis
|
||||
|
||||
## Сборка Windows
|
||||
|
||||
GUI — тот же HTML UI, что и на macOS: локальный HTTP API + окно [glaze](https://github.com/crgimenes/glaze) (на Windows под капотом Edge WebView2 Runtime).
|
||||
|
||||
```bat
|
||||
build.bat
|
||||
```
|
||||
|
||||
Артефакты: `Navis-<version>.<build>.exe`, `vpnclient-<version>.<build>.exe` (и копии в `dist\navis-release\`).
|
||||
|
||||
или:
|
||||
|
||||
```bat
|
||||
|
||||
@@ -13,8 +13,8 @@ android {
|
||||
minSdk = 26
|
||||
targetSdk = 35
|
||||
// versionCode = major*1_000_000 + minor*10_000 + patch*100 + build
|
||||
versionCode = 3080203
|
||||
versionName = "3.8.2+3"
|
||||
versionCode = 3080204
|
||||
versionName = "3.8.2+4"
|
||||
vectorDrawables.useSupportLibrary = true
|
||||
ndk {
|
||||
abiFilters += listOf("arm64-v8a", "armeabi-v7a", "x86_64")
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 22 KiB After Width: | Height: | Size: 18 KiB |
+12
-4
@@ -1,7 +1,15 @@
|
||||
@echo off
|
||||
setlocal
|
||||
setlocal EnableExtensions
|
||||
cd /d "%~dp0"
|
||||
|
||||
echo Bumping build number...
|
||||
where py >nul 2>&1 && py -3 scripts\bump-build.py && goto :bumped
|
||||
where python >nul 2>&1 && python scripts\bump-build.py && goto :bumped
|
||||
where python3 >nul 2>&1 && python3 scripts\bump-build.py && goto :bumped
|
||||
powershell -NoProfile -ExecutionPolicy Bypass -File "%~dp0scripts\bump-build.ps1"
|
||||
if errorlevel 1 exit /b 1
|
||||
:bumped
|
||||
|
||||
mkdir "dist\navis-release\darwin-arm64" 2>nul
|
||||
mkdir "dist\navis-release\darwin-amd64" 2>nul
|
||||
mkdir "dist\navis-release\darwin-universal" 2>nul
|
||||
@@ -34,11 +42,11 @@ if errorlevel 1 exit /b 1
|
||||
go build -o "tools\packmac\packmac.exe" .\tools\packmac
|
||||
if errorlevel 1 exit /b 1
|
||||
|
||||
tools\packmac\packmac.exe -bin "dist\navis-release\darwin-arm64\Navis" -out "dist\navis-release\darwin-arm64" -version 3.8.2 -build 3.8.2.3 -arch arm64
|
||||
tools\packmac\packmac.exe -bin "dist\navis-release\darwin-arm64\Navis" -out "dist\navis-release\darwin-arm64" -version 3.8.2 -build 3.8.2.4 -arch arm64
|
||||
if errorlevel 1 exit /b 1
|
||||
tools\packmac\packmac.exe -bin "dist\navis-release\darwin-amd64\Navis" -out "dist\navis-release\darwin-amd64" -version 3.8.2 -build 3.8.2.3 -arch amd64
|
||||
tools\packmac\packmac.exe -bin "dist\navis-release\darwin-amd64\Navis" -out "dist\navis-release\darwin-amd64" -version 3.8.2 -build 3.8.2.4 -arch amd64
|
||||
if errorlevel 1 exit /b 1
|
||||
tools\packmac\packmac.exe -bin "dist\navis-release\darwin-universal\Navis" -out "dist\navis-release\darwin-universal" -version 3.8.2 -build 3.8.2.3 -arch universal
|
||||
tools\packmac\packmac.exe -bin "dist\navis-release\darwin-universal\Navis" -out "dist\navis-release\darwin-universal" -version 3.8.2 -build 3.8.2.4 -arch universal
|
||||
if errorlevel 1 exit /b 1
|
||||
|
||||
echo Built Mac GUI + CLI:
|
||||
|
||||
@@ -1,7 +1,24 @@
|
||||
@echo off
|
||||
setlocal
|
||||
setlocal EnableExtensions
|
||||
cd /d "%~dp0"
|
||||
|
||||
echo Bumping build number...
|
||||
call :bump_build
|
||||
if errorlevel 1 exit /b 1
|
||||
|
||||
REM Full version = CurrentVersion.BuildNumber from versioninfo.json (e.g. 3.8.2.4)
|
||||
set "VER="
|
||||
for /f "usebackq delims=" %%v in (`powershell -NoProfile -Command "(Get-Content -Raw 'versioninfo.json' | ConvertFrom-Json).StringFileInfo.FileVersion"`) do set "VER=%%v"
|
||||
if not defined VER (
|
||||
echo Failed to read FileVersion from versioninfo.json
|
||||
exit /b 1
|
||||
)
|
||||
|
||||
set "OUT_GUI=Navis-%VER%.exe"
|
||||
set "OUT_CLI=vpnclient-%VER%.exe"
|
||||
set "DIST=dist\navis-release"
|
||||
|
||||
echo Building Navis %VER% for Windows...
|
||||
echo Generating Windows icon resources...
|
||||
go run ./tools/mkico
|
||||
if errorlevel 1 exit /b 1
|
||||
@@ -14,16 +31,59 @@ if errorlevel 1 (
|
||||
goversioninfo -64 -icon assets\navis.ico -manifest assets\app.manifest -o cmd\vpnapp\resource.syso versioninfo.json
|
||||
if errorlevel 1 exit /b 1
|
||||
|
||||
echo Building Navis GUI and CLI...
|
||||
go build -ldflags="-H windowsgui -s -w" -trimpath -o Navis.exe ./cmd/vpnapp
|
||||
echo Building GUI -^> %OUT_GUI%
|
||||
go build -ldflags="-H windowsgui -s -w" -trimpath -o "%OUT_GUI%" ./cmd/vpnapp
|
||||
if errorlevel 1 exit /b 1
|
||||
|
||||
go build -ldflags="-s -w" -trimpath -o vpnclient.exe ./cmd/vpnclient
|
||||
echo Building CLI -^> %OUT_CLI%
|
||||
go build -ldflags="-s -w" -trimpath -o "%OUT_CLI%" ./cmd/vpnclient
|
||||
if errorlevel 1 exit /b 1
|
||||
|
||||
mkdir "%DIST%" 2>nul
|
||||
copy /Y "%OUT_GUI%" "%DIST%\%OUT_GUI%" >nul
|
||||
if errorlevel 1 exit /b 1
|
||||
copy /Y "%OUT_CLI%" "%DIST%\%OUT_CLI%" >nul
|
||||
if errorlevel 1 exit /b 1
|
||||
REM Stable names for the update feed / installers (best-effort if file is locked)
|
||||
call :copy_alias "%OUT_GUI%" "%DIST%\Navis.exe"
|
||||
call :copy_alias "%OUT_CLI%" "%DIST%\vpnclient.exe"
|
||||
|
||||
echo.
|
||||
echo Done:
|
||||
echo Navis.exe
|
||||
echo vpnclient.exe
|
||||
echo %OUT_GUI%
|
||||
echo %OUT_CLI%
|
||||
echo %DIST%\%OUT_GUI%
|
||||
echo %DIST%\%OUT_CLI%
|
||||
echo %DIST%\Navis.exe (update alias)
|
||||
echo assets\navis.ico
|
||||
endlocal
|
||||
exit /b 0
|
||||
|
||||
:bump_build
|
||||
where py >nul 2>&1
|
||||
if not errorlevel 1 (
|
||||
py -3 scripts\bump-build.py
|
||||
exit /b %ERRORLEVEL%
|
||||
)
|
||||
where python >nul 2>&1
|
||||
if not errorlevel 1 (
|
||||
python scripts\bump-build.py
|
||||
exit /b %ERRORLEVEL%
|
||||
)
|
||||
where python3 >nul 2>&1
|
||||
if not errorlevel 1 (
|
||||
python3 scripts\bump-build.py
|
||||
exit /b %ERRORLEVEL%
|
||||
)
|
||||
REM PowerShell fallback when Python is not installed
|
||||
powershell -NoProfile -ExecutionPolicy Bypass -File "%~dp0scripts\bump-build.ps1"
|
||||
exit /b %ERRORLEVEL%
|
||||
|
||||
:copy_alias
|
||||
if exist "%~2" del /F /Q "%~2" >nul 2>&1
|
||||
copy /Y "%~1" "%~2" >nul 2>&1
|
||||
if errorlevel 1 (
|
||||
echo Warning: could not refresh %~2 ^(file in use^). Versioned build is still ready.
|
||||
exit /b 0
|
||||
)
|
||||
exit /b 0
|
||||
|
||||
@@ -1,16 +1,13 @@
|
||||
//go:build darwin
|
||||
//go:build windows || darwin
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
"os/exec"
|
||||
"os/signal"
|
||||
"runtime"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"github.com/crgimenes/glaze"
|
||||
@@ -28,6 +25,7 @@ func main() {
|
||||
return
|
||||
}
|
||||
update.CleanupStaleDownloads()
|
||||
|
||||
log.SetFlags(log.LstdFlags | log.Lshortfile)
|
||||
log.Printf("Navis %s · GOMAXPROCS=%d NumCPU=%d", update.DisplayVersion(), runtime.GOMAXPROCS(0), runtime.NumCPU())
|
||||
|
||||
@@ -44,6 +42,7 @@ func main() {
|
||||
if err != nil {
|
||||
fatalf("Ошибка конфига %s: %v", cfgPath, err)
|
||||
}
|
||||
|
||||
logBuf := logbuf.New(0)
|
||||
mgr, err := core.NewManager(cfgPath, cfg, logBuf)
|
||||
if err != nil {
|
||||
@@ -51,10 +50,9 @@ func main() {
|
||||
}
|
||||
|
||||
a := apphost.New(mgr, cfgPath, logBuf)
|
||||
a.OpenURL = func(u string) error {
|
||||
return exec.Command("open", u).Start()
|
||||
}
|
||||
a.OpenURL = openExternal
|
||||
a.OnAfterUpdate = func() {
|
||||
// Hard-exit so the OS unlocks the binary for replacement.
|
||||
go func() {
|
||||
time.Sleep(300 * time.Millisecond)
|
||||
_ = a.Disconnect()
|
||||
@@ -71,6 +69,7 @@ func main() {
|
||||
go func() {
|
||||
_ = srv.Serve(ln)
|
||||
}()
|
||||
|
||||
a.StartBackground()
|
||||
trayhost.Start("Navis", trayhost.Hooks{
|
||||
Connect: func() { _ = a.Connect() },
|
||||
@@ -86,12 +85,13 @@ func main() {
|
||||
w, err := glaze.New(false)
|
||||
if err != nil || w == nil {
|
||||
log.Printf("webview unavailable (%v); opening default browser", err)
|
||||
if err2 := exec.Command("open", uiURL).Run(); err2 != nil {
|
||||
if err2 := openExternal(uiURL); err2 != nil {
|
||||
_ = srv.Close()
|
||||
fatalf("Не удалось открыть интерфейс:\n%v\n%v\n%s", err, err2, uiURL)
|
||||
}
|
||||
log.Printf("Navis UI: %s", uiURL)
|
||||
sig := make(chan os.Signal, 1)
|
||||
signal.Notify(sig, os.Interrupt, syscall.SIGTERM)
|
||||
signal.Notify(sig, shutdownSignals()...)
|
||||
<-sig
|
||||
_ = mgr.Disconnect()
|
||||
_ = srv.Close()
|
||||
@@ -105,14 +105,8 @@ func main() {
|
||||
|
||||
w.SetTitle("Navis")
|
||||
w.SetSize(500, 900, glaze.HintNone)
|
||||
decorateWindow(w)
|
||||
w.Navigate(uiURL)
|
||||
log.Printf("Navis UI: %s", uiURL)
|
||||
w.Run()
|
||||
}
|
||||
|
||||
func fatalf(format string, args ...any) {
|
||||
msg := fmt.Sprintf(format, args...)
|
||||
log.Println(msg)
|
||||
_ = exec.Command("osascript", "-e", fmt.Sprintf(`display alert "Navis" message %q`, msg)).Run()
|
||||
os.Exit(1)
|
||||
}
|
||||
@@ -8,7 +8,7 @@ import (
|
||||
)
|
||||
|
||||
func main() {
|
||||
fmt.Fprintln(os.Stderr, "Navis GUI supports Windows (WebView2) and macOS.")
|
||||
fmt.Fprintln(os.Stderr, "Navis GUI supports Windows and macOS (glaze + local HTTP UI).")
|
||||
fmt.Fprintln(os.Stderr, "On this OS use the CLI: ./Navis connect | install-core | check-update")
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
@@ -1,202 +0,0 @@
|
||||
//go:build windows
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"time"
|
||||
"unsafe"
|
||||
|
||||
"github.com/jchv/go-webview2"
|
||||
"golang.org/x/sys/windows"
|
||||
|
||||
"vpnclient/internal/apphost"
|
||||
"vpnclient/internal/appui"
|
||||
"vpnclient/internal/config"
|
||||
"vpnclient/internal/core"
|
||||
"vpnclient/internal/logbuf"
|
||||
"vpnclient/internal/trayhost"
|
||||
"vpnclient/internal/update"
|
||||
)
|
||||
|
||||
func main() {
|
||||
if update.MaybeFinishUpdate(os.Args[1:]) {
|
||||
return
|
||||
}
|
||||
update.CleanupStaleDownloads()
|
||||
|
||||
log.SetFlags(log.LstdFlags | log.Lshortfile)
|
||||
log.Printf("Navis %s · GOMAXPROCS=%d NumCPU=%d", update.DisplayVersion(), runtime.GOMAXPROCS(0), runtime.NumCPU())
|
||||
|
||||
cfgPath, err := config.LocateConfig()
|
||||
if err != nil {
|
||||
fatalDialog("Не удалось найти путь конфига: %v", err)
|
||||
}
|
||||
if _, err := os.Stat(cfgPath); os.IsNotExist(err) {
|
||||
if err := config.WriteExample(cfgPath); err != nil {
|
||||
fatalDialog("Не удалось создать конфиг: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
cfg, err := config.Load(cfgPath)
|
||||
if err != nil {
|
||||
fatalDialog("Ошибка конфига %s: %v", cfgPath, err)
|
||||
}
|
||||
|
||||
logBuf := logbuf.New(0)
|
||||
mgr, err := core.NewManager(cfgPath, cfg, logBuf)
|
||||
if err != nil {
|
||||
fatalDialog("%v", err)
|
||||
}
|
||||
|
||||
a := apphost.New(mgr, cfgPath, logBuf)
|
||||
a.OpenURL = shellOpen
|
||||
a.OnAfterUpdate = func() {
|
||||
// Hard-exit so Windows unlocks Navis.exe; do not wait on webview.Terminate.
|
||||
go func() {
|
||||
time.Sleep(300 * time.Millisecond)
|
||||
_ = a.Disconnect()
|
||||
os.Exit(0)
|
||||
}()
|
||||
}
|
||||
|
||||
dataPath := filepath.Join(filepath.Dir(cfgPath), ".navis-webview")
|
||||
_ = os.MkdirAll(dataPath, 0o755)
|
||||
|
||||
w := webview2.NewWithOptions(webview2.WebViewOptions{
|
||||
Debug: false,
|
||||
AutoFocus: true,
|
||||
DataPath: dataPath,
|
||||
WindowOptions: webview2.WindowOptions{
|
||||
Title: "Navis",
|
||||
Width: 500,
|
||||
Height: 900,
|
||||
Center: true,
|
||||
IconId: 1,
|
||||
},
|
||||
})
|
||||
if w == nil {
|
||||
fatalDialog("Не удалось создать окно WebView2.\nУстановите Microsoft Edge WebView2 Runtime.")
|
||||
}
|
||||
defer func() {
|
||||
_ = mgr.Disconnect()
|
||||
w.Destroy()
|
||||
}()
|
||||
|
||||
applyWindowIcon(w.Window())
|
||||
w.SetSize(500, 900, webview2.HintNone)
|
||||
|
||||
mustBind(w, "getState", a.GetState)
|
||||
mustBind(w, "getEditState", a.GetEditState)
|
||||
mustBind(w, "connect", a.Connect)
|
||||
mustBind(w, "disconnect", a.Disconnect)
|
||||
mustBind(w, "connectProfile", a.ConnectProfile)
|
||||
mustBind(w, "saveProfile", a.SaveProfile)
|
||||
mustBind(w, "createProfile", a.CreateProfile)
|
||||
mustBind(w, "selectProfile", a.SelectProfile)
|
||||
mustBind(w, "deleteProfile", a.DeleteProfile)
|
||||
mustBind(w, "installCore", a.InstallCore)
|
||||
mustBind(w, "openURL", a.OpenShopURL)
|
||||
mustBind(w, "pingServers", a.PingServers)
|
||||
mustBind(w, "pingBest", a.PingBest)
|
||||
mustBind(w, "checkUpdate", a.CheckUpdate)
|
||||
mustBind(w, "applyUpdate", a.ApplyUpdate)
|
||||
mustBind(w, "saveHy2", a.SaveHy2)
|
||||
mustBind(w, "importSubscription", a.ImportSubscription)
|
||||
mustBind(w, "getLogs", a.GetLogs)
|
||||
mustBind(w, "probeTunnel", a.ProbeTunnel)
|
||||
mustBind(w, "savePrefs", a.SavePrefs)
|
||||
|
||||
a.StartBackground()
|
||||
trayhost.Start("Navis", trayhost.Hooks{
|
||||
Connect: func() { _ = a.Connect() },
|
||||
Disconnect: func() { _ = a.Disconnect() },
|
||||
Quit: func() {
|
||||
_ = a.Disconnect()
|
||||
os.Exit(0)
|
||||
},
|
||||
IsUp: func() bool { return a.Mgr.Status().Connected },
|
||||
})
|
||||
|
||||
go a.AutoCheckUpdate()
|
||||
|
||||
w.SetHtml(appui.IndexHTML)
|
||||
w.Run()
|
||||
}
|
||||
|
||||
func mustBind(w webview2.WebView, name string, fn interface{}) {
|
||||
if err := w.Bind(name, fn); err != nil {
|
||||
fatalDialog("bind %s: %v", name, err)
|
||||
}
|
||||
}
|
||||
|
||||
func shellOpen(raw string) error {
|
||||
verb, err := windows.UTF16PtrFromString("open")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
file, err := windows.UTF16PtrFromString(raw)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return windows.ShellExecute(0, verb, file, nil, nil, windows.SW_SHOWNORMAL)
|
||||
}
|
||||
|
||||
func applyWindowIcon(hwnd unsafe.Pointer) {
|
||||
ico := findIconPath()
|
||||
if ico == "" || hwnd == nil {
|
||||
return
|
||||
}
|
||||
pathPtr, err := windows.UTF16PtrFromString(ico)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
user32 := windows.NewLazySystemDLL("user32.dll")
|
||||
loadImage := user32.NewProc("LoadImageW")
|
||||
sendMessage := user32.NewProc("SendMessageW")
|
||||
const (
|
||||
imageIcon = 1
|
||||
lrLoadFromFile = 0x0010
|
||||
lrDefaultSize = 0x0040
|
||||
wmSetIcon = 0x0080
|
||||
iconSmall = 0
|
||||
iconBig = 1
|
||||
)
|
||||
h, _, _ := loadImage.Call(0, uintptr(unsafe.Pointer(pathPtr)), imageIcon, 0, 0, lrLoadFromFile|lrDefaultSize)
|
||||
if h == 0 {
|
||||
return
|
||||
}
|
||||
sendMessage.Call(uintptr(hwnd), wmSetIcon, iconSmall, h)
|
||||
sendMessage.Call(uintptr(hwnd), wmSetIcon, iconBig, h)
|
||||
}
|
||||
|
||||
func findIconPath() string {
|
||||
candidates := []string{}
|
||||
if exe, err := os.Executable(); err == nil {
|
||||
dir := filepath.Dir(exe)
|
||||
candidates = append(candidates,
|
||||
filepath.Join(dir, "assets", "navis.ico"),
|
||||
filepath.Join(dir, "navis.ico"),
|
||||
)
|
||||
}
|
||||
if cwd, err := os.Getwd(); err == nil {
|
||||
candidates = append(candidates, filepath.Join(cwd, "assets", "navis.ico"))
|
||||
}
|
||||
for _, c := range candidates {
|
||||
if st, err := os.Stat(c); err == nil && !st.IsDir() {
|
||||
return c
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func fatalDialog(format string, args ...interface{}) {
|
||||
msg := fmt.Sprintf(format, args...)
|
||||
log.Println(msg)
|
||||
messageBox(msg)
|
||||
os.Exit(1)
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
//go:build darwin
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"os/exec"
|
||||
"syscall"
|
||||
|
||||
"github.com/crgimenes/glaze"
|
||||
)
|
||||
|
||||
func openExternal(raw string) error {
|
||||
return exec.Command("open", raw).Start()
|
||||
}
|
||||
|
||||
func fatalf(format string, args ...any) {
|
||||
msg := fmt.Sprintf(format, args...)
|
||||
log.Println(msg)
|
||||
_ = exec.Command("osascript", "-e", fmt.Sprintf(`display alert "Navis" message %q`, msg)).Run()
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
func shutdownSignals() []os.Signal {
|
||||
return []os.Signal{os.Interrupt, syscall.SIGTERM}
|
||||
}
|
||||
|
||||
func decorateWindow(w glaze.WebView) {
|
||||
// macOS Dock/app icon comes from the .app bundle; nothing to apply here.
|
||||
_ = w
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
//go:build windows
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"unsafe"
|
||||
|
||||
"github.com/crgimenes/glaze"
|
||||
"golang.org/x/sys/windows"
|
||||
)
|
||||
|
||||
func openExternal(raw string) error {
|
||||
verb, err := windows.UTF16PtrFromString("open")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
file, err := windows.UTF16PtrFromString(raw)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return windows.ShellExecute(0, verb, file, nil, nil, windows.SW_SHOWNORMAL)
|
||||
}
|
||||
|
||||
func fatalf(format string, args ...any) {
|
||||
msg := fmt.Sprintf(format, args...)
|
||||
log.Println(msg)
|
||||
messageBox(msg)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
func shutdownSignals() []os.Signal {
|
||||
return []os.Signal{os.Interrupt}
|
||||
}
|
||||
|
||||
func decorateWindow(w glaze.WebView) {
|
||||
if w == nil {
|
||||
return
|
||||
}
|
||||
applyWindowIcon(w.Window())
|
||||
}
|
||||
|
||||
func applyWindowIcon(hwnd unsafe.Pointer) {
|
||||
ico := findIconPath()
|
||||
if ico == "" || hwnd == nil {
|
||||
return
|
||||
}
|
||||
pathPtr, err := windows.UTF16PtrFromString(ico)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
user32 := windows.NewLazySystemDLL("user32.dll")
|
||||
loadImage := user32.NewProc("LoadImageW")
|
||||
sendMessage := user32.NewProc("SendMessageW")
|
||||
const (
|
||||
imageIcon = 1
|
||||
lrLoadFromFile = 0x0010
|
||||
lrDefaultSize = 0x0040
|
||||
wmSetIcon = 0x0080
|
||||
iconSmall = 0
|
||||
iconBig = 1
|
||||
)
|
||||
h, _, _ := loadImage.Call(0, uintptr(unsafe.Pointer(pathPtr)), imageIcon, 0, 0, lrLoadFromFile|lrDefaultSize)
|
||||
if h == 0 {
|
||||
return
|
||||
}
|
||||
sendMessage.Call(uintptr(hwnd), wmSetIcon, iconSmall, h)
|
||||
sendMessage.Call(uintptr(hwnd), wmSetIcon, iconBig, h)
|
||||
}
|
||||
|
||||
func findIconPath() string {
|
||||
candidates := []string{}
|
||||
if exe, err := os.Executable(); err == nil {
|
||||
dir := filepath.Dir(exe)
|
||||
candidates = append(candidates,
|
||||
filepath.Join(dir, "assets", "navis.ico"),
|
||||
filepath.Join(dir, "navis.ico"),
|
||||
)
|
||||
}
|
||||
if cwd, err := os.Getwd(); err == nil {
|
||||
candidates = append(candidates, filepath.Join(cwd, "assets", "navis.ico"))
|
||||
}
|
||||
for _, c := range candidates {
|
||||
if st, err := os.Stat(c); err == nil && !st.IsDir() {
|
||||
return c
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
Vendored
BIN
Binary file not shown.
Vendored
BIN
Binary file not shown.
Vendored
+2
-2
@@ -5,12 +5,12 @@
|
||||
"os": "windows",
|
||||
"arch": "amd64",
|
||||
"url": "https://git.evilfox.cc/test2/navi/raw/branch/main/dist/navis-release/Navis.exe",
|
||||
"sha256": "9a64a3b059c53eb818ff696d5f446c39f52e243508f92d05365dd06a05c57a1e",
|
||||
"sha256": "d3c1ff19f14de16768680770391b9d5565e13508890f48f0c19e087ada94e9d9",
|
||||
"mandatory": false,
|
||||
"platforms": {
|
||||
"windows-amd64": {
|
||||
"url": "https://git.evilfox.cc/test2/navi/raw/branch/main/dist/navis-release/Navis.exe",
|
||||
"sha256": "9a64a3b059c53eb818ff696d5f446c39f52e243508f92d05365dd06a05c57a1e",
|
||||
"sha256": "d3c1ff19f14de16768680770391b9d5565e13508890f48f0c19e087ada94e9d9",
|
||||
"os": "windows",
|
||||
"arch": "amd64"
|
||||
},
|
||||
|
||||
BIN
Binary file not shown.
Vendored
BIN
Binary file not shown.
Vendored
+2
-2
@@ -5,12 +5,12 @@
|
||||
"os": "windows",
|
||||
"arch": "amd64",
|
||||
"url": "https://git.evilfox.cc/test2/navi/raw/branch/main/dist/navis-release/Navis.exe",
|
||||
"sha256": "9a64a3b059c53eb818ff696d5f446c39f52e243508f92d05365dd06a05c57a1e",
|
||||
"sha256": "d3c1ff19f14de16768680770391b9d5565e13508890f48f0c19e087ada94e9d9",
|
||||
"mandatory": false,
|
||||
"platforms": {
|
||||
"windows-amd64": {
|
||||
"url": "https://git.evilfox.cc/test2/navi/raw/branch/main/dist/navis-release/Navis.exe",
|
||||
"sha256": "9a64a3b059c53eb818ff696d5f446c39f52e243508f92d05365dd06a05c57a1e",
|
||||
"sha256": "d3c1ff19f14de16768680770391b9d5565e13508890f48f0c19e087ada94e9d9",
|
||||
"os": "windows",
|
||||
"arch": "amd64"
|
||||
},
|
||||
|
||||
@@ -4,21 +4,19 @@ go 1.26.5
|
||||
|
||||
require (
|
||||
github.com/amnezia-vpn/amneziawg-go v0.2.19
|
||||
github.com/crgimenes/glaze v0.0.33
|
||||
github.com/diskfs/go-diskfs v1.9.4
|
||||
github.com/jchv/go-webview2 v0.0.0-20260205173254-56598839c808
|
||||
github.com/ebitengine/purego v0.10.1
|
||||
golang.org/x/image v0.44.0
|
||||
golang.org/x/sys v0.43.0
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/anchore/go-lzo v0.1.0 // indirect
|
||||
github.com/crgimenes/glaze v0.0.33 // indirect
|
||||
github.com/djherbis/times v1.6.0 // indirect
|
||||
github.com/ebitengine/purego v0.10.1 // indirect
|
||||
github.com/elliotwutingfeng/asciiset v0.0.0-20260129054604-cfde2086bc57 // indirect
|
||||
github.com/google/btree v1.1.3 // indirect
|
||||
github.com/google/uuid v1.6.0 // indirect
|
||||
github.com/jchv/go-winloader v0.0.0-20250406163304-c1995be93bd1 // indirect
|
||||
github.com/klauspost/compress v1.18.5 // indirect
|
||||
github.com/pierrec/lz4/v4 v4.1.26 // indirect
|
||||
github.com/pkg/xattr v0.4.12 // indirect
|
||||
|
||||
@@ -22,10 +22,6 @@ github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
||||
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/jchv/go-webview2 v0.0.0-20260205173254-56598839c808 h1:ftnsTqIUH57XQEF+PnXX9++nlHCzdkuB5zbWyMMruZo=
|
||||
github.com/jchv/go-webview2 v0.0.0-20260205173254-56598839c808/go.mod h1:rWifBlzkgrvd7zUqlfq91sWt3473OikgnglnIILx/Jo=
|
||||
github.com/jchv/go-winloader v0.0.0-20250406163304-c1995be93bd1 h1:njuLRcjAuMKr7kI3D85AXWkw6/+v9PwtV6M6o11sWHQ=
|
||||
github.com/jchv/go-winloader v0.0.0-20250406163304-c1995be93bd1/go.mod h1:alcuEEnZsY1WQsagKhZDsoPCRoOijYqhZvPwLG0kzVs=
|
||||
github.com/klauspost/compress v1.18.5 h1:/h1gH5Ce+VWNLSWqPzOVn6XBO+vJbCNGvjoaGBFW2IE=
|
||||
github.com/klauspost/compress v1.18.5/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ=
|
||||
github.com/pierrec/lz4/v4 v4.1.26 h1:GrpZw1gZttORinvzBdXPUXATeqlJjqUG/D87TKMnhjY=
|
||||
@@ -48,8 +44,6 @@ golang.org/x/image v0.44.0 h1:+tDekMZED9+LrtB3G5xzRggpVh9CARjZqROla3R3R+I=
|
||||
golang.org/x/image v0.44.0/go.mod h1:V8K3KE9KKKE+pLpQDOeN18w9oacNSvy1tDOirTu4xtY=
|
||||
golang.org/x/net v0.44.0 h1:evd8IRDyfNBMBTTY5XRF1vaZlD+EmWx6x8PkhR04H/I=
|
||||
golang.org/x/net v0.44.0/go.mod h1:ECOoLqd5U3Lhyeyo/QDCEVQ4sNgYsqvCZ722XogGieY=
|
||||
golang.org/x/sys v0.0.0-20200810151505-1b9f1253b3ed/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210218145245-beda7e5e158e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20220408201424-a24fb2fb8a0f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220615213510-4f61da869c0c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.43.0 h1:Rlag2XtaFTxp19wS8MXlJwTvoh8ArU6ezoyFsMyCTNI=
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
package apphost
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"vpnclient/internal/config"
|
||||
"vpnclient/internal/core"
|
||||
"vpnclient/internal/logbuf"
|
||||
)
|
||||
|
||||
func TestHTTPBridgeGetState(t *testing.T) {
|
||||
cfg := &config.Config{
|
||||
Active: "demo",
|
||||
Profiles: []config.Profile{
|
||||
{Name: "demo", Protocol: config.ProtocolNaive, Proxy: "https://u:p@example.com"},
|
||||
},
|
||||
}
|
||||
mgr, err := core.NewManager("", cfg, logbuf.New(0))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
a := New(mgr, "", logbuf.New(0))
|
||||
a.APIToken = "test-token"
|
||||
|
||||
body, _ := json.Marshal(map[string]any{"args": []any{}})
|
||||
req := httptest.NewRequest(http.MethodPost, "/api/getState", bytes.NewReader(body))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
req.Header.Set("X-Navis-Token", "test-token")
|
||||
rr := httptest.NewRecorder()
|
||||
a.Handler().ServeHTTP(rr, req)
|
||||
if rr.Code != http.StatusOK {
|
||||
t.Fatalf("status %d body %s", rr.Code, rr.Body.String())
|
||||
}
|
||||
var resp struct {
|
||||
Result UIState `json:"result"`
|
||||
Error string `json:"error"`
|
||||
}
|
||||
if err := json.Unmarshal(rr.Body.Bytes(), &resp); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if resp.Error != "" {
|
||||
t.Fatalf("api error: %s", resp.Error)
|
||||
}
|
||||
if resp.Result.ActiveProfile != "demo" {
|
||||
t.Fatalf("active_profile=%q", resp.Result.ActiveProfile)
|
||||
}
|
||||
}
|
||||
|
||||
func TestHTTPBridgeRejectsBadToken(t *testing.T) {
|
||||
cfg := &config.Config{}
|
||||
mgr, err := core.NewManager("", cfg, logbuf.New(0))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
a := New(mgr, "", logbuf.New(0))
|
||||
a.APIToken = "secret"
|
||||
req := httptest.NewRequest(http.MethodPost, "/api/getState", bytes.NewReader([]byte(`{"args":[]}`)))
|
||||
req.Header.Set("X-Navis-Token", "wrong")
|
||||
rr := httptest.NewRecorder()
|
||||
a.Handler().ServeHTTP(rr, req)
|
||||
if rr.Code != http.StatusUnauthorized {
|
||||
t.Fatalf("want 401, got %d", rr.Code)
|
||||
}
|
||||
}
|
||||
@@ -14,7 +14,7 @@ var appCSS string
|
||||
//go:embed app.js
|
||||
var appJS string
|
||||
|
||||
// IndexHTML is the full GUI document (CSS/JS inlined for WebView2 / glaze).
|
||||
// IndexHTML is the full GUI document (CSS/JS inlined for glaze HTTP UI).
|
||||
var IndexHTML = buildIndexHTML()
|
||||
|
||||
func buildIndexHTML() string {
|
||||
|
||||
@@ -22,7 +22,7 @@ const CurrentVersion = "3.8.2"
|
||||
|
||||
// BuildNumber is the monotonic build within CurrentVersion (Windows FileVersion 4th part,
|
||||
// macOS CFBundleVersion suffix, Android versionCode low digits). Bump on every release build.
|
||||
const BuildNumber = 3
|
||||
const BuildNumber = 4
|
||||
|
||||
// DefaultManifestURL is the update feed (hosted in the project git repo).
|
||||
const DefaultManifestURL = "https://git.evilfox.cc/test2/navi/raw/branch/main/dist/update.json"
|
||||
|
||||
@@ -6,7 +6,7 @@ cd "$(dirname "$0")/.."
|
||||
# Prefer local toolchain if present.
|
||||
export PATH="$(pwd)/.tools/go/bin:/tmp/go-sdk/go/bin:/usr/local/go/bin:$PATH"
|
||||
|
||||
python3 scripts/sync-version.py
|
||||
python3 scripts/bump-build.py
|
||||
|
||||
VERSION="$(python3 - <<'PY'
|
||||
import re
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
# Increment BuildNumber in update.go and sync version artifacts (no Python required).
|
||||
$ErrorActionPreference = "Stop"
|
||||
$Root = Split-Path -Parent $PSScriptRoot
|
||||
|
||||
$updateGo = Join-Path $Root "internal\update\update.go"
|
||||
$text = [IO.File]::ReadAllText($updateGo)
|
||||
$m = [regex]::Match($text, 'const BuildNumber\s*=\s*(\d+)')
|
||||
if (-not $m.Success) { throw "cannot find BuildNumber in update.go" }
|
||||
$old = [int]$m.Groups[1].Value
|
||||
$new = $old + 1
|
||||
$text2 = [regex]::Replace($text, 'const BuildNumber\s*=\s*\d+', "const BuildNumber = $new", 1)
|
||||
[IO.File]::WriteAllText($updateGo, $text2)
|
||||
Write-Host "BuildNumber: $old -> $new"
|
||||
|
||||
$verM = [regex]::Match($text2, 'CurrentVersion\s*=\s*"([^"]+)"')
|
||||
if (-not $verM.Success) { throw "cannot find CurrentVersion" }
|
||||
$ver = $verM.Groups[1].Value
|
||||
$parts = @($ver.Split('.') | ForEach-Object { [int]$_ })
|
||||
while ($parts.Count -lt 3) { $parts += 0 }
|
||||
$major, $minor, $patch = $parts[0], $parts[1], $parts[2]
|
||||
$full = "$ver.$new"
|
||||
$display = "$ver+$new"
|
||||
$versionCode = $major * 1000000 + $minor * 10000 + $patch * 100 + $new
|
||||
|
||||
$viPath = Join-Path $Root "versioninfo.json"
|
||||
$vi = [IO.File]::ReadAllText($viPath)
|
||||
$vi = [regex]::Replace($vi, '("FileVersion"\s*:\s*\{\s*"Major"\s*:\s*)\d+', "`${1}$major", 1)
|
||||
$vi = [regex]::Replace($vi, '("Minor"\s*:\s*)\d+(\s*,\s*"Patch")', "`${1}$minor`${2}", 1)
|
||||
$vi = [regex]::Replace($vi, '("Patch"\s*:\s*)\d+(\s*,\s*"Build")', "`${1}$patch`${2}", 1)
|
||||
$vi = [regex]::Replace($vi, '("Build"\s*:\s*)\d+', "`${1}$new", 2)
|
||||
$vi = [regex]::Replace($vi, '("FileVersion"\s*:\s*")\d+\.\d+\.\d+\.\d+(")', "`${1}$full`${2}")
|
||||
$vi = [regex]::Replace($vi, '("ProductVersion"\s*:\s*")\d+\.\d+\.\d+\.\d+(")', "`${1}$full`${2}")
|
||||
[IO.File]::WriteAllText($viPath, $vi)
|
||||
Write-Host "updated versioninfo.json -> $full"
|
||||
|
||||
$batPath = Join-Path $Root "build-macos.bat"
|
||||
$bat = [IO.File]::ReadAllText($batPath)
|
||||
$bat2 = [regex]::Replace(
|
||||
$bat,
|
||||
'-version\s+\d+\.\d+\.\d+\s+-build\s+\d+\.\d+\.\d+\.\d+',
|
||||
"-version $ver -build $full"
|
||||
)
|
||||
if ($bat2 -ne $bat) {
|
||||
[IO.File]::WriteAllText($batPath, $bat2)
|
||||
Write-Host "updated build-macos.bat -> $ver $full"
|
||||
}
|
||||
|
||||
$gradlePath = Join-Path $Root "android\app\build.gradle.kts"
|
||||
if (Test-Path $gradlePath) {
|
||||
$g = [IO.File]::ReadAllText($gradlePath)
|
||||
$g2 = [regex]::Replace($g, 'versionCode\s*=\s*[\d_]+', "versionCode = $versionCode")
|
||||
$g2 = [regex]::Replace($g2, 'versionName\s*=\s*"[^"]+"', "versionName = `"$display`"")
|
||||
if ($g2 -ne $g) {
|
||||
[IO.File]::WriteAllText($gradlePath, $g2)
|
||||
Write-Host "updated android gradle -> $display $versionCode"
|
||||
}
|
||||
}
|
||||
|
||||
Write-Host "bump complete: $full"
|
||||
@@ -0,0 +1,42 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Increment BuildNumber in update.go, then sync versioninfo / macOS bat / Android."""
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
import subprocess
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
UPDATE_GO = ROOT / "internal" / "update" / "update.go"
|
||||
|
||||
|
||||
def bump() -> int:
|
||||
text = UPDATE_GO.read_text(encoding="utf-8")
|
||||
m = re.search(r"const BuildNumber\s*=\s*(\d+)", text)
|
||||
if not m:
|
||||
raise SystemExit("cannot find BuildNumber in update.go")
|
||||
n = int(m.group(1)) + 1
|
||||
text2, count = re.subn(
|
||||
r"const BuildNumber\s*=\s*\d+",
|
||||
f"const BuildNumber = {n}",
|
||||
text,
|
||||
count=1,
|
||||
)
|
||||
if count != 1:
|
||||
raise SystemExit("failed to rewrite BuildNumber")
|
||||
UPDATE_GO.write_text(text2, encoding="utf-8")
|
||||
print(f"BuildNumber: {m.group(1)} -> {n}", flush=True)
|
||||
return n
|
||||
|
||||
|
||||
def main() -> None:
|
||||
bump()
|
||||
sync = ROOT / "scripts" / "sync-version.py"
|
||||
r = subprocess.run([sys.executable, str(sync)], cwd=ROOT, check=False)
|
||||
if r.returncode != 0:
|
||||
raise SystemExit(r.returncode)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
+2
-2
@@ -5,12 +5,12 @@
|
||||
"os": "windows",
|
||||
"arch": "amd64",
|
||||
"url": "https://git.evilfox.cc/test2/navi/raw/branch/main/dist/navis-release/Navis.exe",
|
||||
"sha256": "9a64a3b059c53eb818ff696d5f446c39f52e243508f92d05365dd06a05c57a1e",
|
||||
"sha256": "d3c1ff19f14de16768680770391b9d5565e13508890f48f0c19e087ada94e9d9",
|
||||
"mandatory": false,
|
||||
"platforms": {
|
||||
"windows-amd64": {
|
||||
"url": "https://git.evilfox.cc/test2/navi/raw/branch/main/dist/navis-release/Navis.exe",
|
||||
"sha256": "9a64a3b059c53eb818ff696d5f446c39f52e243508f92d05365dd06a05c57a1e",
|
||||
"sha256": "d3c1ff19f14de16768680770391b9d5565e13508890f48f0c19e087ada94e9d9",
|
||||
"os": "windows",
|
||||
"arch": "amd64"
|
||||
},
|
||||
|
||||
+4
-4
@@ -4,13 +4,13 @@
|
||||
"Major": 3,
|
||||
"Minor": 8,
|
||||
"Patch": 2,
|
||||
"Build": 3
|
||||
"Build": 4
|
||||
},
|
||||
"ProductVersion": {
|
||||
"Major": 3,
|
||||
"Minor": 8,
|
||||
"Patch": 2,
|
||||
"Build": 3
|
||||
"Build": 4
|
||||
},
|
||||
"FileFlagsMask": "3f",
|
||||
"FileFlags": "00",
|
||||
@@ -21,12 +21,12 @@
|
||||
"StringFileInfo": {
|
||||
"CompanyName": "EvilFox",
|
||||
"FileDescription": "Navis — VPN client (Naive / Hy2 / AWG / VLESS / VMess / Trojan)",
|
||||
"FileVersion": "3.8.2.3",
|
||||
"FileVersion": "3.8.2.4",
|
||||
"InternalName": "Navis",
|
||||
"LegalCopyright": "Copyright (c) EvilFox",
|
||||
"OriginalFilename": "Navis.exe",
|
||||
"ProductName": "Navis",
|
||||
"ProductVersion": "3.8.2.3",
|
||||
"ProductVersion": "3.8.2.4",
|
||||
"Comments": "Open-source VPN/proxy client. https://evilfox.win/"
|
||||
},
|
||||
"VarFileInfo": {
|
||||
|
||||
Reference in New Issue
Block a user