diff --git a/docs/cli.md b/docs/cli.md index e6e64bf..2f008cb 100644 --- a/docs/cli.md +++ b/docs/cli.md @@ -16,6 +16,8 @@ This page lists the `flock` commands. The other pages show where to find each ac | `flock uninstall [--purge]` | Remove Flock. Keeps site settings and databases unless `--purge` is given | | `flock status` | Show the state of the daemon and its services | | `flock restart` | Restart the daemon and all services | +| `flock start` | Start the daemon and all services after `flock stop` | +| `flock stop` | Stop the daemon and all services until `flock start` | | `flock logs [name] [-n 50] [-f]` | Show service logs (daemon, web, php8.5-fpm on macOS and Linux or php8.5-cgi on Windows, postgres, …). Without a name, lists the logs | | `flock update` | Update Flock, CLI and daemon included: the app installs the new version (on Linux, apt or dnf) | | `flock version` | Print the Flock version | diff --git a/docs/installation.md b/docs/installation.md index 762f599..ea97d84 100644 --- a/docs/installation.md +++ b/docs/installation.md @@ -95,3 +95,17 @@ Stop that program, then click **Restart** at the bottom of the Flock panel. On L :::cli Stop that program and run `flock restart`. ::: + +## Stopping Flock + +Quitting the menu bar or tray app leaves Flock running, so your sites keep working. To stop Flock and everything it runs, for example to free memory, or ports 80 and 443 for another local environment: + +:::app +On macOS, switch off the switch at the top of the Flock panel. Switch it on to start Flock again. +::: + +:::cli +Run `flock stop`, and `flock start` to start Flock again. +::: + +Flock starts again at your next login. diff --git a/internal/cli/install_darwin.go b/internal/cli/install_darwin.go index 172795e..20dd748 100644 --- a/internal/cli/install_darwin.go +++ b/internal/cli/install_darwin.go @@ -487,11 +487,26 @@ func appDirs() []string { } } -// restartService makes launchd kill and relaunch the daemon. +// stopDaemon unloads the agent, which stops the daemon and everything it runs, until restartService +// or the next login loads it again. +func stopDaemon() error { + stopService("gui/" + strconv.Itoa(os.Getuid())) + return nil +} + +// restartService makes launchd kill and relaunch the daemon, or loads the agent after `flock stop`. func restartService() error { - target := "gui/" + strconv.Itoa(os.Getuid()) + "/" + launchLabel + domain := "gui/" + strconv.Itoa(os.Getuid()) + target := domain + "/" + launchLabel if err := exec.Command("launchctl", "print", target).Run(); err != nil { - return fmt.Errorf("the background daemon is not installed — run `flock install`") + path, _ := plistPath() + if !serviceInstalled() { + return fmt.Errorf("the background daemon is not installed — run `flock install`") + } + if out, err := exec.Command("launchctl", "bootstrap", domain, path).CombinedOutput(); err != nil { + return fmt.Errorf("launchctl bootstrap: %v: %s", err, out) + } + return nil } if out, err := exec.Command("launchctl", "kickstart", "-k", target).CombinedOutput(); err != nil { return fmt.Errorf("launchctl kickstart: %v: %s", err, out) diff --git a/internal/cli/install_linux.go b/internal/cli/install_linux.go index 21883d7..19d673b 100644 --- a/internal/cli/install_linux.go +++ b/internal/cli/install_linux.go @@ -339,6 +339,11 @@ func uninstallService() error { return systemctl("stop", unitName(name)) } +// stopDaemon makes systemd stop the daemon until restartService or the next boot starts it. +func stopDaemon() error { + return uninstallService() +} + // restartService makes systemd stop and start the daemon. func restartService() error { name, err := serviceUser() diff --git a/internal/cli/install_windows.go b/internal/cli/install_windows.go index 4f85dc1..cb61187 100644 --- a/internal/cli/install_windows.go +++ b/internal/cli/install_windows.go @@ -492,6 +492,18 @@ func stopDaemonProcess() { _ = exec.Command("taskkill", "/F", "/IM", "flockd.exe").Run() } +// stopDaemon ends the scheduled task and the daemon, until restartService or the next login starts it. +func stopDaemon() error { + root, err := paths.Root() + if err != nil { + return err + } + _ = exec.Command("schtasks", "/End", "/TN", taskName).Run() + stopDaemonProcess() + waitDaemonGone(root) + return nil +} + // restartService ends the scheduled task (killing the daemon and everything it runs) and starts it again. func restartService() error { root, err := paths.Root() diff --git a/internal/cli/restart.go b/internal/cli/restart.go index c9a1cf1..e22757f 100644 --- a/internal/cli/restart.go +++ b/internal/cli/restart.go @@ -21,7 +21,11 @@ func newRestartCommand() *cobra.Command { return err } - before, _ := s.client().Status(cmd.Context()) + before, err := s.client().Status(cmd.Context()) + if err == nil && cmd.Name() == "start" { + cmd.Printf("already running (pid %d)\n", before.PID) + return nil + } if err := restartService(); err != nil { return err } @@ -31,7 +35,7 @@ func newRestartCommand() *cobra.Command { for time.Now().Before(deadline) { st, err := s.client().Status(cmd.Context()) if err == nil && st.PID != before.PID { - cmd.Printf("restarted (pid %d)\n", st.PID) + cmd.Printf("%sed (pid %d)\n", cmd.Name(), st.PID) // restarted, started for _, w := range st.Warnings { cmd.Println("warning: " + w) } @@ -46,3 +50,26 @@ func newRestartCommand() *cobra.Command { }, } } + +// newStartCommand starts the daemon after `flock stop` (restartService also loads it again) and +// leaves a running one alone. +func newStartCommand() *cobra.Command { + cmd := newRestartCommand() + cmd.Use, cmd.Short = "start", "Start the background daemon and all services after `flock stop`" + return cmd +} + +func newStopCommand() *cobra.Command { + return &cobra.Command{ + Use: "stop", + Short: "Stop the background daemon and all services until `flock start`", + Args: cobra.NoArgs, + RunE: func(cmd *cobra.Command, _ []string) error { + if err := stopDaemon(); err != nil { + return err + } + cmd.Println("stopped") + return nil + }, + } +} diff --git a/internal/cli/root.go b/internal/cli/root.go index ac2a5ba..f92541e 100644 --- a/internal/cli/root.go +++ b/internal/cli/root.go @@ -24,7 +24,7 @@ func NewRootCommand() *cobra.Command { root.AddCommand( newInstallCommand(), newConflictsCommand(), newUninstallCommand(), newPrivilegedSetupCommand(), - newDaemonCommand(), newRestartCommand(), newStatusCommand(), newLogsCommand(), newVersionCommand(), newUpdateCommand(), + newDaemonCommand(), newRestartCommand(), newStartCommand(), newStopCommand(), newStatusCommand(), newLogsCommand(), newVersionCommand(), newUpdateCommand(), newSitesCommand(), newLinkCommand(), newUnlinkCommand(), newParkCommand(), newUnparkCommand(), newProxyCommand(), newUnproxyCommand(), newProxiesCommand(), newSecureCommand(true), newSecureCommand(false), newDomainCommand(), newWorktreeCommand(), newShareCommand(), newUseCommand(), newIsolateCommand(), newUnisolateCommand(), newIsolatedCommand(), newPinCommand(), newUnpinCommand(), newRootCommand(), diff --git a/macos/Sources/FlockUI/AppModel.swift b/macos/Sources/FlockUI/AppModel.swift index dbe6dc6..de2ac78 100644 --- a/macos/Sources/FlockUI/AppModel.swift +++ b/macos/Sources/FlockUI/AppModel.swift @@ -211,6 +211,20 @@ final class AppModel { /// Runs `flock restart` (which also starts a stopped daemon via launchd). func restart() async { + await run("restart") + } + + /// Runs `flock start` after `stop`. + func start() async { + await run("start") + } + + /// Runs `flock stop`: the daemon and everything it runs stop until `start`. + func stop() async { + await run("stop") + } + + private func run(_ command: String) async { isRestarting = true defer { isRestarting = false } @@ -221,7 +235,7 @@ final class AppModel { : "Flock is not installed for this user (\(binary.path) is missing) — run `flock install` once." return } - let result = await Installation.run(binary, ["restart"]) + let result = await Installation.run(binary, [command]) actionError = result.ok ? nil : result.output await refresh() } diff --git a/macos/Sources/FlockUI/PanelView.swift b/macos/Sources/FlockUI/PanelView.swift index 51646dd..3e23241 100644 --- a/macos/Sources/FlockUI/PanelView.swift +++ b/macos/Sources/FlockUI/PanelView.swift @@ -72,6 +72,14 @@ struct PanelView: View { if model.isRestarting || model.isUpdating { ProgressView().controlSize(.small) } + // Flock's on/off switch: off stops the daemon and everything it runs. + if !model.needsSetup { + Toggle("Flock", isOn: Binding(get: { model.isRunning }, set: { on in Task { on ? await model.start() : await model.stop() } })) + .labelsHidden() + .toggleStyle(.switch) + .controlSize(.small) + .disabled(model.isRestarting || model.isUpdating) + } } } @@ -199,10 +207,10 @@ struct PanelView: View { private func toggles(_ status: Status) -> some View { VStack(alignment: .leading, spacing: 6) { Toggle(isOn: Binding(get: { status.xdebug }, set: { on in Task { await model.setXdebug(on) } })) { - Label("Xdebug", systemImage: "ladybug") + Label("Xdebug", systemImage: "ladybug").frame(maxWidth: .infinity, alignment: .leading) } Toggle(isOn: Binding(get: { status.dumps }, set: { on in Task { await model.setDumps(on) } })) { - Label("Capture dumps & queries", systemImage: "tray.and.arrow.down") + Label("Capture dumps & queries", systemImage: "tray.and.arrow.down").frame(maxWidth: .infinity, alignment: .leading) } } .toggleStyle(.switch) @@ -236,17 +244,6 @@ struct PanelView: View { } .buttonStyle(.borderedProminent) } - } else { - VStack(alignment: .leading, spacing: 8) { - Text(model.isUpdating ? "Updating Flock…" : model.error ?? "Flock is not running") - .font(.callout) - .foregroundStyle(.secondary) - .fixedSize(horizontal: false, vertical: true) - Button("Start Flock") { - Task { await model.restart() } - } - .disabled(model.isRestarting || model.isUpdating) - } } }