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()
|