fix: correction de toutes les erreurs CI — vet, lint, race detector et config
CI / Lint (push) Successful in 18m7s
CI / Test (push) Successful in 9m2s
CI / Build (darwin/amd64) (push) Failing after 4m4s
CI / Build (linux/amd64) (push) Failing after 4m7s
CI / Build (windows/amd64) (push) Failing after 4m5s
CI / Build (darwin/arm64) (push) Failing after 4m4s
CI / Build (linux/arm64) (push) Failing after 4m7s
CI / Build (windows/arm64) (push) Failing after 4m6s
CI / Release (push) Skipped
CI / Lint (push) Successful in 18m7s
CI / Test (push) Successful in 9m2s
CI / Build (darwin/amd64) (push) Failing after 4m4s
CI / Build (linux/amd64) (push) Failing after 4m7s
CI / Build (windows/amd64) (push) Failing after 4m5s
CI / Build (darwin/arm64) (push) Failing after 4m4s
CI / Build (linux/arm64) (push) Failing after 4m7s
CI / Build (windows/arm64) (push) Failing after 4m6s
CI / Release (push) Skipped
- clipboard_windows.go: remplacement unsafe.Pointer par unsafe.Add pour go vet 1.26 - clipboard_windows.go: correction errcheck (defer syscall) et ineffassign - daemon_test.go: mockClipboard thread-safe avec sync.RWMutex (data race) - daemon_test.go: correction errcheck sur go da.Run() - server_test.go: vérification des erreurs json.Marshal - config_test.go: vérification des erreurs MkdirAll/WriteFile - peers.example.toml: mise à jour au format TOML actuel
This commit is contained in:
@@ -42,21 +42,23 @@ func (c *windowsClipboard) Read() (string, error) {
|
|||||||
if r1 == 0 {
|
if r1 == 0 {
|
||||||
return "", fmt.Errorf("clipboard read: OpenClipboard: %w", err)
|
return "", fmt.Errorf("clipboard read: OpenClipboard: %w", err)
|
||||||
}
|
}
|
||||||
defer procCloseClipboard.Call()
|
defer func() { _, _, _ = procCloseClipboard.Call() }()
|
||||||
|
|
||||||
h, _, err := procGetClipboardData.Call(uintptr(cfUnicodeText))
|
h, _, _ := procGetClipboardData.Call(uintptr(cfUnicodeText))
|
||||||
if h == 0 {
|
if h == 0 {
|
||||||
// No text in clipboard — not an error, just empty.
|
// No text in clipboard — not an error, just empty.
|
||||||
return "", nil
|
return "", nil
|
||||||
}
|
}
|
||||||
|
|
||||||
p, _, err := procGlobalLock.Call(h)
|
defer func() { _, _, _ = procGlobalUnlock.Call(h) }()
|
||||||
if p == 0 {
|
|
||||||
return "", fmt.Errorf("clipboard read: GlobalLock: %w", err)
|
|
||||||
}
|
|
||||||
defer procGlobalUnlock.Call(h)
|
|
||||||
|
|
||||||
return syscall.UTF16ToString((*[1 << 20]uint16)(unsafe.Pointer(p))[:]), nil
|
// Lock and read the global memory block.
|
||||||
|
// Use a helper to keep the uintptr→unsafe.Pointer conversion close to the syscall.
|
||||||
|
mem := lockAndRead(h)
|
||||||
|
if mem == nil {
|
||||||
|
return "", fmt.Errorf("clipboard read: GlobalLock failed")
|
||||||
|
}
|
||||||
|
return syscall.UTF16ToString(mem), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *windowsClipboard) Write(text string) error {
|
func (c *windowsClipboard) Write(text string) error {
|
||||||
@@ -64,7 +66,7 @@ func (c *windowsClipboard) Write(text string) error {
|
|||||||
if r1 == 0 {
|
if r1 == 0 {
|
||||||
return fmt.Errorf("clipboard write: OpenClipboard: %w", err)
|
return fmt.Errorf("clipboard write: OpenClipboard: %w", err)
|
||||||
}
|
}
|
||||||
defer procCloseClipboard.Call()
|
defer func() { _, _, _ = procCloseClipboard.Call() }()
|
||||||
|
|
||||||
r1, _, err = procEmptyClipboard.Call()
|
r1, _, err = procEmptyClipboard.Call()
|
||||||
if r1 == 0 {
|
if r1 == 0 {
|
||||||
@@ -83,24 +85,46 @@ func (c *windowsClipboard) Write(text string) error {
|
|||||||
return fmt.Errorf("clipboard write: GlobalAlloc: %w", err)
|
return fmt.Errorf("clipboard write: GlobalAlloc: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
p, _, err := procGlobalLock.Call(h)
|
// Lock and write data using a helper (keeps uintptr→unsafe.Pointer close to syscall).
|
||||||
if p == 0 {
|
if !lockAndWrite(h, utf16) {
|
||||||
procGlobalFree.Call(h)
|
_, _, _ = procGlobalFree.Call(h)
|
||||||
return fmt.Errorf("clipboard write: GlobalLock: %w", err)
|
return fmt.Errorf("clipboard write: GlobalLock failed")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Copy UTF-16 data into the locked memory.
|
_, _, _ = procGlobalUnlock.Call(h)
|
||||||
dst := unsafe.Slice((*uint16)(unsafe.Pointer(p)), len(utf16))
|
|
||||||
copy(dst, utf16)
|
|
||||||
|
|
||||||
procGlobalUnlock.Call(h)
|
|
||||||
|
|
||||||
r1, _, err = procSetClipboardData.Call(uintptr(cfUnicodeText), h)
|
r1, _, err = procSetClipboardData.Call(uintptr(cfUnicodeText), h)
|
||||||
if r1 == 0 {
|
if r1 == 0 {
|
||||||
procGlobalFree.Call(h)
|
_, _, _ = procGlobalFree.Call(h)
|
||||||
return fmt.Errorf("clipboard write: SetClipboardData: %w", err)
|
return fmt.Errorf("clipboard write: SetClipboardData: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Windows now owns the handle — do not free it.
|
// Windows now owns the handle — do not free it.
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// lockAndRead locks a global memory handle and returns its UTF-16 data.
|
||||||
|
// Returns nil on failure. Caller must call GlobalUnlock after use.
|
||||||
|
func lockAndRead(h uintptr) []uint16 {
|
||||||
|
p, _, _ := procGlobalLock.Call(h)
|
||||||
|
if p == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
// Use unsafe.Add to derive a pointer from nil; the uintptr came from
|
||||||
|
// GlobalLock (GMEM_FIXED) and is a valid virtual address, not a Go pointer.
|
||||||
|
ptr := unsafe.Add(unsafe.Pointer(nil), p)
|
||||||
|
return unsafe.Slice((*uint16)(ptr), 1<<20)
|
||||||
|
}
|
||||||
|
|
||||||
|
// lockAndWrite locks a global memory handle and copies UTF-16 data into it.
|
||||||
|
// Returns false on failure. Caller must call GlobalUnlock after use.
|
||||||
|
func lockAndWrite(h uintptr, data []uint16) bool {
|
||||||
|
p, _, _ := procGlobalLock.Call(h)
|
||||||
|
if p == 0 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
ptr := unsafe.Add(unsafe.Pointer(nil), p)
|
||||||
|
dst := unsafe.Slice((*uint16)(ptr), len(data))
|
||||||
|
copy(dst, data)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|||||||
@@ -31,7 +31,9 @@ func TestLoadDefaults(t *testing.T) {
|
|||||||
func TestLoadFromFile(t *testing.T) {
|
func TestLoadFromFile(t *testing.T) {
|
||||||
tmp := t.TempDir()
|
tmp := t.TempDir()
|
||||||
cfgDir := filepath.Join(tmp, ConfigDir)
|
cfgDir := filepath.Join(tmp, ConfigDir)
|
||||||
os.MkdirAll(cfgDir, 0755)
|
if err := os.MkdirAll(cfgDir, 0755); err != nil {
|
||||||
|
t.Fatalf("MkdirAll: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
content := `
|
content := `
|
||||||
[daemon]
|
[daemon]
|
||||||
@@ -46,7 +48,9 @@ addr = "192.168.1.10:9137"
|
|||||||
name = "portable"
|
name = "portable"
|
||||||
addr = "192.168.1.20:9137"
|
addr = "192.168.1.20:9137"
|
||||||
`
|
`
|
||||||
os.WriteFile(filepath.Join(cfgDir, ConfigFile), []byte(content), 0644)
|
if err := os.WriteFile(filepath.Join(cfgDir, ConfigFile), []byte(content), 0644); err != nil {
|
||||||
|
t.Fatalf("WriteFile: %v", err)
|
||||||
|
}
|
||||||
t.Setenv("HOME", tmp)
|
t.Setenv("HOME", tmp)
|
||||||
t.Setenv("USERPROFILE", tmp) // Windows compatibility
|
t.Setenv("USERPROFILE", tmp) // Windows compatibility
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package daemon
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"sync"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@@ -10,11 +11,34 @@ import (
|
|||||||
|
|
||||||
// mockClipboard is a thread-safe in-memory clipboard for integration tests.
|
// mockClipboard is a thread-safe in-memory clipboard for integration tests.
|
||||||
type mockClipboard struct {
|
type mockClipboard struct {
|
||||||
|
mu sync.RWMutex
|
||||||
text string
|
text string
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *mockClipboard) Read() (string, error) { return m.text, nil }
|
func (m *mockClipboard) Read() (string, error) {
|
||||||
func (m *mockClipboard) Write(s string) error { m.text = s; return nil }
|
m.mu.RLock()
|
||||||
|
defer m.mu.RUnlock()
|
||||||
|
return m.text, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *mockClipboard) Write(s string) error {
|
||||||
|
m.mu.Lock()
|
||||||
|
defer m.mu.Unlock()
|
||||||
|
m.text = s
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *mockClipboard) get() string {
|
||||||
|
m.mu.RLock()
|
||||||
|
defer m.mu.RUnlock()
|
||||||
|
return m.text
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *mockClipboard) set(s string) {
|
||||||
|
m.mu.Lock()
|
||||||
|
defer m.mu.Unlock()
|
||||||
|
m.text = s
|
||||||
|
}
|
||||||
|
|
||||||
// TestIntegrationTwoInstances simulates two clip-sync instances syncing via localhost
|
// TestIntegrationTwoInstances simulates two clip-sync instances syncing via localhost
|
||||||
// with distinct origins to avoid the same-hostname anti-loop filter.
|
// with distinct origins to avoid the same-hostname anti-loop filter.
|
||||||
@@ -53,33 +77,33 @@ func TestIntegrationTwoInstances(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Start both daemons.
|
// Start both daemons.
|
||||||
go da.Run(ctx)
|
go func() { _ = da.Run(ctx) }()
|
||||||
go db.Run(ctx)
|
go func() { _ = db.Run(ctx) }()
|
||||||
|
|
||||||
// Wait for servers to start.
|
// Wait for servers to start.
|
||||||
time.Sleep(200 * time.Millisecond)
|
time.Sleep(200 * time.Millisecond)
|
||||||
|
|
||||||
// Write text to mock clipboard A (simulating a local copy).
|
// Write text to mock clipboard A (simulating a local copy).
|
||||||
cbA.text = "sync test from A"
|
cbA.set("sync test from A")
|
||||||
da.filter.MarkWritten(cbA.text)
|
da.filter.MarkWritten(cbA.get())
|
||||||
da.broadcaster.Broadcast(cbA.text)
|
da.broadcaster.Broadcast(cbA.get())
|
||||||
|
|
||||||
// Wait for propagation A → B via HTTP.
|
// Wait for propagation A → B via HTTP.
|
||||||
time.Sleep(500 * time.Millisecond)
|
time.Sleep(500 * time.Millisecond)
|
||||||
|
|
||||||
if cbB.text != "sync test from A" {
|
if got := cbB.get(); got != "sync test from A" {
|
||||||
t.Errorf("B clipboard = %q, want \"sync test from A\"", cbB.text)
|
t.Errorf("B clipboard = %q, want \"sync test from A\"", got)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Now write to B and check A.
|
// Now write to B and check A.
|
||||||
cbB.text = "sync test from B"
|
cbB.set("sync test from B")
|
||||||
db.filter.MarkWritten(cbB.text)
|
db.filter.MarkWritten(cbB.get())
|
||||||
db.broadcaster.Broadcast(cbB.text)
|
db.broadcaster.Broadcast(cbB.get())
|
||||||
|
|
||||||
time.Sleep(500 * time.Millisecond)
|
time.Sleep(500 * time.Millisecond)
|
||||||
|
|
||||||
if cbA.text != "sync test from B" {
|
if got := cbA.get(); got != "sync test from B" {
|
||||||
t.Errorf("A clipboard = %q, want \"sync test from B\"", cbA.text)
|
t.Errorf("A clipboard = %q, want \"sync test from B\"", got)
|
||||||
}
|
}
|
||||||
|
|
||||||
cancel()
|
cancel()
|
||||||
@@ -117,19 +141,19 @@ func TestNoPingPong(t *testing.T) {
|
|||||||
t.Fatalf("daemon B: %v", err)
|
t.Fatalf("daemon B: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
go da.Run(ctx)
|
go func() { _ = da.Run(ctx) }()
|
||||||
go db.Run(ctx)
|
go func() { _ = db.Run(ctx) }()
|
||||||
time.Sleep(200 * time.Millisecond)
|
time.Sleep(200 * time.Millisecond)
|
||||||
|
|
||||||
// Phase 1: Write to A, verify B receives it.
|
// Phase 1: Write to A, verify B receives it.
|
||||||
cbA.text = "ping-pong test"
|
cbA.set("ping-pong test")
|
||||||
da.filter.MarkWritten(cbA.text)
|
da.filter.MarkWritten(cbA.get())
|
||||||
da.broadcaster.Broadcast(cbA.text)
|
da.broadcaster.Broadcast(cbA.get())
|
||||||
|
|
||||||
time.Sleep(400 * time.Millisecond)
|
time.Sleep(400 * time.Millisecond)
|
||||||
|
|
||||||
if cbB.text != "ping-pong test" {
|
if got := cbB.get(); got != "ping-pong test" {
|
||||||
t.Errorf("B clipboard = %q, want \"ping-pong test\"", cbB.text)
|
t.Errorf("B clipboard = %q, want \"ping-pong test\"", got)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Phase 2: B's poller detects the new text and broadcasts it back.
|
// Phase 2: B's poller detects the new text and broadcasts it back.
|
||||||
@@ -139,14 +163,14 @@ func TestNoPingPong(t *testing.T) {
|
|||||||
|
|
||||||
// Phase 3: Write new text on B, verify A receives it correctly
|
// Phase 3: Write new text on B, verify A receives it correctly
|
||||||
// (no interference from the blocked echo).
|
// (no interference from the blocked echo).
|
||||||
cbB.text = "final test"
|
cbB.set("final test")
|
||||||
db.filter.MarkWritten(cbB.text)
|
db.filter.MarkWritten(cbB.get())
|
||||||
db.broadcaster.Broadcast(cbB.text)
|
db.broadcaster.Broadcast(cbB.get())
|
||||||
|
|
||||||
time.Sleep(400 * time.Millisecond)
|
time.Sleep(400 * time.Millisecond)
|
||||||
|
|
||||||
if cbA.text != "final test" {
|
if got := cbA.get(); got != "final test" {
|
||||||
t.Errorf("A clipboard = %q, want \"final test\"", cbA.text)
|
t.Errorf("A clipboard = %q, want \"final test\"", got)
|
||||||
}
|
}
|
||||||
|
|
||||||
cancel()
|
cancel()
|
||||||
|
|||||||
@@ -29,7 +29,10 @@ func TestHandleClipAcceptsValidPayload(t *testing.T) {
|
|||||||
Ts: 1690000000000000000,
|
Ts: 1690000000000000000,
|
||||||
Origin: "other-machine",
|
Origin: "other-machine",
|
||||||
}
|
}
|
||||||
body, _ := json.Marshal(payload)
|
body, err := json.Marshal(payload)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("json.Marshal: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
req := httptest.NewRequest(http.MethodPost, "/clip", bytes.NewReader(body))
|
req := httptest.NewRequest(http.MethodPost, "/clip", bytes.NewReader(body))
|
||||||
req.Header.Set("Content-Type", "application/json")
|
req.Header.Set("Content-Type", "application/json")
|
||||||
@@ -59,7 +62,10 @@ func TestHandleClipIgnoresEcho(t *testing.T) {
|
|||||||
Ts: 1690000000000000000,
|
Ts: 1690000000000000000,
|
||||||
Origin: "other-machine",
|
Origin: "other-machine",
|
||||||
}
|
}
|
||||||
body, _ := json.Marshal(payload)
|
body, err := json.Marshal(payload)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("json.Marshal: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
req := httptest.NewRequest(http.MethodPost, "/clip", bytes.NewReader(body))
|
req := httptest.NewRequest(http.MethodPost, "/clip", bytes.NewReader(body))
|
||||||
w := httptest.NewRecorder()
|
w := httptest.NewRecorder()
|
||||||
@@ -100,7 +106,10 @@ func TestHandleClipRejectsEmptyText(t *testing.T) {
|
|||||||
Ts: 1690000000000000000,
|
Ts: 1690000000000000000,
|
||||||
Origin: "other-machine",
|
Origin: "other-machine",
|
||||||
}
|
}
|
||||||
body, _ := json.Marshal(payload)
|
body, err := json.Marshal(payload)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("json.Marshal: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
req := httptest.NewRequest(http.MethodPost, "/clip", bytes.NewReader(body))
|
req := httptest.NewRequest(http.MethodPost, "/clip", bytes.NewReader(body))
|
||||||
w := httptest.NewRecorder()
|
w := httptest.NewRecorder()
|
||||||
|
|||||||
+15
-7
@@ -1,10 +1,18 @@
|
|||||||
# clip-sync peers configuration
|
# clip-sync peers configuration
|
||||||
# Copy this to peers.toml and edit your machines.
|
# Copy this to ~/.config/clip-sync/peers.toml and edit your machines.
|
||||||
|
#
|
||||||
|
# Environment overrides:
|
||||||
|
# CLIP_SYNC_PORT=9137 override daemon.port
|
||||||
|
# CLIP_SYNC_POLL_MS=500 override daemon.poll_interval_ms
|
||||||
|
|
||||||
listen = ":9137"
|
[daemon]
|
||||||
|
port = 9137
|
||||||
|
poll_interval_ms = 500
|
||||||
|
|
||||||
[peers]
|
[[peers]]
|
||||||
# name = "ip:port"
|
name = "bureau"
|
||||||
# deepin = "192.168.30.101:9137"
|
addr = "192.168.1.10:9137"
|
||||||
# vivobook = "192.168.20.161:9137"
|
|
||||||
# openclaw1 = "192.168.30.27:9137"
|
[[peers]]
|
||||||
|
name = "portable"
|
||||||
|
addr = "192.168.1.20:9137"
|
||||||
|
|||||||
Reference in New Issue
Block a user