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.SimpleLogListener;
063import org.xml.sax.SAXException;
064
065import javax.crypto.Cipher;
066import javax.crypto.spec.SecretKeySpec;
067import javax.management.InstanceAlreadyExistsException;
068import javax.management.InstanceNotFoundException;
069import javax.management.MBeanServer;
070import javax.management.ObjectInstance;
071import javax.management.ObjectName;
072import java.io.*;
073import java.lang.management.ManagementFactory;
074import java.nio.file.FileSystem;
075import java.nio.file.Path;
076import java.nio.file.Paths;
077import java.nio.file.StandardWatchEventKinds;
078import java.nio.file.WatchEvent;
079import java.nio.file.WatchKey;
080import java.nio.file.WatchService;
081import java.security.GeneralSecurityException;
082import java.text.ParseException;
083import java.time.Duration;
084import java.time.Instant;
085import java.util.*;
086import java.util.concurrent.CountDownLatch;
087import java.util.concurrent.TimeUnit;
088import java.util.stream.Stream;
089
090import io.micrometer.core.instrument.binder.jvm.ClassLoaderMetrics;
091import io.micrometer.core.instrument.binder.jvm.JvmGcMetrics;
092import io.micrometer.core.instrument.binder.jvm.JvmMemoryMetrics;
093import io.micrometer.core.instrument.binder.jvm.JvmThreadMetrics;
094import io.micrometer.core.instrument.binder.system.ProcessorMetrics;
095
096
097import static java.util.ResourceBundle.getBundle;
098
099
100/**
101 * @author <a href="mailto:taherkordy@dpi2.dpi.net.ir">Alireza Taherkordi</a>
102 * @author <a href="mailto:apr@cs.com.uy">Alejandro P. Revilla</a>
103 * @author <a href="mailto:alwynschoeman@yahoo.com">Alwyn Schoeman</a>
104 * @author <a href="mailto:vsalaman@vmantek.com">Victor Salaman</a>
105 */
106@SuppressWarnings("unchecked")
107public class Q2 implements FileFilter, Runnable {
108    public static final String DEFAULT_DEPLOY_DIR  = "deploy";
109    public static final String JMX_NAME            = "Q2";
110    public static final String LOGGER_NAME         = "Q2";
111    public static final String REALM               = "Q2.system";
112    public static final String LOGGER_CONFIG       = "00_logger.xml";
113    public static final String QBEAN_NAME          = "Q2:type=qbean,service=";
114    public static final String Q2_CLASS_LOADER     = "Q2:type=system,service=loader";
115    public static final String DUPLICATE_EXTENSION = "DUP";
116    public static final String ERROR_EXTENSION     = "BAD";
117    public static final String ENV_EXTENSION       = "ENV";
118    public static final String LICENSEE            = "LICENSEE.asc";
119    public static final byte[] PUBKEYHASH          = ISOUtil.hex2byte("C0C73A47A5A27992267AC825F3C8B0666DF3F8A544210851821BFCC1CFA9136C");
120
121    public static final String PROTECTED_QBEAN        = "protected-qbean";
122    public static final int SCAN_INTERVAL             = 2500;
123    public static final long SHUTDOWN_TIMEOUT         = 60000;
124
125    private MBeanServer server;
126    private File deployDir, libDir;
127    private Map<File,QEntry> dirMap;
128    private QFactory factory;
129    private QClassLoader loader;
130    private ClassLoader mainClassLoader;
131    private Log log;
132    private volatile boolean started;
133    private CountDownLatch ready = new CountDownLatch(1);
134    private CountDownLatch shutdown = new CountDownLatch(1);
135    private volatile boolean shuttingDown;
136    private volatile Thread q2Thread;
137    private String[] args;
138    private boolean hasSystemLogger;
139    private boolean exit;
140    private Instant startTime;
141    private CLI cli;
142    private boolean recursive;
143    private ConfigDecorationProvider decorator=null;
144    private UUID instanceId;
145    private String pidFile;
146    private String name = JMX_NAME;
147    private long lastVersionLog;
148    private String watchServiceClassname;
149    private boolean disableDeployScan;
150    private boolean disableDynamicClassloader;
151    private boolean disableJFR;
152    private int sshPort;
153    private String sshAuthorizedKeys;
154    private String sshUser;
155    private String sshHostKeyFile;
156    private static String DEPLOY_PREFIX = "META-INF/q2/deploy/";
157    private static String CFG_PREFIX = "META-INF/q2/cfg/";
158    private String nameRegistrarKey;
159    private Recording recording;
160    private CompositeMeterRegistry meterRegistry = io.micrometer.core.instrument.Metrics.globalRegistry;
161    private PrometheusMeterRegistry prometheusRegistry = new PrometheusMeterRegistry(PrometheusConfig.DEFAULT);
162    private int metricsPort;
163    private String metricsPath;
164    private final String statusPath = "/jpos/q2/status";
165
166    private Counter instancesCounter = Metrics.counter("jpos.q2.instances");
167    private boolean noShutdownHook;
168    private long shutdownHookDelay = 0L;
169
170    /**
171     * Constructs a new {@code Q2} instance with the specified command-line arguments and class loader.
172     * This constructor initializes various configurations, processes command-line arguments,
173     * sets up directories, and registers necessary components for the application.
174     *
175     * @param args an array of {@code String} containing the command-line arguments.
176     * @param classLoader the {@code ClassLoader} to be used by the application.
177     *                    If {@code null}, the class loader of the current class is used.
178     *
179     * <p>Key Initialization Steps:</p>
180     * <ul>
181     *     <li>Parses the command-line arguments twice:
182     *         once before environment variable substitution and once after.</li>
183     *     <li>Initializes the deployment directory and library directory (`lib`).</li>
184     *     <li>Generates a unique instance identifier for the application instance.</li>
185     *     <li>Sets the application start time to the current moment.</li>
186     *     <li>Registers MicroMeter metrics and Q2-specific components.</li>
187     * </ul>
188     *
189     * <p>Note: The {@code deployDir} directory is created if it does not already exist.</p>
190     *
191     * @see #parseCmdLine(String[], boolean)
192     * @see #registerMicroMeter()
193     * @see #registerQ2()
194     */
195    public Q2 (String[] args, ClassLoader classLoader) {
196        super();
197        parseCmdLine (args, true);
198        this.args = environmentArgs(args);
199        startTime = Instant.now();
200        instanceId = UUID.randomUUID();
201        parseCmdLine (this.args, false);
202        libDir     = new File (deployDir, "lib");
203        dirMap     = new TreeMap<>();
204        deployDir.mkdirs ();
205        mainClassLoader = classLoader == null ? getClass().getClassLoader() : classLoader;
206        registerMicroMeter();
207        registerQ2();
208    }
209
210    /**
211     * Constructs a new {@code Q2} instance with the specified command-line arguments
212     * and the default class loader.
213     *
214     * @param args an array of {@code String} containing the command-line arguments.
215     *             If no arguments are provided, the application initializes with
216     *             default settings.
217     *
218     * <p>This constructor delegates to {@link #Q2(String[], ClassLoader)} with
219     * a {@code null} class loader, causing the default class loader to be used.</p>
220     *
221     * @see #Q2(String[], ClassLoader)
222     */
223    public Q2 (String[] args) {
224        this (args, null);
225    }
226
227    /**
228     * Constructs a new {@code Q2} instance with no command-line arguments
229     * and the default class loader.
230     *
231     * <p>This constructor is equivalent to calling {@code Q2(new String[]{})}.
232     * It initializes the application with default settings.</p>
233     *
234     * @see #Q2(String[], ClassLoader)
235     * @see #Q2(String[])
236     */
237
238    public Q2 () {
239        this (new String[] {});
240    }
241
242    /**
243     * Constructs a new {@code Q2} instance with the specified deployment directory
244     * and the default class loader.
245     *
246     * @param deployDir a {@code String} specifying the path to the deployment directory.
247     *                  This is passed as a command-line argument using the {@code -d} option.
248     *
249     * <p>This constructor is equivalent to calling {@code Q2(new String[]{"-d", deployDir})}.
250     * It sets the deployment directory and initializes the application.</p>
251     *
252     * @see #Q2(String[], ClassLoader)
253     * @see #Q2(String[])
254     */
255    public Q2 (String deployDir) {
256        this (new String[] { "-d", deployDir });
257    }
258    public void start () {
259        if (shutdown.getCount() == 0)
260            throw new IllegalStateException("Q2 has been stopped");
261        new Thread(this).start();
262    }
263    public void stop () {
264        shutdown(true);
265    }
266    public MeterRegistry getMeterRegistry() {
267        return meterRegistry;
268    }
269
270    public PrometheusMeterRegistry getPrometheusMeterRegistry () {
271        return prometheusRegistry;
272    }
273    public void run () {
274        started = true;
275        Thread.currentThread().setName ("Q2-"+getInstanceId().toString());
276        startJFR();
277        instancesCounter.increment();
278
279        Path dir = Paths.get(deployDir.getAbsolutePath());
280        FileSystem fs = dir.getFileSystem();
281        try (WatchService service = fs.newWatchService()) {
282            watchServiceClassname = service.getClass().getName();
283            dir.register(
284              service,
285              StandardWatchEventKinds.ENTRY_CREATE,
286              StandardWatchEventKinds.ENTRY_MODIFY,
287              StandardWatchEventKinds.ENTRY_DELETE
288            );
289            server = ManagementFactory.getPlatformMBeanServer();
290            final ObjectName loaderName = new ObjectName(Q2_CLASS_LOADER);
291            try {
292                loader = new QClassLoader(server, libDir, loaderName, mainClassLoader);
293                if (server.isRegistered(loaderName))
294                    server.unregisterMBean(loaderName);
295                server.registerMBean(loader, loaderName);
296                loader = loader.scan(false);
297            } catch (Throwable t) {
298                if (log != null)
299                    log.error("initial-scan", t);
300                else
301                    t.printStackTrace();
302            }
303            factory = new QFactory(loaderName, this);
304            writePidFile();
305            initSystemLogger();
306            if (!noShutdownHook)
307                addShutdownHook();
308            q2Thread = Thread.currentThread();
309            q2Thread.setContextClassLoader(loader);
310            if (cli != null)
311                cli.start();
312            initConfigDecorator();
313            if (metricsPort != 0) {
314                deployElement(
315                  PrometheusService.createDescriptor(metricsPort, metricsPath, statusPath),
316                  "00_prometheus-" + getInstanceId() + ".xml", false, true);
317            }
318
319            deployInternal();
320            for (int i = 1; shutdown.getCount() > 0; i++) {
321                try {
322                    if (i > 1 && disableDeployScan) {
323                        shutdown.await();
324                        break;
325                    }
326                    boolean forceNewClassLoader = scan() && i > 1;
327                    QClassLoader oldClassLoader = loader;
328                    loader = loader.scan(forceNewClassLoader);
329                    if (loader != oldClassLoader) {
330                        oldClassLoader = null; // We want this to be null so it gets GCed.
331                        System.gc();  // force a GC
332                        log.info(
333                          "new classloader ["
334                            + Integer.toString(loader.hashCode(), 16)
335                            + "] has been created"
336                        );
337                        q2Thread.setContextClassLoader(loader);
338                    }
339                    logVersion();
340
341                    deploy();
342                    checkModified();
343                    ready.countDown();
344                    if (!waitForChanges(service))
345                        break;
346                } catch (InterruptedException | IllegalAccessError ignored) {
347                    // NOPMD
348                } catch (Throwable t) {
349                    log.error("start", t.getMessage());
350                    relax();
351                }
352            }
353            undeploy();
354            try {
355                if (server.isRegistered(loaderName))
356                    server.unregisterMBean(loaderName);
357            } catch (InstanceNotFoundException e) {
358                log.error(e);
359            }
360            if (decorator != null) {
361                decorator.uninitialize();
362            }
363            if (exit && !shuttingDown)
364                System.exit(0);
365        } catch (IllegalAccessError ignored) {
366            // NOPMD OK to happen
367        } catch (Exception e) {
368            if (log != null)
369                log.error (e);
370            else
371                e.printStackTrace();
372            System.exit (1);
373        } finally {
374            stopJFR();
375        }
376    }
377    public void shutdown () {
378        shutdown(false);
379    }
380    public boolean running() {
381        return started && shutdown.getCount() > 0;
382    }
383    public boolean ready() {
384        return ready.getCount() == 0 && shutdown.getCount() > 0;
385    }
386    public boolean ready (long millis) {
387        try {
388            ready.await(millis, TimeUnit.MILLISECONDS);
389        } catch (InterruptedException ignored) { }
390        return ready();
391    }
392    public void shutdown (boolean join) {
393        if (log != null) {
394            audit(auditStop(Duration.between(startTime, Instant.now())));
395        }
396        shutdown.countDown();
397        unregisterQ2();
398        if (q2Thread != null) {
399            // log.info ("shutting down");
400            q2Thread.interrupt ();
401            if (join) {
402                try {
403                    q2Thread.join(SHUTDOWN_TIMEOUT);
404                    log.info ("shutdown done");
405                } catch (InterruptedException e) {
406                    log.warn (e);
407                }
408            }
409        }
410        q2Thread = null;
411    }
412    public QClassLoader getLoader () {
413        return loader;
414    }
415    public QFactory getFactory () {
416        return factory;
417    }
418    public String[] getCommandLineArgs() {
419        return args;
420    }
421    public boolean accept (File f) {
422        return f.canRead() &&
423            (isXml(f) ||
424                    recursive && f.isDirectory() && !"lib".equalsIgnoreCase (f.getName()));
425    }
426    public File getDeployDir () {
427        return deployDir;
428    }
429
430    public String getWatchServiceClassname() {
431        return watchServiceClassname;
432    }
433
434    public static Q2 getQ2() {
435        return NameRegistrar.getIfExists(JMX_NAME);
436    }
437    public static Q2 getQ2(long timeout) {
438        return NameRegistrar.get(JMX_NAME, timeout);
439    }
440
441    public static int node() {
442        return PGPHelper.node();
443    }
444    private boolean isXml(File f) {
445        return f != null && f.getName().toLowerCase().endsWith(".xml");
446    }
447    private boolean scan () {
448        boolean rc = false;
449        File file[] = deployDir.listFiles (this);
450        // Arrays.sort (file); --apr not required - we use TreeMap
451        if (file == null) {
452            // Shutting down might be best, how to trigger from within?
453            throw new Error("Deploy directory \""+deployDir.getAbsolutePath()+"\" is not available");
454        } else {
455            for (File f : file) {
456                if (register(f))
457                    rc = true;
458            }
459        }
460        return rc;
461    }
462
463    private void deploy () {
464        List<ObjectInstance> startList = new ArrayList<ObjectInstance>();
465        Iterator<Map.Entry<File,QEntry>> iter = dirMap.entrySet().iterator();
466
467        try {
468            while (iter.hasNext() && shutdown.getCount() > 0) {
469                Map.Entry<File,QEntry> entry = iter.next();
470                File   f        = entry.getKey ();
471                QEntry qentry   = entry.getValue ();
472                long deployed   = qentry.getDeployed ();
473                if (deployed == 0) {
474                    if (deploy(f)) {
475                        if (qentry.isQBean ()) {
476                            if (qentry.isEagerStart())
477                                start(qentry.getInstance());
478                            else
479                                startList.add(qentry.getInstance());
480                        }
481                        qentry.setDeployed (f.lastModified ());
482                    } else {
483                        // deploy failed, clean up.
484                        iter.remove();
485                    }
486                } else if (deployed != f.lastModified ()) {
487                    undeploy (f);
488                    iter.remove ();
489                    loader.forceNewClassLoaderOnNextScan();
490                }
491            }
492            for (ObjectInstance instance : startList)
493                start(instance);
494        }
495        catch (Exception e){
496            log.error ("deploy", e);
497        }
498    }
499
500    private void undeploy () {
501        Object[] set = dirMap.entrySet().toArray ();
502        int l = set.length;
503
504        while (l-- > 0) {
505            Map.Entry entry = (Map.Entry) set[l];
506            File   f  = (File) entry.getKey ();
507            undeploy (f);
508        }
509    }
510
511    private void addShutdownHook () {
512        Runtime.getRuntime().addShutdownHook (
513            new Thread ("Q2-ShutdownHook") {
514                public void run () {
515                    audit (new Shutdown(getInstanceId(), shutdownHookDelay));
516                    if (shutdownHookDelay > 0)
517                        ISOUtil.sleep(shutdownHookDelay);
518
519                    audit(auditStop(Duration.between(startTime, Instant.now())));
520                    shuttingDown = true;
521                    shutdown.countDown();
522                    if (q2Thread != null) {
523                        try {
524                            q2Thread.join (SHUTDOWN_TIMEOUT);
525                        } catch (InterruptedException ignored) {
526                            // NOPMD nothing to do
527                        } catch (NullPointerException ignored) {
528                            // NOPMD
529                            // on thin Q2 systems where shutdown is very fast,
530                            // q2Thread can become null between the upper if and
531                            // the actual join. Not a big deal so we ignore the
532                            // exception.
533                        }
534                    }
535                }
536            }
537        );
538    }
539
540    private void checkModified () {
541        Iterator iter = dirMap.entrySet().iterator();
542        while (iter.hasNext()) {
543            Map.Entry entry = (Map.Entry) iter.next();
544            File   f        = (File)   entry.getKey ();
545            QEntry qentry   = (QEntry) entry.getValue ();
546            if (qentry.isQBean() && qentry.isQPersist()) {
547                ObjectName name = qentry.getObjectName ();
548                if (getState (name) == QBean.STARTED && isModified (name)) {
549                    qentry.setDeployed (persist (f, name));
550                }
551            }
552        }
553    }
554
555    private int getState (ObjectName name) {
556        int status = -1;
557        if (name != null) {
558            try {
559                status = (Integer) server.getAttribute(name, "State");
560            } catch (Exception e) {
561                log.warn ("getState", e);
562            }
563        }
564        return status;
565    }
566    private boolean isModified (ObjectName name) {
567        boolean modified = false;
568        if (name != null) {
569            try {
570                modified = (Boolean) server.getAttribute(name, "Modified");
571            } catch (Exception ignored) {
572                // NOPMD Okay to fail
573            }
574        }
575        return modified;
576    }
577    private long persist (File f, ObjectName name) {
578        long deployed = f.lastModified ();
579        try {
580            Element e = (Element) server.getAttribute (name, "Persist");
581            if (e != null) {
582                XMLOutputter out = new XMLOutputter (Format.getPrettyFormat());
583                Document doc = new Document ();
584                e.detach();
585                doc.setRootElement (e);
586                File tmp = new File (f.getAbsolutePath () + ".tmp");
587                Writer writer = new BufferedWriter(new FileWriter(tmp));
588                try {
589                    out.output (doc, writer);
590                } finally {
591                    writer.close ();
592                }
593                f.delete();
594                tmp.renameTo (f);
595                deployed = f.lastModified ();
596            }
597        } catch (Exception ex) {
598            log.warn ("persist", ex);
599        }
600        return deployed;
601    }
602
603    private void undeploy (File f) {
604        QEntry qentry = dirMap.get (f);
605        LogEvent evt = log != null ? log.createInfo().withTraceId(getInstanceId()) : null;
606        try {
607            if (evt != null)
608                evt.addMessage (new UnDeploy(f.getCanonicalPath()));
609
610            if (qentry.isQBean()) {
611                Object obj      = qentry.getObject ();
612                ObjectName name = qentry.getObjectName ();
613                factory.destroyQBean (this, name, obj);
614            }
615        } catch (Exception e) {
616            if (evt != null)
617                evt.addMessage (e);
618        } finally {
619            if (evt != null)
620                Logger.log(evt);
621        }
622    }
623
624    private boolean register (File f) {
625        boolean rc = false;
626        if (f.isDirectory()) {
627            File file[] = f.listFiles (this);
628            for (File aFile : file) {
629                if (register(aFile))
630                    rc = true;
631            }
632        } else if (dirMap.get (f) == null) {
633            dirMap.put (f, new QEntry ());
634            rc = true;
635        }
636        return rc;
637    }
638
639    private boolean deploy (File f) {
640        LogEvent evt = log != null ? log.createInfo().withTraceId(getInstanceId()) : null;
641        boolean enabled;
642        try {
643            QEntry qentry = dirMap.get (f);
644            SAXBuilder builder = createSAXBuilder();
645            Document doc;
646            if(decorator!=null && !f.getName().equals(LOGGER_CONFIG))
647            {
648                doc=decrypt(builder.build(new StringReader(decorator.decorateFile(f))));
649            }
650            else
651            {
652                doc=decrypt(builder.build(f));
653            }
654
655            Element rootElement = doc.getRootElement();
656            String iuuid = rootElement.getAttributeValue ("instance");
657            if (iuuid != null) {
658                UUID uuid = UUID.fromString(iuuid);
659                if (!uuid.equals (getInstanceId())) {
660                    deleteFile (f, iuuid);
661                    return false;
662                }
663            }
664            enabled = QFactory.isEnabled(rootElement);
665            qentry.setEagerStart(QFactory.isEagerStart(rootElement));
666            if (evt != null)
667                evt.addMessage(new Deploy(f.getCanonicalPath(), enabled, qentry.isEagerStart()));
668            if (enabled) {
669                Object obj = factory.instantiate (this, factory.expandEnvProperties(rootElement));
670                qentry.setObject (obj);
671                ObjectInstance instance = factory.createQBean (
672                    this, doc.getRootElement(), obj
673                );
674                qentry.setInstance (instance);
675            }
676        }
677        catch (InstanceAlreadyExistsException e) {
678           /*
679            * Ok, the file we tried to deploy, holds an object
680            *  that already has been deployed.
681            *
682            * Rename it out of the way.
683            *
684            */
685            tidyFileAway(f,DUPLICATE_EXTENSION, evt);
686            if (evt != null)
687                evt.addMessage(e);
688            return false;
689        }
690        catch (Exception e) {
691            if (evt != null) {
692                evt.addMessage(e);
693            }
694            tidyFileAway(f,ERROR_EXTENSION, evt);
695            // This will also save deploy error repeats...
696            return false;
697        }
698        catch (Error e) {
699            if (evt != null)
700                evt.addMessage(e);
701            tidyFileAway(f,ENV_EXTENSION, evt);
702            // This will also save deploy error repeats...
703            return false;
704        } finally {
705            if (evt != null) {
706                Logger.log(evt);
707            }
708        }
709        return true ;
710    }
711
712    private void start (ObjectInstance instance) {
713        try {
714            factory.startQBean (this, instance.getObjectName());
715        } catch (Exception e) {
716            getLog().warn ("start", e);
717        }
718    }
719    public void relax (long sleep) {
720        try {
721            shutdown.await(sleep, TimeUnit.MILLISECONDS);
722        } catch (InterruptedException ignored) { }
723    }
724    public void relax () {
725        relax (1000);
726    }
727    private void initSystemLogger () {
728        File loggerConfig = new File (deployDir, LOGGER_CONFIG);
729        if (loggerConfig.canRead()) {
730            hasSystemLogger = true;
731            try {
732                register (loggerConfig);
733                deploy ();
734            } catch (Exception e) {
735                getLog().warn ("init-system-logger", e);
736            }
737        }
738        audit (auditStart());
739    }
740    public Log getLog () {
741        if (log == null) {
742            Logger logger = Logger.getLogger (LOGGER_NAME);
743            if (!hasSystemLogger && !logger.hasListeners() && cli == null)
744                logger.addListener (new SimpleLogListener (System.out));
745            log = new Log (logger, REALM);
746        }
747        return log;
748    }
749    public MBeanServer getMBeanServer () {
750        return server;
751    }
752    public Duration getUptime() {
753        return Duration.between(startTime, Instant.now());
754    }
755    public void displayVersion () {
756        System.out.println(getVersionString());
757    }
758    public UUID getInstanceId() {
759        return instanceId;
760    }
761    public static String getVersionString() {
762        String appVersionString = getAppVersionString();
763        int l = PGPHelper.checkLicense();
764        String sl = l > 0 ? " " + Integer.toString(l,16) : "";
765        StringBuilder vs = new StringBuilder();
766        if (appVersionString != null) {
767            vs.append(
768              String.format ("jPOS %s %s/%s%s (%s)%n%s%s",
769                getVersion(), getBranch(), getRevision(), sl, getBuildTimestamp(), appVersionString, getLicensee()
770              )
771            );
772        } else {
773            vs.append(
774              String.format("jPOS %s %s/%s%s (%s) %s",
775                    getVersion(), getBranch(), getRevision(), sl, getBuildTimestamp(), getLicensee()
776              )
777            );
778        }
779//        if ((l & 0xE0000) > 0)
780//            throw new IllegalAccessError(vs);
781
782        return vs.toString();
783    }
784
785    private static String getLicensee() {
786        String s = null;
787        try {
788            s = PGPHelper.getLicensee();
789        } catch (IOException ignored) {
790            // NOPMD: ignore
791        }
792        return s;
793    }
794    private void parseCmdLine (String[] args, boolean environmentOnly) {
795        CommandLineParser parser = new DefaultParser ();
796
797        Options options = new Options ();
798        options.addOption ("v","version", false, "Q2's version");
799        options.addOption ("d","deploydir", true, "Deployment directory");
800        options.addOption ("r","recursive", false, "Deploy subdirectories recursively");
801        options.addOption ("h","help", false, "Usage information");
802        options.addOption ("C","config", true, "Configuration bundle");
803        options.addOption ("e","encrypt", true, "Encrypt configuration bundle");
804        options.addOption ("i","cli", false, "Command Line Interface");
805        options.addOption ("c","command", true, "Command to execute");
806        options.addOption ("p", "pid-file", true, "Store project's pid");
807        options.addOption ("n", "name", true, "Optional name (defaults to 'Q2')");
808        options.addOption ("sd", "shutdown-delay", true, "Shutdown delay in seconds (defaults to immediate)");
809        options.addOption ("Ns", "no-scan", false, "Disables deploy directory scan");
810        options.addOption ("Nd", "no-dynamic", false, "Disables dynamic classloader");
811        options.addOption ("Nf", "no-jfr", false, "Disables Java Flight Recorder");
812        options.addOption ("Nh", "no-shutdown-hook", false, "Disable shutdown hook");
813        options.addOption ("E", "environment", true, "Environment name.\nCan be given multiple times (applied in order, and values may override previous ones)");
814        options.addOption ("Ed", "envdir", true, "Environment file directory, defaults to cfg");
815        options.addOption ("mp", "metrics-port", true, "Metrics port");
816        options.addOption ("mP", "metrics-path", true, "Metrics path");
817
818        try {
819            System.setProperty("log4j2.formatMsgNoLookups", "true"); // log4shell prevention
820
821            CommandLine line = parser.parse (options, args);
822            // set up envdir and env before other parts of the system, so env is available
823            // force reload if any of the env options was changed
824            if (line.hasOption("Ed")) {
825                System.setProperty("jpos.envdir", line.getOptionValue("Ed"));
826            }
827            if (line.hasOption("E")) {
828                System.setProperty("jpos.env", ISOUtil.commaEncode(line.getOptionValues("E")));
829            }
830
831            if (environmentOnly) // first call just to properly parse environment, in order to get optional q2.args from yaml
832                return;
833
834            if (line.hasOption ("v")) {
835                displayVersion();
836                System.exit (0);
837            }
838            if (line.hasOption ("h")) {
839                HelpFormatter helpFormatter = new HelpFormatter ();
840                helpFormatter.printHelp ("Q2", options);
841                System.exit (0);
842            }
843            if (line.hasOption ("c")) {
844                cli = new CLI(this, line.getOptionValue("c"), line.hasOption("i"));
845            } else if (line.hasOption ("i"))
846                cli = new CLI(this, null, true);
847
848            String dir = DEFAULT_DEPLOY_DIR;
849            if (line.hasOption ("d")) {
850                dir = line.getOptionValue ("d");
851            } else if (cli != null)
852                dir = dir + "-" + "cli";
853            recursive = line.hasOption ("r");
854            this.deployDir  = new File (dir);
855            if (line.hasOption ("C"))
856                deployBundle (new File (line.getOptionValue ("C")), false);
857            if (line.hasOption ("e"))
858                deployBundle (new File (line.getOptionValue ("e")), true);
859            if (line.hasOption("p"))
860                pidFile = line.getOptionValue("p");
861            if (line.hasOption("n"))
862                name = line.getOptionValue("n");
863
864            disableDeployScan = line.hasOption("Ns");
865            disableDynamicClassloader = line.hasOption("Nd");
866            disableJFR = line.hasOption("Nf");
867            sshPort = Integer.parseInt(line.getOptionValue("sp", "2222"));
868            sshAuthorizedKeys = line.getOptionValue ("sa", "cfg/authorized_keys");
869            sshUser = line.getOptionValue("su", "admin");
870            sshHostKeyFile = line.getOptionValue("sh", "cfg/hostkeys.ser");
871            if (line.hasOption("mp"))
872                metricsPort = Integer.parseInt(line.getOptionValue("mp"));
873            metricsPath = line.hasOption("mP") ? line.getOptionValue("mP") : "/metrics";
874            noShutdownHook = line.hasOption("Nh");
875            shutdownHookDelay = line.hasOption ("sd") ? 1000L*Integer.parseInt(line.getOptionValue("sd")) : 0;
876
877            if (noShutdownHook && shutdownHookDelay > 0)
878                throw new IllegalArgumentException ("--no-shutdown-hook incompatible with --shutdown-delay argument");
879        } catch (MissingArgumentException | IllegalArgumentException | IllegalAccessError |
880                 UnrecognizedOptionException e) {
881            System.out.println("ERROR: " + e.getMessage());
882            System.exit(1);
883        } catch (Exception e) {
884            e.printStackTrace ();
885            System.exit (1);
886        }
887    }
888    private void deployBundle (File bundle, boolean encrypt)
889        throws JDOMException, IOException,
890                ISOException, GeneralSecurityException
891    {
892        SAXBuilder builder = createSAXBuilder();
893        Document doc = builder.build (bundle);
894        Iterator iter = doc.getRootElement().getChildren ().iterator ();
895        for (int i=1; iter.hasNext (); i ++) {
896            Element e = (Element) iter.next();
897            deployElement (e, String.format ("%02d_%s.xml",i, e.getName()), encrypt, !encrypt);
898            // the !encrypt above is tricky and deserves an explanation
899            // if we are encrypting a QBean, we want it to stay in the deploy
900            // directory for future runs. If on the other hand we are deploying
901            // a bundle, we want it to be transient.
902        }
903    }
904    public void deployElement (Element e, String fileName, boolean encrypt, boolean isTransient)
905        throws ISOException, IOException, GeneralSecurityException
906    {
907        e = e.clone ();
908
909        boolean pretty = "true".equalsIgnoreCase(Environment.get("template.pretty", "true"));
910        XMLOutputter out = new XMLOutputter (pretty ? Format.getPrettyFormat() : Format.getRawFormat());
911        Document doc = new Document ();
912        doc.setRootElement(e);
913        File qbean = new File (deployDir, fileName);
914        if (isTransient) {
915            e.setAttribute("instance", getInstanceId().toString());
916            qbean.deleteOnExit();
917        }
918        if (encrypt) {
919            doc = encrypt (doc);
920        }
921        try (Writer writer = new BufferedWriter(new FileWriter(qbean))) {
922            out.output(doc, writer);
923        }
924    }
925
926    private byte[] dodes (byte[] data, int mode)
927       throws GeneralSecurityException
928    {
929        Cipher cipher = Cipher.getInstance("DES");
930        cipher.init (mode, new SecretKeySpec(getKey(), "DES"));
931        return cipher.doFinal (data);
932    }
933    protected byte[] getKey() {
934        return
935          ISOUtil.xor(SystemSeed.getSeed(8, 8),
936          ISOUtil.hex2byte(System.getProperty("jpos.deploy.key", "BD653F60F980F788")));
937    }
938    protected Document encrypt (Document doc)
939        throws GeneralSecurityException, IOException
940    {
941        ByteArrayOutputStream os = new ByteArrayOutputStream ();
942        OutputStreamWriter writer = new OutputStreamWriter (os);
943        XMLOutputter out = new XMLOutputter (Format.getPrettyFormat());
944        out.output(doc, writer);
945        writer.close ();
946
947        byte[] crypt = dodes (os.toByteArray(), Cipher.ENCRYPT_MODE);
948
949        Document secureDoc = new Document ();
950        Element root = new Element (PROTECTED_QBEAN);
951        secureDoc.setRootElement (root);
952        Element secureData = new Element ("data");
953        root.addContent (secureData);
954
955        secureData.setText (
956            ISOUtil.hexString (crypt)
957        );
958        return secureDoc;
959    }
960
961    protected Document decrypt (Document doc)
962        throws GeneralSecurityException, IOException, JDOMException
963    {
964        Element root = doc.getRootElement ();
965        if (PROTECTED_QBEAN.equals (root.getName ())) {
966            Element data = root.getChild ("data");
967            if (data != null) {
968                ByteArrayInputStream is = new ByteArrayInputStream (
969                    dodes (
970                        ISOUtil.hex2byte (data.getTextTrim()),
971                        Cipher.DECRYPT_MODE)
972                );
973                SAXBuilder builder = createSAXBuilder();
974                doc = builder.build (is);
975            }
976        }
977        return doc;
978    }
979
980    private void tidyFileAway (File f, String extension, LogEvent evt) {
981        File rename = new File(f.getAbsolutePath()+"."+extension);
982        while (rename.exists()){
983            rename = new File(rename.getAbsolutePath()+"."+extension);
984        }
985        if (evt != null) {
986            if (f.renameTo(rename)){
987                evt.addMessage(
988                  new DeployActivity(DeployActivity.Action.RENAME, String.format ("%s to %s", f.getAbsolutePath(), rename.getAbsolutePath()))
989                );
990            }
991            else {
992                evt.addMessage(
993                  new DeployActivity(DeployActivity.Action.RENAME_ERROR, String.format ("%s to %s", f.getAbsolutePath(), rename.getAbsolutePath()))
994                );
995            }
996        }
997    }
998
999    private void deleteFile (File f, String iuuid) {
1000        f.delete();
1001        getLog().info(String.format("Deleted transient descriptor %s (%s)", f.getAbsolutePath(), iuuid));
1002    }
1003
1004    private void initConfigDecorator()
1005    {
1006        InputStream in=Q2.class.getClassLoader().getResourceAsStream("META-INF/org/jpos/config/Q2-decorator.properties");
1007        try
1008        {
1009            if(in!=null)
1010            {
1011                PropertyResourceBundle bundle=new PropertyResourceBundle(in);
1012                String ccdClass=bundle.getString("config-decorator-class");
1013                if(log!=null) log.info("Initializing config decoration provider: "+ccdClass);
1014                decorator= (ConfigDecorationProvider) Q2.class.getClassLoader().loadClass(ccdClass).newInstance();
1015                decorator.initialize(getDeployDir());
1016            }
1017        }
1018        catch (IOException ignored)
1019        {
1020            // NOPMD OK to happen
1021        }
1022        catch (Exception e)
1023        {
1024            if(log!=null) log.error(e);
1025            else
1026            {
1027                e.printStackTrace();
1028            }
1029        }
1030        finally
1031        {
1032            if(in!=null)
1033            {
1034                try
1035                {
1036                    in.close();
1037                }
1038                catch (IOException ignored)
1039                {
1040                    // NOPMD nothing to do
1041                }
1042            }
1043        }
1044    }
1045    private void logVersion () throws IOException {
1046        long now = System.currentTimeMillis();
1047        if (now - lastVersionLog > 86400000L) {
1048            License l = PGPHelper.getLicense();
1049            audit(l);
1050            lastVersionLog = now;
1051            while (running() && (l.status() & 0xF0000) != 0)
1052                relax(60000L);
1053        }
1054    }
1055    private void setExit (boolean exit) {
1056        this.exit = exit;
1057    }
1058    private SAXBuilder createSAXBuilder () {
1059        SAXBuilder builder = new SAXBuilder ();
1060        builder.setFeature("http://xml.org/sax/features/namespaces", true);
1061        builder.setFeature("http://apache.org/xml/features/xinclude", true);
1062        return builder;
1063    }
1064    public static void main (String[] args) throws Exception {
1065        Q2 q2 = new Q2(args);
1066        q2.setExit (true);
1067        q2.start();
1068    }
1069    public static String getVersion() {
1070        return getBundle("org/jpos/q2/buildinfo").getString ("version");
1071    }
1072    public static String getRevision() {
1073        return getBundle("org/jpos/q2/revision").getString ("revision");
1074    }
1075    public static String getBranch() {
1076        return getBundle("org/jpos/q2/revision").getString ("branch");
1077    }
1078    public static String getBuildTimestamp() {
1079        return getBundle("org/jpos/q2/buildinfo").getString ("buildTimestamp");
1080    }
1081    public static String getRelease() {
1082        return getVersion() + " " + getRevision();
1083    }
1084    public static String getAppVersionString() {
1085        try {
1086            ResourceBundle buildinfo = getBundle("buildinfo");
1087            ResourceBundle revision = getBundle("revision");
1088
1089            return String.format ("%s %s %s/%s (%s)",
1090                buildinfo.getString("projectName"),
1091                buildinfo.getString("version"),
1092                revision.getString("branch"),
1093                revision.getString("revision"),
1094                buildinfo.getString("buildTimestamp")
1095            );
1096        } catch (MissingResourceException ignored) {
1097            return null;
1098        }
1099    }
1100    public boolean isDisableDynamicClassloader() {
1101        return disableDynamicClassloader;
1102    }
1103
1104    public static class QEntry {
1105        long deployed;
1106        ObjectInstance instance;
1107        Object obj;
1108        boolean eagerStart;
1109        public QEntry () {
1110            super();
1111        }
1112        public QEntry (long deployed, ObjectInstance instance) {
1113            super();
1114            this.deployed = deployed;
1115            this.instance = instance;
1116        }
1117        public long getDeployed () {
1118            return deployed;
1119        }
1120        public void setDeployed (long deployed) {
1121            this.deployed = deployed;
1122        }
1123        public void setInstance (ObjectInstance instance) {
1124            this.instance = instance;
1125        }
1126        public ObjectInstance getInstance () {
1127            return instance;
1128        }
1129        public ObjectName getObjectName () {
1130            return instance != null ? instance.getObjectName () : null;
1131        }
1132        public void setObject (Object obj) {
1133            this.obj = obj;
1134        }
1135        public Object getObject () {
1136            return obj;
1137        }
1138        public boolean isQBean () {
1139            return obj instanceof QBean;
1140        }
1141        public boolean isQPersist () {
1142            return obj instanceof QPersist;
1143        }
1144
1145        public boolean isEagerStart() {
1146            return eagerStart;
1147        }
1148
1149        public void setEagerStart(boolean eagerStart) {
1150            this.eagerStart = eagerStart;
1151        }
1152    }
1153
1154    private void writePidFile() {
1155        if (pidFile == null)
1156            return;
1157
1158        File f = new File(pidFile);
1159        try {
1160            if (f.isDirectory()) {
1161                System.err.printf("Q2: pid-file (%s) is a directory%n", pidFile);
1162                System.exit(21); // EISDIR
1163            }
1164            if (!f.createNewFile()) {
1165                System.err.printf("Q2: Unable to write pid-file (%s)%n", pidFile);
1166                System.exit(17); // EEXIST
1167            }
1168            f.deleteOnExit();
1169            FileOutputStream fow = new FileOutputStream(f);
1170            fow.write(ManagementFactory.getRuntimeMXBean().getName().split("@")[0].getBytes());
1171            fow.write(System.lineSeparator().getBytes());
1172            fow.close();
1173        } catch (IOException e) {
1174            throw new IllegalArgumentException(String.format("Unable to write pid-file (%s)", pidFile), e);
1175        }
1176    }
1177
1178    public void deployTemplate (String template, String filename, String prefix)
1179      throws IOException, JDOMException, GeneralSecurityException, ISOException, NullPointerException {
1180        if (template.startsWith("jar:")) {
1181            deployResourceTemplate(template, filename, prefix);
1182        } else {
1183            deployFileTemplate(template, filename, prefix);
1184        }
1185    }
1186
1187    private void deployResourceTemplate (String resourceUri, String filename, String prefix)
1188      throws IOException, JDOMException, GeneralSecurityException, ISOException {
1189        // Assume resourceUri starts with  "jar:"
1190        try (InputStream is = loader.getResourceAsStream(resourceUri.substring(4))) {
1191            Objects.requireNonNull(is, "resource " + resourceUri + " not present");
1192            deployTemplateInternal(is, resourceUri, filename, prefix);
1193        }
1194    }
1195
1196    private void deployFileTemplate (String resourceFile, String filename, String prefix) throws IOException, JDOMException, ISOException, GeneralSecurityException {
1197        try (InputStream is = new FileInputStream(resourceFile)) {
1198            deployTemplateInternal(is, resourceFile, filename, prefix);
1199        }
1200    }
1201
1202    private void deployTemplateInternal(InputStream is, String originalResource, String filename, String prefix) throws IOException, JDOMException, ISOException, GeneralSecurityException {
1203        SAXBuilder builder = new SAXBuilder();
1204        String s = new String(is.readAllBytes()).replaceAll("__PREFIX__", prefix);
1205        Document doc = builder.build(new ByteArrayInputStream(s.getBytes()));
1206        Element root = doc.getRootElement();
1207        root.addContent(0, new Text("\n    "));
1208        root.addContent(1, new Comment(" Source template "+originalResource+" "));
1209        deployElement(root, filename, false, true);
1210    }
1211
1212    private boolean waitForChanges (WatchService service) throws InterruptedException {
1213        WatchKey key = service.poll (SCAN_INTERVAL, TimeUnit.MILLISECONDS);
1214        if (key != null) {
1215            LogEvent evt = getLog().createInfo().withTraceId(getInstanceId());
1216            for (WatchEvent<?> ev : key.pollEvents()) {
1217                if (ev.kind() == StandardWatchEventKinds.ENTRY_CREATE) {
1218                    evt.addMessage(new DeployActivity(DeployActivity.Action.CREATE, String.format ("%s/%s", deployDir.getName(), ev.context())));
1219                } else if (ev.kind() == StandardWatchEventKinds.ENTRY_DELETE) {
1220                    evt.addMessage(new DeployActivity(DeployActivity.Action.DELETE, String.format ("%s/%s", deployDir.getName(), ev.context())));
1221                } else if (ev.kind() == StandardWatchEventKinds.ENTRY_MODIFY) {
1222                    evt.addMessage(new DeployActivity(DeployActivity.Action.MODIFY, String.format ("%s/%s", deployDir.getName(), ev.context())));
1223                }
1224            }
1225            Logger.log(evt);
1226            if (!key.reset()) {
1227                getLog().warn(
1228                  String.format (
1229                    "deploy directory '%s' no longer valid",
1230                    deployDir.getAbsolutePath())
1231                );
1232                return false; // deploy directory no longer valid
1233            }
1234            try {
1235                Environment.reload();
1236            } catch (IOException e) {
1237                getLog().warn(e);
1238            }
1239        }
1240        return true;
1241    }
1242
1243    private void registerQ2() {
1244        synchronized (Q2.class) {
1245            for (int i=0; ; i++) {
1246                String key = name + (i > 0 ? "-" + i : "");
1247                if (NameRegistrar.getIfExists(key) == null) {
1248                    NameRegistrar.register(key, this);
1249                    this.nameRegistrarKey = key;
1250                    break;
1251                }
1252            }
1253        }
1254    }
1255
1256    private void unregisterQ2() {
1257        synchronized (Q2.class) {
1258            if (nameRegistrarKey != null) {
1259                NameRegistrar.unregister(nameRegistrarKey);
1260                nameRegistrarKey = null;
1261            }
1262        }
1263
1264    }
1265
1266    private void deployInternal() throws IOException, JDOMException, SAXException, ISOException, GeneralSecurityException {
1267        extractCfg();
1268        extractDeploy();
1269    }
1270    private void extractCfg() throws IOException {
1271        List<String> resources = ModuleUtils.getModuleEntries(CFG_PREFIX);
1272        if (resources.size() > 0)
1273            new File("cfg").mkdirs();
1274        for (String resource : resources)
1275            copyResourceToFile(resource, new File("cfg", resource.substring(CFG_PREFIX.length())));
1276    }
1277    private void extractDeploy() throws IOException, JDOMException, SAXException, ISOException, GeneralSecurityException {
1278        List<String> qbeans = ModuleUtils.getModuleEntries(DEPLOY_PREFIX);
1279        for (String resource : qbeans) {
1280            if (resource.toLowerCase().endsWith(".xml"))
1281                deployResource(resource);
1282            else
1283                copyResourceToFile(resource, new File("cfg", resource.substring(DEPLOY_PREFIX.length())));
1284
1285        }
1286    }
1287
1288    private void copyResourceToFile(String resource, File destination) throws IOException {
1289        // taken from @vsalaman's Install using human readable braces as God mandates
1290        try (InputStream source = getClass().getClassLoader().getResourceAsStream(resource)) {
1291            try (FileOutputStream output = new FileOutputStream(destination)) {
1292                int n;
1293                byte[] buffer = new byte[4096];
1294                while (-1 != (n = source.read(buffer))) {
1295                    output.write(buffer, 0, n);
1296                }
1297            }
1298        }
1299    }
1300    private void deployResource(String resource)
1301      throws IOException, JDOMException, GeneralSecurityException, ISOException
1302    {
1303        SAXBuilder builder = new SAXBuilder();
1304        try (InputStream source = getClass().getClassLoader().getResourceAsStream(resource)) {
1305            Document doc = builder.build(source);
1306            deployElement (doc.getRootElement(), resource.substring(DEPLOY_PREFIX.length()), false,true);
1307        }
1308    }
1309    private void startJFR () {
1310        try {
1311            if (!disableJFR) {
1312                recording = new Recording(Configuration.getConfiguration("default"));
1313                recording.start();
1314            }
1315        } catch (IOException | ParseException e) {
1316            throw new RuntimeException(e);
1317        }
1318    }
1319
1320    private void stopJFR () {
1321        if (recording != null && recording.getState() == RecordingState.RUNNING) {
1322            recording.stop();
1323            recording = null;
1324        }
1325    }
1326
1327    private void registerMicroMeter () {
1328        System.setProperty("slf4j.internal.verbosity","ERROR");
1329
1330        meterRegistry.clear(); // start Q2 off a fresh meter registry
1331        meterRegistry.config().meterFilter(new MeterFilter() {
1332            @Override
1333            public DistributionStatisticConfig configure(Meter.Id id, DistributionStatisticConfig config) {
1334                if (id.getName().equals(MeterInfo.TM_OPERATION.id())) {
1335                    return DistributionStatisticConfig.builder().serviceLevelObjectives(
1336                        Duration.ofMillis(10).toNanos(),
1337                        Duration.ofMillis(100).toNanos(),
1338                        Duration.ofMillis(500).toNanos(),
1339                        Duration.ofMillis(1000).toNanos(),
1340                        Duration.ofMillis(5000).toNanos(),
1341                        Duration.ofMillis(15000).toNanos())
1342                      .build()
1343                      .merge(config);
1344                }
1345                return config;
1346            }
1347        });
1348        new ClassLoaderMetrics().bindTo(meterRegistry);
1349        new JvmMemoryMetrics().bindTo(meterRegistry);
1350        new JvmGcMetrics().bindTo(meterRegistry);
1351        new ProcessorMetrics().bindTo(meterRegistry);
1352        new JvmThreadMetrics().bindTo(meterRegistry);
1353
1354        prometheusRegistry.throwExceptionOnRegistrationFailure();
1355        meterRegistry.add (prometheusRegistry);
1356    }
1357
1358    public String[] environmentArgs (String[] args) {
1359        String envArgs = Environment.getEnvironment().getProperty("${q2.args}", null);
1360        return (envArgs != null && !"${q2.args}".equals(envArgs) ?
1361            Stream.concat(
1362              Arrays.stream(ISOUtil.commaDecode(envArgs)), Arrays.stream(args))
1363                .toArray(String[]::new) : args);
1364    }
1365
1366    private void audit (AuditLogEvent sal) {
1367        Logger.log(getLog().createInfo(sal).withTraceId(getInstanceId()));
1368    }
1369
1370    private Start auditStart() {
1371        Environment env = Environment.getEnvironment();
1372        String envName = env.getName();
1373        if (env.getErrorString() != null)
1374            envName = envName + " (" + env.getErrorString() + ")";
1375        return new Start(
1376          getQ2().getInstanceId(),
1377          getVersion(),
1378          getAppVersionString(),
1379          getDeployDir().getAbsolutePath(),
1380          envName
1381        );
1382    }
1383
1384    private Stop auditStop(Duration dur) {
1385        return new Stop(
1386          getInstanceId(),
1387          dur
1388        );
1389    }
1390}