github.com/google/fleetspeak@v0.1.15-0.20240426164851-4f31f62c1aea/fleetspeak/src/client/socketservice/socketservice_test.go (about)

     1  // Copyright 2017 Google Inc.
     2  //
     3  // Licensed under the Apache License, Version 2.0 (the "License");
     4  // you may not use this file except in compliance with the License.
     5  // You may obtain a copy of the License at
     6  //
     7  //     https://www.apache.org/licenses/LICENSE-2.0
     8  //
     9  // Unless required by applicable law or agreed to in writing, software
    10  // distributed under the License is distributed on an "AS IS" BASIS,
    11  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12  // See the License for the specific language governing permissions and
    13  // limitations under the License.
    14  
    15  package socketservice
    16  
    17  import (
    18  	"context"
    19  	"fmt"
    20  	"os"
    21  	"os/exec"
    22  	"path/filepath"
    23  	"runtime"
    24  	"strings"
    25  	"sync/atomic"
    26  	"testing"
    27  	"time"
    28  
    29  	log "github.com/golang/glog"
    30  	"google.golang.org/protobuf/proto"
    31  
    32  	"github.com/google/fleetspeak/fleetspeak/src/client/clitesting"
    33  	"github.com/google/fleetspeak/fleetspeak/src/client/stats"
    34  	"github.com/google/fleetspeak/fleetspeak/src/common/anypbtest"
    35  	"github.com/google/fleetspeak/fleetspeak/src/comtesting"
    36  
    37  	sspb "github.com/google/fleetspeak/fleetspeak/src/client/socketservice/proto/fleetspeak_socketservice"
    38  	fspb "github.com/google/fleetspeak/fleetspeak/src/common/proto/fleetspeak"
    39  	mpb "github.com/google/fleetspeak/fleetspeak/src/common/proto/fleetspeak_monitoring"
    40  )
    41  
    42  type testStatsCollector struct {
    43  	stats.NoopCollector
    44  	socketsClosed atomic.Int64
    45  }
    46  
    47  func (sc *testStatsCollector) SocketServiceSocketClosed(_ string, _ error) {
    48  	sc.socketsClosed.Add(1)
    49  }
    50  
    51  func SetUpSocketPath(t *testing.T, nameHint string) string {
    52  	t.Helper()
    53  
    54  	return filepath.Join(SetUpTempDir(t), nameHint)
    55  }
    56  
    57  func SetUpTempDir(t *testing.T) string {
    58  	t.Helper()
    59  
    60  	if runtime.GOOS == "windows" {
    61  		tmpDir, cleanUpFn := comtesting.GetTempDir("socketservice")
    62  		t.Cleanup(cleanUpFn)
    63  		return tmpDir
    64  	}
    65  
    66  	// We need unix socket paths to be short (see socketservice_unix.go), so we ignore the $TMPDIR env variable,
    67  	// which could point to arbitrary directories, and use /tmp.
    68  	tmpDir, err := os.MkdirTemp("/tmp", "socketservice_")
    69  	if err != nil {
    70  		t.Fatalf("os.MkdirTemp: %v", err)
    71  	}
    72  	t.Cleanup(func() { os.RemoveAll(tmpDir) })
    73  
    74  	// Explicitly set the group owner for tempDir in case it was inherited from /tmp.
    75  	if err := os.Chown(tmpDir, os.Getuid(), os.Getgid()); err != nil {
    76  		t.Fatalf("os.Chown(%q): %v", tmpDir, err)
    77  	}
    78  	return tmpDir
    79  }
    80  
    81  func isErrKilled(err error) bool {
    82  	if runtime.GOOS == "windows" {
    83  		return strings.HasSuffix(err.Error(), "exit status 1")
    84  	}
    85  	return strings.HasSuffix(err.Error(), "signal: killed")
    86  }
    87  
    88  // exerciseLoopback attempts to send messages through a testclient in loopback mode using
    89  // socketPath.
    90  func exerciseLoopback(t *testing.T, socketPath string) {
    91  	t.Helper()
    92  
    93  	s, err := Factory(&fspb.ClientServiceConfig{
    94  		Name: "TestSocketService",
    95  		Config: anypbtest.New(t, &sspb.Config{
    96  			ApiProxyPath: socketPath,
    97  		}),
    98  	})
    99  	if err != nil {
   100  		t.Fatalf("Factory(...): %v", err)
   101  	}
   102  
   103  	stats := &testStatsCollector{}
   104  	sc := clitesting.MockServiceContext{
   105  		OutChan:        make(chan *fspb.Message, 5),
   106  		StatsCollector: stats,
   107  	}
   108  	if err := s.Start(&sc); err != nil {
   109  		t.Fatalf("socketservice.Start(...): %v", err)
   110  	}
   111  	defer func() {
   112  		// We are shutting down this service. Give the service a bit of time to ack
   113  		// the messages to the testclient, so that they won't be repeated.
   114  		time.Sleep(time.Second)
   115  		if err := s.Stop(); err != nil {
   116  			t.Errorf("Error stopping service: %v", err)
   117  		}
   118  	}()
   119  
   120  	msgs := []*fspb.Message{
   121  		{
   122  			MessageId:   []byte("\000\000\000"),
   123  			MessageType: "RequestTypeA",
   124  		},
   125  		{
   126  			MessageId:   []byte("\000\000\001"),
   127  			MessageType: "RequestTypeB",
   128  		},
   129  	}
   130  	ctx := context.Background()
   131  	go func() {
   132  		for _, m := range msgs {
   133  			log.Infof("Processing message [%x]", m.MessageId)
   134  			err := s.ProcessMessage(ctx, m)
   135  			if err != nil {
   136  				t.Errorf("Error processing message: %v", err)
   137  			}
   138  			log.Info("Message Processed.")
   139  		}
   140  		log.Info("Done processing messages.")
   141  	}()
   142  	for _, m := range msgs {
   143  		g := <-sc.OutChan
   144  		got := proto.Clone(g).(*fspb.Message)
   145  		got.SourceMessageId = nil
   146  
   147  		want := proto.Clone(m).(*fspb.Message)
   148  		want.MessageType = want.MessageType + "Response"
   149  
   150  		if !proto.Equal(got, want) {
   151  			t.Errorf("Unexpected message from loopback: got [%v], want [%v]", got, want)
   152  		}
   153  	}
   154  
   155  	socketsClosed := stats.socketsClosed.Load()
   156  	if want := int64(0); socketsClosed != want {
   157  		t.Errorf("Got %d sockets closed, want %d", socketsClosed, want)
   158  	}
   159  	log.Infof("looped %d messages", len(msgs))
   160  }
   161  
   162  func TestLoopback(t *testing.T) {
   163  	socketPath := SetUpSocketPath(t, "Loopback")
   164  	cmd := exec.Command(testClient(t), "--mode=loopback", "--socket_path="+socketPath)
   165  	if err := cmd.Start(); err != nil {
   166  		t.Fatalf("cmd.Start() returned error: %v", err)
   167  	}
   168  	defer func() {
   169  		if err := cmd.Process.Kill(); err != nil {
   170  			t.Errorf("failed to kill testclient[%d]: %v", cmd.Process.Pid, err)
   171  		}
   172  		if err := cmd.Wait(); err != nil {
   173  			if !isErrKilled(err) {
   174  				t.Errorf("error waiting for testclient: %v", err)
   175  			}
   176  		}
   177  	}()
   178  	// Each call to exerciseLoopback closes and recreates the socket,
   179  	// simulating a fs client restart. The testclient in loopback mode should
   180  	// be able to handle this.
   181  	exerciseLoopback(t, socketPath)
   182  	exerciseLoopback(t, socketPath)
   183  	exerciseLoopback(t, socketPath)
   184  }
   185  
   186  func TestAckLoopback(t *testing.T) {
   187  	socketPath := SetUpSocketPath(t, "AckLoopback")
   188  	cmd := exec.Command(testClient(t), "--mode=ackLoopback", "--socket_path="+socketPath)
   189  	if err := cmd.Start(); err != nil {
   190  		t.Fatalf("cmd.Start() returned error: %v", err)
   191  	}
   192  	defer func() {
   193  		if err := cmd.Process.Kill(); err != nil {
   194  			t.Errorf("failed to kill testclient[%d]: %v", cmd.Process.Pid, err)
   195  		}
   196  		if err := cmd.Wait(); err != nil {
   197  			if !isErrKilled(err) {
   198  				t.Errorf("error waiting for testclient: %v", err)
   199  			}
   200  		}
   201  	}()
   202  	exerciseLoopback(t, socketPath)
   203  }
   204  
   205  func TestStutteringLoopback(t *testing.T) {
   206  	socketPath := SetUpSocketPath(t, "StutteringLoopback")
   207  	cmd := exec.Command(testClient(t), "--mode=stutteringLoopback", "--socket_path="+socketPath)
   208  	if err := cmd.Start(); err != nil {
   209  		t.Fatalf("cmd.Start() returned error: %v", err)
   210  	}
   211  	defer func() {
   212  		if err := cmd.Process.Kill(); err != nil {
   213  			t.Errorf("failed to kill testclient[%d]: %v", cmd.Process.Pid, err)
   214  		}
   215  		if err := cmd.Wait(); err != nil {
   216  			if !isErrKilled(err) {
   217  				t.Errorf("error waiting for testclient: %v", err)
   218  			}
   219  		}
   220  	}()
   221  
   222  	s, err := Factory(&fspb.ClientServiceConfig{
   223  		Name: "TestSocketService",
   224  		Config: anypbtest.New(t, &sspb.Config{
   225  			ApiProxyPath: socketPath,
   226  		}),
   227  	})
   228  	if err != nil {
   229  		t.Fatalf("Factory(...): %v", err)
   230  	}
   231  
   232  	starts := make(chan struct{}, 1)
   233  	s.(*Service).newChan = func() {
   234  		log.Infof("starting new channel")
   235  		starts <- struct{}{}
   236  	}
   237  
   238  	stats := &testStatsCollector{}
   239  	sc := clitesting.MockServiceContext{
   240  		OutChan:        make(chan *fspb.Message),
   241  		StatsCollector: stats,
   242  	}
   243  	if err := s.Start(&sc); err != nil {
   244  		t.Fatalf("socketservice.Start(...): %v", err)
   245  	}
   246  	ctx := context.Background()
   247  
   248  	// Stuttering loopback will close the connection and reconnect after
   249  	// every message.
   250  	for i := range 5 {
   251  		// Wait for our end of the connection to start/restart.
   252  		<-starts
   253  		err := s.ProcessMessage(ctx, &fspb.Message{
   254  			MessageId:   []byte{0, 0, byte(i)},
   255  			MessageType: "RequestType",
   256  		})
   257  		if err != nil {
   258  			t.Errorf("Error processing message: %v", err)
   259  		}
   260  		// Wait for the looped message. After sending the message, the other end
   261  		// will close and restart.
   262  		m := <-sc.OutChan
   263  		log.Infof("received looped back message: %x", m.MessageId)
   264  	}
   265  	if err := s.Stop(); err != nil {
   266  		t.Errorf("Error stopping service: %v", err)
   267  	}
   268  	socketsClosed := stats.socketsClosed.Load()
   269  	if want := int64(5); socketsClosed != want {
   270  		t.Errorf("Got %d sockets closed, want %d", socketsClosed, want)
   271  	}
   272  }
   273  
   274  func TestMaxSockLenUnix(t *testing.T) {
   275  	var maxSockLen int
   276  	if runtime.GOOS == "windows" {
   277  		t.Skip("Not applicable to windows.")
   278  	} else if runtime.GOOS == "darwin" {
   279  		maxSockLen = 103
   280  	} else {
   281  		maxSockLen = 107
   282  	}
   283  	sockPath := "/tmp/This is a really really long path that definitely will not fit in the sockaddr_un struct on most unix platforms"
   284  	want := fmt.Errorf("socket path is too long (120 chars) - max allowed length is %d: %s.tmp", maxSockLen, sockPath)
   285  	_, got := listen(sockPath)
   286  	if got.Error() != want.Error() {
   287  		t.Errorf("Wrong error received. Want '%s'; got '%s'", want, got)
   288  	}
   289  }
   290  
   291  func TestResourceMonitoring(t *testing.T) {
   292  	socketPath := SetUpSocketPath(t, "Loopback")
   293  	cmd := exec.Command(testClient(t), "--mode=loopback", "--socket_path="+socketPath)
   294  	if err := cmd.Start(); err != nil {
   295  		t.Fatalf("cmd.Start() returned error: %v", err)
   296  	}
   297  	defer func() {
   298  		if err := cmd.Process.Kill(); err != nil {
   299  			t.Errorf("failed to kill testclient[%d]: %v", cmd.Process.Pid, err)
   300  		}
   301  		if err := cmd.Wait(); err != nil {
   302  			if !isErrKilled(err) {
   303  				t.Errorf("error waiting for testclient: %v", err)
   304  			}
   305  		}
   306  	}()
   307  
   308  	s, err := Factory(&fspb.ClientServiceConfig{
   309  		Name: "TestSocketService",
   310  		Config: anypbtest.New(t, &sspb.Config{
   311  			ApiProxyPath:                          socketPath,
   312  			ResourceMonitoringSampleSize:          2,
   313  			ResourceMonitoringSamplePeriodSeconds: 1,
   314  		}),
   315  	})
   316  	if err != nil {
   317  		t.Fatalf("Factory(...): %v", err)
   318  	}
   319  
   320  	sc := clitesting.MockServiceContext{
   321  		OutChan: make(chan *fspb.Message, 5),
   322  	}
   323  	if err := s.Start(&sc); err != nil {
   324  		t.Fatalf("socketservice.Start(...): %v", err)
   325  	}
   326  	defer func() {
   327  		if err := s.Stop(); err != nil {
   328  			t.Errorf("Error stopping service: %v", err)
   329  		}
   330  	}()
   331  
   332  	to := time.After(10 * time.Second)
   333  	select {
   334  	case <-to:
   335  		t.Errorf("resource usage report not received")
   336  		return
   337  	case m := <-sc.OutChan:
   338  		if m.MessageType != "ResourceUsage" {
   339  			t.Errorf("expected ResourceUsage, got %+v", m)
   340  		}
   341  		rud := &mpb.ResourceUsageData{}
   342  		if err := m.Data.UnmarshalTo(rud); err != nil {
   343  			t.Fatalf("Unable to unmarshal ResourceUsageData: %v", err)
   344  		}
   345  		if rud.Pid != int64(cmd.Process.Pid) {
   346  			t.Errorf("ResourceUsageData.Pid=%d, but test client has pid %d", rud.Pid, cmd.Process.Pid)
   347  		}
   348  		if rud.Version != "0.5" {
   349  			t.Errorf("ResourceUsageData.Version=\"%s\" but expected \"0.5\"", rud.Version)
   350  		}
   351  	}
   352  }
   353  
   354  func TestStopDoesNotBlock(t *testing.T) {
   355  	socketPath := SetUpSocketPath(t, "Loopback")
   356  	stopTimeout := 10 * time.Second
   357  	s, err := Factory(&fspb.ClientServiceConfig{
   358  		Name: "TestSocketService",
   359  		Config: anypbtest.New(t, &sspb.Config{
   360  			ApiProxyPath: socketPath,
   361  		}),
   362  	})
   363  	if err != nil {
   364  		t.Fatalf("Factory(...): %v", err)
   365  	}
   366  
   367  	sc := clitesting.MockServiceContext{
   368  		OutChan: make(chan *fspb.Message, 5),
   369  	}
   370  	if err := s.Start(&sc); err != nil {
   371  		t.Fatalf("socketservice.Start(...): %v", err)
   372  	}
   373  
   374  	timer := time.AfterFunc(stopTimeout, func() {
   375  		panic(fmt.Sprintf("Socket service failed to stop after %v.", stopTimeout))
   376  	})
   377  	defer timer.Stop()
   378  
   379  	// Make sure Fleetspeak does not block waiting for the (non-existent)
   380  	// process on the other side of the socket to connect.
   381  	err = s.Stop()
   382  
   383  	if err != nil {
   384  		t.Errorf("socketservice.Stop(): %v", err)
   385  	}
   386  }
   387  
   388  func init() {
   389  	// Hack so that socketservice logs get written to the right place.
   390  	if d := os.Getenv("TEST_UNDECLARED_OUTPUTS_DIR"); d != "" {
   391  		os.Setenv("GOOGLE_LOG_DIR", d)
   392  	}
   393  }