@@ -41,6 +41,9 @@ type Service struct {
4141 pluginRootsMu sync.Mutex
4242 pluginDataRoots map [string ]* ScopedRoot
4343 pluginStoreRoots map [string ]* ScopedRoot
44+
45+ closeOnce sync.Once
46+ closeErr error
4447}
4548
4649type config struct {
@@ -140,41 +143,45 @@ func (s *Service) closePartial() {
140143
141144// Close releases all resources held by the Service.
142145// Errors from individual close operations are aggregated and returned together.
146+ // Close is idempotent; subsequent calls return the result of the first call.
143147func (s * Service ) Close () error {
144- var allErrors []error
145-
146- s .pluginRootsMu .Lock ()
147- for id , r := range s .pluginDataRoots {
148- if err := r .Close (); err != nil {
149- allErrors = append (allErrors , fmt .Errorf ("close plugin data root %q: %w" , id , err ))
148+ s .closeOnce .Do (func () {
149+ var allErrors []error
150+
151+ s .pluginRootsMu .Lock ()
152+ for id , r := range s .pluginDataRoots {
153+ if err := r .Close (); err != nil {
154+ allErrors = append (allErrors , fmt .Errorf ("close plugin data root %q: %w" , id , err ))
155+ }
150156 }
151- }
152- for id , r := range s . pluginStoreRoots {
153- if err := r . Close (); err != nil {
154- allErrors = append ( allErrors , fmt . Errorf ( "close plugin store root %q: %w" , id , err ))
157+ for id , r := range s . pluginStoreRoots {
158+ if err := r . Close (); err != nil {
159+ allErrors = append ( allErrors , fmt . Errorf ( "close plugin store root %q: %w" , id , err ))
160+ }
155161 }
156- }
157- s .pluginDataRoots = nil
158- s .pluginStoreRoots = nil
159- s .pluginRootsMu .Unlock ()
162+ s .pluginDataRoots = nil
163+ s .pluginStoreRoots = nil
164+ s .pluginRootsMu .Unlock ()
160165
161- if err := s .plugins .Close (); err != nil {
162- allErrors = append (allErrors , fmt .Errorf ("close plugins root: %w" , err ))
163- }
164- if err := s .logs .Close (); err != nil {
165- allErrors = append (allErrors , fmt .Errorf ("close logs root: %w" , err ))
166- }
167- if err := s .rootDir .Close (); err != nil {
168- allErrors = append (allErrors , fmt .Errorf ("close root dir: %w" , err ))
169- }
170- if s .osRoot != nil {
171- if err := s .osRoot .Close (); err != nil {
172- allErrors = append (allErrors , fmt .Errorf ("close os root: %w" , err ))
166+ if err := s .plugins .Close (); err != nil {
167+ allErrors = append (allErrors , fmt .Errorf ("close plugins root: %w" , err ))
173168 }
174- }
175- releaseLock (s .lock )
169+ if err := s .logs .Close (); err != nil {
170+ allErrors = append (allErrors , fmt .Errorf ("close logs root: %w" , err ))
171+ }
172+ if err := s .rootDir .Close (); err != nil {
173+ allErrors = append (allErrors , fmt .Errorf ("close root dir: %w" , err ))
174+ }
175+ if s .osRoot != nil {
176+ if err := s .osRoot .Close (); err != nil {
177+ allErrors = append (allErrors , fmt .Errorf ("close os root: %w" , err ))
178+ }
179+ }
180+ releaseLock (s .lock )
176181
177- return errors .Join (allErrors ... )
182+ s .closeErr = errors .Join (allErrors ... )
183+ })
184+ return s .closeErr
178185}
179186
180187// Root returns the absolute path to the state directory.
@@ -191,52 +198,46 @@ func (s *Service) RootDir() *ScopedRoot { return s.rootDir }
191198
192199// PluginData returns a scoped root for a specific plugin's data directory.
193200// The result is cached; subsequent calls for the same id return the cached instance.
194- // Returns nil if the scoped root cannot be created.
195- func (s * Service ) PluginData (id string ) * ScopedRoot {
201+ func (s * Service ) PluginData (id string ) (* ScopedRoot , error ) {
196202 if err := validatePluginID (id ); err != nil {
197- fmt .Fprintf (os .Stderr , "appstate: PluginData: %v\n " , err )
198- return nil
203+ return nil , fmt .Errorf ("appstate: PluginData: %w" , err )
199204 }
200205
201206 s .pluginRootsMu .Lock ()
202207 defer s .pluginRootsMu .Unlock ()
203208
204209 if r , ok := s .pluginDataRoots [id ]; ok {
205- return r
210+ return r , nil
206211 }
207212
208213 dir := filepath .Join (s .root , "plugins" , id , "data" )
209214 r , err := newScopedRoot (dir )
210215 if err != nil {
211- fmt .Fprintf (os .Stderr , "appstate: failed to create plugin data root for %q: %v\n " , id , err )
212- return nil
216+ return nil , fmt .Errorf ("appstate: create plugin data root for %q: %w" , id , err )
213217 }
214218 s .pluginDataRoots [id ] = r
215- return r
219+ return r , nil
216220}
217221
218222// PluginStore returns a scoped root for a specific plugin's store directory.
219223// The result is cached; subsequent calls for the same id return the cached instance.
220- // Returns nil if the scoped root cannot be created.
221- func (s * Service ) PluginStore (id string ) * ScopedRoot {
224+ func (s * Service ) PluginStore (id string ) (* ScopedRoot , error ) {
222225 if err := validatePluginID (id ); err != nil {
223- fmt .Fprintf (os .Stderr , "appstate: PluginStore: %v\n " , err )
224- return nil
226+ return nil , fmt .Errorf ("appstate: PluginStore: %w" , err )
225227 }
226228
227229 s .pluginRootsMu .Lock ()
228230 defer s .pluginRootsMu .Unlock ()
229231
230232 if r , ok := s .pluginStoreRoots [id ]; ok {
231- return r
233+ return r , nil
232234 }
233235
234236 dir := filepath .Join (s .root , "plugins" , id , "store" )
235237 r , err := newScopedRoot (dir )
236238 if err != nil {
237- fmt .Fprintf (os .Stderr , "appstate: failed to create plugin store root for %q: %v\n " , id , err )
238- return nil
239+ return nil , fmt .Errorf ("appstate: create plugin store root for %q: %w" , id , err )
239240 }
240241 s .pluginStoreRoots [id ] = r
241- return r
242+ return r , nil
242243}
0 commit comments