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