/* * Copyright 2004 - 2008 Christian Sprajc. All rights reserved. * * This file is part of PowerFolder. * * PowerFolder is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation. * * PowerFolder 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 General Public License for more details. * * You should have received a copy of the GNU General Public License * along with PowerFolder. If not, see <http://www.gnu.org/licenses/>. * * $Id$ */ package de.dal33t.powerfolder.net; import java.io.Externalizable; import java.io.File; import java.io.IOException; import java.net.Inet4Address; import java.net.InetAddress; import java.net.InterfaceAddress; import java.net.MalformedURLException; import java.net.URL; import java.text.ParseException; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.HashSet; import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.Set; import java.util.TimerTask; import java.util.concurrent.CopyOnWriteArrayList; import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.Future; import java.util.logging.Level; import java.util.logging.Logger; import de.dal33t.powerfolder.ConfigurationEntry; import de.dal33t.powerfolder.ConnectResult; import de.dal33t.powerfolder.Constants; import de.dal33t.powerfolder.Controller; import de.dal33t.powerfolder.Feature; import de.dal33t.powerfolder.Member; import de.dal33t.powerfolder.PFComponent; import de.dal33t.powerfolder.clientserver.ServerClient; import de.dal33t.powerfolder.event.ListenerSupportFactory; import de.dal33t.powerfolder.event.NodeManagerEvent; import de.dal33t.powerfolder.event.NodeManagerListener; import de.dal33t.powerfolder.light.MemberInfo; import de.dal33t.powerfolder.message.AddFriendNotification; import de.dal33t.powerfolder.message.Identity; import de.dal33t.powerfolder.message.KnownNodes; import de.dal33t.powerfolder.message.KnownNodesExt; import de.dal33t.powerfolder.message.Message; import de.dal33t.powerfolder.message.MessageListener; import de.dal33t.powerfolder.message.MessageProducer; import de.dal33t.powerfolder.message.Problem; import de.dal33t.powerfolder.message.RequestNodeList; import de.dal33t.powerfolder.message.SearchNodeRequest; import de.dal33t.powerfolder.message.SingleMessageProducer; import de.dal33t.powerfolder.message.TransferStatus; import de.dal33t.powerfolder.task.RemoveComputerFromAccountTask; import de.dal33t.powerfolder.task.SendMessageTask; import de.dal33t.powerfolder.util.Convert; import de.dal33t.powerfolder.util.Debug; import de.dal33t.powerfolder.util.Filter; import de.dal33t.powerfolder.util.IdGenerator; import de.dal33t.powerfolder.util.MessageListenerSupport; import de.dal33t.powerfolder.util.Reject; import de.dal33t.powerfolder.util.StringUtils; import de.dal33t.powerfolder.util.Util; import de.dal33t.powerfolder.util.intern.MemberInfoInternalizer; import de.dal33t.powerfolder.util.net.AddressRange; import de.dal33t.powerfolder.util.net.NetworkUtil; /** * Managing class which takes care about all old and new nodes. reconnects those * who disconnected and connectes to new ones * * @author <a href="mailto:totmacher@powerfolder.com">Christian Sprajc </a> * @version $Revision: 1.123 $ */ public class NodeManager extends PFComponent { public static final String SERVER_NODES_URI = "/client_deployment/server.nodes"; private static final Logger log = Logger.getLogger(NodeManager.class .getName()); /** The list of active acceptors for incoming connections */ List<AbstractAcceptor> acceptors; // Lock which is hold while a acception is pending private Object acceptLock = new Object(); private Map<String, Member> knownNodes; private Map<String, Member> friends; private Map<String, Member> connectedNodes; private List<AddressRange> lanRanges; private Member mySelf; /** * Set containing all nodes, that went online in the meanwhile (since last * broadcast) */ private Set<MemberInfo> nodesWentOnline; // Message listener which are fired for every member private MessageListenerSupport valveMessageListenerSupport; private boolean started; private boolean nodefileLoaded; private NodeManagerListener listenerSupport; /** Filter for the internal node database */ private List<NodeFilter> nodeFilters; public NodeManager(Controller controller) { super(controller); started = false; nodefileLoaded = false; // initzialize myself if available in config String nick = ConfigurationEntry.NICK.getValue(getController()); if (controller.getCommandLine() != null && controller.getCommandLine().hasOption("n")) { // Take nick from command line nick = controller.getCommandLine().getOptionValue("n"); } // check for manual id String id = ConfigurationEntry.NODE_ID.getValue(getController()); if (id == null) { id = IdGenerator.makeId(); // store ID logInfo("Generated new ID for '" + nick + "': " + id); ConfigurationEntry.NODE_ID.setValue(getController(), id); } String networkId = ConfigurationEntry.NETWORK_ID .getValue(getController()); if (StringUtils.isBlank(networkId)) { networkId = ConfigurationEntry.NETWORK_ID.getDefaultValue(); } mySelf = new Member(getController(), new MemberInfo(nick, id, networkId)); logInfo("I am '" + mySelf.getNick() + "'"); // Use concurrent hashmap knownNodes = Util.createConcurrentHashMap(); friends = Util.createConcurrentHashMap(); connectedNodes = Util.createConcurrentHashMap(); // The nodes, that went online in the meantime nodesWentOnline = Collections .synchronizedSet(new HashSet<MemberInfo>()); // Acceptors acceptors = new CopyOnWriteArrayList<AbstractAcceptor>(); // Value message/event listner support valveMessageListenerSupport = new MessageListenerSupport(this); this.listenerSupport = ListenerSupportFactory .createListenerSupport(NodeManagerListener.class); nodeFilters = new ArrayList<NodeFilter>(); // Default behaviour: // 1) Add all nodes when acting as supernode // 2) Add if remote side is supernode or connected. nodeFilters.add(new DefaultNodeFilter()); lanRanges = new LinkedList<AddressRange>(); String lrs[] = ConfigurationEntry.LANLIST.getValue(controller).split( ","); for (String ipr : lrs) { ipr = ipr.trim(); if (ipr.length() > 0) { try { lanRanges.add(AddressRange.parseRange(ipr)); } catch (ParseException e) { logWarning("Invalid IP range format: " + ipr); } } } } public void init() { // load local nodes loadNodes(); // Okay nodefile is loaded nodefileLoaded = true; // #1976 if (MemberInfo.INTERNALIZER != null) { logFine("Overwriting old MemberInfo internalizer: " + MemberInfo.INTERNALIZER); } MemberInfo.INTERNALIZER = new MemberInfoInternalizer(this); } /** * Starts the node manager thread */ public void start() { if (!ConfigurationEntry.NODEMANAGER_ENABLED .getValueBoolean(getController())) { logWarning("Not starting NodeManager. disabled by config"); return; } setupPeridicalTasks(); started = true; listenerSupport.startStop(new NodeManagerEvent(this, null)); logFine("Started"); } /** * Shuts the nodemanager down */ public void shutdown() { // Remove listeners, not bothering them by boring shutdown events started = false; // Store the latest supernodes // Note: This call is here to save the nodes before shutting them down. storeOnlineSupernodes(); logFine("Shutting down " + acceptors.size() + " incoming connections (Acceptors)"); for (AbstractAcceptor acceptor : acceptors) { acceptor.shutdown(); } acceptors.clear(); logFine("Shutting down nodes"); Collection<Member> conNode = new ArrayList<Member>( connectedNodes.values()); logFine("Shutting down connected nodes (" + conNode.size() + ")"); ExecutorService shutdownThreadPool = Executors.newFixedThreadPool(Math .max(1, conNode.size() / 5)); Collection<Future<?>> shutdowns = new ArrayList<Future<?>>(); for (final Member node : conNode) { Runnable killer = new Runnable() { public void run() { node.shutdown(); } }; shutdowns.add(shutdownThreadPool.submit(killer)); } for (Future<?> future : shutdowns) { try { future.get(); } catch (InterruptedException e) { logFiner("InterruptedException", e); break; } catch (ExecutionException e) { } } shutdownThreadPool.shutdownNow(); // "Traditional" shutdown logFine("Shutting down " + knownNodes.size() + " nodes"); for (Member node : getNodesAsCollection()) { node.shutdown(); } // first save current members connection state if (nodefileLoaded) { // Only store if was fully started storeNodes(); // Shutdown, unloaded nodefile nodefileLoaded = false; } listenerSupport.startStop(new NodeManagerEvent(this, null)); logFine("Stopped"); } /** * for debug * * @param suspended */ public void setSuspendFireEvents(boolean suspended) { ListenerSupportFactory.setSuspended(listenerSupport, suspended); logFine("setSuspendFireEvents: " + suspended); } /** * @return true if the nodemanager is started */ public boolean isStarted() { return started; } /** * @param node * the node to ask the friend status for * @return true if that node is on the friendlist. */ public boolean isFriend(Member node) { if (node.isMySelf()) { return true; } return friends.containsKey(node.getId()); } /** * @return the number of nodes, which are online on the network. */ public int countOnlineNodes() { int nConnected = 1; synchronized (knownNodes) { for (Member node : knownNodes.values()) { if (node.isConnected() || node.isConnectedToNetwork()) { nConnected++; } } } return nConnected; } /** * @return the number of supernodes, which are online on the network. */ public int countOnlineSupernodes() { int nConnected = 1; for (Member node : knownNodes.values()) { if (node.isSupernode() && (node.isConnected() || node.isConnectedToNetwork())) { nConnected++; } } return nConnected; } /** * @return the number of known supernodes. */ public int countSupernodes() { int nSupernodes = 0; for (Member node : knownNodes.values()) { if (node.isSupernode()) { nSupernodes++; } } return nSupernodes; } /** * @return the number of connected supernodes. */ public int countConnectedSupernodes() { int nSupernodes = 0; for (Member node : connectedNodes.values()) { if (node.isSupernode()) { nSupernodes++; } } return nSupernodes; } /** * @return if we have reached the maximum number of connections allwed */ public boolean maxConnectionsReached() { // Assume unlimited upload if (getController().getTransferManager().getUploadCPSForWAN() <= 0) { // Unlimited upload return false; } // logWarning("Max allowed: " + // getController().getTransferManager().getAllowedUploadCPS()); double uploadKBs = (double) getController().getTransferManager() .getUploadCPSForWAN() / 1024; int nConnected = countConnectedNodes(); int maxConnectionsAllowed = (int) (uploadKBs * Constants.MAX_NODES_CONNECTIONS_PER_KBS_UPLOAD); if (nConnected > maxConnectionsAllowed) { logFiner("Not more connection slots open. Used " + nConnected + "/" + maxConnectionsAllowed); } // Still capable of new connections? return nConnected > maxConnectionsAllowed; } /** * @return the own identity, of course with no connection */ public Member getMySelf() { return mySelf; } /** * @return the network ID this nodemanager belongs to. #1373 */ public String getNetworkId() { return mySelf.getInfo().networkId; } /** * @param member * the member to check * @return if we know this member */ public boolean knowsNode(Member member) { if (member == null) { return false; } // do i know him ? return knowsNode(member.getId()); } /** * Returns true if the IP of the given member is within one of the * configured ranges Those are setup in advanced settings "LANlist". ONLY if * any of my own IP is on the LAN list aswell. * * @param adr * the internet addedss * @return true if the member's ip is within one of the ranges */ private boolean isNodeOnConfiguredLan(InetAddress adr) { boolean iamOnLANlist = false; try { for (InterfaceAddress ia : NetworkUtil .getAllLocalNetworkAddressesCached().keySet()) { if (ia.getAddress() instanceof Inet4Address && isNodeOnConfiguredLan0(ia.getAddress())) { iamOnLANlist = true; break; } } } catch (Exception e) { logWarning("Unable to get LAN/Adapter configuration. " + e); // Fallback / Old behavior iamOnLANlist = true; } if (!iamOnLANlist) { return false; } for (AddressRange ar : lanRanges) { if (ar.contains((Inet4Address) adr)) { return true; } } return false; } private boolean isNodeOnConfiguredLan0(InetAddress adr) { for (AddressRange ar : lanRanges) { if (ar.contains((Inet4Address) adr)) { return true; } } return false; } /** * @param adr * the internet addedss * @return true if this address is on LAN (by IP/subnet mask) OR configured * on LAN */ public boolean isOnLANorConfiguredOnLAN(InetAddress adr) { Reject.ifNull(adr, "Address is null"); if (!(adr instanceof Inet4Address)) { return false; } if (Feature.CORRECT_LAN_DETECTION.isDisabled()) { return true; } if (Feature.CORRECT_INTERNET_DETECTION.isDisabled()) { return false; } if (NetworkUtil.isNullIP(adr)) { // Unknown / Probably tunneled addresses return false; } return NetworkUtil.isOnLanOrLoopback(adr) || isNodeOnConfiguredLan(adr) || (getController().getBroadcastManager() != null && getController() .getBroadcastManager().receivedBroadcast(adr)); } /** * @param member * @return if we know this member */ public boolean knowsNode(MemberInfo member) { if (member == null) { return false; } return knowsNode(member.id); } /** * @return the number of connected nodes */ public int countConnectedNodes() { return connectedNodes.size(); } /** * ATTENTION: May change after returned! Make copy if stable state if * required. * * @return a unmodifiable version of the internal list of connected nodes. */ public Collection<Member> getConnectedNodes() { return Collections.unmodifiableCollection(connectedNodes.values()); } /** * @param id * the id of the member * @return true if we know this member */ public boolean knowsNode(String id) { if (id == null) { return false; } // do i know him ? return id.equals(mySelf.getId()) || knownNodes.containsKey(id); } /** * @param mInfo * the memberinfo * @return the member for a memberinfo */ public Member getNode(MemberInfo mInfo) { if (mInfo == null) { return null; } return getNode(mInfo.id); } /** * @param id * @return the member for this id */ public Member getNode(String id) { if (id == null) { return null; } if (mySelf.getId().equals(id)) { return mySelf; } return knownNodes.get(id); } /** * @return all known nodes in a collection. The collection is a unmodifiable * referece to the internal know nodes storage. May change after has * been returned! */ public Collection<Member> getNodesAsCollection() { return Collections.unmodifiableCollection(knownNodes.values()); } /** * Removes a member from the known list * * @param node */ public void removeNode(Member node) { if (isFiner()) { logFiner("Removing " + node.getNick() + " from nodelist"); } // Shut down node node.shutdown(); // removed from folders getController().getFolderRepository().removeFromAllFolders(node); knownNodes.remove(node.getId()); // Remove all his listeners node.removeAllListeners(); // Fire event fireNodeRemoved(node); logFine(node + " removed from from know nodes list"); } /** * Counts the number of friends that are online * * @return the number of friends that are online */ public int countOnlineFriends() { int nOnlineFriends = 0; for (Member friend : getFriends()) { if (friend.isConnectedToNetwork()) { nOnlineFriends++; } } return nOnlineFriends; } /** * @return the number of friends */ public int countFriends() { return friends.size(); } /** * @return the list of friends */ public Member[] getFriends() { return friends.values().toArray(new Member[friends.values().size()]); } /** * Called by member. Not getting this event from event handling because we * have to handle things definitively before other elements work on that. * * @param node * @param server */ public void serverStateChanged(Member node, boolean server) { if (node.isMySelf()) { // Ignore change on myself return; } fireNodeSettingsChanged(node); if (nodefileLoaded) { // Only store after start // Store nodes storeNodes(); } } /** * Called by member. Not getting this event from event handling because we * have to handle things definitively before other elements work on that. * * @param node * @param friend * @param personalMessage */ public void friendStateChanged(final Member node, boolean friend, String personalMessage) { if (node.isMySelf()) { // Ignore change on myself return; } boolean wasFriend = node.isFriend(); boolean nodesChanged = false; if (friend) { friends.put(node.getId(), node); nodesChanged = true; fireFriendAdded(node); // Mark node for immediate connection node.markForImmediateConnect(); // Send a "you were added" if (getController().getTaskManager().isStarted()) { getController().getTaskManager().scheduleTask( new SendMessageTask(new AddFriendNotification(mySelf .getInfo(), personalMessage), node.getId())); } } else if (wasFriend) { friends.remove(node.getId()); nodesChanged = true; fireFriendRemoved(node); // Remove computer from the list of my last logged in computers. if (getController().getOSClient().isLoggedIn()) { getController().getTaskManager().scheduleTask( new RemoveComputerFromAccountTask(getController() .getOSClient().getAccountInfo(), node.getInfo())); } } if (nodefileLoaded && nodesChanged) { // Only store after start // Store nodes storeNodes(); } } /** * Callback method to inform that this node connected or disconnected from * the public network. * * @param node */ public void networkConnectionStateChanged(Member node) { Reject.ifNull(node, "Node"); if (node.isConnectedToNetwork()) { fireNodeOnline(node); } else { fireNodeOffline(node); } } /** * Callback to inform nodemanager that this nodes connecting state changed. * * @param node */ public void connectingStateChanged(Member node) { fireNodeConnecting(node); } /** * Callback method from node to inform nodemanager about an online state * change * * @param node */ public void connectStateChanged(Member node) { boolean nodeConnected = node.isCompletelyConnected(); if (nodeConnected) { // Add to online nodes connectedNodes.put(node.getId(), node); // add to broadcastlist nodesWentOnline.add(node.getInfo()); // if (!mySelf.isSupernode() // && countConnectedSupernodes() >= // Constants.N_SUPERNODES_TO_CONNECT) // { // // # of necessary connections probably reached, avoid more // // reconnection tries. // logFine("Max # of connections reached. " // + "Rebuilding reconnection queue"); // getController().getReconnectManager().buildReconnectionQueue(); // } if (getController().getIOProvider().getRelayedConnectionManager() .isRelay(node.getInfo())) { logFine("Connect to relay detected. Rebuilding reconnection queue"); getController().getReconnectManager().buildReconnectionQueue(); } } else { // Remove from list connectedNodes.remove(node.getId()); nodesWentOnline.remove(node.getInfo()); getController().getTransferManager().breakTransfers(node); // Try instant reconnect (if not dupe connection detection). Problem lastProblem = node.getLastProblem(); boolean instantReconnect = true; if (lastProblem != null) { instantReconnect = lastProblem.problemCode != Problem.DUPLICATE_CONNECTION && lastProblem.problemCode != Problem.DO_NOT_LONGER_CONNECT; } if (instantReconnect && node.isInteresting()) { getController().getReconnectManager().considerReconnectionTo( node); } } // Event handling if (nodeConnected) { fireNodeConnected(node); } else { fireNodeDisconnected(node); } } /** * Callback method from Member. * * @param from * @param message */ public void messageReceived(Member from, Message message) { valveMessageListenerSupport.fireMessage(from, message); } /** * Processes a request for nodelist. * * @param request * the request. * @param from * the origin of the request */ public void receivedRequestNodeList(RequestNodeList request, Member from) { List<MemberInfo> list; list = request.filter(knownNodes.values()); from.sendMessagesAsynchron(KnownNodes.createKnownNodesList(list, from.getProtocolVersion() >= 107)); } public void receivedSearchNodeRequest(final SearchNodeRequest request, final Member from) { Runnable searcher = new Runnable() { public void run() { List<MemberInfo> reply = new LinkedList<MemberInfo>(); for (Member m : getController().getNodeManager() .getNodesAsCollection()) { if (m.getInfo().isInvalid(getController())) { continue; } if (m.matches(request.searchString)) { reply.add(m.getInfo()); } } if (!reply.isEmpty()) { if (from.getProtocolVersion() >= 107) { from.sendMessageAsynchron(new KnownNodesExt(reply .toArray(new MemberInfo[reply.size()]))); } else { from.sendMessageAsynchron(new KnownNodes(reply .toArray(new MemberInfo[reply.size()]))); } } } }; getController().getIOProvider().startIO(searcher); } /** * Creates the default request for nodelist according to our own status. In * supernode mode we might want to request more node information that in * normal peer mode. * <p> * Attention: This method synchronizes on the internal friendlist. Avoid * holding a lock while calling this method. * * @return the message. */ public RequestNodeList createDefaultNodeListRequestMessage() { if (mySelf.isSupernode()) { return RequestNodeList.createRequestAllNodes(); } // TODO Change second paramter to RequestNodeList.NodesCriteria.NONE // when network folder list also covers private folders. return RequestNodeList.createRequest(friends.values(), RequestNodeList.NodesCriteria.ONLINE, RequestNodeList.NodesCriteria.ONLINE); } /** * Processes new node informations. Reconnects if required. * * @param newNodes * the new nodes to queue. */ public void queueNewNodes(MemberInfo[] newNodes) { if (newNodes == null || newNodes.length == 0) { return; } // queue new members if (isFiner()) { logFiner("Received new list of " + newNodes.length + " nodes"); } int nNewNodes = 0; int nQueuedNodes = 0; ReconnectManager reconnectManger = getController() .getReconnectManager(); for (int i = 0; i < newNodes.length; i++) { MemberInfo newNode = newNodes[i]; // just check, for a faster shutdown if (Thread.currentThread().isInterrupted()) { return; } if (newNode == null || newNode.isInvalid(getController())) { // Member is too old, ignore if (isFiner()) { logFiner("Not adding new node: " + newNode); } continue; } if (!newNode.isOnSameNetwork(getController())) { // Never add nodes from other networks continue; } // Ask filters if this node is valueable to us boolean ignoreNode = true; for (NodeFilter filter : nodeFilters) { if (filter.shouldAddNode(newNode)) { ignoreNode = false; break; } } if (!ignoreNode) { // Ignore temporary nodes if (ServerClient.isTempServerNode(newNode)) { logWarning("Ignoring temporary server node: " + newNode); ignoreNode = true; } } // Disabled: This causes problems when executing a search for users // blocks that normal nodes get added to the database. // boolean supernodeOrConnected = newNode.isSupernode // || newNode.isConnected; // if (!mySelf.isSupernode() && !supernodeOrConnected) { // // Skip unuselful nodes // continue; // } if (ignoreNode) { // Skil unuseful nodes continue; } Member thisNode = getNode(newNode); if (newNode.matches(mySelf)) { // ignore myself continue; } else if (getController().isLanOnly() && (newNode.getConnectAddress() != null) && (newNode.getConnectAddress().getAddress() != null) && !getController().getNodeManager().isOnLANorConfiguredOnLAN( newNode.getConnectAddress().getAddress())) { // ignore if lan only mode && newNode not is onlan continue; } else if (thisNode == null) { // add node thisNode = addNode(newNode); nNewNodes++; } else { // update own information if more valueable if (!thisNode.isServer()) { thisNode.updateInfo(newNode); } } if (newNode.isConnected) { // Node is connected to the network thisNode.setConnectedToNetwork(newNode.isConnected); boolean queued = reconnectManger .considerReconnectionTo(thisNode); if (queued) { nQueuedNodes++; } } } if (nQueuedNodes > 0 || nNewNodes > 0) { if (isFiner()) { logFiner("Queued " + nQueuedNodes + " new nodes for reconnection, " + nNewNodes + " added"); } } } /** * Accept a node, method does not block. * * @param acceptor */ public void acceptConnectionAsynchron(AbstractAcceptor acceptor) { Reject.ifNull(acceptor, "Acceptor is null"); // Create acceptor on socket if (!started) { logFine("Not accepting connection " + acceptor + ". NodeManager is not started"); acceptor.shutdown(); return; } if (isFiner()) { logFiner("Connection queued for acception: " + acceptor + ""); } // Enqueue for later processing acceptors.add(acceptor); getController().getIOProvider().startIO(acceptor); // Throttle acception a bit depending on how much incoming connections // we are currently processing. long waitTime = (acceptors.size() * Controller.getWaitTime()) / 400; if (isFiner()) { logFiner("Currently processing incoming connections (" + acceptors.size() + "), throttled (" + waitTime + "ms wait)"); } if (acceptors.size() > Constants.MAX_INCOMING_CONNECTIONS) { // Show warning logWarning("Processing too many incoming connections (" + acceptors.size() + "), throttled (" + waitTime + "ms wait)"); } try { Thread.sleep(waitTime); } catch (InterruptedException e) { logFiner(e); } } /** * Internal method for accepting nodes on a connection handler * * @param handler * @throws ConnectionException * @return the connected node or null if problem occurred */ public Member acceptConnection(ConnectionHandler handler) throws ConnectionException { if (!started) { logFine("Not accepting node from " + handler + ". NodeManager is not started"); handler.shutdown(); throw new ConnectionException("Not accepting node from " + handler + ". NodeManager is not started").with(handler); } // Accepts a node from a connection handler Identity remoteIdentity = handler.getIdentity(); // check for valid identity if (remoteIdentity == null || !remoteIdentity.isValid()) { logWarning("Received an illegal identity from " + handler + ". disconnecting. " + remoteIdentity); handler.shutdown(); throw new ConnectionException("Received an illegal identity from " + handler + ". disconnecting. " + remoteIdentity).with(handler); } if (getMySelf().getInfo().equals(remoteIdentity.getMemberInfo())) { logFine("Loopback connection detected to " + handler + ", disconnecting"); handler.shutdown(); throw new ConnectionException("Loopback connection detected to " + handler + ", disconnecting").with(handler); } if (!remoteIdentity.getMemberInfo().isOnSameNetwork(getController())) { if (getController().getOSClient().isPrimaryServer(handler) && !mySelf.isServer()) { logWarning("Server not on same network " + handler + ", disconnecting. remote network ID: " + remoteIdentity.getMemberInfo().networkId + ". Expected/Ours: " + getNetworkId()); } else { logFine("Remote client not on same network " + handler + ", disconnecting. remote network ID: " + remoteIdentity.getMemberInfo().networkId + ". Expected/Ours: " + getNetworkId()); } handler.shutdown(); throw new ConnectionException("Remote client not on same network " + handler + ", disconnecting. remote network ID: " + remoteIdentity.getMemberInfo().networkId + ". Expected/Ours: " + getNetworkId()).with(handler); } if (!mySelf.isServer() && !ConfigurationEntry.SERVER_DISCONNECT_SYNC_ANYWAYS .getValueBoolean(getController())) { ServerClient client = getController().getOSClient(); // Only actually connect to other clients if logged into server. if (!client.isLoggedIn() && !client.isPrimaryServer(handler)) { handler.shutdown(); logWarning("Not connected to server (" + client.getServer().getNick() + ") yet. Disconnecting " + handler.getMyIdentity().getMemberInfo()); throw new ConnectionException("Not connected to server (" + client.getServer().getNick() + ") yet. Disconnecting " + handler.getMyIdentity().getMemberInfo()).with(handler); } } Member member; // Accept node ? boolean acceptHandler; String rejectCause = null; // Accept only one node at a time synchronized (acceptLock) { if (isFiner()) { logFiner("Accept lock taken. Member: " + remoteIdentity.getMemberInfo() + ", Handler: " + handler); } // Is this member already known to us ? member = getNode(remoteIdentity.getMemberInfo()); if (member == null) { // Create new node member = new Member(getController(), handler.getIdentity() .getMemberInfo()); // add node addNode(member); // Accept handler acceptHandler = true; } else { // Old member, check its connection state if (!member.isOnLAN() && handler.isOnLAN()) { // Only accept handler, if our one is disco! or our is not // on LAN acceptHandler = true; // #841 NOT isCompletelyConnected() } else if (member.isConnected()) { rejectCause = "Duplicate connection detected to " + member.getNick() + " (" + member.getReconnectAddress() + ")"; acceptHandler = false; } else { // Otherwise accept. (our member = disco) acceptHandler = true; } } if (isFiner()) { logFiner("Accept lock released. Member: " + remoteIdentity.getMemberInfo() + ", Handler: " + handler); } } if (acceptHandler) { if (member.getPeer() != handler) { if (member.isConnected()) { logWarning("Taking a better conHandler for " + member.getNick() + ". current: " + member.getPeer() + ", onLAN? " + member.isOnLAN() + "/" + member.getPeer().isOnLAN() + ". new: " + handler + ", onLAN? " + handler.isOnLAN()); } // Complete handshake try { int connectionTries = member.markConnecting(); if (connectionTries >= 2) { logFine("Multiple connection tries detected (" + connectionTries + ") to " + member); } ConnectResult res = member.setPeer(handler); if (res.isFailure()) { throw new ConnectionException( "Unable to connect to node " + member + ". " + res); } } finally { member.unmarkConnecting(); } } } else { if (isFine()) { logFine(rejectCause + ", connected? " + handler.isConnected()); } // Tell remote side, fatal problem try { handler.sendMessage(new Problem(rejectCause, true, Problem.DUPLICATE_CONNECTION)); } finally { handler.shutdown(); } throw new ConnectionException(rejectCause + ", connected? " + handler.isConnected()); } return member; } /** * Adds a new node to the nodemanager. Initalize from memberinfo * * @param newNode * @return the new node */ public Member addNode(MemberInfo newNode) { Member member = new Member(getController(), newNode); addNode(member); return member; } /** * Adds a new node to the nodemanger * * @param node * the new node */ private void addNode(Member node) { if (node == null) { throw new NullPointerException("Node is null"); } // logFiner("Adding new node: " + node); Member oldNode = knownNodes.get(node.getId()); if (oldNode != null) { logWarning("Overwriting old node: " + oldNode + " with " + node); removeNode(oldNode); } knownNodes.put(node.getId(), node); if (!node.isOnSameNetwork()) { if (isFine()) { logFine("Changed network ID of node " + node.getNick() + " from " + node.getInfo().networkId + " to " + getNetworkId()); } node.getInfo().networkId = getNetworkId(); } // Fire new node event fireNodeAdded(node); } /** * Broadcasts a message to all nodes, does not block. Message enqueued to be * sent asynchron * * @param message */ public void broadcastMessage(final Message message) { broadcastMessage(message, null); } /** * Broadcasts a message to all nodes, does not block. Message enqueued to be * sent asynchron * * @param message * @param filter * to filter the members to send the messages to */ public void broadcastMessage(final Message message, final Filter<Member> filter) { if (!started) { logFine("Not started. Not broadcasting message: " + message); return; } if (isFiner()) { logFiner("Broadcasting message: " + message); } broadcastMessage(0, new MessageProducer() { public Message[] getMessages(boolean useExternalizable) { return new Message[]{message}; } }, filter); } /** * Broadcasts a message to all nodes, does not block. Message enqueued to be * sent asynchron * * @param msgProd * The producer of the message(s) * @param minProtocolVersion * #2072: the minimum protocol version a remote client has to * support to produce {@link Externalizable} messages. * Identity#getProtocolVersion() * @param filter * to filter the members to send the messages to */ public void broadcastMessage(final int minProtocolVersion, final MessageProducer msgProd, final Filter<Member> filter) { if (!started) { logFine("Not started. Not broadcasting message: " + Arrays.asList(msgProd.getMessages(true))); return; } if (isFiner()) { logFiner("Broadcasting message of producer " + msgProd); } Runnable broadcaster = new Runnable() { public void run() { Message[] msgs = null; Message[] msgsExt = null; for (Member node : knownNodes.values()) { if (!node.isCompletelyConnected()) { continue; } if (filter != null && !filter.accept(node)) { // Skip continue; } if (node.getProtocolVersion() >= minProtocolVersion) { if (msgsExt == null) { msgsExt = msgProd.getMessages(true); } if (msgsExt != null && msgsExt.length > 0) { node.sendMessagesAsynchron(msgsExt); } } else { if (msgs == null) { msgs = msgProd.getMessages(false); } if (msgs != null && msgs.length > 0) { node.sendMessagesAsynchron(msgs); } } try { // Slight delay to prevent abnormal threadpool // growth of Sender threads. Thread.sleep(5); } catch (InterruptedException e) { logFiner("InterruptedException", e); break; } } } }; getController().getIOProvider().startIO(broadcaster); } /** * Broadcasts a message along a number of supernodes * * @param message * the message to broadcast * @param nSupernodes * the maximum numbers to supernodes to send the message to. 0 or * lower means to all supernodes * @return the number of nodes where the message has been broadcasted */ public int broadcastMessageToSupernodes(Message message, int nSupernodes) { if (!started) { logFine("Not started. Not broadcasting message: " + message); return 0; } if (isFiner()) { logFiner("Broadcasting message to supernodes: " + message); } int nNodes = 0; List<Member> supernodes = new LinkedList<Member>(); for (Member node : knownNodes.values()) { if (node.isCompletelyConnected() && node.isSupernode()) { // Only broadcast after completely connected supernodes.add(node); } } if (nSupernodes <= 0) { // Broadcast to all supernodes nSupernodes = supernodes.size(); } nSupernodes = Math.min(supernodes.size(), nSupernodes); for (int i = 0; i < nSupernodes; i++) { // Take a random supernode int index = (int) (Math.random() * supernodes.size()); Member supernode = supernodes.get(index); supernodes.remove(index); logFine("Sending message to supernode: " + supernode.getNick() + ". " + message); supernode.sendMessageAsynchron(message); nNodes++; } return nNodes; } /** * Broadcasts a message along a number of nodes on lan * * @param message * the message to broadcast * @param nBroadcasted * the maximum numbers of lan nodes to send the message to. 0 or * lower means to all nodes on lan * @return the number of nodes where the message has been broadcasted */ public int broadcastMessageLANNodes(Message message, int nBroadcasted) { if (!started) { logFine("Not started. Not broadcasting message: " + message); return 0; } if (isFiner()) { logFiner("Broadcasting message to LAN nodes: " + message); } int nNodes = 0; List<Member> lanNodes = new LinkedList<Member>(); for (Member node : knownNodes.values()) { if (node.isCompletelyConnected() && node.isOnLAN()) { // Only broadcast after completely connected lanNodes.add(node); } } if (nBroadcasted <= 0) { // Broadcast to all supernodes nBroadcasted = lanNodes.size(); } nBroadcasted = Math.min(lanNodes.size(), nBroadcasted); for (int i = 0; i < nBroadcasted; i++) { // Take a random supernode int index = (int) (Math.random() * lanNodes.size()); Member node = lanNodes.get(index); lanNodes.remove(index); logFine("Sending message to lan node: " + node.getNick() + ". " + message); node.sendMessageAsynchron(message); nNodes++; } return nNodes; } /** * Also cleansweeps all servers except primary server. * * @return the number of total servers know now. */ public void loadServerNodes() { String serverNodesURL = getController().getOSClient().getWebURL( SERVER_NODES_URI, false); if (StringUtils.isNotBlank(serverNodesURL)) { try { loadNodesFrom(new URL(serverNodesURL)); } catch (MalformedURLException e) { logWarning(e.toString()); } } } /** * Loads members from url and adds them. Also removes unsets all servers, * except primary server. * * @param url */ private boolean loadNodesFrom(URL url) { try { NodeList nodeList = new NodeList(); nodeList.load(url); logFine("I know " + nodeList.getServersSet().size() + " servers from cluster @ " + url + " : " + nodeList.getServersSet()); return processNodeList(nodeList); } catch (IOException e) { logWarning("Unable to load servers from url '" + url + "'. " + e.getMessage()); logFiner("IOException", e); } catch (ClassCastException e) { logWarning("Illegal format of servers url '" + url); logFiner("ClassCastException", e); } catch (ClassNotFoundException e) { logWarning("Illegal format of servers files '" + url); logFiner("ClassNotFoundException", e); } return false; } /** * Loads members from disk and adds them * * @param nodeList */ private boolean loadNodesFrom(String filename) { File nodesFile = new File(Controller.getMiscFilesLocation(), filename); if (!nodesFile.exists()) { // Try harder in local base nodesFile = new File(filename); } if (!nodesFile.exists()) { logFine("Unable to load nodes, file not found " + nodesFile.getAbsolutePath()); return false; } try { NodeList nodeList = new NodeList(); nodeList.load(nodesFile); logFine("Loaded " + nodeList.getNodeList().size() + " nodes from " + nodesFile.getAbsolutePath()); return processNodeList(nodeList); } catch (IOException e) { logWarning("Unable to load nodes from file '" + filename + "'. " + e.getMessage()); logFiner("IOException", e); } catch (ClassCastException e) { logWarning("Illegal format of supernodes files '" + filename + "', deleted"); logFiner("ClassCastException", e); if (!nodesFile.delete()) { logSevere("Failed to delete supernodes file: " + nodesFile.getAbsolutePath()); } } catch (ClassNotFoundException e) { logWarning("Illegal format of supernodes files '" + filename + "', deleted"); logFiner("ClassNotFoundException", e); nodesFile.delete(); } return false; } private boolean processNodeList(NodeList nodeList) { queueNewNodes(nodeList.getNodeList().toArray( new MemberInfo[nodeList.getNodeList().size()])); for (MemberInfo friend : nodeList.getFriendsSet()) { Member node = friend.getNode(getController(), true); if (!this.friends.containsKey(node.getId()) && !node.isMySelf()) { this.friends.put(node.getId(), node); } } // Cleanup old servers: for (Member node : knownNodes.values()) { ServerClient client = getController().getOSClient(); if (client != null && client.isPrimaryServer(node)) { continue; } if (!nodeList.getServersSet().contains(node.getInfo())) { node.setServer(false); } } for (MemberInfo server : nodeList.getServersSet()) { Member node = server.getNode(getController(), true); node.updateInfo(server); node.setServer(true); logFine("Loaded server: " + node); } return !nodeList.getNodeList().isEmpty(); } /** * Loads members from disk and connects to them */ private void loadNodes() { String filename = getController().getConfigName() + ".nodes"; if (!loadNodesFrom(filename)) { filename += ".backup"; logFine("Failed to load nodes, trying backup nodefile '" + filename + "'"); if (!loadNodesFrom(filename)) { return; } } if (ConfigurationEntry.SERVER_LOAD_NODES .getValueBoolean(getController())) { getController().getIOProvider().startIO(new Runnable() { public void run() { loadServerNodes(); } }); } } /** * Saves the state of current members/connections. members will get * reconnected at start */ private void storeNodes() { Collection<MemberInfo> allNodesInfos = Convert.asMemberInfos(knownNodes .values()); allNodesInfos.add(getMySelf().getInfo()); // Add myself to know nodes Collection<MemberInfo> friendInfos = Convert.asMemberInfos(friends .values()); NodeList nodeList = new NodeList(allNodesInfos, friendInfos, getServers()); if (!storeNodes0(getController().getConfigName() + ".nodes", nodeList)) { logFine("Nodes file could not be written"); } } private Collection<MemberInfo> getServers() { Collection<MemberInfo> servers = new LinkedList<MemberInfo>(); for (Member member : knownNodes.values()) { if (member.isServer()) { logFine("Server: " + member); servers.add(member.getInfo()); } } return servers; } /** * Stores the supernodes that are currently online in a separate file. */ private void storeOnlineSupernodes() { Collection<MemberInfo> latestSupernodesInfos = new ArrayList<MemberInfo>(); Collection<Member> latestSupernodes = new ArrayList<Member>(); for (Member node : getNodesAsCollection()) { if (!node.isSupernode()) { // Skip non-supernode continue; } if (!node.isConnectedToNetwork()) { continue; } latestSupernodesInfos.add(node.getInfo()); latestSupernodes.add(node); } if (getMySelf().isSupernode()) { latestSupernodesInfos.add(getMySelf().getInfo()); latestSupernodes.add(getMySelf()); } if (getController().isVerbose()) { Debug.writeNodeListCSV(latestSupernodes, "SupernodesOnline.csv"); } } /** * Internal method for storing nodes into a files * <p> */ private boolean storeNodes0(String filename, NodeList nodeList) { File nodesFile = new File(Controller.getMiscFilesLocation(), filename); if (!nodesFile.getParentFile().exists()) { // for testing this directory needs to be created because we have // subs in the config name if (!nodesFile.getParentFile().mkdirs()) { logSevere("Failed to create directory: " + nodesFile.getAbsolutePath()); } } if (nodeList.getNodeList().isEmpty()) { logFine("Not storing list of nodes, none known"); return false; } logFine("Saving known nodes/friends with " + nodeList.getNodeList().size() + " nodes to " + filename); try { nodeList.save(nodesFile); return true; } catch (IOException e) { logWarning("Unable to write supernodes to file '" + filename + "'. " + e.getMessage()); logFiner("IOException", e); return false; } } // Message listener code ************************************************** public void addMessageListenerToAllNodes(MessageListener listener) { valveMessageListenerSupport.addMessageListener(listener); } public void addMessageListenerToAllNodes( Class<? extends Message> messageType, MessageListener listener) { valveMessageListenerSupport.addMessageListener(messageType, listener); } public void removeMessageListener(MessageListener listener) { valveMessageListenerSupport.removeMessageListener(listener); } // Internal classes ******************************************************* /** * Sets up all tasks, that needs to be periodically executed. */ private void setupPeridicalTasks() { // Broadcast transfer status getController().scheduleAndRepeat(new TransferStatusBroadcaster(), Constants.TRANSFER_STATUS_BROADCAST_INTERVAL * 1000 / 2, Constants.TRANSFER_STATUS_BROADCAST_INTERVAL * 1000); // Request network folder list // timer.schedule(new NetworkFolderListRequestor(), // Constants.NETWORK_FOLDER_LIST_REQUEST_INTERVAL * 1000 / 2, // Constants.NETWORK_FOLDER_LIST_REQUEST_INTERVAL * 1000); // Request new node list from time to time getController().scheduleAndRepeat(new NodeListRequestor(), Constants.NODE_LIST_REQUEST_INTERVAL * 1000 / 2, Constants.NODE_LIST_REQUEST_INTERVAL * 1000); // Broadcast the nodes that went online getController().scheduleAndRepeat( new NodesThatWentOnlineListBroadcaster(), Constants.NODES_THAN_WENT_ONLINE_BROADCAST_TIME * 1000 / 2, Constants.NODES_THAN_WENT_ONLINE_BROADCAST_TIME * 1000); // Check incoming connection tries getController().scheduleAndRepeat(new AcceptorsChecker(), 0, Constants.INCOMING_CONNECTION_CHECK_TIME * 1000); // Write statistics and other infos. if (getController().isVerbose()) { getController().scheduleAndRepeat(new StatisticsWriter(), 59 * 1000, 60 * 1000); } } // Workers **************************************************************** /** * Default behaviour: 1) Add all nodes when acting as supernode 2) Add if * remote side is supernode or connected. */ private final class DefaultNodeFilter implements NodeFilter { public boolean shouldAddNode(MemberInfo nodeInfo) { boolean supernodeOrConnected = nodeInfo.isSupernode || nodeInfo.isConnected; return mySelf.isSupernode() || supernodeOrConnected; } } /** * Broadcasts the transferstatus */ private class TransferStatusBroadcaster extends TimerTask { @Override public void run() { // Broadcast new transfer status TransferStatus status = getController().getTransferManager() .getStatus(); if (isFiner()) { logFiner("Broadcasting transfer status: " + status); } broadcastMessage(status); } } /** * Broadcasts all nodes, that went online since the last execution. */ private class NodesThatWentOnlineListBroadcaster extends TimerTask { @Override public void run() { if (nodesWentOnline.isEmpty()) { return; } logFine("Broadcasting " + nodesWentOnline.size() + " nodes that went online"); final MemberInfo[] nodes; synchronized (nodesWentOnline) { nodes = new MemberInfo[nodesWentOnline.size()]; nodesWentOnline.toArray(nodes); nodesWentOnline.clear(); } broadcastMessage(107, new SingleMessageProducer() { @Override public Message getMessage(boolean useExt) { return useExt ? new KnownNodesExt(nodes) : new KnownNodes( nodes); } }, null); } } /** * Checks the currently attempted connection tries for timeouts. */ private class AcceptorsChecker extends TimerTask { @Override public void run() { int size = acceptors.size(); logFine("Checking incoming connection queue (" + size + ")"); if (size > Constants.MAX_INCOMING_CONNECTIONS) { logWarning("Processing too many incoming connections (" + size + ")"); } for (AbstractAcceptor acceptor : acceptors) { if (acceptor.hasTimeout()) { logWarning("Acceptor has timeout: " + acceptor); acceptor.shutdown(); acceptors.remove(acceptor); } else if (acceptor.isShutdown()) { logWarning("Acceptor has been shutdown: " + acceptor); acceptors.remove(acceptor); } } } } /** * Requests the required nodelist. */ private class NodeListRequestor extends TimerTask { @Override public void run() { // Request new nodelist from supernodes RequestNodeList request = createDefaultNodeListRequestMessage(); if (log.isLoggable(Level.FINE)) { logFine("Requesting nodelist: " + request); } broadcastMessageToSupernodes(request, Constants.N_SUPERNODES_TO_CONTACT_FOR_NODE_LIST); } } /** * Writes the statistic to disk. */ private class StatisticsWriter extends TimerTask { @Override public void run() { Debug.writeStatistics(getController()); Debug.writeNodeListCSV(new ArrayList<Member>(getController() .getReconnectManager().getReconnectionQueue()), "ReconnectionQueue.csv"); } } // Listener support ******************************************************* public void addNodeManagerListener(NodeManagerListener listener) { ListenerSupportFactory.addListener(listenerSupport, listener); } public void addWeakNodeManagerListener(NodeManagerListener listener) { ListenerSupportFactory.addListener(listenerSupport, listener, true); } public void removeNodeManagerListener(NodeManagerListener listener) { ListenerSupportFactory.removeListener(listenerSupport, listener); } public void addNodeFilter(NodeFilter filter) { Reject.ifNull(filter, "Filter is null"); nodeFilters.add(filter); } public void removeNodeFilter(NodeFilter filter) { Reject.ifNull(filter, "Filter is null"); nodeFilters.remove(filter); } // Helper ***************************************************************** private void fireNodeRemoved(Member node) { listenerSupport.nodeRemoved(new NodeManagerEvent(this, node)); } private void fireNodeAdded(final Member node) { listenerSupport.nodeAdded(new NodeManagerEvent(this, node)); } private void fireNodeConnecting(Member node) { listenerSupport.nodeConnecting(new NodeManagerEvent(this, node)); } private void fireNodeConnected(final Member node) { listenerSupport.nodeConnected(new NodeManagerEvent(this, node)); } private void fireNodeDisconnected(final Member node) { listenerSupport.nodeDisconnected(new NodeManagerEvent(this, node)); } private void fireNodeOnline(final Member node) { listenerSupport.nodeOnline(new NodeManagerEvent(this, node)); } private void fireNodeOffline(final Member node) { listenerSupport.nodeOffline(new NodeManagerEvent(this, node)); } private void fireFriendAdded(final Member node) { listenerSupport.friendAdded(new NodeManagerEvent(this, node)); } private void fireFriendRemoved(final Member node) { listenerSupport.friendRemoved(new NodeManagerEvent(this, node)); } public void fireNodeSettingsChanged(final Member node) { listenerSupport.settingsChanged(new NodeManagerEvent(this, node)); } }