Nelze vybrat více než 25 témat Téma musí začínat písmenem nebo číslem, může obsahovat pomlčky („-“) a může být dlouhé až 35 znaků.

12KB

WSC-MVC — Architecture (as implemented through M3)

Request flow

GET / or GET /hello
  -> IIS URL Rewrite rule "WscMvc-Root" (^$) or "WscMvc-Hello" (^hello/?$)
  -> /Default.asp?route=/hello
  -> Server.CreateObject("WscMvc.RequestContext"); ctx.Initialize path, httpMethod, logDir
  -> Server.CreateObject("WscMvc.Application")
  -> Application.Run(ctx, "production", statusLine, contentType, body, allowHeader) [Framework/Application.wsc]
       -> CreateObject("WscMvc.Router")
       -> Router.Match(ctx, "production", ..., handlerKey)          [Framework/Router.wsc]
       -> CreateObject("WscMvc.HomeController")
       -> HomeController.Hello(body)                             [Controllers/HomeController.wsc]
       -> LogOutcome ctx, statusLine  (best-effort append to logs/app.log)
  -> Default.asp sets Response.Status/ContentType, optional Allow header, writes body

Component boundary contract

  • Default.asp is the only file that touches ASP intrinsic objects (Request, Response, Server). It contains no business logic — only reading the route query parameter and REQUEST_METHOD, resolving logs/'s physical path via Server.MapPath, invoking WscMvc.RequestContext and WscMvc.Application, and writing the response.
  • Framework/RequestContext.wsc, Framework/Router.wsc, Framework/Application.wsc, Controllers/HomeController.wsc, and test-app/Controllers/SelfTestController.wsc never reference Request/Response/Server/Session. All data crosses the ASP-to-WSC and WSC-to-WSC boundaries as either VBScript scalars (strings) or our own RequestContext COM object (not an ASP intrinsic) — never an ASP host object. See docs/DECISIONS.md for why the original SPEC §15 open question about passing ASP intrinsics into a WSC never needed a direct experiment: the architecture never crosses that boundary by design.
  • WSC public members are exposed as plain <method> entries backed by ordinary Sub/Function procedures, never <property>/Property Get. VBScript's Property Get/Let/Set requires a Class...End Class block and cannot appear at a WSC's top-level script scope — confirmed experimentally (see docs/DECISIONS.md), not assumed from general WSC documentation.
  • Routing is an explicit literal allowlist in Framework/Router.wsc. Every call includes a fixed application name (production or tests) supplied by that app's own bootstrap; a route must match both the application and normalized path. Literal routes are checked directly before any future parameterized route support. Router decisions return either a hardcoded handler key for Application.wsc to dispatch with explicit Select Case branches, or a completed expected response (400, 404, 405 with Allow). No URL segment is ever treated as a ProgID, method name, template name, or filesystem path.
  • Route path handling is deliberately conservative in M3: ASP/IIS has already decoded the route query parameter once, so Router.wsc does not decode again. It rejects remaining % escapes, backslashes, query/fragment markers, doubled slashes, dot-segments, and control characters, then lowercases and trims one trailing slash for literal matching. The original path remains available through RequestContext.Path for logging.
  • Application.Run is the single central point that decides expected (404, ordinary control flow) vs. unexpected (COM/method failure, 500) outcomes, and the single point that logs every outcome. Default.asp still independently guards its own three sequential calls (RequestContext creation, Initialize, Application creation, Run) since a WSC failing to even instantiate happens outside Application.Run's reach.

Per-request lifetime and diagnostics

  • RequestContext and Application are both created fresh per request via Server.CreateObject in Default.asp and dropped (Set ... = Nothing) at the end of the page; neither is ever cached in ASP Session/Application scope, per SPEC §10.
  • RequestContext.Initialize generates a correlation id from Fix(Timer) plus Scripting.FileSystemObject.GetTempName() — not Randomize/Rnd(), which was tried first and shown experimentally to collide when two contexts are created within the same clock tick (see docs/DECISIONS.md).
  • Application.LogOutcome appends one line per request (timestamp, correlation id, method, path, status line, elapsed ms) to logs/app.log, guarded end-to-end by On Error Resume Next so a logging failure can never affect the HTTP response. Concurrent writers are serialized with a manual lock-file mutex (logs/app.log.lock, via CreateTextFile(..., OverwriteExisting:=False)) since no ASP intrinsic locking primitive (Application.Lock) is available to code that must not reference ASP intrinsics. The retry budget is deliberately short (see docs/DECISIONS.md): logging is explicitly best-effort and may drop lines under heavy concurrency without affecting correctness of the response.

Test harness: GET /self-test — its own app, only Framework/ is shared

test-app/Controllers/SelfTestController.wsc (WscMvc.SelfTestController) exposes framework checks over plain HTTP and returns JSON — no PowerShell, cscript, or SSH access to the VM required. Both the test site's root (GET /) and GET /self-test rewrite to the test app's /self-test route. The route table allows both GET and POST for /self-test. Production similarly maps both GET / and GET /hello to /hello, which is GET only.

This lives on its own IIS site (test-app/), not the production site. Changed 2026-09-19 at Daniel's explicit direction: a diagnostics endpoint permanently reachable on the same site/port as real traffic was the wrong shape. test-app/public/ is a second, separate IIS site's physical path (own site name, own app pool, own port, own logs/), whose web.config routes /self-test and nothing else. The production site's web.config no longer has a /self-test rewrite rule at all — GET /self-test against the production site now returns a plain 404 (nothing rewrites that path to Default.asp).

