Merge pull request #18651 from vbatts/dm-cleanup
loopback (and devicemapper) cleanup
This commit is contained in:
commit
17792e996c
7 changed files with 200 additions and 166 deletions
|
@ -47,32 +47,29 @@ const (
|
||||||
|
|
||||||
// List of errors returned when using devicemapper.
|
// List of errors returned when using devicemapper.
|
||||||
var (
|
var (
|
||||||
ErrTaskRun = errors.New("dm_task_run failed")
|
ErrTaskRun = errors.New("dm_task_run failed")
|
||||||
ErrTaskSetName = errors.New("dm_task_set_name failed")
|
ErrTaskSetName = errors.New("dm_task_set_name failed")
|
||||||
ErrTaskSetMessage = errors.New("dm_task_set_message failed")
|
ErrTaskSetMessage = errors.New("dm_task_set_message failed")
|
||||||
ErrTaskSetAddNode = errors.New("dm_task_set_add_node failed")
|
ErrTaskSetAddNode = errors.New("dm_task_set_add_node failed")
|
||||||
ErrTaskSetRo = errors.New("dm_task_set_ro failed")
|
ErrTaskSetRo = errors.New("dm_task_set_ro failed")
|
||||||
ErrTaskAddTarget = errors.New("dm_task_add_target failed")
|
ErrTaskAddTarget = errors.New("dm_task_add_target failed")
|
||||||
ErrTaskSetSector = errors.New("dm_task_set_sector failed")
|
ErrTaskSetSector = errors.New("dm_task_set_sector failed")
|
||||||
ErrTaskGetDeps = errors.New("dm_task_get_deps failed")
|
ErrTaskGetDeps = errors.New("dm_task_get_deps failed")
|
||||||
ErrTaskGetInfo = errors.New("dm_task_get_info failed")
|
ErrTaskGetInfo = errors.New("dm_task_get_info failed")
|
||||||
ErrTaskGetDriverVersion = errors.New("dm_task_get_driver_version failed")
|
ErrTaskGetDriverVersion = errors.New("dm_task_get_driver_version failed")
|
||||||
ErrTaskDeferredRemove = errors.New("dm_task_deferred_remove failed")
|
ErrTaskDeferredRemove = errors.New("dm_task_deferred_remove failed")
|
||||||
ErrTaskSetCookie = errors.New("dm_task_set_cookie failed")
|
ErrTaskSetCookie = errors.New("dm_task_set_cookie failed")
|
||||||
ErrNilCookie = errors.New("cookie ptr can't be nil")
|
ErrNilCookie = errors.New("cookie ptr can't be nil")
|
||||||
ErrAttachLoopbackDevice = errors.New("loopback mounting failed")
|
ErrGetBlockSize = errors.New("Can't get block size")
|
||||||
ErrGetBlockSize = errors.New("Can't get block size")
|
ErrUdevWait = errors.New("wait on udev cookie failed")
|
||||||
ErrUdevWait = errors.New("wait on udev cookie failed")
|
ErrSetDevDir = errors.New("dm_set_dev_dir failed")
|
||||||
ErrSetDevDir = errors.New("dm_set_dev_dir failed")
|
ErrGetLibraryVersion = errors.New("dm_get_library_version failed")
|
||||||
ErrGetLibraryVersion = errors.New("dm_get_library_version failed")
|
ErrCreateRemoveTask = errors.New("Can't create task of type deviceRemove")
|
||||||
ErrCreateRemoveTask = errors.New("Can't create task of type deviceRemove")
|
ErrRunRemoveDevice = errors.New("running RemoveDevice failed")
|
||||||
ErrRunRemoveDevice = errors.New("running RemoveDevice failed")
|
ErrInvalidAddNode = errors.New("Invalid AddNode type")
|
||||||
ErrInvalidAddNode = errors.New("Invalid AddNode type")
|
ErrBusy = errors.New("Device is Busy")
|
||||||
ErrGetLoopbackBackingFile = errors.New("Unable to get loopback backing file")
|
ErrDeviceIDExists = errors.New("Device Id Exists")
|
||||||
ErrLoopbackSetCapacity = errors.New("Unable set loopback capacity")
|
ErrEnxio = errors.New("No such device or address")
|
||||||
ErrBusy = errors.New("Device is Busy")
|
|
||||||
ErrDeviceIDExists = errors.New("Device Id Exists")
|
|
||||||
ErrEnxio = errors.New("No such device or address")
|
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
|
@ -257,58 +254,6 @@ func (t *Task) getNextTarget(next unsafe.Pointer) (nextPtr unsafe.Pointer, start
|
||||||
start, length, targetType, params
|
start, length, targetType, params
|
||||||
}
|
}
|
||||||
|
|
||||||
func getLoopbackBackingFile(file *os.File) (uint64, uint64, error) {
|
|
||||||
loopInfo, err := ioctlLoopGetStatus64(file.Fd())
|
|
||||||
if err != nil {
|
|
||||||
logrus.Errorf("devicemapper: Error get loopback backing file: %s", err)
|
|
||||||
return 0, 0, ErrGetLoopbackBackingFile
|
|
||||||
}
|
|
||||||
return loopInfo.loDevice, loopInfo.loInode, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// LoopbackSetCapacity reloads the size for the loopback device.
|
|
||||||
func LoopbackSetCapacity(file *os.File) error {
|
|
||||||
if err := ioctlLoopSetCapacity(file.Fd(), 0); err != nil {
|
|
||||||
logrus.Errorf("devicemapper: Error loopbackSetCapacity: %s", err)
|
|
||||||
return ErrLoopbackSetCapacity
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// FindLoopDeviceFor returns a loopback device file for the specified file which
|
|
||||||
// is backing file of a loop back device.
|
|
||||||
func FindLoopDeviceFor(file *os.File) *os.File {
|
|
||||||
stat, err := file.Stat()
|
|
||||||
if err != nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
targetInode := stat.Sys().(*syscall.Stat_t).Ino
|
|
||||||
targetDevice := stat.Sys().(*syscall.Stat_t).Dev
|
|
||||||
|
|
||||||
for i := 0; true; i++ {
|
|
||||||
path := fmt.Sprintf("/dev/loop%d", i)
|
|
||||||
|
|
||||||
file, err := os.OpenFile(path, os.O_RDWR, 0)
|
|
||||||
if err != nil {
|
|
||||||
if os.IsNotExist(err) {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Ignore all errors until the first not-exist
|
|
||||||
// we want to continue looking for the file
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
dev, inode, err := getLoopbackBackingFile(file)
|
|
||||||
if err == nil && dev == targetDevice && inode == targetInode {
|
|
||||||
return file
|
|
||||||
}
|
|
||||||
file.Close()
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// UdevWait waits for any processes that are waiting for udev to complete the specified cookie.
|
// UdevWait waits for any processes that are waiting for udev to complete the specified cookie.
|
||||||
func UdevWait(cookie *uint) error {
|
func UdevWait(cookie *uint) error {
|
||||||
if res := DmUdevWait(*cookie); res != 1 {
|
if res := DmUdevWait(*cookie); res != 1 {
|
||||||
|
|
|
@ -5,17 +5,8 @@ package devicemapper
|
||||||
/*
|
/*
|
||||||
#cgo LDFLAGS: -L. -ldevmapper
|
#cgo LDFLAGS: -L. -ldevmapper
|
||||||
#include <libdevmapper.h>
|
#include <libdevmapper.h>
|
||||||
#include <linux/loop.h> // FIXME: present only for defines, maybe we can remove it?
|
|
||||||
#include <linux/fs.h> // FIXME: present only for BLKGETSIZE64, maybe we can remove it?
|
#include <linux/fs.h> // FIXME: present only for BLKGETSIZE64, maybe we can remove it?
|
||||||
|
|
||||||
#ifndef LOOP_CTL_GET_FREE
|
|
||||||
#define LOOP_CTL_GET_FREE 0x4C82
|
|
||||||
#endif
|
|
||||||
|
|
||||||
#ifndef LO_FLAGS_PARTSCAN
|
|
||||||
#define LO_FLAGS_PARTSCAN 8
|
|
||||||
#endif
|
|
||||||
|
|
||||||
// FIXME: Can't we find a way to do the logging in pure Go?
|
// FIXME: Can't we find a way to do the logging in pure Go?
|
||||||
extern void DevmapperLogCallback(int level, char *file, int line, int dm_errno_or_class, char *str);
|
extern void DevmapperLogCallback(int level, char *file, int line, int dm_errno_or_class, char *str);
|
||||||
|
|
||||||
|
@ -45,45 +36,12 @@ import (
|
||||||
|
|
||||||
type (
|
type (
|
||||||
cdmTask C.struct_dm_task
|
cdmTask C.struct_dm_task
|
||||||
|
|
||||||
cLoopInfo64 C.struct_loop_info64
|
|
||||||
loopInfo64 struct {
|
|
||||||
loDevice uint64 /* ioctl r/o */
|
|
||||||
loInode uint64 /* ioctl r/o */
|
|
||||||
loRdevice uint64 /* ioctl r/o */
|
|
||||||
loOffset uint64
|
|
||||||
loSizelimit uint64 /* bytes, 0 == max available */
|
|
||||||
loNumber uint32 /* ioctl r/o */
|
|
||||||
loEncryptType uint32
|
|
||||||
loEncryptKeySize uint32 /* ioctl w/o */
|
|
||||||
loFlags uint32 /* ioctl r/o */
|
|
||||||
loFileName [LoNameSize]uint8
|
|
||||||
loCryptName [LoNameSize]uint8
|
|
||||||
loEncryptKey [LoKeySize]uint8 /* ioctl w/o */
|
|
||||||
loInit [2]uint64
|
|
||||||
}
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// IOCTL consts
|
// IOCTL consts
|
||||||
const (
|
const (
|
||||||
BlkGetSize64 = C.BLKGETSIZE64
|
BlkGetSize64 = C.BLKGETSIZE64
|
||||||
BlkDiscard = C.BLKDISCARD
|
BlkDiscard = C.BLKDISCARD
|
||||||
|
|
||||||
LoopSetFd = C.LOOP_SET_FD
|
|
||||||
LoopCtlGetFree = C.LOOP_CTL_GET_FREE
|
|
||||||
LoopGetStatus64 = C.LOOP_GET_STATUS64
|
|
||||||
LoopSetStatus64 = C.LOOP_SET_STATUS64
|
|
||||||
LoopClrFd = C.LOOP_CLR_FD
|
|
||||||
LoopSetCapacity = C.LOOP_SET_CAPACITY
|
|
||||||
)
|
|
||||||
|
|
||||||
// LOOP consts.
|
|
||||||
const (
|
|
||||||
LoFlagsAutoClear = C.LO_FLAGS_AUTOCLEAR
|
|
||||||
LoFlagsReadOnly = C.LO_FLAGS_READ_ONLY
|
|
||||||
LoFlagsPartScan = C.LO_FLAGS_PARTSCAN
|
|
||||||
LoKeySize = C.LO_KEY_SIZE
|
|
||||||
LoNameSize = C.LO_NAME_SIZE
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// Devicemapper cookie flags.
|
// Devicemapper cookie flags.
|
||||||
|
|
|
@ -7,51 +7,6 @@ import (
|
||||||
"unsafe"
|
"unsafe"
|
||||||
)
|
)
|
||||||
|
|
||||||
func ioctlLoopCtlGetFree(fd uintptr) (int, error) {
|
|
||||||
index, _, err := syscall.Syscall(syscall.SYS_IOCTL, fd, LoopCtlGetFree, 0)
|
|
||||||
if err != 0 {
|
|
||||||
return 0, err
|
|
||||||
}
|
|
||||||
return int(index), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func ioctlLoopSetFd(loopFd, sparseFd uintptr) error {
|
|
||||||
if _, _, err := syscall.Syscall(syscall.SYS_IOCTL, loopFd, LoopSetFd, sparseFd); err != 0 {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func ioctlLoopSetStatus64(loopFd uintptr, loopInfo *loopInfo64) error {
|
|
||||||
if _, _, err := syscall.Syscall(syscall.SYS_IOCTL, loopFd, LoopSetStatus64, uintptr(unsafe.Pointer(loopInfo))); err != 0 {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func ioctlLoopClrFd(loopFd uintptr) error {
|
|
||||||
if _, _, err := syscall.Syscall(syscall.SYS_IOCTL, loopFd, LoopClrFd, 0); err != 0 {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func ioctlLoopGetStatus64(loopFd uintptr) (*loopInfo64, error) {
|
|
||||||
loopInfo := &loopInfo64{}
|
|
||||||
|
|
||||||
if _, _, err := syscall.Syscall(syscall.SYS_IOCTL, loopFd, LoopGetStatus64, uintptr(unsafe.Pointer(loopInfo))); err != 0 {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return loopInfo, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func ioctlLoopSetCapacity(loopFd uintptr, value int) error {
|
|
||||||
if _, _, err := syscall.Syscall(syscall.SYS_IOCTL, loopFd, LoopSetCapacity, uintptr(value)); err != 0 {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func ioctlBlkGetSize64(fd uintptr) (int64, error) {
|
func ioctlBlkGetSize64(fd uintptr) (int64, error) {
|
||||||
var size int64
|
var size int64
|
||||||
if _, _, err := syscall.Syscall(syscall.SYS_IOCTL, fd, BlkGetSize64, uintptr(unsafe.Pointer(&size))); err != 0 {
|
if _, _, err := syscall.Syscall(syscall.SYS_IOCTL, fd, BlkGetSize64, uintptr(unsafe.Pointer(&size))); err != 0 {
|
||||||
|
|
|
@ -1,8 +1,9 @@
|
||||||
// +build linux
|
// +build linux
|
||||||
|
|
||||||
package devicemapper
|
package loopback
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
"syscall"
|
"syscall"
|
||||||
|
@ -10,6 +11,13 @@ import (
|
||||||
"github.com/Sirupsen/logrus"
|
"github.com/Sirupsen/logrus"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Loopback related errors
|
||||||
|
var (
|
||||||
|
ErrAttachLoopbackDevice = errors.New("loopback attach failed")
|
||||||
|
ErrGetLoopbackBackingFile = errors.New("Unable to get loopback backing file")
|
||||||
|
ErrSetCapacity = errors.New("Unable set loopback capacity")
|
||||||
|
)
|
||||||
|
|
||||||
func stringToLoopName(src string) [LoNameSize]uint8 {
|
func stringToLoopName(src string) [LoNameSize]uint8 {
|
||||||
var dst [LoNameSize]uint8
|
var dst [LoNameSize]uint8
|
||||||
copy(dst[:], src[:])
|
copy(dst[:], src[:])
|
53
loopback/ioctl.go
Normal file
53
loopback/ioctl.go
Normal file
|
@ -0,0 +1,53 @@
|
||||||
|
// +build linux
|
||||||
|
|
||||||
|
package loopback
|
||||||
|
|
||||||
|
import (
|
||||||
|
"syscall"
|
||||||
|
"unsafe"
|
||||||
|
)
|
||||||
|
|
||||||
|
func ioctlLoopCtlGetFree(fd uintptr) (int, error) {
|
||||||
|
index, _, err := syscall.Syscall(syscall.SYS_IOCTL, fd, LoopCtlGetFree, 0)
|
||||||
|
if err != 0 {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
return int(index), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func ioctlLoopSetFd(loopFd, sparseFd uintptr) error {
|
||||||
|
if _, _, err := syscall.Syscall(syscall.SYS_IOCTL, loopFd, LoopSetFd, sparseFd); err != 0 {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func ioctlLoopSetStatus64(loopFd uintptr, loopInfo *loopInfo64) error {
|
||||||
|
if _, _, err := syscall.Syscall(syscall.SYS_IOCTL, loopFd, LoopSetStatus64, uintptr(unsafe.Pointer(loopInfo))); err != 0 {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func ioctlLoopClrFd(loopFd uintptr) error {
|
||||||
|
if _, _, err := syscall.Syscall(syscall.SYS_IOCTL, loopFd, LoopClrFd, 0); err != 0 {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func ioctlLoopGetStatus64(loopFd uintptr) (*loopInfo64, error) {
|
||||||
|
loopInfo := &loopInfo64{}
|
||||||
|
|
||||||
|
if _, _, err := syscall.Syscall(syscall.SYS_IOCTL, loopFd, LoopGetStatus64, uintptr(unsafe.Pointer(loopInfo))); err != 0 {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return loopInfo, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func ioctlLoopSetCapacity(loopFd uintptr, value int) error {
|
||||||
|
if _, _, err := syscall.Syscall(syscall.SYS_IOCTL, loopFd, LoopSetCapacity, uintptr(value)); err != 0 {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
52
loopback/loop_wrapper.go
Normal file
52
loopback/loop_wrapper.go
Normal file
|
@ -0,0 +1,52 @@
|
||||||
|
// +build linux
|
||||||
|
|
||||||
|
package loopback
|
||||||
|
|
||||||
|
/*
|
||||||
|
#include <linux/loop.h> // FIXME: present only for defines, maybe we can remove it?
|
||||||
|
|
||||||
|
#ifndef LOOP_CTL_GET_FREE
|
||||||
|
#define LOOP_CTL_GET_FREE 0x4C82
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#ifndef LO_FLAGS_PARTSCAN
|
||||||
|
#define LO_FLAGS_PARTSCAN 8
|
||||||
|
#endif
|
||||||
|
|
||||||
|
*/
|
||||||
|
import "C"
|
||||||
|
|
||||||
|
type loopInfo64 struct {
|
||||||
|
loDevice uint64 /* ioctl r/o */
|
||||||
|
loInode uint64 /* ioctl r/o */
|
||||||
|
loRdevice uint64 /* ioctl r/o */
|
||||||
|
loOffset uint64
|
||||||
|
loSizelimit uint64 /* bytes, 0 == max available */
|
||||||
|
loNumber uint32 /* ioctl r/o */
|
||||||
|
loEncryptType uint32
|
||||||
|
loEncryptKeySize uint32 /* ioctl w/o */
|
||||||
|
loFlags uint32 /* ioctl r/o */
|
||||||
|
loFileName [LoNameSize]uint8
|
||||||
|
loCryptName [LoNameSize]uint8
|
||||||
|
loEncryptKey [LoKeySize]uint8 /* ioctl w/o */
|
||||||
|
loInit [2]uint64
|
||||||
|
}
|
||||||
|
|
||||||
|
// IOCTL consts
|
||||||
|
const (
|
||||||
|
LoopSetFd = C.LOOP_SET_FD
|
||||||
|
LoopCtlGetFree = C.LOOP_CTL_GET_FREE
|
||||||
|
LoopGetStatus64 = C.LOOP_GET_STATUS64
|
||||||
|
LoopSetStatus64 = C.LOOP_SET_STATUS64
|
||||||
|
LoopClrFd = C.LOOP_CLR_FD
|
||||||
|
LoopSetCapacity = C.LOOP_SET_CAPACITY
|
||||||
|
)
|
||||||
|
|
||||||
|
// LOOP consts.
|
||||||
|
const (
|
||||||
|
LoFlagsAutoClear = C.LO_FLAGS_AUTOCLEAR
|
||||||
|
LoFlagsReadOnly = C.LO_FLAGS_READ_ONLY
|
||||||
|
LoFlagsPartScan = C.LO_FLAGS_PARTSCAN
|
||||||
|
LoKeySize = C.LO_KEY_SIZE
|
||||||
|
LoNameSize = C.LO_NAME_SIZE
|
||||||
|
)
|
63
loopback/loopback.go
Normal file
63
loopback/loopback.go
Normal file
|
@ -0,0 +1,63 @@
|
||||||
|
// +build linux
|
||||||
|
|
||||||
|
package loopback
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"syscall"
|
||||||
|
|
||||||
|
"github.com/Sirupsen/logrus"
|
||||||
|
)
|
||||||
|
|
||||||
|
func getLoopbackBackingFile(file *os.File) (uint64, uint64, error) {
|
||||||
|
loopInfo, err := ioctlLoopGetStatus64(file.Fd())
|
||||||
|
if err != nil {
|
||||||
|
logrus.Errorf("Error get loopback backing file: %s", err)
|
||||||
|
return 0, 0, ErrGetLoopbackBackingFile
|
||||||
|
}
|
||||||
|
return loopInfo.loDevice, loopInfo.loInode, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetCapacity reloads the size for the loopback device.
|
||||||
|
func SetCapacity(file *os.File) error {
|
||||||
|
if err := ioctlLoopSetCapacity(file.Fd(), 0); err != nil {
|
||||||
|
logrus.Errorf("Error loopbackSetCapacity: %s", err)
|
||||||
|
return ErrSetCapacity
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// FindLoopDeviceFor returns a loopback device file for the specified file which
|
||||||
|
// is backing file of a loop back device.
|
||||||
|
func FindLoopDeviceFor(file *os.File) *os.File {
|
||||||
|
stat, err := file.Stat()
|
||||||
|
if err != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
targetInode := stat.Sys().(*syscall.Stat_t).Ino
|
||||||
|
targetDevice := stat.Sys().(*syscall.Stat_t).Dev
|
||||||
|
|
||||||
|
for i := 0; true; i++ {
|
||||||
|
path := fmt.Sprintf("/dev/loop%d", i)
|
||||||
|
|
||||||
|
file, err := os.OpenFile(path, os.O_RDWR, 0)
|
||||||
|
if err != nil {
|
||||||
|
if os.IsNotExist(err) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ignore all errors until the first not-exist
|
||||||
|
// we want to continue looking for the file
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
dev, inode, err := getLoopbackBackingFile(file)
|
||||||
|
if err == nil && dev == targetDevice && inode == targetInode {
|
||||||
|
return file
|
||||||
|
}
|
||||||
|
file.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
Loading…
Reference in a new issue