jobobject.go 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669
  1. //go:build windows
  2. package jobobject
  3. import (
  4. "context"
  5. "errors"
  6. "fmt"
  7. "os"
  8. "path/filepath"
  9. "sync"
  10. "sync/atomic"
  11. "unsafe"
  12. "github.com/Microsoft/hcsshim/internal/queue"
  13. "github.com/Microsoft/hcsshim/internal/winapi"
  14. "golang.org/x/sys/windows"
  15. )
  16. // JobObject is a high level wrapper around a Windows job object. Holds a handle to
  17. // the job, a queue to receive iocp notifications about the lifecycle
  18. // of the job and a mutex for synchronized handle access.
  19. type JobObject struct {
  20. handle windows.Handle
  21. // All accesses to this MUST be done atomically except in `Open` as the object
  22. // is being created in the function. 1 signifies that this job is currently a silo.
  23. silo uint32
  24. mq *queue.MessageQueue
  25. handleLock sync.RWMutex
  26. }
  27. // JobLimits represents the resource constraints that can be applied to a job object.
  28. type JobLimits struct {
  29. CPULimit uint32
  30. CPUWeight uint32
  31. MemoryLimitInBytes uint64
  32. MaxIOPS int64
  33. MaxBandwidth int64
  34. }
  35. type CPURateControlType uint32
  36. const (
  37. WeightBased CPURateControlType = iota
  38. RateBased
  39. )
  40. // Processor resource controls
  41. const (
  42. cpuLimitMin = 1
  43. cpuLimitMax = 10000
  44. cpuWeightMin = 1
  45. cpuWeightMax = 9
  46. )
  47. var (
  48. ErrAlreadyClosed = errors.New("the handle has already been closed")
  49. ErrNotRegistered = errors.New("job is not registered to receive notifications")
  50. ErrNotSilo = errors.New("job is not a silo")
  51. )
  52. // Options represents the set of configurable options when making or opening a job object.
  53. type Options struct {
  54. // `Name` specifies the name of the job object if a named job object is desired.
  55. Name string
  56. // `Notifications` specifies if the job will be registered to receive notifications.
  57. // Defaults to false.
  58. Notifications bool
  59. // `UseNTVariant` specifies if we should use the `Nt` variant of Open/CreateJobObject.
  60. // Defaults to false.
  61. UseNTVariant bool
  62. // `Silo` specifies to promote the job to a silo. This additionally sets the flag
  63. // JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE as it is required for the upgrade to complete.
  64. Silo bool
  65. // `IOTracking` enables tracking I/O statistics on the job object. More specifically this
  66. // calls SetInformationJobObject with the JobObjectIoAttribution class.
  67. EnableIOTracking bool
  68. }
  69. // Create creates a job object.
  70. //
  71. // If options.Name is an empty string, the job will not be assigned a name.
  72. //
  73. // If options.Notifications are not enabled `PollNotifications` will return immediately with error `errNotRegistered`.
  74. //
  75. // If `options` is nil, use default option values.
  76. //
  77. // Returns a JobObject structure and an error if there is one.
  78. func Create(ctx context.Context, options *Options) (_ *JobObject, err error) {
  79. if options == nil {
  80. options = &Options{}
  81. }
  82. var jobName *winapi.UnicodeString
  83. if options.Name != "" {
  84. jobName, err = winapi.NewUnicodeString(options.Name)
  85. if err != nil {
  86. return nil, err
  87. }
  88. }
  89. var jobHandle windows.Handle
  90. if options.UseNTVariant {
  91. oa := winapi.ObjectAttributes{
  92. Length: unsafe.Sizeof(winapi.ObjectAttributes{}),
  93. ObjectName: jobName,
  94. Attributes: 0,
  95. }
  96. status := winapi.NtCreateJobObject(&jobHandle, winapi.JOB_OBJECT_ALL_ACCESS, &oa)
  97. if status != 0 {
  98. return nil, winapi.RtlNtStatusToDosError(status)
  99. }
  100. } else {
  101. var jobNameBuf *uint16
  102. if jobName != nil && jobName.Buffer != nil {
  103. jobNameBuf = jobName.Buffer
  104. }
  105. jobHandle, err = windows.CreateJobObject(nil, jobNameBuf)
  106. if err != nil {
  107. return nil, err
  108. }
  109. }
  110. defer func() {
  111. if err != nil {
  112. windows.Close(jobHandle)
  113. }
  114. }()
  115. job := &JobObject{
  116. handle: jobHandle,
  117. }
  118. // If the IOCP we'll be using to receive messages for all jobs hasn't been
  119. // created, create it and start polling.
  120. if options.Notifications {
  121. mq, err := setupNotifications(ctx, job)
  122. if err != nil {
  123. return nil, err
  124. }
  125. job.mq = mq
  126. }
  127. if options.EnableIOTracking {
  128. if err := enableIOTracking(jobHandle); err != nil {
  129. return nil, err
  130. }
  131. }
  132. if options.Silo {
  133. // This is a required setting for upgrading to a silo.
  134. if err := job.SetTerminateOnLastHandleClose(); err != nil {
  135. return nil, err
  136. }
  137. if err := job.PromoteToSilo(); err != nil {
  138. return nil, err
  139. }
  140. }
  141. return job, nil
  142. }
  143. // Open opens an existing job object with name provided in `options`. If no name is provided
  144. // return an error since we need to know what job object to open.
  145. //
  146. // If options.Notifications is false `PollNotifications` will return immediately with error `errNotRegistered`.
  147. //
  148. // Returns a JobObject structure and an error if there is one.
  149. func Open(ctx context.Context, options *Options) (_ *JobObject, err error) {
  150. if options == nil || (options != nil && options.Name == "") {
  151. return nil, errors.New("no job object name specified to open")
  152. }
  153. unicodeJobName, err := winapi.NewUnicodeString(options.Name)
  154. if err != nil {
  155. return nil, err
  156. }
  157. var jobHandle windows.Handle
  158. if options.UseNTVariant {
  159. oa := winapi.ObjectAttributes{
  160. Length: unsafe.Sizeof(winapi.ObjectAttributes{}),
  161. ObjectName: unicodeJobName,
  162. Attributes: 0,
  163. }
  164. status := winapi.NtOpenJobObject(&jobHandle, winapi.JOB_OBJECT_ALL_ACCESS, &oa)
  165. if status != 0 {
  166. return nil, winapi.RtlNtStatusToDosError(status)
  167. }
  168. } else {
  169. jobHandle, err = winapi.OpenJobObject(winapi.JOB_OBJECT_ALL_ACCESS, 0, unicodeJobName.Buffer)
  170. if err != nil {
  171. return nil, err
  172. }
  173. }
  174. defer func() {
  175. if err != nil {
  176. windows.Close(jobHandle)
  177. }
  178. }()
  179. job := &JobObject{
  180. handle: jobHandle,
  181. }
  182. if isJobSilo(jobHandle) {
  183. job.silo = 1
  184. }
  185. // If the IOCP we'll be using to receive messages for all jobs hasn't been
  186. // created, create it and start polling.
  187. if options.Notifications {
  188. mq, err := setupNotifications(ctx, job)
  189. if err != nil {
  190. return nil, err
  191. }
  192. job.mq = mq
  193. }
  194. return job, nil
  195. }
  196. // helper function to setup notifications for creating/opening a job object
  197. func setupNotifications(ctx context.Context, job *JobObject) (*queue.MessageQueue, error) {
  198. job.handleLock.RLock()
  199. defer job.handleLock.RUnlock()
  200. if job.handle == 0 {
  201. return nil, ErrAlreadyClosed
  202. }
  203. ioInitOnce.Do(func() {
  204. h, err := windows.CreateIoCompletionPort(windows.InvalidHandle, 0, 0, 0xffffffff)
  205. if err != nil {
  206. initIOErr = err
  207. return
  208. }
  209. ioCompletionPort = h
  210. go pollIOCP(ctx, h)
  211. })
  212. if initIOErr != nil {
  213. return nil, initIOErr
  214. }
  215. mq := queue.NewMessageQueue()
  216. jobMap.Store(uintptr(job.handle), mq)
  217. if err := attachIOCP(job.handle, ioCompletionPort); err != nil {
  218. jobMap.Delete(uintptr(job.handle))
  219. return nil, fmt.Errorf("failed to attach job to IO completion port: %w", err)
  220. }
  221. return mq, nil
  222. }
  223. // PollNotification will poll for a job object notification. This call should only be called once
  224. // per job (ideally in a goroutine loop) and will block if there is not a notification ready.
  225. // This call will return immediately with error `ErrNotRegistered` if the job was not registered
  226. // to receive notifications during `Create`. Internally, messages will be queued and there
  227. // is no worry of messages being dropped.
  228. func (job *JobObject) PollNotification() (interface{}, error) {
  229. if job.mq == nil {
  230. return nil, ErrNotRegistered
  231. }
  232. return job.mq.Dequeue()
  233. }
  234. // UpdateProcThreadAttribute updates the passed in ProcThreadAttributeList to contain what is necessary to
  235. // launch a process in a job at creation time. This can be used to avoid having to call Assign() after a process
  236. // has already started running.
  237. func (job *JobObject) UpdateProcThreadAttribute(attrList *windows.ProcThreadAttributeListContainer) error {
  238. job.handleLock.RLock()
  239. defer job.handleLock.RUnlock()
  240. if job.handle == 0 {
  241. return ErrAlreadyClosed
  242. }
  243. if err := attrList.Update(
  244. winapi.PROC_THREAD_ATTRIBUTE_JOB_LIST,
  245. unsafe.Pointer(&job.handle),
  246. unsafe.Sizeof(job.handle),
  247. ); err != nil {
  248. return fmt.Errorf("failed to update proc thread attributes for job object: %w", err)
  249. }
  250. return nil
  251. }
  252. // Close closes the job object handle.
  253. func (job *JobObject) Close() error {
  254. job.handleLock.Lock()
  255. defer job.handleLock.Unlock()
  256. if job.handle == 0 {
  257. return ErrAlreadyClosed
  258. }
  259. if err := windows.Close(job.handle); err != nil {
  260. return err
  261. }
  262. if job.mq != nil {
  263. job.mq.Close()
  264. }
  265. // Handles now invalid so if the map entry to receive notifications for this job still
  266. // exists remove it so we can stop receiving notifications.
  267. if _, ok := jobMap.Load(uintptr(job.handle)); ok {
  268. jobMap.Delete(uintptr(job.handle))
  269. }
  270. job.handle = 0
  271. return nil
  272. }
  273. // Assign assigns a process to the job object.
  274. func (job *JobObject) Assign(pid uint32) error {
  275. job.handleLock.RLock()
  276. defer job.handleLock.RUnlock()
  277. if job.handle == 0 {
  278. return ErrAlreadyClosed
  279. }
  280. if pid == 0 {
  281. return errors.New("invalid pid: 0")
  282. }
  283. hProc, err := windows.OpenProcess(winapi.PROCESS_ALL_ACCESS, true, pid)
  284. if err != nil {
  285. return err
  286. }
  287. defer windows.Close(hProc)
  288. return windows.AssignProcessToJobObject(job.handle, hProc)
  289. }
  290. // Terminate terminates the job, essentially calls TerminateProcess on every process in the
  291. // job.
  292. func (job *JobObject) Terminate(exitCode uint32) error {
  293. job.handleLock.RLock()
  294. defer job.handleLock.RUnlock()
  295. if job.handle == 0 {
  296. return ErrAlreadyClosed
  297. }
  298. return windows.TerminateJobObject(job.handle, exitCode)
  299. }
  300. // Pids returns all of the process IDs in the job object.
  301. func (job *JobObject) Pids() ([]uint32, error) {
  302. job.handleLock.RLock()
  303. defer job.handleLock.RUnlock()
  304. if job.handle == 0 {
  305. return nil, ErrAlreadyClosed
  306. }
  307. info := winapi.JOBOBJECT_BASIC_PROCESS_ID_LIST{}
  308. err := winapi.QueryInformationJobObject(
  309. job.handle,
  310. winapi.JobObjectBasicProcessIdList,
  311. unsafe.Pointer(&info),
  312. uint32(unsafe.Sizeof(info)),
  313. nil,
  314. )
  315. // This is either the case where there is only one process or no processes in
  316. // the job. Any other case will result in ERROR_MORE_DATA. Check if info.NumberOfProcessIdsInList
  317. // is 1 and just return this, otherwise return an empty slice.
  318. if err == nil {
  319. if info.NumberOfProcessIdsInList == 1 {
  320. return []uint32{uint32(info.ProcessIdList[0])}, nil
  321. }
  322. // Return empty slice instead of nil to play well with the caller of this.
  323. // Do not return an error if no processes are running inside the job
  324. return []uint32{}, nil
  325. }
  326. if err != winapi.ERROR_MORE_DATA {
  327. return nil, fmt.Errorf("failed initial query for PIDs in job object: %w", err)
  328. }
  329. jobBasicProcessIDListSize := unsafe.Sizeof(info) + (unsafe.Sizeof(info.ProcessIdList[0]) * uintptr(info.NumberOfAssignedProcesses-1))
  330. buf := make([]byte, jobBasicProcessIDListSize)
  331. if err = winapi.QueryInformationJobObject(
  332. job.handle,
  333. winapi.JobObjectBasicProcessIdList,
  334. unsafe.Pointer(&buf[0]),
  335. uint32(len(buf)),
  336. nil,
  337. ); err != nil {
  338. return nil, fmt.Errorf("failed to query for PIDs in job object: %w", err)
  339. }
  340. bufInfo := (*winapi.JOBOBJECT_BASIC_PROCESS_ID_LIST)(unsafe.Pointer(&buf[0]))
  341. pids := make([]uint32, bufInfo.NumberOfProcessIdsInList)
  342. for i, bufPid := range bufInfo.AllPids() {
  343. pids[i] = uint32(bufPid)
  344. }
  345. return pids, nil
  346. }
  347. // QueryMemoryStats gets the memory stats for the job object.
  348. func (job *JobObject) QueryMemoryStats() (*winapi.JOBOBJECT_MEMORY_USAGE_INFORMATION, error) {
  349. job.handleLock.RLock()
  350. defer job.handleLock.RUnlock()
  351. if job.handle == 0 {
  352. return nil, ErrAlreadyClosed
  353. }
  354. info := winapi.JOBOBJECT_MEMORY_USAGE_INFORMATION{}
  355. if err := winapi.QueryInformationJobObject(
  356. job.handle,
  357. winapi.JobObjectMemoryUsageInformation,
  358. unsafe.Pointer(&info),
  359. uint32(unsafe.Sizeof(info)),
  360. nil,
  361. ); err != nil {
  362. return nil, fmt.Errorf("failed to query for job object memory stats: %w", err)
  363. }
  364. return &info, nil
  365. }
  366. // QueryProcessorStats gets the processor stats for the job object.
  367. func (job *JobObject) QueryProcessorStats() (*winapi.JOBOBJECT_BASIC_ACCOUNTING_INFORMATION, error) {
  368. job.handleLock.RLock()
  369. defer job.handleLock.RUnlock()
  370. if job.handle == 0 {
  371. return nil, ErrAlreadyClosed
  372. }
  373. info := winapi.JOBOBJECT_BASIC_ACCOUNTING_INFORMATION{}
  374. if err := winapi.QueryInformationJobObject(
  375. job.handle,
  376. winapi.JobObjectBasicAccountingInformation,
  377. unsafe.Pointer(&info),
  378. uint32(unsafe.Sizeof(info)),
  379. nil,
  380. ); err != nil {
  381. return nil, fmt.Errorf("failed to query for job object process stats: %w", err)
  382. }
  383. return &info, nil
  384. }
  385. // QueryStorageStats gets the storage (I/O) stats for the job object. This call will error
  386. // if either `EnableIOTracking` wasn't set to true on creation of the job, or SetIOTracking()
  387. // hasn't been called since creation of the job.
  388. func (job *JobObject) QueryStorageStats() (*winapi.JOBOBJECT_IO_ATTRIBUTION_INFORMATION, error) {
  389. job.handleLock.RLock()
  390. defer job.handleLock.RUnlock()
  391. if job.handle == 0 {
  392. return nil, ErrAlreadyClosed
  393. }
  394. info := winapi.JOBOBJECT_IO_ATTRIBUTION_INFORMATION{
  395. ControlFlags: winapi.JOBOBJECT_IO_ATTRIBUTION_CONTROL_ENABLE,
  396. }
  397. if err := winapi.QueryInformationJobObject(
  398. job.handle,
  399. winapi.JobObjectIoAttribution,
  400. unsafe.Pointer(&info),
  401. uint32(unsafe.Sizeof(info)),
  402. nil,
  403. ); err != nil {
  404. return nil, fmt.Errorf("failed to query for job object storage stats: %w", err)
  405. }
  406. return &info, nil
  407. }
  408. // ApplyFileBinding makes a file binding using the Bind Filter from target to root. If the job has
  409. // not been upgraded to a silo this call will fail. The binding is only applied and visible for processes
  410. // running in the job, any processes on the host or in another job will not be able to see the binding.
  411. func (job *JobObject) ApplyFileBinding(root, target string, readOnly bool) error {
  412. job.handleLock.RLock()
  413. defer job.handleLock.RUnlock()
  414. if job.handle == 0 {
  415. return ErrAlreadyClosed
  416. }
  417. if !job.isSilo() {
  418. return ErrNotSilo
  419. }
  420. // The parent directory needs to exist for the bind to work. MkdirAll stats and
  421. // returns nil if the directory exists internally so we should be fine to mkdirall
  422. // every time.
  423. if err := os.MkdirAll(filepath.Dir(root), 0); err != nil {
  424. return err
  425. }
  426. rootPtr, err := windows.UTF16PtrFromString(root)
  427. if err != nil {
  428. return err
  429. }
  430. targetPtr, err := windows.UTF16PtrFromString(target)
  431. if err != nil {
  432. return err
  433. }
  434. flags := winapi.BINDFLT_FLAG_USE_CURRENT_SILO_MAPPING
  435. if readOnly {
  436. flags |= winapi.BINDFLT_FLAG_READ_ONLY_MAPPING
  437. }
  438. if err := winapi.BfSetupFilter(
  439. job.handle,
  440. flags,
  441. rootPtr,
  442. targetPtr,
  443. nil,
  444. 0,
  445. ); err != nil {
  446. return fmt.Errorf("failed to bind target %q to root %q for job object: %w", target, root, err)
  447. }
  448. return nil
  449. }
  450. // isJobSilo is a helper to determine if a job object that was opened is a silo. This should ONLY be called
  451. // from `Open` and any callers in this package afterwards should use `job.isSilo()`
  452. func isJobSilo(h windows.Handle) bool {
  453. // None of the information from the structure that this info class expects will be used, this is just used as
  454. // the call will fail if the job hasn't been upgraded to a silo so we can use this to tell when we open a job
  455. // if it's a silo or not. Because none of the info matters simply define a dummy struct with the size that the call
  456. // expects which is 16 bytes.
  457. type isSiloObj struct {
  458. _ [16]byte
  459. }
  460. var siloInfo isSiloObj
  461. err := winapi.QueryInformationJobObject(
  462. h,
  463. winapi.JobObjectSiloBasicInformation,
  464. unsafe.Pointer(&siloInfo),
  465. uint32(unsafe.Sizeof(siloInfo)),
  466. nil,
  467. )
  468. return err == nil
  469. }
  470. // PromoteToSilo promotes a job object to a silo. There must be no running processess
  471. // in the job for this to succeed. If the job is already a silo this is a no-op.
  472. func (job *JobObject) PromoteToSilo() error {
  473. job.handleLock.RLock()
  474. defer job.handleLock.RUnlock()
  475. if job.handle == 0 {
  476. return ErrAlreadyClosed
  477. }
  478. if job.isSilo() {
  479. return nil
  480. }
  481. pids, err := job.Pids()
  482. if err != nil {
  483. return err
  484. }
  485. if len(pids) != 0 {
  486. return fmt.Errorf("job cannot have running processes to be promoted to a silo, found %d running processes", len(pids))
  487. }
  488. _, err = windows.SetInformationJobObject(
  489. job.handle,
  490. winapi.JobObjectCreateSilo,
  491. 0,
  492. 0,
  493. )
  494. if err != nil {
  495. return fmt.Errorf("failed to promote job to silo: %w", err)
  496. }
  497. atomic.StoreUint32(&job.silo, 1)
  498. return nil
  499. }
  500. // isSilo returns if the job object is a silo.
  501. func (job *JobObject) isSilo() bool {
  502. return atomic.LoadUint32(&job.silo) == 1
  503. }
  504. // QueryPrivateWorkingSet returns the private working set size for the job. This is calculated by adding up the
  505. // private working set for every process running in the job.
  506. func (job *JobObject) QueryPrivateWorkingSet() (uint64, error) {
  507. pids, err := job.Pids()
  508. if err != nil {
  509. return 0, err
  510. }
  511. openAndQueryWorkingSet := func(pid uint32) (uint64, error) {
  512. h, err := windows.OpenProcess(windows.PROCESS_QUERY_LIMITED_INFORMATION, false, pid)
  513. if err != nil {
  514. // Continue to the next if OpenProcess doesn't return a valid handle (fails). Handles a
  515. // case where one of the pids in the job exited before we open.
  516. return 0, nil
  517. }
  518. defer func() {
  519. _ = windows.Close(h)
  520. }()
  521. // Check if the process is actually running in the job still. There's a small chance
  522. // that the process could have exited and had its pid re-used between grabbing the pids
  523. // in the job and opening the handle to it above.
  524. var inJob int32
  525. if err := winapi.IsProcessInJob(h, job.handle, &inJob); err != nil {
  526. // This shouldn't fail unless we have incorrect access rights which we control
  527. // here so probably best to error out if this failed.
  528. return 0, err
  529. }
  530. // Don't report stats for this process as it's not running in the job. This shouldn't be
  531. // an error condition though.
  532. if inJob == 0 {
  533. return 0, nil
  534. }
  535. var vmCounters winapi.VM_COUNTERS_EX2
  536. status := winapi.NtQueryInformationProcess(
  537. h,
  538. winapi.ProcessVmCounters,
  539. unsafe.Pointer(&vmCounters),
  540. uint32(unsafe.Sizeof(vmCounters)),
  541. nil,
  542. )
  543. if !winapi.NTSuccess(status) {
  544. return 0, fmt.Errorf("failed to query information for process: %w", winapi.RtlNtStatusToDosError(status))
  545. }
  546. return uint64(vmCounters.PrivateWorkingSetSize), nil
  547. }
  548. var jobWorkingSetSize uint64
  549. for _, pid := range pids {
  550. workingSet, err := openAndQueryWorkingSet(pid)
  551. if err != nil {
  552. return 0, err
  553. }
  554. jobWorkingSetSize += workingSet
  555. }
  556. return jobWorkingSetSize, nil
  557. }
  558. // SetIOTracking enables IO tracking for processes in the job object.
  559. // This enables use of the QueryStorageStats method.
  560. func (job *JobObject) SetIOTracking() error {
  561. job.handleLock.RLock()
  562. defer job.handleLock.RUnlock()
  563. if job.handle == 0 {
  564. return ErrAlreadyClosed
  565. }
  566. return enableIOTracking(job.handle)
  567. }
  568. func enableIOTracking(job windows.Handle) error {
  569. info := winapi.JOBOBJECT_IO_ATTRIBUTION_INFORMATION{
  570. ControlFlags: winapi.JOBOBJECT_IO_ATTRIBUTION_CONTROL_ENABLE,
  571. }
  572. if _, err := windows.SetInformationJobObject(
  573. job,
  574. winapi.JobObjectIoAttribution,
  575. uintptr(unsafe.Pointer(&info)),
  576. uint32(unsafe.Sizeof(info)),
  577. ); err != nil {
  578. return fmt.Errorf("failed to enable IO tracking on job object: %w", err)
  579. }
  580. return nil
  581. }