Program and Hosting
Program.cs is the entire host of the HiNC web service: one class whose Main builds an ASP.NET
Core application and runs it. It presents no screen of its own — it is the process that starts and
stops HiAPI, registers every service the controllers and hubs resolve, maps those controllers and
the eight SignalR hubs, serves the built Quasar SPA out of the web root, and falls back to the SPA
shell for every URL the Vue router owns. BuildApp is public and returns the built application
without starting it, and RunApp runs one, so the build phase and the run phase are separable.
Main applies HINC_COREDUMP_FILTER on Linux when it is set, then calls BuildApp,
AnnounceWhenStarted and RunApp; another project can host the application through the same
members instead — see Hosting from another project.
HiAPI Lifetime
The host brackets its whole life in one pair of HiAPI calls.
AppBegin(API) runs once during BuildApp, after the
service container is built and before the request pipeline is configured. It logs in every licence
License holds, initialises the display engine (DispEngine), and
opens the SQLite step-cache database at the path it is given, publishing it as
SqliteStepStorage's default instance. The overload the webservice calls takes the
library's host configuration, LocalAppConfig, with the cache-database path
set in it, and a logger; the host passes a logger of category Hi.HiNcKits.LocalApp, so the
library's own lines — licence, OpenGL availability, step storage — are filed under its name rather
than the host's.
AppEnd(API) first closes every native machining pipeline still alive — a runner that was never disposed leaves its own behind — cancels the steps left in it and waits for its threads to finish before deleting it, so no pipeline thread is still inside the native core when the process exits. It then unwinds the same set in reverse: it waits for queued background CubeTree frees to drain so no native delete runs against a half-torn-down runtime, disposes the step and identity storages, shuts the display engine down, and logs the licences out.
AppEnd runs last, once nothing that calls into the native core is left, and a static latch
guarantees it runs at most once. Two paths reach it:
- The end of
RunApp, afterapp.Run()returns — the host has stopped serving and disposed every singleton (the display engines, the project service with its play and workpiece trees) — the ordinary graceful stop. The call sits in afinally, so aRunthat throws, such as one whose port is already bound, still reaches it. AppDomain.CurrentDomain.ProcessExit— the fallback for an exit that bypassesRunApp's tail, such as the licence watchdog'sEnvironment.Exit; a second caller waits for the first teardown to finish.
IHostApplicationLifetime.ApplicationStopping only closes the rendering intake
(RenderingService.BeginShutdown): it is the START of the stop, with clients still connected, so
tearing the core down there would leave later native calls to run into a torn-down core.
Console.CancelKeyPress (Ctrl+C, Ctrl+Break) cancels the immediate termination and asks the
lifetime for a graceful stop, so the first path runs. On Windows, closing the console window raises
none of these, ProcessExit included, so the host handles that signal itself: it asks for the same
graceful stop and waits for RunApp's tail, at most four seconds from the close — Windows ends the
process once the handler returns, and about five seconds after the close in any case.
The cache-database path is Cache/{CacheDbId}.db beneath the admin directory.
CacheDbId is the HiNC:CacheDbId configuration value when one is set and
the hosting environment name otherwise. Supplying a path matters:
SqliteStepStorage given none falls back to a single per-user file, so several
instances on one account would otherwise share it. Two instances launched under different
--environment names get different cache files with no further configuration.
Startup Order
Three registrations run before the builder exists, because each seeds a table that is read — or frozen — the moment anything else touches it:
LocalProjectService.Reg()fills XFactory's default generator table with every type the simulation pipeline may deserialise, which is what every project XML read resolves against.UserConfig.Reg()adds the per-user preference file's own type to the same table; without it, a persisted preference file cannot be read back and everyUserServiceresolution fails once one exists.- Lang is appended to StringLocalizer's extended type list, so session-command titles resolve through that assembly's satellite resources. The type list is static, but each localizer builds its own resource-manager list lazily on its first lookup and keeps it, so a type added after a given localizer has been used never reaches that localizer. Registering before the builder exists is what puts the addition ahead of every first lookup.
BuildApp then creates the builder — on ContentRootPath when that is set — and calls
ConfigureBuilder, when set, before it reads a setting or registers a service. Both hooks are unset
when the web service runs on its own; Hosting from another project
covers them.
After the container is built and before the pipeline is assembled, the host subscribes the
unhandled-exception and unobserved-task hooks, wires the native core's log output into the
application logger through CppLogUtil, sets the cache identifier, calls AppBegin,
and then runs
Seed(API) over the admin Resource root. The
seeder copies in only the marked .default items, leaves unmarked items alone as user property, and
skips the whole pass when its version stamp already matches the shipped resource assembly.
Registered Services
Every application service is registered as a singleton. The repository contains no AddScoped
and no AddTransient call, so no service this host registers has a per-request lifetime and any
state one of them holds is process-wide. Any shorter lifetime in the container is one of the
framework's own registrations rather than an application service.
Four registrations are worth reading closely:
UserServiceis built by a factory rather than by type, so that its configuration path is assigned unconditionally — including when no preference file exists yet, which is the only way the first save can create one. When the file does exist, the factory deserialises it through XFactory.ProxyConfigis bound by hand into a plain singleton from theProxyConfigconfiguration section. A separateservices.Configure<ProxyConfig>call also registers it through the options system, but nothing resolvesIOptions<ProxyConfig>; the plain singleton is what ProxyProjectService and the startup code receive. That binding happens once at startup and is not reloaded.- ProxyProjectService is registered twice — once under its own type and once as IProjectService — so the container builds one instance per registration. Both hold nothing but the same two injected singletons, LocalProjectService and ProxyConfig, which is where all the state lives.
AuthConfigis bound from theAuthsection and registered as an instance, so the authentication controller and the pipeline decision below read the same object. See Login and Authentication for what it switches on.
Six singletons are resolved eagerly at the end of BuildApp so they subscribe to their engine events
at startup rather than when the first client happens to connect: the CL-strip broadcast service, the
four per-sink message broadcast services, and the NC-program registry.
Kestrel is configured with AllowSynchronousIO = true, which permits handlers to write to the
response body synchronously.
Controllers
Controllers are added with AddControllers(), with the web service's own assembly registered as
an explicit application part, and reached by MapControllers() at the end of the pipeline. Every
controller in that assembly is attribute-routed, and every route template begins with api/ — most as
api/[controller], the rest as an explicit kebab-case path such as api/mech/machine-tool or
api/execution/cl-strip. No controller route sits outside api/, which is what makes the fallback
rules below safe: a GET under api/ that no route serves answers 404, and everything else is the SPA.
A project hosting the web service has its own
controllers served beside these, since MVC takes the entry assembly as an application part by
default; the rule holds for them only while their routes begin with api/ as well.
One controller is exempt from the login gate: the authentication controller carries
[AllowAnonymous] so it stays reachable when the fallback policy locks everything else.
SignalR Hubs
AddSignalR() is called without protocol configuration, and eight hub routes are mapped:
| Route | Hub type | What it carries |
|---|---|---|
/renderingHub |
RenderingHub |
server-rendered canvas frames and the input that drives them, one display engine per connection |
/shellMessageHub |
ShellMessageHub |
session-level routine and lifecycle messages |
/ncDiagnosticHub |
NcDiagnosticHub |
NC-pipeline diagnostics |
/stepDiagnosticHub |
StepDiagnosticHub |
step-anchored diagnostics |
/ncManipulationDiagnosticHub |
NcManipulationDiagnosticHub |
NC-manipulation diagnostics — writeback conversion and optimisation |
/executionStatusHub |
ExecutionStatusHub |
ExecutionStatusUpdated, SessionCursor and SessionStatusMessage, broadcast to all clients; the client-callable GetExecutionStatus() answers the caller alone, on the same ExecutionStatusUpdated name |
/clStripHub |
ClStripHub |
strip-chart display range, selected and entered step, and throttled data-update hints |
/cleanupHub |
CleanupHub |
index keys, and optional follow-up action keys, that a client registers against its connection with Add; 90 seconds after the connection drops, the sweep evicts the keys no live connection holds by then — see below |
The four message hubs share one base class and one contract — the client calls GetMessages(limit)
and the server answers on MessagesUpdate — but they are deliberately separate routes rather than
one hub with a discriminator, so a client subscribes to exactly the sink it wants and an idle panel
means an idle hub rather than a lost connection.
The cleanup hub keeps its registry outside the hub instance. SignalR builds a fresh hub instance
for every invocation, so the key dictionary Add writes to is a static registry bucketed by
connection id. The disconnect handler removes its own connection's bucket and hands it to a sweep
that runs 90 seconds later and evicts from IndexService every key in it that no live connection
holds by then and that no later disconnect has claimed (removing a registered action key from the
index too, and invoking its object when it is an Action); the grace exists because a reconnect
is a new connection id, and the client registers its keys again when it reconnects. The client does not rely on the hub alone: useCleanupHub.ts keeps its own set
of registered keys and posts each one to /api/Index/Remove before the component unmounts, then
stops its connection — so a key replaced while the page stays up, or a host whose hub never
connected, is still released.
JSON Serialisation
The controllers' serializer options carry two settings, both applied through AddJsonOptions.
JsonNumberHandling.AllowNamedFloatingPointLiterals. Several engine values use
double.PositiveInfinity as their “no limit” state — the optimizer's Max Feed Per Tooth and
Preferred Force among them — and the snapshots the SPA reads carry those doubles straight into the
response object. System.Text.Json refuses to write an infinite or NaN double as a JSON number, so
without this setting every such response would fault during serialisation instead of returning. With
it, those values travel as the JSON strings "Infinity", "-Infinity" and "NaN". The reading side
is not uniform: the numeric input widgets take all three spellings case-insensitively, while the
option-snapshot reader in the mission API recognises the two infinity spellings only and resolves
anything else non-numeric — "NaN" included — to the fallback its caller passed, which is positive
infinity for Max Feed Per Tooth and Preferred Force.
Numeric Input/Output covers the client half of that boundary, and
NC Optimization Option Panel (NC Optimization Config) the panel that leans hardest on it — including the two write
endpoints that take a string body so the same spelling survives the round trip.
JsonStringEnumConverter. Enums are read and written as their names rather than as integers,
which is what the API wrappers expect and what keeps the OpenAPI schema self-describing.
Both settings belong to the MVC controllers only. Hub payloads are serialised by SignalR's own protocol, which is left at its defaults, so neither the named floating-point literals nor the string enums extend to hub messages.
The Request Pipeline
The order the middleware is added in is load-bearing, and the host sets it explicitly rather than accepting the default arrangement:
- Forwarded headers first, honouring
X-Forwarded-ForandX-Forwarded-Protoso that every later stage sees the real client scheme. Both the known-proxy and the known-network lists are cleared, so those headers are accepted from any caller. Right after them, repeated leading slashes in the path are merged into one: a base URL that ends in a slash, joined to/api/..., asks for//api/..., which no route matches, so every later stage — the cross-site guard included — reads the path a route is matched against. - The cross-site guard, only while the login gate is off: a request its browser marks as coming
from any page but the service's own — another site, or another port of the same host — is
answered
403here — see Login and Authentication. - Swagger — the Swashbuckle document and its UI, both middleware.
MapOpenApibeside them registers the framework's own document as an endpoint instead, so that one is reached at step 7 with everything else that is routed. - Default files, then static files, serving the built SPA and everything else physically present
in the web root. When the web root is not the build output's own
wwwroot— adotnet runfrom the project folder — the host layers that output folder beneath it, so the doc site HiNc-Resource ships there (/HiAPI-docsite/, the menu bar's Help → Documentation) is served in both layouts; the web root wins on any path both hold. UseRouting()— placed here on purpose. An application that never calls it gets routing inserted at the front of the pipeline instead, which would select the SPA fallback endpoint before the static-file middleware ran; that middleware stands down once an endpoint is chosen, so a directory URL under the web root would be answered by the Vue router's not-found view rather than by the directory's own default document.- Authentication, then authorization. Both are no-ops when the login gate is off, because no scheme and no fallback policy are registered in that case.
- Endpoints — controllers, the eight hubs, the
/apifallback, and the two SPA fallbacks.
Ahead of the whole list, the framework's host filtering answers 400 to a request addressed to a
host name AllowedHosts does not admit; see Configuration for what it admits.
There is no CORS stage — see CORS — and no HTTPS-redirection stage, and no HSTS header is
sent: a redirect to HTTPS belongs to the deployment's proxy, and an instance that must serve HTTPS
only binds only an HTTPS endpoint.
A request answered before step 6 never reaches the authorization stage at all: the static-file
middleware and the Swagger middleware both sit ahead of it and short-circuit the request when they
match. The fallbacks are endpoints rather than middleware, so they do reach it. The two SPA
fallbacks are marked AllowAnonymous — which is what lets the SPA shell, carrying no data of its
own, load and show its own login view when the gate is on; the /api fallback is not, so with the
gate on an unsigned call to a route that does not exist answers 401.
The SPA Fallback
A GET or HEAD under api/ that no route serves does not reach the shell: the api/{**rest}
fallback answers it 404 with an application/problem+json body naming the method and the path.
Like the SPA fallbacks it takes GET and HEAD only, so any other method there still finds no
endpoint and answers 405. A fallback is chosen only when no route matches, and the literal api
segment ranks this one ahead of the bare fallback.
Two fallback registrations serve index.html:
util/file-explorer/{**location}— an explicit pattern with no file-name constraint.- The bare fallback, for everything else outside
api/.
The bare form's implicit route pattern carries a nonfile constraint that rejects any URL whose last
segment contains a dot. File Explorer deep links mirror real folder and file names into the URL, and
those names routinely contain dots, so reloading or pasting such a location needs the explicit
pattern. Genuine static assets cannot be shadowed by it, because the static-file middleware has
already run by the time either fallback is reached.
Serving the SPA
The Quasar build writes its output directly into the host's web root — the front end's build
configuration sets its distribution directory to the web service's wwwroot — so there is no copy
step between building the SPA and serving it. The router runs in history mode, which is what makes
the SPA fallback necessary: every client-side route is an extension-less URL the server has no
endpoint for.
The front-end build script runs the i18n census, the glossary lint and the engine-message lint before invoking the Quasar build, so a build that fails any of those checks produces no new bundle.
The menu bar's version mark, the Help menu's items after the Log Viewer, the menu bar's cloud licence
badge and the last boot file come from the module the build-time product alias resolves to: the web
service's own, unless the build names another — see
Hosting from another project.
For front-end work the Quasar dev server runs on its own port and proxies the routes the backend owns
to the running service. That proxy list holds /api, /swagger, /HiAPI-docsite, /renderingHub,
/executionStatusHub, /clStripHub, /cleanupHub, and /sessionMessageHub — a path the host maps
no hub on. The four per-sink message hubs are absent from it, so the Session Messages panels receive
no live messages through the dev server.
OpenAPI and Swagger
A single private constant holds the REST contract version. It is simultaneously the Swashbuckle document group name — so it appears in the document's route — and the version the UI displays, which is why it is defined in one place. It is raised by hand on a breaking REST change only, and is deliberately not tied to the assembly version or to the project-file format version.
Both OpenAPI surfaces are exposed in every environment rather than behind a development gate, so an
automated caller always has a machine-readable contract. The Swashbuckle UI is served at /swagger
and its document at /swagger/{version}/swagger.json — v3.1 at present, so the document is
/swagger/v3.1/swagger.json, and the UI always opens the current one. The framework's own document
is published at /openapi/v1.json.
Schema ids differ between the two documents. The Swashbuckle document keys them on the full type name
with the nested-type separator normalised; without that, two controllers' nested request types that
share a simple name collide and its generator throws — which surfaces as an HTTP 500 from the document
route rather than as a startup failure. The framework document is registered with no such option: it
names schemas by simple type name and merges same-named types without an error, so a route whose
request type shares a name with another controller's advertises the other body. The machine-tool,
spindle-capability, workpiece-material and cutting-parameter loaders all show the object-management
load body there, and the file rename shares its schema name with the equipment topology's
{ name } rename body, so one of those two routes advertises the other's. The Swashbuckle
document is the one to read request shapes from;
Driving the Web Service over HTTP lists the true
bodies of the routes the framework document gets wrong.
The Swagger UI and its document are middleware ahead of authorization and answer without a session. The framework document is a routed endpoint and sits behind the login gate like any controller.
XML documentation comments are folded in, each file from the application's base directory: the web
service's own assembly's, the entry assembly's when that is another one — a project hosting the web
service — and those of every assembly the web service references whose name starts with Hi. Every
read is skipped when its file is missing, and each referenced-assembly read carries a catch-all of its
own on top of that, so one whose file is present but unreadable is ignored; the first two reads have
no such catch-all.
CORS
The host registers no CORS policy, so no answer carries an Access-Control-Allow-Origin header and
a browser lets no page from another origin read one. Nothing needs it: the SPA is served from the
same origin as the API and the hubs, and the Quasar dev server reaches the backend through its own
proxy. CORS governs reading, not sending — a page elsewhere can still send the requests a browser
sends without a preflight, and SignalR's WebSocket handshake is not subject to CORS at all — so it is
no defence on its own: the cross-site guard at step 2 refuses those requests while the login gate is
off, and while it is on the Lax cookie stays off a cross-site page's requests.
Logging
The default providers are cleared and two are added: a console provider, and DailyFileLoggerProvider,
which writes one file per instance and per day, log-{environment}-{yyyy-MM-dd}.txt, in the directory
Logging:File:Directory names — logs by default, a relative path being taken from the process's
current working directory — and deletes this instance's files older than Logging:File:RetainDays,
30 by default. The same provider instance is registered as a singleton, which is how the log endpoints
read today's file through it. It swallows every I/O failure, on the principle that logging must not
fault a request or a startup; Log Viewer Page covers its naming, its fallbacks and its writer.
The two providers are filtered independently. The file provider's alias is File, so a
Logging:File:LogLevel section configures it alone, while the console follows the provider-neutral
Logging:LogLevel section, which the shipped configuration sets to Warning. The host seeds five file
rules in code — Information by default; Warning for Microsoft and System; Information for
Microsoft.Hosting.Lifetime, so the host's start lines (listening address, environment, content root)
reach the file; and Information for Microsoft.AspNetCore.HostFiltering, so a request that host
filtering refuses with a bare 400 leaves the refused host name there — and it inserts them
ahead of the rules built from configuration rather than adding them after. On a tie the later rule
wins, so any configured Logging:File:LogLevel key overrides its seeded default, while a deployment
whose configuration has no File section still gets the seeded levels rather than the console's. The
shipped appsettings.json spells the first four of those levels out, and the Development overlay adds
None for the development-time BrowserLink and browser-refresh middleware. The environment-variable
provider reaches the same keys: Logging__File__LogLevel__Default=Warning leaves only warnings and
worse in the file, apart from the host's Microsoft.Hosting.Lifetime lines and the host-filtering
refusals, which their own, more specific Information rules still admit (set
Logging__File__LogLevel__Microsoft.Hosting.Lifetime=Warning and
Logging__File__LogLevel__Microsoft.AspNetCore.HostFiltering=Warning as well to drop them).
RunApp writes a startup summary line just before it runs the application — the instance
(environment name), the build, the REST API version, the working directory, the admin directory and
whether the login gate is on — so the file for the day the service starts has content before the first
request arrives. That guarantee stops at midnight: the writer opens the file named for each entry's
date and creates nothing ahead of time, so a host left running into a new day has no file for it until
the next entry is written.
Three more sources feed the log from the host itself. AppDomain.CurrentDomain.UnhandledException is
logged Critical and TaskScheduler.UnobservedTaskException Error, so a crash on a background thread or
a faulted task nobody awaited leaves a line. The native core's log function is routed into a logger of
category Hi.Native, at a level read off each line's wording: a licence failure is Critical; a refused
NC composition, no usable GL, a shader that will not build, an EGL display that will not open or
initialise, and a GL debug message of high severity or error type are Error; Mesa software
rasterisation, the picking-table ceiling, medium-severity GL messages, a line asked wider than the
driver draws and a drawing skipped for its primitive mode are Warning; the OpenGL context record, the
line width range and frame statistics stay Information. And AppBegin's logger, above, carries the
library's licence abort line at Critical.
The host never disposes a provider registered as an instance, so the shutdown path handles this one
itself: it flushes the queue just before AppEnd, so a crash inside the native teardown still leaves
the line announcing it, and shuts the writer down after AppEnd — in RunApp's exit path and in the
ProcessExit handler alike — after which lines are written synchronously.
Configuration
Configuration is the framework's default chain, its files read from the content root:
appsettings.json, then appsettings.{Environment}.json, then {ApplicationName}.settings.json and
{ApplicationName}.settings.{Environment}.json — named after the entry assembly; the web service
ships no such file — then environment variables, then command-line arguments. A project hosting the
web service can add sources of its own; see
Hosting from another project. The keys this host reads are:
| Key | What it controls |
|---|---|
Logging:LogLevel |
the console provider's levels |
Logging:File:LogLevel |
the log file's levels, over the defaults the host seeds in code |
Logging:File:Directory |
the log file's directory; logs under the working directory by default |
Logging:File:RetainDays |
how many days of this instance's log files are kept; 30 by default, 0 keeps all |
AllowedHosts |
host filtering; absent from the shipped file. While the login gate is off and the key is not set, the host admits only the loopback names localhost, 127.0.0.1 and [::1]; with the gate on, or once the key is set, the framework's own rule applies — the configured names, or any name when none is configured |
Kestrel:Endpoints |
the addresses the server binds |
ProxyConfig:AdminDirectory |
the admin working root, under which Resource and Cache live |
Auth |
the optional login gate, and the name of its cookie, Auth:CookieName; off as shipped, on in the Development overlay |
HiNC:CacheDbId |
an explicit step-cache database id, overriding the environment name |
HiNC:LicenseCapsForTest |
<N>,<stepCap>: tightens the two licence caps once at boot — the resolution cap ratio and the step cap, a 0 leaving that cap as it is — so an acceptance instance runs under a cap. Tighten-only: it never loosens what the licence source set, and no endpoint sets a cap at runtime. The start-up log names the caps in force, at Warning when the switch took and at Error when it did not |
The Development overlay repeats the logging and admin-directory sections — its logging section adding
two Logging:File:LogLevel keys of its own — and turns the login gate on with one credential entry,
where the shipped file leaves it off. Because that entry is public, it also sets AllowedHosts to the
three loopback names, which the gate-on default would otherwise open to any name. It declares no
Kestrel section, so the listening address is the same under either environment.
Kestrel Endpoints Win
Kestrel:Endpoints is the authoritative source of the listening address. When it declares endpoints,
Kestrel binds those and ignores addresses supplied any other way — ASPNETCORE_URLS, --urls,
and the launch profile's applicationUrl alike — logging that it is overriding them. The shipped
configuration declares one HTTP endpoint on loopback port 5000. The http launch profile names the
same address, so the override is invisible there; the https profile also names an HTTPS address on
a second port, and that address is one of the ones Kestrel discards, so launching under it still
yields plain HTTP on 5000 and nothing else.
To move a local instance to another port, set the Kestrel__Endpoints__Http__Url environment
variable, which the environment-variable provider maps onto the same key. Setting ASPNETCORE_URLS
alone changes nothing.
The endpoint binds localhost, so the shipped configuration answers only on the machine it runs on.
Reaching it from elsewhere means either overriding that key or placing a reverse proxy in front,
which is the arrangement the forwarded-headers configuration handles. Either way the login gate, off
as shipped, has to be turned on. Until it is, the loopback-only host list answers 400 to a widened
endpoint's callers from elsewhere, while a proxy on the same machine that forwards to localhost can
pass every caller through with no session; turning the gate on lifts the host restriction unless the
deployment sets AllowedHosts. A proxy has to mount the service at / — the SPA addresses the API,
the hubs and its own assets from the root — and pass WebSocket upgrades on. An HTTPS endpoint that
Kestrel serves itself needs its Certificate configured: without one, Kestrel falls back to the
ASP.NET Core development certificate, which only a development machine holds, and otherwise cannot
bind the endpoint, ending the process at start-up.
Environment and Working Directory
The environment name reaches further here than usual. Both launch profiles set it to Development,
which selects the Development overlay, names the instance's log file, and —
unless HiNC:CacheDbId overrides it — names the step-cache database file.
Two paths follow the process's current working directory rather than the content root: the file
logger's directory, while Logging:File:Directory is relative or left at its logs default, and the
per-user preference file. An instance launched from the project directory finds both where they are
expected; one launched from elsewhere puts them beside wherever it was started. The web service never
moves its working directory itself. A project hosting it that keeps them in a folder of its own moves
the working directory before calling BuildApp, and sets ContentRootPath so that the settings files
and wwwroot are still read from the program folder. The installed HiNC keeps both in a data folder
of its own; see Installing HiNC on Windows.
Hosting from another project
Another project can host the web service instead of running its Main. It references the web service
project, and its own Main sets the hooks it needs, then takes the steps this Main takes from
BuildApp on. The class is HiNC_2025_webservice.Program; a hosting project whose own entry class
is also named Program refers to it by that full name or through a using alias.
The two hooks are static properties, and both default to null, which is how the web service runs
on its own:
ContentRootPath— the folderappsettings*.jsonandwwwrootare read from.nullleaves the framework's default: the working directory, or--contentRooton the command line. A value set here wins over--contentRoot, and the content root cannot change once the builder exists.ConfigureBuilder— called with theWebApplicationBuilderright afterBuildAppcreates it, beforeBuildAppreads any setting or registers any service: the place for the hosting project's own configuration sources and services. A source added there comes after the command line, so it wins over every default source unless the environment variables and the command line are added again after it. A logging provider added there does not survive:BuildAppclears the providers right after the call and adds its console and file loggers.
Then, in order:
Program.BuildApp(args)builds the application without starting it. What needs only the built application — middleware, a project to load, lifetime callbacks — goes between this call and the last; middleware added to it runs after step 6 of the request pipeline, ahead of the endpoints.- Optionally,
Program.AnnounceWhenStarted(app, version, settingsFile): once the server listens, it prints the given version and settings file, the addresses and the working directory to the console, with a line saying that closing the window stops HiNC, and on Windows puts the address in the console window's title. Under systemd — whereINVOCATION_IDis set — it leaves out the addresses and that line, since the output goes to the journal, nobody opens the site at its local address there, and there is no window to close. It opens no browser. The web service's ownMainpasses the HiNc assembly version andappsettings.json— or, where the content root has none,appsettings.{Environment}.json. Program.RunApp(app), rather thanapp.Run(): it writes the startup summary line, runs, then ends the native core and closes the log file. Without it the core is ended only when the process exits normally, which an exception out ofRunskips, leaving the licence seat taken.
The hosting project's own controllers are served beside the web service's, because MVC takes the
entry assembly as an application part. BuildApp also reads the entry assembly's XML file, so their
documentation shows in Swagger when the project builds with GenerateDocumentationFile. A
settings file named after the entry assembly, {ApplicationName}.settings.json in the content root,
joins the configuration chain after the web service's appsettings files with no
code at all. ExecutionStatusService.StatusRunning and ExecutionStatusService.StatusPaused are
public constants: a hosting project that must not act while a run is in progress or paused compares
against them, so a rename breaks its build rather than its check.
The front end's counterpart is the build-time alias product. It resolves to the web service's own
module — the menu bar's version mark, the items the Help menu appends after the Log Viewer (none), the
menu bar's cloud licence badge (none) and a last boot file (empty) — unless the SPA is built with
HINC_PRODUCT_SPA set to the absolute path of a folder with the same exports, VersionBadge,
HelpMenuItems and CloudStatusBadge; see Web Service SPA Source Tree.
The installed HiNC on Windows is the web service hosted this way: its host adds the installation's data folder, its self-update and a second licence source — a cloud account, signed in on a page of its own before the service starts, in place of the Sentinel key the web service uses on its own — see Installing HiNC on Windows, Updating HiNC on Windows and Signing In with a Cloud Account.
Source Code Path
See HiNC App Anatomy for git repository links.
Web Application
HiNC-2025-webservice (Quasar CLI SPA):
Program.cs— the whole subject of this page:Main, theContentRootPathandConfigureBuilderhooks,AnnounceWhenStartedandRunApp, the pre-builder registrations, every service registration, the JSON and Swagger options with the entry assembly's XML read, the host-filtering default that follows the login gate, the pipeline order with the build output'swwwrootlayered under the web root, the eightMapHubcalls, the/apifallback and the two SPA fallbacks, theAppBegin/AppEndpair and the four shutdown triggers; also the file log provider's creation, its seeded default rules, the startup summary line, the two exception hooks, theHi.Nativelevel mapping and the flush and shutdown around the native teardown.appsettings.json— the shipped listening address, the admin directory, the login section (off, no account) and theLogging:Filesection; noAllowedHostskey.appsettings.Development.json— the Development overlay, which declares noKestrelsection, its twoLogging:File:LogLevelkeys, and the login section that turns the gate on with one account.Properties/launchSettings.json— the two launch profiles whoseapplicationUrltheKestrelsection overrides.Common/DailyFileLoggerProvider.cs— the per-instance, per-day file logger under theFilealias, which the log endpoints read back through.Common/AuthConfig.cs— the shape bound from theAuthsection.Common/AuthController.cs— the anonymous carve-out that keeps login reachable under the fallback policy.Common/CrossSiteRequestGuard.cs— pipeline step 2, added only while the login gate is off.Common/NamedRootResolver.cs— resolves the admin, project and resource roots the seeder and the file endpoints work against.Common/CleanupHub.cs— the cleanup hub, and the per-connection registry whose buckets its disconnect handler hands to the deferred sweep.Common/IndexController.cs— the index remove endpoint the client calls before unmount.Disp/RenderingHub.cs— the canvas hub.Execution/SessionSinkHub.cs— the shared base and the four per-sink message hubs.Execution/ExecutionStatusHub.cs— the status, cursor and session-message hub.Execution/ClStripHub.cs— the strip-chart hub.Execution/ExecutionStatusService.cs— the publicStatusRunningandStatusPausedconstants a hosting project compares a run's state against.Environments/UserConfig.cs— the preference type registered before the builder is created.Environments/ProjectController.cs— the log endpoints that read today's file through the provider.wwwroot-src/src/composables/useCleanupHub.ts— the client's own key set and the index-remove calls that release it.wwwroot-src/src/api/mission.ts— the option-snapshot reader that maps the named-literal strings back to numbers, and the string-bodied setters that send them.wwwroot-src/quasar.config.ts— the build output directory that puts the bundle in the web root, the history router mode, theproductalias with itsHINC_PRODUCT_SPAoverride, and the dev-server proxy list.wwwroot-src/src/product/index.ts— the product module's exports, each the web service's own choice when it runs on its own.wwwroot-src/package.json— the build script that runs the i18n lint before the Quasar build.
HiAPI Engine
HiNc/HiNcKits/LocalApp.cs—AppBeginandAppEnd: licence log-in and log-out, display-engine start and finish, the step-storage open and dispose, and the close of the machining pipelines left alive thatAppEndruns first; also the licence abort line it logs at Critical on the logger it is given.HiMech/MachiningProcs/MachiningParallelProc.cs— the native machining pipeline a runner plays through, and the close, drain and delete of every pipeline still alive thatAppEndcalls.HiNc/HiNcKits/HiNcHost.cs— the cache-database identifier the host assigns at startup.HiNc/HiNcKits/ProxyConfig.cs— the admin-directory setting and its own default.HiNc/HiNcKits/ResourceSeeder.cs— the marked-defaults seeding pass and its version stamp.HiNc/SqliteUtils/SqliteStepStorage.cs— the step cache, and the per-user default path that makes an explicit per-instance path necessary.HiNc/MachiningProcs/ProxyProjectService.cs— the project service registered under two service types, holding only the singletons it is given.HiGeom/Common/StringLocalizer.cs— the static extended type list, and the per-instance resource-manager list each localizer builds on its own first lookup.
See Also
- Session State — what survives a project change inside the singletons this host registers, and what is rebuilt
- Login and Authentication — the
Authsection this host binds, and the cookie scheme and fallback policy it registers when the gate is on - Rendering Canvas on Web Service Application — the canvas hub this host maps, and the per-connection display engine behind it
- Log Viewer Page — the page that reads the file this host's log provider writes, and what that file holds
- Main Panel — the menu bar whose version mark, Help items after the Log Viewer and cloud licence badge come from the product module, which a hosting project can replace with its own
- Driving the Web Service over HTTP — the contract this host publishes, read from a script's side: sign-in, the reply envelope and the known schema errors
- Installing HiNC on Windows — the installed HiNC, this web service hosted with the installation's data folder and self-update, from the operator's side