site preview: serve any generator, leave nothing running, and start a preview in a VM #236

Closed
allod-agent wants to merge 5 commits from agent/site-preview-generators into master
Member

Owner summary

  • What changes on a running machine: allod site preview works for a site that is not built with zola. A site whose flake has an app named preview runs that app; a site with config.toml runs zola as before; any other site is built and served as static files by allod itself.
  • Also changes: stopping the preview, by an interrupt, a terminal closing, or allod being killed, takes the server down with it and frees the port. allod site preview --vm <name>, run on the machine hosting the VM, starts the preview in the VM and forwards the ports over its own connection.
  • What stays identical: the command zola is started with, for every existing flag. deploy, check and config are untouched.
  • Three small differences a zola site can notice: a page port already in use is refused by allod before zola starts; --port must be a number; Ctrl-Z does nothing while a preview runs.
  • What proves it: the Go tests pass under all four build variants, including tests that start real processes, signal or kill allod, and check that nothing is left and the port is free, and one that types Ctrl-Z and Ctrl-C through a real terminal.
  • Not proven: --vm has never been run against a VM. Only the two ssh command lines are tested. The manual tests in the issue are still to do, from a hypervisor: page loads, an edit reloads the browser, an interrupt frees the ports on both machines, and the same with the connection dropped.
  • If it is wrong: revert this PR. Nothing here writes state; the worst outcome is a leftover server process holding a port, which ends when that process is killed.

Closes allod/tools#234

What was decided

Where the issue left a choice open:

  • How a preview app learns where to listen: ALLOD_PREVIEW_PORT and ALLOD_PREVIEW_INTERFACE, set on every run, defaults 1111 and the literal 127.0.0.1.
  • How --vm learns which ports to forward: it asks the VM. A new flag, --ports, performs selection and prints the ports without starting anything, and --vm runs that over a first connection. The hypervisor's copy of the site can be stale; the VM's is the one being served. The first port answered must be the one asked for.
  • Ports a preview app opens beyond the page port: --forward <port>[,<port>...], repeatable, valid only with --vm.
  • --open is treated as zola's, with --drafts and --base-url: refused by name on the other two kinds, and refused with --vm.
  • Zola's second port: --ports answers the page port and 1024 for a zola site. Zola 0.23.6 was measured serving live reload on the page port; 1024 is what 0.21 used when the issue was measured. It stays because a site pinned to an older zola needs it. The cost: --vm refuses to start when 1024 is taken on the hypervisor, even for a site whose zola no longer uses it.
  • Ctrl-Z: the server's process group holds the terminal, so a suspend would stop it and leave nothing to hand the terminal back. The server is started with that signal ignored.
  • A flake that does not evaluate, or a flake.nix that cannot be read, is an error naming what failed, never a fall through to zola or static.
  • The static server serves only what is inside the built output, follows no symlink that leaves it, lists no directory, and answers anything but GET and HEAD with 405.

nixBuild and posixShellQuote moved out of files carrying a build tag so the untagged preview can use them; their behavior is unchanged.

Risk

R2. The change is local to one command, writes no state, and listens on loopback only. What the tests cannot reach is --vm end to end, which needs a hypervisor.

Known limits:

  • A connection that drops without the VM noticing. The hypervisor side gives up after about 15 seconds and frees its ports. The server in the VM stops only when the VM's SSH daemon notices the drop and closes the terminal. There is no heartbeat.
  • allod killed outright (SIGKILL). The kernel signals the server itself (Linux only). A process the server started, and does not take down with it, can survive.
  • A process that outlives a forced kill. allod waits a bounded time, says so, and exits 8.
  • The gap between allod checking a port and the server binding it is not closed; a process taking the port in between gets the server's own error, not allod's.
  • The exit code 8 path is tested at the function that decides it, not with a real unkillable process.

Validation

Run by the integrator on e69f76d, through nix develop:

Command Result
gofmt -l ./cmd ./internal no output
go vet ./..., go vet -tags site,secret ./... clean
go test -count=1 ./... all six packages ok
go test -count=1 -tags site ./..., -tags secret, -tags site,secret all ok
go test -count=10 -run 'TestPreviewGroup|TestPreviewSuspend|TestPreviewFinalExit' ./cmd/allod/ ok, ten runs
nix build .#checks.x86_64-linux.go-checks exit 0

