Java tutorial
/* * 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: Controller.java 21251 2013-03-19 01:46:23Z sprajc $ */ package de.dal33t.powerfolder; import java.awt.Component; import java.awt.Desktop; import java.awt.GraphicsEnvironment; import java.io.BufferedInputStream; import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.IOException; import java.io.ObjectInputStream; import java.io.ObjectOutputStream; import java.net.InetSocketAddress; import java.nio.file.DirectoryStream; import java.nio.file.DirectoryStream.Filter; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; import java.security.Security; import java.util.ArrayList; import java.util.Calendar; import java.util.Collection; import java.util.Collections; import java.util.Date; import java.util.GregorianCalendar; import java.util.List; import java.util.Properties; import java.util.ResourceBundle; import java.util.ServiceLoader; import java.util.StringTokenizer; import java.util.TimerTask; import java.util.concurrent.Callable; import java.util.concurrent.CopyOnWriteArrayList; import java.util.concurrent.FutureTask; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.ScheduledFuture; import java.util.concurrent.ScheduledThreadPoolExecutor; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicBoolean; import java.util.logging.Level; import java.util.logging.Logger; import java.util.prefs.BackingStoreException; import java.util.prefs.Preferences; import javax.swing.JOptionPane; import org.apache.commons.cli.CommandLine; import de.dal33t.powerfolder.clientserver.ServerClient; import de.dal33t.powerfolder.disk.Folder; import de.dal33t.powerfolder.disk.FolderRepository; import de.dal33t.powerfolder.disk.SyncProfile; import de.dal33t.powerfolder.distribution.Distribution; import de.dal33t.powerfolder.distribution.PowerFolderBasic; import de.dal33t.powerfolder.distribution.PowerFolderPro; import de.dal33t.powerfolder.event.InvitationHandler; import de.dal33t.powerfolder.event.LimitedConnectivityEvent; import de.dal33t.powerfolder.event.LimitedConnectivityListener; import de.dal33t.powerfolder.event.ListenerSupportFactory; import de.dal33t.powerfolder.event.LocalMassDeletionEvent; import de.dal33t.powerfolder.event.MassDeletionHandler; import de.dal33t.powerfolder.event.NetworkingModeEvent; import de.dal33t.powerfolder.event.NetworkingModeListener; import de.dal33t.powerfolder.event.PausedModeEvent; import de.dal33t.powerfolder.event.PausedModeListener; import de.dal33t.powerfolder.event.RemoteMassDeletionEvent; import de.dal33t.powerfolder.light.MemberInfo; import de.dal33t.powerfolder.message.FolderList; import de.dal33t.powerfolder.message.Invitation; import de.dal33t.powerfolder.message.RequestNodeInformation; import de.dal33t.powerfolder.message.SettingsChange; import de.dal33t.powerfolder.net.BroadcastMananger; import de.dal33t.powerfolder.net.ConnectionException; import de.dal33t.powerfolder.net.ConnectionHandler; import de.dal33t.powerfolder.net.ConnectionListener; import de.dal33t.powerfolder.net.DynDnsManager; import de.dal33t.powerfolder.net.HTTPProxySettings; import de.dal33t.powerfolder.net.IOProvider; import de.dal33t.powerfolder.net.NodeManager; import de.dal33t.powerfolder.net.ReconnectManager; import de.dal33t.powerfolder.plugin.PluginManager; import de.dal33t.powerfolder.security.SecurityManager; import de.dal33t.powerfolder.security.SecurityManagerClient; import de.dal33t.powerfolder.task.PersistentTaskManager; import de.dal33t.powerfolder.transfer.TransferManager; import de.dal33t.powerfolder.ui.UIController; import de.dal33t.powerfolder.ui.dialog.SyncFolderDialog; import de.dal33t.powerfolder.ui.dialog.UIUnLockDialog; import de.dal33t.powerfolder.ui.model.ApplicationModel; import de.dal33t.powerfolder.ui.notices.Notice; import de.dal33t.powerfolder.ui.util.LimitedConnectivityChecker; import de.dal33t.powerfolder.util.ByteSerializer; import de.dal33t.powerfolder.util.ConfigurationLoader; import de.dal33t.powerfolder.util.Debug; import de.dal33t.powerfolder.util.ForcedLanguageFileResourceBundle; import de.dal33t.powerfolder.util.Format; import de.dal33t.powerfolder.util.JavaVersion; import de.dal33t.powerfolder.util.LoginUtil; import de.dal33t.powerfolder.util.NamedThreadFactory; import de.dal33t.powerfolder.util.PathUtils; import de.dal33t.powerfolder.util.ProUtil; import de.dal33t.powerfolder.util.Profiling; import de.dal33t.powerfolder.util.PropertiesUtil; import de.dal33t.powerfolder.util.Reject; import de.dal33t.powerfolder.util.SplitConfig; import de.dal33t.powerfolder.util.StringUtils; import de.dal33t.powerfolder.util.Translation; import de.dal33t.powerfolder.util.Util; import de.dal33t.powerfolder.util.Waiter; import de.dal33t.powerfolder.util.WrappedScheduledThreadPoolExecutor; import de.dal33t.powerfolder.util.logging.LoggingManager; import de.dal33t.powerfolder.util.os.OSUtil; import de.dal33t.powerfolder.util.os.SystemUtil; import de.dal33t.powerfolder.util.os.Win32.FirewallUtil; import de.dal33t.powerfolder.util.os.Win32.WinUtils; import de.dal33t.powerfolder.util.os.mac.MacUtils; import de.dal33t.powerfolder.util.update.UpdateSetting; /** * Central class gives access to all core components in PowerFolder. Make sure * to extend PFComponent so you always have a reference to the main * {@link Controller}. * * @author Christian Sprajc * @version $Revision: 1.107 $ */ public class Controller extends PFComponent { private static final Logger log = Logger.getLogger(Controller.class.getName()); private static final int MAJOR_VERSION = 10; private static final int MINOR_VERSION = 0; private static final int REVISION_VERSION = 30; /** * Program version. */ public static final String PROGRAM_VERSION = MAJOR_VERSION + "." + MINOR_VERSION + "." + REVISION_VERSION; /** general wait time for all threads (5000 is a balanced value) */ private static final long WAIT_TIME = 5000; /** The command line entered by the user when starting the program */ private CommandLine commandLine; /** filename of the current configFile */ private String configFilename; /** * The actual config file. */ private Path configFile; private Path configFolderFile; /** The config properties */ private SplitConfig config; /** * The preferences */ private Preferences preferences; /** * The distribution running. */ private Distribution distribution; /** Program start time */ private Date startTime; /** Are we in started state? */ private volatile boolean started; /** Are we trying to shutdown? */ private volatile boolean shuttingDown; /** Is a restart requested */ private boolean restartRequested; /** Are we in verbose mode? */ private boolean verbose; /** Should we request debug reports? */ private boolean debugReports; /** * If running is paused mode */ private volatile boolean paused; /** * cache the networking mode in a field so we dont heve to do all this * comparing */ private NetworkingMode networkingMode; /** The nodemanager that holds all members */ private NodeManager nodeManager; /** * Responsible for (re-)connecting to other nodes. */ private ReconnectManager reconnectManager; /** The FolderRepository that holds all "joined" folders */ private FolderRepository folderRepository; /** The Listener to incomming connections of other PowerFolder clients */ private ConnectionListener connectionListener; /** The basic io provider */ private IOProvider ioProvider; /** * besides the default listener we may have a list of connection listeners * that listen on other ports */ private List<ConnectionListener> additionalConnectionListeners; private final List<InvitationHandler> invitationHandlers; private final List<MassDeletionHandler> massDeletionHandlers; /** The BroadcastManager send "broadcasts" on the LAN so we can */ private BroadcastMananger broadcastManager; /** * The DynDNS manager that handles the working arwound for user with a * dynnamip IP address. */ private DynDnsManager dyndnsManager; private PersistentTaskManager taskManager; private Callable<TransferManager> transferManagerFactory = new Callable<TransferManager>() { @Override public TransferManager call() throws Exception { return new TransferManager(Controller.this); } }; /** Handels the up and downloads */ private TransferManager transferManager; /** * Remote Commands listener, a protocol handler for powerfolder links: * powerfolder:// */ private RemoteCommandManager rconManager; /** Holds the User interface */ private UIController uiController; /** holds all installed plugins */ private PluginManager pluginManager; /** * The security manager, handles access etc. */ private SecurityManager securityManager; /** * The Online Storage client */ private ServerClient osClient; /** global Threadpool */ private ScheduledExecutorService threadPool; /** Remembers if a port on the local firewall was opened */ private boolean portWasOpened = false; /** * If we have limited connectivity */ private boolean limitedConnectivity; private final PausedModeListener pausedModeListenerSupport; private final NetworkingModeListener networkingModeListenerSupport; private final LimitedConnectivityListener limitedConnectivityListenerSupport; private ScheduledFuture<?> pauseResumeFuture; private Controller() { // Do some TTL fixing for dyndns resolving Security.setProperty("networkaddress.cache.ttl", "0"); Security.setProperty("networkaddress.cache.negative.ttl", "0"); System.setProperty("sun.net.inetaddr.ttl", "0"); System.setProperty("com.apple.mrj.application.apple.menu.about.name", "PowerFolder"); invitationHandlers = new CopyOnWriteArrayList<InvitationHandler>(); massDeletionHandlers = new CopyOnWriteArrayList<MassDeletionHandler>(); pausedModeListenerSupport = ListenerSupportFactory.createListenerSupport(PausedModeListener.class); networkingModeListenerSupport = ListenerSupportFactory.createListenerSupport(NetworkingModeListener.class); limitedConnectivityListenerSupport = ListenerSupportFactory .createListenerSupport(LimitedConnectivityListener.class); } /** * Overwite the PFComponent.getController() otherwise that one returns null * for this Controller itself. * * @return a reference to this */ @Override public Controller getController() { return this; } /** * Creates a fresh Controller. * * @return the controller */ public static Controller createController() { return new Controller(); } /** * Starts this controller loading the config from the default config file */ public void startDefaultConfig() { startConfig(Constants.DEFAULT_CONFIG_FILE); } /** * Starts a config with the given command line arguments * * @param aCommandLine * the command line as specified by the user */ public void startConfig(CommandLine aCommandLine) { commandLine = aCommandLine; String[] configNames = aCommandLine.getOptionValues("c"); String configName = configNames != null && configNames.length > 0 && StringUtils.isNotBlank(configNames[0]) ? configNames[0] : null; if (StringUtils.isNotBlank(configName) && (configName.startsWith("http:") || configName.startsWith("https:"))) { if (configNames.length > 1) { configName = configNames[1]; } else { configName = Constants.DEFAULT_CONFIG_FILE; } } startConfig(configName); } /** * Starts controller with a special config file, and creates and starts all * components of PowerFolder. * * @param filename * The filename to uses as config file (located in the * "getConfigLocationBase()") */ public void startConfig(String filename) { if (started) { throw new IllegalStateException("Configuration already started, shutdown controller first"); } additionalConnectionListeners = Collections.synchronizedList(new ArrayList<ConnectionListener>()); started = false; shuttingDown = false; threadPool = new WrappedScheduledThreadPoolExecutor(Constants.CONTROLLER_THREADS_IN_THREADPOOL, new NamedThreadFactory("Controller-Thread-")); // Initialize resource bundle eager // check forced language file from commandline if (commandLine != null && commandLine.hasOption("f")) { String langfilename = commandLine.getOptionValue("f"); try { ResourceBundle resourceBundle = new ForcedLanguageFileResourceBundle(langfilename); Translation.setResourceBundle(resourceBundle); logInfo("Loading language bundle from file " + langfilename); } catch (FileNotFoundException fnfe) { logSevere("forced language file (" + langfilename + ") not found: " + fnfe.getMessage()); logSevere("using setup language"); Translation.resetResourceBundle(); } catch (IOException ioe) { logSevere("forced language file io error: " + ioe.getMessage()); logSevere("using setup language"); Translation.resetResourceBundle(); } } else { Translation.resetResourceBundle(); } Translation.getResourceBundle(); // loadConfigFile if (!loadConfigFile(filename)) { return; } boolean isDefaultConfig = Constants.DEFAULT_CONFIG_FILE.startsWith(getConfigName()); if (isDefaultConfig) { // To keep compatible with previous versions preferences = Preferences.userNodeForPackage(PowerFolder.class); } else { preferences = Preferences.userNodeForPackage(PowerFolder.class).node(getConfigName()); } // initialize logger // Enabled verbose mode if in config. // This logs to file for analysis. verbose = ConfigurationEntry.VERBOSE.getValueBoolean(this); initLogger(); if (verbose) { ByteSerializer.BENCHMARK = true; scheduleAndRepeat(new Runnable() { @Override public void run() { ByteSerializer.printStats(); } }, 600000L, 600000L); Profiling.setEnabled(false); Profiling.reset(); } String arch = OSUtil.is64BitPlatform() ? "64bit" : "32bit"; logFine("OS: " + System.getProperty("os.name") + " (" + arch + ')'); logFine("Java: " + JavaVersion.systemVersion().toString() + " (" + System.getProperty("java.vendor") + ')'); logFine("Current time: " + new Date()); Runtime runtime = Runtime.getRuntime(); long maxMemory = runtime.maxMemory(); long totalMemory = runtime.totalMemory(); logFine("Max Memory: " + Format.formatBytesShort(maxMemory) + ", Total Memory: " + Format.formatBytesShort(totalMemory)); if (!Desktop.isDesktopSupported() && isUIEnabled()) { logWarning("Desktop utility not supported"); } // If we have a new config. clear the preferences. clearPreferencesOnConfigSwitch(); // Load and set http proxy settings HTTPProxySettings.loadFromConfig(this); // #2179: Load from server. How to handle timeouts? // Command line option -c http://are.de ConfigurationLoader.loadAndMergeCLI(this); // Config entry in file ConfigurationLoader.loadAndMergeConfigURL(this); // Read from installer temp file ConfigurationLoader.loadAndMergeFromInstaller(this); if (verbose != ConfigurationEntry.VERBOSE.getValueBoolean(this)) { verbose = ConfigurationEntry.VERBOSE.getValueBoolean(this); initLogger(); } // Init paused only if user expects pause to be permanent or // "while I work" int pauseSecs = ConfigurationEntry.PAUSE_RESUME_SECONDS.getValueInt(getController()); paused = PreferencesEntry.PAUSED.getValueBoolean(this) && (pauseSecs == Integer.MAX_VALUE || pauseSecs == 0); // Now set it, just in case it was paused in permanent mode. PreferencesEntry.PAUSED.setValue(this, paused); // Load and set http proxy settings again. HTTPProxySettings.loadFromConfig(this); // Initialize branding/preconfiguration of the client initDistribution(); logFine("Build time: " + getBuildTime()); logInfo("Program version " + PROGRAM_VERSION); if (getDistribution().getBinaryName().toLowerCase().contains("powerfolder")) { Debug.writeSystemProperties(); } if (ConfigurationEntry.KILL_RUNNING_INSTANCE.getValueBoolean(this)) { killRunningInstance(); } FolderList.removeMemberFiles(this); // Initialize plugins setupProPlugins(); pluginManager = new PluginManager(this); pluginManager.init(); // create node manager nodeManager = new NodeManager(this); // Only one task brother left... taskManager = new PersistentTaskManager(this); // Folder repository folderRepository = new FolderRepository(this); setLoadingCompletion(0, 10); // Create transfer manager // If this is a unit test it might have been set before. try { transferManager = transferManagerFactory.call(); } catch (Exception e) { logSevere("Exception", e); } reconnectManager = new ReconnectManager(this); // Create os client osClient = new ServerClient(this); if (isUIEnabled()) { uiController = new UIController(this); if (ConfigurationEntry.USER_INTERFACE_LOCKED.getValueBoolean(this)) { // Don't let the user pass this step. new UIUnLockDialog(this).openAndWait(); } } setLoadingCompletion(10, 20); // The io provider. ioProvider = new IOProvider(this); ioProvider.start(); // Set hostname by CLI if (commandLine != null && commandLine.hasOption('d')) { String host = commandLine.getOptionValue("d"); if (StringUtils.isNotBlank(host)) { InetSocketAddress addr = Util.parseConnectionString(host); if (addr != null) { ConfigurationEntry.HOSTNAME.setValue(this, addr.getHostName()); ConfigurationEntry.NET_BIND_PORT.setValue(this, addr.getPort()); } } } // initialize dyndns manager dyndnsManager = new DynDnsManager(this); setLoadingCompletion(20, 30); // initialize listener on local port if (!initializeListenerOnLocalPort()) { return; } if (!isUIEnabled()) { // Disable paused function paused = false; } setLoadingCompletion(30, 35); // Start the nodemanager nodeManager.init(); if (!ProUtil.isRunningProVersion()) { // Nodemanager gets later (re) started by ProLoader. nodeManager.start(); } setLoadingCompletion(35, 60); securityManager = new SecurityManagerClient(this, osClient); // init repo (read folders) folderRepository.init(); logInfo("Dataitems: " + Debug.countDataitems(Controller.this)); // init of folders takes rather long so a big difference with // last number to get smooth bar... ;-) setLoadingCompletion(60, 65); // start repo maintainance Thread folderRepository.start(); setLoadingCompletion(65, 70); // Start the transfer manager thread transferManager.start(); setLoadingCompletion(70, 75); // Initalize rcon manager startRConManager(); setLoadingCompletion(75, 80); // Start all configured listener if not in paused mode startConfiguredListener(); setLoadingCompletion(80, 85); // open broadcast listener openBroadcastManager(); setLoadingCompletion(85, 90); // Controller now started started = true; startTime = new Date(); // Now taskmanager taskManager.start(); logInfo("Controller started"); // dyndns updater /* * boolean onStartUpdate = ConfigurationEntry.DYNDNS_AUTO_UPDATE * .getValueBoolean(this).booleanValue(); if (onStartUpdate) { * getDynDnsManager().onStartUpdate(); } */ dyndnsManager.updateIfNessesary(); setLoadingCompletion(90, 100); // Login to OS if (Feature.OS_CLIENT.isEnabled()) { try { osClient.loginWithLastKnown(); } catch (Exception e) { logWarning("Unable to login with last known username. " + e); logFiner(e); } } // Start Plugins pluginManager.start(); // open UI if (isConsoleMode()) { logFine("Running in console"); } else { logFine("Opening UI"); openUI(); } // Load anything that was not handled last time. loadPersistentObjects(); setLoadingCompletion(100, 100); if (!isConsoleMode()) { uiController.hideSplash(); } if (ConfigurationEntry.AUTO_CONNECT.getValueBoolean(this)) { // Now start the connecting process reconnectManager.start(); } else { logFine("Not starting reconnection process. " + "Config auto.connect set to false"); } // Start connecting to OS client. if (Feature.OS_CLIENT.isEnabled() && ConfigurationEntry.SERVER_CONNECT.getValueBoolean(this)) { osClient.start(); } else { logInfo("Not connecting to server (" + osClient.getServerString() + "): Disabled"); } // Setup our background working tasks setupPeriodicalTasks(); if (MacUtils.isSupported()) { if (isFirstStart()) { MacUtils.getInstance().setPFStartup(true, this); } MacUtils.getInstance().setAppReOpenedListener(this); } if (pauseSecs == 0) { // Activate adaptive logic setPaused(paused); } } private void clearPreferencesOnConfigSwitch() { String lastNodeIdObf = PreferencesEntry.LAST_NODE_ID.getValueString(this); String thisNodeId = ConfigurationEntry.NODE_ID.getValue(this); try { if (StringUtils.isNotBlank(lastNodeIdObf) && !LoginUtil.matches(Util.toCharArray(thisNodeId), lastNodeIdObf)) { int i = 0; for (String key : preferences.keys()) { preferences.remove(key); i++; } logWarning("Cleared " + i + " preferences, new config/nodeid found"); } } catch (BackingStoreException e1) { logWarning("Unable to clear preferences. " + e1); } } /** * For each folder, kick off scan. */ public void performFullSync() { // Let other nodes scan now! folderRepository.broadcastScanCommandOnAllFolders(); // Force scan on all folders, of repository was selected Collection<Folder> folders = folderRepository.getFolders(); for (Folder folder : folders) { // Never sync preview folders if (folder != null && !folder.isPreviewOnly()) { // Ask for more sync options on that folder if on project sync if (Util.isAwtAvailable() && folder.getSyncProfile().equals(SyncProfile.MANUAL_SYNCHRONIZATION)) { new SyncFolderDialog(this, folder).open(); } else { // Recommend scan on this folder folder.recommendScanOnNextMaintenance(); } } } setPaused(false); // Now trigger the scan folderRepository.triggerMaintenance(); // Trigger file requesting folderRepository.getFileRequestor().triggerFileRequesting(); // Fresh reconnection try! reconnectManager.buildReconnectionQueue(); } /** * Add invitation listener. * * @param l */ public void addInvitationHandler(InvitationHandler l) { invitationHandlers.add(l); } /** * Remove invitation listener. * * @param l */ public void removeInvitationHandler(InvitationHandler l) { invitationHandlers.remove(l); } /** * Add mass delete listener. * * @param l */ public void addMassDeletionHandler(MassDeletionHandler l) { massDeletionHandlers.add(l); } /** * Remove mass delete listener. * * @param l */ public void removeMassDeletionHandler(MassDeletionHandler l) { massDeletionHandlers.remove(l); } private void setupProPlugins() { String pluginConfig = ConfigurationEntry.PLUGINS.getValue(this); boolean autoSetupPlugins = StringUtils.isEmpty(pluginConfig) || !pluginConfig.contains(Constants.PRO_LOADER_PLUGIN_CLASS); if (ProUtil.isRunningProVersion() && autoSetupPlugins) { logFine("Setting up pro loader"); String newPluginConfig = Constants.PRO_LOADER_PLUGIN_CLASS; if (!StringUtils.isBlank(pluginConfig)) { newPluginConfig += ',' + pluginConfig; } ConfigurationEntry.PLUGINS.setValue(this, newPluginConfig); } } private void initLogger() { // Set a nice prefix for file looging file names. String configName = getConfigName(); if (configName != null) { LoggingManager.setPrefix(configName); } if (verbose) { String str = ConfigurationEntry.LOG_LEVEL_CONSOLE.getValue(this); Level consoleLevel = LoggingManager.levelForName(str); LoggingManager.setConsoleLogging(consoleLevel != null ? consoleLevel : Level.WARNING); // Enable file logging str = ConfigurationEntry.LOG_LEVEL_FILE.getValue(this); boolean rotate = ConfigurationEntry.LOG_FILE_ROTATE.getValueBoolean(this); Level fileLevel = LoggingManager.levelForName(str); LoggingManager.setFileLogging(fileLevel != null ? fileLevel : Level.FINE, rotate); // Switch on the document handler. if (isUIEnabled()) { str = PreferencesEntry.DOCUMENT_LOGGING.getValueString(this); Level uiLogLevel = LoggingManager.levelForName(str); LoggingManager.setDocumentLogging(uiLogLevel != null ? uiLogLevel : Level.WARNING, this); } if (LoggingManager.isLogToFile()) { logFine("Logging to file '" + LoggingManager.getLoggingFileName() + '\''); } else { logInfo("No logging to file"); } str = ConfigurationEntry.LOG_SYSLOG_HOST.getValue(this); if (str != null) { str = ConfigurationEntry.LOG_SYSLOG_LEVEL.getValue(this); Level syslogLevel = LoggingManager.levelForName(str); LoggingManager.setSyslogLogging(syslogLevel != null ? syslogLevel : Level.WARNING, this); } } if (commandLine != null && commandLine.hasOption('l')) { String str = commandLine.getOptionValue('l'); Level consoleLevel = LoggingManager.levelForName(str); if (consoleLevel != null) { LoggingManager.setConsoleLogging(consoleLevel); } } // Enable debug reports. debugReports = ConfigurationEntry.DEBUG_REPORTS.getValueBoolean(this); LoggingManager.clearBuffer(); int maxDays = ConfigurationEntry.LOG_FILE_DELETE_DAYS.getValueInt(getController()); if (maxDays >= 0) { LoggingManager.removeOldLogs(maxDays); } } /** * Loads a config file (located in "getConfigLocationBase()") * * @param theFilename * @return false if unsuccessful, true if file found and reading succeeded. */ private boolean loadConfigFile(String theFilename) { String filename = theFilename; if (filename == null) { filename = Constants.DEFAULT_CONFIG_FILE; } if (filename.indexOf('.') < 0) { // append .config extension filename += ".config"; } configFilename = null; config = new SplitConfig(); configFilename = filename; configFile = getConfigLocationBase(); if (configFile == null) { configFile = Paths.get(filename).toAbsolutePath(); } else { configFile = configFile.resolve(filename); } BufferedInputStream bis = null; try { if (Files.exists(configFile)) { logInfo("Loading configfile " + configFile.toString()); } else { logFine("Config file does not exist. " + configFile.toString()); throw new FileNotFoundException(); } if (OSUtil.isWebStart()) { logFine("WebStart, config file location: " + configFile.toString()); } bis = new BufferedInputStream(Files.newInputStream(configFile)); config.load(bis); } catch (FileNotFoundException e) { logWarning("Unable to start config, file '" + filename + "' not found, using defaults"); } catch (IOException e) { logSevere("Unable to start config from file '" + filename + '\''); config = null; return false; } finally { try { if (bis != null) { bis.close(); } } catch (Exception e) { // Ignore. } } String folderfilename = filename.replace(".config", "-Folder.config"); configFolderFile = getConfigLocationBase(); if (configFolderFile == null) { configFolderFile = Paths.get(folderfilename).toAbsolutePath(); } else { configFolderFile = configFolderFile.resolve(folderfilename); } if (Files.exists(configFolderFile)) { try { logInfo("Loading folder configfile " + configFolderFile.toString()); bis = new BufferedInputStream(Files.newInputStream(configFolderFile)); config.load(bis); } catch (FileNotFoundException e) { logWarning("Unable to start config, file '" + folderfilename + "' not found, using defaults"); } catch (IOException e) { logSevere("Unable to start config from file '" + folderfilename + '\''); configFolderFile = null; return false; } finally { try { if (bis != null) { bis.close(); } } catch (Exception e) { // Ignore. } } } else { logFine("Folder config file does not exist. " + configFolderFile.toString()); } return true; } /** * Use to schedule a lightweight short running task that gets repeated * periodically. * * @param task * the task to schedule * @param period * the time in ms between executions */ public ScheduledFuture<?> scheduleAndRepeat(Runnable task, long period) { if (!shuttingDown) { return threadPool.scheduleWithFixedDelay(task, 0, period, TimeUnit.MILLISECONDS); } return null; } /** * Use to schedule a lightweight short running task that gets repeated * periodically. * * @param task * the task to schedule * @param initialDelay * the initial delay in ms * @param period * the time in ms between executions * @return */ public ScheduledFuture<?> scheduleAndRepeat(Runnable task, long initialDelay, long period) { if (!shuttingDown) { return threadPool.scheduleWithFixedDelay(task, initialDelay, period, TimeUnit.MILLISECONDS); } return null; } /** * Use to schedule a lightweight short running task. * * @param task * the task to schedule * @param delay * the initial delay in ms */ public ScheduledFuture<?> schedule(Runnable task, long delay) { if (!shuttingDown) { return threadPool.schedule(task, delay, TimeUnit.MILLISECONDS); } return null; } /** * Removes a schduled task for the threadpool * * @param task */ public void removeScheduled(Runnable task) { if (!shuttingDown) { if (threadPool instanceof ScheduledThreadPoolExecutor) { ((ScheduledThreadPoolExecutor) threadPool).remove(task); ((ScheduledThreadPoolExecutor) threadPool).purge(); } else { logSevere("Unable to remove scheduled task. Wrong threadpool. " + task); } } } /** * Removes a scheduled task for the threadpool * * @param future */ public boolean removeScheduled(ScheduledFuture<?> future) { if (!shuttingDown) { if (threadPool instanceof ScheduledThreadPoolExecutor) { return ((ScheduledThreadPoolExecutor) threadPool).remove((Runnable) future); } else { logSevere("Unable to remove scheduled task. Wrong threadpool. " + future); } } return false; } /** * Sets up the task, which should be executes periodically. */ private void setupPeriodicalTasks() { // ============ // Test the connectivity after a while. // ============ LimitedConnectivityChecker.install(this); // ============ // Schedule a task to do housekeeping every day, just after midnight. // ============ Calendar cal = new GregorianCalendar(); long now = cal.getTime().getTime(); // Midnight tomorrow morning. cal.set(Calendar.MILLISECOND, 0); cal.set(Calendar.SECOND, 0); cal.set(Calendar.MINUTE, 0); cal.set(Calendar.HOUR_OF_DAY, 0); cal.add(Calendar.DATE, 1); // Add a few seconds to be sure the file name definately is for // tomorrow. cal.add(Calendar.SECOND, 2); long midnight = cal.getTime().getTime(); // How long to wait initially? long secondsToMidnight = (midnight - now) / 1000; logFine("Initial log reconfigure in " + secondsToMidnight + " seconds"); threadPool.scheduleAtFixedRate(new TimerTask() { @Override public void run() { performHousekeeping(true); } }, secondsToMidnight, 60 * 60 * 24, TimeUnit.SECONDS); // Also run housekeeping one minute after start up. threadPool.schedule(new TimerTask() { @Override public void run() { performHousekeeping(false); } }, 1, TimeUnit.MINUTES); // ============ // Do profiling // ============ if (Profiling.ENABLED) { threadPool.scheduleWithFixedDelay(new TimerTask() { @Override public void run() { logFine(Profiling.dumpStats()); } }, 0, 1, TimeUnit.MINUTES); } // ============ // Monitor the default directory for possible new folders. // ============ threadPool.scheduleAtFixedRate(new TimerTask() { @Override public void run() { if (ConfigurationEntry.LOOK_FOR_FOLDER_CANDIDATES.getValueBoolean(Controller.this)) { folderRepository.lookForNewFolders(); } if (ConfigurationEntry.LOOK_FOR_FOLDERS_TO_BE_REMOVED.getValueBoolean(Controller.this)) { folderRepository.lookForFoldersToBeRemoved(); } } }, 10L, 10L, TimeUnit.SECONDS); // ============ // Hourly tasks // ============ // @todo what's this for? comment? boolean alreadyDetected = ConfigurationEntry.TRANSFER_LIMIT_AUTODETECT.getValueBoolean(this) && ConfigurationEntry.UPLOAD_LIMIT_WAN.getValueInt(this) > 0; // If already detected wait 10 mins before next test. Otherwise start // instantly. long initialDelay = alreadyDetected ? 600 : 5; threadPool.scheduleAtFixedRate(new TimerTask() { @Override public void run() { performHourly(); } }, initialDelay, 3600, TimeUnit.SECONDS); // ========= // Profiling // ========= // final Collector cpu = CollectorFactory.getFactory().createCollector( // CollectorID.CPU_USAGE.id); threadPool.scheduleAtFixedRate(new Runnable() { @Override public void run() { if (!verbose) { return; } // if (cpu == null || cpu.getMaxValue() == 0) { // return; // } // double cpuUsage = cpu.getValue() * 100 / cpu.getMaxValue(); // logWarning("" + cpuUsage + "% " + cpu.getValue() + " / " + // cpu.getMaxValue()); // if (cpuUsage > 1) { if (isFine()) { logFine("Dataitems: " + Debug.countDataitems(Controller.this)); } String dump = Debug.dumpCurrentStacktraces(false); if (StringUtils.isNotBlank(dump) && isFine() && ConfigurationEntry.LOG_ACTIVE_THREADS.getValueBoolean(getController())) { logFine("Active threads:\n\n" + dump); } else { logFine("No active threads"); } // } } }, 1, 5, TimeUnit.MINUTES); } /** * These tasks get performed every hour. */ private void performHourly() { if (ConfigurationEntry.TRANSFER_LIMIT_AUTODETECT.getValueBoolean(this)) { FutureTask<Object> recalculateRunnable = transferManager.getRecalculateAutomaticRate(); threadPool.execute(recalculateRunnable); } } private void openBroadcastManager() { if (ConfigurationEntry.NET_BROADCAST.getValueBoolean(this)) { try { broadcastManager = new BroadcastMananger(this); broadcastManager.start(); } catch (ConnectionException e) { logSevere("Unable to open broadcast manager, you wont automatically connect to clients on LAN: " + e.getMessage()); logSevere("ConnectionException", e); } } else { logInfo("Auto client discovery in LAN via broadcast disabled"); } } /** * General houskeeping task. Runs one minute after start and every midnight. * * @param midnightRun * true if this is the midnight invokation, false if this is at * start up. */ private void performHousekeeping(boolean midnightRun) { log.fine("Performing housekeeping " + midnightRun); Date now = new Date(); if (midnightRun) { // Reconfigure log file after midnight. logFine("Reconfiguring logs for new day: " + now); initLogger(); LoggingManager.resetFileLogging(); int days = ConfigurationEntry.LOG_FILE_DELETE_DAYS.getValueInt(getController()); if (days >= 0) { LoggingManager.removeOldLogs(days); } logFine("Reconfigured logs for new day: " + now); backupConfigAssets(); } // Prune stats. transferManager.pruneStats(); // Cleanup old archives. if (midnightRun) { folderRepository.cleanupOldArchiveFiles(); } } /** * #2526 */ private void backupConfigAssets() { Path backupDir = getMiscFilesLocation().resolve("backups/" + Format.formatDateCanonical(new Date())); if (Files.notExists(backupDir)) { try { Files.createDirectories(backupDir); } catch (IOException ioe) { logInfo("Could not create directory '" + backupDir.toAbsolutePath().toString() + "'"); } } Path configBackup = backupDir.resolve(configFile.getFileName()); try { PathUtils.copyFile(configFile, configBackup); } catch (IOException e) { logWarning("Unable to backup file " + configFile + ". " + e); } if (Files.exists(configFolderFile)) { Path configFolderBackup = backupDir.resolve(configFolderFile.getFileName()); try { PathUtils.copyFile(configFolderFile, configFolderBackup); } catch (IOException e) { logWarning("Unable to backup file " + configFolderFile + ". " + e); } } Path myKeyFile = getMiscFilesLocation().resolve(getConfigName() + ".mykey"); Path mykeyBackup = backupDir.resolve(myKeyFile.getFileName()); if (Files.exists(myKeyFile)) { try { PathUtils.copyFile(myKeyFile, mykeyBackup); } catch (IOException e) { logWarning("Unable to backup file " + myKeyFile + ". " + e); } } Path dbFile = getMiscFilesLocation().resolve("Accounts.h2.db"); Path dbBackup = backupDir.resolve(dbFile.getFileName()); if (Files.exists(dbFile)) { try { PathUtils.copyFile(dbFile, dbBackup); } catch (IOException e) { logWarning("Unable to backup file " + dbFile + ". " + e); } } } /** * Starts the rcon manager */ private void startRConManager() { if (RemoteCommandManager.hasRunningInstance()) { alreadyRunningCheck(); } if (!ConfigurationEntry.NET_RCON_MANAGER.getValueBoolean(this)) { logWarning("Not starting RemoteCommandManager"); return; } rconManager = new RemoteCommandManager(this); rconManager.start(); } /** * Starts a connection listener for each port found in config property * "port" ("," separeted), if "random-port" is set to "true" this "port" * entry will be ignored and a random port will be used (between 49152 and * 65535). */ private boolean initializeListenerOnLocalPort() { if (ConfigurationEntry.NET_BIND_RANDOM_PORT.getValueBoolean(this)) { bindRandomPort(); } else { String ports = ConfigurationEntry.NET_BIND_PORT.getValue(this); if ("0".equals(ports)) { logWarning("Not opening connection listener. (port=0)"); } else { if (ports == null) { ports = String.valueOf(ConnectionListener.DEFAULT_PORT); } StringTokenizer nizer = new StringTokenizer(ports, ","); while (nizer.hasMoreElements()) { String portStr = nizer.nextToken(); try { int port = Integer.parseInt(portStr); boolean listenerOpened = openListener(port); if (listenerOpened && connectionListener != null) { // set reconnect on first successfull listener nodeManager.getMySelf().getInfo().setConnectAddress(connectionListener.getAddress()); } if (!listenerOpened && !isUIOpen()) { logSevere("Couldn't bind to port " + port); // exit(1); // fatalStartError(Translation // .getTranslation("dialog.bind_error")); // return false; // Shouldn't reach this! } } catch (NumberFormatException e) { logFine("Unable to read listener port ('" + portStr + "') from config"); } } // If this is the GUI version we didn't kill the program yet, // even though // there might have been multiple failed tries. if (connectionListener == null) { portBindFailureProblem(ports); } } } if (ConfigurationEntry.NET_FIREWALL_OPENPORT.getValueBoolean(this)) { if (FirewallUtil.isFirewallAccessible() && connectionListener != null) { Thread opener = new Thread(new Runnable() { @Override public void run() { try { logFine("Opening port on Firewall."); FirewallUtil.openport(connectionListener.getPort()); portWasOpened = true; } catch (IOException e) { logInfo("Unable to open port " + connectionListener.getPort() + "/TCP in Windows Firewall. " + e); } } }, "Portopener"); opener.start(); try { opener.join(12000); } catch (InterruptedException e) { logSevere("Opening of ports failed: " + e); } } } return true; } /** * Call to notify the Controller of a problem while binding a required * listening socket. * * @param ports */ private void portBindFailureProblem(String ports) { if (!isUIEnabled()) { logSevere("Unable to open incoming port from the portlist: " + ports); exit(1); return; } // Must use JOptionPane here because there is no Controller yet for // DialogFactory! int response = JOptionPane.showOptionDialog(null, Translation.getTranslation("dialog.bind_error.option.text"), Translation.getTranslation("dialog.bind_error.option.title"), JOptionPane.YES_NO_OPTION, JOptionPane.WARNING_MESSAGE, null, new String[] { Translation.getTranslation("dialog.bind_error.option.ignore"), Translation.getTranslation("dialog.bind_error.option.exit") }, 0); switch (response) { case 1: exit(0); break; default: bindRandomPort(); break; } } /** * Tries to bind a random port */ private void bindRandomPort() { if ((openListener(ConnectionListener.DEFAULT_PORT) || openListener(0)) && connectionListener != null) { nodeManager.getMySelf().getInfo().setConnectAddress(connectionListener.getAddress()); } else { logSevere("failed to open random port!!!"); fatalStartError(Translation.getTranslation("dialog.bind_error")); } } /** * Starts all configures connection listener */ private void startConfiguredListener() { // Start the connection listener if (connectionListener != null) { try { connectionListener.start(); } catch (ConnectionException e) { logSevere("Problems starting listener " + connectionListener, e); } for (ConnectionListener additionalConnectionListener : additionalConnectionListeners) { try { additionalConnectionListener.start(); } catch (ConnectionException e) { logSevere("Problems starting listener " + connectionListener, e); } } } } /** * Saves the current config to disk */ public synchronized void saveConfig() { if (!started) { return; } logFine("Saving config (" + getConfigName() + ".config)"); Path file; Path tempFile; Path folderFile; Path tempFolderFile; Path backupFile; if (getConfigLocationBase() == null) { file = Paths.get(getConfigName() + ".config").toAbsolutePath(); tempFile = Paths.get(getConfigName() + ".writing.config").toAbsolutePath(); folderFile = Paths.get(getConfigName() + "-Folder.config").toAbsolutePath(); tempFolderFile = Paths.get(getConfigName() + "-Folder.writing.config").toAbsolutePath(); backupFile = Paths.get(getConfigName() + ".config.backup").toAbsolutePath(); } else { file = getConfigLocationBase().resolve(getConfigName() + ".config"); tempFile = getConfigLocationBase().resolve(getConfigName() + ".writing.config").toAbsolutePath(); backupFile = getConfigLocationBase().resolve(getConfigName() + ".config.backup"); folderFile = getConfigLocationBase().resolve(getConfigName() + "-Folder.config"); tempFolderFile = getConfigLocationBase().resolve(getConfigName() + "-Folder.writing.config") .toAbsolutePath(); } try { // Backup is done in #backupConfigAssets Files.deleteIfExists(backupFile); String distName = "PowerFolder"; if (distribution != null && StringUtils.isNotBlank(distribution.getName())) { distName = distribution.getName(); } Properties prev = new Properties(); if (Files.exists(file)) { try (BufferedInputStream in = new BufferedInputStream(Files.newInputStream(file))) { prev.load(in); } } if (!prev.equals(config.getRegular())) { // Store config in misc base PropertiesUtil.saveConfig(tempFile, config.getRegular(), distName + " config file (v" + PROGRAM_VERSION + ')'); Files.deleteIfExists(file); try { Files.move(tempFile, file); } catch (IOException e) { Files.copy(tempFile, file); Files.delete(tempFile); } } else { if (isFine()) { logFine("Not storing config to " + file + ". Base config remains unchanged"); } } if (!config.getFolders().isEmpty()) { Properties prevFolders = new Properties(); if (Files.exists(folderFile)) { try (BufferedInputStream in = new BufferedInputStream(Files.newInputStream(folderFile))) { prevFolders.load(in); } } if (!prevFolders.equals(config.getFolders())) { PropertiesUtil.saveConfig(tempFolderFile, config.getFolders(), distName + " folders config file (v" + PROGRAM_VERSION + ')'); Files.deleteIfExists(folderFile); try { Files.move(tempFolderFile, folderFile); } catch (IOException e) { Files.copy(tempFolderFile, folderFile); Files.delete(tempFolderFile); } } } } catch (IOException e) { // FATAL logSevere("Unable to save config. " + e, e); exit(1); } catch (Exception e) { // major problem , setting code is wrong e.printStackTrace(); logSevere("major problem , setting code is wrong", e); } } /** * Answers if controller is started (by config) * * @return true if controller is started (by config) */ public boolean isStarted() { return started; } /** * @return true is shutdown still in progress */ public boolean isShuttingDown() { return shuttingDown; } /** * the uptime in milliseconds. * * @return The uptime time in millis, or -1 if not started yet */ public long getUptime() { if (startTime == null) { return -1; } return System.currentTimeMillis() - startTime.getTime(); } /** * @return Name of the JAR file on windows installations. */ public String getJARName() { if (distribution != null && distribution.getBinaryName() != null) { return distribution.getBinaryName() + ".jar"; } logSevere("Unable to get JAR name for distribution: " + distribution, new RuntimeException()); return "PowerFolder.jar"; } /** * @return Name of the L4J INI file on windows installations. */ public String getL4JININame() { if (distribution != null && distribution.getBinaryName() != null) { return distribution.getBinaryName() + ".l4j.ini"; } logSevere("Unable to get l4j.ini name for distribution: " + distribution); return "PowerFolder.l4j.ini"; } /** * Sets the paused mode. * * @param newPausedValue */ public void setPaused(boolean newPausedValue) { setPaused0(newPausedValue, false); } /** * Sets the paused mode. * * @param newPausedValue */ private synchronized void setPaused0(boolean newPausedValue, boolean changedByAdaptiveLogic) { boolean oldPausedValue = paused; paused = newPausedValue; if (newPausedValue) { folderRepository.getFolderScanner().abortScan(); transferManager.abortAllDownloads(); transferManager.abortAllUploads(); } else { folderRepository.triggerMaintenance(); folderRepository.getFileRequestor().triggerFileRequesting(); for (Folder folder : folderRepository.getFolders()) { folder.broadcastFileRequestCommand(); } } if (oldPausedValue != newPausedValue) { transferManager.updateSpeedLimits(); } PreferencesEntry.PAUSED.setValue(this, newPausedValue); pausedModeListenerSupport.setPausedMode(new PausedModeEvent(newPausedValue)); if (pauseResumeFuture != null) { try { pauseResumeFuture.cancel(true); if (!removeScheduled(pauseResumeFuture)) { logSevere("Unable to remove pause task: " + pauseResumeFuture, new RuntimeException("Unable to remove pause task: " + pauseResumeFuture)); } logFine("Cancelled resume task"); } catch (Exception e) { e.printStackTrace(); logSevere(e); } } int delay = ConfigurationEntry.PAUSE_RESUME_SECONDS.getValueInt(this); if (newPausedValue) { if (delay == 0) { // User adaptive. Check for user inactivity pauseResumeFuture = scheduleAndRepeat(new PauseResumeTask(true), 1000); } else if (delay < Integer.MAX_VALUE) { pauseResumeFuture = schedule(new PauseResumeTask(false), delay * 1000); logFine("Scheduled resume task in " + delay + " seconds."); } } else { if (delay == 0 && changedByAdaptiveLogic) { // Turn on pause again when user gets active pauseResumeFuture = scheduleAndRepeat(new PauseResumeTask(true), 1000); } else { pauseResumeFuture = null; } } } /** * @return true if the controller is paused. */ public boolean isPaused() { return paused; } /** * Answers if node is running in LAN only networking mode * * @return true if in LAN only mode else false */ public boolean isLanOnly() { return getNetworkingMode() == NetworkingMode.LANONLYMODE; } /** * If this client is running in backup only mode. * <p> * Backup only client feature. Controls: * <p> * 1) If the client can send invitations * <p> * 2) If the client can add friends * <p> * 3) The client can connect to others except the server. * * @return true if running as backup only client. */ public boolean isBackupOnly() { return false; } /** * returns the enum with the current networkin mode. * * @return The Networking mode either NetworkingMode.PUBLICMODE, * NetworkingMode.PRIVATEMODE or NetworkingMode.LANONLYMODE */ public NetworkingMode getNetworkingMode() { if (networkingMode == null) { if (isBackupOnly()) { // ALWAYS server only mode. networkingMode = NetworkingMode.SERVERONLYMODE; return networkingMode; } // default = private String value = ConfigurationEntry.NETWORKING_MODE.getValue(this); try { networkingMode = NetworkingMode.valueOf(value); } catch (Exception e) { logSevere("Unable to read networking mode, reverting to PRIVATE_ONLY_MODE: " + e.toString(), e); networkingMode = NetworkingMode.PRIVATEMODE; } } return networkingMode; } public void addPausedModeListener(PausedModeListener listener) { ListenerSupportFactory.addListener(pausedModeListenerSupport, listener); } public void removePausedModeListener(PausedModeListener listener) { ListenerSupportFactory.removeListener(pausedModeListenerSupport, listener); } public void addNetworkingModeListener(NetworkingModeListener listener) { ListenerSupportFactory.addListener(networkingModeListenerSupport, listener); } public void removeNetworkingModeListener(NetworkingModeListener listener) { ListenerSupportFactory.removeListener(networkingModeListenerSupport, listener); } public void addLimitedConnectivityListener(LimitedConnectivityListener listener) { ListenerSupportFactory.addListener(limitedConnectivityListenerSupport, listener); } public void removeLimitedConnectivityListener(LimitedConnectivityListener listener) { ListenerSupportFactory.removeListener(limitedConnectivityListenerSupport, listener); } public void setNetworkingMode(NetworkingMode newMode) { if (isBackupOnly() && newMode != NetworkingMode.SERVERONLYMODE) { // ALWAYS server only mode if backup-only. newMode = NetworkingMode.SERVERONLYMODE; logWarning("Backup only client. Only supports server only networking mode"); } logFine("setNetworkingMode: " + newMode); NetworkingMode oldMode = getNetworkingMode(); if (newMode != oldMode) { ConfigurationEntry.NETWORKING_MODE.setValue(this, newMode.name()); networkingMode = newMode; networkingModeListenerSupport.setNetworkingMode(new NetworkingModeEvent(oldMode, newMode)); // Restart nodeManager nodeManager.shutdown(); nodeManager.start(); reconnectManager.buildReconnectionQueue(); } } /** * Answers if this controller has restricted connection to the network * * @return true if no incoming connections, else false. */ public boolean isLimitedConnectivity() { return limitedConnectivity; } public void setLimitedConnectivity(boolean limitedConnectivity) { boolean oldValue = this.limitedConnectivity; this.limitedConnectivity = limitedConnectivity; LimitedConnectivityEvent e = new LimitedConnectivityEvent(oldValue, limitedConnectivity); limitedConnectivityListenerSupport.setLimitedConnectivity(e); } /** * Shuts down controller and exits to system with the given status * * @param status * the status to exit with. */ public void exit(int status) { if (Feature.EXIT_ON_SHUTDOWN.isDisabled()) { System.err.println("Running in JUnit testmode, no system.exit() called"); return; } shutdown(); System.exit(status); } /** * Shuts down the controller and requests and moves into restart requested * state */ public void shutdownAndRequestRestart() { restartRequested = true; shutdown(); } /** * @return true if the controller was shut down, with the request to restart */ public boolean isRestartRequested() { return restartRequested; } /** * Shuts down all activities of this controller */ public synchronized void shutdown() { if (shuttingDown || !started) { return; } shuttingDown = true; logInfo("Shutting down..."); setFirstStart(false); // if (started && !OSUtil.isSystemService()) { // // Save config need a started in that method so do that first // saveConfig(); // } if (Profiling.isEnabled()) { logFine(Profiling.dumpStats()); } // Save anything important that has not been handled. savePersistentObjects(); // stop boolean wasStarted = started; started = false; startTime = null; PreferencesEntry.LAST_NODE_ID.setValue(this, LoginUtil.hashAndSalt(getMySelf().getId())); if (taskManager != null) { logFine("Shutting down task manager"); taskManager.shutdown(); } if (threadPool != null) { logFine("Shutting down global threadpool"); threadPool.shutdownNow(); } if (isUIOpen()) { logFine("Shutting down UI"); uiController.shutdown(); } if ((portWasOpened || ConfigurationEntry.NET_FIREWALL_OPENPORT.getValueBoolean(this)) && connectionListener != null) { if (FirewallUtil.isFirewallAccessible()) { Thread closer = new Thread(new Runnable() { @Override public void run() { try { logFine("Closing port on Firewall."); FirewallUtil.closeport(connectionListener.getPort()); } catch (IOException e) { logFine("Unable to remove firewall rule in Windows Firewall. " + e); } } }, "Firewallcloser"); closer.start(); try { closer.join(12000); } catch (InterruptedException e) { logFine("Closing of listener port failed: " + e); } } } if (rconManager != null) { logFine("Shutting down RConManager"); rconManager.shutdown(); } logFine("Shutting down connection listener(s)"); if (connectionListener != null) { connectionListener.shutdown(); } for (ConnectionListener addListener : additionalConnectionListeners) { addListener.shutdown(); } additionalConnectionListeners.clear(); if (broadcastManager != null) { logFine("Shutting down broadcast manager"); broadcastManager.shutdown(); } if (transferManager != null) { logFine("Shutting down transfer manager"); transferManager.shutdown(); } if (nodeManager != null) { logFine("Shutting down node manager"); nodeManager.shutdown(); } if (ioProvider != null) { logFine("Shutting down io provider"); ioProvider.shutdown(); } // shut down folder repository if (folderRepository != null) { logFine("Shutting down folder repository"); folderRepository.shutdown(); } if (reconnectManager != null) { logFine("Shutting down reconnect manager"); reconnectManager.shutdown(); } if (pluginManager != null) { logFine("Shutting down plugin manager"); pluginManager.shutdown(); } if (MacUtils.isSupported()) { MacUtils.getInstance().removeAppReOpenedListener(this); } if (wasStarted) { System.out.println("------------ " + PowerFolder.NAME + " " + PROGRAM_VERSION + " Controller Shutdown ------------"); } // remove current config // config = null; shuttingDown = false; logInfo("Shutting down done"); LoggingManager.closeFileLogging(); backupConfigAssets(); } public ScheduledExecutorService getThreadPool() { return threadPool; } /** * Returns a debug report * * @return the Debug report. */ public String getDebugReport() { return Debug.buildDebugReport(this); } /** * Writes the debug report to diks */ public void writeDebugReport() { try { FileOutputStream fOut = new FileOutputStream(getConfigName() + ".report.txt"); String report = getDebugReport(); fOut.write(report.getBytes()); fOut.close(); } catch (FileNotFoundException e) { logSevere("FileNotFoundException", e); } catch (IOException e) { logSevere("IOException", e); } } /** * Answers the current config name loaded <configname>.properties * * @return The name of the current config */ public String getConfigName() { if (configFilename == null) { return null; } String configName = configFilename; int dot = configName.indexOf('.'); if (dot > 0) { configName = configName.substring(0, dot); } return configName; } public Path getConfigFile() { return configFile; } public Path getConfigFolderFile() { return configFolderFile; } /** * Returns the config, read from the configfile. * * @return the config as properties object */ public Properties getConfig() { return config; } /** * Returns the command line of the start * * @return The command line */ public CommandLine getCommandLine() { return commandLine; } public String getCLIUsername() { return commandLine != null ? commandLine.getOptionValue("u") : null; } public String getCLIPassword() { return commandLine != null ? commandLine.getOptionValue("p") : null; } /** * Returns local preferences, Preferences are stored till the next start. On * windows they are stored in the registry. * * @return The preferences */ public Preferences getPreferences() { return preferences; } /** * @return true if this is the first start of PowerFolder of this config. */ public boolean isFirstStart() { return preferences.getBoolean("openwizard2", true); } public void setFirstStart(boolean bool) { preferences.putBoolean("openwizard2", bool); } /** * @return the distribution of this client. */ public Distribution getDistribution() { return distribution; } /** * @return the configured update settings for the current distribution */ public UpdateSetting getUpdateSettings() { return UpdateSetting.create(this); } /** * Answers the own identity, of course with no connection * * @return a referens to the member object representing myself. */ @Override public Member getMySelf() { return nodeManager != null ? nodeManager.getMySelf() : null; } /** * Changes the nick and tells other nodes * * @param newNick * the new nick * @param saveConfig * true if the config should be save directly otherwise you have * to do it by hand */ public void changeNick(String newNick, boolean saveConfig) { getMySelf().setNick(newNick); ConfigurationEntry.NICK.setValue(this, getMySelf().getNick()); if (saveConfig) { saveConfig(); } // broadcast nickchange nodeManager.broadcastMessage(new SettingsChange(getMySelf())); if (isUIOpen()) { // Update title uiController.getMainFrame().updateTitle(); } } /** * @return the io provider. */ public IOProvider getIOProvider() { return ioProvider; } /** * @return the Online Storage client. */ public ServerClient getOSClient() { return osClient; } /** * Retruns the plugin manager * * @return the plugin manager */ public PluginManager getPluginManager() { return pluginManager; } /** * Returns the dyndns manager * * @return the dyndns manager */ public DynDnsManager getDynDnsManager() { return dyndnsManager; } /** * Returns the broadcast manager * * @return broadcast manager */ public BroadcastMananger getBroadcastManager() { return broadcastManager; } /** * Returns the NodeManager * * @return the NodeManager */ public NodeManager getNodeManager() { return nodeManager; } public ReconnectManager getReconnectManager() { return reconnectManager; } public PersistentTaskManager getTaskManager() { return taskManager; } /** * Returns the folder repository * * @return the folder repository */ public FolderRepository getFolderRepository() { return folderRepository; } /** * Returns the transfer manager of the controller * * @return transfer manager */ public TransferManager getTransferManager() { return transferManager; } /** * ONLY USE THIS METHOD FOR TESTING PURPOSES! * * @param factory */ public void setTransferManagerFactory(Callable<TransferManager> factory) { Reject.ifNull(factory, "TransferManager factory is null"); if (transferManager != null) { throw new IllegalStateException("TransferManager was already set!"); } transferManagerFactory = factory; } public SecurityManager getSecurityManager() { return securityManager; } /** * Injects a security manager. * * @param securityManager * the security manager to set. */ public void setSecurityManager(SecurityManager securityManager) { logFiner("Security manager set: " + securityManager); this.securityManager = securityManager; } /** * Connects to a remote peer, with ip and port * * @param address * @return the node that connected * @throws ConnectionException * if connection failed * @returns the connected node */ public Member connect(InetSocketAddress address) throws ConnectionException { if (!started) { logInfo("NOT Connecting to " + address + ". Controller not started"); throw new ConnectionException("NOT Connecting to " + address + ". Controller not started"); } if (address.getPort() <= 0) { // connect to defaul port logWarning("Unable to connect, port illegal " + address.getPort()); } logFine("Connecting to " + address + "..."); ConnectionHandler conHan = ioProvider.getConnectionHandlerFactory().tryToConnect(address); // Accept new node return nodeManager.acceptConnection(conHan); } /** * Connect to a remote node Interprets a string as connection string Format * is expeced as ' <connect host>' or ' <connect host>: <port>' * * @param connectStr * @return the member that connected under the given addresse * @throws ConnectionException * @returns the connected node */ public Member connect(String connectStr) throws ConnectionException { return connect(Util.parseConnectionString(connectStr)); } /** * Answers if controller is started in console mode * * @return true if in console mode */ public boolean isConsoleMode() { if (commandLine != null) { if (commandLine.hasOption('s')) { return true; } } if (config != null) { if (ConfigurationEntry.DISABLE_GUI.getValueBoolean(this)) { return true; } } if (Feature.UI_ENABLED.isDisabled()) { return true; } return GraphicsEnvironment.isHeadless(); } /** * Whether to display notifications bottom-left instead of the normal * bottom-right. Primarily a development switch for running two PFs on one * PC. * * @return true if notifications should be displayed on the left. */ public boolean isNotifyLeft() { return commandLine != null && commandLine.hasOption('y'); } /** * Opens the graphical user interface */ private void openUI() { uiController.start(); } /** * Answers if the user interface (ui) is enabled * * @return true if the user interface is enabled, else false */ public boolean isUIEnabled() { return !isConsoleMode(); } /** * Answers if we have the ui open * * @return true if the uiserinterface is actualy started */ public boolean isUIOpen() { return uiController != null && uiController.isStarted(); } /** * Exposing UIController, acces to all UserInterface elements * * @return the UIController */ public UIController getUIController() { return uiController; } /** * Waits for the ui to open, afterwards it is guranteed that uicontroller is * started */ public void waitForUIOpen() { if (!isUIEnabled()) { throw new IllegalStateException("Unable to ui to open, ui is not enabled"); } while (!isUIOpen()) { try { // Wait.... Thread.sleep(1000); } catch (InterruptedException e) { logFiner("InterruptedException", e); break; } } } /** * Opens the listener on local port. The first listener is set to * "connectionListener". All others are added the the list of * additionalConnectionListeners. * * @return if succeeded */ private boolean openListener(int port) { String bind = ConfigurationEntry.NET_BIND_ADDRESS.getValue(this); logFine("Opening incoming connection listener on port " + port + " on interface " + (bind != null ? bind : "(all)")); while (true) { try { ConnectionListener newListener = new ConnectionListener(this, port, bind); if (connectionListener == null) { // its our primary listener connectionListener = newListener; } else { additionalConnectionListeners.add(newListener); } return true; } catch (ConnectionException e) { logWarning("Unable to bind to port " + port); logFiner("ConnectionException", e); if (bind != null) { logSevere( "This could've been caused by a binding error on the interface... Retrying without binding"); bind = null; } else { // Already tried binding once or not at all so get // out return false; } } } } /** * Do we have a connection listener? * * @return true if we have a connection listener, otherwise false */ public boolean hasConnectionListener() { return connectionListener != null; } /** * Answers the connection listener * * @return the connection listener */ public ConnectionListener getConnectionListener() { return connectionListener; } /** * Answers if this controller is runing in verbose mode. Set verbose=true on * config file to enable this, this gives access to all kinds of debugging * stuff. * * @return true if we are in verbose mode */ public boolean isVerbose() { return verbose; } /** * Answers if debug reports should be requested. Set debugReports=true on * config file to enable this, this request node information. Only enabled * if in verbose mode. * * @see RequestNodeInformation * @return true if we are in verbose mode */ public boolean isDebugReports() { return debugReports && verbose; } /** * Returns the buildtime of this jar * * @return the Date the application jar was build. */ public Date getBuildTime() { Path jar = Paths.get(getJARName()); if (Files.exists(jar)) { try { return new Date(Files.getLastModifiedTime(jar).toMillis()); } catch (IOException ioe) { return null; } } return null; } /** * Sets the loading completion of this controller. Used in the splash * screen. * * @param percentage * the percentage complete */ private void setLoadingCompletion(int percentage, int nextPerc) { if (uiController != null) { uiController.setLoadingCompletion(percentage, nextPerc); } } /** * Answers if minimized start is wanted. Use startup option -m to enable * this. * * @return if a minimized startup should be performed. */ public boolean isStartMinimized() { return commandLine != null && commandLine.hasOption('m'); } /** * The base directory where to store/load config files. or null if on * working path * * @return The File object representing the absolute location of where the * config files are/should be stored. */ private Path getConfigLocationBase() { // First check if we have a config file in local path Path aConfigFile = Paths.get(getConfigName() + ".config").toAbsolutePath(); // Load configuration in misc file if config file if in if (OSUtil.isWebStart() || Files.notExists(aConfigFile)) { if (isFiner()) { logFiner("Config location base: " + getMiscFilesLocation().toString()); } return getMiscFilesLocation(); } // Otherwise use local path as configuration base return null; } /** * Answers the path, where to load/store miscellanouse files created by * PowerFolder. e.g. .nodes files * * @return the file base, a directory */ public static Path getMiscFilesLocation() { Path base; Path unixConfigDir = Paths.get(System.getProperty("user.home"), "." + Constants.MISC_DIR_NAME) .toAbsolutePath(); if (OSUtil.isWindowsSystem() && Feature.WINDOWS_MISC_DIR_USE_APP_DATA.isEnabled()) { String appData; if (Feature.CONFIGURATION_ALL_USERS.isEnabled()) { appData = WinUtils.getAppDataAllUsers(); } else { appData = WinUtils.getAppDataCurrentUser(); } if (StringUtils.isBlank(appData)) { // Appdata not found. Fallback. return unixConfigDir; } Path windowsConfigDir = Paths.get(appData, Constants.MISC_DIR_NAME).toAbsolutePath(); base = windowsConfigDir; // Check if migration is necessary if (Files.exists(unixConfigDir)) { boolean migrateConfig; if (Files.exists(windowsConfigDir)) { // APPDATA/PowerFolder does not yet contain a config file OR Filter<Path> filter = new Filter<Path>() { @Override public boolean accept(Path entry) { return entry.getFileName().toString().endsWith("config"); } }; try (DirectoryStream<Path> stream = Files.newDirectoryStream(windowsConfigDir, filter)) { migrateConfig = !stream.iterator().hasNext(); } catch (IOException ioe) { log.info(ioe.getMessage()); migrateConfig = true; } } else { // Migrate if APPDATA/PowerFolder not existing yet. migrateConfig = true; } if (migrateConfig) { boolean migrationOk = migrateWindowsMiscLocation(unixConfigDir, windowsConfigDir); if (!migrationOk) { // Fallback, migration failed. base = unixConfigDir; } } } } else { base = unixConfigDir; } if (Files.notExists(base)) { try { Files.createDirectories(base); } catch (IOException ioe) { log.severe("Failed to create " + base.toAbsolutePath().toString() + ". " + ioe); } } return base; } /** * Migrate config dir (if necessary) in Windows from user.home to APPDATA. * Pre Version 4, the config was in 'user.home'/.PowerFolder. * 'APPDATA'/PowerFolder is a more normal Windows location for application * data. * * @param unixBaseDir * the old user.home based config directory. * @param windowsBaseDir * the preferred APPDATA based config directory. */ private static boolean migrateWindowsMiscLocation(Path unixBaseDir, Path windowsBaseDir) { if (Files.notExists(windowsBaseDir)) { try { Files.createDirectories(windowsBaseDir); } catch (IOException ioe) { log.severe("Failed to create " + windowsBaseDir.toAbsolutePath().toString() + ". " + ioe); } } try { PathUtils.recursiveMove(unixBaseDir, windowsBaseDir); log.warning("Migrated config from " + unixBaseDir + " to " + windowsBaseDir); return true; } catch (IOException e) { log.warning("Failed to migrate config from " + unixBaseDir + " to " + windowsBaseDir + ". " + e); return false; } } /** * Answers the path, where to load/store temp files created by PowerFolder. * * @return the file base, a directory */ public static Path getTempFilesLocation() { Path base = Paths.get(System.getProperty("java.io.tmpdir")); if (Files.notExists(base)) { try { Files.createDirectories(base); } catch (IOException ioe) { log.warning( "Could not create temp files location '" + base.toAbsolutePath().toString() + "'. " + ioe); } } return base; } private void killRunningInstance() { if (RemoteCommandManager.hasRunningInstance()) { logWarning("Found a running instance. Trying to shut it down..."); RemoteCommandManager.sendCommand(RemoteCommandManager.QUIT); Waiter w = new Waiter(10000L); while (RemoteCommandManager.hasRunningInstance() && !w.isTimeout()) { w.waitABit(); } if (!RemoteCommandManager.hasRunningInstance()) { logInfo("Was able to shut down running instance. Continue normal"); return; } logWarning("Was NOT able to shut down running instance."); } } /** * Called if controller has detected a already running instance */ private void alreadyRunningCheck() { Component parent = null; if (isUIOpen()) { parent = uiController.getMainFrame().getUIComponent(); } if (!isStartMinimized() && isUIEnabled() && !commandLine.hasOption('z')) { Object[] options = { Translation.getTranslation("dialog.already_running.show_button") }; int exitOption = 0; if (verbose) { options = new Object[] { Translation.getTranslation("dialog.already_running.start_button"), Translation.getTranslation("dialog.already_running.exit_button") }; exitOption = 1; } if (JOptionPane.showOptionDialog(parent, Translation.getTranslation("dialog.already_running.warning"), Translation.getTranslation("dialog.already_running.title"), JOptionPane.DEFAULT_OPTION, JOptionPane.INFORMATION_MESSAGE, null, options, options[0]) == exitOption) { // exit pressed // Try to bring existing instance to the foreground. RemoteCommandManager.sendCommand(RemoteCommandManager.SHOW_UI); exit(1); } else { exit(1); } } else { // If no gui show error but start anyways logWarning("PowerFolder already running"); } } private void fatalStartError(String message) { Component parent = null; if (isUIOpen()) { parent = uiController.getMainFrame().getUIComponent(); } if (isUIEnabled()) { Object[] options = { Translation.getTranslation("dialog.already_running.exit_button") }; JOptionPane.showOptionDialog(parent, message, Translation.getTranslation("dialog.fatal_error.title"), JOptionPane.DEFAULT_OPTION, JOptionPane.ERROR_MESSAGE, null, options, options[0]); } else { logSevere(message); } exit(1); } private void initDistribution() { try { if (ConfigurationEntry.DIST_CLASSNAME.hasValue(getController())) { Class<?> distClass = Class.forName(ConfigurationEntry.DIST_CLASSNAME.getValue(getController())); distribution = (Distribution) distClass.newInstance(); } if (distribution == null) { ServiceLoader<Distribution> brandingLoader = ServiceLoader.load(Distribution.class); for (Distribution br : brandingLoader) { if (distribution != null) { logWarning("Found multiple distribution classes: " + br.getName() + ", already using " + distribution.getName()); break; } distribution = br; } } if (distribution == null) { if (ProUtil.isRunningProVersion()) { distribution = new PowerFolderPro(); } else { distribution = new PowerFolderBasic(); } logFine("Distributon not found. Falling back to " + distribution.getName()); } distribution.init(this); logInfo("Running distribution: " + distribution.getName()); } catch (Exception e) { logSevere( "Failed to initialize distribution " + (distribution == null ? "null" : distribution.getName()), e); // Fallback try { if (distribution == null) { if (ProUtil.isRunningProVersion()) { distribution = new PowerFolderPro(); } else { distribution = new PowerFolderBasic(); } } logInfo("Running distribution: " + distribution.getName()); distribution.init(this); } catch (Exception e2) { logSevere("Failed to initialize fallback distribution", e2); } } } /** * Answers the waittime for threads time differst a bit to avoid * concurrencies * * @return The time to wait */ public static long getWaitTime() { return WAIT_TIME; } @Override public String toString() { return "Controller '" + getMySelf() + '\''; } /** * Distribute ask for friendship events. * * @param event */ public void makeFriendship(MemberInfo memberInfo) { if (networkingMode == NetworkingMode.SERVERONLYMODE) { logFine("Ignoring ask for friendship from client " + memberInfo + ". Running in server only mode"); return; } // Is this a friend already? Member member = memberInfo.getNode(this, false); if (member != null) { if (member.isFriend()) { log.fine("Ignoring ask for friendship from " + memberInfo.getNick() + ". Already friend."); return; } if (member.isServer()) { log.fine("Ignoring ask for friendship from " + memberInfo.getNick() + ". is a server."); return; } // Hack alert(tm): String lnick = member.getNick().toLowerCase(); boolean isPowerFolderCloud = lnick.contains("powerfolder") && lnick.contains("cloud"); if (isPowerFolderCloud) { log.fine("Ignoring ask for friendship from " + memberInfo.getNick() + ". is a pf server."); return; } } // A new friend! member.setFriend(true, null); } /** * Distribute invitations. * * @param invitation */ public void invitationReceived(Invitation invitation) { for (InvitationHandler handler : invitationHandlers) { handler.gotInvitation(invitation); } } /** * Distribute local mass deletion notifications. * * @param event */ public void localMassDeletionDetected(LocalMassDeletionEvent event) { for (MassDeletionHandler massDeletionHandler : massDeletionHandlers) { massDeletionHandler.localMassDeletion(event); } } /** * Distribute remote mass deletion notifications. * * @param event */ public void remoteMassDeletionDetected(RemoteMassDeletionEvent event) { for (MassDeletionHandler massDeletionHandler : massDeletionHandlers) { massDeletionHandler.remoteMassDeletion(event); } } /** * Save anything important that was not handled. */ private void savePersistentObjects() { if (started && isUIEnabled()) { // Save unhandled notices. List<Notice> notices = new ArrayList<Notice>(); for (Notice notice : uiController.getApplicationModel().getNoticesModel().getAllNotices()) { if (notice.isPersistable()) { notices.add(notice); } } Path file = getMiscFilesLocation().resolve(getConfigName() + ".notices"); try (ObjectOutputStream outputStream = new ObjectOutputStream(Files.newOutputStream(file))) { logInfo("There are " + notices.size() + " notices to persist."); outputStream.writeUnshared(notices); } catch (FileNotFoundException e) { logSevere("FileNotFoundException", e); } catch (IOException e) { logSevere("IOException", e); } } } /** * Load anything that was not handled last time. */ @SuppressWarnings("unchecked") private void loadPersistentObjects() { if (isUIEnabled()) { // Load notices. Path file = getMiscFilesLocation().resolve(getConfigName() + ".notices"); if (Files.exists(file)) { logInfo("Loading notices"); try (ObjectInputStream inputStream = new ObjectInputStream(Files.newInputStream(file))) { List<Notice> notices = (List<Notice>) inputStream.readObject(); inputStream.close(); for (Notice notice : notices) { uiController.getApplicationModel().getNoticesModel().handleSystemNotice(notice, true); } logFine("Loaded " + notices.size() + " notices."); } catch (FileNotFoundException e) { logSevere("FileNotFoundException", e); } catch (IOException e) { logSevere("IOException", e); } catch (ClassNotFoundException e) { logSevere("ClassNotFoundException", e); } catch (ClassCastException e) { logSevere("ClassCastException", e); } } else { logInfo("No notices found - probably first start of PF."); } } } /** * Wait for the repo to finish syncing. Then request system shutdown and * exit PF. * * @param password * required only for Linux shutdowns. */ public void shutdownAfterSync(final String password) { final AtomicBoolean oneShot = new AtomicBoolean(true); scheduleAndRepeat(new Runnable() { @Override public void run() { // ALPS Problem: Change to check for all in sync. if (oneShot.get() && folderRepository.isInSync()) { // Make sure we only try to shutdown once, // in case the user aborts the shutdown. oneShot.set(false); log.info("Sync and shutdown in sync."); if (SystemUtil.shutdown(password)) { log.info("Shutdown command issued."); exit(0); } else { log.warning("Shutdown command failed."); } } } }, 10000, 10000); } /** * Waits for the repo to finish syncing. Then request system shutdown and * exit PF. * * @param secWait * number of seconds to wait. */ public void exitAfterSync(int secWait) { logInfo("Sync and exit initiated. Begin check in " + secWait + 's'); final AtomicBoolean oneShot = new AtomicBoolean(true); scheduleAndRepeat(new Runnable() { @Override public void run() { // ALPS Problem: Change to check for all in sync. if (oneShot.get() && folderRepository.isInSync()) { // Make sure we only try to shutdown once, // in case the user aborts the shutdown. oneShot.set(false); log.info("I'm in sync - exit now. Sync and exit was triggered."); exit(0); } } }, 1000L * secWait, 10000); } /** * #2485 */ private class PauseResumeTask extends TimerTask { private final boolean userAdaptive; public PauseResumeTask(boolean whenUserIsInactive) { this.userAdaptive = whenUserIsInactive; } @Override public void run() { if (userAdaptive && isUIOpen()) { ApplicationModel appModel = uiController.getApplicationModel(); if (appModel.isUserActive()) { if (!isPaused()) { getController().schedule(new Runnable() { @Override public void run() { setPaused0(true, true); log.info("User active. Executed pause task."); } }, 50); } } else { // Resume if user is not active if (isPaused()) { getController().schedule(new Resumer(), 50); } } } else { // Simply unpause after X seconds setPaused0(false, true); log.info("Executed resume task."); } } } private class Resumer implements Runnable { @Override public void run() { setPaused0(false, true); log.info("User inactive. Executed resume task."); } } }