Only Framework/ is genuinely shared between the two sites — Controllers/ is not. Application.wsc/RequestContext.wsc in Framework/ are the reusable dispatch/context engine, registered once globally via COM and used by both sites. Controllers/ holds application-specific business logic: production owns Controllers/HomeController.wsc, while the test app owns test-app/Controllers/SelfTestController.wsc. COM registration for a .wsc records its exact file path, so registration tooling points each controller at its app-owned directory.

Each site also owns its thin Default.asp. The files differ only in the fixed application name passed to the shared Application.Run: production passes "production"; the test app passes "tests". That selector is part of the route match, so neither ordinary URL Rewrite nor a direct Default.asp?route=... request can activate a controller belonging to the other app. SelfTestController no longer invokes /hello or HomeController; its framework checks use the tests route set and explicitly verify that /hello is rejected.

The HTTP status is always 200 if the self-test mechanism itself ran (a broken SelfTestController/Application still degrades to the normal 500 path via the same central error handling); the JSON body's top-level "ok" field and each check's "pass" field carry the actual test results — conventional health-check design (5xx is reserved for the diagnostics mechanism being broken, not for a failed assertion inside it).

curl http://100.127.62.31:8091/self-test
{"ok":true,"checks":[{"name":"request_context_contract","pass":true,"detail":""}, ...]}

tests/run-self-test.sh wraps this in curl + python3 -m json.tool, runnable from any CLI — point it at the test-app site's URL. It also probes direct Default.asp?route=/hello and requires a 404. tests/Test-Http.ps1 tests production and, when -TestBaseUrl is supplied, verifies direct-query isolation in both directions.

SelfTestController's error details intentionally include raw Err.Description text — unlike every other route, which must never leak internals to an arbitrary caller, this route's entire purpose is diagnostics. It is a dev/test-milestone tool; reconsider whether the test-app site should be gated or removed before any production deployment during the M6 hardening pass.

COM identity

Component ProgID CLSID
Framework/RequestContext.wsc WscMvc.RequestContext {1C36FA55-34DF-4974-94B9-D657389362B2}
Framework/Router.wsc WscMvc.Router {C92F9338-B478-4EAD-B865-892FFB1E1C51}
Framework/Application.wsc WscMvc.Application {851C7763-1638-42FE-A166-BF3DD3A96A88}
Controllers/HomeController.wsc WscMvc.HomeController {87488446-60BE-4068-8368-0B709BB68F3F}
test-app/Controllers/SelfTestController.wsc WscMvc.SelfTestController {D2634944-4646-4C55-956E-4C05E7E10904}

CLSIDs are fixed at creation and must never be recycled for a different component (AGENTS.md). Application's public Run signature changed during pre-release development (M2 added a leading ctx parameter; M3 added an allowHeader output parameter) under the same CLSID; acceptable because this is active pre-release (v0.1) development with exactly one caller shape (Default.asp, updated in lockstep) — not a claim that live interface changes are safe for a published/external client.

IIS sites (test host: win2025test, 100.127.62.31)

Two separate IIS sites now, both set up/reconciled by the same tools/Setup-Site.ps1 (parameterized by name/pool/path/port; idempotent, safe to re-run):

Production Test app
Site name WscMvc WscMvcTests
Port *:8090 *:8091
Physical path C:\Projects\wsc-mvc\public C:\Projects\wsc-mvc\test-app\public
App pool WscMvc WscMvcTests
Routes /hello /self-test
Logs C:\Projects\wsc-mvc\logs\app.log C:\Projects\wsc-mvc\test-app\logs\app.log

Both are 64-bit, no managed code, anonymous auth identity IUSR. Each site's public/ contains only Default.asp and web.config; every other directory (Framework/, Controllers/, test-app/Controllers/, tests/, tools/, docs/, both logs/ folders) is a sibling of whichever public/ is actually served, outside both sites’ physical paths entirely — see the “only Framework/ is shared” note above for which of those directories are truly cross-site versus app-specific. Changed 2026-09-19 at Daniel's explicit direction (see docs/DECISIONS.md); previously there was one site with the whole project directory as its root and those folders hidden via hiddenSegments, and /self-test was a route on that same site.

  • Each site's web.config keeps only a .wsc/.vbs/.ps1/.md extension denylist as defense-in-depth (in case a stray file ever lands directly in its public/); no hiddenSegments are needed since the directories they used to hide no longer exist under either served root at all.
  • enableParentPaths=true is set for both sites (scoped via appcmd ... /commit:apphost, a separate <location> block per site in applicationHost.config — not a machine-wide unlock of the locked system.webServer/asp section) so each site's Default.asp can Server.MapPath("../logs") to reach its own logs/. This setting affects only server-side script MapPath/#include resolution, not client-supplied URL paths — IIS's own URL normalization independently rejects ..-traversal in an incoming request regardless of this setting; verified with a direct request attempt, see docs/DECISIONS.md.
  • Each site's logs/ has an explicit, scoped icacls ... /grant "IIS_IUSRS:(OI)(CI)M" (now automated by tools/Setup-Site.ps1, previously a manual one-off command) so classic ASP (impersonating IUSR for anonymous requests) can write app.log. No other project directory grants IUSR/IIS_IUSRS write access — verified with a recursive icacls /T scan, see docs/DECISIONS.md.
  • Default document is Default.asp on both sites; each site's one route is served via a rewrite rule, not the default document.

Deferred to later milestones (do not implement early)

Per SPEC §3 non-goals and IMPLEMENTATION_PLAN M4+: HTML views/templates, ADODB, auth. A more robust logging mechanism (if complete coverage under concurrent load is ever required) is deferred to M6 — see the concurrency finding in docs/DECISIONS.md.

Powered by TurnKey Linux.