No server or test helper process was left running afterwards.

Reported by the implementing agent and not repeated by the integrator:

  • Real runs with nix and curl. Static: pages served, a missing file and an escaping symlink answered 404, port free after SIGTERM. Zola 0.23.6: page served with the reload script, no zola left and both ports free after an interrupt. Preview app: the app received the port and interface and nothing was left after SIGTERM; its test app sent a malformed response, so a successful page load was not seen for this kind.
  • A signal landing while the server is starting: 40 runs of the real binary with SIGTERM at a random offset of 0 to 40 ms left a server behind in 40 of 40 before the fix and 0 of 40 after. This loop is not in the repository.
  • Sabotage: disabling the parent-death signal, and signalling only the direct child, each turned the cleanup tests red.
  • A red run before each fix below and a green one after.
  • One flaky assertion found and corrected during the fix round: on the terminal path the grandchild can see either the interrupt or the terminate that follows it, and the test now accepts both.

Not run: --vm against a VM; the dropped-connection case; the skip of the parent-death test off Linux.

Review

One cold review by gpt-5.6-sol at high effort, of ad19b75, followed by one fix round in e69f76d. The reviewer could not run the tests in its sandbox, so every finding was from reading. Two high, seven medium, four low defect findings, and a list of comment findings.

Finding Outcome
Signals caught only after the server started fixed
Ctrl-Z leaves the terminal stuck fixed by ignoring the suspend signal; shown through a real terminal in site_preview_suspend_linux_test.go
--interface '' binds every interface on the static kind fixed: empty values refused by name
A flag takes the next flag as its value fixed: a value beginning with - is a missing value. --port -1 is now reported that way
The VM's port answer trusted without checking fixed: first port must match, repeats dropped
Any failure to stat flake.nix read as "no flake" fixed
Page port checked after the flake was evaluated fixed
Exit as success after outliving a forced kill; exit state read while being written fixed, exit 8; go test -race reported nothing
Test cleanup could signal a recycled process id; parent-death test off Linux fixed
Comments that narrate trimmed; the ones that warn of a trap stay
Probe evaluated with --impure, nix run without discarded: builtins.currentSystem needs it
Process group id reused between polls discarded, inherent to polling
Gap between the port check and the server binding discarded, see Known limits
## Owner summary - **What changes on a running machine:** `allod site preview` works for a site that is not built with zola. A site whose flake has an app named `preview` runs that app; a site with `config.toml` runs zola as before; any other site is built and served as static files by `allod` itself. - **Also changes:** stopping the preview, by an interrupt, a terminal closing, or `allod` being killed, takes the server down with it and frees the port. `allod site preview --vm <name>`, run on the machine hosting the VM, starts the preview in the VM and forwards the ports over its own connection. - **What stays identical:** the command zola is started with, for every existing flag. `deploy`, `check` and `config` are untouched. - **Three small differences a zola site can notice:** a page port already in use is refused by `allod` before zola starts; `--port` must be a number; Ctrl-Z does nothing while a preview runs. - **What proves it:** the Go tests pass under all four build variants, including tests that start real processes, signal or kill `allod`, and check that nothing is left and the port is free, and one that types Ctrl-Z and Ctrl-C through a real terminal. - **Not proven:** `--vm` has never been run against a VM. Only the two `ssh` command lines are tested. The manual tests in the issue are still to do, from a hypervisor: page loads, an edit reloads the browser, an interrupt frees the ports on both machines, and the same with the connection dropped. - **If it is wrong:** revert this PR. Nothing here writes state; the worst outcome is a leftover server process holding a port, which ends when that process is killed. Closes allod/tools#234 ## What was decided Where the issue left a choice open: - **How a preview app learns where to listen:** `ALLOD_PREVIEW_PORT` and `ALLOD_PREVIEW_INTERFACE`, set on every run, defaults `1111` and the literal `127.0.0.1`. - **How `--vm` learns which ports to forward:** it asks the VM. A new flag, `--ports`, performs selection and prints the ports without starting anything, and `--vm` runs that over a first connection. The hypervisor's copy of the site can be stale; the VM's is the one being served. The first port answered must be the one asked for. - **Ports a preview app opens beyond the page port:** `--forward <port>[,<port>...]`, repeatable, valid only with `--vm`. - **`--open`** is treated as zola's, with `--drafts` and `--base-url`: refused by name on the other two kinds, and refused with `--vm`. - **Zola's second port:** `--ports` answers the page port and `1024` for a zola site. Zola 0.23.6 was measured serving live reload on the page port; `1024` is what 0.21 used when the issue was measured. It stays because a site pinned to an older zola needs it. The cost: `--vm` refuses to start when `1024` is taken on the hypervisor, even for a site whose zola no longer uses it. - **Ctrl-Z:** the server's process group holds the terminal, so a suspend would stop it and leave nothing to hand the terminal back. The server is started with that signal ignored. - **A flake that does not evaluate, or a `flake.nix` that cannot be read,** is an error naming what failed, never a fall through to zola or static. - **The static server** serves only what is inside the built output, follows no symlink that leaves it, lists no directory, and answers anything but GET and HEAD with 405. `nixBuild` and `posixShellQuote` moved out of files carrying a build tag so the untagged `preview` can use them; their behavior is unchanged. ## Risk R2. The change is local to one command, writes no state, and listens on loopback only. What the tests cannot reach is `--vm` end to end, which needs a hypervisor. Known limits: - **A connection that drops without the VM noticing.** The hypervisor side gives up after about 15 seconds and frees its ports. The server in the VM stops only when the VM's SSH daemon notices the drop and closes the terminal. There is no heartbeat. - **`allod` killed outright (SIGKILL).** The kernel signals the server itself (Linux only). A process the server started, and does not take down with it, can survive. - **A process that outlives a forced kill.** `allod` waits a bounded time, says so, and exits 8. - The gap between `allod` checking a port and the server binding it is not closed; a process taking the port in between gets the server's own error, not `allod`'s. - The exit code 8 path is tested at the function that decides it, not with a real unkillable process. ## Validation Run by the integrator on `e69f76d`, through `nix develop`: | Command | Result | | --- | --- | | `gofmt -l ./cmd ./internal` | no output | | `go vet ./...`, `go vet -tags site,secret ./...` | clean | | `go test -count=1 ./...` | all six packages ok | | `go test -count=1 -tags site ./...`, `-tags secret`, `-tags site,secret` | all ok | | `go test -count=10 -run 'TestPreviewGroup\|TestPreviewSuspend\|TestPreviewFinalExit' ./cmd/allod/` | ok, ten runs | | `nix build .#checks.x86_64-linux.go-checks` | exit 0 | No server or test helper process was left running afterwards. Reported by the implementing agent and not repeated by the integrator: - Real runs with `nix` and `curl`. Static: pages served, a missing file and an escaping symlink answered 404, port free after SIGTERM. Zola 0.23.6: page served with the reload script, no zola left and both ports free after an interrupt. Preview app: the app received the port and interface and nothing was left after SIGTERM; its test app sent a malformed response, so a successful page load was not seen for this kind. - A signal landing while the server is starting: 40 runs of the real binary with SIGTERM at a random offset of 0 to 40 ms left a server behind in 40 of 40 before the fix and 0 of 40 after. This loop is not in the repository. - Sabotage: disabling the parent-death signal, and signalling only the direct child, each turned the cleanup tests red. - A red run before each fix below and a green one after. - One flaky assertion found and corrected during the fix round: on the terminal path the grandchild can see either the interrupt or the terminate that follows it, and the test now accepts both. Not run: `--vm` against a VM; the dropped-connection case; the skip of the parent-death test off Linux. ### Review One cold review by `gpt-5.6-sol` at `high` effort, of `ad19b75`, followed by one fix round in `e69f76d`. The reviewer could not run the tests in its sandbox, so every finding was from reading. Two high, seven medium, four low defect findings, and a list of comment findings. | Finding | Outcome | | --- | --- | | Signals caught only after the server started | fixed | | Ctrl-Z leaves the terminal stuck | fixed by ignoring the suspend signal; shown through a real terminal in `site_preview_suspend_linux_test.go` | | `--interface ''` binds every interface on the static kind | fixed: empty values refused by name | | A flag takes the next flag as its value | fixed: a value beginning with `-` is a missing value. `--port -1` is now reported that way | | The VM's port answer trusted without checking | fixed: first port must match, repeats dropped | | Any failure to stat `flake.nix` read as "no flake" | fixed | | Page port checked after the flake was evaluated | fixed | | Exit as success after outliving a forced kill; exit state read while being written | fixed, exit 8; `go test -race` reported nothing | | Test cleanup could signal a recycled process id; parent-death test off Linux | fixed | | Comments that narrate | trimmed; the ones that warn of a trap stay | | Probe evaluated with `--impure`, `nix run` without | discarded: `builtins.currentSystem` needs it | | Process group id reused between polls | discarded, inherent to polling | | Gap between the port check and the server binding | discarded, see Known limits |
'site preview' compiles into every build and needs both: the default
package's build for the row it serves itself, and shell quoting for the
command it runs over ssh. Neither changes behaviour; nixBuild moves from
site.go to site_common.go and posixShellQuote from secret_rotate.go to
exec.go.

