using concurrent::Actor
using concurrent::ActorPool
using concurrent::AtomicRef
using concurrent::AtomicBool
using afConcurrent::LocalRef
using web::WebMod
using web::WebReq
using web::WebRes
using afIoc::IocErr
using afIoc::IocShutdownErr
using afIoc::Registry
using afIoc::RegistryMeta
using afIocEnv::IocEnv
using afIocConfig::ConfigSource
** The `web::WebMod` to be passed to [Wisp]`http://fantom.org/doc/wisp/index.html`.
const class BedSheetWebMod : WebMod {
private const static Log log := Utils.getLog(BedSheetWebMod#)
@NoDoc @Deprecated { msg="Use 'appName' instead" }
const Str moduleName
** Returns 'proj.name' from the application's pod meta, or the pod name if not defined.
const Str appName
** The port number this Bed App will be listening on.
const Int port
** The IoC registry. Can be 'null' if BedSheet has not yet started.
Registry? registry {
get { registryRef.val }
private set { registryRef.val = it }
}
** The Err (if any) that occurred on service startup
Err? startupErr {
get { startupErrRef.val }
private set { startupErrRef.val = it }
}
** When HTTP requests are received when BedSheet is starting up, then this message is returned to the client with a 500 status code.
**
** Defaults to: 'The website is starting up... Please retry in a moment.'
**
** Change it using a ctor it-block:
**
** BedSheetWebMod(reg) {
** it.startupMessage = "Computer Says No..."
** }
const Str startupMessage := "The website is starting up... Please retry in a moment."
private const AtomicBool started := AtomicBool(false)
private const AtomicRef startupErrRef := AtomicRef()
private const AtomicRef registryRef := AtomicRef()
private const AtomicRef pipelineRef := AtomicRef()
private const AtomicRef errPrinterRef := AtomicRef()
private const IocEnv iocEnv := IocEnv()
private const LocalRef bobRef := LocalRef("bedSheetBuilder")
** Creates this 'WebMod'. Use 'BedSheetBuilder' to create the 'Registry' instance - it ensures all the options have been set.
new make(BedSheetBuilder bob, |This|? f := null) {
pod := (Pod?) bob.options[BsConstants.meta_appPod]
mod := (Type?) bob.options[BsConstants.meta_appModule]
this.moduleName = (pod?.name ?: mod?.qname) ?: "UNKNOWN"
this.appName = bob.options[BsConstants.meta_appName]
this.port = bob.options[BsConstants.meta_appPort]
// it would be cleaner to just pass in a Registry instance, but because it takes a second
// or two to build, we want to handle requests in this period
this.bobRef.val = bob
f?.call(this)
}
@NoDoc
override Void onService() {
req.mod = this
// Hey! Why you call us when we're not running, eh!??
if (!started.val)
return
if (queueRequestsOnStartup)
while ((registry == null || middlewarePipeline == null) && startupErr == null) {
// 200ms should be un-noticable to humans but a lifetime to a computer!
Actor.sleep(200ms)
}
// web reqs still come in while we're processing onStart() so dispatch them quickly
// We used to sleep / queue them up until ready but then, when processing 100s at once,
// it was easy to run into race conditions when lazily creating services.
if ((registry == null || middlewarePipeline == null) && startupErr == null) {
res := (WebRes) Actor.locals["web.res"]
res.sendErr(503, startupMessage)
return
}
// rethrow the startup err if one occurred and let Wisp handle it
if (startupErr != null)
throw startupErr
try {
// this is actual call to BedSheet!
// the rest of this class is just startup and error handling fluff!
middlewarePipeline.service
} catch (IocShutdownErr err) {
// nothing we can do here
return
// theoretically, this should have already been dealt with by our ErrMiddleware...
// ...but it's handy for BedSheet development!
} catch (Err err) {
// try to send something to the browser
errLog := err.traceToStr
if (registry != null) {
try {
errPrinter := (ErrPrinterStr) registry.serviceById(ErrPrinterStr#.qname)
errLog = errPrinter.errToStr(err)
} catch {}
}
// log and throw, because we don't trust Wisp to log it
Env.cur.err.printLine(errLog)
if (!webRes.isCommitted)
webRes.sendErr(500, "${err.typeof} - ${err.msg}")
throw err
}
}
@NoDoc
override Void onStart() {
started.val = true
try {
log.info(BsLogMsgs.bedSheetWebMod_starting(appName, port))
// Go!!!
bob := (BedSheetBuilder) bobRef.val
registry = bob.buildRegistry.startup
// start the destroyer!
meta := (RegistryMeta) registry.serviceById(RegistryMeta#.qname)
if (meta.options[BsConstants.meta_pingProxy] == true) {
pingPort := (Int) meta.options[BsConstants.meta_proxyPort]
destroyer := (AppDestroyer) registry.autobuild(AppDestroyer#, [ActorPool(), pingPort])
destroyer.start
}
// print BedSheet connection details
configSrc := (ConfigSource) registry.dependencyByType(ConfigSource#)
host := (Uri) configSrc.get(BedSheetConfigIds.host, Uri#)
log.info(BsLogMsgs.bedSheetWebMod_started(appName, host))
// BUGFIX: eager load the middleware pipeline, so we can use the ErrMiddleware
// otherwise Errs thrown when instantiating middleware end up in limbo
// Errs from the FileHandler ctor are a prime example
pipelineRef.val = registry.serviceById(MiddlewarePipeline#.qname)
} catch (Err err) {
startupErr = err
throw err
}
}
@NoDoc
override Void onStop() {
registry.shutdown
log.info(BsLogMsgs.bedSheetWebMod_stopping(appName))
}
** Should HTTP requests be queued while BedSheet is starting up?
** It is handy in dev, because it prevents you from constantly refreshing your browser!
** But under heavy load in prod, the requests can quickly build up to 100s; so not such a good idea.
**
** Returns 'false' in prod, 'true' otherwise.
virtual Bool queueRequestsOnStartup() {
!iocEnv.isProd
}
private MiddlewarePipeline? middlewarePipeline() {
pipelineRef.val
}
private static WebRes webRes() {
try return Actor.locals["web.res"]
catch (NullErr e)
throw Err("No web request active in thread")
}
}