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  }