Basic RPC server wiring for dut_service.proto

BUG=None
TEST=go test

Change-Id: Ideea3e5ac6251519042976765562142ac7b400c7
Reviewed-on: https://chromium-review.googlesource.com/c/chromiumos/platform/dev-util/+/2900977
Tested-by: C Shapiro <shapiroc@chromium.org>
Commit-Queue: C Shapiro <shapiroc@chromium.org>
Reviewed-by: Seewai Fu <seewaifu@google.com>
diff --git a/test/src/chromiumos/dutservice/cmd/dutserver/dutserver.go b/test/src/chromiumos/dutservice/cmd/dutserver/dutserver.go
new file mode 100644
index 0000000..d57b2a7
--- /dev/null
+++ b/test/src/chromiumos/dutservice/cmd/dutserver/dutserver.go
@@ -0,0 +1,86 @@
+// Copyright 2021 The Chromium OS Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+// Implements dut_service.proto (see proto for details)
+package main
+
+import (
+	"context"
+	"log"
+	"net"
+
+	"chromiumos/lro"
+
+	"go.chromium.org/chromiumos/config/go/longrunning"
+	"go.chromium.org/chromiumos/config/go/test/api"
+	"google.golang.org/grpc"
+)
+
+// DutServer implementation of dut_service.proto
+type DutServer struct {
+	Manager *lro.Manager
+	logger  *log.Logger
+}
+
+// newDutServer creates a new dut service server to listen to rpc requests.
+func newDutServer(l net.Listener, logger *log.Logger) (*grpc.Server, error) {
+	s := &DutServer{
+		Manager: lro.New(),
+		logger:  logger,
+	}
+	defer s.Manager.Close()
+	server := grpc.NewServer()
+	api.RegisterDutServiceServer(server, s)
+	longrunning.RegisterOperationsServer(server, s.Manager)
+	logger.Println("dutservice listen to request at ", l.Addr().String())
+	return server, nil
+}
+
+// ProvisionDut installs a specified version of Chrome OS on the DUT, along
+// with any specified DLCs.
+//
+// If the DUT is already on the specified version of Chrome OS, the OS will
+// not be provisioned.
+//
+// If the DUT already has the specified list of DLCs, only the missing DLCs
+// will be provisioned.
+func (s *DutServer) ProvisionDut(ctx context.Context, req *api.ProvisionDutRequest) (*longrunning.Operation, error) {
+	s.logger.Println("Received api.ProvisionDutRequest: ", *req)
+	op := s.Manager.NewOperation()
+	s.Manager.SetResult(op.Name, &api.ProvisionDutResponse{})
+	return op, nil
+}
+
+// ProvisionLacros installs a specified version of Lacros on the DUT.
+//
+// If the DUT already has the specified version of Lacros, Lacros will not be
+// provisioned.
+func (s *DutServer) ProvisionLacros(ctx context.Context, req *api.ProvisionLacrosRequest) (*longrunning.Operation, error) {
+	s.logger.Println("Received api.ProvisionLacrosRequest: ", *req)
+	op := s.Manager.NewOperation()
+	s.Manager.SetResult(op.Name, &api.ProvisionLacrosResponse{})
+	return op, nil
+}
+
+// ProvisionAsh installs a specified version of ash-chrome on the DUT.
+//
+// This directly overwrites the version of ash-chrome on the current root
+// disk partition.
+func (s *DutServer) ProvisionAsh(ctx context.Context, req *api.ProvisionAshRequest) (*longrunning.Operation, error) {
+	s.logger.Println("Received api.ProvisionAshRequest: ", *req)
+	op := s.Manager.NewOperation()
+	s.Manager.SetResult(op.Name, &api.ProvisionAshResponse{})
+	return op, nil
+}
+
+// ProvisionArc installs a specified version of ARC on the DUT.
+//
+// This directly overwrites the version of ARC on the current root
+// disk partition.
+func (s *DutServer) ProvisionArc(ctx context.Context, req *api.ProvisionArcRequest) (*longrunning.Operation, error) {
+	s.logger.Println("Received api.ProvisionArcRequest: ", *req)
+	op := s.Manager.NewOperation()
+	s.Manager.SetResult(op.Name, &api.ProvisionArcResponse{})
+	return op, nil
+}
diff --git a/test/src/chromiumos/dutservice/cmd/dutserver/dutserver_test.go b/test/src/chromiumos/dutservice/cmd/dutserver/dutserver_test.go
new file mode 100644
index 0000000..905c7bd
--- /dev/null
+++ b/test/src/chromiumos/dutservice/cmd/dutserver/dutserver_test.go
@@ -0,0 +1,44 @@
+// Copyright 2021 The Chromium OS Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+package main
+
+import (
+	"bytes"
+	"context"
+	"log"
+	"net"
+	"testing"
+
+	"go.chromium.org/chromiumos/config/go/test/api"
+	"google.golang.org/grpc"
+)
+
+// TestDutServer_Empty tests if DutServer can handle emtpy requst without problem.
+func TestDutServer_Empty(t *testing.T) {
+	var logBuf bytes.Buffer
+	l, err := net.Listen("tcp", ":0")
+	if err != nil {
+		t.Fatal("Failed to create a net listener: ", err)
+	}
+
+	ctx := context.Background()
+	srv, err := newDutServer(l, log.New(&logBuf, "", log.LstdFlags|log.LUTC))
+	if err != nil {
+		t.Fatalf("Failed to start DutServer: %v", err)
+	}
+	go srv.Serve(l)
+	defer srv.Stop()
+
+	conn, err := grpc.Dial(l.Addr().String(), grpc.WithInsecure())
+	if err != nil {
+		t.Fatalf("Failed to dial: %v", err)
+	}
+	defer conn.Close()
+
+	cl := api.NewDutServiceClient(conn)
+	if _, err := cl.ProvisionDut(ctx, &api.ProvisionDutRequest{}); err != nil {
+		t.Fatalf("Failed at api.ProvisionDut: %v", err)
+	}
+}
diff --git a/test/src/chromiumos/dutservice/cmd/dutserver/main.go b/test/src/chromiumos/dutservice/cmd/dutserver/main.go
new file mode 100644
index 0000000..5c21bb1
--- /dev/null
+++ b/test/src/chromiumos/dutservice/cmd/dutserver/main.go
@@ -0,0 +1,77 @@
+// Copyright 2021 The Chromium OS Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+// Package main implements the dutservice used to run tests in RTD.
+package main
+
+import (
+	"flag"
+	"fmt"
+	"io"
+	"log"
+	"net"
+	"os"
+	"path/filepath"
+	"time"
+)
+
+// Version is the version info of this command. It is filled in during emerge.
+var Version = "<unknown>"
+
+// createLogFile creates a file and its parent directory for logging purpose.
+func createLogFile() (*os.File, error) {
+	t := time.Now()
+	fullPath := filepath.Join("/tmp/dutservice/", t.Format("20060102-150405"))
+	if err := os.MkdirAll(fullPath, 0755); err != nil {
+		return nil, fmt.Errorf("failed to create directory %v: %v", fullPath, err)
+	}
+
+	logFullPathName := filepath.Join(fullPath, "log.txt")
+
+	// Log the full output of the command to disk.
+	logFile, err := os.Create(logFullPathName)
+	if err != nil {
+		return nil, fmt.Errorf("failed to create file %v: %v", fullPath, err)
+	}
+	return logFile, nil
+}
+
+// newLogger creates a logger. Using go default logger for now.
+func newLogger(logFile *os.File) *log.Logger {
+	mw := io.MultiWriter(logFile, os.Stderr)
+	return log.New(mw, "", log.LstdFlags|log.LUTC)
+}
+
+func main() {
+	os.Exit(func() int {
+		version := flag.Bool("version", false, "print version and exit")
+		flag.Parse()
+
+		if *version {
+			fmt.Println("dutservice version ", Version)
+			return 0
+		}
+
+		logFile, err := createLogFile()
+		if err != nil {
+			log.Fatalln("Failed to create log file: ", err)
+		}
+		defer logFile.Close()
+
+		logger := newLogger(logFile)
+		logger.Println("Starting dutservice version ", Version)
+		l, err := net.Listen("tcp", ":0")
+		if err != nil {
+			logger.Fatalln("Failed to create a net listener: ", err)
+			return 2
+		}
+		server, err := newDutServer(l, logger)
+		if err != nil {
+			logger.Fatalln("Failed to start dutservice server: ", err)
+		}
+
+		server.Serve(l)
+		return 0
+	}())
+}