VaadinUndertow/src/dorkbox/vaadin/VaadinApplication.kt

872 lines
37 KiB
Kotlin

/*
* Copyright 2023 dorkbox, llc
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package dorkbox.vaadin
import com.vaadin.flow.server.VaadinContext
import com.vaadin.flow.server.frontend.FrontendUtils
import dorkbox.fsm.DoubleArrayStringTrie
import dorkbox.vaadin.undertow.*
import dorkbox.vaadin.util.CallingClass
import dorkbox.vaadin.util.TrieClassLoader
import dorkbox.vaadin.util.UndertowBuilder
import dorkbox.vaadin.util.VaadinConfig
import io.github.classgraph.ClassGraph
import io.github.classgraph.ScanResult
import io.undertow.Undertow
import io.undertow.UndertowMessages
import io.undertow.UndertowOptions
import io.undertow.server.HttpHandler
import io.undertow.server.HttpServerExchange
import io.undertow.server.handlers.cache.CacheHandler
import io.undertow.server.handlers.cache.DirectBufferCache
import io.undertow.server.handlers.resource.CachingResourceManager
import io.undertow.server.handlers.resource.ResourceManager
import io.undertow.servlet.Servlets
import io.undertow.servlet.api.*
import io.undertow.websockets.jsr.WebSocketDeploymentInfo
import mu.KotlinLogging
import org.xnio.Xnio
import org.xnio.XnioWorker
import java.io.File
import java.io.IOException
import java.net.URL
import java.util.*
import java.util.concurrent.*
import java.util.concurrent.atomic.*
import javax.servlet.Servlet
import javax.servlet.ServletContainerInitializer
import javax.servlet.ServletRequest
import javax.servlet.ServletResponse
import javax.servlet.SessionTrackingMode
import javax.servlet.annotation.HandlesTypes
import kotlin.reflect.KClass
/**
* Loads, Configures, and Starts a Vaadin 14 application
*/
@Suppress("unused")
class VaadinApplication : ExceptionHandler {
companion object {
/**
* Gets the version number.
*/
const val version = "14.10"
// this must match the version information in the build.gradle.kts file (this is automatically passed into the plugin)
const val vaadinVersion = "14.10.1"
const val undertowVersion = "2.2.21.Final"
// Vaadin 14.9 changed how license checking works, and doesn't include this.
const val oshiVersion = "6.4.0"
// license checker requires JNA
const val jnaVersion = "5.12.1" //5.13 isn't properly published? It cannot be found.
init {
// Add this project to the updates system, which verifies this class + UUID + version information
dorkbox.updates.Updates.add(VaadinApplication::class.java, "fc74a52b08c8410fabfea67ac5dca566", version)
}
}
private val logger = KotlinLogging.logger {}
private val httpLogger = KotlinLogging.logger(logger.name + ".http")
val runningAsJar: Boolean
var enableCachedHandlers = false
val tempDir: File = File(System.getProperty("java.io.tmpdir", "tmpDir"), "undertow").absoluteFile
private val onStopList = mutableListOf<Runnable>()
val vaadinConfig: VaadinConfig
private lateinit var trieClassLoader: TrieClassLoader
private lateinit var resourceCollectionManager: ResourceCollectionManager
private val resources = ArrayList<ResourceManager>()
// private val exactResources = ArrayList<String>()
// private val prefixResources = ArrayList<String>()
private val originalClassLoader: ClassLoader
private lateinit var cacheHandler: HttpHandler
private lateinit var servletHttpHandler: HttpHandler
private lateinit var servletManager: DeploymentManager
private lateinit var jarStringTrie: DoubleArrayStringTrie<String>
private lateinit var jarUrlTrie: DoubleArrayStringTrie<URL>
private lateinit var diskTrie: DoubleArrayStringTrie<URL>
private lateinit var servletBuilder: DeploymentInfo
private lateinit var serverBuilder: UndertowBuilder
private lateinit var servlet: ServletInfo
/** This url is used to define what the base url is for accessing the Vaadin stats.json file */
lateinit var baseUrl: String
private val threadGroup = ThreadGroup("Web Server")
@Volatile
private var undertowServer: Undertow? = null
init {
// THIS code might be as a jar, however we want to test if the **TOP** leve; code that called this is running as a jar.
runningAsJar = CallingClass.get().getResource("")!!.protocol == "jar"
vaadinConfig = VaadinConfig(runningAsJar, tempDir)
originalClassLoader = Thread.currentThread().contextClassLoader
}
private fun addAnnotated(annotationScanner: ScanResult,
kClass: KClass<*>,
classSet: MutableSet<Class<*>>) {
val javaClass = kClass.java
val canonicalName = javaClass.canonicalName
when {
javaClass.isAnnotation -> {
val routes = annotationScanner.getClassesWithAnnotation(canonicalName)
val loadedClasses = routes.loadClasses()
classSet.addAll(loadedClasses)
}
javaClass.isInterface -> {
val classesImplementing = annotationScanner.getClassesImplementing(canonicalName)
val loadedClasses = classesImplementing.loadClasses()
classSet.addAll(loadedClasses)
}
kClass.isAbstract -> { /* do nothing! */ }
else -> throw RuntimeException("Annotation scan for type $canonicalName:$javaClass not supported yet")
}
}
private fun recurseAllFiles(allRelativeFilePaths: MutableSet<WebResource>, file: File, rootFileSize: Int) {
file.listFiles()?.forEach {
if (it.isDirectory) {
recurseAllFiles(allRelativeFilePaths, it, rootFileSize)
} else {
val resourcePath = it.toURI().toURL()
// must ALSO use forward slash (unix)!!
val relativePath = resourcePath.path.substring(rootFileSize)
logger.trace {
"Disk resource: $relativePath"
}
allRelativeFilePaths.add(WebResource(relativePath, resourcePath))
}
}
}
@Suppress("DuplicatedCode")
fun initResources() {
val metaInfResources = "META-INF/resources"
val metaInfValLength = metaInfResources.length + 1
val buildMetaInfResources = "build/resources/main/META-INF/resources"
// resource locations are tricky...
// when a JAR : META-INF/resources
// when on disk: webApp/META-INF/resources
// TODO: check if the modules restriction (see following note) is still the case for vaadin 14
// NOTE: we cannot use "modules" yet (so no module-info.java file...) otherwise every dependency gets added to the module path,
// and since almost NONE of them support modules, this will break us.
// NOTE: we cannot use "modules" yet (so no module-info.java file...) otherwise every dependency gets added to the module path,
// and since almost NONE of them support modules, this will break us.
// find all of the jars in the module/classpath with resources in the META-INF directory
logger.info { "Discovering all bundled jar $metaInfResources locations" }
val scanResultJarDependencies = ClassGraph()
.filterClasspathElements { it.endsWith(".jar") }
.acceptPaths(metaInfResources)
.scan()
val scanResultLocalDependencies = ClassGraph()
.filterClasspathElements { !it.endsWith(".jar") }
.acceptPaths(metaInfResources)
.scan()
val jarLocations = mutableSetOf<WebResourceString>()
val diskLocations = mutableSetOf<WebResource>()
val urlClassLoader = mutableSetOf<URL>()
if (runningAsJar && vaadinConfig.extractFromJar) {
// running from JAR (really just temp-dir, since the jar is extracted!)
logger.info { "Running from jar and extracting all jar [$metaInfResources] files to [$tempDir]" }
var lastFile: File? = null
scanResultJarDependencies.allResources.forEach { resource ->
val resourcePath = resource.pathRelativeToClasspathElement
val relativePath = resourcePath.substring(metaInfValLength)
logger.trace { "Jar resource: $relativePath" }
if (lastFile != resource.classpathElementFile) {
lastFile = resource.classpathElementFile
logger.trace { "Jar resource: ${resource.classpathElementFile}" }
}
// we should copy this resource out, since loading resources from jar files is time+memory intensive
val outputFile = tempDir.resolve(relativePath)
// // TODO: should overwrite file? check hashes?
// if there is ever a NEW version of our code run, the OLD version will still run if the files are not overwritten!
// if (!outputFile.exists()) {
val parentFile = outputFile.parentFile
if (!parentFile.isDirectory && !parentFile.mkdirs()) {
logger.trace { "Unable to create output directory $parentFile" }
} else {
resource.open().use { input ->
outputFile.outputStream().use { input.copyTo(it) }
}
}
// }
diskLocations.add(WebResource(relativePath, outputFile.toURI().toURL()))
}
} else if (runningAsJar) {
// running from JAR (not-extracted!)
logger.info { "Running from jar files" }
var lastFile: File? = null
scanResultJarDependencies.allResources.forEach { resource ->
val resourcePath = resource.pathRelativeToClasspathElement
val relativePath = resourcePath.substring(metaInfValLength)
val resourceUrl = resource.url
logger.trace { "Jar resource: $relativePath" }
if (lastFile != resource.classpathElementFile) {
lastFile = resource.classpathElementFile
logger.trace { "Jar resource: ${resource.classpathElementFile}" }
}
val path = resourceUrl.path
val resourceDir = path.substring(0, path.length - relativePath.length)
// these are all resources inside JAR files.
jarLocations.add(WebResourceString(relativePath, resource.classpathElementURL, resourcePath, URL(resourceDir)))
// jar file this resource is from -- BUT NOT THE RESOURCE ITSELF
urlClassLoader.add(resource.classpathElementURL)
}
} else {
// running from IDE
logger.info { "Running from IDE files" }
var lastFile: File? = null
scanResultJarDependencies.allResources.forEach { resource ->
val resourcePath = resource.pathRelativeToClasspathElement
val relativePath = resourcePath.substring(metaInfValLength)
val resourceUrl = resource.url
logger.trace { "Jar resource: $relativePath" }
if (lastFile != resource.classpathElementFile) {
lastFile = resource.classpathElementFile
logger.trace { "Jar resource: ${resource.classpathElementFile}" }
}
val path = resourceUrl.path
val resourceDir = path.substring(0, path.length - relativePath.length)
// these are all resources inside JAR files.
jarLocations.add(WebResourceString(relativePath, resourceUrl, resourcePath, URL(resourceDir)))
// jar file this resource is from -- BUT NOT THE RESOURCE ITSELF
urlClassLoader.add(resource.classpathElementURL)
}
// some static resources from disk are ALSO loaded by the classloader.
scanResultLocalDependencies.allResources.forEach { resource ->
val resourcePath = resource.pathRelativeToClasspathElement
val relativePath = resourcePath.substring(metaInfValLength)
logger.trace { "Local resource: $relativePath" }
diskLocations.add(WebResource(relativePath, resource.url))
}
// we also have resources that are OUTSIDE the classpath (ie: in the temp build dir)
// This is necessary BECAUSE we have to be able to ALSO serve resources via the classloader!
val buildDirMetaInfResources = File(buildMetaInfResources).absoluteFile.normalize()
val rootPathLength = buildDirMetaInfResources.toURI().toURL().path.length
recurseAllFiles(diskLocations, buildDirMetaInfResources, rootPathLength)
}
// we use the TRIE data structure to QUICKLY find what we are looking for.
// This is so our classloader can find the resource without having to manually configure each request.
val jarStringResourceRequestMap = mutableMapOf<String, String>()
val jarUrlResourceRequestMap = mutableMapOf<String, URL>()
val diskResourceRequestMap = mutableMapOf<String, URL>()
jarLocations.forEach { (requestPath, resourcePath, relativeResourcePath) ->
// make sure the path is WWW request compatible (ie: no spaces/etc)
val wwwCompatiblePath = java.net.URLDecoder.decode(requestPath, Charsets.UTF_8)
// this adds the resource to our request map, used by our trie
jarStringResourceRequestMap[wwwCompatiblePath] = relativeResourcePath
jarUrlResourceRequestMap[wwwCompatiblePath] = resourcePath
if (!wwwCompatiblePath.startsWith("META-INF")) {
// some-of the resources are loaded with a "META-INF" prefix by the vaadin servlet
jarStringResourceRequestMap["META-INF/$wwwCompatiblePath"] = relativeResourcePath
jarUrlResourceRequestMap["META-INF/$wwwCompatiblePath"] = resourcePath
}
if (!wwwCompatiblePath.startsWith('/')) {
// some-of the resources are loaded with a "/" prefix by the vaadin servlet
jarStringResourceRequestMap["/$wwwCompatiblePath"] = relativeResourcePath
jarUrlResourceRequestMap["/$wwwCompatiblePath"] = resourcePath
}
}
diskLocations.forEach { (requestPath, resourcePath) ->
// make sure the path is WWW request compatible (ie: no spaces/etc)
val wwwCompatiblePath = java.net.URLDecoder.decode(requestPath, Charsets.UTF_8)
// this adds the resource to our request map, used by our trie
diskResourceRequestMap[wwwCompatiblePath] = resourcePath
if (!wwwCompatiblePath.startsWith("META-INF")) {
// some-of the resources are loaded with a "META-INF" prefix by the vaadin servlet
diskResourceRequestMap["META-INF/$wwwCompatiblePath"] = resourcePath
}
if (!wwwCompatiblePath.startsWith('/')) {
// some-of the resources are loaded with a "/" prefix by the vaadin servlet
diskResourceRequestMap["/$wwwCompatiblePath"] = resourcePath
}
}
// EVERYTHING IS ACCESSED VIA TRIE, NOT VIA HASHMAP! (it's faster this way)
jarStringTrie = DoubleArrayStringTrie(jarStringResourceRequestMap)
jarUrlTrie = DoubleArrayStringTrie(jarUrlResourceRequestMap)
diskTrie = DoubleArrayStringTrie(diskResourceRequestMap)
// so we can use the undertow cache to serve resources, instead of the vaadin servlet (which doesn't cache, and is really slow)
// NOTE: this will load the stats.json file!
// for our classloader, we have to make sure that we are BY DIRECTORY, not by file, for the resource array!
val toTypedArray = jarLocations.map { it.resourceDir }.toSet().toTypedArray()
this.trieClassLoader = TrieClassLoader(diskTrie, jarStringTrie, toTypedArray, this.javaClass.classLoader, logger)
// we want to start ALL aspects of the application using our NEW classloader (instead of the "current" classloader)
Thread.currentThread().contextClassLoader = this.trieClassLoader
val strictFileResourceManager = StrictFileResourceManager("Static Files", diskTrie, httpLogger)
val jarResourceManager = JarResourceManager("Jar Files", jarUrlTrie, httpLogger)
// When we are searching for resources, the following search order is optimized for access speed and request hit order
// DISK
// files
// jars (since they are containers)
// flow-client
// flow-push
// flow-server
// then every other jar
resources.add(strictFileResourceManager)
resources.add(jarResourceManager)
// val client = jarResources.firstOrNull { it.name.contains("flow-client") }
// val push = jarResources.firstOrNull { it.name.contains("flow-push") }
// val server = jarResources.firstOrNull { it.name.contains("flow-server") }
//
// if (client != null && push != null && server != null) {
// // these jars will ALWAYS be available (as of Vaadin 14.2)
// // if we are running from a fatjar, then the resources will likely be extracted (so this is not necessary)
// jarResources.remove(client)
// jarResources.remove(push)
// jarResources.remove(server)
//
// resources.add(client)
// resources.add(push)
// resources.add(server)
// }
//
// resources.addAll(jarResources)
// TODO: Have a 404 resource handler to log when a requested file is not found!
// NOTE: atmosphere is requesting the full path of 'WEB-INF/classes/'.
// What to do? search this with classgraph OR we re-map this to 'out/production/classes/' ??
// also accessed is : WEB-INF/lib/
// TODO: runtime GZ compression of resources!?! only necessary in the JAR run mode (which is what runs on servers)
}
fun initServlet(enableCachedHandlers: Boolean, cacheTimeoutSeconds: Int,
servletClass: Class<out Servlet> = com.vaadin.flow.server.VaadinServlet::class.java,
servletName: String = "Vaadin",
secureService: Boolean = false,
servletConfig: ServletInfo.() -> Unit = {},
undertowConfig: UndertowBuilder.() -> Unit) {
this.enableCachedHandlers = enableCachedHandlers
resourceCollectionManager = ResourceCollectionManager(resources)
val conditionalResourceManager =
when {
enableCachedHandlers -> {
val cacheSize = 1024 // size of the cache
val maxFileSize = 1024*1024*1024*10L // 10 mb file. The biggest file size we cache
val maxFileAge = TimeUnit.HOURS.toMillis(1) // How long an item can stay in the cache in milliseconds
val bufferCache = DirectBufferCache(1024, 10, 1024 * 1024 * 200)
CachingResourceManager(cacheSize, maxFileSize, bufferCache, resourceCollectionManager, maxFileAge.toInt())
}
else -> {
// sometimes it is really hard to debug when using the cache
resourceCollectionManager
}
}
onStopList.add(Runnable {
conditionalResourceManager.close()
})
// directly serve our static requests in the IO thread (and not in a worker/coroutine)
val staticResourceHandler = DirectResourceHandler(resourceCollectionManager)
if (enableCachedHandlers) {
staticResourceHandler.setCacheTime(cacheTimeoutSeconds) // tell the browser to cache our static resources (in seconds)
}
cacheHandler = when {
enableCachedHandlers -> {
val cache = DirectBufferCache(1024, 10, 1024 * 1024 * 200)
CacheHandler(cache, staticResourceHandler)
}
else -> {
// sometimes it is really hard to debug when using the cache
staticResourceHandler
}
}
// servletClass: Class<out Servlet> = VaadinServlet::class.java,
// we have to load the instance of the VaadinServlet INSIDE our url handler! (so all traffic/requests go through the url classloader!)
// val forceReloadClassLoader = object : ClassLoader(trieClassLoader) {
// override fun loadClass(name: String?, resolve: Boolean): Class<*> {
// if (name == servletClassName) {
// throw ClassNotFoundException()
//// return trieClassLoader.findClass(name)
// }
//
// return super.loadClass(name, resolve)
// }
// }
//
// val forceReloadClassLoader2 = object : ClassLoader(forceReloadClassLoader) { }
// val servletClass = Class.forName(servletClassName, true, trieClassLoader) as Class<out Servlet>
// val cl = servletClass.classLoader
// val instance = servletClass.constructors[0].newInstance()
// val immediateInstanceFactory = ImmediateInstanceFactory(instance) as ImmediateInstanceFactory<out Servlet>
val executor = Executors.newCachedThreadPool(DaemonThreadFactory("HttpWrapper", threadGroup, trieClassLoader))
servlet = Servlets.servlet(servletName, servletClass)
.setLoadOnStartup(1)
.setAsyncSupported(true)
.setExecutor(executor)
// have to say where our NPM/dev mode files live.
.addInitParam(FrontendUtils.PROJECT_BASEDIR, File("").absolutePath)
.addInitParam("enable-websockets", "true")
.addMapping("/*")
vaadinConfig.addServletInitParameters(servlet)
// setup (or change) custom config options (
servletConfig(servlet)
servletBuilder = Servlets.deployment()
.setClassLoader(trieClassLoader)
.setResourceManager(conditionalResourceManager)
.setDisplayName(servletName)
.setDefaultEncoding("UTF-8")
.setSecurityDisabled(true) // security is controlled in memory using vaadin
.setContextPath("/") // root context path
.setDeploymentName(servletName)
.setExceptionHandler(this)
.addServlets(servlet)
.setSessionPersistenceManager(FileSessionPersistence(tempDir))
.addServletContextAttribute(WebSocketDeploymentInfo.ATTRIBUTE_NAME, WebSocketDeploymentInfo())
.addServletContainerInitializers(listOf())
val sessionCookieName = ServletSessionConfig.DEFAULT_SESSION_ID
// // use the created actor
// // FIXME: 8 actors and 2 threads concurrency on the actor map?
// val coroutineHttpWrapper = CoroutineHttpWrapper(sessionCookieName, 8, 2)
//
// onStopList.add(Runnable {
// // launch new coroutine in background and continue, since we want to stop our http wrapper in a different coroutine!
// GlobalScope.launch {
// coroutineHttpWrapper.stop()
// }
// })
//
// servletBuilder.initialHandlerChainWrappers.add(coroutineHttpWrapper)
//
// // destroy the actors on session invalidation
// servletBuilder.addSessionListener(ActorSessionCleanup(coroutineHttpWrapper.actorsPerSession))
// NOTE: To use a DIFFERENT lock strategy (ie: one compatible with coroutines), start here
// flow-server/src/main/java/com/vaadin/flow/server/VaadinService.java
// protected Lock lockSession(WrappedSession wrappedSession
// protected void unlockSession(WrappedSession wrappedSession, Lock lock) {
// and
// for custom lock storage
// * strategy override {@link #getSessionLock(WrappedSession)} and
// * {@link #setSessionLock(WrappedSession,Lock)} instead.
// configure how the servlet behaves
val servletSessionConfig = ServletSessionConfig()
servletSessionConfig.isSecure = secureService // cookies are only possible when via HTTPS
servletSessionConfig.sessionTrackingModes = setOf(SessionTrackingMode.COOKIE)
servletSessionConfig.name = sessionCookieName
servletBuilder.servletSessionConfig = servletSessionConfig
// Regardless of metadata, if there are any ServletContainerInitializers with @HandlesTypes, we must add the classes
// Next, scan all these classes so we can call their onStartup() methods correctly
val serviceLoader = ServiceLoader.load(ServletContainerInitializer::class.java)
ClassGraph().enableAnnotationInfo().enableClassInfo().scan().use { annotationScanner ->
for (service in serviceLoader) {
val classSet: HashSet<Class<*>> = hashSetOf()
val javaClass = service.javaClass
val annotation= javaClass.getAnnotation(HandlesTypes::class.java)
if (annotation != null) {
val classes = annotation.value
for (aClass in classes) {
addAnnotated(annotationScanner, aClass, classSet)
}
}
if (classSet.isNotEmpty()) {
if (javaClass == com.vaadin.flow.server.startup.DevModeInitializer::class.java) {
if (vaadinConfig.devMode) {
// instead of the default, we load **OUR** dev-mode initializer.
// The vaadin one is super buggy for custom environments
servletBuilder.addServletContainerInitializer(
ServletContainerInitializerInfo(com.vaadin.flow.server.startup.DevModeInitializer::class.java, classSet))
}
} else {
// do not load the dev-mode initializer for production mode
servletBuilder.addServletContainerInitializer(ServletContainerInitializerInfo(javaClass, classSet))
}
}
}
}
if (vaadinConfig.devMode) {
// NOTE: The vaadin flow files only exist AFTER vaadin is initialized, so this block MUST be after 'manager.deploy()'
// in dev mode, the local resources are hardcoded to an **INCORRECT** location. They
// are hardcoded to "src/main/resources/META-INF/resources/frontend", so we have to
// copy them ourselves from the correct location... ( node_modules/@vaadin/flow-frontend/ )
val targetDir = File("build", FrontendUtils.NODE_MODULES + FrontendUtils.FLOW_NPM_PACKAGE_NAME).absoluteFile
logger.info { "Copying local frontend resources to $targetDir" }
if (!targetDir.exists()) {
throw RuntimeException("Startup directories are missing! Unable to continue - please run compileResources for DEV mode!")
}
File("frontend").absoluteFile.copyRecursively(targetDir, true)
}
serverBuilder = UndertowBuilder()
.setSocketOption(org.xnio.Options.BACKLOG, 10000)
// Let the server workers have time to close when we shutdown
.setServerOption(UndertowOptions.SHUTDOWN_TIMEOUT, 10000)
.setSocketOption(org.xnio.Options.REUSE_ADDRESSES, true)
// In HTTP/1.1, connections are persistent unless declared otherwise.
// Adding a "Connection: keep-alive" header to every response would only add useless bytes.
.setServerOption(UndertowOptions.SSL_USER_CIPHER_SUITES_ORDER, true)
.setServerOption(UndertowOptions.ENABLE_STATISTICS, false)
// configure or override options
undertowConfig(serverBuilder)
// setup the base URL from the server builder
val (isHttps, host, port) = serverBuilder.httpListener
val transport = if (isHttps) {
"https://"
} else {
"http://"
}
val hostInfo = if (host == "0.0.0.0") {
"127.0.0.1"
} else {
host
}
val portInfo = when {
isHttps && port == 443 -> ""
!isHttps && port == 80 -> ""
else -> ":$port"
}
baseUrl = "$transport$hostInfo$portInfo"
}
////
////
//// undertow server specific methods
////
////
val xnio: Xnio?
get() {
return undertowServer?.xnio
}
val worker: XnioWorker?
get() {
return undertowServer?.worker
}
val listenerInfo: MutableList<Undertow.ListenerInfo>
get() {
return undertowServer?.listenerInfo ?: throw UndertowMessages.MESSAGES.serverNotStarted()
}
@Throws(IOException::class)
fun start() {
// if we don't have it defined, then we use the classloader.
val statsUrlFromConfig = vaadinConfig.statsUrl
if (statsUrlFromConfig.isEmpty()) {
val statsFile = "META-INF/resources/VAADIN/config/stats.json"
// in a roundabout way, this is how vaadin actually load the stats.json file.
// (it could be different, but this is the generic way vaadin does it)
if (VaadinContext::class.java.classLoader.getResource(statsFile) == null) {
throw IOException("Unable to startup the VAADIN webserver. The 'stats.json' definition file is not available. (Usually on the classloader at '$statsFile'')" )
}
logger.info("Loading the stats.json file via the classloader")
vaadinConfig.setupStatsJsonClassloader(servlet, statsFile)
} else {
// make sure that the stats.json file is accessible
// the request will come in as 'VAADIN/config/stats.json' or '/VAADIN/config/stats.json'
//
// If stats.json DOES NOT EXIST, there will be infinite recursive lookups for this file.
val statsFile = "VAADIN/config/stats.json"
// our resource manager ONLY manages disk + jars!
if (diskTrie[statsFile] == null && jarStringTrie[statsFile] == null) {
throw IOException("Unable to startup the VAADIN webserver. The 'stats.json' definition file is not available. (Usually at '$statsFile'')" )
}
val statsUrl = "$baseUrl/$statsFile"
logger.info("Loading the stats.json file via URL: $statsUrl")
vaadinConfig.setupStatsJsonUrl(servlet, statsUrl)
}
undertowServer = serverBuilder.build()
/////////////////////////////////////////////////////////////////
// INITIALIZING AND STARTING THE SERVLET
/////////////////////////////////////////////////////////////////
servletManager = Servlets.defaultContainer().addDeployment(servletBuilder)
servletManager.deploy()
// TODO: adjust the session timeout (default is 30 minutes) from when the LAST heartbeat is detected
// manager.deployment.sessionManager.setDefaultSessionTimeout(TimeUnit.MINUTES.toSeconds(Args.webserver.sessionTimeout).toInt())
servletHttpHandler = servletManager.start()
// NOTE: look into SessionRestoringHandler to keep session state across re-deploys (this is normally not used in production). this might just be tricks with classloaders to keep sessions around
// we also want to save sessions to disk, and be able to read from them if we want See InMemorySessionManager (we would have to write our own)
/*
* look at the following
* GracefulShutdownHandler
* LearningPushHandler
* RedirectHandler
* RequestLimitingHandler
* SecureCookieHandler
*
*
* to setup ALPN and ssl, it's FASTER to use openssl instead of java
* http://wildfly.org/news/2017/10/06/OpenSSL-Support-In-Wildfly/
* https://github.com/undertow-io/undertow/blob/master/core/src/main/java/io/undertow/protocols/alpn/OpenSSLAlpnProvider.java
*/
// NOTE: we start this in a NEW THREAD so we can create and use a thread-group for all of the undertow threads created. This allows
// us to keep our main thread group "un-cluttered" when analyzing thread/stack traces.
//
// This is a hacky, but undertow does not support setting the thread group in the builder.
val exceptionThrown = AtomicReference<Exception>()
val latch = CountDownLatch(1)
val thread = Thread(threadGroup) {
try {
undertowServer?.start()
} catch (e: Exception) {
exceptionThrown.set(e)
} finally {
latch.countDown()
}
}
thread.contextClassLoader = this.trieClassLoader
thread.start()
latch.await()
Thread.currentThread().contextClassLoader = this.originalClassLoader
val exception = exceptionThrown.get()
if (exception != null) {
throw exception
}
}
fun stop() {
try {
servletManager.stop()
} catch (e: Exception) {
// ignored
}
try {
val worker = worker
if (worker != null) {
worker.shutdown()
worker.awaitTermination(10L, TimeUnit.SECONDS)
}
undertowServer?.stop()
} finally {
onStopList.forEach {
it.run()
}
}
}
fun handleRequest(exchange: HttpServerExchange) {
// dev-mode : incoming requests USUALLY start with a '/'
val path = exchange.relativePath
httpLogger.trace { "REQUEST undertow: $path" }
if (path.length == 1) {
httpLogger.trace { "REQUEST of length 1: $path" }
servletHttpHandler.handleRequest(exchange)
return
}
// serve the following directly via the resource handler, so we can do it directly in the networking IO thread.
// Because this is non-blocking, this is also the preferred way to do this for performance.
// at the time of writing, this was "/icons", "/images", and in production mode "/VAADIN"
// our resource manager ONLY manages disk + jars!
val diskResourcePath: URL? = diskTrie[path]
if (diskResourcePath != null) {
httpLogger.trace { "URL DISK TRIE: $diskResourcePath" }
cacheHandler.handleRequest(exchange)
return
}
val jarResourcePath: String? = jarStringTrie[path]
if (jarResourcePath != null) {
httpLogger.trace { "URL JAR TRIE: $jarResourcePath" }
cacheHandler.handleRequest(exchange)
return
}
// this is the default, and will use the servlet to handle the request
httpLogger.trace { "Forwarding request to servlet" }
servletHttpHandler.handleRequest(exchange)
}
fun logStartupInfo() {
logger.info { "Temp dir: $tempDir" }
logger.info { "Launched from jar: $runningAsJar" }
logger.info { "Cached HTTP handlers: $enableCachedHandlers" }
if (vaadinConfig.devMode) {
logger.info { "Vaadin running in DEVELOPMENT mode" }
} else {
logger.info { "Vaadin running in PRODUCTION mode" }
}
logger.info { "Loader version: $version" }
logger.info { "Vaadin version: $vaadinVersion" }
}
/**
* Handles an exception. If this method returns true then the request/response cycle is considered to be finished,
* and no further action will take place, if this returns false then standard error page redirect will take place.
*
* The default implementation of this simply logs the exception and returns false, allowing error page and async context
* error handling to proceed as normal.
*
* @param exchange The exchange
* @param request The request
* @param response The response
* @param throwable The exception
* @return <code>true</code> true if the error was handled by this method
*/
override fun handleThrowable(
exchange: HttpServerExchange?,
request: ServletRequest?,
response: ServletResponse?,
throwable: Throwable?
): Boolean {
logger.error("Error ${request} : ${response}", throwable)
return false
}
}