mirror of
https://github.com/multica-ai/multica.git
synced 2026-07-27 21:33:41 +02:00
* fix(daemon): bound pre-start task preparation Co-authored-by: multica-agent <github@multica.ai> * fix(daemon): isolate pre-start env preparation Run execution-environment Prepare and Reuse in a killable helper process so a timed-out attempt cannot keep writing after retry. Add FIFO lifecycle and squad Stage retry regression coverage. Co-authored-by: multica-agent <github@multica.ai> * fix(daemon): terminate Windows prepare process trees Assign the pre-start helper to a kill-on-close Job Object before releasing its request, wait for all job members to exit on cancellation, and add a Windows runtime regression job. Co-authored-by: multica-agent <github@multica.ai> * ci: target Windows prepare tree regression Co-authored-by: multica-agent <github@multica.ai> --------- Co-authored-by: Bohan-J <bohan@devv.ai> Co-authored-by: multica-agent <github@multica.ai>
124 lines
3.1 KiB
Go
124 lines
3.1 KiB
Go
//go:build windows
|
|
|
|
package execenv
|
|
|
|
import (
|
|
"fmt"
|
|
"os/exec"
|
|
"time"
|
|
"unsafe"
|
|
|
|
"golang.org/x/sys/windows"
|
|
)
|
|
|
|
type preparationProcessController struct {
|
|
job windows.Handle
|
|
}
|
|
|
|
// jobObjectBasicAccountingInformation mirrors JOBOBJECT_BASIC_ACCOUNTING_INFORMATION.
|
|
// x/sys exposes QueryInformationJobObject and the information-class constant,
|
|
// but not this result struct.
|
|
type jobObjectBasicAccountingInformation struct {
|
|
TotalUserTime int64
|
|
TotalKernelTime int64
|
|
ThisPeriodTotalUserTime int64
|
|
ThisPeriodTotalKernelTime int64
|
|
TotalPageFaultCount uint32
|
|
TotalProcesses uint32
|
|
ActiveProcesses uint32
|
|
TotalTerminatedProcesses uint32
|
|
}
|
|
|
|
func newPreparationProcessController(_ *exec.Cmd) (*preparationProcessController, error) {
|
|
job, err := windows.CreateJobObject(nil, nil)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
info := windows.JOBOBJECT_EXTENDED_LIMIT_INFORMATION{}
|
|
info.BasicLimitInformation.LimitFlags = windows.JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE
|
|
if _, err := windows.SetInformationJobObject(
|
|
job,
|
|
windows.JobObjectExtendedLimitInformation,
|
|
uintptr(unsafe.Pointer(&info)),
|
|
uint32(unsafe.Sizeof(info)),
|
|
); err != nil {
|
|
windows.CloseHandle(job)
|
|
return nil, fmt.Errorf("set KILL_ON_JOB_CLOSE: %w", err)
|
|
}
|
|
return &preparationProcessController{job: job}, nil
|
|
}
|
|
|
|
func (c *preparationProcessController) attach(cmd *exec.Cmd) error {
|
|
process, err := windows.OpenProcess(
|
|
windows.PROCESS_SET_QUOTA|windows.PROCESS_TERMINATE,
|
|
false,
|
|
uint32(cmd.Process.Pid),
|
|
)
|
|
if err != nil {
|
|
return fmt.Errorf("open helper process: %w", err)
|
|
}
|
|
defer windows.CloseHandle(process)
|
|
if err := windows.AssignProcessToJobObject(c.job, process); err != nil {
|
|
return fmt.Errorf("assign helper to job object: %w", err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (c *preparationProcessController) stop(_ *exec.Cmd) error {
|
|
if err := windows.TerminateJobObject(c.job, 1); err != nil {
|
|
active, queryErr := c.activeProcesses()
|
|
if queryErr == nil && active == 0 {
|
|
return nil
|
|
}
|
|
return err
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// finish is called only after the direct helper has exited. Terminate any
|
|
// descendant it left behind, then wait until the Job Object reports no active
|
|
// members before the daemon may release directory ownership and retry.
|
|
func (c *preparationProcessController) finish() error {
|
|
active, err := c.activeProcesses()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if active > 0 {
|
|
if err := windows.TerminateJobObject(c.job, 1); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
for {
|
|
active, err = c.activeProcesses()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if active == 0 {
|
|
return nil
|
|
}
|
|
time.Sleep(10 * time.Millisecond)
|
|
}
|
|
}
|
|
|
|
func (c *preparationProcessController) activeProcesses() (uint32, error) {
|
|
var info jobObjectBasicAccountingInformation
|
|
if err := windows.QueryInformationJobObject(
|
|
c.job,
|
|
windows.JobObjectBasicAccountingInformation,
|
|
uintptr(unsafe.Pointer(&info)),
|
|
uint32(unsafe.Sizeof(info)),
|
|
nil,
|
|
); err != nil {
|
|
return 0, fmt.Errorf("query job object members: %w", err)
|
|
}
|
|
return info.ActiveProcesses, nil
|
|
}
|
|
|
|
func (c *preparationProcessController) close() {
|
|
if c.job == 0 {
|
|
return
|
|
}
|
|
_ = windows.CloseHandle(c.job)
|
|
c.job = 0
|
|
}
|