View Javadoc
1   /*
2    * junixsocket
3    *
4    * Copyright 2009-2024 Christian Kohlschütter
5    *
6    * Licensed under the Apache License, Version 2.0 (the "License");
7    * you may not use this file except in compliance with the License.
8    * You may obtain a copy of the License at
9    *
10   *     http://www.apache.org/licenses/LICENSE-2.0
11   *
12   * Unless required by applicable law or agreed to in writing, software
13   * distributed under the License is distributed on an "AS IS" BASIS,
14   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15   * See the License for the specific language governing permissions and
16   * limitations under the License.
17   */
18  package org.newsclub.net.unix.rmi;
19  
20  import java.io.Closeable;
21  import java.io.IOException;
22  import java.net.MalformedURLException;
23  import java.rmi.AccessException;
24  import java.rmi.AlreadyBoundException;
25  import java.rmi.ConnectIOException;
26  import java.rmi.Naming;
27  import java.rmi.NoSuchObjectException;
28  import java.rmi.NotBoundException;
29  import java.rmi.Remote;
30  import java.rmi.RemoteException;
31  import java.rmi.ServerException;
32  import java.rmi.registry.LocateRegistry;
33  import java.rmi.registry.Registry;
34  import java.rmi.server.RMISocketFactory;
35  import java.rmi.server.UnicastRemoteObject;
36  import java.util.HashMap;
37  import java.util.Map;
38  import java.util.Objects;
39  import java.util.concurrent.TimeUnit;
40  import java.util.concurrent.atomic.AtomicBoolean;
41  
42  import org.eclipse.jdt.annotation.NonNull;
43  import org.newsclub.net.unix.AFSocket;
44  import org.newsclub.net.unix.rmi.ShutdownHookSupport.ShutdownHook;
45  
46  import com.kohlschutter.annotations.compiletime.SuppressFBWarnings;
47  
48  /**
49   * The {@link AFSocket}-compatible equivalent of {@link Naming}. Use this class for accessing RMI
50   * registries that are reachable by {@link AFSocket}s.
51   *
52   * @author Christian Kohlschütter
53   */
54  public abstract class AFNaming extends AFRegistryAccess {
55    private static final String RMI_SERVICE_NAME = AFRMIService.class.getName();
56  
57    private static final Map<AFNamingRef, AFNaming> INSTANCES = new HashMap<>();
58  
59    private AFRegistry registry = null;
60    private AFRMIService rmiService = null;
61    private final int registryPort;
62    private final int servicePort;
63    AFRMISocketFactory socketFactory;
64    private final AtomicBoolean remoteShutdownAllowed = new AtomicBoolean(true);
65    private final AtomicBoolean shutdownInProgress = new AtomicBoolean(false);
66    private final AtomicBoolean addedShutdownHook = new AtomicBoolean(false);
67  
68    /**
69     * Creates a new naming instance with the given ports.
70     *
71     * @param registryPort The registry port.
72     * @param servicePort The port for AFRMIService.
73     */
74    protected AFNaming(final int registryPort, final int servicePort) {
75      super();
76      this.registryPort = registryPort;
77      this.servicePort = servicePort;
78    }
79  
80    /**
81     * Creates a new {@link AFRegistry} given a {@link Registry} implementation.
82     *
83     * @param impl The implementation.
84     * @return The new {@link AFRegistry} instance.
85     * @throws RemoteException on error.
86     */
87    protected abstract AFRegistry newAFRegistry(Registry impl) throws RemoteException;
88  
89    /**
90     * Creates or returns the {@link AFRMISocketFactory} to be used with this instance.
91     *
92     * @return The socket factory.
93     * @throws IOException on error.
94     */
95    protected abstract AFRMISocketFactory initSocketFactory() throws IOException;
96  
97    @SuppressWarnings("unchecked")
98    static <T extends AFNaming> T getInstance(final int registryPort,
99        @NonNull AFNamingProvider<T> provider) throws RemoteException {
100     Objects.requireNonNull(provider);
101     final AFNamingRef sap = new AFNamingRef(provider, registryPort);
102     T instance;
103     synchronized (AFNaming.class) {
104       instance = (T) INSTANCES.get(sap);
105       if (instance == null) {
106         try {
107           instance = provider.newInstance(registryPort);
108           Objects.requireNonNull(instance);
109           synchronized (instance) {
110             instance.socketFactory = instance.initSocketFactory();
111           }
112         } catch (RemoteException e) {
113           throw e;
114         } catch (IOException e) {
115           throw new RemoteException(e.getMessage(), e);
116         }
117         INSTANCES.put(sap, instance);
118       }
119     }
120     return instance;
121   }
122 
123   /**
124    * Returns the {@link AFRMISocketFactory} associated with this instance.
125    *
126    * @return The {@link AFRMISocketFactory}.
127    */
128   @SuppressFBWarnings("EI_EXPOSE_REP")
129   public synchronized AFRMISocketFactory getSocketFactory() {
130     return socketFactory;
131   }
132 
133   /**
134    * Returns the registry port.
135    *
136    * @return The port.
137    */
138   public final int getRegistryPort() {
139     return registryPort;
140   }
141 
142   AFRMIService getRMIService() throws RemoteException, NotBoundException {
143     return getRMIService(getRegistry());
144   }
145 
146   synchronized AFRMIService getRMIService(AFRegistry reg) throws RemoteException,
147       NotBoundException {
148     if (rmiService == null) {
149       this.rmiService = getRMIServiceFromRegistry(reg);
150     }
151     return rmiService;
152   }
153 
154   AFRMIService getRMIServiceFromRegistry(AFRegistry reg) throws RemoteException, NotBoundException {
155     AFRMIService service;
156     service = (AFRMIService) reg.lookup(RMI_SERVICE_NAME, 5, TimeUnit.SECONDS);
157     this.remoteShutdownAllowed.set(service.isShutdownAllowed());
158     return service;
159   }
160 
161   private void closeUponRuntimeShutdown() {
162     if (addedShutdownHook.compareAndSet(false, true)) {
163       ShutdownHookSupport.addWeakShutdownHook(new ShutdownHook() {
164 
165         @Override
166         @SuppressWarnings("LockOnNonEnclosingClassLiteral" /* errorprone */)
167         public synchronized void onRuntimeShutdown(Thread thread) throws IOException {
168           if (registry != null && registry.isLocal()) {
169             shutdownRegistry();
170           }
171         }
172       });
173     }
174   }
175 
176   private synchronized void rebindRMIService(final AFRMIService assigner) throws RemoteException {
177     rmiService = assigner;
178     getRegistry().rebind(RMI_SERVICE_NAME, assigner);
179   }
180 
181   @Override
182   public AFRegistry getRegistry() throws RemoteException {
183     return getRegistry(0, TimeUnit.SECONDS);
184   }
185 
186   /**
187    * Returns a reference to the existing RMI registry.
188    *
189    * If there's no registry running at this port after waiting for up to the given time, an
190    * exception is thrown.
191    *
192    * @param timeout The timeout value.
193    * @param unit The timeout unit.
194    * @return The registry.
195    * @throws RemoteException If there was a problem.
196    */
197   public AFRegistry getRegistry(long timeout, TimeUnit unit) throws RemoteException {
198     if (shutdownInProgress.get()) {
199       throw new ShutdownException();
200     }
201     synchronized (this) {
202       AFRegistry reg = getRegistry(false);
203       if (reg == null) {
204         reg = openRegistry(timeout, unit);
205       }
206       return reg;
207     }
208   }
209 
210   /**
211    * Tries to access the registry, waiting some time if necessary.
212    *
213    * @param timeout The timeout.
214    * @param unit The unit for the timeout.
215    * @return The registry instance.
216    * @throws RemoteException on error.
217    */
218   protected abstract AFRegistry openRegistry(long timeout, TimeUnit unit) throws RemoteException;
219 
220   /**
221    * Returns a reference to the RMI registry, or {@code null}.
222    *
223    * If there's no registry running at this port, and {@code create} is set to {@code true}, a new
224    * one is created; when {@code create} is set to {@code false}, {@code null} is returned.
225    *
226    * @param create {@code true} if a new register may be created if necessary.
227    * @return The registry, or {@code null}
228    * @throws RemoteException If there was a problem.
229    */
230   public AFRegistry getRegistry(boolean create) throws RemoteException {
231     if (shutdownInProgress.get()) {
232       throw new ShutdownException();
233     }
234     synchronized (this) {
235       if (registry != null) {
236         return registry;
237       } else if (!socketFactory.hasRegisteredPort(registryPort)) {
238         return create ? createRegistry() : null;
239       }
240 
241       AFRegistry reg = locateRegistry();
242       setRegistry(reg);
243 
244       try {
245         getRMIService(reg);
246       } catch (NotBoundException | NoSuchObjectException | ConnectIOException e) {
247         if (create) {
248           setRegistry(null);
249           return createRegistry();
250         } else {
251           throw new ServerException("Could not access " + AFRMIService.class.getName(), e);
252         }
253       }
254 
255       return registry;
256     }
257   }
258 
259   private AFRegistry locateRegistry() throws RemoteException {
260     Registry regImpl = LocateRegistry.getRegistry(null, registryPort, socketFactory);
261     return regImpl == null ? null : newAFRegistry(regImpl);
262   }
263 
264   /**
265    * Shuts this RMI Registry down.
266    *
267    * @throws RemoteException if the operation fails.
268    */
269   public void shutdownRegistry() throws RemoteException {
270     synchronized (this) {
271       if (registry == null) {
272         return;
273       }
274 
275       AFRegistry registryToBeClosed = registry;
276       AFRMIService rmiServiceToBeClosed = rmiService;
277 
278       if (!registryToBeClosed.isLocal()) {
279         if (!isRemoteShutdownAllowed()) {
280           throw new ServerException("The server refuses to be shutdown remotely");
281         }
282         setRegistry(null);
283 
284         try {
285           shutdownViaRMIService(registryToBeClosed, rmiServiceToBeClosed);
286         } catch (Exception e) {
287           // ignore
288         }
289         return;
290       }
291 
292       setRegistry(null);
293 
294       if (!shutdownInProgress.compareAndSet(false, true)) {
295         return;
296       }
297       try {
298         unexportRMIService(registryToBeClosed, (AFRMIServiceImpl) rmiServiceToBeClosed);
299         forceUnexportBound(registryToBeClosed);
300         closeSocketFactory();
301         shutdownRegistryFinishingTouches();
302       } finally {
303         shutdownInProgress.set(false);
304       }
305     }
306   }
307 
308   /**
309    * Called by {@link #shutdownRegistry()} as the final step.
310    */
311   protected abstract void shutdownRegistryFinishingTouches();
312 
313   private synchronized void unexportRMIService(AFRegistry reg, AFRMIServiceImpl serv)
314       throws AccessException, RemoteException {
315     if (serv != null) {
316       serv.shutdownRegisteredCloseables();
317     }
318 
319     try {
320       if (serv != null) {
321         unexportObject(serv);
322       }
323       reg.unbind(RMI_SERVICE_NAME);
324     } catch (ShutdownException | NotBoundException e) {
325       // ignore
326     }
327     this.rmiService = null;
328   }
329 
330   private void forceUnexportBound(AFRegistry reg) {
331     try {
332       reg.forceUnexportBound();
333     } catch (Exception e) {
334       // ignore
335     }
336   }
337 
338   private void closeSocketFactory() {
339     if (socketFactory != null) {
340       try {
341         socketFactory.close();
342       } catch (IOException e) {
343         // ignore
344       }
345     }
346   }
347 
348   private void shutdownViaRMIService(AFRegistry reg, AFRMIService serv) throws RemoteException {
349     try {
350       if (serv == null) {
351         serv = getRMIService(reg);
352       }
353       if (serv.isShutdownAllowed()) {
354         serv.shutdown();
355       }
356     } catch (ServerException | ConnectIOException | NotBoundException e) {
357       // ignore
358     }
359   }
360 
361   /**
362    * Creates a new RMI {@link Registry}.
363    *
364    * If there already was a registry created previously, it is shut down and replaced by the current
365    * one.
366    *
367    * Use {@link #getRegistry()} to try to reuse an existing registry.
368    *
369    * @return The registry
370    * @throws RemoteException if the operation fails.
371    * @see #getRegistry()
372    */
373   public synchronized AFRegistry createRegistry() throws RemoteException {
374     AFRegistry existingRegistry = registry;
375     if (existingRegistry == null) {
376       try {
377         existingRegistry = getRegistry(false);
378       } catch (ServerException e) {
379         Throwable cause = e.getCause();
380         if (cause instanceof NotBoundException || cause instanceof ConnectIOException) {
381           existingRegistry = null;
382         } else {
383           throw e;
384         }
385       }
386     }
387     if (existingRegistry != null) {
388       if (!isRemoteShutdownAllowed()) {
389         throw new ServerException("The server refuses to be shutdown remotely");
390       }
391       shutdownRegistry();
392     }
393 
394     initRegistryPrerequisites();
395     AFRegistry newAFRegistry = newAFRegistry(LocateRegistry.createRegistry(registryPort,
396         socketFactory, socketFactory));
397     setRegistry(newAFRegistry);
398 
399     final AFRMIService service = new AFRMIServiceImpl(this);
400     UnicastRemoteObject.exportObject(service, servicePort, socketFactory, socketFactory);
401 
402     rebindRMIService(service);
403 
404     return registry;
405   }
406 
407   /**
408    * Called by {@link #createRegistry()} right before creating/setting the registry.
409    *
410    * @throws ServerException on error.
411    */
412   protected abstract void initRegistryPrerequisites() throws ServerException;
413 
414   /**
415    * Checks if this {@link AFNaming} instance can be shut down remotely.
416    *
417    * @return {@code true} if remote shutdown is allowed.
418    */
419   public boolean isRemoteShutdownAllowed() {
420     return remoteShutdownAllowed.get();
421   }
422 
423   /**
424    * Controls whether this {@link AFNaming} instance can be shut down remotely.
425    *
426    * @param remoteShutdownAllowed {@code true} if remote shutdown is allowed.
427    */
428   public void setRemoteShutdownAllowed(boolean remoteShutdownAllowed) {
429     this.remoteShutdownAllowed.set(remoteShutdownAllowed);
430   }
431 
432   /**
433    * Exports and binds the given Remote object to the given name, using the given {@link AFNaming}
434    * setup.
435    *
436    * @param name The name to use to bind the object in the registry.
437    * @param obj The object to export and bind.
438    * @throws RemoteException if the operation fails.
439    * @throws AlreadyBoundException if there already was something bound at that name
440    */
441   public void exportAndBind(String name, Remote obj) throws RemoteException, AlreadyBoundException {
442     exportObject(obj, getSocketFactory());
443 
444     getRegistry().bind(name, obj);
445   }
446 
447   /**
448    * Exports and re-binds the given Remote object to the given name, using the given
449    * {@link AFNaming} setup.
450    *
451    * @param name The name to use to bind the object in the registry.
452    * @param obj The object to export and bind.
453    * @throws RemoteException if the operation fails.
454    */
455   public void exportAndRebind(String name, Remote obj) throws RemoteException {
456     exportObject(obj, getSocketFactory());
457 
458     getRegistry().rebind(name, obj);
459   }
460 
461   /**
462    * Forcibly un-exports the given object, if it exists, and unbinds the object from the registry
463    * (otherwise returns without an error).
464    *
465    * @param name The name used to bind the object.
466    * @param obj The object to un-export.
467    * @throws RemoteException if the operation fails.
468    */
469   public void unexportAndUnbind(String name, Remote obj) throws RemoteException {
470     unexportObject(obj);
471     try {
472       unbind(name);
473     } catch (MalformedURLException | NotBoundException e) {
474       // ignore
475     }
476   }
477 
478   /**
479    * Exports the given Remote object, using the given socket factory and a randomly assigned port.
480    *
481    * NOTE: This helper function can also be used for regular RMI servers.
482    *
483    * @param obj The object to export.
484    * @param socketFactory The socket factory to use.
485    * @return The remote stub.
486    * @throws RemoteException if the operation fails.
487    */
488   public static Remote exportObject(Remote obj, RMISocketFactory socketFactory)
489       throws RemoteException {
490     return UnicastRemoteObject.exportObject(obj, 0, socketFactory, socketFactory);
491   }
492 
493   /**
494    * Forcibly un-exports the given object, if it exists (otherwise returns without an error). This
495    * should be called upon closing a {@link Closeable} {@link Remote} object.
496    *
497    * NOTE: This helper function can also be used for regular RMI servers.
498    *
499    * @param obj The object to un-export.
500    */
501   public static void unexportObject(Remote obj) {
502     try {
503       UnicastRemoteObject.unexportObject(obj, true);
504     } catch (NoSuchObjectException e) {
505       // ignore
506     }
507   }
508 
509   private synchronized void setRegistry(AFRegistry registry) {
510     this.registry = registry;
511     if (registry == null) {
512       rmiService = null;
513     } else if (registry.isLocal()) {
514       closeUponRuntimeShutdown();
515     }
516   }
517 }