Refs allod/tools#234
Selection happens at the site root, first match winning: an app named
'preview' in the flake is run with 'nix run'; else zola when config.toml is
there, with exactly the argv it had before; else the default package is built
and served over HTTP by allod itself. A flake that will not evaluate is an
error naming what failed, never a move to the next row. The app is told where
to listen through ALLOD_PREVIEW_PORT and ALLOD_PREVIEW_INTERFACE; zola's own
flags are refused by name on the other two rows. The page port is bound
before anything is built or started, and a taken one is refused by name.

The static row serves only what is inside the built output: os.Root bounds
it, so a symlink leading out answers 404, a directory serves index.html, and
nothing lists a directory.

The server now runs in its own process group, made the terminal's foreground
group when there is one, and a signal to allod is passed to the whole group,
then SIGKILL, with allod exiting only once the group is gone. A SIGKILL of
allod reaches the server through the parent-death signal on Linux, which does
not reach what the server started.

'--vm <name>' opens two unshared connections: one asking the VM for its ports
with the new '--ports', one with a terminal on the VM side and a forward per
port. '--forward' adds ports a preview app opens beyond the page port.

useSiteRepo now writes config.toml, since a fixture with neither that nor a
'preview' app is no longer a zola site.

Refs allod/tools#234
Refs allod/tools#234
Signals are caught before the server exists, so one arriving around Start no
longer kills allod by default action and leaves what the server spawned
behind; one that arrives before the child exists ends the run without
starting anything.

