diff --git a/docs/getting-started/requirements.md b/docs/getting-started/requirements.md index 9a4017e96..1a92b00c6 100644 --- a/docs/getting-started/requirements.md +++ b/docs/getting-started/requirements.md @@ -81,10 +81,10 @@ On first start `lerd` sizes the Podman Machine VM based on your host RAM so 8 GB | 9-31 GB | 4 GB | | ≥ 32 GB | 6 GB | -The memory value is a ceiling, not a reservation: the VM only uses what your containers actually request. If sites slow down under load on an 8 GB host, bump the VM manually: +The memory value is a ceiling, not a reservation: the VM only uses what your containers actually request. If the VM runs out of memory the dashboard says so and offers a restart with more, or you can do it from the terminal: ```bash -podman machine stop -podman machine set --memory 4096 -podman machine start +lerd machine memory 6 ``` + +See [`lerd machine memory`](../usage/lifecycle.md#lerd-machine-memory-macos). diff --git a/docs/usage/lifecycle.md b/docs/usage/lifecycle.md index 24e1e66c0..6c787a1be 100644 --- a/docs/usage/lifecycle.md +++ b/docs/usage/lifecycle.md @@ -261,6 +261,22 @@ This command is macOS-only. On Linux podman writes to the host filesystem direct --- +## `lerd machine memory` (macOS) + +```bash +lerd machine memory # has the VM run out of memory since it booted? +lerd machine memory 6 # restart the VM with 6 GB, asks for confirmation first +lerd machine memory 6 --yes # skip the prompt +``` + +When the Podman Machine VM runs out of memory, its kernel kills whatever is using the most, usually MySQL or a PHP worker, and commands that need them hang or fail. lerd reads the VM's console log on the host and counts those kills since the VM last booted, so checking costs nothing inside the VM. + +With no argument the command reports the kills and suggests a size: 2 GB more than the VM has, but never past half of the Mac's memory. With a size in GB it stops the VM, sets its memory and runs `lerd start`, so every container is stopped for the minute or so that takes. Site data and databases live on the host and are untouched. + +The dashboard shows the same finding as a banner while the VM has had kills, with a button that does the restart, and `lerd doctor` reports it under Containers & Images. A size you set this way is kept: `lerd start` only ever raises the VM to its default, never lowers it. + +--- + ## Autostart on login Lerd can boot itself every time you log in. Autostart is a single switch over every lerd-owned systemd user unit on the machine: @@ -352,6 +368,7 @@ Where a digest is given the download is checked against it and rejected on a mis | Full shutdown before a reinstall | `lerd quit` | | `lerd start` fails with an overlay / graph-driver storage error (macOS) | `lerd machine reset` | | lerd's VM is holding more host disk than it uses (macOS) | `lerd machine reclaim` | +| lerd's VM keeps running out of memory (macOS) | `lerd machine memory` | | Verify everything's healthy | `lerd status` | | Update Composer / mise / fnm / mkcert to their pinned versions | `lerd tools:update` | | Uninstall a service entirely (data preserved) | `lerd service remove ` | diff --git a/internal/cli/doctor.go b/internal/cli/doctor.go index 081f7660c..bdc17f82b 100644 --- a/internal/cli/doctor.go +++ b/internal/cli/doctor.go @@ -595,6 +595,19 @@ func runDoctorInto(w io.Writer, useColor bool) (DoctorReport, error) { section = "Containers & Images" fmt.Fprintln(w, "\n[Containers & Images]") + if platform.Current.UsesMachineVM { + if st, err := MachineMemoryStatus(); err != nil { + warn("Podman Machine memory", "could not check: "+err.Error()) + } else if len(st.Kills) > 0 { + fail("Podman Machine memory", st.Summary(), st.Hint()) + if st.SuggestedMiB > 0 { + rep.fixLast(manualFixWith("run `" + st.Hint() + "` (it restarts every container)")) + } + } else { + ok("Podman Machine has not run out of memory") + } + } + if detail, hint, healthy := nginxServiceFinding(nginxInstalled, nginxRunning); healthy { ok("lerd-nginx service running") } else { diff --git a/internal/cli/machine.go b/internal/cli/machine.go index 5e94dfd00..ab38d9d67 100644 --- a/internal/cli/machine.go +++ b/internal/cli/machine.go @@ -1,6 +1,12 @@ package cli -import "github.com/spf13/cobra" +import ( + "fmt" + "strconv" + + "github.com/geodro/lerd/internal/feedback" + "github.com/spf13/cobra" +) // NewMachineCmd returns the machine parent command for Podman Machine // maintenance operations. @@ -11,6 +17,7 @@ func NewMachineCmd() *cobra.Command { } cmd.AddCommand(newMachineResetCmd()) cmd.AddCommand(newMachineReclaimCmd()) + cmd.AddCommand(newMachineMemoryCmd()) return cmd } @@ -49,3 +56,49 @@ preserved; container images are rebuilt automatically on the next start.`, cmd.Flags().BoolVarP(&assumeYes, "yes", "y", false, "Skip the confirmation prompt") return cmd } + +func newMachineMemoryCmd() *cobra.Command { + var assumeYes bool + cmd := &cobra.Command{ + Use: "memory [GB]", + Short: "Show or set the Podman Machine VM's memory (macOS)", + Long: `Show whether the Podman Machine VM has run out of memory since it booted, +or restart it with GB of memory. The restart stops every container and brings +them back with lerd start; site data is untouched.`, + Args: cobra.MaximumNArgs(1), + RunE: func(_ *cobra.Command, args []string) error { + if len(args) == 0 { + st, err := MachineMemoryStatus() + if err != nil { + return fmt.Errorf("could not check the Podman Machine's memory: %w", err) + } + printMachineMemory(st) + return nil + } + gb, err := strconv.Atoi(args[0]) + if err != nil { + return fmt.Errorf("%q is not a whole number of GB", args[0]) + } + if !assumeYes && !confirmMachineRestart(gb) { + fmt.Println("Cancelled.") + return nil + } + return ResizeMachineMemory(int64(gb)*1024, nil) + }, + } + cmd.Flags().BoolVarP(&assumeYes, "yes", "y", false, "Skip the confirmation prompt") + return cmd +} + +func printMachineMemory(st MachineMemory) { + if len(st.Kills) == 0 { + fmt.Println("The Podman Machine has not run out of memory since it booted.") + return + } + fmt.Println("The Podman Machine " + st.Summary() + ".") + fmt.Println("Fix: " + st.Hint()) +} + +func confirmMachineRestart(gb int) bool { + return feedback.Confirm(fmt.Sprintf("Restart the Podman Machine with %d GB? Every container stops until lerd start brings them back.", gb), false) +} diff --git a/internal/cli/machine_memory_darwin.go b/internal/cli/machine_memory_darwin.go new file mode 100644 index 000000000..2829396be --- /dev/null +++ b/internal/cli/machine_memory_darwin.go @@ -0,0 +1,84 @@ +//go:build darwin + +package cli + +import ( + "errors" + "fmt" + "os" + "os/exec" + "path/filepath" + "strconv" + "strings" + + "github.com/geodro/lerd/internal/podman" +) + +// MachineMemoryStatus reports the processes the VM's kernel killed for lack of +// memory since it booted, read from its serial console log on the host, so no +// call reaches the VM. The memory sizes are only looked up when there were kills. +// An error means the check could not run, which must not read as a healthy VM. +func MachineMemoryStatus() (MachineMemory, error) { + name, err := selectedMachine() + if err != nil { + return MachineMemory{}, fmt.Errorf("listing Podman Machines: %w", err) + } + if name == "" { + return MachineMemory{}, nil + } + b, err := os.ReadFile(machineConsoleLog(name)) + if err != nil { + return MachineMemory{}, fmt.Errorf("reading the VM's console log: %w", err) + } + st := MachineMemory{Kills: podman.ParseOOMKills(string(b))} + if len(st.Kills) == 0 { + return st, nil + } + out, err := machineQuery("machine", "inspect", "--format", "{{.Resources.Memory}}", name) + if err != nil { + return st, nil + } + st.CurrentMiB, _ = strconv.ParseInt(strings.TrimSpace(string(out)), 10, 64) + st.SuggestedMiB = raisedVMMemoryMiB(st.CurrentMiB, hostMemoryGiB()) + return st, nil +} + +// machineConsoleLog is where podman keeps the VM's serial console, in the +// per-user temp dir. launchd jobs do not always carry TMPDIR, hence getconf. +func machineConsoleLog(name string) string { + dir := os.Getenv("TMPDIR") + if dir == "" { + out, _ := exec.Command("getconf", "DARWIN_USER_TEMP_DIR").Output() + dir = strings.TrimSpace(string(out)) + } + return filepath.Join(dir, "podman", name+".log") +} + +// ResizeMachineMemory restarts the VM with mib of memory, then runs the normal +// start so every container comes back with its port forwards. +func ResizeMachineMemory(mib int64, emit func(StartEvent), skip ...string) error { + name := selectedMachineName() + if name == "" { + return fmt.Errorf("no Podman Machine found; run `lerd start` to create one") + } + if ceiling := int64(hostMemoryGiB()) * 1024 / 2; mib < 2048 || mib > ceiling { + return fmt.Errorf("%d MB is outside 2048 to %d MB, half of this Mac's memory", mib, ceiling) + } + release, err := podman.LockLifecycle() + if err != nil { + return fmt.Errorf("lifecycle lock: %w", err) + } + if emit != nil { + emit(StartEvent{Phase: "step", Step: "machine"}) + } + _ = runMachineStreaming(machineStopTimeout, "machine", "stop", name) + setErr := runMachineStreaming(machineQueryTimeout, "machine", "set", "--memory", strconv.FormatInt(mib, 10), name) + release() + // Start even when the set failed: the VM is already down, and leaving it + // there takes every site with it. + startErr := RunStart(emit, skip...) + if setErr != nil { + return errors.Join(fmt.Errorf("podman machine set --memory: %w", setErr), startErr) + } + return startErr +} diff --git a/internal/cli/machine_memory_darwin_test.go b/internal/cli/machine_memory_darwin_test.go new file mode 100644 index 000000000..7e5da503b --- /dev/null +++ b/internal/cli/machine_memory_darwin_test.go @@ -0,0 +1,78 @@ +//go:build darwin + +package cli + +import ( + "context" + "fmt" + "os" + "os/exec" + "path/filepath" + "testing" +) + +// TestMachineListHelperProcess is the child the fake machineCmdContext spawns: +// it prints GO_MACHINE_LIST_OUT, or fails when that is unset. +func TestMachineListHelperProcess(t *testing.T) { + if os.Getenv("GO_WANT_MACHINE_LIST_HELPER") != "1" { + return + } + out, ok := os.LookupEnv("GO_MACHINE_LIST_OUT") + if !ok { + os.Exit(1) + } + fmt.Print(out) + os.Exit(0) +} + +func fakeMachineList(t *testing.T, out *string) { + t.Helper() + prev := machineCmdContext + t.Cleanup(func() { machineCmdContext = prev }) + machineCmdContext = func(ctx context.Context, args ...string) *exec.Cmd { + cmd := exec.CommandContext(ctx, os.Args[0], "-test.run=TestMachineListHelperProcess", "--") + cmd.Env = append(os.Environ(), "GO_WANT_MACHINE_LIST_HELPER=1") + if out != nil { + cmd.Env = append(cmd.Env, "GO_MACHINE_LIST_OUT="+*out) + } + return cmd + } +} + +// A machine list that fails must not read as a VM with no kills. +func TestMachineMemoryStatusReportsAFailedMachineList(t *testing.T) { + fakeMachineList(t, nil) + + if _, err := MachineMemoryStatus(); err == nil { + t.Fatal("expected an error when the machine list fails") + } +} + +// An unreadable console log means the check could not run, not that it passed. +func TestMachineMemoryStatusReportsAMissingConsoleLog(t *testing.T) { + out := "lerd*\n" + fakeMachineList(t, &out) + t.Setenv("TMPDIR", t.TempDir()) + + if _, err := MachineMemoryStatus(); err == nil { + t.Fatal("expected an error when the console log is missing") + } +} + +func TestMachineMemoryStatusReadsTheConsoleLog(t *testing.T) { + out := "lerd*\n" + fakeMachineList(t, &out) + dir := t.TempDir() + t.Setenv("TMPDIR", dir) + if err := os.MkdirAll(filepath.Join(dir, "podman"), 0o755); err != nil { + t.Fatal(err) + } + if err := os.WriteFile(filepath.Join(dir, "podman", "lerd.log"), []byte("booted\n"), 0o644); err != nil { + t.Fatal(err) + } + + st, err := MachineMemoryStatus() + if err != nil || len(st.Kills) != 0 { + t.Fatalf("got %+v, %v; want no kills and no error", st, err) + } +} diff --git a/internal/cli/machine_memory_other.go b/internal/cli/machine_memory_other.go new file mode 100644 index 000000000..1a10230e2 --- /dev/null +++ b/internal/cli/machine_memory_other.go @@ -0,0 +1,13 @@ +//go:build !darwin + +package cli + +import "fmt" + +// MachineMemoryStatus is empty off macOS: there is no Podman Machine VM whose +// memory lerd sizes. +func MachineMemoryStatus() (MachineMemory, error) { return MachineMemory{}, nil } + +func ResizeMachineMemory(int64, func(StartEvent), ...string) error { + return fmt.Errorf("resizing the Podman Machine is only supported on macOS") +} diff --git a/internal/cli/machine_restart_heal_darwin.go b/internal/cli/machine_restart_heal_darwin.go index e0a21545c..0143a848f 100644 --- a/internal/cli/machine_restart_heal_darwin.go +++ b/internal/cli/machine_restart_heal_darwin.go @@ -21,12 +21,18 @@ func machineLastUpFile() string { // selectedMachineName mirrors ensurePodmanMachineRunning's selection order // (default-marked, else first) so inspect targets the VM lerd actually -// uses. Returns "" when no machine exists yet. +// uses. Returns "" when no machine exists yet or the list fails. func selectedMachineName() string { - out, err := podman.Cmd("machine", "list", - "--format", "{{.Name}}").Output() + name, _ := selectedMachine() + return name +} + +// selectedMachine is selectedMachineName keeping the list's error, bounded so a +// wedged VM cannot hang the caller. +func selectedMachine() (string, error) { + out, err := machineQuery("machine", "list", "--format", "{{.Name}}") if err != nil { - return "" + return "", err } var first, def string for _, line := range strings.Split(strings.TrimSpace(string(out)), "\n") { @@ -44,9 +50,9 @@ func selectedMachineName() string { } } if def != "" { - return def + return def, nil } - return first + return first, nil } // currentMachineLastUp returns the LastUp of lerd's Podman Machine, or "" diff --git a/internal/cli/podman_memory.go b/internal/cli/podman_memory.go index 828828542..10508d8db 100644 --- a/internal/cli/podman_memory.go +++ b/internal/cli/podman_memory.go @@ -1,5 +1,10 @@ package cli +import ( + "fmt" + "strings" +) + // recommendedVMMemoryMiB picks Podman Machine memory based on host RAM and // the active worker runtime mode. 8 GB is the minimum host RAM tier; anything // below (including detection failures) uses the 8 GB values. @@ -33,3 +38,39 @@ func recommendedVMMemoryMiB(hostMemoryGiB int, execMode bool) int64 { return 3072 } } + +// raisedVMMemoryMiB is the size to offer a VM that ran out of memory: 2 GB +// more, but never past half the host's RAM so macOS keeps room of its own. +// 0 when there is nothing more to give or the host size is unknown. +func raisedVMMemoryMiB(currentMiB int64, hostMemoryGiB int) int64 { + ceiling := int64(hostMemoryGiB) * 1024 / 2 + next := min(currentMiB+2048, ceiling) + if next <= currentMiB { + return 0 + } + return next +} + +// MachineMemory is the Podman Machine's out-of-memory record since it booted. +type MachineMemory struct { + Kills []string `json:"kills"` + CurrentMiB int64 `json:"current_mib,omitempty"` + SuggestedMiB int64 `json:"suggested_mib,omitempty"` // 0: nothing more to give +} + +// Summary says how often the VM ran out of memory and what the kernel killed. +func (st MachineMemory) Summary() string { + return fmt.Sprintf("ran out of memory %d times since it booted (%s)", len(st.Kills), strings.Join(dedupeStrings(st.Kills), ", ")) +} + +// Hint is the command that gives the VM more memory, or what to do instead +// when the host has none left to give. +func (st MachineMemory) Hint() string { + if st.CurrentMiB == 0 { + return "could not read the VM's memory size; run `lerd machine memory` again" + } + if st.SuggestedMiB == 0 { + return "this Mac has no more memory to give it; stop sites or workers you are not using" + } + return fmt.Sprintf("lerd machine memory %d", st.SuggestedMiB/1024) +} diff --git a/internal/cli/podman_memory_test.go b/internal/cli/podman_memory_test.go index 14ac31839..db0eeb298 100644 --- a/internal/cli/podman_memory_test.go +++ b/internal/cli/podman_memory_test.go @@ -1,6 +1,9 @@ package cli -import "testing" +import ( + "strings" + "testing" +) func TestRecommendedVMMemoryMiB(t *testing.T) { cases := []struct { @@ -39,3 +42,29 @@ func TestRecommendedVMMemoryMiB(t *testing.T) { }) } } + +func TestRaisedVMMemoryMiB(t *testing.T) { + cases := []struct { + current int64 + hostGiB int + want int64 + }{ + {4096, 48, 6144}, // room to spare: 2 GB more + {4096, 10, 5120}, // capped at half the host + {8192, 16, 0}, // already at half the host + {3072, 0, 0}, // host size unknown: offer nothing + } + for _, c := range cases { + if got := raisedVMMemoryMiB(c.current, c.hostGiB); got != c.want { + t.Errorf("raisedVMMemoryMiB(%d, %d) = %d, want %d", c.current, c.hostGiB, got, c.want) + } + } +} + +// A failed size lookup must not read as "this Mac has no memory to give". +func TestMachineMemoryHintWhenTheSizeIsUnknown(t *testing.T) { + st := MachineMemory{Kills: []string{"mysqld"}} + if got := st.Hint(); !strings.Contains(got, "could not read") { + t.Errorf("Hint() = %q, want the unknown-size advice", got) + } +} diff --git a/internal/cli/startstop_darwin.go b/internal/cli/startstop_darwin.go index b48b8a8a8..4187fa15b 100644 --- a/internal/cli/startstop_darwin.go +++ b/internal/cli/startstop_darwin.go @@ -408,7 +408,7 @@ func startPodmanMachineIfNeeded() error { if needsMemory { if hostGiB > 0 && hostGiB <= 8 { feedback.Line(fmt.Sprintf("Host has %d GB RAM; setting Podman Machine to %d MB (tight but workable)…", hostGiB, targetMemoryMiB)) - feedback.Note("If sites slow down under load, run: podman machine set --memory 4096") + feedback.Note("If the VM runs out of memory, run: lerd machine memory 4") } else { feedback.Line(fmt.Sprintf("Setting Podman Machine memory to %d MB…", targetMemoryMiB)) } diff --git a/internal/podman/machine_oom.go b/internal/podman/machine_oom.go new file mode 100644 index 000000000..001466ad3 --- /dev/null +++ b/internal/podman/machine_oom.go @@ -0,0 +1,35 @@ +package podman + +import ( + "regexp" + "strconv" + "strings" +) + +var ( + kernelStamp = regexp.MustCompile(`\[\s*(\d+)\.\d+\]`) + oomKill = regexp.MustCompile(`Out of memory: Killed process \d+ \(([^)]+)\)`) +) + +// ParseOOMKills returns the processes the VM's kernel killed for lack of memory +// since its last boot, read from the machine's serial console log. The log can +// span boots, so a kernel uptime stamp going backwards starts the count over. +func ParseOOMKills(log string) []string { + var kills []string + last := int64(-1) + for _, line := range strings.Split(log, "\n") { + m := kernelStamp.FindStringSubmatch(line) + if m == nil { + continue + } + stamp, _ := strconv.ParseInt(m[1], 10, 64) + if stamp < last { + kills = nil + } + last = stamp + if k := oomKill.FindStringSubmatch(line); k != nil { + kills = append(kills, k[1]) + } + } + return kills +} diff --git a/internal/podman/machine_oom_test.go b/internal/podman/machine_oom_test.go new file mode 100644 index 000000000..fca86da39 --- /dev/null +++ b/internal/podman/machine_oom_test.go @@ -0,0 +1,32 @@ +package podman + +import ( + "reflect" + "testing" +) + +// Trimmed from a real applehv console log: the kernel's uptime stamp starting +// over marks a reboot, and only the kills after it are this boot's. +const oomConsoleLog = `[ 0.000000] Booting Linux on physical CPU 0x0000000000 [0x610f0000] +[708961.947962] Out of memory: Killed process 1318 (dbus-broker-lau) total-vm:9292kB, anon-rss:276kB +[709000.465360] Out of memory: Killed process 2357719 (mysqld) total-vm:2178448kB, anon-rss:435684kB +[ 0.000000] Booting Linux on physical CPU 0x0000000000 [0x610f0000] +[ 2.114023] systemd[1]: Detected virtualization apple. +[root@localhost ~]# [ 239.106785] Out of memory: Killed process 4157 (mysqld) total-vm:2112916kB, anon-rss:456404kB +[ 919.398937] Out of memory: Killed process 1144 (systemd) total-vm:24620kB, anon-rss:6588kB +[ 948.967036] Out of memory: Killed process 3952 (php-fpm) total-vm:268840kB, anon-rss:64160kB +` + +func TestParseOOMKills_countsOnlyTheCurrentBoot(t *testing.T) { + got := ParseOOMKills(oomConsoleLog) + want := []string{"mysqld", "systemd", "php-fpm"} + if !reflect.DeepEqual(got, want) { + t.Errorf("ParseOOMKills = %v, want %v", got, want) + } +} + +func TestParseOOMKills_noneOnAHealthyBoot(t *testing.T) { + if got := ParseOOMKills("[ 0.000000] Booting Linux\n[ 2.1] systemd[1]: ok\n"); len(got) != 0 { + t.Errorf("ParseOOMKills = %v, want none", got) + } +} diff --git a/internal/ui/machine_memory.go b/internal/ui/machine_memory.go new file mode 100644 index 000000000..9e5748da7 --- /dev/null +++ b/internal/ui/machine_memory.go @@ -0,0 +1,46 @@ +package ui + +import ( + "encoding/json" + "net/http" + + "github.com/geodro/lerd/internal/cli" +) + +// Seams so the handler can be exercised without a Podman Machine. +var ( + machineMemoryStatusFn = cli.MachineMemoryStatus + resizeMachineMemoryFn = cli.ResizeMachineMemory +) + +// handleMachineMemory reports the VM's out-of-memory kills since boot (GET), or +// restarts it with more memory and streams the start that follows (POST). +func handleMachineMemory(w http.ResponseWriter, r *http.Request) { + switch r.Method { + case http.MethodGet: + st, err := machineMemoryStatusFn() + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + writeJSON(w, st) + case http.MethodPost: + if !hasHostActionAuthority(r) { + http.Error(w, "forbidden", http.StatusForbidden) + return + } + var body struct { + MiB int64 `json:"mib"` + } + if err := json.NewDecoder(r.Body).Decode(&body); err != nil { + http.Error(w, "invalid body", http.StatusBadRequest) + return + } + writeLine, _ := startNDJSONStream(w, r) + if err := resizeMachineMemoryFn(body.MiB, func(evt cli.StartEvent) { writeLine(evt) }, uiUnit); err != nil { + writeLine(cli.StartEvent{Phase: "failed", Error: err.Error()}) + } + default: + http.Error(w, "method not allowed", http.StatusMethodNotAllowed) + } +} diff --git a/internal/ui/machine_memory_test.go b/internal/ui/machine_memory_test.go new file mode 100644 index 000000000..0419f5a07 --- /dev/null +++ b/internal/ui/machine_memory_test.go @@ -0,0 +1,95 @@ +package ui + +import ( + "encoding/json" + "errors" + "io" + "net/http" + "net/http/httptest" + "strings" + "testing" + + "github.com/geodro/lerd/internal/cli" +) + +func withMachineMemorySeams(t *testing.T, st cli.MachineMemory, resize func(int64, func(cli.StartEvent), ...string) error) { + t.Helper() + oldStatus, oldResize := machineMemoryStatusFn, resizeMachineMemoryFn + machineMemoryStatusFn = func() (cli.MachineMemory, error) { return st, nil } + resizeMachineMemoryFn = resize + t.Cleanup(func() { machineMemoryStatusFn, resizeMachineMemoryFn = oldStatus, oldResize }) +} + +func TestMachineMemoryReportsKills(t *testing.T) { + withMachineMemorySeams(t, cli.MachineMemory{Kills: []string{"mysqld"}, CurrentMiB: 4096, SuggestedMiB: 6144}, nil) + + rec := httptest.NewRecorder() + handleMachineMemory(rec, httptest.NewRequest(http.MethodGet, "/api/machine/memory", nil)) + + var got cli.MachineMemory + if err := json.Unmarshal(rec.Body.Bytes(), &got); err != nil { + t.Fatal(err) + } + if len(got.Kills) != 1 || got.SuggestedMiB != 6144 { + t.Errorf("got %+v, want the kills and the suggested size", got) + } +} + +// The resize restarts every unit, so lerd-ui must be left out of the start the +// same way the dashboard's start button leaves it out. +func TestMachineMemoryResizeSkipsTheUIUnit(t *testing.T) { + var gotMiB int64 + var gotSkip []string + withMachineMemorySeams(t, cli.MachineMemory{}, func(mib int64, emit func(cli.StartEvent), skip ...string) error { + gotMiB, gotSkip = mib, skip + emit(cli.StartEvent{Phase: "done"}) + return nil + }) + + rec := httptest.NewRecorder() + req := httptest.NewRequest(http.MethodPost, "/api/machine/memory", strings.NewReader(`{"mib":6144}`)) + req.RemoteAddr = "127.0.0.1:54321" + handleMachineMemory(rec, req) + + if gotMiB != 6144 || len(gotSkip) != 1 || gotSkip[0] != uiUnit { + t.Errorf("resize got (%d, %v), want (6144, [%s])", gotMiB, gotSkip, uiUnit) + } + if !strings.Contains(rec.Body.String(), `"phase":"done"`) { + t.Errorf("stream = %q, want the start events", rec.Body.String()) + } +} + +// The resize stops every container, so a remote session without full access +// may read the finding but not act on it. +func TestMachineMemoryResizeRefusesRemoteWithoutFullAccess(t *testing.T) { + setupConfigDirFullAccess(t, "alice", "s3cret", false) + called := false + withMachineMemorySeams(t, cli.MachineMemory{}, func(int64, func(cli.StartEvent), ...string) error { + called = true + return nil + }) + + req := remoteRequest(http.MethodPost, "/api/machine/memory") + req.Body = io.NopCloser(strings.NewReader(`{"mib":6144}`)) + rec := httptest.NewRecorder() + withRemoteControlGate(http.HandlerFunc(handleMachineMemory)).ServeHTTP(rec, req) + + if rec.Code != http.StatusForbidden || called { + t.Errorf("status = %d, resized = %v, want 403 and no resize", rec.Code, called) + } +} + +// A check that could not run answers an error, so the banner keeps what it had +// instead of reading the failure as a VM with no kills. +func TestMachineMemoryReportsAFailedCheck(t *testing.T) { + old := machineMemoryStatusFn + machineMemoryStatusFn = func() (cli.MachineMemory, error) { return cli.MachineMemory{}, errors.New("no console log") } + t.Cleanup(func() { machineMemoryStatusFn = old }) + + rec := httptest.NewRecorder() + handleMachineMemory(rec, httptest.NewRequest(http.MethodGet, "/api/machine/memory", nil)) + + if rec.Code != http.StatusInternalServerError { + t.Errorf("status = %d, want 500", rec.Code) + } +} diff --git a/internal/ui/server.go b/internal/ui/server.go index 2ab21c340..151957631 100644 --- a/internal/ui/server.go +++ b/internal/ui/server.go @@ -379,6 +379,7 @@ func Start(currentVersion string) error { mux.HandleFunc("/api/disk", withCORS(handleDisk)) mux.HandleFunc("/api/xdebug/", withCORS(publishAfter(handleXdebugAction, eventbus.KindStatus))) mux.HandleFunc("/api/lerd/start", withCORS(handleLerdStart)) + mux.HandleFunc("/api/machine/memory", withCORS(handleMachineMemory)) mux.HandleFunc("/api/lerd/stop", withCORS(handleLerdStop)) mux.HandleFunc("/api/lerd/quit", withCORS(handleLerdQuit)) mux.HandleFunc("/api/lerd/update-terminal", withCORS(handleLerdUpdateTerminal)) diff --git a/internal/ui/web/messages/de.json b/internal/ui/web/messages/de.json index ef203842d..dccb533de 100644 --- a/internal/ui/web/messages/de.json +++ b/internal/ui/web/messages/de.json @@ -127,6 +127,7 @@ "dashboard_hero_startLerd": "Lerd starten", "dashboard_hero_startStep_dns": "DNS wird konfiguriert", "dashboard_hero_startStep_images": "Images werden geprüft", + "dashboard_hero_startStep_machine": "Podman Machine wird neu gestartet", "dashboard_hero_startStep_preparing": "Wird vorbereitet", "dashboard_hero_startStep_units": "Dienste werden gestartet", "dashboard_hero_startingLerd": "Wird gestartet...", @@ -397,6 +398,14 @@ "link_noSubdirs": "Keine Unterverzeichnisse", "link_waitingOutput": "Warte auf Ausgabe...", "logs_filter_allLevels": "Alle Level", + "machine_memory_banner_dismiss": "Ausblenden", + "machine_memory_banner_failed": "Der Neustart wurde nicht abgeschlossen: {error}", + "machine_memory_banner_killed": "Beendet: {processes}", + "machine_memory_banner_noRoom": "Dieser Mac hat keinen Speicher mehr übrig. Stoppe Sites oder Worker, die du nicht nutzt.", + "machine_memory_banner_restart": "Mit {gb} GB neu starten", + "machine_memory_banner_title": "Der Podman Machine ist seit dem Start {count} Mal der Speicher ausgegangen", + "machine_memory_confirm_body": "Alle Container stoppen, während die VM mit {gb} GB neu startet, danach startet lerd sie wieder. Site-Daten bleiben unberührt.", + "machine_memory_confirm_title": "Podman Machine neu starten?", "messages_filter_allChannels": "Alle Kanäle", "meta_code": "DE", "meta_language": "Deutsch", diff --git a/internal/ui/web/messages/en.json b/internal/ui/web/messages/en.json index d4314d399..198c5e325 100644 --- a/internal/ui/web/messages/en.json +++ b/internal/ui/web/messages/en.json @@ -127,6 +127,7 @@ "dashboard_hero_startLerd": "Start Lerd", "dashboard_hero_startStep_dns": "Configuring DNS", "dashboard_hero_startStep_images": "Checking images", + "dashboard_hero_startStep_machine": "Restarting the Podman Machine", "dashboard_hero_startStep_preparing": "Preparing", "dashboard_hero_startStep_units": "Starting services", "dashboard_hero_startingLerd": "Starting...", @@ -397,6 +398,14 @@ "link_noSubdirs": "No subdirectories", "link_waitingOutput": "Waiting for output...", "logs_filter_allLevels": "All levels", + "machine_memory_banner_dismiss": "Dismiss", + "machine_memory_banner_failed": "The restart did not finish: {error}", + "machine_memory_banner_killed": "Killed: {processes}", + "machine_memory_banner_noRoom": "This Mac has no more memory to give it. Stop sites or workers you are not using.", + "machine_memory_banner_restart": "Restart with {gb} GB", + "machine_memory_banner_title": "The Podman Machine ran out of memory {count} times since it started", + "machine_memory_confirm_body": "Every container stops while the VM restarts with {gb} GB, then lerd starts them again. Site data is untouched.", + "machine_memory_confirm_title": "Restart the Podman Machine?", "messages_filter_allChannels": "All channels", "meta_code": "EN", "meta_language": "English", diff --git a/internal/ui/web/messages/es.json b/internal/ui/web/messages/es.json index 1da283d5c..32f79fb34 100644 --- a/internal/ui/web/messages/es.json +++ b/internal/ui/web/messages/es.json @@ -127,6 +127,7 @@ "dashboard_hero_startLerd": "Iniciar Lerd", "dashboard_hero_startStep_dns": "Configurando DNS", "dashboard_hero_startStep_images": "Comprobando imágenes", + "dashboard_hero_startStep_machine": "Reiniciando la Podman Machine", "dashboard_hero_startStep_preparing": "Preparando", "dashboard_hero_startStep_units": "Iniciando servicios", "dashboard_hero_startingLerd": "Iniciando...", @@ -397,6 +398,14 @@ "link_noSubdirs": "No hay subdirectorios", "link_waitingOutput": "Esperando salida...", "logs_filter_allLevels": "Todos los niveles", + "machine_memory_banner_dismiss": "Descartar", + "machine_memory_banner_failed": "El reinicio no terminó: {error}", + "machine_memory_banner_killed": "Terminados: {processes}", + "machine_memory_banner_noRoom": "Este Mac no tiene más memoria para darle. Detén los sitios o workers que no uses.", + "machine_memory_banner_restart": "Reiniciar con {gb} GB", + "machine_memory_banner_title": "La Podman Machine se quedó sin memoria {count} veces desde que arrancó", + "machine_memory_confirm_body": "Todos los contenedores se detienen mientras la VM se reinicia con {gb} GB, y luego lerd los vuelve a iniciar. Los datos de los sitios no se tocan.", + "machine_memory_confirm_title": "¿Reiniciar la Podman Machine?", "messages_filter_allChannels": "Todos los canales", "meta_code": "ES", "meta_language": "Español", diff --git a/internal/ui/web/messages/fr.json b/internal/ui/web/messages/fr.json index 5a4984ffd..9f60f1706 100644 --- a/internal/ui/web/messages/fr.json +++ b/internal/ui/web/messages/fr.json @@ -127,6 +127,7 @@ "dashboard_hero_startLerd": "Démarrer Lerd", "dashboard_hero_startStep_dns": "Configuration du DNS", "dashboard_hero_startStep_images": "Vérification des images", + "dashboard_hero_startStep_machine": "Redémarrage de la Podman Machine", "dashboard_hero_startStep_preparing": "Préparation", "dashboard_hero_startStep_units": "Démarrage des services", "dashboard_hero_startingLerd": "Démarrage...", @@ -397,6 +398,14 @@ "link_noSubdirs": "Aucun sous-répertoire", "link_waitingOutput": "En attente de sortie...", "logs_filter_allLevels": "Tous les niveaux", + "machine_memory_banner_dismiss": "Ignorer", + "machine_memory_banner_failed": "Le redémarrage n'a pas abouti : {error}", + "machine_memory_banner_killed": "Arrêtés : {processes}", + "machine_memory_banner_noRoom": "Ce Mac n'a plus de mémoire à lui donner. Arrêtez les sites ou workers inutilisés.", + "machine_memory_banner_restart": "Redémarrer avec {gb} Go", + "machine_memory_banner_title": "La Podman Machine a manqué de mémoire {count} fois depuis son démarrage", + "machine_memory_confirm_body": "Tous les conteneurs s'arrêtent pendant que la VM redémarre avec {gb} Go, puis lerd les relance. Les données des sites ne sont pas touchées.", + "machine_memory_confirm_title": "Redémarrer la Podman Machine ?", "messages_filter_allChannels": "Tous les canaux", "meta_code": "FR", "meta_language": "Français", diff --git a/internal/ui/web/messages/id.json b/internal/ui/web/messages/id.json index 3075d7686..0c4e65f32 100644 --- a/internal/ui/web/messages/id.json +++ b/internal/ui/web/messages/id.json @@ -127,6 +127,7 @@ "dashboard_hero_startLerd": "Mulai Lerd", "dashboard_hero_startStep_dns": "Mengonfigurasi DNS", "dashboard_hero_startStep_images": "Memeriksa image", + "dashboard_hero_startStep_machine": "Memulai ulang Podman Machine", "dashboard_hero_startStep_preparing": "Menyiapkan", "dashboard_hero_startStep_units": "Memulai layanan", "dashboard_hero_startingLerd": "Memulai...", @@ -397,6 +398,14 @@ "link_noSubdirs": "Tidak ada subdirektori", "link_waitingOutput": "Menunggu keluaran...", "logs_filter_allLevels": "Semua level", + "machine_memory_banner_dismiss": "Tutup", + "machine_memory_banner_failed": "Mulai ulang tidak selesai: {error}", + "machine_memory_banner_killed": "Dihentikan: {processes}", + "machine_memory_banner_noRoom": "Mac ini tidak punya memori lagi untuk diberikan. Hentikan situs atau worker yang tidak dipakai.", + "machine_memory_banner_restart": "Mulai ulang dengan {gb} GB", + "machine_memory_banner_title": "Podman Machine kehabisan memori {count} kali sejak dimulai", + "machine_memory_confirm_body": "Semua kontainer berhenti saat VM dimulai ulang dengan {gb} GB, lalu lerd menjalankannya lagi. Data situs tidak tersentuh.", + "machine_memory_confirm_title": "Mulai ulang Podman Machine?", "messages_filter_allChannels": "Semua saluran", "meta_code": "ID", "meta_language": "Bahasa Indonesia", diff --git a/internal/ui/web/messages/it.json b/internal/ui/web/messages/it.json index f7827df34..a76b455a7 100644 --- a/internal/ui/web/messages/it.json +++ b/internal/ui/web/messages/it.json @@ -127,6 +127,7 @@ "dashboard_hero_startLerd": "Avvia Lerd", "dashboard_hero_startStep_dns": "Configurazione del DNS", "dashboard_hero_startStep_images": "Controllo delle immagini", + "dashboard_hero_startStep_machine": "Riavvio della Podman Machine", "dashboard_hero_startStep_preparing": "Preparazione", "dashboard_hero_startStep_units": "Avvio dei servizi", "dashboard_hero_startingLerd": "Avvio...", @@ -397,6 +398,14 @@ "link_noSubdirs": "Nessuna sottodirectory", "link_waitingOutput": "In attesa di output...", "logs_filter_allLevels": "Tutti i livelli", + "machine_memory_banner_dismiss": "Ignora", + "machine_memory_banner_failed": "Il riavvio non è terminato: {error}", + "machine_memory_banner_killed": "Terminati: {processes}", + "machine_memory_banner_noRoom": "Questo Mac non ha altra memoria da darle. Ferma i siti o i worker che non usi.", + "machine_memory_banner_restart": "Riavvia con {gb} GB", + "machine_memory_banner_title": "La Podman Machine ha esaurito la memoria {count} volte dall'avvio", + "machine_memory_confirm_body": "Tutti i container si fermano mentre la VM si riavvia con {gb} GB, poi lerd li riavvia. I dati dei siti non vengono toccati.", + "machine_memory_confirm_title": "Riavviare la Podman Machine?", "messages_filter_allChannels": "Tutti i canali", "meta_code": "IT", "meta_language": "Italiano", diff --git a/internal/ui/web/messages/ja.json b/internal/ui/web/messages/ja.json index 70cd65203..a72471e9c 100644 --- a/internal/ui/web/messages/ja.json +++ b/internal/ui/web/messages/ja.json @@ -127,6 +127,7 @@ "dashboard_hero_startLerd": "Lerd を開始", "dashboard_hero_startStep_dns": "DNS を設定中", "dashboard_hero_startStep_images": "イメージを確認中", + "dashboard_hero_startStep_machine": "Podman Machine を再起動しています", "dashboard_hero_startStep_preparing": "準備中", "dashboard_hero_startStep_units": "サービスを起動中", "dashboard_hero_startingLerd": "開始中...", @@ -397,6 +398,14 @@ "link_noSubdirs": "サブディレクトリなし", "link_waitingOutput": "出力を待機中...", "logs_filter_allLevels": "すべてのレベル", + "machine_memory_banner_dismiss": "閉じる", + "machine_memory_banner_failed": "再起動が完了しませんでした: {error}", + "machine_memory_banner_killed": "終了: {processes}", + "machine_memory_banner_noRoom": "この Mac にはこれ以上割り当てられるメモリがありません。使っていないサイトやワーカーを停止してください。", + "machine_memory_banner_restart": "{gb} GB で再起動", + "machine_memory_banner_title": "Podman Machine は起動以来 {count} 回メモリ不足になりました", + "machine_memory_confirm_body": "VM が {gb} GB で再起動する間すべてのコンテナが停止し、その後 lerd が再び起動します。サイトのデータには影響しません。", + "machine_memory_confirm_title": "Podman Machine を再起動しますか?", "messages_filter_allChannels": "すべてのチャネル", "meta_code": "JA", "meta_language": "日本語", diff --git a/internal/ui/web/messages/nl.json b/internal/ui/web/messages/nl.json index 9b5c0c1d3..4e798acb0 100644 --- a/internal/ui/web/messages/nl.json +++ b/internal/ui/web/messages/nl.json @@ -127,6 +127,7 @@ "dashboard_hero_startLerd": "Lerd starten", "dashboard_hero_startStep_dns": "DNS configureren", "dashboard_hero_startStep_images": "Images controleren", + "dashboard_hero_startStep_machine": "Podman Machine wordt herstart", "dashboard_hero_startStep_preparing": "Voorbereiden", "dashboard_hero_startStep_units": "Services starten", "dashboard_hero_startingLerd": "Bezig met starten...", @@ -397,6 +398,14 @@ "link_noSubdirs": "Geen submappen", "link_waitingOutput": "Wachten op uitvoer...", "logs_filter_allLevels": "Alle niveaus", + "machine_memory_banner_dismiss": "Sluiten", + "machine_memory_banner_failed": "De herstart is niet voltooid: {error}", + "machine_memory_banner_killed": "Beëindigd: {processes}", + "machine_memory_banner_noRoom": "Deze Mac heeft geen geheugen meer over. Stop sites of workers die je niet gebruikt.", + "machine_memory_banner_restart": "Herstarten met {gb} GB", + "machine_memory_banner_title": "De Podman Machine had sinds de start {count} keer geen geheugen meer", + "machine_memory_confirm_body": "Alle containers stoppen terwijl de VM herstart met {gb} GB, daarna start lerd ze opnieuw. Sitedata blijft onaangeroerd.", + "machine_memory_confirm_title": "Podman Machine herstarten?", "messages_filter_allChannels": "Alle kanalen", "meta_code": "NL", "meta_language": "Nederlands", diff --git a/internal/ui/web/messages/pl.json b/internal/ui/web/messages/pl.json index c1abbb025..9b7b078f4 100644 --- a/internal/ui/web/messages/pl.json +++ b/internal/ui/web/messages/pl.json @@ -127,6 +127,7 @@ "dashboard_hero_startLerd": "Uruchom Lerd", "dashboard_hero_startStep_dns": "Konfigurowanie DNS", "dashboard_hero_startStep_images": "Sprawdzanie obrazów", + "dashboard_hero_startStep_machine": "Ponowne uruchamianie Podman Machine", "dashboard_hero_startStep_preparing": "Przygotowywanie", "dashboard_hero_startStep_units": "Uruchamianie usług", "dashboard_hero_startingLerd": "Uruchamianie...", @@ -397,6 +398,14 @@ "link_noSubdirs": "Brak podkatalogów", "link_waitingOutput": "Oczekiwanie na wynik...", "logs_filter_allLevels": "Wszystkie poziomy", + "machine_memory_banner_dismiss": "Zamknij", + "machine_memory_banner_failed": "Ponowne uruchomienie nie powiodło się: {error}", + "machine_memory_banner_killed": "Zakończone: {processes}", + "machine_memory_banner_noRoom": "Ten Mac nie ma więcej pamięci do przydzielenia. Zatrzymaj nieużywane strony lub workery.", + "machine_memory_banner_restart": "Uruchom ponownie z {gb} GB", + "machine_memory_banner_title": "Podman Machine zabrakło pamięci {count} razy od uruchomienia", + "machine_memory_confirm_body": "Wszystkie kontenery zatrzymają się, gdy VM uruchomi się ponownie z {gb} GB, potem lerd uruchomi je znowu. Dane stron pozostają nietknięte.", + "machine_memory_confirm_title": "Uruchomić ponownie Podman Machine?", "messages_filter_allChannels": "Wszystkie kanały", "meta_code": "PL", "meta_language": "Polski", diff --git a/internal/ui/web/messages/pt.json b/internal/ui/web/messages/pt.json index 8cbc4708c..a411774f5 100644 --- a/internal/ui/web/messages/pt.json +++ b/internal/ui/web/messages/pt.json @@ -127,6 +127,7 @@ "dashboard_hero_startLerd": "Iniciar Lerd", "dashboard_hero_startStep_dns": "Configurando DNS", "dashboard_hero_startStep_images": "Verificando imagens", + "dashboard_hero_startStep_machine": "Reiniciando a Podman Machine", "dashboard_hero_startStep_preparing": "Preparando", "dashboard_hero_startStep_units": "Iniciando serviços", "dashboard_hero_startingLerd": "Iniciando...", @@ -397,6 +398,14 @@ "link_noSubdirs": "Sem subdiretórios", "link_waitingOutput": "Aguardando saída...", "logs_filter_allLevels": "Todos os níveis", + "machine_memory_banner_dismiss": "Dispensar", + "machine_memory_banner_failed": "O reinício não terminou: {error}", + "machine_memory_banner_killed": "Encerrados: {processes}", + "machine_memory_banner_noRoom": "Este Mac não tem mais memória para dar. Pare os sites ou workers que não está usando.", + "machine_memory_banner_restart": "Reiniciar com {gb} GB", + "machine_memory_banner_title": "A Podman Machine ficou sem memória {count} vezes desde que iniciou", + "machine_memory_confirm_body": "Todos os contêineres param enquanto a VM reinicia com {gb} GB, e depois o lerd os inicia de novo. Os dados dos sites não são alterados.", + "machine_memory_confirm_title": "Reiniciar a Podman Machine?", "messages_filter_allChannels": "Todos os canais", "meta_code": "PT", "meta_language": "Português", diff --git a/internal/ui/web/messages/ro.json b/internal/ui/web/messages/ro.json index 24c2d2ab6..7fceacd90 100644 --- a/internal/ui/web/messages/ro.json +++ b/internal/ui/web/messages/ro.json @@ -127,6 +127,7 @@ "dashboard_hero_startLerd": "Pornește Lerd", "dashboard_hero_startStep_dns": "Se configurează DNS", "dashboard_hero_startStep_images": "Se verifică imaginile", + "dashboard_hero_startStep_machine": "Se repornește Podman Machine", "dashboard_hero_startStep_preparing": "Se pregătește", "dashboard_hero_startStep_units": "Se pornesc serviciile", "dashboard_hero_startingLerd": "Se pornește...", @@ -397,6 +398,14 @@ "link_noSubdirs": "Niciun subdirector", "link_waitingOutput": "Se așteaptă rezultatul...", "logs_filter_allLevels": "Toate nivelurile", + "machine_memory_banner_dismiss": "Închide", + "machine_memory_banner_failed": "Repornirea nu s-a încheiat: {error}", + "machine_memory_banner_killed": "Oprite: {processes}", + "machine_memory_banner_noRoom": "Acest Mac nu mai are memorie de oferit. Oprește site-urile sau workerii pe care nu îi folosești.", + "machine_memory_banner_restart": "Repornește cu {gb} GB", + "machine_memory_banner_title": "Podman Machine a rămas fără memorie de {count} ori de la pornire", + "machine_memory_confirm_body": "Toate containerele se opresc cât timp VM-ul repornește cu {gb} GB, apoi lerd le pornește din nou. Datele site-urilor nu sunt atinse.", + "machine_memory_confirm_title": "Repornești Podman Machine?", "messages_filter_allChannels": "Toate canalele", "meta_code": "RO", "meta_language": "Română", diff --git a/internal/ui/web/messages/tr.json b/internal/ui/web/messages/tr.json index a2b16e5ef..696f10cef 100644 --- a/internal/ui/web/messages/tr.json +++ b/internal/ui/web/messages/tr.json @@ -127,6 +127,7 @@ "dashboard_hero_startLerd": "Lerd'i başlat", "dashboard_hero_startStep_dns": "DNS yapılandırılıyor", "dashboard_hero_startStep_images": "İmajlar kontrol ediliyor", + "dashboard_hero_startStep_machine": "Podman Machine yeniden başlatılıyor", "dashboard_hero_startStep_preparing": "Hazırlanıyor", "dashboard_hero_startStep_units": "Servisler başlatılıyor", "dashboard_hero_startingLerd": "Başlatılıyor...", @@ -397,6 +398,14 @@ "link_noSubdirs": "Alt dizin yok", "link_waitingOutput": "Çıktı bekleniyor...", "logs_filter_allLevels": "Tüm seviyeler", + "machine_memory_banner_dismiss": "Kapat", + "machine_memory_banner_failed": "Yeniden başlatma tamamlanmadı: {error}", + "machine_memory_banner_killed": "Sonlandırılanlar: {processes}", + "machine_memory_banner_noRoom": "Bu Mac'in verebileceği başka bellek yok. Kullanmadığın siteleri veya worker'ları durdur.", + "machine_memory_banner_restart": "{gb} GB ile yeniden başlat", + "machine_memory_banner_title": "Podman Machine başladığından beri {count} kez belleği tükendi", + "machine_memory_confirm_body": "VM {gb} GB ile yeniden başlarken tüm konteynerler durur, ardından lerd onları yeniden başlatır. Site verilerine dokunulmaz.", + "machine_memory_confirm_title": "Podman Machine yeniden başlatılsın mı?", "messages_filter_allChannels": "Tüm kanallar", "meta_code": "TR", "meta_language": "Türkçe", diff --git a/internal/ui/web/messages/vi.json b/internal/ui/web/messages/vi.json index 39778fc25..f34f583f6 100644 --- a/internal/ui/web/messages/vi.json +++ b/internal/ui/web/messages/vi.json @@ -127,6 +127,7 @@ "dashboard_hero_startLerd": "Khởi động Lerd", "dashboard_hero_startStep_dns": "Đang cấu hình DNS", "dashboard_hero_startStep_images": "Đang kiểm tra image", + "dashboard_hero_startStep_machine": "Đang khởi động lại Podman Machine", "dashboard_hero_startStep_preparing": "Đang chuẩn bị", "dashboard_hero_startStep_units": "Đang khởi động dịch vụ", "dashboard_hero_startingLerd": "Đang khởi động...", @@ -397,6 +398,14 @@ "link_noSubdirs": "Không có thư mục con", "link_waitingOutput": "Đang chờ kết quả...", "logs_filter_allLevels": "Tất cả cấp độ", + "machine_memory_banner_dismiss": "Bỏ qua", + "machine_memory_banner_failed": "Khởi động lại chưa hoàn tất: {error}", + "machine_memory_banner_killed": "Đã dừng: {processes}", + "machine_memory_banner_noRoom": "Máy Mac này không còn bộ nhớ để cấp thêm. Hãy dừng các site hoặc worker không dùng.", + "machine_memory_banner_restart": "Khởi động lại với {gb} GB", + "machine_memory_banner_title": "Podman Machine đã hết bộ nhớ {count} lần kể từ khi khởi động", + "machine_memory_confirm_body": "Mọi container sẽ dừng trong khi VM khởi động lại với {gb} GB, sau đó lerd khởi chạy lại chúng. Dữ liệu site không bị ảnh hưởng.", + "machine_memory_confirm_title": "Khởi động lại Podman Machine?", "messages_filter_allChannels": "Tất cả kênh", "meta_code": "VI", "meta_language": "Tiếng Việt", diff --git a/internal/ui/web/messages/zh.json b/internal/ui/web/messages/zh.json index b06023e56..ea253bf4b 100644 --- a/internal/ui/web/messages/zh.json +++ b/internal/ui/web/messages/zh.json @@ -127,6 +127,7 @@ "dashboard_hero_startLerd": "启动 Lerd", "dashboard_hero_startStep_dns": "正在配置 DNS", "dashboard_hero_startStep_images": "正在检查镜像", + "dashboard_hero_startStep_machine": "正在重启 Podman Machine", "dashboard_hero_startStep_preparing": "正在准备", "dashboard_hero_startStep_units": "正在启动服务", "dashboard_hero_startingLerd": "正在启动...", @@ -397,6 +398,14 @@ "link_noSubdirs": "无子目录", "link_waitingOutput": "等待输出...", "logs_filter_allLevels": "所有级别", + "machine_memory_banner_dismiss": "关闭", + "machine_memory_banner_failed": "重启未完成:{error}", + "machine_memory_banner_killed": "已终止:{processes}", + "machine_memory_banner_noRoom": "这台 Mac 没有更多内存可分配。请停止不用的站点或 worker。", + "machine_memory_banner_restart": "以 {gb} GB 重启", + "machine_memory_banner_title": "Podman Machine 自启动以来已 {count} 次内存不足", + "machine_memory_confirm_body": "虚拟机以 {gb} GB 重启时所有容器都会停止,随后 lerd 会重新启动它们。站点数据不受影响。", + "machine_memory_confirm_title": "重启 Podman Machine?", "messages_filter_allChannels": "所有渠道", "meta_code": "ZH", "meta_language": "简体中文", diff --git a/internal/ui/web/src/App.svelte b/internal/ui/web/src/App.svelte index 92cfe7687..327799d1b 100644 --- a/internal/ui/web/src/App.svelte +++ b/internal/ui/web/src/App.svelte @@ -25,6 +25,7 @@ import { loadFrameworkMarks } from '$stores/frameworkMarks'; import { loadWorkerMarks } from '$stores/workerMarks'; import { loadWorkerHealth } from '$stores/workerHealth'; + import MachineMemoryBanner from './components/MachineMemoryBanner.svelte'; import { watchActiveRun } from '$stores/wizard'; import { connectWs, disconnectWs } from '$lib/ws'; import { initDashboardRoute } from '$stores/dashboard'; @@ -217,6 +218,7 @@
+
diff --git a/internal/ui/web/src/components/BottomBanner.svelte b/internal/ui/web/src/components/BottomBanner.svelte index aa3ac02bb..1748caf06 100644 --- a/internal/ui/web/src/components/BottomBanner.svelte +++ b/internal/ui/web/src/components/BottomBanner.svelte @@ -15,9 +15,10 @@ icon: Snippet; title: string; subtitle: string; - actionLabel: string; + // Left out when there is nothing to offer beyond the message. + actionLabel?: string; dismissLabel: string; - onAction: () => void; + onAction?: () => void; onDismiss: () => void; } = $props(); @@ -29,10 +30,12 @@

{title}

{subtitle}

- + {#if actionLabel && onAction} + + {/if}