2018-09-05 14:54:38 -04:00
|
|
|
package hcs
|
|
|
|
|
|
|
|
import (
|
|
|
|
"encoding/json"
|
|
|
|
"io"
|
|
|
|
"sync"
|
|
|
|
"syscall"
|
|
|
|
"time"
|
|
|
|
|
2018-12-10 17:21:07 -05:00
|
|
|
"github.com/Microsoft/hcsshim/internal/guestrequest"
|
2018-09-05 14:54:38 -04:00
|
|
|
"github.com/Microsoft/hcsshim/internal/interop"
|
2019-02-28 13:16:30 -05:00
|
|
|
"github.com/Microsoft/hcsshim/internal/logfields"
|
2018-09-05 14:54:38 -04:00
|
|
|
"github.com/sirupsen/logrus"
|
|
|
|
)
|
|
|
|
|
|
|
|
// ContainerError is an error encountered in HCS
|
|
|
|
type Process struct {
|
|
|
|
handleLock sync.RWMutex
|
|
|
|
handle hcsProcess
|
|
|
|
processID int
|
|
|
|
system *System
|
|
|
|
cachedPipes *cachedPipes
|
|
|
|
callbackNumber uintptr
|
2019-02-28 13:16:30 -05:00
|
|
|
|
|
|
|
logctx logrus.Fields
|
2019-04-03 15:06:12 -04:00
|
|
|
|
|
|
|
closedWaitOnce sync.Once
|
|
|
|
waitBlock chan struct{}
|
|
|
|
waitError error
|
2019-02-28 13:16:30 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
func newProcess(process hcsProcess, processID int, computeSystem *System) *Process {
|
|
|
|
return &Process{
|
|
|
|
handle: process,
|
|
|
|
processID: processID,
|
|
|
|
system: computeSystem,
|
|
|
|
logctx: logrus.Fields{
|
|
|
|
logfields.ContainerID: computeSystem.ID(),
|
|
|
|
logfields.ProcessID: processID,
|
|
|
|
},
|
2019-04-03 15:06:12 -04:00
|
|
|
waitBlock: make(chan struct{}),
|
2019-02-28 13:16:30 -05:00
|
|
|
}
|
2018-09-05 14:54:38 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
type cachedPipes struct {
|
|
|
|
stdIn syscall.Handle
|
|
|
|
stdOut syscall.Handle
|
|
|
|
stdErr syscall.Handle
|
|
|
|
}
|
|
|
|
|
|
|
|
type processModifyRequest struct {
|
|
|
|
Operation string
|
|
|
|
ConsoleSize *consoleSize `json:",omitempty"`
|
|
|
|
CloseHandle *closeHandle `json:",omitempty"`
|
|
|
|
}
|
|
|
|
|
|
|
|
type consoleSize struct {
|
|
|
|
Height uint16
|
|
|
|
Width uint16
|
|
|
|
}
|
|
|
|
|
|
|
|
type closeHandle struct {
|
|
|
|
Handle string
|
|
|
|
}
|
|
|
|
|
|
|
|
type ProcessStatus struct {
|
|
|
|
ProcessID uint32
|
|
|
|
Exited bool
|
|
|
|
ExitCode uint32
|
|
|
|
LastWaitResult int32
|
|
|
|
}
|
|
|
|
|
|
|
|
const (
|
|
|
|
stdIn string = "StdIn"
|
|
|
|
stdOut string = "StdOut"
|
|
|
|
stdErr string = "StdErr"
|
|
|
|
)
|
|
|
|
|
|
|
|
const (
|
|
|
|
modifyConsoleSize string = "ConsoleSize"
|
|
|
|
modifyCloseHandle string = "CloseHandle"
|
|
|
|
)
|
|
|
|
|
|
|
|
// Pid returns the process ID of the process within the container.
|
|
|
|
func (process *Process) Pid() int {
|
|
|
|
return process.processID
|
|
|
|
}
|
|
|
|
|
|
|
|
// SystemID returns the ID of the process's compute system.
|
|
|
|
func (process *Process) SystemID() string {
|
|
|
|
return process.system.ID()
|
|
|
|
}
|
|
|
|
|
2019-02-28 13:16:30 -05:00
|
|
|
func (process *Process) logOperationBegin(operation string) {
|
|
|
|
logOperationBegin(
|
|
|
|
process.logctx,
|
|
|
|
operation+" - Begin Operation")
|
|
|
|
}
|
|
|
|
|
|
|
|
func (process *Process) logOperationEnd(operation string, err error) {
|
|
|
|
var result string
|
|
|
|
if err == nil {
|
|
|
|
result = "Success"
|
|
|
|
} else {
|
|
|
|
result = "Error"
|
|
|
|
}
|
|
|
|
|
|
|
|
logOperationEnd(
|
|
|
|
process.logctx,
|
|
|
|
operation+" - End Operation - "+result,
|
|
|
|
err)
|
|
|
|
}
|
|
|
|
|
2018-12-10 17:21:07 -05:00
|
|
|
// Signal signals the process with `options`.
|
2019-02-28 13:16:30 -05:00
|
|
|
func (process *Process) Signal(options guestrequest.SignalProcessOptions) (err error) {
|
2018-12-10 17:21:07 -05:00
|
|
|
process.handleLock.RLock()
|
|
|
|
defer process.handleLock.RUnlock()
|
2019-02-28 13:16:30 -05:00
|
|
|
|
|
|
|
operation := "hcsshim::Process::Signal"
|
|
|
|
process.logOperationBegin(operation)
|
|
|
|
defer func() { process.logOperationEnd(operation, err) }()
|
2018-12-10 17:21:07 -05:00
|
|
|
|
|
|
|
if process.handle == 0 {
|
|
|
|
return makeProcessError(process, operation, ErrAlreadyClosed, nil)
|
|
|
|
}
|
|
|
|
|
|
|
|
optionsb, err := json.Marshal(options)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
optionsStr := string(optionsb)
|
|
|
|
|
|
|
|
var resultp *uint16
|
2019-02-28 13:16:30 -05:00
|
|
|
syscallWatcher(process.logctx, func() {
|
|
|
|
err = hcsSignalProcess(process.handle, optionsStr, &resultp)
|
|
|
|
})
|
2018-12-10 17:21:07 -05:00
|
|
|
events := processHcsResult(resultp)
|
|
|
|
if err != nil {
|
|
|
|
return makeProcessError(process, operation, err, events)
|
|
|
|
}
|
|
|
|
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2018-09-05 14:54:38 -04:00
|
|
|
// Kill signals the process to terminate but does not wait for it to finish terminating.
|
2019-02-28 13:16:30 -05:00
|
|
|
func (process *Process) Kill() (err error) {
|
2018-09-05 14:54:38 -04:00
|
|
|
process.handleLock.RLock()
|
|
|
|
defer process.handleLock.RUnlock()
|
2019-02-28 13:16:30 -05:00
|
|
|
|
|
|
|
operation := "hcsshim::Process::Kill"
|
|
|
|
process.logOperationBegin(operation)
|
|
|
|
defer func() { process.logOperationEnd(operation, err) }()
|
2018-09-05 14:54:38 -04:00
|
|
|
|
|
|
|
if process.handle == 0 {
|
|
|
|
return makeProcessError(process, operation, ErrAlreadyClosed, nil)
|
|
|
|
}
|
|
|
|
|
|
|
|
var resultp *uint16
|
2019-02-28 13:16:30 -05:00
|
|
|
syscallWatcher(process.logctx, func() {
|
|
|
|
err = hcsTerminateProcess(process.handle, &resultp)
|
|
|
|
})
|
2018-09-05 14:54:38 -04:00
|
|
|
events := processHcsResult(resultp)
|
|
|
|
if err != nil {
|
|
|
|
return makeProcessError(process, operation, err, events)
|
|
|
|
}
|
|
|
|
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2019-04-03 15:06:12 -04:00
|
|
|
// waitBackground waits for the process exit notification. Once received sets
|
|
|
|
// `process.waitError` (if any) and unblocks all `Wait` and `WaitTimeout` calls.
|
|
|
|
//
|
|
|
|
// This MUST be called exactly once per `process.handle` but `Wait` and
|
|
|
|
// `WaitTimeout` are safe to call multiple times.
|
|
|
|
func (process *Process) waitBackground() {
|
|
|
|
process.waitError = waitForNotification(process.callbackNumber, hcsNotificationProcessExited, nil)
|
|
|
|
process.closedWaitOnce.Do(func() {
|
|
|
|
close(process.waitBlock)
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
|
|
|
// Wait waits for the process to exit. If the process has already exited returns
|
|
|
|
// the pervious error (if any).
|
2019-02-28 13:16:30 -05:00
|
|
|
func (process *Process) Wait() (err error) {
|
|
|
|
operation := "hcsshim::Process::Wait"
|
|
|
|
process.logOperationBegin(operation)
|
|
|
|
defer func() { process.logOperationEnd(operation, err) }()
|
2018-09-05 14:54:38 -04:00
|
|
|
|
2019-04-03 15:06:12 -04:00
|
|
|
<-process.waitBlock
|
|
|
|
if process.waitError != nil {
|
2018-09-05 14:54:38 -04:00
|
|
|
return makeProcessError(process, operation, err, nil)
|
|
|
|
}
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2019-04-03 15:06:12 -04:00
|
|
|
// WaitTimeout waits for the process to exit or the duration to elapse. If the
|
|
|
|
// process has already exited returns the pervious error (if any). If a timeout
|
|
|
|
// occurs returns `ErrTimeout`.
|
2019-02-28 13:16:30 -05:00
|
|
|
func (process *Process) WaitTimeout(timeout time.Duration) (err error) {
|
|
|
|
operation := "hcssshim::Process::WaitTimeout"
|
|
|
|
process.logOperationBegin(operation)
|
|
|
|
defer func() { process.logOperationEnd(operation, err) }()
|
2018-09-05 14:54:38 -04:00
|
|
|
|
2019-04-03 15:06:12 -04:00
|
|
|
select {
|
|
|
|
case <-process.waitBlock:
|
|
|
|
if process.waitError != nil {
|
|
|
|
return makeProcessError(process, operation, process.waitError, nil)
|
|
|
|
}
|
|
|
|
return nil
|
|
|
|
case <-time.After(timeout):
|
|
|
|
return makeProcessError(process, operation, ErrTimeout, nil)
|
2018-09-05 14:54:38 -04:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// ResizeConsole resizes the console of the process.
|
2019-02-28 13:16:30 -05:00
|
|
|
func (process *Process) ResizeConsole(width, height uint16) (err error) {
|
2018-09-05 14:54:38 -04:00
|
|
|
process.handleLock.RLock()
|
|
|
|
defer process.handleLock.RUnlock()
|
2019-02-28 13:16:30 -05:00
|
|
|
|
|
|
|
operation := "hcsshim::Process::ResizeConsole"
|
|
|
|
process.logOperationBegin(operation)
|
|
|
|
defer func() { process.logOperationEnd(operation, err) }()
|
2018-09-05 14:54:38 -04:00
|
|
|
|
|
|
|
if process.handle == 0 {
|
|
|
|
return makeProcessError(process, operation, ErrAlreadyClosed, nil)
|
|
|
|
}
|
|
|
|
|
|
|
|
modifyRequest := processModifyRequest{
|
|
|
|
Operation: modifyConsoleSize,
|
|
|
|
ConsoleSize: &consoleSize{
|
|
|
|
Height: height,
|
|
|
|
Width: width,
|
|
|
|
},
|
|
|
|
}
|
|
|
|
|
|
|
|
modifyRequestb, err := json.Marshal(modifyRequest)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
modifyRequestStr := string(modifyRequestb)
|
|
|
|
|
|
|
|
var resultp *uint16
|
|
|
|
err = hcsModifyProcess(process.handle, modifyRequestStr, &resultp)
|
|
|
|
events := processHcsResult(resultp)
|
|
|
|
if err != nil {
|
|
|
|
return makeProcessError(process, operation, err, events)
|
|
|
|
}
|
|
|
|
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2019-02-28 13:16:30 -05:00
|
|
|
func (process *Process) Properties() (_ *ProcessStatus, err error) {
|
2018-09-05 14:54:38 -04:00
|
|
|
process.handleLock.RLock()
|
|
|
|
defer process.handleLock.RUnlock()
|
2019-02-28 13:16:30 -05:00
|
|
|
|
|
|
|
operation := "hcsshim::Process::Properties"
|
|
|
|
process.logOperationBegin(operation)
|
|
|
|
defer func() { process.logOperationEnd(operation, err) }()
|
2018-09-05 14:54:38 -04:00
|
|
|
|
|
|
|
if process.handle == 0 {
|
|
|
|
return nil, makeProcessError(process, operation, ErrAlreadyClosed, nil)
|
|
|
|
}
|
|
|
|
|
|
|
|
var (
|
|
|
|
resultp *uint16
|
|
|
|
propertiesp *uint16
|
|
|
|
)
|
2019-02-28 13:16:30 -05:00
|
|
|
syscallWatcher(process.logctx, func() {
|
|
|
|
err = hcsGetProcessProperties(process.handle, &propertiesp, &resultp)
|
|
|
|
})
|
2018-09-05 14:54:38 -04:00
|
|
|
events := processHcsResult(resultp)
|
|
|
|
if err != nil {
|
|
|
|
return nil, makeProcessError(process, operation, err, events)
|
|
|
|
}
|
|
|
|
|
|
|
|
if propertiesp == nil {
|
|
|
|
return nil, ErrUnexpectedValue
|
|
|
|
}
|
|
|
|
propertiesRaw := interop.ConvertAndFreeCoTaskMemBytes(propertiesp)
|
|
|
|
|
|
|
|
properties := &ProcessStatus{}
|
|
|
|
if err := json.Unmarshal(propertiesRaw, properties); err != nil {
|
|
|
|
return nil, makeProcessError(process, operation, err, nil)
|
|
|
|
}
|
|
|
|
|
|
|
|
return properties, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// ExitCode returns the exit code of the process. The process must have
|
|
|
|
// already terminated.
|
2019-02-28 13:16:30 -05:00
|
|
|
func (process *Process) ExitCode() (_ int, err error) {
|
|
|
|
operation := "hcsshim::Process::ExitCode"
|
|
|
|
process.logOperationBegin(operation)
|
|
|
|
defer func() { process.logOperationEnd(operation, err) }()
|
|
|
|
|
2018-09-05 14:54:38 -04:00
|
|
|
properties, err := process.Properties()
|
|
|
|
if err != nil {
|
2019-04-03 15:06:12 -04:00
|
|
|
return -1, makeProcessError(process, operation, err, nil)
|
2018-09-05 14:54:38 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
if properties.Exited == false {
|
2019-04-03 15:06:12 -04:00
|
|
|
return -1, makeProcessError(process, operation, ErrInvalidProcessState, nil)
|
2018-09-05 14:54:38 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
if properties.LastWaitResult != 0 {
|
2019-04-03 15:06:12 -04:00
|
|
|
logrus.WithFields(logrus.Fields{
|
|
|
|
logfields.ContainerID: process.SystemID(),
|
|
|
|
logfields.ProcessID: process.processID,
|
|
|
|
"wait-result": properties.LastWaitResult,
|
|
|
|
}).Warn("hcsshim::Process::ExitCode - Non-zero last wait result")
|
|
|
|
return -1, nil
|
2018-09-05 14:54:38 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
return int(properties.ExitCode), nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// Stdio returns the stdin, stdout, and stderr pipes, respectively. Closing
|
|
|
|
// these pipes does not close the underlying pipes; it should be possible to
|
|
|
|
// call this multiple times to get multiple interfaces.
|
2019-02-28 13:16:30 -05:00
|
|
|
func (process *Process) Stdio() (_ io.WriteCloser, _ io.ReadCloser, _ io.ReadCloser, err error) {
|
2018-09-05 14:54:38 -04:00
|
|
|
process.handleLock.RLock()
|
|
|
|
defer process.handleLock.RUnlock()
|
2019-02-28 13:16:30 -05:00
|
|
|
|
|
|
|
operation := "hcsshim::Process::Stdio"
|
|
|
|
process.logOperationBegin(operation)
|
|
|
|
defer func() { process.logOperationEnd(operation, err) }()
|
2018-09-05 14:54:38 -04:00
|
|
|
|
|
|
|
if process.handle == 0 {
|
|
|
|
return nil, nil, nil, makeProcessError(process, operation, ErrAlreadyClosed, nil)
|
|
|
|
}
|
|
|
|
|
|
|
|
var stdIn, stdOut, stdErr syscall.Handle
|
|
|
|
|
|
|
|
if process.cachedPipes == nil {
|
|
|
|
var (
|
|
|
|
processInfo hcsProcessInformation
|
|
|
|
resultp *uint16
|
|
|
|
)
|
2019-02-28 13:16:30 -05:00
|
|
|
err = hcsGetProcessInfo(process.handle, &processInfo, &resultp)
|
2018-09-05 14:54:38 -04:00
|
|
|
events := processHcsResult(resultp)
|
|
|
|
if err != nil {
|
|
|
|
return nil, nil, nil, makeProcessError(process, operation, err, events)
|
|
|
|
}
|
|
|
|
|
|
|
|
stdIn, stdOut, stdErr = processInfo.StdInput, processInfo.StdOutput, processInfo.StdError
|
|
|
|
} else {
|
|
|
|
// Use cached pipes
|
|
|
|
stdIn, stdOut, stdErr = process.cachedPipes.stdIn, process.cachedPipes.stdOut, process.cachedPipes.stdErr
|
|
|
|
|
|
|
|
// Invalidate the cache
|
|
|
|
process.cachedPipes = nil
|
|
|
|
}
|
|
|
|
|
|
|
|
pipes, err := makeOpenFiles([]syscall.Handle{stdIn, stdOut, stdErr})
|
|
|
|
if err != nil {
|
|
|
|
return nil, nil, nil, makeProcessError(process, operation, err, nil)
|
|
|
|
}
|
|
|
|
|
|
|
|
return pipes[0], pipes[1], pipes[2], nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// CloseStdin closes the write side of the stdin pipe so that the process is
|
|
|
|
// notified on the read side that there is no more data in stdin.
|
2019-02-28 13:16:30 -05:00
|
|
|
func (process *Process) CloseStdin() (err error) {
|
2018-09-05 14:54:38 -04:00
|
|
|
process.handleLock.RLock()
|
|
|
|
defer process.handleLock.RUnlock()
|
2019-02-28 13:16:30 -05:00
|
|
|
|
|
|
|
operation := "hcsshim::Process::CloseStdin"
|
|
|
|
process.logOperationBegin(operation)
|
|
|
|
defer func() { process.logOperationEnd(operation, err) }()
|
2018-09-05 14:54:38 -04:00
|
|
|
|
|
|
|
if process.handle == 0 {
|
|
|
|
return makeProcessError(process, operation, ErrAlreadyClosed, nil)
|
|
|
|
}
|
|
|
|
|
|
|
|
modifyRequest := processModifyRequest{
|
|
|
|
Operation: modifyCloseHandle,
|
|
|
|
CloseHandle: &closeHandle{
|
|
|
|
Handle: stdIn,
|
|
|
|
},
|
|
|
|
}
|
|
|
|
|
|
|
|
modifyRequestb, err := json.Marshal(modifyRequest)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
modifyRequestStr := string(modifyRequestb)
|
|
|
|
|
|
|
|
var resultp *uint16
|
|
|
|
err = hcsModifyProcess(process.handle, modifyRequestStr, &resultp)
|
|
|
|
events := processHcsResult(resultp)
|
|
|
|
if err != nil {
|
|
|
|
return makeProcessError(process, operation, err, events)
|
|
|
|
}
|
|
|
|
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// Close cleans up any state associated with the process but does not kill
|
|
|
|
// or wait on it.
|
2019-02-28 13:16:30 -05:00
|
|
|
func (process *Process) Close() (err error) {
|
2018-09-05 14:54:38 -04:00
|
|
|
process.handleLock.Lock()
|
|
|
|
defer process.handleLock.Unlock()
|
2019-02-28 13:16:30 -05:00
|
|
|
|
|
|
|
operation := "hcsshim::Process::Close"
|
|
|
|
process.logOperationBegin(operation)
|
|
|
|
defer func() { process.logOperationEnd(operation, err) }()
|
2018-09-05 14:54:38 -04:00
|
|
|
|
|
|
|
// Don't double free this
|
|
|
|
if process.handle == 0 {
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2019-02-28 13:16:30 -05:00
|
|
|
if err = process.unregisterCallback(); err != nil {
|
2018-09-05 14:54:38 -04:00
|
|
|
return makeProcessError(process, operation, err, nil)
|
|
|
|
}
|
|
|
|
|
2019-02-28 13:16:30 -05:00
|
|
|
if err = hcsCloseProcess(process.handle); err != nil {
|
2018-09-05 14:54:38 -04:00
|
|
|
return makeProcessError(process, operation, err, nil)
|
|
|
|
}
|
|
|
|
|
|
|
|
process.handle = 0
|
2019-04-03 15:06:12 -04:00
|
|
|
process.closedWaitOnce.Do(func() {
|
|
|
|
close(process.waitBlock)
|
|
|
|
})
|
2018-09-05 14:54:38 -04:00
|
|
|
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func (process *Process) registerCallback() error {
|
|
|
|
context := ¬ifcationWatcherContext{
|
|
|
|
channels: newChannels(),
|
|
|
|
}
|
|
|
|
|
|
|
|
callbackMapLock.Lock()
|
|
|
|
callbackNumber := nextCallback
|
|
|
|
nextCallback++
|
|
|
|
callbackMap[callbackNumber] = context
|
|
|
|
callbackMapLock.Unlock()
|
|
|
|
|
|
|
|
var callbackHandle hcsCallback
|
|
|
|
err := hcsRegisterProcessCallback(process.handle, notificationWatcherCallback, callbackNumber, &callbackHandle)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
context.handle = callbackHandle
|
|
|
|
process.callbackNumber = callbackNumber
|
|
|
|
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func (process *Process) unregisterCallback() error {
|
|
|
|
callbackNumber := process.callbackNumber
|
|
|
|
|
|
|
|
callbackMapLock.RLock()
|
|
|
|
context := callbackMap[callbackNumber]
|
|
|
|
callbackMapLock.RUnlock()
|
|
|
|
|
|
|
|
if context == nil {
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
handle := context.handle
|
|
|
|
|
|
|
|
if handle == 0 {
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// hcsUnregisterProcessCallback has its own syncronization
|
|
|
|
// to wait for all callbacks to complete. We must NOT hold the callbackMapLock.
|
|
|
|
err := hcsUnregisterProcessCallback(handle)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
closeChannels(context.channels)
|
|
|
|
|
|
|
|
callbackMapLock.Lock()
|
2019-04-03 15:06:12 -04:00
|
|
|
delete(callbackMap, callbackNumber)
|
2018-09-05 14:54:38 -04:00
|
|
|
callbackMapLock.Unlock()
|
|
|
|
|
|
|
|
handle = 0
|
|
|
|
|
|
|
|
return nil
|
|
|
|
}
|