/* * Copyright 2013-2016 Cel Skeggs * * This file is part of the CCRE, the Common Chicken Runtime Engine. * * The CCRE is free software: you can redistribute it and/or modify it under the * terms of the GNU Lesser General Public License as published by the Free * Software Foundation, either version 3 of the License, or (at your option) any * later version. * * The CCRE is distributed in the hope that it will be useful, but WITHOUT ANY * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR * A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more * details. * * You should have received a copy of the GNU Lesser General Public License * along with the CCRE. If not, see <http://www.gnu.org/licenses/>. */ package ccre.cluck; import java.io.IOException; import java.io.NotSerializableException; import java.io.ObjectOutputStream; import java.io.Serializable; import java.util.HashMap; import ccre.channel.EventOutput; import ccre.cluck.rpc.RPCManager; import ccre.log.Logger; import ccre.verifier.FlowPhase; import ccre.verifier.SetupPhase; /** * A CluckNode is the core hub of the Cluck networking system on a device. It * handles message routing, publishing, and subscribing. * * Usually the main instance of this is kept in CluckGlobals. * * @author skeggsc */ public class CluckNode implements Serializable { private static final long serialVersionUID = -5439319159206467512L; /** * A map of the current link names to the CluckLinks. */ public final HashMap<String, CluckLink> links = new HashMap<String, CluckLink>(); /** * The time when the last error message was printed about a link not * existing. */ private long lastMissingLinkError = 0; /** * The link name of the last error message about a link not existing. */ private String lastMissingLink = null; /** * The official RPCManager for this node. */ private RPCManager rpcManager = null; /** * Notify everyone on the network that the network structure has been * modified - for example, when a connection is opened or closed. * * A notification message is simply a message with an * {@link CluckConstants#RMT_NOTIFY} header. */ public void notifyNetworkModified() { transmit(CluckConstants.BROADCAST_DESTINATION, "#modsrc", new byte[] { CluckConstants.RMT_NOTIFY }); } /** * Transmit a message to the specified other link (relative to this node), * with the specified return address (relative to this node). * * Paths are /-separated, with each element being a link to follow. * * @param target The target path. * @param source The source path. * @param data The message data to transmit. */ @FlowPhase public void transmit(String target, String source, byte[] data) { transmit(target, source, data, null); } /** * Transmit a message to the specified other link (relative to this node), * with the specified return address (relative to this node). If this is a * broadcast, then don't include the specified link (to prevent infinite * loops). * * Paths are /-separated, with each element being a link to follow. * * @param target The target path. * @param source The source path. * @param data The message data to transmit. * @param denyLink The link for broadcasts to not follow. */ @FlowPhase public void transmit(String target, String source, byte[] data, CluckLink denyLink) { if (data == null) { throw new NullPointerException(); } // TODO: outlaw empty messages here. if (target == null) { if (data.length == 0 || data[0] != CluckConstants.RMT_NEGATIVE_ACK) { Logger.warning("[LOCAL] Received message addressed to unreceving node (source: " + source + ")"); } } else if (CluckConstants.BROADCAST_DESTINATION.equals(target)) { broadcast(source, data, denyLink); } else { int slash = target.indexOf('/'); String direct, indirect; if (slash == -1) { direct = target; indirect = null; } else { direct = target.substring(0, slash); indirect = target.substring(slash + 1); } CluckLink link = links.get(direct); if (link == null) { reportMissingLink(data, source, target, direct); } else { try { boolean shouldLive = link.send(indirect, source, data); if (!shouldLive) { links.remove(direct); } } catch (Throwable ex) { Logger.severe("[LOCAL] Error while dispatching to Cluck link " + target, ex); } } } } /** * Broadcast a message to all receiving nodes. * * This is the same as * <code>transmit(CluckConstants.BROADCAST_DESTINATION, source, data, denyLink)</code> * . * * @param source The source of the message. * @param data The contents of the message. * @param denyLink The link to not send broadcasts to, or null. * @see #transmit(java.lang.String, java.lang.String, byte[], * ccre.cluck.CluckLink) */ @FlowPhase public void broadcast(String source, byte[] data, CluckLink denyLink) { if (data == null) { throw new NullPointerException(); } for (String link : links.keySet().toArray(new String[links.keySet().size()])) { CluckLink cl = links.get(link); if (cl != null && cl != denyLink) { try { boolean shouldLive = cl.send(CluckConstants.BROADCAST_DESTINATION, source, data); if (!shouldLive) { links.remove(link); } } catch (Throwable ex) { Logger.severe("[LOCAL] Error while broadcasting to Cluck link " + link, ex); } } } } @FlowPhase private void reportMissingLink(byte[] data, String source, String target, String direct) { // Warnings about lost RMT_NEGATIVE_ACK messages or research messages // are annoying, so don't send these, and don't warn about the same // message path too quickly. // We use System.currentTimeMillis() instead of Time.currentTimeMillis() // because this is only to prevent message spam. if ((data.length == 0 || data[0] != CluckConstants.RMT_NEGATIVE_ACK) && !target.contains("/rsch-")) { if (!direct.equals(lastMissingLink) || System.currentTimeMillis() >= lastMissingLinkError + 1000) { lastMissingLink = direct; lastMissingLinkError = System.currentTimeMillis(); Logger.warning("[LOCAL] No link for " + target + "(" + direct + ") from " + source + "!"); } transmit(source, target, new byte[] { CluckConstants.RMT_NEGATIVE_ACK }); } } /** * Subscribe to any network structure modification notification messages, * which are sent each time that the structure of the Cluck network changes. * * @param localRecvName The name to bind to. * @param listener The listener to notify. */ public void subscribeToStructureNotifications(String localRecvName, final EventOutput listener) { if (localRecvName == null || listener == null) { throw new NullPointerException(); } new CluckSubscriber(this) { @Override protected void receive(String source, byte[] data) { // Ignore it. } @Override protected void receiveBroadcast(String source, byte[] data) { if (data.length == 1 && data[0] == CluckConstants.RMT_NOTIFY) { listener.event(); } } }.attach(localRecvName); } /** * Get the name of the specified link. * * @param link The link to get the name for. * @throws IllegalArgumentException if the link isn't directly attached. * @return The link name. */ @FlowPhase public String getLinkName(CluckLink link) throws IllegalArgumentException { if (link == null) { throw new NullPointerException(); } for (String key : links.keySet()) { if (links.get(key) == link) { return key; } } throw new IllegalArgumentException("No such link!"); } /** * Add the specified link at the specified link name. * * @param link The link. * @param linkName The link name. * @throws IllegalStateException if the specified link name is already used. */ @SetupPhase public void addLink(CluckLink link, String linkName) throws IllegalStateException { if (link == null || linkName == null) { throw new NullPointerException(); } if (linkName.contains("/")) { throw new IllegalArgumentException("Link name cannot contain slashes: " + linkName); } if (links.get(linkName) != null) { throw new IllegalStateException("Link name already used: " + linkName + " for " + links.get(linkName) + " not " + link); } links.put(linkName, link); } /** * Checks if a link exists. If it is routable, it exists. If it is not * routable, it probably (but not necessarily) doesn't exist - for example, * there is the case of a link pointing to a remote object. * * @param linkName the link name to check. * @return true if the link exists, and false otherwise. */ public boolean hasLink(String linkName) { if (linkName == null) { throw new NullPointerException(); } return links.containsKey(linkName); } /** * Removes the link attached to the specified link name. * * @param linkName The link name to remove. * @return whether or not there had been a link to remove. */ @SetupPhase public boolean removeLink(String linkName) { if (linkName == null) { throw new NullPointerException(); } return links.remove(linkName) != null; } /** * Adds the specified link at the specified link name, replacing the current * link if necessary. * * @param link The link. * @param linkName The link name. */ @SetupPhase public void addOrReplaceLink(CluckLink link, String linkName) { if (link == null || linkName == null) { throw new NullPointerException(); } if (linkName.contains("/")) { throw new IllegalArgumentException("Link name cannot contain slashes: " + linkName); } if (links.get(linkName) != null) { Logger.fine("Replaced current link on: " + linkName); } links.put(linkName, link); } /** * Get the official RPCManager for this node. * * @return The RPCManager for this node. * @see ccre.cluck.rpc.RPCManager */ @SetupPhase public synchronized RPCManager getRPCManager() { if (rpcManager == null) { rpcManager = new RPCManager(this); } return rpcManager; } private void writeObject(ObjectOutputStream out) throws IOException { throw new NotSerializableException("Not serializable!"); } private Object writeReplace() { return this == Cluck.getNode() ? new SerializedGlobalCluckNode() : this; } private static class SerializedGlobalCluckNode implements Serializable { private static final long serialVersionUID = 6554282414281830927L; private Object readResolve() { return Cluck.getNode(); } } }