Skip to content

Commit 06b67e8

Browse files
authored
feat: add master slave (#112)
* feat: add master slave * feat: add master slave * fix: test failed * feat: rule handler support master slave * fix: rule handler support slave * fix: rule handler support slave * doc: master slave * fix: use fixed slaveID * fix: multipart upload * fix: update install script * feat: go mod tidy * fix: split master ip and port * fix: update slave ip and port * fix: missing upload for rule * fix: missing slave when master restart * feat: go mod tidy
1 parent 937cc69 commit 06b67e8

File tree

27 files changed

+4716
-493
lines changed

27 files changed

+4716
-493
lines changed

.github/workflows/release.yaml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,7 @@ jobs:
103103
cos.gz:/coscout/v2/latest/${{ matrix.os }}-${{ matrix.arch }}.gz
104104
${{ matrix.os }}-${{ matrix.arch }}.json:/coscout/v2/latest/${{ matrix.os }}-${{ matrix.arch }}.json
105105
script/install.sh:/coscout/v2/install.sh
106+
script/install-slave.sh:/coscout/v2/install-slave.sh
106107
- name: Upload coscli beta metadata files to oss
107108
if: env.IS_RELEASE == 'false'
108109
uses: tvrcgo/oss-action@master
@@ -115,6 +116,7 @@ jobs:
115116
cos.gz:/coscout/v2/beta/${{ matrix.os }}-${{ matrix.arch }}.gz
116117
${{ matrix.os }}-${{ matrix.arch }}.json:/coscout/v2/beta/${{ matrix.os }}-${{ matrix.arch }}.json
117118
script/install.sh:/coscout/v2/install-beta.sh
119+
script/install-slave.sh:/coscout/v2/install-slave-beta.sh
118120
119121
- name: Replace download link in script/install.sh
120122
run: |

cmd/coscout/commands/root.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,5 +83,6 @@ func NewCommand() *cobra.Command {
8383

8484
cmd.AddCommand(NewVersionCommand())
8585
cmd.AddCommand(NewDaemonCommand(&cfgPath))
86+
cmd.AddCommand(NewSlaveCommand())
8687
return cmd
8788
}

cmd/coscout/commands/slave.go

Lines changed: 149 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,149 @@
1+
// Copyright 2025 coScene
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package commands
16+
17+
import (
18+
"context"
19+
"os"
20+
"os/signal"
21+
"syscall"
22+
"time"
23+
24+
"github.com/coscene-io/coscout/internal/config"
25+
"github.com/coscene-io/coscout/internal/slave"
26+
log "github.com/sirupsen/logrus"
27+
"github.com/spf13/cobra"
28+
)
29+
30+
func NewSlaveCommand() *cobra.Command {
31+
var (
32+
port int
33+
masterIP string
34+
masterPort int
35+
slaveID string
36+
filePrefix string
37+
ip string
38+
)
39+
40+
cmd := &cobra.Command{
41+
Use: "slave",
42+
Short: "Run coScout as a slave node",
43+
Long: `Run coScout as a slave node that connects to a master node.
44+
The slave node will:
45+
- Register with the master node
46+
- Respond to file scan requests from master
47+
- Serve file downloads to master
48+
- Send periodic heartbeats to maintain connection`,
49+
Run: func(cmd *cobra.Command, args []string) {
50+
// Create slave configuration
51+
slaveConfig := config.DefaultSlaveConfig()
52+
53+
slaveConfig.IP = ip
54+
slaveConfig.Port = port
55+
slaveConfig.MasterIP = masterIP
56+
slaveConfig.MasterPort = masterPort
57+
slaveConfig.FilePrefix = filePrefix
58+
if slaveID != "" {
59+
slaveConfig.ID = slaveID
60+
}
61+
if slaveConfig.IP == "" {
62+
log.Fatal("Please set the IP address using --slave-ip flag or in the configuration file")
63+
}
64+
if slaveConfig.FilePrefix == "" {
65+
slaveConfig.FilePrefix = slaveConfig.IP
66+
}
67+
68+
// Validate required parameters
69+
if masterIP == "" {
70+
log.Fatal("Master IP address is required. Use --master-ip flag")
71+
}
72+
73+
log.Infof("Starting slave node on port %d, connecting to master %s:%d", port, masterIP, masterPort)
74+
75+
// Create context
76+
ctx, cancel := context.WithCancel(context.Background())
77+
defer cancel()
78+
79+
// Create slave server
80+
server := slave.NewServer(slaveConfig.Port, slaveConfig.FilePrefix)
81+
82+
// Create slave client
83+
client := slave.NewClient(slaveConfig)
84+
85+
// Start slave server
86+
go func() {
87+
if err := server.Start(ctx); err != nil {
88+
log.Errorf("Slave server failed: %v", err)
89+
cancel()
90+
}
91+
}()
92+
93+
// Register to master and start heartbeat with retry logic
94+
go func() {
95+
for {
96+
select {
97+
case <-ctx.Done():
98+
return
99+
default:
100+
if err := client.RegisterAndStartHeartbeat(ctx); err != nil {
101+
log.Errorf("Failed to register with master: %v", err)
102+
log.Info("Will retry registration in 30 seconds...")
103+
104+
// Wait 30 seconds before retrying, but respect context cancellation
105+
select {
106+
case <-ctx.Done():
107+
return
108+
case <-time.After(30 * time.Second):
109+
log.Info("Retrying registration with master...")
110+
continue
111+
}
112+
} else {
113+
// Registration successful, heartbeat will continue in background
114+
log.Info("Successfully registered with master, heartbeat started")
115+
return
116+
}
117+
}
118+
}
119+
}()
120+
121+
// Wait for signal
122+
shutdownChan := make(chan os.Signal, 1)
123+
signal.Notify(shutdownChan, syscall.SIGINT, syscall.SIGTERM)
124+
125+
select {
126+
case <-shutdownChan:
127+
log.Info("Slave shutdown initiated...")
128+
case <-ctx.Done():
129+
log.Info("Slave context cancelled...")
130+
}
131+
132+
// Graceful shutdown: unregister first, then stop service
133+
log.Info("Unregistering from master...")
134+
if err := client.Unregister(context.Background()); err != nil {
135+
log.Errorf("Failed to unregister from master: %v", err)
136+
}
137+
138+
log.Info("Slave stopped")
139+
},
140+
}
141+
142+
cmd.Flags().IntVarP(&port, "slave-port", "p", 22525, "Port to listen on")
143+
cmd.Flags().StringVarP(&masterIP, "master-ip", "m", "", "Master IP address (required)")
144+
cmd.Flags().IntVar(&masterPort, "master-port", 22525, "Master port (default: 22525)")
145+
cmd.Flags().StringVar(&slaveID, "slave-id", "", "Slave ID (auto-generated if not provided)")
146+
cmd.Flags().StringVar(&filePrefix, "file-prefix", "", "File folder prefix for uploaded files (e.g., 'device1' creates 'device1/filename.log')")
147+
cmd.Flags().StringVar(&ip, "slave-ip", "", "IP address of this slave (required)")
148+
return cmd
149+
}

0 commit comments

Comments
 (0)