diff --git a/include/jestime.h b/include/jestime.h new file mode 100644 index 0000000..7d21160 --- /dev/null +++ b/include/jestime.h @@ -0,0 +1,23 @@ +/* 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:18.000Z" plus NUL. */ +#define JESTIME_LEN 32 + +/* 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. */ +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..a023f01 100644 --- a/src/httpjes2.c +++ b/src/httpjes2.c @@ -1,7 +1,12 @@ /* 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 */ + +/* 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) @@ -130,11 +135,24 @@ 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); - 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) { @@ -317,21 +335,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..0e3347e --- /dev/null +++ b/src/jestime.c @@ -0,0 +1,64 @@ +/* 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; + } + + /* ".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); +}