fix(apiserver): map pidinfo from job start into the job model (interactive_job_url)

The worker packs the interactive session URL (ttyd/XJOB) into the start
report's exdata as {"url": ...}; the controller stores it on
job->job.pidinfo->exdata and the capnp Job wire carries pidinfo (pidinfo@9,
JobPIDInfo.exdata@3) — but apiserver's cJobToModel never mapped pidinfo, so
models.JobInfo.InteractiveJobURL (declared only in the query-field whitelist
JobAdditionalFields) had no producer and interactive_job_url was permanently
empty in API responses. xcecli's waitForInteractiveURL/openBrowser flow could
never fire.

- models.JobInfo: add InteractiveJobURL (json interactive_job_url, matching
  the JobAdditionalFields whitelist name).
- convert_pure.go: rawPidInfo (pids/pgids/exdata) + interactiveURLFromExdata;
  rawJobToModel marshals pids/pgids into PIDInfo (pid_info is a JSONBJFields
  entry, parsed by processJobData) and promotes the exdata URL to
  InteractiveJobURL.
- convert.go: cJobToModel extracts job.pidinfo via pidinfo_pid/pidinfo_pgid
  cgo helpers (CGO cannot index the C arrays directly).

Cluster-verified end to end on t1 (fresh apiserver deployed): tty job
(xcecli -I --tty equivalent, exdata {"interactive":1}) now returns
interactive_job_url=http://<worker>:9000 and a populated pid_info; non-tty
jobs return ""/no pid_info. All daemons healthy, zero errors in window.

Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
lei.chen 2026-10-14 09:20:27 +00:00
parent f661987a60
commit 74121eb72e
4 changed files with 159 additions and 1 deletions

View file

@ -526,6 +526,17 @@ type rawRunUsage struct {
peakCPU float64
}
// rawPidInfo mirrors the C job_pidinfo_t fields used by the Go model —
// the worker-reported process bookkeeping a job start carries. Exdata is
// the worker's start_exdata JSON; for interactive jobs (TTY/XJOB/DISPLAY,
// xce_job_type in src/include/xce/types.h) the worker packs the session
// URL into it as {"url": ...} (generate_exdata, xcecd_exdata.c).
type rawPidInfo struct {
pids []int
pgids []int
exdata string
}
// rawJobData mirrors the C xce_job_t fields used by the Go model.
type rawJobData struct {
jobID int64
@ -538,7 +549,8 @@ type rawJobData struct {
submit *rawSubmitData
execHosts []rawHostSlot
pendReason *rawPendReason
runUsage *rawRunUsage // nil until the job's first usage report
runUsage *rawRunUsage // nil until the job's first usage report
pidInfo *rawPidInfo // nil until the worker's start report
}
// rawWorkerData mirrors the C xce_worker_t fields used by the Go model.
@ -574,6 +586,26 @@ type rawResourceData struct {
interval int
}
// interactiveURLFromExdata extracts the interactive session URL from a job
// start's exdata JSON. The worker packs it as {"url": ...}
// (generate_exdata, xcecd_exdata.c); anything else — non-JSON, other keys,
// an empty/non-string url — yields "". The URL reaches the apiserver ONLY
// via this path: job_pidinfo_t.exdata rides the capnp Job wire (pidinfo@9)
// but was previously dropped by the conversion, leaving the API's
// interactive_job_url permanently empty and xcecli's openBrowser flow dead.
func interactiveURLFromExdata(exdata string) string {
if exdata == "" {
return ""
}
var obj struct {
URL string `json:"url"`
}
if err := json.Unmarshal([]byte(exdata), &obj); err != nil {
return ""
}
return obj.URL
}
// rawJobToModel maps a rawJobData to a models.JobInfo.
// This pure-Go function contains the field-mapping logic and is tested without CGO.
func rawJobToModel(r rawJobData) *models.JobInfo {
@ -676,6 +708,20 @@ func rawJobToModel(r rawJobData) *models.JobInfo {
info.AvgCPUEfficiency = u.avgCPUEfficiency
info.PeakCPUEfficiency = u.peakCPUEfficiency
}
if p := r.pidInfo; p != nil {
// pid_info is a JSONBJFields entry: processJobData parses the JSONB
// string before the row leaves the API, so clients see an object
// with pids/pgids. The interactive URL is promoted to its own
// top-level field — it is what xcecli's waitForInteractiveURL and
// the --browse flow consume.
if data, err := json.Marshal(struct {
Pids []int `json:"pids,omitempty"`
Pgids []int `json:"pgids,omitempty"`
}{Pids: p.pids, Pgids: p.pgids}); err == nil {
info.PIDInfo = models.Jsonb{String: string(data)}
}
info.InteractiveJobURL = interactiveURLFromExdata(p.exdata)
}
return info
}