2222import java .util .List ;
2323import java .util .Map ;
2424import java .util .Objects ;
25- import java .util .concurrent .CompletableFuture ;
25+ import java .util .concurrent .CancellationException ;
2626import java .util .concurrent .ConcurrentHashMap ;
2727import java .util .concurrent .CopyOnWriteArrayList ;
28+ import java .util .concurrent .ExecutionException ;
2829import java .util .concurrent .ExecutorService ;
2930import java .util .concurrent .Executors ;
3031import java .util .concurrent .ScheduledExecutorService ;
3132import java .util .concurrent .ScheduledFuture ;
3233import java .util .concurrent .TimeUnit ;
34+ import java .util .concurrent .TimeoutException ;
35+ import java .util .concurrent .atomic .AtomicReference ;
3336import java .util .function .Predicate ;
3437
3538import org .eclipse .jdt .annotation .NonNullByDefault ;
3639import org .eclipse .jdt .annotation .Nullable ;
3740import org .openhab .core .common .ThreadPoolManager ;
3841import org .openhab .core .service .WatchService ;
3942import org .osgi .framework .BundleContext ;
43+ import org .osgi .framework .InvalidSyntaxException ;
4044import org .osgi .framework .ServiceRegistration ;
45+ import org .osgi .service .cm .Configuration ;
46+ import org .osgi .service .cm .ConfigurationAdmin ;
47+ import org .osgi .service .cm .ConfigurationEvent ;
48+ import org .osgi .service .cm .ConfigurationListener ;
49+ import org .osgi .service .component .ComponentContext ;
4150import org .osgi .service .component .annotations .Activate ;
4251import org .osgi .service .component .annotations .Component ;
4352import org .osgi .service .component .annotations .ConfigurationPolicy ;
4453import org .osgi .service .component .annotations .Deactivate ;
4554import org .osgi .service .component .annotations .Modified ;
55+ import org .osgi .service .component .annotations .Reference ;
4656import org .slf4j .Logger ;
4757import org .slf4j .LoggerFactory ;
4858
@@ -73,21 +83,22 @@ public class WatchServiceImpl implements WatchService, DirectoryChangeListener {
7383 private final List <Listener > dirPathListeners = new CopyOnWriteArrayList <>();
7484 private final List <Listener > subDirPathListeners = new CopyOnWriteArrayList <>();
7585 private final Map <Path , FileHash > hashCache = new ConcurrentHashMap <>();
86+ private final ConfigurationAdmin configurationAdmin ;
7687 private final ExecutorService executor ;
7788 private final ScheduledExecutorService scheduler ;
7889
7990 private final String name ;
8091 private final BundleContext bundleContext ;
81-
82- private @ Nullable Path basePath ;
83- private @ Nullable DirectoryWatcher dirWatcher ;
84- private @ Nullable ServiceRegistration <WatchService > reg ;
92+ private volatile @ Nullable Path basePath ;
93+ volatile @ Nullable DirectoryWatcher dirWatcher ;
8594
8695 private final Map <Path , ScheduledFuture <?>> scheduledEvents = new HashMap <>();
8796 private final Map <Path , List <DirectoryChangeEvent >> scheduledEventKinds = new ConcurrentHashMap <>();
8897
8998 @ Activate
90- public WatchServiceImpl (WatchServiceConfiguration config , BundleContext bundleContext ) throws IOException {
99+ public WatchServiceImpl (@ Reference ConfigurationAdmin configurationAdmin , WatchServiceConfiguration config ,
100+ BundleContext bundleContext , ComponentContext componentContext ) throws IOException {
101+ this .configurationAdmin = configurationAdmin ;
91102 this .bundleContext = bundleContext ;
92103 if (config .name ().isBlank ()) {
93104 throw new IllegalArgumentException ("service name must not be blank" );
@@ -96,11 +107,11 @@ public WatchServiceImpl(WatchServiceConfiguration config, BundleContext bundleCo
96107 this .name = config .name ();
97108 executor = Executors .newSingleThreadExecutor (r -> new Thread (r , name ));
98109 scheduler = ThreadPoolManager .getScheduledPool ("watchservice" );
99- modified (config );
110+ modified (config , componentContext );
100111 }
101112
102113 @ Modified
103- public void modified (WatchServiceConfiguration config ) throws IOException {
114+ public void modified (WatchServiceConfiguration config , final ComponentContext componentContext ) throws IOException {
104115 logger .trace ("Trying to setup WatchService '{}' with path '{}'" , config .name (), config .path ());
105116
106117 Path basePath = Path .of (config .path ()).toAbsolutePath ();
@@ -109,23 +120,76 @@ public void modified(WatchServiceConfiguration config) throws IOException {
109120 return ;
110121 }
111122
123+ final boolean cycle = this .basePath != null ;
112124 this .basePath = basePath ;
113125
114126 try {
115- closeWatcherAndUnregister ();
127+ closeWatcher ();
116128
117129 if (!Files .exists (basePath )) {
118130 logger .info ("Watch directory '{}' does not exist. Trying to create it." , basePath );
119131 Files .createDirectories (basePath );
120132 }
121133
122134 DirectoryWatcher newDirWatcher = DirectoryWatcher .builder ().listener (this ).path (basePath ).build ();
123- CompletableFuture
124- .runAsync (
125- () -> newDirWatcher .watchAsync (executor )
126- .thenRun (() -> logger .debug ("WatchService '{}' has been shut down." , name )),
127- ThreadPoolManager .getScheduledPool (ThreadPoolManager .THREAD_POOL_NAME_COMMON ))
128- .thenRun (this ::registerWatchService );
135+ ThreadPoolManager .getScheduledPool (ThreadPoolManager .THREAD_POOL_NAME_COMMON ).execute (() -> {
136+ if (cycle ) {
137+ Object pid = componentContext .getProperties ().get ("service.pid" );
138+ if (pid instanceof String pidString ) {
139+ Configuration [] configs = null ;
140+ try {
141+ configs = configurationAdmin .listConfigurations ("(service.pid=" + pidString + ")" );
142+ } catch (IOException | InvalidSyntaxException e ) {
143+ logger .warn ("WatchService '{}': Failed to acquire configuration, cannot restart service" ,
144+ name , e );
145+ }
146+
147+ if (configs != null && configs .length > 0 ) {
148+ final AtomicReference <@ Nullable ServiceRegistration <ConfigurationListener >> registrationReference = new AtomicReference <>();
149+
150+ ConfigurationListener tempListener = new ConfigurationListener () {
151+ @ Override
152+ public void configurationEvent (@ Nullable ConfigurationEvent event ) {
153+ if (event != null && event .getType () == ConfigurationEvent .CM_DELETED
154+ && pidString .equals (event .getPid ())) {
155+ logger .debug ("WatchService '{}': Configuration deleted" , name );
156+
157+ // Unregister the listener first, this is a one trick pony
158+ ServiceRegistration <ConfigurationListener > registration = registrationReference
159+ .get ();
160+ if (registration != null ) {
161+ try {
162+ registration .unregister ();
163+ } catch (IllegalStateException e ) {
164+ // Already unregistered
165+ }
166+ }
167+
168+ createConfiguration (basePath );
169+ }
170+ }
171+ };
172+
173+ ServiceRegistration <ConfigurationListener > registration = bundleContext
174+ .registerService (ConfigurationListener .class , tempListener , null );
175+ registrationReference .set (registration );
176+ try {
177+ configs [0 ].delete ();
178+ } catch (IOException | RuntimeException e ) {
179+ logger .warn ("WatchService '{}': Failed to delete configuration, cannot restart service" ,
180+ name , e );
181+ try {
182+ registration .unregister ();
183+ } catch (IllegalStateException e2 ) {
184+ // Already unregistered
185+ }
186+ }
187+ }
188+ }
189+ }
190+ newDirWatcher .watchAsync (executor )
191+ .thenRun (() -> logger .debug ("WatchService '{}' has been shut down." , name ));
192+ });
129193 this .dirWatcher = newDirWatcher ;
130194 } catch (NoSuchFileException e ) {
131195 // log message here, otherwise it'll be swallowed by the call to newInstance in the factory
@@ -143,37 +207,47 @@ public void modified(WatchServiceConfiguration config) throws IOException {
143207 @ Deactivate
144208 public void deactivate () {
145209 try {
146- closeWatcherAndUnregister ();
147- executor .shutdown ();
210+ closeWatcher ();
211+ executor .shutdownNow ();
212+ try {
213+ executor .awaitTermination (1000L , TimeUnit .MILLISECONDS );
214+ } catch (InterruptedException e ) {
215+ Thread .currentThread ().interrupt ();
216+ // We still want to cancel the scheduled events, so let it be re-caught after that
217+ }
218+ synchronized (scheduledEvents ) {
219+ for (ScheduledFuture <?> future : scheduledEvents .values ()) {
220+ if (!future .isDone ()) {
221+ future .cancel (true );
222+ }
223+ }
224+ for (ScheduledFuture <?> future : scheduledEvents .values ()) {
225+ if (!future .isDone ()) {
226+ try {
227+ future .get (1000L , TimeUnit .MILLISECONDS );
228+ } catch (CancellationException e ) {
229+ // This is what we want. move on
230+ } catch (InterruptedException e ) {
231+ Thread .currentThread ().interrupt ();
232+ return ;
233+ } catch (ExecutionException | TimeoutException e ) {
234+ logger .debug ("Failed to conclude scheduled event during deactivate: {}" , e .getMessage (), e );
235+ }
236+ }
237+ }
238+ }
148239 } catch (IOException e ) {
149240 logger .warn ("Failed to shutdown WatchService '{}'" , name , e );
150241 }
151242 }
152243
153- private void registerWatchService () {
154- Dictionary <String , Object > properties = new Hashtable <>();
155- properties .put (WatchService .SERVICE_PROPERTY_NAME , name );
156- this .reg = bundleContext .registerService (WatchService .class , this , properties );
157- logger .debug ("WatchService '{}' completed initialization and registered itself as service." , name );
158- }
159-
160- private void closeWatcherAndUnregister () throws IOException {
244+ private void closeWatcher () throws IOException {
161245 DirectoryWatcher localDirWatcher = this .dirWatcher ;
162246 if (localDirWatcher != null ) {
163247 localDirWatcher .close ();
164248 this .dirWatcher = null ;
165249 }
166250
167- ServiceRegistration <?> localReg = this .reg ;
168- if (localReg != null ) {
169- try {
170- localReg .unregister ();
171- } catch (IllegalStateException e ) {
172- logger .debug ("WatchService '{}' was already unregistered." , name , e );
173- }
174- this .reg = null ;
175- }
176-
177251 hashCache .clear ();
178252 }
179253
@@ -230,11 +304,39 @@ public void onEvent(@Nullable DirectoryChangeEvent directoryChangeEvent) throws
230304 future .cancel (true );
231305 }
232306 future = scheduler .schedule (() -> notifyListeners (path ), PROCESSING_TIME , TimeUnit .MILLISECONDS );
233- scheduledEventKinds .computeIfAbsent (path , k -> new CopyOnWriteArrayList <>()).add (directoryChangeEvent );
307+ Objects .requireNonNull (scheduledEventKinds .computeIfAbsent (path , k -> new CopyOnWriteArrayList <>()))
308+ .add (directoryChangeEvent );
234309 scheduledEvents .put (path , future );
235310 }
236311 }
237312
313+ private void createConfiguration (Path basePath ) {
314+ logger .debug ("WatchService '{}': Creating new configuration for path '{}'" , name , basePath );
315+ try {
316+ String filter = "(&(name=" + name + ")" + "(service.factoryPid=" + WatchService .SERVICE_PID + "))" ;
317+ Configuration [] configurations = configurationAdmin .listConfigurations (filter );
318+
319+ if (configurations == null || configurations .length == 0 ) {
320+ Configuration c = configurationAdmin .createFactoryConfiguration (WatchService .SERVICE_PID , "?" );
321+ Dictionary <String , Object > map = new Hashtable <>();
322+
323+ map .put ("name" , name );
324+ map .put (WatchService .SERVICE_PROPERTY_NAME , name );
325+ map .put ("path" , basePath .toString ());
326+ c .update (map );
327+ } else {
328+ Configuration c = configurations [0 ];
329+ Dictionary <String , Object > map = c .getProperties ();
330+ map .put ("name" , name );
331+ map .put (WatchService .SERVICE_PROPERTY_NAME , name );
332+ map .put ("path" , basePath .toString ());
333+ c .update (map );
334+ }
335+ } catch (IOException | InvalidSyntaxException e ) {
336+ logger .error ("WatchService '{}': Failed to create configuration with path '{}'" , name , basePath , e );
337+ }
338+ }
339+
238340 private void notifyListeners (Path path ) {
239341 List <DirectoryChangeEvent > events = scheduledEventKinds .remove (path );
240342 if (events == null || events .isEmpty ()) {
0 commit comments