Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| c7f1d854bd | |||
| f877dfed4e |
@@ -22,7 +22,6 @@ jobs:
|
||||
if: github.repository_owner == 'syncthing'
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
pkg:
|
||||
- stcrashreceiver
|
||||
|
||||
@@ -164,7 +164,7 @@ jobs:
|
||||
needs:
|
||||
- build-test
|
||||
- package-linux
|
||||
- package-illumos
|
||||
# - package-illumos
|
||||
- package-cross
|
||||
- package-source
|
||||
- package-debian
|
||||
@@ -646,7 +646,7 @@ jobs:
|
||||
needs:
|
||||
- codesign-windows
|
||||
- package-linux
|
||||
- package-illumos
|
||||
# - package-illumos
|
||||
- package-macos
|
||||
- package-cross
|
||||
- package-source
|
||||
|
||||
@@ -1,76 +0,0 @@
|
||||
2026-05-21 10:03:01 INF syncthing v2.1.1-dev.9.gb3b7d228.dirty-morecrashrep "Hafnium Hornet" (go1.26.3 darwin-arm64) jb@jbo-m3wl72rv 2026-05-21 07:58:11 UTC [stnoupgrade] (log.pkg=main)
|
||||
2026-05-21 10:03:01 INF No automatic upgrades; STNOUPGRADE environment variable defined (log.pkg=main)
|
||||
2026-05-21 10:03:01 INF Calculated our device ID (device=I6KAH76-66SLLLB-5PFXSOA-UFJCDZC-YAOMLEK-CP2GB32-BV5RQST-3PSROAU log.pkg=syncthing)
|
||||
2026-05-21 10:03:01 INF Overall rate limit in use (send="is unlimited" recv="is unlimited" log.pkg=connections)
|
||||
2026-05-21 10:03:01 INF Using discovery mechanism (identity="IPv4 local broadcast discovery on port 21027" log.pkg=discover)
|
||||
2026-05-21 10:03:01 INF Using discovery mechanism (identity="IPv6 local multicast discovery on address [ff12::8384]:21027" log.pkg=discover)
|
||||
2026-05-21 10:03:01 INF TCP listener starting (address=127.0.0.1:22001 log.pkg=connections)
|
||||
2026-05-21 10:03:01 INF Ready to synchronize (folder.id=default folder.type=sendreceive log.pkg=model)
|
||||
2026-05-21 10:03:01 INF QUIC listener starting (address=127.0.0.1:22001 log.pkg=connections)
|
||||
2026-05-21 10:03:01 INF GUI and API listening (address=127.0.0.1:8081 log.pkg=api)
|
||||
...
|
||||
2026-05-21 10:03:01 INF Access the GUI via the following URL: http://127.0.0.1:8081/ (log.pkg=api)
|
||||
2026-05-21 10:03:01 INF Loaded configuration (name=s1 log.pkg=syncthing)
|
||||
2026-05-21 10:03:01 INF Loaded peer device configuration (device=MRIW7OK name=s2 address="[tcp://127.0.0.1:22002 quic://127.0.0.1:22002]" log.pkg=syncthing)
|
||||
2026-05-21 10:03:01 INF Completed initial scan (folder.id=default folder.type=sendreceive log.pkg=model)
|
||||
0xee9de1fe260
|
||||
2026-05-21 10:03:02 INF Measured hashing performance (perf="2789.71 MB/s" log.pkg=syncthing)
|
||||
Panic at 2026-05-21T10:03:02+02:00
|
||||
runtime: marked free object in span 0x108b34d20, elemsize=8 freeindex=34 (bad use of unsafe.Pointer or having race conditions? try -d=checkptr or -race)
|
||||
0xee9de1fe000 alloc marked
|
||||
0xee9de1fe008 alloc marked
|
||||
...
|
||||
0xee9de1fe250 free unmarked
|
||||
0xee9de1fe258 free unmarked
|
||||
0xee9de1fe260 free marked zombie
|
||||
7 6 5 4 3 2 1 0 f e d c b a 9 8 0123456789abcdef
|
||||
00000ee9de1fe260: 00000000 00000000 ........
|
||||
0xee9de1fe268 free unmarked
|
||||
0xee9de1fe270 free unmarked
|
||||
...
|
||||
0xee9de1fff60 free unmarked
|
||||
0xee9de1fff68 free unmarked
|
||||
0xee9de1fff70 free unmarked
|
||||
0xee9de1fff78 free unmarked
|
||||
fatal error: found pointer to free object
|
||||
|
||||
runtime stack:
|
||||
runtime.throw({0x105881781?, 0x8?})
|
||||
runtime/panic.go:1229 +0x38 fp=0x16bf82bb0 sp=0x16bf82b80 pc=0x104f0ca48
|
||||
runtime.(*mspan).reportZombies(0x108b34d20)
|
||||
runtime/mgcsweep.go:893 +0x314 fp=0x16bf82c30 sp=0x16bf82bb0 pc=0x104ec10b4
|
||||
runtime.(*sweepLocked).sweep(0x16bf82d88?, 0x0)
|
||||
runtime/mgcsweep.go:673 +0xbd0 fp=0x16bf82d50 sp=0x16bf82c30 pc=0x104ec0840
|
||||
runtime.(*mcentral).uncacheSpan(0x16bf82db8?, 0x104ea4954?)
|
||||
runtime/mcentral.go:237 +0xbc fp=0x16bf82d80 sp=0x16bf82d50 pc=0x104eaac3c
|
||||
runtime.(*mcache).releaseAll(0x1089a85f0)
|
||||
runtime/mcache.go:322 +0x188 fp=0x16bf82df0 sp=0x16bf82d80 pc=0x104eaa4e8
|
||||
runtime.(*mcache).prepareForSweep(0x1089a85f0)
|
||||
runtime/mcache.go:366 +0x4c fp=0x16bf82e20 sp=0x16bf82df0 pc=0x104eaa61c
|
||||
runtime.gcMarkTermination.func4(0xee9de005808)
|
||||
runtime/mgc.go:1546 +0x24 fp=0x16bf82e50 sp=0x16bf82e20 pc=0x104f076e4
|
||||
runtime.forEachPInternal(0x10656f798)
|
||||
runtime/proc.go:2167 +0x178 fp=0x16bf82ee0 sp=0x16bf82e50 pc=0x104eda728
|
||||
runtime.gcMarkTermination.forEachP.func7()
|
||||
runtime/proc.go:2126 +0x40 fp=0x16bf82f10 sp=0x16bf82ee0 pc=0x104eb3130
|
||||
runtime.systemstack(0x7fc000)
|
||||
runtime/asm_arm64.s:399 +0x68 fp=0x16bf82f20 sp=0x16bf82f10 pc=0x104f12888
|
||||
|
||||
goroutine 84 gp=0xee9de45c1e0 m=3 mp=0xee9de019008 [flushing proc caches]:
|
||||
runtime.systemstack_switch()
|
||||
runtime/asm_arm64.s:347 +0x8 fp=0xee9de805c40 sp=0xee9de805c30 pc=0x104f12808
|
||||
runtime.forEachP(...)
|
||||
runtime/proc.go:2112
|
||||
runtime.gcMarkTermination({0xc0?, 0x1331f928480ca?, 0xc?, 0x0?})
|
||||
runtime/mgc.go:1545 +0x5f4 fp=0xee9de805e80 sp=0xee9de805c40 pc=0x104eb28c4
|
||||
runtime.gcMarkDone()
|
||||
runtime/mgc.go:1173 +0x364 fp=0xee9de805f20 sp=0xee9de805e80 pc=0x104eb1bc4
|
||||
runtime.gcBgMarkWorker(0xee9de341810)
|
||||
runtime/mgc.go:1912 +0x29c fp=0xee9de805fb0 sp=0xee9de805f20 pc=0x104eb372c
|
||||
runtime.gcBgMarkStartWorkers.gowrap1()
|
||||
runtime/mgc.go:1695 +0x20 fp=0xee9de805fd0 sp=0xee9de805fb0 pc=0x104eb3470
|
||||
runtime.goexit({})
|
||||
runtime/asm_arm64.s:1447 +0x4 fp=0xee9de805fd0 sp=0xee9de805fd0 pc=0x104f14a04
|
||||
created by runtime.gcBgMarkStartWorkers in goroutine 1
|
||||
runtime/mgc.go:1695 +0x134
|
||||
|
||||
@@ -136,25 +136,15 @@ func (d *diskStore) Exists(path string) bool {
|
||||
}
|
||||
|
||||
func (d *diskStore) clean() {
|
||||
numDeleted := 0
|
||||
for idx := range d.currentFiles {
|
||||
if len(d.currentFiles)-numDeleted < d.maxFiles && d.currentSize < d.maxBytes {
|
||||
break
|
||||
}
|
||||
|
||||
f := d.currentFiles[idx]
|
||||
for len(d.currentFiles) > 0 && (len(d.currentFiles) > d.maxFiles || d.currentSize > d.maxBytes) {
|
||||
f := d.currentFiles[0]
|
||||
log.Println("Removing", f.path)
|
||||
if err := os.Remove(f.path); err != nil {
|
||||
log.Println("Failed to remove file:", err)
|
||||
}
|
||||
d.currentFiles = d.currentFiles[1:]
|
||||
d.currentSize -= f.size
|
||||
numDeleted = idx + 1
|
||||
}
|
||||
|
||||
// Compact currentFiles
|
||||
copy(d.currentFiles, d.currentFiles[numDeleted:])
|
||||
d.currentFiles = d.currentFiles[:len(d.currentFiles)-numDeleted]
|
||||
|
||||
var oldest time.Duration
|
||||
if len(d.currentFiles) > 0 {
|
||||
oldest = time.Since(time.Unix(d.currentFiles[0].mtime, 0)).Truncate(time.Minute)
|
||||
@@ -168,7 +158,7 @@ func (d *diskStore) clean() {
|
||||
}
|
||||
|
||||
func (d *diskStore) inventory() error {
|
||||
d.currentFiles = d.currentFiles[:0]
|
||||
d.currentFiles = nil
|
||||
d.currentSize = 0
|
||||
err := filepath.Walk(d.dir, func(path string, info os.FileInfo, err error) error {
|
||||
if err != nil {
|
||||
|
||||
@@ -20,7 +20,6 @@ import (
|
||||
"io"
|
||||
"log"
|
||||
"net/http"
|
||||
"net/http/pprof"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
@@ -30,7 +29,7 @@ import (
|
||||
raven "github.com/getsentry/raven-go"
|
||||
"github.com/prometheus/client_golang/prometheus/promhttp"
|
||||
"github.com/syncthing/syncthing/lib/build"
|
||||
"github.com/syncthing/syncthing/lib/ur/contract"
|
||||
"github.com/syncthing/syncthing/lib/ur"
|
||||
)
|
||||
|
||||
const maxRequestSize = 1 << 20 // 1 MiB
|
||||
@@ -90,7 +89,6 @@ func main() {
|
||||
if params.MetricsListen != "" {
|
||||
mmux := http.NewServeMux()
|
||||
mmux.Handle("/metrics", promhttp.Handler())
|
||||
mmux.HandleFunc("/debug/pprof/", pprof.Index)
|
||||
go func() {
|
||||
if err := http.ListenAndServe(params.MetricsListen, mmux); err != nil {
|
||||
log.Fatalln("HTTP serve metrics:", err)
|
||||
@@ -125,13 +123,12 @@ func handleFailureFn(dsn, failureDir string, ignore *ignorePatterns) func(w http
|
||||
return
|
||||
}
|
||||
|
||||
if pat, ok := ignore.match(bs); ok {
|
||||
metricIgnoreMatchesTotal.WithLabelValues(pat).Inc()
|
||||
if ignore.match(bs) {
|
||||
result = "ignored"
|
||||
return
|
||||
}
|
||||
|
||||
var reports []contract.FailureReport
|
||||
var reports []ur.FailureReport
|
||||
err = json.Unmarshal(bs, &reports)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||
@@ -179,7 +176,7 @@ func handleFailureFn(dsn, failureDir string, ignore *ignorePatterns) func(w http
|
||||
}
|
||||
}
|
||||
|
||||
func saveFailureWithGoroutines(data contract.FailureData, failureDir string) (string, error) {
|
||||
func saveFailureWithGoroutines(data ur.FailureData, failureDir string) (string, error) {
|
||||
bs := make([]byte, len(data.Description)+len(data.Goroutines))
|
||||
copy(bs, data.Description)
|
||||
copy(bs[len(data.Description):], data.Goroutines)
|
||||
@@ -219,14 +216,14 @@ func loadIgnorePatterns(path string) (*ignorePatterns, error) {
|
||||
return &ignorePatterns{patterns: patterns}, nil
|
||||
}
|
||||
|
||||
func (i *ignorePatterns) match(report []byte) (string, bool) {
|
||||
func (i *ignorePatterns) match(report []byte) bool {
|
||||
if i == nil {
|
||||
return "", false
|
||||
return false
|
||||
}
|
||||
for _, re := range i.patterns {
|
||||
if re.Match(report) {
|
||||
return re.String(), true
|
||||
return true
|
||||
}
|
||||
}
|
||||
return "", false
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -37,24 +37,4 @@ var (
|
||||
Subsystem: "crashreceiver",
|
||||
Name: "diskstore_oldest_age_seconds",
|
||||
})
|
||||
metricSentryReportsTotal = promauto.NewCounterVec(prometheus.CounterOpts{
|
||||
Namespace: "syncthing",
|
||||
Subsystem: "crashreceiver",
|
||||
Name: "sentry_reports_total",
|
||||
}, []string{"result"})
|
||||
metricIgnoreMatchesTotal = promauto.NewCounterVec(prometheus.CounterOpts{
|
||||
Namespace: "syncthing",
|
||||
Subsystem: "crashreceiver",
|
||||
Name: "ignore_matches_total",
|
||||
}, []string{"pattern"})
|
||||
metricSourceCodeLoadsTotal = promauto.NewCounterVec(prometheus.CounterOpts{
|
||||
Namespace: "syncthing",
|
||||
Subsystem: "crashreceiver",
|
||||
Name: "source_code_loads_total",
|
||||
}, []string{"result"})
|
||||
metricSourceCodeCacheSize = promauto.NewGauge(prometheus.GaugeOpts{
|
||||
Namespace: "syncthing",
|
||||
Subsystem: "crashreceiver",
|
||||
Name: "source_code_cache_size",
|
||||
})
|
||||
)
|
||||
|
||||
@@ -10,7 +10,6 @@ import (
|
||||
"bytes"
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"regexp"
|
||||
@@ -53,15 +52,11 @@ func (s *sentryService) Serve(ctx context.Context) {
|
||||
pkt, err := parseCrashReport(req.reportID, req.data)
|
||||
if err != nil {
|
||||
log.Println("Failed to parse crash report:", err)
|
||||
metricSentryReportsTotal.WithLabelValues("parse_failure").Inc()
|
||||
continue
|
||||
}
|
||||
if err := sendReport(s.dsn, pkt, req.userID); err != nil {
|
||||
log.Println("Failed to send crash report:", err)
|
||||
metricSentryReportsTotal.WithLabelValues("send_failure").Inc()
|
||||
continue
|
||||
}
|
||||
metricSentryReportsTotal.WithLabelValues("success").Inc()
|
||||
|
||||
case <-ctx.Done():
|
||||
return
|
||||
@@ -74,7 +69,6 @@ func (s *sentryService) Send(reportID, userID string, data []byte) bool {
|
||||
case s.inbox <- sentryRequest{reportID, userID, data}:
|
||||
return true
|
||||
default:
|
||||
metricCrashReportsTotal.WithLabelValues("overflow").Inc()
|
||||
return false
|
||||
}
|
||||
}
|
||||
@@ -114,10 +108,11 @@ func parseCrashReport(path string, report []byte) (*raven.Packet, error) {
|
||||
|
||||
version, err := build.ParseVersion(string(parts[0]))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("%w in %q", err, parts[0])
|
||||
return nil, err
|
||||
}
|
||||
report = parts[1]
|
||||
|
||||
foundPanic := false
|
||||
var subjectLine []byte
|
||||
for {
|
||||
parts = bytes.SplitN(report, []byte("\n"), 2)
|
||||
@@ -128,9 +123,14 @@ func parseCrashReport(path string, report []byte) (*raven.Packet, error) {
|
||||
line := parts[0]
|
||||
report = parts[1]
|
||||
|
||||
if bytes.HasPrefix(line, []byte("panic:")) || bytes.HasPrefix(line, []byte("fatal error:")) {
|
||||
if foundPanic {
|
||||
// The previous line was our "Panic at ..." header. We are now
|
||||
// at the beginning of the real panic trace and this is our
|
||||
// subject line.
|
||||
subjectLine = line
|
||||
break
|
||||
} else if bytes.HasPrefix(line, []byte("Panic at")) {
|
||||
foundPanic = true
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -9,33 +9,26 @@ package main
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestParseReport(t *testing.T) {
|
||||
files, err := filepath.Glob("_testdata/*.log")
|
||||
bs, err := os.ReadFile("_testdata/panic.log")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
for _, file := range files {
|
||||
bs, err := os.ReadFile(file)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
pkt, err := parseCrashReport("1/2/345", bs)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
bs, err = pkt.JSON()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
fmt.Printf("%s\n", bs)
|
||||
pkt, err := parseCrashReport("1/2/345", bs)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
bs, err = pkt.JSON()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
fmt.Printf("%s\n", bs)
|
||||
}
|
||||
|
||||
func TestCrashReportFingerprint(t *testing.T) {
|
||||
|
||||
@@ -15,33 +15,23 @@ import (
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
lru "github.com/hashicorp/golang-lru/v2"
|
||||
)
|
||||
|
||||
const (
|
||||
urlPrefix = "https://raw.githubusercontent.com/syncthing/syncthing/"
|
||||
httpTimeout = 10 * time.Second
|
||||
maxCacheEntries = 1000
|
||||
urlPrefix = "https://raw.githubusercontent.com/syncthing/syncthing/"
|
||||
httpTimeout = 10 * time.Second
|
||||
)
|
||||
|
||||
type cacheKey struct {
|
||||
version string
|
||||
file string
|
||||
}
|
||||
|
||||
type githubSourceCodeLoader struct {
|
||||
mut sync.Mutex
|
||||
version string
|
||||
|
||||
cache *lru.TwoQueueCache[cacheKey, [][]byte] // version & file -> lines
|
||||
client *http.Client
|
||||
cache map[string]map[string][][]byte // version -> file -> lines
|
||||
client *http.Client
|
||||
}
|
||||
|
||||
func newGithubSourceCodeLoader() *githubSourceCodeLoader {
|
||||
cache, _ := lru.New2Q[cacheKey, [][]byte](maxCacheEntries)
|
||||
return &githubSourceCodeLoader{
|
||||
cache: cache,
|
||||
cache: make(map[string]map[string][][]byte),
|
||||
client: &http.Client{Timeout: httpTimeout},
|
||||
}
|
||||
}
|
||||
@@ -49,6 +39,9 @@ func newGithubSourceCodeLoader() *githubSourceCodeLoader {
|
||||
func (l *githubSourceCodeLoader) LockWithVersion(version string) {
|
||||
l.mut.Lock()
|
||||
l.version = version
|
||||
if _, ok := l.cache[version]; !ok {
|
||||
l.cache[version] = make(map[string][][]byte)
|
||||
}
|
||||
}
|
||||
|
||||
func (l *githubSourceCodeLoader) Unlock() {
|
||||
@@ -57,13 +50,11 @@ func (l *githubSourceCodeLoader) Unlock() {
|
||||
|
||||
func (l *githubSourceCodeLoader) Load(filename string, line, context int) ([][]byte, int) {
|
||||
filename = filepath.ToSlash(filename)
|
||||
key := cacheKey{version: l.version, file: filename}
|
||||
lines, ok := l.cache.Get(key)
|
||||
lines, ok := l.cache[l.version][filename]
|
||||
if !ok {
|
||||
// Cache whatever we managed to find (or nil if nothing, so we don't try again)
|
||||
defer func() {
|
||||
l.cache.Add(key, lines)
|
||||
metricSourceCodeCacheSize.Set(float64(l.cache.Len()))
|
||||
l.cache[l.version][filename] = lines
|
||||
}()
|
||||
|
||||
knownPrefixes := []string{"/lib/", "/cmd/"}
|
||||
@@ -82,25 +73,19 @@ func (l *githubSourceCodeLoader) Load(filename string, line, context int) ([][]b
|
||||
resp, err := l.client.Get(url)
|
||||
if err != nil {
|
||||
fmt.Println("Loading source:", err)
|
||||
metricSourceCodeLoadsTotal.WithLabelValues("failed").Inc()
|
||||
return nil, 0
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
fmt.Println("Loading source:", resp.Status)
|
||||
metricSourceCodeLoadsTotal.WithLabelValues("failed").Inc()
|
||||
return nil, 0
|
||||
}
|
||||
data, err := io.ReadAll(resp.Body)
|
||||
_ = resp.Body.Close()
|
||||
if err != nil {
|
||||
fmt.Println("Loading source:", err.Error())
|
||||
metricSourceCodeLoadsTotal.WithLabelValues("failed").Inc()
|
||||
return nil, 0
|
||||
}
|
||||
lines = bytes.Split(data, []byte{'\n'})
|
||||
metricSourceCodeLoadsTotal.WithLabelValues("loaded").Inc()
|
||||
} else {
|
||||
metricSourceCodeLoadsTotal.WithLabelValues("cached").Inc()
|
||||
}
|
||||
|
||||
return getLineFromLines(lines, line, context)
|
||||
|
||||
@@ -7,18 +7,21 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"io"
|
||||
"log"
|
||||
"net/http"
|
||||
"path"
|
||||
"strings"
|
||||
"sync"
|
||||
)
|
||||
|
||||
type crashReceiver struct {
|
||||
store *diskStore
|
||||
sentry *sentryService
|
||||
ignore *ignorePatterns
|
||||
|
||||
ignoredMut sync.RWMutex
|
||||
ignored map[string]struct{}
|
||||
}
|
||||
|
||||
func (r *crashReceiver) ServeHTTP(w http.ResponseWriter, req *http.Request) {
|
||||
@@ -66,6 +69,12 @@ func (r *crashReceiver) serveGet(reportID string, w http.ResponseWriter, _ *http
|
||||
// serveHead responds to HEAD requests by checking if the named report
|
||||
// already exists in the system.
|
||||
func (r *crashReceiver) serveHead(reportID string, w http.ResponseWriter, _ *http.Request) {
|
||||
r.ignoredMut.RLock()
|
||||
_, ignored := r.ignored[reportID]
|
||||
r.ignoredMut.RUnlock()
|
||||
if ignored {
|
||||
return // found
|
||||
}
|
||||
if !r.store.Exists(reportID) {
|
||||
http.Error(w, "Not found", http.StatusNotFound)
|
||||
}
|
||||
@@ -78,7 +87,17 @@ func (r *crashReceiver) servePut(reportID string, w http.ResponseWriter, req *ht
|
||||
metricCrashReportsTotal.WithLabelValues(result).Inc()
|
||||
}()
|
||||
|
||||
r.ignoredMut.RLock()
|
||||
_, ignored := r.ignored[reportID]
|
||||
r.ignoredMut.RUnlock()
|
||||
if ignored {
|
||||
result = "ignored_cached"
|
||||
io.Copy(io.Discard, req.Body)
|
||||
return // found
|
||||
}
|
||||
|
||||
// Read at most maxRequestSize of report data.
|
||||
log.Println("Receiving report", reportID)
|
||||
lr := io.LimitReader(req.Body, maxRequestSize)
|
||||
bs, err := io.ReadAll(lr)
|
||||
if err != nil {
|
||||
@@ -87,12 +106,14 @@ func (r *crashReceiver) servePut(reportID string, w http.ResponseWriter, req *ht
|
||||
return
|
||||
}
|
||||
|
||||
first := string(bytes.TrimSpace(bytes.Split(bs, []byte("\n"))[0]))
|
||||
|
||||
if pat, ok := r.ignore.match(bs); ok {
|
||||
metricIgnoreMatchesTotal.WithLabelValues(pat).Inc()
|
||||
if r.ignore.match(bs) {
|
||||
r.ignoredMut.Lock()
|
||||
if r.ignored == nil {
|
||||
r.ignored = make(map[string]struct{})
|
||||
}
|
||||
r.ignored[reportID] = struct{}{}
|
||||
r.ignoredMut.Unlock()
|
||||
result = "ignored"
|
||||
log.Printf("Ignored report %s, matched: %s (%s)", reportID[:8], pat, first)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -100,15 +121,13 @@ func (r *crashReceiver) servePut(reportID string, w http.ResponseWriter, req *ht
|
||||
|
||||
// Store the report
|
||||
if !r.store.Put(reportID, bs) {
|
||||
log.Println("Failed to store report (queue full):", reportID[:8])
|
||||
log.Println("Failed to store report (queue full):", reportID)
|
||||
result = "queue_failure"
|
||||
}
|
||||
|
||||
// Send the report to Sentry
|
||||
if !r.sentry.Send(reportID, userIDFor(req), bs) {
|
||||
log.Println("Failed to send report to sentry (queue full):", reportID[:8])
|
||||
log.Println("Failed to send report to sentry (queue full):", reportID)
|
||||
result = "sentry_failure"
|
||||
}
|
||||
|
||||
log.Printf("Received report %s (%s)", reportID[:8], first)
|
||||
}
|
||||
|
||||
@@ -15,7 +15,7 @@ import (
|
||||
"io"
|
||||
"log/slog"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"path"
|
||||
"runtime"
|
||||
"slices"
|
||||
"strings"
|
||||
@@ -77,7 +77,7 @@ func newInMemoryStore(dir string, flushInterval time.Duration, blobs blob.Store)
|
||||
slog.Error("Failed to find database in blob storage", "error", cerr)
|
||||
return s
|
||||
}
|
||||
fd, cerr := os.Create(filepath.Join(s.dir, "records.db"))
|
||||
fd, cerr := os.Create(path.Join(s.dir, "records.db"))
|
||||
if cerr != nil {
|
||||
slog.Error("Failed to create database file", "error", cerr)
|
||||
return s
|
||||
@@ -257,7 +257,7 @@ func (s *inMemoryStore) write() (err error) {
|
||||
}
|
||||
}()
|
||||
|
||||
dbf := filepath.Join(s.dir, "records.db")
|
||||
dbf := path.Join(s.dir, "records.db")
|
||||
fd, err := os.Create(dbf + ".tmp")
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -340,7 +340,7 @@ func (s *inMemoryStore) write() (err error) {
|
||||
}
|
||||
|
||||
func (s *inMemoryStore) read() (int, error) {
|
||||
fd, err := os.Open(filepath.Join(s.dir, "records.db"))
|
||||
fd, err := os.Open(path.Join(s.dir, "records.db"))
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
+8
-27
@@ -7,8 +7,6 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
)
|
||||
|
||||
@@ -115,11 +113,14 @@ var (
|
||||
)
|
||||
|
||||
const (
|
||||
dbOpGet = "get"
|
||||
dbOpPut = "put"
|
||||
dbOpMerge = "merge"
|
||||
dbResSuccess = "success"
|
||||
dbResNotFound = "not_found"
|
||||
dbOpGet = "get"
|
||||
dbOpPut = "put"
|
||||
dbOpMerge = "merge"
|
||||
dbOpDelete = "delete"
|
||||
dbResSuccess = "success"
|
||||
dbResNotFound = "not_found"
|
||||
dbResError = "error"
|
||||
dbResUnmarshalError = "unmarsh_err"
|
||||
)
|
||||
|
||||
func init() {
|
||||
@@ -131,24 +132,4 @@ func init() {
|
||||
databaseOperations, databaseOperationSeconds,
|
||||
databaseWriteSeconds, databaseLastWritten,
|
||||
retryAfterLevel)
|
||||
|
||||
// Prewarm important counters so they're available with zero values at
|
||||
// startup
|
||||
|
||||
apiRequestsTotal.WithLabelValues(http.MethodGet, "200")
|
||||
apiRequestsTotal.WithLabelValues(http.MethodGet, "404")
|
||||
apiRequestsTotal.WithLabelValues(http.MethodPost, "204")
|
||||
apiRequestsTotal.WithLabelValues(http.MethodPost, "400")
|
||||
apiRequestsTotal.WithLabelValues(http.MethodPost, "403")
|
||||
|
||||
lookupRequestsTotal.WithLabelValues("success")
|
||||
lookupRequestsTotal.WithLabelValues("not_found_ever")
|
||||
lookupRequestsTotal.WithLabelValues("not_found_recent")
|
||||
|
||||
announceRequestsTotal.WithLabelValues("success")
|
||||
announceRequestsTotal.WithLabelValues("bad_request")
|
||||
announceRequestsTotal.WithLabelValues("no_certificate")
|
||||
|
||||
replicationSendsTotal.WithLabelValues("success")
|
||||
replicationRecvsTotal.WithLabelValues("success")
|
||||
}
|
||||
|
||||
@@ -915,14 +915,10 @@ func (u upgradeCmd) Run() error {
|
||||
case err != nil && !os.IsNotExist(err):
|
||||
slog.Error("Failed to lock for upgrade", slogutil.Error(err))
|
||||
os.Exit(1)
|
||||
case locked || os.IsNotExist(err):
|
||||
// We got the lock, or the config directory didn't exist, so we
|
||||
// can do a direct upgrade
|
||||
err = upgrade.To(release)
|
||||
default:
|
||||
// We didn't get the lock, because Syncthing was running, so
|
||||
// upgrade via REST.
|
||||
case locked:
|
||||
err = upgradeViaRest()
|
||||
default:
|
||||
err = upgrade.To(release)
|
||||
}
|
||||
}
|
||||
if err != nil {
|
||||
|
||||
@@ -233,7 +233,7 @@ func copyStderr(stderr io.Reader, dst io.Writer) {
|
||||
|
||||
dst.Write([]byte(line))
|
||||
|
||||
if panicFd == nil && (strings.HasPrefix(line, "panic:") || strings.HasPrefix(line, "fatal error:") || strings.HasPrefix(line, "runtime:")) {
|
||||
if panicFd == nil && (strings.HasPrefix(line, "panic:") || strings.HasPrefix(line, "fatal error:")) {
|
||||
panicFd, err = os.Create(locations.GetTimestamped(locations.PanicLog))
|
||||
if err != nil {
|
||||
slog.Error("Failed to create panic log", slogutil.Error(err))
|
||||
|
||||
@@ -42,8 +42,8 @@ require (
|
||||
github.com/wlynxg/anet v0.0.5
|
||||
golang.org/x/crypto v0.51.0
|
||||
golang.org/x/exp v0.0.0-20260508232706-74f9aab9d74a
|
||||
golang.org/x/net v0.55.0
|
||||
golang.org/x/sys v0.45.0
|
||||
golang.org/x/net v0.54.0
|
||||
golang.org/x/sys v0.44.0
|
||||
golang.org/x/text v0.37.0
|
||||
golang.org/x/time v0.15.0
|
||||
google.golang.org/protobuf v1.36.11
|
||||
@@ -111,7 +111,9 @@ replace github.com/gobwas/glob v0.2.3 => github.com/calmh/glob v0.0.0-2022061508
|
||||
replace github.com/jackpal/gateway v1.1.1 => github.com/marbens-arch/gateway v1.1.2-0.20260308173556-c567cc04e7d4
|
||||
|
||||
// https://github.com/mattn/go-sqlite3/pull/1338
|
||||
replace github.com/mattn/go-sqlite3 v1.14.44 => github.com/calmh/go-sqlite3 v1.14.35-0.20260509063420-822b4765116d
|
||||
// https://github.com/mattn/go-sqlite3/pull/1399
|
||||
// https://github.com/mattn/go-sqlite3/pull/1400
|
||||
replace github.com/mattn/go-sqlite3 v1.14.44 => github.com/calmh/go-sqlite3 v1.14.45-0.20260519121030-00c8bf368e65
|
||||
|
||||
tool (
|
||||
github.com/calmh/xdr/cmd/genxdr
|
||||
|
||||
@@ -19,8 +19,8 @@ github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
|
||||
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
|
||||
github.com/calmh/glob v0.0.0-20220615080505-1d823af5017b h1:Fjm4GuJ+TGMgqfGHN42IQArJb77CfD/mAwLbDUoJe6g=
|
||||
github.com/calmh/glob v0.0.0-20220615080505-1d823af5017b/go.mod h1:91K7jfEsgJSyfSrX+gmrRfZMtntx6JsHolWubGXDopg=
|
||||
github.com/calmh/go-sqlite3 v1.14.35-0.20260509063420-822b4765116d h1:liVtRMlDBqDyR4qKkMRIFTdUK6+aoG6Oh97DKnhrCHc=
|
||||
github.com/calmh/go-sqlite3 v1.14.35-0.20260509063420-822b4765116d/go.mod h1:pjEuOr8IwzLJP2MfGeTb0A35jauH+C2kbHKBr7yXKVQ=
|
||||
github.com/calmh/go-sqlite3 v1.14.45-0.20260519121030-00c8bf368e65 h1:4tbv1D+AkdxV4si6cdT3+8lr/c/gJqBdW6yGi8I7z3E=
|
||||
github.com/calmh/go-sqlite3 v1.14.45-0.20260519121030-00c8bf368e65/go.mod h1:pjEuOr8IwzLJP2MfGeTb0A35jauH+C2kbHKBr7yXKVQ=
|
||||
github.com/calmh/incontainer v1.0.0 h1:g2cTUtZuFGmMGX8GoykPkN1Judj2uw8/3/aEtq4Z/rg=
|
||||
github.com/calmh/incontainer v1.0.0/go.mod h1:eOhqnw15c9X+4RNBe0W3HlUZFfX16O0EDsCOInTndHY=
|
||||
github.com/calmh/xdr v1.2.0 h1:GaGSNH4ZDw9kNdYqle6+RcAENiaQ8/611Ok+jQbBEeU=
|
||||
@@ -280,8 +280,8 @@ golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwY
|
||||
golang.org/x/net v0.0.0-20210428140749-89ef3d95e781/go.mod h1:OJAsFXCWl8Ukc7SiCT/9KSuxbyM7479/AVlXFRxuMCk=
|
||||
golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
|
||||
golang.org/x/net v0.0.0-20220607020251-c690dde0001d/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
||||
golang.org/x/net v0.55.0 h1:bcvxaJn3e1U6InsFWt1JUq1aSjnRxLzT2rtD2KfkDF8=
|
||||
golang.org/x/net v0.55.0/go.mod h1:L5U2KuzuOe1lY7Z+aWVIKK6qEeJXnXV9yzGA+WCHJww=
|
||||
golang.org/x/net v0.54.0 h1:2zJIZAxAHV/OHCDTCOHAYehQzLfSXuf/5SoL/Dv6w/w=
|
||||
golang.org/x/net v0.54.0/go.mod h1:Sj4oj8jK6XmHpBZU/zWHw3BV3abl4Kvi+Ut7cQcY+cQ=
|
||||
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
@@ -309,8 +309,8 @@ golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBc
|
||||
golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.45.0 h1:dO4czNzziLiiXplLQgBCEpCvXQ3dnkn0SdaZSYdQ+FY=
|
||||
golang.org/x/sys v0.45.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
|
||||
golang.org/x/sys v0.44.0 h1:ildZl3J4uzeKP07r2F++Op7E9B29JRUy+a27EibtBTQ=
|
||||
golang.org/x/sys v0.44.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
|
||||
golang.org/x/telemetry v0.0.0-20260508192327-42602be52be6 h1:HjU6IWBiAgRIdAJ9/y1rwCn+UELEmwV+VsTLzj/W4sE=
|
||||
golang.org/x/telemetry v0.0.0-20260508192327-42602be52be6/go.mod h1:Eqhaxk/wZsWEH8CRxLwj6xzEJbz7k1EFGqx7nyCoabE=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
|
||||
@@ -13,6 +13,7 @@ import (
|
||||
"errors"
|
||||
"iter"
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"sync"
|
||||
"testing"
|
||||
@@ -1167,7 +1168,7 @@ func TestOpenSpecialName(t *testing.T) {
|
||||
|
||||
// Create a "base" dir that is in the way if the path becomes
|
||||
// incorrectly truncated in the next steps.
|
||||
base := filepath.Join(dir, "test")
|
||||
base := path.Join(dir, "test")
|
||||
if err := os.Mkdir(base, 0o755); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
+1
-1
@@ -16,7 +16,7 @@ import (
|
||||
// or, somewhere along the way the "+" in the version tag disappeared:
|
||||
// syncthing v1.23.7-dev.26.gdf7b56ae.dirty-stversionextra "Fermium Flea" (go1.20.5 darwin-arm64) jb@ok.kastelo.net 2023-07-12 06:55:26 UTC [Some Wrapper, purego, stnoupgrade]
|
||||
var (
|
||||
longVersionRE = regexp.MustCompile(`syncthing\s+(v[^\s]+)\s+"([^"]+)"\s\(([^\s]+)\s+([^-]+)-([^)]+)\)\s+([^\s]+)[^\[]*(?:\[(.+)\])?`)
|
||||
longVersionRE = regexp.MustCompile(`syncthing\s+(v[^\s]+)\s+"([^"]+)"\s\(([^\s]+)\s+([^-]+)-([^)]+)\)\s+([^\s]+)[^\[]*(?:\[(.+)\])?$`)
|
||||
gitExtraRE = regexp.MustCompile(`\.\d+\.g[0-9a-f]+`) // ".1.g6aaae618"
|
||||
gitExtraSepRE = regexp.MustCompile(`[.-]`) // dot or dash
|
||||
)
|
||||
|
||||
@@ -57,20 +57,6 @@ func TestParseVersion(t *testing.T) {
|
||||
Extra: []string{"Some Wrapper", "purego", "stnoupgrade"},
|
||||
},
|
||||
},
|
||||
{
|
||||
longVersion: `2026-05-18 14:53:32 INF syncthing v2.0.3 "Hafnium Hornet" (go1.25.0 darwin-amd64) builder@github.syncthing.net 2025-08-22 07:00:05 UTC [stnoupgrade] (log.pkg=main)`,
|
||||
parsed: VersionParts{
|
||||
Version: "v2.0.3",
|
||||
Tag: "v2.0.3",
|
||||
Commit: "",
|
||||
Codename: "Hafnium Hornet",
|
||||
Runtime: "go1.25.0",
|
||||
GOOS: "darwin",
|
||||
GOARCH: "amd64",
|
||||
Builder: "builder@github.syncthing.net",
|
||||
Extra: []string{"stnoupgrade"},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
|
||||
@@ -13,7 +13,6 @@ import (
|
||||
"log/slog"
|
||||
"net"
|
||||
"net/url"
|
||||
"slices"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
@@ -186,10 +185,9 @@ func (t *tcpListener) WANAddresses() []*url.URL {
|
||||
|
||||
t.mut.RUnlock()
|
||||
|
||||
// If we support ReusePort, and we are already announcing an unspecified
|
||||
// address, add an unspecified zero port address, which will be resolved
|
||||
// by the discovery server in hopes that TCP punch through works.
|
||||
if dialer.SupportsReusePort && slices.ContainsFunc(uris, func(u *url.URL) bool { return u.Hostname() == "0.0.0.0" }) {
|
||||
// If we support ReusePort, add an unspecified zero port address, which will be resolved by the discovery server
|
||||
// in hopes that TCP punch through works.
|
||||
if dialer.SupportsReusePort {
|
||||
uri := *t.uri
|
||||
uri.Host = "0.0.0.0:0"
|
||||
uris = append([]*url.URL{&uri}, uris...)
|
||||
|
||||
@@ -289,6 +289,7 @@ func (c *globalClient) sendAnnouncement(ctx context.Context, timer *time.Timer)
|
||||
resp.Body.Close()
|
||||
|
||||
if resp.StatusCode < 200 || resp.StatusCode > 299 {
|
||||
slog.DebugContext(ctx, "announce POST", "server", c.server, "status", resp.Status)
|
||||
c.setError(errors.New(resp.Status))
|
||||
|
||||
if h := resp.Header.Get("Retry-After"); h != "" {
|
||||
|
||||
@@ -20,7 +20,7 @@ import (
|
||||
"github.com/syncthing/syncthing/lib/events"
|
||||
"github.com/syncthing/syncthing/lib/protocol"
|
||||
"github.com/syncthing/syncthing/lib/svcutil"
|
||||
"github.com/syncthing/syncthing/lib/ur/contract"
|
||||
"github.com/syncthing/syncthing/lib/ur"
|
||||
)
|
||||
|
||||
type indexHandler struct {
|
||||
@@ -470,7 +470,7 @@ func (s *indexHandler) logSequenceAnomaly(msg string, extra map[string]any) {
|
||||
extraStrs[k] = fmt.Sprint(v)
|
||||
}
|
||||
|
||||
s.evLogger.Log(events.Failure, contract.FailureData{
|
||||
s.evLogger.Log(events.Failure, ur.FailureData{
|
||||
Description: msg,
|
||||
Extra: extraStrs,
|
||||
})
|
||||
|
||||
@@ -282,16 +282,3 @@ func clear(v interface{}, since int) error {
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type FailureReport struct {
|
||||
FailureData
|
||||
|
||||
Count int
|
||||
Version string
|
||||
}
|
||||
|
||||
type FailureData struct {
|
||||
Description string
|
||||
Goroutines string
|
||||
Extra map[string]string
|
||||
}
|
||||
|
||||
+26
-14
@@ -23,7 +23,6 @@ import (
|
||||
"github.com/syncthing/syncthing/lib/events"
|
||||
"github.com/syncthing/syncthing/lib/svcutil"
|
||||
"github.com/syncthing/syncthing/lib/tlsutil"
|
||||
"github.com/syncthing/syncthing/lib/ur/contract"
|
||||
|
||||
"github.com/thejerf/suture/v4"
|
||||
)
|
||||
@@ -40,10 +39,23 @@ var (
|
||||
invalidEventDataType = "failure event data is not a string"
|
||||
)
|
||||
|
||||
func FailureDataWithGoroutines(description string) contract.FailureData {
|
||||
type FailureReport struct {
|
||||
FailureData
|
||||
|
||||
Count int
|
||||
Version string
|
||||
}
|
||||
|
||||
type FailureData struct {
|
||||
Description string
|
||||
Goroutines string
|
||||
Extra map[string]string
|
||||
}
|
||||
|
||||
func FailureDataWithGoroutines(description string) FailureData {
|
||||
var buf strings.Builder
|
||||
pprof.Lookup("goroutine").WriteTo(&buf, 1)
|
||||
return contract.FailureData{
|
||||
return FailureData{
|
||||
Description: description,
|
||||
Goroutines: buf.String(),
|
||||
Extra: make(map[string]string),
|
||||
@@ -74,7 +86,7 @@ type failureHandler struct {
|
||||
type failureStat struct {
|
||||
first, last time.Time
|
||||
count int
|
||||
data contract.FailureData
|
||||
data FailureData
|
||||
}
|
||||
|
||||
func (h *failureHandler) Serve(ctx context.Context) error {
|
||||
@@ -93,24 +105,24 @@ func (h *failureHandler) Serve(ctx context.Context) error {
|
||||
if !ok {
|
||||
// Just to be safe - shouldn't ever happen, as
|
||||
// evChan is set to nil when unsubscribing.
|
||||
h.addReport(contract.FailureData{Description: evChanClosed}, time.Now())
|
||||
h.addReport(FailureData{Description: evChanClosed}, time.Now())
|
||||
evChan = nil
|
||||
continue
|
||||
}
|
||||
var data contract.FailureData
|
||||
var data FailureData
|
||||
switch d := e.Data.(type) {
|
||||
case string:
|
||||
data.Description = d
|
||||
case contract.FailureData:
|
||||
case FailureData:
|
||||
data = d
|
||||
default:
|
||||
// Same here, shouldn't ever happen.
|
||||
h.addReport(contract.FailureData{Description: invalidEventDataType}, time.Now())
|
||||
h.addReport(FailureData{Description: invalidEventDataType}, time.Now())
|
||||
continue
|
||||
}
|
||||
h.addReport(data, e.Time)
|
||||
case <-timer.C:
|
||||
reports := make([]contract.FailureReport, 0, len(h.buf))
|
||||
reports := make([]FailureReport, 0, len(h.buf))
|
||||
now := time.Now()
|
||||
for descr, stat := range h.buf {
|
||||
if now.Sub(stat.last) > minDelay || now.Sub(stat.first) > maxDelay {
|
||||
@@ -140,7 +152,7 @@ func (h *failureHandler) Serve(ctx context.Context) error {
|
||||
if sub != nil {
|
||||
sub.Unsubscribe()
|
||||
if len(h.buf) > 0 {
|
||||
reports := make([]contract.FailureReport, 0, len(h.buf))
|
||||
reports := make([]FailureReport, 0, len(h.buf))
|
||||
for _, stat := range h.buf {
|
||||
reports = append(reports, newFailureReport(stat))
|
||||
}
|
||||
@@ -167,7 +179,7 @@ func (h *failureHandler) applyOpts(opts config.OptionsConfiguration, sub events.
|
||||
return url, nil, nil
|
||||
}
|
||||
|
||||
func (h *failureHandler) addReport(data contract.FailureData, evTime time.Time) {
|
||||
func (h *failureHandler) addReport(data FailureData, evTime time.Time) {
|
||||
if stat, ok := h.buf[data.Description]; ok {
|
||||
stat.last = evTime
|
||||
stat.count++
|
||||
@@ -192,7 +204,7 @@ func (*failureHandler) String() string {
|
||||
return "FailureHandler"
|
||||
}
|
||||
|
||||
func sendFailureReports(ctx context.Context, reports []contract.FailureReport, url string) {
|
||||
func sendFailureReports(ctx context.Context, reports []FailureReport, url string) {
|
||||
var b bytes.Buffer
|
||||
if err := json.NewEncoder(&b).Encode(reports); err != nil {
|
||||
panic(err)
|
||||
@@ -223,8 +235,8 @@ func sendFailureReports(ctx context.Context, reports []contract.FailureReport, u
|
||||
resp.Body.Close()
|
||||
}
|
||||
|
||||
func newFailureReport(stat *failureStat) contract.FailureReport {
|
||||
return contract.FailureReport{
|
||||
func newFailureReport(stat *failureStat) FailureReport {
|
||||
return FailureReport{
|
||||
FailureData: stat.data,
|
||||
Count: stat.count,
|
||||
Version: build.LongVersion,
|
||||
|
||||
@@ -15,7 +15,6 @@ import (
|
||||
|
||||
"github.com/syncthing/syncthing/lib/build"
|
||||
"github.com/syncthing/syncthing/lib/config"
|
||||
"github.com/syncthing/syncthing/lib/fs"
|
||||
)
|
||||
|
||||
func TestTaggedFilename(t *testing.T) {
|
||||
@@ -257,57 +256,3 @@ func TestArchiveFoldersCreationPermission(t *testing.T) {
|
||||
t.Errorf("földer2 permissions %v, want %v", folder2VersionsInfo.Mode(), folder2Perms)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDupDirTreeWritePermissions(t *testing.T) {
|
||||
// The structure should be replicated, with user permission bits set along the way
|
||||
|
||||
srcFs := fs.NewFilesystem(fs.FilesystemTypeFake, "TestDupDirTreeWritePermissions/srcFs")
|
||||
dstFs := fs.NewFilesystem(fs.FilesystemTypeFake, "TestDupDirTreeWritePermissions/dstFs")
|
||||
|
||||
// A source dir to duplicate
|
||||
_ = srcFs.Mkdir("foo", 0o444)
|
||||
_ = srcFs.Mkdir("foo/bar", 0o555)
|
||||
_ = srcFs.Mkdir("foo/bar/baz", 0o000)
|
||||
|
||||
// Duplication should succeed
|
||||
if err := dupDirTree(srcFs, dstFs, "foo/bar/baz"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Permissions should be the same, but with read/write/execute bits for
|
||||
// the user
|
||||
if info, err := dstFs.Lstat("foo"); err != nil || info.Mode() != 0o744 {
|
||||
t.Fatalf("foo: 0o%o", info.Mode())
|
||||
}
|
||||
if info, err := dstFs.Lstat("foo/bar"); err != nil || info.Mode() != 0o755 {
|
||||
t.Fatalf("foo/bar: 0o%o", info.Mode())
|
||||
}
|
||||
if info, err := dstFs.Lstat("foo/bar/baz"); err != nil || info.Mode() != 0o700 {
|
||||
t.Fatalf("foo/bar/baz: 0o%o", info.Mode())
|
||||
}
|
||||
}
|
||||
|
||||
func TestDupDirFastPath(t *testing.T) {
|
||||
srcFs := fs.NewFilesystem(fs.FilesystemTypeFake, "TestDupDirFastPath/srcFs")
|
||||
dstFs := fs.NewFilesystem(fs.FilesystemTypeFake, "TestDupDirFastPath/dstFs")
|
||||
|
||||
// A source dir to duplicate
|
||||
_ = srcFs.Mkdir("foo", 0o444)
|
||||
_ = srcFs.Mkdir("foo/bar", 0o555)
|
||||
_ = srcFs.Mkdir("foo/bar/baz", 0o000)
|
||||
|
||||
// The destination exists, but with too few permission bits
|
||||
_ = dstFs.MkdirAll("foo/bar/baz", 0o555)
|
||||
|
||||
// Duplication should succeed
|
||||
if err := dupDirTree(srcFs, dstFs, "foo/bar/baz"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Permissions for the destination should have been updated. (This
|
||||
// differs from what would have been created by the duplication of the
|
||||
// 0o000 dir in the src, because it already existed.)
|
||||
if info, err := dstFs.Lstat("foo/bar/baz"); err != nil || info.Mode() != 0o755 {
|
||||
t.Fatalf("foo/bar/baz: 0o%o", info.Mode())
|
||||
}
|
||||
}
|
||||
|
||||
+39
-69
@@ -192,78 +192,48 @@ func archiveFile(method fs.CopyRangeMethod, srcFs, dstFs fs.Filesystem, filePath
|
||||
return err
|
||||
}
|
||||
|
||||
// dupDirTree ensures folderPath exists in dstFs, copying permissions mostly
|
||||
// from srcFs. Permissions are altered to have the user read, write, and
|
||||
// execute bits set so that Syncthing file operations are possible within
|
||||
// the destination directory.
|
||||
//
|
||||
// We want to retain the source group and other bits so that we do not
|
||||
// inadvertently open up a directory for users who shouldn't have access to
|
||||
// it, but we do not consider it a security issue to open up the permissions
|
||||
// for the current user.
|
||||
//
|
||||
// This is based on os.MkdirAll with our srcFs adjustments.
|
||||
func dupDirTree(srcFs, dstFs fs.Filesystem, path string) error {
|
||||
const (
|
||||
allPerms = 0o777
|
||||
minDirPerms = 0o700
|
||||
)
|
||||
|
||||
// Fast path: if we can tell whether path is a directory or file, stop with success or error.
|
||||
if dir, err := dstFs.Lstat(path); err == nil {
|
||||
if !dir.IsDir() {
|
||||
return errors.New("destination exists but is not a directory")
|
||||
}
|
||||
if dir.Mode()&minDirPerms != minDirPerms {
|
||||
// We want all the required permission bits set
|
||||
_ = dstFs.Chmod(path, dir.Mode()&allPerms|minDirPerms)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Slow path: make sure parent exists and then call Mkdir for path.
|
||||
|
||||
// Extract the parent folder from path by first removing any trailing
|
||||
// path separator and then scanning backward until finding a path
|
||||
// separator or reaching the beginning of the string.
|
||||
i := len(path) - 1
|
||||
for i >= 0 && os.IsPathSeparator(path[i]) {
|
||||
i--
|
||||
}
|
||||
for i >= 0 && !os.IsPathSeparator(path[i]) {
|
||||
i--
|
||||
}
|
||||
if i < 0 {
|
||||
i = 0
|
||||
}
|
||||
|
||||
// If there is a parent directory, and it is not the volume name,
|
||||
// recurse to ensure parent directory exists.
|
||||
if parent := path[:i]; len(parent) > len(filepath.VolumeName(path)) {
|
||||
if err := dupDirTree(srcFs, dstFs, parent); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// Parent now exists; invoke Mkdir and use its result.
|
||||
srcPerms := fs.FileMode(minDirPerms)
|
||||
if srcDir, err := srcFs.Lstat(path); err == nil {
|
||||
srcPerms = srcDir.Mode()&allPerms | minDirPerms
|
||||
}
|
||||
if err := dstFs.Mkdir(path, srcPerms); err != nil {
|
||||
// Handle arguments like "foo/." by
|
||||
// double-checking that directory doesn't exist.
|
||||
dir, err1 := dstFs.Lstat(path)
|
||||
if err1 == nil && dir.IsDir() {
|
||||
return nil
|
||||
}
|
||||
func dupDirTree(srcFs, dstFs fs.Filesystem, folderPath string) error {
|
||||
// Return early if the folder already exists.
|
||||
_, err := dstFs.Stat(folderPath)
|
||||
if err == nil || !fs.IsNotExist(err) {
|
||||
return err
|
||||
}
|
||||
hadParent := true
|
||||
for i := range folderPath {
|
||||
if os.IsPathSeparator(folderPath[i]) {
|
||||
// If the parent folder didn't exist, then this folder doesn't exist
|
||||
// so we can skip the check
|
||||
if hadParent {
|
||||
_, err := dstFs.Stat(folderPath[:i])
|
||||
if err == nil {
|
||||
continue
|
||||
}
|
||||
if !fs.IsNotExist(err) {
|
||||
return err
|
||||
}
|
||||
}
|
||||
hadParent = false
|
||||
err := dupDirWithPerms(srcFs, dstFs, folderPath[:i])
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
return dupDirWithPerms(srcFs, dstFs, folderPath)
|
||||
}
|
||||
|
||||
// Extra chmod to ensure our permissions override umask
|
||||
_ = dstFs.Chmod(path, srcPerms)
|
||||
|
||||
return nil
|
||||
func dupDirWithPerms(srcFs, dstFs fs.Filesystem, folderPath string) error {
|
||||
srcStat, err := srcFs.Stat(folderPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// If we call Mkdir with srcStat.Mode(), we won't get the expected perms because of umask
|
||||
// So, we create the folder with 0700, and then change the perms to the srcStat.Mode()
|
||||
err = dstFs.Mkdir(folderPath, 0o700)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return dstFs.Chmod(folderPath, srcStat.Mode())
|
||||
}
|
||||
|
||||
func restoreFile(method fs.CopyRangeMethod, src, dst fs.Filesystem, filePath string, versionTime time.Time, tagger fileTagger) error {
|
||||
|
||||
Reference in New Issue
Block a user