Compare commits
25 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 8ca3cca0a0 | |||
| 0c489f4ae2 | |||
| deb1e5b38a | |||
| 49a2688caa | |||
| 05b4f6abda | |||
| 9152d7fb2f | |||
| 4404b4dfb4 | |||
| b537090d91 | |||
| 79423edbdf | |||
| 33075974cb | |||
| 8a3a06f7ca | |||
| d0b35021c6 | |||
| 6322091462 | |||
| 5464970c5d | |||
| 3962a23723 | |||
| feaa90408e | |||
| a8ed6e4855 | |||
| 5b1e1c0520 | |||
| c17be06192 | |||
| 4ba01b05a1 | |||
| 14c4ad3af2 | |||
| 08036b1d87 | |||
| c0c401efeb | |||
| 658ea62052 | |||
| 7435e762fb |
@@ -21,8 +21,8 @@ jobs:
|
||||
name: Build and push Docker images
|
||||
if: github.repository_owner == 'syncthing'
|
||||
runs-on: ubuntu-latest
|
||||
environment: docker
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
pkg:
|
||||
- stcrashreceiver
|
||||
|
||||
@@ -9,6 +9,11 @@ on:
|
||||
workflow_call:
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
issues: read
|
||||
pull-requests: read
|
||||
|
||||
env:
|
||||
# The go version to use for builds. We set check-latest to true when
|
||||
# installing, so we get the latest patch version that matches the
|
||||
@@ -229,7 +234,6 @@ jobs:
|
||||
codesign-windows:
|
||||
name: Codesign for Windows
|
||||
if: github.repository_owner == 'syncthing' && (github.event_name == 'push' || github.event_name == 'workflow_dispatch') && (github.ref == 'refs/heads/release-nightly' || startsWith(github.ref, 'refs/tags/v'))
|
||||
environment: release
|
||||
runs-on: windows-latest
|
||||
needs:
|
||||
- package-windows
|
||||
@@ -402,7 +406,6 @@ jobs:
|
||||
package-macos:
|
||||
name: Package for macOS
|
||||
if: github.repository_owner == 'syncthing' && (github.event_name == 'push' || github.event_name == 'workflow_dispatch') && (github.ref == 'refs/heads/release-nightly' || startsWith(github.ref, 'refs/tags/v'))
|
||||
environment: release
|
||||
runs-on: macos-latest
|
||||
needs:
|
||||
- facts
|
||||
@@ -500,7 +503,6 @@ jobs:
|
||||
notarize-macos:
|
||||
name: Notarize for macOS
|
||||
if: github.repository_owner == 'syncthing' && (github.event_name == 'push' || github.event_name == 'workflow_dispatch') && (github.ref == 'refs/heads/release-nightly' || startsWith(github.ref, 'refs/tags/v'))
|
||||
environment: release
|
||||
needs:
|
||||
- package-macos
|
||||
runs-on: macos-latest
|
||||
@@ -641,7 +643,6 @@ jobs:
|
||||
sign-for-upgrade:
|
||||
name: Sign for upgrade
|
||||
if: github.repository_owner == 'syncthing' && (github.event_name == 'push' || github.event_name == 'workflow_dispatch') && (github.ref == 'refs/heads/release-nightly' || startsWith(github.ref, 'refs/tags/v'))
|
||||
environment: release
|
||||
needs:
|
||||
- codesign-windows
|
||||
- package-linux
|
||||
@@ -788,7 +789,6 @@ jobs:
|
||||
publish-nightly:
|
||||
name: Publish nightly build
|
||||
if: github.repository_owner == 'syncthing' && (github.event_name == 'push' || github.event_name == 'workflow_dispatch') && startsWith(github.ref, 'refs/heads/release-nightly')
|
||||
environment: release
|
||||
needs:
|
||||
- sign-for-upgrade
|
||||
- facts
|
||||
@@ -837,7 +837,6 @@ jobs:
|
||||
publish-release-files:
|
||||
name: Publish release files
|
||||
if: github.repository_owner == 'syncthing' && (github.event_name == 'push' || github.event_name == 'workflow_dispatch') && (github.ref == 'refs/heads/release' || startsWith(github.ref, 'refs/tags/v'))
|
||||
environment: release
|
||||
permissions:
|
||||
contents: write
|
||||
id-token: write
|
||||
@@ -920,6 +919,7 @@ jobs:
|
||||
packages/syncthing-*.tar.gz \
|
||||
packages/syncthing-*.zip \
|
||||
packages/syncthing_*.deb
|
||||
gh release edit "$VERSION" --draft=false
|
||||
|
||||
PKGS=$(pwd)/packages
|
||||
cd /tmp # gh will not release for repo x while inside repo y
|
||||
@@ -928,15 +928,15 @@ jobs:
|
||||
if ! gh release view --json name "$VERSION" >/dev/null 2>&1 ; then
|
||||
gh release create "$VERSION" \
|
||||
$maybePrerelease \
|
||||
--draft \
|
||||
--title "$VERSION" \
|
||||
--notes "https://github.com/syncthing/syncthing/releases/tag/$VERSION"
|
||||
fi
|
||||
gh release upload --clobber "$VERSION" \
|
||||
$PKGS/*.asc \
|
||||
$PKGS/*${repo}*
|
||||
gh release edit "$VERSION" --draft=false
|
||||
done
|
||||
|
||||
gh release edit "$VERSION" --draft=false
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.ACTIONS_GITHUB_TOKEN }}
|
||||
|
||||
@@ -947,7 +947,6 @@ jobs:
|
||||
publish-apt:
|
||||
name: Publish APT
|
||||
if: github.repository_owner == 'syncthing' && (github.event_name == 'push' || github.event_name == 'workflow_dispatch') && (github.ref == 'refs/heads/release-nightly' || startsWith(github.ref, 'refs/tags/v'))
|
||||
environment: release
|
||||
needs:
|
||||
- package-debian
|
||||
- facts
|
||||
@@ -1019,6 +1018,7 @@ jobs:
|
||||
VERSION: ${{ needs.facts.outputs.version }}
|
||||
RELEASE_KIND: ${{ needs.facts.outputs.release-kind }}
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
pkg:
|
||||
- syncthing
|
||||
@@ -1137,7 +1137,6 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
needs:
|
||||
- docker-ghcr
|
||||
environment: docker
|
||||
env:
|
||||
DOCKERHUB_USERNAME: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
DOCKERHUB_TOKEN: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
|
||||
@@ -2,6 +2,9 @@ name: Mirrors
|
||||
|
||||
on: [push, delete]
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
codeberg:
|
||||
name: Mirror to Codeberg
|
||||
|
||||
@@ -14,7 +14,6 @@ jobs:
|
||||
name: Create release tag
|
||||
if: github.repository_owner == 'syncthing'
|
||||
runs-on: ubuntu-latest
|
||||
environment: release
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
with:
|
||||
|
||||
@@ -5,6 +5,9 @@ on:
|
||||
# Run nightly build at 01:00 UTC
|
||||
- cron: '00 01 * * *'
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
jobs:
|
||||
|
||||
trigger-nightly:
|
||||
|
||||
@@ -4,6 +4,9 @@ on:
|
||||
schedule:
|
||||
- cron: '42 3 * * 1'
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
jobs:
|
||||
|
||||
update_transifex_docs:
|
||||
|
||||
@@ -312,6 +312,7 @@ Tom Jakubowski <tom@crystae.net>
|
||||
Tully Robinson (tojrobinson) <tully@tojr.org>
|
||||
Tyler Brazier (tylerbrazier) <tyler@tylerbrazier.com>
|
||||
Tyler Kropp <kropptyler@gmail.com>
|
||||
Umer-Azaz <umer_azaz@yahoo.com>
|
||||
Unrud (Unrud) <unrud@openaliasbox.org> <Unrud@users.noreply.github.com>
|
||||
Val Markovic <val@markovic.io>
|
||||
vapatel2 <149737089+vapatel2@users.noreply.github.com>
|
||||
|
||||
@@ -0,0 +1,76 @@
|
||||
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,15 +136,25 @@ func (d *diskStore) Exists(path string) bool {
|
||||
}
|
||||
|
||||
func (d *diskStore) clean() {
|
||||
for len(d.currentFiles) > 0 && (len(d.currentFiles) > d.maxFiles || d.currentSize > d.maxBytes) {
|
||||
f := d.currentFiles[0]
|
||||
numDeleted := 0
|
||||
for idx := range d.currentFiles {
|
||||
if len(d.currentFiles)-numDeleted < d.maxFiles && d.currentSize < d.maxBytes {
|
||||
break
|
||||
}
|
||||
|
||||
f := d.currentFiles[idx]
|
||||
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)
|
||||
@@ -158,7 +168,7 @@ func (d *diskStore) clean() {
|
||||
}
|
||||
|
||||
func (d *diskStore) inventory() error {
|
||||
d.currentFiles = nil
|
||||
d.currentFiles = d.currentFiles[:0]
|
||||
d.currentSize = 0
|
||||
err := filepath.Walk(d.dir, func(path string, info os.FileInfo, err error) error {
|
||||
if err != nil {
|
||||
|
||||
@@ -20,6 +20,7 @@ import (
|
||||
"io"
|
||||
"log"
|
||||
"net/http"
|
||||
"net/http/pprof"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
@@ -29,7 +30,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"
|
||||
"github.com/syncthing/syncthing/lib/ur/contract"
|
||||
)
|
||||
|
||||
const maxRequestSize = 1 << 20 // 1 MiB
|
||||
@@ -89,6 +90,7 @@ 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)
|
||||
@@ -123,12 +125,13 @@ func handleFailureFn(dsn, failureDir string, ignore *ignorePatterns) func(w http
|
||||
return
|
||||
}
|
||||
|
||||
if ignore.match(bs) {
|
||||
if pat, ok := ignore.match(bs); ok {
|
||||
metricIgnoreMatchesTotal.WithLabelValues(pat).Inc()
|
||||
result = "ignored"
|
||||
return
|
||||
}
|
||||
|
||||
var reports []ur.FailureReport
|
||||
var reports []contract.FailureReport
|
||||
err = json.Unmarshal(bs, &reports)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||
@@ -176,7 +179,7 @@ func handleFailureFn(dsn, failureDir string, ignore *ignorePatterns) func(w http
|
||||
}
|
||||
}
|
||||
|
||||
func saveFailureWithGoroutines(data ur.FailureData, failureDir string) (string, error) {
|
||||
func saveFailureWithGoroutines(data contract.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)
|
||||
@@ -216,14 +219,14 @@ func loadIgnorePatterns(path string) (*ignorePatterns, error) {
|
||||
return &ignorePatterns{patterns: patterns}, nil
|
||||
}
|
||||
|
||||
func (i *ignorePatterns) match(report []byte) bool {
|
||||
func (i *ignorePatterns) match(report []byte) (string, bool) {
|
||||
if i == nil {
|
||||
return false
|
||||
return "", false
|
||||
}
|
||||
for _, re := range i.patterns {
|
||||
if re.Match(report) {
|
||||
return true
|
||||
return re.String(), true
|
||||
}
|
||||
}
|
||||
return false
|
||||
return "", false
|
||||
}
|
||||
|
||||
@@ -37,4 +37,24 @@ 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,6 +10,7 @@ import (
|
||||
"bytes"
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"regexp"
|
||||
@@ -52,11 +53,15 @@ 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
|
||||
@@ -69,6 +74,7 @@ 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
|
||||
}
|
||||
}
|
||||
@@ -108,11 +114,10 @@ func parseCrashReport(path string, report []byte) (*raven.Packet, error) {
|
||||
|
||||
version, err := build.ParseVersion(string(parts[0]))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return nil, fmt.Errorf("%w in %q", err, parts[0])
|
||||
}
|
||||
report = parts[1]
|
||||
|
||||
foundPanic := false
|
||||
var subjectLine []byte
|
||||
for {
|
||||
parts = bytes.SplitN(report, []byte("\n"), 2)
|
||||
@@ -123,14 +128,9 @@ func parseCrashReport(path string, report []byte) (*raven.Packet, error) {
|
||||
line := parts[0]
|
||||
report = parts[1]
|
||||
|
||||
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.
|
||||
if bytes.HasPrefix(line, []byte("panic:")) || bytes.HasPrefix(line, []byte("fatal error:")) {
|
||||
subjectLine = line
|
||||
break
|
||||
} else if bytes.HasPrefix(line, []byte("Panic at")) {
|
||||
foundPanic = true
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -9,26 +9,33 @@ package main
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestParseReport(t *testing.T) {
|
||||
bs, err := os.ReadFile("_testdata/panic.log")
|
||||
files, err := filepath.Glob("_testdata/*.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)
|
||||
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)
|
||||
}
|
||||
|
||||
bs, err = pkt.JSON()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
fmt.Printf("%s\n", bs)
|
||||
}
|
||||
|
||||
func TestCrashReportFingerprint(t *testing.T) {
|
||||
|
||||
@@ -15,23 +15,33 @@ import (
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
lru "github.com/hashicorp/golang-lru/v2"
|
||||
)
|
||||
|
||||
const (
|
||||
urlPrefix = "https://raw.githubusercontent.com/syncthing/syncthing/"
|
||||
httpTimeout = 10 * time.Second
|
||||
urlPrefix = "https://raw.githubusercontent.com/syncthing/syncthing/"
|
||||
httpTimeout = 10 * time.Second
|
||||
maxCacheEntries = 1000
|
||||
)
|
||||
|
||||
type cacheKey struct {
|
||||
version string
|
||||
file string
|
||||
}
|
||||
|
||||
type githubSourceCodeLoader struct {
|
||||
mut sync.Mutex
|
||||
version string
|
||||
cache map[string]map[string][][]byte // version -> file -> lines
|
||||
client *http.Client
|
||||
|
||||
cache *lru.TwoQueueCache[cacheKey, [][]byte] // version & file -> lines
|
||||
client *http.Client
|
||||
}
|
||||
|
||||
func newGithubSourceCodeLoader() *githubSourceCodeLoader {
|
||||
cache, _ := lru.New2Q[cacheKey, [][]byte](maxCacheEntries)
|
||||
return &githubSourceCodeLoader{
|
||||
cache: make(map[string]map[string][][]byte),
|
||||
cache: cache,
|
||||
client: &http.Client{Timeout: httpTimeout},
|
||||
}
|
||||
}
|
||||
@@ -39,9 +49,6 @@ 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() {
|
||||
@@ -50,11 +57,13 @@ func (l *githubSourceCodeLoader) Unlock() {
|
||||
|
||||
func (l *githubSourceCodeLoader) Load(filename string, line, context int) ([][]byte, int) {
|
||||
filename = filepath.ToSlash(filename)
|
||||
lines, ok := l.cache[l.version][filename]
|
||||
key := cacheKey{version: l.version, file: filename}
|
||||
lines, ok := l.cache.Get(key)
|
||||
if !ok {
|
||||
// Cache whatever we managed to find (or nil if nothing, so we don't try again)
|
||||
defer func() {
|
||||
l.cache[l.version][filename] = lines
|
||||
l.cache.Add(key, lines)
|
||||
metricSourceCodeCacheSize.Set(float64(l.cache.Len()))
|
||||
}()
|
||||
|
||||
knownPrefixes := []string{"/lib/", "/cmd/"}
|
||||
@@ -73,19 +82,25 @@ 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,21 +7,18 @@
|
||||
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) {
|
||||
@@ -69,12 +66,6 @@ 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)
|
||||
}
|
||||
@@ -87,17 +78,7 @@ 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 {
|
||||
@@ -106,14 +87,12 @@ func (r *crashReceiver) servePut(reportID string, w http.ResponseWriter, req *ht
|
||||
return
|
||||
}
|
||||
|
||||
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()
|
||||
first := string(bytes.TrimSpace(bytes.Split(bs, []byte("\n"))[0]))
|
||||
|
||||
if pat, ok := r.ignore.match(bs); ok {
|
||||
metricIgnoreMatchesTotal.WithLabelValues(pat).Inc()
|
||||
result = "ignored"
|
||||
log.Printf("Ignored report %s, matched: %s (%s)", reportID[:8], pat, first)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -121,13 +100,15 @@ 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)
|
||||
log.Println("Failed to store report (queue full):", reportID[:8])
|
||||
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)
|
||||
log.Println("Failed to send report to sentry (queue full):", reportID[:8])
|
||||
result = "sentry_failure"
|
||||
}
|
||||
|
||||
log.Printf("Received report %s (%s)", reportID[:8], first)
|
||||
}
|
||||
|
||||
@@ -15,7 +15,7 @@ import (
|
||||
"io"
|
||||
"log/slog"
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"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(path.Join(s.dir, "records.db"))
|
||||
fd, cerr := os.Create(filepath.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 := path.Join(s.dir, "records.db")
|
||||
dbf := filepath.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(path.Join(s.dir, "records.db"))
|
||||
fd, err := os.Open(filepath.Join(s.dir, "records.db"))
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
+27
-8
@@ -7,6 +7,8 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
)
|
||||
|
||||
@@ -113,14 +115,11 @@ var (
|
||||
)
|
||||
|
||||
const (
|
||||
dbOpGet = "get"
|
||||
dbOpPut = "put"
|
||||
dbOpMerge = "merge"
|
||||
dbOpDelete = "delete"
|
||||
dbResSuccess = "success"
|
||||
dbResNotFound = "not_found"
|
||||
dbResError = "error"
|
||||
dbResUnmarshalError = "unmarsh_err"
|
||||
dbOpGet = "get"
|
||||
dbOpPut = "put"
|
||||
dbOpMerge = "merge"
|
||||
dbResSuccess = "success"
|
||||
dbResNotFound = "not_found"
|
||||
)
|
||||
|
||||
func init() {
|
||||
@@ -132,4 +131,24 @@ 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,10 +915,14 @@ 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:
|
||||
err = upgradeViaRest()
|
||||
default:
|
||||
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.
|
||||
err = upgradeViaRest()
|
||||
}
|
||||
}
|
||||
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:")) {
|
||||
if panicFd == nil && (strings.HasPrefix(line, "panic:") || strings.HasPrefix(line, "fatal error:") || strings.HasPrefix(line, "runtime:")) {
|
||||
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.54.0
|
||||
golang.org/x/sys v0.44.0
|
||||
golang.org/x/net v0.55.0
|
||||
golang.org/x/sys v0.45.0
|
||||
golang.org/x/text v0.37.0
|
||||
golang.org/x/time v0.15.0
|
||||
google.golang.org/protobuf v1.36.11
|
||||
|
||||
@@ -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.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/net v0.55.0 h1:bcvxaJn3e1U6InsFWt1JUq1aSjnRxLzT2rtD2KfkDF8=
|
||||
golang.org/x/net v0.55.0/go.mod h1:L5U2KuzuOe1lY7Z+aWVIKK6qEeJXnXV9yzGA+WCHJww=
|
||||
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.44.0 h1:ildZl3J4uzeKP07r2F++Op7E9B29JRUy+a27EibtBTQ=
|
||||
golang.org/x/sys v0.44.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
|
||||
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/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=
|
||||
|
||||
@@ -102,7 +102,7 @@
|
||||
"Device Certificate": "Atestilo de aparato",
|
||||
"Device Group": "Grupo de aparato",
|
||||
"Device ID": "Identigilo de aparato",
|
||||
"Device Identification": "Identigilo de aparato",
|
||||
"Device Identification": "Identigado de aparato",
|
||||
"Device Name": "Nomo de aparato",
|
||||
"Device Status": "Stato de aparato",
|
||||
"Device is untrusted, enter encryption password": "Aparato ne estas fidinda, entajpu pasvorto por ĉifrado",
|
||||
|
||||
@@ -42,15 +42,15 @@
|
||||
"Are you sure you want to upgrade?": "Voulez-vous vraiment mettre à jour ?",
|
||||
"Authentication Required": "Authentification nécessaire",
|
||||
"Authors": "Auteurs",
|
||||
"Auto Accept": "Accepter automatiquement",
|
||||
"Auto Accept": "Accepter automatiquement ses invitations",
|
||||
"Automatic Crash Reporting": "Rapports de plantage automatiques",
|
||||
"Automatic upgrade now offers the choice between stable releases and release candidates.": "Le système de mise à jour automatique propose le choix entre versions stables et versions préliminaires.",
|
||||
"Automatic upgrades": "Mises à jour automatiques",
|
||||
"Automatic upgrades are always enabled for candidate releases.": "Les mises à jour automatiques sont toujours activées pour les versions mineures.",
|
||||
"Automatically create or share folders that this device advertises at the default path.": "Automatiquement créer dans le chemin par défaut les partages que cet appareil annonce, ou accepter leur partage avec ceux pré-existants.",
|
||||
"Automatically create or share folders that this device advertises at the default path.": "Automatiquement créer dans le chemin par défaut les partages auxquels cet appareil vous propose de participer, ou accepter leur partage s'ils pré-existent.",
|
||||
"Available debug logging facilities:": "Outils de débogage disponibles :",
|
||||
"Be careful!": "Faites attention !",
|
||||
"Block Indexing": "Indexation de blocs",
|
||||
"Block Indexing": "Indexation des blocs",
|
||||
"Body:": "Corps du message :",
|
||||
"Bugs": "Bogues",
|
||||
"Cancel": "Annuler",
|
||||
@@ -252,7 +252,7 @@
|
||||
"Log tailing paused. Scroll to the bottom to continue.": "Le défilement du journal est en pause. Faites défiler jusqu'en bas pour continuer.",
|
||||
"Login failed, see Syncthing logs for details.": "Échec de connexion, consultez les journaux de Syncthing pour les détails.",
|
||||
"Logs": "Journaux",
|
||||
"Maintain an index of all blocks in the folder, enabling reuse of blocks from other files when syncing changes. Disable to reduce database size at the cost of not being able to reuse blocks across files.": "Entretient un index de tous les blocs du répertoire pour permettre leur réutilisation depuis d'autres fichiers lors de changements dans la synchronisation. Désactivez pour diminuer la taille de la base de donnée, au prix de téléchargements potentiellement plus long.",
|
||||
"Maintain an index of all blocks in the folder, enabling reuse of blocks from other files when syncing changes. Disable to reduce database size at the cost of not being able to reuse blocks across files.": "Entretient un index de tous les blocs du répertoire pour permettre leur réutilisation depuis d'autres fichiers lors de changements dans la synchronisation. Désactivez pour diminuer la taille de la base de donnée, au prix de téléchargements potentiellement plus longs.",
|
||||
"Major Upgrade": "Mise à jour majeure",
|
||||
"Mass actions": "Actions multiples",
|
||||
"Maximum Age": "Ancienneté maximum",
|
||||
|
||||
@@ -49,6 +49,7 @@
|
||||
"Automatically create or share folders that this device advertises at the default path.": "Automatski stvorite ili dijelite mape koje ovaj uređaj oglašava na zadanoj stazi.",
|
||||
"Available debug logging facilities:": "Dostupni alati za bilježenje grešaka:",
|
||||
"Be careful!": "Pazi!",
|
||||
"Block Indexing": "Indeksiranje blokova",
|
||||
"Body:": "Sadržaj:",
|
||||
"Bugs": "Greške",
|
||||
"Cancel": "Odustani",
|
||||
|
||||
@@ -410,7 +410,7 @@
|
||||
"Syncthing is listening on the following network addresses for connection attempts from other devices:": "Syncthing ожидает подключения от других устройств на следующих сетевых адресах:",
|
||||
"Syncthing is not listening for connection attempts from other devices on any address. Only outgoing connections from this device may work.": "Syncthing не ожидает попыток подключения ни на каких адресах. Только исходящие подключения могут работать на этом устройстве.",
|
||||
"Syncthing is restarting.": "Перезапуск Syncthing.",
|
||||
"Syncthing is saving changes.": "Синхронизация это сохранение изменений.",
|
||||
"Syncthing is saving changes.": "Syncthing сохраняет изменения.",
|
||||
"Syncthing is upgrading.": "Обновление Syncthing.",
|
||||
"Syncthing now supports automatically reporting crashes to the developers. This feature is enabled by default.": "Syncthing теперь поддерживает автоматическую отправку отчетов о сбоях разработчикам. Эта функция включена по умолчанию.",
|
||||
"Syncthing seems to be down, or there is a problem with your Internet connection. Retrying…": "Кажется, Syncthing не запущен или есть проблемы с подключением к Интернету. Переподключаюсь…",
|
||||
|
||||
@@ -50,6 +50,7 @@
|
||||
"Automatically create or share folders that this device advertises at the default path.": "Skapa eller dela automatiskt mappar som denna enhet annonserar på standardsökvägen.",
|
||||
"Available debug logging facilities:": "Tillgängliga felsökningsfunktioner:",
|
||||
"Be careful!": "Var aktsam!",
|
||||
"Block Indexing": "Blockindexering",
|
||||
"Body:": "Meddelande:",
|
||||
"Bugs": "Felrapporter",
|
||||
"Cancel": "Avbryt",
|
||||
@@ -251,6 +252,7 @@
|
||||
"Log tailing paused. Scroll to the bottom to continue.": "Loggning pausad. Bläddra till botten för att fortsätta.",
|
||||
"Login failed, see Syncthing logs for details.": "Inloggningen misslyckades, se Syncthing-loggarna för detaljer.",
|
||||
"Logs": "Loggar",
|
||||
"Maintain an index of all blocks in the folder, enabling reuse of blocks from other files when syncing changes. Disable to reduce database size at the cost of not being able to reuse blocks across files.": "Upprätthåll ett index över alla block i mappen, vilket möjliggör återanvändning av block från andra filer vid synkronisering av ändringar. Inaktivera för att minska databasens storlek på bekostnad av att inte kunna återanvända block mellan filer.",
|
||||
"Major Upgrade": "Större uppgradering",
|
||||
"Mass actions": "Massåtgärder",
|
||||
"Maximum Age": "Högsta ålder",
|
||||
@@ -394,6 +396,7 @@
|
||||
"Staggered": "Förskjuten",
|
||||
"Staggered File Versioning": "Filversionshantering i intervall",
|
||||
"Start Browser": "Starta webbläsaren",
|
||||
"Starting": "Börjar",
|
||||
"Statistics": "Statistik",
|
||||
"Stay logged in": "Förbli inloggad",
|
||||
"Stopped": "Stoppad",
|
||||
|
||||
@@ -30,7 +30,7 @@
|
||||
<h4 class="text-center" translate>The Syncthing Authors</h4>
|
||||
<div class="row">
|
||||
<div class="col-md-12" id="contributor-list">
|
||||
Jakob Borg, Audrius Butkevicius, Simon Frei, Tomasz Wilczyński, Alexander Graf, Alexandre Viau, Anderson Mesquita, André Colomb, Antony Male, Ben Schulz, bt90, Caleb Callaway, Daniel Harte, Emil Lundberg, Eric P, Evgeny Kuznetsov, greatroar, Lars K.W. Gohlke, Lode Hoste, Marcus B Spencer, Michael Ploujnikov, Ross Smith II, Stefan Tatschner, Tommy van der Vorst, Wulf Weich, Adam Piggott, Adel Qalieh, Aleksey Vasenev, Alessandro G., Alex Ionescu, Alex Lindeman, Alex Xu, Alexander Seiler, Alexandre Alves, Aman Gupta, Andreas Sommer, andresvia, Andrew Rabert, Andrey D, andyleap, Anjan Momi, Anthony Goeckner, Antoine Lamielle, Anur, Aranjedeath, ardevd, Arkadiusz Tymiński, Aroun, Arthur Axel fREW Schmidt, Artur Zubilewicz, Ashish Bhate, Aurélien Rainone, BAHADIR YILMAZ, Bart De Vries, Beat Reichenbach, Ben Norcombe, Ben Shepherd, Ben Sidhom, Benedikt Heine, Benno Fünfstück, Benny Ng, boomsquared, Boqin Qin, Boris Rybalkin, Brendan Long, Catfriend1, Cathryne Linenweaver, Cedric Staniewski, Chih-Hsuan Yen, Choongkyu, Chris Howie, Chris Joel, Christian Kujau, Christian Prescott, chucic, cjc7373, Colin Kennedy, Cromefire_, cui, Cyprien Devillez, d-volution, Dan, Daniel Barczyk, Daniel Bergmann, Daniel Martí, Daniel Padrta, Daniil Gentili, Darshil Chanpura, dashangcun, David Rimmer, DeflateAwning, Denis A., Dennis Wilson, derekriemer, DerRockWolf, desbma, Devon G. Redekopp, digital, Dimitri Papadopoulos Orfanos, Dmitry Saveliev, domain, Domenic Horner, Dominik Heidler, Elias Jarlebring, Elliot Huffman, Emil Hessman, Eng Zer Jun, entity0xfe, Epifeny, epifeny, Eric Lesiuta, Erik Meitner, Evan Spensley, Federico Castagnini, Felix, Felix Ableitner, Felix Lampe, Felix Unterpaintner, Francois-Xavier Gsell, Frank Isemann, Gahl Saraf, georgespatton, ghjklw, Gilli Sigurdsson, Gleb Sinyavskiy, Graham Miln, Greg, guangwu, gudvinr, Gusted, Han Boetes, HansK-p, Harrison Jones, Hazem Krimi, Heiko Zuerker, Hireworks, Hugo Locurcio, Iain Barnett, Ian Johnson, ignacy123, Iskander Sharipov, Jaakko Hannikainen, Jack Croft, Jacob, Jake Peterson, James O'Beirne, James Patterson, Jaroslav Lichtblau, Jaroslav Malec, Jaspitta, Jaya Chithra, Jaya Kumar, Jeffery To, jelle van der Waa, Jens Diemer, Jochen Voss, Johan Vromans, John Rinehart, Jonas Thelemann, Jonathan, Jose Manuel Delicado, JRNitre, jtagcat, Julian Lehrhuber, Jörg Thalheim, Jędrzej Kula, Kapil Sareen, Karol Różycki, Kebin Liu, Keith Harrison, Kelong Cong, Ken'ichi Kamada, Kevin Allen, Kevin Bushiri, Kevin White, Jr., klemens, Kurt Fitzner, kylosus, Lars Lehtonen, Laurent Etiemble, Leo Arias, Liu Siyuan, Lord Landon Agahnim, LSmithx2, Luiz Angelo Daros de Luca, Lukas Lihotzki, Luke Hamburg, luzpaz, Majed Abdulaziz, Marc Laporte, Marcel Meyer, Marcin Dziadus, Marcus Legendre, Mario Majila, Mark Pulford, Martchus, Mateusz Naściszewski, Mateusz Ż, mathias4833, Matic Potočnik, Matt Burke, Matt Robenolt, Matteo Ruina, Maurizio Tomasi, Max, Max Schulze, MaximAL, Maximilian, Maxwell G, Michael Jephcote, Michael Rienstra, Michael Wang 汪東陽, MichaIng, Migelo, Mike Boone, MikeLund, MikolajTwarog, Mingxuan Lin, mv1005, Nate Morrison, nf, Nicholas Rishel, Nick Busey, Nico Stapelbroek, Nicolas Braud-Santoni, Nicolas Perraut, Niels Peter Roest, Nils Jakobi, NinoM4ster, Nitroretro, NoLooseEnds, Oliver Freyermuth, orangekame3, otbutz, overkill, Oyebanji Jacob Mayowa, Pablo, Pascal Jungblut, Paul Brit, Paul Donald, Pawel Palenica, perewa, Peter Badida, Peter Dave Hello, Peter Hoeg, Peter Marquardt, Phani Rithvij, Phil Davis, Philippe Schommers, Phill Luby, Piotr Bejda, polyfloyd, Prathik P Kulkarni, pullmerge, Quentin Hibon, Rahmi Pruitt, RealCharlesChia, red_led, Robert Carosi, Roberto Santalla, Robin Schoonover, Roman Zaynetdinov, rubenbe, Ruslan Yevdokymov, Ryan Qian, Ryan Sullivan, Sacheendra Talluri, Scott Klupfel, sec65, Sergey Mishin, Sertonix, Severin von Wnuck-Lipinski, Shaarad Dalvi, Shivam Kumar, Simon Mwepu, Simon Pickup, Sly_tom_cat, Sonu Kumar Saw, Stefan Kuntz, Steven Eckhoff, Suhas Gundimeda, Sven Bachmann, Sébastien WENSKE, Tao, Taylor Khan, Terrance, TheCreeper, Thomas, Thomas Hipp, Tim Abell, Tim Howes, Tobias Frölich, Tobias Klauser, Tobias Nygren, Tobias Tom, Tom Jakubowski, Tully Robinson, Tyler Brazier, Tyler Kropp, Unrud, Val Markovic, vapatel2, Veeti Paananen, Victor Buinsky, Vik, Vil Brekin, villekalliomaki, Vladimir Rusinov, vvaswani, wangguoliang, WangXi, Will Rouesnel, William A. Kennington III, wouter bolsterlee, xarx00, Xavier O., xjtdy888, Yannic A., yparitcher, 佛跳墙, 落心
|
||||
Jakob Borg, Audrius Butkevicius, Simon Frei, Tomasz Wilczyński, Alexander Graf, Alexandre Viau, Anderson Mesquita, André Colomb, Antony Male, Ben Schulz, bt90, Caleb Callaway, Daniel Harte, Emil Lundberg, Eric P, Evgeny Kuznetsov, greatroar, Lars K.W. Gohlke, Lode Hoste, Marcus B Spencer, Michael Ploujnikov, Ross Smith II, Stefan Tatschner, Tommy van der Vorst, Wulf Weich, Adam Piggott, Adel Qalieh, Aleksey Vasenev, Alessandro G., Alex Ionescu, Alex Lindeman, Alex Xu, Alexander Seiler, Alexandre Alves, Aman Gupta, Andreas Sommer, andresvia, Andrew Rabert, Andrey D, andyleap, Anjan Momi, Anthony Goeckner, Antoine Lamielle, Anur, Aranjedeath, ardevd, Arkadiusz Tymiński, Aroun, Arthur Axel fREW Schmidt, Artur Zubilewicz, Ashish Bhate, Aurélien Rainone, BAHADIR YILMAZ, Bart De Vries, Beat Reichenbach, Ben Norcombe, Ben Shepherd, Ben Sidhom, Benedikt Heine, Benno Fünfstück, Benny Ng, boomsquared, Boqin Qin, Boris Rybalkin, Brendan Long, Catfriend1, Cathryne Linenweaver, Cedric Staniewski, Chih-Hsuan Yen, Choongkyu, Chris Howie, Chris Joel, Christian Kujau, Christian Prescott, chucic, cjc7373, Colin Kennedy, Cromefire_, cui, Cyprien Devillez, d-volution, Dan, Daniel Barczyk, Daniel Bergmann, Daniel Martí, Daniel Padrta, Daniil Gentili, Darshil Chanpura, dashangcun, David Rimmer, DeflateAwning, Denis A., Dennis Wilson, derekriemer, DerRockWolf, desbma, Devon G. Redekopp, digital, Dimitri Papadopoulos Orfanos, Dmitry Saveliev, domain, Domenic Horner, Dominik Heidler, Elias Jarlebring, Elliot Huffman, Emil Hessman, Eng Zer Jun, entity0xfe, Epifeny, epifeny, Eric Lesiuta, Erik Meitner, Evan Spensley, Federico Castagnini, Felix, Felix Ableitner, Felix Lampe, Felix Unterpaintner, Francois-Xavier Gsell, Frank Isemann, Gahl Saraf, georgespatton, ghjklw, Gilli Sigurdsson, Gleb Sinyavskiy, Graham Miln, Greg, guangwu, gudvinr, Gusted, Han Boetes, HansK-p, Harrison Jones, Hazem Krimi, Heiko Zuerker, Hireworks, Hugo Locurcio, Iain Barnett, Ian Johnson, ignacy123, Iskander Sharipov, Jaakko Hannikainen, Jack Croft, Jacob, Jake Peterson, James O'Beirne, James Patterson, Jaroslav Lichtblau, Jaroslav Malec, Jaspitta, Jaya Chithra, Jaya Kumar, Jeffery To, jelle van der Waa, Jens Diemer, Jochen Voss, Johan Vromans, John Rinehart, Jonas Thelemann, Jonathan, Jose Manuel Delicado, JRNitre, jtagcat, Julian Lehrhuber, Jörg Thalheim, Jędrzej Kula, Kapil Sareen, Karol Różycki, Kebin Liu, Keith Harrison, Kelong Cong, Ken'ichi Kamada, Kevin Allen, Kevin Bushiri, Kevin White, Jr., klemens, Kurt Fitzner, kylosus, Lars Lehtonen, Laurent Etiemble, Leo Arias, Liu Siyuan, Lord Landon Agahnim, LSmithx2, Luiz Angelo Daros de Luca, Lukas Lihotzki, Luke Hamburg, luzpaz, Majed Abdulaziz, Marc Laporte, Marcel Meyer, Marcin Dziadus, Marcus Legendre, Mario Majila, Mark Pulford, Martchus, Mateusz Naściszewski, Mateusz Ż, mathias4833, Matic Potočnik, Matt Burke, Matt Robenolt, Matteo Ruina, Maurizio Tomasi, Max, Max Schulze, MaximAL, Maximilian, Maxwell G, Michael Jephcote, Michael Rienstra, Michael Wang 汪東陽, MichaIng, Migelo, Mike Boone, MikeLund, MikolajTwarog, Mingxuan Lin, mv1005, Nate Morrison, nf, Nicholas Rishel, Nick Busey, Nico Stapelbroek, Nicolas Braud-Santoni, Nicolas Perraut, Niels Peter Roest, Nils Jakobi, NinoM4ster, Nitroretro, NoLooseEnds, Oliver Freyermuth, orangekame3, otbutz, overkill, Oyebanji Jacob Mayowa, Pablo, Pascal Jungblut, Paul Brit, Paul Donald, Pawel Palenica, perewa, Peter Badida, Peter Dave Hello, Peter Hoeg, Peter Marquardt, Phani Rithvij, Phil Davis, Philippe Schommers, Phill Luby, Piotr Bejda, polyfloyd, Prathik P Kulkarni, pullmerge, Quentin Hibon, Rahmi Pruitt, RealCharlesChia, red_led, Robert Carosi, Roberto Santalla, Robin Schoonover, Roman Zaynetdinov, rubenbe, Ruslan Yevdokymov, Ryan Qian, Ryan Sullivan, Sacheendra Talluri, Scott Klupfel, sec65, Sergey Mishin, Sertonix, Severin von Wnuck-Lipinski, Shaarad Dalvi, Shivam Kumar, Simon Mwepu, Simon Pickup, Sly_tom_cat, Sonu Kumar Saw, Stefan Kuntz, Steven Eckhoff, Suhas Gundimeda, Sven Bachmann, Sébastien WENSKE, Tao, Taylor Khan, Terrance, TheCreeper, Thomas, Thomas Hipp, Tim Abell, Tim Howes, Tobias Frölich, Tobias Klauser, Tobias Nygren, Tobias Tom, Tom Jakubowski, Tully Robinson, Tyler Brazier, Tyler Kropp, Umer-Azaz, Unrud, Val Markovic, vapatel2, Veeti Paananen, Victor Buinsky, Vik, Vil Brekin, villekalliomaki, Vladimir Rusinov, vvaswani, wangguoliang, WangXi, Will Rouesnel, William A. Kennington III, wouter bolsterlee, xarx00, Xavier O., xjtdy888, Yannic A., yparitcher, 佛跳墙, 落心
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -13,7 +13,6 @@ import (
|
||||
"errors"
|
||||
"iter"
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"sync"
|
||||
"testing"
|
||||
@@ -1168,7 +1167,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 := path.Join(dir, "test")
|
||||
base := filepath.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,6 +57,20 @@ 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,6 +13,7 @@ import (
|
||||
"log/slog"
|
||||
"net"
|
||||
"net/url"
|
||||
"slices"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
@@ -185,9 +186,10 @@ func (t *tcpListener) WANAddresses() []*url.URL {
|
||||
|
||||
t.mut.RUnlock()
|
||||
|
||||
// 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 {
|
||||
// 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" }) {
|
||||
uri := *t.uri
|
||||
uri.Host = "0.0.0.0:0"
|
||||
uris = append([]*url.URL{&uri}, uris...)
|
||||
|
||||
@@ -289,7 +289,6 @@ 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"
|
||||
"github.com/syncthing/syncthing/lib/ur/contract"
|
||||
)
|
||||
|
||||
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, ur.FailureData{
|
||||
s.evLogger.Log(events.Failure, contract.FailureData{
|
||||
Description: msg,
|
||||
Extra: extraStrs,
|
||||
})
|
||||
|
||||
@@ -282,3 +282,16 @@ 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
|
||||
}
|
||||
|
||||
+14
-26
@@ -23,6 +23,7 @@ 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"
|
||||
)
|
||||
@@ -39,23 +40,10 @@ var (
|
||||
invalidEventDataType = "failure event data is not a string"
|
||||
)
|
||||
|
||||
type FailureReport struct {
|
||||
FailureData
|
||||
|
||||
Count int
|
||||
Version string
|
||||
}
|
||||
|
||||
type FailureData struct {
|
||||
Description string
|
||||
Goroutines string
|
||||
Extra map[string]string
|
||||
}
|
||||
|
||||
func FailureDataWithGoroutines(description string) FailureData {
|
||||
func FailureDataWithGoroutines(description string) contract.FailureData {
|
||||
var buf strings.Builder
|
||||
pprof.Lookup("goroutine").WriteTo(&buf, 1)
|
||||
return FailureData{
|
||||
return contract.FailureData{
|
||||
Description: description,
|
||||
Goroutines: buf.String(),
|
||||
Extra: make(map[string]string),
|
||||
@@ -86,7 +74,7 @@ type failureHandler struct {
|
||||
type failureStat struct {
|
||||
first, last time.Time
|
||||
count int
|
||||
data FailureData
|
||||
data contract.FailureData
|
||||
}
|
||||
|
||||
func (h *failureHandler) Serve(ctx context.Context) error {
|
||||
@@ -105,24 +93,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(FailureData{Description: evChanClosed}, time.Now())
|
||||
h.addReport(contract.FailureData{Description: evChanClosed}, time.Now())
|
||||
evChan = nil
|
||||
continue
|
||||
}
|
||||
var data FailureData
|
||||
var data contract.FailureData
|
||||
switch d := e.Data.(type) {
|
||||
case string:
|
||||
data.Description = d
|
||||
case FailureData:
|
||||
case contract.FailureData:
|
||||
data = d
|
||||
default:
|
||||
// Same here, shouldn't ever happen.
|
||||
h.addReport(FailureData{Description: invalidEventDataType}, time.Now())
|
||||
h.addReport(contract.FailureData{Description: invalidEventDataType}, time.Now())
|
||||
continue
|
||||
}
|
||||
h.addReport(data, e.Time)
|
||||
case <-timer.C:
|
||||
reports := make([]FailureReport, 0, len(h.buf))
|
||||
reports := make([]contract.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 {
|
||||
@@ -152,7 +140,7 @@ func (h *failureHandler) Serve(ctx context.Context) error {
|
||||
if sub != nil {
|
||||
sub.Unsubscribe()
|
||||
if len(h.buf) > 0 {
|
||||
reports := make([]FailureReport, 0, len(h.buf))
|
||||
reports := make([]contract.FailureReport, 0, len(h.buf))
|
||||
for _, stat := range h.buf {
|
||||
reports = append(reports, newFailureReport(stat))
|
||||
}
|
||||
@@ -179,7 +167,7 @@ func (h *failureHandler) applyOpts(opts config.OptionsConfiguration, sub events.
|
||||
return url, nil, nil
|
||||
}
|
||||
|
||||
func (h *failureHandler) addReport(data FailureData, evTime time.Time) {
|
||||
func (h *failureHandler) addReport(data contract.FailureData, evTime time.Time) {
|
||||
if stat, ok := h.buf[data.Description]; ok {
|
||||
stat.last = evTime
|
||||
stat.count++
|
||||
@@ -204,7 +192,7 @@ func (*failureHandler) String() string {
|
||||
return "FailureHandler"
|
||||
}
|
||||
|
||||
func sendFailureReports(ctx context.Context, reports []FailureReport, url string) {
|
||||
func sendFailureReports(ctx context.Context, reports []contract.FailureReport, url string) {
|
||||
var b bytes.Buffer
|
||||
if err := json.NewEncoder(&b).Encode(reports); err != nil {
|
||||
panic(err)
|
||||
@@ -235,8 +223,8 @@ func sendFailureReports(ctx context.Context, reports []FailureReport, url string
|
||||
resp.Body.Close()
|
||||
}
|
||||
|
||||
func newFailureReport(stat *failureStat) FailureReport {
|
||||
return FailureReport{
|
||||
func newFailureReport(stat *failureStat) contract.FailureReport {
|
||||
return contract.FailureReport{
|
||||
FailureData: stat.data,
|
||||
Count: stat.count,
|
||||
Version: build.LongVersion,
|
||||
|
||||
@@ -15,6 +15,7 @@ import (
|
||||
|
||||
"github.com/syncthing/syncthing/lib/build"
|
||||
"github.com/syncthing/syncthing/lib/config"
|
||||
"github.com/syncthing/syncthing/lib/fs"
|
||||
)
|
||||
|
||||
func TestTaggedFilename(t *testing.T) {
|
||||
@@ -256,3 +257,57 @@ 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())
|
||||
}
|
||||
}
|
||||
|
||||
+66
-36
@@ -192,48 +192,78 @@ func archiveFile(method fs.CopyRangeMethod, srcFs, dstFs fs.Filesystem, filePath
|
||||
return err
|
||||
}
|
||||
|
||||
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
|
||||
// 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
|
||||
}
|
||||
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
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
}
|
||||
return dupDirWithPerms(srcFs, dstFs, folderPath)
|
||||
}
|
||||
|
||||
func dupDirWithPerms(srcFs, dstFs fs.Filesystem, folderPath string) error {
|
||||
srcStat, err := srcFs.Stat(folderPath)
|
||||
if err != nil {
|
||||
// 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
|
||||
}
|
||||
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())
|
||||
|
||||
// Extra chmod to ensure our permissions override umask
|
||||
_ = dstFs.Chmod(path, srcPerms)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func restoreFile(method fs.CopyRangeMethod, src, dst fs.Filesystem, filePath string, versionTime time.Time, tagger fileTagger) error {
|
||||
|
||||
+1
-1
@@ -27,7 +27,7 @@ level margin: \\n[rst2man-indent\\n[rst2man-indent-level]]
|
||||
.\" new: \\n[rst2man-indent\\n[rst2man-indent-level]]
|
||||
.in \\n[rst2man-indent\\n[rst2man-indent-level]]u
|
||||
..
|
||||
.TH "STDISCOSRV" "1" "Apr 30, 2026" "v2.0.0" "Syncthing"
|
||||
.TH "STDISCOSRV" "1" "May 13, 2026" "v2.1.0" "Syncthing"
|
||||
.SH NAME
|
||||
stdiscosrv \- Syncthing Discovery Server
|
||||
.SH SYNOPSIS
|
||||
|
||||
+1
-1
@@ -27,7 +27,7 @@ level margin: \\n[rst2man-indent\\n[rst2man-indent-level]]
|
||||
.\" new: \\n[rst2man-indent\\n[rst2man-indent-level]]
|
||||
.in \\n[rst2man-indent\\n[rst2man-indent-level]]u
|
||||
..
|
||||
.TH "STRELAYSRV" "1" "Apr 30, 2026" "v2.0.0" "Syncthing"
|
||||
.TH "STRELAYSRV" "1" "May 13, 2026" "v2.1.0" "Syncthing"
|
||||
.SH NAME
|
||||
strelaysrv \- Syncthing Relay Server
|
||||
.SH SYNOPSIS
|
||||
|
||||
+1
-1
@@ -28,7 +28,7 @@ level margin: \\n[rst2man-indent\\n[rst2man-indent-level]]
|
||||
.\" new: \\n[rst2man-indent\\n[rst2man-indent-level]]
|
||||
.in \\n[rst2man-indent\\n[rst2man-indent-level]]u
|
||||
..
|
||||
.TH "SYNCTHING-BEP" "7" "Apr 30, 2026" "v2.0.0" "Syncthing"
|
||||
.TH "SYNCTHING-BEP" "7" "May 13, 2026" "v2.1.0" "Syncthing"
|
||||
.SH NAME
|
||||
syncthing-bep \- Block Exchange Protocol v1
|
||||
.SH INTRODUCTION AND DEFINITIONS
|
||||
|
||||
@@ -27,7 +27,7 @@ level margin: \\n[rst2man-indent\\n[rst2man-indent-level]]
|
||||
.\" new: \\n[rst2man-indent\\n[rst2man-indent-level]]
|
||||
.in \\n[rst2man-indent\\n[rst2man-indent-level]]u
|
||||
..
|
||||
.TH "SYNCTHING-CONFIG" "5" "Apr 30, 2026" "v2.0.0" "Syncthing"
|
||||
.TH "SYNCTHING-CONFIG" "5" "May 13, 2026" "v2.1.0" "Syncthing"
|
||||
.SH NAME
|
||||
syncthing-config \- Syncthing Configuration
|
||||
.SH OVERVIEW
|
||||
|
||||
@@ -27,7 +27,7 @@ level margin: \\n[rst2man-indent\\n[rst2man-indent-level]]
|
||||
.\" new: \\n[rst2man-indent\\n[rst2man-indent-level]]
|
||||
.in \\n[rst2man-indent\\n[rst2man-indent-level]]u
|
||||
..
|
||||
.TH "SYNCTHING-DEVICE-IDS" "7" "Apr 30, 2026" "v2.0.0" "Syncthing"
|
||||
.TH "SYNCTHING-DEVICE-IDS" "7" "May 13, 2026" "v2.1.0" "Syncthing"
|
||||
.SH NAME
|
||||
syncthing-device-ids \- Understanding Device IDs
|
||||
.sp
|
||||
|
||||
@@ -27,7 +27,7 @@ level margin: \\n[rst2man-indent\\n[rst2man-indent-level]]
|
||||
.\" new: \\n[rst2man-indent\\n[rst2man-indent-level]]
|
||||
.in \\n[rst2man-indent\\n[rst2man-indent-level]]u
|
||||
..
|
||||
.TH "SYNCTHING-EVENT-API" "7" "Apr 30, 2026" "v2.0.0" "Syncthing"
|
||||
.TH "SYNCTHING-EVENT-API" "7" "May 13, 2026" "v2.1.0" "Syncthing"
|
||||
.SH NAME
|
||||
syncthing-event-api \- Event API
|
||||
.SH DESCRIPTION
|
||||
|
||||
+1
-1
@@ -27,7 +27,7 @@ level margin: \\n[rst2man-indent\\n[rst2man-indent-level]]
|
||||
.\" new: \\n[rst2man-indent\\n[rst2man-indent-level]]
|
||||
.in \\n[rst2man-indent\\n[rst2man-indent-level]]u
|
||||
..
|
||||
.TH "SYNCTHING-FAQ" "7" "Apr 30, 2026" "v2.0.0" "Syncthing"
|
||||
.TH "SYNCTHING-FAQ" "7" "May 13, 2026" "v2.1.0" "Syncthing"
|
||||
.SH NAME
|
||||
syncthing-faq \- Frequently Asked Questions
|
||||
.INDENT 0.0
|
||||
|
||||
@@ -27,7 +27,7 @@ level margin: \\n[rst2man-indent\\n[rst2man-indent-level]]
|
||||
.\" new: \\n[rst2man-indent\\n[rst2man-indent-level]]
|
||||
.in \\n[rst2man-indent\\n[rst2man-indent-level]]u
|
||||
..
|
||||
.TH "SYNCTHING-GLOBALDISCO" "7" "Apr 30, 2026" "v2.0.0" "Syncthing"
|
||||
.TH "SYNCTHING-GLOBALDISCO" "7" "May 13, 2026" "v2.1.0" "Syncthing"
|
||||
.SH NAME
|
||||
syncthing-globaldisco \- Global Discovery Protocol v3
|
||||
.SH ANNOUNCEMENTS
|
||||
|
||||
@@ -27,7 +27,7 @@ level margin: \\n[rst2man-indent\\n[rst2man-indent-level]]
|
||||
.\" new: \\n[rst2man-indent\\n[rst2man-indent-level]]
|
||||
.in \\n[rst2man-indent\\n[rst2man-indent-level]]u
|
||||
..
|
||||
.TH "SYNCTHING-LOCALDISCO" "7" "Apr 30, 2026" "v2.0.0" "Syncthing"
|
||||
.TH "SYNCTHING-LOCALDISCO" "7" "May 13, 2026" "v2.1.0" "Syncthing"
|
||||
.SH NAME
|
||||
syncthing-localdisco \- Local Discovery Protocol v4
|
||||
.SH MODE OF OPERATION
|
||||
|
||||
@@ -27,7 +27,7 @@ level margin: \\n[rst2man-indent\\n[rst2man-indent-level]]
|
||||
.\" new: \\n[rst2man-indent\\n[rst2man-indent-level]]
|
||||
.in \\n[rst2man-indent\\n[rst2man-indent-level]]u
|
||||
..
|
||||
.TH "SYNCTHING-NETWORKING" "7" "Apr 30, 2026" "v2.0.0" "Syncthing"
|
||||
.TH "SYNCTHING-NETWORKING" "7" "May 13, 2026" "v2.1.0" "Syncthing"
|
||||
.SH NAME
|
||||
syncthing-networking \- Firewall Setup
|
||||
.SH ROUTER SETUP
|
||||
|
||||
@@ -28,7 +28,7 @@ level margin: \\n[rst2man-indent\\n[rst2man-indent-level]]
|
||||
.\" new: \\n[rst2man-indent\\n[rst2man-indent-level]]
|
||||
.in \\n[rst2man-indent\\n[rst2man-indent-level]]u
|
||||
..
|
||||
.TH "SYNCTHING-RELAY" "7" "Apr 30, 2026" "v2.0.0" "Syncthing"
|
||||
.TH "SYNCTHING-RELAY" "7" "May 13, 2026" "v2.1.0" "Syncthing"
|
||||
.SH NAME
|
||||
syncthing-relay \- Relay Protocol v1
|
||||
.SH WHAT IS A RELAY?
|
||||
|
||||
@@ -27,7 +27,7 @@ level margin: \\n[rst2man-indent\\n[rst2man-indent-level]]
|
||||
.\" new: \\n[rst2man-indent\\n[rst2man-indent-level]]
|
||||
.in \\n[rst2man-indent\\n[rst2man-indent-level]]u
|
||||
..
|
||||
.TH "SYNCTHING-REST-API" "7" "Apr 30, 2026" "v2.0.0" "Syncthing"
|
||||
.TH "SYNCTHING-REST-API" "7" "May 13, 2026" "v2.1.0" "Syncthing"
|
||||
.SH NAME
|
||||
syncthing-rest-api \- REST API
|
||||
.sp
|
||||
|
||||
@@ -27,7 +27,7 @@ level margin: \\n[rst2man-indent\\n[rst2man-indent-level]]
|
||||
.\" new: \\n[rst2man-indent\\n[rst2man-indent-level]]
|
||||
.in \\n[rst2man-indent\\n[rst2man-indent-level]]u
|
||||
..
|
||||
.TH "SYNCTHING-SECURITY" "7" "Apr 30, 2026" "v2.0.0" "Syncthing"
|
||||
.TH "SYNCTHING-SECURITY" "7" "May 13, 2026" "v2.1.0" "Syncthing"
|
||||
.SH NAME
|
||||
syncthing-security \- Security Principles
|
||||
.sp
|
||||
|
||||
@@ -27,7 +27,7 @@ level margin: \\n[rst2man-indent\\n[rst2man-indent-level]]
|
||||
.\" new: \\n[rst2man-indent\\n[rst2man-indent-level]]
|
||||
.in \\n[rst2man-indent\\n[rst2man-indent-level]]u
|
||||
..
|
||||
.TH "SYNCTHING-STIGNORE" "5" "Apr 30, 2026" "v2.0.0" "Syncthing"
|
||||
.TH "SYNCTHING-STIGNORE" "5" "May 13, 2026" "v2.1.0" "Syncthing"
|
||||
.SH NAME
|
||||
syncthing-stignore \- Prevent files from being synchronized to other nodes
|
||||
.SH SYNOPSIS
|
||||
|
||||
@@ -27,7 +27,7 @@ level margin: \\n[rst2man-indent\\n[rst2man-indent-level]]
|
||||
.\" new: \\n[rst2man-indent\\n[rst2man-indent-level]]
|
||||
.in \\n[rst2man-indent\\n[rst2man-indent-level]]u
|
||||
..
|
||||
.TH "SYNCTHING-VERSIONING" "7" "Apr 30, 2026" "v2.0.0" "Syncthing"
|
||||
.TH "SYNCTHING-VERSIONING" "7" "May 13, 2026" "v2.1.0" "Syncthing"
|
||||
.SH NAME
|
||||
syncthing-versioning \- Keep automatic backups of deleted files by other nodes
|
||||
.sp
|
||||
|
||||
+1
-1
@@ -27,7 +27,7 @@ level margin: \\n[rst2man-indent\\n[rst2man-indent-level]]
|
||||
.\" new: \\n[rst2man-indent\\n[rst2man-indent-level]]
|
||||
.in \\n[rst2man-indent\\n[rst2man-indent-level]]u
|
||||
..
|
||||
.TH "SYNCTHING" "1" "Apr 30, 2026" "v2.0.0" "Syncthing"
|
||||
.TH "SYNCTHING" "1" "May 13, 2026" "v2.1.0" "Syncthing"
|
||||
.SH NAME
|
||||
syncthing \- Syncthing
|
||||
.SH SYNOPSIS
|
||||
|
||||
Reference in New Issue
Block a user