The server is started with the terminal stop signal ignored, which exec
preserves where a handler is not. A suspend typed at the terminal would
otherwise stop the group holding the terminal while allod kept waiting and
nothing gave the terminal back. The help text and README say so.

Every value-taking flag refuses an empty value and a value that begins with
'-'. '--interface ""' would bind every interface rather than loopback, and
'--vm --open' would name a VM '--open' and connect to it.

The VM's port answer must begin with the page port that was asked for, and
repeated ports are dropped: the first port decides where the forward points
and where the preview restarts, and a repeated '-L' fails the session.

Only a flake.nix that does not exist is absence; any other stat error, and one
that is not a regular file, names the file and stops.

The page port is bound before the flake is evaluated, so a taken port is
refused without waiting for nix. '--ports' still binds nothing.

A group that outlived SIGKILL, or a server whose end never arrived, exits 8
instead of passing off a status allod cannot stand behind. The wait goroutine
reads cmd.ProcessState and sends the code over a channel, so no read of it
races with Wait.

Test cleanup no longer signals a pid it watched exit, the parent-death test
skips where that signal does not exist, and a pseudo-terminal test drives a
real suspend and interrupt through a terminal's own line discipline.

Comments across the preview files are cut to contracts, traps and pointers.

Refs allod/tools#234
Author
Member

Decision recorded after the owner asked whether the port and cleanup behaviour should be changed here or later: this PR stays as it is.

The owner's questions were why port 1024 is treated as special, how previews avoid colliding on ports, and how to start, stop and interrupt a preview from either machine with nothing left behind. Those are one design, and it replaces how --vm connects, so it does not belong in a PR that has already had its review and fix round. It is written up as allod/tools#238.

What this PR does on those points, unchanged: ports are fixed (1111, and 1024 for a zola site) and a taken one is refused by name; a dropped connection is cleaned up in the VM only once the VM's SSH daemon notices; Ctrl-Z is ignored while a preview runs. Each is loud or documented, and none leaves anything behind in the paths the tests cover.

