From 4b0274536f93fb08da4612447e4316754cfae7bc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mike=20Gro=C3=9Fmann?= Date: Mon, 24 Aug 2026 20:21:24 +0200 Subject: [PATCH 1/4] Ship the document root as a UFS disk image, uploaded with IND$FILE static/index.html was in the repository and in no release artifact, so the package installed a server whose DOCROOT had nothing behind it. A UFS disk cannot travel the way the rest of the package does, and not for lack of tooling. SMP has no element type for a DSORG=PS/RECFM=U image and must never touch site content; TSO RECEIVE allocates its own target and refuses to merge into an existing dataset, which makes it a first-install-only transport for something an operator replaces. So the image ships as a plain file in the archive and goes up over IND$FILE, which needs nothing on the target beyond a 3270 session. make webroot builds dist/httpd-webroot.img from static/ with a pinned ufsd-utils (fetched into .mbt/tools/ when absent, because release CI runs the shared workflow and cannot install anything of its own). package and dist depend on it, and [distribution] extra puts it in the zip/tar.gz. samplib/httpwebr allocates HTTPD.@VRM@.WEBROOT.UFS -- SPACE=(4096,256), RECFM=U BLKSIZE=4096, primary extent only. installation.md 9 the operator's side: allocate, upload, mount, verify, and unmount before replacing. Measured rather than assumed: Blocking RECFM=U buffers to BLKSIZE through __fputc (libc370 @@fputc.c:24), so BLKSIZE(4096) writes whole 4096-byte blocks; the 1 MB image is 256 of them with no remainder. Options binary is IND$FILE's default and RECFM then defaults to U (ind_file370 indparse.c:249). Pre-allocate IND$FILE allocates an existing dataset DISP=SHR and takes its DCB (indmain.c:190, :273), so the documented upload needs no options and does not depend on which IND$FILE build a system carries. The trap that same DISP=SHR rewrites a dataset UFSD has mounted, so UNMOUNT is a numbered step rather than a footnote. Detection a BLKSIZE mismatch surfaces at MOUNT as UFSD062E (ufsd ufsd#sbl.c:69), not as a corrupt page. Encoding nothing to configure: http_send_file() translates UFS files with the hard-coded IBM-1047 table independent of CODEPAGE= (httpfile.c:70), which is what ufsd-utils writes. The round trip was measured byte for byte over the whole file and over all 256 values; only 0x85 and 0xF7 do not survive. The version in the dataset name settles what smp-todo called U6: a shipped image cannot land on a webroot the site built for itself. Fixes #252 --- Makefile | 71 +++++++++++++++ docs/installation.md | 207 ++++++++++++++++++++++++++++++++++++++++--- project.toml | 24 ++++- samplib/httpwebr | 35 ++++++++ smp-todo.md | 83 ++++++++++------- 5 files changed, 372 insertions(+), 48 deletions(-) create mode 100644 samplib/httpwebr diff --git a/Makefile b/Makefile index a25ecfe..a0f922b 100644 --- a/Makefile +++ b/Makefile @@ -1,2 +1,73 @@ MBT_ROOT := mbt include $(MBT_ROOT)/mk/mbt.mk + +# -- Webroot disk (issue #252) -------------------------------------- +# static/ ships as a formatted UFS370 image, because a UFS disk cannot +# travel any of the ways the rest of the package does: SMP has no element +# type for a DSORG=PS/RECFM=U image and must never touch site content, and +# TSO RECEIVE allocates its own target and refuses to merge into an +# existing dataset -- a first-install-only transport for something that +# gets updated. The operator uploads the image with IND$FILE instead; +# docs/installation.md carries the procedure. +# +# There is deliberately no codepage handling here. http_send_file() +# translates UFS files with the hard-coded IBM-1047 table, independent of +# the CODEPAGE= setting (src/httpfile.c), and IBM-1047 is what +# `ufsd-utils cp` writes -- measured byte for byte over the whole file, +# UTF-8 sequences included. Only 0x85 and 0xF7 fail to round-trip, so a +# text file must avoid characters whose UTF-8 encoding contains them. +# +# ufsd-utils is pinned rather than taken from PATH: `create` stamps an +# owner and a timestamp into the image, so the version that builds a +# release artifact is part of what is shipped. Release CI runs the shared +# mbt workflow and cannot install anything of its own, hence the fetch into +# .mbt/tools/. Pass UFSD_UTILS= to build against a different one. +# 1M is 256 blocks of 4096, which the inode list caps at 62 files -- the same +# 62 any disk up to 512 blocks gets, so a larger image buys space, not files. +# Owner and group are metadata: UFSD enforces the MOUNT's OWNER(), not the +# inode owner. They are named anyway, so a release artifact does not carry +# whatever userid happened to build it. +WEBROOT_IMG := $(DISTDIR)/httpd-webroot.img +WEBROOT_SRC := static +WEBROOT_SIZE := 1M +WEBROOT_OWNER := IBMUSER +WEBROOT_GROUP := SYSPROG + +UFSD_UTILS_VER := 1.0.1 +UFSD_UTILS_BIN := .mbt/tools/ufsd-utils-$(UFSD_UTILS_VER) +UFSD_UTILS ?= $(UFSD_UTILS_BIN) + +# Only the pinned binary is a prerequisite -- an overridden UFSD_UTILS names +# a program on PATH, which make would try (and fail) to build as a file. +WEBROOT_TOOL_DEP := $(if $(filter $(UFSD_UTILS_BIN),$(UFSD_UTILS)),$(UFSD_UTILS_BIN)) + +$(UFSD_UTILS_BIN): + $(E) "[webroot] fetching ufsd-utils $(UFSD_UTILS_VER)" + @mkdir -p $(dir $@) + @os=`uname -s | tr '[:upper:]' '[:lower:]'`; \ + arch=`uname -m`; \ + case "$$arch" in \ + x86_64|amd64) arch=amd64 ;; \ + arm64|aarch64) arch=arm64 ;; \ + *) echo "[webroot] no ufsd-utils release for $$arch" >&2; exit 1 ;; \ + esac; \ + name=ufsd-utils-$$os-$$arch; \ + curl -sSfL "https://github.com/mvslovers/ufsd-utils/releases/download/v$(UFSD_UTILS_VER)/$$name.tar.gz" \ + | tar xzOf - $$name > $@ || { rm -f $@; exit 1; } + @chmod +x $@ + +webroot: $(WEBROOT_IMG) + +$(WEBROOT_IMG): $(shell find $(WEBROOT_SRC) -type f) $(WEBROOT_TOOL_DEP) + $(E) "[webroot] $(notdir $@) ($(WEBROOT_SIZE), from $(WEBROOT_SRC)/)" + @mkdir -p $(DISTDIR) + @rm -f $@ + $(Q)$(UFSD_UTILS) create $@ --size $(WEBROOT_SIZE) --blksize 4096 \ + --owner $(WEBROOT_OWNER) --group $(WEBROOT_GROUP) > /dev/null + $(Q)$(UFSD_UTILS) cp -r $(WEBROOT_SRC)/ $@:/ + +# Both read it: 'package' ships it in the archive ([distribution] extra), +# and 'dist' re-renders that archive on its own. +package dist: webroot + +.PHONY: webroot diff --git a/docs/installation.md b/docs/installation.md index 3ce6864..5a89863 100644 --- a/docs/installation.md +++ b/docs/installation.md @@ -6,7 +6,7 @@ Hercules build) from the release distribution archive. The installation is managed by **SMP Release 4** — the SMP that ships with MVS 3.8j, not SMP/E. That means the system keeps a record of what was installed, and there is a supported way back out (see -[Removing HTTPD](#11-removing-httpd)). +[Removing HTTPD](#12-removing-httpd)). ## Getting help @@ -15,7 +15,7 @@ installed, and there is a supported way back out (see That is the only place bug reports are tracked. Please include the two build stamps HTTPD writes at startup (`HTTPD000I` and `HTTPD005I`, see -[step 9](#9-start-and-verify)) — they identify the exact build — plus the +[step 10](#10-start-and-verify)) — they identify the exact build — plus the console messages and, for an install problem, the job output. **Questions and general support** are on Discord: @@ -44,6 +44,7 @@ recognise which file is which. | `httpd--samplib.xmit` | the sample library: STC procedure and configuration pattern | | `httpd--alloc.jcl` | allocates the datasets SMP installs into — **run once** | | `httpd--inst.jcl` | receives everything and installs it — repeatable | +| `httpd-webroot.img` | the document root as a UFS370 disk image — outside SMP, see [step 9](#9-the-webroot-disk) | The load library holds five modules: @@ -73,8 +74,9 @@ See [step 7](#7-install-the-procedure-and-the-configuration). > and removing the other four changes no behaviour of a server that never > routed to them. -The sample library holds two members, `HTTPD` (the started task procedure) and -`HTTPPRM0` (the configuration pattern). +The sample library holds three members: `HTTPD` (the started task procedure), +`HTTPPRM0` (the configuration pattern) and `HTTPWEBR` (a job that allocates the +dataset the webroot image is uploaded to). Where everything ends up: @@ -82,8 +84,12 @@ Where everything ends up: HTTPD..LINKLIB the load modules -- point STEPLIB here HTTPD..SAMPLIB the patterns you copy from in step 7 HTTPD..AHTTPLOD SMP's distribution library, the base a RESTORE returns to +HTTPD..WEBROOT.UFS the document root, if you upload the shipped one (step 9) ``` +The last one is not SMP's: it is a filesystem image, and it holds site content +that an `APPLY` must never touch. Step 9 is where it comes from. + --- ## 2. Prerequisites @@ -94,7 +100,8 @@ HTTPD..AHTTPLOD SMP's distribution library, the base a RESTORE returns t - **RAKF** — see [Authorisation](#authorisation) below. HTTPD does not start without either RAKF or an APF list entry, and no configuration option turns that off. -- **UFSD**, only if you want to serve static files — see below. +- **UFSD**, only if you want to serve static files — see below. The archive + ships a document root for it as a disk image (step 9). - **mvsMF**, only if you want the REST API the shipped configuration routes to — see below. - A userid authorised to submit jobs, to update a PROCLIB in the started-task @@ -129,6 +136,10 @@ the start. On a deployment that will never have UFSD, `UFS=0` in the configuration member skips the initialisation altogether and the warning with it. +With UFSD there, the archive has a document root ready for it — +`httpd-webroot.img`, a formatted filesystem holding the welcome page, which +[step 9](#9-the-webroot-disk) uploads and mounts. + Install HTTPD now and add UFSD later if you want it: , with its own guide at [ufsd/docs/installation.md](https://github.com/mvslovers/ufsd/blob/main/docs/installation.md). @@ -246,6 +257,11 @@ primary for the load XMIT (roughly 650 KB), 5 for the sample library, secondary about 10% of each. Method b) below shows this with zowe; TSO or ISPF 3.2 does the same job. +The third file that has to reach the host, `httpd-webroot.img`, is **not** one of +these: it is a filesystem image with its own record format, it is not received +by anything, and it is only needed once the server runs. It has its own step — +[step 9](#9-the-webroot-disk) — and nothing here applies to it. + Pick **one** method: ### a) FTP — mvslovers/ftpd (no pre-allocation) @@ -303,6 +319,8 @@ them unnecessary. No pre-allocation needed. Use your emulator's file transfer in **binary** mode (no ASCII/CRLF translation) with `RECFM=FB LRECL=80 BLKSIZE=3120`. The exact option syntax is client-dependent — consult its file-transfer documentation. +[Step 9](#9-the-webroot-disk) walks an IND$FILE upload through in full, for the +webroot image; the same emulator handling applies here. --- @@ -323,7 +341,7 @@ Expect `COND CODE 0000`. > install, `HTTPD..AHTTPLOD` holds SMP's accepted copy of the modules; a > re-run that scratched it would leave the SMP inventory reporting an install > that is no longer on the system, and nothing would say so. To start over, -> reject the SYSMOD first — see [Removing HTTPD](#11-removing-httpd). +> reject the SYSMOD first — see [Removing HTTPD](#12-removing-httpd). --- @@ -332,7 +350,7 @@ Expect `COND CODE 0000`. Only relevant when you are upgrading. The APPLY writes into `HTTPD..LINKLIB`, and each release has its own — so a running *older* HTTPD does not block the install. It does, however, keep running the old modules until -you restart it (step 9) against the procedure you copy in step 7. +you restart it (step 10) against the procedure you copy in step 7. ``` /P HTTPD @@ -419,6 +437,9 @@ Copy from `HTTPD..SAMPLIB`: `SYS2.PROCLIB` is the usual home for the procedure. +The third member, `HTTPWEBR`, is not copied anywhere: it is a job you submit +straight from the sample library when you get to [step 9](#9-the-webroot-disk). + ### Which PARMLIB The shipped procedure defaults to `D='SYS2.PARMLIB'`. **On TK5 that dataset @@ -576,7 +597,166 @@ client can log in. --- -## 9. Start and verify +## 9. The webroot disk + +`DOCROOT` names a path in the UFSD filesystem, so the pages HTTPD serves live on +a UFS disk and not in a library. The archive carries one ready to use: +`httpd-webroot.img`, a 1 MB UFS370 filesystem holding the welcome page. Three +steps put it on the system — allocate, upload, mount — and none of them belongs +to SMP. + +**Why it travels on its own.** SMP has no element type for a `DSORG=PS`, +`RECFM=U` image, and site content is precisely what an `APPLY` must never +touch. TSO RECEIVE is no better: it allocates its own target and refuses to +merge into an existing dataset, which makes it a first-install-only transport +for something you will replace. So the disk ships as a plain file and goes up +over **IND$FILE**, which needs nothing on the target beyond a 3270 session. + +This step needs **UFSD** (step 2). Without it there is nothing to mount and +nothing static to serve; every `MOD=` route works regardless. + +**Already have a webroot?** Then read this for the mechanics and keep your own +disk. Nothing here can overwrite it — the dataset is named for this release. + +### a) Allocate the dataset + +`HTTPD..SAMPLIB(HTTPWEBR)` does exactly this and nothing else: + +```jcl +//ALLOC EXEC PGM=IEFBR14 +//WEBROOT DD DSN=HTTPD..WEBROOT.UFS,DISP=(NEW,CATLG), +// UNIT=SYSDA,SPACE=(4096,256), +// DCB=(DSORG=PS,RECFM=U,BLKSIZE=4096) +``` + +**Run it before the upload, not after.** IND$FILE allocates an *existing* +dataset `DISP=SHR` and takes the DCB from its label; only when the dataset is +missing does it invent one of its own. Allocating it here is what pins +`BLKSIZE=4096` — the block size the image was formatted with, and the one thing +about the transfer that has to be right. + +`SPACE` is counted in blocks of 4096, so 256 blocks is the megabyte the image +occupies. **Primary extent only**: the filesystem is addressed by block number +within the primary extent, so anything in a secondary would never be reached. + +### b) Upload it with IND$FILE + +The image must arrive byte for byte: **binary**, no ASCII translation, no CRLF +conversion. With the dataset allocated, the command carries no options at all — +which also makes it independent of which IND$FILE build your system has: + +``` +IND$FILE PUT HTTPD.V4R0M0.WEBROOT.UFS +``` + +Binary is IND$FILE's default; `ASCII` and `CRLF` are what you would have to add +to break it. + +Most emulators issue that command for you. x3270, c3270 and wc3270 take it as +one action, and their file-transfer dialog asks for the same fields: + +``` +Transfer(Direction=send, + LocalFile=httpd-webroot.img, + HostFile=HTTPD.V4R0M0.WEBROOT.UFS, + Host=tso, Mode=binary, Exist=replace) +``` + +Leave `Recfm`, `Lrecl`, `Blksize` and the space fields **unset**: the dataset +exists, so its label decides, and an emulator's allocation defaults are not +worth auditing. Position the session on a cleared TSO **READY** screen (ISPF +option 6 also works) before starting the transfer. + +If you would rather let IND$FILE create the dataset, it needs to be told +everything the allocation job otherwise says: + +``` +IND$FILE PUT HTTPD.V4R0M0.WEBROOT.UFS (RECFM(U) BLKSIZE(4096) TRACKS SPACE(70) +``` + +70 tracks holds a megabyte even on a 3350, where a 4096-byte block packs four to +a track; a 3380 or 3390 needs half that. There is no secondary quantity on +purpose (see above). + +A megabyte over a 3270 session takes minutes rather than seconds. That is the +transfer, not a hang. + +### c) Mount it + +The mount belongs to **UFSD**, not to HTTPD. In its Parmlib member: + +``` +MOUNT DSN(HTTPD.V4R0M0.WEBROOT.UFS) PATH(/www) MODE(RO) +``` + +or, without a restart — note that the operator command spells its operands with +`=` and commas where the Parmlib statement uses parentheses: + +``` +/F UFSD,MOUNT DSN=HTTPD.V4R0M0.WEBROOT.UFS,PATH=/www,MODE=RO +``` + +`/www` is HTTPD's default `DOCROOT`; if you changed it in `HTTPPRM0`, mount it +there instead. `MODE(RO)` is the default and the right setting here — the +shipped disk is product content and nothing in the server writes to it. + +**UFSD first, then HTTPD.** The filesystem has to be up and mounted before the +server that serves from it — a runtime dependency SMP cannot express, which is +why the SYSMOD declares no prerequisite on UFSD at all. + +### d) Verify + +``` +/F UFSD,MOUNT LIST +curl -v http://your-mvs-host:8080/ +``` + +`/` serves `/www/index.html`: HTTPD appends `index.html` to a request for a +directory. + +| What you see | What it means | +|---|---| +| `UFSD062E SUPERBLOCK VALIDATION FAILED FOR …` | the dataset's `BLKSIZE` is not the image's 4096 — or the transfer was not binary | +| `HTTPD044W UNABLE TO INITIALIZE FILE SYSTEM` | UFSD is not running (step 2) | +| `404` on `/` | mounted somewhere other than `DOCROOT` | +| a page of mojibake | the transfer translated the bytes; upload again in binary | + +### Replacing the content later + +**Unmount before you re-upload.** IND$FILE allocates `DISP=SHR`, so it will +happily rewrite a dataset UFSD has open — under the server's own buffers: + +``` +/F UFSD,UNMOUNT PATH=/www + ... upload ... +/F UFSD,MOUNT DSN=HTTPD.V4R0M0.WEBROOT.UFS,PATH=/www,MODE=RO +``` + +For pages of your own, build the image on your workstation with +[ufsd-utils](https://github.com/mvslovers/ufsd-utils) and upload it the same way: + +``` +ufsd-utils create mysite.img --size 1M --blksize 4096 --owner IBMUSER +ufsd-utils cp -r ./mysite/ mysite.img:/ +``` + +Text files are stored in **IBM-1047**, which is what `ufsd-utils` writes by +default and what HTTPD's static file path translates back with — a UFS file is +converted with that table regardless of the `CODEPAGE=` setting, which governs +MVS datasets and the server's own output. So there is nothing to configure here, +and nothing to match up. + +With mvsMF installed there is a second route for single files — +`PUT /zosmf/restfiles/fs/www/index.html` — but only into a filesystem mounted +`MODE(RW)`, which the shipped disk is not. + +One thing the image is not: reproducible. It carries its own creation +timestamp, so two builds of the same commit differ byte for byte. Compare it +against the archive you downloaded, never against one you rebuilt. + +--- + +## 10. Start and verify ``` /S HTTPD default member (HTTPPRM0) @@ -653,7 +833,7 @@ and `DISPLAY TIme` need one and two letters respectively. --- -## 10. What HTTPD serves +## 11. What HTTPD serves Two kinds of route, both declared in the configuration member, both carrying the same per-route auth policy: @@ -679,7 +859,7 @@ writing your own module is --- -## 11. Removing HTTPD +## 12. Removing HTTPD Because the installation is SMP-managed, there is a defined way back — but it is **not** the `RESTORE` followed by `REJECT` that SMP documentation leads you to @@ -779,8 +959,9 @@ Leave `HTTPD..SAMPLIB` alone if you like — the install job's `DELOLD` ste scratches it on its own. **5. What is not removed, because SMP never owned it:** the procedure and the -configuration member you copied in step 7, and your RAKF definitions. Those are -yours to delete. +configuration member you copied in step 7, your RAKF definitions, and the +webroot disk `HTTPD..WEBROOT.UFS` with the `MOUNT` statement that names it. +Those are yours to delete — unmount the disk before you scratch it. > The `RESTORE`/`REJECT` behaviour above was measured on 2026-08-14 against an > accepted FMID from a package built by this same generator, on an MVS/CE system @@ -813,6 +994,8 @@ yours to delete. | A route answers `200` to anyone | It carries no `AUTH=`, or no route claims the path at all. Both are public by design — step 7. `/.dsrv?target=MOD` prints the policy each route actually got | | `401` on a route showing `AUTH=NONE` | Not HTTPD's gate. mvsMF runs its own auth track on `/zosmf/*` — establish which layer answered before debugging HTTPD's | | `S106` at start on a freshly installed library | The XMIT was uploaded in text mode. Re-upload in **binary** and re-run the install job | +| `UFSD062E SUPERBLOCK VALIDATION FAILED` at `MOUNT` | The webroot dataset was not allocated `RECFM=U BLKSIZE=4096`, or the image was uploaded in text mode — step 9 | +| `404` on `/` with UFSD running | The webroot is mounted somewhere other than `DOCROOT`, or nothing is mounted there at all — step 9 | | The step ends `CC 0000` but nothing was ever served | A clean `/P HTTPD` and a refused start are `CC 0000` and `CC 0008`. If it is 0000, the server ran — look for what stopped it | For the complete message reference, see diff --git a/project.toml b/project.toml index ad75ee0..3dc849c 100644 --- a/project.toml +++ b/project.toml @@ -283,11 +283,21 @@ headers = [ ] # -- Distribution (SMP4 install package) -------------------------- -# Turns `make package` into an installable product, the same two transports -# ufsd and ftpd ship with: +# Turns `make package` into an installable product, on the two transports ufsd +# and ftpd ship with, plus a third one they have no need for: # # load modules -> XMIT -> staging HTTPLOAD -> SMP ++MOD -> HTTPD..LINKLIB # samplib/ -> XMIT -> TSO RECEIVE -> HTTPD..SAMPLIB +# static/ -> UFS370 image -> IND$FILE -> HTTPD..WEBROOT.UFS +# +# The webroot disk (issue #252) rides neither of the first two, and not because +# of a gap in the tooling: SMP has no element type for a DSORG=PS/RECFM=U image +# and must never touch site content, and TSO RECEIVE allocates its own target +# and refuses to merge into an existing one -- a first-install-only transport +# for something an operator updates. So it travels as a plain file in the +# archive (`extra` below) and goes up over IND$FILE, which needs nothing on the +# target beyond a 3270 session. `make webroot` builds it; the Makefile carries +# the why, docs/installation.md the operator's side. # # SMP *copies* the host-bound modules rather than re-binding them (a COPY-style # ++JCLIN is what says so), which is what keeps HTTPD's ac = 1 and every custom @@ -327,6 +337,16 @@ headers = [ # package tells the operator to read a file it does not contain. readme = "docs/installation.md" +# The webroot disk, put in the archive as `httpd-webroot.img` -- `extra` takes +# the basename, so the path here is where `make webroot` writes it and the name +# an operator sees is the file's own. Listed rather than generated by mbtdist: +# the image is not a dataset the install job creates, and nothing in the SYSMOD +# or the RECEIVE plan knows about it. `make package` and `make dist` both +# depend on the target that builds it, because a missing `extra` file is a hard +# error here -- deliberately: an archive quietly missing the webroot would look +# complete and serve nothing on /. +extra = ["dist/httpd-webroot.img"] + [distribution.smp] # One FMID per minor release, spent exactly once: it names a functional level, # and a changed one is a new level. Patches on top of it ship as PTFs. 4.0.x diff --git a/samplib/httpwebr b/samplib/httpwebr new file mode 100644 index 0000000..5a32130 --- /dev/null +++ b/samplib/httpwebr @@ -0,0 +1,35 @@ +//HTTPWEBR JOB (ACCT),'ALLOC HTTPD WEBROOT',CLASS=A,MSGCLASS=H, +// MSGLEVEL=(1,1),NOTIFY=&SYSUID +//* +//* HTTPWEBR - allocate the dataset the webroot disk is uploaded to +//* +//* The release archive carries httpd-webroot.img: a formatted UFS370 +//* filesystem holding the pages HTTPD serves from DOCROOT. You upload +//* it with IND$FILE and mount it with UFSD. The installation guide +//* has the whole procedure -- this job is only its first step. +//* +//* RUN THIS BEFORE THE UPLOAD. IND$FILE allocates an existing dataset +//* DISP=SHR and takes the DCB from its label; it invents one only when +//* the dataset is not there. Allocating it here is what pins +//* BLKSIZE=4096, the block size the image was formatted with. UFSD +//* compares the two at MOUNT and refuses a mismatch with: +//* +//* UFSD062E SUPERBLOCK VALIDATION FAILED FOR +//* +//* Primary extent only, on purpose. The filesystem is addressed by +//* block number within the primary extent, so blocks in a secondary +//* would never be reached. SPACE is counted in blocks of 4096 here, +//* which makes 256 blocks the 1 MB the shipped image occupies. MVS +//* rounds the request up to a track boundary; the few blocks that adds +//* sit past the end of the filesystem, because the superblock -- not +//* the dataset -- is what says how large the filesystem is. +//* +//* Change the dataset name, UNIT and VOLUME to suit. The name below +//* is the one the installation guide uses. An image larger than the +//* shipped one needs a matching SPACE: count it in 4096-byte blocks. +//* +//ALLOC EXEC PGM=IEFBR14 +//WEBROOT DD DSN=HTTPD.@VRM@.WEBROOT.UFS,DISP=(NEW,CATLG), +// UNIT=SYSDA,SPACE=(4096,256), +// DCB=(DSORG=PS,RECFM=U,BLKSIZE=4096) +// diff --git a/smp-todo.md b/smp-todo.md index 638d485..a28d2de 100644 --- a/smp-todo.md +++ b/smp-todo.md @@ -9,7 +9,7 @@ die Messungen vermerkt. Was ein Betreiber tun muss, steht in Dieses Dokument hält nur noch das, was **für httpd offen** ist. -> *Stand: 2026-08-23, gegen `[distribution]` in `project.toml` abgeglichen.* +> *Stand: 2026-08-24, gegen `[distribution]` in `project.toml` abgeglichen.* > **Die frühere Fassung verwies auf `../SMP-COOKBOOK.md` und > `../SMP-INSTALLATION.md`. Beide Dateien gibt es nicht mehr.** Der Spike liegt @@ -50,7 +50,7 @@ haben diesen Job vor ihrem Tag gefahren, httpd noch nicht — der Kommentar in Die Generalprobe aus einem `-dev`-Baum. Ein Testlauf unter `THTP400`, der abbricht oder halb angewendet wird, belegt genau die ID, die das getaggte 4.0.0 braucht — und das CDS lässt sich hinterher nur mit `UCLIN` bereinigen -(`docs/installation.md` §11). +(`docs/installation.md` §12). Mit mbt ist das eine Zeile: `fmid` in `project.toml` auf `TTST400` setzen, `make package`, installieren — **und die Änderung nie committen**. @@ -60,40 +60,55 @@ man etwas dafür tun muss: `@VRM@` kommt aus der Projektversion, und `4.0.0-dev` ergibt `V4R0M0D`, das getaggte `4.0.0` dagegen `V4R0M0`. Der Test lebt also in `HTTPD.V4R0M0D.*`, das Release in `HTTPD.V4R0M0.*`. -Aufräumen danach: UCLIN-Job aus `docs/installation.md` §11 mit `TTST400`, dann +Aufräumen danach: UCLIN-Job aus `docs/installation.md` §12 mit `TTST400`, dann die Datasets löschen, dann `LIST CDS/ACDS SYSMOD(THTP400)` — das ist zugleich O1. -### O3 — UFS-Webroot: SMP kann das nicht ausliefern - -Eine UFS-Disk ist ein `DSORG=PS`, `RECFM=U`, `BLKSIZE=4096`-Binärimage; SMP4 -kennt dafür keinen Elementtyp (es kennt nur -`++MOD/MAC/SRC/MACUPD/SRCUPD/ZAP`). Sie ist außerdem **Site-Inhalt** — ein -erneutes APPLY dürfte sie nie anfassen. - -Heute steht in `docs/installation.md`, dass ohne UFSD nur die `MOD=`-Routen -laufen und `HTTPD044W` kommt. Das ist ehrlich, aber es ist keine Auslieferung. -Offen: - -- [ ] **U1** Image bauen — `ufsd-utils create webroot.img --size 10M`, - `ufsd-utils cp -r static/ webroot.img:/`, danach `ufsd-utils ls -l` - gegenprüfen (Falle T3) -- [ ] **U2** Hochladen: `ufsd-utils upload webroot.img --dsn HTTPD.WEBROOT` -- [ ] **U3** Mount dokumentieren — `UFSDPRMx` gehört **ufsd**, nicht httpd: - `MOUNT DSN(HTTPD.WEBROOT) PATH(/www) MODE(RO)`, oder dynamisch - `/F UFSD,MOUNT DSN=HTTPD.WEBROOT,PATH=/www,MODE=RO`. Kein `++MACUPD` in - ufsds Parmlib -- [ ] **U4** Reihenfolge: **ufsd zuerst**, Dateisystem gemountet, `/www` - existiert — *dann* httpd. Eine Laufzeitabhängigkeit, die SMP nicht - abbilden kann -- [ ] **U5** `make webroot` + CI-Asset `httpd--webroot.img`, mit - SHA256 statt eines Reproduzierbarkeitsversprechens -- [ ] **U6** **Update-Verhalten festlegen** — `.SAMPLE`-Name oder Existenzprüfung. - Ein `ufsd-utils upload --replace` im Update-Job wäre Datenverlust beim - Betreiber - -`[distribution] extra = [...]` kann eine Datei ins Archiv legen, ohne SMP zu -behelligen — das ist der wahrscheinliche Weg für U5, sobald U6 entschieden ist. +### O3 — UFS-Webroot: entschieden und umgesetzt (Issue #252) + +Die Disk fährt **nicht** über SMP und **nicht** über XMIT, sondern als Datei im +Archiv und per **IND$FILE** aufs Ziel. Beides aus einem strukturellen Grund, +nicht aus Werkzeugmangel: + +- SMP4 kennt für ein `DSORG=PS`/`RECFM=U`-Image keinen Elementtyp + (`++MOD/MAC/SRC/MACUPD/SRCUPD/ZAP` ist die ganze Liste), und es ist + **Site-Inhalt** — ein APPLY dürfte es nie anfassen. +- TSO RECEIVE legt sein Ziel selbst an und weigert sich, in ein bestehendes + Dataset zu mischen: ein Transport nur für die Erstinstallation, für etwas, das + der Betreiber ersetzt. (`xmit370` könnte das Format ohnehin nicht — Verzeichnis + → PDS, `--recfm fb|f`, LRECL 80.) + +Umgesetzt: + +- **`make webroot`** baut `dist/httpd-webroot.img` aus `static/` mit einem + gepinnten `ufsd-utils` (1 MB = 256 Blöcke, `--owner`/`--group` gesetzt, damit + im Artefakt keine Build-Maschinen-Userid steht). `package` und `dist` hängen + davon ab, `[distribution] extra` legt es ins Archiv. +- **`samplib/httpwebr`** allokiert `HTTPD.@VRM@.WEBROOT.UFS` + (`SPACE=(4096,256)`, `RECFM=U BLKSIZE=4096`, nur Primärextent). +- **`docs/installation.md` §9** ist der Betreiberweg: allokieren, IND$FILE, + mounten, prüfen, ersetzen. + +Was dabei gemessen wurde und in der Doku steht: + +| Frage | Befund | +|---|---| +| Blockung | RECFM=U puffert über `__fputc` bis BLKSIZE (libc370 `@@fputc.c:24`) → `BLKSIZE(4096)` schreibt exakte 4096er-Blöcke; 1 MB = 256 ganze Blöcke | +| Optionen | Binär ist Default, RECFM dann `U` (ind_file370 `indparse.c:249`) | +| Vorher allokieren | IND$FILE nimmt ein existierendes Dataset DISP=SHR mitsamt DCB (`indmain.c:190`, `:273`) → Upload ohne jede Option, unabhängig vom IND$FILE-Build | +| Falle | dasselbe DISP=SHR schreibt auch in ein *gemountetes* Dataset → `UNMOUNT PATH=/www` ist ein nummerierter Schritt | +| Fehlerbild | BLKSIZE-Mismatch fällt beim MOUNT auf: `UFSD062E` (ufsd `ufsd#sbl.c:69`) | +| Codepage | keine Entscheidung nötig: `http_send_file()` übersetzt UFS-Dateien fest mit IBM-1047 (`src/httpfile.c:70`), unabhängig von `CODEPAGE=` — genau was `ufsd-utils cp` schreibt. Round-Trip byteweise gemessen, inkl. UTF-8; nur `0x85` und `0xF7` überleben ihn nicht | + +Damit ist auch **U6** (Datenverlust beim Update) erledigt, ohne +`.SAMPLE`-Konstruktion: das Dataset trägt die Version im Namen, kann also die +Platte des Betreibers gar nicht treffen. + +Offen geblieben: + +- [ ] **U5-Rest** SHA256 der Release-Assets — mbt veröffentlicht heute keine + Prüfsummen. Das Image trägt einen Erstellungszeitstempel, ist also nicht + reproduzierbar; die Doku sagt das auch so, statt es zu versprechen. ### O4 — Setup-Job: ausliefern oder Handarbeit lassen? @@ -153,7 +168,7 @@ abgearbeitet oder ersetzt: zurückzunehmen. Der Preis ist, dass die FMID selbst permanent wird: `RESTORE` scheitert, weil akzeptiert, und `REJECT`, weil der ACCEPT die MCS aus dem SMPPTS entfernt. Der Weg zurück ist `UCLIN`, beschrieben in - `docs/installation.md` §11. **Service (PTFs) wird weiterhin nie akzeptiert.** + `docs/installation.md` §12. **Service (PTFs) wird weiterhin nie akzeptiert.** --- From 4032125425d571a772adaca5bbe7a581ecf18ccd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mike=20Gro=C3=9Fmann?= Date: Mon, 24 Aug 2026 20:22:45 +0200 Subject: [PATCH 2/4] TODO: reconcile after #252, and name what actually blocks the 4.0.0 tag The ranking said nothing open blocks 4.0.0, which was true and unhelpful: the release work is in smp-todo.md (the FMID check and the dry run), not in the tracker at all. Say so, and record that O3 closed with #252. --- TODO.md | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/TODO.md b/TODO.md index de659e6..fbcad28 100644 --- a/TODO.md +++ b/TODO.md @@ -11,9 +11,9 @@ stops. `CLAUDE.md` forbids a task list in itself because a copy of a tracker is wrong the first time someone closes something, and the only defence that works is to hold nothing worth going stale. -*Last reconciled against the tracker: 2026-08-23, three issues open (#250 filed -that day; #237 closed by PR #249, and before it #245 by PR #248, #233 by -PR #244, #242 by PR #246 and #243 by PR #247).* +*Last reconciled against the tracker: 2026-08-24, three issues open (#252 filed +and closed the same day by PR #253; before it #250, #237 by PR #249, #245 by +PR #248, #233 by PR #244, #242 by PR #246 and #243 by PR #247).* --- @@ -39,6 +39,12 @@ exit that could end a refused start `CC 0000`; nothing in that thread is open, and `docs/messages.md` §*A refused start* is where the resulting contract is written down. +**What is left before the 4.0.0 tag is not in the tracker at all** — it is the +install package, and [`smp-todo.md`](smp-todo.md) holds it: the `THTP400` check +against CDS and ACDS on both stands (O1), and the dry run under the throwaway +FMID (O2). O3 closed with #252, which put the document root in the archive as a +UFS image; what remains of it is a checksum for the release assets. + --- ### 1 · #250 — does a LINKed module survive in the Job Pack Area? From cfe697ab27b3f07b9d16e1ae84fc1480ea3b8481 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mike=20Gro=C3=9Fmann?= Date: Mon, 24 Aug 2026 20:27:51 +0200 Subject: [PATCH 3/4] Rebuild the webroot image when a file leaves static/, not only when one changes The prerequisite list was files only. Adding one is covered -- make expands the wildcard when it parses the Makefile, so the new file is in the list and is newer than the image -- but deleting one is not: the image stays newer than everything still there, and the archive would go on shipping the file that was removed. A directory's mtime moves when an entry is added or removed, so the directories join the file list. Measured both ways with the image backdated so the comparison is not decided by whole-second timestamps: adding rebuilds, deleting rebuilds and drops the file from the image, an unchanged tree does nothing. Also two notes rather than behaviour: why the webroot dataset is the one name with a fourth qualifier, and that the emulator types IND$FILE into whatever field the cursor is on -- it does not find a command line for you. --- Makefile | 7 ++++++- docs/installation.md | 8 ++++++-- project.toml | 6 +++++- 3 files changed, 17 insertions(+), 4 deletions(-) diff --git a/Makefile b/Makefile index a0f922b..6096b14 100644 --- a/Makefile +++ b/Makefile @@ -58,7 +58,12 @@ $(UFSD_UTILS_BIN): webroot: $(WEBROOT_IMG) -$(WEBROOT_IMG): $(shell find $(WEBROOT_SRC) -type f) $(WEBROOT_TOOL_DEP) +# The directories are prerequisites next to the files: a *new* file is picked +# up either way, because make expands the wildcard when it parses this and the +# file is then newer than the image -- but a *deleted* one leaves an image that +# is newer than everything still there, and it would go on shipping the file. +# A directory's mtime moves when an entry is added or removed, which covers it. +$(WEBROOT_IMG): $(shell find $(WEBROOT_SRC) -type f -o -type d) $(WEBROOT_TOOL_DEP) $(E) "[webroot] $(notdir $@) ($(WEBROOT_SIZE), from $(WEBROOT_SRC)/)" @mkdir -p $(DISTDIR) @rm -f $@ diff --git a/docs/installation.md b/docs/installation.md index 5a89863..0aa971a 100644 --- a/docs/installation.md +++ b/docs/installation.md @@ -664,8 +664,12 @@ Transfer(Direction=send, Leave `Recfm`, `Lrecl`, `Blksize` and the space fields **unset**: the dataset exists, so its label decides, and an emulator's allocation defaults are not -worth auditing. Position the session on a cleared TSO **READY** screen (ISPF -option 6 also works) before starting the transfer. +worth auditing. + +Either way the session has to sit on an input field that can accept the command +before the transfer starts — a cleared TSO **READY** screen, or ISPF option 6. +The emulator types `IND$FILE` into whatever field the cursor is on; it does not +find one for you. If you would rather let IND$FILE create the dataset, it needs to be told everything the allocation job otherwise says: diff --git a/project.toml b/project.toml index 3dc849c..dfe7783 100644 --- a/project.toml +++ b/project.toml @@ -318,7 +318,11 @@ headers = [ # # Every value is a dataset name; the ddname SMP addresses it by is always the # last qualifier, so LINKLIB, SAMPLIB, HTTPLOAD and AHTTPLOD fall out of the -# names. @VRM@ becomes V4R0M0 at package time (V4R0M0D while the version still +# names. The webroot is the one name with a fourth qualifier +# (HTTPD.@VRM@.WEBROOT.UFS) and it is deliberate: no ddname is derived from it +# because nothing here allocates or receives it, and .UFS says at a glance that +# the dataset is a filesystem image rather than a library. Do not normalise it +# into the three-qualifier pattern -- it is not part of that scheme. @VRM@ becomes V4R0M0 at package time (V4R0M0D while the version still # carries -dev). ufsd/ftpd spell these LOAD / ALOD, which for # a five-letter product would be nine characters; HTTPLOAD/AHTTPLOD keeps the # suffix and drops the letter, rather than keeping HTTPD and truncating the From 8051007f77fb29a0692632d670f84ea8c90a7676 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mike=20Gro=C3=9Fmann?= Date: Mon, 24 Aug 2026 20:55:21 +0200 Subject: [PATCH 4/4] Build the webroot image under build/, not dist/ dist/ is not a scratch directory: the release workflow publishes every file in it as a GitHub Release asset (`gh release create ... dist/*`). An image built there therefore shipped twice -- once inside the archive, where it belongs beside the README and the jobs, and once on its own as `httpd-webroot.img`, a name with no version in it sitting between httpd--load.xmit and httpd--dist.zip. Nothing else moves: `extra` takes the basename, so the file inside the archive is called what it was called before, and the installation guide is unchanged. --- Makefile | 11 +++++++++-- project.toml | 21 +++++++++++++-------- smp-todo.md | 2 +- 3 files changed, 23 insertions(+), 11 deletions(-) diff --git a/Makefile b/Makefile index 6096b14..a2b152b 100644 --- a/Makefile +++ b/Makefile @@ -27,7 +27,14 @@ include $(MBT_ROOT)/mk/mbt.mk # Owner and group are metadata: UFSD enforces the MOUNT's OWNER(), not the # inode owner. They are named anyway, so a release artifact does not carry # whatever userid happened to build it. -WEBROOT_IMG := $(DISTDIR)/httpd-webroot.img +# +# It is built under $(BUILDDIR) and NOT under $(DISTDIR), which is not a +# detail: the release workflow publishes `dist/*` as GitHub Release assets, so +# an image sitting there would appear beside the versioned artifacts under a +# name carrying no version at all -- and as a second copy of what the archive +# already holds. The archive is where it belongs, next to the README and the +# jobs an operator needs with it. +WEBROOT_IMG := $(BUILDDIR)/webroot/httpd-webroot.img WEBROOT_SRC := static WEBROOT_SIZE := 1M WEBROOT_OWNER := IBMUSER @@ -65,7 +72,7 @@ webroot: $(WEBROOT_IMG) # A directory's mtime moves when an entry is added or removed, which covers it. $(WEBROOT_IMG): $(shell find $(WEBROOT_SRC) -type f -o -type d) $(WEBROOT_TOOL_DEP) $(E) "[webroot] $(notdir $@) ($(WEBROOT_SIZE), from $(WEBROOT_SRC)/)" - @mkdir -p $(DISTDIR) + @mkdir -p $(dir $@) @rm -f $@ $(Q)$(UFSD_UTILS) create $@ --size $(WEBROOT_SIZE) --blksize 4096 \ --owner $(WEBROOT_OWNER) --group $(WEBROOT_GROUP) > /dev/null diff --git a/project.toml b/project.toml index dfe7783..f86692b 100644 --- a/project.toml +++ b/project.toml @@ -342,14 +342,19 @@ headers = [ readme = "docs/installation.md" # The webroot disk, put in the archive as `httpd-webroot.img` -- `extra` takes -# the basename, so the path here is where `make webroot` writes it and the name -# an operator sees is the file's own. Listed rather than generated by mbtdist: -# the image is not a dataset the install job creates, and nothing in the SYSMOD -# or the RECEIVE plan knows about it. `make package` and `make dist` both -# depend on the target that builds it, because a missing `extra` file is a hard -# error here -- deliberately: an archive quietly missing the webroot would look -# complete and serve nothing on /. -extra = ["dist/httpd-webroot.img"] +# the basename, so the path here is only where `make webroot` writes it and the +# name an operator sees is the file's own. Listed rather than generated by +# mbtdist: the image is not a dataset the install job creates, and nothing in +# the SYSMOD or the RECEIVE plan knows about it. `make package` and `make dist` +# both depend on the target that builds it, because a missing `extra` file is a +# hard error here -- deliberately: an archive quietly missing the webroot would +# look complete and serve nothing on /. +# +# Under build/ rather than dist/ on purpose. The release workflow publishes +# `dist/*` as Release assets, so an image there would show up beside the +# versioned artifacts under a version-less name, duplicating what the archive +# already carries. +extra = ["build/webroot/httpd-webroot.img"] [distribution.smp] # One FMID per minor release, spent exactly once: it names a functional level, diff --git a/smp-todo.md b/smp-todo.md index a28d2de..0db5839 100644 --- a/smp-todo.md +++ b/smp-todo.md @@ -80,7 +80,7 @@ nicht aus Werkzeugmangel: Umgesetzt: -- **`make webroot`** baut `dist/httpd-webroot.img` aus `static/` mit einem +- **`make webroot`** baut `build/webroot/httpd-webroot.img` aus `static/` mit einem gepinnten `ufsd-utils` (1 MB = 256 Blöcke, `--owner`/`--group` gesetzt, damit im Artefakt keine Build-Maschinen-Userid steht). `package` und `dist` hängen davon ab, `[distribution] extra` legt es ins Archiv.