summaryrefslogtreecommitdiff
path: root/src/SomhairlesDream.Server/Program.fs
blob: b2ea8a86f602e7304279e3357444da7d42af65f7 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
open System
open System.IO
open System.Text.Json
open System.Threading
open System.Threading.Tasks
open Microsoft.AspNetCore.Builder
open Microsoft.AspNetCore.Hosting
open Microsoft.AspNetCore.Http
open Microsoft.AspNetCore.StaticFiles
open Microsoft.Extensions.FileProviders
open Microsoft.Extensions.Hosting
open SomhairlesDream.Server
open SomhairlesDream.Shared

let builder = WebApplication.CreateBuilder(Environment.GetCommandLineArgs() |> Array.skip 1)
let publicRoot = Path.Combine(AppContext.BaseDirectory, "public")
builder.Environment.WebRootPath <- publicRoot
builder.Environment.WebRootFileProvider <- new PhysicalFileProvider(publicRoot)

let app = builder.Build()
let store = RunStore("heritage-001", "run-001", TimeSpan.FromMinutes 2.)
let jsonOptions = JsonSerializerOptions(JsonSerializerDefaults.Web)

let environmentOrDefault name fallback =
    match Environment.GetEnvironmentVariable(name) with
    | value when not (String.IsNullOrWhiteSpace(value)) -> value
    | _ -> fallback

let artifactRoot = environmentOrDefault "SOMHAIRLES_ARTIFACT_ROOT" (Path.Combine(Environment.CurrentDirectory, ".artifacts"))
let stageRoot = environmentOrDefault "SOMHAIRLES_STAGE_ROOT" (Path.Combine(Environment.CurrentDirectory, "stage"))
let python = environmentOrDefault "SOMHAIRLES_BPYTHON" "python3"
let artifactClock () = DateTimeOffset.UtcNow
let coordinator =
    ArtifactRunCoordinator(
        artifactRoot,
        (fun () -> SomhairlesDream.Modeling.BlenderProcessBridge(python, stageRoot, TimeSpan.FromSeconds 120.) :> SomhairlesDream.Modeling.IArtifactBridge),
        artifactClock,
        TimeSpan.FromMinutes 2.
    )

let json value = Results.Json(value, jsonOptions)

let events (context: HttpContext) : Task =
    (task {
        context.Response.ContentType <- "text/event-stream"
        context.Response.Headers.CacheControl <- "no-cache"

        let reader = store.Subscribe()

        try
            while not context.RequestAborted.IsCancellationRequested do
                let! snapshot = reader.ReadAsync(context.RequestAborted).AsTask()
                let payload = JsonSerializer.Serialize(snapshot, jsonOptions)
                do! context.Response.WriteAsync($"data: {payload}\n\n", context.RequestAborted)
                do! context.Response.Body.FlushAsync(context.RequestAborted)
        with
        | :? OperationCanceledException -> ()
    } :> Task)

app.UseDefaultFiles() |> ignore

// Serve geometry and decoder binaries with correct MIME types: ASP.NET's
// default map has no entry for .glb/.wasm, so those requests would 404.
let staticFileOptions =
    let provider = FileExtensionContentTypeProvider()
    provider.Mappings.[".glb"] <- "model/gltf-binary"
    provider.Mappings.[".gltf"] <- "model/gltf+json"
    provider.Mappings.[".wasm"] <- "application/wasm"
    StaticFileOptions(ContentTypeProvider = provider)

app.UseStaticFiles(staticFileOptions) |> ignore

app.MapGet("/health", Func<IResult>(fun () -> Results.Ok({| status = "ok" |}))) |> ignore

ArtifactRunApi.register
    app
    { Coordinator = coordinator
      Clock = artifactClock
      JsonOptions = jsonOptions }

let observeRuns =
    Task.Run(
        Func<Task>(fun () ->
            task {
                try
                    while not app.Lifetime.ApplicationStopping.IsCancellationRequested do
                        coordinator.ObserveAll() |> ignore
                        do! Task.Delay(TimeSpan.FromSeconds 1., app.Lifetime.ApplicationStopping)
                with
                | :? OperationCanceledException -> ()
            } :> Task)
    )

app.MapGet(
    "/api/state",
    Func<IResult>(fun () -> store.Observe(DateTimeOffset.UtcNow) |> json)
)
|> ignore

app.MapPost(
    "/api/runs/heartbeat",
    Func<IResult>(fun () -> store.Heartbeat(DateTimeOffset.UtcNow) |> json)
)
|> ignore

app.MapGet("/api/events", Func<HttpContext, Task>(events)) |> ignore

app.Run()