Still needed before merge: the manual --vm runs from a hypervisor listed in the owner summary. Their results are also the baseline allod/tools#238 compares against.

Decision recorded after the owner asked whether the port and cleanup behaviour should be changed here or later: this PR stays as it is. The owner's questions were why port `1024` is treated as special, how previews avoid colliding on ports, and how to start, stop and interrupt a preview from either machine with nothing left behind. Those are one design, and it replaces how `--vm` connects, so it does not belong in a PR that has already had its review and fix round. It is written up as allod/tools#238. What this PR does on those points, unchanged: ports are fixed (`1111`, and `1024` for a zola site) and a taken one is refused by name; a dropped connection is cleaned up in the VM only once the VM's SSH daemon notices; Ctrl-Z is ignored while a preview runs. Each is loud or documented, and none leaves anything behind in the paths the tests cover. Still needed before merge: the manual `--vm` runs from a hypervisor listed in the owner summary. Their results are also the baseline allod/tools#238 compares against.
Author
Member

Owner request recorded before it is carried out: a comment deletion pass over this PR, then merge.

The pass removes code comments this PR added and changes nothing else. A comment survives only where, without it, a careful reader would plausibly change the code into something wrong, and a survivor is cut to a line or two. Help text and the README are not comments and stay as they are.

How it is checked: the syntax tree of every Go file, with comments and positions left out, is compared before and after the pass and must be identical; then the full test runs are repeated.

Owner request recorded before it is carried out: a comment deletion pass over this PR, then merge. The pass removes code comments this PR added and changes nothing else. A comment survives only where, without it, a careful reader would plausibly change the code into something wrong, and a survivor is cut to a line or two. Help text and the README are not comments and stay as they are. How it is checked: the syntax tree of every Go file, with comments and positions left out, is compared before and after the pass and must be identical; then the full test runs are repeated.
Author
Member

The comment deletion pass is pushed as 023bc37. It removes comments this PR added and changes no code.

Before After
Comment lines added by this PR 245 12
Lines added by this PR in total 2435 2203

Six comments remain, each a trap a reader could fall into: why the flake root travels in the environment, why the thread is held for the life of the server, why the server takes the terminal before it starts, why the exit state is read through a channel, why the terminal-output stop signal is ignored while taking the terminal back, and why the test cleanup skips a process it already saw exit.

Checked by the integrator on 023bc37:

  • The syntax tree of every Go file in the repository, with comments and positions left out, is identical to the one at e69f76d.
  • The only changed lines that are not comments: three test lines that lost a trailing comment, and one struct whose field alignment gofmt changed once a comment between the fields was gone.
  • gofmt prints nothing, go vet is clean, and the cmd/allod tests pass under all four build variants.
  • The branch merges cleanly with current master.

The validation in the PR body was run on e69f76d; this comment is what covers 023bc37. Still not run: --vm against a VM.

The comment deletion pass is pushed as `023bc37`. It removes comments this PR added and changes no code. | | Before | After | | --- | --- | --- | | Comment lines added by this PR | 245 | 12 | | Lines added by this PR in total | 2435 | 2203 | Six comments remain, each a trap a reader could fall into: why the flake root travels in the environment, why the thread is held for the life of the server, why the server takes the terminal before it starts, why the exit state is read through a channel, why the terminal-output stop signal is ignored while taking the terminal back, and why the test cleanup skips a process it already saw exit. Checked by the integrator on `023bc37`: - The syntax tree of every Go file in the repository, with comments and positions left out, is identical to the one at `e69f76d`. - The only changed lines that are not comments: three test lines that lost a trailing comment, and one struct whose field alignment `gofmt` changed once a comment between the fields was gone. - `gofmt` prints nothing, `go vet` is clean, and the `cmd/allod` tests pass under all four build variants. - The branch merges cleanly with current `master`. The validation in the PR body was run on `e69f76d`; this comment is what covers `023bc37`. Still not run: `--vm` against a VM.
Delete the comments this branch added, keeping the six where the code
would plausibly be changed into something wrong without them: the OS
thread held for the child's life, the terminal handed over by the child
before it execs, cmd.ProcessState read only through the channel, SIGTTOU
ignored while taking the terminal back, the flake root travelling in the
environment rather than in the nix expression, and pid reuse in the
helper group's cleanup. Each survivor is one or two lines.

