+4
.gitignore
+4
.gitignore
+39
bin/debug_codec.ml
+39
bin/debug_codec.ml
···
1
+
open Swim.Types
2
+
3
+
let hex_of_string s =
4
+
String.to_seq s
5
+
|> Seq.map (fun c -> Printf.sprintf "%02x" (Char.code c))
6
+
|> List.of_seq |> String.concat ""
7
+
8
+
let () =
9
+
let node =
10
+
make_node_info
11
+
~id:(node_id_of_string "test-node")
12
+
~addr:(`Udp (Eio.Net.Ipaddr.of_raw "\127\000\000\001", 7947))
13
+
~meta:""
14
+
in
15
+
let ping =
16
+
Ping { seq = 1; target = node_id_of_string "go-node"; sender = node }
17
+
in
18
+
19
+
let wire_msg = msg_to_wire ~self_name:"ocaml-node" ~self_port:7947 ping in
20
+
Printf.printf "Wire message type: %s\n"
21
+
(match wire_msg with
22
+
| Wire.Ping _ -> "Ping"
23
+
| Wire.Indirect_ping _ -> "Indirect_ping"
24
+
| Wire.Ack _ -> "Ack"
25
+
| Wire.Nack _ -> "Nack"
26
+
| Wire.Suspect _ -> "Suspect"
27
+
| Wire.Alive _ -> "Alive"
28
+
| Wire.Dead _ -> "Dead"
29
+
| Wire.User_data _ -> "User_data"
30
+
| Wire.Compound _ -> "Compound"
31
+
| Wire.Compressed _ -> "Compressed"
32
+
| Wire.Err _ -> "Err");
33
+
34
+
let encoded =
35
+
Swim.Codec.encode_internal_msg ~self_name:"ocaml-node" ~self_port:7947 ping
36
+
in
37
+
Printf.printf "Encoded length: %d bytes\n" (String.length encoded);
38
+
Printf.printf "Encoded hex: %s\n" (hex_of_string encoded);
39
+
Printf.printf "First byte (msg type): %d\n" (Char.code encoded.[0])
+65
bin/debug_ping.ml
+65
bin/debug_ping.ml
···
1
+
open Swim.Types
2
+
3
+
external env_cast : 'a -> 'b = "%identity"
4
+
5
+
let hex_of_cstruct cs =
6
+
let len = Cstruct.length cs in
7
+
let buf = Buffer.create (len * 2) in
8
+
for i = 0 to len - 1 do
9
+
Buffer.add_string buf (Printf.sprintf "%02x" (Cstruct.get_uint8 cs i))
10
+
done;
11
+
Buffer.contents buf
12
+
13
+
let () =
14
+
Eio_main.run @@ fun env ->
15
+
let env = env_cast env in
16
+
Eio.Switch.run @@ fun sw ->
17
+
let net = env#net in
18
+
let sock =
19
+
Swim.Transport.create_udp_socket net ~sw ~addr:"\127\000\000\001" ~port:7947
20
+
in
21
+
22
+
Printf.printf "Bound to 127.0.0.1:7947\n%!";
23
+
24
+
(* Create and send a ping to Go memberlist *)
25
+
let self =
26
+
make_node_info
27
+
~id:(node_id_of_string "ocaml-node")
28
+
~addr:(`Udp (Eio.Net.Ipaddr.of_raw "\127\000\000\001", 7947))
29
+
~meta:""
30
+
in
31
+
let target_id = node_id_of_string "go-node" in
32
+
let ping = Ping { seq = 1; target = target_id; sender = self } in
33
+
34
+
let packet = { cluster = ""; primary = ping; piggyback = [] } in
35
+
let send_buf = Cstruct.create 1500 in
36
+
match Swim.Codec.encode_packet packet ~buf:send_buf with
37
+
| Error _ -> Printf.eprintf "Encode failed\n"
38
+
| Ok len ->
39
+
let encoded = Cstruct.sub send_buf 0 len in
40
+
Printf.printf "Sending ping (%d bytes): %s\n%!" len
41
+
(hex_of_cstruct encoded);
42
+
43
+
let dst = `Udp (Eio.Net.Ipaddr.of_raw "\127\000\000\001", 7946) in
44
+
Eio.Net.send sock ~dst [ encoded ];
45
+
Printf.printf "Sent! Waiting for ack...\n%!";
46
+
47
+
(* Wait for response *)
48
+
let recv_buf = Cstruct.create 1500 in
49
+
for i = 1 to 5 do
50
+
Printf.printf "Waiting for packet %d (with 2s timeout)...\n%!" i;
51
+
Eio.Fiber.fork ~sw (fun () ->
52
+
let src, n = Eio.Net.recv sock recv_buf in
53
+
let received = Cstruct.sub recv_buf 0 n in
54
+
Printf.printf "Received %d bytes from %s\n%!" n
55
+
(match src with
56
+
| `Udp (ip, port) ->
57
+
Printf.sprintf "%s:%d"
58
+
(Fmt.to_to_string Eio.Net.Ipaddr.pp ip)
59
+
port
60
+
| _ -> "unknown");
61
+
Printf.printf "Hex: %s\n%!" (hex_of_cstruct received);
62
+
Printf.printf "First byte (msg type): %d\n%!"
63
+
(Cstruct.get_uint8 received 0));
64
+
Eio.Time.sleep env#clock 2.0
65
+
done
+41
bin/debug_recv.ml
+41
bin/debug_recv.ml
···
1
+
open Swim.Types
2
+
3
+
external env_cast : 'a -> 'b = "%identity"
4
+
5
+
let hex_of_cstruct cs =
6
+
let len = Cstruct.length cs in
7
+
let buf = Buffer.create (len * 2) in
8
+
for i = 0 to len - 1 do
9
+
Buffer.add_string buf (Printf.sprintf "%02x" (Cstruct.get_uint8 cs i))
10
+
done;
11
+
Buffer.contents buf
12
+
13
+
let () =
14
+
Eio_main.run @@ fun env ->
15
+
let env = env_cast env in
16
+
Eio.Switch.run @@ fun sw ->
17
+
let net = env#net in
18
+
let sock =
19
+
Swim.Transport.create_udp_socket net ~sw ~addr:"\127\000\000\001" ~port:7947
20
+
in
21
+
22
+
Printf.printf "Listening on 127.0.0.1:7947 for UDP packets...\n%!";
23
+
24
+
let buf = Cstruct.create 1500 in
25
+
for i = 1 to 10 do
26
+
Printf.printf "Waiting for packet %d...\n%!" i;
27
+
let src, n = Eio.Net.recv sock buf in
28
+
let received = Cstruct.sub buf 0 n in
29
+
Printf.printf "Received %d bytes from %s\n%!" n
30
+
(match src with
31
+
| `Udp (ip, port) ->
32
+
Printf.sprintf "%s:%d" (Fmt.to_to_string Eio.Net.Ipaddr.pp ip) port
33
+
| _ -> "unknown");
34
+
Printf.printf "Hex: %s\n%!" (hex_of_cstruct received);
35
+
Printf.printf "First byte (msg type): %d\n%!" (Cstruct.get_uint8 received 0);
36
+
37
+
(* Try to decode *)
38
+
match Swim.Codec.decode_packet received with
39
+
| Ok packet -> Printf.printf "Decoded! Cluster: %s\n%!" packet.cluster
40
+
| Error e -> Printf.printf "Decode error: %s\n%!" (decode_error_to_string e)
41
+
done
+49
interop/debug/debug_sender.go
+49
interop/debug/debug_sender.go
···
1
+
package main
2
+
3
+
import (
4
+
"fmt"
5
+
"net"
6
+
7
+
"github.com/hashicorp/go-msgpack/codec"
8
+
)
9
+
10
+
type ping struct {
11
+
SeqNo uint32
12
+
Node string
13
+
SourceAddr []byte
14
+
SourcePort uint16
15
+
SourceNode string
16
+
}
17
+
18
+
func main() {
19
+
conn, err := net.Dial("udp", "127.0.0.1:7946")
20
+
if err != nil {
21
+
panic(err)
22
+
}
23
+
defer conn.Close()
24
+
25
+
p := ping{
26
+
SeqNo: 1,
27
+
Node: "test-node",
28
+
SourceAddr: []byte{127, 0, 0, 1},
29
+
SourcePort: 7947,
30
+
SourceNode: "ocaml-node",
31
+
}
32
+
33
+
var buf []byte
34
+
enc := codec.NewEncoderBytes(&buf, &codec.MsgpackHandle{})
35
+
if err := enc.Encode(&p); err != nil {
36
+
panic(err)
37
+
}
38
+
39
+
msg := append([]byte{0}, buf...)
40
+
fmt.Printf("Sending %d bytes: %x\n", len(msg), msg)
41
+
fmt.Printf("Message type: 0 (ping)\n")
42
+
fmt.Printf("Msgpack payload: %x\n", buf)
43
+
44
+
n, err := conn.Write(msg)
45
+
if err != nil {
46
+
panic(err)
47
+
}
48
+
fmt.Printf("Sent %d bytes\n", n)
49
+
}
+5
interop/debug/go.mod
+5
interop/debug/go.mod
+2
interop/debug/go.sum
+2
interop/debug/go.sum
+21
interop/go.mod
+21
interop/go.mod
···
1
+
module swim-interop
2
+
3
+
go 1.21
4
+
5
+
require github.com/hashicorp/memberlist v0.5.0
6
+
7
+
require (
8
+
github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da // indirect
9
+
github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c // indirect
10
+
github.com/hashicorp/errwrap v1.0.0 // indirect
11
+
github.com/hashicorp/go-immutable-radix v1.0.0 // indirect
12
+
github.com/hashicorp/go-msgpack v0.5.3 // indirect
13
+
github.com/hashicorp/go-multierror v1.0.0 // indirect
14
+
github.com/hashicorp/go-sockaddr v1.0.0 // indirect
15
+
github.com/hashicorp/golang-lru v0.5.0 // indirect
16
+
github.com/miekg/dns v1.1.26 // indirect
17
+
github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529 // indirect
18
+
golang.org/x/crypto v0.0.0-20190923035154-9ee001bba392 // indirect
19
+
golang.org/x/net v0.0.0-20190923162816-aa69164e4478 // indirect
20
+
golang.org/x/sys v0.0.0-20220728004956-3c1f35247d10 // indirect
21
+
)
+51
interop/go.sum
+51
interop/go.sum
···
1
+
github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da h1:8GUt8eRujhVEGZFFEjBj46YV4rDjvGrNxb0KMWYkL2I=
2
+
github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY=
3
+
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
4
+
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
5
+
github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c h1:964Od4U6p2jUkFxvCydnIczKteheJEzHRToSGK3Bnlw=
6
+
github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
7
+
github.com/hashicorp/errwrap v1.0.0 h1:hLrqtEDnRye3+sgx6z4qVLNuviH3MR5aQ0ykNJa/UYA=
8
+
github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
9
+
github.com/hashicorp/go-immutable-radix v1.0.0 h1:AKDB1HM5PWEA7i4nhcpwOrO2byshxBjXVn/J/3+z5/0=
10
+
github.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60=
11
+
github.com/hashicorp/go-msgpack v0.5.3 h1:zKjpN5BK/P5lMYrLmBHdBULWbJ0XpYR+7NGzqkZzoD4=
12
+
github.com/hashicorp/go-msgpack v0.5.3/go.mod h1:ahLV/dePpqEmjfWmKiqvPkv/twdG7iPBM1vqhUKIvfM=
13
+
github.com/hashicorp/go-multierror v1.0.0 h1:iVjPR7a6H0tWELX5NxNe7bYopibicUzc7uPribsnS6o=
14
+
github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk=
15
+
github.com/hashicorp/go-sockaddr v1.0.0 h1:GeH6tui99pF4NJgfnhp+L6+FfobzVW3Ah46sLo0ICXs=
16
+
github.com/hashicorp/go-sockaddr v1.0.0/go.mod h1:7Xibr9yA9JjQq1JpNB2Vw7kxv8xerXegt+ozgdvDeDU=
17
+
github.com/hashicorp/go-uuid v1.0.0 h1:RS8zrF7PhGwyNPOtxSClXXj9HA8feRnJzgnI1RJCSnM=
18
+
github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
19
+
github.com/hashicorp/golang-lru v0.5.0 h1:CL2msUPvZTLb5O648aiLNJw3hnBxN2+1Jq8rCOH9wdo=
20
+
github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
21
+
github.com/hashicorp/memberlist v0.5.0 h1:EtYPN8DpAURiapus508I4n9CzHs2W+8NZGbmmR/prTM=
22
+
github.com/hashicorp/memberlist v0.5.0/go.mod h1:yvyXLpo0QaGE59Y7hDTsTzDD25JYBZ4mHgHUZ8lrOI0=
23
+
github.com/miekg/dns v1.1.26 h1:gPxPSwALAeHJSjarOs00QjVdV9QoBvc1D2ujQUr5BzU=
24
+
github.com/miekg/dns v1.1.26/go.mod h1:bPDLeHnStXmXAq1m/Ch/hvfNHr14JKNPMBo3VZKjuso=
25
+
github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c h1:Lgl0gzECD8GnQ5QCWA8o6BtfL6mDH5rQgM4/fX3avOs=
26
+
github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc=
27
+
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
28
+
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
29
+
github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529 h1:nn5Wsu0esKSJiIVhscUtVbo7ada43DJhG55ua/hjS5I=
30
+
github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc=
31
+
github.com/stretchr/testify v1.2.2 h1:bSDNvY7ZPG5RlJ8otE/7V6gMiyenm9RtJ7IUVIAoJ1w=
32
+
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
33
+
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
34
+
golang.org/x/crypto v0.0.0-20190923035154-9ee001bba392 h1:ACG4HJsFiNMf47Y4PeRoebLNy/2lXT9EtprMuTFWt1M=
35
+
golang.org/x/crypto v0.0.0-20190923035154-9ee001bba392/go.mod h1:/lpIB1dKB+9EgE3H3cr1v9wB50oz8l4C4h62xy7jSTY=
36
+
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
37
+
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
38
+
golang.org/x/net v0.0.0-20190923162816-aa69164e4478 h1:l5EDrHhldLYb3ZRHDUhXF7Om7MvYXnkV9/iQNo1lX6g=
39
+
golang.org/x/net v0.0.0-20190923162816-aa69164e4478/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
40
+
golang.org/x/sync v0.0.0-20190423024810-112230192c58 h1:8gQV6CLnAEikrhgkHFbMAEhagSSnXWGV915qUMm9mrU=
41
+
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
42
+
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
43
+
golang.org/x/sys v0.0.0-20190922100055-0a153f010e69/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
44
+
golang.org/x/sys v0.0.0-20190924154521-2837fb4f24fe/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
45
+
golang.org/x/sys v0.0.0-20220728004956-3c1f35247d10 h1:WIoqL4EROvwiPdUtaip4VcDdpZ4kha7wBWZrbVKCIZg=
46
+
golang.org/x/sys v0.0.0-20220728004956-3c1f35247d10/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
47
+
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
48
+
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
49
+
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
50
+
golang.org/x/tools v0.0.0-20190907020128-2ca718005c18/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
51
+
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+103
interop/main.go
+103
interop/main.go
···
1
+
package main
2
+
3
+
import (
4
+
"encoding/hex"
5
+
"flag"
6
+
"log"
7
+
"os"
8
+
"os/signal"
9
+
"syscall"
10
+
"time"
11
+
12
+
"github.com/hashicorp/memberlist"
13
+
)
14
+
15
+
type eventDelegate struct{}
16
+
17
+
func (e *eventDelegate) NotifyJoin(node *memberlist.Node) {
18
+
log.Printf("[EVENT] Node joined: %s (%s:%d)", node.Name, node.Addr, node.Port)
19
+
}
20
+
21
+
func (e *eventDelegate) NotifyLeave(node *memberlist.Node) {
22
+
log.Printf("[EVENT] Node left: %s", node.Name)
23
+
}
24
+
25
+
func (e *eventDelegate) NotifyUpdate(node *memberlist.Node) {
26
+
log.Printf("[EVENT] Node updated: %s", node.Name)
27
+
}
28
+
29
+
func main() {
30
+
name := flag.String("name", "go-node", "Node name")
31
+
bind := flag.String("bind", "127.0.0.1", "Bind address")
32
+
port := flag.Int("port", 7946, "Bind port")
33
+
join := flag.String("join", "", "Address to join (host:port)")
34
+
secretKey := flag.String("key", "", "Secret key (hex encoded, 16 bytes for AES-128)")
35
+
flag.Parse()
36
+
37
+
config := memberlist.DefaultLANConfig()
38
+
config.Name = *name
39
+
config.BindAddr = *bind
40
+
config.BindPort = *port
41
+
config.AdvertisePort = *port
42
+
config.Events = &eventDelegate{}
43
+
44
+
config.LogOutput = os.Stdout
45
+
46
+
if *secretKey != "" {
47
+
keyBytes, err := hex.DecodeString(*secretKey)
48
+
if err != nil {
49
+
log.Fatalf("Invalid hex key: %v", err)
50
+
}
51
+
if len(keyBytes) != 16 && len(keyBytes) != 24 && len(keyBytes) != 32 {
52
+
log.Fatalf("Key must be 16, 24, or 32 bytes (got %d)", len(keyBytes))
53
+
}
54
+
keyring, err := memberlist.NewKeyring(nil, keyBytes)
55
+
if err != nil {
56
+
log.Fatalf("Failed to create keyring: %v", err)
57
+
}
58
+
config.Keyring = keyring
59
+
config.GossipVerifyIncoming = true
60
+
config.GossipVerifyOutgoing = true
61
+
log.Printf("Encryption enabled with %d-byte key", len(keyBytes))
62
+
}
63
+
64
+
list, err := memberlist.Create(config)
65
+
if err != nil {
66
+
log.Fatalf("Failed to create memberlist: %v", err)
67
+
}
68
+
69
+
log.Printf("Memberlist started: %s on %s:%d", *name, *bind, *port)
70
+
71
+
if *join != "" {
72
+
log.Printf("Joining cluster via %s", *join)
73
+
n, err := list.Join([]string{*join})
74
+
if err != nil {
75
+
log.Printf("Failed to join: %v", err)
76
+
} else {
77
+
log.Printf("Joined %d nodes", n)
78
+
}
79
+
}
80
+
81
+
go func() {
82
+
ticker := time.NewTicker(5 * time.Second)
83
+
for range ticker.C {
84
+
members := list.Members()
85
+
log.Printf("--- Members (%d) ---", len(members))
86
+
for _, m := range members {
87
+
log.Printf(" %s: %s:%d (state=%v)", m.Name, m.Addr, m.Port, m.State)
88
+
}
89
+
}
90
+
}()
91
+
92
+
sigCh := make(chan os.Signal, 1)
93
+
signal.Notify(sigCh, syscall.SIGINT, syscall.SIGTERM)
94
+
<-sigCh
95
+
96
+
log.Println("Shutting down...")
97
+
if err := list.Leave(time.Second); err != nil {
98
+
log.Printf("Leave error: %v", err)
99
+
}
100
+
if err := list.Shutdown(); err != nil {
101
+
log.Printf("Shutdown error: %v", err)
102
+
}
103
+
}
+18
test_interop.sh
+18
test_interop.sh
···
1
+
#!/bin/bash
2
+
set -e
3
+
4
+
echo "Starting Go memberlist server WITHOUT encryption..."
5
+
cd /home/gdiazlo/data/src/swim/interop
6
+
./memberlist-server -name go-node -port 7946 &
7
+
GO_PID=$!
8
+
sleep 2
9
+
10
+
echo "Starting OCaml SWIM client..."
11
+
cd /home/gdiazlo/data/src/swim
12
+
timeout 25 ./_build/default/bin/interop_test.exe || true
13
+
14
+
echo "Killing Go server..."
15
+
kill $GO_PID 2>/dev/null || true
16
+
wait $GO_PID 2>/dev/null || true
17
+
18
+
echo "Done"