1 /* 2 * Copyright (c) 2006, 2011, Oracle and/or its affiliates. All rights reserved. 3 * 4 * Redistribution and use in source and binary forms, with or without 5 * modification, are permitted provided that the following conditions 6 * are met: 7 * 8 * - Redistributions of source code must retain the above copyright 9 * notice, this list of conditions and the following disclaimer. 10 * 11 * - Redistributions in binary form must reproduce the above copyright 12 * notice, this list of conditions and the following disclaimer in the 13 * documentation and/or other materials provided with the distribution. 14 * 15 * - Neither the name of Oracle nor the names of its 16 * contributors may be used to endorse or promote products derived 17 * from this software without specific prior written permission. 18 * 19 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS 20 * IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, 21 * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR 22 * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR 23 * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, 24 * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, 25 * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR 26 * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF 27 * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING 28 * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 29 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 30 */ 31 32 /* 33 * This source code is provided to illustrate the usage of a given feature 34 * or technique and has been deliberately simplified. Additional steps 35 * required for a production-quality application, such as security checks, 36 * input validation and proper error handling, might not be present in 37 * this sample code. 38 */ 39 40 41 package com.sun.jmx.examples.scandir; 42 43 import static com.sun.jmx.examples.scandir.ScanManagerMXBean.ScanState.*; 44 import com.sun.jmx.examples.scandir.ScanManagerMXBean.ScanState; 45 import com.sun.jmx.examples.scandir.config.DirectoryScannerConfig; 46 import com.sun.jmx.examples.scandir.config.ScanManagerConfig; 47 import java.io.File; 48 49 import java.io.IOException; 50 import java.lang.management.ManagementFactory; 51 import java.util.ArrayList; 52 import java.util.Collections; 53 import java.util.EnumSet; 54 import java.util.HashMap; 55 import java.util.Map; 56 import java.util.Map.Entry; 57 import java.util.Timer; 58 import java.util.TimerTask; 59 import java.util.concurrent.BlockingQueue; 60 import java.util.concurrent.ConcurrentHashMap; 61 import java.util.concurrent.ConcurrentLinkedQueue; 62 import java.util.concurrent.LinkedBlockingQueue; 63 import java.util.concurrent.Semaphore; 64 import java.util.concurrent.TimeUnit; 65 import java.util.logging.Level; 66 import java.util.logging.Logger; 67 import javax.management.AttributeChangeNotification; 68 import javax.management.InstanceNotFoundException; 69 import javax.management.JMException; 70 import javax.management.JMX; 71 import javax.management.ListenerNotFoundException; 72 import javax.management.MBeanNotificationInfo; 73 import javax.management.MBeanRegistration; 74 import javax.management.MBeanServer; 75 import javax.management.MBeanServerConnection; 76 import javax.management.MalformedObjectNameException; 77 import javax.management.Notification; 78 import javax.management.NotificationBroadcasterSupport; 79 import javax.management.NotificationEmitter; 80 import javax.management.NotificationFilter; 81 import javax.management.NotificationListener; 82 import javax.management.ObjectInstance; 83 import javax.management.ObjectName; 84 85 /** 86 * <p> 87 * The <code>ScanManager</code> is responsible for applying a configuration, 88 * starting and scheduling directory scans, and reporting application state. 89 * </p> 90 * <p> 91 * The ScanManager MBean is a singleton MBean which controls 92 * scan session. The ScanManager name is defined by 93 * {@link #SCAN_MANAGER_NAME ScanManager.SCAN_MANAGER_NAME}. 94 * </p> 95 * <p> 96 * The <code>ScanManager</code> MBean is the entry point of the <i>scandir</i> 97 * application management interface. It is from this MBean that all other MBeans 98 * will be created and registered. 99 * </p> 100 * 101 * @author Sun Microsystems, 2006 - All rights reserved. 102 */ 103 public class ScanManager implements ScanManagerMXBean, 104 NotificationEmitter, MBeanRegistration { 105 106 /** 107 * A logger for this class. 108 **/ 109 private static final Logger LOG = 110 Logger.getLogger(ScanManager.class.getName()); 111 112 /** 113 * The name of the ScanManager singleton MBean. 114 **/ 115 public final static ObjectName SCAN_MANAGER_NAME = 116 makeSingletonName(ScanManagerMXBean.class); 117 118 /** 119 * Sequence number used for sending notifications. We use this 120 * sequence number throughout the application. 121 **/ 122 private static long seqNumber=0; 123 124 /** 125 * The NotificationBroadcasterSupport object used to handle 126 * listener registration. 127 **/ 128 private final NotificationBroadcasterSupport broadcaster; 129 130 /** 131 * The MBeanServer in which this MBean is registered. We obtain 132 * this reference by implementing the {@link MBeanRegistration} 133 * interface. 134 **/ 135 private volatile MBeanServer mbeanServer; 136 137 /** 138 * A queue of pending notifications we are about to send. 139 * We're using a BlockingQueue in order to avoid sending 140 * notifications from within a synchronized block. 141 **/ 142 private final BlockingQueue<Notification> pendingNotifs; 143 144 /** 145 * The state of the scan session. 146 **/ 147 private volatile ScanState state = STOPPED; 148 149 /** 150 * The list of DirectoryScannerMBean that are run by a scan session. 151 **/ 152 private final Map<ObjectName,DirectoryScannerMXBean> scanmap; 153 154 /** 155 * The list of ScanDirConfigMXBean that were created by this MBean. 156 **/ 157 private final Map<ObjectName, ScanDirConfigMXBean> configmap; 158 159 // The ResultLogManager for this application. 160 private final ResultLogManager log; 161 162 /** 163 * We use a semaphore to ensure proper sequencing of exclusive 164 * action. The logic we have implemented is to fail - rather 165 * than block, if an exclusive action is already in progress. 166 **/ 167 private final Semaphore sequencer = new Semaphore(1); 168 169 // A proxy to the current ScanDirConfigMXBean which holds the current 170 // configuration data. 171 // 172 private volatile ScanDirConfigMXBean config = null; 173 174 // Avoid to write parameters twices when creating a new ConcurrentHashMap. 175 // 176 private static <K, V> Map<K, V> newConcurrentHashMap() { 177 return new ConcurrentHashMap<K, V>(); 178 } 179 180 // Avoid to write parameters twices when creating a new HashMap. 181 // 182 private static <K, V> Map<K, V> newHashMap() { 183 return new HashMap<K, V>(); 184 } 185 186 /** 187 * Creates a default singleton ObjectName for a given class. 188 * @param clazz The interface class of the MBean for which we want to obtain 189 * a default singleton name, or its implementation class. 190 * Give one or the other depending on what you wish to see in 191 * the value of the key {@code type=}. 192 * @return A default singleton name for a singleton MBean class. 193 * @throws IllegalArgumentException if the name can't be created 194 * for some unfathomable reason (e.g. an unexpected 195 * exception was raised). 196 **/ 197 public final static ObjectName makeSingletonName(Class clazz) { 198 try { 199 final Package p = clazz.getPackage(); 200 final String packageName = (p==null)?null:p.getName(); 201 final String className = clazz.getSimpleName(); 202 final String domain; 203 if (packageName == null || packageName.length()==0) { 204 // We use a reference to ScanDirAgent.class to ease 205 // to keep track of possible class renaming. 206 domain = ScanDirAgent.class.getSimpleName(); 207 } else { 208 domain = packageName; 209 } 210 final ObjectName name = new ObjectName(domain,"type",className); 211 return name; 212 } catch (Exception x) { 213 final IllegalArgumentException iae = 214 new IllegalArgumentException(String.valueOf(clazz),x); 215 throw iae; 216 } 217 } 218 219 /** 220 * Creates a default ObjectName with keys <code>type=</code> and 221 * <code>name=</code> for an instance of a given MBean interface class. 222 * @param clazz The interface class of the MBean for which we want to obtain 223 * a default name, or its implementation class. 224 * Give one or the other depending on what you wish to see in 225 * the value of the key {@code type=}. 226 * @param name The value of the <code>name=</code> key. 227 * @return A default name for an instance of the given MBean interface class. 228 * @throws IllegalArgumentException if the name can't be created. 229 * (e.g. an unexpected exception was raised). 230 **/ 231 public static final ObjectName makeMBeanName(Class clazz, String name) { 232 try { 233 return ObjectName. 234 getInstance(makeSingletonName(clazz) 235 .toString()+",name="+name); 236 } catch (MalformedObjectNameException x) { 237 final IllegalArgumentException iae = 238 new IllegalArgumentException(String.valueOf(name),x); 239 throw iae; 240 } 241 } 242 243 /** 244 * Return the ObjectName for a DirectoryScannerMXBean of that name. 245 * This is {@code makeMBeanName(DirectoryScannerMXBean.class,name)}. 246 * @param name The value of the <code>name=</code> key. 247 * @return the ObjectName for a DirectoryScannerMXBean of that name. 248 */ 249 public static final ObjectName makeDirectoryScannerName(String name) { 250 return makeMBeanName(DirectoryScannerMXBean.class,name); 251 } 252 253 /** 254 * Return the ObjectName for a {@code ScanDirConfigMXBean} of that name. 255 * This is {@code makeMBeanName(ScanDirConfigMXBean.class,name)}. 256 * @param name The value of the <code>name=</code> key. 257 * @return the ObjectName for a {@code ScanDirConfigMXBean} of that name. 258 */ 259 public static final ObjectName makeScanDirConfigName(String name) { 260 return makeMBeanName(ScanDirConfigMXBean.class,name); 261 } 262 263 /** 264 * Create and register a new singleton instance of the ScanManager 265 * MBean in the given {@link MBeanServerConnection}. 266 * @param mbs The MBeanServer in which the new singleton instance 267 * should be created. 268 * @throws JMException The MBeanServer connection raised an exception 269 * while trying to instantiate and register the singleton MBean 270 * instance. 271 * @throws IOException There was a connection problem while trying to 272 * communicate with the underlying MBeanServer. 273 * @return A proxy for the registered MBean. 274 **/ 275 public static ScanManagerMXBean register(MBeanServerConnection mbs) 276 throws IOException, JMException { 277 final ObjectInstance moi = 278 mbs.createMBean(ScanManager.class.getName(),SCAN_MANAGER_NAME); 279 final ScanManagerMXBean proxy = 280 JMX.newMXBeanProxy(mbs,moi.getObjectName(), 281 ScanManagerMXBean.class,true); 282 return proxy; 283 } 284 285 /** 286 * Creates a new {@code ScanManagerMXBean} proxy over the given 287 * {@code MBeanServerConnection}. Does not check whether a 288 * {@code ScanManagerMXBean} 289 * is actually registered in that {@code MBeanServerConnection}. 290 * @return a new {@code ScanManagerMXBean} proxy. 291 * @param mbs The {@code MBeanServerConnection} which holds the 292 * {@code ScanManagerMXBean} to proxy. 293 */ 294 public static ScanManagerMXBean 295 newSingletonProxy(MBeanServerConnection mbs) { 296 final ScanManagerMXBean proxy = 297 JMX.newMXBeanProxy(mbs,SCAN_MANAGER_NAME, 298 ScanManagerMXBean.class,true); 299 return proxy; 300 } 301 302 /** 303 * Creates a new {@code ScanManagerMXBean} proxy over the platform 304 * {@code MBeanServer}. This is equivalent to 305 * {@code newSingletonProxy(ManagementFactory.getPlatformMBeanServer())}. 306 * @return a new {@code ScanManagerMXBean} proxy. 307 **/ 308 public static ScanManagerMXBean newSingletonProxy() { 309 return newSingletonProxy(ManagementFactory.getPlatformMBeanServer()); 310 } 311 312 /** 313 * Create and register a new singleton instance of the ScanManager 314 * MBean in the given {@link MBeanServerConnection}. 315 * @throws JMException The MBeanServer connection raised an exception 316 * while trying to instantiate and register the singleton MBean 317 * instance. 318 * @throws IOException There was a connection problem while trying to 319 * communicate with the underlying MBeanServer. 320 * @return A proxy for the registered MBean. 321 **/ 322 public static ScanManagerMXBean register() 323 throws IOException, JMException { 324 final MBeanServer mbs = ManagementFactory.getPlatformMBeanServer(); 325 return register(mbs); 326 } 327 328 /** 329 * Create a new ScanManager MBean 330 **/ 331 public ScanManager() { 332 broadcaster = new NotificationBroadcasterSupport(); 333 pendingNotifs = new LinkedBlockingQueue<Notification>(100); 334 scanmap = newConcurrentHashMap(); 335 configmap = newConcurrentHashMap(); 336 log = new ResultLogManager(); 337 } 338 339 340 // Creates a new DirectoryScannerMXBean, from the given configuration data. 341 DirectoryScannerMXBean createDirectoryScanner(DirectoryScannerConfig config) { 342 return new DirectoryScanner(config,log); 343 } 344 345 // Applies a configuration. 346 // throws IllegalStateException if lock can't be acquired. 347 // Unregisters all existing directory scanners, the create and registers 348 // new directory scanners according to the given config. 349 // Then pushes the log config to the result log manager. 350 // 351 private void applyConfiguration(ScanManagerConfig bean) 352 throws IOException, JMException { 353 if (bean == null) return; 354 if (!sequencer.tryAcquire()) { 355 throw new IllegalStateException("Can't acquire lock"); 356 } 357 try { 358 unregisterScanners(); 359 final DirectoryScannerConfig[] scans = bean.getScanList(); 360 if (scans == null) return; 361 for (DirectoryScannerConfig scan : scans) { 362 addDirectoryScanner(scan); 363 } 364 log.setConfig(bean.getInitialResultLogConfig()); 365 } finally { 366 sequencer.release(); 367 } 368 } 369 370 // See ScanManagerMXBean 371 public void applyConfiguration(boolean fromMemory) 372 throws IOException, JMException { 373 if (fromMemory == false) config.load(); 374 applyConfiguration(config.getConfiguration()); 375 } 376 377 // See ScanManagerMXBean 378 public void applyCurrentResultLogConfig(boolean toMemory) 379 throws IOException, JMException { 380 final ScanManagerConfig bean = config.getConfiguration(); 381 bean.setInitialResultLogConfig(log.getConfig()); 382 config.setConfiguration(bean); 383 if (toMemory==false) config.save(); 384 } 385 386 // See ScanManagerMXBean 387 public void setConfigurationMBean(ScanDirConfigMXBean config) { 388 this.config = config; 389 } 390 391 // See ScanManagerMXBean 392 public ScanDirConfigMXBean getConfigurationMBean() { 393 return config; 394 } 395 396 // Creates and registers a new directory scanner. 397 // Called by applyConfiguration. 398 // throws IllegalStateException if state is not STOPPED or COMPLETED 399 // (you cannot change the config while scanning is scheduled or running). 400 // 401 private DirectoryScannerMXBean addDirectoryScanner( 402 DirectoryScannerConfig bean) 403 throws JMException { 404 try { 405 final DirectoryScannerMXBean scanner; 406 final ObjectName scanName; 407 synchronized (this) { 408 if (state != STOPPED && state != COMPLETED) 409 throw new IllegalStateException(state.toString()); 410 scanner = createDirectoryScanner(bean); 411 scanName = makeDirectoryScannerName(bean.getName()); 412 } 413 LOG.fine("server: "+mbeanServer); 414 LOG.fine("scanner: "+scanner); 415 LOG.fine("scanName: "+scanName); 416 final ObjectInstance moi = 417 mbeanServer.registerMBean(scanner,scanName); 418 final ObjectName moiName = moi.getObjectName(); 419 final DirectoryScannerMXBean proxy = 420 JMX.newMXBeanProxy(mbeanServer,moiName, 421 DirectoryScannerMXBean.class,true); 422 scanmap.put(moiName,proxy); 423 return proxy; 424 } catch (RuntimeException x) { 425 final String msg = "Operation failed: "+x; 426 if (LOG.isLoggable(Level.FINEST)) 427 LOG.log(Level.FINEST,msg,x); 428 else LOG.fine(msg); 429 throw x; 430 } catch (JMException x) { 431 final String msg = "Operation failed: "+x; 432 if (LOG.isLoggable(Level.FINEST)) 433 LOG.log(Level.FINEST,msg,x); 434 else LOG.fine(msg); 435 throw x; 436 } 437 } 438 439 // See ScanManagerMXBean 440 public ScanDirConfigMXBean createOtherConfigurationMBean(String name, 441 String filename) 442 throws JMException { 443 final ScanDirConfig profile = new ScanDirConfig(filename); 444 final ObjectName profName = makeScanDirConfigName(name); 445 final ObjectInstance moi = mbeanServer.registerMBean(profile,profName); 446 final ScanDirConfigMXBean proxy = 447 JMX.newMXBeanProxy(mbeanServer,profName, 448 ScanDirConfigMXBean.class,true); 449 configmap.put(moi.getObjectName(),proxy); 450 return proxy; 451 } 452 453 454 // See ScanManagerMXBean 455 public Map<String,DirectoryScannerMXBean> getDirectoryScanners() { 456 final Map<String,DirectoryScannerMXBean> proxyMap = newHashMap(); 457 for (Entry<ObjectName,DirectoryScannerMXBean> item : scanmap.entrySet()){ 458 proxyMap.put(item.getKey().getKeyProperty("name"),item.getValue()); 459 } 460 return proxyMap; 461 } 462 463 // --------------------------------------------------------------- 464 // State Management 465 // --------------------------------------------------------------- 466 467 /** 468 * For each operation, this map stores a list of states from 469 * which the corresponding operation can be legally called. 470 * For instance, it is legal to call "stop" regardless of the 471 * application state. However, "schedule" can be called only if 472 * the application state is STOPPED, etc... 473 **/ 474 private final static Map<String,EnumSet<ScanState>> allowedStates; 475 static { 476 allowedStates = newHashMap(); 477 // You can always call stop 478 allowedStates.put("stop",EnumSet.allOf(ScanState.class)); 479 480 // You can only call closed when stopped 481 allowedStates.put("close",EnumSet.of(STOPPED,COMPLETED,CLOSED)); 482 483 // You can call schedule only when the current task is 484 // completed or stopped. 485 allowedStates.put("schedule",EnumSet.of(STOPPED,COMPLETED)); 486 487 // switch reserved for background task: goes from SCHEDULED to 488 // RUNNING when it enters the run() method. 489 allowedStates.put("scan-running",EnumSet.of(SCHEDULED)); 490 491 // switch reserved for background task: goes from RUNNING to 492 // SCHEDULED when it has completed but needs to reschedule 493 // itself for specified interval. 494 allowedStates.put("scan-scheduled",EnumSet.of(RUNNING)); 495 496 // switch reserved for background task: 497 // goes from RUNNING to COMPLETED upon successful completion 498 allowedStates.put("scan-done",EnumSet.of(RUNNING)); 499 } 500 501 // Get this object's state. No need to synchronize because 502 // state is volatile. 503 // See ScanManagerMXBean 504 public ScanState getState() { 505 return state; 506 } 507 508 /** 509 * Enqueue a state changed notification for the given states. 510 **/ 511 private void queueStateChangedNotification( 512 long sequence, 513 long time, 514 ScanState old, 515 ScanState current) { 516 final AttributeChangeNotification n = 517 new AttributeChangeNotification(SCAN_MANAGER_NAME,sequence,time, 518 "ScanManager State changed to "+current,"State", 519 ScanState.class.getName(),old.toString(),current.toString()); 520 // Queue the notification. We have created an unlimited queue, so 521 // this method should always succeed. 522 try { 523 if (!pendingNotifs.offer(n,2,TimeUnit.SECONDS)) { 524 LOG.fine("Can't queue Notification: "+n); 525 } 526 } catch (InterruptedException x) { 527 LOG.fine("Can't queue Notification: "+x); 528 } 529 } 530 531 /** 532 * Send all notifications present in the queue. 533 **/ 534 private void sendQueuedNotifications() { 535 Notification n; 536 while ((n = pendingNotifs.poll()) != null) { 537 broadcaster.sendNotification(n); 538 } 539 } 540 541 /** 542 * Checks that the current state is allowed for the given operation, 543 * and if so, switch its value to the new desired state. 544 * This operation also enqueue the appropriate state changed 545 * notification. 546 **/ 547 private ScanState switchState(ScanState desired,String forOperation) { 548 return switchState(desired,allowedStates.get(forOperation)); 549 } 550 551 /** 552 * Checks that the current state is one of the allowed states, 553 * and if so, switch its value to the new desired state. 554 * This operation also enqueue the appropriate state changed 555 * notification. 556 **/ 557 private ScanState switchState(ScanState desired,EnumSet<ScanState> allowed) { 558 final ScanState old; 559 final long timestamp; 560 final long sequence; 561 synchronized(this) { 562 old = state; 563 if (!allowed.contains(state)) 564 throw new IllegalStateException(state.toString()); 565 state = desired; 566 timestamp = System.currentTimeMillis(); 567 sequence = getNextSeqNumber(); 568 } 569 LOG.fine("switched state: "+old+" -> "+desired); 570 if (old != desired) 571 queueStateChangedNotification(sequence,timestamp,old,desired); 572 return old; 573 } 574 575 576 // --------------------------------------------------------------- 577 // schedule() creates a new SessionTask that will be executed later 578 // (possibly right away if delay=0) by a Timer thread. 579 // --------------------------------------------------------------- 580 581 // The timer used by this object. Lazzy evaluation. Cleaned in 582 // postDeregister() 583 // 584 private Timer timer = null; 585 586 // See ScanManagerMXBean 587 public void schedule(long delay, long interval) { 588 if (!sequencer.tryAcquire()) { 589 throw new IllegalStateException("Can't acquire lock"); 590 } 591 try { 592 LOG.fine("scheduling new task: state="+state); 593 final ScanState old = switchState(SCHEDULED,"schedule"); 594 final boolean scheduled = 595 scheduleSession(new SessionTask(interval),delay); 596 if (scheduled) 597 LOG.fine("new task scheduled: state="+state); 598 } finally { 599 sequencer.release(); 600 } 601 sendQueuedNotifications(); 602 } 603 604 // Schedule a SessionTask. The session task may reschedule 605 // a new identical task when it eventually ends. 606 // We use this logic so that the 'interval' time is measured 607 // starting at the end of the task that finishes, rather than 608 // at its beginning. Therefore if a repeated task takes x ms, 609 // it will be repeated every x+interval ms. 610 // 611 private synchronized boolean scheduleSession(SessionTask task, long delay) { 612 if (state == STOPPED) return false; 613 if (timer == null) timer = new Timer("ScanManager"); 614 tasklist.add(task); 615 timer.schedule(task,delay); 616 return true; 617 } 618 619 // --------------------------------------------------------------- 620 // start() is equivalent to schedule(0,0) 621 // --------------------------------------------------------------- 622 623 // See ScanManagerMXBean 624 public void start() throws IOException, InstanceNotFoundException { 625 schedule(0,0); 626 } 627 628 // --------------------------------------------------------------- 629 // Methods used to implement stop() - stop() is asynchronous, 630 // and needs to notify any running background task that it needs 631 // to stop. It also needs to prevent scheduled task from being 632 // run. 633 // --------------------------------------------------------------- 634 635 // See ScanManagerMXBean 636 public void stop() { 637 if (!sequencer.tryAcquire()) 638 throw new IllegalStateException("Can't acquire lock"); 639 int errcount = 0; 640 final StringBuilder b = new StringBuilder(); 641 642 try { 643 switchState(STOPPED,"stop"); 644 645 errcount += cancelSessionTasks(b); 646 errcount += stopDirectoryScanners(b); 647 } finally { 648 sequencer.release(); 649 } 650 651 sendQueuedNotifications(); 652 if (errcount > 0) { 653 b.insert(0,"stop partially failed with "+errcount+" error(s):"); 654 throw new RuntimeException(b.toString()); 655 } 656 } 657 658 // See ScanManagerMXBean 659 public void close() { 660 switchState(CLOSED,"close"); 661 sendQueuedNotifications(); 662 } 663 664 // Appends exception to a StringBuilder message. 665 // 666 private void append(StringBuilder b,String prefix,Throwable t) { 667 final String first = (prefix==null)?"\n":"\n"+prefix; 668 b.append(first).append(String.valueOf(t)); 669 Throwable cause = t; 670 while ((cause = cause.getCause())!=null) { 671 b.append(first).append("Caused by:").append(first); 672 b.append('\t').append(String.valueOf(cause)); 673 } 674 } 675 676 // Cancels all scheduled session tasks 677 // 678 private int cancelSessionTasks(StringBuilder b) { 679 int errcount = 0; 680 // Stops scheduled tasks if any... 681 // 682 for (SessionTask task : tasklist) { 683 try { 684 task.cancel(); 685 tasklist.remove(task); 686 } catch (Exception ex) { 687 errcount++; 688 append(b,"\t",ex); 689 } 690 } 691 return errcount; 692 } 693 694 // Stops all DirectoryScanners configured for this object. 695 // 696 private int stopDirectoryScanners(StringBuilder b) { 697 int errcount = 0; 698 // Stops directory scanners if any... 699 // 700 for (DirectoryScannerMXBean s : scanmap.values()) { 701 try { 702 s.stop(); 703 } catch (Exception ex) { 704 errcount++; 705 append(b,"\t",ex); 706 } 707 } 708 return errcount; 709 } 710 711 712 // --------------------------------------------------------------- 713 // We start scanning in background in a Timer thread. 714 // The methods below implement that logic. 715 // --------------------------------------------------------------- 716 717 private void scanAllDirectories() 718 throws IOException, InstanceNotFoundException { 719 720 int errcount = 0; 721 final StringBuilder b = new StringBuilder(); 722 for (ObjectName key : scanmap.keySet()) { 723 final DirectoryScannerMXBean s = scanmap.get(key); 724 try { 725 if (state == STOPPED) return; 726 s.scan(); 727 } catch (Exception ex) { 728 LOG.log(Level.FINE,key + " failed to scan: "+ex,ex); 729 errcount++; 730 append(b,"\t",ex); 731 } 732 } 733 if (errcount > 0) { 734 b.insert(0,"scan partially performed with "+errcount+" error(s):"); 735 throw new RuntimeException(b.toString()); 736 } 737 } 738 739 // List of scheduled session task. Needed by stop() to cancel 740 // scheduled sessions. There's usually at most 1 session in 741 // this list (unless there's a bug somewhere ;-)) 742 // 743 private final ConcurrentLinkedQueue<SessionTask> tasklist = 744 new ConcurrentLinkedQueue<SessionTask>(); 745 746 // Used to give a unique id to session task - useful for 747 // debugging. 748 // 749 private volatile static long taskcount = 0; 750 751 /** 752 * A session task will be scheduled to run in background in a 753 * timer thread. There can be at most one session task running 754 * at a given time (this is ensured by using a timer - which is 755 * a single threaded object). 756 * 757 * If the session needs to be repeated, it will reschedule an 758 * identical session when it finishes to run. This ensure that 759 * two session runs are separated by the given interval time. 760 * 761 **/ 762 private class SessionTask extends TimerTask { 763 764 /** 765 * Delay after which the next iteration of this task will 766 * start. This delay is measured starting at the end of 767 * the previous iteration. 768 **/ 769 final long delayBeforeNext; 770 771 /** 772 * A unique id for this task. 773 **/ 774 final long taskid; 775 776 /** 777 * Whether it's been cancelled by stop() 778 **/ 779 volatile boolean cancelled=false; 780 781 /** 782 * create a new SessionTask. 783 **/ 784 SessionTask(long scheduleNext) { 785 delayBeforeNext = scheduleNext; 786 taskid = taskcount++; 787 } 788 789 /** 790 * When run() begins, the state is switched to RUNNING. 791 * When run() ends then: 792 * If the task is repeated, the state will be switched 793 * to SCHEDULED (because a new task was scheduled). 794 * Otherwise the state will be switched to either 795 * STOPPED (if it was stopped before it could complete) 796 * or COMPLETED (if it completed gracefully) 797 * This method is used to switch to the desired state and 798 * send the appropriate notifications. 799 * When entering the method, we check whether the state is 800 * STOPPED. If so, we return false - and the SessionTask will 801 * stop. Otherwise, we switch the state to the desired value. 802 **/ 803 private boolean notifyStateChange(ScanState newState,String condition) { 804 synchronized (ScanManager.this) { 805 if (state == STOPPED || state == CLOSED) return false; 806 switchState(newState,condition); 807 } 808 sendQueuedNotifications(); 809 return true; 810 } 811 812 // Cancels this task. 813 public boolean cancel() { 814 cancelled=true; 815 return super.cancel(); 816 } 817 818 /** 819 * Invoke all directories scanners in sequence. At each 820 * step, checks to see whether the task should stop. 821 **/ 822 private boolean execute() { 823 final String tag = "Scheduled session["+taskid+"]"; 824 try { 825 if (cancelled) { 826 LOG.finer(tag+" cancelled: done"); 827 return false; 828 } 829 if (!notifyStateChange(RUNNING,"scan-running")) { 830 LOG.finer(tag+" stopped: done"); 831 return false; 832 } 833 scanAllDirectories(); 834 } catch (Exception x) { 835 if (LOG.isLoggable(Level.FINEST)) { 836 LOG.log(Level.FINEST, 837 tag+" failed to scan: "+x,x); 838 } else if (LOG.isLoggable(Level.FINE)) { 839 LOG.fine(tag+" failed to scan: "+x); 840 } 841 } 842 return true; 843 } 844 845 /** 846 * Schedule an identical task for next iteration. 847 **/ 848 private boolean scheduleNext() { 849 final String tag = "Scheduled session["+taskid+"]"; 850 851 // We need now to reschedule a new task for after 'delayBeforeNext' ms. 852 try { 853 LOG.finer(tag+": scheduling next session for "+ delayBeforeNext + "ms"); 854 if (cancelled || !notifyStateChange(SCHEDULED,"scan-scheduled")) { 855 LOG.finer(tag+" stopped: do not reschedule"); 856 return false; 857 } 858 final SessionTask nextTask = new SessionTask(delayBeforeNext); 859 if (!scheduleSession(nextTask,delayBeforeNext)) return false; 860 LOG.finer(tag+": next session successfully scheduled"); 861 } catch (Exception x) { 862 if (LOG.isLoggable(Level.FINEST)) { 863 LOG.log(Level.FINEST,tag+ 864 " failed to schedule next session: "+x,x); 865 } else if (LOG.isLoggable(Level.FINE)) { 866 LOG.fine(tag+" failed to schedule next session: "+x); 867 } 868 } 869 return true; 870 } 871 872 873 /** 874 * The run method: 875 * executes scanning logic, the schedule next iteration if needed. 876 **/ 877 public void run() { 878 final String tag = "Scheduled session["+taskid+"]"; 879 LOG.entering(SessionTask.class.getName(),"run"); 880 LOG.finer(tag+" starting..."); 881 try { 882 if (execute()==false) return; 883 884 LOG.finer(tag+" terminating - state is "+state+ 885 ((delayBeforeNext >0)?(" next session is due in "+delayBeforeNext+" ms."): 886 " no additional session scheduled")); 887 888 // if delayBeforeNext <= 0 we are done, either because the session was 889 // stopped or because it successfully completed. 890 if (delayBeforeNext <= 0) { 891 if (!notifyStateChange(COMPLETED,"scan-done")) 892 LOG.finer(tag+" stopped: done"); 893 else 894 LOG.finer(tag+" completed: done"); 895 return; 896 } 897 898 // we need to reschedule a new session for 'delayBeforeNext' ms. 899 scheduleNext(); 900 901 } finally { 902 tasklist.remove(this); 903 LOG.finer(tag+" finished..."); 904 LOG.exiting(SessionTask.class.getName(),"run"); 905 } 906 } 907 } 908 909 // --------------------------------------------------------------- 910 // --------------------------------------------------------------- 911 912 // --------------------------------------------------------------- 913 // MBean Notification support 914 // The methods below are imported from {@link NotificationEmitter} 915 // --------------------------------------------------------------- 916 917 /** 918 * Delegates the implementation of this method to the wrapped 919 * {@code NotificationBroadcasterSupport} object. 920 **/ 921 public void addNotificationListener(NotificationListener listener, NotificationFilter filter, Object handback) throws IllegalArgumentException { 922 broadcaster.addNotificationListener(listener, filter, handback); 923 } 924 925 926 /** 927 * We emit an {@code AttributeChangeNotification} when the {@code State} 928 * attribute changes. 929 **/ 930 public MBeanNotificationInfo[] getNotificationInfo() { 931 return new MBeanNotificationInfo[] { 932 new MBeanNotificationInfo(new String[] { 933 AttributeChangeNotification.ATTRIBUTE_CHANGE}, 934 AttributeChangeNotification.class.getName(), 935 "Emitted when the State attribute changes") 936 }; 937 } 938 939 /** 940 * Delegates the implementation of this method to the wrapped 941 * {@code NotificationBroadcasterSupport} object. 942 **/ 943 public void removeNotificationListener(NotificationListener listener) throws ListenerNotFoundException { 944 broadcaster.removeNotificationListener(listener); 945 } 946 947 /** 948 * Delegates the implementation of this method to the wrapped 949 * {@code NotificationBroadcasterSupport} object. 950 **/ 951 public void removeNotificationListener(NotificationListener listener, NotificationFilter filter, Object handback) throws ListenerNotFoundException { 952 broadcaster.removeNotificationListener(listener, filter, handback); 953 } 954 955 /** 956 * Returns and increment the sequence number used for 957 * notifications. We use the same sequence number throughout the 958 * application - this is why this method is only package protected. 959 * @return A unique sequence number for the next notification. 960 */ 961 static synchronized long getNextSeqNumber() { 962 return seqNumber++; 963 } 964 965 // --------------------------------------------------------------- 966 // End of MBean Notification support 967 // --------------------------------------------------------------- 968 969 // --------------------------------------------------------------- 970 // MBeanRegistration support 971 // The methods below are imported from {@link MBeanRegistration} 972 // --------------------------------------------------------------- 973 974 /** 975 * Allows the MBean to perform any operations it needs before being 976 * registered in the MBean server. If the name of the MBean is not 977 * specified, the MBean can provide a name for its registration. If 978 * any exception is raised, the MBean will not be registered in the 979 * MBean server. 980 * <p>In this implementation, we check that the provided name is 981 * either {@code null} or equals to {@link #SCAN_MANAGER_NAME}. If it 982 * isn't then we throw an IllegalArgumentException, otherwise we return 983 * {@link #SCAN_MANAGER_NAME}.</p> 984 * <p>This ensures that there will be a single instance of ScanManager 985 * registered in a given MBeanServer, and that it will always be 986 * registered with the singleton's {@link #SCAN_MANAGER_NAME}.</p> 987 * <p>We do not need to check whether an MBean by that name is 988 * already registered because the MBeanServer will perform 989 * this check just after having called preRegister().</p> 990 * @param server The MBean server in which the MBean will be registered. 991 * @param name The object name of the MBean. This name is null if the 992 * name parameter to one of the createMBean or registerMBean methods in 993 * the MBeanServer interface is null. In that case, this method must 994 * return a non-null ObjectName for the new MBean. 995 * @return The name under which the MBean is to be registered. This value 996 * must not be null. If the name parameter is not null, it will usually 997 * but not necessarily be the returned value. 998 * @throws Exception This exception will be caught by the MBean server and 999 * re-thrown as an MBeanRegistrationException. 1000 */ 1001 public ObjectName preRegister(MBeanServer server, ObjectName name) throws Exception { 1002 if (name != null) { 1003 if (!SCAN_MANAGER_NAME.equals(name)) 1004 throw new IllegalArgumentException(String.valueOf(name)); 1005 } 1006 mbeanServer = server; 1007 return SCAN_MANAGER_NAME; 1008 } 1009 1010 // Returns the default configuration filename 1011 static String getDefaultConfigurationFileName() { 1012 // This is a file calles 'jmx-scandir.xml' located 1013 // in the user directory. 1014 final String user = System.getProperty("user.home"); 1015 final String defconf = user+File.separator+"jmx-scandir.xml"; 1016 return defconf; 1017 } 1018 1019 /** 1020 * Allows the MBean to perform any operations needed after having 1021 * been registered in the MBean server or after the registration has 1022 * failed. 1023 * <p> 1024 * If registration was not successful, the method returns immediately. 1025 * <p> 1026 * If registration is successful, register the {@link ResultLogManager} 1027 * and default {@link ScanDirConfigMXBean}. If registering these 1028 * MBean fails, the {@code ScanManager} state will be switched to 1029 * {@link #close CLOSED}, and postRegister ends there. 1030 * </p> 1031 * <p>Otherwise the {@code ScanManager} will ask the 1032 * {@link ScanDirConfigMXBean} to load its configuration. 1033 * If it succeeds, the configuration will be {@link 1034 * #applyConfiguration applied}. Otherwise, the method simply returns, 1035 * assuming that the user will later create/update a configuration and 1036 * apply it. 1037 * @param registrationDone Indicates whether or not the MBean has been 1038 * successfully registered in the MBean server. The value false means 1039 * that the registration has failed. 1040 */ 1041 public void postRegister(Boolean registrationDone) { 1042 if (!registrationDone) return; 1043 Exception test=null; 1044 try { 1045 mbeanServer.registerMBean(log, 1046 ResultLogManager.RESULT_LOG_MANAGER_NAME); 1047 final String defconf = getDefaultConfigurationFileName(); 1048 final String conf = System.getProperty("scandir.config.file",defconf); 1049 final String confname = ScanDirConfig.guessConfigName(conf,defconf); 1050 final ObjectName defaultProfileName = 1051 makeMBeanName(ScanDirConfigMXBean.class,confname); 1052 if (!mbeanServer.isRegistered(defaultProfileName)) 1053 mbeanServer.registerMBean(new ScanDirConfig(conf), 1054 defaultProfileName); 1055 config = JMX.newMXBeanProxy(mbeanServer,defaultProfileName, 1056 ScanDirConfigMXBean.class,true); 1057 configmap.put(defaultProfileName,config); 1058 } catch (Exception x) { 1059 LOG.config("Failed to populate MBeanServer: "+x); 1060 close(); 1061 return; 1062 } 1063 try { 1064 config.load(); 1065 } catch (Exception x) { 1066 LOG.finest("No config to load: "+x); 1067 test = x; 1068 } 1069 if (test == null) { 1070 try { 1071 applyConfiguration(config.getConfiguration()); 1072 } catch (Exception x) { 1073 if (LOG.isLoggable(Level.FINEST)) 1074 LOG.log(Level.FINEST,"Failed to apply config: "+x,x); 1075 LOG.config("Failed to apply config: "+x); 1076 } 1077 } 1078 } 1079 1080 // Unregisters all created DirectoryScanners 1081 private void unregisterScanners() throws JMException { 1082 unregisterMBeans(scanmap); 1083 } 1084 1085 // Unregisters all created ScanDirConfigs 1086 private void unregisterConfigs() throws JMException { 1087 unregisterMBeans(configmap); 1088 } 1089 1090 // Unregisters all MBeans named by the given map 1091 private void unregisterMBeans(Map<ObjectName,?> map) throws JMException { 1092 for (ObjectName key : map.keySet()) { 1093 if (mbeanServer.isRegistered(key)) 1094 mbeanServer.unregisterMBean(key); 1095 map.remove(key); 1096 } 1097 } 1098 1099 // Unregisters the ResultLogManager. 1100 private void unregisterResultLogManager() throws JMException { 1101 final ObjectName name = ResultLogManager.RESULT_LOG_MANAGER_NAME; 1102 if (mbeanServer.isRegistered(name)) { 1103 mbeanServer.unregisterMBean(name); 1104 } 1105 } 1106 1107 /** 1108 * Allows the MBean to perform any operations it needs before being 1109 * unregistered by the MBean server. 1110 * This implementation also unregisters all the MXBeans 1111 * that were created by this object. 1112 * @throws IllegalStateException if the lock can't be acquire, or if 1113 * the MBean's state doesn't allow the MBean to be unregistered 1114 * (e.g. because it's scheduled or running). 1115 * @throws Exception This exception will be caught by the MBean server and 1116 * re-thrown as an MBeanRegistrationException. 1117 */ 1118 public void preDeregister() throws Exception { 1119 try { 1120 close(); 1121 if (!sequencer.tryAcquire()) 1122 throw new IllegalStateException("can't acquire lock"); 1123 try { 1124 unregisterScanners(); 1125 unregisterConfigs(); 1126 unregisterResultLogManager(); 1127 } finally { 1128 sequencer.release(); 1129 } 1130 } catch (Exception x) { 1131 LOG.log(Level.FINEST,"Failed to unregister: "+x,x); 1132 throw x; 1133 } 1134 } 1135 1136 /** 1137 * Allows the MBean to perform any operations needed after having been 1138 * unregistered in the MBean server. 1139 * Cancels the internal timer - if any. 1140 */ 1141 public synchronized void postDeregister() { 1142 if (timer != null) { 1143 try { 1144 timer.cancel(); 1145 } catch (Exception x) { 1146 if (LOG.isLoggable(Level.FINEST)) 1147 LOG.log(Level.FINEST,"Failed to cancel timer",x); 1148 else if (LOG.isLoggable(Level.FINE)) 1149 LOG.fine("Failed to cancel timer: "+x); 1150 } finally { 1151 timer = null; 1152 } 1153 } 1154 } 1155 1156 // --------------------------------------------------------------- 1157 // End of MBeanRegistration support 1158 // --------------------------------------------------------------- 1159 1160 } 1161