summaryrefslogtreecommitdiff
path: root/src/SomhairlesDream.Server/Program.fs
blob: 53cfef685c42aa8938ed8837db1157e3edc73ba9 (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
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.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
app.UseStaticFiles() |> 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()