Desktop app for Windows and Linux
The Windows app cross-compiles (WebView2, no cgo) as a GUI program with an icon, version info and a DPI-aware manifest; run from a terminal with a command it borrows the console. The Linux app links GTK 3 and WebKitGTK 4.1. Off the Mac the app updates its own binary from its own build: downloaded in the background, swapped in on restart or quit, and the relaunched process waits for the old one before taking the gateway. The window leaves room for traffic lights only on a Mac.
This commit is contained in:
19 files changed
+286
-36
No files matched your search
@@ -3,3 +3,4 @@
|
||||
/magpie.app/
|
||||
/dist/
|
||||
/magpie-dev
|
||||
/rsrc_windows_*.syso
|
||||
@@ -9,17 +9,22 @@ ifeq ($(shell uname -s),Darwin)
|
||||
export CGO_CFLAGS = -mmacosx-version-min=11.0
|
||||
export CGO_LDFLAGS = -mmacosx-version-min=11.0
|
||||
endif
|
||||
# On Linux, GTK 3 and WebKitGTK 4.1: older distributions have them, where
|
||||
# Wails' default GTK 4 and WebKitGTK 6 would leave them out.
|
||||
ifeq ($(shell uname -s),Linux)
|
||||
TAGS += gtk3
|
||||
endif
|
||||
|
||||
.PHONY: build cli install test app icons release release-cli clean dev dev-once
|
||||
.PHONY: build cli install test app icons release release-cli release-windows release-linux clean dev dev-once
|
||||
|
||||
build:
|
||||
go build -tags $(TAGS) -trimpath -ldflags="$(LDFLAGS)" -o magpie .
|
||||
go build -tags "$(TAGS)" -trimpath -ldflags="$(LDFLAGS)" -o magpie .
|
||||
|
||||
cli:
|
||||
CGO_ENABLED=0 go build -tags nogui -trimpath -ldflags="$(LDFLAGS)" -o magpie .
|
||||
|
||||
install:
|
||||
go install -tags $(TAGS) -trimpath -ldflags="$(LDFLAGS)" .
|
||||
go install -tags "$(TAGS)" -trimpath -ldflags="$(LDFLAGS)" .
|
||||
|
||||
test:
|
||||
go vet ./... && go test ./...
|
||||
@@ -56,8 +61,28 @@ release-cli:
|
||||
CGO_ENABLED=0 GOOS=$$os GOARCH=$$arch go build -tags nogui -trimpath -ldflags="$(LDFLAGS)" -o dist/magpie-cli-$$os-$$arch$$ext . ; \
|
||||
done
|
||||
|
||||
# The desktop app for Windows: the system WebView2 needs no cgo, so both
|
||||
# arches cross-compile. Linked as a GUI program (no console window), with
|
||||
# the icon, version and a DPI-aware manifest from build/windows.
|
||||
WINVER = $(shell echo $(VERSION) | sed -E 's/^v//; s/[^0-9.].*//')
|
||||
release-windows:
|
||||
@mkdir -p dist
|
||||
@go run github.com/tc-hib/go-winres@v0.3.3 make --in build/windows/winres.json --arch amd64,arm64 --out rsrc \
|
||||
--file-version "$(or $(WINVER),0.0.0)" --product-version "$(or $(WINVER),0.0.0)"
|
||||
@for arch in amd64 arm64; do \
|
||||
echo " windows/$$arch (app)"; \
|
||||
CGO_ENABLED=0 GOOS=windows GOARCH=$$arch go build -tags production -trimpath -ldflags="$(LDFLAGS) -H windowsgui" -o dist/magpie-windows-$$arch.exe . || exit 1; \
|
||||
done; rm -f rsrc_windows_*.syso
|
||||
|
||||
# The desktop app for Linux links GTK 3 and WebKitGTK 4.1 through cgo, so it
|
||||
# is built natively, one arch per machine.
|
||||
release-linux:
|
||||
@mkdir -p dist
|
||||
@echo " linux/$(shell go env GOARCH) (app)"
|
||||
@go build -tags "$(TAGS)" -trimpath -ldflags="$(LDFLAGS)" -o dist/magpie-linux-$(shell go env GOARCH) .
|
||||
|
||||
clean:
|
||||
rm -rf magpie magpie.exe magpie.app dist
|
||||
rm -rf magpie magpie.exe magpie.app dist rsrc_windows_*.syso
|
||||
|
||||
# Development: the UI is served from internal/gui/assets and the window
|
||||
# reloads itself when a file there is saved; with fswatch installed, Go
|
||||
|
||||
@@ -25,7 +25,7 @@ and there is a terminal version (`magpie tui`) and a plain CLI.
|
||||
↑↓ agent · ←→ field · ↵ change · s save profile · p profiles · q quit
|
||||
```
|
||||
|
||||
- **One small binary.** About 11 MB with the desktop app (it uses the system
|
||||
- **One small binary.** Under 15 MB with the desktop app (it uses the system
|
||||
webview through [Wails](https://wails.io), nothing bundled), 7 MB for the
|
||||
terminal-only build. macOS, Linux and Windows.
|
||||
- **Edits config files surgically.** Only the one key you change is touched;
|
||||
@@ -179,14 +179,18 @@ the API key goes to `~/.gemini/.env`. Picking a catalog model points
|
||||
|
||||
## Install
|
||||
|
||||
Download the Mac app from [usemagpie.ai](https://usemagpie.ai), or from a
|
||||
terminal (macOS installs the app, Linux the command):
|
||||
Download the app for macOS, Windows or Linux from
|
||||
[usemagpie.ai](https://usemagpie.ai), or install it from a terminal (on
|
||||
Linux, the desktop app when WebKitGTK 4.1 is installed, the command
|
||||
otherwise):
|
||||
|
||||
```sh
|
||||
curl -fsSL https://usemagpie.ai/install.sh | sh
|
||||
```
|
||||
|
||||
Releases are signed and notarised, and keep themselves current: the app
|
||||
Mac releases are signed and notarised; the Windows and Linux builds are not
|
||||
signed yet (Windows SmartScreen may ask before the first run). Every build
|
||||
keeps itself current: the app
|
||||
downloads a new version in the background and installs it when you restart
|
||||
(*Restart to Update* in the menu) or quit; `magpie update` does the same from
|
||||
a terminal. Every release is on
|
||||
@@ -205,9 +209,12 @@ make build # ./magpie with the desktop app (needs cgo + the platform
|
||||
make app # macOS: magpie.app, a menu bar app with no Dock icon
|
||||
make cli # terminal-only build, no cgo, cross-compiles anywhere
|
||||
make release # dist/: native app build + cli builds for every platform
|
||||
make release-windows # dist/: the Windows app, amd64 and arm64 (cross-compiles)
|
||||
make release-linux # dist/: the Linux app for this machine's arch
|
||||
```
|
||||
|
||||
Linux needs `libgtk-3-dev` and `libwebkit2gtk-4.1-dev` for the app build;
|
||||
Linux needs `libgtk-3-dev` and `libwebkit2gtk-4.1-dev` for the app build
|
||||
(the Makefile adds the `gtk3` tag; with plain `go build`, pass `-tags gtk3`);
|
||||
Windows uses the WebView2 runtime that ships with the OS.
|
||||
|
||||
### Developing
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 3.2 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 459 B |
Binary file not shown.
|
After Width: | Height: | Size: 6.5 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 948 B |
Binary file not shown.
|
After Width: | Height: | Size: 1.4 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 1.9 KiB |
@@ -0,0 +1,45 @@
|
||||
{
|
||||
"RT_GROUP_ICON": {
|
||||
"APP": {
|
||||
"0000": ["icon-256.png", "icon-128.png", "icon-64.png", "icon-48.png", "icon-32.png", "icon-16.png"]
|
||||
}
|
||||
},
|
||||
"RT_MANIFEST": {
|
||||
"#1": {
|
||||
"0409": {
|
||||
"identity": { "name": "ai.usemagpie.magpie", "version": "" },
|
||||
"description": "magpie",
|
||||
"minimum-os": "win10",
|
||||
"execution-level": "as invoker",
|
||||
"ui-access": false,
|
||||
"auto-elevate": false,
|
||||
"dpi-awareness": "per monitor v2",
|
||||
"disable-theming": false,
|
||||
"disable-window-filtering": false,
|
||||
"high-resolution-scrolling-aware": false,
|
||||
"ultra-high-resolution-scrolling-aware": false,
|
||||
"long-path-aware": true,
|
||||
"printer-driver-isolation": false,
|
||||
"gdi-scaling": false,
|
||||
"segment-heap": false,
|
||||
"use-common-controls-v6": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"RT_VERSION": {
|
||||
"#1": {
|
||||
"0000": {
|
||||
"fixed": { "file_version": "0.0.0.0", "product_version": "0.0.0.0" },
|
||||
"info": {
|
||||
"0409": {
|
||||
"CompanyName": "magpie",
|
||||
"FileDescription": "magpie",
|
||||
"LegalCopyright": "© 2026 yetone · MIT",
|
||||
"OriginalFilename": "magpie.exe",
|
||||
"ProductName": "magpie"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"os"
|
||||
"syscall"
|
||||
)
|
||||
|
||||
// The desktop build is linked as a GUI program so double-clicking it opens
|
||||
// no console window; such a program starts with no standard streams. When it
|
||||
// is run from a terminal with a command, borrow that terminal's console so
|
||||
// `magpie ls` and friends still print. Output that is redirected already has
|
||||
// somewhere to go and is left alone.
|
||||
func init() {
|
||||
if len(os.Args) < 2 {
|
||||
return
|
||||
}
|
||||
if t, err := syscall.GetFileType(syscall.Stdout); err == nil && t != 0 {
|
||||
return // FILE_TYPE_UNKNOWN is 0: nothing attached
|
||||
}
|
||||
const attachParentProcess = ^uintptr(0) // (DWORD)-1
|
||||
r, _, _ := syscall.NewLazyDLL("kernel32.dll").NewProc("AttachConsole").Call(attachParentProcess)
|
||||
if r == 0 {
|
||||
return
|
||||
}
|
||||
if out, err := os.OpenFile("CONOUT$", os.O_RDWR, 0); err == nil {
|
||||
os.Stdout, os.Stderr = out, out
|
||||
}
|
||||
if in, err := os.OpenFile("CONIN$", os.O_RDWR, 0); err == nil {
|
||||
os.Stdin = in
|
||||
}
|
||||
}
|
||||
@@ -14,6 +14,7 @@ import (
|
||||
"github.com/yetone/magpie/internal/catalog"
|
||||
"github.com/yetone/magpie/internal/gateway"
|
||||
"github.com/yetone/magpie/internal/provider"
|
||||
"github.com/yetone/magpie/internal/update"
|
||||
)
|
||||
|
||||
//go:embed tray.png
|
||||
@@ -64,6 +65,9 @@ func (h *host) FitPanel(height int) {
|
||||
// panel, plus a regular window for when you want it to stay around.
|
||||
// showMain opens the window immediately; otherwise only the tray icon appears.
|
||||
func Run(version string, showMain bool) error {
|
||||
// After an update off the Mac, the old process starts this one and then
|
||||
// quits; let it go before looking for the gateway.
|
||||
update.AwaitPredecessor()
|
||||
// The gateway runs inside the app. If another magpie already has the
|
||||
// port, that one serves and this one only shows its status.
|
||||
var gw *gateway.Server
|
||||
|
||||
@@ -109,8 +109,8 @@ code { font: 11.5px var(--code); }
|
||||
|
||||
/* ---- header --------------------------------------------------------------- */
|
||||
.top { display: flex; align-items: center; gap: 8px; padding: 0 10px 0 14px; height: 46px; flex: none; position: relative; }
|
||||
body.window .top { padding-left: 88px; height: 50px; padding-top: 2px; } /* the traffic lights centre 25.75pt from the top, not 25 */
|
||||
body.window .seg { top: calc(50% + 1px); } /* 50% centres in the padding box; the 2px top padding puts the flex row a pixel lower */
|
||||
body.window.mac .top { padding-left: 88px; height: 50px; padding-top: 2px; } /* the traffic lights centre 25.75pt from the top, not 25 */
|
||||
body.window.mac .seg { top: calc(50% + 1px); } /* 50% centres in the padding box; the 2px top padding puts the flex row a pixel lower */
|
||||
.brand { display: flex; align-items: center; gap: 6px; font-weight: 600; letter-spacing: .01em; }
|
||||
.logo { color: var(--fg); display: inline-flex; } /* ink, like the wordmark; the accent blue fought the text in light mode */
|
||||
.seg {
|
||||
@@ -688,7 +688,8 @@ html.theming, html.theming *, html.theming *::before, html.theming *::after {
|
||||
.modal .dialog::-webkit-scrollbar { width: 0; }
|
||||
/* Keep native window chrome outside the scrim and dialog. In particular, the
|
||||
macOS traffic lights must never sit on top of modal content. */
|
||||
body.window .modal { top: 50px; padding: 16px 24px 24px; }
|
||||
body.window.mac .modal { top: 50px; padding: 16px 24px 24px; }
|
||||
body.window .modal { padding: 16px 24px 24px; }
|
||||
body.panel .modal { padding: 16px 12px; }
|
||||
@keyframes fade-in { from { opacity: 0; } }
|
||||
@keyframes fade-out { to { opacity: 0; } }
|
||||
|
||||
@@ -4,6 +4,8 @@ const $$ = (s) => document.querySelectorAll(s);
|
||||
const params = new URLSearchParams(location.search);
|
||||
const mode = params.get("mode") || "window";
|
||||
document.body.classList.add(mode);
|
||||
// Only the Mac window draws its title bar inside the page (the traffic lights).
|
||||
if (/^Mac/.test(navigator.platform)) document.body.classList.add("mac");
|
||||
if (params.get("theme")) document.documentElement.dataset.theme = params.get("theme");
|
||||
|
||||
let state = { agents: [], profiles: [], catalog: "", settings: {} };
|
||||
|
||||
+33
-8
@@ -4,7 +4,9 @@ import (
|
||||
"context"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
@@ -12,9 +14,9 @@ import (
|
||||
)
|
||||
|
||||
// updater keeps the app current. It asks the feed at start-up and every six
|
||||
// hours; when a newer release is out and the app may replace its own
|
||||
// bundle, the new one is downloaded and unpacked straight away, so all that
|
||||
// is left is a restart. Quitting installs it too, and the next launch is
|
||||
// hours; when a newer release is out and the app may replace itself — the
|
||||
// bundle on a Mac, the binary elsewhere — the new one is downloaded
|
||||
// straight away, so all that is left is a restart. Quitting installs it too, and the next launch is
|
||||
// the new version.
|
||||
type updater struct {
|
||||
mu sync.Mutex
|
||||
@@ -22,6 +24,7 @@ type updater struct {
|
||||
latest *update.Release
|
||||
err string
|
||||
bundle string // the .app to replace, "" when not in one or not writable
|
||||
exe string // off the Mac: the binary to replace, "" when not writable
|
||||
staged string
|
||||
onReady func(version string)
|
||||
}
|
||||
@@ -42,6 +45,11 @@ const updateEvery = 6 * time.Hour
|
||||
func (u *updater) start() {
|
||||
if b := update.Bundle(); b != "" && update.Writable(filepath.Dir(b)) {
|
||||
u.bundle = b
|
||||
} else if runtime.GOOS != "darwin" {
|
||||
if exe, err := update.Executable(); err == nil && update.Writable(filepath.Dir(exe)) {
|
||||
u.exe = exe
|
||||
os.Remove(exe + ".old") // what the last update on Windows moved aside
|
||||
}
|
||||
}
|
||||
go func() {
|
||||
time.Sleep(5 * time.Second) // let the app settle first
|
||||
@@ -79,13 +87,18 @@ func (u *updater) check() {
|
||||
case !update.Newer(rel.Version, Version):
|
||||
u.state = "latest"
|
||||
return
|
||||
case u.bundle == "":
|
||||
case u.bundle == "" && u.exe == "":
|
||||
u.state = "available" // the user fetches it from the release page
|
||||
return
|
||||
}
|
||||
u.state = "downloading"
|
||||
u.mu.Unlock()
|
||||
staged, err := update.Stage(ctx, rel, u.bundle)
|
||||
var staged string
|
||||
if u.bundle != "" {
|
||||
staged, err = update.Stage(ctx, rel, u.bundle)
|
||||
} else {
|
||||
staged, err = update.StageBinary(ctx, rel)
|
||||
}
|
||||
u.mu.Lock()
|
||||
if err != nil {
|
||||
log.Println("update:", err)
|
||||
@@ -105,7 +118,13 @@ func (u *updater) install() bool {
|
||||
if u.staged == "" {
|
||||
return false
|
||||
}
|
||||
if err := update.Install(u.staged, u.bundle); err != nil {
|
||||
var err error
|
||||
if u.bundle != "" {
|
||||
err = update.Install(u.staged, u.bundle)
|
||||
} else {
|
||||
err = update.InstallBinary(u.staged)
|
||||
}
|
||||
if err != nil {
|
||||
log.Println("update:", err)
|
||||
u.state, u.err = "error", err.Error()
|
||||
return false
|
||||
@@ -151,11 +170,17 @@ func updateRoutes(mux *http.ServeMux, w Windows) {
|
||||
// restartToUpdate installs the staged version and arranges for it to open
|
||||
// once this process is gone; the caller then quits.
|
||||
func restartToUpdate() bool {
|
||||
bundle := updates.bundle
|
||||
bundle, exe := updates.bundle, updates.exe
|
||||
if !updates.install() {
|
||||
return false
|
||||
}
|
||||
if err := update.Relaunch(bundle); err != nil {
|
||||
var err error
|
||||
if bundle != "" {
|
||||
err = update.Relaunch(bundle)
|
||||
} else {
|
||||
err = update.RelaunchBinary(exe)
|
||||
}
|
||||
if err != nil {
|
||||
log.Println("update:", err)
|
||||
}
|
||||
return true
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
//go:build !windows
|
||||
|
||||
package update
|
||||
|
||||
import (
|
||||
"os"
|
||||
"os/exec"
|
||||
"syscall"
|
||||
)
|
||||
|
||||
// alive reports whether pid is still running. A relaunched magpie is that
|
||||
// process's child, so its exit shows as a new parent even before the
|
||||
// process table lets go of it.
|
||||
func alive(pid int) bool {
|
||||
if os.Getppid() != pid {
|
||||
return syscall.Kill(pid, 0) == nil
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// detach keeps a relaunched magpie out of this one's process group, so it
|
||||
// outlives it.
|
||||
func detach(cmd *exec.Cmd) { cmd.SysProcAttr = &syscall.SysProcAttr{Setsid: true} }
|
||||
@@ -0,0 +1,30 @@
|
||||
package update
|
||||
|
||||
import (
|
||||
"os/exec"
|
||||
"syscall"
|
||||
)
|
||||
|
||||
const (
|
||||
synchronize = 0x00100000
|
||||
waitTimeout = 0x00000102
|
||||
detachedProc = 0x00000008
|
||||
newProcGroup = 0x00000200
|
||||
)
|
||||
|
||||
// alive reports whether pid is still running.
|
||||
func alive(pid int) bool {
|
||||
h, err := syscall.OpenProcess(synchronize, false, uint32(pid))
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
defer syscall.CloseHandle(h)
|
||||
ev, _ := syscall.WaitForSingleObject(h, 0)
|
||||
return ev == waitTimeout
|
||||
}
|
||||
|
||||
// detach starts a relaunched magpie outside this one's console, so it
|
||||
// outlives it.
|
||||
func detach(cmd *exec.Cmd) {
|
||||
cmd.SysProcAttr = &syscall.SysProcAttr{CreationFlags: detachedProc | newProcGroup}
|
||||
}
|
||||
+70
-16
@@ -4,8 +4,9 @@
|
||||
//
|
||||
// The macOS app replaces its own bundle: the new zip is downloaded, checked
|
||||
// against its hash, unpacked, and accepted only if it is signed by the same
|
||||
// team as the app already installed. A bare binary (the terminal build)
|
||||
// replaces itself the same way, minus the signature.
|
||||
// team as the app already installed. A bare binary (the terminal build,
|
||||
// and the desktop app on Windows and Linux) replaces itself the same way,
|
||||
// minus the signature.
|
||||
package update
|
||||
|
||||
import (
|
||||
@@ -138,10 +139,18 @@ func parse(v string) *semver {
|
||||
return &s
|
||||
}
|
||||
|
||||
// GUI says this binary has the desktop app in it. Off the Mac that app is a
|
||||
// single binary too, and it updates from its own build, not the terminal
|
||||
// one.
|
||||
var GUI bool
|
||||
|
||||
// AppAsset and BinaryAsset name the files this machine would install.
|
||||
func AppAsset() string { return "magpie-darwin-" + runtime.GOARCH + ".zip" }
|
||||
func BinaryAsset() string {
|
||||
name := "magpie-cli-" + runtime.GOOS + "-" + runtime.GOARCH
|
||||
if GUI && runtime.GOOS != "darwin" {
|
||||
name = "magpie-" + runtime.GOOS + "-" + runtime.GOARCH
|
||||
}
|
||||
if runtime.GOOS == "windows" {
|
||||
name += ".exe"
|
||||
}
|
||||
@@ -154,7 +163,7 @@ func Bundle() string {
|
||||
if runtime.GOOS != "darwin" {
|
||||
return ""
|
||||
}
|
||||
exe, err := executable()
|
||||
exe, err := Executable()
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
@@ -166,7 +175,8 @@ func Bundle() string {
|
||||
return app
|
||||
}
|
||||
|
||||
func executable() (string, error) {
|
||||
// Executable is the running binary, symlinks resolved.
|
||||
func Executable() (string, error) {
|
||||
exe, err := os.Executable()
|
||||
if err != nil {
|
||||
return "", err
|
||||
@@ -264,40 +274,84 @@ func Relaunch(bundle string) error {
|
||||
return cmd.Start()
|
||||
}
|
||||
|
||||
// ReplaceBinary puts the release's terminal build where the running binary
|
||||
// is.
|
||||
// ReplaceBinary puts the release's build for this binary where the
|
||||
// running one is.
|
||||
func ReplaceBinary(ctx context.Context, rel *Release) error {
|
||||
a, ok := rel.Assets[BinaryAsset()]
|
||||
if !ok {
|
||||
return fmt.Errorf("release %s has no %s", rel.Version, BinaryAsset())
|
||||
}
|
||||
exe, err := executable()
|
||||
staged, err := StageBinary(ctx, rel)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return InstallBinary(staged)
|
||||
}
|
||||
|
||||
// StageBinary downloads the release's build for this binary next to it,
|
||||
// ready for InstallBinary.
|
||||
func StageBinary(ctx context.Context, rel *Release) (string, error) {
|
||||
a, ok := rel.Assets[BinaryAsset()]
|
||||
if !ok {
|
||||
return "", fmt.Errorf("release %s has no %s", rel.Version, BinaryAsset())
|
||||
}
|
||||
exe, err := Executable()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
tmp := exe + ".new"
|
||||
if err := download(ctx, a, tmp); err != nil {
|
||||
return err
|
||||
return "", err
|
||||
}
|
||||
if err := os.Chmod(tmp, 0o755); err != nil {
|
||||
os.Remove(tmp)
|
||||
return err
|
||||
return "", err
|
||||
}
|
||||
return tmp, nil
|
||||
}
|
||||
|
||||
// InstallBinary swaps a staged binary in for the running one, which keeps
|
||||
// going until it exits.
|
||||
func InstallBinary(staged string) error {
|
||||
exe := strings.TrimSuffix(staged, ".new")
|
||||
if runtime.GOOS == "windows" {
|
||||
// A running .exe cannot be overwritten, but it can be moved aside.
|
||||
os.Remove(exe + ".old")
|
||||
if err := os.Rename(exe, exe+".old"); err != nil {
|
||||
os.Remove(tmp)
|
||||
os.Remove(staged)
|
||||
return err
|
||||
}
|
||||
}
|
||||
if err := os.Rename(tmp, exe); err != nil {
|
||||
os.Remove(tmp)
|
||||
if err := os.Rename(staged, exe); err != nil {
|
||||
os.Remove(staged)
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// RelaunchBinary starts exe again as the tray app. The new process waits
|
||||
// for this one to exit before it takes the gateway's port; see
|
||||
// AwaitPredecessor.
|
||||
func RelaunchBinary(exe string) error {
|
||||
cmd := exec.Command(exe, "tray")
|
||||
cmd.Env = append(os.Environ(), "MAGPIE_REPLACES="+strconv.Itoa(os.Getpid()))
|
||||
cmd.Stdin, cmd.Stdout, cmd.Stderr = nil, nil, nil
|
||||
detach(cmd)
|
||||
if err := cmd.Start(); err != nil {
|
||||
return err
|
||||
}
|
||||
return cmd.Process.Release()
|
||||
}
|
||||
|
||||
// AwaitPredecessor blocks, for a while at most, until the magpie that
|
||||
// relaunched this one has exited.
|
||||
func AwaitPredecessor() {
|
||||
pid, err := strconv.Atoi(os.Getenv("MAGPIE_REPLACES"))
|
||||
os.Unsetenv("MAGPIE_REPLACES")
|
||||
if err != nil || pid <= 0 {
|
||||
return
|
||||
}
|
||||
for deadline := time.Now().Add(20 * time.Second); time.Now().Before(deadline) && alive(pid); {
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
}
|
||||
}
|
||||
|
||||
// download fetches a to path and checks its hash.
|
||||
func download(ctx context.Context, a Asset, path string) error {
|
||||
if a.SHA256 == "" {
|
||||
|
||||
@@ -17,6 +17,7 @@ import (
|
||||
"github.com/yetone/magpie/internal/profile"
|
||||
"github.com/yetone/magpie/internal/settings"
|
||||
"github.com/yetone/magpie/internal/tui"
|
||||
"github.com/yetone/magpie/internal/update"
|
||||
)
|
||||
|
||||
var version = "dev"
|
||||
@@ -62,6 +63,7 @@ var (
|
||||
|
||||
func main() {
|
||||
gateway.Version = version
|
||||
update.GUI = hasGUI
|
||||
if err := run(os.Args[1:]); err != nil {
|
||||
fmt.Fprintln(os.Stderr, "magpie:", err)
|
||||
os.Exit(1)
|
||||
|
||||
Reference in new issue
Block a user