Files
magpie/healthcheck_cli.go
T
yetone 2880b95b60 settings, gateway: the gateway's port is set in Settings › Network › Gateway, and every agent magpie connected moves with it (Magic_zero on Discord: 能否添加自定义端口号 — 3425 is taken on their machine, and they want a port of their own that is easy to remember and tell apart)
Before this, only MAGPIE_ADDR could move the gateway, and the app on a Mac or one started from the Start menu has no easy way to set an env var.

The setting:
- Settings has a Gateway port field. Any port from 1024 to 65535 can be typed, then applied with Apply or Enter.
- The port is saved in settings.json and kept on this computer (KeepOwn), so a backup or WebDAV doesn't carry it across.
- MAGPIE_ADDR, where it is set, still comes first. The field is then off and says so.
- A port out of range is refused in the page.
- A port another program listens on is refused: "port N is in use by another program: pick another one". The gateway stays where it was.
- With another magpie serving the gateway, the field says to set the port there.

On a change:
- The listener moves live (Relisten listens on the new port before closing the old one).
- When the old port had been taken by another program and nothing served, the gateway starts on the new one.
- Every agent connected to magpie has its config rewritten with the new URL (agent.OnGateway before the move, agent.Rewire after):
  - each one whose wiring is now "unwired" is set again;
  - Codex signed in with ChatGPT is joined again and keeps its own model;
  - Cursor Private Inference's variables and dsh's routes follow.
- What each agent had before magpie (the stash) is kept through this. Before, setting an agent again on the old URL would have written magpie's old URL and token over it.
- The page says how many agents moved.

What follows the saved port (through gateway.Addr()/URL() and settings.GatewayAddr):
- the CLI and healthcheck;
- Running();
- LAN sharing (Port);
- the drift check for the installed magpie, from beside a MAGPIE_ADDR one;
- importapps' "is this entry magpie's own" check.

zh, ja and de strings.

Tests, all failing on the old code:
- TestPortMovesEveryAgent: every agent with a config file is connected at 3591 and moved to 3592. No file keeps 3591, and none is off afterwards.
- TestPortMovesJoinedCodex.
- TestPortLeavesAnUnwiredAgent.
- TestPortKeepsWhatAnAgentHadBefore: the relay URL and own token come back on disconnect.
- TestDriftInstalledMagpieOnItsPort.
- TestSetPortMovesTheGateway, through the API: taken, out of range, moved, the old port silent, POST /api/settings keeps it, MAGPIE_ADDR refused.
- TestSetPortServesWhenTheOldOneWasTaken.
- TestGatewayURLOnSettingsPort.
- gateway-port.test.cjs on WebKit, en and zh:
  - out of range, taken (translated), applied by Enter, the agents counted, the new URL shown;
  - the MAGPIE_ADDR state;
  - no select, no left border, and the page never scrolls (the row is swapped whole, or WebKit laid the list out empty and the view jumped).

Sandbox run (own HOME, settings port 3591, magpie web on 3593):
- Claude Code (over a relay with its own token) and Codex were connected at 3591.
- POST /api/settings/port:
  - 3594, held by another program: refused, in use;
  - 80: refused;
  - 3592: moved [Claude Code, Codex].
- Both configs then say 3592. The gateway answers on 3592 and not on 3591, and `magpie healthcheck` exits 0.
- `magpie claude default` gave back https://relay.example and own-token.

lan-docker, settings-sections, settings-groups, click-scroll, gateway-caller-keys, gui-ja, gui-de and webdav-settings-refresh pass on WebKit.

go vet, the builds (darwin, GOOS=linux nogui, GOOS=windows) and go test -tags nogui -count=1 ./... under a temp HOME pass.
2026-10-05 11:28:51 +08:00

50 lines
1.4 KiB
Go

package main
import (
"bytes"
"errors"
"fmt"
"io"
"net/http"
"os"
"path/filepath"
"time"
"github.com/yetone/magpie/internal/gateway"
)
// healthcheck: magpie healthcheck — exits 0 when a magpie gateway answers
// at its address (MAGPIE_ADDR, else Settings' Gateway port on 127.0.0.1,
// 3425 unless set), for a container's
// HEALTHCHECK, which has no curl to ask it with.
func healthcheck() error {
c := &http.Client{Timeout: 3 * time.Second}
res, err := c.Get(gateway.URL() + "/")
if err != nil {
return fmt.Errorf("no gateway at %s: %w", gateway.URL(), err)
}
defer res.Body.Close()
b, _ := io.ReadAll(io.LimitReader(res.Body, 4096))
if res.StatusCode != http.StatusOK || !bytes.Contains(b, []byte(`"magpie"`)) {
return fmt.Errorf("%s answers %s, not as a magpie gateway", gateway.URL(), res.Status)
}
return nil
}
// makeDirs creates $HOME and the XDG folders the environment names when
// they don't exist yet. The Docker image points them all into its volume
// (/config/home, /config/cache…), and a volume made by an older image has
// none of them: whatever writes there first must not find its parent
// missing.
func makeDirs() {
for _, k := range []string{"HOME", "XDG_CONFIG_HOME", "XDG_CACHE_HOME", "XDG_DATA_HOME", "XDG_STATE_HOME"} {
d := os.Getenv(k)
if d == "" || !filepath.IsAbs(d) {
continue
}
if _, err := os.Stat(d); errors.Is(err, os.ErrNotExist) {
_ = os.MkdirAll(d, 0o700)
}
}
}