...

Source file src/github.com/Microsoft/hcsshim/internal/jobobject/jobobject_test.go

Documentation: github.com/Microsoft/hcsshim/internal/jobobject

     1  //go:build windows
     2  
     3  package jobobject
     4  
     5  import (
     6  	"context"
     7  	"errors"
     8  	"os"
     9  	"os/exec"
    10  	"path/filepath"
    11  	"syscall"
    12  	"testing"
    13  	"time"
    14  
    15  	"golang.org/x/sys/windows"
    16  )
    17  
    18  func TestJobNilOptions(t *testing.T) {
    19  	_, err := Create(context.Background(), nil)
    20  	if err != nil {
    21  		t.Fatal(err)
    22  	}
    23  }
    24  
    25  func TestJobCreateAndOpen(t *testing.T) {
    26  	var (
    27  		ctx     = context.Background()
    28  		options = &Options{Name: "test"}
    29  	)
    30  	jobCreate, err := Create(ctx, options)
    31  	if err != nil {
    32  		t.Fatal(err)
    33  	}
    34  	defer jobCreate.Close()
    35  
    36  	jobOpen, err := Open(ctx, options)
    37  	if err != nil {
    38  		t.Fatal(err)
    39  	}
    40  	defer jobOpen.Close()
    41  }
    42  
    43  func TestSiloCreateAndOpen(t *testing.T) {
    44  	var (
    45  		ctx     = context.Background()
    46  		options = &Options{
    47  			Name: "test",
    48  			Silo: true,
    49  		}
    50  	)
    51  	jobCreate, err := Create(ctx, options)
    52  	if err != nil {
    53  		t.Fatal(err)
    54  	}
    55  	defer jobCreate.Close()
    56  
    57  	jobOpen, err := Open(ctx, options)
    58  	if err != nil {
    59  		t.Fatal(err)
    60  	}
    61  	defer jobOpen.Close()
    62  
    63  	if !jobOpen.isSilo() {
    64  		t.Fatal("job is supposed to be a silo")
    65  	}
    66  }
    67  
    68  func TestJobStats(t *testing.T) {
    69  	var (
    70  		ctx     = context.Background()
    71  		options = &Options{
    72  			Name:             "test",
    73  			EnableIOTracking: true,
    74  		}
    75  	)
    76  	job, err := Create(ctx, options)
    77  	if err != nil {
    78  		t.Fatal(err)
    79  	}
    80  	defer job.Close()
    81  
    82  	_, err = createProcsAndAssign(1, job)
    83  	if err != nil {
    84  		t.Fatal(err)
    85  	}
    86  
    87  	_, err = job.QueryMemoryStats()
    88  	if err != nil {
    89  		t.Fatal(err)
    90  	}
    91  
    92  	_, err = job.QueryProcessorStats()
    93  	if err != nil {
    94  		t.Fatal(err)
    95  	}
    96  
    97  	_, err = job.QueryStorageStats()
    98  	if err != nil {
    99  		t.Fatal(err)
   100  	}
   101  
   102  	if err := job.Terminate(1); err != nil {
   103  		t.Fatal(err)
   104  	}
   105  }
   106  
   107  func TestIOTracking(t *testing.T) {
   108  	var (
   109  		ctx     = context.Background()
   110  		options = &Options{
   111  			Name: "test",
   112  		}
   113  	)
   114  	job, err := Create(ctx, options)
   115  	if err != nil {
   116  		t.Fatal(err)
   117  	}
   118  	defer job.Close()
   119  
   120  	_, err = createProcsAndAssign(1, job)
   121  	if err != nil {
   122  		t.Fatal(err)
   123  	}
   124  
   125  	_, err = job.QueryStorageStats()
   126  	// Element not found is returned if IO tracking isn't enabled.
   127  	if err != nil && !errors.Is(err, windows.ERROR_NOT_FOUND) {
   128  		t.Fatal(err)
   129  	}
   130  
   131  	// Turn it on and now the call should function.
   132  	if err := job.SetIOTracking(); err != nil {
   133  		t.Fatal(err)
   134  	}
   135  
   136  	_, err = job.QueryStorageStats()
   137  	if err != nil {
   138  		t.Fatal(err)
   139  	}
   140  
   141  	if err := job.Terminate(1); err != nil {
   142  		t.Fatal(err)
   143  	}
   144  }
   145  
   146  func createProcsAndAssign(num int, job *JobObject) (_ []*exec.Cmd, err error) {
   147  	var procs []*exec.Cmd
   148  
   149  	defer func() {
   150  		if err != nil {
   151  			for _, proc := range procs {
   152  				_ = proc.Process.Kill()
   153  			}
   154  		}
   155  	}()
   156  
   157  	for i := 0; i < num; i++ {
   158  		cmd := exec.Command("ping", "-t", "127.0.0.1")
   159  		cmd.SysProcAttr = &syscall.SysProcAttr{
   160  			CreationFlags: windows.CREATE_NEW_PROCESS_GROUP,
   161  		}
   162  
   163  		if err := cmd.Start(); err != nil {
   164  			return nil, err
   165  		}
   166  
   167  		if err := job.Assign(uint32(cmd.Process.Pid)); err != nil {
   168  			return nil, err
   169  		}
   170  		procs = append(procs, cmd)
   171  	}
   172  	return procs, nil
   173  }
   174  
   175  func TestSetTerminateOnLastHandleClose(t *testing.T) {
   176  	job, err := Create(context.Background(), nil)
   177  	if err != nil {
   178  		t.Fatal(err)
   179  	}
   180  	defer job.Close()
   181  
   182  	if err := job.SetTerminateOnLastHandleClose(); err != nil {
   183  		t.Fatal(err)
   184  	}
   185  
   186  	procs, err := createProcsAndAssign(1, job)
   187  	if err != nil {
   188  		t.Fatal(err)
   189  	}
   190  
   191  	errCh := make(chan error)
   192  	go func() {
   193  		if err := job.Close(); err != nil {
   194  			errCh <- err
   195  		}
   196  		if err := procs[0].Wait(); err != nil {
   197  			errCh <- err
   198  		}
   199  		// Check if process is still alive after job handle close (it should not be).
   200  		// If wait returned it should be gone but just to be explicit check anyways.
   201  		if !procs[0].ProcessState.Exited() {
   202  			errCh <- errors.New("process should have exited after closing job handle")
   203  		}
   204  		close(errCh)
   205  	}()
   206  
   207  	select {
   208  	case err := <-errCh:
   209  		if err != nil {
   210  			t.Fatal(err)
   211  		}
   212  	case <-time.After(time.Second * 10):
   213  		_ = procs[0].Process.Kill()
   214  		t.Fatal("process didn't complete wait within timeout")
   215  	}
   216  }
   217  
   218  func TestSetMultipleExtendedLimits(t *testing.T) {
   219  	// Tests setting two different properties on the job that modify
   220  	// JOBOBJECT_EXTENDED_LIMIT_INFORMATION
   221  	job, err := Create(context.Background(), nil)
   222  	if err != nil {
   223  		t.Fatal(err)
   224  	}
   225  	defer job.Close()
   226  
   227  	// No reason for this limit in particular. Could be any value.
   228  	memLimitInMB := uint64(10 * 1024 * 1204)
   229  	if err := job.SetMemoryLimit(memLimitInMB); err != nil {
   230  		t.Fatal(err)
   231  	}
   232  
   233  	if err := job.SetTerminateOnLastHandleClose(); err != nil {
   234  		t.Fatal(err)
   235  	}
   236  
   237  	eli, err := job.getExtendedInformation()
   238  	if err != nil {
   239  		t.Fatal(err)
   240  	}
   241  
   242  	if !isFlagSet(windows.JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE, eli.BasicLimitInformation.LimitFlags) {
   243  		t.Fatal("the job does not have cpu rate control enabled")
   244  	}
   245  
   246  	if !isFlagSet(windows.JOB_OBJECT_LIMIT_JOB_MEMORY, eli.BasicLimitInformation.LimitFlags) {
   247  		t.Fatal("the job does not have cpu rate control enabled")
   248  	}
   249  
   250  	if eli.JobMemoryLimit != uintptr(memLimitInMB) {
   251  		t.Fatal("job memory limit not persisted")
   252  	}
   253  }
   254  
   255  func TestNoMoreProcessesMessageKill(t *testing.T) {
   256  	// Test that we receive the no more processes in job message after killing all of
   257  	// the processes in the job.
   258  	options := &Options{
   259  		Notifications: true,
   260  	}
   261  	job, err := Create(context.Background(), options)
   262  	if err != nil {
   263  		t.Fatal(err)
   264  	}
   265  	defer job.Close()
   266  
   267  	if err := job.SetTerminateOnLastHandleClose(); err != nil {
   268  		t.Fatal(err)
   269  	}
   270  
   271  	procs, err := createProcsAndAssign(2, job)
   272  	if err != nil {
   273  		t.Fatal(err)
   274  	}
   275  
   276  	errCh := make(chan error)
   277  	go func() {
   278  		for _, proc := range procs {
   279  			if err := proc.Process.Kill(); err != nil {
   280  				errCh <- err
   281  			}
   282  		}
   283  
   284  		for {
   285  			notif, err := job.PollNotification()
   286  			if err != nil {
   287  				errCh <- err
   288  			}
   289  
   290  			switch notif.(type) {
   291  			case MsgAllProcessesExited:
   292  				close(errCh)
   293  				return
   294  			case MsgUnimplemented:
   295  			default:
   296  			}
   297  		}
   298  	}()
   299  
   300  	select {
   301  	case err := <-errCh:
   302  		if err != nil {
   303  			t.Fatal(err)
   304  		}
   305  	case <-time.After(time.Second * 10):
   306  		t.Fatal("didn't receive no more processes message within timeout")
   307  	}
   308  }
   309  
   310  func TestNoMoreProcessesMessageTerminate(t *testing.T) {
   311  	// Test that we receive the no more processes in job message after terminating the
   312  	// job (terminates every process in the job).
   313  	options := &Options{
   314  		Notifications: true,
   315  	}
   316  	job, err := Create(context.Background(), options)
   317  	if err != nil {
   318  		t.Fatal(err)
   319  	}
   320  	defer job.Close()
   321  
   322  	if err := job.SetTerminateOnLastHandleClose(); err != nil {
   323  		t.Fatal(err)
   324  	}
   325  
   326  	_, err = createProcsAndAssign(2, job)
   327  	if err != nil {
   328  		t.Fatal(err)
   329  	}
   330  
   331  	errCh := make(chan error)
   332  	go func() {
   333  		if err := job.Terminate(1); err != nil {
   334  			errCh <- err
   335  		}
   336  
   337  		for {
   338  			notif, err := job.PollNotification()
   339  			if err != nil {
   340  				errCh <- err
   341  			}
   342  
   343  			switch notif.(type) {
   344  			case MsgAllProcessesExited:
   345  				close(errCh)
   346  				return
   347  			case MsgUnimplemented:
   348  			default:
   349  			}
   350  		}
   351  	}()
   352  
   353  	select {
   354  	case err := <-errCh:
   355  		if err != nil {
   356  			t.Fatal(err)
   357  		}
   358  	case <-time.After(time.Second * 10):
   359  		t.Fatal("didn't receive no more processes message within timeout")
   360  	}
   361  }
   362  
   363  func TestVerifyPidCount(t *testing.T) {
   364  	// This test verifies that job.Pids() returns the right info and works with > 1
   365  	// process.
   366  	job, err := Create(context.Background(), nil)
   367  	if err != nil {
   368  		t.Fatal(err)
   369  	}
   370  	defer job.Close()
   371  
   372  	numProcs := 2
   373  	_, err = createProcsAndAssign(numProcs, job)
   374  	if err != nil {
   375  		t.Fatal(err)
   376  	}
   377  
   378  	pids, err := job.Pids()
   379  	if err != nil {
   380  		t.Fatal(err)
   381  	}
   382  
   383  	if len(pids) != numProcs {
   384  		t.Fatalf("expected %d processes in the job, got: %d", numProcs, len(pids))
   385  	}
   386  
   387  	if err := job.Terminate(1); err != nil {
   388  		t.Fatal(err)
   389  	}
   390  }
   391  
   392  func TestSilo(t *testing.T) {
   393  	// Test asking for a silo in the options.
   394  	options := &Options{
   395  		Silo: true,
   396  	}
   397  	job, err := Create(context.Background(), options)
   398  	if err != nil {
   399  		t.Fatal(err)
   400  	}
   401  	defer job.Close()
   402  }
   403  
   404  func TestSiloFileBinding(t *testing.T) {
   405  	// Can't use osversion as the binary needs to be manifested for it to work.
   406  	// Just stat for the bindflt dll.
   407  	if _, err := os.Stat(`C:\windows\system32\bindfltapi.dll`); err != nil {
   408  		t.Skip("Bindflt not present on RS5 or lower, skipping.")
   409  	}
   410  	// Test upgrading to a silo and binding a file only the silo can see.
   411  	options := &Options{
   412  		Silo: true,
   413  	}
   414  	job, err := Create(context.Background(), options)
   415  	if err != nil {
   416  		t.Fatal(err)
   417  	}
   418  	defer job.Close()
   419  
   420  	target := t.TempDir()
   421  	hostPath := filepath.Join(target, "bind-test.txt")
   422  	f, err := os.Create(hostPath)
   423  	if err != nil {
   424  		t.Fatal(err)
   425  	}
   426  	defer f.Close()
   427  
   428  	root := t.TempDir()
   429  	siloPath := filepath.Join(root, "silo-path.txt")
   430  	if err := job.ApplyFileBinding(siloPath, hostPath, false); err != nil {
   431  		t.Fatal(err)
   432  	}
   433  
   434  	// First check that we can't see the file on the host.
   435  	if _, err := os.Stat(siloPath); err == nil {
   436  		t.Fatalf("expected to not be able to see %q on the host", siloPath)
   437  	}
   438  
   439  	// Now check that we can see it in the silo. Couple second timeout (ping something) so
   440  	// we can be relatively sure the process has been assigned to the job before we go to check
   441  	// on the file. Unfortunately we can't use our internal/exec package that has support for
   442  	// assigning a process to a job at creation time as it causes a cyclical import.
   443  	cmd := exec.Command("cmd", "/c", "ping", "localhost", "&&", "dir", siloPath)
   444  	if err := cmd.Start(); err != nil {
   445  		t.Fatal(err)
   446  	}
   447  
   448  	if err := job.Assign(uint32(cmd.Process.Pid)); err != nil {
   449  		t.Fatal(err)
   450  	}
   451  
   452  	// Process will have an exit code of 1 if dir couldn't find the file; if we get
   453  	// no error here we should be A-OK.
   454  	if err := cmd.Wait(); err != nil {
   455  		t.Fatal(err)
   456  	}
   457  }
   458  

View as plain text