|
@@ -0,0 +1,432 @@
|
|
|
|
+/*
|
|
|
|
+ * Copyright 2016-2021 Pnoker. All Rights Reserved.
|
|
|
|
+ * Licensed under the Apache License, Version 2.0 (the "License");
|
|
|
|
+ * you may not use this file except in compliance with the License.
|
|
|
|
+ * You may obtain a copy of the License at
|
|
|
|
+ * http://www.apache.org/licenses/LICENSE-2.0
|
|
|
|
+ * Unless required by applicable law or agreed to in writing, software
|
|
|
|
+ * distributed under the License is distributed on an "AS IS" BASIS,
|
|
|
|
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
|
|
+ * See the License for the specific language governing permissions and
|
|
|
|
+ * limitations under the License.
|
|
|
|
+ */
|
|
|
|
+
|
|
|
|
+package org.openscada.opc.lib.da;
|
|
|
|
+
|
|
|
|
+import lombok.extern.slf4j.Slf4j;
|
|
|
|
+import org.jinterop.dcom.common.JIException;
|
|
|
|
+import org.jinterop.dcom.core.JIClsid;
|
|
|
|
+import org.jinterop.dcom.core.JIComServer;
|
|
|
|
+import org.jinterop.dcom.core.JIProgId;
|
|
|
|
+import org.jinterop.dcom.core.JISession;
|
|
|
|
+import org.openscada.opc.dcom.da.OPCNAMESPACETYPE;
|
|
|
|
+import org.openscada.opc.dcom.da.OPCSERVERSTATUS;
|
|
|
|
+import org.openscada.opc.dcom.da.impl.OPCBrowseServerAddressSpace;
|
|
|
|
+import org.openscada.opc.dcom.da.impl.OPCGroupStateMgt;
|
|
|
|
+import org.openscada.opc.dcom.da.impl.OPCServer;
|
|
|
|
+import org.openscada.opc.lib.common.AlreadyConnectedException;
|
|
|
|
+import org.openscada.opc.lib.common.ConnectionInformation;
|
|
|
|
+import org.openscada.opc.lib.common.NotConnectedException;
|
|
|
|
+import org.openscada.opc.lib.da.browser.FlatBrowser;
|
|
|
|
+import org.openscada.opc.lib.da.browser.TreeBrowser;
|
|
|
|
+import org.openscada.opc.lib.list.ServerList;
|
|
|
|
+
|
|
|
|
+import java.net.UnknownHostException;
|
|
|
|
+import java.util.ArrayList;
|
|
|
|
+import java.util.HashMap;
|
|
|
|
+import java.util.List;
|
|
|
|
+import java.util.Map;
|
|
|
|
+import java.util.concurrent.CopyOnWriteArrayList;
|
|
|
|
+import java.util.concurrent.ScheduledExecutorService;
|
|
|
|
+
|
|
|
|
+@Slf4j
|
|
|
|
+public class Server {
|
|
|
|
+
|
|
|
|
+ private final ConnectionInformation connectionInformation;
|
|
|
|
+
|
|
|
|
+ private JISession session;
|
|
|
|
+
|
|
|
|
+ private JIComServer comServer;
|
|
|
|
+
|
|
|
|
+ private OPCServer server;
|
|
|
|
+
|
|
|
|
+ private boolean defaultActive = true;
|
|
|
|
+
|
|
|
|
+ private int defaultUpdateRate = 1000;
|
|
|
|
+
|
|
|
|
+ private Integer defaultTimeBias;
|
|
|
|
+
|
|
|
|
+ private Float defaultPercentDeadband;
|
|
|
|
+
|
|
|
|
+ private int defaultLocaleID = 0;
|
|
|
|
+
|
|
|
|
+ private ErrorMessageResolver errorMessageResolver;
|
|
|
|
+
|
|
|
|
+ private final Map<Integer, Group> groups = new HashMap<>(16);
|
|
|
|
+
|
|
|
|
+ private final List<ServerConnectionStateListener> stateListeners = new CopyOnWriteArrayList<>();
|
|
|
|
+
|
|
|
|
+ private final ScheduledExecutorService scheduler;
|
|
|
|
+
|
|
|
|
+ public Server(final ConnectionInformation connectionInformation,
|
|
|
|
+ final ScheduledExecutorService scheduler) {
|
|
|
|
+ super();
|
|
|
|
+ this.connectionInformation = connectionInformation;
|
|
|
|
+ this.scheduler = scheduler;
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ /**
|
|
|
|
+ * Gets the scheduler for the server. Note that this scheduler might get
|
|
|
|
+ * blocked for a short time if the connection breaks. It should not be used
|
|
|
|
+ * for time critical operations.
|
|
|
|
+ *
|
|
|
|
+ * @return the scheduler for the server
|
|
|
|
+ */
|
|
|
|
+ public ScheduledExecutorService getScheduler() {
|
|
|
|
+ return this.scheduler;
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ protected synchronized boolean isConnected() {
|
|
|
|
+ return this.session != null;
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ public synchronized void connect() throws IllegalArgumentException, UnknownHostException, JIException, AlreadyConnectedException {
|
|
|
|
+ if (isConnected()) {
|
|
|
|
+ throw new AlreadyConnectedException();
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ final int socketTimeout = Integer.getInteger("rpc.socketTimeout", 0);
|
|
|
|
+ log.debug(String.format("Socket timeout: %s ", socketTimeout));
|
|
|
|
+
|
|
|
|
+ try {
|
|
|
|
+ if (this.connectionInformation.getClsid() != null) {
|
|
|
|
+ this.session = JISession.createSession(
|
|
|
|
+ this.connectionInformation.getDomain(),
|
|
|
|
+ this.connectionInformation.getUser(),
|
|
|
|
+ this.connectionInformation.getPassword());
|
|
|
|
+ this.session.setGlobalSocketTimeout(socketTimeout);
|
|
|
|
+ this.session.useSessionSecurity(true);
|
|
|
|
+ this.comServer = new JIComServer(
|
|
|
|
+ JIClsid.valueOf(this.connectionInformation.getClsid()),
|
|
|
|
+ this.connectionInformation.getHost(), this.session);
|
|
|
|
+ } else if (this.connectionInformation.getProgId() != null) {
|
|
|
|
+ this.session = JISession.createSession(
|
|
|
|
+ this.connectionInformation.getDomain(),
|
|
|
|
+ this.connectionInformation.getUser(),
|
|
|
|
+ this.connectionInformation.getPassword());
|
|
|
|
+ this.session.setGlobalSocketTimeout(socketTimeout);
|
|
|
|
+
|
|
|
|
+ ServerList serverList = new ServerList(this.connectionInformation.getHost(),
|
|
|
|
+ this.connectionInformation.getUser(),
|
|
|
|
+ this.connectionInformation.getPassword(),
|
|
|
|
+ this.connectionInformation.getDomain());
|
|
|
|
+ String clsIdFromProgId = serverList.getClsIdFromProgId(this.connectionInformation.getProgId());
|
|
|
|
+
|
|
|
|
+// this.comServer = new JIComServer(JIProgId.valueOf(this.connectionInformation.getProgId()),this.connectionInformation.getHost(), this.session);
|
|
|
|
+
|
|
|
|
+ this.comServer = new JIComServer(
|
|
|
|
+ JIClsid.valueOf(clsIdFromProgId),
|
|
|
|
+ this.connectionInformation.getHost(), this.session);
|
|
|
|
+ } else {
|
|
|
|
+ throw new IllegalArgumentException("Neither clsid nor progid is valid!");
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ this.server = new OPCServer(this.comServer.createInstance());
|
|
|
|
+ this.errorMessageResolver = new ErrorMessageResolver(
|
|
|
|
+ this.server.getCommon(), this.defaultLocaleID);
|
|
|
|
+ } catch (final UnknownHostException e) {
|
|
|
|
+ log.error("Unknown host when connecting to server", e);
|
|
|
|
+ cleanup();
|
|
|
|
+ throw e;
|
|
|
|
+ } catch (final JIException e) {
|
|
|
|
+ log.error("Failed to connect to server", e);
|
|
|
|
+ cleanup();
|
|
|
|
+ throw e;
|
|
|
|
+ } catch (final Throwable e) {
|
|
|
|
+ e.printStackTrace();
|
|
|
|
+ log.error("Unknown error", e);
|
|
|
|
+ cleanup();
|
|
|
|
+ throw new RuntimeException(e);
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ notifyConnectionStateChange(true);
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ /**
|
|
|
|
+ * cleanup after the connection is closed
|
|
|
|
+ */
|
|
|
|
+ protected void cleanup() {
|
|
|
|
+ log.debug("Destroying DCOM session...");
|
|
|
|
+ final JISession destructSession = this.session;
|
|
|
|
+ final Thread destructor = new Thread(new Runnable() {
|
|
|
|
+
|
|
|
|
+ public void run() {
|
|
|
|
+ final long ts = System.currentTimeMillis();
|
|
|
|
+ try {
|
|
|
|
+ log.debug("Starting destruction of DCOM session");
|
|
|
|
+ JISession.destroySession(destructSession);
|
|
|
|
+ log.debug("Destructed DCOM session");
|
|
|
|
+ } catch (final Throwable e) {
|
|
|
|
+ log.error("Failed to destruct DCOM session", e);
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+ }, "UtgardSessionDestructor");
|
|
|
|
+ destructor.setName("OPCSessionDestructor");
|
|
|
|
+ destructor.setDaemon(true);
|
|
|
|
+ destructor.start();
|
|
|
|
+ log.debug("Destroying DCOM session... forked");
|
|
|
|
+
|
|
|
|
+ this.errorMessageResolver = null;
|
|
|
|
+ this.session = null;
|
|
|
|
+ this.comServer = null;
|
|
|
|
+ this.server = null;
|
|
|
|
+
|
|
|
|
+ this.groups.clear();
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ /**
|
|
|
|
+ * Disconnect the connection if it is connected
|
|
|
|
+ */
|
|
|
|
+ public synchronized void disconnect() {
|
|
|
|
+ if (!isConnected()) {
|
|
|
|
+ return;
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ try {
|
|
|
|
+ notifyConnectionStateChange(false);
|
|
|
|
+ } catch (final Throwable t) {
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ cleanup();
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ /**
|
|
|
|
+ * Dispose the connection in the case of an error
|
|
|
|
+ */
|
|
|
|
+ public void dispose() {
|
|
|
|
+ disconnect();
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ protected synchronized Group getGroup(final OPCGroupStateMgt groupMgt) throws JIException, IllegalArgumentException, UnknownHostException {
|
|
|
|
+ final Integer serverHandle = groupMgt.getState().getServerHandle();
|
|
|
|
+ if (this.groups.containsKey(serverHandle)) {
|
|
|
|
+ return this.groups.get(serverHandle);
|
|
|
|
+ } else {
|
|
|
|
+ final Group group = new Group(this, serverHandle, groupMgt);
|
|
|
|
+ this.groups.put(serverHandle, group);
|
|
|
|
+ return group;
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ /**
|
|
|
|
+ * Add a new named group to the server
|
|
|
|
+ *
|
|
|
|
+ * @param name The name of the group to use. Must be unique or
|
|
|
|
+ * <code>null</code> so that the server creates a unique name.
|
|
|
|
+ * @return The new group
|
|
|
|
+ * @throws NotConnectedException If the server is not connected using {@link Server#connect()}
|
|
|
|
+ * @throws IllegalArgumentException
|
|
|
|
+ * @throws UnknownHostException
|
|
|
|
+ * @throws JIException
|
|
|
|
+ * @throws DuplicateGroupException If a group with this name already exists
|
|
|
|
+ */
|
|
|
|
+ public synchronized Group addGroup(final String name) throws NotConnectedException, IllegalArgumentException, UnknownHostException, JIException, DuplicateGroupException {
|
|
|
|
+ if (!isConnected()) {
|
|
|
|
+ throw new NotConnectedException();
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ try {
|
|
|
|
+ final OPCGroupStateMgt groupMgt = this.server.addGroup(name,
|
|
|
|
+ this.defaultActive, this.defaultUpdateRate, 0,
|
|
|
|
+ this.defaultTimeBias, this.defaultPercentDeadband,
|
|
|
|
+ this.defaultLocaleID);
|
|
|
|
+ return getGroup(groupMgt);
|
|
|
|
+ } catch (final JIException e) {
|
|
|
|
+ if (e.getErrorCode() == 0xC004000C) {
|
|
|
|
+ throw new DuplicateGroupException();
|
|
|
|
+ }
|
|
|
|
+ throw e;
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ /**
|
|
|
|
+ * Add a new group and let the server generate a group name
|
|
|
|
+ * <p>
|
|
|
|
+ * Actually this method only calls {@link Server#addGroup(String)} with
|
|
|
|
+ * <code>null</code> as parameter.
|
|
|
|
+ *
|
|
|
|
+ * @return the new group
|
|
|
|
+ * @throws IllegalArgumentException
|
|
|
|
+ * @throws UnknownHostException
|
|
|
|
+ * @throws NotConnectedException
|
|
|
|
+ * @throws JIException
|
|
|
|
+ * @throws DuplicateGroupException
|
|
|
|
+ */
|
|
|
|
+ public Group addGroup() throws IllegalArgumentException,
|
|
|
|
+ UnknownHostException, NotConnectedException, JIException,
|
|
|
|
+ DuplicateGroupException {
|
|
|
|
+ return addGroup(null);
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ /**
|
|
|
|
+ * Find a group by its name
|
|
|
|
+ *
|
|
|
|
+ * @param name The name to look for
|
|
|
|
+ * @return The group
|
|
|
|
+ * @throws IllegalArgumentException
|
|
|
|
+ * @throws UnknownHostException
|
|
|
|
+ * @throws JIException
|
|
|
|
+ * @throws UnknownGroupException If the group was not found
|
|
|
|
+ * @throws NotConnectedException If the server is not connected
|
|
|
|
+ */
|
|
|
|
+ public Group findGroup(final String name) throws IllegalArgumentException, UnknownHostException, JIException, UnknownGroupException, NotConnectedException {
|
|
|
|
+ if (!isConnected()) {
|
|
|
|
+ throw new NotConnectedException();
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ try {
|
|
|
|
+ final OPCGroupStateMgt groupMgt = this.server.getGroupByName(name);
|
|
|
|
+ return getGroup(groupMgt);
|
|
|
|
+ } catch (final JIException e) {
|
|
|
|
+ switch (e.getErrorCode()) {
|
|
|
|
+ case 0x80070057:
|
|
|
|
+ throw new UnknownGroupException(name);
|
|
|
|
+ default:
|
|
|
|
+ throw e;
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ public int getDefaultLocaleID() {
|
|
|
|
+ return this.defaultLocaleID;
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ public void setDefaultLocaleID(final int defaultLocaleID) {
|
|
|
|
+ this.defaultLocaleID = defaultLocaleID;
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ public Float getDefaultPercentDeadband() {
|
|
|
|
+ return this.defaultPercentDeadband;
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ public void setDefaultPercentDeadband(final Float defaultPercentDeadband) {
|
|
|
|
+ this.defaultPercentDeadband = defaultPercentDeadband;
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ public Integer getDefaultTimeBias() {
|
|
|
|
+ return this.defaultTimeBias;
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ public void setDefaultTimeBias(final Integer defaultTimeBias) {
|
|
|
|
+ this.defaultTimeBias = defaultTimeBias;
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ public int getDefaultUpdateRate() {
|
|
|
|
+ return this.defaultUpdateRate;
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ public void setDefaultUpdateRate(final int defaultUpdateRate) {
|
|
|
|
+ this.defaultUpdateRate = defaultUpdateRate;
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ public boolean isDefaultActive() {
|
|
|
|
+ return this.defaultActive;
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ public void setDefaultActive(final boolean defaultActive) {
|
|
|
|
+ this.defaultActive = defaultActive;
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ /**
|
|
|
|
+ * Get the flat browser
|
|
|
|
+ *
|
|
|
|
+ * @return The flat browser or <code>null</code> if the functionality is not
|
|
|
|
+ * supported
|
|
|
|
+ */
|
|
|
|
+ public FlatBrowser getFlatBrowser() {
|
|
|
|
+ final OPCBrowseServerAddressSpace browser = this.server.getBrowser();
|
|
|
|
+ if (browser == null) {
|
|
|
|
+ return null;
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ return new FlatBrowser(browser);
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ /**
|
|
|
|
+ * Get the tree browser
|
|
|
|
+ *
|
|
|
|
+ * @return The tree browser or <code>null</code> if the functionality is not
|
|
|
|
+ * supported
|
|
|
|
+ * @throws JIException
|
|
|
|
+ */
|
|
|
|
+ public TreeBrowser getTreeBrowser() throws JIException {
|
|
|
|
+ final OPCBrowseServerAddressSpace browser = this.server.getBrowser();
|
|
|
|
+ if (browser == null) {
|
|
|
|
+ return null;
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ if (browser.queryOrganization() != OPCNAMESPACETYPE.OPC_NS_HIERARCHIAL) {
|
|
|
|
+ return null;
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ return new TreeBrowser(browser);
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ public synchronized String getErrorMessage(final int errorCode) {
|
|
|
|
+ if (this.errorMessageResolver == null) {
|
|
|
|
+ return String.format("Unknown error (%08X)", errorCode);
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ // resolve message
|
|
|
|
+ final String message = this.errorMessageResolver.getMessage(errorCode);
|
|
|
|
+
|
|
|
|
+ // and return if successfull
|
|
|
|
+ if (message != null) {
|
|
|
|
+ return message;
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ // return default message
|
|
|
|
+ return String.format("Unknown error (%08X)", errorCode);
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ public synchronized void addStateListener(
|
|
|
|
+ final ServerConnectionStateListener listener) {
|
|
|
|
+ this.stateListeners.add(listener);
|
|
|
|
+ listener.connectionStateChanged(isConnected());
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ public synchronized void removeStateListener(
|
|
|
|
+ final ServerConnectionStateListener listener) {
|
|
|
|
+ this.stateListeners.remove(listener);
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ protected void notifyConnectionStateChange(final boolean connected) {
|
|
|
|
+ final List<ServerConnectionStateListener> list = new ArrayList<ServerConnectionStateListener>(
|
|
|
|
+ this.stateListeners);
|
|
|
|
+ for (final ServerConnectionStateListener listener : list) {
|
|
|
|
+ listener.connectionStateChanged(connected);
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ public OPCSERVERSTATUS getServerState(final int timeout) throws Throwable {
|
|
|
|
+ return new ServerStateOperation(this.server).getServerState(timeout);
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ public OPCSERVERSTATUS getServerState() {
|
|
|
|
+ try {
|
|
|
|
+ return getServerState(2500);
|
|
|
|
+ } catch (final Throwable e) {
|
|
|
|
+ log.error("Server connection failed", e);
|
|
|
|
+ dispose();
|
|
|
|
+ return null;
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ public void removeGroup(final Group group, final boolean force)
|
|
|
|
+ throws JIException {
|
|
|
|
+ if (this.groups.containsKey(group.getServerHandle())) {
|
|
|
|
+ this.server.removeGroup(group.getServerHandle(), force);
|
|
|
|
+ this.groups.remove(group.getServerHandle());
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+}
|