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 c81d8e8102c832364b5815a0c87c97a660828e08..0b625d86c93094c46a5b17216f1da2234605f7bd 100644 --- a/service/service.go +++ b/service/service.go @@ -15,9 +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 @@ -69,20 +71,30 @@ func WithExitChannel(exit chan<- struct{}) Option { } } -// Service implements the D-Bus service capable of downloading system updates. +// 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 stateGuard ota.StateGuard confGuard ota.ConfigGuard - updateService UpdateService - + bootProto boot.Protocol raucAdapter *raucadapter.Adapter - raucService *raucinterface.Service + exit chan<- struct{} - test testhosted.HostedService - exit chan<- struct{} + boot boothosted.HostedService + update updatehosted.HostedService + rauc rauchosted.HostedService + test testhosted.HostedService } var bootProviders = map[ota.BootLoaderType]func(config *ota.Config) (boot.Protocol, error){ @@ -119,6 +131,8 @@ func configuredBootProtocol(cfgGuard *ota.ConfigGuard) (boot.Protocol, error) { return nil, unsupportedBootLoaderError(cfg.BootLoaderType) } +type dbusNotificationKey struct{} + // New returns a new service. // // In practice you want to call it with both the WithConfigurer WithStater @@ -126,7 +140,6 @@ func configuredBootProtocol(cfgGuard *ota.ConfigGuard) (boot.Protocol, error) { // configuration and state and all changes will be ephemeral. func New(conn *dbus.Conn, opts ...Option) (*Service, error) { svc := &Service{} - svc.ServiceHost.Init(conn) for _, opt := range opts { if err := opt(svc); err != nil { @@ -134,35 +147,63 @@ func New(conn *dbus.Conn, opts ...Option) (*Service, error) { } } + svc.stateGuard.RegisterObserver(dbusNotificationKey{}, func(st *ota.SystemState) error { + // TODO: send dbus notifications once we expose the BootMode property. + return nil + }) + + svc.ServiceHost.Init(conn) + bootProto, err := configuredBootProtocol(&svc.confGuard) if err != nil { return nil, err } - svc.raucAdapter = raucadapter.New(bootProto, svc) + svc.bootProto = bootProto - // Configure and add the boot service if the corresponding - // debug flag is enabled. - if svc.confGuard.Config().DebugBootAPI { - bootService := &BootProtocolService{ - bootProto: bootProto, - } - svc.AddHostedService(bootService) + svc.setupRaucAdapter() + svc.setupBootProtocolService() + svc.setupUpdateService() + svc.setupRaucService() + svc.setupTestService() + + return svc, nil +} + +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) +} + +// 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 } - // Configure and add the update service. - svc.updateService.maker = "dummy-maker" - svc.updateService.model = "dummy-model" - svc.updateService.stream = "dummy-stream" - svc.AddHostedService(&svc.updateService) + svc.boot.Init(svc.bootProto) + svc.AddHostedService(&svc.boot) +} - // Configure and add the RAUC service - svc.raucService = raucinterface.NewService(svc.raucAdapter, svc.raucAdapter) - svc.AddHostedService(svc.raucService) +func (svc *Service) setupUpdateService() { + svc.update.Init(&svc.confGuard) - svc.setupTestService() + svc.AddHostedService(&svc.update) +} - return svc, nil +func (svc *Service) setupRaucService() { + svc.rauc.Init(svc.raucAdapter, svc.raucAdapter) + svc.AddHostedService(&svc.rauc) +} + +// 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 } // setupTestService configures and adds the test service @@ -177,27 +218,7 @@ func (svc *Service) setupTestService() { svc.AddHostedService(&svc.test) } -// BootMode implements raucadapter.BootModeHolder and returns the current boot mode. -func (svc *Service) BootMode() boot.Mode { - return svc.stateGuard.State().BootMode -} - -// SetBootMode implements raucadapter.BootModeHolder and sets the new boot mode. -func (svc *Service) SetBootMode(bootMode boot.Mode) error { - return svc.stateGuard.AlterState(func(st *ota.SystemState) (bool, error) { - if st.BootMode == bootMode { - return false, nil - } - - // TODO(zyga): emit the PropertyChanged signal here. - // Any errors should be logged and ignored. - st.BootMode = bootMode - - return true, nil - }) -} - // 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 0dfebde220f27bc63ab816beec44719cdceece32..6ebc46c059453b7a6dbf16d5a7ec4d029a108859 100644 --- a/service/service_test.go +++ b/service/service_test.go @@ -65,7 +65,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{} } @@ -85,6 +89,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) @@ -220,15 +234,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`) @@ -241,9 +255,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) @@ -291,14 +305,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/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))