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