Skip to content

Commit 62419ab

Browse files
authored
Merge pull request #1048 from starius/loop-stop
cli: add command "loop stop"
2 parents bb41f0b + a4dc590 commit 62419ab

File tree

15 files changed

+1991
-1526
lines changed

15 files changed

+1991
-1526
lines changed

cmd/loop/main.go

Lines changed: 22 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -89,7 +89,7 @@ var (
8989
listSwapsCommand, swapInfoCommand, getLiquidityParamsCommand,
9090
setLiquidityRuleCommand, suggestSwapCommand, setParamsCommand,
9191
getInfoCommand, abandonSwapCommand, reservationsCommands,
92-
instantOutCommand, listInstantOutsCommand,
92+
instantOutCommand, listInstantOutsCommand, stopCommand,
9393
printManCommand, printMarkdownCommand,
9494
}
9595
)
@@ -190,20 +190,37 @@ func main() {
190190
}
191191
}
192192

193-
func getClient(ctx context.Context, cmd *cli.Command) (looprpc.SwapClientClient, func(), error) {
193+
// getClient establishes a SwapClient RPC connection and returns the client and
194+
// a cleanup handler.
195+
func getClient(ctx context.Context, cmd *cli.Command) (looprpc.SwapClientClient,
196+
func(), error) {
197+
198+
client, _, cleanup, err := getClientWithConn(ctx, cmd)
199+
if err != nil {
200+
return nil, nil, err
201+
}
202+
203+
return client, cleanup, nil
204+
}
205+
206+
// getClientWithConn returns both the SwapClient RPC client and the underlying
207+
// gRPC connection so callers can perform connection-aware actions.
208+
func getClientWithConn(ctx context.Context, cmd *cli.Command) (
209+
looprpc.SwapClientClient, *grpc.ClientConn, func(), error) {
210+
194211
rpcServer := cmd.String("rpcserver")
195212
tlsCertPath, macaroonPath, err := extractPathArgs(cmd)
196213
if err != nil {
197-
return nil, nil, err
214+
return nil, nil, nil, err
198215
}
199216
conn, err := getClientConn(ctx, rpcServer, tlsCertPath, macaroonPath)
200217
if err != nil {
201-
return nil, nil, err
218+
return nil, nil, nil, err
202219
}
203220
cleanup := func() { conn.Close() }
204221

205222
loopClient := looprpc.NewSwapClientClient(conn)
206-
return loopClient, cleanup, nil
223+
return loopClient, conn, cleanup, nil
207224
}
208225

209226
func getMaxRoutingFee(amt btcutil.Amount) btcutil.Amount {

cmd/loop/stop.go

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
package main
2+
3+
import (
4+
"context"
5+
"fmt"
6+
7+
"github.com/lightninglabs/loop/looprpc"
8+
"github.com/urfave/cli/v3"
9+
"google.golang.org/grpc"
10+
"google.golang.org/grpc/connectivity"
11+
)
12+
13+
var stopCommand = &cli.Command{
14+
Name: "stop",
15+
Usage: "stop the loop daemon",
16+
Description: "Requests loopd to perform a graceful shutdown.",
17+
Flags: []cli.Flag{
18+
&cli.BoolFlag{
19+
Name: "wait",
20+
Usage: "wait until loopd fully shuts down",
21+
},
22+
},
23+
Action: stopDaemon,
24+
}
25+
26+
// stopDaemon requests the daemon to shut down gracefully and optionally waits
27+
// for the gRPC connection to terminate.
28+
func stopDaemon(ctx context.Context, cmd *cli.Command) error {
29+
waitForShutdown := cmd.Bool("wait")
30+
31+
// Establish a client connection to loopd.
32+
client, conn, cleanup, err := getClientWithConn(ctx, cmd)
33+
if err != nil {
34+
return err
35+
}
36+
defer cleanup()
37+
38+
// Request loopd to shut down.
39+
_, err = client.StopDaemon(ctx, &looprpc.StopDaemonRequest{})
40+
if err != nil {
41+
return err
42+
}
43+
44+
fmt.Println("Shutting down loopd")
45+
46+
// Optionally wait for the gRPC connection to terminate.
47+
if !waitForShutdown {
48+
return nil
49+
}
50+
51+
fmt.Println("Waiting for loopd to exit...")
52+
53+
err = waitForDaemonShutdown(ctx, conn)
54+
if err != nil {
55+
return err
56+
}
57+
58+
fmt.Println("Loopd shut down")
59+
60+
return nil
61+
}
62+
63+
// waitForDaemonShutdown monitors the gRPC connectivity state until the daemon
64+
// disappears. To avoid getting stuck in idle mode we nudge the connection to
65+
// reconnect when needed.
66+
func waitForDaemonShutdown(ctx context.Context, conn *grpc.ClientConn) error {
67+
for {
68+
state := conn.GetState()
69+
70+
switch state {
71+
case connectivity.Shutdown:
72+
return nil
73+
74+
// Connection attempts now fail which means loopd is offline.
75+
case connectivity.TransientFailure:
76+
return nil
77+
78+
// Force the channel out of Idle so we'll see failure states
79+
// once loopd stops serving.
80+
case connectivity.Idle:
81+
conn.Connect()
82+
}
83+
84+
if !conn.WaitForStateChange(ctx, state) {
85+
return ctx.Err()
86+
}
87+
}
88+
}

docs/loop.1

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -431,6 +431,16 @@ list all instant out swaps
431431
.PP
432432
\fB--help, -h\fP: show help
433433

434+
.SH stop
435+
.PP
436+
stop the loop daemon
437+
438+
.PP
439+
\fB--help, -h\fP: show help
440+
441+
.PP
442+
\fB--wait\fP: wait until loopd fully shuts down
443+
434444
.SH static, s
435445
.PP
436446
perform on-chain to off-chain swaps using static addresses.

docs/loop.md

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -468,6 +468,25 @@ The following flags are supported:
468468
|-----------------|-------------|------|:-------------:|
469469
| `--help` (`-h`) | show help | bool | `false` |
470470

471+
### `stop` command
472+
473+
stop the loop daemon.
474+
475+
Requests loopd to perform a graceful shutdown.
476+
477+
Usage:
478+
479+
```bash
480+
$ loop [GLOBAL FLAGS] stop [COMMAND FLAGS] [ARGUMENTS...]
481+
```
482+
483+
The following flags are supported:
484+
485+
| Name | Description | Type | Default value |
486+
|-----------------|-----------------------------------|------|:-------------:|
487+
| `--wait` | wait until loopd fully shuts down | bool | `false` |
488+
| `--help` (`-h`) | show help | bool | `false` |
489+
471490
### `static` command (aliases: `s`)
472491

473492
perform on-chain to off-chain swaps using static addresses.

loopd/daemon.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -746,6 +746,7 @@ func (d *Daemon) initialize(withMacaroonService bool) error {
746746
withdrawalManager: withdrawalManager,
747747
staticLoopInManager: staticLoopInManager,
748748
assetClient: d.assetClient,
749+
stopDaemon: d.Stop,
749750
}
750751

751752
// Retrieve all currently existing swaps from the database.

loopd/swapclient_server.go

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,9 @@ type swapClientServer struct {
104104
nextSubscriberID int
105105
swapsLock sync.Mutex
106106
mainCtx context.Context
107+
108+
// stopDaemon is invoked to trigger a graceful shutdown of the daemon.
109+
stopDaemon func()
107110
}
108111

109112
// LoopOut initiates a loop out swap with the given parameters. The call returns
@@ -1346,6 +1349,22 @@ func (s *swapClientServer) GetInfo(ctx context.Context,
13461349
}, nil
13471350
}
13481351

1352+
// StopDaemon triggers a graceful shutdown of the daemon process.
1353+
func (s *swapClientServer) StopDaemon(ctx context.Context,
1354+
_ *looprpc.StopDaemonRequest) (*looprpc.StopDaemonResponse, error) {
1355+
1356+
// Ensure we have a shutdown handler to invoke.
1357+
if s.stopDaemon == nil {
1358+
return nil, status.Error(codes.Unimplemented,
1359+
"stop daemon not supported")
1360+
}
1361+
1362+
// Initiate the shutdown sequence.
1363+
s.stopDaemon()
1364+
1365+
return &looprpc.StopDaemonResponse{}, nil
1366+
}
1367+
13491368
// GetLiquidityParams gets our current liquidity manager's parameters.
13501369
func (s *swapClientServer) GetLiquidityParams(_ context.Context,
13511370
_ *looprpc.GetLiquidityParamsRequest) (*looprpc.LiquidityParameters,

loopd/swapclient_server_test.go

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -261,6 +261,29 @@ func TestValidateLoopInRequest(t *testing.T) {
261261
}
262262
}
263263

264+
// TestSwapClientServerStopDaemon ensures that calling StopDaemon triggers the
265+
// daemon shutdown.
266+
func TestSwapClientServerStopDaemon(t *testing.T) {
267+
t.Parallel()
268+
269+
// Prepare a server instance that tracks whether shutdown is requested.
270+
var stopCalled bool
271+
server := &swapClientServer{
272+
stopDaemon: func() {
273+
stopCalled = true
274+
},
275+
}
276+
277+
// Request the daemon to stop and assert the callback executed.
278+
_, err := server.StopDaemon(
279+
context.Background(), &looprpc.StopDaemonRequest{},
280+
)
281+
require.NoError(t, err)
282+
283+
// Ensure our shutdown callback executed.
284+
require.True(t, stopCalled)
285+
}
286+
264287
// TestValidateLoopOutRequest tests validation of loop out requests.
265288
func TestValidateLoopOutRequest(t *testing.T) {
266289
tests := []struct {

0 commit comments

Comments
 (0)