You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

Main.java 18KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508
  1. /*
  2. * Copyright (c) 2006-2011 Chris Smith, Shane Mc Cormack, Gregory Holmes
  3. *
  4. * Permission is hereby granted, free of charge, to any person obtaining a copy
  5. * of this software and associated documentation files (the "Software"), to deal
  6. * in the Software without restriction, including without limitation the rights
  7. * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
  8. * copies of the Software, and to permit persons to whom the Software is
  9. * furnished to do so, subject to the following conditions:
  10. *
  11. * The above copyright notice and this permission notice shall be included in
  12. * all copies or substantial portions of the Software.
  13. *
  14. * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  15. * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  16. * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  17. * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  18. * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  19. * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
  20. * SOFTWARE.
  21. */
  22. package com.dmdirc;
  23. import com.dmdirc.actions.ActionManager;
  24. import com.dmdirc.actions.CoreActionType;
  25. import com.dmdirc.commandline.CommandLineParser;
  26. import com.dmdirc.commandparser.CommandManager;
  27. import com.dmdirc.config.ConfigManager;
  28. import com.dmdirc.config.IdentityManager;
  29. import com.dmdirc.config.InvalidIdentityFileException;
  30. import com.dmdirc.logger.DMDircExceptionHandler;
  31. import com.dmdirc.logger.ErrorLevel;
  32. import com.dmdirc.logger.Logger;
  33. import com.dmdirc.plugins.PluginInfo;
  34. import com.dmdirc.plugins.PluginManager;
  35. import com.dmdirc.plugins.Service;
  36. import com.dmdirc.plugins.ServiceProvider;
  37. import com.dmdirc.ui.WarningDialog;
  38. import com.dmdirc.ui.interfaces.UIController;
  39. import com.dmdirc.ui.themes.ThemeManager;
  40. import com.dmdirc.updater.UpdateChecker;
  41. import com.dmdirc.updater.Version;
  42. import com.dmdirc.util.resourcemanager.ResourceManager;
  43. import java.awt.GraphicsEnvironment;
  44. import java.io.File;
  45. import java.io.IOException;
  46. import java.text.SimpleDateFormat;
  47. import java.util.Collection;
  48. import java.util.Date;
  49. import java.util.HashSet;
  50. import java.util.List;
  51. import java.util.Map;
  52. import java.util.Timer;
  53. import java.util.TimerTask;
  54. import java.util.logging.Handler;
  55. import java.util.logging.Level;
  56. /**
  57. * Main class, handles initialisation.
  58. *
  59. * @author chris
  60. */
  61. public class Main {
  62. /** Feedback nag delay. */
  63. private static final int FEEDBACK_DELAY = 30 * 60 * 1000;
  64. /** The UI to use for the client. */
  65. private static final Collection<UIController> CONTROLLERS = new HashSet<UIController>();
  66. /** The config dir to use for the client. */
  67. private static String configdir;
  68. /**
  69. * Prevents creation of main.
  70. */
  71. private Main() {
  72. }
  73. /**
  74. * Entry procedure.
  75. *
  76. * @param args the command line arguments
  77. */
  78. @SuppressWarnings("PMD.AvoidCatchingThrowable")
  79. public static void main(final String[] args) {
  80. try {
  81. init(args);
  82. } catch (Throwable ex) {
  83. Logger.appError(ErrorLevel.FATAL, "Exception while initialising",
  84. ex);
  85. }
  86. }
  87. /**
  88. * Initialises the client.
  89. *
  90. * @param args The command line arguments
  91. */
  92. private static void init(final String[] args) {
  93. Thread.setDefaultUncaughtExceptionHandler(new DMDircExceptionHandler());
  94. for (Handler handler : java.util.logging.Logger.getLogger("").getHandlers()) {
  95. handler.setLevel(Level.OFF); // Needs to be changed to enable debugging
  96. }
  97. // Enable finer debugging for specific components like so:
  98. //java.util.logging.Logger.getLogger("com.dmdirc.plugins").setLevel(Level.ALL);
  99. IdentityManager.loadVersion();
  100. final CommandLineParser clp = new CommandLineParser(args);
  101. try {
  102. IdentityManager.load();
  103. } catch (InvalidIdentityFileException iife) {
  104. handleInvalidConfigFile();
  105. }
  106. final PluginManager pm = PluginManager.getPluginManager();
  107. checkBundledPlugins(pm, IdentityManager.getGlobalConfig());
  108. ThemeManager.loadThemes();
  109. clp.applySettings();
  110. CommandManager.initCommands();
  111. for (String service : new String[]{"ui", "tabcompletion", "parser"}) {
  112. ensureExists(pm, service);
  113. }
  114. // The user may have an existing parser plugin (e.g. twitter) which
  115. // will satisfy the service existance check above, but will render the
  116. // client pretty useless, so we'll force IRC extraction for now.
  117. extractCorePlugins("parser_irc");
  118. pm.getPossiblePluginInfos(true);
  119. loadUIs(pm);
  120. doFirstRun();
  121. ActionManager.init();
  122. pm.doAutoLoad();
  123. ActionManager.loadActions();
  124. ActionManager.processEvent(CoreActionType.CLIENT_OPENED, null);
  125. UpdateChecker.init();
  126. clp.processArguments();
  127. GlobalWindow.init();
  128. Runtime.getRuntime().addShutdownHook(new Thread(new Runnable() {
  129. /** {@inheritDoc} */
  130. @Override
  131. public void run() {
  132. ActionManager.processEvent(CoreActionType.CLIENT_CLOSED, null);
  133. ServerManager.getServerManager().disconnectAll("Unexpected shutdown");
  134. IdentityManager.save();
  135. }
  136. }, "Shutdown thread"));
  137. }
  138. /**
  139. * Called when the UI has failed to initialise correctly. This method
  140. * attempts to extract any and all UI plugins bundled with the client, and
  141. * requests a restart. If this has already been attempted, it shows an error
  142. * and exits.
  143. */
  144. private static void handleMissingUI() {
  145. // Check to see if we have already tried this
  146. if (IdentityManager.getGlobalConfig().hasOptionBool("debug", "uiFixAttempted")) {
  147. System.out.println("DMDirc is unable to load any compatible UI plugins.");
  148. if (!GraphicsEnvironment.isHeadless()) {
  149. new WarningDialog(WarningDialog.NO_COMPAT_UIS_TITLE,
  150. WarningDialog.NO_RECOV_UIS).displayBlocking();
  151. }
  152. IdentityManager.getConfigIdentity().unsetOption("debug", "uiFixAttempted");
  153. System.exit(1);
  154. } else {
  155. // Try to extract the UIs again incase they changed between versions
  156. // and the user didn't update the UI plugin.
  157. extractCorePlugins("ui_");
  158. System.out.println("DMDirc has updated the UI plugins and needs to restart.");
  159. if (!GraphicsEnvironment.isHeadless()) {
  160. new WarningDialog(WarningDialog.NO_COMPAT_UIS_TITLE,
  161. WarningDialog.NO_COMPAT_UIS_BODY).displayBlocking();
  162. }
  163. // Allow the rebooted DMDirc to know that we have attempted restarting.
  164. IdentityManager.getConfigIdentity().setOption("debug", "uiFixAttempted", "true");
  165. // Tell the launcher to restart!
  166. System.exit(42);
  167. }
  168. }
  169. /**
  170. * Called when the global config cannot be loaded due to an error. This
  171. * method informs the user of the problem and installs a new default config
  172. * file, backing up the old one.
  173. */
  174. private static void handleInvalidConfigFile() {
  175. final String date = new SimpleDateFormat("yyyyMMddkkmmss").format(new Date());
  176. final String message = "DMDirc has detected that your config file "
  177. + "has become corrupted.<br><br>DMDirc will now backup "
  178. + "your current config and try restarting with a default "
  179. + "config.<br><br>Your old config will be saved as:<br>"
  180. + "dmdirc.config." + date;
  181. if (!GraphicsEnvironment.isHeadless()) {
  182. new WarningDialog("Invalid Config File", message).displayBlocking();
  183. }
  184. // Let command-line users know what is happening.
  185. System.out.println(message.replace("<br>", "\n"));
  186. final File configFile = new File(getConfigDir() + "dmdirc.config");
  187. final File newConfigFile = new File(getConfigDir() + "dmdirc.config." + date);
  188. if (configFile.renameTo(newConfigFile)) {
  189. try {
  190. IdentityManager.load();
  191. } catch (InvalidIdentityFileException iife2) {
  192. // This shouldn't happen!
  193. Logger.appError(ErrorLevel.FATAL, "Unable to load global config", iife2);
  194. }
  195. } else {
  196. final String newMessage = "DMDirc was unable to rename the "
  197. + "global config file and is unable to fix this issue.";
  198. if (!GraphicsEnvironment.isHeadless()) {
  199. new WarningDialog("Invalid Config File", newMessage).displayBlocking();
  200. }
  201. System.out.println(newMessage.replace("<br>", "\n"));
  202. System.exit(1);
  203. }
  204. }
  205. /**
  206. * Ensures that there is at least one provider of the specified
  207. * service type by extracting matching core plugins. Plugins must be named
  208. * so that their file name starts with the service type, and then an
  209. * underscore.
  210. *
  211. * @param pm The plugin manager to use to access services
  212. * @param serviceType The type of service that should exist
  213. */
  214. public static void ensureExists(final PluginManager pm, final String serviceType) {
  215. if (pm.getServicesByType(serviceType).isEmpty()) {
  216. extractCorePlugins(serviceType + "_");
  217. pm.getPossiblePluginInfos(true);
  218. }
  219. }
  220. /**
  221. * Checks whether the plugins bundled with this release of DMDirc are newer
  222. * than the plugins known by the specified {@link PluginManager}. If the
  223. * bundled plugins are newer, they are automatically extracted.
  224. *
  225. * @param pm The plugin manager to use to check plugins
  226. * @param config The configuration source for bundled versions
  227. */
  228. private static void checkBundledPlugins(final PluginManager pm, final ConfigManager config) {
  229. for (PluginInfo plugin : pm.getPluginInfos()) {
  230. if (config.hasOptionString("bundledplugins_versions", plugin.getName())) {
  231. final Version bundled = new Version(config.getOption("bundledplugins_versions",
  232. plugin.getName()));
  233. final Version installed = plugin.getVersion();
  234. if (installed.compareTo(bundled) < 0) {
  235. extractCorePlugins(plugin.getName());
  236. PluginManager.getPluginManager().reloadPlugin(plugin.getFilename());
  237. }
  238. }
  239. }
  240. }
  241. /**
  242. * Attempts to find and activate a service which provides a UI that we
  243. * can use.
  244. *
  245. * @param pm The plugin manager to use to load plugins
  246. */
  247. protected static void loadUIs(final PluginManager pm) {
  248. final List<Service> uis = pm.getServicesByType("ui");
  249. // First try: go for our desired service type
  250. for (Service service : uis) {
  251. if (service.activate()) {
  252. final ServiceProvider provider = service.getActiveProvider();
  253. final Object export = provider.getExportedService("getController").execute();
  254. if (export == null) {
  255. if (provider instanceof PluginInfo
  256. && ((PluginInfo) provider).getPlugin() instanceof UIController) {
  257. // @Deprecated - remove post 0.6.4
  258. // Hack for compatibility with older plugins
  259. CONTROLLERS.add((UIController) ((PluginInfo) provider).getPlugin());
  260. Logger.appError(ErrorLevel.LOW,
  261. "UI plugin doesn't export getController: "
  262. + provider.getProviderName(), new UnsupportedOperationException());
  263. }
  264. } else {
  265. CONTROLLERS.add((UIController) export);
  266. }
  267. }
  268. }
  269. if (CONTROLLERS.isEmpty()) {
  270. handleMissingUI();
  271. } else {
  272. // The fix worked!
  273. if (IdentityManager.getGlobalConfig().hasOptionBool("debug", "uiFixAttempted")) {
  274. IdentityManager.getConfigIdentity().unsetOption("debug", "uiFixAttempted");
  275. }
  276. }
  277. }
  278. /**
  279. * Executes the first run or migration wizards as required.
  280. */
  281. private static void doFirstRun() {
  282. if (IdentityManager.getGlobalConfig().getOptionBool("general", "firstRun")) {
  283. IdentityManager.getConfigIdentity().setOption("general", "firstRun", "false");
  284. for (UIController controller : CONTROLLERS) {
  285. controller.showFirstRunWizard();
  286. }
  287. new Timer().schedule(new TimerTask() {
  288. /** {@inheritDoc} */
  289. @Override
  290. public void run() {
  291. for (UIController controller : CONTROLLERS) {
  292. controller.showFeedbackNag();
  293. }
  294. }
  295. }, FEEDBACK_DELAY);
  296. }
  297. }
  298. /**
  299. * Quits the client nicely, with the default closing message.
  300. */
  301. public static void quit() {
  302. quit(0);
  303. }
  304. /**
  305. * Quits the client nicely, with the default closing message.
  306. *
  307. * @param exitCode This is the exit code that will be returned to the
  308. * operating system when the client exits
  309. */
  310. public static void quit(final int exitCode) {
  311. quit(IdentityManager.getGlobalConfig().getOption("general",
  312. "closemessage"), exitCode);
  313. }
  314. /**
  315. * Quits the client nicely.
  316. *
  317. * @param reason The quit reason to send
  318. */
  319. public static void quit(final String reason) {
  320. quit(reason, 0);
  321. }
  322. /**
  323. * Quits the client nicely.
  324. *
  325. * @param reason The quit reason to send
  326. * @param exitCode This is the exit code that will be returned to the
  327. * operating system when the client exits
  328. */
  329. public static void quit(final String reason, final int exitCode) {
  330. ServerManager.getServerManager().disconnectAll(reason);
  331. System.exit(exitCode);
  332. }
  333. /**
  334. * Retrieves the UI controller that's being used by the client.
  335. *
  336. * @return The client's UI controller
  337. * @deprecated Shouldn't be used. There may be multiple or no controllers.
  338. */
  339. @Deprecated
  340. public static UIController getUI() {
  341. return CONTROLLERS.iterator().next();
  342. }
  343. /**
  344. * Sets the UI controller that should be used by this client.
  345. *
  346. * @param newController The new UI Controller
  347. * @deprecated Shouldn't be used. UI plugins should declare services.
  348. */
  349. @Deprecated
  350. public static synchronized void setUI(final UIController newController) {
  351. // Do nothing.
  352. }
  353. /**
  354. * Returns the application's config directory.
  355. *
  356. * @return configuration directory
  357. */
  358. public static String getConfigDir() {
  359. if (configdir == null) {
  360. initialiseConfigDir();
  361. }
  362. return configdir;
  363. }
  364. /**
  365. * Initialises the location of the configuration directory.
  366. */
  367. protected static void initialiseConfigDir() {
  368. final String fs = System.getProperty("file.separator");
  369. final String osName = System.getProperty("os.name");
  370. if (System.getenv("DMDIRC_HOME") != null) {
  371. configdir = System.getenv("DMDIRC_HOME");
  372. } else if (osName.startsWith("Mac OS")) {
  373. configdir = System.getProperty("user.home") + fs + "Library"
  374. + fs + "Preferences" + fs + "DMDirc" + fs;
  375. } else if (osName.startsWith("Windows")) {
  376. if (System.getenv("APPDATA") == null) {
  377. configdir = System.getProperty("user.home") + fs + "DMDirc" + fs;
  378. } else {
  379. configdir = System.getenv("APPDATA") + fs + "DMDirc" + fs;
  380. }
  381. } else {
  382. configdir = System.getProperty("user.home") + fs + ".DMDirc" + fs;
  383. final File testFile = new File(configdir);
  384. if (!testFile.exists()) {
  385. final String configHome = System.getenv("XDG_CONFIG_HOME");
  386. configdir = (configHome == null || configHome.isEmpty())
  387. ? System.getProperty("user.home") + fs + ".config" + fs
  388. : configHome;
  389. configdir += fs + "DMDirc" + fs;
  390. }
  391. }
  392. configdir = new File(configdir).getAbsolutePath() + fs;
  393. }
  394. /**
  395. * Sets the config directory for this client.
  396. *
  397. * @param newdir The new configuration directory
  398. */
  399. public static void setConfigDir(final String newdir) {
  400. configdir = newdir;
  401. }
  402. /**
  403. * Extracts plugins bundled with DMDirc to the user's profile's plugin
  404. * directory.
  405. *
  406. * @param prefix If non-null, only plugins whose file name starts with
  407. * this prefix will be extracted.
  408. */
  409. public static void extractCorePlugins(final String prefix) {
  410. final Map<String, byte[]> resources = ResourceManager.getResourceManager()
  411. .getResourcesStartingWithAsBytes("plugins");
  412. for (Map.Entry<String, byte[]> resource : resources.entrySet()) {
  413. try {
  414. final String resourceName = Main.getConfigDir() + "plugins"
  415. + resource.getKey().substring(7);
  416. if (prefix != null && !resource.getKey().substring(8).startsWith(prefix)) {
  417. continue;
  418. }
  419. final File newDir = new File(resourceName.substring(0,
  420. resourceName.lastIndexOf('/')) + "/");
  421. if (!newDir.exists()) {
  422. newDir.mkdirs();
  423. }
  424. final File newFile = new File(newDir,
  425. resourceName.substring(resourceName.lastIndexOf('/') + 1,
  426. resourceName.length()));
  427. if (!newFile.isDirectory()) {
  428. ResourceManager.getResourceManager().
  429. resourceToFile(resource.getValue(), newFile);
  430. }
  431. } catch (IOException ex) {
  432. Logger.userError(ErrorLevel.LOW, "Failed to extract plugins", ex);
  433. }
  434. }
  435. }
  436. }