From 0d8680cd54934d6066ad397da2f3502e532dc619 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mike=20Gro=C3=9Fmann?= Date: Fri, 7 Aug 2026 19:56:52 +0200 Subject: [PATCH 1/3] Return JES2 job timestamps as ISO 8601 UTC The JSON carried two timestamps per job that came from different sources and disagreed. start_stamp is the epoch httpjes2.c computes explicitly: tzadjust = httpd->tzoffset * -1; /* convert local time to GMT */ start_time = j->start_time + tzadjust; which is correct now that tzoffset defaults to the system offset (#150). start_display then ran that same instant through ctime64(), which converts through crt->crttzoff -- the *task's* timezone, taken from CVTTZ -- shifting it a second time by an unrelated offset. On the reference system a job that started at 17:25:18 UTC was reported as "Fri Aug 7 05:25:18 2026": neither UTC, nor the system's local time, nor the caller's. Beyond the double shift, a local time is the wrong thing for an API to return. The server cannot know the caller's timezone, so any local rendering is unlabelled and the client cannot tell which zone it got. Returning an unambiguous instant and letting the client localize is what zowe and every browser already do. That is also the convention mvsMF settled on elsewhere: ussapi.c formats mtime with mgmtime64() and a literal "Z", and docs/endpoints/uss/list.md and console/hardcopy-log.md both specify ISO 8601 UTC. Those timestamps never had this class of bug because they never touch localtime. So both display fields now render through gmtime64_r() as "2026-08-07T17:25:18Z", via one shared jestime() rather than two open-coded ctime64() calls that had already drifted apart. A zero timestamp still yields "...", as before. start_stamp is unchanged -- it is the machine-readable epoch and is correct. Note for the reader: /jes/status is served by httpjes2.c, not jesst.c. Both define main(), so only httpjes2.o can be in the HTTPJES2 module and jesst.o is never autocalled; jesst.c looks like dead code. It is changed here anyway so the two do not drift again if it is ever wired up, but it is a candidate for deletion alongside the rest of the deprecated JES CGI. Refs #145 --- include/jestime.h | 21 +++++++++++++++++ src/httpjes2.c | 18 +++++---------- src/jesst.c | 18 +++++---------- src/jestime.c | 57 +++++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 90 insertions(+), 24 deletions(-) create mode 100644 include/jestime.h create mode 100644 src/jestime.c diff --git a/include/jestime.h b/include/jestime.h new file mode 100644 index 0000000..260a005 --- /dev/null +++ b/include/jestime.h @@ -0,0 +1,21 @@ +/* JESTIME.H +** Render a JES2 job timestamp for a JSON response. +** +** Shared by jesst.c (/jes/status, /jes/ddlist) and httpjes2.c, which emitted +** the same field two ways and both wrong -- see src/jestime.c for why. +*/ +#ifndef JESTIME_H +#define JESTIME_H + +#include + +/* Longest output is "2026-08-07T17:25:18Z" plus NUL. */ +#define JESTIME_LEN 24 + +/* Format *t as an ISO 8601 instant in UTC ("2026-08-07T17:25:18Z") into out. +** A zero timestamp (a job that has not started or ended) yields "...", which +** is what the JSON carried before and what clients already tolerate. +** out must hold at least JESTIME_LEN bytes. */ +void jestime(const time64_t *t, char *out, unsigned outlen); + +#endif /* JESTIME_H */ diff --git a/src/httpjes2.c b/src/httpjes2.c index 84a71ac..fb66489 100644 --- a/src/httpjes2.c +++ b/src/httpjes2.c @@ -1,6 +1,7 @@ /* HTTPJES2.C - CGI Program, REST style CGI program for access of JES2 resources */ #include "httpd.h" #include "clibjes2.h" /* JES prototypes */ +#include "jestime.h" /* ISO 8601 UTC job timestamps */ #include "clibcp.h" /* JES checkpoint struct */ #define httpx (httpd->httpx) @@ -130,6 +131,7 @@ do_status(HTTPD *httpd, HTTPC *httpc, const char *jobname, const char *jobid, in char jesinfo[20] = "unknown"; time64_t start_time; time64_t end_time; + char tbuf[JESTIME_LEN]; const char *smfid; httpsecs(&start); @@ -317,21 +319,13 @@ do_status(HTTPD *httpd, HTTPC *httpc, const char *jobname, const char *jobid, in #else rc = http_printf(httpc, " \"start_stamp\": \"%llu\",\n", start_time); if (rc < 0) goto quit; - if (__64_cmp_u32(&start_time, 0) != __64_EQUAL) { - rc = http_printf(httpc, " \"start_display\": \"%-24.24s\",\n", ctime64(&start_time) ); - } - else { - rc = http_printf(httpc, " \"start_display\": \"...\",\n"); - } + jestime(&start_time, tbuf, sizeof(tbuf)); + rc = http_printf(httpc, " \"start_display\": \"%s\",\n", tbuf); if (rc < 0) goto quit; rc = http_printf(httpc, " \"end_stamp\": \"%llu\",\n", end_time); if (rc < 0) goto quit; - if (__64_cmp_u32(&end_time, 0) != __64_EQUAL) { - rc = http_printf(httpc, " \"end_display\": \"%-24.24s\",\n", ctime64(&end_time) ); - } - else { - rc = http_printf(httpc, " \"end_display\": \"...\",\n" ); - } + jestime(&end_time, tbuf, sizeof(tbuf)); + rc = http_printf(httpc, " \"end_display\": \"%s\",\n", tbuf); if (rc < 0) goto quit; #endif rc = do_status_ddlist(httpd, httpc, j, " "); diff --git a/src/jesst.c b/src/jesst.c index 64ec91b..1c4ee08 100644 --- a/src/jesst.c +++ b/src/jesst.c @@ -1,6 +1,7 @@ /* JESST.C - CGI Program, display JOE from JES checkpoint */ #include "httpd.h" #include "clibjes2.h" /* JES prototypes */ +#include "jestime.h" /* ISO 8601 UTC job timestamps */ #define httpx (httpd->httpx) @@ -21,6 +22,7 @@ int main(int argc, char **argv) double start = 0.0; double end = 0.0; unsigned n; + char tbuf[JESTIME_LEN]; #if 0 struct { unsigned short len; @@ -123,19 +125,11 @@ int main(int argc, char **argv) } #else printf(" \"start_stamp\": \"%llu\",\n", j->start_time64.u64 ); - if (__64_cmp_u32(&j->start_time64, 0) != __64_EQUAL) { - printf(" \"start_display\": \"%-24.24s\",\n", ctime64(&j->start_time64) ); - } - else { - printf(" \"start_display\": \"...\",\n"); - } + jestime(&j->start_time64, tbuf, sizeof(tbuf)); + printf(" \"start_display\": \"%s\",\n", tbuf); printf(" \"end_stamp\": \"%llu\",\n", j->end_time64.u64); - if (__64_cmp_u32(&j->end_time64, 0) != __64_EQUAL) { - printf(" \"end_display\": \"%-24.24s\",\n", ctime64(&j->end_time64) ); - } - else { - printf(" \"end_display\": \"...\",\n" ); - } + jestime(&j->end_time64, tbuf, sizeof(tbuf)); + printf(" \"end_display\": \"%s\",\n", tbuf); #endif print_dd(httpd, j, " "); printf(" }%s\n", (n+1) < count ? ",":""); diff --git a/src/jestime.c b/src/jestime.c new file mode 100644 index 0000000..89b4008 --- /dev/null +++ b/src/jestime.c @@ -0,0 +1,57 @@ +/* JESTIME.C +** Render a JES2 job timestamp for a JSON response, as an ISO 8601 instant in +** UTC. +** +** The field used to be produced by ctime64(), which converts through +** crt->crttzoff -- the *task's* timezone, filled in from the system's CVTTZ +** when nothing else set it. Two things were wrong with that. +** +** It applied a second, unrelated offset. The epoch value beside it comes from +** the JES2 conversion, which uses httpd->tzoffset; running the same instant +** through ctime64() then shifted it again by the CRT's offset. On the +** reference system (CVTTZ = -5h) a job that started at 17:25:18 UTC was +** reported as "Fri Aug 7 05:25:18 2026" -- neither UTC, nor the system's local +** time, nor the caller's. See issue #145 for how the two offsets came to +** disagree. +** +** And a local time is the wrong thing for an API to return at all. The server +** cannot know the caller's timezone, so any local rendering is unlabelled and +** unusable -- the caller cannot tell which zone it is in. Returning an +** unambiguous instant and letting the client localize is what zowe and every +** browser already do, and it is the convention mvsMF settled on elsewhere: +** ussapi.c formats mtime with mgmtime64() and a literal "Z" (see +** mvsmf docs/endpoints/uss/list.md). Those timestamps never had this class of +** bug precisely because they never touch localtime. +** +** So: gmtime64_r(), never ctime64() or localtime64(). That removes the +** dependency on per-task timezone state rather than trying to keep two copies +** of it in step. +*/ +#include "jestime.h" +#include "clib64.h" +#include + +void +jestime(const time64_t *t, char *out, unsigned outlen) +{ + struct tm tm; + + if (!out || outlen == 0) return; + out[0] = 0; + if (!t) return; + + /* a job that has not started (or has not ended) carries a zero time */ + if (__64_cmp_u32((time64_t *)t, 0) == __64_EQUAL) { + snprintf(out, outlen, "..."); + return; + } + + if (!gmtime64_r(t, &tm)) { + snprintf(out, outlen, "..."); + return; + } + + snprintf(out, outlen, "%04d-%02d-%02dT%02d:%02d:%02dZ", + tm.tm_year + 1900, tm.tm_mon + 1, tm.tm_mday, + tm.tm_hour, tm.tm_min, tm.tm_sec); +} From d09375e51f3c7ec8beda3c6039accb6294b1e68b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mike=20Gro=C3=9Fmann?= Date: Fri, 7 Aug 2026 20:33:01 +0200 Subject: [PATCH 2/3] Match z/OSMF's millisecond form in the ISO 8601 timestamps Real z/OSMF emits the exec-* fields as "exec-started":"2018-11-03T09:05:18.010Z" so jestime() now renders ".000Z" rather than "Z". JES2 gives second resolution here (start_time64 is a time64_t), so the fraction is always zero -- which is what z/OSMF itself reports for exec-submitted anyway. httpjes2 is the reference mvsMF's own implementation gets compared against, so the two formats should be identical rather than merely similar. --- include/jestime.h | 8 +++++--- src/jestime.c | 9 ++++++++- 2 files changed, 13 insertions(+), 4 deletions(-) diff --git a/include/jestime.h b/include/jestime.h index 260a005..7d21160 100644 --- a/include/jestime.h +++ b/include/jestime.h @@ -9,10 +9,12 @@ #include -/* Longest output is "2026-08-07T17:25:18Z" plus NUL. */ -#define JESTIME_LEN 24 +/* Longest output is "2026-08-07T17:25:18.000Z" plus NUL. */ +#define JESTIME_LEN 32 -/* Format *t as an ISO 8601 instant in UTC ("2026-08-07T17:25:18Z") into out. +/* Format *t as an ISO 8601 instant in UTC ("2026-08-07T17:25:18.000Z") into +** out -- the shape real z/OSMF uses for exec-submitted / exec-started / +** exec-ended, so this API and mvsMF's stay directly comparable. ** A zero timestamp (a job that has not started or ended) yields "...", which ** is what the JSON carried before and what clients already tolerate. ** out must hold at least JESTIME_LEN bytes. */ diff --git a/src/jestime.c b/src/jestime.c index 89b4008..0e3347e 100644 --- a/src/jestime.c +++ b/src/jestime.c @@ -51,7 +51,14 @@ jestime(const time64_t *t, char *out, unsigned outlen) return; } - snprintf(out, outlen, "%04d-%02d-%02dT%02d:%02d:%02dZ", + /* ".000" rather than no fraction: this is the shape real z/OSMF emits for + ** exec-submitted / exec-started / exec-ended, + ** "exec-started":"2018-11-03T09:05:18.010Z" + ** and httpjes2 is the reference mvsMF's own implementation is compared + ** against, so the two should be directly comparable. JES2 gives us second + ** resolution here (start_time64 is a time64_t), so the fraction is always + ** zero -- which is what z/OSMF itself reports for exec-submitted. */ + snprintf(out, outlen, "%04d-%02d-%02dT%02d:%02d:%02d.000Z", tm.tm_year + 1900, tm.tm_mon + 1, tm.tm_mday, tm.tm_hour, tm.tm_min, tm.tm_sec); } From 1f8a840faa0a730d80f2b58ca408cdf99cc6cadf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mike=20Gro=C3=9Fmann?= Date: Fri, 7 Aug 2026 20:51:14 +0200 Subject: [PATCH 3/3] Convert JES2 timestamps with the system offset, not the configured one The "Z" on these fields is a promise, and TZOFFSET could break it. The epoch was computed as tzadjust = httpd->tzoffset * -1; so an operator who set TZOFFSET to their own zone rather than the machine's skewed every timestamp the API reports. On the reference system, TZOFFSET +02:00 against a CVTTZ of -5h put a job that ran at 17:25:18 UTC at 10:25:18 UTC -- seven hours out, and labelled Z. Converting JES2's stored local timestamps to UTC needs the offset the machine actually runs at, which is a fact rather than a setting. __tzget() returns crt->crttzoff for the calling task, which cgistart's tzset() filled in from TZ or the system's CVTTZ. That is immune to what the Parmlib says, which is the point: the field is UTC by contract. httpd->tzoffset keeps its job -- httpd's own local-time rendering, DISPLAY TIME and the SMF records. The Date: header was never affected; it goes through gmtime64() on a time64() value, and measuring it against a UTC clock gives the same second. Leaves jesst.c alone. It applies no offset at all, so it is a third behaviour again -- but it defines its own main(), is therefore never autocalled into HTTPJES2, and looks like dead code. Deleting it is the right fix, not adjusting it. Refs #145 --- src/httpjes2.c | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/src/httpjes2.c b/src/httpjes2.c index fb66489..a023f01 100644 --- a/src/httpjes2.c +++ b/src/httpjes2.c @@ -3,6 +3,10 @@ #include "clibjes2.h" /* JES prototypes */ #include "jestime.h" /* ISO 8601 UTC job timestamps */ #include "clibcp.h" /* JES checkpoint struct */ + +/* libc370 ships __tzget() (src/clib/@@tzget.c) but declares it in no header + -- see libc370 #70. Returns crt->crttzoff for the calling task. */ +extern int __tzget(void); #define httpx (httpd->httpx) @@ -136,7 +140,19 @@ do_status(HTTPD *httpd, HTTPC *httpc, const char *jobname, const char *jobid, in httpsecs(&start); - tzadjust = httpd->tzoffset * -1; /* change sign as we want to convert local time to GMT */ + /* JES2 stores its checkpoint timestamps in system local time, so they need + ** the system's offset to become UTC -- and specifically the SYSTEM's, not + ** httpd->tzoffset. That field is a configured preference for httpd's own + ** local-time rendering (Date: is unaffected, it goes through gmtime64), and + ** an operator who sets TZOFFSET to their own zone rather than the machine's + ** would otherwise skew every timestamp this API reports. These fields are + ** labelled "Z", so they have to be UTC whatever the Parmlib says. + ** + ** __tzget() returns crt->crttzoff for this task, which cgistart's tzset() + ** filled in from TZ or the system's CVTTZ -- a fact about the machine, not a + ** setting. Sign: crttzoff is seconds east of UTC (negative west), and + ** UTC = local - offset, hence the * -1 to give an addend. */ + tzadjust = __tzget() * -1; /* select filtering criteria */ if (jobname && !jobid) {