Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[RFC] shutdown/notify: allow for custom implementations #243

Closed
wants to merge 2 commits into from

Conversation

flotter
Copy link
Contributor

@flotter flotter commented Jun 21, 2023

The current shutdown and notify logic in pebble is designed with the
assumption that it will run under systemd, or at least that some
systemd components, such as shutdown, is installed.

Allow custom flavours of pebble to change the default assumptions.

  • The daemon now defines a notification and shutdown interface, and
    configures the default implementation to use systemd.

  • The shutdown logic (of which we currently only support reboot) is
    moved to the systemd package, as its based on systemd.

  • Two globals daemon.Notify and daemon.Shutdown now provides a way
    to override the defaults.

This is an alternative approach to what we have in #197

An hypothetical derivative project based on Pebble can now supply its own shutdown and notification implementations before starting the daemon:

daemon.Notify = custom.Notifier 
daemon.Shutdown = custom.Shutdown
:
d, err := daemon.New(&dopts)
:
d.Start()

FakeCommand allows unit tests to check which command-line arguments
were passed, by recording a log of calls with their arguments.

The following examples do not work:

1. shutdown -r "+0" ""

2. echo ""

The last argument supplies the wall message, and an empty argument
is valid. Having this support in place allows code to simply pass
an empty argument for exec.Command(...).

The issue is that both the argument and command delimiter uses null
characters, which makes the command split operation split on the
first occurrence of two null chars, which breaks the examples above.

To add support for this:

- Change the command delimiter from \000\000 => \000\f\n\r

- Update the Calls() function to split the lines correctly.
The current shutdown and notify logic in pebble is designed with the
assumption that it will run under systemd, or at least that some
systemd components, such as shutdown, is installed.

Allow custom flavours of pebble to change the default assumptions.

- The daemon now defines an notification and shutdown interface, and
  configures the default implementation to use systemd.

- The shutdown logic (of which we currently only support reboot) is
  moved to the systemd package, as its based on systemd.

- Two globals daemon.Notify and daemon.Shutdown now provides a way
  to override the defaults.
Copy link
Collaborator

@anpep anpep left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Really clean approach!
Since I've seen pebble have platform-specific code, maybe we could consider moving internals/systemd/{notify,shutdown}.go -> internals/systemd/{notify,shutdown}_linux.go, then have _darwin.go return ErrDarwin.

@@ -717,22 +742,6 @@ func (d *Daemon) doReboot(sigCh chan<- os.Signal, waitTimeout time.Duration) err
return fmt.Errorf("expected reboot did not happen")
}

var shutdownMsg = "reboot scheduled to update the system"
Copy link
Contributor Author

@flotter flotter Jun 21, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This has been moved to internals/systemd/shutdown.go. Note that I am reusing the shutdown command group name here, where the command offers halt, power-off and reboot functionality.

@@ -724,32 +741,6 @@ func (s *daemonSuite) TestRestartSystemWiring(c *check.C) {
c.Check(rebootAt.After(approxAt) || rebootAt.Equal(approxAt), check.Equals, true)
}

func (s *daemonSuite) TestRebootHelper(c *check.C) {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This was moved to internals/systemd/shutdown_test.go.

Comment on lines -929 to -932
notifySocket := filepath.Join(c.MkDir(), "notify.socket")
os.Setenv("NOTIFY_SOCKET", notifySocket)
defer os.Setenv("NOTIFY_SOCKET", "")

Copy link
Contributor Author

@flotter flotter Jun 21, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The folllowing blocks of removals are deliberate, as they add very little value here apart for re-testing the systemd/notify.go functionalty. The exact same tests can already be found under systemd/notify_test.go and removing it here solves the problem that I want to use a mocked notifier when testing this package, not the real notifier as is required by these code snippets.

Comment on lines -947 to -949
c.Assert(d.standbyOpinions.CanStandby(), check.Equals, false)
f, _ := os.Create(notifySocket)
f.Close()
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

See comment above.

Comment on lines -964 to -966
os.Setenv("NOTIFY_SOCKET", c.MkDir())
defer os.Setenv("NOTIFY_SOCKET", "")

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

See comment above.

@@ -114,7 +113,7 @@ func runWatchdog(d *daemon.Daemon) (*time.Ticker, error) {
case <-wt.C:
// TODO: poke the API here and only report WATCHDOG=1 if it
// replies with valid data.
systemd.SdNotify("WATCHDOG=1")
daemon.Notify.Send("WATCHDOG=1")
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not really sure what WATCHDOG=1 does, but it seems like it's pretty specific to systemd? In that case, could it not be a string in the Send interface, but just included in the systemd implementation? Or something like Send(daemon.SendWatchdog) and Send(daemon.SendReady) (not the best API, but spitballing here).


// shutdown defines the required API for reboot, poweroff
// and halt control of the underlying system
type shutdown interface {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Because external packages need to implement these interfaces (and might want to pass around the type), shouldn't they be exported/capitalised?


// Shutdown provides the system specific functionality to
// halt, poweroff or reboot.
Shutdown shutdown = systemd.Shutdown
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It seems really unfortunate that these need to be globals when the Daemon is created via the more typical daemon.New pattern (that is, not a global). Can these somehow be part of the Options struct and not be globals of the daemon package? But I guess the tricky part is then having the alternative CLI customize it.

One approach would be to add Notify and Shutdown to daemon.Options, but then have a ConfigureDaemon that's part of the CLI's "personality". The signature would be ConfigureDaemon func(opts *daemon.Options), and the alternative CLI would do something like:

cmd.Personality.ConfigureDaemon = func(opts *daemon.Options) {
    opts.Notify = mySpecialNotify
    opts.Shutdown = mySpecialShutdown
}

I dunno, maybe it's not any better, but at least it would confine the messy globals to one place, the "personality" struct. Or we could put the globals in the internals/cli package -- putting them in the CLI still seems better to me than in the daemon package.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I like the idea of confinement but is this really a part of the Personality, I can see this behaviour growing with other options, so a new struct to embody them?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think this is part of Personality, because (i) it does not concern other components apart from the daemon, and (ii) Termus will have its own initialization of the daemon during bootstrap.

I'm on board @benhoyt's idea of having those be inside daemon.Options 😃

@flotter flotter closed this Jun 22, 2023
@flotter flotter reopened this Jun 22, 2023
@flotter
Copy link
Contributor Author

flotter commented Jun 22, 2023

Discussed with Gustavo.

One big issue is that even trying to move systemd out of the daemon, by refactoring shutdown and notify, lots of systemd assumptions remain, including the notification messages (as also pointed out by @benhoyt).

Gustavo helped me define a simpler approach. Thank you for everyone's review time. New PR to follow ...

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants