All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.openbase.jul.pattern.launch.AbstractLauncher.kt Maven / Gradle / Ivy

The newest version!
package org.openbase.jul.pattern.launch

import ch.qos.logback.classic.LoggerContext
import ch.qos.logback.classic.joran.JoranConfigurator
import ch.qos.logback.core.joran.spi.JoranException
import ch.qos.logback.core.util.StatusPrinter
import org.openbase.jps.core.JPService
import org.openbase.jps.exception.JPNotAvailableException
import org.openbase.jul.communication.controller.AbstractIdentifiableController
import org.openbase.jul.communication.iface.RPCServer
import org.openbase.jul.exception.*
import org.openbase.jul.exception.ExceptionProcessor.getInitialCause
import org.openbase.jul.exception.ExceptionProcessor.isCausedBySystemShutdown
import org.openbase.jul.exception.MultiException.ExceptionStack
import org.openbase.jul.exception.printer.ExceptionPrinter
import org.openbase.jul.extension.type.processing.ScopeProcessor
import org.openbase.jul.iface.Launchable
import org.openbase.jul.iface.VoidInitializable
import org.openbase.jul.iface.provider.NameProvider
import org.openbase.jul.pattern.Launcher
import org.openbase.jul.pattern.launch.jp.*
import org.openbase.jul.processing.StringProcessor
import org.openbase.jul.schedule.FutureProcessor
import org.openbase.jul.schedule.GlobalCachedExecutorService
import org.openbase.jul.schedule.SyncObject
import org.openbase.type.domotic.state.ConnectionStateType
import org.openbase.type.execution.LauncherDataType
import org.slf4j.Logger
import org.slf4j.LoggerFactory
import java.io.File
import java.lang.InstantiationException
import java.lang.reflect.InvocationTargetException
import java.util.*
import java.util.concurrent.*
import java.util.concurrent.TimeoutException
import java.util.concurrent.locks.ReentrantReadWriteLock

/*-
 * #%L
 * JUL Pattern Launch
 * %%
 * Copyright (C) 2015 - 2022 openbase.org
 * %%
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Lesser General Public License as
 * published by the Free Software Foundation, either version 3 of the
 * License, or (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Lesser Public License for more details.
 *
 * You should have received a copy of the GNU General Lesser Public
 * License along with this program.  If not, see
 * .
 * #L%
 */ /**
 * @param  the launchable to launch by this launcher.
 *
 * @author [Divine Threepwood](mailto:[email protected])
 */
