View Javadoc
1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one or more
3    * contributor license agreements.  See the NOTICE file distributed with
4    * this work for additional information regarding copyright ownership.
5    * The ASF licenses this file to You under the Apache License, Version 2.0
6    * (the "License"); you may not use this file except in compliance with
7    * the License.  You may obtain a copy of the License at
8    *
9    *      http://www.apache.org/licenses/LICENSE-2.0
10   *
11   * Unless required by applicable law or agreed to in writing, software
12   * distributed under the License is distributed on an "AS IS" BASIS,
13   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14   * See the License for the specific language governing permissions and
15   * limitations under the License.
16   */
17  package org.apache.log4j.chainsaw.zeroconf;
18  
19  import java.awt.BorderLayout;
20  import java.awt.Component;
21  import java.awt.event.ActionEvent;
22  import java.awt.event.MouseAdapter;
23  import java.awt.event.MouseEvent;
24  import java.io.File;
25  import java.io.FileReader;
26  import java.io.FileWriter;
27  import java.util.HashMap;
28  import java.util.HashSet;
29  import java.util.Iterator;
30  import java.util.Map;
31  import java.util.Set;
32  
33  import javax.jmdns.JmDNS;
34  import javax.jmdns.ServiceEvent;
35  import javax.jmdns.ServiceInfo;
36  import javax.jmdns.ServiceListener;
37  import javax.swing.AbstractAction;
38  import javax.swing.ImageIcon;
39  import javax.swing.JFrame;
40  import javax.swing.JMenu;
41  import javax.swing.JMenuBar;
42  import javax.swing.JMenuItem;
43  import javax.swing.JPopupMenu;
44  import javax.swing.JScrollPane;
45  import javax.swing.JTable;
46  import javax.swing.JToolBar;
47  import javax.swing.SwingUtilities;
48  import javax.swing.event.TableModelEvent;
49  import javax.swing.event.TableModelListener;
50  
51  import org.apache.log4j.BasicConfigurator;
52  import org.apache.log4j.LogManager;
53  import org.apache.log4j.Logger;
54  import org.apache.log4j.chainsaw.ChainsawConstants;
55  import org.apache.log4j.chainsaw.LogFilePatternLayoutBuilder;
56  import org.apache.log4j.chainsaw.SmallButton;
57  import org.apache.log4j.chainsaw.help.HelpManager;
58  import org.apache.log4j.chainsaw.icons.ChainsawIcons;
59  import org.apache.log4j.chainsaw.plugins.GUIPluginSkeleton;
60  import org.apache.log4j.chainsaw.prefs.SettingsManager;
61  import org.apache.log4j.chainsaw.vfs.VFSLogFilePatternReceiver;
62  import org.apache.log4j.helpers.LogLog;
63  import org.apache.log4j.net.MulticastReceiver;
64  import org.apache.log4j.net.SocketHubReceiver;
65  import org.apache.log4j.net.SocketReceiver;
66  import org.apache.log4j.net.UDPReceiver;
67  import org.apache.log4j.net.XMLSocketReceiver;
68  import org.apache.log4j.net.ZeroConfSupport;
69  import org.apache.log4j.plugins.Plugin;
70  import org.apache.log4j.plugins.PluginEvent;
71  import org.apache.log4j.plugins.PluginListener;
72  import org.apache.log4j.plugins.Receiver;
73  import org.apache.log4j.spi.LoggerRepositoryEx;
74  
75  import com.thoughtworks.xstream.XStream;
76  import com.thoughtworks.xstream.io.xml.DomDriver;
77  
78  /**
79   * This plugin is designed to detect specific Zeroconf zones (Rendevouz/Bonjour,
80   * whatever people are calling it) and allow the user to double click on
81   * 'devices' to try and connect to them with no configuration needed.
82   * 
83   * TODO need to handle
84   * NON-log4j devices that may be broadcast in the interested zones 
85   * TODO add the
86   * default Zone, and the list of user-specified zones to a preferenceModel
87   * 
88   * @author psmith
89   * 
90   */
91  public class ZeroConfPlugin extends GUIPluginSkeleton {
92  
93      private static final Logger LOG = Logger.getLogger(ZeroConfPlugin.class);
94  
95      private ZeroConfDeviceModel discoveredDevices = new ZeroConfDeviceModel();
96  
97      private JTable deviceTable = new JTable(discoveredDevices);
98      
99      private final JScrollPane scrollPane = new JScrollPane(deviceTable);
100 
101     private ZeroConfPreferenceModel preferenceModel;
102     
103     private Map serviceInfoToReceiveMap = new HashMap();
104 
105     private JMenu connectToMenu = new JMenu("Connect to");
106     private JMenuItem helpItem = new JMenuItem(new AbstractAction("Learn more about ZeroConf...",
107             ChainsawIcons.ICON_HELP) {
108 
109         public void actionPerformed(ActionEvent e) {
110             HelpManager.getInstance()
111                     .showHelpForClass(ZeroConfPlugin.class);
112         }
113     });  
114     
115     private JMenuItem nothingToConnectTo = new JMenuItem("No devices discovered");
116     private static final String MULTICAST_APPENDER_SERVICE_NAME = "_log4j_xml_mcast_appender.local.";
117     private static final String UDP_APPENDER_SERVICE_NAME = "_log4j_xml_udp_appender.local.";
118     private static final String XML_SOCKET_APPENDER_SERVICE_NAME = "_log4j_xml_tcpconnect_appender.local.";
119     private static final String SOCKET_APPENDER_SERVICE_NAME = "_log4j_obj_tcpconnect_appender.local.";
120     private static final String SOCKETHUB_APPENDER_SERVICE_NAME = "_log4j_obj_tcpaccept_appender.local.";
121     private static final String TCP_APPENDER_SERVICE_NAME = "_log4j._tcp.local.";
122     private static final String NEW_UDP_APPENDER_SERVICE_NAME = "_log4j._udp.local.";
123 
124     private JmDNS jmDNS;
125 
126     public ZeroConfPlugin() {
127         setName("Zeroconf");
128         deviceTable.setRowHeight(ChainsawConstants.DEFAULT_ROW_HEIGHT);
129     }
130 
131     public void shutdown() {
132         if (jmDNS != null) {
133             try {
134                 jmDNS.close();
135             } catch (Exception e) {
136                 LOG.error("Unable to close JMDNS", e);
137             }
138         }
139         save();
140     }
141 
142     private void save() {
143         File fileLocation = getPreferenceFileLocation();
144         XStream stream = new XStream(new DomDriver());
145         try {
146             stream.toXML(preferenceModel, new FileWriter(fileLocation));
147         } catch (Exception e) {
148             LOG.error("Failed to save ZeroConfPlugin configuration file",e);
149         }
150     }
151 
152     private File getPreferenceFileLocation() {
153         return new File(SettingsManager.getInstance().getSettingsDirectory(), "zeroconfprefs.xml");
154     }
155 
156     public void activateOptions() {
157         setLayout(new BorderLayout());
158         jmDNS = (JmDNS) ZeroConfSupport.getJMDNSInstance();
159 
160         registerServiceListenersForAppenders();
161 
162         deviceTable.addMouseListener(new ConnectorMouseListener());
163 
164         
165         JToolBar toolbar = new JToolBar();
166         SmallButton helpButton = new SmallButton(helpItem.getAction());
167         helpButton.setText(helpItem.getText());
168         toolbar.add(helpButton);
169         toolbar.setFloatable(false);
170         add(toolbar, BorderLayout.NORTH);
171         add(scrollPane, BorderLayout.CENTER);
172         
173         injectMenu();
174         
175         ((LoggerRepositoryEx)LogManager.getLoggerRepository()).getPluginRegistry().addPluginListener(new PluginListener() {
176 
177             public void pluginStarted(PluginEvent e) {
178                 
179             }
180 
181             public void pluginStopped(PluginEvent e) {
182                 Plugin plugin = e.getPlugin();
183                 synchronized(serviceInfoToReceiveMap) {
184                     for (Iterator iter = serviceInfoToReceiveMap.entrySet().iterator(); iter.hasNext();) {
185                         Map.Entry entry = (Map.Entry) iter.next();
186                         if(entry.getValue() == plugin) {
187                                 iter.remove();
188                         }
189                     }
190                 }
191 //                 need to make sure that the menu item tracking this item has it's icon and enabled state updade
192                 discoveredDevices.fireTableDataChanged();
193             }});
194 
195         File fileLocation = getPreferenceFileLocation();
196         XStream stream = new XStream(new DomDriver());
197         if (fileLocation.exists()) {
198             try {
199                 this.preferenceModel = (ZeroConfPreferenceModel) stream
200                         .fromXML(new FileReader(fileLocation));
201             } catch (Exception e) {
202                 LOG.error("Failed to load ZeroConfPlugin configuration file",e);
203             }
204         }else {
205             this.preferenceModel = new ZeroConfPreferenceModel();
206         }
207         discoveredDevices.setZeroConfPreferenceModel(preferenceModel);
208         discoveredDevices.setZeroConfPluginParent(this);
209     }
210 
211     private void registerServiceListenersForAppenders()
212     {
213         Set serviceNames = new HashSet();
214         serviceNames.add(MULTICAST_APPENDER_SERVICE_NAME);
215         serviceNames.add(SOCKET_APPENDER_SERVICE_NAME);
216         serviceNames.add(SOCKETHUB_APPENDER_SERVICE_NAME);
217         serviceNames.add(UDP_APPENDER_SERVICE_NAME);
218         serviceNames.add(XML_SOCKET_APPENDER_SERVICE_NAME);
219         serviceNames.add(TCP_APPENDER_SERVICE_NAME);
220         serviceNames.add(NEW_UDP_APPENDER_SERVICE_NAME);
221 
222         for (Iterator iter = serviceNames.iterator(); iter.hasNext();) {
223             String serviceName = iter.next().toString();
224             jmDNS.addServiceListener(
225                     serviceName,
226                     new ZeroConfServiceListener());
227 
228             jmDNS.addServiceListener(serviceName, discoveredDevices);
229         }
230 
231         //now add each appender constant
232     }
233 
234     /**
235      * Attempts to find a JFrame container as a parent,and addse a "Connect to" menu
236      *
237      */
238     private void injectMenu() {
239         
240         JFrame frame = (JFrame) SwingUtilities.getWindowAncestor(this);
241         if(frame == null) {
242             LOG.info("Could not locate parent JFrame to add menu to");
243         }else {
244             JMenuBar menuBar = frame.getJMenuBar();
245             if(menuBar==null ) {
246                 menuBar = new JMenuBar();
247                 frame.setJMenuBar(menuBar);
248             }
249             insertToLeftOfHelp(menuBar, connectToMenu);
250             connectToMenu.add(nothingToConnectTo);
251             
252             discoveredDevices.addTableModelListener(new TableModelListener (){
253 
254                 public void tableChanged(TableModelEvent e) {
255                     if(discoveredDevices.getRowCount()==0) {
256                         connectToMenu.add(nothingToConnectTo,0);
257                     }else if(discoveredDevices.getRowCount()>0) {
258                         connectToMenu.remove(nothingToConnectTo);
259                     }
260                     
261                 }});
262             
263             nothingToConnectTo.setEnabled(false);
264 
265             connectToMenu.addSeparator();
266             connectToMenu.add(helpItem);
267         }
268     }
269 
270     /**
271      * Hack method to locate the JMenu that is the Help menu, and inserts the new menu
272      * just to the left of it.
273      * @param menuBar
274      * @param item
275      */
276     private void insertToLeftOfHelp(JMenuBar menuBar, JMenu item) {
277         for (int i = 0; i < menuBar.getMenuCount(); i++) {
278             JMenu menu = menuBar.getMenu(i);
279             if(menu.getText().equalsIgnoreCase("help")) {
280                 menuBar.add(item, i-1);
281             }
282         }
283         LOG.warn("menu '" + item.getText() + "' was NOT added because the 'Help' menu could not be located");
284     }
285 
286     /**
287      * When a device is discovered, we create a menu item for it so it can be connected to via that
288      * GUI mechanism, and also if the device is one of the auto-connect devices then a background thread
289      * is created to connect the device.
290      * @param info
291      */
292     private void deviceDiscovered(final ServiceInfo info) {
293         final String name = info.getName();
294 //        TODO currently adding ALL devices to autoConnectlist
295 //        preferenceModel.addAutoConnectDevice(name);
296         
297         
298         JMenuItem connectToDeviceMenuItem = new JMenuItem(new AbstractAction(info.getName()) {
299 
300             public void actionPerformed(ActionEvent e) {
301                 connectTo(info);
302             }});
303         
304         if(discoveredDevices.getRowCount()>0) {
305             Component[] menuComponents = connectToMenu.getMenuComponents();
306             boolean located = false;
307             for (int i = 0; i < menuComponents.length; i++) {
308                 Component c = menuComponents[i];
309                 if (!(c instanceof JPopupMenu.Separator)) {
310                     JMenuItem item = (JMenuItem) menuComponents[i];
311                     if (item.getText().compareToIgnoreCase(name) < 0) {
312                         connectToMenu.insert(connectToDeviceMenuItem, i);
313                         located = true;
314                         break;
315                     }
316                 }
317             }
318             if(!located) {
319                 connectToMenu.insert(connectToDeviceMenuItem,0);
320             }
321         }else {
322             connectToMenu.insert(connectToDeviceMenuItem,0);
323         }
324 //         if the device name is one of the autoconnect devices, then connect immediately
325         if (preferenceModel != null && preferenceModel.getAutoConnectDevices() != null && preferenceModel.getAutoConnectDevices().contains(name)) {
326             new Thread(new Runnable() {
327 
328                 public void run() {
329                     LOG.info("Auto-connecting to " + name);
330                     connectTo(info);
331                 }
332             }).start();
333         }
334     }
335     
336     /**
337      * When a device is removed or disappears we need to remove any JMenu item associated with it.
338      * @param name
339      */
340     private void deviceRemoved(String name) {
341         Component[] menuComponents = connectToMenu.getMenuComponents();
342         for (int i = 0; i < menuComponents.length; i++) {
343             Component c = menuComponents[i];
344             if (!(c instanceof JPopupMenu.Separator)) {
345                 JMenuItem item = (JMenuItem) menuComponents[i];
346                 if (item.getText().compareToIgnoreCase(name) == 0) {
347                     connectToMenu.remove(item);
348                     break;
349                 }
350             }
351         }
352     }
353         
354     /**
355      * Listens out on the JmDNS/ZeroConf network for new devices that appear
356      * and adds/removes these device information from the list/model.
357      *
358      */
359     private class ZeroConfServiceListener implements ServiceListener {
360 
361         public void serviceAdded(final ServiceEvent event) {
362             LOG.info("Service Added: " + event);
363             /**
364              * it's not very clear whether we should do the resolving in a
365              * background thread or not.. All it says is to NOT do it in the AWT
366              * thread, so I'm thinking it probably should be a background thread
367              */
368             Runnable runnable = new Runnable() {
369                 public void run() {
370                     ZeroConfPlugin.this.jmDNS.requestServiceInfo(event
371                             .getType(), event.getName());
372                 }
373             };
374             Thread thread = new Thread(runnable,
375                     "ChainsawZeroConfRequestResolutionThread");
376             thread.setPriority(Thread.MIN_PRIORITY);
377             thread.start();
378         }
379 
380         public void serviceRemoved(ServiceEvent event) {
381             LOG.info("Service Removed: " + event);
382             deviceRemoved(event.getName());
383         }
384 
385         public void serviceResolved(ServiceEvent event) {
386             LOG.info("Service Resolved: " + event);
387             deviceDiscovered(event.getInfo());
388         }
389 
390     }
391 
392 
393     /**
394      * When the user double clicks on a row, then the device is connected to,
395      * the only exception is when clicking in the check box column for auto connect.
396      */
397     private class ConnectorMouseListener extends MouseAdapter {
398 
399         public void mouseClicked(MouseEvent e) {
400             if (e.getClickCount() == 2) {
401                 int row = deviceTable.rowAtPoint(e.getPoint());
402                 if(deviceTable.columnAtPoint(e.getPoint())==2) {
403                     return;
404                 }
405                 ServiceInfo info = discoveredDevices.getServiceInfoAtRow(row);
406                 
407                 if (!isConnectedTo(info)) {
408                     connectTo(info);
409                 } else {
410                     disconnectFrom(info);
411                 }
412             }
413         }
414 
415         public void mousePressed(MouseEvent e) {
416             /**
417              * This methodh handles when the user clicks the
418              * auto-connect
419              */
420 //            int index = listBox.locationToIndex(e.getPoint());
421 //
422 //            if (index != -1) {
423 ////                Point p = SwingUtilities.convertPoint(e.getComponent(), e.getPoint(), )
424 //                Component c = SwingUtilities.getDeepestComponentAt(ZeroConfPlugin.this, e.getX(), e.getY());
425 //                if (c instanceof JCheckBox) {
426 //                    ServiceInfo info = (ServiceInfo) listBox.getModel()
427 //                            .getElementAt(index);
428 //                    String name = info.getName();
429 //                    if (preferenceModel.getAutoConnectDevices().contains(name)) {
430 //                        preferenceModel.removeAutoConnectDevice(name);
431 //                    } else {
432 //                        preferenceModel.addAutoConnectDevice(name);
433 //                    }
434 //                    discoveredDevices.fireContentsChanged();
435 //                    repaint();
436 //                }
437 //            }
438         }
439     }
440 
441     private void disconnectFrom(ServiceInfo info) {
442         if(!isConnectedTo(info)) {
443             return; // not connected, who cares
444         }
445         Plugin plugin;
446         synchronized (serviceInfoToReceiveMap) {
447             plugin = (Plugin) serviceInfoToReceiveMap.get(info);
448         }
449         ((LoggerRepositoryEx)LogManager.getLoggerRepository()).getPluginRegistry().stopPlugin(plugin.getName());
450         
451         JMenuItem item = locateMatchingMenuItem(info.getName());
452         if (item!=null) {
453             item.setIcon(null);
454             item.setEnabled(true);
455         }
456     }
457     /**
458      * returns true if the serviceInfo record already has a matching connected receiver
459      * @param info
460      * @return
461      */
462     boolean isConnectedTo(ServiceInfo info) {
463         return serviceInfoToReceiveMap.containsKey(info);
464     }
465     /**
466      * Starts a receiver to the appender referenced within the ServiceInfo
467      * @param info
468      */
469     private void connectTo(ServiceInfo info) {
470         LOG.info("Connection request for " + info);
471         //Chainsaw can construct receivers from discovered appenders
472         Receiver receiver = getReceiver(info);
473         //if null, unable to resolve the service name..no-op
474         if (receiver == null) {
475             return;
476         }
477         ((LoggerRepositoryEx)LogManager.getLoggerRepository()).getPluginRegistry().addPlugin(receiver);
478         receiver.activateOptions();
479         LOG.info("Receiver '" + receiver.getName() + "' has been started");
480         
481         // ServiceInfo obeys equals() and hashCode() contracts, so this should be safe.
482         synchronized (serviceInfoToReceiveMap) {
483             serviceInfoToReceiveMap.put(info, receiver);
484         }
485         
486 //         this instance of the menu item needs to be disabled, and have an icon added
487         JMenuItem item = locateMatchingMenuItem(info.getName());
488         if (item!=null) {
489             item.setIcon(new ImageIcon(ChainsawIcons.ANIM_NET_CONNECT));
490             item.setEnabled(false);
491         }
492 //        // now notify the list model has changed, it needs redrawing of the receiver icon now it's connected
493 //        discoveredDevices.fireContentsChanged();
494     }
495 
496     private Receiver getReceiver(ServiceInfo info) {
497         String zone = info.getType();
498         int port = info.getPort();
499         String hostAddress = info.getHostAddress();
500         String name = info.getName();
501         String decoderClass = info.getPropertyString("decoder");
502 
503         if (NEW_UDP_APPENDER_SERVICE_NAME.equals(zone))
504         {
505             UDPReceiver receiver = new UDPReceiver();
506             receiver.setPort(port);
507             receiver.setName(name + "-receiver");
508             return receiver;
509         }
510         //FileAppender or socketappender
511         //TODO: add more checks (actual layout format, etc)
512         if (TCP_APPENDER_SERVICE_NAME.equals(zone)) {
513             //CHECK content type
514             //application/octet-stream = SocketReceiver
515             //text/plain = VFSLogFilePatternReceiver (if structured=false)
516             String contentType = info.getPropertyString("contentType").toLowerCase();
517             //won't work with log4j2, as Chainsaw depends on log4j1.x
518             if ("application/octet-stream".equals(contentType))
519             {
520                 SocketReceiver receiver = new SocketReceiver();
521                 receiver.setPort(port);
522                 receiver.setName(name + "-receiver");
523                 return receiver;
524             }
525             //this will work - regular text log files are fine
526             if ("text/plain".equals(contentType))
527             {
528                 VFSLogFilePatternReceiver receiver = new VFSLogFilePatternReceiver();
529                 receiver.setAppendNonMatches(true);
530                 receiver.setFileURL(info.getPropertyString("fileURI"));
531                 receiver.setLogFormat(LogFilePatternLayoutBuilder.getLogFormatFromPatternLayout(info.getPropertyString("format")));
532                 receiver.setTimestampFormat(LogFilePatternLayoutBuilder.getTimeStampFormat(info.getPropertyString("format")));
533                 receiver.setName(name + "-receiver");
534                 receiver.setTailing(true);
535                 return receiver;
536             }
537         }
538 
539         //MulticastAppender
540         if (MULTICAST_APPENDER_SERVICE_NAME.equals(zone)) {
541             MulticastReceiver receiver = new MulticastReceiver();
542             //this needs to be a multicast address, not the host address, so we need to use a property
543             receiver.setAddress(info.getPropertyString("multicastAddress"));
544             receiver.setPort(port);
545             receiver.setName(name + "-receiver");
546             if (decoderClass != null && !decoderClass.equals("")) {
547                 receiver.setDecoder(decoderClass);
548             }
549 
550             return receiver;
551         }
552         //UDPAppender
553         if (UDP_APPENDER_SERVICE_NAME.equals(zone)) {
554             UDPReceiver receiver = new UDPReceiver();
555             receiver.setPort(port);
556             receiver.setName(name + "-receiver");
557             if (decoderClass != null && !decoderClass.equals("")) {
558                 receiver.setDecoder(decoderClass);
559             }
560             return receiver;
561         }
562 
563         //non-log4j XML-based socketappender
564         if (XML_SOCKET_APPENDER_SERVICE_NAME.equals(zone)) {
565             XMLSocketReceiver receiver = new XMLSocketReceiver();
566             receiver.setPort(port);
567             receiver.setName(name + "-receiver");
568             if (decoderClass != null && !decoderClass.equals("")) {
569                 receiver.setDecoder(decoderClass);
570             }
571             return receiver;
572         }
573 
574         //SocketAppender
575         if (SOCKET_APPENDER_SERVICE_NAME.equals(zone)) {
576             SocketReceiver receiver = new SocketReceiver();
577             receiver.setPort(port);
578             receiver.setName(name + "-receiver");
579             return receiver;
580         }
581 
582         //SocketHubAppender
583         if (SOCKETHUB_APPENDER_SERVICE_NAME.equals(zone)) {
584             SocketHubReceiver receiver = new SocketHubReceiver();
585             receiver.setHost(hostAddress);
586             receiver.setPort(port);
587             receiver.setName(name + "-receiver");
588             return receiver;
589         }
590         //not recognized
591         LogLog.debug("Unable to find receiver for appender with service name: " + zone);
592         return null;
593     }
594 
595     /**
596      * Finds the matching JMenuItem based on name, may return null if there is no match.
597      * 
598      * @param name
599      * @return
600      */
601     private JMenuItem locateMatchingMenuItem(String name) {
602         Component[] menuComponents = connectToMenu.getMenuComponents();
603         for (int i = 0; i < menuComponents.length; i++) {
604             Component c = menuComponents[i];
605             if (!(c instanceof JPopupMenu.Separator)) {
606                 JMenuItem item = (JMenuItem) menuComponents[i];
607                 if (item.getText().compareToIgnoreCase(name) == 0) {
608                     return item;
609                 }
610             }
611         }
612         return null;
613     }
614 
615     public static void main(String[] args) throws InterruptedException {
616 
617         BasicConfigurator.resetConfiguration();
618         BasicConfigurator.configure();
619 
620         final ZeroConfPlugin plugin = new ZeroConfPlugin();
621 
622 
623         JFrame frame = new JFrame();
624         frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
625 
626         frame.getContentPane().setLayout(new BorderLayout());
627         frame.getContentPane().add(plugin, BorderLayout.CENTER);
628 
629         // needs to be activated after being added to the JFrame for Menu injection to work
630         plugin.activateOptions();
631 
632         frame.pack();
633         frame.setVisible(true);
634 
635         Thread thread = new Thread(new Runnable() {
636             public void run() {
637                 plugin.shutdown();
638             }
639         });
640         Runtime.getRuntime().addShutdownHook(thread);
641     }
642 
643 }