blob: ddd9bdc448fc03bf84bab58bd631c390383e334d [file] [log] [blame]
/*=============================================================================#
# Copyright (c) 2009, 2019 Stephan Wahlbrink and others.
#
# This program and the accompanying materials are made available under the
# terms of the Eclipse Public License 2.0 which is available at
# https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0
# which is available at https://www.apache.org/licenses/LICENSE-2.0.
#
# SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
#
# Contributors:
# Stephan Wahlbrink <sw@wahlbrink.eu> - initial API and implementation
#=============================================================================*/
package org.eclipse.statet.jcommons.rmi;
import static org.eclipse.statet.internal.jcommons.rmi.CommonsRmiInternals.BUNDLE_ID;
import java.io.File;
import java.net.BindException;
import java.net.InetAddress;
import java.net.URI;
import java.rmi.RemoteException;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
import java.text.MessageFormat;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import javax.rmi.ssl.SslRMIClientSocketFactory;
import javax.rmi.ssl.SslRMIServerSocketFactory;
import org.eclipse.statet.internal.jcommons.rmi.Messages;
import org.eclipse.statet.internal.jcommons.rmi.eruntime.ERuntimeContributor;
import org.eclipse.statet.jcommons.lang.NonNullByDefault;
import org.eclipse.statet.jcommons.lang.Nullable;
import org.eclipse.statet.jcommons.net.Port;
import org.eclipse.statet.jcommons.net.PortRange;
import org.eclipse.statet.jcommons.runtime.CommonsRuntime;
import org.eclipse.statet.jcommons.status.ErrorStatus;
import org.eclipse.statet.jcommons.status.InfoStatus;
import org.eclipse.statet.jcommons.status.ProgressMonitor;
import org.eclipse.statet.jcommons.status.Status;
import org.eclipse.statet.jcommons.status.StatusException;
import org.eclipse.statet.jcommons.status.Statuses;
/**
* Utility managing local RMI registries:
* <ul>
* <li>Embedded private RMI registry: A single registry
* intended to use for the current application only.</li>
* <li>Separate RMI registries: Started in a separate
* process at a specified port. Multiple registries and
* shutdown behaviour possible.</li>
* </ul>
*
* Note: stop modes are not applied if the registry is already started.
*/
@NonNullByDefault
public class RMIRegistryManager {
public static enum StopRule {
/**
* Mode to stop the registry always automatically.
*/
ALWAYS,
/**
* Mode to stop the registry never automatically.
*/
NEVER,
/**
* Mode to stop the registry if empty.
*/
IF_EMPTY,
}
private static final class ManagedRegistry {
public static final int SEPERATE= 1 << 0;
public static final int EMBEDDED= 1 << 1;
public static final int FOREIGN= 1 << 2;
private final RMIRegistry registry;
private final int type;
private @Nullable Process process;
private final StopRule stopRule;
public ManagedRegistry(final RMIRegistry registry, final int type,
final StopRule stopRule) {
this.registry= registry;
this.type= type;
this.stopRule= stopRule;
}
}
private static final String EMBEDDED_PORT_RANGE_KEY= "org.eclipse.statet.jcommons.rmi.registry.TcpPort"; //$NON-NLS-1$
private static final PortRange EMBEDDED_PORT_RANGE_DEFAULT= new PortRange(51100, 51199);
public static final RMIRegistryManager INSTANCE= new RMIRegistryManager(true);
private final Map<Port, ManagedRegistry> registries= new HashMap<>();
private final Object embeddedLock= new Object();
private PortRange embeddedPortRange;
private boolean embeddedStartSeparate= true;
private boolean embeddedSsl= false;
private @Nullable ManagedRegistry embeddedRegistry;
private final List<ManagedRegistry> embeddedRegistries= new ArrayList<>(4);
private @Nullable List<URI> embeddedCodebaseEntries;
private boolean embeddedCodebaseLoadContrib;
private RMIRegistryManager(final boolean instance) {
initDispose();
this.embeddedPortRange= EMBEDDED_PORT_RANGE_DEFAULT;
if (instance) {
this.embeddedCodebaseLoadContrib= true;
final String s= System.getProperty(EMBEDDED_PORT_RANGE_KEY);
if (s != null && s.length() > 0) {
try {
this.embeddedPortRange= PortRange.valueOf(s);
}
catch (final IllegalArgumentException e) {
this.embeddedPortRange= EMBEDDED_PORT_RANGE_DEFAULT;
CommonsRuntime.log(new ErrorStatus(BUNDLE_ID,
"The value of the Java property '" + EMBEDDED_PORT_RANGE_KEY + "' is invalid.",
e ));
}
}
}
}
protected void initDispose() {
CommonsRuntime.getEnvironment().addStoppingListener(() -> RMIRegistryManager.this.dispose());
}
/**
* Returns a handler for the RMI registry at the local host and the given port.
*
* The registry must be started by this util instance.
*
* @param port the registry port
* @return the registry handler or <code>null</code>
*/
public @Nullable RMIRegistry getRegistry(int port) {
if (port <= 0) {
port= Registry.REGISTRY_PORT;
}
final Port key= new Port(port);
final ManagedRegistry r;
synchronized (this) {
r= this.registries.get(key);
}
return (r != null) ? r.registry : null;
}
/**
* Sets the port for the managed embedded private RMI registry.
*
* @param port the registry port
*/
public void setEmbeddedPrivatePort(final int port) {
setEmbeddedPrivatePortDynamic(port, port);
}
/**
* Sets the valid port range for the managed embedded private RMI registry.
* An unused port for the registry is search inside this range.
*
* @param min lowest valid registry port
* @param max highest valid the registry port
*/
public void setEmbeddedPrivatePortDynamic(final int min, final int max) {
if (min > 0 && min > max) {
throw new IllegalArgumentException("min > max");
}
synchronized (this.embeddedLock) {
this.embeddedRegistry= null;
this.embeddedPortRange= (min > 0) ? new PortRange(min, max) : EMBEDDED_PORT_RANGE_DEFAULT;
}
}
/**
* Sets the start mode for the managed embedded private RMI registry.
*
* @param separate start registry in separate process
*/
public void setEmbeddedPrivateMode(final boolean separate) {
this.setEmbeddedPrivateMode(separate, this.embeddedSsl);
}
/**
* Sets the start mode for the managed embedded private RMI registry.
*
* @param separate start registry in separate process
* @param ssl start using SSL sockets
*
* @since 1.4
*/
public void setEmbeddedPrivateMode(final boolean separate, final boolean ssl) {
if (separate && ssl) {
throw new IllegalArgumentException("ssl is only supported if separate is not enabled");
}
synchronized (this.embeddedLock) {
this.embeddedRegistry= null;
this.embeddedStartSeparate= separate;
this.embeddedSsl= ssl;
}
}
public void addEmbeddedCodebaseEntry(final URI entry) {
if (entry.getScheme() == null) {
throw new IllegalArgumentException("entry: missing scheme"); //$NON-NLS-1$
}
synchronized (this.embeddedLock) {
if (this.embeddedCodebaseEntries == null) {
this.embeddedCodebaseEntries= new ArrayList<>();
}
if (!this.embeddedCodebaseEntries.contains(entry)) {
this.embeddedRegistry= null;
this.embeddedCodebaseEntries.add(entry);
}
}
}
/**
* Returns the managed embedded private RMI registry.
*
* @return the registry or <code>null</code> if not available
*/
public @Nullable RMIRegistry getEmbeddedPrivateRegistry(final ProgressMonitor m)
throws StatusException {
ManagedRegistry r= null;
Status status= null;
synchronized (this.embeddedLock) {
if (this.embeddedRegistry != null) {
return this.embeddedRegistry.registry;
}
for (final Iterator<ManagedRegistry> iter= this.embeddedRegistries.iterator(); iter.hasNext();) {
r= iter.next();
final RMIAddress address= r.registry.getAddress();
if (this.embeddedPortRange.contains(address.getPort())
&& address.isSsl() == this.embeddedSsl
&& this.embeddedStartSeparate == (r.process != null) ) {
this.embeddedRegistry= r;
return this.embeddedRegistry.registry;
}
}
r= null;
m.beginTask("Starting embedded service registry", ProgressMonitor.UNKNOWN);
m.beginSubTask((this.embeddedStartSeparate) ? "Starting service registry (RMI) process." : "Starting service registry (RMI).");
if (this.embeddedCodebaseLoadContrib && this.embeddedStartSeparate) {
loadCodebaseContrib();
}
int loop= 1;
for (int portNum= this.embeddedPortRange.getMin(); ; ) {
m.checkCanceled();
try {
final RMIAddress rmiAddress= new RMIAddress(
InetAddress.getLoopbackAddress(), new Port(portNum), this.embeddedSsl,
RMIAddress.REGISTRY_NAME );
if (this.embeddedStartSeparate) {
status= startSeparateRegistry(rmiAddress, false,
(this.embeddedPortRange.getLength() == 1),
ManagedRegistry.EMBEDDED, StopRule.ALWAYS,
this.embeddedCodebaseEntries );
if (status.getSeverity() < Status.ERROR) {
r= this.registries.get(rmiAddress.getPort());
}
}
else {
final Registry javaRegistry;
if (rmiAddress.isSsl()) {
javaRegistry= LocateRegistry.createRegistry(portNum,
new SslRMIClientSocketFactory(),
new SslRMIServerSocketFactory(null, null, true) );
}
else {
javaRegistry= LocateRegistry.createRegistry(portNum);
}
r= new ManagedRegistry(
new RMIRegistry(rmiAddress, javaRegistry, false),
ManagedRegistry.EMBEDDED, StopRule.ALWAYS );
}
if (r != null) {
this.embeddedRegistry= r;
this.embeddedRegistries.add(r);
break;
}
}
catch (final Exception e) {
if (!(e.getCause() instanceof BindException)) {
status= new ErrorStatus(BUNDLE_ID,
"An unknown exception was thrown when starting the embedded registry.",
e );
}
portNum++;
if (portNum % 10 == 0) {
portNum+= 10;
}
if (portNum > this.embeddedPortRange.getMax()) {
if (loop == 1) {
loop= 2;
portNum= this.embeddedPortRange.getMin() + 10;
if (portNum <= this.embeddedPortRange.getMax()) {
continue;
}
}
break;
}
}
}
}
if (r != null) {
synchronized (this) {
this.registries.put(r.registry.getAddress().getPort(), r);
}
return r.registry;
}
if (status != null) {
throw new StatusException(status);
}
return null;
}
private void loadCodebaseContrib() {
try {
if (this.embeddedCodebaseEntries == null) {
this.embeddedCodebaseEntries= new ArrayList<>();
}
new ERuntimeContributor().addCodebaseEntries(this.embeddedCodebaseEntries);
}
catch (final Exception e) {}
this.embeddedCodebaseLoadContrib= false;
}
public Status startSeparateRegistry(final RMIAddress address, final boolean allowExisting,
final @Nullable StopRule stopRule, final @Nullable List<URI> codebaseEntries) {
return startSeparateRegistry(address, allowExisting, false, 0, stopRule, codebaseEntries);
}
private Status startSeparateRegistry(RMIAddress address,
final boolean allowExisting, final boolean noCheck,
final int addType, final @Nullable StopRule stopRule,
final @Nullable List<URI> codebaseEntries) {
if (allowExisting && codebaseEntries != null) {
throw new IllegalArgumentException("allow existing not valid in combination with codebase entries");
}
final InetAddress hostAddress= address.getHostAddress();
if (!(hostAddress.isLinkLocalAddress() || hostAddress.isLoopbackAddress())) {
throw new IllegalArgumentException("address: not local"); //$NON-NLS-1$
}
if (address.getName() != null) {
address= new RMIAddress(address, RMIAddress.REGISTRY_NAME);
}
if (!noCheck) {
try {
RMIUtils.checkRegistryAccess(address);
if (allowExisting) {
try {
final Registry registry= LocateRegistry.getRegistry(address.getHost(), address.getPort().get());
registry.list();
synchronized (this) {
if (!this.registries.containsKey(address.getPort())) {
final ManagedRegistry r= new ManagedRegistry(
new RMIRegistry(address, registry, false),
ManagedRegistry.FOREIGN,
StopRule.NEVER );
this.registries.put(address.getPort(), r);
}
}
return new InfoStatus(BUNDLE_ID,
MessageFormat.format(Messages.RMI_status_RegistryAlreadyStarted_message, address.getPort()) );
}
catch (final RemoteException e) {}
}
return new ErrorStatus(BUNDLE_ID,
MessageFormat.format(Messages.RMI_status_RegistryStartFailedPortAlreadyUsed_message, address.getPort()) );
}
catch (final RemoteException e) {}
}
final Process process;
try {
final List<String> command= new ArrayList<>();
final StringBuilder sb= new StringBuilder();
sb.setLength(0);
sb.append(System.getProperty("java.home")); //$NON-NLS-1$
sb.append(File.separator).append("bin"); //$NON-NLS-1$
sb.append(File.separator).append("rmiregistry"); //$NON-NLS-1$
command.add(sb.toString());
command.add(address.getPort().toString());
if (codebaseEntries != null && !codebaseEntries.isEmpty()) {
sb.setLength(0);
sb.append("-J-Djava.rmi.server.codebase="); //$NON-NLS-1$
sb.append(codebaseEntries.get(0));
for (int i= 1; i < codebaseEntries.size(); i++) {
sb.append(' ');
sb.append(codebaseEntries.get(i));
}
command.add(sb.toString());
}
if (address.getHost() != null) {
sb.setLength(0);
sb.append("-J-Djava.rmi.server.hostname="); //$NON-NLS-1$
sb.append(address.getHost());
command.add(sb.toString());
}
process= Runtime.getRuntime().exec(command.toArray(new String[command.size()]));
}
catch (final Exception e) {
return new ErrorStatus(BUNDLE_ID,
MessageFormat.format(Messages.RMI_status_RegistryStartFailed_message, address.getPort()),
e );
}
RemoteException lastException= null;
for (int i= 1; ; i++) {
try {
final int exit= process.exitValue();
return new ErrorStatus(BUNDLE_ID,
MessageFormat.format(Messages.RMI_status_RegistryStartFailedWithExitValue_message, address.getPort(), exit));
}
catch (final IllegalThreadStateException e) {
}
if (i > 1) {
try {
RMIUtils.checkRegistryAccess(address);
final Registry registry= LocateRegistry.getRegistry(address.getHost(), address.getPort().get());
registry.list();
final ManagedRegistry r= new ManagedRegistry(
new RMIRegistry(address, registry, true),
ManagedRegistry.SEPERATE | addType,
(stopRule != null) ? stopRule : StopRule.IF_EMPTY );
r.process= process;
synchronized (this) {
this.registries.put(address.getPort(), r);
}
return Statuses.OK_STATUS;
}
catch (final RemoteException e) {
lastException= e;
}
}
if (Thread.interrupted()) {
process.destroy();
return Statuses.CANCEL_STATUS;
}
if (i >= 25) {
process.destroy();
return new ErrorStatus(BUNDLE_ID,
MessageFormat.format(Messages.RMI_status_RegistryStartFailed_message, address.getPort()),
lastException );
}
try {
Thread.sleep(50);
continue;
}
catch (final InterruptedException e) {
Thread.currentThread().interrupt();
}
}
}
public Status stopSeparateRegistry(final RMIAddress address) {
return stopSeparateRegistry(address.getPort().get());
}
public Status stopSeparateRegistry(int port) {
if (port <= 0) {
port= Registry.REGISTRY_PORT;
}
final Port key= new Port(port);
final ManagedRegistry r;
synchronized (this) {
r= this.registries.get(key);
if (r == null || r.process == null) {
return new ErrorStatus(BUNDLE_ID,
MessageFormat.format(Messages.RMI_status_RegistryStopFailedNotFound_message, port) );
}
this.registries.remove(key);
}
r.process.destroy();
return Statuses.OK_STATUS;
}
protected void dispose() {
synchronized(this) {
for (final ManagedRegistry r : this.registries.values()) {
if (r.process == null) {
continue;
}
switch (r.stopRule) {
case ALWAYS:
break;
case NEVER:
continue;
case IF_EMPTY:
try {
final Registry registry= r.registry.getRegistry();
if (registry.list().length > 0) {
continue;
}
}
catch (final RemoteException e) {}
break;
}
r.process.destroy();
r.process= null;
}
this.registries.clear();
}
}
}