fix(apiserver): send the requested signal, force kill and checkpoint options (bug-1940)

Cause: ControlJob (backend/clib.go) looked its content argument up as a
signal name, but the job control handlers pass a JSON JobControlEvent, so
the lookup always missed and every kill sent TERM (stop TSTP, resume
CONT); sig.force and ckpt.kill were hard-coded to 0 and the checkpoint
method was never set.

Fix: new pure jobCtrlParams (backend/convert_pure.go) decodes the event
(or a bare signal name/number), validates the signal against
JobSignalMapTable and returns an error instead of falling back, and maps
force_kill to xce_signal_t.force (kill) or xce_ckpt_options_t.kill
(checkpoint) plus the checkpoint method. ControlJob fills the C structs
from it. Tests: backend/jobctrl_params_test.go.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Rongsong Shen 2026-10-10 08:31:16 +00:00
parent 8f164b8205
commit 4d2e80c4bb
Signed by untrusted user: rshen
GPG key ID: 80533536B0EB651C
3 changed files with 236 additions and 25 deletions

View file

@ -7,6 +7,7 @@ import (
"fmt"
"regexp"
"strconv"
"strings"
"time"
)
@ -215,6 +216,107 @@ func jobCtrlUnionDiscriminant(action int) int {
return 0 // JobControl_u_siginfo (stop/resume/kill all signal the job)
}
// jobCtrlArgs is what ControlJob (clib.go) puts into xce_jobctrl_t's union
// branch: Signo/Force fill xce_signal_t (stop/resume/kill), CkptKill/Method
// fill xce_ckpt_options_t (checkpoint).
type jobCtrlArgs struct {
Signo int
Force int
CkptKill int
Method string
}
// jobCtrlParams maps a job action and ControlJob's content argument to the
// values sent to the controller. content is either the JSON-encoded
// models.JobControlEvent the job control handlers pass (controllers/jobs.go)
// or, for older callers, a bare signal name or number; empty means the
// action's default signal.
//
// bug-1940: content used to be looked up only as a signal name, so the JSON
// event always missed and every kill sent TERM with force 0, and checkpoint
// kill/method were never set. An unknown signal is now an error instead of a
// silent fallback to the default.
func jobCtrlParams(action int, content string) (jobCtrlArgs, error) {
var (
args jobCtrlArgs
signo int
forceKill bool
)
content = strings.TrimSpace(content)
if strings.HasPrefix(content, "{") {
var evt models.JobControlEvent
if err := json.Unmarshal([]byte(content), &evt); err != nil {
return args, fmt.Errorf("invalid job control content: %w", err)
}
if evt.Signal != 0 && !isKnownSignal(evt.Signal) {
return args, fmt.Errorf("unsupported signal: %d", evt.Signal)
}
signo, forceKill, args.Method = evt.Signal, evt.ForceKill, evt.Method
} else if content != "" {
n, err := parseSignal(content)
if err != nil {
return args, err
}
signo = n
}
switch action {
case models.JOB_STOP, models.JOB_RESUME, models.JOB_KILL:
if signo == 0 {
signo = defaultActionSignal(action)
}
args.Signo = signo
if action == models.JOB_KILL && forceKill {
args.Force = 1
}
case models.JOB_CHKPNT:
if forceKill {
args.CkptKill = 1
}
}
return args, nil
}
// defaultActionSignal is the signal a stop/resume/kill sends when the caller
// gave none.
func defaultActionSignal(action int) int {
switch action {
case models.JOB_STOP:
return models.JobSignalMapTable["TSTP"] // 20
case models.JOB_RESUME:
return models.JobSignalMapTable["CONT"] // 18
}
return models.JobSignalMapTable["TERM"] // 15
}
// isKnownSignal reports whether n is one of models.JobSignalMapTable's values.
func isKnownSignal(n int) bool {
for _, v := range models.JobSignalMapTable {
if v == n {
return true
}
}
return false
}
// parseSignal accepts a signal number or name (case-insensitive, with or
// without the SIG prefix) and returns its number.
func parseSignal(s string) (int, error) {
if n, err := strconv.Atoi(s); err == nil {
if !isKnownSignal(n) {
return 0, fmt.Errorf("unsupported signal: %s", s)
}
return n, nil
}
name := strings.TrimPrefix(strings.ToUpper(s), "SIG")
if n, ok := models.JobSignalMapTable[name]; ok {
return n, nil
}
return 0, fmt.Errorf("unsupported signal: %s", s)
}
// resolveExecUser returns the effective exec user for a job submission: the
// caller-specified ExecUser if present, otherwise the submitting user
// (bug-1780: an unset execuser in the startjob request reaches xcecd as