cmd/loop: add session gRPC transport hook

This commit is contained in:
Boris Nagaev 2026-02-04 16:31:27 -05:00
parent d0591e62b9
commit c3e102527a
No known key found for this signature in database
4 changed files with 97 additions and 16 deletions

View file

@ -43,12 +43,7 @@ func forceAutoloop(ctx context.Context, cmd *cli.Command) error {
}
func getDebugClient(ctx context.Context, cmd *cli.Command) (looprpc.DebugClient, func(), error) {
rpcServer := cmd.String("rpcserver")
tlsCertPath, macaroonPath, err := extractPathArgs(cmd)
if err != nil {
return nil, nil, err
}
conn, cleanup, err := getClientConn(rpcServer, tlsCertPath, macaroonPath)
conn, cleanup, err := sessionTransport.Dial(cmd)
if err != nil {
return nil, nil, err
}

View file

@ -252,14 +252,9 @@ func getClient(cmd *cli.Command) (looprpc.SwapClientClient,
// getClientWithConn returns both the SwapClient RPC client and the underlying
// gRPC connection so callers can perform connection-aware actions.
func getClientWithConn(cmd *cli.Command) (looprpc.SwapClientClient,
*grpc.ClientConn, func(), error) {
daemonConn, func(), error) {
rpcServer := cmd.String("rpcserver")
tlsCertPath, macaroonPath, err := extractPathArgs(cmd)
if err != nil {
return nil, nil, nil, err
}
conn, cleanup, err := getClientConn(rpcServer, tlsCertPath, macaroonPath)
conn, cleanup, err := sessionTransport.Dial(cmd)
if err != nil {
return nil, nil, nil, err
}
@ -481,7 +476,8 @@ func logSwap(swap *looprpc.SwapStatus) {
fmt.Println()
}
func getClientConn(address, tlsCertPath, macaroonPath string) (*grpc.ClientConn,
// getClientConn dials the loopd gRPC server with TLS and macaroon auth.
func getClientConn(address, tlsCertPath, macaroonPath string) (daemonConn,
func(), error) {
// We always need to send a macaroon.
@ -495,6 +491,14 @@ func getClientConn(address, tlsCertPath, macaroonPath string) (*grpc.ClientConn,
macOption,
}
// Install gRPC interceptors for session recording if needed.
if unary := sessionTransport.UnaryInterceptor(); unary != nil {
opts = append(opts, grpc.WithChainUnaryInterceptor(unary))
}
if stream := sessionTransport.StreamInterceptor(); stream != nil {
opts = append(opts, grpc.WithChainStreamInterceptor(stream))
}
// Since TLS cannot be disabled, we'll always have a cert file to read.
creds, err := credentials.NewClientTLSFromFile(tlsCertPath, "")
if err != nil {

View file

@ -0,0 +1,83 @@
package main
import (
"context"
"github.com/urfave/cli/v3"
"google.golang.org/grpc"
"google.golang.org/grpc/connectivity"
)
// grpcTransport customizes gRPC dialing and interceptors for sessions.
type grpcTransport interface {
// Dial returns a gRPC connection for the CLI.
Dial(cmd *cli.Command) (daemonConn, func(), error)
// UnaryInterceptor returns the unary interceptor to apply for session
// flows.
UnaryInterceptor() grpc.UnaryClientInterceptor
// StreamInterceptor returns the stream interceptor to apply for session
// flows.
StreamInterceptor() grpc.StreamClientInterceptor
}
// daemonConn is the client connection interface required by stop/wait flows.
type daemonConn interface {
grpc.ClientConnInterface
// GetState reports the current connectivity state of the client
// channel.
GetState() connectivity.State
// WaitForStateChange blocks until the state changes or the context
// expires.
WaitForStateChange(ctx context.Context,
sourceState connectivity.State) bool
// Connect forces the channel out of idle mode.
Connect()
}
// directGrpcTransport establishes real gRPC connections to loopd.
type directGrpcTransport struct{}
// Dial opens a direct gRPC connection.
func (t *directGrpcTransport) Dial(
cmd *cli.Command) (daemonConn, func(), error) {
return dialDirectConn(cmd)
}
// UnaryInterceptor returns nil because direct connections do not need wrapping.
func (t *directGrpcTransport) UnaryInterceptor() grpc.UnaryClientInterceptor {
return nil
}
// StreamInterceptor returns nil because direct connections do not need
// wrapping.
func (t *directGrpcTransport) StreamInterceptor() grpc.StreamClientInterceptor {
return nil
}
// sessionTransport defines the active gRPC transport for CLI commands.
var sessionTransport grpcTransport = &directGrpcTransport{}
// hookGrpc installs the active gRPC session transport hook.
func hookGrpc(transport grpcTransport) func() {
prev := sessionTransport
sessionTransport = transport
return func() { sessionTransport = prev }
}
// dialDirectConn returns the standard CLI gRPC connection.
func dialDirectConn(cmd *cli.Command) (daemonConn, func(), error) {
rpcServer := cmd.String("rpcserver")
tlsCertPath, macaroonPath, err := extractPathArgs(cmd)
if err != nil {
return nil, nil, err
}
return getClientConn(rpcServer, tlsCertPath, macaroonPath)
}

View file

@ -6,7 +6,6 @@ import (
"github.com/lightninglabs/loop/looprpc"
"github.com/urfave/cli/v3"
"google.golang.org/grpc"
"google.golang.org/grpc/connectivity"
)
@ -63,7 +62,7 @@ func stopDaemon(ctx context.Context, cmd *cli.Command) error {
// waitForDaemonShutdown monitors the gRPC connectivity state until the daemon
// disappears. To avoid getting stuck in idle mode we nudge the connection to
// reconnect when needed.
func waitForDaemonShutdown(ctx context.Context, conn *grpc.ClientConn) error {
func waitForDaemonShutdown(ctx context.Context, conn daemonConn) error {
for {
state := conn.GetState()