mirror of https://github.com/k3d-io/k3d
parent
16d400aa05
commit
05d839b2b8
@ -0,0 +1,41 @@ |
|||||||
|
<!-- BEGIN MICROSOFT SECURITY.MD V0.0.8 BLOCK --> |
||||||
|
|
||||||
|
## Security |
||||||
|
|
||||||
|
Microsoft takes the security of our software products and services seriously, which includes all source code repositories managed through our GitHub organizations, which include [Microsoft](https://github.com/microsoft), [Azure](https://github.com/Azure), [DotNet](https://github.com/dotnet), [AspNet](https://github.com/aspnet), [Xamarin](https://github.com/xamarin), and [our GitHub organizations](https://opensource.microsoft.com/). |
||||||
|
|
||||||
|
If you believe you have found a security vulnerability in any Microsoft-owned repository that meets [Microsoft's definition of a security vulnerability](https://aka.ms/opensource/security/definition), please report it to us as described below. |
||||||
|
|
||||||
|
## Reporting Security Issues |
||||||
|
|
||||||
|
**Please do not report security vulnerabilities through public GitHub issues.** |
||||||
|
|
||||||
|
Instead, please report them to the Microsoft Security Response Center (MSRC) at [https://msrc.microsoft.com/create-report](https://aka.ms/opensource/security/create-report). |
||||||
|
|
||||||
|
If you prefer to submit without logging in, send email to [secure@microsoft.com](mailto:secure@microsoft.com). If possible, encrypt your message with our PGP key; please download it from the [Microsoft Security Response Center PGP Key page](https://aka.ms/opensource/security/pgpkey). |
||||||
|
|
||||||
|
You should receive a response within 24 hours. If for some reason you do not, please follow up via email to ensure we received your original message. Additional information can be found at [microsoft.com/msrc](https://aka.ms/opensource/security/msrc). |
||||||
|
|
||||||
|
Please include the requested information listed below (as much as you can provide) to help us better understand the nature and scope of the possible issue: |
||||||
|
|
||||||
|
* Type of issue (e.g. buffer overflow, SQL injection, cross-site scripting, etc.) |
||||||
|
* Full paths of source file(s) related to the manifestation of the issue |
||||||
|
* The location of the affected source code (tag/branch/commit or direct URL) |
||||||
|
* Any special configuration required to reproduce the issue |
||||||
|
* Step-by-step instructions to reproduce the issue |
||||||
|
* Proof-of-concept or exploit code (if possible) |
||||||
|
* Impact of the issue, including how an attacker might exploit the issue |
||||||
|
|
||||||
|
This information will help us triage your report more quickly. |
||||||
|
|
||||||
|
If you are reporting for a bug bounty, more complete reports can contribute to a higher bounty award. Please visit our [Microsoft Bug Bounty Program](https://aka.ms/opensource/security/bounty) page for more details about our active programs. |
||||||
|
|
||||||
|
## Preferred Languages |
||||||
|
|
||||||
|
We prefer all communications to be in English. |
||||||
|
|
||||||
|
## Policy |
||||||
|
|
||||||
|
Microsoft follows the principle of [Coordinated Vulnerability Disclosure](https://aka.ms/opensource/security/cvd). |
||||||
|
|
||||||
|
<!-- END MICROSOFT SECURITY.MD BLOCK --> |
@ -0,0 +1 @@ |
|||||||
|
* text=auto eol=lf |
@ -1 +1,10 @@ |
|||||||
|
.vscode/ |
||||||
|
|
||||||
*.exe |
*.exe |
||||||
|
|
||||||
|
# testing |
||||||
|
testdata |
||||||
|
|
||||||
|
# go workspaces |
||||||
|
go.work |
||||||
|
go.work.sum |
||||||
|
@ -0,0 +1,144 @@ |
|||||||
|
run: |
||||||
|
skip-dirs: |
||||||
|
- pkg/etw/sample |
||||||
|
|
||||||
|
linters: |
||||||
|
enable: |
||||||
|
# style |
||||||
|
- containedctx # struct contains a context |
||||||
|
- dupl # duplicate code |
||||||
|
- errname # erorrs are named correctly |
||||||
|
- goconst # strings that should be constants |
||||||
|
- godot # comments end in a period |
||||||
|
- misspell |
||||||
|
- nolintlint # "//nolint" directives are properly explained |
||||||
|
- revive # golint replacement |
||||||
|
- stylecheck # golint replacement, less configurable than revive |
||||||
|
- unconvert # unnecessary conversions |
||||||
|
- wastedassign |
||||||
|
|
||||||
|
# bugs, performance, unused, etc ... |
||||||
|
- contextcheck # function uses a non-inherited context |
||||||
|
- errorlint # errors not wrapped for 1.13 |
||||||
|
- exhaustive # check exhaustiveness of enum switch statements |
||||||
|
- gofmt # files are gofmt'ed |
||||||
|
- gosec # security |
||||||
|
- nestif # deeply nested ifs |
||||||
|
- nilerr # returns nil even with non-nil error |
||||||
|
- prealloc # slices that can be pre-allocated |
||||||
|
- structcheck # unused struct fields |
||||||
|
- unparam # unused function params |
||||||
|
|
||||||
|
issues: |
||||||
|
exclude-rules: |
||||||
|
# err is very often shadowed in nested scopes |
||||||
|
- linters: |
||||||
|
- govet |
||||||
|
text: '^shadow: declaration of "err" shadows declaration' |
||||||
|
|
||||||
|
# ignore long lines for skip autogen directives |
||||||
|
- linters: |
||||||
|
- revive |
||||||
|
text: "^line-length-limit: " |
||||||
|
source: "^//(go:generate|sys) " |
||||||
|
|
||||||
|
# allow unjustified ignores of error checks in defer statements |
||||||
|
- linters: |
||||||
|
- nolintlint |
||||||
|
text: "^directive `//nolint:errcheck` should provide explanation" |
||||||
|
source: '^\s*defer ' |
||||||
|
|
||||||
|
# allow unjustified ignores of error lints for io.EOF |
||||||
|
- linters: |
||||||
|
- nolintlint |
||||||
|
text: "^directive `//nolint:errorlint` should provide explanation" |
||||||
|
source: '[=|!]= io.EOF' |
||||||
|
|
||||||
|
|
||||||
|
linters-settings: |
||||||
|
govet: |
||||||
|
enable-all: true |
||||||
|
disable: |
||||||
|
# struct order is often for Win32 compat |
||||||
|
# also, ignore pointer bytes/GC issues for now until performance becomes an issue |
||||||
|
- fieldalignment |
||||||
|
check-shadowing: true |
||||||
|
nolintlint: |
||||||
|
allow-leading-space: false |
||||||
|
require-explanation: true |
||||||
|
require-specific: true |
||||||
|
revive: |
||||||
|
# revive is more configurable than static check, so likely the preferred alternative to static-check |
||||||
|
# (once the perf issue is solved: https://github.com/golangci/golangci-lint/issues/2997) |
||||||
|
enable-all-rules: |
||||||
|
true |
||||||
|
# https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md |
||||||
|
rules: |
||||||
|
# rules with required arguments |
||||||
|
- name: argument-limit |
||||||
|
disabled: true |
||||||
|
- name: banned-characters |
||||||
|
disabled: true |
||||||
|
- name: cognitive-complexity |
||||||
|
disabled: true |
||||||
|
- name: cyclomatic |
||||||
|
disabled: true |
||||||
|
- name: file-header |
||||||
|
disabled: true |
||||||
|
- name: function-length |
||||||
|
disabled: true |
||||||
|
- name: function-result-limit |
||||||
|
disabled: true |
||||||
|
- name: max-public-structs |
||||||
|
disabled: true |
||||||
|
# geneally annoying rules |
||||||
|
- name: add-constant # complains about any and all strings and integers |
||||||
|
disabled: true |
||||||
|
- name: confusing-naming # we frequently use "Foo()" and "foo()" together |
||||||
|
disabled: true |
||||||
|
- name: flag-parameter # excessive, and a common idiom we use |
||||||
|
disabled: true |
||||||
|
# general config |
||||||
|
- name: line-length-limit |
||||||
|
arguments: |
||||||
|
- 140 |
||||||
|
- name: var-naming |
||||||
|
arguments: |
||||||
|
- [] |
||||||
|
- - CID |
||||||
|
- CRI |
||||||
|
- CTRD |
||||||
|
- DACL |
||||||
|
- DLL |
||||||
|
- DOS |
||||||
|
- ETW |
||||||
|
- FSCTL |
||||||
|
- GCS |
||||||
|
- GMSA |
||||||
|
- HCS |
||||||
|
- HV |
||||||
|
- IO |
||||||
|
- LCOW |
||||||
|
- LDAP |
||||||
|
- LPAC |
||||||
|
- LTSC |
||||||
|
- MMIO |
||||||
|
- NT |
||||||
|
- OCI |
||||||
|
- PMEM |
||||||
|
- PWSH |
||||||
|
- RX |
||||||
|
- SACl |
||||||
|
- SID |
||||||
|
- SMB |
||||||
|
- TX |
||||||
|
- VHD |
||||||
|
- VHDX |
||||||
|
- VMID |
||||||
|
- VPCI |
||||||
|
- WCOW |
||||||
|
- WIM |
||||||
|
stylecheck: |
||||||
|
checks: |
||||||
|
- "all" |
||||||
|
- "-ST1003" # use revive's var naming |
@ -0,0 +1,41 @@ |
|||||||
|
<!-- BEGIN MICROSOFT SECURITY.MD V0.0.7 BLOCK --> |
||||||
|
|
||||||
|
## Security |
||||||
|
|
||||||
|
Microsoft takes the security of our software products and services seriously, which includes all source code repositories managed through our GitHub organizations, which include [Microsoft](https://github.com/Microsoft), [Azure](https://github.com/Azure), [DotNet](https://github.com/dotnet), [AspNet](https://github.com/aspnet), [Xamarin](https://github.com/xamarin), and [our GitHub organizations](https://opensource.microsoft.com/). |
||||||
|
|
||||||
|
If you believe you have found a security vulnerability in any Microsoft-owned repository that meets [Microsoft's definition of a security vulnerability](https://aka.ms/opensource/security/definition), please report it to us as described below. |
||||||
|
|
||||||
|
## Reporting Security Issues |
||||||
|
|
||||||
|
**Please do not report security vulnerabilities through public GitHub issues.** |
||||||
|
|
||||||
|
Instead, please report them to the Microsoft Security Response Center (MSRC) at [https://msrc.microsoft.com/create-report](https://aka.ms/opensource/security/create-report). |
||||||
|
|
||||||
|
If you prefer to submit without logging in, send email to [secure@microsoft.com](mailto:secure@microsoft.com). If possible, encrypt your message with our PGP key; please download it from the [Microsoft Security Response Center PGP Key page](https://aka.ms/opensource/security/pgpkey). |
||||||
|
|
||||||
|
You should receive a response within 24 hours. If for some reason you do not, please follow up via email to ensure we received your original message. Additional information can be found at [microsoft.com/msrc](https://aka.ms/opensource/security/msrc). |
||||||
|
|
||||||
|
Please include the requested information listed below (as much as you can provide) to help us better understand the nature and scope of the possible issue: |
||||||
|
|
||||||
|
* Type of issue (e.g. buffer overflow, SQL injection, cross-site scripting, etc.) |
||||||
|
* Full paths of source file(s) related to the manifestation of the issue |
||||||
|
* The location of the affected source code (tag/branch/commit or direct URL) |
||||||
|
* Any special configuration required to reproduce the issue |
||||||
|
* Step-by-step instructions to reproduce the issue |
||||||
|
* Proof-of-concept or exploit code (if possible) |
||||||
|
* Impact of the issue, including how an attacker might exploit the issue |
||||||
|
|
||||||
|
This information will help us triage your report more quickly. |
||||||
|
|
||||||
|
If you are reporting for a bug bounty, more complete reports can contribute to a higher bounty award. Please visit our [Microsoft Bug Bounty Program](https://aka.ms/opensource/security/bounty) page for more details about our active programs. |
||||||
|
|
||||||
|
## Preferred Languages |
||||||
|
|
||||||
|
We prefer all communications to be in English. |
||||||
|
|
||||||
|
## Policy |
||||||
|
|
||||||
|
Microsoft follows the principle of [Coordinated Vulnerability Disclosure](https://aka.ms/opensource/security/cvd). |
||||||
|
|
||||||
|
<!-- END MICROSOFT SECURITY.MD BLOCK --> |
@ -0,0 +1,22 @@ |
|||||||
|
// This package provides utilities for efficiently performing Win32 IO operations in Go.
|
||||||
|
// Currently, this package is provides support for genreal IO and management of
|
||||||
|
// - named pipes
|
||||||
|
// - files
|
||||||
|
// - [Hyper-V sockets]
|
||||||
|
//
|
||||||
|
// This code is similar to Go's [net] package, and uses IO completion ports to avoid
|
||||||
|
// blocking IO on system threads, allowing Go to reuse the thread to schedule other goroutines.
|
||||||
|
//
|
||||||
|
// This limits support to Windows Vista and newer operating systems.
|
||||||
|
//
|
||||||
|
// Additionally, this package provides support for:
|
||||||
|
// - creating and managing GUIDs
|
||||||
|
// - writing to [ETW]
|
||||||
|
// - opening and manageing VHDs
|
||||||
|
// - parsing [Windows Image files]
|
||||||
|
// - auto-generating Win32 API code
|
||||||
|
//
|
||||||
|
// [Hyper-V sockets]: https://docs.microsoft.com/en-us/virtualization/hyper-v-on-windows/user-guide/make-integration-service
|
||||||
|
// [ETW]: https://docs.microsoft.com/en-us/windows-hardware/drivers/devtest/event-tracing-for-windows--etw-
|
||||||
|
// [Windows Image files]: https://docs.microsoft.com/en-us/windows-hardware/manufacture/desktop/work-with-windows-images
|
||||||
|
package winio |
@ -0,0 +1,20 @@ |
|||||||
|
package socket |
||||||
|
|
||||||
|
import ( |
||||||
|
"unsafe" |
||||||
|
) |
||||||
|
|
||||||
|
// RawSockaddr allows structs to be used with [Bind] and [ConnectEx]. The
|
||||||
|
// struct must meet the Win32 sockaddr requirements specified here:
|
||||||
|
// https://docs.microsoft.com/en-us/windows/win32/winsock/sockaddr-2
|
||||||
|
//
|
||||||
|
// Specifically, the struct size must be least larger than an int16 (unsigned short)
|
||||||
|
// for the address family.
|
||||||
|
type RawSockaddr interface { |
||||||
|
// Sockaddr returns a pointer to the RawSockaddr and its struct size, allowing
|
||||||
|
// for the RawSockaddr's data to be overwritten by syscalls (if necessary).
|
||||||
|
//
|
||||||
|
// It is the callers responsibility to validate that the values are valid; invalid
|
||||||
|
// pointers or size can cause a panic.
|
||||||
|
Sockaddr() (unsafe.Pointer, int32, error) |
||||||
|
} |
@ -0,0 +1,179 @@ |
|||||||
|
//go:build windows
|
||||||
|
|
||||||
|
package socket |
||||||
|
|
||||||
|
import ( |
||||||
|
"errors" |
||||||
|
"fmt" |
||||||
|
"net" |
||||||
|
"sync" |
||||||
|
"syscall" |
||||||
|
"unsafe" |
||||||
|
|
||||||
|
"github.com/Microsoft/go-winio/pkg/guid" |
||||||
|
"golang.org/x/sys/windows" |
||||||
|
) |
||||||
|
|
||||||
|
//go:generate go run github.com/Microsoft/go-winio/tools/mkwinsyscall -output zsyscall_windows.go socket.go
|
||||||
|
|
||||||
|
//sys getsockname(s windows.Handle, name unsafe.Pointer, namelen *int32) (err error) [failretval==socketError] = ws2_32.getsockname
|
||||||
|
//sys getpeername(s windows.Handle, name unsafe.Pointer, namelen *int32) (err error) [failretval==socketError] = ws2_32.getpeername
|
||||||
|
//sys bind(s windows.Handle, name unsafe.Pointer, namelen int32) (err error) [failretval==socketError] = ws2_32.bind
|
||||||
|
|
||||||
|
const socketError = uintptr(^uint32(0)) |
||||||
|
|
||||||
|
var ( |
||||||
|
// todo(helsaawy): create custom error types to store the desired vs actual size and addr family?
|
||||||
|
|
||||||
|
ErrBufferSize = errors.New("buffer size") |
||||||
|
ErrAddrFamily = errors.New("address family") |
||||||
|
ErrInvalidPointer = errors.New("invalid pointer") |
||||||
|
ErrSocketClosed = fmt.Errorf("socket closed: %w", net.ErrClosed) |
||||||
|
) |
||||||
|
|
||||||
|
// todo(helsaawy): replace these with generics, ie: GetSockName[S RawSockaddr](s windows.Handle) (S, error)
|
||||||
|
|
||||||
|
// GetSockName writes the local address of socket s to the [RawSockaddr] rsa.
|
||||||
|
// If rsa is not large enough, the [windows.WSAEFAULT] is returned.
|
||||||
|
func GetSockName(s windows.Handle, rsa RawSockaddr) error { |
||||||
|
ptr, l, err := rsa.Sockaddr() |
||||||
|
if err != nil { |
||||||
|
return fmt.Errorf("could not retrieve socket pointer and size: %w", err) |
||||||
|
} |
||||||
|
|
||||||
|
// although getsockname returns WSAEFAULT if the buffer is too small, it does not set
|
||||||
|
// &l to the correct size, so--apart from doubling the buffer repeatedly--there is no remedy
|
||||||
|
return getsockname(s, ptr, &l) |
||||||
|
} |
||||||
|
|
||||||
|
// GetPeerName returns the remote address the socket is connected to.
|
||||||
|
//
|
||||||
|
// See [GetSockName] for more information.
|
||||||
|
func GetPeerName(s windows.Handle, rsa RawSockaddr) error { |
||||||
|
ptr, l, err := rsa.Sockaddr() |
||||||
|
if err != nil { |
||||||
|
return fmt.Errorf("could not retrieve socket pointer and size: %w", err) |
||||||
|
} |
||||||
|
|
||||||
|
return getpeername(s, ptr, &l) |
||||||
|
} |
||||||
|
|
||||||
|
func Bind(s windows.Handle, rsa RawSockaddr) (err error) { |
||||||
|
ptr, l, err := rsa.Sockaddr() |
||||||
|
if err != nil { |
||||||
|
return fmt.Errorf("could not retrieve socket pointer and size: %w", err) |
||||||
|
} |
||||||
|
|
||||||
|
return bind(s, ptr, l) |
||||||
|
} |
||||||
|
|
||||||
|
// "golang.org/x/sys/windows".ConnectEx and .Bind only accept internal implementations of the
|
||||||
|
// their sockaddr interface, so they cannot be used with HvsockAddr
|
||||||
|
// Replicate functionality here from
|
||||||
|
// https://cs.opensource.google/go/x/sys/+/master:windows/syscall_windows.go
|
||||||
|
|
||||||
|
// The function pointers to `AcceptEx`, `ConnectEx` and `GetAcceptExSockaddrs` must be loaded at
|
||||||
|
// runtime via a WSAIoctl call:
|
||||||
|
// https://docs.microsoft.com/en-us/windows/win32/api/Mswsock/nc-mswsock-lpfn_connectex#remarks
|
||||||
|
|
||||||
|
type runtimeFunc struct { |
||||||
|
id guid.GUID |
||||||
|
once sync.Once |
||||||
|
addr uintptr |
||||||
|
err error |
||||||
|
} |
||||||
|
|
||||||
|
func (f *runtimeFunc) Load() error { |
||||||
|
f.once.Do(func() { |
||||||
|
var s windows.Handle |
||||||
|
s, f.err = windows.Socket(windows.AF_INET, windows.SOCK_STREAM, windows.IPPROTO_TCP) |
||||||
|
if f.err != nil { |
||||||
|
return |
||||||
|
} |
||||||
|
defer windows.CloseHandle(s) //nolint:errcheck
|
||||||
|
|
||||||
|
var n uint32 |
||||||
|
f.err = windows.WSAIoctl(s, |
||||||
|
windows.SIO_GET_EXTENSION_FUNCTION_POINTER, |
||||||
|
(*byte)(unsafe.Pointer(&f.id)), |
||||||
|
uint32(unsafe.Sizeof(f.id)), |
||||||
|
(*byte)(unsafe.Pointer(&f.addr)), |
||||||
|
uint32(unsafe.Sizeof(f.addr)), |
||||||
|
&n, |
||||||
|
nil, //overlapped
|
||||||
|
0, //completionRoutine
|
||||||
|
) |
||||||
|
}) |
||||||
|
return f.err |
||||||
|
} |
||||||
|
|
||||||
|
var ( |
||||||
|
// todo: add `AcceptEx` and `GetAcceptExSockaddrs`
|
||||||
|
WSAID_CONNECTEX = guid.GUID{ //revive:disable-line:var-naming ALL_CAPS
|
||||||
|
Data1: 0x25a207b9, |
||||||
|
Data2: 0xddf3, |
||||||
|
Data3: 0x4660, |
||||||
|
Data4: [8]byte{0x8e, 0xe9, 0x76, 0xe5, 0x8c, 0x74, 0x06, 0x3e}, |
||||||
|
} |
||||||
|
|
||||||
|
connectExFunc = runtimeFunc{id: WSAID_CONNECTEX} |
||||||
|
) |
||||||
|
|
||||||
|
func ConnectEx( |
||||||
|
fd windows.Handle, |
||||||
|
rsa RawSockaddr, |
||||||
|
sendBuf *byte, |
||||||
|
sendDataLen uint32, |
||||||
|
bytesSent *uint32, |
||||||
|
overlapped *windows.Overlapped, |
||||||
|
) error { |
||||||
|
if err := connectExFunc.Load(); err != nil { |
||||||
|
return fmt.Errorf("failed to load ConnectEx function pointer: %w", err) |
||||||
|
} |
||||||
|
ptr, n, err := rsa.Sockaddr() |
||||||
|
if err != nil { |
||||||
|
return err |
||||||
|
} |
||||||
|
return connectEx(fd, ptr, n, sendBuf, sendDataLen, bytesSent, overlapped) |
||||||
|
} |
||||||
|
|
||||||
|
// BOOL LpfnConnectex(
|
||||||
|
// [in] SOCKET s,
|
||||||
|
// [in] const sockaddr *name,
|
||||||
|
// [in] int namelen,
|
||||||
|
// [in, optional] PVOID lpSendBuffer,
|
||||||
|
// [in] DWORD dwSendDataLength,
|
||||||
|
// [out] LPDWORD lpdwBytesSent,
|
||||||
|
// [in] LPOVERLAPPED lpOverlapped
|
||||||
|
// )
|
||||||
|
|
||||||
|
func connectEx( |
||||||
|
s windows.Handle, |
||||||
|
name unsafe.Pointer, |
||||||
|
namelen int32, |
||||||
|
sendBuf *byte, |
||||||
|
sendDataLen uint32, |
||||||
|
bytesSent *uint32, |
||||||
|
overlapped *windows.Overlapped, |
||||||
|
) (err error) { |
||||||
|
// todo: after upgrading to 1.18, switch from syscall.Syscall9 to syscall.SyscallN
|
||||||
|
r1, _, e1 := syscall.Syscall9(connectExFunc.addr, |
||||||
|
7, |
||||||
|
uintptr(s), |
||||||
|
uintptr(name), |
||||||
|
uintptr(namelen), |
||||||
|
uintptr(unsafe.Pointer(sendBuf)), |
||||||
|
uintptr(sendDataLen), |
||||||
|
uintptr(unsafe.Pointer(bytesSent)), |
||||||
|
uintptr(unsafe.Pointer(overlapped)), |
||||||
|
0, |
||||||
|
0) |
||||||
|
if r1 == 0 { |
||||||
|
if e1 != 0 { |
||||||
|
err = error(e1) |
||||||
|
} else { |
||||||
|
err = syscall.EINVAL |
||||||
|
} |
||||||
|
} |
||||||
|
return err |
||||||
|
} |
@ -0,0 +1,72 @@ |
|||||||
|
//go:build windows
|
||||||
|
|
||||||
|
// Code generated by 'go generate' using "github.com/Microsoft/go-winio/tools/mkwinsyscall"; DO NOT EDIT.
|
||||||
|
|
||||||
|
package socket |
||||||
|
|
||||||
|
import ( |
||||||
|
"syscall" |
||||||
|
"unsafe" |
||||||
|
|
||||||
|
"golang.org/x/sys/windows" |
||||||
|
) |
||||||
|
|
||||||
|
var _ unsafe.Pointer |
||||||
|
|
||||||
|
// Do the interface allocations only once for common
|
||||||
|
// Errno values.
|
||||||
|
const ( |
||||||
|
errnoERROR_IO_PENDING = 997 |
||||||
|
) |
||||||
|
|
||||||
|
var ( |
||||||
|
errERROR_IO_PENDING error = syscall.Errno(errnoERROR_IO_PENDING) |
||||||
|
errERROR_EINVAL error = syscall.EINVAL |
||||||
|
) |
||||||
|
|
||||||
|
// errnoErr returns common boxed Errno values, to prevent
|
||||||
|
// allocations at runtime.
|
||||||
|
func errnoErr(e syscall.Errno) error { |
||||||
|
switch e { |
||||||
|
case 0: |
||||||
|
return errERROR_EINVAL |
||||||
|
case errnoERROR_IO_PENDING: |
||||||
|
return errERROR_IO_PENDING |
||||||
|
} |
||||||
|
// TODO: add more here, after collecting data on the common
|
||||||
|
// error values see on Windows. (perhaps when running
|
||||||
|
// all.bat?)
|
||||||
|
return e |
||||||
|
} |
||||||
|
|
||||||
|
var ( |
||||||
|
modws2_32 = windows.NewLazySystemDLL("ws2_32.dll") |
||||||
|
|
||||||
|
procbind = modws2_32.NewProc("bind") |
||||||
|
procgetpeername = modws2_32.NewProc("getpeername") |
||||||
|
procgetsockname = modws2_32.NewProc("getsockname") |
||||||
|
) |
||||||
|
|
||||||
|
func bind(s windows.Handle, name unsafe.Pointer, namelen int32) (err error) { |
||||||
|
r1, _, e1 := syscall.Syscall(procbind.Addr(), 3, uintptr(s), uintptr(name), uintptr(namelen)) |
||||||
|
if r1 == socketError { |
||||||
|
err = errnoErr(e1) |
||||||
|
} |
||||||
|
return |
||||||
|
} |
||||||
|
|
||||||
|
func getpeername(s windows.Handle, name unsafe.Pointer, namelen *int32) (err error) { |
||||||
|
r1, _, e1 := syscall.Syscall(procgetpeername.Addr(), 3, uintptr(s), uintptr(name), uintptr(unsafe.Pointer(namelen))) |
||||||
|
if r1 == socketError { |
||||||
|
err = errnoErr(e1) |
||||||
|
} |
||||||
|
return |
||||||
|
} |
||||||
|
|
||||||
|
func getsockname(s windows.Handle, name unsafe.Pointer, namelen *int32) (err error) { |
||||||
|
r1, _, e1 := syscall.Syscall(procgetsockname.Addr(), 3, uintptr(s), uintptr(name), uintptr(unsafe.Pointer(namelen))) |
||||||
|
if r1 == socketError { |
||||||
|
err = errnoErr(e1) |
||||||
|
} |
||||||
|
return |
||||||
|
} |
@ -0,0 +1,27 @@ |
|||||||
|
// Code generated by "stringer -type=Variant -trimprefix=Variant -linecomment"; DO NOT EDIT.
|
||||||
|
|
||||||
|
package guid |
||||||
|
|
||||||
|
import "strconv" |
||||||
|
|
||||||
|
func _() { |
||||||
|
// An "invalid array index" compiler error signifies that the constant values have changed.
|
||||||
|
// Re-run the stringer command to generate them again.
|
||||||
|
var x [1]struct{} |
||||||
|
_ = x[VariantUnknown-0] |
||||||
|
_ = x[VariantNCS-1] |
||||||
|
_ = x[VariantRFC4122-2] |
||||||
|
_ = x[VariantMicrosoft-3] |
||||||
|
_ = x[VariantFuture-4] |
||||||
|
} |
||||||
|
|
||||||
|
const _Variant_name = "UnknownNCSRFC 4122MicrosoftFuture" |
||||||
|
|
||||||
|
var _Variant_index = [...]uint8{0, 7, 10, 18, 27, 33} |
||||||
|
|
||||||
|
func (i Variant) String() string { |
||||||
|
if i >= Variant(len(_Variant_index)-1) { |
||||||
|
return "Variant(" + strconv.FormatInt(int64(i), 10) + ")" |
||||||
|
} |
||||||
|
return _Variant_name[_Variant_index[i]:_Variant_index[i+1]] |
||||||
|
} |
@ -1,3 +1,5 @@ |
|||||||
|
//go:build windows
|
||||||
|
|
||||||
package winio |
package winio |
||||||
|
|
||||||
//go:generate go run golang.org/x/sys/windows/mkwinsyscall -output zsyscall_windows.go file.go pipe.go sd.go fileinfo.go privilege.go backup.go hvsock.go
|
//go:generate go run github.com/Microsoft/go-winio/tools/mkwinsyscall -output zsyscall_windows.go ./*.go
|
||||||
|
@ -0,0 +1,5 @@ |
|||||||
|
//go:build tools
|
||||||
|
|
||||||
|
package winio |
||||||
|
|
||||||
|
import _ "golang.org/x/tools/cmd/stringer" |
@ -0,0 +1,10 @@ |
|||||||
|
#!/bin/bash |
||||||
|
set -eu -o pipefail |
||||||
|
|
||||||
|
# Small convenience script for running the tests with various combinations of |
||||||
|
# arch/tags. This assumes we're running on amd64 and have qemu available. |
||||||
|
|
||||||
|
go test ./... |
||||||
|
go test -tags purego ./... |
||||||
|
GOARCH=arm64 go test |
||||||
|
GOARCH=arm64 go test -tags purego |
@ -1,215 +1,209 @@ |
|||||||
|
//go:build !appengine && gc && !purego |
||||||
// +build !appengine |
// +build !appengine |
||||||
// +build gc |
// +build gc |
||||||
// +build !purego |
// +build !purego |
||||||
|
|
||||||
#include "textflag.h" |
#include "textflag.h" |
||||||
|
|
||||||
// Register allocation: |
// Registers: |
||||||
// AX h |
#define h AX |
||||||
// SI pointer to advance through b |
#define d AX |
||||||
// DX n |
#define p SI // pointer to advance through b |
||||||
// BX loop end |
#define n DX |
||||||
// R8 v1, k1 |
#define end BX // loop end |
||||||
// R9 v2 |
#define v1 R8 |
||||||
// R10 v3 |
#define v2 R9 |
||||||
// R11 v4 |
#define v3 R10 |
||||||
// R12 tmp |
#define v4 R11 |
||||||
// R13 prime1v |
#define x R12 |
||||||
// R14 prime2v |
#define prime1 R13 |
||||||
// DI prime4v |
#define prime2 R14 |
||||||
|
#define prime4 DI |
||||||
// round reads from and advances the buffer pointer in SI. |
|
||||||
// It assumes that R13 has prime1v and R14 has prime2v. |
#define round(acc, x) \ |
||||||
#define round(r) \ |
IMULQ prime2, x \ |
||||||
MOVQ (SI), R12 \ |
ADDQ x, acc \ |
||||||
ADDQ $8, SI \ |
ROLQ $31, acc \ |
||||||
IMULQ R14, R12 \ |
IMULQ prime1, acc |
||||||
ADDQ R12, r \ |
|
||||||
ROLQ $31, r \ |
// round0 performs the operation x = round(0, x). |
||||||
IMULQ R13, r |
#define round0(x) \ |
||||||
|
IMULQ prime2, x \ |
||||||
// mergeRound applies a merge round on the two registers acc and val. |
ROLQ $31, x \ |
||||||
// It assumes that R13 has prime1v, R14 has prime2v, and DI has prime4v. |
IMULQ prime1, x |
||||||
#define mergeRound(acc, val) \ |
|
||||||
IMULQ R14, val \ |
// mergeRound applies a merge round on the two registers acc and x. |
||||||
ROLQ $31, val \ |
// It assumes that prime1, prime2, and prime4 have been loaded. |
||||||
IMULQ R13, val \ |
#define mergeRound(acc, x) \ |
||||||
XORQ val, acc \ |
round0(x) \ |
||||||
IMULQ R13, acc \ |
XORQ x, acc \ |
||||||
ADDQ DI, acc |
IMULQ prime1, acc \ |
||||||
|
ADDQ prime4, acc |
||||||
|
|
||||||
|
// blockLoop processes as many 32-byte blocks as possible, |
||||||
|
// updating v1, v2, v3, and v4. It assumes that there is at least one block |
||||||
|
// to process. |
||||||
|
#define blockLoop() \ |
||||||
|
loop: \ |
||||||
|
MOVQ +0(p), x \ |
||||||
|
round(v1, x) \ |
||||||
|
MOVQ +8(p), x \ |
||||||
|
round(v2, x) \ |
||||||
|
MOVQ +16(p), x \ |
||||||
|
round(v3, x) \ |
||||||
|
MOVQ +24(p), x \ |
||||||
|
round(v4, x) \ |
||||||
|
ADDQ $32, p \ |
||||||
|
CMPQ p, end \ |
||||||
|
JLE loop |
||||||
|
|
||||||
// func Sum64(b []byte) uint64 |
// func Sum64(b []byte) uint64 |
||||||
TEXT ·Sum64(SB), NOSPLIT, $0-32 |
TEXT ·Sum64(SB), NOSPLIT|NOFRAME, $0-32 |
||||||
// Load fixed primes. |
// Load fixed primes. |
||||||
MOVQ ·prime1v(SB), R13 |
MOVQ ·primes+0(SB), prime1 |
||||||
MOVQ ·prime2v(SB), R14 |
MOVQ ·primes+8(SB), prime2 |
||||||
MOVQ ·prime4v(SB), DI |
MOVQ ·primes+24(SB), prime4 |
||||||
|
|
||||||
// Load slice. |
// Load slice. |
||||||
MOVQ b_base+0(FP), SI |
MOVQ b_base+0(FP), p |
||||||
MOVQ b_len+8(FP), DX |
MOVQ b_len+8(FP), n |
||||||
LEAQ (SI)(DX*1), BX |
LEAQ (p)(n*1), end |
||||||
|
|
||||||
// The first loop limit will be len(b)-32. |
// The first loop limit will be len(b)-32. |
||||||
SUBQ $32, BX |
SUBQ $32, end |
||||||
|
|
||||||
// Check whether we have at least one block. |
// Check whether we have at least one block. |
||||||
CMPQ DX, $32 |
CMPQ n, $32 |
||||||
JLT noBlocks |
JLT noBlocks |
||||||
|
|
||||||
// Set up initial state (v1, v2, v3, v4). |
// Set up initial state (v1, v2, v3, v4). |
||||||
MOVQ R13, R8 |
MOVQ prime1, v1 |
||||||
ADDQ R14, R8 |
ADDQ prime2, v1 |
||||||
MOVQ R14, R9 |
MOVQ prime2, v2 |
||||||
XORQ R10, R10 |
XORQ v3, v3 |
||||||
XORQ R11, R11 |
XORQ v4, v4 |
||||||
SUBQ R13, R11 |
SUBQ prime1, v4 |
||||||
|
|
||||||
// Loop until SI > BX. |
blockLoop() |
||||||
blockLoop: |
|
||||||
round(R8) |
MOVQ v1, h |
||||||
round(R9) |
ROLQ $1, h |
||||||
round(R10) |
MOVQ v2, x |
||||||
round(R11) |
ROLQ $7, x |
||||||
|
ADDQ x, h |
||||||
CMPQ SI, BX |
MOVQ v3, x |
||||||
JLE blockLoop |
ROLQ $12, x |
||||||
|
ADDQ x, h |
||||||
MOVQ R8, AX |
MOVQ v4, x |
||||||
ROLQ $1, AX |
ROLQ $18, x |
||||||
MOVQ R9, R12 |
ADDQ x, h |
||||||
ROLQ $7, R12 |
|
||||||
ADDQ R12, AX |
mergeRound(h, v1) |
||||||
MOVQ R10, R12 |
mergeRound(h, v2) |
||||||
ROLQ $12, R12 |
mergeRound(h, v3) |
||||||
ADDQ R12, AX |
mergeRound(h, v4) |
||||||
MOVQ R11, R12 |
|
||||||
ROLQ $18, R12 |
|
||||||
ADDQ R12, AX |
|
||||||
|
|
||||||
mergeRound(AX, R8) |
|
||||||
mergeRound(AX, R9) |
|
||||||
mergeRound(AX, R10) |
|
||||||
mergeRound(AX, R11) |
|
||||||
|
|
||||||
JMP afterBlocks |
JMP afterBlocks |
||||||
|
|
||||||
noBlocks: |
noBlocks: |
||||||
MOVQ ·prime5v(SB), AX |
MOVQ ·primes+32(SB), h |
||||||
|
|
||||||
afterBlocks: |
afterBlocks: |
||||||
ADDQ DX, AX |
ADDQ n, h |
||||||
|
|
||||||
// Right now BX has len(b)-32, and we want to loop until SI > len(b)-8. |
ADDQ $24, end |
||||||
ADDQ $24, BX |
CMPQ p, end |
||||||
|
JG try4 |
||||||
CMPQ SI, BX |
|
||||||
JG fourByte |
loop8: |
||||||
|
MOVQ (p), x |
||||||
wordLoop: |
ADDQ $8, p |
||||||
// Calculate k1. |
round0(x) |
||||||
MOVQ (SI), R8 |
XORQ x, h |
||||||
ADDQ $8, SI |
ROLQ $27, h |
||||||
IMULQ R14, R8 |
IMULQ prime1, h |
||||||
ROLQ $31, R8 |
ADDQ prime4, h |
||||||
IMULQ R13, R8 |
|
||||||
|
CMPQ p, end |
||||||
XORQ R8, AX |
JLE loop8 |
||||||
ROLQ $27, AX |
|
||||||
IMULQ R13, AX |
try4: |
||||||
ADDQ DI, AX |
ADDQ $4, end |
||||||
|
CMPQ p, end |
||||||
CMPQ SI, BX |
JG try1 |
||||||
JLE wordLoop |
|
||||||
|
MOVL (p), x |
||||||
fourByte: |
ADDQ $4, p |
||||||
ADDQ $4, BX |
IMULQ prime1, x |
||||||
CMPQ SI, BX |
XORQ x, h |
||||||
JG singles |
|
||||||
|
ROLQ $23, h |
||||||
MOVL (SI), R8 |
IMULQ prime2, h |
||||||
ADDQ $4, SI |
ADDQ ·primes+16(SB), h |
||||||
IMULQ R13, R8 |
|
||||||
XORQ R8, AX |
try1: |
||||||
|
ADDQ $4, end |
||||||
ROLQ $23, AX |
CMPQ p, end |
||||||
IMULQ R14, AX |
|
||||||
ADDQ ·prime3v(SB), AX |
|
||||||
|
|
||||||
singles: |
|
||||||
ADDQ $4, BX |
|
||||||
CMPQ SI, BX |
|
||||||
JGE finalize |
JGE finalize |
||||||
|
|
||||||
singlesLoop: |
loop1: |
||||||
MOVBQZX (SI), R12 |
MOVBQZX (p), x |
||||||
ADDQ $1, SI |
ADDQ $1, p |
||||||
IMULQ ·prime5v(SB), R12 |
IMULQ ·primes+32(SB), x |
||||||
XORQ R12, AX |
XORQ x, h |
||||||
|
ROLQ $11, h |
||||||
|
IMULQ prime1, h |
||||||
|
|
||||||
ROLQ $11, AX |
CMPQ p, end |
||||||
IMULQ R13, AX |
JL loop1 |
||||||
|
|
||||||
CMPQ SI, BX |
|
||||||
JL singlesLoop |
|
||||||
|
|
||||||
finalize: |
finalize: |
||||||
MOVQ AX, R12 |
MOVQ h, x |
||||||
SHRQ $33, R12 |
SHRQ $33, x |
||||||
XORQ R12, AX |
XORQ x, h |
||||||
IMULQ R14, AX |
IMULQ prime2, h |
||||||
MOVQ AX, R12 |
MOVQ h, x |
||||||
SHRQ $29, R12 |
SHRQ $29, x |
||||||
XORQ R12, AX |
XORQ x, h |
||||||
IMULQ ·prime3v(SB), AX |
IMULQ ·primes+16(SB), h |
||||||
MOVQ AX, R12 |
MOVQ h, x |
||||||
SHRQ $32, R12 |
SHRQ $32, x |
||||||
XORQ R12, AX |
XORQ x, h |
||||||
|
|
||||||
MOVQ AX, ret+24(FP) |
MOVQ h, ret+24(FP) |
||||||
RET |
RET |
||||||
|
|
||||||
// writeBlocks uses the same registers as above except that it uses AX to store |
|
||||||
// the d pointer. |
|
||||||
|
|
||||||
// func writeBlocks(d *Digest, b []byte) int |
// func writeBlocks(d *Digest, b []byte) int |
||||||
TEXT ·writeBlocks(SB), NOSPLIT, $0-40 |
TEXT ·writeBlocks(SB), NOSPLIT|NOFRAME, $0-40 |
||||||
// Load fixed primes needed for round. |
// Load fixed primes needed for round. |
||||||
MOVQ ·prime1v(SB), R13 |
MOVQ ·primes+0(SB), prime1 |
||||||
MOVQ ·prime2v(SB), R14 |
MOVQ ·primes+8(SB), prime2 |
||||||
|
|
||||||
// Load slice. |
// Load slice. |
||||||
MOVQ b_base+8(FP), SI |
MOVQ b_base+8(FP), p |
||||||
MOVQ b_len+16(FP), DX |
MOVQ b_len+16(FP), n |
||||||
LEAQ (SI)(DX*1), BX |
LEAQ (p)(n*1), end |
||||||
SUBQ $32, BX |
SUBQ $32, end |
||||||
|
|
||||||
// Load vN from d. |
// Load vN from d. |
||||||
MOVQ d+0(FP), AX |
MOVQ s+0(FP), d |
||||||
MOVQ 0(AX), R8 // v1 |
MOVQ 0(d), v1 |
||||||
MOVQ 8(AX), R9 // v2 |
MOVQ 8(d), v2 |
||||||
MOVQ 16(AX), R10 // v3 |
MOVQ 16(d), v3 |
||||||
MOVQ 24(AX), R11 // v4 |
MOVQ 24(d), v4 |
||||||
|
|
||||||
// We don't need to check the loop condition here; this function is
|
// We don't need to check the loop condition here; this function is
|
||||||
// always called with at least one block of data to process. |
// always called with at least one block of data to process. |
||||||
blockLoop: |
blockLoop() |
||||||
round(R8) |
|
||||||
round(R9) |
|
||||||
round(R10) |
|
||||||
round(R11) |
|
||||||
|
|
||||||
CMPQ SI, BX |
|
||||||
JLE blockLoop |
|
||||||
|
|
||||||
// Copy vN back to d. |
// Copy vN back to d. |
||||||
MOVQ R8, 0(AX) |
MOVQ v1, 0(d) |
||||||
MOVQ R9, 8(AX) |
MOVQ v2, 8(d) |
||||||
MOVQ R10, 16(AX) |
MOVQ v3, 16(d) |
||||||
MOVQ R11, 24(AX) |
MOVQ v4, 24(d) |
||||||
|
|
||||||
// The number of bytes written is SI minus the old base pointer. |
// The number of bytes written is p minus the old base pointer. |
||||||
SUBQ b_base+8(FP), SI |
SUBQ b_base+8(FP), p |
||||||
MOVQ SI, ret+32(FP) |
MOVQ p, ret+32(FP) |
||||||
|
|
||||||
RET |
RET |
||||||
|
@ -0,0 +1,183 @@ |
|||||||
|
//go:build !appengine && gc && !purego |
||||||
|
// +build !appengine |
||||||
|
// +build gc |
||||||
|
// +build !purego |
||||||
|
|
||||||
|
#include "textflag.h" |
||||||
|
|
||||||
|
// Registers: |
||||||
|
#define digest R1 |
||||||
|
#define h R2 // return value |
||||||
|
#define p R3 // input pointer |
||||||
|
#define n R4 // input length |
||||||
|
#define nblocks R5 // n / 32 |
||||||
|
#define prime1 R7 |
||||||
|
#define prime2 R8 |
||||||
|
#define prime3 R9 |
||||||
|
#define prime4 R10 |
||||||
|
#define prime5 R11 |
||||||
|
#define v1 R12 |
||||||
|
#define v2 R13 |
||||||
|
#define v3 R14 |
||||||
|
#define v4 R15 |
||||||
|
#define x1 R20 |
||||||
|
#define x2 R21 |
||||||
|
#define x3 R22 |
||||||
|
#define x4 R23 |
||||||
|
|
||||||
|
#define round(acc, x) \ |
||||||
|
MADD prime2, acc, x, acc \ |
||||||
|
ROR $64-31, acc \ |
||||||
|
MUL prime1, acc |
||||||
|
|
||||||
|
// round0 performs the operation x = round(0, x). |
||||||
|
#define round0(x) \ |
||||||
|
MUL prime2, x \ |
||||||
|
ROR $64-31, x \ |
||||||
|
MUL prime1, x |
||||||
|
|
||||||
|
#define mergeRound(acc, x) \ |
||||||
|
round0(x) \ |
||||||
|
EOR x, acc \ |
||||||
|
MADD acc, prime4, prime1, acc |
||||||
|
|
||||||
|
// blockLoop processes as many 32-byte blocks as possible, |
||||||
|
// updating v1, v2, v3, and v4. It assumes that n >= 32. |
||||||
|
#define blockLoop() \ |
||||||
|
LSR $5, n, nblocks \ |
||||||
|
PCALIGN $16 \ |
||||||
|
loop: \ |
||||||
|
LDP.P 16(p), (x1, x2) \ |
||||||
|
LDP.P 16(p), (x3, x4) \ |
||||||
|
round(v1, x1) \ |
||||||
|
round(v2, x2) \ |
||||||
|
round(v3, x3) \ |
||||||
|
round(v4, x4) \ |
||||||
|
SUB $1, nblocks \ |
||||||
|
CBNZ nblocks, loop |
||||||
|
|
||||||
|
// func Sum64(b []byte) uint64 |
||||||
|
TEXT ·Sum64(SB), NOSPLIT|NOFRAME, $0-32 |
||||||
|
LDP b_base+0(FP), (p, n) |
||||||
|
|
||||||
|
LDP ·primes+0(SB), (prime1, prime2) |
||||||
|
LDP ·primes+16(SB), (prime3, prime4) |
||||||
|
MOVD ·primes+32(SB), prime5 |
||||||
|
|
||||||
|
CMP $32, n |
||||||
|
CSEL LT, prime5, ZR, h // if n < 32 { h = prime5 } else { h = 0 } |
||||||
|
BLT afterLoop |
||||||
|
|
||||||
|
ADD prime1, prime2, v1 |
||||||
|
MOVD prime2, v2 |
||||||
|
MOVD $0, v3 |
||||||
|
NEG prime1, v4 |
||||||
|
|
||||||
|
blockLoop() |
||||||
|
|
||||||
|
ROR $64-1, v1, x1 |
||||||
|
ROR $64-7, v2, x2 |
||||||
|
ADD x1, x2 |
||||||
|
ROR $64-12, v3, x3 |
||||||
|
ROR $64-18, v4, x4 |
||||||
|
ADD x3, x4 |
||||||
|
ADD x2, x4, h |
||||||
|
|
||||||
|
mergeRound(h, v1) |
||||||
|
mergeRound(h, v2) |
||||||
|
mergeRound(h, v3) |
||||||
|
mergeRound(h, v4) |
||||||
|
|
||||||
|
afterLoop: |
||||||
|
ADD n, h |
||||||
|
|
||||||
|
TBZ $4, n, try8 |
||||||
|
LDP.P 16(p), (x1, x2) |
||||||
|
|
||||||
|
round0(x1) |
||||||
|
|
||||||
|
// NOTE: here and below, sequencing the EOR after the ROR (using a |
||||||
|
// rotated register) is worth a small but measurable speedup for small |
||||||
|
// inputs. |
||||||
|
ROR $64-27, h |
||||||
|
EOR x1 @> 64-27, h, h
|
||||||
|
MADD h, prime4, prime1, h |
||||||
|
|
||||||
|
round0(x2) |
||||||
|
ROR $64-27, h |
||||||
|
EOR x2 @> 64-27, h, h
|
||||||
|
MADD h, prime4, prime1, h |
||||||
|
|
||||||
|
try8: |
||||||
|
TBZ $3, n, try4 |
||||||
|
MOVD.P 8(p), x1 |
||||||
|
|
||||||
|
round0(x1) |
||||||
|
ROR $64-27, h |
||||||
|
EOR x1 @> 64-27, h, h
|
||||||
|
MADD h, prime4, prime1, h |
||||||
|
|
||||||
|
try4: |
||||||
|
TBZ $2, n, try2 |
||||||
|
MOVWU.P 4(p), x2 |
||||||
|
|
||||||
|
MUL prime1, x2 |
||||||
|
ROR $64-23, h |
||||||
|
EOR x2 @> 64-23, h, h
|
||||||
|
MADD h, prime3, prime2, h |
||||||
|
|
||||||
|
try2: |
||||||
|
TBZ $1, n, try1 |
||||||
|
MOVHU.P 2(p), x3 |
||||||
|
AND $255, x3, x1 |
||||||
|
LSR $8, x3, x2 |
||||||
|
|
||||||
|
MUL prime5, x1 |
||||||
|
ROR $64-11, h |
||||||
|
EOR x1 @> 64-11, h, h
|
||||||
|
MUL prime1, h |
||||||
|
|
||||||
|
MUL prime5, x2 |
||||||
|
ROR $64-11, h |
||||||
|
EOR x2 @> 64-11, h, h
|
||||||
|
MUL prime1, h |
||||||
|
|
||||||
|
try1: |
||||||
|
TBZ $0, n, finalize |
||||||
|
MOVBU (p), x4 |
||||||
|
|
||||||
|
MUL prime5, x4 |
||||||
|
ROR $64-11, h |
||||||
|
EOR x4 @> 64-11, h, h
|
||||||
|
MUL prime1, h |
||||||
|
|
||||||
|
finalize: |
||||||
|
EOR h >> 33, h |
||||||
|
MUL prime2, h |
||||||
|
EOR h >> 29, h |
||||||
|
MUL prime3, h |
||||||
|
EOR h >> 32, h |
||||||
|
|
||||||
|
MOVD h, ret+24(FP) |
||||||
|
RET |
||||||
|
|
||||||
|
// func writeBlocks(d *Digest, b []byte) int |
||||||
|
TEXT ·writeBlocks(SB), NOSPLIT|NOFRAME, $0-40 |
||||||
|
LDP ·primes+0(SB), (prime1, prime2) |
||||||
|
|
||||||
|
// Load state. Assume v[1-4] are stored contiguously. |
||||||
|
MOVD d+0(FP), digest |
||||||
|
LDP 0(digest), (v1, v2) |
||||||
|
LDP 16(digest), (v3, v4) |
||||||
|
|
||||||
|
LDP b_base+8(FP), (p, n) |
||||||
|
|
||||||
|
blockLoop() |
||||||
|
|
||||||
|
// Store updated state. |
||||||
|
STP (v1, v2), 0(digest) |
||||||
|
STP (v3, v4), 16(digest) |
||||||
|
|
||||||
|
BIC $31, n |
||||||
|
MOVD n, ret+32(FP) |
||||||
|
RET |
@ -1,3 +1,5 @@ |
|||||||
|
//go:build (amd64 || arm64) && !appengine && gc && !purego
|
||||||
|
// +build amd64 arm64
|
||||||
// +build !appengine
|
// +build !appengine
|
||||||
// +build gc
|
// +build gc
|
||||||
// +build !purego
|
// +build !purego
|
@ -1,4 +1,16 @@ |
|||||||
package credentials |
package credentials |
||||||
|
|
||||||
// Version holds a string describing the current version
|
var ( |
||||||
const Version = "0.6.4" |
// Name is filled at linking time
|
||||||
|
Name = "" |
||||||
|
|
||||||
|
// Package is filled at linking time
|
||||||
|
Package = "github.com/docker/docker-credential-helpers" |
||||||
|
|
||||||
|
// Version holds the complete version number. Filled in at linking time.
|
||||||
|
Version = "v0.0.0+unknown" |
||||||
|
|
||||||
|
// Revision is filled with the VCS (e.g. git) revision being used to build
|
||||||
|
// the program at linking time.
|
||||||
|
Revision = "" |
||||||
|
) |
||||||
|
@ -1,6 +1,6 @@ |
|||||||
# Setup a Global .gitignore for OS and editor generated files: |
# go test -c output |
||||||
# https://help.github.com/articles/ignoring-files |
*.test |
||||||
# git config --global core.excludesfile ~/.gitignore_global |
*.test.exe |
||||||
|
|
||||||
.vagrant |
# Output of go build ./cmd/fsnotify |
||||||
*.sublime-project |
/fsnotify |
||||||
|
@ -1,62 +0,0 @@ |
|||||||
# Names should be added to this file as |
|
||||||
# Name or Organization <email address> |
|
||||||
# The email address is not required for organizations. |
|
||||||
|
|
||||||
# You can update this list using the following command: |
|
||||||
# |
|
||||||
# $ (head -n10 AUTHORS && git shortlog -se | sed -E 's/^\s+[0-9]+\t//') | tee AUTHORS |
|
||||||
|
|
||||||
# Please keep the list sorted. |
|
||||||
|
|
||||||
Aaron L <aaron@bettercoder.net> |
|
||||||
Adrien Bustany <adrien@bustany.org> |
|
||||||
Alexey Kazakov <alkazako@redhat.com> |
|
||||||
Amit Krishnan <amit.krishnan@oracle.com> |
|
||||||
Anmol Sethi <me@anmol.io> |
|
||||||
Bjørn Erik Pedersen <bjorn.erik.pedersen@gmail.com> |
|
||||||
Brian Goff <cpuguy83@gmail.com> |
|
||||||
Bruno Bigras <bigras.bruno@gmail.com> |
|
||||||
Caleb Spare <cespare@gmail.com> |
|
||||||
Case Nelson <case@teammating.com> |
|
||||||
Chris Howey <howeyc@gmail.com> |
|
||||||
Christoffer Buchholz <christoffer.buchholz@gmail.com> |
|
||||||
Daniel Wagner-Hall <dawagner@gmail.com> |
|
||||||
Dave Cheney <dave@cheney.net> |
|
||||||
Eric Lin <linxiulei@gmail.com> |
|
||||||
Evan Phoenix <evan@fallingsnow.net> |
|
||||||
Francisco Souza <f@souza.cc> |
|
||||||
Gautam Dey <gautam.dey77@gmail.com> |
|
||||||
Hari haran <hariharan.uno@gmail.com> |
|
||||||
Ichinose Shogo <shogo82148@gmail.com> |
|
||||||
Johannes Ebke <johannes@ebke.org> |
|
||||||
John C Barstow <jbowtie@amathaine.com> |
|
||||||
Kelvin Fo <vmirage@gmail.com> |
|
||||||
Ken-ichirou MATSUZAWA <chamas@h4.dion.ne.jp> |
|
||||||
Matt Layher <mdlayher@gmail.com> |
|
||||||
Matthias Stone <matthias@bellstone.ca> |
|
||||||
Nathan Youngman <git@nathany.com> |
|
||||||
Nickolai Zeldovich <nickolai@csail.mit.edu> |
|
||||||
Oliver Bristow <evilumbrella+github@gmail.com> |
|
||||||
Patrick <patrick@dropbox.com> |
|
||||||
Paul Hammond <paul@paulhammond.org> |
|
||||||
Pawel Knap <pawelknap88@gmail.com> |
|
||||||
Pieter Droogendijk <pieter@binky.org.uk> |
|
||||||
Pratik Shinde <pratikshinde320@gmail.com> |
|
||||||
Pursuit92 <JoshChase@techpursuit.net> |
|
||||||
Riku Voipio <riku.voipio@linaro.org> |
|
||||||
Rob Figueiredo <robfig@gmail.com> |
|
||||||
Rodrigo Chiossi <rodrigochiossi@gmail.com> |
|
||||||
Slawek Ligus <root@ooz.ie> |
|
||||||
Soge Zhang <zhssoge@gmail.com> |
|
||||||
Tiffany Jernigan <tiffany.jernigan@intel.com> |
|
||||||
Tilak Sharma <tilaks@google.com> |
|
||||||
Tobias Klauser <tobias.klauser@gmail.com> |
|
||||||
Tom Payne <twpayne@gmail.com> |
|
||||||
Travis Cline <travis.cline@gmail.com> |
|
||||||
Tudor Golubenco <tudor.g@gmail.com> |
|
||||||
Vahe Khachikyan <vahe@live.ca> |
|
||||||
Yukang <moorekang@gmail.com> |
|
||||||
bronze1man <bronze1man@gmail.com> |
|
||||||
debrando <denis.brandolini@gmail.com> |
|
||||||
henrikedwards <henrik.edwards@gmail.com> |
|
||||||
铁哥 <guotie.9@gmail.com> |
|
@ -1,60 +1,26 @@ |
|||||||
# Contributing |
Thank you for your interest in contributing to fsnotify! We try to review and |
||||||
|
merge PRs in a reasonable timeframe, but please be aware that: |
||||||
|
|
||||||
## Issues |
- To avoid "wasted" work, please discus changes on the issue tracker first. You |
||||||
|
can just send PRs, but they may end up being rejected for one reason or the |
||||||
|
other. |
||||||
|
|
||||||
* Request features and report bugs using the [GitHub Issue Tracker](https://github.com/fsnotify/fsnotify/issues). |
- fsnotify is a cross-platform library, and changes must work reasonably well on |
||||||
* Please indicate the platform you are using fsnotify on. |
all supported platforms. |
||||||
* A code example to reproduce the problem is appreciated. |
|
||||||
|
|
||||||
## Pull Requests |
- Changes will need to be compatible; old code should still compile, and the |
||||||
|
runtime behaviour can't change in ways that are likely to lead to problems for |
||||||
|
users. |
||||||
|
|
||||||
### Contributor License Agreement |
Testing |
||||||
|
------- |
||||||
|
Just `go test ./...` runs all the tests; the CI runs this on all supported |
||||||
|
platforms. Testing different platforms locally can be done with something like |
||||||
|
[goon] or [Vagrant], but this isn't super-easy to set up at the moment. |
||||||
|
|
||||||
fsnotify is derived from code in the [golang.org/x/exp](https://godoc.org/golang.org/x/exp) package and it may be included [in the standard library](https://github.com/fsnotify/fsnotify/issues/1) in the future. Therefore fsnotify carries the same [LICENSE](https://github.com/fsnotify/fsnotify/blob/master/LICENSE) as Go. Contributors retain their copyright, so you need to fill out a short form before we can accept your contribution: [Google Individual Contributor License Agreement](https://developers.google.com/open-source/cla/individual). |
Use the `-short` flag to make the "stress test" run faster. |
||||||
|
|
||||||
Please indicate that you have signed the CLA in your pull request. |
|
||||||
|
|
||||||
### How fsnotify is Developed |
[goon]: https://github.com/arp242/goon |
||||||
|
[Vagrant]: https://www.vagrantup.com/ |
||||||
* Development is done on feature branches. |
[integration_test.go]: /integration_test.go |
||||||
* Tests are run on BSD, Linux, macOS and Windows. |
|
||||||
* Pull requests are reviewed and [applied to master][am] using [hub][]. |
|
||||||
* Maintainers may modify or squash commits rather than asking contributors to. |
|
||||||
* To issue a new release, the maintainers will: |
|
||||||
* Update the CHANGELOG |
|
||||||
* Tag a version, which will become available through gopkg.in. |
|
||||||
|
|
||||||
### How to Fork |
|
||||||
|
|
||||||
For smooth sailing, always use the original import path. Installing with `go get` makes this easy. |
|
||||||
|
|
||||||
1. Install from GitHub (`go get -u github.com/fsnotify/fsnotify`) |
|
||||||
2. Create your feature branch (`git checkout -b my-new-feature`) |
|
||||||
3. Ensure everything works and the tests pass (see below) |
|
||||||
4. Commit your changes (`git commit -am 'Add some feature'`) |
|
||||||
|
|
||||||
Contribute upstream: |
|
||||||
|
|
||||||
1. Fork fsnotify on GitHub |
|
||||||
2. Add your remote (`git remote add fork git@github.com:mycompany/repo.git`) |
|
||||||
3. Push to the branch (`git push fork my-new-feature`) |
|
||||||
4. Create a new Pull Request on GitHub |
|
||||||
|
|
||||||
This workflow is [thoroughly explained by Katrina Owen](https://splice.com/blog/contributing-open-source-git-repositories-go/). |
|
||||||
|
|
||||||
### Testing |
|
||||||
|
|
||||||
fsnotify uses build tags to compile different code on Linux, BSD, macOS, and Windows. |
|
||||||
|
|
||||||
Before doing a pull request, please do your best to test your changes on multiple platforms, and list which platforms you were able/unable to test on. |
|
||||||
|
|
||||||
### Maintainers |
|
||||||
|
|
||||||
Help maintaining fsnotify is welcome. To be a maintainer: |
|
||||||
|
|
||||||
* Submit a pull request and sign the CLA as above. |
|
||||||
* You must be able to run the test suite on Mac, Windows, Linux and BSD. |
|
||||||
|
|
||||||
All code changes should be internal pull requests. |
|
||||||
|
|
||||||
Releases are tagged using [Semantic Versioning](http://semver.org/). |
|
||||||
|
@ -1,28 +1,25 @@ |
|||||||
Copyright (c) 2012 The Go Authors. All rights reserved. |
Copyright © 2012 The Go Authors. All rights reserved. |
||||||
Copyright (c) 2012-2019 fsnotify Authors. All rights reserved. |
Copyright © fsnotify Authors. All rights reserved. |
||||||
|
|
||||||
Redistribution and use in source and binary forms, with or without |
Redistribution and use in source and binary forms, with or without modification, |
||||||
modification, are permitted provided that the following conditions are |
are permitted provided that the following conditions are met: |
||||||
met: |
|
||||||
|
|
||||||
* Redistributions of source code must retain the above copyright |
* Redistributions of source code must retain the above copyright notice, this |
||||||
notice, this list of conditions and the following disclaimer. |
list of conditions and the following disclaimer. |
||||||
* Redistributions in binary form must reproduce the above |
* Redistributions in binary form must reproduce the above copyright notice, this |
||||||
copyright notice, this list of conditions and the following disclaimer |
list of conditions and the following disclaimer in the documentation and/or |
||||||
in the documentation and/or other materials provided with the |
other materials provided with the distribution. |
||||||
distribution. |
* Neither the name of Google Inc. nor the names of its contributors may be used |
||||||
* Neither the name of Google Inc. nor the names of its |
to endorse or promote products derived from this software without specific |
||||||
contributors may be used to endorse or promote products derived from |
prior written permission. |
||||||
this software without specific prior written permission. |
|
||||||
|
|
||||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS |
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND |
||||||
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT |
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED |
||||||
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR |
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE |
||||||
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT |
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR |
||||||
OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, |
ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES |
||||||
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT |
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; |
||||||
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, |
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON |
||||||
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY |
ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT |
||||||
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT |
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS |
||||||
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE |
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. |
||||||
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. |
|
||||||
|
@ -0,0 +1,162 @@ |
|||||||
|
//go:build solaris
|
||||||
|
// +build solaris
|
||||||
|
|
||||||
|
package fsnotify |
||||||
|
|
||||||
|
import ( |
||||||
|
"errors" |
||||||
|
) |
||||||
|
|
||||||
|
// Watcher watches a set of paths, delivering events on a channel.
|
||||||
|
//
|
||||||
|
// A watcher should not be copied (e.g. pass it by pointer, rather than by
|
||||||
|
// value).
|
||||||
|
//
|
||||||
|
// # Linux notes
|
||||||
|
//
|
||||||
|
// When a file is removed a Remove event won't be emitted until all file
|
||||||
|
// descriptors are closed, and deletes will always emit a Chmod. For example:
|
||||||
|
//
|
||||||
|
// fp := os.Open("file")
|
||||||
|
// os.Remove("file") // Triggers Chmod
|
||||||
|
// fp.Close() // Triggers Remove
|
||||||
|
//
|
||||||
|
// This is the event that inotify sends, so not much can be changed about this.
|
||||||
|
//
|
||||||
|
// The fs.inotify.max_user_watches sysctl variable specifies the upper limit
|
||||||
|
// for the number of watches per user, and fs.inotify.max_user_instances
|
||||||
|
// specifies the maximum number of inotify instances per user. Every Watcher you
|
||||||
|
// create is an "instance", and every path you add is a "watch".
|
||||||
|
//
|
||||||
|
// These are also exposed in /proc as /proc/sys/fs/inotify/max_user_watches and
|
||||||
|
// /proc/sys/fs/inotify/max_user_instances
|
||||||
|
//
|
||||||
|
// To increase them you can use sysctl or write the value to the /proc file:
|
||||||
|
//
|
||||||
|
// # Default values on Linux 5.18
|
||||||
|
// sysctl fs.inotify.max_user_watches=124983
|
||||||
|
// sysctl fs.inotify.max_user_instances=128
|
||||||
|
//
|
||||||
|
// To make the changes persist on reboot edit /etc/sysctl.conf or
|
||||||
|
// /usr/lib/sysctl.d/50-default.conf (details differ per Linux distro; check
|
||||||
|
// your distro's documentation):
|
||||||
|
//
|
||||||
|
// fs.inotify.max_user_watches=124983
|
||||||
|
// fs.inotify.max_user_instances=128
|
||||||
|
//
|
||||||
|
// Reaching the limit will result in a "no space left on device" or "too many open
|
||||||
|
// files" error.
|
||||||
|
//
|
||||||
|
// # kqueue notes (macOS, BSD)
|
||||||
|
//
|
||||||
|
// kqueue requires opening a file descriptor for every file that's being watched;
|
||||||
|
// so if you're watching a directory with five files then that's six file
|
||||||
|
// descriptors. You will run in to your system's "max open files" limit faster on
|
||||||
|
// these platforms.
|
||||||
|
//
|
||||||
|
// The sysctl variables kern.maxfiles and kern.maxfilesperproc can be used to
|
||||||
|
// control the maximum number of open files, as well as /etc/login.conf on BSD
|
||||||
|
// systems.
|
||||||
|
//
|
||||||
|
// # macOS notes
|
||||||
|
//
|
||||||
|
// Spotlight indexing on macOS can result in multiple events (see [#15]). A
|
||||||
|
// temporary workaround is to add your folder(s) to the "Spotlight Privacy
|
||||||
|
// Settings" until we have a native FSEvents implementation (see [#11]).
|
||||||
|
//
|
||||||
|
// [#11]: https://github.com/fsnotify/fsnotify/issues/11
|
||||||
|
// [#15]: https://github.com/fsnotify/fsnotify/issues/15
|
||||||
|
type Watcher struct { |
||||||
|
// Events sends the filesystem change events.
|
||||||
|
//
|
||||||
|
// fsnotify can send the following events; a "path" here can refer to a
|
||||||
|
// file, directory, symbolic link, or special file like a FIFO.
|
||||||
|
//
|
||||||
|
// fsnotify.Create A new path was created; this may be followed by one
|
||||||
|
// or more Write events if data also gets written to a
|
||||||
|
// file.
|
||||||
|
//
|
||||||
|
// fsnotify.Remove A path was removed.
|
||||||
|
//
|
||||||
|
// fsnotify.Rename A path was renamed. A rename is always sent with the
|
||||||
|
// old path as Event.Name, and a Create event will be
|
||||||
|
// sent with the new name. Renames are only sent for
|
||||||
|
// paths that are currently watched; e.g. moving an
|
||||||
|
// unmonitored file into a monitored directory will
|
||||||
|
// show up as just a Create. Similarly, renaming a file
|
||||||
|
// to outside a monitored directory will show up as
|
||||||
|
// only a Rename.
|
||||||
|
//
|
||||||
|
// fsnotify.Write A file or named pipe was written to. A Truncate will
|
||||||
|
// also trigger a Write. A single "write action"
|
||||||
|
// initiated by the user may show up as one or multiple
|
||||||
|
// writes, depending on when the system syncs things to
|
||||||
|
// disk. For example when compiling a large Go program
|
||||||
|
// you may get hundreds of Write events, so you
|
||||||
|
// probably want to wait until you've stopped receiving
|
||||||
|
// them (see the dedup example in cmd/fsnotify).
|
||||||
|
//
|
||||||
|
// fsnotify.Chmod Attributes were changed. On Linux this is also sent
|
||||||
|
// when a file is removed (or more accurately, when a
|
||||||
|
// link to an inode is removed). On kqueue it's sent
|
||||||
|
// and on kqueue when a file is truncated. On Windows
|
||||||
|
// it's never sent.
|
||||||
|
Events chan Event |
||||||
|
|
||||||
|
// Errors sends any errors.
|
||||||
|
Errors chan error |
||||||
|
} |
||||||
|
|
||||||
|
// NewWatcher creates a new Watcher.
|
||||||
|
func NewWatcher() (*Watcher, error) { |
||||||
|
return nil, errors.New("FEN based watcher not yet supported for fsnotify\n") |
||||||
|
} |
||||||
|
|
||||||
|
// Close removes all watches and closes the events channel.
|
||||||
|
func (w *Watcher) Close() error { |
||||||
|
return nil |
||||||
|
} |
||||||
|
|
||||||
|
// Add starts monitoring the path for changes.
|
||||||
|
//
|
||||||
|
// A path can only be watched once; attempting to watch it more than once will
|
||||||
|
// return an error. Paths that do not yet exist on the filesystem cannot be
|
||||||
|
// added. A watch will be automatically removed if the path is deleted.
|
||||||
|
//
|
||||||
|
// A path will remain watched if it gets renamed to somewhere else on the same
|
||||||
|
// filesystem, but the monitor will get removed if the path gets deleted and
|
||||||
|
// re-created, or if it's moved to a different filesystem.
|
||||||
|
//
|
||||||
|
// Notifications on network filesystems (NFS, SMB, FUSE, etc.) or special
|
||||||
|
// filesystems (/proc, /sys, etc.) generally don't work.
|
||||||
|
//
|
||||||
|
// # Watching directories
|
||||||
|
//
|
||||||
|
// All files in a directory are monitored, including new files that are created
|
||||||
|
// after the watcher is started. Subdirectories are not watched (i.e. it's
|
||||||
|
// non-recursive).
|
||||||
|
//
|
||||||
|
// # Watching files
|
||||||
|
//
|
||||||
|
// Watching individual files (rather than directories) is generally not
|
||||||
|
// recommended as many tools update files atomically. Instead of "just" writing
|
||||||
|
// to the file a temporary file will be written to first, and if successful the
|
||||||
|
// temporary file is moved to to destination removing the original, or some
|
||||||
|
// variant thereof. The watcher on the original file is now lost, as it no
|
||||||
|
// longer exists.
|
||||||
|
//
|
||||||
|
// Instead, watch the parent directory and use Event.Name to filter out files
|
||||||
|
// you're not interested in. There is an example of this in [cmd/fsnotify/file.go].
|
||||||
|
func (w *Watcher) Add(name string) error { |
||||||
|
return nil |
||||||
|
} |
||||||
|
|
||||||
|
// Remove stops monitoring the path for changes.
|
||||||
|
//
|
||||||
|
// Directories are always removed non-recursively. For example, if you added
|
||||||
|
// /tmp/dir and /tmp/dir/subdir then you will need to remove both.
|
||||||
|
//
|
||||||
|
// Removing a path that has not yet been added returns [ErrNonExistentWatch].
|
||||||
|
func (w *Watcher) Remove(name string) error { |
||||||
|
return nil |
||||||
|
} |
@ -0,0 +1,459 @@ |
|||||||
|
//go:build linux
|
||||||
|
// +build linux
|
||||||
|
|
||||||
|
package fsnotify |
||||||
|
|
||||||
|
import ( |
||||||
|
"errors" |
||||||
|
"fmt" |
||||||
|
"io" |
||||||
|
"os" |
||||||
|
"path/filepath" |
||||||
|
"strings" |
||||||
|
"sync" |
||||||
|
"unsafe" |
||||||
|
|
||||||
|
"golang.org/x/sys/unix" |
||||||
|
) |
||||||
|
|
||||||
|
// Watcher watches a set of paths, delivering events on a channel.
|
||||||
|
//
|
||||||
|
// A watcher should not be copied (e.g. pass it by pointer, rather than by
|
||||||
|
// value).
|
||||||
|
//
|
||||||
|
// # Linux notes
|
||||||
|
//
|
||||||
|
// When a file is removed a Remove event won't be emitted until all file
|
||||||
|
// descriptors are closed, and deletes will always emit a Chmod. For example:
|
||||||
|
//
|
||||||
|
// fp := os.Open("file")
|
||||||
|
// os.Remove("file") // Triggers Chmod
|
||||||
|
// fp.Close() // Triggers Remove
|
||||||
|
//
|
||||||
|
// This is the event that inotify sends, so not much can be changed about this.
|
||||||
|
//
|
||||||
|
// The fs.inotify.max_user_watches sysctl variable specifies the upper limit
|
||||||
|
// for the number of watches per user, and fs.inotify.max_user_instances
|
||||||
|
// specifies the maximum number of inotify instances per user. Every Watcher you
|
||||||
|
// create is an "instance", and every path you add is a "watch".
|
||||||
|
//
|
||||||
|
// These are also exposed in /proc as /proc/sys/fs/inotify/max_user_watches and
|
||||||
|
// /proc/sys/fs/inotify/max_user_instances
|
||||||
|
//
|
||||||
|
// To increase them you can use sysctl or write the value to the /proc file:
|
||||||
|
//
|
||||||
|
// # Default values on Linux 5.18
|
||||||
|
// sysctl fs.inotify.max_user_watches=124983
|
||||||
|
// sysctl fs.inotify.max_user_instances=128
|
||||||
|
//
|
||||||
|
// To make the changes persist on reboot edit /etc/sysctl.conf or
|
||||||
|
// /usr/lib/sysctl.d/50-default.conf (details differ per Linux distro; check
|
||||||
|
// your distro's documentation):
|
||||||
|
//
|
||||||
|
// fs.inotify.max_user_watches=124983
|
||||||
|
// fs.inotify.max_user_instances=128
|
||||||
|
//
|
||||||
|
// Reaching the limit will result in a "no space left on device" or "too many open
|
||||||
|
// files" error.
|
||||||
|
//
|
||||||
|
// # kqueue notes (macOS, BSD)
|
||||||
|
//
|
||||||
|
// kqueue requires opening a file descriptor for every file that's being watched;
|
||||||
|
// so if you're watching a directory with five files then that's six file
|
||||||
|
// descriptors. You will run in to your system's "max open files" limit faster on
|
||||||
|
// these platforms.
|
||||||
|
//
|
||||||
|
// The sysctl variables kern.maxfiles and kern.maxfilesperproc can be used to
|
||||||
|
// control the maximum number of open files, as well as /etc/login.conf on BSD
|
||||||
|
// systems.
|
||||||
|
//
|
||||||
|
// # macOS notes
|
||||||
|
//
|
||||||
|
// Spotlight indexing on macOS can result in multiple events (see [#15]). A
|
||||||
|
// temporary workaround is to add your folder(s) to the "Spotlight Privacy
|
||||||
|
// Settings" until we have a native FSEvents implementation (see [#11]).
|
||||||
|
//
|
||||||
|
// [#11]: https://github.com/fsnotify/fsnotify/issues/11
|
||||||
|
// [#15]: https://github.com/fsnotify/fsnotify/issues/15
|
||||||
|
type Watcher struct { |
||||||
|
// Events sends the filesystem change events.
|
||||||
|
//
|
||||||
|
// fsnotify can send the following events; a "path" here can refer to a
|
||||||
|
// file, directory, symbolic link, or special file like a FIFO.
|
||||||
|
//
|
||||||
|
// fsnotify.Create A new path was created; this may be followed by one
|
||||||
|
// or more Write events if data also gets written to a
|
||||||
|
// file.
|
||||||
|
//
|
||||||
|
// fsnotify.Remove A path was removed.
|
||||||
|
//
|
||||||
|
// fsnotify.Rename A path was renamed. A rename is always sent with the
|
||||||
|
// old path as Event.Name, and a Create event will be
|
||||||
|
// sent with the new name. Renames are only sent for
|
||||||
|
// paths that are currently watched; e.g. moving an
|
||||||
|
// unmonitored file into a monitored directory will
|
||||||
|
// show up as just a Create. Similarly, renaming a file
|
||||||
|
// to outside a monitored directory will show up as
|
||||||
|
// only a Rename.
|
||||||
|
//
|
||||||
|
// fsnotify.Write A file or named pipe was written to. A Truncate will
|
||||||
|
// also trigger a Write. A single "write action"
|
||||||
|
// initiated by the user may show up as one or multiple
|
||||||
|
// writes, depending on when the system syncs things to
|
||||||
|
// disk. For example when compiling a large Go program
|
||||||
|
// you may get hundreds of Write events, so you
|
||||||
|
// probably want to wait until you've stopped receiving
|
||||||
|
// them (see the dedup example in cmd/fsnotify).
|
||||||
|
//
|
||||||
|
// fsnotify.Chmod Attributes were changed. On Linux this is also sent
|
||||||
|
// when a file is removed (or more accurately, when a
|
||||||
|
// link to an inode is removed). On kqueue it's sent
|
||||||
|
// and on kqueue when a file is truncated. On Windows
|
||||||
|
// it's never sent.
|
||||||
|
Events chan Event |
||||||
|
|
||||||
|
// Errors sends any errors.
|
||||||
|
Errors chan error |
||||||
|
|
||||||
|
// Store fd here as os.File.Read() will no longer return on close after
|
||||||
|
// calling Fd(). See: https://github.com/golang/go/issues/26439
|
||||||
|
fd int |
||||||
|
mu sync.Mutex // Map access
|
||||||
|
inotifyFile *os.File |
||||||
|
watches map[string]*watch // Map of inotify watches (key: path)
|
||||||
|
paths map[int]string // Map of watched paths (key: watch descriptor)
|
||||||
|
done chan struct{} // Channel for sending a "quit message" to the reader goroutine
|
||||||
|
doneResp chan struct{} // Channel to respond to Close
|
||||||
|
} |
||||||
|
|
||||||
|
// NewWatcher creates a new Watcher.
|
||||||
|
func NewWatcher() (*Watcher, error) { |
||||||
|
// Create inotify fd
|
||||||
|
// Need to set the FD to nonblocking mode in order for SetDeadline methods to work
|
||||||
|
// Otherwise, blocking i/o operations won't terminate on close
|
||||||
|
fd, errno := unix.InotifyInit1(unix.IN_CLOEXEC | unix.IN_NONBLOCK) |
||||||
|
if fd == -1 { |
||||||
|
return nil, errno |
||||||
|
} |
||||||
|
|
||||||
|
w := &Watcher{ |
||||||
|
fd: fd, |
||||||
|
inotifyFile: os.NewFile(uintptr(fd), ""), |
||||||
|
watches: make(map[string]*watch), |
||||||
|
paths: make(map[int]string), |
||||||
|
Events: make(chan Event), |
||||||
|
Errors: make(chan error), |
||||||
|
done: make(chan struct{}), |
||||||
|
doneResp: make(chan struct{}), |
||||||
|
} |
||||||
|
|
||||||
|
go w.readEvents() |
||||||
|
return w, nil |
||||||
|
} |
||||||
|
|
||||||
|
// Returns true if the event was sent, or false if watcher is closed.
|
||||||
|
func (w *Watcher) sendEvent(e Event) bool { |
||||||
|
select { |
||||||
|
case w.Events <- e: |
||||||
|
return true |
||||||
|
case <-w.done: |
||||||
|
} |
||||||
|
return false |
||||||
|
} |
||||||
|
|
||||||
|
// Returns true if the error was sent, or false if watcher is closed.
|
||||||
|
func (w *Watcher) sendError(err error) bool { |
||||||
|
select { |
||||||
|
case w.Errors <- err: |
||||||
|
return true |
||||||
|
case <-w.done: |
||||||
|
return false |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
func (w *Watcher) isClosed() bool { |
||||||
|
select { |
||||||
|
case <-w.done: |
||||||
|
return true |
||||||
|
default: |
||||||
|
return false |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
// Close removes all watches and closes the events channel.
|
||||||
|
func (w *Watcher) Close() error { |
||||||
|
w.mu.Lock() |
||||||
|
if w.isClosed() { |
||||||
|
w.mu.Unlock() |
||||||
|
return nil |
||||||
|
} |
||||||
|
|
||||||
|
// Send 'close' signal to goroutine, and set the Watcher to closed.
|
||||||
|
close(w.done) |
||||||
|
w.mu.Unlock() |
||||||
|
|
||||||
|
// Causes any blocking reads to return with an error, provided the file
|
||||||
|
// still supports deadline operations.
|
||||||
|
err := w.inotifyFile.Close() |
||||||
|
if err != nil { |
||||||
|
return err |
||||||
|
} |
||||||
|
|
||||||
|
// Wait for goroutine to close
|
||||||
|
<-w.doneResp |
||||||
|
|
||||||
|
return nil |
||||||
|
} |
||||||
|
|
||||||
|
// Add starts monitoring the path for changes.
|
||||||
|
//
|
||||||
|
// A path can only be watched once; attempting to watch it more than once will
|
||||||
|
// return an error. Paths that do not yet exist on the filesystem cannot be
|
||||||
|
// added. A watch will be automatically removed if the path is deleted.
|
||||||
|
//
|
||||||
|
// A path will remain watched if it gets renamed to somewhere else on the same
|
||||||
|
// filesystem, but the monitor will get removed if the path gets deleted and
|
||||||
|
// re-created, or if it's moved to a different filesystem.
|
||||||
|
//
|
||||||
|
// Notifications on network filesystems (NFS, SMB, FUSE, etc.) or special
|
||||||
|
// filesystems (/proc, /sys, etc.) generally don't work.
|
||||||
|
//
|
||||||
|
// # Watching directories
|
||||||
|
//
|
||||||
|
// All files in a directory are monitored, including new files that are created
|
||||||
|
// after the watcher is started. Subdirectories are not watched (i.e. it's
|
||||||
|
// non-recursive).
|
||||||
|
//
|
||||||
|
// # Watching files
|
||||||
|
//
|
||||||
|
// Watching individual files (rather than directories) is generally not
|
||||||
|
// recommended as many tools update files atomically. Instead of "just" writing
|
||||||
|
// to the file a temporary file will be written to first, and if successful the
|
||||||
|
// temporary file is moved to to destination removing the original, or some
|
||||||
|
// variant thereof. The watcher on the original file is now lost, as it no
|
||||||
|
// longer exists.
|
||||||
|
//
|
||||||
|
// Instead, watch the parent directory and use Event.Name to filter out files
|
||||||
|
// you're not interested in. There is an example of this in [cmd/fsnotify/file.go].
|
||||||
|
func (w *Watcher) Add(name string) error { |
||||||
|
name = filepath.Clean(name) |
||||||
|
if w.isClosed() { |
||||||
|
return errors.New("inotify instance already closed") |
||||||
|
} |
||||||
|
|
||||||
|
var flags uint32 = unix.IN_MOVED_TO | unix.IN_MOVED_FROM | |
||||||
|
unix.IN_CREATE | unix.IN_ATTRIB | unix.IN_MODIFY | |
||||||
|
unix.IN_MOVE_SELF | unix.IN_DELETE | unix.IN_DELETE_SELF |
||||||
|
|
||||||
|
w.mu.Lock() |
||||||
|
defer w.mu.Unlock() |
||||||
|
watchEntry := w.watches[name] |
||||||
|
if watchEntry != nil { |
||||||
|
flags |= watchEntry.flags | unix.IN_MASK_ADD |
||||||
|
} |
||||||
|
wd, errno := unix.InotifyAddWatch(w.fd, name, flags) |
||||||
|
if wd == -1 { |
||||||
|
return errno |
||||||
|
} |
||||||
|
|
||||||
|
if watchEntry == nil { |
||||||
|
w.watches[name] = &watch{wd: uint32(wd), flags: flags} |
||||||
|
w.paths[wd] = name |
||||||
|
} else { |
||||||
|
watchEntry.wd = uint32(wd) |
||||||
|
watchEntry.flags = flags |
||||||
|
} |
||||||
|
|
||||||
|
return nil |
||||||
|
} |
||||||
|
|
||||||
|
// Remove stops monitoring the path for changes.
|
||||||
|
//
|
||||||
|
// Directories are always removed non-recursively. For example, if you added
|
||||||
|
// /tmp/dir and /tmp/dir/subdir then you will need to remove both.
|
||||||
|
//
|
||||||
|
// Removing a path that has not yet been added returns [ErrNonExistentWatch].
|
||||||
|
func (w *Watcher) Remove(name string) error { |
||||||
|
name = filepath.Clean(name) |
||||||
|
|
||||||
|
// Fetch the watch.
|
||||||
|
w.mu.Lock() |
||||||
|
defer w.mu.Unlock() |
||||||
|
watch, ok := w.watches[name] |
||||||
|
|
||||||
|
// Remove it from inotify.
|
||||||
|
if !ok { |
||||||
|
return fmt.Errorf("%w: %s", ErrNonExistentWatch, name) |
||||||
|
} |
||||||
|
|
||||||
|
// We successfully removed the watch if InotifyRmWatch doesn't return an
|
||||||
|
// error, we need to clean up our internal state to ensure it matches
|
||||||
|
// inotify's kernel state.
|
||||||
|
delete(w.paths, int(watch.wd)) |
||||||
|
delete(w.watches, name) |
||||||
|
|
||||||
|
// inotify_rm_watch will return EINVAL if the file has been deleted;
|
||||||
|
// the inotify will already have been removed.
|
||||||
|
// watches and pathes are deleted in ignoreLinux() implicitly and asynchronously
|
||||||
|
// by calling inotify_rm_watch() below. e.g. readEvents() goroutine receives IN_IGNORE
|
||||||
|
// so that EINVAL means that the wd is being rm_watch()ed or its file removed
|
||||||
|
// by another thread and we have not received IN_IGNORE event.
|
||||||
|
success, errno := unix.InotifyRmWatch(w.fd, watch.wd) |
||||||
|
if success == -1 { |
||||||
|
// TODO: Perhaps it's not helpful to return an error here in every case;
|
||||||
|
// The only two possible errors are:
|
||||||
|
//
|
||||||
|
// - EBADF, which happens when w.fd is not a valid file descriptor
|
||||||
|
// of any kind.
|
||||||
|
// - EINVAL, which is when fd is not an inotify descriptor or wd
|
||||||
|
// is not a valid watch descriptor. Watch descriptors are
|
||||||
|
// invalidated when they are removed explicitly or implicitly;
|
||||||
|
// explicitly by inotify_rm_watch, implicitly when the file they
|
||||||
|
// are watching is deleted.
|
||||||
|
return errno |
||||||
|
} |
||||||
|
|
||||||
|
return nil |
||||||
|
} |
||||||
|
|
||||||
|
// WatchList returns all paths added with [Add] (and are not yet removed).
|
||||||
|
func (w *Watcher) WatchList() []string { |
||||||
|
w.mu.Lock() |
||||||
|
defer w.mu.Unlock() |
||||||
|
|
||||||
|
entries := make([]string, 0, len(w.watches)) |
||||||
|
for pathname := range w.watches { |
||||||
|
entries = append(entries, pathname) |
||||||
|
} |
||||||
|
|
||||||
|
return entries |
||||||
|
} |
||||||
|
|
||||||
|
type watch struct { |
||||||
|
wd uint32 // Watch descriptor (as returned by the inotify_add_watch() syscall)
|
||||||
|
flags uint32 // inotify flags of this watch (see inotify(7) for the list of valid flags)
|
||||||
|
} |
||||||
|
|
||||||
|
// readEvents reads from the inotify file descriptor, converts the
|
||||||
|
// received events into Event objects and sends them via the Events channel
|
||||||
|
func (w *Watcher) readEvents() { |
||||||
|
defer func() { |
||||||
|
close(w.doneResp) |
||||||
|
close(w.Errors) |
||||||
|
close(w.Events) |
||||||
|
}() |
||||||
|
|
||||||
|
var ( |
||||||
|
buf [unix.SizeofInotifyEvent * 4096]byte // Buffer for a maximum of 4096 raw events
|
||||||
|
errno error // Syscall errno
|
||||||
|
) |
||||||
|
for { |
||||||
|
// See if we have been closed.
|
||||||
|
if w.isClosed() { |
||||||
|
return |
||||||
|
} |
||||||
|
|
||||||
|
n, err := w.inotifyFile.Read(buf[:]) |
||||||
|
switch { |
||||||
|
case errors.Unwrap(err) == os.ErrClosed: |
||||||
|
return |
||||||
|
case err != nil: |
||||||
|
if !w.sendError(err) { |
||||||
|
return |
||||||
|
} |
||||||
|
continue |
||||||
|
} |
||||||
|
|
||||||
|
if n < unix.SizeofInotifyEvent { |
||||||
|
var err error |
||||||
|
if n == 0 { |
||||||
|
// If EOF is received. This should really never happen.
|
||||||
|
err = io.EOF |
||||||
|
} else if n < 0 { |
||||||
|
// If an error occurred while reading.
|
||||||
|
err = errno |
||||||
|
} else { |
||||||
|
// Read was too short.
|
||||||
|
err = errors.New("notify: short read in readEvents()") |
||||||
|
} |
||||||
|
if !w.sendError(err) { |
||||||
|
return |
||||||
|
} |
||||||
|
continue |
||||||
|
} |
||||||
|
|
||||||
|
var offset uint32 |
||||||
|
// We don't know how many events we just read into the buffer
|
||||||
|
// While the offset points to at least one whole event...
|
||||||
|
for offset <= uint32(n-unix.SizeofInotifyEvent) { |
||||||
|
var ( |
||||||
|
// Point "raw" to the event in the buffer
|
||||||
|
raw = (*unix.InotifyEvent)(unsafe.Pointer(&buf[offset])) |
||||||
|
mask = uint32(raw.Mask) |
||||||
|
nameLen = uint32(raw.Len) |
||||||
|
) |
||||||
|
|
||||||
|
if mask&unix.IN_Q_OVERFLOW != 0 { |
||||||
|
if !w.sendError(ErrEventOverflow) { |
||||||
|
return |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
// If the event happened to the watched directory or the watched file, the kernel
|
||||||
|
// doesn't append the filename to the event, but we would like to always fill the
|
||||||
|
// the "Name" field with a valid filename. We retrieve the path of the watch from
|
||||||
|
// the "paths" map.
|
||||||
|
w.mu.Lock() |
||||||
|
name, ok := w.paths[int(raw.Wd)] |
||||||
|
// IN_DELETE_SELF occurs when the file/directory being watched is removed.
|
||||||
|
// This is a sign to clean up the maps, otherwise we are no longer in sync
|
||||||
|
// with the inotify kernel state which has already deleted the watch
|
||||||
|
// automatically.
|
||||||
|
if ok && mask&unix.IN_DELETE_SELF == unix.IN_DELETE_SELF { |
||||||
|
delete(w.paths, int(raw.Wd)) |
||||||
|
delete(w.watches, name) |
||||||
|
} |
||||||
|
w.mu.Unlock() |
||||||
|
|
||||||
|
if nameLen > 0 { |
||||||
|
// Point "bytes" at the first byte of the filename
|
||||||
|
bytes := (*[unix.PathMax]byte)(unsafe.Pointer(&buf[offset+unix.SizeofInotifyEvent]))[:nameLen:nameLen] |
||||||
|
// The filename is padded with NULL bytes. TrimRight() gets rid of those.
|
||||||
|
name += "/" + strings.TrimRight(string(bytes[0:nameLen]), "\000") |
||||||
|
} |
||||||
|
|
||||||
|
event := w.newEvent(name, mask) |
||||||
|
|
||||||
|
// Send the events that are not ignored on the events channel
|
||||||
|
if mask&unix.IN_IGNORED == 0 { |
||||||
|
if !w.sendEvent(event) { |
||||||
|
return |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
// Move to the next event in the buffer
|
||||||
|
offset += unix.SizeofInotifyEvent + nameLen |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
// newEvent returns an platform-independent Event based on an inotify mask.
|
||||||
|
func (w *Watcher) newEvent(name string, mask uint32) Event { |
||||||
|
e := Event{Name: name} |
||||||
|
if mask&unix.IN_CREATE == unix.IN_CREATE || mask&unix.IN_MOVED_TO == unix.IN_MOVED_TO { |
||||||
|
e.Op |= Create |
||||||
|
} |
||||||
|
if mask&unix.IN_DELETE_SELF == unix.IN_DELETE_SELF || mask&unix.IN_DELETE == unix.IN_DELETE { |
||||||
|
e.Op |= Remove |
||||||
|
} |
||||||
|
if mask&unix.IN_MODIFY == unix.IN_MODIFY { |
||||||
|
e.Op |= Write |
||||||
|
} |
||||||
|
if mask&unix.IN_MOVE_SELF == unix.IN_MOVE_SELF || mask&unix.IN_MOVED_FROM == unix.IN_MOVED_FROM { |
||||||
|
e.Op |= Rename |
||||||
|
} |
||||||
|
if mask&unix.IN_ATTRIB == unix.IN_ATTRIB { |
||||||
|
e.Op |= Chmod |
||||||
|
} |
||||||
|
return e |
||||||
|
} |
@ -0,0 +1,707 @@ |
|||||||
|
//go:build freebsd || openbsd || netbsd || dragonfly || darwin
|
||||||
|
// +build freebsd openbsd netbsd dragonfly darwin
|
||||||
|
|
||||||
|
package fsnotify |
||||||
|
|
||||||
|
import ( |
||||||
|
"errors" |
||||||
|
"fmt" |
||||||
|
"io/ioutil" |
||||||
|
"os" |
||||||
|
"path/filepath" |
||||||
|
"sync" |
||||||
|
|
||||||
|
"golang.org/x/sys/unix" |
||||||
|
) |
||||||
|
|
||||||
|
// Watcher watches a set of paths, delivering events on a channel.
|
||||||
|
//
|
||||||
|
// A watcher should not be copied (e.g. pass it by pointer, rather than by
|
||||||
|
// value).
|
||||||
|
//
|
||||||
|
// # Linux notes
|
||||||
|
//
|
||||||
|
// When a file is removed a Remove event won't be emitted until all file
|
||||||
|
// descriptors are closed, and deletes will always emit a Chmod. For example:
|
||||||
|
//
|
||||||
|
// fp := os.Open("file")
|
||||||
|
// os.Remove("file") // Triggers Chmod
|
||||||
|
// fp.Close() // Triggers Remove
|
||||||
|
//
|
||||||
|
// This is the event that inotify sends, so not much can be changed about this.
|
||||||
|
//
|
||||||
|
// The fs.inotify.max_user_watches sysctl variable specifies the upper limit
|
||||||
|
// for the number of watches per user, and fs.inotify.max_user_instances
|
||||||
|
// specifies the maximum number of inotify instances per user. Every Watcher you
|
||||||
|
// create is an "instance", and every path you add is a "watch".
|
||||||
|
//
|
||||||
|
// These are also exposed in /proc as /proc/sys/fs/inotify/max_user_watches and
|
||||||
|
// /proc/sys/fs/inotify/max_user_instances
|
||||||
|
//
|
||||||
|
// To increase them you can use sysctl or write the value to the /proc file:
|
||||||
|
//
|
||||||
|
// # Default values on Linux 5.18
|
||||||
|
// sysctl fs.inotify.max_user_watches=124983
|
||||||
|
// sysctl fs.inotify.max_user_instances=128
|
||||||
|
//
|
||||||
|
// To make the changes persist on reboot edit /etc/sysctl.conf or
|
||||||
|
// /usr/lib/sysctl.d/50-default.conf (details differ per Linux distro; check
|
||||||
|
// your distro's documentation):
|
||||||
|
//
|
||||||
|
// fs.inotify.max_user_watches=124983
|
||||||
|
// fs.inotify.max_user_instances=128
|
||||||
|
//
|
||||||
|
// Reaching the limit will result in a "no space left on device" or "too many open
|
||||||
|
// files" error.
|
||||||
|
//
|
||||||
|
// # kqueue notes (macOS, BSD)
|
||||||
|
//
|
||||||
|
// kqueue requires opening a file descriptor for every file that's being watched;
|
||||||
|
// so if you're watching a directory with five files then that's six file
|
||||||
|
// descriptors. You will run in to your system's "max open files" limit faster on
|
||||||
|
// these platforms.
|
||||||
|
//
|
||||||
|
// The sysctl variables kern.maxfiles and kern.maxfilesperproc can be used to
|
||||||
|
// control the maximum number of open files, as well as /etc/login.conf on BSD
|
||||||
|
// systems.
|
||||||
|
//
|
||||||
|
// # macOS notes
|
||||||
|
//
|
||||||
|
// Spotlight indexing on macOS can result in multiple events (see [#15]). A
|
||||||
|
// temporary workaround is to add your folder(s) to the "Spotlight Privacy
|
||||||
|
// Settings" until we have a native FSEvents implementation (see [#11]).
|
||||||
|
//
|
||||||
|
// [#11]: https://github.com/fsnotify/fsnotify/issues/11
|
||||||
|
// [#15]: https://github.com/fsnotify/fsnotify/issues/15
|
||||||
|
type Watcher struct { |
||||||
|
// Events sends the filesystem change events.
|
||||||
|
//
|
||||||
|
// fsnotify can send the following events; a "path" here can refer to a
|
||||||
|
// file, directory, symbolic link, or special file like a FIFO.
|
||||||
|
//
|
||||||
|
// fsnotify.Create A new path was created; this may be followed by one
|
||||||
|
// or more Write events if data also gets written to a
|
||||||
|
// file.
|
||||||
|
//
|
||||||
|
// fsnotify.Remove A path was removed.
|
||||||
|
//
|
||||||
|
// fsnotify.Rename A path was renamed. A rename is always sent with the
|
||||||
|
// old path as Event.Name, and a Create event will be
|
||||||
|
// sent with the new name. Renames are only sent for
|
||||||
|
// paths that are currently watched; e.g. moving an
|
||||||
|
// unmonitored file into a monitored directory will
|
||||||
|
// show up as just a Create. Similarly, renaming a file
|
||||||
|
// to outside a monitored directory will show up as
|
||||||
|
// only a Rename.
|
||||||
|
//
|
||||||
|
// fsnotify.Write A file or named pipe was written to. A Truncate will
|
||||||
|
// also trigger a Write. A single "write action"
|
||||||
|
// initiated by the user may show up as one or multiple
|
||||||
|
// writes, depending on when the system syncs things to
|
||||||
|
// disk. For example when compiling a large Go program
|
||||||
|
// you may get hundreds of Write events, so you
|
||||||
|
// probably want to wait until you've stopped receiving
|
||||||
|
// them (see the dedup example in cmd/fsnotify).
|
||||||
|
//
|
||||||
|
// fsnotify.Chmod Attributes were changed. On Linux this is also sent
|
||||||
|
// when a file is removed (or more accurately, when a
|
||||||
|
// link to an inode is removed). On kqueue it's sent
|
||||||
|
// and on kqueue when a file is truncated. On Windows
|
||||||
|
// it's never sent.
|
||||||
|
Events chan Event |
||||||
|
|
||||||
|
// Errors sends any errors.
|
||||||
|
Errors chan error |
||||||
|
|
||||||
|
done chan struct{} |
||||||
|
kq int // File descriptor (as returned by the kqueue() syscall).
|
||||||
|
closepipe [2]int // Pipe used for closing.
|
||||||
|
mu sync.Mutex // Protects access to watcher data
|
||||||
|
watches map[string]int // Watched file descriptors (key: path).
|
||||||
|
watchesByDir map[string]map[int]struct{} // Watched file descriptors indexed by the parent directory (key: dirname(path)).
|
||||||
|
userWatches map[string]struct{} // Watches added with Watcher.Add()
|
||||||
|
dirFlags map[string]uint32 // Watched directories to fflags used in kqueue.
|
||||||
|
paths map[int]pathInfo // File descriptors to path names for processing kqueue events.
|
||||||
|
fileExists map[string]struct{} // Keep track of if we know this file exists (to stop duplicate create events).
|
||||||
|
isClosed bool // Set to true when Close() is first called
|
||||||
|
} |
||||||
|
|
||||||
|
type pathInfo struct { |
||||||
|
name string |
||||||
|
isDir bool |
||||||
|
} |
||||||
|
|
||||||
|
// NewWatcher creates a new Watcher.
|
||||||
|
func NewWatcher() (*Watcher, error) { |
||||||
|
kq, closepipe, err := newKqueue() |
||||||
|
if err != nil { |
||||||
|
return nil, err |
||||||
|
} |
||||||
|
|
||||||
|
w := &Watcher{ |
||||||
|
kq: kq, |
||||||
|
closepipe: closepipe, |
||||||
|
watches: make(map[string]int), |
||||||
|
watchesByDir: make(map[string]map[int]struct{}), |
||||||
|
dirFlags: make(map[string]uint32), |
||||||
|
paths: make(map[int]pathInfo), |
||||||
|
fileExists: make(map[string]struct{}), |
||||||
|
userWatches: make(map[string]struct{}), |
||||||
|
Events: make(chan Event), |
||||||
|
Errors: make(chan error), |
||||||
|
done: make(chan struct{}), |
||||||
|
} |
||||||
|
|
||||||
|
go w.readEvents() |
||||||
|
return w, nil |
||||||
|
} |
||||||
|
|
||||||
|
// newKqueue creates a new kernel event queue and returns a descriptor.
|
||||||
|
//
|
||||||
|
// This registers a new event on closepipe, which will trigger an event when
|
||||||
|
// it's closed. This way we can use kevent() without timeout/polling; without
|
||||||
|
// the closepipe, it would block forever and we wouldn't be able to stop it at
|
||||||
|
// all.
|
||||||
|
func newKqueue() (kq int, closepipe [2]int, err error) { |
||||||
|
kq, err = unix.Kqueue() |
||||||
|
if kq == -1 { |
||||||
|
return kq, closepipe, err |
||||||
|
} |
||||||
|
|
||||||
|
// Register the close pipe.
|
||||||
|
err = unix.Pipe(closepipe[:]) |
||||||
|
if err != nil { |
||||||
|
unix.Close(kq) |
||||||
|
return kq, closepipe, err |
||||||
|
} |
||||||
|
|
||||||
|
// Register changes to listen on the closepipe.
|
||||||
|
changes := make([]unix.Kevent_t, 1) |
||||||
|
// SetKevent converts int to the platform-specific types.
|
||||||
|
unix.SetKevent(&changes[0], closepipe[0], unix.EVFILT_READ, |
||||||
|
unix.EV_ADD|unix.EV_ENABLE|unix.EV_ONESHOT) |
||||||
|
|
||||||
|
ok, err := unix.Kevent(kq, changes, nil, nil) |
||||||
|
if ok == -1 { |
||||||
|
unix.Close(kq) |
||||||
|
unix.Close(closepipe[0]) |
||||||
|
unix.Close(closepipe[1]) |
||||||
|
return kq, closepipe, err |
||||||
|
} |
||||||
|
return kq, closepipe, nil |
||||||
|
} |
||||||
|
|
||||||
|
// Returns true if the event was sent, or false if watcher is closed.
|
||||||
|
func (w *Watcher) sendEvent(e Event) bool { |
||||||
|
select { |
||||||
|
case w.Events <- e: |
||||||
|
return true |
||||||
|
case <-w.done: |
||||||
|
} |
||||||
|
return false |
||||||
|
} |
||||||
|
|
||||||
|
// Returns true if the error was sent, or false if watcher is closed.
|
||||||
|
func (w *Watcher) sendError(err error) bool { |
||||||
|
select { |
||||||
|
case w.Errors <- err: |
||||||
|
return true |
||||||
|
case <-w.done: |
||||||
|
} |
||||||
|
return false |
||||||
|
} |
||||||
|
|
||||||
|
// Close removes all watches and closes the events channel.
|
||||||
|
func (w *Watcher) Close() error { |
||||||
|
w.mu.Lock() |
||||||
|
if w.isClosed { |
||||||
|
w.mu.Unlock() |
||||||
|
return nil |
||||||
|
} |
||||||
|
w.isClosed = true |
||||||
|
|
||||||
|
// copy paths to remove while locked
|
||||||
|
pathsToRemove := make([]string, 0, len(w.watches)) |
||||||
|
for name := range w.watches { |
||||||
|
pathsToRemove = append(pathsToRemove, name) |
||||||
|
} |
||||||
|
w.mu.Unlock() // Unlock before calling Remove, which also locks
|
||||||
|
for _, name := range pathsToRemove { |
||||||
|
w.Remove(name) |
||||||
|
} |
||||||
|
|
||||||
|
// Send "quit" message to the reader goroutine.
|
||||||
|
unix.Close(w.closepipe[1]) |
||||||
|
close(w.done) |
||||||
|
|
||||||
|
return nil |
||||||
|
} |
||||||
|
|
||||||
|
// Add starts monitoring the path for changes.
|
||||||
|
//
|
||||||
|
// A path can only be watched once; attempting to watch it more than once will
|
||||||
|
// return an error. Paths that do not yet exist on the filesystem cannot be
|
||||||
|
// added. A watch will be automatically removed if the path is deleted.
|
||||||
|
//
|
||||||
|
// A path will remain watched if it gets renamed to somewhere else on the same
|
||||||
|
// filesystem, but the monitor will get removed if the path gets deleted and
|
||||||
|
// re-created, or if it's moved to a different filesystem.
|
||||||
|
//
|
||||||
|
// Notifications on network filesystems (NFS, SMB, FUSE, etc.) or special
|
||||||
|
// filesystems (/proc, /sys, etc.) generally don't work.
|
||||||
|
//
|
||||||
|
// # Watching directories
|
||||||
|
//
|
||||||
|
// All files in a directory are monitored, including new files that are created
|
||||||
|
// after the watcher is started. Subdirectories are not watched (i.e. it's
|
||||||
|
// non-recursive).
|
||||||
|
//
|
||||||
|
// # Watching files
|
||||||
|
//
|
||||||
|
// Watching individual files (rather than directories) is generally not
|
||||||
|
// recommended as many tools update files atomically. Instead of "just" writing
|
||||||
|
// to the file a temporary file will be written to first, and if successful the
|
||||||
|
// temporary file is moved to to destination removing the original, or some
|
||||||
|
// variant thereof. The watcher on the original file is now lost, as it no
|
||||||
|
// longer exists.
|
||||||
|
//
|
||||||
|
// Instead, watch the parent directory and use Event.Name to filter out files
|
||||||
|
// you're not interested in. There is an example of this in [cmd/fsnotify/file.go].
|
||||||
|
func (w *Watcher) Add(name string) error { |
||||||
|
w.mu.Lock() |
||||||
|
w.userWatches[name] = struct{}{} |
||||||
|
w.mu.Unlock() |
||||||
|
_, err := w.addWatch(name, noteAllEvents) |
||||||
|
return err |
||||||
|
} |
||||||
|
|
||||||
|
// Remove stops monitoring the path for changes.
|
||||||
|
//
|
||||||
|
// Directories are always removed non-recursively. For example, if you added
|
||||||
|
// /tmp/dir and /tmp/dir/subdir then you will need to remove both.
|
||||||
|
//
|
||||||
|
// Removing a path that has not yet been added returns [ErrNonExistentWatch].
|
||||||
|
func (w *Watcher) Remove(name string) error { |
||||||
|
name = filepath.Clean(name) |
||||||
|
w.mu.Lock() |
||||||
|
watchfd, ok := w.watches[name] |
||||||
|
w.mu.Unlock() |
||||||
|
if !ok { |
||||||
|
return fmt.Errorf("%w: %s", ErrNonExistentWatch, name) |
||||||
|
} |
||||||
|
|
||||||
|
err := w.register([]int{watchfd}, unix.EV_DELETE, 0) |
||||||
|
if err != nil { |
||||||
|
return err |
||||||
|
} |
||||||
|
|
||||||
|
unix.Close(watchfd) |
||||||
|
|
||||||
|
w.mu.Lock() |
||||||
|
isDir := w.paths[watchfd].isDir |
||||||
|
delete(w.watches, name) |
||||||
|
delete(w.userWatches, name) |
||||||
|
|
||||||
|
parentName := filepath.Dir(name) |
||||||
|
delete(w.watchesByDir[parentName], watchfd) |
||||||
|
|
||||||
|
if len(w.watchesByDir[parentName]) == 0 { |
||||||
|
delete(w.watchesByDir, parentName) |
||||||
|
} |
||||||
|
|
||||||
|
delete(w.paths, watchfd) |
||||||
|
delete(w.dirFlags, name) |
||||||
|
delete(w.fileExists, name) |
||||||
|
w.mu.Unlock() |
||||||
|
|
||||||
|
// Find all watched paths that are in this directory that are not external.
|
||||||
|
if isDir { |
||||||
|
var pathsToRemove []string |
||||||
|
w.mu.Lock() |
||||||
|
for fd := range w.watchesByDir[name] { |
||||||
|
path := w.paths[fd] |
||||||
|
if _, ok := w.userWatches[path.name]; !ok { |
||||||
|
pathsToRemove = append(pathsToRemove, path.name) |
||||||
|
} |
||||||
|
} |
||||||
|
w.mu.Unlock() |
||||||
|
for _, name := range pathsToRemove { |
||||||
|
// Since these are internal, not much sense in propagating error
|
||||||
|
// to the user, as that will just confuse them with an error about
|
||||||
|
// a path they did not explicitly watch themselves.
|
||||||
|
w.Remove(name) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
return nil |
||||||
|
} |
||||||
|
|
||||||
|
// WatchList returns all paths added with [Add] (and are not yet removed).
|
||||||
|
func (w *Watcher) WatchList() []string { |
||||||
|
w.mu.Lock() |
||||||
|
defer w.mu.Unlock() |
||||||
|
|
||||||
|
entries := make([]string, 0, len(w.userWatches)) |
||||||
|
for pathname := range w.userWatches { |
||||||
|
entries = append(entries, pathname) |
||||||
|
} |
||||||
|
|
||||||
|
return entries |
||||||
|
} |
||||||
|
|
||||||
|
// Watch all events (except NOTE_EXTEND, NOTE_LINK, NOTE_REVOKE)
|
||||||
|
const noteAllEvents = unix.NOTE_DELETE | unix.NOTE_WRITE | unix.NOTE_ATTRIB | unix.NOTE_RENAME |
||||||
|
|
||||||
|
// addWatch adds name to the watched file set.
|
||||||
|
// The flags are interpreted as described in kevent(2).
|
||||||
|
// Returns the real path to the file which was added, if any, which may be different from the one passed in the case of symlinks.
|
||||||
|
func (w *Watcher) addWatch(name string, flags uint32) (string, error) { |
||||||
|
var isDir bool |
||||||
|
// Make ./name and name equivalent
|
||||||
|
name = filepath.Clean(name) |
||||||
|
|
||||||
|
w.mu.Lock() |
||||||
|
if w.isClosed { |
||||||
|
w.mu.Unlock() |
||||||
|
return "", errors.New("kevent instance already closed") |
||||||
|
} |
||||||
|
watchfd, alreadyWatching := w.watches[name] |
||||||
|
// We already have a watch, but we can still override flags.
|
||||||
|
if alreadyWatching { |
||||||
|
isDir = w.paths[watchfd].isDir |
||||||
|
} |
||||||
|
w.mu.Unlock() |
||||||
|
|
||||||
|
if !alreadyWatching { |
||||||
|
fi, err := os.Lstat(name) |
||||||
|
if err != nil { |
||||||
|
return "", err |
||||||
|
} |
||||||
|
|
||||||
|
// Don't watch sockets or named pipes
|
||||||
|
if (fi.Mode()&os.ModeSocket == os.ModeSocket) || (fi.Mode()&os.ModeNamedPipe == os.ModeNamedPipe) { |
||||||
|
return "", nil |
||||||
|
} |
||||||
|
|
||||||
|
// Follow Symlinks
|
||||||
|
//
|
||||||
|
// Linux can add unresolvable symlinks to the watch list without issue,
|
||||||
|
// and Windows can't do symlinks period. To maintain consistency, we
|
||||||
|
// will act like everything is fine if the link can't be resolved.
|
||||||
|
// There will simply be no file events for broken symlinks. Hence the
|
||||||
|
// returns of nil on errors.
|
||||||
|
if fi.Mode()&os.ModeSymlink == os.ModeSymlink { |
||||||
|
name, err = filepath.EvalSymlinks(name) |
||||||
|
if err != nil { |
||||||
|
return "", nil |
||||||
|
} |
||||||
|
|
||||||
|
w.mu.Lock() |
||||||
|
_, alreadyWatching = w.watches[name] |
||||||
|
w.mu.Unlock() |
||||||
|
|
||||||
|
if alreadyWatching { |
||||||
|
return name, nil |
||||||
|
} |
||||||
|
|
||||||
|
fi, err = os.Lstat(name) |
||||||
|
if err != nil { |
||||||
|
return "", nil |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
// Retry on EINTR; open() can return EINTR in practice on macOS.
|
||||||
|
// See #354, and go issues 11180 and 39237.
|
||||||
|
for { |
||||||
|
watchfd, err = unix.Open(name, openMode, 0) |
||||||
|
if err == nil { |
||||||
|
break |
||||||
|
} |
||||||
|
if errors.Is(err, unix.EINTR) { |
||||||
|
continue |
||||||
|
} |
||||||
|
|
||||||
|
return "", err |
||||||
|
} |
||||||
|
|
||||||
|
isDir = fi.IsDir() |
||||||
|
} |
||||||
|
|
||||||
|
err := w.register([]int{watchfd}, unix.EV_ADD|unix.EV_CLEAR|unix.EV_ENABLE, flags) |
||||||
|
if err != nil { |
||||||
|
unix.Close(watchfd) |
||||||
|
return "", err |
||||||
|
} |
||||||
|
|
||||||
|
if !alreadyWatching { |
||||||
|
w.mu.Lock() |
||||||
|
parentName := filepath.Dir(name) |
||||||
|
w.watches[name] = watchfd |
||||||
|
|
||||||
|
watchesByDir, ok := w.watchesByDir[parentName] |
||||||
|
if !ok { |
||||||
|
watchesByDir = make(map[int]struct{}, 1) |
||||||
|
w.watchesByDir[parentName] = watchesByDir |
||||||
|
} |
||||||
|
watchesByDir[watchfd] = struct{}{} |
||||||
|
|
||||||
|
w.paths[watchfd] = pathInfo{name: name, isDir: isDir} |
||||||
|
w.mu.Unlock() |
||||||
|
} |
||||||
|
|
||||||
|
if isDir { |
||||||
|
// Watch the directory if it has not been watched before,
|
||||||
|
// or if it was watched before, but perhaps only a NOTE_DELETE (watchDirectoryFiles)
|
||||||
|
w.mu.Lock() |
||||||
|
|
||||||
|
watchDir := (flags&unix.NOTE_WRITE) == unix.NOTE_WRITE && |
||||||
|
(!alreadyWatching || (w.dirFlags[name]&unix.NOTE_WRITE) != unix.NOTE_WRITE) |
||||||
|
// Store flags so this watch can be updated later
|
||||||
|
w.dirFlags[name] = flags |
||||||
|
w.mu.Unlock() |
||||||
|
|
||||||
|
if watchDir { |
||||||
|
if err := w.watchDirectoryFiles(name); err != nil { |
||||||
|
return "", err |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
return name, nil |
||||||
|
} |
||||||
|
|
||||||
|
// readEvents reads from kqueue and converts the received kevents into
|
||||||
|
// Event values that it sends down the Events channel.
|
||||||
|
func (w *Watcher) readEvents() { |
||||||
|
defer func() { |
||||||
|
err := unix.Close(w.kq) |
||||||
|
if err != nil { |
||||||
|
w.Errors <- err |
||||||
|
} |
||||||
|
unix.Close(w.closepipe[0]) |
||||||
|
close(w.Events) |
||||||
|
close(w.Errors) |
||||||
|
}() |
||||||
|
|
||||||
|
eventBuffer := make([]unix.Kevent_t, 10) |
||||||
|
for closed := false; !closed; { |
||||||
|
kevents, err := w.read(eventBuffer) |
||||||
|
// EINTR is okay, the syscall was interrupted before timeout expired.
|
||||||
|
if err != nil && err != unix.EINTR { |
||||||
|
if !w.sendError(fmt.Errorf("fsnotify.readEvents: %w", err)) { |
||||||
|
closed = true |
||||||
|
} |
||||||
|
continue |
||||||
|
} |
||||||
|
|
||||||
|
// Flush the events we received to the Events channel
|
||||||
|
for _, kevent := range kevents { |
||||||
|
var ( |
||||||
|
watchfd = int(kevent.Ident) |
||||||
|
mask = uint32(kevent.Fflags) |
||||||
|
) |
||||||
|
|
||||||
|
// Shut down the loop when the pipe is closed, but only after all
|
||||||
|
// other events have been processed.
|
||||||
|
if watchfd == w.closepipe[0] { |
||||||
|
closed = true |
||||||
|
continue |
||||||
|
} |
||||||
|
|
||||||
|
w.mu.Lock() |
||||||
|
path := w.paths[watchfd] |
||||||
|
w.mu.Unlock() |
||||||
|
|
||||||
|
event := w.newEvent(path.name, mask) |
||||||
|
|
||||||
|
if path.isDir && !event.Has(Remove) { |
||||||
|
// Double check to make sure the directory exists. This can
|
||||||
|
// happen when we do a rm -fr on a recursively watched folders
|
||||||
|
// and we receive a modification event first but the folder has
|
||||||
|
// been deleted and later receive the delete event.
|
||||||
|
if _, err := os.Lstat(event.Name); os.IsNotExist(err) { |
||||||
|
event.Op |= Remove |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
if event.Has(Rename) || event.Has(Remove) { |
||||||
|
w.Remove(event.Name) |
||||||
|
w.mu.Lock() |
||||||
|
delete(w.fileExists, event.Name) |
||||||
|
w.mu.Unlock() |
||||||
|
} |
||||||
|
|
||||||
|
if path.isDir && event.Has(Write) && !event.Has(Remove) { |
||||||
|
w.sendDirectoryChangeEvents(event.Name) |
||||||
|
} else { |
||||||
|
if !w.sendEvent(event) { |
||||||
|
closed = true |
||||||
|
continue |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
if event.Has(Remove) { |
||||||
|
// Look for a file that may have overwritten this.
|
||||||
|
// For example, mv f1 f2 will delete f2, then create f2.
|
||||||
|
if path.isDir { |
||||||
|
fileDir := filepath.Clean(event.Name) |
||||||
|
w.mu.Lock() |
||||||
|
_, found := w.watches[fileDir] |
||||||
|
w.mu.Unlock() |
||||||
|
if found { |
||||||
|
// make sure the directory exists before we watch for changes. When we
|
||||||
|
// do a recursive watch and perform rm -fr, the parent directory might
|
||||||
|
// have gone missing, ignore the missing directory and let the
|
||||||
|
// upcoming delete event remove the watch from the parent directory.
|
||||||
|
if _, err := os.Lstat(fileDir); err == nil { |
||||||
|
w.sendDirectoryChangeEvents(fileDir) |
||||||
|
} |
||||||
|
} |
||||||
|
} else { |
||||||
|
filePath := filepath.Clean(event.Name) |
||||||
|
if fileInfo, err := os.Lstat(filePath); err == nil { |
||||||
|
w.sendFileCreatedEventIfNew(filePath, fileInfo) |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
// newEvent returns an platform-independent Event based on kqueue Fflags.
|
||||||
|
func (w *Watcher) newEvent(name string, mask uint32) Event { |
||||||
|
e := Event{Name: name} |
||||||
|
if mask&unix.NOTE_DELETE == unix.NOTE_DELETE { |
||||||
|
e.Op |= Remove |
||||||
|
} |
||||||
|
if mask&unix.NOTE_WRITE == unix.NOTE_WRITE { |
||||||
|
e.Op |= Write |
||||||
|
} |
||||||
|
if mask&unix.NOTE_RENAME == unix.NOTE_RENAME { |
||||||
|
e.Op |= Rename |
||||||
|
} |
||||||
|
if mask&unix.NOTE_ATTRIB == unix.NOTE_ATTRIB { |
||||||
|
e.Op |= Chmod |
||||||
|
} |
||||||
|
return e |
||||||
|
} |
||||||
|
|
||||||
|
// watchDirectoryFiles to mimic inotify when adding a watch on a directory
|
||||||
|
func (w *Watcher) watchDirectoryFiles(dirPath string) error { |
||||||
|
// Get all files
|
||||||
|
files, err := ioutil.ReadDir(dirPath) |
||||||
|
if err != nil { |
||||||
|
return err |
||||||
|
} |
||||||
|
|
||||||
|
for _, fileInfo := range files { |
||||||
|
path := filepath.Join(dirPath, fileInfo.Name()) |
||||||
|
|
||||||
|
cleanPath, err := w.internalWatch(path, fileInfo) |
||||||
|
if err != nil { |
||||||
|
// No permission to read the file; that's not a problem: just skip.
|
||||||
|
// But do add it to w.fileExists to prevent it from being picked up
|
||||||
|
// as a "new" file later (it still shows up in the directory
|
||||||
|
// listing).
|
||||||
|
switch { |
||||||
|
case errors.Is(err, unix.EACCES) || errors.Is(err, unix.EPERM): |
||||||
|
cleanPath = filepath.Clean(path) |
||||||
|
default: |
||||||
|
return fmt.Errorf("%q: %w", filepath.Join(dirPath, fileInfo.Name()), err) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
w.mu.Lock() |
||||||
|
w.fileExists[cleanPath] = struct{}{} |
||||||
|
w.mu.Unlock() |
||||||
|
} |
||||||
|
|
||||||
|
return nil |
||||||
|
} |
||||||
|
|
||||||
|
// Search the directory for new files and send an event for them.
|
||||||
|
//
|
||||||
|
// This functionality is to have the BSD watcher match the inotify, which sends
|
||||||
|
// a create event for files created in a watched directory.
|
||||||
|
func (w *Watcher) sendDirectoryChangeEvents(dir string) { |
||||||
|
// Get all files
|
||||||
|
files, err := ioutil.ReadDir(dir) |
||||||
|
if err != nil { |
||||||
|
if !w.sendError(fmt.Errorf("fsnotify.sendDirectoryChangeEvents: %w", err)) { |
||||||
|
return |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
// Search for new files
|
||||||
|
for _, fi := range files { |
||||||
|
err := w.sendFileCreatedEventIfNew(filepath.Join(dir, fi.Name()), fi) |
||||||
|
if err != nil { |
||||||
|
return |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
// sendFileCreatedEvent sends a create event if the file isn't already being tracked.
|
||||||
|
func (w *Watcher) sendFileCreatedEventIfNew(filePath string, fileInfo os.FileInfo) (err error) { |
||||||
|
w.mu.Lock() |
||||||
|
_, doesExist := w.fileExists[filePath] |
||||||
|
w.mu.Unlock() |
||||||
|
if !doesExist { |
||||||
|
if !w.sendEvent(Event{Name: filePath, Op: Create}) { |
||||||
|
return |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
// like watchDirectoryFiles (but without doing another ReadDir)
|
||||||
|
filePath, err = w.internalWatch(filePath, fileInfo) |
||||||
|
if err != nil { |
||||||
|
return err |
||||||
|
} |
||||||
|
|
||||||
|
w.mu.Lock() |
||||||
|
w.fileExists[filePath] = struct{}{} |
||||||
|
w.mu.Unlock() |
||||||
|
|
||||||
|
return nil |
||||||
|
} |
||||||
|
|
||||||
|
func (w *Watcher) internalWatch(name string, fileInfo os.FileInfo) (string, error) { |
||||||
|
if fileInfo.IsDir() { |
||||||
|
// mimic Linux providing delete events for subdirectories
|
||||||
|
// but preserve the flags used if currently watching subdirectory
|
||||||
|
w.mu.Lock() |
||||||
|
flags := w.dirFlags[name] |
||||||
|
w.mu.Unlock() |
||||||
|
|
||||||
|
flags |= unix.NOTE_DELETE | unix.NOTE_RENAME |
||||||
|
return w.addWatch(name, flags) |
||||||
|
} |
||||||
|
|
||||||
|
// watch file to mimic Linux inotify
|
||||||
|
return w.addWatch(name, noteAllEvents) |
||||||
|
} |
||||||
|
|
||||||
|
// Register events with the queue.
|
||||||
|
func (w *Watcher) register(fds []int, flags int, fflags uint32) error { |
||||||
|
changes := make([]unix.Kevent_t, len(fds)) |
||||||
|
for i, fd := range fds { |
||||||
|
// SetKevent converts int to the platform-specific types.
|
||||||
|
unix.SetKevent(&changes[i], fd, unix.EVFILT_VNODE, flags) |
||||||
|
changes[i].Fflags = fflags |
||||||
|
} |
||||||
|
|
||||||
|
// Register the events.
|
||||||
|
success, err := unix.Kevent(w.kq, changes, nil, nil) |
||||||
|
if success == -1 { |
||||||
|
return err |
||||||
|
} |
||||||
|
return nil |
||||||
|
} |
||||||
|
|
||||||
|
// read retrieves pending events, or waits until an event occurs.
|
||||||
|
func (w *Watcher) read(events []unix.Kevent_t) ([]unix.Kevent_t, error) { |
||||||
|
n, err := unix.Kevent(w.kq, nil, events, nil) |
||||||
|
if err != nil { |
||||||
|
return nil, err |
||||||
|
} |
||||||
|
return events[0:n], nil |
||||||
|
} |
@ -0,0 +1,66 @@ |
|||||||
|
//go:build !darwin && !dragonfly && !freebsd && !openbsd && !linux && !netbsd && !solaris && !windows
|
||||||
|
// +build !darwin,!dragonfly,!freebsd,!openbsd,!linux,!netbsd,!solaris,!windows
|
||||||
|
|
||||||
|
package fsnotify |
||||||
|
|
||||||
|
import ( |
||||||
|
"fmt" |
||||||
|
"runtime" |
||||||
|
) |
||||||
|
|
||||||
|
// Watcher watches a set of files, delivering events to a channel.
|
||||||
|
type Watcher struct{} |
||||||
|
|
||||||
|
// NewWatcher creates a new Watcher.
|
||||||
|
func NewWatcher() (*Watcher, error) { |
||||||
|
return nil, fmt.Errorf("fsnotify not supported on %s", runtime.GOOS) |
||||||
|
} |
||||||
|
|
||||||
|
// Close removes all watches and closes the events channel.
|
||||||
|
func (w *Watcher) Close() error { |
||||||
|
return nil |
||||||
|
} |
||||||
|
|
||||||
|
// Add starts monitoring the path for changes.
|
||||||
|
//
|
||||||
|
// A path can only be watched once; attempting to watch it more than once will
|
||||||
|
// return an error. Paths that do not yet exist on the filesystem cannot be
|
||||||
|
// added. A watch will be automatically removed if the path is deleted.
|
||||||
|
//
|
||||||
|
// A path will remain watched if it gets renamed to somewhere else on the same
|
||||||
|
// filesystem, but the monitor will get removed if the path gets deleted and
|
||||||
|
// re-created, or if it's moved to a different filesystem.
|
||||||
|
//
|
||||||
|
// Notifications on network filesystems (NFS, SMB, FUSE, etc.) or special
|
||||||
|
// filesystems (/proc, /sys, etc.) generally don't work.
|
||||||
|
//
|
||||||
|
// # Watching directories
|
||||||
|
//
|
||||||
|
// All files in a directory are monitored, including new files that are created
|
||||||
|
// after the watcher is started. Subdirectories are not watched (i.e. it's
|
||||||
|
// non-recursive).
|
||||||
|
//
|
||||||
|
// # Watching files
|
||||||
|
//
|
||||||
|
// Watching individual files (rather than directories) is generally not
|
||||||
|
// recommended as many tools update files atomically. Instead of "just" writing
|
||||||
|
// to the file a temporary file will be written to first, and if successful the
|
||||||
|
// temporary file is moved to to destination removing the original, or some
|
||||||
|
// variant thereof. The watcher on the original file is now lost, as it no
|
||||||
|
// longer exists.
|
||||||
|
//
|
||||||
|
// Instead, watch the parent directory and use Event.Name to filter out files
|
||||||
|
// you're not interested in. There is an example of this in [cmd/fsnotify/file.go].
|
||||||
|
func (w *Watcher) Add(name string) error { |
||||||
|
return nil |
||||||
|
} |
||||||
|
|
||||||
|
// Remove stops monitoring the path for changes.
|
||||||
|
//
|
||||||
|
// Directories are always removed non-recursively. For example, if you added
|
||||||
|
// /tmp/dir and /tmp/dir/subdir then you will need to remove both.
|
||||||
|
//
|
||||||
|
// Removing a path that has not yet been added returns [ErrNonExistentWatch].
|
||||||
|
func (w *Watcher) Remove(name string) error { |
||||||
|
return nil |
||||||
|
} |
@ -0,0 +1,746 @@ |
|||||||
|
//go:build windows
|
||||||
|
// +build windows
|
||||||
|
|
||||||
|
package fsnotify |
||||||
|
|
||||||
|
import ( |
||||||
|
"errors" |
||||||
|
"fmt" |
||||||
|
"os" |
||||||
|
"path/filepath" |
||||||
|
"reflect" |
||||||
|
"runtime" |
||||||
|
"strings" |
||||||
|
"sync" |
||||||
|
"unsafe" |
||||||
|
|
||||||
|
"golang.org/x/sys/windows" |
||||||
|
) |
||||||
|
|
||||||
|
// Watcher watches a set of paths, delivering events on a channel.
|
||||||
|
//
|
||||||
|
// A watcher should not be copied (e.g. pass it by pointer, rather than by
|
||||||
|
// value).
|
||||||
|
//
|
||||||
|
// # Linux notes
|
||||||
|
//
|
||||||
|
// When a file is removed a Remove event won't be emitted until all file
|
||||||
|
// descriptors are closed, and deletes will always emit a Chmod. For example:
|
||||||
|
//
|
||||||
|
// fp := os.Open("file")
|
||||||
|
// os.Remove("file") // Triggers Chmod
|
||||||
|
// fp.Close() // Triggers Remove
|
||||||
|
//
|
||||||
|
// This is the event that inotify sends, so not much can be changed about this.
|
||||||
|
//
|
||||||
|
// The fs.inotify.max_user_watches sysctl variable specifies the upper limit
|
||||||
|
// for the number of watches per user, and fs.inotify.max_user_instances
|
||||||
|
// specifies the maximum number of inotify instances per user. Every Watcher you
|
||||||
|
// create is an "instance", and every path you add is a "watch".
|
||||||
|
//
|
||||||
|
// These are also exposed in /proc as /proc/sys/fs/inotify/max_user_watches and
|
||||||
|
// /proc/sys/fs/inotify/max_user_instances
|
||||||
|
//
|
||||||
|
// To increase them you can use sysctl or write the value to the /proc file:
|
||||||
|
//
|
||||||
|
// # Default values on Linux 5.18
|
||||||
|
// sysctl fs.inotify.max_user_watches=124983
|
||||||
|
// sysctl fs.inotify.max_user_instances=128
|
||||||
|
//
|
||||||
|
// To make the changes persist on reboot edit /etc/sysctl.conf or
|
||||||
|
// /usr/lib/sysctl.d/50-default.conf (details differ per Linux distro; check
|
||||||
|
// your distro's documentation):
|
||||||
|
//
|
||||||
|
// fs.inotify.max_user_watches=124983
|
||||||
|
// fs.inotify.max_user_instances=128
|
||||||
|
//
|
||||||
|
// Reaching the limit will result in a "no space left on device" or "too many open
|
||||||
|
// files" error.
|
||||||
|
//
|
||||||
|
// # kqueue notes (macOS, BSD)
|
||||||
|
//
|
||||||
|
// kqueue requires opening a file descriptor for every file that's being watched;
|
||||||
|
// so if you're watching a directory with five files then that's six file
|
||||||
|
// descriptors. You will run in to your system's "max open files" limit faster on
|
||||||
|
// these platforms.
|
||||||
|
//
|
||||||
|
// The sysctl variables kern.maxfiles and kern.maxfilesperproc can be used to
|
||||||
|
// control the maximum number of open files, as well as /etc/login.conf on BSD
|
||||||
|
// systems.
|
||||||
|
//
|
||||||
|
// # macOS notes
|
||||||
|
//
|
||||||
|
// Spotlight indexing on macOS can result in multiple events (see [#15]). A
|
||||||
|
// temporary workaround is to add your folder(s) to the "Spotlight Privacy
|
||||||
|
// Settings" until we have a native FSEvents implementation (see [#11]).
|
||||||
|
//
|
||||||
|
// [#11]: https://github.com/fsnotify/fsnotify/issues/11
|
||||||
|
// [#15]: https://github.com/fsnotify/fsnotify/issues/15
|
||||||
|
type Watcher struct { |
||||||
|
// Events sends the filesystem change events.
|
||||||
|
//
|
||||||
|
// fsnotify can send the following events; a "path" here can refer to a
|
||||||
|
// file, directory, symbolic link, or special file like a FIFO.
|
||||||
|
//
|
||||||
|
// fsnotify.Create A new path was created; this may be followed by one
|
||||||
|
// or more Write events if data also gets written to a
|
||||||
|
// file.
|
||||||
|
//
|
||||||
|
// fsnotify.Remove A path was removed.
|
||||||
|
//
|
||||||
|
// fsnotify.Rename A path was renamed. A rename is always sent with the
|
||||||
|
// old path as Event.Name, and a Create event will be
|
||||||
|
// sent with the new name. Renames are only sent for
|
||||||
|
// paths that are currently watched; e.g. moving an
|
||||||
|
// unmonitored file into a monitored directory will
|
||||||
|
// show up as just a Create. Similarly, renaming a file
|
||||||
|
// to outside a monitored directory will show up as
|
||||||
|
// only a Rename.
|
||||||
|
//
|
||||||
|
// fsnotify.Write A file or named pipe was written to. A Truncate will
|
||||||
|
// also trigger a Write. A single "write action"
|
||||||
|
// initiated by the user may show up as one or multiple
|
||||||
|
// writes, depending on when the system syncs things to
|
||||||
|
// disk. For example when compiling a large Go program
|
||||||
|
// you may get hundreds of Write events, so you
|
||||||
|
// probably want to wait until you've stopped receiving
|
||||||
|
// them (see the dedup example in cmd/fsnotify).
|
||||||
|
//
|
||||||
|
// fsnotify.Chmod Attributes were changed. On Linux this is also sent
|
||||||
|
// when a file is removed (or more accurately, when a
|
||||||
|
// link to an inode is removed). On kqueue it's sent
|
||||||
|
// and on kqueue when a file is truncated. On Windows
|
||||||
|
// it's never sent.
|
||||||
|
Events chan Event |
||||||
|
|
||||||
|
// Errors sends any errors.
|
||||||
|
Errors chan error |
||||||
|
|
||||||
|
port windows.Handle // Handle to completion port
|
||||||
|
input chan *input // Inputs to the reader are sent on this channel
|
||||||
|
quit chan chan<- error |
||||||
|
|
||||||
|
mu sync.Mutex // Protects access to watches, isClosed
|
||||||
|
watches watchMap // Map of watches (key: i-number)
|
||||||
|
isClosed bool // Set to true when Close() is first called
|
||||||
|
} |
||||||
|
|
||||||
|
// NewWatcher creates a new Watcher.
|
||||||
|
func NewWatcher() (*Watcher, error) { |
||||||
|
port, err := windows.CreateIoCompletionPort(windows.InvalidHandle, 0, 0, 0) |
||||||
|
if err != nil { |
||||||
|
return nil, os.NewSyscallError("CreateIoCompletionPort", err) |
||||||
|
} |
||||||
|
w := &Watcher{ |
||||||
|
port: port, |
||||||
|
watches: make(watchMap), |
||||||
|
input: make(chan *input, 1), |
||||||
|
Events: make(chan Event, 50), |
||||||
|
Errors: make(chan error), |
||||||
|
quit: make(chan chan<- error, 1), |
||||||
|
} |
||||||
|
go w.readEvents() |
||||||
|
return w, nil |
||||||
|
} |
||||||
|
|
||||||
|
func (w *Watcher) sendEvent(name string, mask uint64) bool { |
||||||
|
if mask == 0 { |
||||||
|
return false |
||||||
|
} |
||||||
|
|
||||||
|
event := w.newEvent(name, uint32(mask)) |
||||||
|
select { |
||||||
|
case ch := <-w.quit: |
||||||
|
w.quit <- ch |
||||||
|
case w.Events <- event: |
||||||
|
} |
||||||
|
return true |
||||||
|
} |
||||||
|
|
||||||
|
// Returns true if the error was sent, or false if watcher is closed.
|
||||||
|
func (w *Watcher) sendError(err error) bool { |
||||||
|
select { |
||||||
|
case w.Errors <- err: |
||||||
|
return true |
||||||
|
case <-w.quit: |
||||||
|
} |
||||||
|
return false |
||||||
|
} |
||||||
|
|
||||||
|
// Close removes all watches and closes the events channel.
|
||||||
|
func (w *Watcher) Close() error { |
||||||
|
w.mu.Lock() |
||||||
|
if w.isClosed { |
||||||
|
w.mu.Unlock() |
||||||
|
return nil |
||||||
|
} |
||||||
|
w.isClosed = true |
||||||
|
w.mu.Unlock() |
||||||
|
|
||||||
|
// Send "quit" message to the reader goroutine
|
||||||
|
ch := make(chan error) |
||||||
|
w.quit <- ch |
||||||
|
if err := w.wakeupReader(); err != nil { |
||||||
|
return err |
||||||
|
} |
||||||
|
return <-ch |
||||||
|
} |
||||||
|
|
||||||
|
// Add starts monitoring the path for changes.
|
||||||
|
//
|
||||||
|
// A path can only be watched once; attempting to watch it more than once will
|
||||||
|
// return an error. Paths that do not yet exist on the filesystem cannot be
|
||||||
|
// added. A watch will be automatically removed if the path is deleted.
|
||||||
|
//
|
||||||
|
// A path will remain watched if it gets renamed to somewhere else on the same
|
||||||
|
// filesystem, but the monitor will get removed if the path gets deleted and
|
||||||
|
// re-created, or if it's moved to a different filesystem.
|
||||||
|
//
|
||||||
|
// Notifications on network filesystems (NFS, SMB, FUSE, etc.) or special
|
||||||
|
// filesystems (/proc, /sys, etc.) generally don't work.
|
||||||
|
//
|
||||||
|
// # Watching directories
|
||||||
|
//
|
||||||
|
// All files in a directory are monitored, including new files that are created
|
||||||
|
// after the watcher is started. Subdirectories are not watched (i.e. it's
|
||||||
|
// non-recursive).
|
||||||
|
//
|
||||||
|
// # Watching files
|
||||||
|
//
|
||||||
|
// Watching individual files (rather than directories) is generally not
|
||||||
|
// recommended as many tools update files atomically. Instead of "just" writing
|
||||||
|
// to the file a temporary file will be written to first, and if successful the
|
||||||
|
// temporary file is moved to to destination removing the original, or some
|
||||||
|
// variant thereof. The watcher on the original file is now lost, as it no
|
||||||
|
// longer exists.
|
||||||
|
//
|
||||||
|
// Instead, watch the parent directory and use Event.Name to filter out files
|
||||||
|
// you're not interested in. There is an example of this in [cmd/fsnotify/file.go].
|
||||||
|
func (w *Watcher) Add(name string) error { |
||||||
|
w.mu.Lock() |
||||||
|
if w.isClosed { |
||||||
|
w.mu.Unlock() |
||||||
|
return errors.New("watcher already closed") |
||||||
|
} |
||||||
|
w.mu.Unlock() |
||||||
|
|
||||||
|
in := &input{ |
||||||
|
op: opAddWatch, |
||||||
|
path: filepath.Clean(name), |
||||||
|
flags: sysFSALLEVENTS, |
||||||
|
reply: make(chan error), |
||||||
|
} |
||||||
|
w.input <- in |
||||||
|
if err := w.wakeupReader(); err != nil { |
||||||
|
return err |
||||||
|
} |
||||||
|
return <-in.reply |
||||||
|
} |
||||||
|
|
||||||
|
// Remove stops monitoring the path for changes.
|
||||||
|
//
|
||||||
|
// Directories are always removed non-recursively. For example, if you added
|
||||||
|
// /tmp/dir and /tmp/dir/subdir then you will need to remove both.
|
||||||
|
//
|
||||||
|
// Removing a path that has not yet been added returns [ErrNonExistentWatch].
|
||||||
|
func (w *Watcher) Remove(name string) error { |
||||||
|
in := &input{ |
||||||
|
op: opRemoveWatch, |
||||||
|
path: filepath.Clean(name), |
||||||
|
reply: make(chan error), |
||||||
|
} |
||||||
|
w.input <- in |
||||||
|
if err := w.wakeupReader(); err != nil { |
||||||
|
return err |
||||||
|
} |
||||||
|
return <-in.reply |
||||||
|
} |
||||||
|
|
||||||
|
// WatchList returns all paths added with [Add] (and are not yet removed).
|
||||||
|
func (w *Watcher) WatchList() []string { |
||||||
|
w.mu.Lock() |
||||||
|
defer w.mu.Unlock() |
||||||
|
|
||||||
|
entries := make([]string, 0, len(w.watches)) |
||||||
|
for _, entry := range w.watches { |
||||||
|
for _, watchEntry := range entry { |
||||||
|
entries = append(entries, watchEntry.path) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
return entries |
||||||
|
} |
||||||
|
|
||||||
|
// These options are from the old golang.org/x/exp/winfsnotify, where you could
|
||||||
|
// add various options to the watch. This has long since been removed.
|
||||||
|
//
|
||||||
|
// The "sys" in the name is misleading as they're not part of any "system".
|
||||||
|
//
|
||||||
|
// This should all be removed at some point, and just use windows.FILE_NOTIFY_*
|
||||||
|
const ( |
||||||
|
sysFSALLEVENTS = 0xfff |
||||||
|
sysFSATTRIB = 0x4 |
||||||
|
sysFSCREATE = 0x100 |
||||||
|
sysFSDELETE = 0x200 |
||||||
|
sysFSDELETESELF = 0x400 |
||||||
|
sysFSMODIFY = 0x2 |
||||||
|
sysFSMOVE = 0xc0 |
||||||
|
sysFSMOVEDFROM = 0x40 |
||||||
|
sysFSMOVEDTO = 0x80 |
||||||
|
sysFSMOVESELF = 0x800 |
||||||
|
sysFSIGNORED = 0x8000 |
||||||
|
) |
||||||
|
|
||||||
|
func (w *Watcher) newEvent(name string, mask uint32) Event { |
||||||
|
e := Event{Name: name} |
||||||
|
if mask&sysFSCREATE == sysFSCREATE || mask&sysFSMOVEDTO == sysFSMOVEDTO { |
||||||
|
e.Op |= Create |
||||||
|
} |
||||||
|
if mask&sysFSDELETE == sysFSDELETE || mask&sysFSDELETESELF == sysFSDELETESELF { |
||||||
|
e.Op |= Remove |
||||||
|
} |
||||||
|
if mask&sysFSMODIFY == sysFSMODIFY { |
||||||
|
e.Op |= Write |
||||||
|
} |
||||||
|
if mask&sysFSMOVE == sysFSMOVE || mask&sysFSMOVESELF == sysFSMOVESELF || mask&sysFSMOVEDFROM == sysFSMOVEDFROM { |
||||||
|
e.Op |= Rename |
||||||
|
} |
||||||
|
if mask&sysFSATTRIB == sysFSATTRIB { |
||||||
|
e.Op |= Chmod |
||||||
|
} |
||||||
|
return e |
||||||
|
} |
||||||
|
|
||||||
|
const ( |
||||||
|
opAddWatch = iota |
||||||
|
opRemoveWatch |
||||||
|
) |
||||||
|
|
||||||
|
const ( |
||||||
|
provisional uint64 = 1 << (32 + iota) |
||||||
|
) |
||||||
|
|
||||||
|
type input struct { |
||||||
|
op int |
||||||
|
path string |
||||||
|
flags uint32 |
||||||
|
reply chan error |
||||||
|
} |
||||||
|
|
||||||
|
type inode struct { |
||||||
|
handle windows.Handle |
||||||
|
volume uint32 |
||||||
|
index uint64 |
||||||
|
} |
||||||
|
|
||||||
|
type watch struct { |
||||||
|
ov windows.Overlapped |
||||||
|
ino *inode // i-number
|
||||||
|
path string // Directory path
|
||||||
|
mask uint64 // Directory itself is being watched with these notify flags
|
||||||
|
names map[string]uint64 // Map of names being watched and their notify flags
|
||||||
|
rename string // Remembers the old name while renaming a file
|
||||||
|
buf [65536]byte // 64K buffer
|
||||||
|
} |
||||||
|
|
||||||
|
type ( |
||||||
|
indexMap map[uint64]*watch |
||||||
|
watchMap map[uint32]indexMap |
||||||
|
) |
||||||
|
|
||||||
|
func (w *Watcher) wakeupReader() error { |
||||||
|
err := windows.PostQueuedCompletionStatus(w.port, 0, 0, nil) |
||||||
|
if err != nil { |
||||||
|
return os.NewSyscallError("PostQueuedCompletionStatus", err) |
||||||
|
} |
||||||
|
return nil |
||||||
|
} |
||||||
|
|
||||||
|
func (w *Watcher) getDir(pathname string) (dir string, err error) { |
||||||
|
attr, err := windows.GetFileAttributes(windows.StringToUTF16Ptr(pathname)) |
||||||
|
if err != nil { |
||||||
|
return "", os.NewSyscallError("GetFileAttributes", err) |
||||||
|
} |
||||||
|
if attr&windows.FILE_ATTRIBUTE_DIRECTORY != 0 { |
||||||
|
dir = pathname |
||||||
|
} else { |
||||||
|
dir, _ = filepath.Split(pathname) |
||||||
|
dir = filepath.Clean(dir) |
||||||
|
} |
||||||
|
return |
||||||
|
} |
||||||
|
|
||||||
|
func (w *Watcher) getIno(path string) (ino *inode, err error) { |
||||||
|
h, err := windows.CreateFile(windows.StringToUTF16Ptr(path), |
||||||
|
windows.FILE_LIST_DIRECTORY, |
||||||
|
windows.FILE_SHARE_READ|windows.FILE_SHARE_WRITE|windows.FILE_SHARE_DELETE, |
||||||
|
nil, windows.OPEN_EXISTING, |
||||||
|
windows.FILE_FLAG_BACKUP_SEMANTICS|windows.FILE_FLAG_OVERLAPPED, 0) |
||||||
|
if err != nil { |
||||||
|
return nil, os.NewSyscallError("CreateFile", err) |
||||||
|
} |
||||||
|
|
||||||
|
var fi windows.ByHandleFileInformation |
||||||
|
err = windows.GetFileInformationByHandle(h, &fi) |
||||||
|
if err != nil { |
||||||
|
windows.CloseHandle(h) |
||||||
|
return nil, os.NewSyscallError("GetFileInformationByHandle", err) |
||||||
|
} |
||||||
|
ino = &inode{ |
||||||
|
handle: h, |
||||||
|
volume: fi.VolumeSerialNumber, |
||||||
|
index: uint64(fi.FileIndexHigh)<<32 | uint64(fi.FileIndexLow), |
||||||
|
} |
||||||
|
return ino, nil |
||||||
|
} |
||||||
|
|
||||||
|
// Must run within the I/O thread.
|
||||||
|
func (m watchMap) get(ino *inode) *watch { |
||||||
|
if i := m[ino.volume]; i != nil { |
||||||
|
return i[ino.index] |
||||||
|
} |
||||||
|
return nil |
||||||
|
} |
||||||
|
|
||||||
|
// Must run within the I/O thread.
|
||||||
|
func (m watchMap) set(ino *inode, watch *watch) { |
||||||
|
i := m[ino.volume] |
||||||
|
if i == nil { |
||||||
|
i = make(indexMap) |
||||||
|
m[ino.volume] = i |
||||||
|
} |
||||||
|
i[ino.index] = watch |
||||||
|
} |
||||||
|
|
||||||
|
// Must run within the I/O thread.
|
||||||
|
func (w *Watcher) addWatch(pathname string, flags uint64) error { |
||||||
|
dir, err := w.getDir(pathname) |
||||||
|
if err != nil { |
||||||
|
return err |
||||||
|
} |
||||||
|
|
||||||
|
ino, err := w.getIno(dir) |
||||||
|
if err != nil { |
||||||
|
return err |
||||||
|
} |
||||||
|
w.mu.Lock() |
||||||
|
watchEntry := w.watches.get(ino) |
||||||
|
w.mu.Unlock() |
||||||
|
if watchEntry == nil { |
||||||
|
_, err := windows.CreateIoCompletionPort(ino.handle, w.port, 0, 0) |
||||||
|
if err != nil { |
||||||
|
windows.CloseHandle(ino.handle) |
||||||
|
return os.NewSyscallError("CreateIoCompletionPort", err) |
||||||
|
} |
||||||
|
watchEntry = &watch{ |
||||||
|
ino: ino, |
||||||
|
path: dir, |
||||||
|
names: make(map[string]uint64), |
||||||
|
} |
||||||
|
w.mu.Lock() |
||||||
|
w.watches.set(ino, watchEntry) |
||||||
|
w.mu.Unlock() |
||||||
|
flags |= provisional |
||||||
|
} else { |
||||||
|
windows.CloseHandle(ino.handle) |
||||||
|
} |
||||||
|
if pathname == dir { |
||||||
|
watchEntry.mask |= flags |
||||||
|
} else { |
||||||
|
watchEntry.names[filepath.Base(pathname)] |= flags |
||||||
|
} |
||||||
|
|
||||||
|
err = w.startRead(watchEntry) |
||||||
|
if err != nil { |
||||||
|
return err |
||||||
|
} |
||||||
|
|
||||||
|
if pathname == dir { |
||||||
|
watchEntry.mask &= ^provisional |
||||||
|
} else { |
||||||
|
watchEntry.names[filepath.Base(pathname)] &= ^provisional |
||||||
|
} |
||||||
|
return nil |
||||||
|
} |
||||||
|
|
||||||
|
// Must run within the I/O thread.
|
||||||
|
func (w *Watcher) remWatch(pathname string) error { |
||||||
|
dir, err := w.getDir(pathname) |
||||||
|
if err != nil { |
||||||
|
return err |
||||||
|
} |
||||||
|
ino, err := w.getIno(dir) |
||||||
|
if err != nil { |
||||||
|
return err |
||||||
|
} |
||||||
|
|
||||||
|
w.mu.Lock() |
||||||
|
watch := w.watches.get(ino) |
||||||
|
w.mu.Unlock() |
||||||
|
|
||||||
|
err = windows.CloseHandle(ino.handle) |
||||||
|
if err != nil { |
||||||
|
w.sendError(os.NewSyscallError("CloseHandle", err)) |
||||||
|
} |
||||||
|
if watch == nil { |
||||||
|
return fmt.Errorf("%w: %s", ErrNonExistentWatch, pathname) |
||||||
|
} |
||||||
|
if pathname == dir { |
||||||
|
w.sendEvent(watch.path, watch.mask&sysFSIGNORED) |
||||||
|
watch.mask = 0 |
||||||
|
} else { |
||||||
|
name := filepath.Base(pathname) |
||||||
|
w.sendEvent(filepath.Join(watch.path, name), watch.names[name]&sysFSIGNORED) |
||||||
|
delete(watch.names, name) |
||||||
|
} |
||||||
|
|
||||||
|
return w.startRead(watch) |
||||||
|
} |
||||||
|
|
||||||
|
// Must run within the I/O thread.
|
||||||
|
func (w *Watcher) deleteWatch(watch *watch) { |
||||||
|
for name, mask := range watch.names { |
||||||
|
if mask&provisional == 0 { |
||||||
|
w.sendEvent(filepath.Join(watch.path, name), mask&sysFSIGNORED) |
||||||
|
} |
||||||
|
delete(watch.names, name) |
||||||
|
} |
||||||
|
if watch.mask != 0 { |
||||||
|
if watch.mask&provisional == 0 { |
||||||
|
w.sendEvent(watch.path, watch.mask&sysFSIGNORED) |
||||||
|
} |
||||||
|
watch.mask = 0 |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
// Must run within the I/O thread.
|
||||||
|
func (w *Watcher) startRead(watch *watch) error { |
||||||
|
err := windows.CancelIo(watch.ino.handle) |
||||||
|
if err != nil { |
||||||
|
w.sendError(os.NewSyscallError("CancelIo", err)) |
||||||
|
w.deleteWatch(watch) |
||||||
|
} |
||||||
|
mask := w.toWindowsFlags(watch.mask) |
||||||
|
for _, m := range watch.names { |
||||||
|
mask |= w.toWindowsFlags(m) |
||||||
|
} |
||||||
|
if mask == 0 { |
||||||
|
err := windows.CloseHandle(watch.ino.handle) |
||||||
|
if err != nil { |
||||||
|
w.sendError(os.NewSyscallError("CloseHandle", err)) |
||||||
|
} |
||||||
|
w.mu.Lock() |
||||||
|
delete(w.watches[watch.ino.volume], watch.ino.index) |
||||||
|
w.mu.Unlock() |
||||||
|
return nil |
||||||
|
} |
||||||
|
|
||||||
|
rdErr := windows.ReadDirectoryChanges(watch.ino.handle, &watch.buf[0], |
||||||
|
uint32(unsafe.Sizeof(watch.buf)), false, mask, nil, &watch.ov, 0) |
||||||
|
if rdErr != nil { |
||||||
|
err := os.NewSyscallError("ReadDirectoryChanges", rdErr) |
||||||
|
if rdErr == windows.ERROR_ACCESS_DENIED && watch.mask&provisional == 0 { |
||||||
|
// Watched directory was probably removed
|
||||||
|
w.sendEvent(watch.path, watch.mask&sysFSDELETESELF) |
||||||
|
err = nil |
||||||
|
} |
||||||
|
w.deleteWatch(watch) |
||||||
|
w.startRead(watch) |
||||||
|
return err |
||||||
|
} |
||||||
|
return nil |
||||||
|
} |
||||||
|
|
||||||
|
// readEvents reads from the I/O completion port, converts the
|
||||||
|
// received events into Event objects and sends them via the Events channel.
|
||||||
|
// Entry point to the I/O thread.
|
||||||
|
func (w *Watcher) readEvents() { |
||||||
|
var ( |
||||||
|
n uint32 |
||||||
|
key uintptr |
||||||
|
ov *windows.Overlapped |
||||||
|
) |
||||||
|
runtime.LockOSThread() |
||||||
|
|
||||||
|
for { |
||||||
|
qErr := windows.GetQueuedCompletionStatus(w.port, &n, &key, &ov, windows.INFINITE) |
||||||
|
// This error is handled after the watch == nil check below. NOTE: this
|
||||||
|
// seems odd, note sure if it's correct.
|
||||||
|
|
||||||
|
watch := (*watch)(unsafe.Pointer(ov)) |
||||||
|
if watch == nil { |
||||||
|
select { |
||||||
|
case ch := <-w.quit: |
||||||
|
w.mu.Lock() |
||||||
|
var indexes []indexMap |
||||||
|
for _, index := range w.watches { |
||||||
|
indexes = append(indexes, index) |
||||||
|
} |
||||||
|
w.mu.Unlock() |
||||||
|
for _, index := range indexes { |
||||||
|
for _, watch := range index { |
||||||
|
w.deleteWatch(watch) |
||||||
|
w.startRead(watch) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
err := windows.CloseHandle(w.port) |
||||||
|
if err != nil { |
||||||
|
err = os.NewSyscallError("CloseHandle", err) |
||||||
|
} |
||||||
|
close(w.Events) |
||||||
|
close(w.Errors) |
||||||
|
ch <- err |
||||||
|
return |
||||||
|
case in := <-w.input: |
||||||
|
switch in.op { |
||||||
|
case opAddWatch: |
||||||
|
in.reply <- w.addWatch(in.path, uint64(in.flags)) |
||||||
|
case opRemoveWatch: |
||||||
|
in.reply <- w.remWatch(in.path) |
||||||
|
} |
||||||
|
default: |
||||||
|
} |
||||||
|
continue |
||||||
|
} |
||||||
|
|
||||||
|
switch qErr { |
||||||
|
case windows.ERROR_MORE_DATA: |
||||||
|
if watch == nil { |
||||||
|
w.sendError(errors.New("ERROR_MORE_DATA has unexpectedly null lpOverlapped buffer")) |
||||||
|
} else { |
||||||
|
// The i/o succeeded but the buffer is full.
|
||||||
|
// In theory we should be building up a full packet.
|
||||||
|
// In practice we can get away with just carrying on.
|
||||||
|
n = uint32(unsafe.Sizeof(watch.buf)) |
||||||
|
} |
||||||
|
case windows.ERROR_ACCESS_DENIED: |
||||||
|
// Watched directory was probably removed
|
||||||
|
w.sendEvent(watch.path, watch.mask&sysFSDELETESELF) |
||||||
|
w.deleteWatch(watch) |
||||||
|
w.startRead(watch) |
||||||
|
continue |
||||||
|
case windows.ERROR_OPERATION_ABORTED: |
||||||
|
// CancelIo was called on this handle
|
||||||
|
continue |
||||||
|
default: |
||||||
|
w.sendError(os.NewSyscallError("GetQueuedCompletionPort", qErr)) |
||||||
|
continue |
||||||
|
case nil: |
||||||
|
} |
||||||
|
|
||||||
|
var offset uint32 |
||||||
|
for { |
||||||
|
if n == 0 { |
||||||
|
w.sendError(errors.New("short read in readEvents()")) |
||||||
|
break |
||||||
|
} |
||||||
|
|
||||||
|
// Point "raw" to the event in the buffer
|
||||||
|
raw := (*windows.FileNotifyInformation)(unsafe.Pointer(&watch.buf[offset])) |
||||||
|
|
||||||
|
// Create a buf that is the size of the path name
|
||||||
|
size := int(raw.FileNameLength / 2) |
||||||
|
var buf []uint16 |
||||||
|
// TODO: Use unsafe.Slice in Go 1.17; https://stackoverflow.com/questions/51187973
|
||||||
|
sh := (*reflect.SliceHeader)(unsafe.Pointer(&buf)) |
||||||
|
sh.Data = uintptr(unsafe.Pointer(&raw.FileName)) |
||||||
|
sh.Len = size |
||||||
|
sh.Cap = size |
||||||
|
name := windows.UTF16ToString(buf) |
||||||
|
fullname := filepath.Join(watch.path, name) |
||||||
|
|
||||||
|
var mask uint64 |
||||||
|
switch raw.Action { |
||||||
|
case windows.FILE_ACTION_REMOVED: |
||||||
|
mask = sysFSDELETESELF |
||||||
|
case windows.FILE_ACTION_MODIFIED: |
||||||
|
mask = sysFSMODIFY |
||||||
|
case windows.FILE_ACTION_RENAMED_OLD_NAME: |
||||||
|
watch.rename = name |
||||||
|
case windows.FILE_ACTION_RENAMED_NEW_NAME: |
||||||
|
// Update saved path of all sub-watches.
|
||||||
|
old := filepath.Join(watch.path, watch.rename) |
||||||
|
w.mu.Lock() |
||||||
|
for _, watchMap := range w.watches { |
||||||
|
for _, ww := range watchMap { |
||||||
|
if strings.HasPrefix(ww.path, old) { |
||||||
|
ww.path = filepath.Join(fullname, strings.TrimPrefix(ww.path, old)) |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
w.mu.Unlock() |
||||||
|
|
||||||
|
if watch.names[watch.rename] != 0 { |
||||||
|
watch.names[name] |= watch.names[watch.rename] |
||||||
|
delete(watch.names, watch.rename) |
||||||
|
mask = sysFSMOVESELF |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
sendNameEvent := func() { |
||||||
|
w.sendEvent(fullname, watch.names[name]&mask) |
||||||
|
} |
||||||
|
if raw.Action != windows.FILE_ACTION_RENAMED_NEW_NAME { |
||||||
|
sendNameEvent() |
||||||
|
} |
||||||
|
if raw.Action == windows.FILE_ACTION_REMOVED { |
||||||
|
w.sendEvent(fullname, watch.names[name]&sysFSIGNORED) |
||||||
|
delete(watch.names, name) |
||||||
|
} |
||||||
|
|
||||||
|
w.sendEvent(fullname, watch.mask&w.toFSnotifyFlags(raw.Action)) |
||||||
|
if raw.Action == windows.FILE_ACTION_RENAMED_NEW_NAME { |
||||||
|
fullname = filepath.Join(watch.path, watch.rename) |
||||||
|
sendNameEvent() |
||||||
|
} |
||||||
|
|
||||||
|
// Move to the next event in the buffer
|
||||||
|
if raw.NextEntryOffset == 0 { |
||||||
|
break |
||||||
|
} |
||||||
|
offset += raw.NextEntryOffset |
||||||
|
|
||||||
|
// Error!
|
||||||
|
if offset >= n { |
||||||
|
w.sendError(errors.New( |
||||||
|
"Windows system assumed buffer larger than it is, events have likely been missed.")) |
||||||
|
break |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
if err := w.startRead(watch); err != nil { |
||||||
|
w.sendError(err) |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
func (w *Watcher) toWindowsFlags(mask uint64) uint32 { |
||||||
|
var m uint32 |
||||||
|
if mask&sysFSMODIFY != 0 { |
||||||
|
m |= windows.FILE_NOTIFY_CHANGE_LAST_WRITE |
||||||
|
} |
||||||
|
if mask&sysFSATTRIB != 0 { |
||||||
|
m |= windows.FILE_NOTIFY_CHANGE_ATTRIBUTES |
||||||
|
} |
||||||
|
if mask&(sysFSMOVE|sysFSCREATE|sysFSDELETE) != 0 { |
||||||
|
m |= windows.FILE_NOTIFY_CHANGE_FILE_NAME | windows.FILE_NOTIFY_CHANGE_DIR_NAME |
||||||
|
} |
||||||
|
return m |
||||||
|
} |
||||||
|
|
||||||
|
func (w *Watcher) toFSnotifyFlags(action uint32) uint64 { |
||||||
|
switch action { |
||||||
|
case windows.FILE_ACTION_ADDED: |
||||||
|
return sysFSCREATE |
||||||
|
case windows.FILE_ACTION_REMOVED: |
||||||
|
return sysFSDELETE |
||||||
|
case windows.FILE_ACTION_MODIFIED: |
||||||
|
return sysFSMODIFY |
||||||
|
case windows.FILE_ACTION_RENAMED_OLD_NAME: |
||||||
|
return sysFSMOVEDFROM |
||||||
|
case windows.FILE_ACTION_RENAMED_NEW_NAME: |
||||||
|
return sysFSMOVEDTO |
||||||
|
} |
||||||
|
return 0 |
||||||
|
} |
@ -1,38 +0,0 @@ |
|||||||
// Copyright 2010 The Go Authors. All rights reserved.
|
|
||||||
// Use of this source code is governed by a BSD-style
|
|
||||||
// license that can be found in the LICENSE file.
|
|
||||||
|
|
||||||
//go:build solaris
|
|
||||||
// +build solaris
|
|
||||||
|
|
||||||
package fsnotify |
|
||||||
|
|
||||||
import ( |
|
||||||
"errors" |
|
||||||
) |
|
||||||
|
|
||||||
// Watcher watches a set of files, delivering events to a channel.
|
|
||||||
type Watcher struct { |
|
||||||
Events chan Event |
|
||||||
Errors chan error |
|
||||||
} |
|
||||||
|
|
||||||
// NewWatcher establishes a new watcher with the underlying OS and begins waiting for events.
|
|
||||||
func NewWatcher() (*Watcher, error) { |
|
||||||
return nil, errors.New("FEN based watcher not yet supported for fsnotify\n") |
|
||||||
} |
|
||||||
|
|
||||||
// Close removes all watches and closes the events channel.
|
|
||||||
func (w *Watcher) Close() error { |
|
||||||
return nil |
|
||||||
} |
|
||||||
|
|
||||||
// Add starts watching the named file or directory (non-recursively).
|
|
||||||
func (w *Watcher) Add(name string) error { |
|
||||||
return nil |
|
||||||
} |
|
||||||
|
|
||||||
// Remove stops watching the the named file or directory (non-recursively).
|
|
||||||
func (w *Watcher) Remove(name string) error { |
|
||||||
return nil |
|
||||||
} |
|
@ -1,36 +0,0 @@ |
|||||||
// Copyright 2022 The Go Authors. All rights reserved.
|
|
||||||
// Use of this source code is governed by a BSD-style
|
|
||||||
// license that can be found in the LICENSE file.
|
|
||||||
|
|
||||||
//go:build !darwin && !dragonfly && !freebsd && !openbsd && !linux && !netbsd && !solaris && !windows
|
|
||||||
// +build !darwin,!dragonfly,!freebsd,!openbsd,!linux,!netbsd,!solaris,!windows
|
|
||||||
|
|
||||||
package fsnotify |
|
||||||
|
|
||||||
import ( |
|
||||||
"fmt" |
|
||||||
"runtime" |
|
||||||
) |
|
||||||
|
|
||||||
// Watcher watches a set of files, delivering events to a channel.
|
|
||||||
type Watcher struct{} |
|
||||||
|
|
||||||
// NewWatcher establishes a new watcher with the underlying OS and begins waiting for events.
|
|
||||||
func NewWatcher() (*Watcher, error) { |
|
||||||
return nil, fmt.Errorf("fsnotify not supported on %s", runtime.GOOS) |
|
||||||
} |
|
||||||
|
|
||||||
// Close removes all watches and closes the events channel.
|
|
||||||
func (w *Watcher) Close() error { |
|
||||||
return nil |
|
||||||
} |
|
||||||
|
|
||||||
// Add starts watching the named file or directory (non-recursively).
|
|
||||||
func (w *Watcher) Add(name string) error { |
|
||||||
return nil |
|
||||||
} |
|
||||||
|
|
||||||
// Remove stops watching the the named file or directory (non-recursively).
|
|
||||||
func (w *Watcher) Remove(name string) error { |
|
||||||
return nil |
|
||||||
} |
|
@ -1,351 +0,0 @@ |
|||||||
// Copyright 2010 The Go Authors. All rights reserved.
|
|
||||||
// Use of this source code is governed by a BSD-style
|
|
||||||
// license that can be found in the LICENSE file.
|
|
||||||
|
|
||||||
//go:build linux
|
|
||||||
// +build linux
|
|
||||||
|
|
||||||
package fsnotify |
|
||||||
|
|
||||||
import ( |
|
||||||
"errors" |
|
||||||
"fmt" |
|
||||||
"io" |
|
||||||
"os" |
|
||||||
"path/filepath" |
|
||||||
"strings" |
|
||||||
"sync" |
|
||||||
"unsafe" |
|
||||||
|
|
||||||
"golang.org/x/sys/unix" |
|
||||||
) |
|
||||||
|
|
||||||
// Watcher watches a set of files, delivering events to a channel.
|
|
||||||
type Watcher struct { |
|
||||||
Events chan Event |
|
||||||
Errors chan error |
|
||||||
mu sync.Mutex // Map access
|
|
||||||
fd int |
|
||||||
poller *fdPoller |
|
||||||
watches map[string]*watch // Map of inotify watches (key: path)
|
|
||||||
paths map[int]string // Map of watched paths (key: watch descriptor)
|
|
||||||
done chan struct{} // Channel for sending a "quit message" to the reader goroutine
|
|
||||||
doneResp chan struct{} // Channel to respond to Close
|
|
||||||
} |
|
||||||
|
|
||||||
// NewWatcher establishes a new watcher with the underlying OS and begins waiting for events.
|
|
||||||
func NewWatcher() (*Watcher, error) { |
|
||||||
// Create inotify fd
|
|
||||||
fd, errno := unix.InotifyInit1(unix.IN_CLOEXEC) |
|
||||||
if fd == -1 { |
|
||||||
return nil, errno |
|
||||||
} |
|
||||||
// Create epoll
|
|
||||||
poller, err := newFdPoller(fd) |
|
||||||
if err != nil { |
|
||||||
unix.Close(fd) |
|
||||||
return nil, err |
|
||||||
} |
|
||||||
w := &Watcher{ |
|
||||||
fd: fd, |
|
||||||
poller: poller, |
|
||||||
watches: make(map[string]*watch), |
|
||||||
paths: make(map[int]string), |
|
||||||
Events: make(chan Event), |
|
||||||
Errors: make(chan error), |
|
||||||
done: make(chan struct{}), |
|
||||||
doneResp: make(chan struct{}), |
|
||||||
} |
|
||||||
|
|
||||||
go w.readEvents() |
|
||||||
return w, nil |
|
||||||
} |
|
||||||
|
|
||||||
func (w *Watcher) isClosed() bool { |
|
||||||
select { |
|
||||||
case <-w.done: |
|
||||||
return true |
|
||||||
default: |
|
||||||
return false |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
// Close removes all watches and closes the events channel.
|
|
||||||
func (w *Watcher) Close() error { |
|
||||||
if w.isClosed() { |
|
||||||
return nil |
|
||||||
} |
|
||||||
|
|
||||||
// Send 'close' signal to goroutine, and set the Watcher to closed.
|
|
||||||
close(w.done) |
|
||||||
|
|
||||||
// Wake up goroutine
|
|
||||||
w.poller.wake() |
|
||||||
|
|
||||||
// Wait for goroutine to close
|
|
||||||
<-w.doneResp |
|
||||||
|
|
||||||
return nil |
|
||||||
} |
|
||||||
|
|
||||||
// Add starts watching the named file or directory (non-recursively).
|
|
||||||
func (w *Watcher) Add(name string) error { |
|
||||||
name = filepath.Clean(name) |
|
||||||
if w.isClosed() { |
|
||||||
return errors.New("inotify instance already closed") |
|
||||||
} |
|
||||||
|
|
||||||
const agnosticEvents = unix.IN_MOVED_TO | unix.IN_MOVED_FROM | |
|
||||||
unix.IN_CREATE | unix.IN_ATTRIB | unix.IN_MODIFY | |
|
||||||
unix.IN_MOVE_SELF | unix.IN_DELETE | unix.IN_DELETE_SELF |
|
||||||
|
|
||||||
var flags uint32 = agnosticEvents |
|
||||||
|
|
||||||
w.mu.Lock() |
|
||||||
defer w.mu.Unlock() |
|
||||||
watchEntry := w.watches[name] |
|
||||||
if watchEntry != nil { |
|
||||||
flags |= watchEntry.flags | unix.IN_MASK_ADD |
|
||||||
} |
|
||||||
wd, errno := unix.InotifyAddWatch(w.fd, name, flags) |
|
||||||
if wd == -1 { |
|
||||||
return errno |
|
||||||
} |
|
||||||
|
|
||||||
if watchEntry == nil { |
|
||||||
w.watches[name] = &watch{wd: uint32(wd), flags: flags} |
|
||||||
w.paths[wd] = name |
|
||||||
} else { |
|
||||||
watchEntry.wd = uint32(wd) |
|
||||||
watchEntry.flags = flags |
|
||||||
} |
|
||||||
|
|
||||||
return nil |
|
||||||
} |
|
||||||
|
|
||||||
// Remove stops watching the named file or directory (non-recursively).
|
|
||||||
func (w *Watcher) Remove(name string) error { |
|
||||||
name = filepath.Clean(name) |
|
||||||
|
|
||||||
// Fetch the watch.
|
|
||||||
w.mu.Lock() |
|
||||||
defer w.mu.Unlock() |
|
||||||
watch, ok := w.watches[name] |
|
||||||
|
|
||||||
// Remove it from inotify.
|
|
||||||
if !ok { |
|
||||||
return fmt.Errorf("can't remove non-existent inotify watch for: %s", name) |
|
||||||
} |
|
||||||
|
|
||||||
// We successfully removed the watch if InotifyRmWatch doesn't return an
|
|
||||||
// error, we need to clean up our internal state to ensure it matches
|
|
||||||
// inotify's kernel state.
|
|
||||||
delete(w.paths, int(watch.wd)) |
|
||||||
delete(w.watches, name) |
|
||||||
|
|
||||||
// inotify_rm_watch will return EINVAL if the file has been deleted;
|
|
||||||
// the inotify will already have been removed.
|
|
||||||
// watches and pathes are deleted in ignoreLinux() implicitly and asynchronously
|
|
||||||
// by calling inotify_rm_watch() below. e.g. readEvents() goroutine receives IN_IGNORE
|
|
||||||
// so that EINVAL means that the wd is being rm_watch()ed or its file removed
|
|
||||||
// by another thread and we have not received IN_IGNORE event.
|
|
||||||
success, errno := unix.InotifyRmWatch(w.fd, watch.wd) |
|
||||||
if success == -1 { |
|
||||||
// TODO: Perhaps it's not helpful to return an error here in every case.
|
|
||||||
// the only two possible errors are:
|
|
||||||
// EBADF, which happens when w.fd is not a valid file descriptor of any kind.
|
|
||||||
// EINVAL, which is when fd is not an inotify descriptor or wd is not a valid watch descriptor.
|
|
||||||
// Watch descriptors are invalidated when they are removed explicitly or implicitly;
|
|
||||||
// explicitly by inotify_rm_watch, implicitly when the file they are watching is deleted.
|
|
||||||
return errno |
|
||||||
} |
|
||||||
|
|
||||||
return nil |
|
||||||
} |
|
||||||
|
|
||||||
// WatchList returns the directories and files that are being monitered.
|
|
||||||
func (w *Watcher) WatchList() []string { |
|
||||||
w.mu.Lock() |
|
||||||
defer w.mu.Unlock() |
|
||||||
|
|
||||||
entries := make([]string, 0, len(w.watches)) |
|
||||||
for pathname := range w.watches { |
|
||||||
entries = append(entries, pathname) |
|
||||||
} |
|
||||||
|
|
||||||
return entries |
|
||||||
} |
|
||||||
|
|
||||||
type watch struct { |
|
||||||
wd uint32 // Watch descriptor (as returned by the inotify_add_watch() syscall)
|
|
||||||
flags uint32 // inotify flags of this watch (see inotify(7) for the list of valid flags)
|
|
||||||
} |
|
||||||
|
|
||||||
// readEvents reads from the inotify file descriptor, converts the
|
|
||||||
// received events into Event objects and sends them via the Events channel
|
|
||||||
func (w *Watcher) readEvents() { |
|
||||||
var ( |
|
||||||
buf [unix.SizeofInotifyEvent * 4096]byte // Buffer for a maximum of 4096 raw events
|
|
||||||
n int // Number of bytes read with read()
|
|
||||||
errno error // Syscall errno
|
|
||||||
ok bool // For poller.wait
|
|
||||||
) |
|
||||||
|
|
||||||
defer close(w.doneResp) |
|
||||||
defer close(w.Errors) |
|
||||||
defer close(w.Events) |
|
||||||
defer unix.Close(w.fd) |
|
||||||
defer w.poller.close() |
|
||||||
|
|
||||||
for { |
|
||||||
// See if we have been closed.
|
|
||||||
if w.isClosed() { |
|
||||||
return |
|
||||||
} |
|
||||||
|
|
||||||
ok, errno = w.poller.wait() |
|
||||||
if errno != nil { |
|
||||||
select { |
|
||||||
case w.Errors <- errno: |
|
||||||
case <-w.done: |
|
||||||
return |
|
||||||
} |
|
||||||
continue |
|
||||||
} |
|
||||||
|
|
||||||
if !ok { |
|
||||||
continue |
|
||||||
} |
|
||||||
|
|
||||||
n, errno = unix.Read(w.fd, buf[:]) |
|
||||||
// If a signal interrupted execution, see if we've been asked to close, and try again.
|
|
||||||
// http://man7.org/linux/man-pages/man7/signal.7.html :
|
|
||||||
// "Before Linux 3.8, reads from an inotify(7) file descriptor were not restartable"
|
|
||||||
if errno == unix.EINTR { |
|
||||||
continue |
|
||||||
} |
|
||||||
|
|
||||||
// unix.Read might have been woken up by Close. If so, we're done.
|
|
||||||
if w.isClosed() { |
|
||||||
return |
|
||||||
} |
|
||||||
|
|
||||||
if n < unix.SizeofInotifyEvent { |
|
||||||
var err error |
|
||||||
if n == 0 { |
|
||||||
// If EOF is received. This should really never happen.
|
|
||||||
err = io.EOF |
|
||||||
} else if n < 0 { |
|
||||||
// If an error occurred while reading.
|
|
||||||
err = errno |
|
||||||
} else { |
|
||||||
// Read was too short.
|
|
||||||
err = errors.New("notify: short read in readEvents()") |
|
||||||
} |
|
||||||
select { |
|
||||||
case w.Errors <- err: |
|
||||||
case <-w.done: |
|
||||||
return |
|
||||||
} |
|
||||||
continue |
|
||||||
} |
|
||||||
|
|
||||||
var offset uint32 |
|
||||||
// We don't know how many events we just read into the buffer
|
|
||||||
// While the offset points to at least one whole event...
|
|
||||||
for offset <= uint32(n-unix.SizeofInotifyEvent) { |
|
||||||
// Point "raw" to the event in the buffer
|
|
||||||
raw := (*unix.InotifyEvent)(unsafe.Pointer(&buf[offset])) |
|
||||||
|
|
||||||
mask := uint32(raw.Mask) |
|
||||||
nameLen := uint32(raw.Len) |
|
||||||
|
|
||||||
if mask&unix.IN_Q_OVERFLOW != 0 { |
|
||||||
select { |
|
||||||
case w.Errors <- ErrEventOverflow: |
|
||||||
case <-w.done: |
|
||||||
return |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
// If the event happened to the watched directory or the watched file, the kernel
|
|
||||||
// doesn't append the filename to the event, but we would like to always fill the
|
|
||||||
// the "Name" field with a valid filename. We retrieve the path of the watch from
|
|
||||||
// the "paths" map.
|
|
||||||
w.mu.Lock() |
|
||||||
name, ok := w.paths[int(raw.Wd)] |
|
||||||
// IN_DELETE_SELF occurs when the file/directory being watched is removed.
|
|
||||||
// This is a sign to clean up the maps, otherwise we are no longer in sync
|
|
||||||
// with the inotify kernel state which has already deleted the watch
|
|
||||||
// automatically.
|
|
||||||
if ok && mask&unix.IN_DELETE_SELF == unix.IN_DELETE_SELF { |
|
||||||
delete(w.paths, int(raw.Wd)) |
|
||||||
delete(w.watches, name) |
|
||||||
} |
|
||||||
w.mu.Unlock() |
|
||||||
|
|
||||||
if nameLen > 0 { |
|
||||||
// Point "bytes" at the first byte of the filename
|
|
||||||
bytes := (*[unix.PathMax]byte)(unsafe.Pointer(&buf[offset+unix.SizeofInotifyEvent]))[:nameLen:nameLen] |
|
||||||
// The filename is padded with NULL bytes. TrimRight() gets rid of those.
|
|
||||||
name += "/" + strings.TrimRight(string(bytes[0:nameLen]), "\000") |
|
||||||
} |
|
||||||
|
|
||||||
event := newEvent(name, mask) |
|
||||||
|
|
||||||
// Send the events that are not ignored on the events channel
|
|
||||||
if !event.ignoreLinux(mask) { |
|
||||||
select { |
|
||||||
case w.Events <- event: |
|
||||||
case <-w.done: |
|
||||||
return |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
// Move to the next event in the buffer
|
|
||||||
offset += unix.SizeofInotifyEvent + nameLen |
|
||||||
} |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
// Certain types of events can be "ignored" and not sent over the Events
|
|
||||||
// channel. Such as events marked ignore by the kernel, or MODIFY events
|
|
||||||
// against files that do not exist.
|
|
||||||
func (e *Event) ignoreLinux(mask uint32) bool { |
|
||||||
// Ignore anything the inotify API says to ignore
|
|
||||||
if mask&unix.IN_IGNORED == unix.IN_IGNORED { |
|
||||||
return true |
|
||||||
} |
|
||||||
|
|
||||||
// If the event is not a DELETE or RENAME, the file must exist.
|
|
||||||
// Otherwise the event is ignored.
|
|
||||||
// *Note*: this was put in place because it was seen that a MODIFY
|
|
||||||
// event was sent after the DELETE. This ignores that MODIFY and
|
|
||||||
// assumes a DELETE will come or has come if the file doesn't exist.
|
|
||||||
if !(e.Op&Remove == Remove || e.Op&Rename == Rename) { |
|
||||||
_, statErr := os.Lstat(e.Name) |
|
||||||
return os.IsNotExist(statErr) |
|
||||||
} |
|
||||||
return false |
|
||||||
} |
|
||||||
|
|
||||||
// newEvent returns an platform-independent Event based on an inotify mask.
|
|
||||||
func newEvent(name string, mask uint32) Event { |
|
||||||
e := Event{Name: name} |
|
||||||
if mask&unix.IN_CREATE == unix.IN_CREATE || mask&unix.IN_MOVED_TO == unix.IN_MOVED_TO { |
|
||||||
e.Op |= Create |
|
||||||
} |
|
||||||
if mask&unix.IN_DELETE_SELF == unix.IN_DELETE_SELF || mask&unix.IN_DELETE == unix.IN_DELETE { |
|
||||||
e.Op |= Remove |
|
||||||
} |
|
||||||
if mask&unix.IN_MODIFY == unix.IN_MODIFY { |
|
||||||
e.Op |= Write |
|
||||||
} |
|
||||||
if mask&unix.IN_MOVE_SELF == unix.IN_MOVE_SELF || mask&unix.IN_MOVED_FROM == unix.IN_MOVED_FROM { |
|
||||||
e.Op |= Rename |
|
||||||
} |
|
||||||
if mask&unix.IN_ATTRIB == unix.IN_ATTRIB { |
|
||||||
e.Op |= Chmod |
|
||||||
} |
|
||||||
return e |
|
||||||
} |
|
@ -1,187 +0,0 @@ |
|||||||
// Copyright 2015 The Go Authors. All rights reserved.
|
|
||||||
// Use of this source code is governed by a BSD-style
|
|
||||||
// license that can be found in the LICENSE file.
|
|
||||||
|
|
||||||
//go:build linux
|
|
||||||
// +build linux
|
|
||||||
|
|
||||||
package fsnotify |
|
||||||
|
|
||||||
import ( |
|
||||||
"errors" |
|
||||||
|
|
||||||
"golang.org/x/sys/unix" |
|
||||||
) |
|
||||||
|
|
||||||
type fdPoller struct { |
|
||||||
fd int // File descriptor (as returned by the inotify_init() syscall)
|
|
||||||
epfd int // Epoll file descriptor
|
|
||||||
pipe [2]int // Pipe for waking up
|
|
||||||
} |
|
||||||
|
|
||||||
func emptyPoller(fd int) *fdPoller { |
|
||||||
poller := new(fdPoller) |
|
||||||
poller.fd = fd |
|
||||||
poller.epfd = -1 |
|
||||||
poller.pipe[0] = -1 |
|
||||||
poller.pipe[1] = -1 |
|
||||||
return poller |
|
||||||
} |
|
||||||
|
|
||||||
// Create a new inotify poller.
|
|
||||||
// This creates an inotify handler, and an epoll handler.
|
|
||||||
func newFdPoller(fd int) (*fdPoller, error) { |
|
||||||
var errno error |
|
||||||
poller := emptyPoller(fd) |
|
||||||
defer func() { |
|
||||||
if errno != nil { |
|
||||||
poller.close() |
|
||||||
} |
|
||||||
}() |
|
||||||
|
|
||||||
// Create epoll fd
|
|
||||||
poller.epfd, errno = unix.EpollCreate1(unix.EPOLL_CLOEXEC) |
|
||||||
if poller.epfd == -1 { |
|
||||||
return nil, errno |
|
||||||
} |
|
||||||
// Create pipe; pipe[0] is the read end, pipe[1] the write end.
|
|
||||||
errno = unix.Pipe2(poller.pipe[:], unix.O_NONBLOCK|unix.O_CLOEXEC) |
|
||||||
if errno != nil { |
|
||||||
return nil, errno |
|
||||||
} |
|
||||||
|
|
||||||
// Register inotify fd with epoll
|
|
||||||
event := unix.EpollEvent{ |
|
||||||
Fd: int32(poller.fd), |
|
||||||
Events: unix.EPOLLIN, |
|
||||||
} |
|
||||||
errno = unix.EpollCtl(poller.epfd, unix.EPOLL_CTL_ADD, poller.fd, &event) |
|
||||||
if errno != nil { |
|
||||||
return nil, errno |
|
||||||
} |
|
||||||
|
|
||||||
// Register pipe fd with epoll
|
|
||||||
event = unix.EpollEvent{ |
|
||||||
Fd: int32(poller.pipe[0]), |
|
||||||
Events: unix.EPOLLIN, |
|
||||||
} |
|
||||||
errno = unix.EpollCtl(poller.epfd, unix.EPOLL_CTL_ADD, poller.pipe[0], &event) |
|
||||||
if errno != nil { |
|
||||||
return nil, errno |
|
||||||
} |
|
||||||
|
|
||||||
return poller, nil |
|
||||||
} |
|
||||||
|
|
||||||
// Wait using epoll.
|
|
||||||
// Returns true if something is ready to be read,
|
|
||||||
// false if there is not.
|
|
||||||
func (poller *fdPoller) wait() (bool, error) { |
|
||||||
// 3 possible events per fd, and 2 fds, makes a maximum of 6 events.
|
|
||||||
// I don't know whether epoll_wait returns the number of events returned,
|
|
||||||
// or the total number of events ready.
|
|
||||||
// I decided to catch both by making the buffer one larger than the maximum.
|
|
||||||
events := make([]unix.EpollEvent, 7) |
|
||||||
for { |
|
||||||
n, errno := unix.EpollWait(poller.epfd, events, -1) |
|
||||||
if n == -1 { |
|
||||||
if errno == unix.EINTR { |
|
||||||
continue |
|
||||||
} |
|
||||||
return false, errno |
|
||||||
} |
|
||||||
if n == 0 { |
|
||||||
// If there are no events, try again.
|
|
||||||
continue |
|
||||||
} |
|
||||||
if n > 6 { |
|
||||||
// This should never happen. More events were returned than should be possible.
|
|
||||||
return false, errors.New("epoll_wait returned more events than I know what to do with") |
|
||||||
} |
|
||||||
ready := events[:n] |
|
||||||
epollhup := false |
|
||||||
epollerr := false |
|
||||||
epollin := false |
|
||||||
for _, event := range ready { |
|
||||||
if event.Fd == int32(poller.fd) { |
|
||||||
if event.Events&unix.EPOLLHUP != 0 { |
|
||||||
// This should not happen, but if it does, treat it as a wakeup.
|
|
||||||
epollhup = true |
|
||||||
} |
|
||||||
if event.Events&unix.EPOLLERR != 0 { |
|
||||||
// If an error is waiting on the file descriptor, we should pretend
|
|
||||||
// something is ready to read, and let unix.Read pick up the error.
|
|
||||||
epollerr = true |
|
||||||
} |
|
||||||
if event.Events&unix.EPOLLIN != 0 { |
|
||||||
// There is data to read.
|
|
||||||
epollin = true |
|
||||||
} |
|
||||||
} |
|
||||||
if event.Fd == int32(poller.pipe[0]) { |
|
||||||
if event.Events&unix.EPOLLHUP != 0 { |
|
||||||
// Write pipe descriptor was closed, by us. This means we're closing down the
|
|
||||||
// watcher, and we should wake up.
|
|
||||||
} |
|
||||||
if event.Events&unix.EPOLLERR != 0 { |
|
||||||
// If an error is waiting on the pipe file descriptor.
|
|
||||||
// This is an absolute mystery, and should never ever happen.
|
|
||||||
return false, errors.New("Error on the pipe descriptor.") |
|
||||||
} |
|
||||||
if event.Events&unix.EPOLLIN != 0 { |
|
||||||
// This is a regular wakeup, so we have to clear the buffer.
|
|
||||||
err := poller.clearWake() |
|
||||||
if err != nil { |
|
||||||
return false, err |
|
||||||
} |
|
||||||
} |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
if epollhup || epollerr || epollin { |
|
||||||
return true, nil |
|
||||||
} |
|
||||||
return false, nil |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
// Close the write end of the poller.
|
|
||||||
func (poller *fdPoller) wake() error { |
|
||||||
buf := make([]byte, 1) |
|
||||||
n, errno := unix.Write(poller.pipe[1], buf) |
|
||||||
if n == -1 { |
|
||||||
if errno == unix.EAGAIN { |
|
||||||
// Buffer is full, poller will wake.
|
|
||||||
return nil |
|
||||||
} |
|
||||||
return errno |
|
||||||
} |
|
||||||
return nil |
|
||||||
} |
|
||||||
|
|
||||||
func (poller *fdPoller) clearWake() error { |
|
||||||
// You have to be woken up a LOT in order to get to 100!
|
|
||||||
buf := make([]byte, 100) |
|
||||||
n, errno := unix.Read(poller.pipe[0], buf) |
|
||||||
if n == -1 { |
|
||||||
if errno == unix.EAGAIN { |
|
||||||
// Buffer is empty, someone else cleared our wake.
|
|
||||||
return nil |
|
||||||
} |
|
||||||
return errno |
|
||||||
} |
|
||||||
return nil |
|
||||||
} |
|
||||||
|
|
||||||
// Close all poller file descriptors, but not the one passed to it.
|
|
||||||
func (poller *fdPoller) close() { |
|
||||||
if poller.pipe[1] != -1 { |
|
||||||
unix.Close(poller.pipe[1]) |
|
||||||
} |
|
||||||
if poller.pipe[0] != -1 { |
|
||||||
unix.Close(poller.pipe[0]) |
|
||||||
} |
|
||||||
if poller.epfd != -1 { |
|
||||||
unix.Close(poller.epfd) |
|
||||||
} |
|
||||||
} |
|
@ -1,535 +0,0 @@ |
|||||||
// Copyright 2010 The Go Authors. All rights reserved.
|
|
||||||
// Use of this source code is governed by a BSD-style
|
|
||||||
// license that can be found in the LICENSE file.
|
|
||||||
|
|
||||||
//go:build freebsd || openbsd || netbsd || dragonfly || darwin
|
|
||||||
// +build freebsd openbsd netbsd dragonfly darwin
|
|
||||||
|
|
||||||
package fsnotify |
|
||||||
|
|
||||||
import ( |
|
||||||
"errors" |
|
||||||
"fmt" |
|
||||||
"io/ioutil" |
|
||||||
"os" |
|
||||||
"path/filepath" |
|
||||||
"sync" |
|
||||||
"time" |
|
||||||
|
|
||||||
"golang.org/x/sys/unix" |
|
||||||
) |
|
||||||
|
|
||||||
// Watcher watches a set of files, delivering events to a channel.
|
|
||||||
type Watcher struct { |
|
||||||
Events chan Event |
|
||||||
Errors chan error |
|
||||||
done chan struct{} // Channel for sending a "quit message" to the reader goroutine
|
|
||||||
|
|
||||||
kq int // File descriptor (as returned by the kqueue() syscall).
|
|
||||||
|
|
||||||
mu sync.Mutex // Protects access to watcher data
|
|
||||||
watches map[string]int // Map of watched file descriptors (key: path).
|
|
||||||
externalWatches map[string]bool // Map of watches added by user of the library.
|
|
||||||
dirFlags map[string]uint32 // Map of watched directories to fflags used in kqueue.
|
|
||||||
paths map[int]pathInfo // Map file descriptors to path names for processing kqueue events.
|
|
||||||
fileExists map[string]bool // Keep track of if we know this file exists (to stop duplicate create events).
|
|
||||||
isClosed bool // Set to true when Close() is first called
|
|
||||||
} |
|
||||||
|
|
||||||
type pathInfo struct { |
|
||||||
name string |
|
||||||
isDir bool |
|
||||||
} |
|
||||||
|
|
||||||
// NewWatcher establishes a new watcher with the underlying OS and begins waiting for events.
|
|
||||||
func NewWatcher() (*Watcher, error) { |
|
||||||
kq, err := kqueue() |
|
||||||
if err != nil { |
|
||||||
return nil, err |
|
||||||
} |
|
||||||
|
|
||||||
w := &Watcher{ |
|
||||||
kq: kq, |
|
||||||
watches: make(map[string]int), |
|
||||||
dirFlags: make(map[string]uint32), |
|
||||||
paths: make(map[int]pathInfo), |
|
||||||
fileExists: make(map[string]bool), |
|
||||||
externalWatches: make(map[string]bool), |
|
||||||
Events: make(chan Event), |
|
||||||
Errors: make(chan error), |
|
||||||
done: make(chan struct{}), |
|
||||||
} |
|
||||||
|
|
||||||
go w.readEvents() |
|
||||||
return w, nil |
|
||||||
} |
|
||||||
|
|
||||||
// Close removes all watches and closes the events channel.
|
|
||||||
func (w *Watcher) Close() error { |
|
||||||
w.mu.Lock() |
|
||||||
if w.isClosed { |
|
||||||
w.mu.Unlock() |
|
||||||
return nil |
|
||||||
} |
|
||||||
w.isClosed = true |
|
||||||
|
|
||||||
// copy paths to remove while locked
|
|
||||||
var pathsToRemove = make([]string, 0, len(w.watches)) |
|
||||||
for name := range w.watches { |
|
||||||
pathsToRemove = append(pathsToRemove, name) |
|
||||||
} |
|
||||||
w.mu.Unlock() |
|
||||||
// unlock before calling Remove, which also locks
|
|
||||||
|
|
||||||
for _, name := range pathsToRemove { |
|
||||||
w.Remove(name) |
|
||||||
} |
|
||||||
|
|
||||||
// send a "quit" message to the reader goroutine
|
|
||||||
close(w.done) |
|
||||||
|
|
||||||
return nil |
|
||||||
} |
|
||||||
|
|
||||||
// Add starts watching the named file or directory (non-recursively).
|
|
||||||
func (w *Watcher) Add(name string) error { |
|
||||||
w.mu.Lock() |
|
||||||
w.externalWatches[name] = true |
|
||||||
w.mu.Unlock() |
|
||||||
_, err := w.addWatch(name, noteAllEvents) |
|
||||||
return err |
|
||||||
} |
|
||||||
|
|
||||||
// Remove stops watching the the named file or directory (non-recursively).
|
|
||||||
func (w *Watcher) Remove(name string) error { |
|
||||||
name = filepath.Clean(name) |
|
||||||
w.mu.Lock() |
|
||||||
watchfd, ok := w.watches[name] |
|
||||||
w.mu.Unlock() |
|
||||||
if !ok { |
|
||||||
return fmt.Errorf("can't remove non-existent kevent watch for: %s", name) |
|
||||||
} |
|
||||||
|
|
||||||
const registerRemove = unix.EV_DELETE |
|
||||||
if err := register(w.kq, []int{watchfd}, registerRemove, 0); err != nil { |
|
||||||
return err |
|
||||||
} |
|
||||||
|
|
||||||
unix.Close(watchfd) |
|
||||||
|
|
||||||
w.mu.Lock() |
|
||||||
isDir := w.paths[watchfd].isDir |
|
||||||
delete(w.watches, name) |
|
||||||
delete(w.paths, watchfd) |
|
||||||
delete(w.dirFlags, name) |
|
||||||
w.mu.Unlock() |
|
||||||
|
|
||||||
// Find all watched paths that are in this directory that are not external.
|
|
||||||
if isDir { |
|
||||||
var pathsToRemove []string |
|
||||||
w.mu.Lock() |
|
||||||
for _, path := range w.paths { |
|
||||||
wdir, _ := filepath.Split(path.name) |
|
||||||
if filepath.Clean(wdir) == name { |
|
||||||
if !w.externalWatches[path.name] { |
|
||||||
pathsToRemove = append(pathsToRemove, path.name) |
|
||||||
} |
|
||||||
} |
|
||||||
} |
|
||||||
w.mu.Unlock() |
|
||||||
for _, name := range pathsToRemove { |
|
||||||
// Since these are internal, not much sense in propagating error
|
|
||||||
// to the user, as that will just confuse them with an error about
|
|
||||||
// a path they did not explicitly watch themselves.
|
|
||||||
w.Remove(name) |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
return nil |
|
||||||
} |
|
||||||
|
|
||||||
// WatchList returns the directories and files that are being monitered.
|
|
||||||
func (w *Watcher) WatchList() []string { |
|
||||||
w.mu.Lock() |
|
||||||
defer w.mu.Unlock() |
|
||||||
|
|
||||||
entries := make([]string, 0, len(w.watches)) |
|
||||||
for pathname := range w.watches { |
|
||||||
entries = append(entries, pathname) |
|
||||||
} |
|
||||||
|
|
||||||
return entries |
|
||||||
} |
|
||||||
|
|
||||||
// Watch all events (except NOTE_EXTEND, NOTE_LINK, NOTE_REVOKE)
|
|
||||||
const noteAllEvents = unix.NOTE_DELETE | unix.NOTE_WRITE | unix.NOTE_ATTRIB | unix.NOTE_RENAME |
|
||||||
|
|
||||||
// keventWaitTime to block on each read from kevent
|
|
||||||
var keventWaitTime = durationToTimespec(100 * time.Millisecond) |
|
||||||
|
|
||||||
// addWatch adds name to the watched file set.
|
|
||||||
// The flags are interpreted as described in kevent(2).
|
|
||||||
// Returns the real path to the file which was added, if any, which may be different from the one passed in the case of symlinks.
|
|
||||||
func (w *Watcher) addWatch(name string, flags uint32) (string, error) { |
|
||||||
var isDir bool |
|
||||||
// Make ./name and name equivalent
|
|
||||||
name = filepath.Clean(name) |
|
||||||
|
|
||||||
w.mu.Lock() |
|
||||||
if w.isClosed { |
|
||||||
w.mu.Unlock() |
|
||||||
return "", errors.New("kevent instance already closed") |
|
||||||
} |
|
||||||
watchfd, alreadyWatching := w.watches[name] |
|
||||||
// We already have a watch, but we can still override flags.
|
|
||||||
if alreadyWatching { |
|
||||||
isDir = w.paths[watchfd].isDir |
|
||||||
} |
|
||||||
w.mu.Unlock() |
|
||||||
|
|
||||||
if !alreadyWatching { |
|
||||||
fi, err := os.Lstat(name) |
|
||||||
if err != nil { |
|
||||||
return "", err |
|
||||||
} |
|
||||||
|
|
||||||
// Don't watch sockets.
|
|
||||||
if fi.Mode()&os.ModeSocket == os.ModeSocket { |
|
||||||
return "", nil |
|
||||||
} |
|
||||||
|
|
||||||
// Don't watch named pipes.
|
|
||||||
if fi.Mode()&os.ModeNamedPipe == os.ModeNamedPipe { |
|
||||||
return "", nil |
|
||||||
} |
|
||||||
|
|
||||||
// Follow Symlinks
|
|
||||||
// Unfortunately, Linux can add bogus symlinks to watch list without
|
|
||||||
// issue, and Windows can't do symlinks period (AFAIK). To maintain
|
|
||||||
// consistency, we will act like everything is fine. There will simply
|
|
||||||
// be no file events for broken symlinks.
|
|
||||||
// Hence the returns of nil on errors.
|
|
||||||
if fi.Mode()&os.ModeSymlink == os.ModeSymlink { |
|
||||||
name, err = filepath.EvalSymlinks(name) |
|
||||||
if err != nil { |
|
||||||
return "", nil |
|
||||||
} |
|
||||||
|
|
||||||
w.mu.Lock() |
|
||||||
_, alreadyWatching = w.watches[name] |
|
||||||
w.mu.Unlock() |
|
||||||
|
|
||||||
if alreadyWatching { |
|
||||||
return name, nil |
|
||||||
} |
|
||||||
|
|
||||||
fi, err = os.Lstat(name) |
|
||||||
if err != nil { |
|
||||||
return "", nil |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
watchfd, err = unix.Open(name, openMode, 0700) |
|
||||||
if watchfd == -1 { |
|
||||||
return "", err |
|
||||||
} |
|
||||||
|
|
||||||
isDir = fi.IsDir() |
|
||||||
} |
|
||||||
|
|
||||||
const registerAdd = unix.EV_ADD | unix.EV_CLEAR | unix.EV_ENABLE |
|
||||||
if err := register(w.kq, []int{watchfd}, registerAdd, flags); err != nil { |
|
||||||
unix.Close(watchfd) |
|
||||||
return "", err |
|
||||||
} |
|
||||||
|
|
||||||
if !alreadyWatching { |
|
||||||
w.mu.Lock() |
|
||||||
w.watches[name] = watchfd |
|
||||||
w.paths[watchfd] = pathInfo{name: name, isDir: isDir} |
|
||||||
w.mu.Unlock() |
|
||||||
} |
|
||||||
|
|
||||||
if isDir { |
|
||||||
// Watch the directory if it has not been watched before,
|
|
||||||
// or if it was watched before, but perhaps only a NOTE_DELETE (watchDirectoryFiles)
|
|
||||||
w.mu.Lock() |
|
||||||
|
|
||||||
watchDir := (flags&unix.NOTE_WRITE) == unix.NOTE_WRITE && |
|
||||||
(!alreadyWatching || (w.dirFlags[name]&unix.NOTE_WRITE) != unix.NOTE_WRITE) |
|
||||||
// Store flags so this watch can be updated later
|
|
||||||
w.dirFlags[name] = flags |
|
||||||
w.mu.Unlock() |
|
||||||
|
|
||||||
if watchDir { |
|
||||||
if err := w.watchDirectoryFiles(name); err != nil { |
|
||||||
return "", err |
|
||||||
} |
|
||||||
} |
|
||||||
} |
|
||||||
return name, nil |
|
||||||
} |
|
||||||
|
|
||||||
// readEvents reads from kqueue and converts the received kevents into
|
|
||||||
// Event values that it sends down the Events channel.
|
|
||||||
func (w *Watcher) readEvents() { |
|
||||||
eventBuffer := make([]unix.Kevent_t, 10) |
|
||||||
|
|
||||||
loop: |
|
||||||
for { |
|
||||||
// See if there is a message on the "done" channel
|
|
||||||
select { |
|
||||||
case <-w.done: |
|
||||||
break loop |
|
||||||
default: |
|
||||||
} |
|
||||||
|
|
||||||
// Get new events
|
|
||||||
kevents, err := read(w.kq, eventBuffer, &keventWaitTime) |
|
||||||
// EINTR is okay, the syscall was interrupted before timeout expired.
|
|
||||||
if err != nil && err != unix.EINTR { |
|
||||||
select { |
|
||||||
case w.Errors <- err: |
|
||||||
case <-w.done: |
|
||||||
break loop |
|
||||||
} |
|
||||||
continue |
|
||||||
} |
|
||||||
|
|
||||||
// Flush the events we received to the Events channel
|
|
||||||
for len(kevents) > 0 { |
|
||||||
kevent := &kevents[0] |
|
||||||
watchfd := int(kevent.Ident) |
|
||||||
mask := uint32(kevent.Fflags) |
|
||||||
w.mu.Lock() |
|
||||||
path := w.paths[watchfd] |
|
||||||
w.mu.Unlock() |
|
||||||
event := newEvent(path.name, mask) |
|
||||||
|
|
||||||
if path.isDir && !(event.Op&Remove == Remove) { |
|
||||||
// Double check to make sure the directory exists. This can happen when
|
|
||||||
// we do a rm -fr on a recursively watched folders and we receive a
|
|
||||||
// modification event first but the folder has been deleted and later
|
|
||||||
// receive the delete event
|
|
||||||
if _, err := os.Lstat(event.Name); os.IsNotExist(err) { |
|
||||||
// mark is as delete event
|
|
||||||
event.Op |= Remove |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
if event.Op&Rename == Rename || event.Op&Remove == Remove { |
|
||||||
w.Remove(event.Name) |
|
||||||
w.mu.Lock() |
|
||||||
delete(w.fileExists, event.Name) |
|
||||||
w.mu.Unlock() |
|
||||||
} |
|
||||||
|
|
||||||
if path.isDir && event.Op&Write == Write && !(event.Op&Remove == Remove) { |
|
||||||
w.sendDirectoryChangeEvents(event.Name) |
|
||||||
} else { |
|
||||||
// Send the event on the Events channel.
|
|
||||||
select { |
|
||||||
case w.Events <- event: |
|
||||||
case <-w.done: |
|
||||||
break loop |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
if event.Op&Remove == Remove { |
|
||||||
// Look for a file that may have overwritten this.
|
|
||||||
// For example, mv f1 f2 will delete f2, then create f2.
|
|
||||||
if path.isDir { |
|
||||||
fileDir := filepath.Clean(event.Name) |
|
||||||
w.mu.Lock() |
|
||||||
_, found := w.watches[fileDir] |
|
||||||
w.mu.Unlock() |
|
||||||
if found { |
|
||||||
// make sure the directory exists before we watch for changes. When we
|
|
||||||
// do a recursive watch and perform rm -fr, the parent directory might
|
|
||||||
// have gone missing, ignore the missing directory and let the
|
|
||||||
// upcoming delete event remove the watch from the parent directory.
|
|
||||||
if _, err := os.Lstat(fileDir); err == nil { |
|
||||||
w.sendDirectoryChangeEvents(fileDir) |
|
||||||
} |
|
||||||
} |
|
||||||
} else { |
|
||||||
filePath := filepath.Clean(event.Name) |
|
||||||
if fileInfo, err := os.Lstat(filePath); err == nil { |
|
||||||
w.sendFileCreatedEventIfNew(filePath, fileInfo) |
|
||||||
} |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
// Move to next event
|
|
||||||
kevents = kevents[1:] |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
// cleanup
|
|
||||||
err := unix.Close(w.kq) |
|
||||||
if err != nil { |
|
||||||
// only way the previous loop breaks is if w.done was closed so we need to async send to w.Errors.
|
|
||||||
select { |
|
||||||
case w.Errors <- err: |
|
||||||
default: |
|
||||||
} |
|
||||||
} |
|
||||||
close(w.Events) |
|
||||||
close(w.Errors) |
|
||||||
} |
|
||||||
|
|
||||||
// newEvent returns an platform-independent Event based on kqueue Fflags.
|
|
||||||
func newEvent(name string, mask uint32) Event { |
|
||||||
e := Event{Name: name} |
|
||||||
if mask&unix.NOTE_DELETE == unix.NOTE_DELETE { |
|
||||||
e.Op |= Remove |
|
||||||
} |
|
||||||
if mask&unix.NOTE_WRITE == unix.NOTE_WRITE { |
|
||||||
e.Op |= Write |
|
||||||
} |
|
||||||
if mask&unix.NOTE_RENAME == unix.NOTE_RENAME { |
|
||||||
e.Op |= Rename |
|
||||||
} |
|
||||||
if mask&unix.NOTE_ATTRIB == unix.NOTE_ATTRIB { |
|
||||||
e.Op |= Chmod |
|
||||||
} |
|
||||||
return e |
|
||||||
} |
|
||||||
|
|
||||||
func newCreateEvent(name string) Event { |
|
||||||
return Event{Name: name, Op: Create} |
|
||||||
} |
|
||||||
|
|
||||||
// watchDirectoryFiles to mimic inotify when adding a watch on a directory
|
|
||||||
func (w *Watcher) watchDirectoryFiles(dirPath string) error { |
|
||||||
// Get all files
|
|
||||||
files, err := ioutil.ReadDir(dirPath) |
|
||||||
if err != nil { |
|
||||||
return err |
|
||||||
} |
|
||||||
|
|
||||||
for _, fileInfo := range files { |
|
||||||
filePath := filepath.Join(dirPath, fileInfo.Name()) |
|
||||||
filePath, err = w.internalWatch(filePath, fileInfo) |
|
||||||
if err != nil { |
|
||||||
return err |
|
||||||
} |
|
||||||
|
|
||||||
w.mu.Lock() |
|
||||||
w.fileExists[filePath] = true |
|
||||||
w.mu.Unlock() |
|
||||||
} |
|
||||||
|
|
||||||
return nil |
|
||||||
} |
|
||||||
|
|
||||||
// sendDirectoryEvents searches the directory for newly created files
|
|
||||||
// and sends them over the event channel. This functionality is to have
|
|
||||||
// the BSD version of fsnotify match Linux inotify which provides a
|
|
||||||
// create event for files created in a watched directory.
|
|
||||||
func (w *Watcher) sendDirectoryChangeEvents(dirPath string) { |
|
||||||
// Get all files
|
|
||||||
files, err := ioutil.ReadDir(dirPath) |
|
||||||
if err != nil { |
|
||||||
select { |
|
||||||
case w.Errors <- err: |
|
||||||
case <-w.done: |
|
||||||
return |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
// Search for new files
|
|
||||||
for _, fileInfo := range files { |
|
||||||
filePath := filepath.Join(dirPath, fileInfo.Name()) |
|
||||||
err := w.sendFileCreatedEventIfNew(filePath, fileInfo) |
|
||||||
|
|
||||||
if err != nil { |
|
||||||
return |
|
||||||
} |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
// sendFileCreatedEvent sends a create event if the file isn't already being tracked.
|
|
||||||
func (w *Watcher) sendFileCreatedEventIfNew(filePath string, fileInfo os.FileInfo) (err error) { |
|
||||||
w.mu.Lock() |
|
||||||
_, doesExist := w.fileExists[filePath] |
|
||||||
w.mu.Unlock() |
|
||||||
if !doesExist { |
|
||||||
// Send create event
|
|
||||||
select { |
|
||||||
case w.Events <- newCreateEvent(filePath): |
|
||||||
case <-w.done: |
|
||||||
return |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
// like watchDirectoryFiles (but without doing another ReadDir)
|
|
||||||
filePath, err = w.internalWatch(filePath, fileInfo) |
|
||||||
if err != nil { |
|
||||||
return err |
|
||||||
} |
|
||||||
|
|
||||||
w.mu.Lock() |
|
||||||
w.fileExists[filePath] = true |
|
||||||
w.mu.Unlock() |
|
||||||
|
|
||||||
return nil |
|
||||||
} |
|
||||||
|
|
||||||
func (w *Watcher) internalWatch(name string, fileInfo os.FileInfo) (string, error) { |
|
||||||
if fileInfo.IsDir() { |
|
||||||
// mimic Linux providing delete events for subdirectories
|
|
||||||
// but preserve the flags used if currently watching subdirectory
|
|
||||||
w.mu.Lock() |
|
||||||
flags := w.dirFlags[name] |
|
||||||
w.mu.Unlock() |
|
||||||
|
|
||||||
flags |= unix.NOTE_DELETE | unix.NOTE_RENAME |
|
||||||
return w.addWatch(name, flags) |
|
||||||
} |
|
||||||
|
|
||||||
// watch file to mimic Linux inotify
|
|
||||||
return w.addWatch(name, noteAllEvents) |
|
||||||
} |
|
||||||
|
|
||||||
// kqueue creates a new kernel event queue and returns a descriptor.
|
|
||||||
func kqueue() (kq int, err error) { |
|
||||||
kq, err = unix.Kqueue() |
|
||||||
if kq == -1 { |
|
||||||
return kq, err |
|
||||||
} |
|
||||||
return kq, nil |
|
||||||
} |
|
||||||
|
|
||||||
// register events with the queue
|
|
||||||
func register(kq int, fds []int, flags int, fflags uint32) error { |
|
||||||
changes := make([]unix.Kevent_t, len(fds)) |
|
||||||
|
|
||||||
for i, fd := range fds { |
|
||||||
// SetKevent converts int to the platform-specific types:
|
|
||||||
unix.SetKevent(&changes[i], fd, unix.EVFILT_VNODE, flags) |
|
||||||
changes[i].Fflags = fflags |
|
||||||
} |
|
||||||
|
|
||||||
// register the events
|
|
||||||
success, err := unix.Kevent(kq, changes, nil, nil) |
|
||||||
if success == -1 { |
|
||||||
return err |
|
||||||
} |
|
||||||
return nil |
|
||||||
} |
|
||||||
|
|
||||||
// read retrieves pending events, or waits until an event occurs.
|
|
||||||
// A timeout of nil blocks indefinitely, while 0 polls the queue.
|
|
||||||
func read(kq int, events []unix.Kevent_t, timeout *unix.Timespec) ([]unix.Kevent_t, error) { |
|
||||||
n, err := unix.Kevent(kq, nil, events, timeout) |
|
||||||
if err != nil { |
|
||||||
return nil, err |
|
||||||
} |
|
||||||
return events[0:n], nil |
|
||||||
} |
|
||||||
|
|
||||||
// durationToTimespec prepares a timeout value
|
|
||||||
func durationToTimespec(d time.Duration) unix.Timespec { |
|
||||||
return unix.NsecToTimespec(d.Nanoseconds()) |
|
||||||
} |
|
@ -0,0 +1,208 @@ |
|||||||
|
#!/usr/bin/env zsh |
||||||
|
[ "${ZSH_VERSION:-}" = "" ] && echo >&2 "Only works with zsh" && exit 1 |
||||||
|
setopt err_exit no_unset pipefail extended_glob |
||||||
|
|
||||||
|
# Simple script to update the godoc comments on all watchers. Probably took me |
||||||
|
# more time to write this than doing it manually, but ah well 🙃 |
||||||
|
|
||||||
|
watcher=$(<<EOF |
||||||
|
// Watcher watches a set of paths, delivering events on a channel. |
||||||
|
// |
||||||
|
// A watcher should not be copied (e.g. pass it by pointer, rather than by |
||||||
|
// value). |
||||||
|
// |
||||||
|
// # Linux notes |
||||||
|
// |
||||||
|
// When a file is removed a Remove event won't be emitted until all file |
||||||
|
// descriptors are closed, and deletes will always emit a Chmod. For example: |
||||||
|
// |
||||||
|
// fp := os.Open("file") |
||||||
|
// os.Remove("file") // Triggers Chmod |
||||||
|
// fp.Close() // Triggers Remove |
||||||
|
// |
||||||
|
// This is the event that inotify sends, so not much can be changed about this. |
||||||
|
// |
||||||
|
// The fs.inotify.max_user_watches sysctl variable specifies the upper limit |
||||||
|
// for the number of watches per user, and fs.inotify.max_user_instances |
||||||
|
// specifies the maximum number of inotify instances per user. Every Watcher you |
||||||
|
// create is an "instance", and every path you add is a "watch". |
||||||
|
// |
||||||
|
// These are also exposed in /proc as /proc/sys/fs/inotify/max_user_watches and |
||||||
|
// /proc/sys/fs/inotify/max_user_instances |
||||||
|
// |
||||||
|
// To increase them you can use sysctl or write the value to the /proc file: |
||||||
|
// |
||||||
|
// # Default values on Linux 5.18 |
||||||
|
// sysctl fs.inotify.max_user_watches=124983 |
||||||
|
// sysctl fs.inotify.max_user_instances=128 |
||||||
|
// |
||||||
|
// To make the changes persist on reboot edit /etc/sysctl.conf or |
||||||
|
// /usr/lib/sysctl.d/50-default.conf (details differ per Linux distro; check |
||||||
|
// your distro's documentation): |
||||||
|
// |
||||||
|
// fs.inotify.max_user_watches=124983 |
||||||
|
// fs.inotify.max_user_instances=128 |
||||||
|
// |
||||||
|
// Reaching the limit will result in a "no space left on device" or "too many open |
||||||
|
// files" error. |
||||||
|
// |
||||||
|
// # kqueue notes (macOS, BSD) |
||||||
|
// |
||||||
|
// kqueue requires opening a file descriptor for every file that's being watched; |
||||||
|
// so if you're watching a directory with five files then that's six file |
||||||
|
// descriptors. You will run in to your system's "max open files" limit faster on |
||||||
|
// these platforms. |
||||||
|
// |
||||||
|
// The sysctl variables kern.maxfiles and kern.maxfilesperproc can be used to |
||||||
|
// control the maximum number of open files, as well as /etc/login.conf on BSD |
||||||
|
// systems. |
||||||
|
// |
||||||
|
// # macOS notes |
||||||
|
// |
||||||
|
// Spotlight indexing on macOS can result in multiple events (see [#15]). A |
||||||
|
// temporary workaround is to add your folder(s) to the "Spotlight Privacy |
||||||
|
// Settings" until we have a native FSEvents implementation (see [#11]). |
||||||
|
// |
||||||
|
// [#11]: https://github.com/fsnotify/fsnotify/issues/11 |
||||||
|
// [#15]: https://github.com/fsnotify/fsnotify/issues/15 |
||||||
|
EOF |
||||||
|
) |
||||||
|
|
||||||
|
new=$(<<EOF |
||||||
|
// NewWatcher creates a new Watcher. |
||||||
|
EOF |
||||||
|
) |
||||||
|
|
||||||
|
add=$(<<EOF |
||||||
|
// Add starts monitoring the path for changes. |
||||||
|
// |
||||||
|
// A path can only be watched once; attempting to watch it more than once will |
||||||
|
// return an error. Paths that do not yet exist on the filesystem cannot be |
||||||
|
// added. A watch will be automatically removed if the path is deleted. |
||||||
|
// |
||||||
|
// A path will remain watched if it gets renamed to somewhere else on the same |
||||||
|
// filesystem, but the monitor will get removed if the path gets deleted and |
||||||
|
// re-created, or if it's moved to a different filesystem. |
||||||
|
// |
||||||
|
// Notifications on network filesystems (NFS, SMB, FUSE, etc.) or special |
||||||
|
// filesystems (/proc, /sys, etc.) generally don't work. |
||||||
|
// |
||||||
|
// # Watching directories |
||||||
|
// |
||||||
|
// All files in a directory are monitored, including new files that are created |
||||||
|
// after the watcher is started. Subdirectories are not watched (i.e. it's |
||||||
|
// non-recursive). |
||||||
|
// |
||||||
|
// # Watching files |
||||||
|
// |
||||||
|
// Watching individual files (rather than directories) is generally not |
||||||
|
// recommended as many tools update files atomically. Instead of "just" writing |
||||||
|
// to the file a temporary file will be written to first, and if successful the |
||||||
|
// temporary file is moved to to destination removing the original, or some |
||||||
|
// variant thereof. The watcher on the original file is now lost, as it no |
||||||
|
// longer exists. |
||||||
|
// |
||||||
|
// Instead, watch the parent directory and use Event.Name to filter out files |
||||||
|
// you're not interested in. There is an example of this in [cmd/fsnotify/file.go]. |
||||||
|
EOF |
||||||
|
) |
||||||
|
|
||||||
|
remove=$(<<EOF |
||||||
|
// Remove stops monitoring the path for changes. |
||||||
|
// |
||||||
|
// Directories are always removed non-recursively. For example, if you added |
||||||
|
// /tmp/dir and /tmp/dir/subdir then you will need to remove both. |
||||||
|
// |
||||||
|
// Removing a path that has not yet been added returns [ErrNonExistentWatch]. |
||||||
|
EOF |
||||||
|
) |
||||||
|
|
||||||
|
close=$(<<EOF |
||||||
|
// Close removes all watches and closes the events channel. |
||||||
|
EOF |
||||||
|
) |
||||||
|
|
||||||
|
watchlist=$(<<EOF |
||||||
|
// WatchList returns all paths added with [Add] (and are not yet removed). |
||||||
|
EOF |
||||||
|
) |
||||||
|
|
||||||
|
events=$(<<EOF |
||||||
|
// Events sends the filesystem change events. |
||||||
|
// |
||||||
|
// fsnotify can send the following events; a "path" here can refer to a |
||||||
|
// file, directory, symbolic link, or special file like a FIFO. |
||||||
|
// |
||||||
|
// fsnotify.Create A new path was created; this may be followed by one |
||||||
|
// or more Write events if data also gets written to a |
||||||
|
// file. |
||||||
|
// |
||||||
|
// fsnotify.Remove A path was removed. |
||||||
|
// |
||||||
|
// fsnotify.Rename A path was renamed. A rename is always sent with the |
||||||
|
// old path as Event.Name, and a Create event will be |
||||||
|
// sent with the new name. Renames are only sent for |
||||||
|
// paths that are currently watched; e.g. moving an |
||||||
|
// unmonitored file into a monitored directory will |
||||||
|
// show up as just a Create. Similarly, renaming a file |
||||||
|
// to outside a monitored directory will show up as |
||||||
|
// only a Rename. |
||||||
|
// |
||||||
|
// fsnotify.Write A file or named pipe was written to. A Truncate will |
||||||
|
// also trigger a Write. A single "write action" |
||||||
|
// initiated by the user may show up as one or multiple |
||||||
|
// writes, depending on when the system syncs things to |
||||||
|
// disk. For example when compiling a large Go program |
||||||
|
// you may get hundreds of Write events, so you |
||||||
|
// probably want to wait until you've stopped receiving |
||||||
|
// them (see the dedup example in cmd/fsnotify). |
||||||
|
// |
||||||
|
// fsnotify.Chmod Attributes were changed. On Linux this is also sent |
||||||
|
// when a file is removed (or more accurately, when a |
||||||
|
// link to an inode is removed). On kqueue it's sent |
||||||
|
// and on kqueue when a file is truncated. On Windows |
||||||
|
// it's never sent. |
||||||
|
EOF |
||||||
|
) |
||||||
|
|
||||||
|
errors=$(<<EOF |
||||||
|
// Errors sends any errors. |
||||||
|
EOF |
||||||
|
) |
||||||
|
|
||||||
|
set-cmt() { |
||||||
|
local pat=$1 |
||||||
|
local cmt=$2 |
||||||
|
|
||||||
|
IFS=$'\n' local files=($(grep -n $pat backend_*~*_test.go)) |
||||||
|
for f in $files; do |
||||||
|
IFS=':' local fields=($=f) |
||||||
|
local file=$fields[1] |
||||||
|
local end=$(( $fields[2] - 1 )) |
||||||
|
|
||||||
|
# Find start of comment. |
||||||
|
local start=0 |
||||||
|
IFS=$'\n' local lines=($(head -n$end $file)) |
||||||
|
for (( i = 1; i <= $#lines; i++ )); do |
||||||
|
local line=$lines[-$i] |
||||||
|
if ! grep -q '^[[:space:]]*//' <<<$line; then |
||||||
|
start=$(( end - (i - 2) )) |
||||||
|
break |
||||||
|
fi |
||||||
|
done |
||||||
|
|
||||||
|
head -n $(( start - 1 )) $file >/tmp/x |
||||||
|
print -r -- $cmt >>/tmp/x |
||||||
|
tail -n+$(( end + 1 )) $file >>/tmp/x |
||||||
|
mv /tmp/x $file |
||||||
|
done |
||||||
|
} |
||||||
|
|
||||||
|
set-cmt '^type Watcher struct ' $watcher |
||||||
|
set-cmt '^func NewWatcher(' $new |
||||||
|
set-cmt '^func (w \*Watcher) Add(' $add |
||||||
|
set-cmt '^func (w \*Watcher) Remove(' $remove |
||||||
|
set-cmt '^func (w \*Watcher) Close(' $close |
||||||
|
set-cmt '^func (w \*Watcher) WatchList(' $watchlist |
||||||
|
set-cmt '^[[:space:]]*Events *chan Event$' $events |
||||||
|
set-cmt '^[[:space:]]*Errors *chan error$' $errors |
@ -1,7 +1,3 @@ |
|||||||
// Copyright 2013 The Go Authors. All rights reserved.
|
|
||||||
// Use of this source code is governed by a BSD-style
|
|
||||||
// license that can be found in the LICENSE file.
|
|
||||||
|
|
||||||
//go:build freebsd || openbsd || netbsd || dragonfly
|
//go:build freebsd || openbsd || netbsd || dragonfly
|
||||||
// +build freebsd openbsd netbsd dragonfly
|
// +build freebsd openbsd netbsd dragonfly
|
||||||
|
|
@ -1,7 +1,3 @@ |
|||||||
// Copyright 2013 The Go Authors. All rights reserved.
|
|
||||||
// Use of this source code is governed by a BSD-style
|
|
||||||
// license that can be found in the LICENSE file.
|
|
||||||
|
|
||||||
//go:build darwin
|
//go:build darwin
|
||||||
// +build darwin
|
// +build darwin
|
||||||
|
|
@ -1,586 +0,0 @@ |
|||||||
// Copyright 2011 The Go Authors. All rights reserved.
|
|
||||||
// Use of this source code is governed by a BSD-style
|
|
||||||
// license that can be found in the LICENSE file.
|
|
||||||
|
|
||||||
//go:build windows
|
|
||||||
// +build windows
|
|
||||||
|
|
||||||
package fsnotify |
|
||||||
|
|
||||||
import ( |
|
||||||
"errors" |
|
||||||
"fmt" |
|
||||||
"os" |
|
||||||
"path/filepath" |
|
||||||
"reflect" |
|
||||||
"runtime" |
|
||||||
"sync" |
|
||||||
"syscall" |
|
||||||
"unsafe" |
|
||||||
) |
|
||||||
|
|
||||||
// Watcher watches a set of files, delivering events to a channel.
|
|
||||||
type Watcher struct { |
|
||||||
Events chan Event |
|
||||||
Errors chan error |
|
||||||
isClosed bool // Set to true when Close() is first called
|
|
||||||
mu sync.Mutex // Map access
|
|
||||||
port syscall.Handle // Handle to completion port
|
|
||||||
watches watchMap // Map of watches (key: i-number)
|
|
||||||
input chan *input // Inputs to the reader are sent on this channel
|
|
||||||
quit chan chan<- error |
|
||||||
} |
|
||||||
|
|
||||||
// NewWatcher establishes a new watcher with the underlying OS and begins waiting for events.
|
|
||||||
func NewWatcher() (*Watcher, error) { |
|
||||||
port, e := syscall.CreateIoCompletionPort(syscall.InvalidHandle, 0, 0, 0) |
|
||||||
if e != nil { |
|
||||||
return nil, os.NewSyscallError("CreateIoCompletionPort", e) |
|
||||||
} |
|
||||||
w := &Watcher{ |
|
||||||
port: port, |
|
||||||
watches: make(watchMap), |
|
||||||
input: make(chan *input, 1), |
|
||||||
Events: make(chan Event, 50), |
|
||||||
Errors: make(chan error), |
|
||||||
quit: make(chan chan<- error, 1), |
|
||||||
} |
|
||||||
go w.readEvents() |
|
||||||
return w, nil |
|
||||||
} |
|
||||||
|
|
||||||
// Close removes all watches and closes the events channel.
|
|
||||||
func (w *Watcher) Close() error { |
|
||||||
if w.isClosed { |
|
||||||
return nil |
|
||||||
} |
|
||||||
w.isClosed = true |
|
||||||
|
|
||||||
// Send "quit" message to the reader goroutine
|
|
||||||
ch := make(chan error) |
|
||||||
w.quit <- ch |
|
||||||
if err := w.wakeupReader(); err != nil { |
|
||||||
return err |
|
||||||
} |
|
||||||
return <-ch |
|
||||||
} |
|
||||||
|
|
||||||
// Add starts watching the named file or directory (non-recursively).
|
|
||||||
func (w *Watcher) Add(name string) error { |
|
||||||
if w.isClosed { |
|
||||||
return errors.New("watcher already closed") |
|
||||||
} |
|
||||||
in := &input{ |
|
||||||
op: opAddWatch, |
|
||||||
path: filepath.Clean(name), |
|
||||||
flags: sysFSALLEVENTS, |
|
||||||
reply: make(chan error), |
|
||||||
} |
|
||||||
w.input <- in |
|
||||||
if err := w.wakeupReader(); err != nil { |
|
||||||
return err |
|
||||||
} |
|
||||||
return <-in.reply |
|
||||||
} |
|
||||||
|
|
||||||
// Remove stops watching the the named file or directory (non-recursively).
|
|
||||||
func (w *Watcher) Remove(name string) error { |
|
||||||
in := &input{ |
|
||||||
op: opRemoveWatch, |
|
||||||
path: filepath.Clean(name), |
|
||||||
reply: make(chan error), |
|
||||||
} |
|
||||||
w.input <- in |
|
||||||
if err := w.wakeupReader(); err != nil { |
|
||||||
return err |
|
||||||
} |
|
||||||
return <-in.reply |
|
||||||
} |
|
||||||
|
|
||||||
// WatchList returns the directories and files that are being monitered.
|
|
||||||
func (w *Watcher) WatchList() []string { |
|
||||||
w.mu.Lock() |
|
||||||
defer w.mu.Unlock() |
|
||||||
|
|
||||||
entries := make([]string, 0, len(w.watches)) |
|
||||||
for _, entry := range w.watches { |
|
||||||
for _, watchEntry := range entry { |
|
||||||
entries = append(entries, watchEntry.path) |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
return entries |
|
||||||
} |
|
||||||
|
|
||||||
const ( |
|
||||||
// Options for AddWatch
|
|
||||||
sysFSONESHOT = 0x80000000 |
|
||||||
sysFSONLYDIR = 0x1000000 |
|
||||||
|
|
||||||
// Events
|
|
||||||
sysFSACCESS = 0x1 |
|
||||||
sysFSALLEVENTS = 0xfff |
|
||||||
sysFSATTRIB = 0x4 |
|
||||||
sysFSCLOSE = 0x18 |
|
||||||
sysFSCREATE = 0x100 |
|
||||||
sysFSDELETE = 0x200 |
|
||||||
sysFSDELETESELF = 0x400 |
|
||||||
sysFSMODIFY = 0x2 |
|
||||||
sysFSMOVE = 0xc0 |
|
||||||
sysFSMOVEDFROM = 0x40 |
|
||||||
sysFSMOVEDTO = 0x80 |
|
||||||
sysFSMOVESELF = 0x800 |
|
||||||
|
|
||||||
// Special events
|
|
||||||
sysFSIGNORED = 0x8000 |
|
||||||
sysFSQOVERFLOW = 0x4000 |
|
||||||
) |
|
||||||
|
|
||||||
func newEvent(name string, mask uint32) Event { |
|
||||||
e := Event{Name: name} |
|
||||||
if mask&sysFSCREATE == sysFSCREATE || mask&sysFSMOVEDTO == sysFSMOVEDTO { |
|
||||||
e.Op |= Create |
|
||||||
} |
|
||||||
if mask&sysFSDELETE == sysFSDELETE || mask&sysFSDELETESELF == sysFSDELETESELF { |
|
||||||
e.Op |= Remove |
|
||||||
} |
|
||||||
if mask&sysFSMODIFY == sysFSMODIFY { |
|
||||||
e.Op |= Write |
|
||||||
} |
|
||||||
if mask&sysFSMOVE == sysFSMOVE || mask&sysFSMOVESELF == sysFSMOVESELF || mask&sysFSMOVEDFROM == sysFSMOVEDFROM { |
|
||||||
e.Op |= Rename |
|
||||||
} |
|
||||||
if mask&sysFSATTRIB == sysFSATTRIB { |
|
||||||
e.Op |= Chmod |
|
||||||
} |
|
||||||
return e |
|
||||||
} |
|
||||||
|
|
||||||
const ( |
|
||||||
opAddWatch = iota |
|
||||||
opRemoveWatch |
|
||||||
) |
|
||||||
|
|
||||||
const ( |
|
||||||
provisional uint64 = 1 << (32 + iota) |
|
||||||
) |
|
||||||
|
|
||||||
type input struct { |
|
||||||
op int |
|
||||||
path string |
|
||||||
flags uint32 |
|
||||||
reply chan error |
|
||||||
} |
|
||||||
|
|
||||||
type inode struct { |
|
||||||
handle syscall.Handle |
|
||||||
volume uint32 |
|
||||||
index uint64 |
|
||||||
} |
|
||||||
|
|
||||||
type watch struct { |
|
||||||
ov syscall.Overlapped |
|
||||||
ino *inode // i-number
|
|
||||||
path string // Directory path
|
|
||||||
mask uint64 // Directory itself is being watched with these notify flags
|
|
||||||
names map[string]uint64 // Map of names being watched and their notify flags
|
|
||||||
rename string // Remembers the old name while renaming a file
|
|
||||||
buf [4096]byte |
|
||||||
} |
|
||||||
|
|
||||||
type indexMap map[uint64]*watch |
|
||||||
type watchMap map[uint32]indexMap |
|
||||||
|
|
||||||
func (w *Watcher) wakeupReader() error { |
|
||||||
e := syscall.PostQueuedCompletionStatus(w.port, 0, 0, nil) |
|
||||||
if e != nil { |
|
||||||
return os.NewSyscallError("PostQueuedCompletionStatus", e) |
|
||||||
} |
|
||||||
return nil |
|
||||||
} |
|
||||||
|
|
||||||
func getDir(pathname string) (dir string, err error) { |
|
||||||
attr, e := syscall.GetFileAttributes(syscall.StringToUTF16Ptr(pathname)) |
|
||||||
if e != nil { |
|
||||||
return "", os.NewSyscallError("GetFileAttributes", e) |
|
||||||
} |
|
||||||
if attr&syscall.FILE_ATTRIBUTE_DIRECTORY != 0 { |
|
||||||
dir = pathname |
|
||||||
} else { |
|
||||||
dir, _ = filepath.Split(pathname) |
|
||||||
dir = filepath.Clean(dir) |
|
||||||
} |
|
||||||
return |
|
||||||
} |
|
||||||
|
|
||||||
func getIno(path string) (ino *inode, err error) { |
|
||||||
h, e := syscall.CreateFile(syscall.StringToUTF16Ptr(path), |
|
||||||
syscall.FILE_LIST_DIRECTORY, |
|
||||||
syscall.FILE_SHARE_READ|syscall.FILE_SHARE_WRITE|syscall.FILE_SHARE_DELETE, |
|
||||||
nil, syscall.OPEN_EXISTING, |
|
||||||
syscall.FILE_FLAG_BACKUP_SEMANTICS|syscall.FILE_FLAG_OVERLAPPED, 0) |
|
||||||
if e != nil { |
|
||||||
return nil, os.NewSyscallError("CreateFile", e) |
|
||||||
} |
|
||||||
var fi syscall.ByHandleFileInformation |
|
||||||
if e = syscall.GetFileInformationByHandle(h, &fi); e != nil { |
|
||||||
syscall.CloseHandle(h) |
|
||||||
return nil, os.NewSyscallError("GetFileInformationByHandle", e) |
|
||||||
} |
|
||||||
ino = &inode{ |
|
||||||
handle: h, |
|
||||||
volume: fi.VolumeSerialNumber, |
|
||||||
index: uint64(fi.FileIndexHigh)<<32 | uint64(fi.FileIndexLow), |
|
||||||
} |
|
||||||
return ino, nil |
|
||||||
} |
|
||||||
|
|
||||||
// Must run within the I/O thread.
|
|
||||||
func (m watchMap) get(ino *inode) *watch { |
|
||||||
if i := m[ino.volume]; i != nil { |
|
||||||
return i[ino.index] |
|
||||||
} |
|
||||||
return nil |
|
||||||
} |
|
||||||
|
|
||||||
// Must run within the I/O thread.
|
|
||||||
func (m watchMap) set(ino *inode, watch *watch) { |
|
||||||
i := m[ino.volume] |
|
||||||
if i == nil { |
|
||||||
i = make(indexMap) |
|
||||||
m[ino.volume] = i |
|
||||||
} |
|
||||||
i[ino.index] = watch |
|
||||||
} |
|
||||||
|
|
||||||
// Must run within the I/O thread.
|
|
||||||
func (w *Watcher) addWatch(pathname string, flags uint64) error { |
|
||||||
dir, err := getDir(pathname) |
|
||||||
if err != nil { |
|
||||||
return err |
|
||||||
} |
|
||||||
if flags&sysFSONLYDIR != 0 && pathname != dir { |
|
||||||
return nil |
|
||||||
} |
|
||||||
ino, err := getIno(dir) |
|
||||||
if err != nil { |
|
||||||
return err |
|
||||||
} |
|
||||||
w.mu.Lock() |
|
||||||
watchEntry := w.watches.get(ino) |
|
||||||
w.mu.Unlock() |
|
||||||
if watchEntry == nil { |
|
||||||
if _, e := syscall.CreateIoCompletionPort(ino.handle, w.port, 0, 0); e != nil { |
|
||||||
syscall.CloseHandle(ino.handle) |
|
||||||
return os.NewSyscallError("CreateIoCompletionPort", e) |
|
||||||
} |
|
||||||
watchEntry = &watch{ |
|
||||||
ino: ino, |
|
||||||
path: dir, |
|
||||||
names: make(map[string]uint64), |
|
||||||
} |
|
||||||
w.mu.Lock() |
|
||||||
w.watches.set(ino, watchEntry) |
|
||||||
w.mu.Unlock() |
|
||||||
flags |= provisional |
|
||||||
} else { |
|
||||||
syscall.CloseHandle(ino.handle) |
|
||||||
} |
|
||||||
if pathname == dir { |
|
||||||
watchEntry.mask |= flags |
|
||||||
} else { |
|
||||||
watchEntry.names[filepath.Base(pathname)] |= flags |
|
||||||
} |
|
||||||
if err = w.startRead(watchEntry); err != nil { |
|
||||||
return err |
|
||||||
} |
|
||||||
if pathname == dir { |
|
||||||
watchEntry.mask &= ^provisional |
|
||||||
} else { |
|
||||||
watchEntry.names[filepath.Base(pathname)] &= ^provisional |
|
||||||
} |
|
||||||
return nil |
|
||||||
} |
|
||||||
|
|
||||||
// Must run within the I/O thread.
|
|
||||||
func (w *Watcher) remWatch(pathname string) error { |
|
||||||
dir, err := getDir(pathname) |
|
||||||
if err != nil { |
|
||||||
return err |
|
||||||
} |
|
||||||
ino, err := getIno(dir) |
|
||||||
if err != nil { |
|
||||||
return err |
|
||||||
} |
|
||||||
w.mu.Lock() |
|
||||||
watch := w.watches.get(ino) |
|
||||||
w.mu.Unlock() |
|
||||||
if watch == nil { |
|
||||||
return fmt.Errorf("can't remove non-existent watch for: %s", pathname) |
|
||||||
} |
|
||||||
if pathname == dir { |
|
||||||
w.sendEvent(watch.path, watch.mask&sysFSIGNORED) |
|
||||||
watch.mask = 0 |
|
||||||
} else { |
|
||||||
name := filepath.Base(pathname) |
|
||||||
w.sendEvent(filepath.Join(watch.path, name), watch.names[name]&sysFSIGNORED) |
|
||||||
delete(watch.names, name) |
|
||||||
} |
|
||||||
return w.startRead(watch) |
|
||||||
} |
|
||||||
|
|
||||||
// Must run within the I/O thread.
|
|
||||||
func (w *Watcher) deleteWatch(watch *watch) { |
|
||||||
for name, mask := range watch.names { |
|
||||||
if mask&provisional == 0 { |
|
||||||
w.sendEvent(filepath.Join(watch.path, name), mask&sysFSIGNORED) |
|
||||||
} |
|
||||||
delete(watch.names, name) |
|
||||||
} |
|
||||||
if watch.mask != 0 { |
|
||||||
if watch.mask&provisional == 0 { |
|
||||||
w.sendEvent(watch.path, watch.mask&sysFSIGNORED) |
|
||||||
} |
|
||||||
watch.mask = 0 |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
// Must run within the I/O thread.
|
|
||||||
func (w *Watcher) startRead(watch *watch) error { |
|
||||||
if e := syscall.CancelIo(watch.ino.handle); e != nil { |
|
||||||
w.Errors <- os.NewSyscallError("CancelIo", e) |
|
||||||
w.deleteWatch(watch) |
|
||||||
} |
|
||||||
mask := toWindowsFlags(watch.mask) |
|
||||||
for _, m := range watch.names { |
|
||||||
mask |= toWindowsFlags(m) |
|
||||||
} |
|
||||||
if mask == 0 { |
|
||||||
if e := syscall.CloseHandle(watch.ino.handle); e != nil { |
|
||||||
w.Errors <- os.NewSyscallError("CloseHandle", e) |
|
||||||
} |
|
||||||
w.mu.Lock() |
|
||||||
delete(w.watches[watch.ino.volume], watch.ino.index) |
|
||||||
w.mu.Unlock() |
|
||||||
return nil |
|
||||||
} |
|
||||||
e := syscall.ReadDirectoryChanges(watch.ino.handle, &watch.buf[0], |
|
||||||
uint32(unsafe.Sizeof(watch.buf)), false, mask, nil, &watch.ov, 0) |
|
||||||
if e != nil { |
|
||||||
err := os.NewSyscallError("ReadDirectoryChanges", e) |
|
||||||
if e == syscall.ERROR_ACCESS_DENIED && watch.mask&provisional == 0 { |
|
||||||
// Watched directory was probably removed
|
|
||||||
if w.sendEvent(watch.path, watch.mask&sysFSDELETESELF) { |
|
||||||
if watch.mask&sysFSONESHOT != 0 { |
|
||||||
watch.mask = 0 |
|
||||||
} |
|
||||||
} |
|
||||||
err = nil |
|
||||||
} |
|
||||||
w.deleteWatch(watch) |
|
||||||
w.startRead(watch) |
|
||||||
return err |
|
||||||
} |
|
||||||
return nil |
|
||||||
} |
|
||||||
|
|
||||||
// readEvents reads from the I/O completion port, converts the
|
|
||||||
// received events into Event objects and sends them via the Events channel.
|
|
||||||
// Entry point to the I/O thread.
|
|
||||||
func (w *Watcher) readEvents() { |
|
||||||
var ( |
|
||||||
n, key uint32 |
|
||||||
ov *syscall.Overlapped |
|
||||||
) |
|
||||||
runtime.LockOSThread() |
|
||||||
|
|
||||||
for { |
|
||||||
e := syscall.GetQueuedCompletionStatus(w.port, &n, &key, &ov, syscall.INFINITE) |
|
||||||
watch := (*watch)(unsafe.Pointer(ov)) |
|
||||||
|
|
||||||
if watch == nil { |
|
||||||
select { |
|
||||||
case ch := <-w.quit: |
|
||||||
w.mu.Lock() |
|
||||||
var indexes []indexMap |
|
||||||
for _, index := range w.watches { |
|
||||||
indexes = append(indexes, index) |
|
||||||
} |
|
||||||
w.mu.Unlock() |
|
||||||
for _, index := range indexes { |
|
||||||
for _, watch := range index { |
|
||||||
w.deleteWatch(watch) |
|
||||||
w.startRead(watch) |
|
||||||
} |
|
||||||
} |
|
||||||
var err error |
|
||||||
if e := syscall.CloseHandle(w.port); e != nil { |
|
||||||
err = os.NewSyscallError("CloseHandle", e) |
|
||||||
} |
|
||||||
close(w.Events) |
|
||||||
close(w.Errors) |
|
||||||
ch <- err |
|
||||||
return |
|
||||||
case in := <-w.input: |
|
||||||
switch in.op { |
|
||||||
case opAddWatch: |
|
||||||
in.reply <- w.addWatch(in.path, uint64(in.flags)) |
|
||||||
case opRemoveWatch: |
|
||||||
in.reply <- w.remWatch(in.path) |
|
||||||
} |
|
||||||
default: |
|
||||||
} |
|
||||||
continue |
|
||||||
} |
|
||||||
|
|
||||||
switch e { |
|
||||||
case syscall.ERROR_MORE_DATA: |
|
||||||
if watch == nil { |
|
||||||
w.Errors <- errors.New("ERROR_MORE_DATA has unexpectedly null lpOverlapped buffer") |
|
||||||
} else { |
|
||||||
// The i/o succeeded but the buffer is full.
|
|
||||||
// In theory we should be building up a full packet.
|
|
||||||
// In practice we can get away with just carrying on.
|
|
||||||
n = uint32(unsafe.Sizeof(watch.buf)) |
|
||||||
} |
|
||||||
case syscall.ERROR_ACCESS_DENIED: |
|
||||||
// Watched directory was probably removed
|
|
||||||
w.sendEvent(watch.path, watch.mask&sysFSDELETESELF) |
|
||||||
w.deleteWatch(watch) |
|
||||||
w.startRead(watch) |
|
||||||
continue |
|
||||||
case syscall.ERROR_OPERATION_ABORTED: |
|
||||||
// CancelIo was called on this handle
|
|
||||||
continue |
|
||||||
default: |
|
||||||
w.Errors <- os.NewSyscallError("GetQueuedCompletionPort", e) |
|
||||||
continue |
|
||||||
case nil: |
|
||||||
} |
|
||||||
|
|
||||||
var offset uint32 |
|
||||||
for { |
|
||||||
if n == 0 { |
|
||||||
w.Events <- newEvent("", sysFSQOVERFLOW) |
|
||||||
w.Errors <- errors.New("short read in readEvents()") |
|
||||||
break |
|
||||||
} |
|
||||||
|
|
||||||
// Point "raw" to the event in the buffer
|
|
||||||
raw := (*syscall.FileNotifyInformation)(unsafe.Pointer(&watch.buf[offset])) |
|
||||||
// TODO: Consider using unsafe.Slice that is available from go1.17
|
|
||||||
// https://stackoverflow.com/questions/51187973/how-to-create-an-array-or-a-slice-from-an-array-unsafe-pointer-in-golang
|
|
||||||
// instead of using a fixed syscall.MAX_PATH buf, we create a buf that is the size of the path name
|
|
||||||
size := int(raw.FileNameLength / 2) |
|
||||||
var buf []uint16 |
|
||||||
sh := (*reflect.SliceHeader)(unsafe.Pointer(&buf)) |
|
||||||
sh.Data = uintptr(unsafe.Pointer(&raw.FileName)) |
|
||||||
sh.Len = size |
|
||||||
sh.Cap = size |
|
||||||
name := syscall.UTF16ToString(buf) |
|
||||||
fullname := filepath.Join(watch.path, name) |
|
||||||
|
|
||||||
var mask uint64 |
|
||||||
switch raw.Action { |
|
||||||
case syscall.FILE_ACTION_REMOVED: |
|
||||||
mask = sysFSDELETESELF |
|
||||||
case syscall.FILE_ACTION_MODIFIED: |
|
||||||
mask = sysFSMODIFY |
|
||||||
case syscall.FILE_ACTION_RENAMED_OLD_NAME: |
|
||||||
watch.rename = name |
|
||||||
case syscall.FILE_ACTION_RENAMED_NEW_NAME: |
|
||||||
if watch.names[watch.rename] != 0 { |
|
||||||
watch.names[name] |= watch.names[watch.rename] |
|
||||||
delete(watch.names, watch.rename) |
|
||||||
mask = sysFSMOVESELF |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
sendNameEvent := func() { |
|
||||||
if w.sendEvent(fullname, watch.names[name]&mask) { |
|
||||||
if watch.names[name]&sysFSONESHOT != 0 { |
|
||||||
delete(watch.names, name) |
|
||||||
} |
|
||||||
} |
|
||||||
} |
|
||||||
if raw.Action != syscall.FILE_ACTION_RENAMED_NEW_NAME { |
|
||||||
sendNameEvent() |
|
||||||
} |
|
||||||
if raw.Action == syscall.FILE_ACTION_REMOVED { |
|
||||||
w.sendEvent(fullname, watch.names[name]&sysFSIGNORED) |
|
||||||
delete(watch.names, name) |
|
||||||
} |
|
||||||
if w.sendEvent(fullname, watch.mask&toFSnotifyFlags(raw.Action)) { |
|
||||||
if watch.mask&sysFSONESHOT != 0 { |
|
||||||
watch.mask = 0 |
|
||||||
} |
|
||||||
} |
|
||||||
if raw.Action == syscall.FILE_ACTION_RENAMED_NEW_NAME { |
|
||||||
fullname = filepath.Join(watch.path, watch.rename) |
|
||||||
sendNameEvent() |
|
||||||
} |
|
||||||
|
|
||||||
// Move to the next event in the buffer
|
|
||||||
if raw.NextEntryOffset == 0 { |
|
||||||
break |
|
||||||
} |
|
||||||
offset += raw.NextEntryOffset |
|
||||||
|
|
||||||
// Error!
|
|
||||||
if offset >= n { |
|
||||||
w.Errors <- errors.New("Windows system assumed buffer larger than it is, events have likely been missed.") |
|
||||||
break |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
if err := w.startRead(watch); err != nil { |
|
||||||
w.Errors <- err |
|
||||||
} |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
func (w *Watcher) sendEvent(name string, mask uint64) bool { |
|
||||||
if mask == 0 { |
|
||||||
return false |
|
||||||
} |
|
||||||
event := newEvent(name, uint32(mask)) |
|
||||||
select { |
|
||||||
case ch := <-w.quit: |
|
||||||
w.quit <- ch |
|
||||||
case w.Events <- event: |
|
||||||
} |
|
||||||
return true |
|
||||||
} |
|
||||||
|
|
||||||
func toWindowsFlags(mask uint64) uint32 { |
|
||||||
var m uint32 |
|
||||||
if mask&sysFSACCESS != 0 { |
|
||||||
m |= syscall.FILE_NOTIFY_CHANGE_LAST_ACCESS |
|
||||||
} |
|
||||||
if mask&sysFSMODIFY != 0 { |
|
||||||
m |= syscall.FILE_NOTIFY_CHANGE_LAST_WRITE |
|
||||||
} |
|
||||||
if mask&sysFSATTRIB != 0 { |
|
||||||
m |= syscall.FILE_NOTIFY_CHANGE_ATTRIBUTES |
|
||||||
} |
|
||||||
if mask&(sysFSMOVE|sysFSCREATE|sysFSDELETE) != 0 { |
|
||||||
m |= syscall.FILE_NOTIFY_CHANGE_FILE_NAME | syscall.FILE_NOTIFY_CHANGE_DIR_NAME |
|
||||||
} |
|
||||||
return m |
|
||||||
} |
|
||||||
|
|
||||||
func toFSnotifyFlags(action uint32) uint64 { |
|
||||||
switch action { |
|
||||||
case syscall.FILE_ACTION_ADDED: |
|
||||||
return sysFSCREATE |
|
||||||
case syscall.FILE_ACTION_REMOVED: |
|
||||||
return sysFSDELETE |
|
||||||
case syscall.FILE_ACTION_MODIFIED: |
|
||||||
return sysFSMODIFY |
|
||||||
case syscall.FILE_ACTION_RENAMED_OLD_NAME: |
|
||||||
return sysFSMOVEDFROM |
|
||||||
case syscall.FILE_ACTION_RENAMED_NEW_NAME: |
|
||||||
return sysFSMOVEDTO |
|
||||||
} |
|
||||||
return 0 |
|
||||||
} |
|
@ -1,13 +0,0 @@ |
|||||||
language: go |
|
||||||
|
|
||||||
go: |
|
||||||
- "1.15" |
|
||||||
- "1.16" |
|
||||||
- "1.17" |
|
||||||
|
|
||||||
before_install: |
|
||||||
- go get github.com/mattn/goveralls |
|
||||||
- go get golang.org/x/tools/cover |
|
||||||
|
|
||||||
script: |
|
||||||
- $HOME/gopath/bin/goveralls -service=travis-pro -package github.com/go-test/deep |
|
@ -1,179 +0,0 @@ |
|||||||
// Copyright 2016 The Go Authors. All rights reserved.
|
|
||||||
// Use of this source code is governed by a BSD-style
|
|
||||||
// license that can be found in the LICENSE file.
|
|
||||||
|
|
||||||
package ptypes |
|
||||||
|
|
||||||
import ( |
|
||||||
"fmt" |
|
||||||
"strings" |
|
||||||
|
|
||||||
"github.com/golang/protobuf/proto" |
|
||||||
"google.golang.org/protobuf/reflect/protoreflect" |
|
||||||
"google.golang.org/protobuf/reflect/protoregistry" |
|
||||||
|
|
||||||
anypb "github.com/golang/protobuf/ptypes/any" |
|
||||||
) |
|
||||||
|
|
||||||
const urlPrefix = "type.googleapis.com/" |
|
||||||
|
|
||||||
// AnyMessageName returns the message name contained in an anypb.Any message.
|
|
||||||
// Most type assertions should use the Is function instead.
|
|
||||||
//
|
|
||||||
// Deprecated: Call the any.MessageName method instead.
|
|
||||||
func AnyMessageName(any *anypb.Any) (string, error) { |
|
||||||
name, err := anyMessageName(any) |
|
||||||
return string(name), err |
|
||||||
} |
|
||||||
func anyMessageName(any *anypb.Any) (protoreflect.FullName, error) { |
|
||||||
if any == nil { |
|
||||||
return "", fmt.Errorf("message is nil") |
|
||||||
} |
|
||||||
name := protoreflect.FullName(any.TypeUrl) |
|
||||||
if i := strings.LastIndex(any.TypeUrl, "/"); i >= 0 { |
|
||||||
name = name[i+len("/"):] |
|
||||||
} |
|
||||||
if !name.IsValid() { |
|
||||||
return "", fmt.Errorf("message type url %q is invalid", any.TypeUrl) |
|
||||||
} |
|
||||||
return name, nil |
|
||||||
} |
|
||||||
|
|
||||||
// MarshalAny marshals the given message m into an anypb.Any message.
|
|
||||||
//
|
|
||||||
// Deprecated: Call the anypb.New function instead.
|
|
||||||
func MarshalAny(m proto.Message) (*anypb.Any, error) { |
|
||||||
switch dm := m.(type) { |
|
||||||
case DynamicAny: |
|
||||||
m = dm.Message |
|
||||||
case *DynamicAny: |
|
||||||
if dm == nil { |
|
||||||
return nil, proto.ErrNil |
|
||||||
} |
|
||||||
m = dm.Message |
|
||||||
} |
|
||||||
b, err := proto.Marshal(m) |
|
||||||
if err != nil { |
|
||||||
return nil, err |
|
||||||
} |
|
||||||
return &anypb.Any{TypeUrl: urlPrefix + proto.MessageName(m), Value: b}, nil |
|
||||||
} |
|
||||||
|
|
||||||
// Empty returns a new message of the type specified in an anypb.Any message.
|
|
||||||
// It returns protoregistry.NotFound if the corresponding message type could not
|
|
||||||
// be resolved in the global registry.
|
|
||||||
//
|
|
||||||
// Deprecated: Use protoregistry.GlobalTypes.FindMessageByName instead
|
|
||||||
// to resolve the message name and create a new instance of it.
|
|
||||||
func Empty(any *anypb.Any) (proto.Message, error) { |
|
||||||
name, err := anyMessageName(any) |
|
||||||
if err != nil { |
|
||||||
return nil, err |
|
||||||
} |
|
||||||
mt, err := protoregistry.GlobalTypes.FindMessageByName(name) |
|
||||||
if err != nil { |
|
||||||
return nil, err |
|
||||||
} |
|
||||||
return proto.MessageV1(mt.New().Interface()), nil |
|
||||||
} |
|
||||||
|
|
||||||
// UnmarshalAny unmarshals the encoded value contained in the anypb.Any message
|
|
||||||
// into the provided message m. It returns an error if the target message
|
|
||||||
// does not match the type in the Any message or if an unmarshal error occurs.
|
|
||||||
//
|
|
||||||
// The target message m may be a *DynamicAny message. If the underlying message
|
|
||||||
// type could not be resolved, then this returns protoregistry.NotFound.
|
|
||||||
//
|
|
||||||
// Deprecated: Call the any.UnmarshalTo method instead.
|
|
||||||
func UnmarshalAny(any *anypb.Any, m proto.Message) error { |
|
||||||
if dm, ok := m.(*DynamicAny); ok { |
|
||||||
if dm.Message == nil { |
|
||||||
var err error |
|
||||||
dm.Message, err = Empty(any) |
|
||||||
if err != nil { |
|
||||||
return err |
|
||||||
} |
|
||||||
} |
|
||||||
m = dm.Message |
|
||||||
} |
|
||||||
|
|
||||||
anyName, err := AnyMessageName(any) |
|
||||||
if err != nil { |
|
||||||
return err |
|
||||||
} |
|
||||||
msgName := proto.MessageName(m) |
|
||||||
if anyName != msgName { |
|
||||||
return fmt.Errorf("mismatched message type: got %q want %q", anyName, msgName) |
|
||||||
} |
|
||||||
return proto.Unmarshal(any.Value, m) |
|
||||||
} |
|
||||||
|
|
||||||
// Is reports whether the Any message contains a message of the specified type.
|
|
||||||
//
|
|
||||||
// Deprecated: Call the any.MessageIs method instead.
|
|
||||||
func Is(any *anypb.Any, m proto.Message) bool { |
|
||||||
if any == nil || m == nil { |
|
||||||
return false |
|
||||||
} |
|
||||||
name := proto.MessageName(m) |
|
||||||
if !strings.HasSuffix(any.TypeUrl, name) { |
|
||||||
return false |
|
||||||
} |
|
||||||
return len(any.TypeUrl) == len(name) || any.TypeUrl[len(any.TypeUrl)-len(name)-1] == '/' |
|
||||||
} |
|
||||||
|
|
||||||
// DynamicAny is a value that can be passed to UnmarshalAny to automatically
|
|
||||||
// allocate a proto.Message for the type specified in an anypb.Any message.
|
|
||||||
// The allocated message is stored in the embedded proto.Message.
|
|
||||||
//
|
|
||||||
// Example:
|
|
||||||
// var x ptypes.DynamicAny
|
|
||||||
// if err := ptypes.UnmarshalAny(a, &x); err != nil { ... }
|
|
||||||
// fmt.Printf("unmarshaled message: %v", x.Message)
|
|
||||||
//
|
|
||||||
// Deprecated: Use the any.UnmarshalNew method instead to unmarshal
|
|
||||||
// the any message contents into a new instance of the underlying message.
|
|
||||||
type DynamicAny struct{ proto.Message } |
|
||||||
|
|
||||||
func (m DynamicAny) String() string { |
|
||||||
if m.Message == nil { |
|
||||||
return "<nil>" |
|
||||||
} |
|
||||||
return m.Message.String() |
|
||||||
} |
|
||||||
func (m DynamicAny) Reset() { |
|
||||||
if m.Message == nil { |
|
||||||
return |
|
||||||
} |
|
||||||
m.Message.Reset() |
|
||||||
} |
|
||||||
func (m DynamicAny) ProtoMessage() { |
|
||||||
return |
|
||||||
} |
|
||||||
func (m DynamicAny) ProtoReflect() protoreflect.Message { |
|
||||||
if m.Message == nil { |
|
||||||
return nil |
|
||||||
} |
|
||||||
return dynamicAny{proto.MessageReflect(m.Message)} |
|
||||||
} |
|
||||||
|
|
||||||
type dynamicAny struct{ protoreflect.Message } |
|
||||||
|
|
||||||
func (m dynamicAny) Type() protoreflect.MessageType { |
|
||||||
return dynamicAnyType{m.Message.Type()} |
|
||||||
} |
|
||||||
func (m dynamicAny) New() protoreflect.Message { |
|
||||||
return dynamicAnyType{m.Message.Type()}.New() |
|
||||||
} |
|
||||||
func (m dynamicAny) Interface() protoreflect.ProtoMessage { |
|
||||||
return DynamicAny{proto.MessageV1(m.Message.Interface())} |
|
||||||
} |
|
||||||
|
|
||||||
type dynamicAnyType struct{ protoreflect.MessageType } |
|
||||||
|
|
||||||
func (t dynamicAnyType) New() protoreflect.Message { |
|
||||||
return dynamicAny{t.MessageType.New()} |
|
||||||
} |
|
||||||
func (t dynamicAnyType) Zero() protoreflect.Message { |
|
||||||
return dynamicAny{t.MessageType.Zero()} |
|
||||||
} |
|
@ -1,62 +0,0 @@ |
|||||||
// Code generated by protoc-gen-go. DO NOT EDIT.
|
|
||||||
// source: github.com/golang/protobuf/ptypes/any/any.proto
|
|
||||||
|
|
||||||
package any |
|
||||||
|
|
||||||
import ( |
|
||||||
protoreflect "google.golang.org/protobuf/reflect/protoreflect" |
|
||||||
protoimpl "google.golang.org/protobuf/runtime/protoimpl" |
|
||||||
anypb "google.golang.org/protobuf/types/known/anypb" |
|
||||||
reflect "reflect" |
|
||||||
) |
|
||||||
|
|
||||||
// Symbols defined in public import of google/protobuf/any.proto.
|
|
||||||
|
|
||||||
type Any = anypb.Any |
|
||||||
|
|
||||||
var File_github_com_golang_protobuf_ptypes_any_any_proto protoreflect.FileDescriptor |
|
||||||
|
|
||||||
var file_github_com_golang_protobuf_ptypes_any_any_proto_rawDesc = []byte{ |
|
||||||
0x0a, 0x2f, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x67, 0x6f, 0x6c, |
|
||||||
0x61, 0x6e, 0x67, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2f, 0x70, 0x74, 0x79, |
|
||||||
0x70, 0x65, 0x73, 0x2f, 0x61, 0x6e, 0x79, 0x2f, 0x61, 0x6e, 0x79, 0x2e, 0x70, 0x72, 0x6f, 0x74, |
|
||||||
0x6f, 0x1a, 0x19, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, |
|
||||||
0x75, 0x66, 0x2f, 0x61, 0x6e, 0x79, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x42, 0x2b, 0x5a, 0x29, |
|
||||||
0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x67, 0x6f, 0x6c, 0x61, 0x6e, |
|
||||||
0x67, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2f, 0x70, 0x74, 0x79, 0x70, 0x65, |
|
||||||
0x73, 0x2f, 0x61, 0x6e, 0x79, 0x3b, 0x61, 0x6e, 0x79, 0x50, 0x00, 0x62, 0x06, 0x70, 0x72, 0x6f, |
|
||||||
0x74, 0x6f, 0x33, |
|
||||||
} |
|
||||||
|
|
||||||
var file_github_com_golang_protobuf_ptypes_any_any_proto_goTypes = []interface{}{} |
|
||||||
var file_github_com_golang_protobuf_ptypes_any_any_proto_depIdxs = []int32{ |
|
||||||
0, // [0:0] is the sub-list for method output_type
|
|
||||||
0, // [0:0] is the sub-list for method input_type
|
|
||||||
0, // [0:0] is the sub-list for extension type_name
|
|
||||||
0, // [0:0] is the sub-list for extension extendee
|
|
||||||
0, // [0:0] is the sub-list for field type_name
|
|
||||||
} |
|
||||||
|
|
||||||
func init() { file_github_com_golang_protobuf_ptypes_any_any_proto_init() } |
|
||||||
func file_github_com_golang_protobuf_ptypes_any_any_proto_init() { |
|
||||||
if File_github_com_golang_protobuf_ptypes_any_any_proto != nil { |
|
||||||
return |
|
||||||
} |
|
||||||
type x struct{} |
|
||||||
out := protoimpl.TypeBuilder{ |
|
||||||
File: protoimpl.DescBuilder{ |
|
||||||
GoPackagePath: reflect.TypeOf(x{}).PkgPath(), |
|
||||||
RawDescriptor: file_github_com_golang_protobuf_ptypes_any_any_proto_rawDesc, |
|
||||||
NumEnums: 0, |
|
||||||
NumMessages: 0, |
|
||||||
NumExtensions: 0, |
|
||||||
NumServices: 0, |
|
||||||
}, |
|
||||||
GoTypes: file_github_com_golang_protobuf_ptypes_any_any_proto_goTypes, |
|
||||||
DependencyIndexes: file_github_com_golang_protobuf_ptypes_any_any_proto_depIdxs, |
|
||||||
}.Build() |
|
||||||
File_github_com_golang_protobuf_ptypes_any_any_proto = out.File |
|
||||||
file_github_com_golang_protobuf_ptypes_any_any_proto_rawDesc = nil |
|
||||||
file_github_com_golang_protobuf_ptypes_any_any_proto_goTypes = nil |
|
||||||
file_github_com_golang_protobuf_ptypes_any_any_proto_depIdxs = nil |
|
||||||
} |
|
@ -1,10 +0,0 @@ |
|||||||
// Copyright 2016 The Go Authors. All rights reserved.
|
|
||||||
// Use of this source code is governed by a BSD-style
|
|
||||||
// license that can be found in the LICENSE file.
|
|
||||||
|
|
||||||
// Package ptypes provides functionality for interacting with well-known types.
|
|
||||||
//
|
|
||||||
// Deprecated: Well-known types have specialized functionality directly
|
|
||||||
// injected into the generated packages for each message type.
|
|
||||||
// See the deprecation notice for each function for the suggested alternative.
|
|
||||||
package ptypes |
|
@ -1,76 +0,0 @@ |
|||||||
// Copyright 2016 The Go Authors. All rights reserved.
|
|
||||||
// Use of this source code is governed by a BSD-style
|
|
||||||
// license that can be found in the LICENSE file.
|
|
||||||
|
|
||||||
package ptypes |
|
||||||
|
|
||||||
import ( |
|
||||||
"errors" |
|
||||||
"fmt" |
|
||||||
"time" |
|
||||||
|
|
||||||
durationpb "github.com/golang/protobuf/ptypes/duration" |
|
||||||
) |
|
||||||
|
|
||||||
// Range of google.protobuf.Duration as specified in duration.proto.
|
|
||||||
// This is about 10,000 years in seconds.
|
|
||||||
const ( |
|
||||||
maxSeconds = int64(10000 * 365.25 * 24 * 60 * 60) |
|
||||||
minSeconds = -maxSeconds |
|
||||||
) |
|
||||||
|
|
||||||
// Duration converts a durationpb.Duration to a time.Duration.
|
|
||||||
// Duration returns an error if dur is invalid or overflows a time.Duration.
|
|
||||||
//
|
|
||||||
// Deprecated: Call the dur.AsDuration and dur.CheckValid methods instead.
|
|
||||||
func Duration(dur *durationpb.Duration) (time.Duration, error) { |
|
||||||
if err := validateDuration(dur); err != nil { |
|
||||||
return 0, err |
|
||||||
} |
|
||||||
d := time.Duration(dur.Seconds) * time.Second |
|
||||||
if int64(d/time.Second) != dur.Seconds { |
|
||||||
return 0, fmt.Errorf("duration: %v is out of range for time.Duration", dur) |
|
||||||
} |
|
||||||
if dur.Nanos != 0 { |
|
||||||
d += time.Duration(dur.Nanos) * time.Nanosecond |
|
||||||
if (d < 0) != (dur.Nanos < 0) { |
|
||||||
return 0, fmt.Errorf("duration: %v is out of range for time.Duration", dur) |
|
||||||
} |
|
||||||
} |
|
||||||
return d, nil |
|
||||||
} |
|
||||||
|
|
||||||
// DurationProto converts a time.Duration to a durationpb.Duration.
|
|
||||||
//
|
|
||||||
// Deprecated: Call the durationpb.New function instead.
|
|
||||||
func DurationProto(d time.Duration) *durationpb.Duration { |
|
||||||
nanos := d.Nanoseconds() |
|
||||||
secs := nanos / 1e9 |
|
||||||
nanos -= secs * 1e9 |
|
||||||
return &durationpb.Duration{ |
|
||||||
Seconds: int64(secs), |
|
||||||
Nanos: int32(nanos), |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
// validateDuration determines whether the durationpb.Duration is valid
|
|
||||||
// according to the definition in google/protobuf/duration.proto.
|
|
||||||
// A valid durpb.Duration may still be too large to fit into a time.Duration
|
|
||||||
// Note that the range of durationpb.Duration is about 10,000 years,
|
|
||||||
// while the range of time.Duration is about 290 years.
|
|
||||||
func validateDuration(dur *durationpb.Duration) error { |
|
||||||
if dur == nil { |
|
||||||
return errors.New("duration: nil Duration") |
|
||||||
} |
|
||||||
if dur.Seconds < minSeconds || dur.Seconds > maxSeconds { |
|
||||||
return fmt.Errorf("duration: %v: seconds out of range", dur) |
|
||||||
} |
|
||||||
if dur.Nanos <= -1e9 || dur.Nanos >= 1e9 { |
|
||||||
return fmt.Errorf("duration: %v: nanos out of range", dur) |
|
||||||
} |
|
||||||
// Seconds and Nanos must have the same sign, unless d.Nanos is zero.
|
|
||||||
if (dur.Seconds < 0 && dur.Nanos > 0) || (dur.Seconds > 0 && dur.Nanos < 0) { |
|
||||||
return fmt.Errorf("duration: %v: seconds and nanos have different signs", dur) |
|
||||||
} |
|
||||||
return nil |
|
||||||
} |
|
@ -1,63 +0,0 @@ |
|||||||
// Code generated by protoc-gen-go. DO NOT EDIT.
|
|
||||||
// source: github.com/golang/protobuf/ptypes/duration/duration.proto
|
|
||||||
|
|
||||||
package duration |
|
||||||
|
|
||||||
import ( |
|
||||||
protoreflect "google.golang.org/protobuf/reflect/protoreflect" |
|
||||||
protoimpl "google.golang.org/protobuf/runtime/protoimpl" |
|
||||||
durationpb "google.golang.org/protobuf/types/known/durationpb" |
|
||||||
reflect "reflect" |
|
||||||
) |
|
||||||
|
|
||||||
// Symbols defined in public import of google/protobuf/duration.proto.
|
|
||||||
|
|
||||||
type Duration = durationpb.Duration |
|
||||||
|
|
||||||
var File_github_com_golang_protobuf_ptypes_duration_duration_proto protoreflect.FileDescriptor |
|
||||||
|
|
||||||
var file_github_com_golang_protobuf_ptypes_duration_duration_proto_rawDesc = []byte{ |
|
||||||
0x0a, 0x39, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x67, 0x6f, 0x6c, |
|
||||||
0x61, 0x6e, 0x67, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2f, 0x70, 0x74, 0x79, |
|
||||||
0x70, 0x65, 0x73, 0x2f, 0x64, 0x75, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x2f, 0x64, 0x75, 0x72, |
|
||||||
0x61, 0x74, 0x69, 0x6f, 0x6e, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x1a, 0x1e, 0x67, 0x6f, 0x6f, |
|
||||||
0x67, 0x6c, 0x65, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2f, 0x64, 0x75, 0x72, |
|
||||||
0x61, 0x74, 0x69, 0x6f, 0x6e, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x42, 0x35, 0x5a, 0x33, 0x67, |
|
||||||
0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x67, 0x6f, 0x6c, 0x61, 0x6e, 0x67, |
|
||||||
0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2f, 0x70, 0x74, 0x79, 0x70, 0x65, 0x73, |
|
||||||
0x2f, 0x64, 0x75, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x3b, 0x64, 0x75, 0x72, 0x61, 0x74, 0x69, |
|
||||||
0x6f, 0x6e, 0x50, 0x00, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, |
|
||||||
} |
|
||||||
|
|
||||||
var file_github_com_golang_protobuf_ptypes_duration_duration_proto_goTypes = []interface{}{} |
|
||||||
var file_github_com_golang_protobuf_ptypes_duration_duration_proto_depIdxs = []int32{ |
|
||||||
0, // [0:0] is the sub-list for method output_type
|
|
||||||
0, // [0:0] is the sub-list for method input_type
|
|
||||||
0, // [0:0] is the sub-list for extension type_name
|
|
||||||
0, // [0:0] is the sub-list for extension extendee
|
|
||||||
0, // [0:0] is the sub-list for field type_name
|
|
||||||
} |
|
||||||
|
|
||||||
func init() { file_github_com_golang_protobuf_ptypes_duration_duration_proto_init() } |
|
||||||
func file_github_com_golang_protobuf_ptypes_duration_duration_proto_init() { |
|
||||||
if File_github_com_golang_protobuf_ptypes_duration_duration_proto != nil { |
|
||||||
return |
|
||||||
} |
|
||||||
type x struct{} |
|
||||||
out := protoimpl.TypeBuilder{ |
|
||||||
File: protoimpl.DescBuilder{ |
|
||||||
GoPackagePath: reflect.TypeOf(x{}).PkgPath(), |
|
||||||
RawDescriptor: file_github_com_golang_protobuf_ptypes_duration_duration_proto_rawDesc, |
|
||||||
NumEnums: 0, |
|
||||||
NumMessages: 0, |
|
||||||
NumExtensions: 0, |
|
||||||
NumServices: 0, |
|
||||||
}, |
|
||||||
GoTypes: file_github_com_golang_protobuf_ptypes_duration_duration_proto_goTypes, |
|
||||||
DependencyIndexes: file_github_com_golang_protobuf_ptypes_duration_duration_proto_depIdxs, |
|
||||||
}.Build() |
|
||||||
File_github_com_golang_protobuf_ptypes_duration_duration_proto = out.File |
|
||||||
file_github_com_golang_protobuf_ptypes_duration_duration_proto_rawDesc = nil |
|
||||||
file_github_com_golang_protobuf_ptypes_duration_duration_proto_goTypes = nil |
|
||||||
file_github_com_golang_protobuf_ptypes_duration_duration_proto_depIdxs = nil |
|
||||||
} |
|
@ -1,112 +0,0 @@ |
|||||||
// Copyright 2016 The Go Authors. All rights reserved.
|
|
||||||
// Use of this source code is governed by a BSD-style
|
|
||||||
// license that can be found in the LICENSE file.
|
|
||||||
|
|
||||||
package ptypes |
|
||||||
|
|
||||||
import ( |
|
||||||
"errors" |
|
||||||
"fmt" |
|
||||||
"time" |
|
||||||
|
|
||||||
timestamppb "github.com/golang/protobuf/ptypes/timestamp" |
|
||||||
) |
|
||||||
|
|
||||||
// Range of google.protobuf.Duration as specified in timestamp.proto.
|
|
||||||
const ( |
|
||||||
// Seconds field of the earliest valid Timestamp.
|
|
||||||
// This is time.Date(1, 1, 1, 0, 0, 0, 0, time.UTC).Unix().
|
|
||||||
minValidSeconds = -62135596800 |
|
||||||
// Seconds field just after the latest valid Timestamp.
|
|
||||||
// This is time.Date(10000, 1, 1, 0, 0, 0, 0, time.UTC).Unix().
|
|
||||||
maxValidSeconds = 253402300800 |
|
||||||
) |
|
||||||
|
|
||||||
// Timestamp converts a timestamppb.Timestamp to a time.Time.
|
|
||||||
// It returns an error if the argument is invalid.
|
|
||||||
//
|
|
||||||
// Unlike most Go functions, if Timestamp returns an error, the first return
|
|
||||||
// value is not the zero time.Time. Instead, it is the value obtained from the
|
|
||||||
// time.Unix function when passed the contents of the Timestamp, in the UTC
|
|
||||||
// locale. This may or may not be a meaningful time; many invalid Timestamps
|
|
||||||
// do map to valid time.Times.
|
|
||||||
//
|
|
||||||
// A nil Timestamp returns an error. The first return value in that case is
|
|
||||||
// undefined.
|
|
||||||
//
|
|
||||||
// Deprecated: Call the ts.AsTime and ts.CheckValid methods instead.
|
|
||||||
func Timestamp(ts *timestamppb.Timestamp) (time.Time, error) { |
|
||||||
// Don't return the zero value on error, because corresponds to a valid
|
|
||||||
// timestamp. Instead return whatever time.Unix gives us.
|
|
||||||
var t time.Time |
|
||||||
if ts == nil { |
|
||||||
t = time.Unix(0, 0).UTC() // treat nil like the empty Timestamp
|
|
||||||
} else { |
|
||||||
t = time.Unix(ts.Seconds, int64(ts.Nanos)).UTC() |
|
||||||
} |
|
||||||
return t, validateTimestamp(ts) |
|
||||||
} |
|
||||||
|
|
||||||
// TimestampNow returns a google.protobuf.Timestamp for the current time.
|
|
||||||
//
|
|
||||||
// Deprecated: Call the timestamppb.Now function instead.
|
|
||||||
func TimestampNow() *timestamppb.Timestamp { |
|
||||||
ts, err := TimestampProto(time.Now()) |
|
||||||
if err != nil { |
|
||||||
panic("ptypes: time.Now() out of Timestamp range") |
|
||||||
} |
|
||||||
return ts |
|
||||||
} |
|
||||||
|
|
||||||
// TimestampProto converts the time.Time to a google.protobuf.Timestamp proto.
|
|
||||||
// It returns an error if the resulting Timestamp is invalid.
|
|
||||||
//
|
|
||||||
// Deprecated: Call the timestamppb.New function instead.
|
|
||||||
func TimestampProto(t time.Time) (*timestamppb.Timestamp, error) { |
|
||||||
ts := ×tamppb.Timestamp{ |
|
||||||
Seconds: t.Unix(), |
|
||||||
Nanos: int32(t.Nanosecond()), |
|
||||||
} |
|
||||||
if err := validateTimestamp(ts); err != nil { |
|
||||||
return nil, err |
|
||||||
} |
|
||||||
return ts, nil |
|
||||||
} |
|
||||||
|
|
||||||
// TimestampString returns the RFC 3339 string for valid Timestamps.
|
|
||||||
// For invalid Timestamps, it returns an error message in parentheses.
|
|
||||||
//
|
|
||||||
// Deprecated: Call the ts.AsTime method instead,
|
|
||||||
// followed by a call to the Format method on the time.Time value.
|
|
||||||
func TimestampString(ts *timestamppb.Timestamp) string { |
|
||||||
t, err := Timestamp(ts) |
|
||||||
if err != nil { |
|
||||||
return fmt.Sprintf("(%v)", err) |
|
||||||
} |
|
||||||
return t.Format(time.RFC3339Nano) |
|
||||||
} |
|
||||||
|
|
||||||
// validateTimestamp determines whether a Timestamp is valid.
|
|
||||||
// A valid timestamp represents a time in the range [0001-01-01, 10000-01-01)
|
|
||||||
// and has a Nanos field in the range [0, 1e9).
|
|
||||||
//
|
|
||||||
// If the Timestamp is valid, validateTimestamp returns nil.
|
|
||||||
// Otherwise, it returns an error that describes the problem.
|
|
||||||
//
|
|
||||||
// Every valid Timestamp can be represented by a time.Time,
|
|
||||||
// but the converse is not true.
|
|
||||||
func validateTimestamp(ts *timestamppb.Timestamp) error { |
|
||||||
if ts == nil { |
|
||||||
return errors.New("timestamp: nil Timestamp") |
|
||||||
} |
|
||||||
if ts.Seconds < minValidSeconds { |
|
||||||
return fmt.Errorf("timestamp: %v before 0001-01-01", ts) |
|
||||||
} |
|
||||||
if ts.Seconds >= maxValidSeconds { |
|
||||||
return fmt.Errorf("timestamp: %v after 10000-01-01", ts) |
|
||||||
} |
|
||||||
if ts.Nanos < 0 || ts.Nanos >= 1e9 { |
|
||||||
return fmt.Errorf("timestamp: %v: nanos not in range [0, 1e9)", ts) |
|
||||||
} |
|
||||||
return nil |
|
||||||
} |
|
@ -1,48 +0,0 @@ |
|||||||
// Copyright 2017, The Go Authors. All rights reserved.
|
|
||||||
// Use of this source code is governed by a BSD-style
|
|
||||||
// license that can be found in the LICENSE file.
|
|
||||||
|
|
||||||
package value |
|
||||||
|
|
||||||
import ( |
|
||||||
"math" |
|
||||||
"reflect" |
|
||||||
) |
|
||||||
|
|
||||||
// IsZero reports whether v is the zero value.
|
|
||||||
// This does not rely on Interface and so can be used on unexported fields.
|
|
||||||
func IsZero(v reflect.Value) bool { |
|
||||||
switch v.Kind() { |
|
||||||
case reflect.Bool: |
|
||||||
return v.Bool() == false |
|
||||||
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: |
|
||||||
return v.Int() == 0 |
|
||||||
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr: |
|
||||||
return v.Uint() == 0 |
|
||||||
case reflect.Float32, reflect.Float64: |
|
||||||
return math.Float64bits(v.Float()) == 0 |
|
||||||
case reflect.Complex64, reflect.Complex128: |
|
||||||
return math.Float64bits(real(v.Complex())) == 0 && math.Float64bits(imag(v.Complex())) == 0 |
|
||||||
case reflect.String: |
|
||||||
return v.String() == "" |
|
||||||
case reflect.UnsafePointer: |
|
||||||
return v.Pointer() == 0 |
|
||||||
case reflect.Chan, reflect.Func, reflect.Interface, reflect.Ptr, reflect.Map, reflect.Slice: |
|
||||||
return v.IsNil() |
|
||||||
case reflect.Array: |
|
||||||
for i := 0; i < v.Len(); i++ { |
|
||||||
if !IsZero(v.Index(i)) { |
|
||||||
return false |
|
||||||
} |
|
||||||
} |
|
||||||
return true |
|
||||||
case reflect.Struct: |
|
||||||
for i := 0; i < v.NumField(); i++ { |
|
||||||
if !IsZero(v.Field(i)) { |
|
||||||
return false |
|
||||||
} |
|
||||||
} |
|
||||||
return true |
|
||||||
} |
|
||||||
return false |
|
||||||
} |
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in new issue