No code changes: the comment-free syntax tree of every Go file is
unchanged, apart from gofmt realigning previewOptions' field names once
the comment that split the alignment group was gone.
Author
Member

Closing without merging, by the owner's decision. The work is being restarted from a smaller design, and this comment records what went wrong here so the record is not lost with the branch.

What this PR was

About 2,200 added lines for allod site preview: three ways to serve a site (a flake preview app, zola, a static server built into allod), supervision of the server as a child process, and --vm with a port query over one connection and forwards over a second. It passed its tests, one cold review and one fix round. --vm was never run against a VM.

What went wrong

  • The issue was built as written. allod/tools#234 prescribed three rows and each requirement got its own mechanism. Nobody asked what the smallest contract was. It is one: the site says how to preview itself.
  • Supervising a child created the problem a third of the code solved. allod started the server as a child, so it needed a process group, signal forwarding, a terminal handover, a parent-death signal and a workaround for Ctrl-Z. If allod replaces itself with the server there is no parent to outlive.
  • "No edits to the zola sites" was the expensive goal. It kept zola's flags and zola 0.21's reload port 1024 inside a tool that was meant to stop caring which generator a site uses.
  • The built-in static server never met the need. It has no reload, and the point of a preview is a page that refreshes as the site is edited.
  • --vm was designed and reviewed by reading. The owner's manual runs on allod/tools#234 had already shown that a plain ssh -t on its own connection cleans up with no code at all. The design should have started from that measurement.
  • Review made it larger. Thirteen defect findings were each answered with more code and more tests. A cold reviewer asked to find defects in machinery does not ask whether the machinery should exist, and the brief did not ask either.
  • Size was a signal and was not read as one. The comment pass removed 233 lines and the PR was still 2,200.

What carries forward

Nothing from this branch is needed by the new design. The measurements are: zola 0.23.6 serves live reload on the page port, where 0.21 used 1024; localhost resolves to ::1 only on these machines, so the literal 127.0.0.1 is what gets passed; and nix eval's --apply can tell an absent flake attribute from a flake that fails to evaluate.

The replacement issue is linked from allod/tools#234 when it is filed.

Closing without merging, by the owner's decision. The work is being restarted from a smaller design, and this comment records what went wrong here so the record is not lost with the branch. ### What this PR was About 2,200 added lines for `allod site preview`: three ways to serve a site (a flake `preview` app, zola, a static server built into `allod`), supervision of the server as a child process, and `--vm` with a port query over one connection and forwards over a second. It passed its tests, one cold review and one fix round. `--vm` was never run against a VM. ### What went wrong - **The issue was built as written.** allod/tools#234 prescribed three rows and each requirement got its own mechanism. Nobody asked what the smallest contract was. It is one: the site says how to preview itself. - **Supervising a child created the problem a third of the code solved.** `allod` started the server as a child, so it needed a process group, signal forwarding, a terminal handover, a parent-death signal and a workaround for Ctrl-Z. If `allod` replaces itself with the server there is no parent to outlive. - **"No edits to the zola sites" was the expensive goal.** It kept zola's flags and zola 0.21's reload port `1024` inside a tool that was meant to stop caring which generator a site uses. - **The built-in static server never met the need.** It has no reload, and the point of a preview is a page that refreshes as the site is edited. - **`--vm` was designed and reviewed by reading.** The owner's manual runs on allod/tools#234 had already shown that a plain `ssh -t` on its own connection cleans up with no code at all. The design should have started from that measurement. - **Review made it larger.** Thirteen defect findings were each answered with more code and more tests. A cold reviewer asked to find defects in machinery does not ask whether the machinery should exist, and the brief did not ask either. - **Size was a signal and was not read as one.** The comment pass removed 233 lines and the PR was still 2,200. ### What carries forward Nothing from this branch is needed by the new design. The measurements are: zola 0.23.6 serves live reload on the page port, where 0.21 used `1024`; `localhost` resolves to `::1` only on these machines, so the literal `127.0.0.1` is what gets passed; and `nix eval`'s `--apply` can tell an absent flake attribute from a flake that fails to evaluate. The replacement issue is linked from allod/tools#234 when it is filed.
allod-agent closed this pull request 2026-09-27 22:32:58 +01:00

Pull request closed

Sign in to join this conversation.
No description provided.