diff --git a/Makefile b/Makefile index bbf5d2c8b170eef0fbcf2bdd2e9a5bf39889b2ed..90b56e81c91039242b8069365bdda9a4839ad8eb 100644 --- a/Makefile +++ b/Makefile @@ -8,7 +8,7 @@ Configure.LicenseIdentifier = Apache-2.0 Configure.CopyrightText = Huawei Inc. include z.mk -$(if $(value ZMK.Version),,$(error The project depends on zmk -- Get it from https://github.com/zyga/zmk or ./get-zmk.sh;)) +$(if $(value ZMK.Version),,$(error The project depends on zmk -- Get it from https://github.com/zyga/zmk;)) $(eval $(call ZMK.Import,Directories)) $(eval $(call ZMK.Import,Configure)) diff --git a/cmd/sysotad/cmdsysotad/sysotad.go b/cmd/sysotad/cmdsysotad/sysotad.go index 7fe792d4ed3f1100d6c9d39124bfd022f7a008ae..fd46ad082d9807a72319e1ba6889baef55a9838f 100644 --- a/cmd/sysotad/cmdsysotad/sysotad.go +++ b/cmd/sysotad/cmdsysotad/sysotad.go @@ -14,6 +14,7 @@ import ( "time" "booting.oniroproject.org/distro/components/sysota/dbusutil" + "booting.oniroproject.org/distro/components/sysota/errutil" "booting.oniroproject.org/distro/components/sysota/ota" "booting.oniroproject.org/distro/components/sysota/service" ) @@ -42,46 +43,51 @@ func DefaultOptions() *Options { } } -// Run implements the sysotad binary. -func Run(opts *Options) (err error) { - // Load configuration. - conf, err := ota.LoadConfig(opts.ConfigFiles) - if err != nil { - return err +type stateFile string + +// LoadState loads system state. +func (sf stateFile) LoadState() (*ota.SystemState, error) { + return ota.LoadState(string(sf)) +} + +// SaveState saves system state. +func (sf stateFile) SaveState(st *ota.SystemState) error { + err := ota.SaveState(st, string(sf)) + + // Ignore save errors if the state directory does not exist. + // TODO: this code should go away, it was created early on and does not make + // much sense anymore. + var pathErr *os.PathError + if errors.As(err, &pathErr) && os.IsNotExist(pathErr) { + return nil } - // Load state and defer state save on return. - state, err := ota.LoadState(opts.StateFile) - if err != nil { - return err + // Did we save the state successfully? + if err == nil { + _, _ = fmt.Fprintf(Stdout, "System state saved to %s\n", string(sf)) + return nil } - defer func() { - e := ota.SaveState(state, opts.StateFile) + return err +} - // Ignore save errors if the state directory does not exist. - var pathErr *os.PathError - if errors.As(e, &pathErr) && os.IsNotExist(pathErr) { - return - } +type configFiles struct { + readFiles []string + writeFile string +} - // Did we save the state successfully? - if e == nil { - _, _ = fmt.Fprintf(Stdout, "System state saved to %s\n", opts.StateFile) - return - } +// LoadConfig loads service configuration. +func (cf *configFiles) LoadConfig() (*ota.Config, error) { + return ota.LoadConfig(cf.readFiles) +} - // We failed to save the state. What shall we do with the error (e)? - if err == nil { - // Propagate state save error if possible. - err = e - } else { - // Log state save error if it cannot be propagated (the service - // will exit with a failure code in both cases). - _, _ = fmt.Fprintf(Stderr, "%v\n", e) - } - }() +// SaveConfig saves service configuration. +func (cf *configFiles) SaveConfig(cfg *ota.Config) error { + return ota.SaveConfig(cfg, cf.writeFile) +} +// Run implements the sysotad binary. +func Run(opts *Options) (err error) { // Connect to the system bus and defer disconnect on return. conn, err := dbusutil.SystemBus() if err != nil { @@ -89,14 +95,21 @@ func Run(opts *Options) (err error) { } defer func() { - e := conn.Close() - if err == nil { - err = e - } + errutil.Forward(&err, conn.Close()) }() - // Create the SystemOTA service - svc, err := service.New(conf, state, conn) + exitCh := make(chan struct{}) + defer close(exitCh) + + // Create the SystemOTA service. + svc, err := service.New(conn, + service.WithConfigFiles(&configFiles{ + readFiles: opts.ConfigFiles, + writeFile: ota.DefaultStatefulConfigFile(), + }), + service.WithStateFile(stateFile(opts.StateFile)), + service.WithExitChannel(exitCh), + ) if err != nil { return err } @@ -135,6 +148,10 @@ func Run(opts *Options) (err error) { case sigNum := <-sigCh: _, _ = fmt.Fprintf(Stdout, "Exiting due to signal %v\n", sigNum) + run = false + case <-exitCh: + _, _ = fmt.Fprintf(Stdout, "Exiting due to exit request\n") + run = false } } diff --git a/cmd/sysotad/cmdsysotad/sysotad_test.go b/cmd/sysotad/cmdsysotad/sysotad_test.go index e89fcc31f6cb2d53e4e5d842f41743451d567591..375b9a6c07860320439229de03c15a50c5cc3158 100644 --- a/cmd/sysotad/cmdsysotad/sysotad_test.go +++ b/cmd/sysotad/cmdsysotad/sysotad_test.go @@ -4,7 +4,6 @@ package cmdsysotad_test import ( "bytes" - "io/ioutil" "os" "path/filepath" "testing" @@ -59,80 +58,3 @@ func (s *sysotadSuite) TestStartupAndIdleShutdown(c *C) { "Exiting due to inactivity\n") c.Check(s.stderr.String(), Equals, "") } - -func (s *sysotadSuite) TestStateFileSavedOnExit(c *C) { - d := c.MkDir() - stateFile := filepath.Join(d, "state.ini") - - opts := cmdsysotad.DefaultOptions() - opts.IdleDuration = time.Second - opts.StateFile = stateFile - - err := cmdsysotad.Run(opts) - - c.Assert(err, IsNil) - c.Check(s.stdout.String(), Equals, ""+ - "Listening ...\n"+ - "Exiting due to inactivity\n"+ - "System state saved to "+stateFile+"\n") - c.Check(s.stderr.String(), Equals, "") - - // The zero value is an empty state file but it gets created. - fi, err := os.Stat(stateFile) - c.Assert(err, IsNil) - c.Check(fi.Size(), Equals, int64(0)) -} - -func (s *sysotadSuite) TestStateLoadSaveDance(c *C) { - d := c.MkDir() - stateFile := filepath.Join(d, "state.ini") - err := ioutil.WriteFile(stateFile, []byte("[System]\nBootMode=try\n"), 0600) - c.Assert(err, IsNil) - - opts := cmdsysotad.DefaultOptions() - opts.IdleDuration = time.Second - opts.StateFile = stateFile - - err = cmdsysotad.Run(opts) - c.Assert(err, IsNil) - - c.Check(s.stdout.String(), Equals, ""+ - "Listening ...\n"+ - "Exiting due to inactivity\n"+ - "System state saved to "+stateFile+"\n") - c.Check(s.stderr.String(), Equals, "") - - data, err := ioutil.ReadFile(stateFile) - c.Assert(err, IsNil) - c.Check(data, DeepEquals, []byte("[System]\nBootMode=try\n")) -} - -func (s *sysotadSuite) TestBrokenDBusButWhatAboutState(c *C) { - restore, err := dbustest.SetDBusSystemBusAddress("potato") - c.Assert(err, IsNil) - - defer func() { - c.Assert(restore(), IsNil) - }() - - d := c.MkDir() - stateFile := filepath.Join(d, "state.ini") - err = ioutil.WriteFile(stateFile, []byte("[System]\nBootMode=try\n"), 0600) - c.Assert(err, IsNil) - - opts := cmdsysotad.DefaultOptions() - opts.IdleDuration = time.Second - opts.StateFile = stateFile - - // The service fails to start but doesn't forget to save the state. - err = cmdsysotad.Run(opts) - c.Assert(err, ErrorMatches, `dbus: invalid bus address \(no transport\)`) - - c.Check(s.stdout.String(), Equals, ""+ - "System state saved to "+stateFile+"\n") - c.Check(s.stderr.String(), Equals, "") - - data, err := ioutil.ReadFile(stateFile) - c.Assert(err, IsNil) - c.Check(data, DeepEquals, []byte("[System]\nBootMode=try\n")) -} diff --git a/cmd/sysotad/spread.suite/introspection/introspection-expected-debug.txt b/cmd/sysotad/spread.suite/introspection/introspection-expected-debug.txt index 22ff9cc0bc70e3b3b0c09f49bf1ed0fa2285ee3a..91d785a9fe4488f7cc3782b36b83498628362668 100644 --- a/cmd/sysotad/spread.suite/introspection/introspection-expected-debug.txt +++ b/cmd/sysotad/spread.suite/introspection/introspection-expected-debug.txt @@ -1,32 +1,38 @@ -NAME TYPE SIGNATURE RESULT/VALUE FLAGS -org.freedesktop.DBus.Introspectable interface - - - -.Introspect method - s - -org.freedesktop.DBus.ObjectManager interface - - - -.GetManagedObjects method - a{oa{sa{sv}}} - -.InterfacesAdded signal oa{sa{sv}} - - -.InterfacesRemoved signal oas - - -org.freedesktop.DBus.Properties interface - - - -.Get method ss v - -.GetAll method s a{sv} - -.Set method ssv - - -.PropertiesChanged signal sa{sv}as - - -org.oniroproject.sysota1.BootLoader interface - - - -.CancelSwitch method - - - -.CommitSwitch method - - - -.QueryActive method - s - -.QueryInactive method - s - -.Reboot method u - - -.TrySwitch method s - - -org.oniroproject.sysota1.RAUC interface - - - -.GetPrimary method - s - -.GetState method s s - -.PostInstall method as - - -.PreInstall method as - - -.SetPrimary method s - - -.SetState method ss - - -org.oniroproject.sysota1.Service interface - - - -.UpdateDevice method a{ss} o - -.UpdateStreams method - - - -.Maker property s "dummy-maker" const -.Model property s "dummy-model" const -.Stream property s "dummy-stream" emits-change writable +NAME TYPE SIGNATURE RESULT/VALUE FLAGS +org.freedesktop.DBus.Introspectable interface - - - +.Introspect method - s - +org.freedesktop.DBus.ObjectManager interface - - - +.GetManagedObjects method - a{oa{sa{sv}}} - +.InterfacesAdded signal oa{sa{sv}} - - +.InterfacesRemoved signal oas - - +org.freedesktop.DBus.Properties interface - - - +.Get method ss v - +.GetAll method s a{sv} - +.Set method ssv - - +.PropertiesChanged signal sa{sv}as - - +org.oniroproject.sysota1.BootLoader interface - - - +.CancelSwitch method - - - +.CommitSwitch method - - - +.QueryActive method - s - +.QueryInactive method - s - +.Reboot method u - - +.TrySwitch method s - - +org.oniroproject.sysota1.RAUC interface - - - +.GetPrimary method - s - +.GetState method s s - +.PostInstall method as - - +.PreInstall method as - - +.SetPrimary method s - - +.SetState method ss - - +org.oniroproject.sysota1.Service interface - - - +.UpdateDevice method a{ss} o - +.UpdateStreams method - - - +.Maker property s "test-maker" const +.Model property s "test-model" const +.Stream property s "test-stream" emits-change writable +org.oniroproject.sysota1.Test interface - - - +.Exit method - - - +.LoadConfig method - - - +.LoadState method - - - +.SaveConfig method - - - +.SaveState method - - - diff --git a/cmd/sysotad/spread.suite/introspection/introspection-expected-production.txt b/cmd/sysotad/spread.suite/introspection/introspection-expected-production.txt index 357c8212ec1c12ffe431582220df68e5b6b78ecd..a425b3c93dd4711d5924c0b7fcf6b2d1885a812a 100644 --- a/cmd/sysotad/spread.suite/introspection/introspection-expected-production.txt +++ b/cmd/sysotad/spread.suite/introspection/introspection-expected-production.txt @@ -1,25 +1,25 @@ -NAME TYPE SIGNATURE RESULT/VALUE FLAGS -org.freedesktop.DBus.Introspectable interface - - - -.Introspect method - s - -org.freedesktop.DBus.ObjectManager interface - - - -.GetManagedObjects method - a{oa{sa{sv}}} - -.InterfacesAdded signal oa{sa{sv}} - - -.InterfacesRemoved signal oas - - -org.freedesktop.DBus.Properties interface - - - -.Get method ss v - -.GetAll method s a{sv} - -.Set method ssv - - -.PropertiesChanged signal sa{sv}as - - -org.oniroproject.sysota1.RAUC interface - - - -.GetPrimary method - s - -.GetState method s s - -.PostInstall method as - - -.PreInstall method as - - -.SetPrimary method s - - -.SetState method ss - - -org.oniroproject.sysota1.Service interface - - - -.UpdateDevice method a{ss} o - -.UpdateStreams method - - - -.Maker property s "dummy-maker" const -.Model property s "dummy-model" const -.Stream property s "dummy-stream" emits-change writable +NAME TYPE SIGNATURE RESULT/VALUE FLAGS +org.freedesktop.DBus.Introspectable interface - - - +.Introspect method - s - +org.freedesktop.DBus.ObjectManager interface - - - +.GetManagedObjects method - a{oa{sa{sv}}} - +.InterfacesAdded signal oa{sa{sv}} - - +.InterfacesRemoved signal oas - - +org.freedesktop.DBus.Properties interface - - - +.Get method ss v - +.GetAll method s a{sv} - +.Set method ssv - - +.PropertiesChanged signal sa{sv}as - - +org.oniroproject.sysota1.RAUC interface - - - +.GetPrimary method - s - +.GetState method s s - +.PostInstall method as - - +.PreInstall method as - - +.SetPrimary method s - - +.SetState method ss - - +org.oniroproject.sysota1.Service interface - - - +.UpdateDevice method a{ss} o - +.UpdateStreams method - - - +.Maker property s "test-maker" const +.Model property s "test-model" const +.Stream property s "test-stream" emits-change writable diff --git a/cmd/sysotad/spread.suite/introspection/task.yaml b/cmd/sysotad/spread.suite/introspection/task.yaml index 25ccb7050a233d0392381a4d31d41b3a308fceb3..5bd242c4f68b708987b34657dd977c64bb9ee282 100644 --- a/cmd/sysotad/spread.suite/introspection/task.yaml +++ b/cmd/sysotad/spread.suite/introspection/task.yaml @@ -4,22 +4,22 @@ summary: OTA Service publishes introspection data environment: BOOT_API_DEBUG/on: "true" + TEST_API_DEBUG/on: "true" EXPECTED/on: introspection-expected-debug.txt BOOT_API_DEBUG/off: "false" + TEST_API_DEBUG/off: "false" EXPECTED/off: introspection-expected-production.txt prepare: | - # We are not expecting a config file since we are clobbering things below. - test ! -e /etc/sysota/sysotad.conf - systemctl stop sysotad.service - cat <<SYSOTAD_CONF >/etc/sysota/sysotad.conf + mkdir -p /run/sysota + cat <<SYSOTAD_CONF >/run/sysota/sysotad.conf [Debug] BootAPI = $BOOT_API_DEBUG + TestAPI = $TEST_API_DEBUG SYSOTAD_CONF execute: | busctl introspect org.oniroproject.sysota1 /org/oniroproject/sysota1/Service >introspection-actual.txt diff -u introspection-actual.txt "$EXPECTED" restore: | rm -f introspection-actual.txt - rm -f /etc/sysota/sysotad.conf systemctl stop sysotad.service diff --git a/cmd/sysotad/spread.suite/smoke/task.yaml b/cmd/sysotad/spread.suite/smoke/task.yaml index 53b9291de7f60785cecc0ad58764336a10c0c5ff..185dce93db77e7f61dbd6302fe2b5246c8e36d00 100644 --- a/cmd/sysotad/spread.suite/smoke/task.yaml +++ b/cmd/sysotad/spread.suite/smoke/task.yaml @@ -4,15 +4,20 @@ summary: Initial smoke tests execute: | # The three properties can be read - test "$(busctl get-property org.oniroproject.sysota1 /org/oniroproject/sysota1/Service org.oniroproject.sysota1.Service Maker)" = 's "dummy-maker"' - test "$(busctl get-property org.oniroproject.sysota1 /org/oniroproject/sysota1/Service org.oniroproject.sysota1.Service Model)" = 's "dummy-model"' - test "$(busctl get-property org.oniroproject.sysota1 /org/oniroproject/sysota1/Service org.oniroproject.sysota1.Service Stream)" = 's "dummy-stream"' + test "$(busctl get-property org.oniroproject.sysota1 /org/oniroproject/sysota1/Service org.oniroproject.sysota1.Service Maker)" = 's "test-maker"' + test "$(busctl get-property org.oniroproject.sysota1 /org/oniroproject/sysota1/Service org.oniroproject.sysota1.Service Model)" = 's "test-model"' + test "$(busctl get-property org.oniroproject.sysota1 /org/oniroproject/sysota1/Service org.oniroproject.sysota1.Service Stream)" = 's "test-stream"' # The 'Stream' property can be written as well - test "$(busctl get-property org.oniroproject.sysota1 /org/oniroproject/sysota1/Service org.oniroproject.sysota1.Service Stream)" = 's "dummy-stream"' + test "$(busctl get-property org.oniroproject.sysota1 /org/oniroproject/sysota1/Service org.oniroproject.sysota1.Service Stream)" = 's "test-stream"' test "$(busctl set-property org.oniroproject.sysota1 /org/oniroproject/sysota1/Service org.oniroproject.sysota1.Service Stream s custom)" = '' test "$(busctl get-property org.oniroproject.sysota1 /org/oniroproject/sysota1/Service org.oniroproject.sysota1.Service Stream)" = 's "custom"' + systemctl restart sysotad.service + + # Changes to the 'Stream' property are persistent. + test "$(busctl get-property org.oniroproject.sysota1 /org/oniroproject/sysota1/Service org.oniroproject.sysota1.Service Stream)" = 's "custom"' + # The UpdateStreams method can be called test "$(busctl call org.oniroproject.sysota1 /org/oniroproject/sysota1/Service org.oniroproject.sysota1.Service UpdateStreams)" = '' journalctl -u sysotad.service | MATCH 'called UpdateStreams' diff --git a/cmd/sysotad/spread.suite/state-dir-disk-full/task.yaml b/cmd/sysotad/spread.suite/state-dir-disk-full/task.yaml index b67689443891361e16181002769794adaf4a75b8..0b5efee7152e78c5a723e8354574c55c1e1a9013 100644 --- a/cmd/sysotad/spread.suite/state-dir-disk-full/task.yaml +++ b/cmd/sysotad/spread.suite/state-dir-disk-full/task.yaml @@ -6,10 +6,12 @@ prepare: | # Stop the service ahead of state modifications. systemctl stop sysotad.service - # Grab the cursor to the journal stream associated with the service. We can - # then look at all the messages logged since this log position. This can fail - # if the service never logged anything, so ignore errors. - journalctl -u sysotad.service --cursor-file=cursor >/dev/null || true + # Enable the load-save debug interface. + mkdir -p /run/sysota + cat <<SYSOTAD_CONF >/run/sysota/sysotad.conf + [Debug] + TestAPI = true + SYSOTAD_CONF # Mount a tmpfs with space for exactly two inodes. # XXX(zyga): why two and not one? @@ -24,16 +26,11 @@ prepare: | truncate --size=0 /var/lib/sysota/state.ini execute: | - # Start the service and then stop it, forcing state save. - systemctl start sysotad.service - systemctl stop sysotad.service + # Ask the service to save its state through the debug interface. + busctl call org.oniroproject.sysota1 /org/oniroproject/sysota1/Service org.oniroproject.sysota1.Test SaveState 2>&1 | MATCH 'Call failed: cannot save state: open /var/lib/sysota/tmp-.*: no space left on device' - # The service has failed to write the state file. - test "$(systemctl is-failed sysotad.service)" = failed - journalctl -u sysotad.service --cursor-file=cursor | MATCH 'error: cannot save state: open /var/lib/sysota/tmp-[0-9]+: no space left on device' restore: | - rm -f cursor umount /var/lib/sysota - # This removes the file created by stopping sysotad.service, from the prepare stage. - rm -f /var/lib/sysota/state.ini - rmdir /var/lib/sysota + rm -f /run/sysotad/sysotad.conf + + systemctl restart sysotad.service diff --git a/cmd/sysotad/spread.suite/state-dir-read-only-fs/task.yaml b/cmd/sysotad/spread.suite/state-dir-read-only-fs/task.yaml index 5e64acff89a3dfbc2d6dd79c4861fcc9b69a5c51..a9d276690ccabe21a21fe9d2d76a43efcb145a1f 100644 --- a/cmd/sysotad/spread.suite/state-dir-read-only-fs/task.yaml +++ b/cmd/sysotad/spread.suite/state-dir-read-only-fs/task.yaml @@ -6,10 +6,12 @@ prepare: | # Stop the service ahead of state modifications. systemctl stop sysotad.service - # Grab the cursor to the journal stream associated with the service. We can - # then look at all the messages logged since this log position. This can fail - # if the service never logged anything, so ignore errors. - journalctl -u sysotad.service --cursor-file=cursor >/dev/null || true + # Enable the load-save debug interface. + mkdir -p /run/sysota + cat <<SYSOTAD_CONF >/run/sysota/sysotad.conf + [Debug] + TestAPI = true + SYSOTAD_CONF # Create an empty state file. Later on the semantics of running sysotad with a # read-only /var/lib/sysota but without a state file may change, specifically @@ -25,18 +27,14 @@ prepare: | # system read only. for the purpose of this test it is sufficient to present a # read-only view of the file system, which is allowed by the sandbox. mount -o bind,ro /var/lib/sysota /var/lib/sysota + execute: | - # Start the service and then stop it, forcing state save. - systemctl start sysotad.service - systemctl stop sysotad.service + # Ask the service to save its state through the debug interface. + busctl call org.oniroproject.sysota1 /org/oniroproject/sysota1/Service org.oniroproject.sysota1.Test SaveState 2>&1 | MATCH 'Call failed: cannot save state: open /var/lib/sysota/tmp-.*: read-only file system' - # The service has failed to write the state file. - test "$(systemctl is-failed sysotad.service)" = failed - journalctl -u sysotad.service --cursor-file=cursor | MATCH 'error: cannot save state: open /var/lib/sysota/tmp-[0-9]+: read-only file system' restore: | - rm -f cursor # Remove the bind mount making the sysota directory read-only. umount /var/lib/sysota - # *Then* remove the file which is now normally writable. - rm -f /var/lib/sysota/state.ini - rmdir /var/lib/sysota + rm -f /run/sysotad/sysotad.conf + + systemctl restart sysotad.service diff --git a/cmd/sysotad/spread.suite/state-save-syscalls/task.yaml b/cmd/sysotad/spread.suite/state-save-syscalls/task.yaml index 7afb19c7b3c1de2decff7d265f1f3baa9aa901f1..f592e86d7fe36b66e79174b0f670b9c53bc82198 100644 --- a/cmd/sysotad/spread.suite/state-save-syscalls/task.yaml +++ b/cmd/sysotad/spread.suite/state-save-syscalls/task.yaml @@ -7,30 +7,52 @@ details: | ota.SaveState. We can do that by running sysotad through strace and looking at the sequence of system calls used by sysotad on shutdown. prepare: | - # Ensure that the service is running so that we cannot claim the bus name and exit quickly. - systemctl start sysotad.service + # Stop the service ahead of state modifications. + systemctl stop sysotad.service + + # Enable the load-save debug interface. + mkdir -p /run/sysota + cat <<SYSOTAD_CONF >/run/sysota/sysotad.conf + [Debug] + TestAPI = true + SYSOTAD_CONF + + sed -e 's!ExecStart=.*!Environment=GOMAXPROCS=1\nExecStart=/usr/bin/strace --string-limit=128 --trace=%%file,%%desc,sync,fsync,fdatasync --follow-forks --output='"$(pwd)"'/sysotad.strace /usr/libexec/sysota/sysotad!' </usr/lib/systemd/system/sysotad.service >/etc/systemd/system/sysotad.service + systemctl daemon-reload + systemctl stop sysotad.service # Create a state file we will attempt to read and write to. mkdir -p /var/lib/sysota truncate --size=0 /var/lib/sysota/state.ini + execute: | - GOMAXPROCS=1 strace --trace=%file,%desc,sync,fsync,fdatasync --follow-forks --output=sysotad.strace /usr/libexec/sysota/sysotad 2>&1 | MATCH 'error: cannot claim bus name org.oniroproject.sysota1' + busctl call org.oniroproject.sysota1 /org/oniroproject/sysota1/Service org.oniroproject.sysota1.Test LoadState + busctl call org.oniroproject.sysota1 /org/oniroproject/sysota1/Service org.oniroproject.sysota1.Test SaveState + systemctl stop sysotad.service # NOTE: MATCH is fancy way to call "grep -E" and show the input on failure. - # We open the file for reading on startup. - MATCH '^[0-9]+\s+openat\(AT_FDCWD, "/var/lib/sysota/state.ini", O_RDONLY\|O_CLOEXEC\) = 3$' <sysotad.strace + # We this is how we read the state. + MATCH '^[0-9]+\s+openat\(AT_FDCWD, "/var/lib/sysota/state.ini", O_RDONLY\|O_CLOEXEC\) = 7$' <sysotad.strace + MATCH '^[0-9]+\s+read\(7, "", 4096\)\s+= 0$' <sysotad.strace + # This is how we write the state. + # # We we do the open+rename+fsync+fsync (dir) dance on shutdown. # Note that fsync may be interrupted so we try to match both: # 47447 fsync(3 <unfinished ...> # 47447 fsync(7) = 0 - MATCH '^[0-9]+\s+openat\(AT_FDCWD, "/var/lib/sysota/tmp-[0-9]+", O_RDWR|O_CREAT|O_EXCL\|O_CLOEXEC, 0600\) = 3$' <sysotad.strace - MATCH '^[0-9]+\s+openat\(AT_FDCWD, "/var/lib/sysota", O_RDONLY\|O_CLOEXEC\) = 7$' <sysotad.strace - MATCH '^[0-9]+\s+fsync\(3( <unfinished \.\.\.>|\)\s+= 0)$' <sysotad.strace - MATCH '^[0-9]+\s+renameat\(AT_FDCWD, "/var/lib/sysota/tmp-[0-9]+", AT_FDCWD, "/var/lib/sysota/state.ini"( <unfinished \.\.\.>|\) = 0)$' <sysotad.strace + MATCH '^[0-9]+\s+openat\(AT_FDCWD, "/var/lib/sysota/tmp-[0-9]+", O_RDWR|O_CREAT|O_EXCL\|O_CLOEXEC, 0600\) = 7$' <sysotad.strace + MATCH '^[0-9]+\s+openat\(AT_FDCWD, "/var/lib/sysota", O_RDONLY\|O_CLOEXEC\) = 8$' <sysotad.strace MATCH '^[0-9]+\s+fsync\(7( <unfinished \.\.\.>|\)\s+= 0)$' <sysotad.strace + MATCH '^[0-9]+\s+renameat\(AT_FDCWD, "/var/lib/sysota/tmp-[0-9]+", AT_FDCWD, "/var/lib/sysota/state.ini"( <unfinished \.\.\.>|\) = 0)$' <sysotad.strace + MATCH '^[0-9]+\s+fsync\(8( <unfinished \.\.\.>|\)\s+= 0)$' <sysotad.strace restore: | rm -f sysotad.strace rm -f /var/lib/sysota/state.ini rmdir /var/lib/sysota + rm -f /run/sysotad/sysotad.conf + + rm -f /etc/systemd/system/sysotad.service + systemctl daemon-reload + systemctl restart sysotad.service diff --git a/dbusutil/servicehost_test.go b/dbusutil/servicehost_test.go index c5df56f1cbfa557d903b9e45f31005ac22498664..bc79a4ade735e72c91b4537c03bfe6beefeb856f 100644 --- a/dbusutil/servicehost_test.go +++ b/dbusutil/servicehost_test.go @@ -40,7 +40,6 @@ type TestService struct { func (s *serviceHostSuite) TestUsageInit(c *C) { var sh dbusutil.ServiceHost - ts := TestService{} sh.Init(s.conn) @@ -54,7 +53,6 @@ func (s *serviceHostSuite) TestUsageInit(c *C) { // Register a bus name to make testing easier. reply, err := s.conn.RequestName("org.example.Test", dbus.NameFlagDoNotQueue) - c.Assert(err, IsNil) c.Assert(reply, Equals, dbus.RequestNameReplyPrimaryOwner) diff --git a/get-zmk.sh b/get-zmk.sh deleted file mode 100755 index 8a618272a2bc271f3e0033ef6b70c210b6ab9e3c..0000000000000000000000000000000000000000 --- a/get-zmk.sh +++ /dev/null @@ -1,19 +0,0 @@ -#!/bin/sh -# SPDX-License-Identifier: Apache-2.0 -# SPDX-FileCopyrightText: Huawei Inc. -set -ex - -D="$(mktemp -d)" -trap 'rm -rf "$D"' EXIT - -cd "$D" - -wget https://github.com/zyga/zmk/releases/download/v0.5.1/zmk-0.5.1.tar.gz -wget https://github.com/zyga/zmk/releases/download/v0.5.1/zmk-0.5.1.tar.gz.asc - -gpg --keyserver keyserver.ubuntu.com --recv-keys B76CED9B45CAF1557D271A6A2894E93A28C67B47 -gpg --verify zmk-0.5.1.tar.gz.asc - -tar zxf zmk-0.5.1.tar.gz -make -C zmk-0.5.1 -sudo make -C zmk-0.5.1 install diff --git a/ota/ota.go b/ota/ota.go index a424add93cc1f5de92c1ce9797f5e88c6475838c..8f3cdd26b206c11f721db2af275ba93e0c7303bc 100644 --- a/ota/ota.go +++ b/ota/ota.go @@ -108,6 +108,9 @@ type Config struct { // to use it over D-Bus but making that available does help with testing. DebugBootAPI bool `ini:"BootAPI,[Debug],omitempty"` + // DebugTestAPI enables exposing the state and config API over D-Bus. + DebugTestAPI bool `ini:"TestAPI,omitempty"` + // Properties of the device and the update server. // DeviceMaker identifies the maker of the device. diff --git a/ota/raucadapter/raucadapter.go b/ota/raucadapter/raucadapter.go index 34b4b2070bfa891fba270f5d8f6392723777307b..6e5e1f6d51769ee600aec73bec3ab0f0a526b8cf 100644 --- a/ota/raucadapter/raucadapter.go +++ b/ota/raucadapter/raucadapter.go @@ -10,6 +10,7 @@ import ( "errors" "booting.oniroproject.org/distro/components/sysota/boot" + "booting.oniroproject.org/distro/components/sysota/ota" "booting.oniroproject.org/distro/components/sysota/rauc/installhandler" ) @@ -22,17 +23,8 @@ var ErrProtocolViolation = errors.New("violation of protocol between SystemOTA a // Adapter uses SystemOTA boot.Protocol and ota.SystemState to implement boothandler.Handler. type Adapter struct { - proto boot.Protocol - bootModeHolder BootModeHolder -} - -// BootModeHolder is an interface exposing boot mode control. -// -// This interface exits, mainly, so that modifications of BootMode can be -// broadcast over D-Bus. -type BootModeHolder interface { - BootMode() boot.Mode - SetBootMode(boot.Mode) error + proto boot.Protocol + ps *ota.StateGuard } // New returns a new Adapter implemented wrapping given boot.Protocol. @@ -43,8 +35,8 @@ type BootModeHolder interface { // interface and has only one element of state that is not captured in the boot // loader itself, namely boot.Mode, where the system may be expected to normally // or in the one-off try mode. -func New(proto boot.Protocol, bootModeHolder BootModeHolder) *Adapter { - return &Adapter{proto: proto, bootModeHolder: bootModeHolder} +func New(proto boot.Protocol, ps *ota.StateGuard) *Adapter { + return &Adapter{proto: proto, ps: ps} } // PrimarySlot returns the name of the RAUC primary boot slot. @@ -53,7 +45,7 @@ func New(proto boot.Protocol, bootModeHolder BootModeHolder) *Adapter { // state is used to pick either active slot (in normal mode) or inactive slot // (in try mode). func (adapter *Adapter) PrimarySlot() (boot.Slot, error) { - switch adapter.bootModeHolder.BootMode() { + switch adapter.bootMode() { case boot.Normal: return adapter.queryActiveOrPristine() case boot.Try: @@ -85,7 +77,7 @@ func (adapter *Adapter) SetPrimarySlot(slot boot.Slot) error { return err } - switch adapter.bootModeHolder.BootMode() { + switch adapter.bootMode() { case boot.Normal: if slot == activeSlot { // No-op, in normal mode, the active slot is always primary. @@ -100,7 +92,7 @@ func (adapter *Adapter) SetPrimarySlot(slot boot.Slot) error { return err } - return adapter.bootModeHolder.SetBootMode(boot.Try) + return adapter.setBootMode(boot.Try) case boot.Try: if slot == activeSlot { // Protocol violation, this does not happen during "rauc install". @@ -182,13 +174,12 @@ func (adapter *Adapter) SetSlotState(slot boot.Slot, state boot.SlotState) error } inactive := boot.SynthesizeInactiveSlot(active) - bootMode := adapter.bootModeHolder.BootMode() // Switch by boot mode, then switch by slot state and finally look at the // slot we've got. This exact same logic can be re-written in several // different styles but I've found this to be easiest to grasp while // retaining consistency. - switch bootMode { + switch adapter.bootMode() { case boot.Normal: switch state { case boot.GoodSlot: @@ -227,13 +218,13 @@ func (adapter *Adapter) SetSlotState(slot boot.Slot, state boot.SlotState) error return err } - return adapter.bootModeHolder.SetBootMode(boot.Normal) + return adapter.setBootMode(boot.Normal) case boot.BadSlot: if slot == inactive { // Rollback of an update transaction. // Note that we set boot mode even if CancelSwitch failed because by // this time, we have rolled back already. - if err := adapter.bootModeHolder.SetBootMode(boot.Normal); err != nil { + if err := adapter.setBootMode(boot.Normal); err != nil { return err } @@ -294,3 +285,18 @@ func (adapter *Adapter) queryInactiveOrPristine() (boot.Slot, error) { return active, err } + +func (adapter *Adapter) bootMode() boot.Mode { + return adapter.ps.State().BootMode +} + +func (adapter *Adapter) setBootMode(bootMode boot.Mode) error { + return adapter.ps.AlterState(func(st *ota.SystemState) (bool, error) { + if st.BootMode == bootMode { + return false, nil + } + + st.BootMode = bootMode + return true, nil + }) +} diff --git a/ota/raucadapter/raucadapter_test.go b/ota/raucadapter/raucadapter_test.go index fe0cf39e929bed6dcde2e049ac3262a74119d3c5..84b2267ee7a6bb32b87e512bee189c31ebb32945 100644 --- a/ota/raucadapter/raucadapter_test.go +++ b/ota/raucadapter/raucadapter_test.go @@ -11,6 +11,7 @@ import ( "booting.oniroproject.org/distro/components/sysota/boot" "booting.oniroproject.org/distro/components/sysota/boot/testboot" + "booting.oniroproject.org/distro/components/sysota/ota" "booting.oniroproject.org/distro/components/sysota/ota/raucadapter" "booting.oniroproject.org/distro/components/sysota/rauc/installhandler" "booting.oniroproject.org/distro/components/sysota/rauc/rauctest" @@ -21,10 +22,10 @@ func Test(t *testing.T) { TestingT(t) } var errBoom = errors.New("boom") type adapterSuite struct { - proto testboot.Protocol - bootState rauctest.BootState - adapter *raucadapter.Adapter - called int + proto testboot.Protocol + ps ota.StateGuard + adapter *raucadapter.Adapter + called int // variant holds the description of the test suite variant. variant CommentInterface @@ -38,19 +39,22 @@ type adapterSuite struct { } func (s *adapterSuite) SetUpSuite(c *C) { - s.adapter = raucadapter.New(&s.proto, &s.bootState) + s.adapter = raucadapter.New(&s.proto, &s.ps) } func (s *adapterSuite) SetUpTest(c *C) { s.cleanSlate(c) // This is only set in the per-test setup so that individual tests // can call cleanSlate and still observe the changes to the boot mode. - c.Assert(s.bootState.SetBootMode(boot.Normal), IsNil) + err := s.ps.AlterState(func(st *ota.SystemState) (bool, error) { + st.BootMode = boot.Normal + return true, nil + }) + c.Assert(err, IsNil) } func (s *adapterSuite) cleanSlate(c *C) { s.proto.ResetCallbacks() - s.bootState.BootModeChanged = nil s.called = 0 } @@ -97,7 +101,7 @@ func (s *adapterSuite) TestInstallGoodPlaythrough(c *C) { err := s.adapter.SetSlotState(s.toSlot, boot.BadSlot) c.Assert(err, IsNil) c.Assert(s.called, Equals, 1) - c.Check(s.bootState.BootMode(), Equals, boot.Normal) + c.Check(s.ps.State().BootMode, Equals, boot.Normal) // custom: set-primary $toSlot s.cleanSlate(c) @@ -124,7 +128,7 @@ func (s *adapterSuite) TestInstallGoodPlaythrough(c *C) { err = s.adapter.SetPrimarySlot(s.toSlot) c.Assert(err, IsNil) c.Assert(s.called, Equals, 2, s.variant) - c.Check(s.bootState.BootMode(), Equals, boot.Try) + c.Check(s.ps.State().BootMode, Equals, boot.Try) // TODO(zyga): Conceptually, we would reboot with the special flag here. // This is not tested yet, as it requires cooperation abetween RAUC @@ -147,7 +151,7 @@ func (s *adapterSuite) TestInstallGoodPlaythrough(c *C) { c.Assert(err, IsNil) c.Check(slot, Equals, s.toSlot) c.Assert(s.called, Equals, 1) - c.Check(s.bootState.BootMode(), Equals, boot.Try) + c.Check(s.ps.State().BootMode, Equals, boot.Try) // custom: get-state $toSlot s.cleanSlate(c) @@ -165,7 +169,7 @@ func (s *adapterSuite) TestInstallGoodPlaythrough(c *C) { c.Assert(err, IsNil) c.Check(state, Equals, boot.BadSlot) c.Assert(s.called, Equals, 1) - c.Check(s.bootState.BootMode(), Equals, boot.Try) + c.Check(s.ps.State().BootMode, Equals, boot.Try) // custom: get-state $fromSlot s.cleanSlate(c) @@ -183,7 +187,7 @@ func (s *adapterSuite) TestInstallGoodPlaythrough(c *C) { c.Assert(err, IsNil) c.Check(state, Equals, boot.GoodSlot, s.variant) c.Assert(s.called, Equals, 1) - c.Check(s.bootState.BootMode(), Equals, boot.Try) + c.Check(s.ps.State().BootMode, Equals, boot.Try) // custom: set-state $toSlot good s.cleanSlate(c) @@ -206,7 +210,7 @@ func (s *adapterSuite) TestInstallGoodPlaythrough(c *C) { c.Assert(err, IsNil) c.Assert(s.called, Equals, 2) // Note that boot.Mode switches to Normal. - c.Check(s.bootState.BootMode(), Equals, boot.Normal) + c.Check(s.ps.State().BootMode, Equals, boot.Normal) } func (s *adapterSuite) TestInstallBadPlaythrough(c *C) { @@ -230,7 +234,7 @@ func (s *adapterSuite) TestInstallBadPlaythrough(c *C) { err := s.adapter.SetSlotState(s.toSlot, boot.BadSlot) c.Assert(err, IsNil) c.Assert(s.called, Equals, 1) - c.Check(s.bootState.BootMode(), Equals, boot.Normal) + c.Check(s.ps.State().BootMode, Equals, boot.Normal) // custom: set-primary $toSlot s.cleanSlate(c) @@ -256,7 +260,7 @@ func (s *adapterSuite) TestInstallBadPlaythrough(c *C) { err = s.adapter.SetPrimarySlot(s.toSlot) c.Assert(err, IsNil) c.Assert(s.called, Equals, 2, s.variant) - c.Check(s.bootState.BootMode(), Equals, boot.Try) + c.Check(s.ps.State().BootMode, Equals, boot.Try) // TODO(zyga): Conceptually, we would reboot with the special flag here. // This is not tested yet, as it requires cooperation abetween RAUC @@ -278,7 +282,7 @@ func (s *adapterSuite) TestInstallBadPlaythrough(c *C) { c.Assert(err, IsNil) c.Check(slot, Equals, s.toSlot) c.Assert(s.called, Equals, 1) - c.Check(s.bootState.BootMode(), Equals, boot.Try) + c.Check(s.ps.State().BootMode, Equals, boot.Try) // custom: get-state $toSlot s.cleanSlate(c) @@ -296,7 +300,7 @@ func (s *adapterSuite) TestInstallBadPlaythrough(c *C) { c.Assert(err, IsNil) c.Check(state, Equals, boot.BadSlot) c.Assert(s.called, Equals, 1) - c.Check(s.bootState.BootMode(), Equals, boot.Try) + c.Check(s.ps.State().BootMode, Equals, boot.Try) // custom: get-state $fromSlot s.cleanSlate(c) @@ -314,7 +318,7 @@ func (s *adapterSuite) TestInstallBadPlaythrough(c *C) { c.Assert(err, IsNil) c.Check(state, Equals, boot.GoodSlot) c.Assert(s.called, Equals, 1) - c.Check(s.bootState.BootMode(), Equals, boot.Try) + c.Check(s.ps.State().BootMode, Equals, boot.Try) // custom: set-state $toSlot bad s.cleanSlate(c) @@ -337,13 +341,16 @@ func (s *adapterSuite) TestInstallBadPlaythrough(c *C) { c.Assert(err, IsNil) c.Assert(s.called, Equals, 2) // Note that boot.Mode switches to Normal. - c.Check(s.bootState.BootMode(), Equals, boot.Normal) + c.Check(s.ps.State().BootMode, Equals, boot.Normal) } // Tests for BootProtocolAdapter.PrimarySlot. func (s *adapterSuite) TestPrimarySlotInNormalMode(c *C) { - c.Assert(s.bootState.SetBootMode(boot.Normal), IsNil) + c.Assert(s.ps.AlterState(func(st *ota.SystemState) (bool, error) { + st.BootMode = boot.Normal + return true, nil + }), IsNil) s.proto.QueryActiveFn = func() (boot.Slot, error) { s.called++ @@ -357,11 +364,14 @@ func (s *adapterSuite) TestPrimarySlotInNormalMode(c *C) { c.Assert(err, IsNil) c.Check(slot, Equals, s.fromSlot) c.Assert(s.called, Equals, 1) - c.Check(s.bootState.BootMode(), Equals, boot.Normal) + c.Check(s.ps.State().BootMode, Equals, boot.Normal) } func (s *adapterSuite) TestPrimarySlotInTryMode(c *C) { - c.Assert(s.bootState.SetBootMode(boot.Try), IsNil) + c.Assert(s.ps.AlterState(func(st *ota.SystemState) (bool, error) { + st.BootMode = boot.Try + return true, nil + }), IsNil) s.proto.QueryInactiveFn = func() (boot.Slot, error) { s.called++ @@ -375,11 +385,14 @@ func (s *adapterSuite) TestPrimarySlotInTryMode(c *C) { c.Assert(err, IsNil, s.variant) c.Check(slot, Equals, s.toSlot) c.Assert(s.called, Equals, 1) - c.Check(s.bootState.BootMode(), Equals, boot.Try) + c.Check(s.ps.State().BootMode, Equals, boot.Try) } func (s *adapterSuite) TestPrimarySlotInInvalidMode(c *C) { - c.Assert(s.bootState.SetBootMode(boot.InvalidBootMode), IsNil) + c.Assert(s.ps.AlterState(func(st *ota.SystemState) (bool, error) { + st.BootMode = boot.InvalidBootMode + return true, nil + }), IsNil) _, err := s.adapter.PrimarySlot() c.Assert(err, ErrorMatches, `invalid boot mode`) } @@ -387,7 +400,10 @@ func (s *adapterSuite) TestPrimarySlotInInvalidMode(c *C) { // Tests for BootProtocolAdapter.SetPrimarySlot. func (s *adapterSuite) TestSetPrimarySlotToActiveInNormalMode(c *C) { - c.Assert(s.bootState.SetBootMode(boot.Normal), IsNil) + c.Assert(s.ps.AlterState(func(st *ota.SystemState) (bool, error) { + st.BootMode = boot.Normal + return true, nil + }), IsNil) s.proto.QueryActiveFn = func() (boot.Slot, error) { s.called++ @@ -400,7 +416,7 @@ func (s *adapterSuite) TestSetPrimarySlotToActiveInNormalMode(c *C) { err := s.adapter.SetPrimarySlot(s.fromSlot) c.Assert(err, IsNil) c.Assert(s.called, Equals, 1) - c.Check(s.bootState.BootMode(), Equals, boot.Normal) + c.Check(s.ps.State().BootMode, Equals, boot.Normal) } func (s *adapterSuite) TestSetPrimaryToInactiveInNormalMode(c *C) { @@ -418,11 +434,14 @@ func (s *adapterSuite) TestSetPrimaryToInactiveInNormalMode(c *C) { } err := s.adapter.SetPrimarySlot(s.toSlot) c.Assert(err, IsNil) - c.Check(s.bootState.BootMode(), Equals, boot.Try, s.variant) + c.Check(s.ps.State().BootMode, Equals, boot.Try, s.variant) } func (s *adapterSuite) TestSetPrimarySlotToActiveInTryMode(c *C) { - c.Assert(s.bootState.SetBootMode(boot.Try), IsNil) + c.Assert(s.ps.AlterState(func(st *ota.SystemState) (bool, error) { + st.BootMode = boot.Try + return true, nil + }), IsNil) s.proto.QueryActiveFn = func() (boot.Slot, error) { if s.pristine { return boot.InvalidSlot, boot.ErrPristineBootConfig @@ -443,7 +462,10 @@ func (s *adapterSuite) TestSetPrimarySlotToInactiveInTryMode(c *C) { activeSlot := s.fromSlot inactiveSlot := boot.SynthesizeInactiveSlot(activeSlot) - c.Assert(s.bootState.SetBootMode(boot.Try), IsNil) + c.Assert(s.ps.AlterState(func(st *ota.SystemState) (bool, error) { + st.BootMode = boot.Try + return true, nil + }), IsNil) s.proto.QueryActiveFn = func() (boot.Slot, error) { s.called++ @@ -456,7 +478,7 @@ func (s *adapterSuite) TestSetPrimarySlotToInactiveInTryMode(c *C) { err := s.adapter.SetPrimarySlot(inactiveSlot) c.Assert(err, IsNil, s.variant) c.Assert(s.called, Equals, 1) - c.Check(s.bootState.BootMode(), Equals, boot.Try) + c.Check(s.ps.State().BootMode, Equals, boot.Try) } func (s *adapterSuite) TestSetPrimarySlotToPrimarySlot(c *C) { @@ -478,15 +500,17 @@ func (s *adapterSuite) TestSetPrimarySlotToPrimarySlot(c *C) { for _, bootMode := range []boot.Mode{boot.Normal, boot.Try} { comment := Commentf("%s, boot mode: %s", s.variant.CheckCommentString(), bootMode) - err := s.bootState.SetBootMode(bootMode) - c.Assert(err, IsNil, comment) + c.Assert(s.ps.AlterState(func(st *ota.SystemState) (bool, error) { + st.BootMode = bootMode + return true, nil + }), IsNil, comment) primarySlot, err := s.adapter.PrimarySlot() c.Assert(err, IsNil, comment) err = s.adapter.SetPrimarySlot(primarySlot) c.Assert(err, IsNil, comment) - c.Check(s.bootState.BootMode(), Equals, bootMode, comment) + c.Check(s.ps.State().BootMode, Equals, bootMode, comment) } } @@ -519,12 +543,14 @@ func (s *adapterSuite) TestSetPrimaryToInactiveButTrySwitchFailed(c *C) { c.Assert(err, ErrorMatches, `boom`, s.variant) // Note that a transaction was not started. - c.Check(s.bootState.BootMode(), Equals, boot.Normal) + c.Check(s.ps.State().BootMode, Equals, boot.Normal) } func (s *adapterSuite) TestSetPrimaryButBootModeIsInvalid(c *C) { - err := s.bootState.SetBootMode(boot.InvalidBootMode) - c.Check(err, IsNil) + c.Assert(s.ps.AlterState(func(st *ota.SystemState) (bool, error) { + st.BootMode = boot.InvalidBootMode + return true, nil + }), IsNil) s.proto.QueryActiveFn = func() (boot.Slot, error) { if s.pristine { @@ -534,7 +560,7 @@ func (s *adapterSuite) TestSetPrimaryButBootModeIsInvalid(c *C) { return s.fromSlot, nil } - err = s.adapter.SetPrimarySlot(s.fromSlot) + err := s.adapter.SetPrimarySlot(s.fromSlot) c.Assert(err, ErrorMatches, `invalid boot mode`, s.variant) } @@ -645,7 +671,10 @@ func (s *adapterSuite) TestSetSlotStateOfActiveSlotInTryModeToBad(c *C) { // that rauc reads and writes the state and any combination can be // expressed. SystemOTA synthesizes the state that RAUC expects based on the // boot.Mode alone (that is, one bit of information). - c.Assert(s.bootState.SetBootMode(boot.Try), IsNil) + c.Assert(s.ps.AlterState(func(st *ota.SystemState) (bool, error) { + st.BootMode = boot.Try + return true, nil + }), IsNil) s.proto.QueryActiveFn = func() (boot.Slot, error) { if s.pristine { return boot.InvalidSlot, boot.ErrPristineBootConfig @@ -658,7 +687,10 @@ func (s *adapterSuite) TestSetSlotStateOfActiveSlotInTryModeToBad(c *C) { } func (s *adapterSuite) TestSetSlotStateOfActiveSlotInTryModeToGood(c *C) { - c.Assert(s.bootState.SetBootMode(boot.Try), IsNil) + c.Assert(s.ps.AlterState(func(st *ota.SystemState) (bool, error) { + st.BootMode = boot.Try + return true, nil + }), IsNil) s.proto.QueryActiveFn = func() (boot.Slot, error) { if s.pristine { return boot.InvalidSlot, boot.ErrPristineBootConfig @@ -673,7 +705,10 @@ func (s *adapterSuite) TestSetSlotStateOfActiveSlotInTryModeToGood(c *C) { func (s *adapterSuite) TestSetSlotStateOfInactiveSlotInTryModeToBad(c *C) { // While in try-boot mode, setting the state of the inactive slot // to bad cancels the update transaction. - c.Assert(s.bootState.SetBootMode(boot.Try), IsNil) + c.Assert(s.ps.AlterState(func(st *ota.SystemState) (bool, error) { + st.BootMode = boot.Try + return true, nil + }), IsNil) s.proto.QueryActiveFn = func() (boot.Slot, error) { s.called++ if s.pristine { @@ -692,13 +727,16 @@ func (s *adapterSuite) TestSetSlotStateOfInactiveSlotInTryModeToBad(c *C) { err := s.adapter.SetSlotState(s.toSlot, boot.BadSlot) c.Assert(err, IsNil, s.variant) c.Assert(s.called, Equals, 2) - c.Check(s.bootState.BootMode(), Equals, boot.Normal) + c.Check(s.ps.State().BootMode, Equals, boot.Normal) } func (s *adapterSuite) TestSetSlotStateOfInactiveSlotInTryModeToGood(c *C) { // While in try-boot mode, setting the state of the inactive slot // to good commits the update transaction. - c.Assert(s.bootState.SetBootMode(boot.Try), IsNil) + c.Assert(s.ps.AlterState(func(st *ota.SystemState) (bool, error) { + st.BootMode = boot.Try + return true, nil + }), IsNil) s.proto.QueryActiveFn = func() (boot.Slot, error) { s.called++ // Note that $fromSlot is active. @@ -713,7 +751,7 @@ func (s *adapterSuite) TestSetSlotStateOfInactiveSlotInTryModeToGood(c *C) { err := s.adapter.SetSlotState(s.toSlot, boot.GoodSlot) c.Assert(err, IsNil) c.Assert(s.called, Equals, 2) - c.Check(s.bootState.BootMode(), Equals, boot.Normal) + c.Check(s.ps.State().BootMode, Equals, boot.Normal) } func (s *adapterSuite) TestSetSlotStateOfInvalidSlot(c *C) { @@ -737,7 +775,10 @@ func (s *adapterSuite) TestSetSlotStateButQueryActiveFailed(c *C) { func (s *adapterSuite) TestSetSlotStateOfInactiveSlotInTryModeToBadButSetBootModeFailed(c *C) { // We are trying to cancel the transaction but we failed to set the new boot mode. - c.Assert(s.bootState.SetBootMode(boot.Try), IsNil) + c.Assert(s.ps.AlterState(func(st *ota.SystemState) (bool, error) { + st.BootMode = boot.Try + return true, nil + }), IsNil) s.proto.QueryActiveFn = func() (boot.Slot, error) { s.called++ if s.pristine { @@ -746,21 +787,26 @@ func (s *adapterSuite) TestSetSlotStateOfInactiveSlotInTryModeToBadButSetBootMod return s.fromSlot, nil } - s.bootState.BootModeChanged = func(bootMode boot.Mode) error { - c.Assert(bootMode, Equals, boot.Normal) + s.ps.RegisterObserver("test", func(st *ota.SystemState) error { + c.Assert(st.BootMode, Equals, boot.Normal) s.called++ return errBoom - } + }) + defer s.ps.RemoveObserver("test") + err := s.adapter.SetSlotState(s.toSlot, boot.BadSlot) c.Assert(err, ErrorMatches, `boom`, s.variant) c.Assert(s.called, Equals, 2) - c.Check(s.bootState.BootMode(), Equals, boot.Try) + c.Check(s.ps.State().BootMode, Equals, boot.Try) } func (s *adapterSuite) TestSetSlotStateOfInactiveSlotInTryModeToBadButCancelSwitchFailed(c *C) { // We are trying to cancel the transaction but that operation fails. - c.Assert(s.bootState.SetBootMode(boot.Try), IsNil) + c.Assert(s.ps.AlterState(func(st *ota.SystemState) (bool, error) { + st.BootMode = boot.Try + return true, nil + }), IsNil) s.proto.QueryActiveFn = func() (boot.Slot, error) { s.called++ @@ -786,13 +832,16 @@ func (s *adapterSuite) TestSetSlotStateOfInactiveSlotInTryModeToBadButCancelSwit // // CancelSwitch exists only to (perhaps) remove temporary files that were // required for the duration of TrySwitch lifetime. - c.Check(s.bootState.BootMode(), Equals, boot.Normal) + c.Check(s.ps.State().BootMode, Equals, boot.Normal) } func (s *adapterSuite) TestSetSlotStateOfInactiveSlotInTryModeToGoodButCommitSwitchFailed(c *C) { // While in try-boot mode, setting the state of the inactive slot // to good commits the update transaction. - c.Assert(s.bootState.SetBootMode(boot.Try), IsNil) + c.Assert(s.ps.AlterState(func(st *ota.SystemState) (bool, error) { + st.BootMode = boot.Try + return true, nil + }), IsNil) s.proto.QueryActiveFn = func() (boot.Slot, error) { s.called++ @@ -821,7 +870,7 @@ func (s *adapterSuite) TestSetSlotStateOfInactiveSlotInTryModeToGoodButCommitSwi // reboot. // // XXX(zyga): This may warrant a special error type to handle properly. - c.Check(s.bootState.BootMode(), Equals, boot.Try) + c.Check(s.ps.State().BootMode, Equals, boot.Try) } func (s *adapterSuite) TestPreInstallHandlerWithUnawareBootProtocol(c *C) { @@ -843,7 +892,7 @@ func (s *adapterSuite) TestPreInstallHandlerWithAwareBootProtocol(c *C) { }, }, } - adapter := raucadapter.New(proto, &s.bootState) + adapter := raucadapter.New(proto, &s.ps) err := adapter.PreInstallHandler(ctx) c.Assert(err, IsNil) @@ -864,7 +913,7 @@ func (s *adapterSuite) TestPostInstallHandlerWithUnawareBootProtocol(c *C) { }, }, } - adapter := raucadapter.New(proto, &s.bootState) + adapter := raucadapter.New(proto, &s.ps) err := adapter.PostInstallHandler(ctx) c.Assert(err, IsNil) diff --git a/ota/raucinterface/service.go b/ota/raucinterface/service.go index a86c292a2a5379dd9c71c8b74b1758f7bce9d8a5..25a9404f02fe6648eccf2e78c9fe394640e09f59 100644 --- a/ota/raucinterface/service.go +++ b/ota/raucinterface/service.go @@ -34,8 +34,8 @@ const ( contextArgName = "context" ) -// Service implements dbusutil.HostedService exposing boothandler.Handler and installhandler.Participant over D-Bus. -type Service struct { +// HostedService implements dbusutil.HostedService exposing boothandler.Handler and installhandler.Participant over D-Bus. +type HostedService struct { bootHandler boothandler.Handler installHandler installhandler.Participant @@ -43,12 +43,12 @@ type Service struct { } // NewService returns a service with the given boot backend. -func NewService(bootHandler boothandler.Handler, installHandler installhandler.Participant) *Service { - return &Service{bootHandler: bootHandler, installHandler: installHandler} +func NewService(bootHandler boothandler.Handler, installHandler installhandler.Participant) *HostedService { + return &HostedService{bootHandler: bootHandler, installHandler: installHandler} } // JoinServiceHost integrates with dbusutil.ServiceHost. -func (svc *Service) JoinServiceHost(reg dbusutil.ServiceRegistration) error { +func (svc *HostedService) JoinServiceHost(reg dbusutil.ServiceRegistration) error { svc.conn = reg.DBusConn() methods := map[string]interface{}{ @@ -143,7 +143,7 @@ func (svc *Service) JoinServiceHost(reg dbusutil.ServiceRegistration) error { } // GetPrimary maps boothandler.Handler.PrimarySlot to D-Bus. -func (svc *Service) GetPrimary() (slotName string, dbusErr *dbus.Error) { +func (svc *HostedService) GetPrimary() (slotName string, dbusErr *dbus.Error) { slot, err := svc.bootHandler.PrimarySlot() if err != nil { return "", dbus.MakeFailedError(err) @@ -153,7 +153,7 @@ func (svc *Service) GetPrimary() (slotName string, dbusErr *dbus.Error) { } // SetPrimary maps boothandler.Handler.SetPrimarySlot to D-Bus. -func (svc *Service) SetPrimary(slotName string) (dbusErr *dbus.Error) { +func (svc *HostedService) SetPrimary(slotName string) (dbusErr *dbus.Error) { var slot boot.Slot if err := slot.UnmarshalText([]byte(slotName)); err != nil { @@ -168,7 +168,7 @@ func (svc *Service) SetPrimary(slotName string) (dbusErr *dbus.Error) { } // GetState maps boothandler.Handler.SlotState to D-Bus. -func (svc *Service) GetState(slotName string) (bootState string, dbusErr *dbus.Error) { +func (svc *HostedService) GetState(slotName string) (bootState string, dbusErr *dbus.Error) { var slot boot.Slot if err := slot.UnmarshalText([]byte(slotName)); err != nil { @@ -184,7 +184,7 @@ func (svc *Service) GetState(slotName string) (bootState string, dbusErr *dbus.E } // SetState maps boothandler.Handler.SetSlotState to D-Bus. -func (svc *Service) SetState(slotName string, stateName string) (dbusErr *dbus.Error) { +func (svc *HostedService) SetState(slotName string, stateName string) (dbusErr *dbus.Error) { var slot boot.Slot if err := slot.UnmarshalText([]byte(slotName)); err != nil { @@ -204,7 +204,7 @@ func (svc *Service) SetState(slotName string, stateName string) (dbusErr *dbus.E return nil } -func (svc *Service) populateHandlerPID(ctx *installhandler.HandlerContext, sender dbus.Sender) error { +func (svc *HostedService) populateHandlerPID(ctx *installhandler.HandlerContext, sender dbus.Sender) error { // Find the pid of the sender and store it int the handler context. // See https://dbus.freedesktop.org/doc/dbus-specification.html var pid uint32 @@ -219,7 +219,7 @@ func (svc *Service) populateHandlerPID(ctx *installhandler.HandlerContext, sende } // PreInstall maps installhandler.Participant.PreInstallHandler to D-Bus. -func (svc *Service) PreInstall(sender dbus.Sender, env []string) (dbusErr *dbus.Error) { +func (svc *HostedService) PreInstall(sender dbus.Sender, env []string) (dbusErr *dbus.Error) { ctx, err := installhandler.NewHandlerContext(env) if err != nil { return dbus.MakeFailedError(err) @@ -237,7 +237,7 @@ func (svc *Service) PreInstall(sender dbus.Sender, env []string) (dbusErr *dbus. } // PostInstall maps installhandler.Participant.PostInstallHandler to D-Bus. -func (svc *Service) PostInstall(sender dbus.Sender, env []string) (dbusErr *dbus.Error) { +func (svc *HostedService) PostInstall(sender dbus.Sender, env []string) (dbusErr *dbus.Error) { ctx, err := installhandler.NewHandlerContext(env) if err != nil { return dbus.MakeFailedError(err) diff --git a/rauc/rauctest/boothandler.go b/rauc/rauctest/boothandler.go index ac6c173b1cd79961650080c97a7156928c4015eb..8b6ca11f4317cc20797ad41deabf6ab957f6d768 100644 --- a/rauc/rauctest/boothandler.go +++ b/rauc/rauctest/boothandler.go @@ -57,36 +57,3 @@ func (handler *BootHandler) SetSlotState(slot boot.Slot, state boot.SlotState) e return handler.SetSlotStateFn(slot, state) } - -// BootState implements rauc.BootModeHolder for the BootProtocolAdapter -type BootState struct { - bootMode boot.Mode - - // BootModeChanged can be used to observe or verify changes to boot mode. - BootModeChanged func(boot.Mode) error -} - -// BootMode returns the current boot mode. -func (state *BootState) BootMode() boot.Mode { - return state.bootMode -} - -// SetBootMode sets the new boot mode. -// -// Changes to boot mode are propagated to BootModeChanged callback before -// becoming effective, enabling both notifications and validation use-cases. -func (state *BootState) SetBootMode(bootMode boot.Mode) error { - if state.bootMode == bootMode { - return nil - } - - if state.BootModeChanged != nil { - if err := state.BootModeChanged(bootMode); err != nil { - return err - } - } - - state.bootMode = bootMode - - return nil -} diff --git a/rauc/rauctest/boothandler_test.go b/rauc/rauctest/boothandler_test.go index d1a49d0b36c19090d49a906c7e971e3db0de6065..609ee552b885697b39d43b812e0bacf8911c4632 100644 --- a/rauc/rauctest/boothandler_test.go +++ b/rauc/rauctest/boothandler_test.go @@ -102,54 +102,3 @@ func (s *bootBackendSuite) TestResetCallbacks(c *C) { c.Check(backend, DeepEquals, rauctest.BootHandler{}) } - -type bootStateSuite struct{} - -var _ = Suite(&bootStateSuite{}) - -func (s *bootStateSuite) TestBootModeReadWrite(c *C) { - bootState := rauctest.BootState{} - - c.Check(bootState.BootMode(), Equals, boot.Normal) - - err := bootState.SetBootMode(boot.Try) - c.Assert(err, IsNil) - c.Check(bootState.BootMode(), Equals, boot.Try) -} - -func (s *bootStateSuite) TestBootModeChangeObserver(c *C) { - changes := make([]boot.Mode, 0, 3) - bootState := rauctest.BootState{ - BootModeChanged: func(newBootMode boot.Mode) error { - changes = append(changes, newBootMode) - return nil - }, - } - - c.Assert(bootState.SetBootMode(boot.Normal), IsNil) - c.Check(bootState.BootMode(), Equals, boot.Normal) - c.Assert(bootState.SetBootMode(boot.Try), IsNil) - c.Check(bootState.BootMode(), Equals, boot.Try) - c.Assert(bootState.SetBootMode(boot.Normal), IsNil) - c.Check(bootState.BootMode(), Equals, boot.Normal) - - c.Check(changes, DeepEquals, []boot.Mode{boot.Try, boot.Normal}) -} - -func (s *bootStateSuite) TestBootModeChangeGuard(c *C) { - bootState := rauctest.BootState{ - BootModeChanged: func(newBootMode boot.Mode) error { - if newBootMode != boot.Normal && newBootMode != boot.Try { - return boot.ErrInvalidBootMode - } - return nil - }, - } - - c.Assert(bootState.SetBootMode(boot.Normal), IsNil) - c.Check(bootState.BootMode(), Equals, boot.Normal) - c.Assert(bootState.SetBootMode(boot.Try), IsNil) - c.Check(bootState.BootMode(), Equals, boot.Try) - c.Assert(bootState.SetBootMode(boot.InvalidBootMode), ErrorMatches, `invalid boot mode`) - c.Check(bootState.BootMode(), Equals, boot.Try) -} diff --git a/service/bootprotoservice.go b/service/boothosted/boothosted.go similarity index 74% rename from service/bootprotoservice.go rename to service/boothosted/boothosted.go index 4ec07588edfe1cd46da576bf8e7ad2c9ec404102..13ebcbcf02c56c92dc43e504efb3844ccf48d690 100644 --- a/service/bootprotoservice.go +++ b/service/boothosted/boothosted.go @@ -1,8 +1,9 @@ // SPDX-License-Identifier: Apache-2.0 // SPDX-FileCopyrightText: Huawei Inc. -// Package service implements the sysotad D-Bus service. -package service +// Package boothosted contains the implementation of hosted service exposing the +// boot protocol over D-Bus. +package boothosted import ( "errors" @@ -16,13 +17,13 @@ import ( ) const ( - // BootLoaderInterfaceName is the name of the SystemOTA BootLoader interface. - BootLoaderInterfaceName = "org.oniroproject.sysota1.BootLoader" + // InterfaceName is the name of the SystemOTA BootLoader interface. + InterfaceName = "org.oniroproject.sysota1.BootLoader" ) -// BootLoaderIntrospectData is the D-Bus introspection data for the BootLoader interface of the service object. -var BootLoaderIntrospectData = introspect.Interface{ - Name: BootLoaderInterfaceName, +// IntrospectData is the D-Bus introspection data for the BootLoader interface of the service object. +var IntrospectData = introspect.Interface{ + Name: InterfaceName, Methods: []introspect.Method{ { Name: "QueryActive", @@ -75,13 +76,17 @@ var BootLoaderIntrospectData = introspect.Interface{ var errNoBootProtocol = errors.New("boot protocol is not set") -// BootProtocolService is a dbusutil.HostedService exporting boot.Protocol over D-BUs. -type BootProtocolService struct { +// HostedService is a dbusutil.HostedService exporting boot.Protocol over D-BUs. +type HostedService struct { bootProto boot.Protocol } +func (svc *HostedService) Init(p boot.Protocol) { + svc.bootProto = p +} + // JoinServiceHost integrates with dbusutil.ServiceHost. -func (svc *BootProtocolService) JoinServiceHost(reg dbusutil.ServiceRegistration) error { +func (svc *HostedService) JoinServiceHost(reg dbusutil.ServiceRegistration) error { methods := map[string]interface{}{ "QueryActive": svc.QueryActive, "QueryInactive": svc.QueryInactive, @@ -91,11 +96,11 @@ func (svc *BootProtocolService) JoinServiceHost(reg dbusutil.ServiceRegistration "CancelSwitch": svc.CancelSwitch, } - return reg.RegisterObject(ota.ServiceObjectPath, BootLoaderInterfaceName, methods, BootLoaderIntrospectData) + return reg.RegisterObject(ota.ServiceObjectPath, InterfaceName, methods, IntrospectData) } // QueryActive exposes the boot.Protocol.QueryActive method over D-Bus. -func (svc *BootProtocolService) QueryActive() (string, *dbus.Error) { +func (svc *HostedService) QueryActive() (string, *dbus.Error) { if svc.bootProto == nil { return "", dbus.MakeFailedError(errNoBootProtocol) } @@ -109,7 +114,7 @@ func (svc *BootProtocolService) QueryActive() (string, *dbus.Error) { } // QueryInactive exposes the boot.Protocol.QueryInactive method over D-Bus. -func (svc *BootProtocolService) QueryInactive() (string, *dbus.Error) { +func (svc *HostedService) QueryInactive() (string, *dbus.Error) { if svc.bootProto == nil { return "", dbus.MakeFailedError(errNoBootProtocol) } @@ -123,7 +128,7 @@ func (svc *BootProtocolService) QueryInactive() (string, *dbus.Error) { } // TrySwitch exposes the boot.Protocol.TrySwitch method over D-Bus. -func (svc *BootProtocolService) TrySwitch(slotName string) *dbus.Error { +func (svc *HostedService) TrySwitch(slotName string) *dbus.Error { if svc.bootProto == nil { return dbus.MakeFailedError(errNoBootProtocol) } @@ -143,7 +148,7 @@ func (svc *BootProtocolService) TrySwitch(slotName string) *dbus.Error { // Reboot exposes the boot.Protocol.Reboot method over D-Bus. // // The flags argument is not translated in any way. -func (svc *BootProtocolService) Reboot(flags uint) *dbus.Error { +func (svc *HostedService) Reboot(flags uint) *dbus.Error { if svc.bootProto == nil { return dbus.MakeFailedError(errNoBootProtocol) } @@ -156,7 +161,7 @@ func (svc *BootProtocolService) Reboot(flags uint) *dbus.Error { } // CommitSwitch exposes the boot.Protocol.CommitSwitch method over D-Bus. -func (svc *BootProtocolService) CommitSwitch() *dbus.Error { +func (svc *HostedService) CommitSwitch() *dbus.Error { if svc.bootProto == nil { return dbus.MakeFailedError(errNoBootProtocol) } @@ -169,7 +174,7 @@ func (svc *BootProtocolService) CommitSwitch() *dbus.Error { } // CancelSwitch exposes the boot.Protocol.CancelSwitch method over D-Bus. -func (svc *BootProtocolService) CancelSwitch() *dbus.Error { +func (svc *HostedService) CancelSwitch() *dbus.Error { if svc.bootProto == nil { return dbus.MakeFailedError(errNoBootProtocol) } diff --git a/service/export_test.go b/service/export_test.go index a56d447a7b259ef648a7e6ce987ddaf79afe6b96..9cdd67f6713a2319f5c1d0a48414e4d03029a427 100644 --- a/service/export_test.go +++ b/service/export_test.go @@ -1,5 +1,6 @@ // SPDX-License-Identifier: Apache-2.0 // SPDX-FileCopyrightText: Huawei Inc. + package service import ( diff --git a/service/rauchosted/rauchosted.go b/service/rauchosted/rauchosted.go new file mode 100644 index 0000000000000000000000000000000000000000..797a0ee090f5c7990b3d79952990e9f893fd0d2e --- /dev/null +++ b/service/rauchosted/rauchosted.go @@ -0,0 +1,260 @@ +// SPDX-License-Identifier: Apache-2.0 +// SPDX-FileCopyrightText: Huawei Inc. + +package rauchosted + +import ( + "github.com/godbus/dbus/v5" + "github.com/godbus/dbus/v5/introspect" + + "booting.oniroproject.org/distro/components/sysota/boot" + "booting.oniroproject.org/distro/components/sysota/dbusutil" + "booting.oniroproject.org/distro/components/sysota/ota" + "booting.oniroproject.org/distro/components/sysota/rauc/boothandler" + "booting.oniroproject.org/distro/components/sysota/rauc/installhandler" +) + +const ( + // InterfaceName is the name of the RAUC interface provided by SystemOTA. + InterfaceName = "org.oniroproject.sysota1.RAUC" +) + +const ( + // Methods of the boot handler + + getPrimaryDBusName = "GetPrimary" + setPrimaryDBusName = "SetPrimary" + getStateDBusName = "GetState" + setStateDBusName = "SetState" + + // Methods of the install handler + + preInstallDBusName = "PreInstall" + postInstallDBusName = "PostInstall" + + // Argument names + + slotArgName = "slot" + stateArgName = "state" + contextArgName = "context" +) + +// HostedService implements dbusutil.HostedService exposing boothandler.Handler and installhandler.Participant over D-Bus. +type HostedService struct { + bootHandler boothandler.Handler + installHandler installhandler.Participant + + conn *dbus.Conn +} + +func (svc *HostedService) Init(bootHandler boothandler.Handler, installHandler installhandler.Participant) { + svc.bootHandler = bootHandler + svc.installHandler = installHandler +} + +// JoinServiceHost integrates with dbusutil.ServiceHost. +func (svc *HostedService) JoinServiceHost(reg dbusutil.ServiceRegistration) error { + svc.conn = reg.DBusConn() + + methods := map[string]interface{}{ + getPrimaryDBusName: svc.GetPrimary, + setPrimaryDBusName: svc.SetPrimary, + getStateDBusName: svc.GetState, + setStateDBusName: svc.SetState, + + preInstallDBusName: svc.PreInstall, + postInstallDBusName: svc.PostInstall, + } + + metadata := introspect.Interface{ + Name: InterfaceName, + Methods: []introspect.Method{ + { + Name: getPrimaryDBusName, + Args: []introspect.Arg{ + { + Name: slotArgName, + Type: "s", + Direction: dbusutil.Out, + }, + }, + }, + { + Name: setPrimaryDBusName, + Args: []introspect.Arg{ + { + Name: slotArgName, + Type: "s", + Direction: dbusutil.In, + }, + }, + }, + { + Name: getStateDBusName, + Args: []introspect.Arg{ + { + Name: slotArgName, + Type: "s", + Direction: dbusutil.In, + }, + { + Name: stateArgName, + Type: "s", + Direction: dbusutil.Out, + }, + }, + }, + { + Name: setStateDBusName, + Args: []introspect.Arg{ + { + Name: slotArgName, + Type: "s", + Direction: dbusutil.In, + }, + { + Name: stateArgName, + Type: "s", + Direction: dbusutil.In, + }, + }, + }, + { + Name: preInstallDBusName, + Args: []introspect.Arg{ + { + Name: contextArgName, + // Pass environment block as input. + // This allows the context to evolve without changing the API. + Type: "as", + Direction: dbusutil.In, + }, + }, + }, + { + Name: postInstallDBusName, + Args: []introspect.Arg{ + { + Name: contextArgName, + Type: "as", + Direction: dbusutil.In, + }, + }, + }, + }, + } + + return reg.RegisterObject(ota.ServiceObjectPath, InterfaceName, methods, metadata) +} + +// GetPrimary maps boothandler.Handler.PrimarySlot to D-Bus. +func (svc *HostedService) GetPrimary() (slotName string, dbusErr *dbus.Error) { + slot, err := svc.bootHandler.PrimarySlot() + if err != nil { + return "", dbus.MakeFailedError(err) + } + + return slot.String(), nil +} + +// SetPrimary maps boothandler.Handler.SetPrimarySlot to D-Bus. +func (svc *HostedService) SetPrimary(slotName string) (dbusErr *dbus.Error) { + var slot boot.Slot + + if err := slot.UnmarshalText([]byte(slotName)); err != nil { + return dbus.MakeFailedError(err) + } + + if err := svc.bootHandler.SetPrimarySlot(slot); err != nil { + return dbus.MakeFailedError(err) + } + + return nil +} + +// GetState maps boothandler.Handler.SlotState to D-Bus. +func (svc *HostedService) GetState(slotName string) (bootState string, dbusErr *dbus.Error) { + var slot boot.Slot + + if err := slot.UnmarshalText([]byte(slotName)); err != nil { + return "", dbus.MakeFailedError(err) + } + + state, err := svc.bootHandler.SlotState(slot) + if err != nil { + return "", dbus.MakeFailedError(err) + } + + return state.String(), nil +} + +// SetState maps boothandler.Handler.SetSlotState to D-Bus. +func (svc *HostedService) SetState(slotName string, stateName string) (dbusErr *dbus.Error) { + var slot boot.Slot + + if err := slot.UnmarshalText([]byte(slotName)); err != nil { + return dbus.MakeFailedError(err) + } + + var state boot.SlotState + + if err := state.UnmarshalText([]byte(stateName)); err != nil { + return dbus.MakeFailedError(err) + } + + if err := svc.bootHandler.SetSlotState(slot, state); err != nil { + return dbus.MakeFailedError(err) + } + + return nil +} + +func (svc *HostedService) populateHandlerPID(ctx *installhandler.HandlerContext, sender dbus.Sender) error { + // Find the pid of the sender and store it int the handler context. + // See https://dbus.freedesktop.org/doc/dbus-specification.html + var pid uint32 + + if err := svc.conn.BusObject().Call("org.freedesktop.DBus.GetConnectionUnixProcessID", 0, sender).Store(&pid); err != nil { + return err + } + + ctx.InitialHandlerPID = int(pid) + + return nil +} + +// PreInstall maps installhandler.Participant.PreInstallHandler to D-Bus. +func (svc *HostedService) PreInstall(sender dbus.Sender, env []string) (dbusErr *dbus.Error) { + ctx, err := installhandler.NewHandlerContext(env) + if err != nil { + return dbus.MakeFailedError(err) + } + + if err := svc.populateHandlerPID(ctx, sender); err != nil { + return dbus.MakeFailedError(err) + } + + if err := svc.installHandler.PreInstallHandler(ctx); err != nil { + return dbus.MakeFailedError(err) + } + + return nil +} + +// PostInstall maps installhandler.Participant.PostInstallHandler to D-Bus. +func (svc *HostedService) PostInstall(sender dbus.Sender, env []string) (dbusErr *dbus.Error) { + ctx, err := installhandler.NewHandlerContext(env) + if err != nil { + return dbus.MakeFailedError(err) + } + + if err := svc.populateHandlerPID(ctx, sender); err != nil { + return dbus.MakeFailedError(err) + } + + if err := svc.installHandler.PostInstallHandler(ctx); err != nil { + return dbus.MakeFailedError(err) + } + + return nil +} diff --git a/service/service.go b/service/service.go index be9013f8e6b310b7c4720e508cd67baea54e1d4a..1b0b3416d7413491bc12dfccab70b8d16557c011 100644 --- a/service/service.go +++ b/service/service.go @@ -6,7 +6,6 @@ package service import ( "fmt" - "sync" dbus "github.com/godbus/dbus/v5" @@ -16,8 +15,11 @@ import ( "booting.oniroproject.org/distro/components/sysota/dirs" "booting.oniroproject.org/distro/components/sysota/ota" "booting.oniroproject.org/distro/components/sysota/ota/raucadapter" - "booting.oniroproject.org/distro/components/sysota/ota/raucinterface" "booting.oniroproject.org/distro/components/sysota/picfg/pimodel" + "booting.oniroproject.org/distro/components/sysota/service/boothosted" + "booting.oniroproject.org/distro/components/sysota/service/rauchosted" + "booting.oniroproject.org/distro/components/sysota/service/testhosted" + "booting.oniroproject.org/distro/components/sysota/service/updatehosted" ) // RequestBusName requests a well-known name on the bus @@ -38,21 +40,6 @@ func RequestBusName(conn *dbus.Conn) error { return nil } -// Service implements the D-Bus service capable of downloading system updates. -type Service struct { - m sync.RWMutex - - state *ota.SystemState - config *ota.Config - - sh *dbusutil.ServiceHost - - updateService UpdateService - - raucAdapter *raucadapter.Adapter - raucService *raucinterface.Service -} - var bootProviders = map[ota.BootLoaderType]func(config *ota.Config) (boot.Protocol, error){ ota.InertBootLoader: func(config *ota.Config) (boot.Protocol, error) { return nil, nil }, ota.PiBoot: func(config *ota.Config) (proto boot.Protocol, err error) { @@ -86,88 +73,150 @@ func configuredBootProtocol(config *ota.Config) (boot.Protocol, error) { return nil, unsupportedBootLoaderError(config.BootLoaderType) } -// New returns a new service. -func New(config *ota.Config, state *ota.SystemState, conn *dbus.Conn) (*Service, error) { - if config == nil { - panic("config cannot be nil") - } +// Option is a function for configuring the service during construction. +type Option func(*Service) error - if state == nil { - panic("state cannot be nil") +// WithConfigFiles returns a ServiceOption for defining the service ConfigFiles interface. +// +// When used, the service knows how to load and save configuration on demand. Without it +// the service runs with all-defaults that are never saved. +func WithConfigFiles(cf ota.ConfigFiles) Option { + return func(svc *Service) error { + return svc.confGuard.BindAndLoad(cf) } +} - if conn == nil { - panic("conn cannot be nil") +// WithStateFile returns a ServiceOption for defining the service StateFile interface. +// +// When used, the service knows how to load and save the state on demand. Without it +// the service is stateless and all state changes are discarded. +func WithStateFile(sf ota.StateFile) Option { + return func(svc *Service) error { + return svc.stateGuard.BindAndLoad(sf) } +} - bootProto, err := configuredBootProtocol(config) - if err != nil { - return nil, err +// WithExitChannel returns a ServiceOption for defining a service exit channel. +func WithExitChannel(exit chan<- struct{}) Option { + return func(svc *Service) error { + svc.exit = exit + return nil } +} - svc := &Service{state: state, config: config} +// Service is implements D-Bus APIs of the SystemOTA device client. +// +// Individual parts of the service, modelled as distinct D-Bus interfaces, are +// implemented as unexported fields. Internally those are UpdateService, +// raucinterface.Service as well TestService. +// +// Service holds unexported PersistentState and PersistentConfig. Usually those +// should be initialized with by passing WithStateFile and WithConfigFiles to +// New. They are made available to several hosted services: update, boot, rauc +// and test hosted services, which implement individual D-Bus interfaces. +type Service struct { + dbusutil.ServiceHost - svc.raucAdapter = raucadapter.New(bootProto, svc) - svc.sh = dbusutil.NewServiceHost(conn) + stateGuard ota.StateGuard + confGuard ota.ConfigGuard - // Configure and add the boot service if the corresponding - // debug flag is enabled. - if config.DebugBootAPI { - bootService := &BootProtocolService{ - bootProto: bootProto, + bootProto boot.Protocol + raucAdapter *raucadapter.Adapter + exit chan<- struct{} + + boot boothosted.HostedService + update updatehosted.HostedService + rauc rauchosted.HostedService + test testhosted.HostedService +} + +// New returns a new service. +// +// In practice you want to call it with both the WithConfigurer WithStater +// service options. In their absence the service will run devoid of +// configuration and state and all changes will be ephemeral. +func New(conn *dbus.Conn, opts ...Option) (*Service, error) { + svc := &Service{} + + for _, opt := range opts { + if err := opt(svc); err != nil { + return nil, err } - svc.sh.AddHostedService(bootService) } - // Configure and add the update service. - svc.updateService.maker = "dummy-maker" - svc.updateService.model = "dummy-model" - svc.updateService.stream = "dummy-stream" - svc.sh.AddHostedService(&svc.updateService) + svc.stateGuard.RegisterObserver("dbus", func(st *ota.SystemState) error { + // TODO: send dbus notifications once we expose the BootMode property. + return nil + }) + + svc.ServiceHost.Init(conn) + + cfg := svc.confGuard.Config() + bootProto, err := configuredBootProtocol(&cfg) + if err != nil { + return nil, err + } + + svc.bootProto = bootProto - // Configure and add the RAUC service - svc.raucService = raucinterface.NewService(svc.raucAdapter, svc.raucAdapter) - svc.sh.AddHostedService(svc.raucService) + svc.setupRaucAdapter() + svc.setupBootProtocolService() + svc.setupUpdateService() + svc.setupRaucService() + svc.setupTestService() return svc, nil } -// BootMode implements raucadapter.BootModeHolder and returns the current boot mode. -func (svc *Service) BootMode() boot.Mode { - return svc.state.BootMode +func (svc *Service) setupRaucAdapter() { + // RAUC adapter takes the boot protocol and a boot mode holder which is + // implemented by the service directly. + svc.raucAdapter = raucadapter.New(svc.bootProto, &svc.stateGuard) } -// SetBootMode implements raucadapter.BootModeHolder and sets the new boot mode. -func (svc *Service) SetBootMode(bootMode boot.Mode) error { - if svc.state.BootMode == bootMode { - return nil +// setupBootProtocolService configures and adds the boot protocol service +// if the corresponding debug flag is enabled in the configuration system. +func (svc *Service) setupBootProtocolService() { + if !svc.confGuard.Config().DebugBootAPI { + return } - svc.state.BootMode = bootMode + svc.boot.Init(svc.bootProto) + svc.AddHostedService(&svc.boot) +} - // TODO(zyga): emit the PropertyChanged signal here. - // Any errors should be logged and ignored. +func (svc *Service) setupUpdateService() { + svc.update.Init(&svc.confGuard) - return nil + svc.AddHostedService(&svc.update) } -// Export exports the service object on the bus. -func (svc *Service) Export() (err error) { - svc.m.Lock() - defer svc.m.Unlock() +func (svc *Service) setupRaucService() { + svc.rauc.Init(svc.raucAdapter, svc.raucAdapter) + svc.AddHostedService(&svc.rauc) +} - return svc.sh.Export() +// Exit returns the exit channel of the service. +// +// The exit channel can be used to send an empty structure to cause the service +// to stop processing requests and quit. +func (svc *Service) Exit() chan<- struct{} { + return svc.exit } -// Unexport removes the object from the set of published bus objects. -func (svc *Service) Unexport() error { - svc.m.Lock() - defer svc.m.Unlock() +// setupTestService configures and adds the test service +// if the corresponding debug flag is enabled in the configuration system. +func (svc *Service) setupTestService() { + if !svc.confGuard.Config().DebugTestAPI { + return + } + + svc.test.Init(&svc.confGuard, &svc.stateGuard, svc.exit) - return svc.sh.Unexport() + svc.AddHostedService(&svc.test) } // IsIdle returns true if all the hosted service are idle. func (svc *Service) IsIdle() bool { - return svc.updateService.IsIdle() + return svc.update.IsIdle() } diff --git a/service/service_test.go b/service/service_test.go index a5c1639eaf9e4653269f189aff45889c4bf702f0..232a3b2d4e4a77bb9a06c1428649d8c859c7db44 100644 --- a/service/service_test.go +++ b/service/service_test.go @@ -44,13 +44,39 @@ func (s *serviceSuite) EnsureConn(c *C) *dbus.Conn { return s.conn } +type configPtr struct { + *ota.Config +} + +func (j configPtr) LoadConfig() (*ota.Config, error) { + return j.Config, nil +} + +func (j configPtr) SaveConfig(config *ota.Config) error { + return nil +} + +type statePtr struct { + *ota.SystemState +} + +func (j statePtr) LoadState() (*ota.SystemState, error) { + return j.SystemState, nil +} + +func (j statePtr) SaveState(state *ota.SystemState) error { + return nil +} + func (s *serviceSuite) EnsureService(c *C) { var err error if s.svc == nil { s.EnsureConn(c) - s.svc, err = service.New(&s.config, &s.state, s.conn) + s.svc, err = service.New(s.conn, + service.WithConfigFiles(configPtr{&s.config}), + service.WithStateFile(statePtr{&s.state})) c.Assert(err, IsNil) err := s.svc.Export() @@ -62,7 +88,11 @@ func (s *serviceSuite) EnsureService(c *C) { } func (s *serviceSuite) SetUpTest(c *C) { - s.config = ota.Config{} + s.config = ota.Config{ + DeviceMaker: "test-maker", + DeviceModel: "test-model", + UpdateStream: "test-stream", + } s.state = ota.SystemState{} } @@ -82,6 +112,16 @@ func (s *serviceSuite) TearDownTest(c *C) { } } +func (s *serviceSuite) TestExitChannel(c *C) { + ch := make(chan struct{}, 1) + s.EnsureConn(c) + svc, err := service.New(s.conn, service.WithExitChannel(ch)) + c.Assert(err, IsNil) + svc.Exit() <- struct{}{} + _, ok := <-ch + c.Check(ok, Equals, true) +} + func (s *serviceSuite) TestSmokeAccessWithoutBootProtocolAcrossDBus(c *C) { s.config.DebugBootAPI = true s.EnsureService(c) @@ -217,15 +257,15 @@ func (s *serviceSuite) TestAccessPropertiesAcrossDBus(c *C) { err := svcObject.Call("org.freedesktop.DBus.Properties.Get", 0, "org.oniroproject.sysota1.Service", "Maker").Store(&propValue) c.Assert(err, IsNil) - c.Check(propValue, Equals, dbus.MakeVariant("dummy-maker")) + c.Check(propValue, Equals, dbus.MakeVariant("test-maker")) err = svcObject.Call("org.freedesktop.DBus.Properties.Get", 0, "org.oniroproject.sysota1.Service", "Model").Store(&propValue) c.Assert(err, IsNil) - c.Check(propValue, Equals, dbus.MakeVariant("dummy-model")) + c.Check(propValue, Equals, dbus.MakeVariant("test-model")) err = svcObject.Call("org.freedesktop.DBus.Properties.Get", 0, "org.oniroproject.sysota1.Service", "Stream").Store(&propValue) c.Assert(err, IsNil) - c.Check(propValue, Equals, dbus.MakeVariant("dummy-stream")) + c.Check(propValue, Equals, dbus.MakeVariant("test-stream")) err = svcObject.Call("org.freedesktop.DBus.Properties.Get", 0, "org.oniroproject.sysota1.Service", "Potato").Store(&propValue) c.Assert(err, ErrorMatches, `org.freedesktop.DBus.Properties.Error.PropertyNotFound`) @@ -238,9 +278,9 @@ func (s *serviceSuite) TestAccessPropertiesAcrossDBus(c *C) { err = svcObject.Call("org.freedesktop.DBus.Properties.GetAll", 0, "org.oniroproject.sysota1.Service").Store(&propMap) c.Assert(err, IsNil) c.Check(propMap, DeepEquals, map[string]dbus.Variant{ - "Maker": dbus.MakeVariant("dummy-maker"), - "Model": dbus.MakeVariant("dummy-model"), - "Stream": dbus.MakeVariant("dummy-stream"), + "Maker": dbus.MakeVariant("test-maker"), + "Model": dbus.MakeVariant("test-model"), + "Stream": dbus.MakeVariant("test-stream"), }) err = svcObject.Call("org.freedesktop.DBus.Properties.GetAll", 0, "org.oniroproject.sysota1.Potato").Store(&propMap) @@ -288,14 +328,3 @@ func (s *serviceSuite) TestGetManagedObjects(c *C) { c.Assert(err, IsNil) c.Check(objs, DeepEquals, map[dbus.ObjectPath]map[string]map[string]dbus.Variant{}) } - -func (s *serviceSuite) TestBootMode(c *C) { - s.EnsureService(c) - - c.Check(s.svc.BootMode(), Equals, boot.Normal) - // TODO(zyga): check that PropertiesChanged signal was *not* sent. - c.Check(s.svc.SetBootMode(boot.Mode(boot.Normal)), IsNil) - // TODO(zyga): check that PropertiesChanged signal was sent. - c.Check(s.svc.SetBootMode(boot.Mode(ota.TestBoot)), IsNil) - c.Check(s.svc.BootMode(), Equals, boot.Try) -} diff --git a/service/testhosted/testhosted.go b/service/testhosted/testhosted.go new file mode 100644 index 0000000000000000000000000000000000000000..036aef7abe50666b9a5fcd92c64cbd864a96d834 --- /dev/null +++ b/service/testhosted/testhosted.go @@ -0,0 +1,134 @@ +// SPDX-License-Identifier: Apache-2.0 +// SPDX-FileCopyrightText: Huawei Inc. + +// Package testhosted contains a hosted service used for testing. +package testhosted + +import ( + dbus "github.com/godbus/dbus/v5" + "github.com/godbus/dbus/v5/introspect" + + "booting.oniroproject.org/distro/components/sysota/dbusutil" + "booting.oniroproject.org/distro/components/sysota/ota" +) + +const ( + // InterfaceName is the name of the debug interface of the service. + InterfaceName = "org.oniroproject.sysota1.Test" + + loadStateFunc = "LoadState" + saveStateFunc = "SaveState" + + loadConfigFunc = "LoadConfig" + saveConfigFunc = "SaveConfig" + + exitFunc = "Exit" +) + +// IntrospectData is the D-Bus introspection data for DebugService interface of the service object. +// +// It is provided explicitly without using reflection for a bit more control +// over the provided data, given that we implement the properties interface +// ourselves. +var IntrospectData = introspect.Interface{ + Name: InterfaceName, + Methods: []introspect.Method{ + { + Name: loadStateFunc, + }, + { + Name: saveStateFunc, + }, + { + Name: loadConfigFunc, + }, + { + Name: saveConfigFunc, + }, + { + Name: exitFunc, + }, + }, +} + +// HostedService exposes debugging functions not meant for production. +type HostedService struct { + cfg *ota.ConfigGuard + st *ota.StateGuard + exit chan<- struct{} +} + +// Init initializes Service with parts of the Service data. +func (svc *HostedService) Init(cfg *ota.ConfigGuard, st *ota.StateGuard, exit chan<- struct{}) { + svc.cfg = cfg + svc.st = st + svc.exit = exit +} + +// JoinServiceHost integrates with dbusutil.ServiceHost. +func (svc *HostedService) JoinServiceHost(reg dbusutil.ServiceRegistration) error { + svcMethods := map[string]interface{}{ + loadStateFunc: svc.LoadState, + saveStateFunc: svc.SaveState, + loadConfigFunc: svc.LoadConfig, + saveConfigFunc: svc.SaveConfig, + exitFunc: svc.Exit, + } + if err := reg.RegisterObject(ota.ServiceObjectPath, InterfaceName, svcMethods, IntrospectData); err != nil { + return err + } + + return nil +} + +// LoadState replaces system state with that loaded from the state file. +func (svc *HostedService) LoadState() *dbus.Error { + state, err := svc.st.StateFile().LoadState() + if err != nil { + return dbus.MakeFailedError(err) + } + + svc.st.SetState(state) + + return nil +} + +// SaveState saves system state to the state file. +func (svc *HostedService) SaveState() *dbus.Error { + st := svc.st.State() + + if err := svc.st.StateFile().SaveState(&st); err != nil { + return dbus.MakeFailedError(err) + } + + return nil +} + +// LoadConfig replaces service configuration with that loaded from the config files. +func (svc *HostedService) LoadConfig() *dbus.Error { + cfg, err := svc.cfg.ConfigFiles().LoadConfig() + if err != nil { + return dbus.MakeFailedError(err) + } + + svc.cfg.SetConfig(cfg) + + return nil +} + +// SaveConfig saves service configuration to the config files. +func (svc *HostedService) SaveConfig() *dbus.Error { + cfg := svc.cfg.Config() + + if err := svc.cfg.ConfigFiles().SaveConfig(&cfg); err != nil { + return dbus.MakeFailedError(err) + } + + return nil +} + +// Exit causes the service to exit immediately. +func (svc *HostedService) Exit() *dbus.Error { + svc.exit <- struct{}{} + return nil +} diff --git a/service/oper/oper.go b/service/updatehosted/oper/oper.go similarity index 100% rename from service/oper/oper.go rename to service/updatehosted/oper/oper.go diff --git a/service/updateservice.go b/service/updatehosted/updatehosted.go similarity index 70% rename from service/updateservice.go rename to service/updatehosted/updatehosted.go index 94669f7acf1aac8b3849d40cf92056f5dedd970b..fb507c45e0d596e047ee082f225b52f2429f56f1 100644 --- a/service/updateservice.go +++ b/service/updatehosted/updatehosted.go @@ -1,8 +1,7 @@ // SPDX-License-Identifier: Apache-2.0 // SPDX-FileCopyrightText: Huawei Inc. -// Package service implements the sysotad D-Bus service. -package service +package updatehosted import ( "fmt" @@ -15,26 +14,26 @@ import ( "booting.oniroproject.org/distro/components/sysota/dbusutil" "booting.oniroproject.org/distro/components/sysota/dbusutil/objmgr" "booting.oniroproject.org/distro/components/sysota/ota" - "booting.oniroproject.org/distro/components/sysota/service/oper" + "booting.oniroproject.org/distro/components/sysota/service/updatehosted/oper" ) const ( // UpdateServiceInterfaceName is the name of the SystemOTA Service interface. // TODO(zyga): re-think the name. - UpdateServiceInterfaceName = "org.oniroproject.sysota1.Service" + InterfaceName = "org.oniroproject.sysota1.Service" makerProperty = "Maker" modelProperty = "Model" streamProperty = "Stream" ) -// ServiceIntrospectData is the D-Bus introspection data for Service interface of the service object. +// UpdateServiceIntrospectData is the D-Bus introspection data for Service interface of the service object. // // It is provided explicitly without using reflection for a bit more control // over the provided data, given that we implement the properties interface // ourselves. -var ServiceIntrospectData = introspect.Interface{ - Name: UpdateServiceInterfaceName, +var IntrospectData = introspect.Interface{ + Name: InterfaceName, Methods: []introspect.Method{ { Name: "UpdateStreams", @@ -92,7 +91,7 @@ var ServiceIntrospectData = introspect.Interface{ }, } -// UpdateService provides functions for downloading and applying updates. +// HostedService provides functions for downloading and applying updates. // // Maker is a near-arbitrary string that identifier the device maker. // Model is a near-arbitrary string that is selected by the device maker. @@ -100,21 +99,23 @@ var ServiceIntrospectData = introspect.Interface{ // branches or stability levels to use. // // Available updates are specific to a given (maker,model,stream) tuple. -type UpdateService struct { - m sync.RWMutex +type HostedService struct { conn *dbus.Conn - maker string - model string - stream string - - ops []*oper.Operation + pc *ota.ConfigGuard + opsMutex sync.RWMutex + ops []*oper.Operation lastOperationID int } +// Init initializes UpdateService with a given persistent config. +func (svc *HostedService) Init(pc *ota.ConfigGuard) { + svc.pc = pc +} + // JoinServiceHost integrates with dbusutil.ServiceHost. -func (svc *UpdateService) JoinServiceHost(reg dbusutil.ServiceRegistration) error { +func (svc *HostedService) JoinServiceHost(reg dbusutil.ServiceRegistration) error { // Remember connection to send signals. svc.conn = reg.DBusConn() @@ -124,7 +125,7 @@ func (svc *UpdateService) JoinServiceHost(reg dbusutil.ServiceRegistration) erro "UpdateStreams": svc.UpdateStreams, "UpdateDevice": svc.UpdateDevice, } - if err := reg.RegisterObject(path, UpdateServiceInterfaceName, svcMethods, ServiceIntrospectData); err != nil { + if err := reg.RegisterObject(path, InterfaceName, svcMethods, IntrospectData); err != nil { return err } @@ -147,57 +148,51 @@ func (svc *UpdateService) JoinServiceHost(reg dbusutil.ServiceRegistration) erro return nil } +/* // Maker returns the name of the maker of the device. -func (svc *UpdateService) Maker() string { - svc.m.RLock() - defer svc.m.RUnlock() - - return svc.maker +func (svc *HostedService) Maker() string { + return svc.pc.Config().DeviceMaker } // Model returns the model name of the device. -func (svc *UpdateService) Model() string { - svc.m.RLock() - defer svc.m.RUnlock() - - return svc.model +func (svc *HostedService) Model() string { + return svc.pc.Config().DeviceModel } // Stream returns the name of the update stream the device is subscribed to. -func (svc *UpdateService) Stream() string { - svc.m.RLock() - defer svc.m.RUnlock() - - return svc.stream +func (svc *HostedService) Stream() string { + return svc.pc.Config().UpdateStream } +*/ -// SetStream sets the new stream name to the given value. -func (svc *UpdateService) SetStream(stream string) error { - svc.m.Lock() - defer svc.m.Unlock() +// setStream sets the new stream name to the given value. +func (svc *HostedService) setStream(stream string) error { + err := svc.pc.AlterConfig(func(cfg *ota.Config) (bool, error) { + if cfg.UpdateStream == stream { + return false, nil + } - return svc.setStreamUnlocked(stream) -} + // TODO(zyga): check that stream is valid/known. + cfg.UpdateStream = stream + + return true, nil + }) + if err != nil { + return err + } -// setStreamUnlocked sets the new stream name to the given value. -// -// It must be called with the write lock held. -func (svc *UpdateService) setStreamUnlocked(stream string) error { - // TODO check that stream is valid/known. - svc.stream = stream - // TODO: persist the configuration updated := map[string]dbus.Variant{ streamProperty: dbus.MakeVariant(stream), } invalidated := []string{} - return svc.conn.Emit(ota.ServiceObjectPath, dbusutil.PropertiesChangedSignal, UpdateServiceInterfaceName, updated, invalidated) + return svc.conn.Emit(ota.ServiceObjectPath, dbusutil.PropertiesChangedSignal, InterfaceName, updated, invalidated) } // IsIdle returns true if the OTA service is idle and can be shut down. -func (svc *UpdateService) IsIdle() bool { - svc.m.RLock() - defer svc.m.RUnlock() +func (svc *HostedService) IsIdle() bool { + svc.opsMutex.RLock() + defer svc.opsMutex.RUnlock() return len(svc.ops) == 0 } @@ -210,9 +205,9 @@ func (svc *UpdateService) IsIdle() bool { // TODO: Implement this for real by calling thing from the OTA backend. // XXX: The return type is probably wrong. It should return "ao" // representing the known streams. -func (svc *UpdateService) UpdateStreams() *dbus.Error { - svc.m.Lock() - defer svc.m.Unlock() +func (svc *HostedService) UpdateStreams() *dbus.Error { + svc.opsMutex.Lock() + defer svc.opsMutex.Unlock() fmt.Printf("called UpdateStreams\n") @@ -225,14 +220,14 @@ func (svc *UpdateService) UpdateStreams() *dbus.Error { // and returns an object which can be used to track the progress and result. // // TODO: Implement this for real by calling thing from the OTA backend. -func (svc *UpdateService) UpdateDevice(hints map[string]string) (dbus.ObjectPath, *dbus.Error) { - svc.m.Lock() - defer svc.m.Unlock() +func (svc *HostedService) UpdateDevice(hints map[string]string) (dbus.ObjectPath, *dbus.Error) { + svc.opsMutex.Lock() + defer svc.opsMutex.Unlock() // TODO: implement this for real fmt.Printf("called UpdateDevice with hints %v\n", hints) - op, err := svc.makeOp() + op, err := svc.makeOpUnlocked() if err != nil { return "", dbus.MakeFailedError(err) } @@ -242,7 +237,7 @@ func (svc *UpdateService) UpdateDevice(hints map[string]string) (dbus.ObjectPath return oper.ObjectPath(op.ID()), nil } -func (svc *UpdateService) makeOp() (*oper.Operation, error) { +func (svc *HostedService) makeOpUnlocked() (*oper.Operation, error) { svc.lastOperationID++ op := oper.New(svc.conn, svc.lastOperationID) @@ -273,19 +268,18 @@ func (svc *UpdateService) makeOp() (*oper.Operation, error) { // more precise control. We may drop this later, depending on how it gets used. // Get implements org.freedesktop.DBus.Properties.Get. -func (svc *UpdateService) Get(iface, property string) (dbus.Variant, *dbus.Error) { - svc.m.RLock() - defer svc.m.RUnlock() +func (svc *HostedService) Get(iface, property string) (dbus.Variant, *dbus.Error) { + cfg := svc.pc.Config() switch iface { - case UpdateServiceInterfaceName: + case InterfaceName: switch property { case makerProperty: - return dbus.MakeVariant(svc.maker), nil + return dbus.MakeVariant(cfg.DeviceMaker), nil case modelProperty: - return dbus.MakeVariant(svc.model), nil + return dbus.MakeVariant(cfg.DeviceModel), nil case streamProperty: - return dbus.MakeVariant(svc.stream), nil + return dbus.MakeVariant(cfg.UpdateStream), nil default: return dbus.Variant{}, prop.ErrPropNotFound } @@ -295,16 +289,15 @@ func (svc *UpdateService) Get(iface, property string) (dbus.Variant, *dbus.Error } // GetAll implements org.freedesktop.DBus.Properties.GetAll. -func (svc *UpdateService) GetAll(iface string) (map[string]dbus.Variant, *dbus.Error) { - svc.m.RLock() - defer svc.m.RUnlock() +func (svc *HostedService) GetAll(iface string) (map[string]dbus.Variant, *dbus.Error) { + cfg := svc.pc.Config() switch iface { - case UpdateServiceInterfaceName: + case InterfaceName: return map[string]dbus.Variant{ - makerProperty: dbus.MakeVariant(svc.maker), - modelProperty: dbus.MakeVariant(svc.model), - streamProperty: dbus.MakeVariant(svc.stream), + makerProperty: dbus.MakeVariant(cfg.DeviceMaker), + modelProperty: dbus.MakeVariant(cfg.DeviceModel), + streamProperty: dbus.MakeVariant(cfg.UpdateStream), }, nil default: return nil, prop.ErrIfaceNotFound @@ -312,21 +305,20 @@ func (svc *UpdateService) GetAll(iface string) (map[string]dbus.Variant, *dbus.E } // Set implements org.freedesktop.Properties.Set. -func (svc *UpdateService) Set(iface, property string, newValue dbus.Variant) (err *dbus.Error) { - svc.m.Lock() - defer svc.m.Unlock() - +func (svc *HostedService) Set(iface, property string, newValue dbus.Variant) (err *dbus.Error) { switch iface { - case UpdateServiceInterfaceName: + case InterfaceName: switch property { case makerProperty, modelProperty: return prop.ErrReadOnly case streamProperty: - if newValue.Signature() != dbus.SignatureOf(svc.stream) { + var strVal string + + if newValue.Signature() != dbus.SignatureOf(strVal) { return prop.ErrInvalidArg } - if err := svc.setStreamUnlocked(newValue.Value().(string)); err != nil { + if err := svc.setStream(newValue.Value().(string)); err != nil { return dbus.MakeFailedError(err) } default: @@ -340,9 +332,9 @@ func (svc *UpdateService) Set(iface, property string, newValue dbus.Variant) (er } // GetManagedObjects implements org.freedesktop.DBus.ObjectManager.GetManagedObjects. -func (svc *UpdateService) GetManagedObjects() (map[dbus.ObjectPath]map[string]map[string]dbus.Variant, *dbus.Error) { - svc.m.RLock() - defer svc.m.RUnlock() +func (svc *HostedService) GetManagedObjects() (map[dbus.ObjectPath]map[string]map[string]dbus.Variant, *dbus.Error) { + svc.opsMutex.RLock() + defer svc.opsMutex.RUnlock() objsIfacesProps := make(map[dbus.ObjectPath]map[string]map[string]dbus.Variant, len(svc.ops)) diff --git a/spread.yaml b/spread.yaml index b44b78e7ac546e8ffb0350e8b6dfdb9f5beae6aa..418449dff19270aee9113ef55564a7f21e4c7c10 100644 --- a/spread.yaml +++ b/spread.yaml @@ -100,7 +100,17 @@ suites: prepare-each: | echo "Commencing $SPREAD_SUITE prepare-each" | systemd-cat -p info -t spread + systemctl stop sysotad.service + mkdir -p /etc/sysota + mkdir -p /run/sysota + cat <<SYSOTAD_CONF >/etc/sysota/sysotad.conf + [Device] + Maker=test-maker + Model=test-model + [Update] + Stream=test-stream + SYSOTAD_CONF echo "Finished $SPREAD_SUITE prepare-each" | systemd-cat -p info -t spread restore-each: | @@ -108,6 +118,7 @@ suites: # Remove any configuration changes. rm -f /etc/sysota/sysotad.conf + rm -f /run/sysota/sysotad.conf rmdir /etc/sysota rm -rf /run/sysotad