Skip to content

Commit 62e3f2b

Browse files
authored
Merge pull request #15 from sqlrsync/v0.0.5
v0.0.5 release
2 parents 0e972e6 + fa8e0d6 commit 62e3f2b

File tree

8 files changed

+194
-14
lines changed

8 files changed

+194
-14
lines changed

client/auth/config.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import (
1515
type DefaultsConfig struct {
1616
Defaults struct {
1717
Server string `toml:"server"`
18+
WsID string `toml:"wsID"`
1819
} `toml:"defaults"`
1920
}
2021

client/config.go

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,12 +9,14 @@ import (
99
"time"
1010

1111
"github.com/BurntSushi/toml"
12+
"github.com/google/uuid"
1213
)
1314

1415
// .config/sqlrsync/defaults.toml
1516
type DefaultsConfig struct {
1617
Defaults struct {
1718
Server string `toml:"server"`
19+
WsID string `toml:"wsID"`
1820
} `toml:"defaults"`
1921
}
2022

@@ -78,6 +80,14 @@ func LoadDefaultsConfig() (*DefaultsConfig, error) {
7880
// Return default config if file doesn't exist
7981
config := &DefaultsConfig{}
8082
config.Defaults.Server = "wss://sqlrsync.com"
83+
// Generate wsID if it doesn't exist
84+
if err := generateAndSetWsID(config); err != nil {
85+
return nil, fmt.Errorf("failed to generate wsID: %w", err)
86+
}
87+
// Save the new config with wsID
88+
if err := SaveDefaultsConfig(config); err != nil {
89+
return nil, fmt.Errorf("failed to save defaults config with wsID: %w", err)
90+
}
8191
return config, nil
8292
}
8393
return nil, fmt.Errorf("failed to read defaults config file %s: %w", path, err)
@@ -93,9 +103,45 @@ func LoadDefaultsConfig() (*DefaultsConfig, error) {
93103
config.Defaults.Server = "wss://sqlrsync.com"
94104
}
95105

106+
// Generate wsID if it doesn't exist
107+
needsSave := false
108+
if config.Defaults.WsID == "" {
109+
if err := generateAndSetWsID(&config); err != nil {
110+
return nil, fmt.Errorf("failed to generate wsID: %w", err)
111+
}
112+
needsSave = true
113+
}
114+
115+
// Save config if we made changes
116+
if needsSave {
117+
if err := SaveDefaultsConfig(&config); err != nil {
118+
return nil, fmt.Errorf("failed to save defaults config with wsID: %w", err)
119+
}
120+
}
121+
96122
return &config, nil
97123
}
98124

