Skip to content

Commit

Permalink
Add rudimentary gRPC support for local client/server communication
Browse files Browse the repository at this point in the history
  • Loading branch information
tillkuhn committed Jan 28, 2025
1 parent f922104 commit 4de4242
Show file tree
Hide file tree
Showing 8 changed files with 105 additions and 37 deletions.
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -174,4 +174,4 @@ fabric.properties

.env
dist/
coverage.html
coverage.*
2 changes: 2 additions & 0 deletions .golangci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,8 @@ issues:
linters: [ gochecknoglobals,mnd ]
- path: cmd/track.go
linters: [ gochecknoglobals,mnd ]
- path: cmd/wsp.go
linters: [ gochecknoglobals,mnd ]
- path: pkg/tracker/report.go
linters: [ mnd ] # magic number detection is annoying here
- path: pkg/tracker/tracker.go
Expand Down
21 changes: 14 additions & 7 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ endif

APP_NAME=billy-idle
BINARY ?= billy
DEFAULT_ENV ?= default
LAUNCHD_LABEL ?= com.github.tillkuhn.$(APP_NAME)

#-------------------
Expand Down Expand Up @@ -68,11 +69,13 @@ lint: ## Lint go code
@go fmt ./...
@golangci-lint run --fix

# $(shell go list ./... | grep -v internal/pb)
.PHONY: test
test: lint ## Run tests with coverage, implies lint, excludes generated *.pb.go files
@if hash gotest 2>/dev/null; then \
gotest -v -coverpkg=./... -coverprofile=coverage.out $(shell go list ./... | grep -v internal/pb); \
else go test -v -coverpkg=./... -coverprofile=coverage.out $(shell go list ./... | grep -v internal/pb); fi
gotest -v -coverpkg=./... -coverprofile=coverage.out.tmp ./... ; \
else go test -v -coverpkg=./... -coverprofile=coverage.out.tmp ./... ; fi
grep -v ".pb.go" coverage.out.tmp > coverage.out
@go tool cover -func coverage.out | grep "total:"
go tool cover -html=coverage.out -o coverage.html
@echo For coverage report open coverage.html
Expand Down Expand Up @@ -103,15 +106,19 @@ run: ## Run app in tracker mode (dev env), add -drop-create to recreate db

.PHONY: punch
punch: ## Show punch clock report for default db
go run main.go --debug punch --env default
go run main.go --debug punch --env $(DEFAULT_ENV)

.PHONY: report-dev
report-dev: ## Show report for dev env db
go run main.go --debug report --env dev
.PHONY: wsp
wsp: ## Show status using gRPC Client
go run main.go --debug wsp

.PHONY: report
report: ## Show report for default db
go run main.go --debug report --env default
go run main.go --debug report --env $(DEFAULT_ENV)

.PHONY: report-dev
report-dev: ## Show report for dev env db
go run main.go --debug report --env dev

.PHONY: run-help
run-help: ## Run app in help mode
Expand Down
32 changes: 20 additions & 12 deletions cmd/wsp.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,48 +2,56 @@ package cmd

import (
"context"
"strconv"
"time"

"github.com/golang/protobuf/ptypes/empty"
"github.com/tillkuhn/billy-idle/internal/pb"

Check failure on line 9 in cmd/wsp.go

View workflow job for this annotation

GitHub Actions / build

no required module provides package github.com/tillkuhn/billy-idle/internal/pb; to add it:
"google.golang.org/grpc"
"google.golang.org/grpc/credentials/insecure"
"log"
"time"

"github.com/spf13/cobra"
)

var (
gRPCPort int
)

