Chapter 2 / 51
Volume 1 · Getting Started & Mental Model

Install & first run: build, boot log, first request

From zero to your first request takes exactly two things: a GCC and a Python. Every log line and response in this chapter is real output from demo-single — you can reproduce it verbatim.

Overview

Positioning: the goal of this chapter is not "installed", it is reading the boot log — your primary evidence when debugging later (chapter 6 builds on exactly this skill). If you have built before, jump straight to 2.3.

2.1 Intro

Deploying a new server stack, the painful part is rarely writing code — it is environment prep: Node wants nvm and node_modules, Go wants a toolchain and GOPATH, a traditional C service wants its own build pipeline. XServer's answer is to fold all of it into build time and the binary itself: at build time your GCC produces xs.exe while the TCC compiler, the full SDK headers and system library definitions are LZMA-compressed into the binary; at runtime the target machine needs nothing installed.

So "installing XServer" is really two actions: build the host once, then run a ready-made app directory (demo-single). We start with those two.

2.2 Building the host

Two prerequisites only: GCC (select with --cc) and Python 3. In the xserver repository root:

Terminal · Windows
$ build.bat                # minimal variant: default
$ build.bat sqlite xtp     # compile in the SQLite and XTP2 extensions
$ build.bat --list-extensions
available extensions: sqlite, xtp

The Linux equivalent is bash build.sh sqlite xtp. The build pipeline does five things:

build the LZMA packer │ ▼ generate VFS resources (SDK headers + TCC includes + system .def → C byte arrays) │ ▼ compile extension objects (-DXS_USE_SQLITE / XS_USE_XTP2, per variant) │ ▼ compile the host (main.c + libtcc + VFS + resources) │ ▼ trial link in a temp dir → replace xs.exe atomically on success ← a failure always keeps the last working version

The last step deserves emphasis: the artifact is replaced only after a successful link. On a failed build the xs.exe in the release directory stays untouched — this "failure protection" is guarded by a dedicated test case; interrupting a build at any moment never yields a broken binary.

2.3 Running the demo, reading the log

demo-single is the complete sample app shipped in the repository (routing, templates, SQLite, controlled pages). Enter the directory and run:

Terminal · real output
$ cd release/demo-single
$ ./xs.exe
[xs] config loaded: D:\GIT\xserver\release\demo-single/xs.json (1 servers)
[xs] server 'demo' class=http 127.0.0.1:9080 tls=off enabled
[xs]   default-host 'app' path=...\wwwroot devfile=...\main.c
[xs] engine started, workers=12
[xs] script loaded: ...\main.c (2.6 KB gen 1)
[demo] database opened: ...\db\main.db
[xs] server 'demo' http bound on 127.0.0.1:9080
[xs] running, press Ctrl+C to stop

What just happened — line by line:

Log lineMeaningWhat to note
config loaded (1 servers)xs.json parsedfail-fast: an invalid field exits right here with the field name
server 'demo' class=http ...service config echoyour last chance to check listen address / port / TLS
engine started, workers=12concurrency engine readyworker count from engine.workers, 0 = xrt default (12 on this machine)
script loaded (2.6 KB gen 1)TCC compile finishednote gen 1 — the "generation", protagonist of chapter 3
[demo] database openedyour ServiceInit is runninglines prefixed [demo] come from the app script itself
http bound on 127.0.0.1:9080listening is livea failed bind rolls back every already-bound listener
running, press Ctrl+C to stopserving stateCtrl+C / SIGTERM triggers the five-phase graceful shutdown
Note the semantics of the ordering: ports bind only after the script compiles. This guarantees the app is fully initialized by the time it becomes externally visible — the same discipline as hot reload's "compile before swap" (volume 4).

2.4 Making requests

Open another terminal and send the demo a few requests (all real output):

Terminal · real output
$ curl -s http://127.0.0.1:9080/json
{"server":"xs3 demo","ok":true,"host":"app"}

$ curl -s http://127.0.0.1:9080/text
demo-single ok (from includes/demo_extra.h)

$ curl -s http://127.0.0.1:9080/api/v1/items/42
{"id":42,"name":"bench-write","created":1788494894}

$ curl -s -o /dev/null -w "%{http_code}" http://127.0.0.1:9080/nope
404
$ curl -sI http://127.0.0.1:9080/json
HTTP/1.1 405 Method Not Allowed
Allow: GET

What just happened:

  • /json went through the app script's Handle_JSON — building an object with xvalue and serializing it (chapter 18's topic);
  • /api/v1/items/42 hit the dynamic route /api/v1/items/{id}; the captured segment 42 was looked up in SQLite (chapter 17);
  • /nope matched no route and no app handler → 404;
  • HEAD /json got 405 + Allow: GET — the route's method mask registered only GET, and HEAD was rejected at the method slot before reaching the handler. A real routing detail; chapter 17 covers how to handle HEAD.

2.5 Contract

  • Threading: workers is fixed at startup and immutable across hot reload (changes are rejected).
  • Variants: the build variant decides which symbols scripts may use — the default variant has no sqlite3_* / xtp2_*; using them fails the compile.
  • Data: the demo's db/main.db is a real data file; experiment freely and rebuild the repository to restore it.

Common mistakes

Port in use, startup fails
Symptom
bind failed, process exits; with multiple listeners configured, previously bound ports are not left half-serving either.
Cause
The target port is occupied. XServer's assembly is transactional: when any listener fails, the already-bound ones are fully rolled back before exit — no "half a service running" intermediate state.
✗ Blind retry
./xs
bind failed
./xs          /* retry over and over, no idea who holds the port */
✓ Check, then reconfigure
netstat -ano | findstr 9080
/* change xs.json's port, or kill the holder */
Double-clicking xs.exe in the wrong directory
Symptom
config load failed — even though you are sure xs.json sits in some directory.
Cause
When run without arguments, XServer looks for xs.json in the directory of the executable, not the current working directory. Copy xs.exe somewhere alone and double-click, and of course it cannot find the app.
✗ Wrong
copy xs.exe C:\tools\
C:\tools\xs.exe      /* config load failed */
✓ Right
/* keep xs.json and the source directory next to the exe;
 * or pass the config path explicitly */
./xs D:\apps\myapp\xs.json

Exercises

BasicChange the port and restart

Change the demo's xs.json port from 9080 to 19080, restart and verify with curl.

Acceptance: the log shows http bound on 127.0.0.1:19080 and curl /json returns the same JSON as this chapter.
AdvancedSwitch variants

Rebuild with build.bat sqlite, run the demo and confirm /api/list works; then switch back to the default variant and observe what the script compile reports.

Acceptance: you can explain "why the demo fails on the default variant" and at which stage the error happens (hint: compare with the log order in 2.3).
ChallengeAdd a route of your own

Register /hello (GET) in the demo's route.h with a handler returning your name. Restart to verify — hot reload is volume 4's business; for now use the "restart to apply" mental model.

Acceptance: curl /hello returns what you wrote; the log shows script loaded ... gen 2 (does a restart bump the generation counter? Try it and find the answer at the start of chapter 3).

Summary

PointConclusion
RequirementsGCC + Python 3, nothing else
Build variantsbuild.bat [sqlite] [xtp]; atomic replace only on a successful link
Boot orderconfig → engine → script compile → ServiceInit → bind → running
Key log linesgen N (generation), workers=N, bound; lines with an app prefix come from your script
Config lookupxs.json is sought next to the exe by default; pass a path to override
xrt further reading