abstract class AbstractLauncher>
/**
 * Constructor prepares the launcher and registers already a shutdown hook.
 * The launcher class is used to instantiate a new launcher instance if the instantiateLaunchable() method is not overwritten.
 *
 *
 * After instantiation of this class the launcher must be initialized and activated before the communication interface is provided.
 *
 * @param launchableClass  the class to be launched.
 * @param applicationClass the class representing this application. This is used for scope generation if the getName() method is not overwritten.
 *
 * @throws org.openbase.jul.exception.InstantiationException
 */(private val applicationClass: Class<*>, private val launchableClass: Class) :
    AbstractIdentifiableController(
        LauncherDataType.LauncherData.newBuilder()
    ), Launcher, VoidInitializable, NameProvider {
    protected val logger = LoggerFactory.getLogger(javaClass)
    var launchable: L? = null
        private set
    private var launchTime: Long = -1
    private var verified = false
    var verificationFailedCause: VerificationFailedException? = null
        private set

    @Volatile
    var launcherTask: Future? = null
        private set
    private val lauchnerLock = ReentrantReadWriteLock()

    @Throws(InitializationException::class, InterruptedException::class)
    override fun init() {
        super.init(
            SCOPE_PREFIX_LAUNCHER + ScopeProcessor.COMPONENT_SEPARATOR + JPService.getApplicationName() + ScopeProcessor.COMPONENT_SEPARATOR + ScopeProcessor.convertIntoValidScopeComponent(
                name
            )
        )
        try {
            verifyNonRedundantExecution()
        } catch (e: VerificationFailedException) {
            throw InitializationException(this, e)
        }
    }

    open val isCoreLauncher: Boolean
        /**
         * This method can be overwritten to identify a core launcher.
         * This means that if the start of this launcher fails the whole launching
         * process will be stopped.
         *
         * @return false, can be overwritten to return true
         */
        get() = false

    /**
     * Method returns the name of this launcher.
     *
     * @return the name as string.
     */
    override fun getName(): String {
        return javaClass.simpleName.replace("Launcher", "")
    }

    /**
     * Method creates a launchable instance without any arguments.. In case the launchable needs arguments you can overwrite this method and instantiate the launchable by ourself.
     *
     * @return the new instantiated launchable.
     *
     * @throws CouldNotPerformException is thrown in case the launchable could not properly be instantiated.
     */
    @Throws(CouldNotPerformException::class)
    protected fun instantiateLaunchable(): L {
        return try {
            launchableClass.getConstructor().newInstance()
        } catch (ex: InstantiationException) {
            throw CouldNotPerformException("Could not load launchable class!", ex)
        } catch (ex: IllegalAccessException) {
            throw CouldNotPerformException("Could not load launchable class!", ex)
        } catch (ex: NoSuchMethodException) {
            throw CouldNotPerformException("Could not load launchable class!", ex)
        } catch (ex: InvocationTargetException) {
            throw CouldNotPerformException("Could not load launchable class!", ex)
        }
    }

    // Load application specific java properties.
    protected abstract fun loadProperties()

    /**
     * Method verifies a running application.
     *
     * @throws VerificationFailedException is thrown if the application is started with any restrictions.
     * @throws InterruptedException        is thrown if the verification process is externally interrupted.
     */
    @Throws(VerificationFailedException::class, InterruptedException::class)
    protected open fun verify() {
        // overwrite for verification.
    }

    private fun setState(state: LauncherDataType.LauncherData.LauncherState) {
        try {
            getDataBuilder(this).use { dataBuilder -> dataBuilder.internalBuilder!!.setLauncherState(state) }
        } catch (e: Exception) {
            ExceptionPrinter.printHistory("Could not apply state change!", e, logger)
        }
    }

    @Throws(VerificationFailedException::class)
    fun verifyNonRedundantExecution() {
        // verify that launcher was not already externally started
        try {
            val launcherRemote = LauncherRemote()
            launcherRemote.init(getScope())
            try {
                launcherRemote.activate()
                launcherRemote.waitForMiddleware()
                launcherRemote.waitForConnectionState(ConnectionStateType.ConnectionState.State.CONNECTED, 1000)
                throw RedundantExecutionException("Launcher[$name]")
            } catch (e: org.openbase.jul.exception.TimeoutException) {
                // this is the default since no other instance should be launched yet.
            } finally {
                launcherRemote.shutdown()
            }
        } catch (e: InterruptedException) {
            Thread.currentThread().interrupt()
        } catch (e: VerificationFailedException) {
            throw e
        } catch (e: CouldNotPerformException) {
            ExceptionPrinter.printHistory("Could not properly detect redundant launcher!", e, logger)
        }
    }

    override fun launch(): Future {
        try {
            getManageWriteLockInterruptible(this).use { ignored ->
                if (launcherTask != null && !launcherTask!!.isDone) {
                    return FutureProcessor.canceledFuture(
                        Void::class.java, InvalidStateException(
                            "Could not launch $name! Application still running!"
                        )
                    )
                }
                launcherTask = GlobalCachedExecutorService.submit {
                    getManageWriteLockInterruptible(this).use { ignored1 ->
                        setState(LauncherDataType.LauncherData.LauncherState.INITIALIZING)
                        try {
                            init()
                            activate()
                        } catch (e: CouldNotPerformException) {
                            setState(LauncherDataType.LauncherData.LauncherState.ERROR)
                            throw e
                        } catch (e: RuntimeException) {
                            setState(LauncherDataType.LauncherData.LauncherState.ERROR)
                            throw e
                        } catch (e: InterruptedException) {
                            setState(LauncherDataType.LauncherData.LauncherState.STOPPED)
                            throw e
                        }
                        launchable = instantiateLaunchable()
                        try {
                            launchable?.init()
                            setState(LauncherDataType.LauncherData.LauncherState.LAUNCHING)
                            launchable?.activate()
                            launchTime = System.currentTimeMillis()
                            setState(LauncherDataType.LauncherData.LauncherState.RUNNING)
                            try {
                                verify()
                                verified = true
                            } catch (ex: VerificationFailedException) {
                                verified = false
                                verificationFailedCause = ex
                            }
                        } catch (ex: InterruptedException) {
                            setState(LauncherDataType.LauncherData.LauncherState.STOPPING)
                            return@submit null
                        } catch (ex: Exception) {
                            setState(LauncherDataType.LauncherData.LauncherState.ERROR)
                            launchable!!.shutdown()
                            if (!isCausedBySystemShutdown(ex)) {
                                ExceptionPrinter.printHistoryAndReturnThrowable(
                                    CouldNotPerformException(
                                        "Could not launch $name",
                                        ex
                                    ), logger
                                )
                            }
                        }
                        return@submit null
                    }
                }
            }
        } catch (ex: InterruptedException) {
            Thread.currentThread().interrupt()
            throw RuntimeException(ex)
        }
        return launcherTask!!
    }

    @Throws(CouldNotPerformException::class, InterruptedException::class)
    override fun relaunch() {
        getManageWriteLockInterruptible(this).use { ignored ->
            stop()
            try {
                launch().get()
            } catch (ex: ExecutionException) {
                throw CouldNotPerformException(ex)
            } catch (ex: CancellationException) {
                throw CouldNotPerformException(ex)
            }
        }
    }

    override fun stop() {
        try {
            getManageWriteLockInterruptible(this).use { ignored ->
                interruptLaunch()
                setState(LauncherDataType.LauncherData.LauncherState.STOPPING)
                if (launchable != null) {
                    launchable!!.shutdown()
                }
                setState(LauncherDataType.LauncherData.LauncherState.STOPPED)
            }
        } catch (ex: InterruptedException) {
            Thread.currentThread().interrupt()
            throw RuntimeException(ex)
        }
    }

    /**
     * Method cancels the launch process of this launcher.
     */
    private fun interruptLaunch() {
        if (isLaunching) {
            launcherTask!!.cancel(true)
        }
    }

    private val isLaunching: Boolean
        /**
         * @return true if the launcher is currently launching.
         */
        private get() = launcherTask != null && !launcherTask!!.isDone

    override fun shutdown() {
        stop()
        super.shutdown()
    }

    override fun getUpTime(): Long {
        return if (launchTime < 0) {
            0
        } else System.currentTimeMillis() - launchTime
    }

    override fun getLaunchTime(): Long {
        return launchTime
    }

    override fun isVerified(): Boolean {
        return verified
    }

    override fun registerMethods(server: RPCServer) {
        // currently, the launcher does not support any rpc methods.
    }

    companion object {
        const val LAUNCHER_TIMEOUT: Long = 60000
        const val SCOPE_PREFIX_LAUNCHER = ScopeProcessor.COMPONENT_SEPARATOR + "launcher"
        private val waitingTaskList: MutableList> = ArrayList()
        private val VERIFICATION_STACK_LOCK = SyncObject("VerificationStackLock")
        private val ERROR_STACK_LOCK = SyncObject("ErrorStackLock")
        private val WAITING_TASK_LIST_LOCK = SyncObject("WaitingTaskLock")
        private var errorExceptionStack: ExceptionStack? = null
        private var verificationExceptionStack: ExceptionStack? = null

        @Throws(CouldNotPerformException::class)
        private fun loadCustomLoggerSettings() {
            // assume SLF4J is bound to logback in the current environment
            val context = LoggerFactory.getILoggerFactory() as LoggerContext
            val defaultLoggerConfig: File
            val debugLoggerConfig: File
            try {
                defaultLoggerConfig = JPService.getValue(
                    JPLoggerConfigFile::class.java, JPService.getValue(
                        JPLoggerDebugConfigFile::class.java
                    )
                )
                debugLoggerConfig = JPService.getValue(JPLoggerDebugConfigFile::class.java)
            } catch (ex: JPNotAvailableException) {
                // no logger config set so just skip custom configuration.
                return
            }
            val loggerConfig: File
            loggerConfig = if (debugLoggerConfig.exists() && JPService.debugMode()) {
                // prefer debug config when available and debug mode was enabled.
                debugLoggerConfig
            } else if (defaultLoggerConfig.exists()) {
                // use default config
                defaultLoggerConfig
            } else if (debugLoggerConfig.exists()) {
                // if default config does not exist just use any available debug config
                debugLoggerConfig
            } else {
                // skip if no logger configuration files could be found.
                return
            }

            // reconfigure logger
            try {
                val configurator = JoranConfigurator()
                configurator.context = context
                // clear any previous configuration
                context.reset()

                // prepare props
                loadProperties(context)

                // configure
                configurator.doConfigure(loggerConfig)

                // print warning in case of syntax errors
                StatusPrinter.printInCaseOfErrorsOrWarnings(context)
            } catch (ex: JoranException) {
                // print warning in case of syntax errors
                StatusPrinter.printInCaseOfErrorsOrWarnings(context)
                throw CouldNotPerformException("Could not load logger settings!", ex)
            }
        }

        private fun loadProperties(context: LoggerContext) {
            // store some variables
            context.putProperty("APPLICATION_NAME", JPService.getApplicationName())
            context.putProperty("SUBMODULE_NAME", JPService.getSubmoduleName())
            context.putProperty("MODULE_SEPARATOR", if (JPService.getSubmoduleName().isEmpty()) "" else "-")
            try {
                context.putProperty("LOGGER_TARGET_DIR", JPService.getValue(JPLogDirectory::class.java).absolutePath)
                // inform user about log redirection
                println(
                    "Logs can be found in: " + JPService.getValue(
                        JPLogDirectory::class.java
                    ).absolutePath
                )
            } catch (e: JPNotAvailableException) {
                // just store nothing if the propertie could not be loaded.
            }
        }

        @JvmStatic
        fun main(
            application: Class<*>,
            submodule: Class<*>,
            args: Array,
            vararg launchers: Class>,
        ) {

            // setup application names
            JPService.setApplicationName(application)
            JPService.setSubmoduleName(submodule) // requires the application name to be set
            main(application, args, *launchers)
        }

        @JvmStatic
        fun main(
            application: Class<*>,
            submoduleName: String,
            args: Array,
            vararg launchers: Class>,
        ) {

            // setup application names
            JPService.setApplicationName(application)
            JPService.setSubmoduleName(submoduleName) // requires the application name to be set
            main(application, args, *launchers)
        }

        @JvmStatic
        fun main(application: Class<*>, args: Array, vararg launchers: Class>) {

            // setup application names
            JPService.setApplicationName(application)

            // register interruption of this thread as shutdown hook
            val mainThread = Thread.currentThread()
            Runtime.getRuntime().addShutdownHook(Thread { mainThread.interrupt() })
            val launcherMap: MutableMap>, AbstractLauncher<*>> = HashMap()
            for (launcherClass in launchers) {
                try {
                    launcherMap[launcherClass] = launcherClass.getConstructor().newInstance()
                } catch (ex: InstantiationException) {
                    errorExceptionStack = MultiException.push(
                        application,
                        CouldNotPerformException("Could not load launcher class!", ex),
                        errorExceptionStack
                    )
                } catch (ex: IllegalAccessException) {
                    errorExceptionStack = MultiException.push(
                        application,
                        CouldNotPerformException("Could not load launcher class!", ex),
                        errorExceptionStack
                    )
                } catch (ex: NoSuchMethodException) {
                    errorExceptionStack = MultiException.push(
                        application,
                        CouldNotPerformException("Could not load launcher class!", ex),
                        errorExceptionStack
                    )
                } catch (ex: InvocationTargetException) {
                    errorExceptionStack = MultiException.push(
                        application,
                        CouldNotPerformException("Could not load launcher class!", ex),
                        errorExceptionStack
                    )
                }
            }
            for (launcher in launcherMap.values) {
                launcher.loadProperties()
            }

            // register launcher jps
            JPService.registerProperty(JPPrintLauncher::class.java)
            JPService.registerProperty(JPExcludeLauncher::class.java)
            JPService.registerProperty(JPLoggerConfigDirectory::class.java)
            JPService.registerProperty(JPLoggerConfigFile::class.java)
            JPService.registerProperty(JPLoggerDebugConfigFile::class.java)
            JPService.registerProperty(JPLogDirectory::class.java)

            // parse properties
            JPService.parseAndExitOnError(args)

            // load custom logger settings
            try {
                loadCustomLoggerSettings()
            } catch (e: CouldNotPerformException) {
                System.exit(2)
                return
            }
            val logger = LoggerFactory.getLogger(Launcher::class.java)

            // print launcher end exit
            try {
                if (JPService.getProperty(JPPrintLauncher::class.java).value) {
                    if (launcherMap.isEmpty()) {
                        println(generateAppName() + " does not provide any launcher!")
                        System.exit(255)
                    }
                    println("Available launcher:")
                    println()
                    var maxLauncherNameSize = 0
                    for ((key) in launcherMap) {
                        maxLauncherNameSize = Math.max(maxLauncherNameSize, key.simpleName.length)
                    }
                    for ((key) in launcherMap) {
                        println(
                            "\t• " + StringProcessor.fillWithSpaces(
                                key.simpleName,
                                maxLauncherNameSize
                            ) + "  ⊳  " + key.name
                        )
                    }
                    println()
                    System.exit(0)
                }
            } catch (ex: JPNotAvailableException) {
                ExceptionPrinter.printHistory("Could not check if launcher should be printed.", ex, logger)
            }
            logger.info("Start " + generateAppName() + "...")
            for ((key, value) in HashSet>, AbstractLauncher<*>>>(
                launcherMap.entries
            )) {

                // check if launcher was excluded
                var exclude = false
                try {
                    //filter excluded launcher
                    for (exclusionPatter in JPService.getProperty(JPExcludeLauncher::class.java).value) {
                        if (key.name.lowercase(Locale.getDefault()).contains(
                                exclusionPatter.replace("-", "").replace("_", "").lowercase(
                                    Locale.getDefault()
                                )
                            )
                        ) {
                            exclude = true
                        }
                    }
                } catch (ex: JPNotAvailableException) {
                    ExceptionPrinter.printHistory("Could not process launcher exclusion!", ex, logger)
                }
                if (exclude) {
                    logger.info(key.simpleName + " excluded from execution.")
                    launcherMap.remove(key)
                    continue
                }
                value.launch()
            }
            synchronized(WAITING_TASK_LIST_LOCK) {
                // start all waiting tasks in parallel
                for ((key, value) in launcherMap) {
                    val waitingTask: Future<*> = GlobalCachedExecutorService.submit {
                        while (!Thread.interrupted()) {
                            try {
                                try {
                                    value.launcherTask!![LAUNCHER_TIMEOUT, TimeUnit.MILLISECONDS]
                                    if (!value.isVerified) {
                                        pushToVerificationExceptionStack(
                                            application,
                                            CouldNotPerformException(
                                                "Could not verify " + key.simpleName + "!",
                                                value.verificationFailedCause
                                            )
                                        )
                                    }
                                } catch (ex: CancellationException) {
                                    // cancellation means the complete launch was canceled anyway and no further steps are required.
                                } catch (ex: ExecutionException) {
                                    // recover Interrupted exception to avoid error printing during system shutdown
                                    val initialCause = getInitialCause(ex)
                                    if (initialCause is InterruptedException) {
                                        throw initialCause
                                    }
                                    throw ex
                                }
                                break
                            } catch (ex: TimeoutException) {
                                logger.warn("Launcher " + key.simpleName + " startup delay detected!")
                            } catch (ex: InterruptedException) {
                                // if a core launcher could not be started the whole startup failed so interrupt
                                if (value.isCoreLauncher) {
                                    // shutdown all launcher
                                    forceStopLauncher(launcherMap)
                                    return@submit null
                                }
                            } catch (ex: Exception) {
                                val exx = CouldNotPerformException("Could not launch " + key.simpleName + "!", ex)
                                pushToErrorExceptionStack(application, exx)

                                // if a core launcher could not be started the whole startup failed so interrupt
                                if (value.isCoreLauncher) {
                                    // shutdown all launcher
                                    forceStopLauncher(launcherMap)
                                }

                                // finish launcher
                                return@submit null
                            }
                        }
                        null
                    }
                    waitingTaskList.add(waitingTask)
                }
            }
            try {
                for (waitingTask in waitingTaskList) {
                    try {
                        waitingTask.get()
                    } catch (ex: ExecutionException) {
                        // these exception will be pushed to the error exception stack anyway and printed in the summary
                    } catch (ex: CancellationException) {
                        // if a core launcher fails a cancellation exception will be thrown
                        printSummary(
                            application,
                            logger,
                            generateAppName() + " will be stopped because at least one core laucher could not be started."
                        )
                        System.exit(200)
                    }
                }
            } catch (ex: InterruptedException) {

                // recover interruption
                Thread.currentThread().interrupt()

                // shutdown all launcher
                forceStopLauncher(launcherMap)

                // print a summary containing the exceptions
                printSummary(application, logger, generateAppName() + " caught shutdown signal during startup phase!")
                return
            }
            printSummary(application, logger, generateAppName() + " was started with restrictions!")
        }

        private fun pushToVerificationExceptionStack(source: Any?, ex: Exception) {
            synchronized(VERIFICATION_STACK_LOCK) {
                verificationExceptionStack = MultiException.push(source, ex, verificationExceptionStack)
            }
        }

        private fun pushToErrorExceptionStack(source: Any?, ex: Exception) {
            synchronized(ERROR_STACK_LOCK) {
                errorExceptionStack = MultiException.push(source, ex, errorExceptionStack)
            }
        }

        private fun stopWaiting() {
            synchronized(WAITING_TASK_LIST_LOCK) {
                for (waitingTask in waitingTaskList) {
                    if (!waitingTask.isDone) {
                        waitingTask.cancel(true)
                    }
                }
            }
        }

        private fun interruptLaunch(launcherMap: Map>, AbstractLauncher<*>>) {

            // stop boot
            stopWaiting()

            // interrupt all launcher
            for ((_, value) in launcherMap) {
                value.interruptLaunch()
            }
        }

        private fun forceStopLauncher(launcherMap: Map>, AbstractLauncher<*>>) {
            interruptLaunch(launcherMap)

            // stop all launcher. This is done in an extra loop since stop can block if the launcher is not yet fully interrupted.
            for ((_, value) in launcherMap) {
                value.stop()
            }
        }

        private fun generateAppName(): String {
            return JPService.getApplicationName() + if (JPService.getSubmoduleName()
                    .isEmpty()
            ) "" else "-" + JPService.getSubmoduleName()
        }

        private fun printSummary(application: Class<*>?, logger: Logger, errorMessage: String) {
            try {
                var exceptionStack: ExceptionStack? = null
                try {
                    MultiException.checkAndThrow({ "Errors during startup phase!" }, errorExceptionStack)
                } catch (ex: MultiException) {
                    exceptionStack = MultiException.push(application, ex, exceptionStack)
                }
                try {
                    MultiException.checkAndThrow({ "Verification process not passed!" }, verificationExceptionStack)
                } catch (ex: MultiException) {
                    exceptionStack = MultiException.push(application, ex, exceptionStack)
                }
                if (Thread.currentThread().isInterrupted) {
                    logger.info(generateAppName() + " was interrupted.")
                    return
                }
                MultiException.checkAndThrow({ errorMessage }, exceptionStack)
                logger.info(generateAppName() + " successfully started.")
            } catch (ex: MultiException) {
                ExceptionPrinter.printHistory(ex, logger)
            }
        }
    }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy