Go bindings for Windows Job Objects:
A job object allows groups of processes to be managed as a unit. Job objects are namable, securable, sharable objects that control attributes of the processes associated with them. Operations performed on a job object affect all processes associated with the job object. Examples include enforcing limits such as working set size and process priority or terminating all processes associated with a job.
The package provides means to manage windows jobs. jobapi sub-package holds supplemental types and functions for low-level interactions with the operating system.
To start using go-winjob, install Go 1.11 or above and run go get:
$ go get github.com/kolesnikovae/go-winjob
The example below demonstrates an efficient way to ensure no descendant processes will be left after the process exit:
cmd := exec.Cmd("app.exe")
job, err := winjob.Start(cmd,
winjob.LimitKillOnJobClose,
winjob.LimitBreakawayOK)
if err != nil {
// ...
}
defer job.Close()
if err := cmd.Wait(); err != nil {
// ...
}
LimitKillOnJobClose
acts similarly to prctl(PR_SET_PDEATHSIG, SIGKILL)
in Linux: the job is destroyed when its last handle has been closed and all associated processes have been terminated. However, if the job has the LimitKillOnJobClose
, closing the last job object handle terminates all associated processes and then destroys the job object itself.
The same result can be achieved by manual assignment:
Show example
job, _ := winjob.Create("",
winjob.LimitKillOnJobClose,
winjob.LimitBreakawayOK)
cmd := exec.Cmd("app.exe")
cmd.SysProcAttr = &windows.SysProcAttr{
CreationFlags: windows.CREATE_SUSPENDED,
}
if err := cmd.Start(); err != nil {
// ...
}
if err := job.Assign(cmd.Process); err != nil {
// ...
}
if err := winjob.ResumeProcess(cmd.Process); err != nil {
// ...
}
if err := cmd.Wait(); err != nil {
// ...
}
go-winjob manages limits of the following types:
- Basic Limits
- Extended Limits
- UI Restriction
- CPU Rate Control
- Net Rate Control
- IO Rate Control (Deprecated)
- Notifications Limits
- Violations Limits
Limits can be applied to a job object at any time either by one, or all together (a full list can be found in the package documentation):
limits := []winjob.Limit{
winjob.WithKillOnJobClose(),
winjob.WithWorkingSetLimit(1<<20, 8<<20),
winjob.WithCPUHardCapLimit(5000),
winjob.WithDSCPTag(0x14),
}
if err := job.SetLimits(limits...); err != nil {
// ...
}
if err := job.ResetLimit(winjob.LimitKillOnJobClose); err != nil {
// ...
}
Also, a particular limit value can be examined:
if err := job.QueryLimits(); err != nil {
// ...
}
winjob.LimitCPU(job).LimitValue()
// Output: {Min:0 Max:0 Weight:0 HardCap:500}
Alternatively, limit values are accessible via JobInfo
member of a JobObject
.
Note: limits should be explicitly queried with job.QueryLimits()
before accessing their values.
A job can also set limits that trigger a notification when they are exceeded but allow the job to continue to run.
It is best to do this when the job is inactive, to reduce the chance of missing notifications for processes whose states change during the association of the completion port.
c := make(chan winjob.Notification, 1)
s, err := winjob.Notify(c, job)
if err != nil {
// ...
}
go func() {
defer s.Close()
for {
select {
case <-ctx.Done():
return
case n := <-c:
switch n.Type {
case winjob.NotificationNewProcess:
// ...
case winjob.NotificationExitProcess:
// ...
case winjob.NotificationNotificationLimit:
// Query limit violations.
default:
log.Println(n.Type, n.PID)
}
}
}
}()
if err := winjob.Start(cmd, limits...); err != nil {
// ...
}
A full list of supported notification types can be found in the package documentation.
Note that, with the exception of limits set with the JobObjectNotificationLimitInformation
information class explicitly, delivery of messages to the completion port is not guaranteed; failure of a message to arrive does not necessarily mean that the event did not occur.
Refer to examples/job
for a full example.
A job object records basic and IO accounting information for all its associated processes, including those that have terminated:
c, err := job.Counters()
if err != nil {
// ...
}
JSON output:
{
"TotalUserTime": 156250,
"TotalKernelTime": 156250,
"ThisPeriodTotalUserTime": 156250,
"ThisPeriodTotalKernelTime": 156250,
"TotalPageFaultCount": 7900,
"TotalProcesses": 2,
"ActiveProcesses": 0,
"TotalTerminatedProcesses": 0,
"ReadOperationCount": 52,
"WriteOperationCount": 0,
"OtherOperationCount": 638,
"ReadTransferCount": 202300,
"WriteTransferCount": 0,
"OtherTransferCount": 638
}
In order to avoid unnecessary allocations, QueryCounters
method can be used instead:
var counters winjob.Counters
if err := job.QueryCounters(&counters); err != nil {
// ...
}