1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269
|
// 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 solaris
package unix
import (
"fmt"
"os"
"runtime"
"testing"
)
func (e *EventPort) checkInternals(t *testing.T, fds, paths, cookies, pending int) {
t.Helper()
p, err := e.Pending()
if err != nil {
t.Fatalf("failed to query how many events are pending")
}
if len(e.fds) != fds || len(e.paths) != paths || len(e.cookies) != cookies || p != pending {
format := "| fds: %d | paths: %d | cookies: %d | pending: %d |"
expected := fmt.Sprintf(format, fds, paths, cookies, pending)
got := fmt.Sprintf(format, len(e.fds), len(e.paths), len(e.cookies), p)
t.Errorf("Internal state mismatch\nfound: %s\nexpected: %s", got, expected)
}
}
// getOneRetry wraps EventPort.GetOne which in turn wraps a syscall that can be
// interrupted causing us to receive EINTR.
// To prevent our tests from flaking, we retry the syscall until it works
// rather than get unexpected results in our tests.
func getOneRetry(t *testing.T, p *EventPort, timeout *Timespec) (e *PortEvent, err error) {
t.Helper()
for {
e, err = p.GetOne(timeout)
if err != EINTR {
break
}
}
return e, err
}
// getRetry wraps EventPort.Get which in turn wraps a syscall that can be
// interrupted causing us to receive EINTR.
// To prevent our tests from flaking, we retry the syscall until it works
// rather than get unexpected results in our tests.
func getRetry(t *testing.T, p *EventPort, s []PortEvent, min int, timeout *Timespec) (n int, err error) {
t.Helper()
for {
n, err = p.Get(s, min, timeout)
if err != EINTR {
break
}
// If we did get EINTR, make sure we got 0 events
if n != 0 {
t.Fatalf("EventPort.Get returned events on EINTR.\ngot: %d\nexpected: 0", n)
}
}
return n, err
}
// Regression test for DissociatePath returning ENOENT
// This test is intended to create a linear worst
// case scenario of events being associated and
// fired but not consumed before additional
// calls to dissociate and associate happen
// This needs to be an internal test so that
// we can validate the state of the private maps
func TestEventPortDissociateAlreadyGone(t *testing.T) {
port, err := NewEventPort()
if err != nil {
t.Fatalf("failed to create an EventPort")
}
defer port.Close()
dir := t.TempDir()
tmpfile, err := os.CreateTemp(dir, "eventport")
if err != nil {
t.Fatalf("unable to create a tempfile: %v", err)
}
path := tmpfile.Name()
stat, err := os.Stat(path)
if err != nil {
t.Fatalf("unexpected failure to Stat file: %v", err)
}
err = port.AssociatePath(path, stat, FILE_MODIFIED, "cookie1")
if err != nil {
t.Fatalf("unexpected failure associating file: %v", err)
}
// We should have 1 path registered and 1 cookie in the jar
port.checkInternals(t, 0, 1, 1, 0)
// The path is associated, let's delete it.
err = os.Remove(path)
if err != nil {
t.Fatalf("unexpected failure deleting file: %v", err)
}
// The file has been deleted, some sort of pending event is probably
// queued in the kernel waiting for us to get it AND the kernel is
// no longer watching for events on it. BUT... Because we haven't
// consumed the event, this API thinks it's still watched:
watched := port.PathIsWatched(path)
if !watched {
t.Errorf("unexpected result from PathIsWatched")
}
// Ok, let's dissociate the file even before reading the event.
// Oh, ENOENT. I guess it's not associated any more
err = port.DissociatePath(path)
if err != ENOENT {
t.Errorf("unexpected result dissociating a seemingly associated path we know isn't: %v", err)
}
// As established by the return value above, this should clearly be false now:
// Narrator voice: in the first version of this API, it wasn't.
watched = port.PathIsWatched(path)
if watched {
t.Errorf("definitively unwatched file still in the map")
}
// We should have nothing registered, but 1 pending event corresponding
// to the cookie in the jar
port.checkInternals(t, 0, 0, 1, 1)
f, err := os.OpenFile(path, os.O_WRONLY|os.O_CREATE, 0666)
if err != nil {
t.Fatalf("creating test file failed: %s", err)
}
err = f.Close()
if err != nil {
t.Fatalf("unexpected failure closing file: %v", err)
}
stat, err = os.Stat(path)
if err != nil {
t.Fatalf("unexpected failure to Stat file: %v", err)
}
c := "cookie2" // c is for cookie, that's good enough for me
err = port.AssociatePath(path, stat, FILE_MODIFIED, c)
if err != nil {
t.Errorf("unexpected failure associating file: %v", err)
}
// We should have 1 registered path and its cookie
// as well as a second cookie corresponding to the pending event
port.checkInternals(t, 0, 1, 2, 1)
// Fire another event
err = os.Remove(path)
if err != nil {
t.Fatalf("unexpected failure deleting file: %v", err)
}
port.checkInternals(t, 0, 1, 2, 2)
err = port.DissociatePath(path)
if err != ENOENT {
t.Errorf("unexpected result dissociating a seemingly associated path we know isn't: %v", err)
}
// By dissociating this path after deletion we ensure that the paths map is now empty
// If we're not careful we could trigger a nil pointer exception
port.checkInternals(t, 0, 0, 2, 2)
f, err = os.OpenFile(path, os.O_WRONLY|os.O_CREATE, 0666)
if err != nil {
t.Fatalf("creating test file failed: %s", err)
}
err = f.Close()
if err != nil {
t.Fatalf("unexpected failure closing file: %v", err)
}
stat, err = os.Stat(path)
if err != nil {
t.Fatalf("unexpected failure to Stat file: %v", err)
}
// Put a seemingly duplicate cookie in the jar to see if we can trigger an incorrect removal from the paths map
err = port.AssociatePath(path, stat, FILE_MODIFIED, c)
if err != nil {
t.Errorf("unexpected failure associating file: %v", err)
}
port.checkInternals(t, 0, 1, 3, 2)
// run the garbage collector so that if we messed up it should be painfully clear
runtime.GC()
// Before the fix, this would cause a nil pointer exception
e, err := getOneRetry(t, port, nil)
if err != nil {
t.Errorf("failed to get an event: %v", err)
}
port.checkInternals(t, 0, 1, 2, 1)
if e.Cookie != "cookie1" {
t.Errorf(`expected "cookie1", got "%v"`, e.Cookie)
}
// Make sure that a cookie of the same value doesn't cause removal from the paths map incorrectly
e, err = getOneRetry(t, port, nil)
if err != nil {
t.Errorf("failed to get an event: %v", err)
}
port.checkInternals(t, 0, 1, 1, 0)
if e.Cookie != "cookie2" {
t.Errorf(`expected "cookie2", got "%v"`, e.Cookie)
}
err = os.Remove(path)
if err != nil {
t.Fatalf("unexpected failure deleting file: %v", err)
}
// Event has fired, but until processed it should still be in the map
port.checkInternals(t, 0, 1, 1, 1)
e, err = getOneRetry(t, port, nil)
if err != nil {
t.Errorf("failed to get an event: %v", err)
}
if e.Cookie != "cookie2" {
t.Errorf(`expected "cookie2", got "%v"`, e.Cookie)
}
// The maps should be empty and there should be no pending events
port.checkInternals(t, 0, 0, 0, 0)
}
// Regression test for spuriously triggering a panic about memory mismanagement
// that can be triggered by an event processing thread trying to process an event
// after a different thread has already called port.Close().
// Implemented as an internal test so that we can just simulate the Close()
// because if you call close first in the same thread, things work properly
// anyway.
func TestEventPortGetAfterClose(t *testing.T) {
port, err := NewEventPort()
if err != nil {
t.Fatalf("NewEventPort failed: %v", err)
}
// Create, associate, and delete 2 files
for i := 0; i < 2; i++ {
tmpfile, err := os.CreateTemp("", "eventport")
if err != nil {
t.Fatalf("unable to create tempfile: %v", err)
}
path := tmpfile.Name()
stat, err := os.Stat(path)
if err != nil {
t.Fatalf("unable to stat tempfile: %v", err)
}
err = port.AssociatePath(path, stat, FILE_MODIFIED, nil)
if err != nil {
t.Fatalf("unable to AssociatePath tempfile: %v", err)
}
err = os.Remove(path)
if err != nil {
t.Fatalf("unable to Remove tempfile: %v", err)
}
}
n, err := port.Pending()
if err != nil {
t.Errorf("Pending failed: %v", err)
}
if n != 2 {
t.Errorf("expected 2 pending events, got %d", n)
}
// Simulate a close from a different thread
port.fds = nil
port.paths = nil
port.cookies = nil
// Ensure that we get back reasonable errors rather than panic
_, err = getOneRetry(t, port, nil)
if err == nil || err.Error() != "this EventPort is already closed" {
t.Errorf("didn't receive expected error of 'this EventPort is already closed'; got: %v", err)
}
events := make([]PortEvent, 2)
n, err = getRetry(t, port, events, 1, nil)
if n != 0 {
t.Errorf("expected to get back 0 events, got %d", n)
}
if err == nil || err.Error() != "this EventPort is already closed" {
t.Errorf("didn't receive expected error of 'this EventPort is already closed'; got: %v", err)
}
}
|