diff --git a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/TunnelController.java b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/TunnelController.java index fddf90e31..babe58a8f 100644 --- a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/TunnelController.java +++ b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/TunnelController.java @@ -789,14 +789,19 @@ public class TunnelController implements Logging { * As of 0.9.1, updates the options on an existing session */ public void setConfig(Properties config, String prefix) { - Properties props = new Properties(); - for (Map.Entry e : config.entrySet()) { - String key = (String) e.getKey(); - if (key.startsWith(prefix)) { - key = key.substring(prefix.length()); - String val = (String) e.getValue(); - props.setProperty(key, val); + Properties props; + if (prefix.length() > 0) { + props = new Properties(); + for (Map.Entry e : config.entrySet()) { + String key = (String) e.getKey(); + if (key.startsWith(prefix)) { + key = key.substring(prefix.length()); + String val = (String) e.getValue(); + props.setProperty(key, val); + } } + } else { + props = config; } Properties oldConfig = _config; _config = props; @@ -921,10 +926,14 @@ public class TunnelController implements Logging { */ public Properties getConfig(String prefix) { Properties rv = new Properties(); - for (Map.Entry e : _config.entrySet()) { - String key = (String) e.getKey(); - String val = (String) e.getValue(); - rv.setProperty(prefix + key, val); + if (prefix.length() > 0) { + for (Map.Entry e : _config.entrySet()) { + String key = (String) e.getKey(); + String val = (String) e.getValue(); + rv.setProperty(prefix + key, val); + } + } else { + rv.putAll(_config); } return rv; } diff --git a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/TunnelControllerGroup.java b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/TunnelControllerGroup.java index f2a1f34b5..d2eb70b83 100644 --- a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/TunnelControllerGroup.java +++ b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/TunnelControllerGroup.java @@ -3,6 +3,7 @@ package net.i2p.i2ptunnel; import java.io.File; import java.io.IOException; import java.util.ArrayList; +import java.util.Arrays; import java.util.HashMap; import java.util.HashSet; import java.util.List; @@ -24,9 +25,11 @@ import static net.i2p.app.ClientAppState.*; import net.i2p.client.I2PSession; import net.i2p.client.I2PSessionException; import net.i2p.data.DataHelper; +import net.i2p.util.FileUtil; import net.i2p.util.I2PAppThread; import net.i2p.util.Log; import net.i2p.util.OrderedProperties; +import net.i2p.util.SecureDirectory; import net.i2p.util.SystemVersion; /** @@ -42,6 +45,8 @@ public class TunnelControllerGroup implements ClientApp { private final ClientAppManager _mgr; private static volatile TunnelControllerGroup _instance; static final String DEFAULT_CONFIG_FILE = "i2ptunnel.config"; + private static final String CONFIG_DIR = "i2ptunnel.config.d"; + private static final String PREFIX = "tunnel."; private final List _controllers; private final ReadWriteLock _controllersLock; @@ -265,18 +270,80 @@ public class TunnelControllerGroup implements ClientApp { public synchronized void loadControllers(String configFile) { if (_controllersLoaded) return; - - Properties cfg = loadConfig(configFile); + boolean shouldMigrate = _context.isRouterContext() && !SystemVersion.isAndroid(); + loadControllers(configFile, shouldMigrate); + } + + /** + * @param shouldMigrate migrate to, and load from, i2ptunnel.config.d + * @since 0.9.34 + * @throws IllegalArgumentException if unable to load from file + */ + private synchronized void loadControllers(String configFile, boolean shouldMigrate) { + File cfgFile = new File(configFile); + if (!cfgFile.isAbsolute()) + cfgFile = new File(_context.getConfigDir(), configFile); + File dir = new SecureDirectory(cfgFile.getParent(), CONFIG_DIR); + List props = null; + if (cfgFile.exists()) { + try { + List cfgs = loadConfig(cfgFile); + if (shouldMigrate) { + boolean ok = migrate(cfgs, cfgFile, dir); + if (!ok) + shouldMigrate = false; + } + } catch (IOException ioe) { + _log.error("Unable to load the controllers from " + cfgFile.getAbsolutePath()); + throw new IllegalArgumentException("Unable to load the controllers from " + cfgFile, ioe); + } + } else if (!shouldMigrate) { + throw new IllegalArgumentException("Unable to load the controllers from " + cfgFile); + } int i = 0; _controllersLock.writeLock().lock(); try { - while (true) { - String type = cfg.getProperty("tunnel." + i + ".type"); - if (type == null) - break; - TunnelController controller = new TunnelController(cfg, "tunnel." + i + "."); - _controllers.add(controller); - i++; + if (shouldMigrate && dir.isDirectory()) { + File[] files = dir.listFiles(); + if (files != null && files.length > 0) { + // sort so the returned order is consistent + Arrays.sort(files); + for (File f : files) { + if (!f.getName().endsWith(".config")) + continue; + if (!f.isFile()) + continue; + try { + props = loadConfig(f); + if (!props.isEmpty()) { + for (Properties cfg : props) { + String type = cfg.getProperty("type"); + if (type == null) + continue; + TunnelController controller = new TunnelController(cfg, ""); + _controllers.add(controller); + i++; + } + } else { + _log.error("Error loading the client app properties from " + f); + System.out.println("Error loading the client app properties from " + f); + } + } catch (IOException ioe) { + _log.error("Error loading the client app properties from " + f, ioe); + System.out.println("Error loading the client app properties from " + f + ' ' + ioe); + } + } + } + } else { + // use what we got from i2ptunnel.config + for (Properties cfg : props) { + String type = cfg.getProperty("type"); + if (type == null) + continue; + TunnelController controller = new TunnelController(cfg, ""); + _controllers.add(controller); + i++; + } } } finally { _controllersLock.writeLock().unlock(); @@ -284,13 +351,55 @@ public class TunnelControllerGroup implements ClientApp { _controllersLoaded = true; if (i > 0) { + _controllersLoaded = true; if (_log.shouldLog(Log.INFO)) _log.info(i + " controllers loaded from " + configFile); } else { - _log.logAlways(Log.WARN, "No i2ptunnel configurations found in " + configFile); + _log.logAlways(Log.WARN, "No i2ptunnel configurations found in " + cfgFile + " or " + dir); } } + /* + * Migrate tunnels from file to individual files in dir + * + * @return success + * @since 0.9.34 + */ + private boolean migrate(List tunnels, File from, File dir) { + if (!dir.isDirectory() && !dir.mkdirs()) + return false; + boolean ok = true; + for (int i = 0; i < tunnels.size(); i++) { + Properties props = tunnels.get(i); + String tname = props.getProperty("name"); + if (tname == null) + tname = "tunnel"; + String name = i + "-" + tname + "-i2ptunnel.config"; + if (i < 10) + name = '0' + name; + File f = new File(dir, name); + props.setProperty("configFile", f.getAbsolutePath()); + Properties save = new OrderedProperties(); + for (Map.Entry e : props.entrySet()) { + String key = (String) e.getKey(); + String val = (String) e.getValue(); + save.setProperty(PREFIX + i + '.' + key, val); + } + try { + DataHelper.storeProps(save, f); + } catch (IOException ioe) { + _log.error("Error migrating the i2ptunnel configuration to " + f, ioe); + System.out.println("Error migrating the i2ptunnel configuration to " + f + ' ' + ioe); + ok = false; + } + } + if (ok) { + if (!FileUtil.rename(from, new File(from.getAbsolutePath() + ".bak"))) + from.delete(); + } + return ok; + } + /** * Start all of the tunnels. Must call loadControllers() first. * @since 0.9.20 @@ -375,6 +484,7 @@ public class TunnelControllerGroup implements ClientApp { /** * Stop and remove the given tunnel. * Side effect - clears all messages the controller. + * Does NOT delete the configuration - must call saveConfig() or removeConfig() also. * * @return list of messages from the controller as it is stopped */ @@ -500,19 +610,22 @@ public class TunnelControllerGroup implements ClientApp { * Save the configuration of all known tunnels to the default config * file * + * @deprecated use saveConfig(TunnelController) or removeConfig(TunnelController) */ + @Deprecated public void saveConfig() throws IOException { saveConfig(_configFile); } /** * Save the configuration of all known tunnels to the given file - * + * @deprecated */ + @Deprecated public synchronized void saveConfig(String configFile) throws IOException { File cfgFile = new File(configFile); if (!cfgFile.isAbsolute()) - cfgFile = new File(I2PAppContext.getGlobalContext().getConfigDir(), configFile); + cfgFile = new File(_context.getConfigDir(), configFile); File parent = cfgFile.getParentFile(); if ( (parent != null) && (!parent.exists()) ) parent.mkdirs(); @@ -522,7 +635,7 @@ public class TunnelControllerGroup implements ClientApp { try { for (int i = 0; i < _controllers.size(); i++) { TunnelController controller = _controllers.get(i); - Properties cur = controller.getConfig("tunnel." + i + "."); + Properties cur = controller.getConfig(PREFIX + i + "."); map.putAll(cur); } } finally { @@ -531,32 +644,50 @@ public class TunnelControllerGroup implements ClientApp { DataHelper.storeProps(map, cfgFile); } + + /** + * Save the configuration of this tunnel only, may be new + * @since 0.9.34 + */ + public synchronized void saveConfig(TunnelController tc) throws IOException { + } + + /** + * Save the configuration of this tunnel only + * @since 0.9.34 + */ + public synchronized void removeConfig(TunnelController tc) throws IOException { + } /** * Load up the config data from the file * - * @return properties loaded - * @throws IllegalArgumentException if unable to load from file + * @return non-null, properties loaded, one for each tunnel + * @throws IOException if unable to load from file */ - private synchronized Properties loadConfig(String configFile) { - File cfgFile = new File(configFile); - if (!cfgFile.isAbsolute()) - cfgFile = new File(I2PAppContext.getGlobalContext().getConfigDir(), configFile); - if (!cfgFile.exists()) { - if (_log.shouldLog(Log.ERROR)) - _log.error("Unable to load the controllers from " + cfgFile.getAbsolutePath()); - throw new IllegalArgumentException("Unable to load the controllers from " + cfgFile.getAbsolutePath()); - } - - Properties props = new Properties(); - try { - DataHelper.loadProps(props, cfgFile); - return props; - } catch (IOException ioe) { - if (_log.shouldLog(Log.ERROR)) - _log.error("Error reading the controllers from " + cfgFile.getAbsolutePath(), ioe); - throw new IllegalArgumentException("Error reading the controllers from " + cfgFile.getAbsolutePath(), ioe); + private synchronized List loadConfig(File cfgFile) throws IOException { + Properties config = new Properties(); + DataHelper.loadProps(config, cfgFile); + List rv = new ArrayList(); + int i = 0; + while (true) { + String prefix = PREFIX + i + '.'; + Properties p = new Properties(); + for (Map.Entry e : config.entrySet()) { + String key = (String) e.getKey(); + if (key.startsWith(prefix)) { + key = key.substring(prefix.length()); + String val = (String) e.getValue(); + p.setProperty(key, val); + } + } + if (p.isEmpty()) + break; + p.setProperty("configFile", cfgFile.getAbsolutePath()); + rv.add(p); + i++; } + return rv; } /** diff --git a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/ui/GeneralHelper.java b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/ui/GeneralHelper.java index 5f23453b4..5191737ee 100644 --- a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/ui/GeneralHelper.java +++ b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/ui/GeneralHelper.java @@ -82,6 +82,7 @@ public class GeneralHelper { public static List saveTunnel( I2PAppContext context, TunnelControllerGroup tcg, int tunnel, TunnelConfig config) { List msgs = updateTunnelConfig(tcg, tunnel, config); +/////////////// msgs.addAll(saveConfig(context, tcg)); return msgs; } @@ -175,6 +176,7 @@ public class GeneralHelper { protected static List saveConfig(I2PAppContext context, TunnelControllerGroup tcg) { List rv = tcg.clearAllMessages(); try { +//////////////// tcg.saveConfig(); rv.add(0, _t("Configuration changes saved", context)); } catch (IOException ioe) { @@ -205,6 +207,7 @@ public class GeneralHelper { return msgs; } +//////////////////////// msgs = tcg.removeController(cur); msgs.addAll(saveConfig(context, tcg)); diff --git a/apps/routerconsole/java/src/net/i2p/router/web/ConfigServiceHandler.java b/apps/routerconsole/java/src/net/i2p/router/web/ConfigServiceHandler.java index 55e42b471..36730d023 100644 --- a/apps/routerconsole/java/src/net/i2p/router/web/ConfigServiceHandler.java +++ b/apps/routerconsole/java/src/net/i2p/router/web/ConfigServiceHandler.java @@ -342,23 +342,29 @@ public class ConfigServiceHandler extends FormHandler { private void browseOnStartup(boolean shouldLaunchBrowser) { List clients = ClientAppConfig.getClientApps(_context); - boolean found = false; + ClientAppConfig ca = null; for (int cur = 0; cur < clients.size(); cur++) { - ClientAppConfig ca = clients.get(cur); - if (UrlLauncher.class.getName().equals(ca.className)) { + ClientAppConfig cac = clients.get(cur); + if (UrlLauncher.class.getName().equals(cac.className)) { + ca = cac; ca.disabled = !shouldLaunchBrowser; - found = true; break; } } // releases <= 0.6.5 deleted the entry completely - if (shouldLaunchBrowser && !found) { + if (shouldLaunchBrowser && ca == null) { String url = _context.portMapper().getConsoleURL(); - ClientAppConfig ca = new ClientAppConfig(UrlLauncher.class.getName(), "consoleBrowser", - url, 5, false); - clients.add(ca); + ca = new ClientAppConfig(UrlLauncher.class.getName(), "consoleBrowser", + url, 5, false); + } + try { + if (ca != null) + ClientAppConfig.writeClientAppConfig(_context, ca); + addFormNotice(_t("Configuration saved successfully")); + } catch (IOException ioe) { + addFormError(_t("Error saving the configuration (applied but not saved) - please see the error logs")); + addFormError(ioe.getLocalizedMessage()); } - ClientAppConfig.writeClientAppConfig(_context, clients); } /** diff --git a/apps/routerconsole/java/src/net/i2p/router/web/helpers/ConfigClientsHandler.java b/apps/routerconsole/java/src/net/i2p/router/web/helpers/ConfigClientsHandler.java index 35cbc9ec4..bc860e626 100644 --- a/apps/routerconsole/java/src/net/i2p/router/web/helpers/ConfigClientsHandler.java +++ b/apps/routerconsole/java/src/net/i2p/router/web/helpers/ConfigClientsHandler.java @@ -244,6 +244,18 @@ public class ConfigClientsHandler extends FormHandler { } private void saveClientChanges() { + try { + synchronized(ClientAppConfig.class) { + saveClientChanges2(); + } + addFormNotice(_t("Client configuration saved successfully")); + } catch (IOException ioe) { + addFormError(_t("Error saving the configuration (applied but not saved) - please see the error logs")); + addFormError(ioe.getLocalizedMessage()); + } + } + + private void saveClientChanges2() throws IOException { List clients = ClientAppConfig.getClientApps(_context); for (int cur = 0; cur < clients.size(); cur++) { ClientAppConfig ca = clients.get(cur); @@ -287,14 +299,12 @@ public class ConfigClientsHandler extends FormHandler { if (name == null || name.trim().length() <= 0) name = "new client"; ClientAppConfig ca = new ClientAppConfig(clss, name, args, 2*60*1000, _settings.get(newClient + ".enabled") == null); // true for disabled - clients.add(ca); + ClientAppConfig.writeClientAppConfig(_context, ca); addFormNotice(_t("New client added") + ": " + name + " (" + clss + ")."); } } - + // Always save, as any of the disabled flags could have changed ClientAppConfig.writeClientAppConfig(_context, clients); - addFormNotice(_t("Client configuration saved successfully")); - //addFormNotice(_t("Restart required to take effect")); } /** @@ -347,9 +357,14 @@ public class ConfigClientsHandler extends FormHandler { addFormError(_t("Bad client index.")); return; } - ClientAppConfig ca = clients.remove(i); - ClientAppConfig.writeClientAppConfig(_context, clients); - addFormNotice(_t("Client {0} deleted", ca.clientName)); + ClientAppConfig ca = clients.get(i); + try { + ClientAppConfig.deleteClientAppConfig(ca); + addFormNotice(_t("Client {0} deleted", ca.clientName)); + } catch (IOException ioe) { + addFormError(_t("Error saving the configuration (applied but not saved) - please see the error logs")); + addFormError(ioe.getLocalizedMessage()); + } } private void saveWebAppChanges() { diff --git a/router/java/src/net/i2p/router/startup/ClientAppConfig.java b/router/java/src/net/i2p/router/startup/ClientAppConfig.java index e0495ee6d..dcfc3c1a5 100644 --- a/router/java/src/net/i2p/router/startup/ClientAppConfig.java +++ b/router/java/src/net/i2p/router/startup/ClientAppConfig.java @@ -1,17 +1,22 @@ package net.i2p.router.startup; import java.io.File; -import java.io.FileOutputStream; import java.io.IOException; +import java.util.Arrays; import java.util.ArrayList; import java.util.Collections; import java.util.List; import java.util.Properties; +import java.util.Set; import net.i2p.I2PAppContext; import net.i2p.data.DataHelper; import net.i2p.router.RouterContext; -import net.i2p.util.SecureFileOutputStream; +import net.i2p.util.FileUtil; +import net.i2p.util.Log; +import net.i2p.util.ObjectCounter; +import net.i2p.util.OrderedProperties; +import net.i2p.util.SecureDirectory; /** @@ -71,6 +76,7 @@ public class ClientAppConfig { private static final String PROP_CLIENT_CONFIG_FILENAME = "router.clientConfigFile"; private static final String DEFAULT_CLIENT_CONFIG_FILENAME = "clients.config"; + private static final String CLIENT_CONFIG_DIR = "clients.config.d"; private static final String PREFIX = "clientApp."; // let's keep this really simple @@ -86,6 +92,8 @@ public class ClientAppConfig { public final String stopargs; /** @since 0.7.12 */ public final String uninstallargs; + /** @since 0.0.34 */ + private File configFile; public ClientAppConfig(String cl, String client, String a, long d, boolean dis) { this(cl, client, a, d, dis, null, null, null); @@ -111,51 +119,104 @@ public class ClientAppConfig { return cfgFile; } - private static Properties getClientAppProps(RouterContext ctx) { - Properties rv = new Properties(); - File cfgFile = configFile(ctx); - - // fall back to use router.config's clientApp.* lines - if (!cfgFile.exists()) { - System.out.println("Warning - No client config file " + cfgFile.getAbsolutePath()); - rv.putAll(ctx.router().getConfigMap()); - return rv; - } - - try { - DataHelper.loadProps(rv, cfgFile); - } catch (IOException ioe) { - System.out.println("Error loading the client app properties from " + cfgFile.getAbsolutePath() + ' ' + ioe); - } - - return rv; - } - /* - * Go through the properties, and return a List of ClientAppConfig structures + * Go through the files, and return a List of ClientAppConfig structures * This is for the router. */ - public static List getClientApps(RouterContext ctx) { - Properties clientApps = getClientAppProps(ctx); - List rv = getClientApps(clientApps); - MigrateJetty.migrate(ctx, rv); + public synchronized static List getClientApps(RouterContext ctx) { + File dir = new SecureDirectory(ctx.getConfigDir(), CLIENT_CONFIG_DIR); + // clients.config + List rv = new ArrayList(8); + File cf = configFile(ctx); + try { + List cacs = getClientApps(cf); + if (!cacs.isEmpty()) { + MigrateJetty.migrate(ctx, cacs); + boolean ok = migrate(ctx, cacs, cf, dir); + if (!ok) + rv.addAll(cacs); + } + } catch (IOException ioe) { + ctx.logManager().getLog(ClientAppConfig.class).error("Error loading the client app properties from " + cf, ioe); + System.out.println("Error loading the client app properties from " + cf + ' ' + ioe); + } + // clients.config.d + if (dir.isDirectory()) { + File[] files = dir.listFiles(); + if (files != null && files.length > 0) { + // sort so the returned order is consistent + Arrays.sort(files); + for (File f : files) { + if (!f.getName().endsWith(".config")) + continue; + if (!f.isFile()) + continue; + try { + List cacs = getClientApps(f); + if (!cacs.isEmpty()) { + rv.addAll(cacs); + } else { + ctx.logManager().getLog(ClientAppConfig.class).error("Error loading the client app properties from " + f); + System.out.println("Error loading the client app properties from " + f); + } + } catch (IOException ioe) { + ctx.logManager().getLog(ClientAppConfig.class).error("Error loading the client app properties from " + f, ioe); + System.out.println("Error loading the client app properties from " + f + ' ' + ioe); + } + } + } + } return rv; } /* - * Go through the properties, and return a List of ClientAppConfig structures - * This is for plugins. + * Go through the file, and return a List of ClientAppConfig structures * * @since 0.7.12 */ - public static List getClientApps(File cfgFile) { + public synchronized static List getClientApps(File cfgFile) throws IOException { + if (!cfgFile.isFile()) + return new ArrayList(); Properties clientApps = new Properties(); - try { - DataHelper.loadProps(clientApps, cfgFile); - } catch (IOException ioe) { - return Collections.emptyList(); + DataHelper.loadProps(clientApps, cfgFile); + List rv = getClientApps(clientApps); + for (ClientAppConfig cac : rv) { + cac.configFile = cfgFile; } - return getClientApps(clientApps); + return rv; + } + + /* + * Migrate apps from file to individual files in dir + * + * @return success + * @since 0.9.34 + */ + private static boolean migrate(I2PAppContext ctx, List apps, File from, File dir) { + if (!dir.isDirectory() && !dir.mkdirs()) + return false; + boolean ok = true; + for (int i = 0; i < apps.size(); i++) { + ClientAppConfig cac = apps.get(i); + String name = i + "-" + cac.className + "-clients.config"; + if (i < 10) + name = '0' + name; + File f = new File(dir, name); + cac.configFile = f; + try { + writeClientAppConfig(ctx, cac); + } catch (IOException ioe) { + ctx.logManager().getLog(ClientAppConfig.class).error("Error migrating the client app properties to " + f, ioe); + System.out.println("Error migrating the client app properties to " + f + ' ' + ioe); + cac.configFile = from; + ok = false; + } + } + if (ok) { + if (!FileUtil.rename(from, new File(from.getAbsolutePath() + ".bak"))) + from.delete(); + } + return ok; } /* @@ -167,18 +228,34 @@ public class ClientAppConfig { List rv = new ArrayList(8); int i = 0; while (true) { - String className = clientApps.getProperty(PREFIX + i + ".main"); - if (className == null) + ClientAppConfig cac = getClientApp(clientApps, PREFIX + i); + if (cac == null) break; - String clientName = clientApps.getProperty(PREFIX + i + ".name"); - String args = clientApps.getProperty(PREFIX + i + ".args"); - String delayStr = clientApps.getProperty(PREFIX + i + ".delay"); - String onBoot = clientApps.getProperty(PREFIX + i + ".onBoot"); - String disabled = clientApps.getProperty(PREFIX + i + ".startOnLoad"); - String classpath = clientApps.getProperty(PREFIX + i + ".classpath"); - String stopargs = clientApps.getProperty(PREFIX + i + ".stopargs"); - String uninstallargs = clientApps.getProperty(PREFIX + i + ".uninstallargs"); i++; + rv.add(cac); + } + return rv; + } + + /* + * Go through the properties, and get a single ClientAppConfig structure + * with this prefix + * + * @return null if none + * @since 0.9.34 split out from above + */ + private static ClientAppConfig getClientApp(Properties clientApps, String prefix) { + String className = clientApps.getProperty(prefix + ".main"); + if (className == null) + return null; + String clientName = clientApps.getProperty(prefix + ".name"); + String args = clientApps.getProperty(prefix + ".args"); + String delayStr = clientApps.getProperty(prefix + ".delay"); + String onBoot = clientApps.getProperty(prefix + ".onBoot"); + String disabled = clientApps.getProperty(prefix + ".startOnLoad"); + String classpath = clientApps.getProperty(prefix + ".classpath"); + String stopargs = clientApps.getProperty(prefix + ".stopargs"); + String uninstallargs = clientApps.getProperty(prefix + ".uninstallargs"); boolean dis = disabled != null && "false".equals(disabled); boolean onStartup = false; @@ -196,33 +273,143 @@ public class ClientAppConfig { if (delayStr != null) try { delay = 1000*Integer.parseInt(delayStr); } catch (NumberFormatException nfe) {} } - rv.add(new ClientAppConfig(className, clientName, args, delay, dis, - classpath, stopargs, uninstallargs)); - } - return rv; + return new ClientAppConfig(className, clientName, args, delay, dis, + classpath, stopargs, uninstallargs); } - /** classpath and stopargs not supported */ - public static void writeClientAppConfig(RouterContext ctx, List apps) { - File cfgFile = configFile(ctx); - FileOutputStream fos = null; - try { - fos = new SecureFileOutputStream(cfgFile); - StringBuilder buf = new StringBuilder(2048); - for(int i = 0; i < apps.size(); i++) { - ClientAppConfig app = apps.get(i); - buf.append(PREFIX).append(i).append(".main=").append(app.className).append("\n"); - buf.append(PREFIX).append(i).append(".name=").append(app.clientName).append("\n"); - if (app.args != null) - buf.append(PREFIX).append(i).append(".args=").append(app.args).append("\n"); - buf.append(PREFIX).append(i).append(".delay=").append(app.delay / 1000).append("\n"); - buf.append(PREFIX).append(i).append(".startOnLoad=").append(!app.disabled).append("\n"); - } - fos.write(buf.toString().getBytes("UTF-8")); - } catch (IOException ioe) { - } finally { - if (fos != null) try { fos.close(); } catch (IOException ioe) {} + /** + * Classpath and stopargs not supported. + * All other apps in the file will be deleted. + * Do not use if multiple apps in a single file - use writeClientAppConfig(ctx, apps). + * If app.configFile is null, a new file will be created and assigned. + * + * @since 0.9.34 + */ + public synchronized static void writeClientAppConfig(I2PAppContext ctx, ClientAppConfig app) throws IOException { + if (app.configFile == null) { + File dir = new SecureDirectory(ctx.getConfigDir(), CLIENT_CONFIG_DIR); + if (!dir.isDirectory() && !dir.mkdirs()) + throw new IOException("Can't create " + dir); + int i = 0; + String[] files = dir.list(); + if (files != null) + i = files.length; + File f; + do { + String name = i + "-" + app.className + "-clients.config"; + if (i < 10) + name = '0' + name; + f = new File(dir, name); + i++; + } while (f.exists()); + app.configFile = f; } + writeClientAppConfig(Collections.singletonList(app), app.configFile); + } + + /** + * Classpath and stopargs not supported. + * All other apps in the files will be deleted. + * Do not add apps with this method - use writeClientAppConfig(ctx, app). + * Do not delete apps with this method - use deleteClientAppConfig(). + * + * @since 0.9.34 split out from above + */ + public synchronized static void writeClientAppConfig(I2PAppContext ctx, List apps) throws IOException { + // Gather the set of config files + ObjectCounter counter = new ObjectCounter(); + for (ClientAppConfig cac : apps) { + File f = cac.configFile; + if (f == null) + throw new IllegalArgumentException("No file for " + cac.className); + counter.increment(f); + } + IOException e = null; + // Write the config files + Set files = counter.objects(); + // For each file, write all the configs for that file + for (File f : files) { + // Gather configs for this file + List cacs = new ArrayList(8); + for (ClientAppConfig cac : apps) { + if (cac.configFile.equals(f)) + cacs.add(cac); + } + try { + writeClientAppConfig(cacs, f); + } catch (IOException ioe) { + if (e == null) + e = ioe; + } + } + if (e != null) + throw e; + } + + /** + * All to a single file, apps.configFile ignored + * + * @throws IllegalArgumentException if null cfgFile + * @since 0.9.34 split out from above + */ + private static void writeClientAppConfig(List apps, File cfgFile) throws IOException { + if (cfgFile == null) + throw new IllegalArgumentException("No file"); + Properties props = new OrderedProperties(); + for(int i = 0; i < apps.size(); i++) { + ClientAppConfig app = apps.get(i); + String pfx = PREFIX + i; + props.setProperty(pfx + ".main", app.className); + props.setProperty(pfx + ".name", app.clientName); + if (app.args != null) + props.setProperty(pfx + ".args", app.args); + props.setProperty(pfx + ".delay", Long.toString(app.delay / 1000)); + props.setProperty(pfx + ".startOnLoad", Boolean.toString(!app.disabled)); + } + DataHelper.storeProps(props, cfgFile); + } + + /** + * @return success + * @throws IllegalArgumentException if cac has a null configfile + * @since 0.9.34 + */ + public synchronized static boolean deleteClientAppConfig(ClientAppConfig cac) throws IOException { + File f = cac.configFile; + if (f == null) + throw new IllegalArgumentException("No file for " + cac.className); + List cacs = getClientApps(f); + if (cacs.remove(cac)) { + if (cacs.isEmpty()) + return f.delete(); + writeClientAppConfig(cacs, f); + return true; + } + return false; + } + + /** + * @since 0.9.34 + */ + @Override + public int hashCode() { + return DataHelper.hashCode(className); + } + + /** + * Matches on class, args, and name only + * @since 0.9.34 + */ + @Override + public boolean equals(Object o) { + if (o == this) return true; + if (o == null) return false; + if (o instanceof ClientAppConfig) { + ClientAppConfig cac = (ClientAppConfig) o; + return DataHelper.eq(className, cac.className) && + DataHelper.eq(clientName, cac.clientName) && + DataHelper.eq(args, cac.args); + } + return false; } } - diff --git a/router/java/src/net/i2p/router/startup/MigrateJetty.java b/router/java/src/net/i2p/router/startup/MigrateJetty.java index db6476baa..2f4b03813 100644 --- a/router/java/src/net/i2p/router/startup/MigrateJetty.java +++ b/router/java/src/net/i2p/router/startup/MigrateJetty.java @@ -196,8 +196,17 @@ abstract class MigrateJetty { File cfgFile = ClientAppConfig.configFile(ctx); boolean ok = backupFile(cfgFile); if (ok) { - ClientAppConfig.writeClientAppConfig(ctx, apps); - System.err.println("WARNING: Migrated clients config file " + cfgFile + + try { + ClientAppConfig.writeClientAppConfig(ctx, apps); + System.err.println("WARNING: Migrated clients config file " + cfgFile + + " from Jetty 5/6 " + OLD_CLASS + '/' + OLD_CLASS_6 + + " to Jetty 9 " + NEW_CLASS); + } catch (IOException ioe) { + ok = false; + } + } + if (!ok) { + System.err.println("WARNING: Failed to migrate clients config file " + cfgFile + " from Jetty 5/6 " + OLD_CLASS + '/' + OLD_CLASS_6 + " to Jetty 9 " + NEW_CLASS); }