125+
// generateAndSetWsID generates a new wsID (UUID + hostname) and sets it in the config
126+
func generateAndSetWsID(config *DefaultsConfig) error {
127+
hostname, err := os.Hostname()
128+
if err != nil {
129+
return fmt.Errorf("failed to get hostname: %w", err)
130+
}
131+
132+
config.Defaults.WsID = hostname + ":" + uuid.New().String()
133+
return nil
134+
}
135+
136+
// GetWsID loads the defaults config and returns the wsID
137+
func GetWsID() (string, error) {
138+
config, err := LoadDefaultsConfig()
139+
if err != nil {
140+
return "", fmt.Errorf("failed to load defaults config: %w", err)
141+
}
142+
return config.Defaults.WsID, nil
143+
}
144+
99145
func SaveDefaultsConfig(config *DefaultsConfig) error {
100146
path, err := GetDefaultsPath()
101147
if err != nil {

client/go.mod

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ go 1.24.5
55
require (
66
github.com/BurntSushi/toml v1.5.0
77
github.com/fatih/color v1.18.0
8+
github.com/google/uuid v1.6.0
89
github.com/gorilla/websocket v1.5.0
910
github.com/spf13/cobra v1.8.0
1011
github.com/sqlrsync/sqlrsync.com/bridge v0.0.0-00010101000000-000000000000

client/go.sum

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c
55
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
66
github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM=
77
github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU=
8+
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
9+
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
810
github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc=
911
github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
1012
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=

client/main.go

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ import (
1515
"github.com/sqlrsync/sqlrsync.com/sync"
1616
)
1717

18-
var VERSION = "0.0.4"
18+
var VERSION = "0.0.5"
1919
var (
2020
serverURL string
2121
verbose bool
@@ -145,6 +145,13 @@ func runSync(cmd *cobra.Command, args []string) error {
145145
visibility = 1
146146
}
147147

148+
// Get workspace ID for client identification
149+
wsID, err := GetWsID()
150+
if err != nil {
151+
logger.Warn("Failed to get workspace ID", zap.Error(err))
152+
wsID = "" // Continue with empty wsID
153+
}
154+
148155
// Create sync coordinator
149156
coordinator := sync.NewCoordinator(&sync.CoordinatorConfig{
150157
ServerURL: serverURL,
@@ -162,6 +169,8 @@ func runSync(cmd *cobra.Command, args []string) error {
162169
DryRun: dryRun,
163170
Logger: logger,
164171
Verbose: verbose,
172+
WsID: wsID, // Add websocket ID
173+
ClientVersion: VERSION,
165174
})
166175

167176
// Execute the operation

client/remote/client.go

Lines changed: 61 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -402,11 +402,13 @@ type Config struct {
402402
InspectionDepth int // How many bytes to inspect (default: 32)
403403
PingPong bool
404404
AuthToken string
405+
ClientVersion string // version of the client software
405406
SendKeyRequest bool // the -sqlrsync file doesn't exist, so make a token
406407

407408
SendConfigCmd bool // we don't have the version number or remote path
408409
LocalHostname string
409410
LocalAbsolutePath string
411+
WsID string // Workspace ID for X-ClientID header
410412

411413
// Progress tracking
412414
ProgressConfig *ProgressConfig
@@ -685,6 +687,14 @@ func (c *Client) Connect() error {
685687

686688
headers.Set("Authorization", c.config.AuthToken)
687689

690+
headers.Set("X-ClientVersion", c.config.ClientVersion);
691+
692+
if c.config.WsID != "" {
693+
headers.Set("X-ClientID", c.config.WsID)
694+
} else {
695+
c.logger.Fatal("No wsID provided for X-ClientID header")
696+
}
697+
688698
if c.config.LocalHostname != "" {
689699
headers.Set("X-LocalHostname", c.config.LocalHostname)
690700
}
@@ -703,12 +713,58 @@ func (c *Client) Connect() error {
703713

704714
conn, response, err := dialer.DialContext(connectCtx, u.String(), headers)
705715
if err != nil {
706-
if response != nil && response.Body != nil {
707-
respStr, _ := io.ReadAll(response.Body)
708-
response.Body.Close()
709-
return fmt.Errorf("%s", respStr)
716+
if response != nil {
717+
// Extract detailed error information from the response
718+
statusCode := response.StatusCode
719+
statusText := response.Status
720+
721+
var respBodyStr string
722+
if response.Body != nil {
723+
respBytes, readErr := io.ReadAll(response.Body)
724+
response.Body.Close()
725+
if readErr == nil {
726+
respBodyStr = strings.TrimSpace(string(respBytes))
727+
}
728+
}
729+
730+
// Create a clean error message
731+
var errorMsg strings.Builder
732+
errorMsg.WriteString(fmt.Sprintf("HTTP %d (%s)", statusCode, statusText))
733+
734+
if respBodyStr != "" {
735+
errorMsg.WriteString(fmt.Sprintf(": %s", respBodyStr))
736+
}
737+
738+
return fmt.Errorf("%s", errorMsg.String())
710739
}
711-
return fmt.Errorf("failed to connect to WebSocket: %w", err)
740+
741+
// Handle cases where response is nil (e.g., network errors, bad handshake)
742+
var errorMsg strings.Builder
743+
errorMsg.WriteString("Failed to connect to WebSocket")
744+
745+
// Analyze the error type and provide helpful context
746+
errorStr := err.Error()
747+
if strings.Contains(errorStr, "bad handshake") {
748+
errorMsg.WriteString(" - WebSocket handshake failed")
749+
errorMsg.WriteString("\nThis could be due to:")
750+
errorMsg.WriteString("\n• Invalid server URL or endpoint")
751+
errorMsg.WriteString("\n• Server not supporting WebSocket connections")
752+
errorMsg.WriteString("\n• Network connectivity issues")
753+
errorMsg.WriteString("\n• Authentication problems")
754+
} else if strings.Contains(errorStr, "timeout") {
755+
errorMsg.WriteString(" - Connection timeout")
756+
errorMsg.WriteString("\nThe server may be overloaded or unreachable")
757+
} else if strings.Contains(errorStr, "refused") {
758+
errorMsg.WriteString(" - Connection refused")
759+
errorMsg.WriteString("\nThe server may be down or the port may be blocked")
760+
} else if strings.Contains(errorStr, "no such host") {
761+
errorMsg.WriteString(" - DNS resolution failed")
762+
errorMsg.WriteString("\nCheck the server hostname in your configuration")
763+
}
764+
765+
errorMsg.WriteString(fmt.Sprintf("\nOriginal error: %v", err))
766+
767+
return fmt.Errorf("%s", errorMsg.String())
712768
}
713769
defer response.Body.Close()
714770

client/subscription/manager.go

Lines changed: 63 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import (
44
"context"
55
"encoding/json"
66
"fmt"
7+
"io"
78
"net/http"
89
"net/url"
910
"strings"
@@ -33,12 +34,14 @@ type Message struct {
3334
Timestamp time.Time `json:"timestamp"`
3435
}
3536

36-
// Config holds subscription manager configuration
37-
type Config struct {
37+
// ManagerConfig holds subscription manager configuration
38+
type ManagerConfig struct {
3839
ServerURL string
3940
ReplicaPath string
4041
AuthToken string
4142
ReplicaID string
43+
WsID string // websocket ID for client identification
44+
ClientVersion string // version of the client software
4245
Logger *zap.Logger
4346
MaxReconnectAttempts int // Maximum number of reconnect attempts (0 = infinite)
4447
InitialReconnectDelay time.Duration // Initial delay before first reconnect
@@ -53,7 +56,7 @@ type Config struct {
5356
// MaxReconnectDelay is reached. Reconnection attempts continue indefinitely unless
5457
// MaxReconnectAttempts is set to a positive value.
5558
type Manager struct {
56-
config *Config
59+
config *ManagerConfig
5760
logger *zap.Logger
5861
conn *websocket.Conn
5962
ctx context.Context
@@ -72,7 +75,7 @@ type Manager struct {
7275
}
7376

7477
// NewManager creates a new subscription manager
75-
func NewManager(config *Config) *Manager {
78+
func NewManager(config *ManagerConfig) *Manager {
7679
ctx, cancel := context.WithCancel(context.Background())
7780

7881
// Set default reconnection parameters if not provided
@@ -201,15 +204,69 @@ func (m *Manager) doConnect() error {
201204
headers.Set("X-ReplicaID", m.config.ReplicaID)
202205
}
203206

207+
headers.Set("X-ClientVersion", m.config.ClientVersion)
208+
headers.Set("X-ClientID", m.config.WsID)
209+
204210
dialer := websocket.Dialer{
205211
HandshakeTimeout: 10 * time.Second,
206212
}
207213

208214
m.logger.Debug("Dialing WebSocket", zap.String("url", u.String()))
209215

210-
conn, _, err := dialer.DialContext(m.ctx, u.String(), headers)
216+
conn, response, err := dialer.DialContext(m.ctx, u.String(), headers)
211217
if err != nil {
212-
return fmt.Errorf("failed to connect to subscription service: %w", err)
218+
if response != nil {
219+
// Extract detailed error information from the response
220+
statusCode := response.StatusCode
221+
statusText := response.Status
222+
223+
var respBodyStr string
224+
if response.Body != nil {
225+
respBytes, readErr := io.ReadAll(response.Body)
226+
response.Body.Close()
227+
if readErr == nil {
228+
respBodyStr = strings.TrimSpace(string(respBytes))
229+
}
230+
}
231+
232+
// Create a clean error message
233+
var errorMsg strings.Builder
234+
errorMsg.WriteString(fmt.Sprintf("HTTP %d (%s)", statusCode, statusText))
235+
236+
if respBodyStr != "" {
237+
errorMsg.WriteString(fmt.Sprintf(": %s", respBodyStr))
238+
}
239+
240+
return fmt.Errorf("%s", errorMsg.String())
241+
}
242+
243+
// Handle cases where response is nil (e.g., network errors, bad handshake)
244+
var errorMsg strings.Builder
245+
errorMsg.WriteString("Failed to connect to subscription service")
246+
247+
// Analyze the error type and provide helpful context
248+
errorStr := err.Error()
249+
if strings.Contains(errorStr, "bad handshake") {
250+
errorMsg.WriteString(" - WebSocket handshake failed")
251+
errorMsg.WriteString("\nThis could be due to:")
252+
errorMsg.WriteString("\n• Invalid server URL or endpoint")
253+
errorMsg.WriteString("\n• Server not supporting WebSocket connections")
254+
errorMsg.WriteString("\n• Network connectivity issues")
255+
errorMsg.WriteString("\n• Authentication problems")
256+
} else if strings.Contains(errorStr, "timeout") {
257+
errorMsg.WriteString(" - Connection timeout")
258+
errorMsg.WriteString("\nThe server may be overloaded or unreachable")
259+
} else if strings.Contains(errorStr, "refused") {
260+
errorMsg.WriteString(" - Connection refused")
261+
errorMsg.WriteString("\nThe server may be down or the port may be blocked")
262+
} else if strings.Contains(errorStr, "no such host") {
263+
errorMsg.WriteString(" - DNS resolution failed")
264+
errorMsg.WriteString("\nCheck the server hostname in your configuration")
265+
}
266+
267+
errorMsg.WriteString(fmt.Sprintf("\nOriginal error: %v", err))
268+
269+
return fmt.Errorf("%s", errorMsg.String())
213270
}
214271

215272
m.mu.Lock()

client/sync/coordinator.go

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,8 @@ type CoordinatorConfig struct {
4646
DryRun bool
4747
Logger *zap.Logger
4848
Verbose bool
49+
WsID string // Websocket ID for client identification
50+
ClientVersion string // version of the client software
4951
}
5052

5153
// Coordinator manages sync operations and subscriptions
@@ -240,11 +242,13 @@ func (c *Coordinator) executeSubscribe() error {
240242
}
241243

242244
// Create subscription manager with reconnection configuration
243-
c.subManager = subscription.NewManager(&subscription.Config{
245+
c.subManager = subscription.NewManager(&subscription.ManagerConfig{
244246
ServerURL: authResult.ServerURL,
245247
ReplicaPath: authResult.RemotePath,
246248
AuthToken: authResult.AccessToken,
247249
ReplicaID: authResult.ReplicaID,
250+
WsID: c.config.WsID,
251+
ClientVersion: c.config.ClientVersion,
248252
Logger: c.logger.Named("subscription"),
249253
MaxReconnectAttempts: 20, // Infinite reconnect attempts
250254
InitialReconnectDelay: 5 * time.Second, // Start with 5 seconds delay
@@ -373,6 +377,8 @@ func (c *Coordinator) executePull(isSubscription bool) error {
373377
Version: version,
374378
SendConfigCmd: true,
375379
SendKeyRequest: c.authResolver.CheckNeedsDashFile(c.config.LocalPath, remotePath),
380+
WsID: c.config.WsID, // Add websocket ID
381+
ClientVersion: c.config.ClientVersion,
376382
//ProgressCallback: remote.DefaultProgressCallback(remote.FormatSimple),
377383
ProgressCallback: nil,
378384
ProgressConfig: &remote.ProgressConfig{
@@ -499,7 +505,9 @@ func (c *Coordinator) executePush() error {
499505
SendConfigCmd: true,
500506
SetVisibility: c.config.SetVisibility,
501507
CommitMessage: c.config.CommitMessage,
502-
ProgressCallback: nil, //remote.DefaultProgressCallback(remote.FormatSimple),
508+
WsID: c.config.WsID, // Add websocket ID
509+
ClientVersion: c.config.ClientVersion,
510+
ProgressCallback: nil, //remote.DefaultProgressCallback(remote.FormatSimple),
503511
ProgressConfig: &remote.ProgressConfig{
504512
Enabled: true,
505513
Format: remote.FormatSimple,

0 commit comments

Comments
 (0)