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}