// wspCmd represents the wsp command
var wspCmd = &cobra.Command{
Use: "wsp",
Short: "What's up?",
Long: `Returns status info from the current tracker instance`,
Run: func(cmd *cobra.Command, args []string) {
status(cmd.Context())

RunE: func(cmd *cobra.Command, _ []string) error {
return status(cmd.Context())
},
}

func status(ctx context.Context) {
addr := "localhost:50051"
func status(ctx context.Context) error {
addr := "localhost:" + strconv.Itoa(gRPCPort)
conn, err := grpc.NewClient(addr, grpc.WithTransportCredentials(insecure.NewCredentials()))
if err != nil {
log.Fatalf("did not connect: %v", err)
return err
}
defer func(conn *grpc.ClientConn) { _ = conn.Close() }(conn)
c := pb.NewBillyClient(conn)

// Contact the server and print out its response.
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
ctx, cancel := context.WithTimeout(ctx, time.Second)
defer cancel()
r, err := c.Status(ctx, &empty.Empty{})
// https://github.com/grpc/grpc-go/blob/master/examples/features/wait_for_ready/main.go#L93
r, err := c.Status(ctx, &empty.Empty{}, grpc.WaitForReady(true))
if err != nil {
log.Fatalf("could not get status: %v", err)
return err
}
log.Printf("Greeting: %s", r.GetMessage())
_, _ = rootCmd.OutOrStdout().Write([]byte("Response: " + r.GetMessage() + "\n"))
// log.Printf("Greeting: %s", r.GetMessage())
return nil
}

func init() {
rootCmd.AddCommand(wspCmd)
wspCmd.PersistentFlags().IntVar(&gRPCPort, "port", 50051, "Port for gRPC Communication")

// Here you will define your flags and configuration settings.

Expand Down
38 changes: 38 additions & 0 deletions cmd/wsp_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
package cmd

import (
"bytes"
"slices"
"strconv"
"testing"

"github.com/stretchr/testify/assert"
"github.com/tillkuhn/billy-idle/pkg/tracker"
)

var grpcPort = 50052 // use different port for test to avoid conflicts
func TestWSPStatusError(t *testing.T) {
actual := new(bytes.Buffer)
rootCmd.SetOut(actual)
rootCmd.SetErr(actual)
wspArgs := []string{"--port", strconv.Itoa(grpcPort)}
rootCmd.SetArgs(slices.Insert(wspArgs, 0, wspCmd.Use))
// Returns "Error: rpc error: code = DeadlineExceeded desc = context deadline exceeded\nUsage:\n b
// if no server
opts := &tracker.Options{
GRPCPort: grpcPort,
ClientID: "test",
AppRoot: defaultAppRoot(),
}
tr := tracker.New(opts)
go func() {
if err := tr.ServeGRCP(); err != nil {
t.Log(err)
t.Fail()
}
}()
// assert.NoError(t, tr.ServeGRCP())
err := rootCmd.Execute()
assert.NoError(t, err)
assert.Contains(t, actual.String(), "I am up and running")
}
2 changes: 2 additions & 0 deletions internal/pb/billy.proto
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ syntax = "proto3";
option go_package = "github.com/tillkuhn/billy-idle/pb";

import "google/protobuf/empty.proto";
import "google/protobuf/timestamp.proto";

package pb;

Expand All @@ -37,6 +38,7 @@ message ModeResponse {
// The response message containing the newly applied mode
message StatusResponse {
string message = 1;
google.protobuf.Timestamp time = 7;
}

// https://www.reddit.com/r/golang/comments/dysrzw/protobuf_and_enum_type/
Expand Down
44 changes: 27 additions & 17 deletions pkg/tracker/tracker.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,34 +3,40 @@ package tracker
import (
"context"
"fmt"
"github.com/golang/protobuf/ptypes/empty"
"github.com/tillkuhn/billy-idle/internal/pb"
"google.golang.org/grpc"
"log"
"math/rand/v2"
"net"
"os"
"sync"
"time"

"github.com/golang/protobuf/ptypes/empty"
"github.com/tillkuhn/billy-idle/internal/pb"
"google.golang.org/grpc"
"google.golang.org/protobuf/types/known/timestamppb"

"github.com/brianvoe/gofakeit/v7"
"github.com/jmoiron/sqlx"
)

// Tracker tracks idle state periodically and persists state changes in DB,
// also used to implement gRPC BillyServer
type Tracker struct {
opts *Options
db *sqlx.DB
wg sync.WaitGroup
pb.UnimplementedBillyServer
opts *Options
db *sqlx.DB
grpcServer *grpc.Server
wg sync.WaitGroup
pb.UnimplementedBillyServer // Tracker implements billy gRPC Server
}

// New returns a new Tracker configured with the given Options
func New(opts *Options) *Tracker {
if opts.Out == nil {
opts.Out = os.Stdout
}
if opts.GRPCPort == 0 {
opts.GRPCPort = 50051
}
db, err := initDB(opts)
if err != nil {
log.Fatal(err)
Expand All @@ -41,31 +47,33 @@ func New(opts *Options) *Tracker {
// NewWithDB returns a new Tracker configured with the given Options and DB, good for testing
func NewWithDB(opts *Options, db *sqlx.DB) *Tracker {
return &Tracker{
opts: opts,
db: db,
opts: opts,
db: db,
grpcServer: grpc.NewServer(),
}
}

// ServeGRCP experimental Server for gRCP support
func (t *Tracker) ServeGRCP() error {
grpcPort := 50051
lis, err := net.Listen("tcp", fmt.Sprintf(":%d", grpcPort))
lis, err := net.Listen("tcp", fmt.Sprintf(":%d", t.opts.GRPCPort))
if err != nil {
return err
}
s := grpc.NewServer()
pb.RegisterBillyServer(s, t)
log.Printf("gRCP server listening at %v", lis.Addr())
if err := s.Serve(lis); err != nil {
log.Printf("👂 Registering gRCP server to listen at %v", lis.Addr())
pb.RegisterBillyServer(t.grpcServer, t)
if err := t.grpcServer.Serve(lis); err != nil {
return err
}
return nil
}

// Status implements pb.BillyServer
// Status as per pb.BillyServer
func (t *Tracker) Status(_ context.Context, _ *empty.Empty) (*pb.StatusResponse, error) {
log.Println("Received: status request")
return &pb.StatusResponse{Message: "Hello I am up and running"}, nil
return &pb.StatusResponse{
Time: timestamppb.Now(),
Message: "Hi! I am up and running in env=" + t.opts.Env,
}, nil
}

// Track starts the idle/Busy tracker in a loop that runs until the context is cancelled
Expand All @@ -88,6 +96,8 @@ func (t *Tracker) Track(ctx context.Context) {
// we're finished here, make sure latest status is written to db, must use a fresh context
msg := fmt.Sprintf("🛑 Tracker stopped after %v %s time", ist.TimeSinceLastSwitch(), ist.State())
_ = t.completeTrackRecord(context.Background(), ist.id, msg)
log.Printf("👂 Stopping gRCP server on port %d", t.opts.GRPCPort)
t.grpcServer.GracefulStop()
done = true
default:
idleMillis, err := IdleTime(ctx, t.opts.Cmd)
Expand Down
1 change: 1 addition & 0 deletions pkg/tracker/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ type Options struct {
MaxBusy time.Duration
RegBusy time.Duration
Out io.Writer
GRPCPort int
}

func (o Options) AppDir() string {
Expand Down

0 comments on commit 4de4242

Please sign in to comment.