001/* 002 * jPOS Project [http://jpos.org] 003 * Copyright (C) 2000-2026 jPOS Software SRL 004 * 005 * This program is free software: you can redistribute it and/or modify 006 * it under the terms of the GNU Affero General Public License as 007 * published by the Free Software Foundation, either version 3 of the 008 * License, or (at your option) any later version. 009 * 010 * This program is distributed in the hope that it will be useful, 011 * but WITHOUT ANY WARRANTY; without even the implied warranty of 012 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 013 * GNU Affero General Public License for more details. 014 * 015 * You should have received a copy of the GNU Affero General Public License 016 * along with this program. If not, see <http://www.gnu.org/licenses/>. 017 */ 018 019package org.jpos.q2; 020 021import io.micrometer.core.instrument.Counter; 022import io.micrometer.core.instrument.Meter; 023import io.micrometer.core.instrument.MeterRegistry; 024import io.micrometer.core.instrument.Metrics; 025import io.micrometer.core.instrument.composite.CompositeMeterRegistry; 026import io.micrometer.core.instrument.config.MeterFilter; 027import io.micrometer.core.instrument.distribution.DistributionStatisticConfig; 028import io.micrometer.prometheusmetrics.PrometheusConfig; 029import io.micrometer.prometheusmetrics.PrometheusMeterRegistry; 030import jdk.jfr.Configuration; 031import jdk.jfr.Recording; 032import jdk.jfr.RecordingState; 033import org.apache.commons.cli.CommandLine; 034import org.apache.commons.cli.CommandLineParser; 035import org.apache.commons.cli.DefaultParser; 036import org.apache.commons.cli.HelpFormatter; 037import org.apache.commons.cli.MissingArgumentException; 038import org.apache.commons.cli.Options; 039import org.apache.commons.cli.UnrecognizedOptionException; 040import org.jdom2.Document; 041import org.jdom2.Element; 042import org.jdom2.Text; 043import org.jdom2.Comment; 044import org.jdom2.JDOMException; 045import org.jdom2.input.SAXBuilder; 046import org.jdom2.output.Format; 047import org.jdom2.output.XMLOutputter; 048import org.jpos.core.Environment; 049import org.jpos.iso.ISOException; 050import org.jpos.iso.ISOUtil; 051import org.jpos.log.AuditLogEvent; 052import org.jpos.log.evt.*; 053import org.jpos.metrics.MeterInfo; 054import org.jpos.metrics.PrometheusService; 055import org.jpos.q2.install.ModuleUtils; 056import org.jpos.security.SystemSeed; 057import org.jpos.util.Log; 058import org.jpos.util.LogEvent; 059import org.jpos.util.Logger; 060import org.jpos.util.NameRegistrar; 061import org.jpos.util.PGPHelper; 062import org.jpos.util.Realm; 063import org.jpos.util.SimpleLogListener; 064import org.xml.sax.SAXException; 065 066import javax.crypto.Cipher; 067import javax.crypto.spec.SecretKeySpec; 068import javax.management.InstanceAlreadyExistsException; 069import javax.management.InstanceNotFoundException; 070import javax.management.MBeanServer; 071import javax.management.ObjectInstance; 072import javax.management.ObjectName; 073import java.io.*; 074import java.lang.management.ManagementFactory; 075import java.nio.file.FileSystem; 076import java.nio.file.Path; 077import java.nio.file.Paths; 078import java.nio.file.StandardWatchEventKinds; 079import java.nio.file.WatchEvent; 080import java.nio.file.WatchKey; 081import java.nio.file.WatchService; 082import java.nio.charset.StandardCharsets; 083import java.security.GeneralSecurityException; 084import java.security.MessageDigest; 085import java.text.ParseException; 086import java.time.Duration; 087import java.time.Instant; 088import java.util.*; 089import java.util.jar.Attributes; 090import java.util.jar.JarFile; 091import java.util.jar.Manifest; 092import java.util.concurrent.CountDownLatch; 093import java.util.concurrent.TimeUnit; 094import java.util.stream.Stream; 095 096import io.micrometer.core.instrument.binder.jvm.ClassLoaderMetrics; 097import io.micrometer.core.instrument.binder.jvm.JvmGcMetrics; 098import io.micrometer.core.instrument.binder.jvm.JvmMemoryMetrics; 099import io.micrometer.core.instrument.binder.jvm.JvmThreadMetrics; 100import io.micrometer.core.instrument.binder.system.ProcessorMetrics; 101 102 103import static java.util.ResourceBundle.getBundle; 104 105 106/** 107 * The Q2 application container — bootstraps and manages the lifecycle of QBean components. 108 * @author <a href="mailto:taherkordy@dpi2.dpi.net.ir">Alireza Taherkordi</a> 109 * @author <a href="mailto:apr@cs.com.uy">Alejandro P. Revilla</a> 110 * @author <a href="mailto:alwynschoeman@yahoo.com">Alwyn Schoeman</a> 111 * @author <a href="mailto:vsalaman@vmantek.com">Victor Salaman</a> 112 */ 113@SuppressWarnings("unchecked") 114public class Q2 implements FileFilter, Runnable { 115 /** Default subdirectory under {@link #getDeployDir()} that Q2 scans for deployable XML. */ 116 public static final String DEFAULT_DEPLOY_DIR = "deploy"; 117 /** JMX domain under which Q2 registers its MBeans. */ 118 public static final String JMX_NAME = "Q2"; 119 /** Logger name used for Q2 lifecycle events. */ 120 public static final String LOGGER_NAME = "Q2"; 121 /** Realm tag for Q2 lifecycle log entries. */ 122 public static final String REALM = Realm.Q2_LIFECYCLE; 123 /** File name for the logger configuration auto-deployed at startup. */ 124 public static final String LOGGER_CONFIG = "00_logger.xml"; 125 /** Object-name prefix used for QBean services. */ 126 public static final String QBEAN_NAME = "Q2:type=qbean,service="; 127 /** Object name of the Q2 class loader MBean. */ 128 public static final String Q2_CLASS_LOADER = "Q2:type=system,service=loader"; 129 /** Suffix appended to deploy files that duplicate a prior deployment. */ 130 public static final String DUPLICATE_EXTENSION = "DUP"; 131 /** Suffix appended to deploy files that fail to load. */ 132 public static final String ERROR_EXTENSION = "BAD"; 133 /** Suffix recognised as environment-variable overlays in deploy. */ 134 public static final String ENV_EXTENSION = "ENV"; 135 /** File name of the embedded licensee descriptor. */ 136 public static final String LICENSEE = "LICENSEE.asc"; 137 /** SHA-256 hash of the public key bundled with the {@link #LICENSEE} verifier. */ 138 public static final byte[] PUBKEYHASH = ISOUtil.hex2byte("C0C73A47A5A27992267AC825F3C8B0666DF3F8A544210851821BFCC1CFA9136C"); 139 140 /** Element name used to mark a QBean as protected (cannot be undeployed). */ 141 public static final String PROTECTED_QBEAN = "protected-qbean"; 142 /** Deploy-directory poll interval in milliseconds. */ 143 public static final int SCAN_INTERVAL = 2500; 144 /** Maximum time, in milliseconds, that Q2 waits for QBeans to stop on shutdown. */ 145 public static final long SHUTDOWN_TIMEOUT = 60000; 146 147 private MBeanServer server; 148 private File deployDir, libDir; 149 private Map<File,QEntry> dirMap; 150 private QFactory factory; 151 private QClassLoader loader; 152 private ClassLoader mainClassLoader; 153 private Log log; 154 private Log deployLog; 155 private volatile boolean started; 156 private CountDownLatch ready = new CountDownLatch(1); 157 private CountDownLatch shutdown = new CountDownLatch(1); 158 private volatile boolean shuttingDown; 159 private volatile Thread q2Thread; 160 private String[] args; 161 private boolean hasSystemLogger; 162 private boolean exit; 163 private Instant startTime; 164 private CLI cli; 165 private boolean recursive; 166 private ConfigDecorationProvider decorator=null; 167 private UUID instanceId; 168 private String pidFile; 169 private String name = JMX_NAME; 170 private long lastVersionLog; 171 private String watchServiceClassname; 172 private boolean disableDeployScan; 173 private boolean disableDynamicClassloader; 174 private boolean disableJFR; 175 private int sshPort; 176 private String sshAuthorizedKeys; 177 private String sshUser; 178 private String sshHostKeyFile; 179 private static String DEPLOY_PREFIX = "META-INF/q2/deploy/"; 180 private static String CFG_PREFIX = "META-INF/q2/cfg/"; 181 private String nameRegistrarKey; 182 private Recording recording; 183 private CompositeMeterRegistry meterRegistry = io.micrometer.core.instrument.Metrics.globalRegistry; 184 private PrometheusMeterRegistry prometheusRegistry = new PrometheusMeterRegistry(PrometheusConfig.DEFAULT); 185 private int metricsPort; 186 private String metricsPath; 187 private final String statusPath = "/jpos/q2/status"; 188 189 private Counter instancesCounter = Metrics.counter("jpos.q2.instances"); 190 private boolean noShutdownHook; 191 private long shutdownHookDelay = 0L; 192 193 /** 194 * Constructs a new {@code Q2} instance with the specified command-line arguments and class loader. 195 * This constructor initializes various configurations, processes command-line arguments, 196 * sets up directories, and registers necessary components for the application. 197 * 198 * @param args an array of {@code String} containing the command-line arguments. 199 * @param classLoader the {@code ClassLoader} to be used by the application. 200 * If {@code null}, the class loader of the current class is used. 201 * 202 * <p>Key Initialization Steps:</p> 203 * <ul> 204 * <li>Parses the command-line arguments twice: 205 * once before environment variable substitution and once after.</li> 206 * <li>Initializes the deployment directory and library directory (`lib`).</li> 207 * <li>Generates a unique instance identifier for the application instance.</li> 208 * <li>Sets the application start time to the current moment.</li> 209 * <li>Registers MicroMeter metrics and Q2-specific components.</li> 210 * </ul> 211 * 212 * <p>Note: The {@code deployDir} directory is created if it does not already exist.</p> 213 * 214 * @see #parseCmdLine(String[], boolean) 215 * @see #registerMicroMeter() 216 * @see #registerQ2() 217 */ 218 public Q2 (String[] args, ClassLoader classLoader) { 219 super(); 220 parseCmdLine (args, true); 221 this.args = environmentArgs(args); 222 startTime = Instant.now(); 223 instanceId = UUID.randomUUID(); 224 parseCmdLine (this.args, false); 225 libDir = new File (deployDir, "lib"); 226 dirMap = new TreeMap<>(); 227 deployDir.mkdirs (); 228 mainClassLoader = classLoader == null ? getClass().getClassLoader() : classLoader; 229 registerMicroMeter(); 230 registerQ2(); 231 } 232 233 /** 234 * Constructs a new {@code Q2} instance with the specified command-line arguments 235 * and the default class loader. 236 * 237 * @param args an array of {@code String} containing the command-line arguments. 238 * If no arguments are provided, the application initializes with 239 * default settings. 240 * 241 * <p>This constructor delegates to {@link #Q2(String[], ClassLoader)} with 242 * a {@code null} class loader, causing the default class loader to be used.</p> 243 * 244 * @see #Q2(String[], ClassLoader) 245 */ 246 public Q2 (String[] args) { 247 this (args, null); 248 } 249 250 /** 251 * Constructs a new {@code Q2} instance with no command-line arguments 252 * and the default class loader. 253 * 254 * <p>This constructor is equivalent to calling {@code Q2(new String[]{})}. 255 * It initializes the application with default settings.</p> 256 * 257 * @see #Q2(String[], ClassLoader) 258 * @see #Q2(String[]) 259 */ 260 261 public Q2 () { 262 this (new String[] {}); 263 } 264 265 /** 266 * Constructs a new {@code Q2} instance with the specified deployment directory 267 * and the default class loader. 268 * 269 * @param deployDir a {@code String} specifying the path to the deployment directory. 270 * This is passed as a command-line argument using the {@code -d} option. 271 * 272 * <p>This constructor is equivalent to calling {@code Q2(new String[]{"-d", deployDir})}. 273 * It sets the deployment directory and initializes the application.</p> 274 * 275 * @see #Q2(String[], ClassLoader) 276 * @see #Q2(String[]) 277 */ 278 public Q2 (String deployDir) { 279 this (new String[] { "-d", deployDir }); 280 } 281 /** 282 * Starts Q2 on a new daemon-style worker thread. 283 * 284 * @throws IllegalStateException if {@link #stop()} has already been called on this instance 285 */ 286 public void start () { 287 if (shutdown.getCount() == 0) 288 throw new IllegalStateException("Q2 has been stopped"); 289 new Thread(this).start(); 290 } 291 /** 292 * Initiates an orderly shutdown of Q2 and the QBeans it has deployed. 293 */ 294 public void stop () { 295 shutdown(true); 296 } 297 /** 298 * Returns the Micrometer registry that aggregates Q2 metrics. 299 * 300 * @return the active {@link MeterRegistry} 301 */ 302 public MeterRegistry getMeterRegistry() { 303 return meterRegistry; 304 } 305 306 /** 307 * Returns the Prometheus-flavoured registry exposed for scraping. 308 * 309 * @return the {@link PrometheusMeterRegistry} backing the Prometheus endpoint 310 */ 311 public PrometheusMeterRegistry getPrometheusMeterRegistry () { 312 return prometheusRegistry; 313 } 314 public void run () { 315 started = true; 316 Thread.currentThread().setName ("Q2-"+getInstanceId().toString()); 317 startJFR(); 318 instancesCounter.increment(); 319 320 Path dir = Paths.get(deployDir.getAbsolutePath()); 321 FileSystem fs = dir.getFileSystem(); 322 try (WatchService service = fs.newWatchService()) { 323 watchServiceClassname = service.getClass().getName(); 324 dir.register( 325 service, 326 StandardWatchEventKinds.ENTRY_CREATE, 327 StandardWatchEventKinds.ENTRY_MODIFY, 328 StandardWatchEventKinds.ENTRY_DELETE 329 ); 330 server = ManagementFactory.getPlatformMBeanServer(); 331 final ObjectName loaderName = new ObjectName(Q2_CLASS_LOADER); 332 try { 333 loader = new QClassLoader(server, libDir, loaderName, mainClassLoader); 334 if (server.isRegistered(loaderName)) 335 server.unregisterMBean(loaderName); 336 server.registerMBean(loader, loaderName); 337 loader = loader.scan(false); 338 } catch (Throwable t) { 339 if (log != null) 340 log.error("initial-scan", t); 341 else 342 t.printStackTrace(); 343 } 344 factory = new QFactory(loaderName, this); 345 writePidFile(); 346 initSystemLogger(); 347 if (!noShutdownHook) 348 addShutdownHook(); 349 q2Thread = Thread.currentThread(); 350 q2Thread.setContextClassLoader(loader); 351 if (cli != null) 352 cli.start(); 353 initConfigDecorator(); 354 if (metricsPort != 0) { 355 deployElement( 356 PrometheusService.createDescriptor(metricsPort, metricsPath, statusPath), 357 "00_prometheus-" + getInstanceId() + ".xml", false, true); 358 } 359 360 deployInternal(); 361 for (int i = 1; shutdown.getCount() > 0; i++) { 362 try { 363 if (i > 1 && disableDeployScan) { 364 shutdown.await(); 365 break; 366 } 367 boolean forceNewClassLoader = scan() && i > 1; 368 QClassLoader oldClassLoader = loader; 369 loader = loader.scan(forceNewClassLoader); 370 if (loader != oldClassLoader) { 371 oldClassLoader = null; // We want this to be null so it gets GCed. 372 System.gc(); // force a GC 373 log.info( 374 "new classloader [" 375 + Integer.toString(loader.hashCode(), 16) 376 + "] has been created" 377 ); 378 q2Thread.setContextClassLoader(loader); 379 } 380 logVersion(); 381 382 deploy(); 383 checkModified(); 384 ready.countDown(); 385 if (!waitForChanges(service)) 386 break; 387 } catch (InterruptedException | IllegalAccessError ignored) { 388 // NOPMD 389 } catch (Throwable t) { 390 log.error("start", t.getMessage()); 391 relax(); 392 } 393 } 394 undeploy(); 395 try { 396 if (server.isRegistered(loaderName)) 397 server.unregisterMBean(loaderName); 398 } catch (InstanceNotFoundException e) { 399 log.error(e); 400 } 401 if (decorator != null) { 402 decorator.uninitialize(); 403 } 404 if (exit && !shuttingDown) 405 System.exit(0); 406 } catch (IllegalAccessError ignored) { 407 // NOPMD OK to happen 408 } catch (Exception e) { 409 if (log != null) 410 log.error (e); 411 else 412 e.printStackTrace(); 413 System.exit (1); 414 } finally { 415 stopJFR(); 416 } 417 } 418 /** 419 * Initiates an orderly shutdown of Q2 without waiting for the worker thread to terminate. 420 */ 421 public void shutdown () { 422 shutdown(false); 423 } 424 /** 425 * Returns whether Q2 has started and has not yet been shut down. 426 * 427 * @return {@code true} once {@link #run()} has begun and shutdown has not been requested 428 */ 429 public boolean running() { 430 return started && shutdown.getCount() > 0; 431 } 432 /** 433 * Returns whether Q2 has finished its initial deployment scan and is ready 434 * to serve requests. 435 * 436 * @return {@code true} once the initial deploy has completed and shutdown is not in progress 437 */ 438 public boolean ready() { 439 return ready.getCount() == 0 && shutdown.getCount() > 0; 440 } 441 /** 442 * Waits up to {@code millis} milliseconds for Q2 to become {@link #ready() ready}. 443 * 444 * @param millis maximum time to wait, in milliseconds 445 * @return {@code true} if Q2 is ready when this method returns 446 */ 447 public boolean ready (long millis) { 448 try { 449 ready.await(millis, TimeUnit.MILLISECONDS); 450 } catch (InterruptedException ignored) { } 451 return ready(); 452 } 453 /** 454 * Initiates shutdown and optionally blocks until the worker thread terminates. 455 * 456 * @param join when {@code true}, waits up to {@link #SHUTDOWN_TIMEOUT} ms for the 457 * Q2 worker thread to terminate 458 */ 459 public void shutdown (boolean join) { 460 if (log != null) { 461 audit(auditStop(Duration.between(startTime, Instant.now()))); 462 } 463 shutdown.countDown(); 464 unregisterQ2(); 465 if (q2Thread != null) { 466 // log.info ("shutting down"); 467 q2Thread.interrupt (); 468 if (join) { 469 try { 470 q2Thread.join(SHUTDOWN_TIMEOUT); 471 log.info ("shutdown done"); 472 } catch (InterruptedException e) { 473 log.warn (e); 474 } 475 } 476 } 477 q2Thread = null; 478 } 479 /** 480 * Returns the dynamic class loader Q2 uses to load deployed QBeans. 481 * 482 * @return the active {@link QClassLoader} 483 */ 484 public QClassLoader getLoader () { 485 return loader; 486 } 487 /** 488 * Returns the {@link QFactory} responsible for instantiating QBeans from deploy descriptors. 489 * 490 * @return the active {@link QFactory} 491 */ 492 public QFactory getFactory () { 493 return factory; 494 } 495 /** 496 * Returns the original command-line arguments Q2 was launched with. 497 * 498 * @return the command-line argument array 499 */ 500 public String[] getCommandLineArgs() { 501 return args; 502 } 503 /** 504 * {@link FileFilter} hook used by the deploy-directory scan: accepts readable 505 * XML descriptors and, when recursive scanning is enabled, sub-directories 506 * other than {@code lib}. 507 * 508 * @param f candidate file 509 * @return {@code true} if {@code f} should be considered for deployment 510 */ 511 public boolean accept (File f) { 512 return f.canRead() && 513 (isXml(f) || 514 recursive && f.isDirectory() && !"lib".equalsIgnoreCase (f.getName())); 515 } 516 /** 517 * Returns the directory Q2 scans for deploy descriptors. 518 * 519 * @return the deploy directory 520 */ 521 public File getDeployDir () { 522 return deployDir; 523 } 524 525 /** 526 * Returns the configured {@code WatchService} class name, or {@code null} 527 * when polling is used. 528 * 529 * @return the fully-qualified watch-service class name, or {@code null} 530 */ 531 public String getWatchServiceClassname() { 532 return watchServiceClassname; 533 } 534 535 /** 536 * Returns the running Q2 instance from the {@link NameRegistrar}, if any. 537 * 538 * @return the registered Q2 instance, or {@code null} if none is registered 539 */ 540 public static Q2 getQ2() { 541 return NameRegistrar.getIfExists(JMX_NAME); 542 } 543 /** 544 * Waits up to {@code timeout} milliseconds for a Q2 instance to register itself. 545 * 546 * @param timeout maximum time to wait, in milliseconds 547 * @return the registered Q2 instance 548 */ 549 public static Q2 getQ2(long timeout) { 550 return NameRegistrar.get(JMX_NAME, timeout); 551 } 552 553 /** 554 * Returns the node identifier embedded in the licensee descriptor. 555 * 556 * @return the node identifier 557 */ 558 public static int node() { 559 return PGPHelper.node(); 560 } 561 private boolean isXml(File f) { 562 return f != null && f.getName().toLowerCase().endsWith(".xml"); 563 } 564 private boolean scan () { 565 boolean rc = false; 566 File file[] = deployDir.listFiles (this); 567 // Arrays.sort (file); --apr not required - we use TreeMap 568 if (file == null) { 569 // Shutting down might be best, how to trigger from within? 570 throw new Error("Deploy directory \""+deployDir.getAbsolutePath()+"\" is not available"); 571 } else { 572 for (File f : file) { 573 if (register(f)) 574 rc = true; 575 } 576 } 577 return rc; 578 } 579 580 private void deploy () { 581 List<ObjectInstance> startList = new ArrayList<ObjectInstance>(); 582 Iterator<Map.Entry<File,QEntry>> iter = dirMap.entrySet().iterator(); 583 584 try { 585 while (iter.hasNext() && shutdown.getCount() > 0) { 586 Map.Entry<File,QEntry> entry = iter.next(); 587 File f = entry.getKey (); 588 QEntry qentry = entry.getValue (); 589 long deployed = qentry.getDeployed (); 590 if (deployed == 0) { 591 if (deploy(f)) { 592 if (qentry.isQBean ()) { 593 if (qentry.isEagerStart()) 594 start(qentry.getInstance()); 595 else 596 startList.add(qentry.getInstance()); 597 } 598 qentry.setDeployed (f.lastModified ()); 599 } else { 600 // deploy failed, clean up. 601 iter.remove(); 602 } 603 } else if (deployed != f.lastModified ()) { 604 undeploy (f); 605 iter.remove (); 606 loader.forceNewClassLoaderOnNextScan(); 607 } 608 } 609 for (ObjectInstance instance : startList) 610 start(instance); 611 } 612 catch (Exception e){ 613 log.error ("deploy", e); 614 } 615 } 616 617 private void undeploy () { 618 Object[] set = dirMap.entrySet().toArray (); 619 int l = set.length; 620 621 while (l-- > 0) { 622 Map.Entry entry = (Map.Entry) set[l]; 623 File f = (File) entry.getKey (); 624 undeploy (f); 625 } 626 } 627 628 private void addShutdownHook () { 629 Runtime.getRuntime().addShutdownHook ( 630 new Thread ("Q2-ShutdownHook") { 631 public void run () { 632 audit (new Shutdown(getInstanceId(), shutdownHookDelay)); 633 if (shutdownHookDelay > 0) 634 ISOUtil.sleep(shutdownHookDelay); 635 636 audit(auditStop(Duration.between(startTime, Instant.now()))); 637 shuttingDown = true; 638 shutdown.countDown(); 639 if (q2Thread != null) { 640 try { 641 q2Thread.join (SHUTDOWN_TIMEOUT); 642 } catch (InterruptedException ignored) { 643 // NOPMD nothing to do 644 } catch (NullPointerException ignored) { 645 // NOPMD 646 // on thin Q2 systems where shutdown is very fast, 647 // q2Thread can become null between the upper if and 648 // the actual join. Not a big deal so we ignore the 649 // exception. 650 } 651 } 652 } 653 } 654 ); 655 } 656 657 private void checkModified () { 658 Iterator iter = dirMap.entrySet().iterator(); 659 while (iter.hasNext()) { 660 Map.Entry entry = (Map.Entry) iter.next(); 661 File f = (File) entry.getKey (); 662 QEntry qentry = (QEntry) entry.getValue (); 663 if (qentry.isQBean() && qentry.isQPersist()) { 664 ObjectName name = qentry.getObjectName (); 665 if (getState (name) == QBean.STARTED && isModified (name)) { 666 qentry.setDeployed (persist (f, name)); 667 } 668 } 669 } 670 } 671 672 private int getState (ObjectName name) { 673 int status = -1; 674 if (name != null) { 675 try { 676 status = (Integer) server.getAttribute(name, "State"); 677 } catch (Exception e) { 678 log.warn ("getState", e); 679 } 680 } 681 return status; 682 } 683 private boolean isModified (ObjectName name) { 684 boolean modified = false; 685 if (name != null) { 686 try { 687 modified = (Boolean) server.getAttribute(name, "Modified"); 688 } catch (Exception ignored) { 689 // NOPMD Okay to fail 690 } 691 } 692 return modified; 693 } 694 private long persist (File f, ObjectName name) { 695 long deployed = f.lastModified (); 696 try { 697 Element e = (Element) server.getAttribute (name, "Persist"); 698 if (e != null) { 699 XMLOutputter out = new XMLOutputter (Format.getPrettyFormat()); 700 Document doc = new Document (); 701 e.detach(); 702 doc.setRootElement (e); 703 File tmp = new File (f.getAbsolutePath () + ".tmp"); 704 Writer writer = new BufferedWriter(new FileWriter(tmp)); 705 try { 706 out.output (doc, writer); 707 } finally { 708 writer.close (); 709 } 710 f.delete(); 711 tmp.renameTo (f); 712 deployed = f.lastModified (); 713 } 714 } catch (Exception ex) { 715 log.warn ("persist", ex); 716 } 717 return deployed; 718 } 719 720 private void undeploy (File f) { 721 QEntry qentry = dirMap.get (f); 722 LogEvent evt = getDeployLog().createInfo().withTraceId(getInstanceId()); 723 try { 724 if (evt != null) 725 evt.addMessage (new UnDeploy(f.getCanonicalPath())); 726 727 if (qentry.isQBean()) { 728 Object obj = qentry.getObject (); 729 ObjectName name = qentry.getObjectName (); 730 factory.destroyQBean (this, name, obj); 731 } 732 } catch (Exception e) { 733 if (evt != null) 734 evt.addMessage (e); 735 } finally { 736 if (evt != null) 737 Logger.log(evt); 738 } 739 } 740 741 private boolean register (File f) { 742 boolean rc = false; 743 if (f.isDirectory()) { 744 File file[] = f.listFiles (this); 745 for (File aFile : file) { 746 if (register(aFile)) 747 rc = true; 748 } 749 } else if (dirMap.get (f) == null) { 750 dirMap.put (f, new QEntry ()); 751 rc = true; 752 } 753 return rc; 754 } 755 756 private boolean deploy (File f) { 757 LogEvent evt = getDeployLog().createInfo().withTraceId(getInstanceId()); 758 boolean enabled; 759 try { 760 QEntry qentry = dirMap.get (f); 761 SAXBuilder builder = createSAXBuilder(); 762 Document doc; 763 if(decorator!=null && !f.getName().equals(LOGGER_CONFIG)) 764 { 765 doc=decrypt(builder.build(new StringReader(decorator.decorateFile(f)))); 766 } 767 else 768 { 769 doc=decrypt(builder.build(f)); 770 } 771 772 Element rootElement = doc.getRootElement(); 773 String iuuid = rootElement.getAttributeValue ("instance"); 774 if (iuuid != null) { 775 UUID uuid = UUID.fromString(iuuid); 776 if (!uuid.equals (getInstanceId())) { 777 deleteFile (f, iuuid); 778 return false; 779 } 780 } 781 enabled = QFactory.isEnabled(rootElement); 782 qentry.setEagerStart(QFactory.isEagerStart(rootElement)); 783 if (evt != null) 784 evt.addMessage(new Deploy(f.getCanonicalPath(), enabled, qentry.isEagerStart())); 785 if (enabled) { 786 Object obj = factory.instantiate (this, factory.expandEnvProperties(rootElement)); 787 qentry.setObject (obj); 788 ObjectInstance instance = factory.createQBean ( 789 this, doc.getRootElement(), obj 790 ); 791 qentry.setInstance (instance); 792 } 793 } 794 catch (InstanceAlreadyExistsException e) { 795 /* 796 * Ok, the file we tried to deploy, holds an object 797 * that already has been deployed. 798 * 799 * Rename it out of the way. 800 * 801 */ 802 tidyFileAway(f,DUPLICATE_EXTENSION, evt); 803 if (evt != null) 804 evt.addMessage(e); 805 return false; 806 } 807 catch (Exception e) { 808 if (evt != null) { 809 evt.addMessage(e); 810 } 811 tidyFileAway(f,ERROR_EXTENSION, evt); 812 // This will also save deploy error repeats... 813 return false; 814 } 815 catch (Error e) { 816 if (evt != null) 817 evt.addMessage(e); 818 tidyFileAway(f,ENV_EXTENSION, evt); 819 // This will also save deploy error repeats... 820 return false; 821 } finally { 822 if (evt != null) { 823 Logger.log(evt); 824 } 825 } 826 return true ; 827 } 828 829 private void start (ObjectInstance instance) { 830 try { 831 factory.startQBean (this, instance.getObjectName()); 832 } catch (Exception e) { 833 getLog().warn ("start", e); 834 } 835 } 836 /** 837 * Sleeps the calling thread for at most {@code sleep} milliseconds, returning 838 * early if shutdown is requested in the meantime. 839 * 840 * @param sleep maximum sleep time, in milliseconds 841 */ 842 public void relax (long sleep) { 843 try { 844 shutdown.await(sleep, TimeUnit.MILLISECONDS); 845 } catch (InterruptedException ignored) { } 846 } 847 /** 848 * Sleeps for one second, returning early on shutdown. 849 */ 850 public void relax () { 851 relax (1000); 852 } 853 private void initSystemLogger () { 854 File loggerConfig = new File (deployDir, LOGGER_CONFIG); 855 if (loggerConfig.canRead()) { 856 hasSystemLogger = true; 857 try { 858 register (loggerConfig); 859 deploy (); 860 } catch (Exception e) { 861 getLog().warn ("init-system-logger", e); 862 } 863 } 864 audit (auditStart()); 865 } 866 /** 867 * Returns the lifecycle log used by Q2 itself, lazily attaching a stdout 868 * listener when no logger is configured and Q2 is not running under the CLI. 869 * 870 * @return the Q2 lifecycle {@link Log} 871 */ 872 public Log getLog () { 873 if (log == null) { 874 Logger logger = Logger.getLogger (LOGGER_NAME); 875 if (!hasSystemLogger && !logger.hasListeners() && cli == null) 876 logger.addListener (new SimpleLogListener (System.out)); 877 log = new Log (logger, REALM); 878 log.setDefaultTag("service", name); 879 } 880 return log; 881 } 882 883 private Log getDeployLog() { 884 if (deployLog == null) { 885 deployLog = new Log(getLog().getLogger(), Realm.Q2_DEPLOY); 886 deployLog.setDefaultTag("service", name); 887 } 888 return deployLog; 889 } 890 /** 891 * Returns the JMX MBean server backing Q2's QBean registrations. 892 * 893 * @return the active {@link MBeanServer} 894 */ 895 public MBeanServer getMBeanServer () { 896 return server; 897 } 898 /** 899 * Returns Q2's uptime since {@link #run()} captured the start instant. 900 * 901 * @return the elapsed duration between start and now 902 */ 903 public Duration getUptime() { 904 return Duration.between(startTime, Instant.now()); 905 } 906 /** 907 * Prints the Q2 version banner to {@code System.out}. 908 */ 909 public void displayVersion () { 910 System.out.println(getVersionString()); 911 } 912 /** 913 * Returns the per-process random identifier assigned to this Q2 instance. 914 * 915 * @return a stable {@link UUID} for the running instance 916 */ 917 public UUID getInstanceId() { 918 return instanceId; 919 } 920 /** 921 * Builds the multi-line version banner used by {@link #displayVersion()}, 922 * combining jPOS version metadata with any embedded application metadata. 923 * 924 * @return the formatted version banner 925 */ 926 public static String getVersionString() { 927 String appVersionString = getAppVersionString(); 928 int l = PGPHelper.checkLicense(); 929 String sl = l > 0 ? " " + Integer.toString(l,16) : ""; 930 StringBuilder vs = new StringBuilder(); 931 if (appVersionString != null) { 932 vs.append( 933 String.format ("jPOS %s %s/%s%s (%s)%n%s%s", 934 getVersion(), getBranch(), getRevision(), sl, getBuildTimestamp(), appVersionString, getLicensee() 935 ) 936 ); 937 } else { 938 vs.append( 939 String.format("jPOS %s %s/%s%s (%s) %s", 940 getVersion(), getBranch(), getRevision(), sl, getBuildTimestamp(), getLicensee() 941 ) 942 ); 943 } 944// if ((l & 0xE0000) > 0) 945// throw new IllegalAccessError(vs); 946 947 return vs.toString(); 948 } 949 950 private static String getLicensee() { 951 String s = null; 952 try { 953 s = PGPHelper.getLicensee(); 954 } catch (IOException ignored) { 955 // NOPMD: ignore 956 } 957 return s; 958 } 959 private void parseCmdLine (String[] args, boolean environmentOnly) { 960 CommandLineParser parser = new DefaultParser (); 961 962 Options options = new Options (); 963 options.addOption ("v","version", false, "Q2's version"); 964 options.addOption ("d","deploydir", true, "Deployment directory"); 965 options.addOption ("r","recursive", false, "Deploy subdirectories recursively"); 966 options.addOption ("h","help", false, "Usage information"); 967 options.addOption ("C","config", true, "Configuration bundle"); 968 options.addOption ("e","encrypt", true, "Encrypt configuration bundle"); 969 options.addOption ("i","cli", false, "Command Line Interface"); 970 options.addOption ("c","command", true, "Command to execute"); 971 options.addOption ("p", "pid-file", true, "Store project's pid"); 972 options.addOption ("n", "name", true, "Optional name (defaults to 'Q2')"); 973 options.addOption ("sd", "shutdown-delay", true, "Shutdown delay in seconds (defaults to immediate)"); 974 options.addOption ("Ns", "no-scan", false, "Disables deploy directory scan"); 975 options.addOption ("Nd", "no-dynamic", false, "Disables dynamic classloader"); 976 options.addOption ("Nf", "no-jfr", false, "Disables Java Flight Recorder"); 977 options.addOption ("Nh", "no-shutdown-hook", false, "Disable shutdown hook"); 978 options.addOption ("E", "environment", true, "Environment name.\nCan be given multiple times (applied in order, and values may override previous ones)"); 979 options.addOption ("Ed", "envdir", true, "Environment file directory, defaults to cfg"); 980 options.addOption ("mp", "metrics-port", true, "Metrics port"); 981 options.addOption ("mP", "metrics-path", true, "Metrics path"); 982 983 try { 984 System.setProperty("log4j2.formatMsgNoLookups", "true"); // log4shell prevention 985 986 CommandLine line = parser.parse (options, args); 987 // set up envdir and env before other parts of the system, so env is available 988 // force reload if any of the env options was changed 989 if (line.hasOption("Ed")) { 990 System.setProperty("jpos.envdir", line.getOptionValue("Ed")); 991 } 992 if (line.hasOption("E")) { 993 System.setProperty("jpos.env", ISOUtil.commaEncode(line.getOptionValues("E"))); 994 } 995 996 if (environmentOnly) // first call just to properly parse environment, in order to get optional q2.args from yaml 997 return; 998 999 if (line.hasOption ("v")) { 1000 displayVersion(); 1001 System.exit (0); 1002 } 1003 if (line.hasOption ("h")) { 1004 HelpFormatter helpFormatter = new HelpFormatter (); 1005 helpFormatter.printHelp ("Q2", options); 1006 System.exit (0); 1007 } 1008 if (line.hasOption ("c")) { 1009 cli = new CLI(this, line.getOptionValue("c"), line.hasOption("i")); 1010 } else if (line.hasOption ("i")) 1011 cli = new CLI(this, null, true); 1012 1013 String dir = DEFAULT_DEPLOY_DIR; 1014 if (line.hasOption ("d")) { 1015 dir = line.getOptionValue ("d"); 1016 } else if (cli != null) 1017 dir = dir + "-" + "cli"; 1018 recursive = line.hasOption ("r"); 1019 this.deployDir = new File (dir); 1020 if (line.hasOption ("C")) 1021 deployBundle (new File (line.getOptionValue ("C")), false); 1022 if (line.hasOption ("e")) 1023 deployBundle (new File (line.getOptionValue ("e")), true); 1024 if (line.hasOption("p")) 1025 pidFile = line.getOptionValue("p"); 1026 if (line.hasOption("n")) 1027 name = line.getOptionValue("n"); 1028 1029 disableDeployScan = line.hasOption("Ns"); 1030 disableDynamicClassloader = line.hasOption("Nd"); 1031 disableJFR = line.hasOption("Nf"); 1032 sshPort = Integer.parseInt(line.getOptionValue("sp", "2222")); 1033 sshAuthorizedKeys = line.getOptionValue ("sa", "cfg/authorized_keys"); 1034 sshUser = line.getOptionValue("su", "admin"); 1035 sshHostKeyFile = line.getOptionValue("sh", "cfg/hostkeys.ser"); 1036 if (line.hasOption("mp")) 1037 metricsPort = Integer.parseInt(line.getOptionValue("mp")); 1038 metricsPath = line.hasOption("mP") ? line.getOptionValue("mP") : "/metrics"; 1039 noShutdownHook = line.hasOption("Nh"); 1040 shutdownHookDelay = line.hasOption ("sd") ? 1000L*Integer.parseInt(line.getOptionValue("sd")) : 0; 1041 1042 if (noShutdownHook && shutdownHookDelay > 0) 1043 throw new IllegalArgumentException ("--no-shutdown-hook incompatible with --shutdown-delay argument"); 1044 } catch (MissingArgumentException | IllegalArgumentException | IllegalAccessError | 1045 UnrecognizedOptionException e) { 1046 System.out.println("ERROR: " + e.getMessage()); 1047 System.exit(1); 1048 } catch (Exception e) { 1049 e.printStackTrace (); 1050 System.exit (1); 1051 } 1052 } 1053 private void deployBundle (File bundle, boolean encrypt) 1054 throws JDOMException, IOException, 1055 ISOException, GeneralSecurityException 1056 { 1057 SAXBuilder builder = createSAXBuilder(); 1058 Document doc = builder.build (bundle); 1059 Iterator iter = doc.getRootElement().getChildren ().iterator (); 1060 for (int i=1; iter.hasNext (); i ++) { 1061 Element e = (Element) iter.next(); 1062 deployElement (e, String.format ("%02d_%s.xml",i, e.getName()), encrypt, !encrypt); 1063 // the !encrypt above is tricky and deserves an explanation 1064 // if we are encrypting a QBean, we want it to stay in the deploy 1065 // directory for future runs. If on the other hand we are deploying 1066 // a bundle, we want it to be transient. 1067 } 1068 } 1069 /** 1070 * Writes a single QBean descriptor to {@code fileName} inside the deploy 1071 * directory, optionally encrypting it and/or marking it transient. 1072 * 1073 * @param e QBean descriptor element 1074 * @param fileName target file name relative to the deploy directory 1075 * @param encrypt when {@code true} the descriptor is DES-encrypted 1076 * before being written 1077 * @param isTransient when {@code true} the file is tagged with the running 1078 * instance id and removed on JVM exit 1079 * @throws ISOException if encryption fails 1080 * @throws IOException if writing the descriptor fails 1081 * @throws GeneralSecurityException if the encryption cipher is unavailable 1082 */ 1083 public void deployElement (Element e, String fileName, boolean encrypt, boolean isTransient) 1084 throws ISOException, IOException, GeneralSecurityException 1085 { 1086 e = e.clone (); 1087 1088 boolean pretty = "true".equalsIgnoreCase(Environment.get("template.pretty", "true")); 1089 XMLOutputter out = new XMLOutputter (pretty ? Format.getPrettyFormat() : Format.getRawFormat()); 1090 Document doc = new Document (); 1091 doc.setRootElement(e); 1092 File qbean = new File (deployDir, fileName); 1093 if (isTransient) { 1094 e.setAttribute("instance", getInstanceId().toString()); 1095 qbean.deleteOnExit(); 1096 } 1097 if (encrypt) { 1098 doc = encrypt (doc); 1099 } 1100 try (Writer writer = new BufferedWriter(new FileWriter(qbean))) { 1101 out.output(doc, writer); 1102 } 1103 } 1104 1105 private byte[] dodes (byte[] data, int mode) 1106 throws GeneralSecurityException 1107 { 1108 Cipher cipher = Cipher.getInstance("DES"); 1109 cipher.init (mode, new SecretKeySpec(getKey(), "DES")); 1110 return cipher.doFinal (data); 1111 } 1112 /** 1113 * Derives the 8-byte DES key used to protect deploy descriptors. Subclasses 1114 * may override to source the key material from a secret manager. 1115 * 1116 * @return the DES key for {@link #encrypt(Document)} / {@link #decrypt(Document)} 1117 */ 1118 protected byte[] getKey() { 1119 return 1120 ISOUtil.xor(SystemSeed.getSeed(8, 8), 1121 ISOUtil.hex2byte(System.getProperty("jpos.deploy.key", "BD653F60F980F788"))); 1122 } 1123 /** 1124 * Wraps {@code doc} in a {@code <protected-qbean>} envelope whose body is the 1125 * DES-encrypted, hex-encoded XML payload. 1126 * 1127 * @param doc QBean descriptor to encrypt 1128 * @return an encrypted document suitable for writing to the deploy directory 1129 * @throws GeneralSecurityException if the DES cipher fails 1130 * @throws IOException if serialising the input document fails 1131 */ 1132 protected Document encrypt (Document doc) 1133 throws GeneralSecurityException, IOException 1134 { 1135 ByteArrayOutputStream os = new ByteArrayOutputStream (); 1136 OutputStreamWriter writer = new OutputStreamWriter (os); 1137 XMLOutputter out = new XMLOutputter (Format.getPrettyFormat()); 1138 out.output(doc, writer); 1139 writer.close (); 1140 1141 byte[] crypt = dodes (os.toByteArray(), Cipher.ENCRYPT_MODE); 1142 1143 Document secureDoc = new Document (); 1144 Element root = new Element (PROTECTED_QBEAN); 1145 secureDoc.setRootElement (root); 1146 Element secureData = new Element ("data"); 1147 root.addContent (secureData); 1148 1149 secureData.setText ( 1150 ISOUtil.hexString (crypt) 1151 ); 1152 return secureDoc; 1153 } 1154 1155 /** 1156 * Reverses {@link #encrypt(Document)}: when {@code doc} is a 1157 * {@code <protected-qbean>} envelope, returns the decrypted descriptor; 1158 * otherwise returns {@code doc} unchanged. 1159 * 1160 * @param doc descriptor read from the deploy directory 1161 * @return decrypted document, or {@code doc} when not an encrypted envelope 1162 * @throws GeneralSecurityException if the DES cipher fails 1163 * @throws IOException if reading the embedded payload fails 1164 * @throws JDOMException if the decrypted payload is not well-formed XML 1165 */ 1166 protected Document decrypt (Document doc) 1167 throws GeneralSecurityException, IOException, JDOMException 1168 { 1169 Element root = doc.getRootElement (); 1170 if (PROTECTED_QBEAN.equals (root.getName ())) { 1171 Element data = root.getChild ("data"); 1172 if (data != null) { 1173 ByteArrayInputStream is = new ByteArrayInputStream ( 1174 dodes ( 1175 ISOUtil.hex2byte (data.getTextTrim()), 1176 Cipher.DECRYPT_MODE) 1177 ); 1178 SAXBuilder builder = createSAXBuilder(); 1179 doc = builder.build (is); 1180 } 1181 } 1182 return doc; 1183 } 1184 1185 private void tidyFileAway (File f, String extension, LogEvent evt) { 1186 File rename = new File(f.getAbsolutePath()+"."+extension); 1187 while (rename.exists()){ 1188 rename = new File(rename.getAbsolutePath()+"."+extension); 1189 } 1190 if (evt != null) { 1191 if (f.renameTo(rename)){ 1192 evt.addMessage( 1193 new DeployActivity(DeployActivity.Action.RENAME, String.format ("%s to %s", f.getAbsolutePath(), rename.getAbsolutePath())) 1194 ); 1195 } 1196 else { 1197 evt.addMessage( 1198 new DeployActivity(DeployActivity.Action.RENAME_ERROR, String.format ("%s to %s", f.getAbsolutePath(), rename.getAbsolutePath())) 1199 ); 1200 } 1201 } 1202 } 1203 1204 private void deleteFile (File f, String iuuid) { 1205 f.delete(); 1206 getLog().info(String.format("Deleted transient descriptor %s (%s)", f.getAbsolutePath(), iuuid)); 1207 } 1208 1209 private void initConfigDecorator() 1210 { 1211 InputStream in=Q2.class.getClassLoader().getResourceAsStream("META-INF/org/jpos/config/Q2-decorator.properties"); 1212 try 1213 { 1214 if(in!=null) 1215 { 1216 PropertyResourceBundle bundle=new PropertyResourceBundle(in); 1217 String ccdClass=bundle.getString("config-decorator-class"); 1218 if(log!=null) log.info("Initializing config decoration provider: "+ccdClass); 1219 decorator= (ConfigDecorationProvider) Q2.class.getClassLoader().loadClass(ccdClass).newInstance(); 1220 decorator.initialize(getDeployDir()); 1221 } 1222 } 1223 catch (IOException ignored) 1224 { 1225 // NOPMD OK to happen 1226 } 1227 catch (Exception e) 1228 { 1229 if(log!=null) log.error(e); 1230 else 1231 { 1232 e.printStackTrace(); 1233 } 1234 } 1235 finally 1236 { 1237 if(in!=null) 1238 { 1239 try 1240 { 1241 in.close(); 1242 } 1243 catch (IOException ignored) 1244 { 1245 // NOPMD nothing to do 1246 } 1247 } 1248 } 1249 } 1250 private void logVersion () throws IOException { 1251 long now = System.currentTimeMillis(); 1252 if (now - lastVersionLog > 86400000L) { 1253 License l = PGPHelper.getLicense(); 1254 audit(l); 1255 lastVersionLog = now; 1256 while (running() && (l.status() & 0xF0000) != 0) 1257 relax(60000L); 1258 } 1259 } 1260 private void setExit (boolean exit) { 1261 this.exit = exit; 1262 } 1263 private SAXBuilder createSAXBuilder () { 1264 SAXBuilder builder = new SAXBuilder (); 1265 builder.setFeature("http://xml.org/sax/features/namespaces", true); 1266 builder.setFeature("http://apache.org/xml/features/xinclude", true); 1267 return builder; 1268 } 1269 /** 1270 * Standalone entry point: instantiates Q2, requests {@code System.exit(0)} 1271 * on shutdown, and starts the worker thread. 1272 * 1273 * @param args command-line arguments accepted by {@link #Q2(String[])} 1274 * @throws Exception if Q2 fails to construct or start 1275 */ 1276 public static void main (String[] args) throws Exception { 1277 Q2 q2 = new Q2(args); 1278 q2.setExit (true); 1279 q2.start(); 1280 } 1281 /** 1282 * Returns the jPOS build version, sourced from the bundled buildinfo resource. 1283 * 1284 * @return the jPOS version string 1285 */ 1286 public static String getVersion() { 1287 return getBundle("org/jpos/q2/buildinfo").getString ("version"); 1288 } 1289 /** 1290 * Returns the source revision of the running jPOS build. 1291 * 1292 * @return the revision string from the bundled revision resource 1293 */ 1294 public static String getRevision() { 1295 return getBundle("org/jpos/q2/revision").getString ("revision"); 1296 } 1297 /** 1298 * Returns the source branch of the running jPOS build. 1299 * 1300 * @return the branch name from the bundled revision resource 1301 */ 1302 public static String getBranch() { 1303 return getBundle("org/jpos/q2/revision").getString ("branch"); 1304 } 1305 /** 1306 * Returns the timestamp at which the running jPOS jar was built. 1307 * 1308 * @return the build timestamp string from the bundled buildinfo resource 1309 */ 1310 public static String getBuildTimestamp() { 1311 return getBundle("org/jpos/q2/buildinfo").getString ("buildTimestamp"); 1312 } 1313 /** 1314 * Returns a release identifier combining {@link #getVersion()} and {@link #getRevision()}. 1315 * 1316 * @return version and revision separated by a space 1317 */ 1318 public static String getRelease() { 1319 return getVersion() + " " + getRevision(); 1320 } 1321 /** 1322 * Returns the application's version banner, when an embedded application has 1323 * deployed its own {@code buildinfo}/{@code revision} resources, or 1324 * {@code null} when none is present. 1325 * 1326 * @return the formatted application version string, or {@code null} when absent 1327 */ 1328 public static String getAppVersionString() { 1329 try { 1330 ResourceBundle buildinfo = getBundle("buildinfo"); 1331 ResourceBundle revision = getBundle("revision"); 1332 1333 return String.format ("%s %s %s/%s (%s)", 1334 buildinfo.getString("projectName"), 1335 buildinfo.getString("version"), 1336 revision.getString("branch"), 1337 revision.getString("revision"), 1338 buildinfo.getString("buildTimestamp") 1339 ); 1340 } catch (MissingResourceException ignored) { 1341 return null; 1342 } 1343 } 1344 /** 1345 * Returns the JVM class path, expanded against the {@code Class-Path} 1346 * manifest entry when the JVM was launched with a single executable jar. 1347 * 1348 * @return the class path; never {@code null}, possibly empty 1349 */ 1350 public static String getClassPath() { 1351 try { 1352 String cp = System.getProperty("java.class.path"); 1353 if (cp != null && !cp.contains(File.pathSeparator)) { 1354 File jarFile = new File(cp); 1355 if (jarFile.isFile()) { 1356 try (JarFile jar = new JarFile(jarFile)) { 1357 Manifest manifest = jar.getManifest(); 1358 if (manifest != null) { 1359 String classPath = manifest.getMainAttributes().getValue(Attributes.Name.CLASS_PATH); 1360 return classPath != null ? jarFile.getName() + " " + classPath : jarFile.getName(); 1361 } 1362 } 1363 } 1364 } 1365 } catch (Exception ignored) { } 1366 return ""; 1367 } 1368 /** 1369 * Returns the SHA-1 hash of the class path returned by {@link #getClassPath()}. 1370 * 1371 * @return the hex-encoded SHA-1 hash, or an empty string when the class path 1372 * is empty or hashing fails 1373 */ 1374 public static String getClassPathHash() { 1375 String cp = getClassPath(); 1376 if (cp.isEmpty()) return ""; 1377 try { 1378 MessageDigest md = MessageDigest.getInstance("SHA-1"); 1379 return HexFormat.of().formatHex(md.digest(cp.getBytes(StandardCharsets.UTF_8))); 1380 } catch (Exception ignored) { } 1381 return ""; 1382 } 1383 /** 1384 * Returns {@code true} if the dynamic class loader has been disabled via command-line options. 1385 * 1386 * @return {@code true} if dynamic classloading is disabled 1387 */ 1388 public boolean isDisableDynamicClassloader() { 1389 return disableDynamicClassloader; 1390 } 1391 1392 /** 1393 * Bookkeeping record for a deployed QBean: when it was deployed, its JMX 1394 * registration, the underlying object, and whether it should start eagerly. 1395 */ 1396 public static class QEntry { 1397 long deployed; 1398 ObjectInstance instance; 1399 Object obj; 1400 boolean eagerStart; 1401 /** Creates an empty entry; fields are populated as deployment progresses. */ 1402 public QEntry () { 1403 super(); 1404 } 1405 /** 1406 * Creates an entry pre-populated with a deploy timestamp and JMX instance. 1407 * 1408 * @param deployed deploy timestamp in milliseconds since the epoch 1409 * @param instance JMX registration for the deployed QBean 1410 */ 1411 public QEntry (long deployed, ObjectInstance instance) { 1412 super(); 1413 this.deployed = deployed; 1414 this.instance = instance; 1415 } 1416 /** 1417 * Returns the deploy timestamp. 1418 * 1419 * @return deploy timestamp in milliseconds since the epoch 1420 */ 1421 public long getDeployed () { 1422 return deployed; 1423 } 1424 /** 1425 * Updates the deploy timestamp. 1426 * 1427 * @param deployed deploy timestamp in milliseconds since the epoch 1428 */ 1429 public void setDeployed (long deployed) { 1430 this.deployed = deployed; 1431 } 1432 /** 1433 * Sets the JMX registration for the deployed QBean. 1434 * 1435 * @param instance JMX registration for the deployed QBean 1436 */ 1437 public void setInstance (ObjectInstance instance) { 1438 this.instance = instance; 1439 } 1440 /** 1441 * Returns the JMX registration for the deployed QBean. 1442 * 1443 * @return JMX registration for the deployed QBean, or {@code null} if not yet registered 1444 */ 1445 public ObjectInstance getInstance () { 1446 return instance; 1447 } 1448 /** 1449 * Returns the {@link ObjectName} of the deployed QBean. 1450 * 1451 * @return the JMX {@link ObjectName} of the deployed QBean, or {@code null} if not registered 1452 */ 1453 public ObjectName getObjectName () { 1454 return instance != null ? instance.getObjectName () : null; 1455 } 1456 /** 1457 * Binds the underlying QBean (or QPersist) instance. 1458 * 1459 * @param obj underlying QBean (or QPersist) instance 1460 */ 1461 public void setObject (Object obj) { 1462 this.obj = obj; 1463 } 1464 /** 1465 * Returns the underlying QBean (or QPersist) instance. 1466 * 1467 * @return underlying QBean (or QPersist) instance, or {@code null} when not bound 1468 */ 1469 public Object getObject () { 1470 return obj; 1471 } 1472 /** 1473 * Returns whether the bound object implements {@link QBean}. 1474 * 1475 * @return {@code true} when the bound object implements {@link QBean} 1476 */ 1477 public boolean isQBean () { 1478 return obj instanceof QBean; 1479 } 1480 /** 1481 * Returns whether the bound object implements {@link QPersist}. 1482 * 1483 * @return {@code true} when the bound object implements {@link QPersist} 1484 */ 1485 public boolean isQPersist () { 1486 return obj instanceof QPersist; 1487 } 1488 1489 /** 1490 * Returns whether this QBean is configured for eager start. 1491 * 1492 * @return {@code true} when this QBean should be started immediately on deploy 1493 */ 1494 public boolean isEagerStart() { 1495 return eagerStart; 1496 } 1497 1498 /** 1499 * Sets whether this QBean should start immediately on deploy. 1500 * 1501 * @param eagerStart whether this QBean should be started immediately on deploy 1502 */ 1503 public void setEagerStart(boolean eagerStart) { 1504 this.eagerStart = eagerStart; 1505 } 1506 } 1507 1508 private void writePidFile() { 1509 if (pidFile == null) 1510 return; 1511 1512 File f = new File(pidFile); 1513 try { 1514 if (f.isDirectory()) { 1515 System.err.printf("Q2: pid-file (%s) is a directory%n", pidFile); 1516 System.exit(21); // EISDIR 1517 } 1518 if (!f.createNewFile()) { 1519 System.err.printf("Q2: Unable to write pid-file (%s)%n", pidFile); 1520 System.exit(17); // EEXIST 1521 } 1522 f.deleteOnExit(); 1523 FileOutputStream fow = new FileOutputStream(f); 1524 fow.write(ManagementFactory.getRuntimeMXBean().getName().split("@")[0].getBytes()); 1525 fow.write(System.lineSeparator().getBytes()); 1526 fow.close(); 1527 } catch (IOException e) { 1528 throw new IllegalArgumentException(String.format("Unable to write pid-file (%s)", pidFile), e); 1529 } 1530 } 1531 1532 /** 1533 * Renders a deploy template, prefixing every QBean inside with {@code prefix} 1534 * and writing the result as {@code filename} under the deploy directory. 1535 * Templates can be loaded either from a {@code jar:} URL or from a file path. 1536 * 1537 * @param template path or {@code jar:} URL of the template document 1538 * @param filename target file name relative to the deploy directory 1539 * @param prefix prefix prepended to each contained QBean's name 1540 * @throws IOException if reading the template or writing the result fails 1541 * @throws JDOMException if the template is not well-formed XML 1542 * @throws GeneralSecurityException if encryption is requested and fails 1543 * @throws ISOException if encryption fails 1544 * @throws NullPointerException if {@code template} is {@code null} 1545 */ 1546 public void deployTemplate (String template, String filename, String prefix) 1547 throws IOException, JDOMException, GeneralSecurityException, ISOException, NullPointerException { 1548 if (template.startsWith("jar:")) { 1549 deployResourceTemplate(template, filename, prefix); 1550 } else { 1551 deployFileTemplate(template, filename, prefix); 1552 } 1553 } 1554 1555 private void deployResourceTemplate (String resourceUri, String filename, String prefix) 1556 throws IOException, JDOMException, GeneralSecurityException, ISOException { 1557 // Assume resourceUri starts with "jar:" 1558 try (InputStream is = loader.getResourceAsStream(resourceUri.substring(4))) { 1559 Objects.requireNonNull(is, "resource " + resourceUri + " not present"); 1560 deployTemplateInternal(is, resourceUri, filename, prefix); 1561 } 1562 } 1563 1564 private void deployFileTemplate (String resourceFile, String filename, String prefix) throws IOException, JDOMException, ISOException, GeneralSecurityException { 1565 try (InputStream is = new FileInputStream(resourceFile)) { 1566 deployTemplateInternal(is, resourceFile, filename, prefix); 1567 } 1568 } 1569 1570 private void deployTemplateInternal(InputStream is, String originalResource, String filename, String prefix) throws IOException, JDOMException, ISOException, GeneralSecurityException { 1571 SAXBuilder builder = new SAXBuilder(); 1572 String s = new String(is.readAllBytes()).replaceAll("__PREFIX__", prefix); 1573 Document doc = builder.build(new ByteArrayInputStream(s.getBytes())); 1574 Element root = doc.getRootElement(); 1575 root.addContent(0, new Text("\n ")); 1576 root.addContent(1, new Comment(" Source template "+originalResource+" ")); 1577 deployElement(root, filename, false, true); 1578 } 1579 1580 private boolean waitForChanges (WatchService service) throws InterruptedException { 1581 WatchKey key = service.poll (SCAN_INTERVAL, TimeUnit.MILLISECONDS); 1582 if (key != null) { 1583 LogEvent evt = getDeployLog().createInfo().withTraceId(getInstanceId()); 1584 for (WatchEvent<?> ev : key.pollEvents()) { 1585 if (ev.kind() == StandardWatchEventKinds.ENTRY_CREATE) { 1586 evt.addMessage(new DeployActivity(DeployActivity.Action.CREATE, String.format ("%s/%s", deployDir.getName(), ev.context()))); 1587 } else if (ev.kind() == StandardWatchEventKinds.ENTRY_DELETE) { 1588 evt.addMessage(new DeployActivity(DeployActivity.Action.DELETE, String.format ("%s/%s", deployDir.getName(), ev.context()))); 1589 } else if (ev.kind() == StandardWatchEventKinds.ENTRY_MODIFY) { 1590 evt.addMessage(new DeployActivity(DeployActivity.Action.MODIFY, String.format ("%s/%s", deployDir.getName(), ev.context()))); 1591 } 1592 } 1593 Logger.log(evt); 1594 if (!key.reset()) { 1595 getLog().warn( 1596 String.format ( 1597 "deploy directory '%s' no longer valid", 1598 deployDir.getAbsolutePath()) 1599 ); 1600 return false; // deploy directory no longer valid 1601 } 1602 try { 1603 Environment.reload(); 1604 } catch (IOException e) { 1605 getLog().warn(e); 1606 } 1607 } 1608 return true; 1609 } 1610 1611 private void registerQ2() { 1612 synchronized (Q2.class) { 1613 for (int i=0; ; i++) { 1614 String key = name + (i > 0 ? "-" + i : ""); 1615 if (NameRegistrar.getIfExists(key) == null) { 1616 NameRegistrar.register(key, this); 1617 this.nameRegistrarKey = key; 1618 break; 1619 } 1620 } 1621 } 1622 } 1623 1624 private void unregisterQ2() { 1625 synchronized (Q2.class) { 1626 if (nameRegistrarKey != null) { 1627 NameRegistrar.unregister(nameRegistrarKey); 1628 nameRegistrarKey = null; 1629 } 1630 } 1631 1632 } 1633 1634 private void deployInternal() throws IOException, JDOMException, SAXException, ISOException, GeneralSecurityException { 1635 extractCfg(); 1636 extractDeploy(); 1637 } 1638 private void extractCfg() throws IOException { 1639 List<String> resources = ModuleUtils.getModuleEntries(CFG_PREFIX); 1640 if (resources.size() > 0) 1641 new File("cfg").mkdirs(); 1642 for (String resource : resources) 1643 copyResourceToFile(resource, new File("cfg", resource.substring(CFG_PREFIX.length()))); 1644 } 1645 private void extractDeploy() throws IOException, JDOMException, SAXException, ISOException, GeneralSecurityException { 1646 List<String> qbeans = ModuleUtils.getModuleEntries(DEPLOY_PREFIX); 1647 for (String resource : qbeans) { 1648 if (resource.toLowerCase().endsWith(".xml")) 1649 deployResource(resource); 1650 else 1651 copyResourceToFile(resource, new File("cfg", resource.substring(DEPLOY_PREFIX.length()))); 1652 1653 } 1654 } 1655 1656 private void copyResourceToFile(String resource, File destination) throws IOException { 1657 // taken from @vsalaman's Install using human readable braces as God mandates 1658 try (InputStream source = getClass().getClassLoader().getResourceAsStream(resource)) { 1659 try (FileOutputStream output = new FileOutputStream(destination)) { 1660 int n; 1661 byte[] buffer = new byte[4096]; 1662 while (-1 != (n = source.read(buffer))) { 1663 output.write(buffer, 0, n); 1664 } 1665 } 1666 } 1667 } 1668 private void deployResource(String resource) 1669 throws IOException, JDOMException, GeneralSecurityException, ISOException 1670 { 1671 SAXBuilder builder = new SAXBuilder(); 1672 try (InputStream source = getClass().getClassLoader().getResourceAsStream(resource)) { 1673 Document doc = builder.build(source); 1674 deployElement (doc.getRootElement(), resource.substring(DEPLOY_PREFIX.length()), false,true); 1675 } 1676 } 1677 private void startJFR () { 1678 try { 1679 if (!disableJFR) { 1680 recording = new Recording(Configuration.getConfiguration("default")); 1681 recording.start(); 1682 } 1683 } catch (IOException | ParseException e) { 1684 throw new RuntimeException(e); 1685 } 1686 } 1687 1688 private void stopJFR () { 1689 if (recording != null && recording.getState() == RecordingState.RUNNING) { 1690 recording.stop(); 1691 recording = null; 1692 } 1693 } 1694 1695 private void registerMicroMeter () { 1696 System.setProperty("slf4j.internal.verbosity","ERROR"); 1697 1698 meterRegistry.clear(); // start Q2 off a fresh meter registry 1699 meterRegistry.config().meterFilter(new MeterFilter() { 1700 @Override 1701 public DistributionStatisticConfig configure(Meter.Id id, DistributionStatisticConfig config) { 1702 if (id.getName().equals(MeterInfo.TM_OPERATION.id())) { 1703 return DistributionStatisticConfig.builder().serviceLevelObjectives( 1704 Duration.ofMillis(10).toNanos(), 1705 Duration.ofMillis(100).toNanos(), 1706 Duration.ofMillis(500).toNanos(), 1707 Duration.ofMillis(1000).toNanos(), 1708 Duration.ofMillis(5000).toNanos(), 1709 Duration.ofMillis(15000).toNanos()) 1710 .build() 1711 .merge(config); 1712 } 1713 return config; 1714 } 1715 }); 1716 new ClassLoaderMetrics().bindTo(meterRegistry); 1717 new JvmMemoryMetrics().bindTo(meterRegistry); 1718 new JvmGcMetrics().bindTo(meterRegistry); 1719 new ProcessorMetrics().bindTo(meterRegistry); 1720 new JvmThreadMetrics().bindTo(meterRegistry); 1721 1722 prometheusRegistry.throwExceptionOnRegistrationFailure(); 1723 meterRegistry.add (prometheusRegistry); 1724 } 1725 1726 /** 1727 * Prepends comma-separated values of the {@code q2.args} environment property, 1728 * if any, to the supplied command-line arguments. 1729 * 1730 * @param args original command-line arguments 1731 * @return the merged argument array; {@code args} unchanged when no override is set 1732 */ 1733 public String[] environmentArgs (String[] args) { 1734 String envArgs = Environment.getEnvironment().getProperty("${q2.args}", null); 1735 return (envArgs != null && !"${q2.args}".equals(envArgs) ? 1736 Stream.concat( 1737 Arrays.stream(ISOUtil.commaDecode(envArgs)), Arrays.stream(args)) 1738 .toArray(String[]::new) : args); 1739 } 1740 1741 private void audit (AuditLogEvent sal) { 1742 Logger.log(getLog().createInfo(sal).withTraceId(getInstanceId())); 1743 } 1744 1745 private Start auditStart() { 1746 Environment env = Environment.getEnvironment(); 1747 String envName = env.getName(); 1748 if (env.getErrorString() != null) 1749 envName = envName + " (" + env.getErrorString() + ")"; 1750 return new Start( 1751 getQ2().getInstanceId(), 1752 getVersion(), 1753 getAppVersionString(), 1754 getDeployDir().getAbsolutePath(), 1755 envName 1756 ); 1757 } 1758 1759 private Stop auditStop(Duration dur) { 1760 return new Stop( 1761 getInstanceId(), 1762 dur 1763 ); 1764 } 1765}