Compare commits

..
4 Commits
Author SHA1 Message Date
Jakob Borg 0945304a79 build: fix detection of next rc version 2025-06-20 11:17:23 +02:00
Jakob Borg 9703dd9f57 build: import release workflow changes from main 2025-06-20 11:12:05 +02:00
yparitcherandJakob Borg 259e9ef08e fix(protocol): slightly loosen/correct ownership comparison criteria (fixes #9879) (#10176)
Only Require either matching UID & GID OR matching Names.

If the 2 devices have a different Name => UID mapping, they can never be
totaly equal. Therefore when syncing we try matching the Name and fall
back to the UID. However when scanning for changes we currently require
both the Name & UID to match. This leads to forever having out of sync
files back and forth, or local additions when receive only.

This patch does not change the sending behavoir. It only change what we
decide is equal for exisiting files with mismapped Name => UID,

The added testcases show the change: Test 1,5,6 are the same as current.
Test 2,3 Are what change with this patch (from false to true). Test 4 is
a subset of test 2 they is currently special cased as true, which does
not chnage.

Co-authored-by: Jakob Borg <jakob@kastelo.net>
2025-06-20 09:55:42 +02:00
Simon Frei 6a0c6128d8 fix(watchaggregator): properly handle sub-second watch durations (fixes #9927) (#10179)
I'll let Audrius words from the ticket explain this :)

> I'm a bit lost, time.Duration is an int64, yet watcher delay is float,
> anything sub 1s gets rounded down to 0, so you just end up going into
an
> infinite loop.


https://github.com/syncthing/syncthing/issues/9927#issuecomment-2967736106
2025-06-15 10:29:33 +02:00
728 changed files with 70208 additions and 31555 deletions
-142
View File
@@ -1,142 +0,0 @@
name: custom release
permissions:
contents: write
releases: write
on:
push:
branches:
- main
paths:
- ".gitea/workflows/custom-release.yml"
- "patches/**"
- "scripts/update-custom-release.sh"
- "scripts/sync-upstream.sh"
workflow_dispatch:
inputs:
upstream_tag:
description: "Optional upstream Syncthing tag, for example v2.1.0"
required: false
suffix:
description: "Optional custom release suffix, for example stignore.7"
required: false
schedule:
- cron: "17 04 * * *"
jobs:
build-custom-release:
runs-on: ffmini_macos_arm64
steps:
- name: Check out repository
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Set up Go
uses: actions/setup-go@v5
with:
go-version-file: go.mod
cache: false
- name: Configure Git author
run: |
git config user.name "Gitea Actions"
git config user.email "actions@git.felixfoertsch.de"
- name: Mirror upstream and rebuild patched main
run: ./scripts/sync-upstream.sh
env:
SYNC_REMOTE: origin
- name: Set up tea
run: |
go install code.gitea.io/tea@v0.14.1
echo "$(go env GOPATH)/bin" >> "$GITHUB_PATH"
"$(go env GOPATH)/bin/tea" logins delete actions >/dev/null 2>&1 || true
"$(go env GOPATH)/bin/tea" logins add --name actions --url https://git.felixfoertsch.de --token "$GITEA_TOKEN" --no-version-check
env:
GITEA_TOKEN: ${{ secrets.GITEA_TOKEN }}
- name: Import Developer ID certificate
run: |
set -euo pipefail
keychain_dir="$HOME/Library/Keychains"
mkdir -p "$keychain_dir"
keychain_path="$keychain_dir/syncthing-release-signing-${GITHUB_RUN_ID:-$$}.keychain-db"
keychain_password="$(openssl rand -hex 24)"
certificate_path="$RUNNER_TEMP/developer-id-application.p12"
previous_default_keychain="$(security default-keychain -d user 2>/dev/null | sed 's/[ "]//g' || true)"
echo "CUSTOM_RELEASE_KEYCHAIN_PATH=$keychain_path" >> "$GITHUB_ENV"
echo "CUSTOM_RELEASE_KEYCHAIN_PASSWORD=$keychain_password" >> "$GITHUB_ENV"
echo "CUSTOM_RELEASE_PREVIOUS_DEFAULT_KEYCHAIN=$previous_default_keychain" >> "$GITHUB_ENV"
if [ -z "$DEVELOPER_ID_APPLICATION_P12_BASE64" ]; then
echo "DEVELOPER_ID_APPLICATION_P12_BASE64 secret is required" >&2
exit 1
fi
printf '%s' "$DEVELOPER_ID_APPLICATION_P12_BASE64" | base64 -D > "$certificate_path"
rm -f "$keychain_path"
security create-keychain -p "$keychain_password" "$keychain_path"
security set-keychain-settings -lut 21600 "$keychain_path"
security unlock-keychain -p "$keychain_password" "$keychain_path"
security import "$certificate_path" -k "$keychain_path" -P "$DEVELOPER_ID_APPLICATION_P12_PASSWORD" -A -T /usr/bin/codesign -T /usr/bin/security
existing_keychains=()
while IFS= read -r existing_keychain; do
existing_keychain="$(printf '%s' "$existing_keychain" | sed 's/[ "]//g')"
if [ -n "$existing_keychain" ] && [ -e "$existing_keychain" ] && [[ "$existing_keychain" != *"/syncthing-release-signing-"*".keychain-db" ]]; then
existing_keychains+=("$existing_keychain")
fi
done < <(security list-keychains)
security list-keychains -s "$keychain_path" "${existing_keychains[@]}"
security list-keychains -d user -s "$keychain_path" "${existing_keychains[@]}" || true
security default-keychain -d user -s "$keychain_path" || true
security list-keychains
security list-keychains -d user || true
security set-key-partition-list -S apple-tool:,apple:,codesign: -s -k "$keychain_password" "$keychain_path"
identity_output="$(security find-identity -v -p codesigning "$keychain_path")"
printf '%s\n' "$identity_output"
security find-identity -v -p codesigning
codesign_identity_sha1="$(printf '%s\n' "$identity_output" | awk '/"Developer ID Application:/ { print $2; exit }')"
codesign_identity="$(printf '%s\n' "$identity_output" | sed -n 's/.*"\(Developer ID Application:[^"]*\)".*/\1/p' | head -n 1)"
if [ -z "$codesign_identity" ]; then
echo "Developer ID Application signing identity is required in DEVELOPER_ID_APPLICATION_P12_BASE64" >&2
exit 1
fi
probe_binary="$RUNNER_TEMP/codesign-probe"
cp /usr/bin/true "$probe_binary"
codesign --force --dryrun --sign "$codesign_identity" --keychain "$keychain_path" --options runtime --timestamp "$probe_binary"
echo "CUSTOM_RELEASE_CODESIGN_IDENTITY=$codesign_identity" >> "$GITHUB_ENV"
echo "CUSTOM_RELEASE_CODESIGN_IDENTITY_SHA1=$codesign_identity_sha1" >> "$GITHUB_ENV"
env:
DEVELOPER_ID_APPLICATION_P12_BASE64: ${{ secrets.DEVELOPER_ID_APPLICATION_P12_BASE64 }}
DEVELOPER_ID_APPLICATION_P12_PASSWORD: ${{ secrets.DEVELOPER_ID_APPLICATION_P12_PASSWORD }}
- name: Build patched Syncthing release
run: ./scripts/update-custom-release.sh
env:
CUSTOM_RELEASE_UPSTREAM_TAG: ${{ github.event.inputs.upstream_tag }}
CUSTOM_RELEASE_SUFFIX: ${{ github.event.inputs.suffix }}
CUSTOM_RELEASE_PUSH: "1"
CUSTOM_RELEASE_PUSH_BRANCH: "0"
CUSTOM_RELEASE_REMOTE: origin
CUSTOM_RELEASE_BUILDS: "darwin/arm64/zip/1 linux/amd64/tar/0 linux/arm64/tar/0"
CUSTOM_RELEASE_CODESIGN_TEAM_ID: "NG5W75WE8U"
CUSTOM_RELEASE_SIGN_DARWIN: "1"
CUSTOM_RELEASE_REQUIRE_GATEKEEPER_ASSESSMENT: "0"
CUSTOM_RELEASE_CREATE_GITEA_RELEASE: "1"
CUSTOM_RELEASE_TEA_REPO: felixfoertsch/syncthing
- name: Delete temporary keychain
if: always()
run: |
if [ -n "${CUSTOM_RELEASE_PREVIOUS_DEFAULT_KEYCHAIN:-}" ] && [ -e "$CUSTOM_RELEASE_PREVIOUS_DEFAULT_KEYCHAIN" ]; then
security default-keychain -d user -s "$CUSTOM_RELEASE_PREVIOUS_DEFAULT_KEYCHAIN" || true
fi
if [ -n "${CUSTOM_RELEASE_KEYCHAIN_PATH:-}" ]; then
security delete-keychain "$CUSTOM_RELEASE_KEYCHAIN_PATH" || true
fi
+2 -17
View File
@@ -1,28 +1,13 @@
version: 2 version: 2
updates: updates:
- package-ecosystem: "github-actions" - package-ecosystem: "github-actions"
directory: "/" directory: "/"
schedule: schedule:
interval: monthly interval: monthly
cooldown: open-pull-requests-limit: 10
default-days: 14
groups:
actions:
applies-to: version-updates
patterns:
- "*"
- package-ecosystem: "gomod" - package-ecosystem: "gomod"
directory: "/" directory: "/"
schedule: schedule:
interval: monthly interval: monthly
allow: open-pull-requests-limit: 10
- dependency-type: direct
cooldown:
default-days: 14
groups:
dependencies:
applies-to: version-updates
patterns:
- "*"
-52
View File
@@ -1,52 +0,0 @@
version: 1
creds:
- registry: docker.io
user: "{{env \"DOCKERHUB_USERNAME\"}}"
pass: "{{env \"DOCKERHUB_TOKEN\"}}"
defaults:
ratelimit:
min: 100
retry: 1m
parallel: 4
sync:
- source: ghcr.io/syncthing/syncthing
target: docker.io/syncthing/syncthing
type: repository
tags:
allow:
- latest
- rc
- edge
- \d+
- \d+\.\d+
- \d+\.\d+\.\d+
- \d+\.\d+\.\d+-rc\.\d+
- source: ghcr.io/syncthing/relaysrv
target: docker.io/syncthing/relaysrv
type: repository
tags:
allow:
- latest
- rc
- edge
- \d+
- \d+\.\d+
- \d+\.\d+\.\d+
- \d+\.\d+\.\d+-rc\.\d+
- source: ghcr.io/syncthing/discosrv
target: docker.io/syncthing/discosrv
type: repository
tags:
allow:
- latest
- rc
- edge
- \d+
- \d+\.\d+
- \d+\.\d+\.\d+
- \d+\.\d+\.\d+-rc\.\d+
+9 -22
View File
@@ -7,7 +7,7 @@ on:
- infra-* - infra-*
env: env:
GO_VERSION: "~1.27.0" GO_VERSION: "~1.24.0"
CGO_ENABLED: "0" CGO_ENABLED: "0"
BUILD_USER: docker BUILD_USER: docker
BUILD_HOST: github.syncthing.net BUILD_HOST: github.syncthing.net
@@ -15,15 +15,14 @@ env:
permissions: permissions:
contents: read contents: read
packages: write packages: write
id-token: write
jobs: jobs:
docker-syncthing: docker-syncthing:
name: Build and push Docker images name: Build and push Docker images
if: github.repository_owner == 'syncthing' if: github.repository == 'syncthing/syncthing'
runs-on: ubuntu-latest runs-on: ubuntu-latest
environment: docker
strategy: strategy:
fail-fast: false
matrix: matrix:
pkg: pkg:
- stcrashreceiver - stcrashreceiver
@@ -31,23 +30,23 @@ jobs:
- stupgrades - stupgrades
- ursrv - ursrv
steps: steps:
- uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # was: actions/checkout@v5 - uses: actions/checkout@v4
with: with:
fetch-depth: 0 fetch-depth: 0
- uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # was: actions/setup-go@v6 - uses: actions/setup-go@v5
with: with:
go-version: ${{ env.GO_VERSION }} go-version: ${{ env.GO_VERSION }}
check-latest: true check-latest: true
- name: Login to Docker Hub - name: Login to Docker Hub
uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3 uses: docker/login-action@v3
with: with:
username: ${{ secrets.DOCKERHUB_USERNAME }} username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }} password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Login to GHCR - name: Login to GHCR
uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3 uses: docker/login-action@v3
with: with:
registry: ghcr.io registry: ghcr.io
username: ${{ github.actor }} username: ${{ github.actor }}
@@ -60,11 +59,8 @@ jobs:
mv ${{ matrix.pkg }} ${{ matrix.pkg }}-linux-"$arch" mv ${{ matrix.pkg }} ${{ matrix.pkg }}-linux-"$arch"
done done
- name: Set up QEMU
uses: docker/setup-qemu-action@c7c53464625b32c7a7e944ae62b3e17d2b600130 # v3
- name: Set up Docker Buildx - name: Set up Docker Buildx
uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3 uses: docker/setup-buildx-action@v3
- name: Set Docker tags (all branches) - name: Set Docker tags (all branches)
run: | run: |
@@ -78,8 +74,7 @@ jobs:
echo "TAGS=$tags" >> $GITHUB_ENV echo "TAGS=$tags" >> $GITHUB_ENV
- name: Build and push - name: Build and push
id: build uses: docker/build-push-action@v5
uses: docker/build-push-action@ca052bb54ab0790a636c9b5f226502c73d547a25 # v5
with: with:
context: . context: .
file: ./Dockerfile.${{ matrix.pkg }} file: ./Dockerfile.${{ matrix.pkg }}
@@ -88,11 +83,3 @@ jobs:
tags: ${{ env.TAGS }} tags: ${{ env.TAGS }}
labels: | labels: |
org.opencontainers.image.revision=${{ github.sha }} org.opencontainers.image.revision=${{ github.sha }}
- name: Install Cosign
if: github.ref == 'refs/heads/infrastructure'
uses: sigstore/cosign-installer@ba7bc0a3fef59531c69a25acd34668d6d3fe6f22 # v4.1.0
- name: Sign image
if: github.ref == 'refs/heads/infrastructure'
run: cosign sign --yes --recursive ghcr.io/syncthing/infra/${{ matrix.pkg }}:latest@${{ steps.build.outputs.digest }}
+18
View File
@@ -0,0 +1,18 @@
name: Build Syncthing (Nightly)
on:
schedule:
# Run nightly build at 05:00 UTC
- cron: '00 05 * * *'
workflow_dispatch:
permissions:
contents: write
packages: write
jobs:
build-syncthing:
uses: ./.github/workflows/build-syncthing.yaml
# if we only want nightlies to run for specific users:
# if: contains(fromJSON('["syncthing", "calmh"]'), github.repository_owner)
secrets: inherit
File diff suppressed because it is too large Load Diff
-150
View File
@@ -1,150 +0,0 @@
name: custom release
permissions:
contents: write
on:
push:
branches:
- main
paths:
- ".github/workflows/custom-release.yml"
- "patches/**"
- "scripts/update-custom-release.sh"
- "scripts/sync-upstream.sh"
workflow_dispatch:
inputs:
upstream_tag:
description: "Optional upstream Syncthing tag, for example v2.1.3"
required: false
suffix:
description: "Optional custom release suffix, for example stignore.7"
required: false
schedule:
- cron: "17 04 * * *"
jobs:
build-custom-release:
runs-on: macos-14
steps:
- name: Check out repository
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Set up Go
uses: actions/setup-go@v5
with:
go-version-file: go.mod
cache: false
- name: Configure Git author
run: |
git config user.name "GitHub Actions"
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
- name: Mirror upstream and rebuild patched main
run: ./scripts/sync-upstream.sh
env:
SYNC_REMOTE: origin
- name: Import Developer ID certificate
run: |
set -euo pipefail
keychain_path="$RUNNER_TEMP/syncthing-release-signing.keychain-db"
keychain_password="$(openssl rand -hex 24)"
certificate_path="$RUNNER_TEMP/developer-id-application.p12"
previous_default_keychain="$(security default-keychain -d user 2>/dev/null | sed 's/[ "]//g' || true)"
echo "::add-mask::$keychain_password"
echo "CUSTOM_RELEASE_KEYCHAIN_PATH=$keychain_path" >> "$GITHUB_ENV"
echo "CUSTOM_RELEASE_KEYCHAIN_PASSWORD=$keychain_password" >> "$GITHUB_ENV"
echo "CUSTOM_RELEASE_CERTIFICATE_PATH=$certificate_path" >> "$GITHUB_ENV"
echo "CUSTOM_RELEASE_PREVIOUS_DEFAULT_KEYCHAIN=$previous_default_keychain" >> "$GITHUB_ENV"
if [ -z "$DEVELOPER_ID_APPLICATION_P12_BASE64" ]; then
echo "DEVELOPER_ID_APPLICATION_P12_BASE64 secret is required" >&2
exit 1
fi
if [ -z "$DEVELOPER_ID_APPLICATION_P12_PASSWORD" ]; then
echo "DEVELOPER_ID_APPLICATION_P12_PASSWORD secret is required" >&2
exit 1
fi
printf '%s' "$DEVELOPER_ID_APPLICATION_P12_BASE64" | base64 -D > "$certificate_path"
security create-keychain -p "$keychain_password" "$keychain_path"
security set-keychain-settings -lut 21600 "$keychain_path"
security unlock-keychain -p "$keychain_password" "$keychain_path"
security import "$certificate_path" -k "$keychain_path" -P "$DEVELOPER_ID_APPLICATION_P12_PASSWORD" -A -T /usr/bin/codesign -T /usr/bin/security
security list-keychains -d user -s "$keychain_path"
security default-keychain -d user -s "$keychain_path"
security set-key-partition-list -S apple-tool:,apple:,codesign: -s -k "$keychain_password" "$keychain_path"
identity_output="$(security find-identity -v -p codesigning "$keychain_path")"
printf '%s\n' "$identity_output"
codesign_identity="$(printf '%s\n' "$identity_output" | sed -n 's/.*"\(Developer ID Application:[^"]*\)".*/\1/p' | head -n 1)"
if [ -z "$codesign_identity" ]; then
echo "Developer ID Application signing identity is required in DEVELOPER_ID_APPLICATION_P12_BASE64" >&2
exit 1
fi
probe_binary="$RUNNER_TEMP/codesign-probe"
cp /usr/bin/true "$probe_binary"
codesign --force --dryrun --sign "$codesign_identity" --keychain "$keychain_path" --options runtime --timestamp "$probe_binary"
echo "CUSTOM_RELEASE_CODESIGN_IDENTITY=$codesign_identity" >> "$GITHUB_ENV"
env:
DEVELOPER_ID_APPLICATION_P12_BASE64: ${{ secrets.DEVELOPER_ID_APPLICATION_P12_BASE64 }}
DEVELOPER_ID_APPLICATION_P12_PASSWORD: ${{ secrets.DEVELOPER_ID_APPLICATION_P12_PASSWORD }}
- name: Build patched Syncthing release
run: |
case "$(uname -m)" in
arm64) darwin_arch=arm64 ;;
x86_64) darwin_arch=amd64 ;;
*) echo "Unsupported macOS runner architecture: $(uname -m)" >&2; exit 1 ;;
esac
export CUSTOM_RELEASE_BUILDS="darwin/$darwin_arch/zip/1 linux/amd64/tar/0 linux/arm64/tar/0"
./scripts/update-custom-release.sh
env:
CUSTOM_RELEASE_UPSTREAM_TAG: ${{ github.event.inputs.upstream_tag }}
CUSTOM_RELEASE_SUFFIX: ${{ github.event.inputs.suffix }}
CUSTOM_RELEASE_PUSH: "1"
CUSTOM_RELEASE_PUSH_BRANCH: "0"
CUSTOM_RELEASE_REMOTE: origin
CUSTOM_RELEASE_CODESIGN_TEAM_ID: "NG5W75WE8U"
CUSTOM_RELEASE_SIGN_DARWIN: "1"
CUSTOM_RELEASE_REQUIRE_GATEKEEPER_ASSESSMENT: "0"
CUSTOM_RELEASE_CREATE_GITEA_RELEASE: "0"
GH_TOKEN: ${{ github.token }}
- name: Publish GitHub release
run: |
tag="${CUSTOM_RELEASE_UPSTREAM_TAG:-$(git ls-remote --refs --tags --sort='version:refname' https://github.com/syncthing/syncthing.git 'v[0-9]*' | awk '{ tag = $2; sub("refs/tags/", "", tag); if (tag ~ /^v[0-9]+\.[0-9]+\.[0-9]+$/) latest = tag } END { print latest }')}-$CUSTOM_RELEASE_SUFFIX"
if gh release view "$tag" >/dev/null 2>&1; then
echo "GitHub release $tag already exists; nothing to do."
exit 0
fi
assets=()
for asset in dist/*; do
[ -f "$asset" ] || continue
[ "$(basename "$asset")" = release-notes.md ] && continue
assets+=("$asset")
done
gh release create "$tag" "${assets[@]}" --title "$tag" --notes-file dist/release-notes.md --verify-tag
env:
CUSTOM_RELEASE_UPSTREAM_TAG: ${{ github.event.inputs.upstream_tag }}
CUSTOM_RELEASE_SUFFIX: ${{ github.event.inputs.suffix || 'stignore.7' }}
GH_TOKEN: ${{ github.token }}
- name: Delete temporary keychain
if: always()
run: |
if [ -n "${CUSTOM_RELEASE_PREVIOUS_DEFAULT_KEYCHAIN:-}" ] && [ -e "$CUSTOM_RELEASE_PREVIOUS_DEFAULT_KEYCHAIN" ]; then
security default-keychain -d user -s "$CUSTOM_RELEASE_PREVIOUS_DEFAULT_KEYCHAIN" || true
fi
if [ -n "${CUSTOM_RELEASE_KEYCHAIN_PATH:-}" ]; then
security delete-keychain "$CUSTOM_RELEASE_KEYCHAIN_PATH" || true
fi
if [ -n "${CUSTOM_RELEASE_CERTIFICATE_PATH:-}" ]; then
rm -f "$CUSTOM_RELEASE_CERTIFICATE_PATH"
fi
-21
View File
@@ -1,21 +0,0 @@
name: Mirrors
on: [push, delete]
permissions:
contents: read
jobs:
codeberg:
name: Mirror to Codeberg
if: github.repository_owner == 'syncthing'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
with:
fetch-depth: 0
- uses: yesolutions/mirror-action@662fce0eced8996f64d7fa264d76cddd84827f33 # master
with:
REMOTE: ssh://git@codeberg.org/${{ github.repository }}.git
GIT_SSH_PRIVATE_KEY: ${{ secrets.CODEBERG_PUSH_KEY }}
GIT_SSH_NO_VERIFY_HOST: "true"
-21
View File
@@ -1,21 +0,0 @@
name: Org membership recommendations
on:
workflow_dispatch:
schedule:
- cron: '0 0 1 * *'
jobs:
run-recommendation:
name: Check for a recommendation
if: github.repository_owner == 'syncthing'
runs-on: ubuntu-latest
steps:
- uses: docker://ghcr.io/calmh/github-org-members:latest
env:
GITHUB_ORGANISATION: syncthing
GITHUB_TOKEN: ${{ secrets.GOM_GITHUB_TOKEN }}
GOM_IGNORE_USERS: ${{ secrets.GOM_IGNORE_USERS }}
GOM_ALSO_REPOS: ${{ secrets.GOM_ALSO_REPOS }}
+49
View File
@@ -0,0 +1,49 @@
name: Run PR linters
on:
pull_request:
workflow_dispatch:
permissions:
contents: read
pull-requests: read
jobs:
#
# golangci-lint runs a suite of static analysis checks on the code
#
golangci:
runs-on: ubuntu-latest
name: Golangci-lint
steps:
- uses: actions/checkout@v4
- uses: actions/setup-go@v5
with:
go-version: 'stable'
- name: ensure asset generation
run: go run build.go assets
- name: golangci-lint
uses: golangci/golangci-lint-action@v8
with:
only-new-issues: true
#
# Meta checks for formatting, copyright, etc
#
meta:
name: Meta checks
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-go@v5
with:
go-version: 'stable'
- run: |
go run build.go assets
go test -v ./meta
+1 -2
View File
@@ -20,9 +20,8 @@ jobs:
labels: labels:
name: Set labels name: Set labels
if: github.repository_owner == 'syncthing'
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: srvaroa/labeler@9c29ad1ef33d169f9ef33c52722faf47a566bcf3 # v1 - uses: srvaroa/labeler@v1
env: env:
GITHUB_TOKEN: "${{ secrets.GITHUB_TOKEN }}" GITHUB_TOKEN: "${{ secrets.GITHUB_TOKEN }}"
+4 -4
View File
@@ -12,16 +12,16 @@ permissions:
jobs: jobs:
create-release-tag: create-release-tag:
name: Create release tag name: Create release tag
if: github.repository_owner == 'syncthing'
runs-on: ubuntu-latest runs-on: ubuntu-latest
environment: release
steps: steps:
- uses: actions/checkout@v5 - uses: actions/checkout@v4
with: with:
fetch-depth: 0 fetch-depth: 0
ref: ${{ github.ref }} # https://github.com/actions/checkout/issues/882 ref: ${{ github.ref }} # https://github.com/actions/checkout/issues/882
token: ${{ secrets.ACTIONS_GITHUB_TOKEN }} token: ${{ secrets.ACTIONS_GITHUB_TOKEN }}
- uses: actions/setup-go@v6 - uses: actions/setup-go@v5
with: with:
go-version: stable go-version: stable
@@ -53,7 +53,7 @@ jobs:
git push origin "$NEXT" git push origin "$NEXT"
- name: Trigger the build - name: Trigger the build
uses: benc-uk/workflow-dispatch@7a027648b88c2413826b6ddd6c76114894dc5ec4 # v1 uses: benc-uk/workflow-dispatch@v1
with: with:
workflow: build-syncthing.yaml workflow: build-syncthing.yaml
ref: refs/tags/${{ env.NEXT }} ref: refs/tags/${{ env.NEXT }}
+2 -6
View File
@@ -5,18 +5,14 @@ on:
# Run nightly build at 01:00 UTC # Run nightly build at 01:00 UTC
- cron: '00 01 * * *' - cron: '00 01 * * *'
permissions:
contents: write
jobs: jobs:
trigger-nightly: trigger-nightly:
name: Push to release-nightly to trigger build
if: github.repository_owner == 'syncthing'
runs-on: ubuntu-latest runs-on: ubuntu-latest
name: Push to release-nightly to trigger build
steps: steps:
- uses: actions/checkout@v5 - uses: actions/checkout@v4
with: with:
token: ${{ secrets.ACTIONS_GITHUB_TOKEN }} token: ${{ secrets.ACTIONS_GITHUB_TOKEN }}
fetch-depth: 0 fetch-depth: 0
@@ -4,20 +4,17 @@ on:
schedule: schedule:
- cron: '42 3 * * 1' - cron: '42 3 * * 1'
permissions:
contents: write
jobs: jobs:
update_transifex_docs: update_transifex_docs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
name: Update translations and documentation name: Update translations and documentation
steps: steps:
- uses: actions/checkout@v5 - uses: actions/checkout@v4
with: with:
fetch-depth: 0 fetch-depth: 0
token: ${{ secrets.ACTIONS_GITHUB_TOKEN }} token: ${{ secrets.ACTIONS_GITHUB_TOKEN }}
- uses: actions/setup-go@v6 - uses: actions/setup-go@v5
with: with:
go-version: stable go-version: stable
- run: | - run: |
+1
View File
@@ -17,4 +17,5 @@ deb
*.bz2 *.bz2
/repos /repos
/proto/scripts/protoc-gen-gosyncthing /proto/scripts/protoc-gen-gosyncthing
/gui/next-gen-gui
/compat.json /compat.json
-32
View File
@@ -4,18 +4,15 @@ linters:
disable: disable:
- cyclop - cyclop
- depguard - depguard
- err113
- exhaustive - exhaustive
- exhaustruct - exhaustruct
- forbidigo - forbidigo
- funcorder
- funlen - funlen
- gochecknoglobals - gochecknoglobals
- gochecknoinits - gochecknoinits
- gocognit - gocognit
- goconst - goconst
- gocyclo - gocyclo
- godot
- godox - godox
- gomoddirectives - gomoddirectives
- inamedparam - inamedparam
@@ -27,7 +24,6 @@ linters:
- musttag - musttag
- nestif - nestif
- nlreturn - nlreturn
- noinlineerr
- nonamedreturns - nonamedreturns
- paralleltest - paralleltest
- prealloc - prealloc
@@ -43,7 +39,6 @@ linters:
- whitespace - whitespace
- wrapcheck - wrapcheck
- wsl - wsl
- wsl_v5
exclusions: exclusions:
generated: lax generated: lax
presets: presets:
@@ -53,38 +48,11 @@ linters:
- std-error-handling - std-error-handling
paths: paths:
- internal/gen - internal/gen
- internal/db/olddb
- cmd/dev - cmd/dev
- repos - repos
- third_party$ - third_party$
- builtin$ - builtin$
- examples$ - examples$
- _test\.go$
rules:
# relax the slog rules for debug lines, for now
- linters: [sloglint]
source: Debug
# contexts are irrelevant for SQLite
- linters: [noctx]
text: database/sql
# Rollback errors can be ignored
- linters: [errcheck]
source: Rollback
# Embedded fields named in selectors may add clarity
- linters: [staticcheck]
text: QF1008
# Don't necessarily rewrite !(foo || bar) to !foo && !bar
- linters: [staticcheck]
text: QF1001
# Don't necessarily pass the context just for the sake of the logger
- linters: [contextcheck]
text: "->log`"
settings:
sloglint:
context: "scope"
static-msg: true
msg-style: capitalized
key-naming-case: camel
formatters: formatters:
enable: enable:
- gofumpt - gofumpt
+5 -20
View File
@@ -8,20 +8,17 @@
policy: policy:
approval: approval:
- subject is conventional commit - subject is conventional commit
- or: - project metadata requires maintainer approval
- project metadata requires maintainer approval
- a maintainer claims responsibility
- or: - or:
- is approved by a syncthing contributor - is approved by a syncthing contributor
- is a translation or dependency update by a contributor - is a translation or dependency update by a contributor
- is a trivial change by a contributor - is a trivial change by a contributor
- a maintainer claims responsibility
# Additionally, maintainers can disapprove of a PR # Additionally, contributors can disapprove of a PR
disapproval: disapproval:
requires: requires:
teams: teams:
- syncthing/maintainers - syncthing/contributors
# The rules for the policy are described below. # The rules for the policy are described below.
@@ -52,8 +49,7 @@ approval_rules:
- syncthing/maintainers - syncthing/maintainers
options: options:
ignore_update_merges: true ignore_update_merges: true
allow_non_author_contributor: true allow_contributor: true
invalidate_on_push: true
# Regular pull requests require approval by an active contributor # Regular pull requests require approval by an active contributor
- name: is approved by a syncthing contributor - name: is approved by a syncthing contributor
@@ -63,8 +59,7 @@ approval_rules:
- syncthing/contributors - syncthing/contributors
options: options:
ignore_update_merges: true ignore_update_merges: true
allow_non_author_contributor: true allow_contributor: true
invalidate_on_push: true
# Changes to some files (translations, dependencies, compatibility) do not # Changes to some files (translations, dependencies, compatibility) do not
# require approval if they were proposed by a contributor and have a # require approval if they were proposed by a contributor and have a
@@ -101,13 +96,3 @@ approval_rules:
has_author_in: has_author_in:
teams: teams:
- syncthing/contributors - syncthing/contributors
# A member of the maintainers group can take responsibility by adding the
# appropriate label.
- name: a maintainer claims responsibility
if:
has_labels:
- maintainer-responsibility
has_author_in:
teams:
- syncthing/maintainers
+79 -52
View File
@@ -13,121 +13,121 @@
# contents of this file. # contents of this file.
# #
Jakob Borg (calmh) <jakob@nym.se> <jakob@kastelo.net> <jborg@coreweave.com> Aaron Bieber (qbit) <qbit@deftly.net>
Audrius Butkevicius (AudriusButkevicius) <audrius.butkevicius@gmail.com> <github@audrius.rocks>
Simon Frei (imsodin) <freisim93@gmail.com>
Tomasz Wilczyński <5626656+tomasz1986@users.noreply.github.com> <twilczynski@naver.com>
Alexander Graf (alex2108) <register-github@alex-graf.de>
Alexandre Viau (aviau) <alexandre@alexandreviau.net> <aviau@debian.org>
Anderson Mesquita (andersonvom) <andersonvom@gmail.com>
André Colomb (acolomb) <src@andre.colomb.de> <github.com@andre.colomb.de>
Antony Male (canton7) <antony.male@gmail.com>
Ben Schulz (uok) <ueomkail@gmail.com> <uok@users.noreply.github.com>
bt90 <btom1990@googlemail.com>
Caleb Callaway (cqcallaw) <enlightened.despot@gmail.com>
Daniel Harte (norgeous) <daniel@harte.me> <daniel@danielharte.co.uk> <norgeous@users.noreply.github.com>
Emil Lundberg <emil@emlun.se>
Eric P <eric@kastelo.net>
Evgeny Kuznetsov <evgeny@kuznetsov.md>
greatroar <61184462+greatroar@users.noreply.github.com>
Lars K.W. Gohlke (lkwg82) <lkwg82@gmx.de>
Lode Hoste (Zillode) <zillode@zillode.be>
Marcus B Spencer <marcus@marcusspencer.xyz> <marcus@marcusspencer.us>
Michael Ploujnikov (plouj) <ploujj@gmail.com>
Ross Smith II (rasa) <ross@smithii.com>
Stefan Tatschner (rumpelsepp) <stefan@sevenbyte.org> <rumpelsepp@sevenbyte.org> <stefan@rumpelsepp.org>
Tommy van der Vorst <tommy-github@pixelspark.nl> <tommy@pixelspark.nl>
Wulf Weich (wweich) <wweich@users.noreply.github.com> <wweich@gmx.de> <wulf@weich-kr.de>
Adam Piggott (ProactiveServices) <aD@simplypeachy.co.uk> <simplypeachy@users.noreply.github.com> <ProactiveServices@users.noreply.github.com> <adam@proactiveservices.co.uk> Adam Piggott (ProactiveServices) <aD@simplypeachy.co.uk> <simplypeachy@users.noreply.github.com> <ProactiveServices@users.noreply.github.com> <adam@proactiveservices.co.uk>
Adel Qalieh (adelq) <aqalieh95@gmail.com> <adelq@users.noreply.github.com> Adel Qalieh (adelq) <aqalieh95@gmail.com> <adelq@users.noreply.github.com>
Alan Pope <alan@popey.com>
Alberto Donato <albertodonato@users.noreply.github.com>
Aleksey Vasenev <margtu-fivt@ya.ru> Aleksey Vasenev <margtu-fivt@ya.ru>
Alessandro G. (alessandro.g89) <alessandro.g89@gmail.com> Alessandro G. (alessandro.g89) <alessandro.g89@gmail.com>
Alex Ionescu <github@ionescu.sh> Alex Ionescu <github@ionescu.sh>
Alex Lindeman <139387+aelindeman@users.noreply.github.com> Alex Lindeman <139387+aelindeman@users.noreply.github.com>
Alex Xu <alex.hello71@gmail.com> Alex Xu <alex.hello71@gmail.com>
Alexander Graf (alex2108) <register-github@alex-graf.de>
Alexander Seiler <seileralex@gmail.com> Alexander Seiler <seileralex@gmail.com>
Alexandre Alves <alexandrealvesdb.contact@gmail.com> Alexandre Alves <alexandrealvesdb.contact@gmail.com>
Alexandre Viau (aviau) <alexandre@alexandreviau.net> <aviau@debian.org>
Aman Gupta <aman@tmm1.net> Aman Gupta <aman@tmm1.net>
Anatoli Babenia <anatoli@rainforce.org>
Anderson Mesquita (andersonvom) <andersonvom@gmail.com>
Andreas Sommer <andreas.sommer87@googlemail.com> Andreas Sommer <andreas.sommer87@googlemail.com>
andresvia <andres.via@gmail.com> andresvia <andres.via@gmail.com>
Andrew Gunnerson <accounts+github@chiller3.com> Andrew Dunham (andrew-d) <andrew@du.nham.ca>
Andrew Meyer <andrewm.bpi@gmail.com>
Andrew Rabert (nvllsvm) <ar@nullsum.net> <6550543+nvllsvm@users.noreply.github.com> Andrew Rabert (nvllsvm) <ar@nullsum.net> <6550543+nvllsvm@users.noreply.github.com>
Andrey D (scienmind) <scintertech@cryptolab.net> <scienmind@users.noreply.github.com> Andrey D (scienmind) <scintertech@cryptolab.net> <scienmind@users.noreply.github.com>
André Colomb (acolomb) <src@andre.colomb.de> <github.com@andre.colomb.de>
andyleap <andyleap@gmail.com> andyleap <andyleap@gmail.com>
Anjan Momi <anjan@momi.ca> Anjan Momi <anjan@momi.ca>
Anthony Goeckner <agoeckner@users.noreply.github.com> Anthony Goeckner <agoeckner@users.noreply.github.com>
Antoine Lamielle (0x010C) <antoine.lamielle@0x010c.fr> <gh@0x010c.fr> Antoine Lamielle (0x010C) <antoine.lamielle@0x010c.fr> <gh@0x010c.fr>
Antony Male (canton7) <antony.male@gmail.com>
Anur <anurnomeru@163.com> Anur <anurnomeru@163.com>
Aranjedeath <Aranjedeath@users.noreply.github.com> Aranjedeath <Aranjedeath@users.noreply.github.com>
ardevd <ardevd@users.noreply.github.com>
Arkadiusz Tymiński <gevleeog@gmail.com> Arkadiusz Tymiński <gevleeog@gmail.com>
Aroun <login@b-vo.fr> Aroun <login@b-vo.fr>
Arthur Axel fREW Schmidt (frioux) <frew@afoolishmanifesto.com> <frioux@gmail.com> Arthur Axel fREW Schmidt (frioux) <frew@afoolishmanifesto.com> <frioux@gmail.com>
Artur Zubilewicz <AkaZecik@users.noreply.github.com> Artur Zubilewicz <AkaZecik@users.noreply.github.com>
Ashish Bhate <bhate.ashish@gmail.com> Ashish Bhate <bhate.ashish@gmail.com>
Audrius Butkevicius (AudriusButkevicius) <audrius.butkevicius@gmail.com> <github@audrius.rocks>
Aurélien Rainone <476650+arl@users.noreply.github.com> Aurélien Rainone <476650+arl@users.noreply.github.com>
BAHADIR YILMAZ <bahadiryilmaz32@gmail.com> BAHADIR YILMAZ <bahadiryilmaz32@gmail.com>
Bart De Vries (mogwa1) <devriesb@gmail.com> Bart De Vries (mogwa1) <devriesb@gmail.com>
Beat Reichenbach <44111292+beatreichenbach@users.noreply.github.com> Beat Reichenbach <44111292+beatreichenbach@users.noreply.github.com>
Ben Norcombe <bennorcombe@pm.me> Ben Curthoys (bencurthoys) <ben@bencurthoys.com>
Ben Schulz (uok) <ueomkail@gmail.com> <uok@users.noreply.github.com>
Ben Shepherd (benshep) <bjashepherd@gmail.com> Ben Shepherd (benshep) <bjashepherd@gmail.com>
Ben Sidhom (bsidhom) <bsidhom@gmail.com> Ben Sidhom (bsidhom) <bsidhom@gmail.com>
Benedikt Heine (bebehei) <bebe@bebehei.de> Benedikt Heine (bebehei) <bebe@bebehei.de>
Benedikt Morbach <benedikt.morbach@googlemail.com>
Benjamin Nater <17193640+bn4t@users.noreply.github.com>
Benno Fünfstück <benno.fuenfstueck@gmail.com> Benno Fünfstück <benno.fuenfstueck@gmail.com>
Benny Ng (tpng) <benny.tpng@gmail.com> Benny Ng (tpng) <benny.tpng@gmail.com>
boomsquared <54829195+boomsquared@users.noreply.github.com> boomsquared <54829195+boomsquared@users.noreply.github.com>
Boqin Qin <bobbqqin@bupt.edu.cn> Boqin Qin <bobbqqin@bupt.edu.cn>
Boris Rybalkin <ribalkin@gmail.com> Boris Rybalkin <ribalkin@gmail.com>
Brandon Philips (philips) <brandon@ifup.org>
Brendan Long (brendanlong) <self@brendanlong.com> Brendan Long (brendanlong) <self@brendanlong.com>
Brian R. Becker (brbecker) <brbecker@gmail.com>
bt90 <btom1990@googlemail.com>
Caleb Callaway (cqcallaw) <enlightened.despot@gmail.com>
Carsten Hagemann (carstenhag) <moter8@gmail.com> <carsten@chagemann.de>
Catfriend1 <16361913+Catfriend1@users.noreply.github.com> Catfriend1 <16361913+Catfriend1@users.noreply.github.com>
Cathryne Linenweaver (Cathryne) <cathryne.linenweaver@gmail.com> <Cathryne@users.noreply.github.com> <katrinleinweber@MAC.local> Cathryne Linenweaver (Cathryne) <cathryne.linenweaver@gmail.com> <Cathryne@users.noreply.github.com> <katrinleinweber@MAC.local>
Cedric Staniewski (xduugu) <cedric@gmx.ca> Cedric Staniewski (xduugu) <cedric@gmx.ca>
chenrui <rui@meetup.com>
Chih-Hsuan Yen <yan12125@gmail.com> <1937689+yan12125@users.noreply.github.com> Chih-Hsuan Yen <yan12125@gmail.com> <1937689+yan12125@users.noreply.github.com>
Choongkyu <choongkyu.kim+gh@gmail.com> <vapidlyrapid+gh@gmail.com> Choongkyu <choongkyu.kim+gh@gmail.com> <vapidlyrapid+gh@gmail.com>
Chris Howie (cdhowie) <me@chrishowie.com> Chris Howie (cdhowie) <me@chrishowie.com>
Chris Joel (cdata) <chris@scriptolo.gy> Chris Joel (cdata) <chris@scriptolo.gy>
Chris Tonkinson <chris@masterbran.ch>
Christian Kujau <ckujau@users.noreply.github.com> Christian Kujau <ckujau@users.noreply.github.com>
Christian Prescott <me@christianprescott.com> Christian Prescott <me@christianprescott.com>
chucic <chucic@seznam.cz> chucic <chucic@seznam.cz>
cjc7373 <niuchangcun@gmail.com> cjc7373 <niuchangcun@gmail.com>
Colin Kennedy (moshen) <moshen.colin@gmail.com> Colin Kennedy (moshen) <moshen.colin@gmail.com>
Cromefire_ <tim.l@nghorst.net> <26320625+cromefire@users.noreply.github.com> Cromefire_ <tim.l@nghorst.net> <26320625+cromefire@users.noreply.github.com>
cui <cuiweixie@gmail.com> cui fliter <imcusg@gmail.com>
Cyprien Devillez <cypx@users.noreply.github.com> Cyprien Devillez <cypx@users.noreply.github.com>
d-volution <49024624+d-volution@users.noreply.github.com> d-volution <49024624+d-volution@users.noreply.github.com>
Dale Visser <dale.visser@live.com>
Dan <benda.daniel@gmail.com> Dan <benda.daniel@gmail.com>
Daniel Barczyk <46358936+DanielBarczyk@users.noreply.github.com> Daniel Barczyk <46358936+DanielBarczyk@users.noreply.github.com>
Daniel Bergmann (brgmnn) <dan.arne.bergmann@gmail.com> <brgmnn@users.noreply.github.com> Daniel Bergmann (brgmnn) <dan.arne.bergmann@gmail.com> <brgmnn@users.noreply.github.com>
Daniel Harte (norgeous) <daniel@harte.me> <daniel@danielharte.co.uk> <norgeous@users.noreply.github.com>
Daniel Martí (mvdan) <mvdan@mvdan.cc> Daniel Martí (mvdan) <mvdan@mvdan.cc>
Daniel Padrta <64928366+danpadcz@users.noreply.github.com> Daniel Padrta <64928366+danpadcz@users.noreply.github.com>
Daniil Gentili <daniil@daniil.it>
Darshil Chanpura (dtchanpura) <dtchanpura@gmail.com> <dcprime314@gmail.com> Darshil Chanpura (dtchanpura) <dtchanpura@gmail.com> <dcprime314@gmail.com>
dashangcun <907225865@qq.com> dashangcun <907225865@qq.com>
David Rimmer (dinosore) <dinosore@dbrsoftware.co.uk> David Rimmer (dinosore) <dinosore@dbrsoftware.co.uk>
deepsource-autofix[bot] <62050782+deepsource-autofix[bot]@users.noreply.github.com>
DeflateAwning <11021263+DeflateAwning@users.noreply.github.com> DeflateAwning <11021263+DeflateAwning@users.noreply.github.com>
Denis A. (dva) <denisva@gmail.com> Denis A. (dva) <denisva@gmail.com>
Dennis Wilson (snnd) <dw@risu.io> Dennis Wilson (snnd) <dw@risu.io>
dependabot-preview[bot] <dependabot-preview[bot]@users.noreply.github.com> <27856297+dependabot-preview[bot]@users.noreply.github.com>
dependabot[bot] <dependabot[bot]@users.noreply.github.com> <49699333+dependabot[bot]@users.noreply.github.com>
derekriemer <derek.riemer@colorado.edu> derekriemer <derek.riemer@colorado.edu>
DerRockWolf <50499906+DerRockWolf@users.noreply.github.com> DerRockWolf <50499906+DerRockWolf@users.noreply.github.com>
desbma <desbma@users.noreply.github.com> desbma <desbma@users.noreply.github.com>
Devon G. Redekopp <devon@redekopp.com> Devon G. Redekopp <devon@redekopp.com>
diemade <spamkill@posteo.ch>
digital <didev@dinid.net> digital <didev@dinid.net>
Dimitri Papadopoulos Orfanos <3234522+DimitriPapadopoulos@users.noreply.github.com> Dimitri Papadopoulos Orfanos <3234522+DimitriPapadopoulos@users.noreply.github.com>
Dmitry Saveliev (dsaveliev) <d.e.saveliev@gmail.com> Dmitry Saveliev (dsaveliev) <d.e.saveliev@gmail.com>
domain <32405309+szu17dmy@users.noreply.github.com> domain <32405309+szu17dmy@users.noreply.github.com>
Domenic Horner <domenic@tgxn.net> Domenic Horner <domenic@tgxn.net>
Dominik Heidler (asdil12) <dominik@heidler.eu> Dominik Heidler (asdil12) <dominik@heidler.eu>
Elias <1elias.bauer@gmail.com>
Elias Jarlebring (jarlebring) <jarlebring@gmail.com> Elias Jarlebring (jarlebring) <jarlebring@gmail.com>
Elliot Huffman <thelich2@gmail.com> Elliot Huffman <thelich2@gmail.com>
Emil Hessman (ceh) <emil@hessman.se> Emil Hessman (ceh) <emil@hessman.se>
Emil Lundberg <emil@emlun.se>
Eng Zer Jun <engzerjun@gmail.com> Eng Zer Jun <engzerjun@gmail.com>
entity0xfe <109791748+entity0xfe@users.noreply.github.com> <entity0xfe@my.domain> entity0xfe <109791748+entity0xfe@users.noreply.github.com> <entity0xfe@my.domain>
Epifeny <batterystarter@gmail.com>
epifeny <epifeny@users.noreply.github.com>
Eric Lesiuta <elesiuta@gmail.com> Eric Lesiuta <elesiuta@gmail.com>
Eric P <eric@kastelo.net>
Erik Meitner (WSGCSysadmin) <e.meitner@willystreet.coop> Erik Meitner (WSGCSysadmin) <e.meitner@willystreet.coop>
Evan Spensley <94762716+0evan@users.noreply.github.com> Evan Spensley <94762716+0evan@users.noreply.github.com>
Evgeny Kuznetsov <evgeny@kuznetsov.md>
Federico Castagnini (facastagnini) <federico.castagnini@gmail.com> Federico Castagnini (facastagnini) <federico.castagnini@gmail.com>
Felix <53702818+f-eliks@users.noreply.github.com> Felix <53702818+f-eliks@users.noreply.github.com>
Felix Ableitner (Nutomic) <me@nutomic.com> Felix Ableitner (Nutomic) <me@nutomic.com>
@@ -135,13 +135,13 @@ Felix Lampe <mail@flampe.de>
Felix Unterpaintner (bigbear2nd) <bigbear2nd@gmail.com> Felix Unterpaintner (bigbear2nd) <bigbear2nd@gmail.com>
Francois-Xavier Gsell (zukoo) <fxgsell@gmail.com> Francois-Xavier Gsell (zukoo) <fxgsell@gmail.com>
Frank Isemann (fti7) <frank@isemann.name> Frank Isemann (fti7) <frank@isemann.name>
Frederic <1665799+Finomosec@users.noreply.github.com>
Gahl Saraf <saraf.gahl@gmail.com> <gahl@raftt.io> Gahl Saraf <saraf.gahl@gmail.com> <gahl@raftt.io>
georgespatton <georgespatton@users.noreply.github.com> georgespatton <georgespatton@users.noreply.github.com>
ghjklw <malo@jaffre.info> ghjklw <malo@jaffre.info>
Gilli Sigurdsson (gillisig) <gilli@vx.is> Gilli Sigurdsson (gillisig) <gilli@vx.is>
Gleb Sinyavskiy <zhulik.gleb@gmail.com> Gleb Sinyavskiy <zhulik.gleb@gmail.com>
Graham Miln (grahammiln) <graham.miln@dssw.co.uk> <graham.miln@miln.eu> Graham Miln (grahammiln) <graham.miln@dssw.co.uk> <graham.miln@miln.eu>
greatroar <61184462+greatroar@users.noreply.github.com>
Greg <gco@jazzhaiku.com> Greg <gco@jazzhaiku.com>
guangwu <guoguangwu@magic-shield.com> guangwu <guoguangwu@magic-shield.com>
gudvinr <gudvinr@gmail.com> gudvinr <gudvinr@gmail.com>
@@ -151,42 +151,55 @@ HansK-p <42314815+HansK-p@users.noreply.github.com>
Harrison Jones (harrisonhjones) <harrisonhjones@users.noreply.github.com> Harrison Jones (harrisonhjones) <harrisonhjones@users.noreply.github.com>
Hazem Krimi <me@hazemkrimi.tech> Hazem Krimi <me@hazemkrimi.tech>
Heiko Zuerker (Smiley73) <heiko@zuerker.org> Heiko Zuerker (Smiley73) <heiko@zuerker.org>
Henrik Bråthen <henrikbs@proton.me>
Hireworks <129852174+hireworksltd@users.noreply.github.com> Hireworks <129852174+hireworksltd@users.noreply.github.com>
Hugo Locurcio <hugo.locurcio@hugo.pro> Hugo Locurcio <hugo.locurcio@hugo.pro>
Iain Barnett <iainspeed@gmail.com> Iain Barnett <iainspeed@gmail.com>
Ian Johnson (anonymouse64) <ian.johnson@canonical.com> <person.uwsome@gmail.com> Ian Johnson (anonymouse64) <ian.johnson@canonical.com> <person.uwsome@gmail.com>
ignacy123 <ignacy.buczek@onet.pl> ignacy123 <ignacy.buczek@onet.pl>
Ikko Ashimine <eltociear@gmail.com>
Ilya Brin <464157+ilyabrin@users.noreply.github.com>
Iskander Sharipov (Alex) <quasilyte@gmail.com> Iskander Sharipov (Alex) <quasilyte@gmail.com>
Jaakko Hannikainen (jgke) <jgke@jgke.fi> Jaakko Hannikainen (jgke) <jgke@jgke.fi>
Jacek Szafarkiewicz (hadogenes) <szafar@linux.pl>
Jack Croft <jccroft1@users.noreply.github.com> Jack Croft <jccroft1@users.noreply.github.com>
Jacob <jyundt@gmail.com> Jacob <jyundt@gmail.com>
Jake Peterson (acogdev) <jake@acogdev.com> Jake Peterson (acogdev) <jake@acogdev.com>
Jakob Borg (calmh) <jakob@nym.se> <jakob@kastelo.net> <jborg@coreweave.com>
James O'Beirne <wild-github@au92.org> James O'Beirne <wild-github@au92.org>
James Patterson (jpjp) <jamespatterson@operamail.com> <jpjp@users.noreply.github.com> James Patterson (jpjp) <jamespatterson@operamail.com> <jpjp@users.noreply.github.com>
janost <janost@tuta.io>
Jaroslav Lichtblau <svetlemodry@users.noreply.github.com> Jaroslav Lichtblau <svetlemodry@users.noreply.github.com>
Jaroslav Malec (dzarda) <dzardacz@gmail.com> Jaroslav Malec (dzarda) <dzardacz@gmail.com>
jaseg <githubaccount@jaseg.net>
Jaspitta <ste.scarpitta@gmail.com> Jaspitta <ste.scarpitta@gmail.com>
Jauder Ho <jauderho@users.noreply.github.com>
Jaya Chithra (jayachithra) <s.k.jayachithra@gmail.com> Jaya Chithra (jayachithra) <s.k.jayachithra@gmail.com>
Jaya Kumar <jaya.kumar@ict.nl> Jaya Kumar <jaya.kumar@ict.nl>
Jeffery To <jeffery.to@gmail.com> Jeffery To <jeffery.to@gmail.com>
jelle van der Waa <jelle@vdwaa.nl> jelle van der Waa <jelle@vdwaa.nl>
Jens Diemer (jedie) <github.com@jensdiemer.de> <git@jensdiemer.de> Jens Diemer (jedie) <github.com@jensdiemer.de> <git@jensdiemer.de>
Jerry Jacobs (xor-gate) <jerry.jacobs@xor-gate.org> <xor-gate@users.noreply.github.com>
Jesse Lucas <jesse@jesselucas.com>
Jochen Voss (seehuhn) <voss@seehuhn.de> Jochen Voss (seehuhn) <voss@seehuhn.de>
Johan Andersson <j@i19.se>
Johan Vromans (sciurius) <jvromans@squirrel.nl> Johan Vromans (sciurius) <jvromans@squirrel.nl>
John Rinehart (fuzzybear3965) <johnrichardrinehart@gmail.com> John Rinehart (fuzzybear3965) <johnrichardrinehart@gmail.com>
Jonas Thelemann <e-mail@jonas-thelemann.de> Jonas Thelemann <e-mail@jonas-thelemann.de>
Jonathan <artback@protonmail.com> <jonagn@gmail.com> Jonathan <artback@protonmail.com> <jonagn@gmail.com>
Jonathan Cross <jcross@gmail.com>
Jonta <359397+Jonta@users.noreply.github.com>
Jose Manuel Delicado (jmdaweb) <jmdaweb@hotmail.com> <jmdaweb@users.noreply.github.com> Jose Manuel Delicado (jmdaweb) <jmdaweb@hotmail.com> <jmdaweb@users.noreply.github.com>
JRNitre <nichinichisou67@outlook.com>
jtagcat <git-514635f7@jtag.cat> <git-12dbd862@jtag.cat> jtagcat <git-514635f7@jtag.cat> <git-12dbd862@jtag.cat>
Julian Lehrhuber <jul13579@users.noreply.github.com> Julian Lehrhuber <jul13579@users.noreply.github.com>
Jörg Thalheim <Mic92@users.noreply.github.com> Jörg Thalheim <Mic92@users.noreply.github.com>
Jędrzej Kula <kula.jedrek@gmail.com> Jędrzej Kula <kula.jedrek@gmail.com>
K.B.Dharun Krishna <kbdharunkrishna@gmail.com>
Kalle Laine <pahakalle@protonmail.com>
Kapil Sareen <kapilsareen584@gmail.com> Kapil Sareen <kapilsareen584@gmail.com>
Karol Różycki (krozycki) <rozycki.karol@gmail.com> Karol Różycki (krozycki) <rozycki.karol@gmail.com>
Kebin Liu <lkebin@gmail.com> Kebin Liu <lkebin@gmail.com>
Keith Harrison <keithh@protonmail.com> Keith Harrison <keithh@protonmail.com>
Keith Turner <kturner@apache.org>
Kelong Cong (kc1212) <kc04bc@gmx.com> <kc1212@users.noreply.github.com> Kelong Cong (kc1212) <kc04bc@gmx.com> <kc1212@users.noreply.github.com>
Ken'ichi Kamada (kamadak) <kamada@nanohz.org> Ken'ichi Kamada (kamadak) <kamada@nanohz.org>
Kevin Allen (ironmig) <kma1660@gmail.com> Kevin Allen (ironmig) <kma1660@gmail.com>
@@ -195,24 +208,31 @@ Kevin White, Jr. (kwhite17) <kevinwhite1710@gmail.com>
klemens <ka7@github.com> klemens <ka7@github.com>
Kurt Fitzner (Kudalufi) <kurt@va1der.ca> <kurt.fitzner@gmail.com> Kurt Fitzner (Kudalufi) <kurt@va1der.ca> <kurt.fitzner@gmail.com>
kylosus <33132401+kylosus@users.noreply.github.com> kylosus <33132401+kylosus@users.noreply.github.com>
Lars K.W. Gohlke (lkwg82) <lkwg82@gmx.de>
Lars Lehtonen <lars.lehtonen@gmail.com> Lars Lehtonen <lars.lehtonen@gmail.com>
Laurent Arnoud <laurent@spkdev.net>
Laurent Etiemble (letiemble) <laurent.etiemble@gmail.com> <laurent.etiemble@monobjc.net> Laurent Etiemble (letiemble) <laurent.etiemble@gmail.com> <laurent.etiemble@monobjc.net>
Leo Arias (elopio) <yo@elopio.net> Leo Arias (elopio) <yo@elopio.net>
Liu Siyuan (liusy182) <liusy182@gmail.com> <liusy182@hotmail.com> Liu Siyuan (liusy182) <liusy182@gmail.com> <liusy182@hotmail.com>
Lode Hoste (Zillode) <zillode@zillode.be>
Lord Landon Agahnim (LordLandon) <lordlandon@gmail.com> Lord Landon Agahnim (LordLandon) <lordlandon@gmail.com>
LSmithx2 <42276854+lsmithx2@users.noreply.github.com> LSmithx2 <42276854+lsmithx2@users.noreply.github.com>
Luiz Angelo Daros de Luca <luizluca@gmail.com> luchenhan <168071714+luchenhan@users.noreply.github.com>
Lukas Lihotzki <lukas@lihotzki.de> Lukas Lihotzki <lukas@lihotzki.de>
Luke Hamburg <1992842+luckman212@users.noreply.github.com> Luke Hamburg <1992842+luckman212@users.noreply.github.com>
luzpaz <luzpaz@users.noreply.github.com> luzpaz <luzpaz@users.noreply.github.com>
Majed Abdulaziz (majedev) <majed.alhajry@gmail.com> Majed Abdulaziz (majedev) <majed.alhajry@gmail.com>
Marc Laporte (marclaporte) <marc@marclaporte.com> <marc@laporte.name> Marc Laporte (marclaporte) <marc@marclaporte.com> <marc@laporte.name>
Marc Pujol (kilburn) <kilburn@la3.org>
Marcel Meyer <mm.marcelmeyer@gmail.com> Marcel Meyer <mm.marcelmeyer@gmail.com>
Marcin Dziadus (marcindziadus) <dziadus.marcin@gmail.com> Marcin Dziadus (marcindziadus) <dziadus.marcin@gmail.com>
marco-m <marco.molteni@laposte.net>
Marcus B Spencer <marcus@marcusspencer.xyz> <marcus@marcusspencer.us>
Marcus Legendre <marcus.legendre@gmail.com> Marcus Legendre <marcus.legendre@gmail.com>
Mario Majila <mariustshipichik@gmail.com> Mario Majila <mariustshipichik@gmail.com>
Mark Pulford (mpx) <mark@kyne.com.au> Mark Pulford (mpx) <mark@kyne.com.au>
Martchus <martchus@gmx.net> Martchus <martchus@gmx.net>
Martin Polehla <p0l0us@users.noreply.github.com>
Mateusz Naściszewski (mateon1) <matin1111@wp.pl> Mateusz Naściszewski (mateon1) <matin1111@wp.pl>
Mateusz Ż <thedead4fun@live.com> Mateusz Ż <thedead4fun@live.com>
mathias4833 <67101597+mathias4833@users.noreply.github.com> mathias4833 <67101597+mathias4833@users.noreply.github.com>
@@ -220,16 +240,18 @@ Matic Potočnik <hairyfotr@gmail.com>
Matt Burke (burkemw3) <mburke@amplify.com> <burkemw3@gmail.com> Matt Burke (burkemw3) <mburke@amplify.com> <burkemw3@gmail.com>
Matt Robenolt <matt@ydekproductions.com> Matt Robenolt <matt@ydekproductions.com>
Matteo Ruina <matteo.ruina@gmail.com> Matteo Ruina <matteo.ruina@gmail.com>
mattn <mattn.jp@gmail.com>
Maurizio Tomasi <ziotom78@gmail.com> Maurizio Tomasi <ziotom78@gmail.com>
Max <github@germancoding.com> Max <github@germancoding.com>
Max Schulze (kralo) <max.schulze@online.de> <kralo@users.noreply.github.com> Max Schulze (kralo) <max.schulze@online.de> <kralo@users.noreply.github.com>
maxice8 <30738253+maxice8@users.noreply.github.com>
MaximAL <almaximal@ya.ru> MaximAL <almaximal@ya.ru>
Maxime Thirouin <m@moox.io>
Maximilian <maxi.rostock@outlook.de> <public@complexvector.space> Maximilian <maxi.rostock@outlook.de> <public@complexvector.space>
Maxwell G <maxwell@gtmx.me> mclang <1721600+mclang@users.noreply.github.com>
Michael Jephcote (Rewt0r) <rewt0r@gmx.com> <Rewt0r@users.noreply.github.com> Michael Jephcote (Rewt0r) <rewt0r@gmx.com> <Rewt0r@users.noreply.github.com>
Michael Ploujnikov (plouj) <ploujj@gmail.com>
Michael Rienstra <mrienstra@gmail.com> Michael Rienstra <mrienstra@gmail.com>
Michael Wang 汪東陽 <michael19920327@gmail.com> Michael Tilli (pyfisch) <pyfisch@gmail.com>
MichaIng <micha@dietpi.com> MichaIng <micha@dietpi.com>
Migelo <miha@filetki.si> Migelo <miha@filetki.si>
Mike Boone <mike@boonedocks.net> Mike Boone <mike@boonedocks.net>
@@ -238,6 +260,7 @@ MikolajTwarog <43782609+MikolajTwarog@users.noreply.github.com>
Mingxuan Lin <gdlmx@users.noreply.github.com> Mingxuan Lin <gdlmx@users.noreply.github.com>
mv1005 <49659413+mv1005@users.noreply.github.com> mv1005 <49659413+mv1005@users.noreply.github.com>
Nate Morrison (nrm21) <natemorrison@gmail.com> Nate Morrison (nrm21) <natemorrison@gmail.com>
Naveen <172697+naveensrinivasan@users.noreply.github.com>
nf <nf@wh3rd.net> nf <nf@wh3rd.net>
Nicholas Rishel (PrototypeNM1) <rishel.nick@gmail.com> <PrototypeNM1@users.noreply.github.com> Nicholas Rishel (PrototypeNM1) <rishel.nick@gmail.com> <PrototypeNM1@users.noreply.github.com>
Nick Busey <NickBusey@users.noreply.github.com> Nick Busey <NickBusey@users.noreply.github.com>
@@ -252,6 +275,7 @@ NoLooseEnds <jon.koslung@gmail.com>
Oliver Freyermuth <o.freyermuth@googlemail.com> Oliver Freyermuth <o.freyermuth@googlemail.com>
orangekame3 <miya.org.0309@gmail.com> orangekame3 <miya.org.0309@gmail.com>
otbutz <tbutz@optitool.de> otbutz <tbutz@optitool.de>
Otiel <Otiel@users.noreply.github.com>
overkill <22098433+0verk1ll@users.noreply.github.com> overkill <22098433+0verk1ll@users.noreply.github.com>
Oyebanji Jacob Mayowa <oyebanji05@gmail.com> Oyebanji Jacob Mayowa <oyebanji05@gmail.com>
Pablo <pbaeyens31+github@gmail.com> Pablo <pbaeyens31+github@gmail.com>
@@ -259,6 +283,7 @@ Pascal Jungblut (pascalj) <github@pascalj.com> <mail@pascal-jungblut.com>
Paul Brit <paulbrit44@gmail.com> Paul Brit <paulbrit44@gmail.com>
Paul Donald <newtwen+github@gmail.com> Paul Donald <newtwen+github@gmail.com>
Pawel Palenica (qepasa) <pawelpalenica11@gmail.com> Pawel Palenica (qepasa) <pawelpalenica11@gmail.com>
Paweł Rozlach <vespian@users.noreply.github.com>
perewa <cavalcante.ten@gmail.com> perewa <cavalcante.ten@gmail.com>
Peter Badida <KeyWeeUsr@users.noreply.github.com> Peter Badida <KeyWeeUsr@users.noreply.github.com>
Peter Dave Hello <hsu@peterdavehello.org> Peter Dave Hello <hsu@peterdavehello.org>
@@ -268,19 +293,20 @@ Phani Rithvij <phanirithvij2000@gmail.com>
Phil Davis <phil.davis@inf.org> Phil Davis <phil.davis@inf.org>
Philippe Schommers (filoozoom) <philippe@schommers.be> Philippe Schommers (filoozoom) <philippe@schommers.be>
Phill Luby (pluby) <phill.luby@newredo.com> Phill Luby (pluby) <phill.luby@newredo.com>
Pier Paolo Ramon <ramonpierre@gmail.com>
Piotr Bejda (piobpl) <piotrb10@gmail.com> Piotr Bejda (piobpl) <piotrb10@gmail.com>
polyfloyd <polyfloyd@users.noreply.github.com> polyfloyd <polyfloyd@users.noreply.github.com>
Prathik P Kulkarni <83969842+prathik8794@users.noreply.github.com> Pramodh KP (pramodhkp) <pramodh.p@directi.com> <1507241+pramodhkp@users.noreply.github.com>
pullmerge <166967364+pullmerge@users.noreply.github.com> pullmerge <166967364+pullmerge@users.noreply.github.com>
Quentin Hibon <qh.public@yahoo.com> Quentin Hibon <qh.public@yahoo.com>
Rahmi Pruitt <rjpruitt16@gmail.com> Rahmi Pruitt <rjpruitt16@gmail.com>
RealCharlesChia <161665317+RealCharlesChia@users.noreply.github.com>
red_led <red-led@users.noreply.github.com> red_led <red-led@users.noreply.github.com>
Richard Hartmann <RichiH@users.noreply.github.com>
Robert Carosi (nov1n) <robert@carosi.nl> Robert Carosi (nov1n) <robert@carosi.nl>
Roberto Santalla <roobre@users.noreply.github.com> Roberto Santalla <roobre@users.noreply.github.com>
Robin Schoonover <robin@cornhooves.org> Robin Schoonover <robin@cornhooves.org>
Rohit Tanwar <31792358+rohitanwar@users.noreply.github.com>
Roman Zaynetdinov (zaynetro) <romanznet@gmail.com> Roman Zaynetdinov (zaynetro) <romanznet@gmail.com>
Ross Smith II (rasa) <ross@smithii.com>
rubenbe <github-com-00ff86@vandamme.email> rubenbe <github-com-00ff86@vandamme.email>
Ruslan Yevdokymov <38809160+ruslanye@users.noreply.github.com> Ruslan Yevdokymov <38809160+ruslanye@users.noreply.github.com>
Ryan Qian <i@bitbili.net> Ryan Qian <i@bitbili.net>
@@ -292,37 +318,39 @@ Sergey Mishin (ralder) <ralder@yandex.ru>
Sertonix <83883937+Sertonix@users.noreply.github.com> Sertonix <83883937+Sertonix@users.noreply.github.com>
Severin von Wnuck-Lipinski <ss7@live.de> Severin von Wnuck-Lipinski <ss7@live.de>
Shaarad Dalvi <60266155+shaaraddalvi@users.noreply.github.com> <shdalv@microsoft.com> Shaarad Dalvi <60266155+shaaraddalvi@users.noreply.github.com> <shdalv@microsoft.com>
Shablone <20610621+Shablone@users.noreply.github.com> Simon Frei (imsodin) <freisim93@gmail.com>
Shivam Kumar <155747305+maishivamhoo123@users.noreply.github.com>
Simon Mwepu <simonmwepu@gmail.com> Simon Mwepu <simonmwepu@gmail.com>
Simon Pickup <simon@pickupinfinity.com> Simon Pickup <simon@pickupinfinity.com>
Sly_tom_cat <slytomcat@mail.ru> Sly_tom_cat <slytomcat@mail.ru>
Sonu Kumar Saw <31889738+dev-saw99@users.noreply.github.com> Sonu Kumar Saw <31889738+dev-saw99@users.noreply.github.com>
Stefan Kuntz (Stefan-Code) <stefan.github@gmail.com> <Stefan.github@gmail.com> Stefan Kuntz (Stefan-Code) <stefan.github@gmail.com> <Stefan.github@gmail.com>
Stefan Tatschner (rumpelsepp) <stefan@sevenbyte.org> <rumpelsepp@sevenbyte.org> <stefan@rumpelsepp.org>
Steven Eckhoff <steven.eckhoff.opensource@gmail.com> Steven Eckhoff <steven.eckhoff.opensource@gmail.com>
Suhas Gundimeda (snugghash) <suhas.gundimeda@gmail.com> <snugghash@gmail.com> Suhas Gundimeda (snugghash) <suhas.gundimeda@gmail.com> <snugghash@gmail.com>
Sven Bachmann <dev@mcbachmann.de> Sven Bachmann <dev@mcbachmann.de>
Syncthing Automation <automation@syncthing.net>
Syncthing Release Automation <release@syncthing.net>
Sébastien WENSKE <sebastien@wenske.fr> Sébastien WENSKE <sebastien@wenske.fr>
Tao <mail@steadytao.com>
Taylor Khan (nelsonkhan) <nelsonkhan@gmail.com> Taylor Khan (nelsonkhan) <nelsonkhan@gmail.com>
tbodt <tbodt@tbodt.com>
Terrance <git@terrance.allofti.me> Terrance <git@terrance.allofti.me>
TheCreeper <TheCreeper@users.noreply.github.com> TheCreeper <TheCreeper@users.noreply.github.com>
Thomas <9749173+uhthomas@users.noreply.github.com> Thomas <9749173+uhthomas@users.noreply.github.com>
Thomas Hipp <thomashipp@gmail.com> Thomas Hipp <thomashipp@gmail.com>
Tim Abell (timabell) <tim@timwise.co.uk> Tim Abell (timabell) <tim@timwise.co.uk>
Tim Howes (timhowes) <timhowes@berkeley.edu> Tim Howes (timhowes) <timhowes@berkeley.edu>
Tim Nordenfur <tim@gurka.se>
Tobias Frölich <40638719+tobifroe@users.noreply.github.com> Tobias Frölich <40638719+tobifroe@users.noreply.github.com>
Tobias Klauser <tobias.klauser@gmail.com> Tobias Klauser <tobias.klauser@gmail.com>
Tobias Nygren (tnn2) <tnn@nygren.pp.se> Tobias Nygren (tnn2) <tnn@nygren.pp.se>
Tobias Tom (tobiastom) <t.tom@succont.de> Tobias Tom (tobiastom) <t.tom@succont.de>
Tom Jakubowski <tom@crystae.net> Tom Jakubowski <tom@crystae.net>
Tomasz Wilczyński <5626656+tomasz1986@users.noreply.github.com> <twilczynski@naver.com>
Tommy Thorn <tommy-github-email@thorn.ws>
Tommy van der Vorst <tommy-github@pixelspark.nl> <tommy@pixelspark.nl>
Tully Robinson (tojrobinson) <tully@tojr.org> Tully Robinson (tojrobinson) <tully@tojr.org>
Tyler Brazier (tylerbrazier) <tyler@tylerbrazier.com> Tyler Brazier (tylerbrazier) <tyler@tylerbrazier.com>
Tyler Kropp <kropptyler@gmail.com> Tyler Kropp <kropptyler@gmail.com>
Umer-Azaz <umer_azaz@yahoo.com>
Unrud (Unrud) <unrud@openaliasbox.org> <Unrud@users.noreply.github.com> Unrud (Unrud) <unrud@openaliasbox.org> <Unrud@users.noreply.github.com>
Val Markovic <val@markovic.io>
vapatel2 <149737089+vapatel2@users.noreply.github.com> vapatel2 <149737089+vapatel2@users.noreply.github.com>
Veeti Paananen (veeti) <veeti.paananen@rojekti.fi> Veeti Paananen (veeti) <veeti.paananen@rojekti.fi>
Victor Buinsky (buinsky) <vix_booja@tut.by> Victor Buinsky (buinsky) <vix_booja@tut.by>
@@ -330,16 +358,15 @@ Vik <63919734+ViktorOn@users.noreply.github.com>
Vil Brekin (Vilbrekin) <vilbrekin@gmail.com> Vil Brekin (Vilbrekin) <vilbrekin@gmail.com>
villekalliomaki <53118179+villekalliomaki@users.noreply.github.com> villekalliomaki <53118179+villekalliomaki@users.noreply.github.com>
Vladimir Rusinov <vrusinov@google.com> <vladimir.rusinov@gmail.com> Vladimir Rusinov <vrusinov@google.com> <vladimir.rusinov@gmail.com>
vvaswani <2571660+vvaswani@users.noreply.github.com>
wangguoliang <liangcszzu@163.com> wangguoliang <liangcszzu@163.com>
WangXi <xib1102@icloud.com> WangXi <xib1102@icloud.com>
Will Rouesnel <wrouesnel@wrouesnel.com> Will Rouesnel <wrouesnel@wrouesnel.com>
William A. Kennington III (wkennington) <william@wkennington.com> William A. Kennington III (wkennington) <william@wkennington.com>
wouter bolsterlee <wouter@bolsterl.ee> wouter bolsterlee <wouter@bolsterl.ee>
Wulf Weich (wweich) <wweich@users.noreply.github.com> <wweich@gmx.de> <wulf@weich-kr.de>
xarx00 <xarx00@users.noreply.github.com> xarx00 <xarx00@users.noreply.github.com>
Xavier O. (damajor) <damajor@gmail.com> Xavier O. (damajor) <damajor@gmail.com>
xjtdy888 (xjtdy888) <xjtdy888@163.com> <xjtdy888@gmail.com> xjtdy888 (xjtdy888) <xjtdy888@163.com>
Yannic A. (eipiminus1) <eipiminusone+github@gmail.com> <eipiminus1@users.noreply.github.com> Yannic A. (eipiminus1) <eipiminusone+github@gmail.com> <eipiminus1@users.noreply.github.com>
yparitcher <y@paritcher.com>
佛跳墙 <daoquan@qq.com> 佛跳墙 <daoquan@qq.com>
落心 <luoxin.ttt@gmail.com> 落心 <luoxin.ttt@gmail.com>
+32 -157
View File
@@ -1,14 +1,25 @@
## Reporting Bugs & Feature Requests ## Reporting Bugs
For general discussion and troubleshooting, please use the [Support Please file bugs in the [GitHub Issue
Forum](https://forum.syncthing.net/). For actionable bug reports and feature Tracker](https://github.com/syncthing/syncthing/issues). Include at
requests, please file issues in the [GitHub Issue least the following:
Tracker](https://github.com/syncthing/syncthing/issues), using one of the
provided templates.
:warning: Do not submit AI generated issues or comments. Report any details - What happened
you experienced as a human, and let anyone who wants to perform AI analysis
do so on their own. - What did you expect to happen instead of what *did* happen, if it's
not crazy obvious
- What operating system, operating system version and version of
Syncthing you are running
- The same for other connected devices, where relevant
- Screenshot if the issue concerns something visible in the GUI
- Console log entries, where possible and relevant
If you're not sure whether something is relevant, erring on the side of
too much information will never get you yelled at. :)
## Contributing Translations ## Contributing Translations
@@ -23,160 +34,19 @@ Note that the previously used service at
retired and we kindly ask you to sign up on Weblate for continued retired and we kindly ask you to sign up on Weblate for continued
involvement. involvement.
## Contributing Code
Every contribution is welcome. If you want to contribute but are unsure
where to start, any open issues are fair game! See the [Contribution
Guidelines](https://docs.syncthing.net/dev/contributing.html) for the full
story on committing code.
## Contributing Documentation ## Contributing Documentation
Updates to the [documentation site](https://docs.syncthing.net/) can be Updates to the [documentation site](https://docs.syncthing.net/) can be
made as pull requests on the [documentation made as pull requests on the [documentation
repository](https://github.com/syncthing/docs). repository](https://github.com/syncthing/docs).
## Contributing Code
We welcome contributions. However:
:warning: We do not accept contributions that are wholly or mostly AI
generated. You may assume that we are fully capable of prompting an AI
ourselves when so inclined.
:warning: We do not accept unsolicited pull requests from new contributors.
Most such contributions fall afoul of the previous rule. If yours does not,
please post a message to the relevant issue with a link to your branch and a
short description of the reasoning behind it. We'll take care of onboarding
you.
### Authorship
All code authors are listed in the AUTHORS file. When your first pull
request is accepted your details are added to the AUTHORS file and the list
of authors in the GUI. Commits must be made with the same name and email as
listed in the AUTHORS file. To accomplish this, ensure that your git
configuration is set correctly prior to making your first commit:
$ git config --global user.name "Jane Doe"
$ git config --global user.email janedoe@example.com
You must be reachable on the given email address. If you do not wish to use
your real name for whatever reason, using a nickname or pseudonym is
perfectly acceptable.
### The Developer Certificate of Origin (DCO)
The Syncthing project requires the Developer Certificate of Origin (DCO)
sign-off on pull requests (PRs). This means that all commit messages must
contain a signature line to indicate that the developer accepts the DCO.
The DCO is a lightweight way for contributors to certify that they wrote (or
otherwise have the right to submit) the code and changes they are
contributing to the project. Here is the full [text of the
DCO](https://developercertificate.org):
---
By making a contribution to this project, I certify that:
1. The contribution was created in whole or in part by me and I have the
right to submit it under the open source license indicated in the file;
or
2. The contribution is based upon previous work that, to the best of my
knowledge, is covered under an appropriate open source license and I have
the right under that license to submit that work with modifications,
whether created in whole or in part by me, under the same open source
license (unless I am permitted to submit under a different license), as
indicated in the file; or
3. The contribution was provided directly to me by some other person who
certified (1), (2) or (3) and I have not modified it.
4. I understand and agree that this project and the contribution are public
and that a record of the contribution (including all personal information
I submit with it, including my sign-off) is maintained indefinitely and
may be redistributed consistent with this project or the open source
license(s) involved.
---
Contributors indicate that they adhere to these requirements by adding
a `Signed-off-by` line to their commit messages. For example:
This is my commit message
Signed-off-by: Random J Developer <random@developer.example.org>
The name and email address in this line must match those of the committing
author, and be the same as what you want in the AUTHORS file as per above.
### Coding Style
#### General
- All text files use Unix line endings. The git settings already present in
the repository attempt to enforce this.
- When making changes, follow the brace and parenthesis style of the
surrounding code.
#### Go Specific
- Follow the conventions laid out in [Effective
Go](https://go.dev/doc/effective_go) as much as makes sense. The review
guidelines in [Go Code Review
Comments](https://github.com/golang/go/wiki/CodeReviewComments) should
generally be followed.
- Each commit should be `go fmt` clean.
- Imports are grouped per `goimports` standard; that is, standard
library first, then third party libraries after a blank line.
### Commits
- Commit messages (and pull request titles) should follow the [conventional
commits](https://www.conventionalcommits.org/en/v1.0.0/) specification and
be in lower case.
- We use a scope description in the commit message subject. This is the
component of Syncthing that the commit affects. For example, `gui`,
`protocol`, `scanner`, `upnp`, etc -- typically, the part after
`internal/`, `lib/` or `cmd/` in the package path. If the commit doesn't
affect a specific component, such as for changes to the build system or
documentation, the scope should be omitted. The same goes for changes that
affect many components which would be cumbersome to list.
- Commits that resolve an existing issue must include the issue number
as `(fixes #123)` at the end of the commit message subject. A correctly
formatted commit message subject looks like this:
feat(dialer): add env var to disable proxy fallback (fixes #3006)
- If the commit message subject doesn't say it all, one or more paragraphs of
describing text should be added to the commit message. This should explain
why the change is made and what it accomplishes.
- When drafting a pull request, please feel free to add commits with
corrections and merge from `main` when necessary. This provides a clear time
line with changes and simplifies review. Do not, in general, rebase your
commits, as this makes review harder.
- Pull requests are merged to `main` using squash merge. The "stream of
consciousness" set of commits described in the previous point will be reduced
to a single commit at merge time. The pull request title and description will
be used as the commit message.
### Tests
Yes please, do add tests when adding features or fixing bugs. Also, when a
pull request is filed a number of automatic tests are run on the code. This
includes:
- That the code actually builds and the test suite passes.
- That the code is correctly formatted (`go fmt`).
- That the commits are based on a reasonably recent `main`.
- That the output from `go lint` and `go vet` is clean. (This checks for a
number of potential problems the compiler doesn't catch.)
## Licensing ## Licensing
All contributions are made available under the same license as the already All contributions are made available under the same license as the already
@@ -189,5 +59,10 @@ otherwise stated this means MPLv2, but there are exceptions:
- The documentation (man/...) is licensed under the Creative Commons - The documentation (man/...) is licensed under the Creative Commons
Attribution 4.0 International License. Attribution 4.0 International License.
- Projects under vendor/... are copyright by and licensed from their
respective original authors. Contributions should be made to the original
project, not here.
Regardless of the license in effect, you retain the copyright to your Regardless of the license in effect, you retain the copyright to your
contribution. contribution.
+64 -20
View File
@@ -23,7 +23,52 @@ example `UMASK=002`.
**Docker cli** **Docker cli**
``` ```
$ docker pull syncthing/syncthing $ docker pull syncthing/syncthing
$ docker run --network=host -e STGUIADDRESS= \ $ docker run -p 8384:8384 -p 22000:22000/tcp -p 22000:22000/udp -p 21027:21027/udp \
-v /wherever/st-sync:/var/syncthing \
--hostname=my-syncthing \
syncthing/syncthing:latest
```
**Docker compose**
```yml
---
version: "3"
services:
syncthing:
image: syncthing/syncthing
container_name: syncthing
hostname: my-syncthing
environment:
- PUID=1000
- PGID=1000
volumes:
- /wherever/st-sync:/var/syncthing
ports:
- 8384:8384 # Web UI
- 22000:22000/tcp # TCP file transfers
- 22000:22000/udp # QUIC file transfers
- 21027:21027/udp # Receive local discovery broadcasts
restart: unless-stopped
healthcheck:
test: curl -fkLsS -m 2 127.0.0.1:8384/rest/noauth/health | grep -o --color=never OK || exit 1
interval: 1m
timeout: 10s
retries: 3
```
## Discovery
Note that Docker's default network mode prevents local IP addresses from
being discovered, as Syncthing is only able to see the internal IP of the
container on the `172.17.0.0/16` subnet. This will result in poor transfer rates
if local device addresses are not manually configured.
It is therefore advisable to use the [host network mode](https://docs.docker.com/network/host/) instead:
**Docker cli**
```
$ docker pull syncthing/syncthing
$ docker run --network=host \
-v /wherever/st-sync:/var/syncthing \ -v /wherever/st-sync:/var/syncthing \
syncthing/syncthing:latest syncthing/syncthing:latest
``` ```
@@ -40,7 +85,6 @@ services:
environment: environment:
- PUID=1000 - PUID=1000
- PGID=1000 - PGID=1000
- STGUIADDRESS=
volumes: volumes:
- /wherever/st-sync:/var/syncthing - /wherever/st-sync:/var/syncthing
network_mode: host network_mode: host
@@ -52,27 +96,27 @@ services:
retries: 3 retries: 3
``` ```
## Discovery
Please note that Docker's default network mode prevents local IP addresses
from being discovered, as Syncthing can only see the internal IP address of
the container on the `172.17.0.0/16` subnet. This would likely break the ability
for nodes to establish LAN connections properly, resulting in poor transfer
rates unless local device addresses are configured manually.
It is therefore strongly recommended to stick to the [host network mode](https://docs.docker.com/network/host/),
as shown above.
Be aware that syncthing alone is now in control of what interfaces and ports it Be aware that syncthing alone is now in control of what interfaces and ports it
listens on. You can edit the syncthing configuration to change the defaults if listens on. You can edit the syncthing configuration to change the defaults if
there are conflicts. there are conflicts.
## GUI Security ## GUI Security
By default Syncthing inside the Docker image listens on `0.0.0.0:8384`. This By default Syncthing inside the Docker image listens on 0.0.0.0:8384 to
allows GUI connections when running without host network mode. The example allow GUI connections via the Docker proxy. This is set by the
above unsets the `STGUIADDRESS` environment variable to have Syncthing fall `STGUIADDRESS` environment variable in the Dockerfile, as it differs from
back to listening on what has been configured in the configuration file or the what Syncthing would otherwise use by default. This means you should set up
GUI settings dialog. By default this is the localhost IP address `127.0.0.1`. authentication in the GUI, like for any other externally reachable Syncthing
If you configure your GUI to be externally reachable, make sure you set up instance. If you do not require the GUI, or you use host networking, you can
authentication and enable TLS. unset the `STGUIADDRESS` variable to have Syncthing fall back to listening
on 127.0.0.1:
```
$ docker pull syncthing/syncthing
$ docker run -e STGUIADDRESS= \
-v /wherever/st-sync:/var/syncthing \
syncthing/syncthing:latest
```
With the environment variable unset Syncthing will follow what is set in the
configuration file / GUI settings dialog.
-8
View File
@@ -1,11 +1,3 @@
# Syncthing with `.stignore` synchronization
This fork synchronizes the root-level `.stignore` file as regular folder
content, while keeping `.stfolder` and `.stversions` protected as Syncthing
internals. The `upstream` branch mirrors the official Syncthing `main` branch;
this fork's `main` branch and releases apply the `.stignore` synchronization
patch.
[![Syncthing][14]][15] [![Syncthing][14]][15]
--- ---
-8
View File
@@ -1,8 +0,0 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<assembly xmlns="urn:schemas-microsoft-com:asm.v1" manifestVersion="1.0">
<application>
<windowsSettings>
<consoleAllocationPolicy xmlns="http://schemas.microsoft.com/SMI/2024/WindowsSettings">detached</consoleAllocationPolicy>
</windowsSettings>
</application>
</assembly>
+82 -42
View File
@@ -38,26 +38,27 @@ import (
) )
var ( var (
goarch string goarch string
goos string goos string
noupgrade bool noupgrade bool
version string version string
goCmd string goCmd string
race bool race bool
debug = os.Getenv("BUILDDEBUG") != "" debug = os.Getenv("BUILDDEBUG") != ""
extraTags string extraTags string
installSuffix string installSuffix string
pkgdir string pkgdir string
cc string cc string
run string run string
benchRun string benchRun string
buildOut string buildOut string
debugBinary bool debugBinary bool
coverage bool coverage bool
long bool long bool
timeout = "120s" timeout = "120s"
longTimeout = "600s" longTimeout = "600s"
numVersions = 5 numVersions = 5
withNextGenGUI = os.Getenv("BUILD_NEXT_GEN_GUI") != ""
) )
type target struct { type target struct {
@@ -288,10 +289,10 @@ func runCommand(cmd string, target target) {
build(target, tags) build(target, tags)
case "test": case "test":
test(strings.Fields(extraTags), "github.com/syncthing/syncthing/internal/...", "github.com/syncthing/syncthing/lib/...", "github.com/syncthing/syncthing/cmd/...") test(strings.Fields(extraTags), "github.com/syncthing/syncthing/lib/...", "github.com/syncthing/syncthing/cmd/...")
case "bench": case "bench":
bench(strings.Fields(extraTags), "github.com/syncthing/syncthing/internal/...", "github.com/syncthing/syncthing/lib/...", "github.com/syncthing/syncthing/cmd/...") bench(strings.Fields(extraTags), "github.com/syncthing/syncthing/lib/...", "github.com/syncthing/syncthing/cmd/...")
case "integration": case "integration":
integration(false) integration(false)
@@ -379,6 +380,7 @@ func parseFlags() {
flag.IntVar(&numVersions, "num-versions", numVersions, "Number of versions for changelog command") flag.IntVar(&numVersions, "num-versions", numVersions, "Number of versions for changelog command")
flag.StringVar(&run, "run", "", "Specify which tests to run") flag.StringVar(&run, "run", "", "Specify which tests to run")
flag.StringVar(&benchRun, "bench", "", "Specify which benchmarks to run") flag.StringVar(&benchRun, "bench", "", "Specify which benchmarks to run")
flag.BoolVar(&withNextGenGUI, "with-next-gen-gui", withNextGenGUI, "Also build 'newgui'")
flag.StringVar(&buildOut, "build-out", "", "Set the '-o' value for 'go build'") flag.StringVar(&buildOut, "build-out", "", "Set the '-o' value for 'go build'")
flag.Parse() flag.Parse()
} }
@@ -451,6 +453,10 @@ func benchArgs() []string {
} }
func install(target target, tags []string) { func install(target target, tags []string) {
if (target.name == "syncthing" || target.name == "") && !withNextGenGUI {
log.Println("Notice: Next generation GUI will not be built; see --with-next-gen-gui.")
}
lazyRebuildAssets() lazyRebuildAssets()
tags = append(target.tags, tags...) tags = append(target.tags, tags...)
@@ -474,12 +480,16 @@ func install(target target, tags []string) {
defer shouldCleanupSyso(sysoPath) defer shouldCleanupSyso(sysoPath)
} }
args := []string{"install"} args := []string{"install", "-v"}
args = appendParameters(args, tags, target.buildPkgs...) args = appendParameters(args, tags, target.buildPkgs...)
runPrint(goCmd, args...) runPrint(goCmd, args...)
} }
func build(target target, tags []string) { func build(target target, tags []string) {
if (target.name == "syncthing" || target.name == "") && !withNextGenGUI {
log.Println("Notice: Next generation GUI will not be built; see --with-next-gen-gui.")
}
lazyRebuildAssets() lazyRebuildAssets()
tags = append(target.tags, tags...) tags = append(target.tags, tags...)
@@ -502,7 +512,7 @@ func build(target target, tags []string) {
defer shouldCleanupSyso(sysoPath) defer shouldCleanupSyso(sysoPath)
} }
args := []string{"build"} args := []string{"build", "-v"}
if buildOut != "" { if buildOut != "" {
args = append(args, "-o", buildOut) args = append(args, "-o", buildOut)
} }
@@ -514,6 +524,13 @@ func setBuildEnvVars() {
os.Setenv("GOOS", goos) os.Setenv("GOOS", goos)
os.Setenv("GOARCH", goarch) os.Setenv("GOARCH", goarch)
os.Setenv("CC", cc) os.Setenv("CC", cc)
if os.Getenv("CGO_ENABLED") == "" {
switch goos {
case "darwin", "solaris":
default:
os.Setenv("CGO_ENABLED", "0")
}
}
} }
func appendParameters(args []string, tags []string, pkgs ...string) []string { func appendParameters(args []string, tags []string, pkgs ...string) []string {
@@ -718,7 +735,7 @@ func shouldBuildSyso(dir string) (string, error) {
} }
jsonPath := filepath.Join(dir, "versioninfo.json") jsonPath := filepath.Join(dir, "versioninfo.json")
err = os.WriteFile(jsonPath, bs, 0o666) err = os.WriteFile(jsonPath, bs, 0o644)
if err != nil { if err != nil {
return "", errors.New("failed to create " + jsonPath + ": " + err.Error()) return "", errors.New("failed to create " + jsonPath + ": " + err.Error())
} }
@@ -732,18 +749,12 @@ func shouldBuildSyso(dir string) (string, error) {
sysoPath := filepath.Join(dir, "cmd", "syncthing", "resource.syso") sysoPath := filepath.Join(dir, "cmd", "syncthing", "resource.syso")
// See https://github.com/josephspurrier/goversioninfo#command-line-flags // See https://github.com/josephspurrier/goversioninfo#command-line-flags
// For manifest see https://learn.microsoft.com/en-us/windows/console/console-allocation-policy armOption := ""
isARM := strings.HasPrefix(goarch, "arm") if strings.Contains(goarch, "arm") {
is64Bit := strings.Contains(goarch, "64") armOption = "-arm=true"
args := []string{
"-manifest=assets/windows/syncthing.exe.manifest", // console-allocation-policy
"-o", sysoPath, // output path
fmt.Sprintf("-arm=%v", isARM),
fmt.Sprintf("-64=%v", is64Bit),
} }
if _, err := runError("goversioninfo", args...); err != nil { if _, err := runError("goversioninfo", "-o", sysoPath, armOption); err != nil {
return "", errors.New("failed to create " + sysoPath + ": " + err.Error()) return "", errors.New("failed to create " + sysoPath + ": " + err.Error())
} }
@@ -783,7 +794,7 @@ func copyFile(src, dst string, perm os.FileMode) error {
} }
copy: copy:
os.MkdirAll(filepath.Dir(dst), os.ModePerm) os.MkdirAll(filepath.Dir(dst), 0o777)
if err := os.WriteFile(dst, in, perm); err != nil { if err := os.WriteFile(dst, in, perm); err != nil {
return err return err
} }
@@ -815,11 +826,43 @@ func lazyRebuildAssets() {
shouldRebuild := shouldRebuildAssets("lib/api/auto/gui.files.go", "gui") || shouldRebuild := shouldRebuildAssets("lib/api/auto/gui.files.go", "gui") ||
shouldRebuildAssets("cmd/infra/strelaypoolsrv/auto/gui.files.go", "cmd/infra/strelaypoolsrv/gui") shouldRebuildAssets("cmd/infra/strelaypoolsrv/auto/gui.files.go", "cmd/infra/strelaypoolsrv/gui")
if withNextGenGUI {
shouldRebuild = buildNextGenGUI() || shouldRebuild
}
if shouldRebuild { if shouldRebuild {
rebuildAssets() rebuildAssets()
} }
} }
func buildNextGenGUI() bool {
// Check if we need to run the npm process, and if so also set the flag
// to rebuild Go assets afterwards. The index.html is regenerated every
// time by the build process. This assumes the new GUI ends up in
// next-gen-gui/dist/next-gen-gui.
if !shouldRebuildAssets("gui/next-gen-gui/index.html", "next-gen-gui") {
// The GUI is up to date.
return false
}
runPrintInDir("next-gen-gui", "npm", "install")
runPrintInDir("next-gen-gui", "npm", "run", "build", "--", "--prod", "--subresource-integrity")
rmr("gui/tech-ui")
for _, src := range listFiles("next-gen-gui/dist") {
rel, _ := filepath.Rel("next-gen-gui/dist", src)
dst := filepath.Join("gui", rel)
if err := copyFile(src, dst, 0o644); err != nil {
fmt.Println("copy:", err)
os.Exit(1)
}
}
return true
}
func shouldRebuildAssets(target, srcdir string) bool { func shouldRebuildAssets(target, srcdir string) bool {
info, err := os.Stat(target) info, err := os.Stat(target)
if err != nil { if err != nil {
@@ -879,6 +922,7 @@ func testmocks() {
"github.com/syncthing/syncthing/lib/connections", "github.com/syncthing/syncthing/lib/connections",
"github.com/syncthing/syncthing/lib/discover", "github.com/syncthing/syncthing/lib/discover",
"github.com/syncthing/syncthing/lib/events", "github.com/syncthing/syncthing/lib/events",
"github.com/syncthing/syncthing/lib/logger",
"github.com/syncthing/syncthing/lib/model", "github.com/syncthing/syncthing/lib/model",
"github.com/syncthing/syncthing/lib/protocol", "github.com/syncthing/syncthing/lib/protocol",
} }
@@ -909,7 +953,6 @@ func weblate() {
func ldflags(tags []string) string { func ldflags(tags []string) string {
b := new(strings.Builder) b := new(strings.Builder)
b.WriteString("-w") b.WriteString("-w")
b.WriteString(" -buildid=")
fmt.Fprintf(b, " -X github.com/syncthing/syncthing/lib/build.Version=%s", version) fmt.Fprintf(b, " -X github.com/syncthing/syncthing/lib/build.Version=%s", version)
fmt.Fprintf(b, " -X github.com/syncthing/syncthing/lib/build.Stamp=%d", buildStamp()) fmt.Fprintf(b, " -X github.com/syncthing/syncthing/lib/build.Stamp=%d", buildStamp())
fmt.Fprintf(b, " -X github.com/syncthing/syncthing/lib/build.User=%s", buildUser()) fmt.Fprintf(b, " -X github.com/syncthing/syncthing/lib/build.User=%s", buildUser())
@@ -931,9 +974,6 @@ func rmr(paths ...string) {
} }
func getReleaseVersion() (string, error) { func getReleaseVersion() (string, error) {
if ver := os.Getenv("VERSION"); ver != "" {
return strings.TrimSpace(ver), nil
}
bs, err := os.ReadFile("RELEASE") bs, err := os.ReadFile("RELEASE")
if err != nil { if err != nil {
return "", err return "", err
@@ -978,7 +1018,7 @@ func getGitVersion() (string, error) {
} }
func getVersion() string { func getVersion() string {
// First try for a RELEASE file or $VERSION env var, // First try for a RELEASE file,
if ver, err := getReleaseVersion(); err == nil { if ver, err := getReleaseVersion(); err == nil {
return ver return ver
} }
@@ -1432,7 +1472,7 @@ func writeCompatJSON() {
continue continue
} }
bs, _ := json.MarshalIndent(e, "", " ") bs, _ := json.MarshalIndent(e, "", " ")
if err := os.WriteFile("compat.json", bs, 0o666); err != nil { if err := os.WriteFile("compat.json", bs, 0o644); err != nil {
log.Fatal("Writing compat.json:", err) log.Fatal("Writing compat.json:", err)
} }
return return
+2
View File
@@ -15,6 +15,8 @@ import (
"log" "log"
"os" "os"
"path/filepath" "path/filepath"
_ "github.com/syncthing/syncthing/lib/automaxprocs"
) )
func main() { func main() {
+1
View File
@@ -18,6 +18,7 @@ import (
"google.golang.org/protobuf/proto" "google.golang.org/protobuf/proto"
"github.com/syncthing/syncthing/internal/gen/discoproto" "github.com/syncthing/syncthing/internal/gen/discoproto"
_ "github.com/syncthing/syncthing/lib/automaxprocs"
"github.com/syncthing/syncthing/lib/beacon" "github.com/syncthing/syncthing/lib/beacon"
"github.com/syncthing/syncthing/lib/discover" "github.com/syncthing/syncthing/lib/discover"
"github.com/syncthing/syncthing/lib/protocol" "github.com/syncthing/syncthing/lib/protocol"
+6 -4
View File
@@ -14,13 +14,15 @@ import (
"net/http" "net/http"
"os" "os"
"time" "time"
_ "github.com/syncthing/syncthing/lib/automaxprocs"
) )
type event struct { type event struct {
ID int `json:"id"` ID int `json:"id"`
Type string `json:"type"` Type string `json:"type"`
Time time.Time `json:"time"` Time time.Time `json:"time"`
Data map[string]any `json:"data"` Data map[string]interface{} `json:"data"`
} }
func main() { func main() {
+2 -1
View File
@@ -13,6 +13,7 @@ import (
"os" "os"
"path/filepath" "path/filepath"
_ "github.com/syncthing/syncthing/lib/automaxprocs"
"github.com/syncthing/syncthing/lib/protocol" "github.com/syncthing/syncthing/lib/protocol"
"github.com/syncthing/syncthing/lib/scanner" "github.com/syncthing/syncthing/lib/scanner"
) )
@@ -71,7 +72,7 @@ func main() {
if *standardBlocks || blockSize < protocol.MinBlockSize { if *standardBlocks || blockSize < protocol.MinBlockSize {
blockSize = protocol.BlockSize(fi.Size()) blockSize = protocol.BlockSize(fi.Size())
} }
bs, err := scanner.Blocks(context.TODO(), fd, blockSize, fi.Size(), nil) bs, err := scanner.Blocks(context.TODO(), fd, blockSize, fi.Size(), nil, true)
if err != nil { if err != nil {
log.Fatal(err) log.Fatal(err)
} }
+2
View File
@@ -16,6 +16,7 @@ import (
"os" "os"
"time" "time"
_ "github.com/syncthing/syncthing/lib/automaxprocs"
"github.com/syncthing/syncthing/lib/config" "github.com/syncthing/syncthing/lib/config"
"github.com/syncthing/syncthing/lib/discover" "github.com/syncthing/syncthing/lib/discover"
"github.com/syncthing/syncthing/lib/events" "github.com/syncthing/syncthing/lib/events"
@@ -60,6 +61,7 @@ func checkServers(deviceID protocol.DeviceID, servers ...string) {
t0 := time.Now() t0 := time.Now()
resc := make(chan checkResult) resc := make(chan checkResult)
for _, srv := range servers { for _, srv := range servers {
srv := srv
go func() { go func() {
res := checkServer(deviceID, srv) res := checkServer(deviceID, srv)
res.server = srv res.server = srv
+1
View File
@@ -12,6 +12,7 @@ import (
"fmt" "fmt"
"os" "os"
_ "github.com/syncthing/syncthing/lib/automaxprocs"
"github.com/syncthing/syncthing/lib/fs" "github.com/syncthing/syncthing/lib/fs"
"github.com/syncthing/syncthing/lib/ignore" "github.com/syncthing/syncthing/lib/ignore"
) )
+7 -2
View File
@@ -15,6 +15,8 @@ import (
"os" "os"
"path/filepath" "path/filepath"
"time" "time"
_ "github.com/syncthing/syncthing/lib/automaxprocs"
) )
func main() { func main() {
@@ -34,7 +36,7 @@ func generateFiles(dir string, files, maxexp int, srcname string) error {
return err return err
} }
for range files { for i := 0; i < files; i++ {
n := randomName() n := randomName()
if rand.Float64() < 0.05 { if rand.Float64() < 0.05 {
@@ -51,7 +53,10 @@ func generateFiles(dir string, files, maxexp int, srcname string) error {
p1 := filepath.Join(p0, n) p1 := filepath.Join(p0, n)
s := int64(1 << uint(rand.Intn(maxexp))) s := int64(1 << uint(rand.Intn(maxexp)))
a := min(int64(128*1024), s) a := int64(128 * 1024)
if a > s {
a = s
}
s += rand.Int63n(a) s += rand.Int63n(a)
if err := generateOneFile(fd, p1, s); err != nil { if err := generateOneFile(fd, p1, s); err != nil {
+1
View File
@@ -12,6 +12,7 @@ import (
"log" "log"
"os" "os"
_ "github.com/syncthing/syncthing/lib/automaxprocs"
"github.com/syncthing/syncthing/lib/signature" "github.com/syncthing/syncthing/lib/signature"
"github.com/syncthing/syncthing/lib/upgrade" "github.com/syncthing/syncthing/lib/upgrade"
) )
+7 -4
View File
@@ -26,6 +26,7 @@ import (
"sync/atomic" "sync/atomic"
"time" "time"
_ "github.com/syncthing/syncthing/lib/automaxprocs"
"github.com/syncthing/syncthing/lib/protocol" "github.com/syncthing/syncthing/lib/protocol"
) )
@@ -52,9 +53,11 @@ func main() {
// Run one certificate generator per CPU core. // Run one certificate generator per CPU core.
var wg sync.WaitGroup var wg sync.WaitGroup
for i := 0; i < runtime.GOMAXPROCS(-1); i++ { for i := 0; i < runtime.GOMAXPROCS(-1); i++ {
wg.Go(func() { wg.Add(1)
go func() {
generatePrefixed(prefix, &count, found, stop) generatePrefixed(prefix, &count, found, stop)
}) wg.Done()
}()
} }
// Save the result, when one has been found. // Save the result, when one has been found.
@@ -138,7 +141,7 @@ func printProgress(prefix string, count *atomic.Int64) {
} }
} }
func saveCert(priv any, derBytes []byte) { func saveCert(priv interface{}, derBytes []byte) {
certOut, err := os.Create("cert.pem") certOut, err := os.Create("cert.pem")
if err != nil { if err != nil {
fmt.Println(err) fmt.Println(err)
@@ -179,7 +182,7 @@ func saveCert(priv any, derBytes []byte) {
} }
} }
func pemBlockForKey(priv any) (*pem.Block, error) { func pemBlockForKey(priv interface{}) (*pem.Block, error) {
switch k := priv.(type) { switch k := priv.(type) {
case *rsa.PrivateKey: case *rsa.PrivateKey:
return &pem.Block{Type: "RSA PRIVATE KEY", Bytes: x509.MarshalPKCS1PrivateKey(k)}, nil return &pem.Block{Type: "RSA PRIVATE KEY", Bytes: x509.MarshalPKCS1PrivateKey(k)}, nil
+2
View File
@@ -13,6 +13,8 @@ import (
"io" "io"
"os" "os"
"time" "time"
_ "github.com/syncthing/syncthing/lib/automaxprocs"
) )
func main() { func main() {
@@ -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
+7 -17
View File
@@ -42,7 +42,7 @@ type currentFile struct {
} }
func (d *diskStore) Serve(ctx context.Context) { func (d *diskStore) Serve(ctx context.Context) {
if err := os.MkdirAll(d.dir, os.ModePerm); err != nil { if err := os.MkdirAll(d.dir, 0o700); err != nil {
log.Println("Creating directory:", err) log.Println("Creating directory:", err)
return return
} }
@@ -62,7 +62,7 @@ func (d *diskStore) Serve(ctx context.Context) {
case entry := <-d.inbox: case entry := <-d.inbox:
path := d.fullPath(entry.path) path := d.fullPath(entry.path)
if err := os.MkdirAll(filepath.Dir(path), os.ModePerm); err != nil { if err := os.MkdirAll(filepath.Dir(path), 0o700); err != nil {
log.Println("Creating directory:", err) log.Println("Creating directory:", err)
continue continue
} }
@@ -77,7 +77,7 @@ func (d *diskStore) Serve(ctx context.Context) {
log.Println("Failed to compress crash report:", err) log.Println("Failed to compress crash report:", err)
continue continue
} }
if err := os.WriteFile(path, buf.Bytes(), 0o666); err != nil { if err := os.WriteFile(path, buf.Bytes(), 0o600); err != nil {
log.Printf("Failed to write %s: %v", entry.path, err) log.Printf("Failed to write %s: %v", entry.path, err)
_ = os.Remove(path) _ = os.Remove(path)
continue continue
@@ -136,25 +136,15 @@ func (d *diskStore) Exists(path string) bool {
} }
func (d *diskStore) clean() { func (d *diskStore) clean() {
numDeleted := 0 for len(d.currentFiles) > 0 && (len(d.currentFiles) > d.maxFiles || d.currentSize > d.maxBytes) {
for idx := range d.currentFiles { f := d.currentFiles[0]
if len(d.currentFiles)-numDeleted < d.maxFiles && d.currentSize < d.maxBytes {
break
}
f := d.currentFiles[idx]
log.Println("Removing", f.path) log.Println("Removing", f.path)
if err := os.Remove(f.path); err != nil { if err := os.Remove(f.path); err != nil {
log.Println("Failed to remove file:", err) log.Println("Failed to remove file:", err)
} }
d.currentFiles = d.currentFiles[1:]
d.currentSize -= f.size 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 var oldest time.Duration
if len(d.currentFiles) > 0 { if len(d.currentFiles) > 0 {
oldest = time.Since(time.Unix(d.currentFiles[0].mtime, 0)).Truncate(time.Minute) 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 { func (d *diskStore) inventory() error {
d.currentFiles = d.currentFiles[:0] d.currentFiles = nil
d.currentSize = 0 d.currentSize = 0
err := filepath.Walk(d.dir, func(path string, info os.FileInfo, err error) error { err := filepath.Walk(d.dir, func(path string, info os.FileInfo, err error) error {
if err != nil { if err != nil {
+16 -20
View File
@@ -20,7 +20,6 @@ import (
"io" "io"
"log" "log"
"net/http" "net/http"
"net/http/pprof"
"os" "os"
"path/filepath" "path/filepath"
"regexp" "regexp"
@@ -29,8 +28,9 @@ import (
"github.com/alecthomas/kong" "github.com/alecthomas/kong"
raven "github.com/getsentry/raven-go" raven "github.com/getsentry/raven-go"
"github.com/prometheus/client_golang/prometheus/promhttp" "github.com/prometheus/client_golang/prometheus/promhttp"
_ "github.com/syncthing/syncthing/lib/automaxprocs"
"github.com/syncthing/syncthing/lib/build" "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 const maxRequestSize = 1 << 20 // 1 MiB
@@ -44,7 +44,7 @@ type cli struct {
SentryQueue int `help:"Maximum number of reports to queue for sending to Sentry" default:"64" env:"SENTRY_QUEUE"` SentryQueue int `help:"Maximum number of reports to queue for sending to Sentry" default:"64" env:"SENTRY_QUEUE"`
DiskQueue int `help:"Maximum number of reports to queue for writing to disk" default:"64" env:"DISK_QUEUE"` DiskQueue int `help:"Maximum number of reports to queue for writing to disk" default:"64" env:"DISK_QUEUE"`
MetricsListen string `help:"HTTP listen address for metrics" default:":8081" env:"METRICS_LISTEN_ADDRESS"` MetricsListen string `help:"HTTP listen address for metrics" default:":8081" env:"METRICS_LISTEN_ADDRESS"`
IgnorePatterns string `help:"File containing ignore patterns (regexp)" env:"IGNORE_PATTERNS" type:"existingfile"` IngorePatterns string `help:"File containing ignore patterns (regexp)" env:"IGNORE_PATTERNS" type:"existingfile"`
} }
func main() { func main() {
@@ -68,9 +68,9 @@ func main() {
go ss.Serve(context.Background()) go ss.Serve(context.Background())
var ip *ignorePatterns var ip *ignorePatterns
if params.IgnorePatterns != "" { if params.IngorePatterns != "" {
var err error var err error
ip, err = loadIgnorePatterns(params.IgnorePatterns) ip, err = loadIgnorePatterns(params.IngorePatterns)
if err != nil { if err != nil {
log.Fatalf("Failed to load ignore patterns: %v", err) log.Fatalf("Failed to load ignore patterns: %v", err)
} }
@@ -90,7 +90,6 @@ func main() {
if params.MetricsListen != "" { if params.MetricsListen != "" {
mmux := http.NewServeMux() mmux := http.NewServeMux()
mmux.Handle("/metrics", promhttp.Handler()) mmux.Handle("/metrics", promhttp.Handler())
mmux.HandleFunc("/debug/pprof/", pprof.Index)
go func() { go func() {
if err := http.ListenAndServe(params.MetricsListen, mmux); err != nil { if err := http.ListenAndServe(params.MetricsListen, mmux); err != nil {
log.Fatalln("HTTP serve metrics:", err) log.Fatalln("HTTP serve metrics:", err)
@@ -103,8 +102,6 @@ func main() {
} }
log.SetOutput(os.Stdout) log.SetOutput(os.Stdout)
log.Println(build.LongVersionFor("stcrashreceiver"))
if err := http.ListenAndServe(params.Listen, mux); err != nil { if err := http.ListenAndServe(params.Listen, mux); err != nil {
log.Fatalln("HTTP serve:", err) log.Fatalln("HTTP serve:", err)
} }
@@ -121,20 +118,19 @@ func handleFailureFn(dsn, failureDir string, ignore *ignorePatterns) func(w http
bs, err := io.ReadAll(lr) bs, err := io.ReadAll(lr)
req.Body.Close() req.Body.Close()
if err != nil { if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError) http.Error(w, err.Error(), 500)
return return
} }
if pat, ok := ignore.match(bs); ok { if ignore.match(bs) {
metricIgnoreMatchesTotal.WithLabelValues(pat).Inc()
result = "ignored" result = "ignored"
return return
} }
var reports []contract.FailureReport var reports []ur.FailureReport
err = json.Unmarshal(bs, &reports) err = json.Unmarshal(bs, &reports)
if err != nil { if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest) http.Error(w, err.Error(), 400)
return return
} }
if len(reports) == 0 { if len(reports) == 0 {
@@ -145,7 +141,7 @@ func handleFailureFn(dsn, failureDir string, ignore *ignorePatterns) func(w http
version, err := build.ParseVersion(reports[0].Version) version, err := build.ParseVersion(reports[0].Version)
if err != nil { if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest) http.Error(w, err.Error(), 400)
return return
} }
for _, r := range reports { for _, r := range reports {
@@ -179,7 +175,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)) bs := make([]byte, len(data.Description)+len(data.Goroutines))
copy(bs, data.Description) copy(bs, data.Description)
copy(bs[len(data.Description):], data.Goroutines) copy(bs[len(data.Description):], data.Goroutines)
@@ -203,7 +199,7 @@ func loadIgnorePatterns(path string) (*ignorePatterns, error) {
} }
var patterns []*regexp.Regexp var patterns []*regexp.Regexp
for line := range strings.SplitSeq(string(bs), "\n") { for _, line := range strings.Split(string(bs), "\n") {
line = strings.TrimSpace(line) line = strings.TrimSpace(line)
if line == "" { if line == "" {
continue continue
@@ -219,14 +215,14 @@ func loadIgnorePatterns(path string) (*ignorePatterns, error) {
return &ignorePatterns{patterns: patterns}, nil return &ignorePatterns{patterns: patterns}, nil
} }
func (i *ignorePatterns) match(report []byte) (string, bool) { func (i *ignorePatterns) match(report []byte) bool {
if i == nil { if i == nil {
return "", false return false
} }
for _, re := range i.patterns { for _, re := range i.patterns {
if re.Match(report) { if re.Match(report) {
return re.String(), true return true
} }
} }
return "", false return false
} }
-20
View File
@@ -37,24 +37,4 @@ var (
Subsystem: "crashreceiver", Subsystem: "crashreceiver",
Name: "diskstore_oldest_age_seconds", 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 -10
View File
@@ -10,7 +10,6 @@ import (
"bytes" "bytes"
"context" "context"
"errors" "errors"
"fmt"
"io" "io"
"log" "log"
"regexp" "regexp"
@@ -53,15 +52,11 @@ func (s *sentryService) Serve(ctx context.Context) {
pkt, err := parseCrashReport(req.reportID, req.data) pkt, err := parseCrashReport(req.reportID, req.data)
if err != nil { if err != nil {
log.Println("Failed to parse crash report:", err) log.Println("Failed to parse crash report:", err)
metricSentryReportsTotal.WithLabelValues("parse_failure").Inc()
continue continue
} }
if err := sendReport(s.dsn, pkt, req.userID); err != nil { if err := sendReport(s.dsn, pkt, req.userID); err != nil {
log.Println("Failed to send crash report:", err) log.Println("Failed to send crash report:", err)
metricSentryReportsTotal.WithLabelValues("send_failure").Inc()
continue
} }
metricSentryReportsTotal.WithLabelValues("success").Inc()
case <-ctx.Done(): case <-ctx.Done():
return return
@@ -74,7 +69,6 @@ func (s *sentryService) Send(reportID, userID string, data []byte) bool {
case s.inbox <- sentryRequest{reportID, userID, data}: case s.inbox <- sentryRequest{reportID, userID, data}:
return true return true
default: default:
metricCrashReportsTotal.WithLabelValues("overflow").Inc()
return false return false
} }
} }
@@ -96,7 +90,7 @@ func sendReport(dsn string, pkt *raven.Packet, userID string) error {
} }
// The client sets release and such on the packet before sending, in the // The client sets release and such on the packet before sending, in the
// misguided idea that it knows this better than the packet we give // misguided idea that it knows this better than than the packet we give
// it. So we copy the values from the packet to the client first... // it. So we copy the values from the packet to the client first...
cli.SetRelease(pkt.Release) cli.SetRelease(pkt.Release)
cli.SetEnvironment(pkt.Environment) cli.SetEnvironment(pkt.Environment)
@@ -114,10 +108,11 @@ func parseCrashReport(path string, report []byte) (*raven.Packet, error) {
version, err := build.ParseVersion(string(parts[0])) version, err := build.ParseVersion(string(parts[0]))
if err != nil { if err != nil {
return nil, fmt.Errorf("%w in %q", err, parts[0]) return nil, err
} }
report = parts[1] report = parts[1]
foundPanic := false
var subjectLine []byte var subjectLine []byte
for { for {
parts = bytes.SplitN(report, []byte("\n"), 2) parts = bytes.SplitN(report, []byte("\n"), 2)
@@ -128,15 +123,20 @@ func parseCrashReport(path string, report []byte) (*raven.Packet, error) {
line := parts[0] line := parts[0]
report = parts[1] 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 subjectLine = line
break break
} else if bytes.HasPrefix(line, []byte("Panic at")) {
foundPanic = true
} }
} }
r := bytes.NewReader(report) r := bytes.NewReader(report)
ctx, _, err := stack.ScanSnapshot(r, io.Discard, stack.DefaultOpts()) ctx, _, err := stack.ScanSnapshot(r, io.Discard, stack.DefaultOpts())
if err != nil && !errors.Is(err, io.EOF) { if err != nil && err != io.EOF {
return nil, err return nil, err
} }
if ctx == nil || len(ctx.Goroutines) == 0 { if ctx == nil || len(ctx.Goroutines) == 0 {
+11 -18
View File
@@ -9,33 +9,26 @@ package main
import ( import (
"fmt" "fmt"
"os" "os"
"path/filepath"
"testing" "testing"
) )
func TestParseReport(t *testing.T) { func TestParseReport(t *testing.T) {
files, err := filepath.Glob("_testdata/*.log") bs, err := os.ReadFile("_testdata/panic.log")
if err != nil { if err != nil {
t.Fatal(err) 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) pkt, err := parseCrashReport("1/2/345", bs)
if err != nil { if err != nil {
t.Fatal(err) 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) { func TestCrashReportFingerprint(t *testing.T) {
+12 -27
View File
@@ -15,33 +15,23 @@ import (
"strings" "strings"
"sync" "sync"
"time" "time"
lru "github.com/hashicorp/golang-lru/v2"
) )
const ( const (
urlPrefix = "https://raw.githubusercontent.com/syncthing/syncthing/" urlPrefix = "https://raw.githubusercontent.com/syncthing/syncthing/"
httpTimeout = 10 * time.Second httpTimeout = 10 * time.Second
maxCacheEntries = 1000
) )
type cacheKey struct {
version string
file string
}
type githubSourceCodeLoader struct { type githubSourceCodeLoader struct {
mut sync.Mutex mut sync.Mutex
version string version string
cache map[string]map[string][][]byte // version -> file -> lines
cache *lru.TwoQueueCache[cacheKey, [][]byte] // version & file -> lines client *http.Client
client *http.Client
} }
func newGithubSourceCodeLoader() *githubSourceCodeLoader { func newGithubSourceCodeLoader() *githubSourceCodeLoader {
cache, _ := lru.New2Q[cacheKey, [][]byte](maxCacheEntries)
return &githubSourceCodeLoader{ return &githubSourceCodeLoader{
cache: cache, cache: make(map[string]map[string][][]byte),
client: &http.Client{Timeout: httpTimeout}, client: &http.Client{Timeout: httpTimeout},
} }
} }
@@ -49,6 +39,9 @@ func newGithubSourceCodeLoader() *githubSourceCodeLoader {
func (l *githubSourceCodeLoader) LockWithVersion(version string) { func (l *githubSourceCodeLoader) LockWithVersion(version string) {
l.mut.Lock() l.mut.Lock()
l.version = version l.version = version
if _, ok := l.cache[version]; !ok {
l.cache[version] = make(map[string][][]byte)
}
} }
func (l *githubSourceCodeLoader) Unlock() { func (l *githubSourceCodeLoader) Unlock() {
@@ -57,16 +50,14 @@ func (l *githubSourceCodeLoader) Unlock() {
func (l *githubSourceCodeLoader) Load(filename string, line, context int) ([][]byte, int) { func (l *githubSourceCodeLoader) Load(filename string, line, context int) ([][]byte, int) {
filename = filepath.ToSlash(filename) filename = filepath.ToSlash(filename)
key := cacheKey{version: l.version, file: filename} lines, ok := l.cache[l.version][filename]
lines, ok := l.cache.Get(key)
if !ok { if !ok {
// Cache whatever we managed to find (or nil if nothing, so we don't try again) // Cache whatever we managed to find (or nil if nothing, so we don't try again)
defer func() { defer func() {
l.cache.Add(key, lines) l.cache[l.version][filename] = lines
metricSourceCodeCacheSize.Set(float64(l.cache.Len()))
}() }()
knownPrefixes := []string{"/internal/", "/lib/", "/cmd/"} knownPrefixes := []string{"/lib/", "/cmd/"}
var idx int var idx int
for _, pref := range knownPrefixes { for _, pref := range knownPrefixes {
idx = strings.Index(filename, pref) idx = strings.Index(filename, pref)
@@ -82,25 +73,19 @@ func (l *githubSourceCodeLoader) Load(filename string, line, context int) ([][]b
resp, err := l.client.Get(url) resp, err := l.client.Get(url)
if err != nil { if err != nil {
fmt.Println("Loading source:", err) fmt.Println("Loading source:", err)
metricSourceCodeLoadsTotal.WithLabelValues("failed").Inc()
return nil, 0 return nil, 0
} }
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK { if resp.StatusCode != http.StatusOK {
fmt.Println("Loading source:", resp.Status) fmt.Println("Loading source:", resp.Status)
metricSourceCodeLoadsTotal.WithLabelValues("failed").Inc()
return nil, 0 return nil, 0
} }
data, err := io.ReadAll(resp.Body) data, err := io.ReadAll(resp.Body)
_ = resp.Body.Close()
if err != nil { if err != nil {
fmt.Println("Loading source:", err.Error()) fmt.Println("Loading source:", err.Error())
metricSourceCodeLoadsTotal.WithLabelValues("failed").Inc()
return nil, 0 return nil, 0
} }
lines = bytes.Split(data, []byte{'\n'}) lines = bytes.Split(data, []byte{'\n'})
metricSourceCodeLoadsTotal.WithLabelValues("loaded").Inc()
} else {
metricSourceCodeLoadsTotal.WithLabelValues("cached").Inc()
} }
return getLineFromLines(lines, line, context) return getLineFromLines(lines, line, context)
+29 -10
View File
@@ -7,18 +7,21 @@
package main package main
import ( import (
"bytes"
"io" "io"
"log" "log"
"net/http" "net/http"
"path" "path"
"strings" "strings"
"sync"
) )
type crashReceiver struct { type crashReceiver struct {
store *diskStore store *diskStore
sentry *sentryService sentry *sentryService
ignore *ignorePatterns ignore *ignorePatterns
ignoredMut sync.RWMutex
ignored map[string]struct{}
} }
func (r *crashReceiver) ServeHTTP(w http.ResponseWriter, req *http.Request) { 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 // serveHead responds to HEAD requests by checking if the named report
// already exists in the system. // already exists in the system.
func (r *crashReceiver) serveHead(reportID string, w http.ResponseWriter, _ *http.Request) { 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) { if !r.store.Exists(reportID) {
http.Error(w, "Not found", http.StatusNotFound) 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() 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. // Read at most maxRequestSize of report data.
log.Println("Receiving report", reportID)
lr := io.LimitReader(req.Body, maxRequestSize) lr := io.LimitReader(req.Body, maxRequestSize)
bs, err := io.ReadAll(lr) bs, err := io.ReadAll(lr)
if err != nil { if err != nil {
@@ -87,12 +106,14 @@ func (r *crashReceiver) servePut(reportID string, w http.ResponseWriter, req *ht
return return
} }
first := string(bytes.TrimSpace(bytes.Split(bs, []byte("\n"))[0])) if r.ignore.match(bs) {
r.ignoredMut.Lock()
if pat, ok := r.ignore.match(bs); ok { if r.ignored == nil {
metricIgnoreMatchesTotal.WithLabelValues(pat).Inc() r.ignored = make(map[string]struct{})
}
r.ignored[reportID] = struct{}{}
r.ignoredMut.Unlock()
result = "ignored" result = "ignored"
log.Printf("Ignored report %s, matched: %s (%s)", reportID[:8], pat, first)
return return
} }
@@ -100,15 +121,13 @@ func (r *crashReceiver) servePut(reportID string, w http.ResponseWriter, req *ht
// Store the report // Store the report
if !r.store.Put(reportID, bs) { 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" result = "queue_failure"
} }
// Send the report to Sentry // Send the report to Sentry
if !r.sentry.Send(reportID, userIDFor(req), bs) { 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" result = "sentry_failure"
} }
log.Printf("Received report %s (%s)", reportID[:8], first)
} }
+4 -4
View File
@@ -10,7 +10,7 @@ import (
"bytes" "bytes"
"compress/gzip" "compress/gzip"
"crypto/sha256" "crypto/sha256"
"encoding/hex" "fmt"
"net" "net"
"net/http" "net/http"
"os" "os"
@@ -23,7 +23,7 @@ import (
// remote IP, and the current month. // remote IP, and the current month.
func userIDFor(req *http.Request) string { func userIDFor(req *http.Request) string {
addr := req.RemoteAddr addr := req.RemoteAddr
if fwd := req.Header.Get("X-Forwarded-For"); fwd != "" { if fwd := req.Header.Get("x-forwarded-for"); fwd != "" {
addr = fwd addr = fwd
} }
if host, _, err := net.SplitHostPort(addr); err == nil { if host, _, err := net.SplitHostPort(addr); err == nil {
@@ -32,7 +32,7 @@ func userIDFor(req *http.Request) string {
now := time.Now().Format("200601") now := time.Now().Format("200601")
salt := "stcrashreporter" salt := "stcrashreporter"
hash := sha256.Sum256([]byte(salt + addr + now)) hash := sha256.Sum256([]byte(salt + addr + now))
return hex.EncodeToString(hash[:8]) return fmt.Sprintf("%x", hash[:8])
} }
// 01234567890abcdef... => 01/23 // 01234567890abcdef... => 01/23
@@ -52,5 +52,5 @@ func compressAndWrite(bs []byte, fullPath string) error {
gw.Close() gw.Close()
// Create an output file with the compressed report // Create an output file with the compressed report
return os.WriteFile(fullPath, buf.Bytes(), 0o666) return os.WriteFile(fullPath, buf.Bytes(), 0o644)
} }
@@ -5,6 +5,7 @@
// You can obtain one at https://mozilla.org/MPL/2.0/. // You can obtain one at https://mozilla.org/MPL/2.0/.
//go:build noassets //go:build noassets
// +build noassets
package auto package auto
+1 -1
View File
@@ -193,7 +193,7 @@
</div> </div>
<script type="text/javascript" src="https://code.jquery.com/jquery-3.7.1.min.js"></script> <script type="text/javascript" src="https://code.jquery.com/jquery-2.1.4.min.js"></script>
<script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/angular.js/1.5.8/angular.min.js"></script> <script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/angular.js/1.5.8/angular.min.js"></script>
<script type="text/javascript" src="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.5/js/bootstrap.min.js"></script> <script type="text/javascript" src="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.5/js/bootstrap.min.js"></script>
</body> </body>
+20 -39
View File
@@ -3,7 +3,6 @@
package main package main
import ( import (
"bytes"
"context" "context"
"crypto/tls" "crypto/tls"
"crypto/x509" "crypto/x509"
@@ -18,7 +17,6 @@ import (
"path/filepath" "path/filepath"
"strconv" "strconv"
"strings" "strings"
"sync"
"sync/atomic" "sync/atomic"
"time" "time"
@@ -28,12 +26,12 @@ import (
"github.com/syncthing/syncthing/cmd/infra/strelaypoolsrv/auto" "github.com/syncthing/syncthing/cmd/infra/strelaypoolsrv/auto"
"github.com/syncthing/syncthing/lib/assets" "github.com/syncthing/syncthing/lib/assets"
"github.com/syncthing/syncthing/lib/build" _ "github.com/syncthing/syncthing/lib/automaxprocs"
"github.com/syncthing/syncthing/lib/geoip" "github.com/syncthing/syncthing/lib/geoip"
"github.com/syncthing/syncthing/lib/osutil"
"github.com/syncthing/syncthing/lib/protocol" "github.com/syncthing/syncthing/lib/protocol"
"github.com/syncthing/syncthing/lib/rand" "github.com/syncthing/syncthing/lib/rand"
"github.com/syncthing/syncthing/lib/relay/client" "github.com/syncthing/syncthing/lib/relay/client"
"github.com/syncthing/syncthing/lib/sync"
"github.com/syncthing/syncthing/lib/tlsutil" "github.com/syncthing/syncthing/lib/tlsutil"
) )
@@ -117,7 +115,7 @@ var (
requests chan request requests chan request
mut sync.RWMutex mut = sync.NewRWMutex()
knownRelays = make([]*relay, 0) knownRelays = make([]*relay, 0)
permanentRelays = make([]*relay, 0) permanentRelays = make([]*relay, 0)
evictionTimers = make(map[string]*time.Timer) evictionTimers = make(map[string]*time.Timer)
@@ -149,8 +147,6 @@ func main() {
flag.Parse() flag.Parse()
log.Println(build.LongVersionFor("strelaypoolsrv"))
requests = make(chan request, requestQueueLen) requests = make(chan request, requestQueueLen)
geoip, err := geoip.NewGeoLite2CityProvider(context.Background(), geoipAccountID, geoipLicenseKey, os.TempDir()) geoip, err := geoip.NewGeoLite2CityProvider(context.Background(), geoipAccountID, geoipLicenseKey, os.TempDir())
if err != nil { if err != nil {
@@ -166,7 +162,7 @@ func main() {
testCert = createTestCertificate() testCert = createTestCertificate()
for range requestProcessors { for i := 0; i < requestProcessors; i++ {
go requestProcessor(geoip) go requestProcessor(geoip)
} }
@@ -184,7 +180,7 @@ func main() {
relayTestsTotal.WithLabelValues("success").Inc() relayTestsTotal.WithLabelValues("success").Inc()
} }
} }
// Run the stats refresher once the relays are loaded. // Run the the stats refresher once the relays are loaded.
statsRefresher(statsRefresh) statsRefresher(statsRefresh)
}() }()
@@ -320,12 +316,11 @@ func handleEndpointFull(rw http.ResponseWriter, r *http.Request) {
relays := make([]*relay, len(permanentRelays)+len(knownRelays)) relays := make([]*relay, len(permanentRelays)+len(knownRelays))
n := copy(relays, permanentRelays) n := copy(relays, permanentRelays)
copy(relays[n:], knownRelays) copy(relays[n:], knownRelays)
bs, _ := json.Marshal(map[string][]*relay{
"relays": relays,
})
mut.RUnlock() mut.RUnlock()
_, _ = rw.Write(bs) _ = json.NewEncoder(rw).Encode(map[string][]*relay{
"relays": relays,
})
} }
// handleEndpointShort returns the relay list with only the URL. // handleEndpointShort returns the relay list with only the URL.
@@ -335,10 +330,7 @@ func handleEndpointShort(rw http.ResponseWriter, r *http.Request) {
mut.RLock() mut.RLock()
relays := make([]relayShort, 0, len(permanentRelays)+len(knownRelays)) relays := make([]relayShort, 0, len(permanentRelays)+len(knownRelays))
for _, r := range permanentRelays { for _, r := range append(permanentRelays, knownRelays...) {
relays = append(relays, relayShort{URL: slimURL(r.URL)})
}
for _, r := range knownRelays {
relays = append(relays, relayShort{URL: slimURL(r.URL)}) relays = append(relays, relayShort{URL: slimURL(r.URL)})
} }
mut.RUnlock() mut.RUnlock()
@@ -434,7 +426,7 @@ func handleRegister(w http.ResponseWriter, r *http.Request) {
newRelay.URL = uri.String() newRelay.URL = uri.String()
} else if host != rhost && relayCert == nil { } else if host != rhost && relayCert == nil {
if debug { if debug {
log.Println("IP address advertised does not match client IP address", rhost, uri) log.Println("IP address advertised does not match client IP address", r.RemoteAddr, uri)
} }
http.Error(w, fmt.Sprintf("IP advertised %s does not match client IP %s", host, rhost), http.StatusUnauthorized) http.Error(w, fmt.Sprintf("IP advertised %s does not match client IP %s", host, rhost), http.StatusUnauthorized)
return return
@@ -458,13 +450,13 @@ func handleRegister(w http.ResponseWriter, r *http.Request) {
case requests <- request{&newRelay, reschan, prometheus.NewTimer(relayTestActionsSeconds.WithLabelValues("queue"))}: case requests <- request{&newRelay, reschan, prometheus.NewTimer(relayTestActionsSeconds.WithLabelValues("queue"))}:
result := <-reschan result := <-reschan
if result.err != nil { if result.err != nil {
log.Println("Join from", rhost, "failed:", result.err) log.Println("Join from", r.RemoteAddr, "failed:", result.err)
globalBlocklist.AddError(rhost) globalBlocklist.AddError(rhost)
relayTestsTotal.WithLabelValues("failed").Inc() relayTestsTotal.WithLabelValues("failed").Inc()
http.Error(w, result.err.Error(), http.StatusBadRequest) http.Error(w, result.err.Error(), http.StatusBadRequest)
return return
} }
log.Println("Join from", rhost, "succeeded") log.Println("Join from", r.RemoteAddr, "succeeded")
globalBlocklist.ClearErrors(rhost) globalBlocklist.ClearErrors(rhost)
relayTestsTotal.WithLabelValues("success").Inc() relayTestsTotal.WithLabelValues("success").Inc()
w.Header().Set("Content-Type", "application/json; charset=utf-8") w.Header().Set("Content-Type", "application/json; charset=utf-8")
@@ -550,7 +542,7 @@ found:
mut.Unlock() mut.Unlock()
if err := saveKnownRelays(knownRelaysFile); err != nil { if err := saveRelays(knownRelaysFile, knownRelays); err != nil {
log.Println("Failed to write known relays: " + err.Error()) log.Println("Failed to write known relays: " + err.Error())
} }
@@ -587,7 +579,7 @@ func loadRelays(file string, geoip *geoip.Provider) []*relay {
} }
var relays []*relay var relays []*relay
for line := range strings.SplitSeq(string(content), "\n") { for _, line := range strings.Split(string(content), "\n") {
if line == "" { if line == "" {
continue continue
} }
@@ -613,22 +605,12 @@ func loadRelays(file string, geoip *geoip.Provider) []*relay {
return relays return relays
} }
func saveKnownRelays(file string) error { func saveRelays(file string, relays []*relay) error {
var buf bytes.Buffer var content string
mut.RLock() for _, relay := range relays {
for _, relay := range knownRelays { content += relay.uri.String() + "\n"
fmt.Fprintln(&buf, relay.uri.String())
} }
mut.RUnlock() return os.WriteFile(file, []byte(content), 0o777)
fd, err := osutil.CreateAtomic(file)
if err != nil {
return err
}
if _, err := fd.Write(buf.Bytes()); err != nil {
return err
}
return fd.Close()
} }
func createTestCertificate() tls.Certificate { func createTestCertificate() tls.Certificate {
@@ -638,7 +620,7 @@ func createTestCertificate() tls.Certificate {
} }
certFile, keyFile := filepath.Join(tmpDir, "cert.pem"), filepath.Join(tmpDir, "key.pem") certFile, keyFile := filepath.Join(tmpDir, "cert.pem"), filepath.Join(tmpDir, "key.pem")
cert, err := tlsutil.NewCertificate(certFile, keyFile, "relaypoolsrv", 20*365, false) cert, err := tlsutil.NewCertificate(certFile, keyFile, "relaypoolsrv", 20*365)
if err != nil { if err != nil {
log.Fatalln("Failed to create test X509 key pair:", err) log.Fatalln("Failed to create test X509 key pair:", err)
} }
@@ -671,7 +653,6 @@ func getLocation(host string, geoip *geoip.Provider) location {
type loggingResponseWriter struct { type loggingResponseWriter struct {
http.ResponseWriter http.ResponseWriter
statusCode int statusCode int
} }
+4 -1
View File
@@ -13,11 +13,12 @@ import (
"net/http/httptest" "net/http/httptest"
"net/url" "net/url"
"strings" "strings"
"sync"
"testing" "testing"
) )
func init() { func init() {
for i := range 10 { for i := 0; i < 10; i++ {
u := fmt.Sprintf("permanent%d", i) u := fmt.Sprintf("permanent%d", i)
permanentRelays = append(permanentRelays, &relay{URL: u}) permanentRelays = append(permanentRelays, &relay{URL: u})
} }
@@ -27,6 +28,8 @@ func init() {
{URL: "known2"}, {URL: "known2"},
{URL: "known3"}, {URL: "known3"},
} }
mut = new(sync.RWMutex)
} }
// Regression test: handleGetRequest should not modify permanentRelays. // Regression test: handleGetRequest should not modify permanentRelays.
+7 -5
View File
@@ -6,10 +6,10 @@ import (
"encoding/json" "encoding/json"
"net" "net"
"net/http" "net/http"
"sync"
"time" "time"
"github.com/prometheus/client_golang/prometheus" "github.com/prometheus/client_golang/prometheus"
"github.com/syncthing/syncthing/lib/sync"
) )
var ( var (
@@ -104,11 +104,12 @@ func refreshStats() {
mut.RUnlock() mut.RUnlock()
now := time.Now() now := time.Now()
var wg sync.WaitGroup wg := sync.NewWaitGroup()
results := make(chan statsFetchResult, len(relays)) results := make(chan statsFetchResult, len(relays))
for _, rel := range relays { for _, rel := range relays {
wg.Go(func() { wg.Add(1)
go func(rel *relay) {
t0 := time.Now() t0 := time.Now()
stats := fetchStats(rel) stats := fetchStats(rel)
duration := time.Since(t0).Seconds() duration := time.Since(t0).Seconds()
@@ -122,7 +123,8 @@ func refreshStats() {
relay: rel, relay: rel,
stats: fetchStats(rel), stats: fetchStats(rel),
} }
}) wg.Done()
}(rel)
} }
wg.Wait() wg.Wait()
@@ -171,7 +173,7 @@ func fetchStats(relay *relay) *stats {
var stats stats var stats stats
if err := json.NewDecoder(response.Body).Decode(&stats); err != nil { if json.NewDecoder(response.Body).Decode(&stats); err != nil {
return nil return nil
} }
return &stats return &stats
+10 -13
View File
@@ -24,8 +24,7 @@ import (
"github.com/alecthomas/kong" "github.com/alecthomas/kong"
"github.com/prometheus/client_golang/prometheus/promhttp" "github.com/prometheus/client_golang/prometheus/promhttp"
"github.com/syncthing/syncthing/internal/slogutil" _ "github.com/syncthing/syncthing/lib/automaxprocs"
"github.com/syncthing/syncthing/lib/build"
"github.com/syncthing/syncthing/lib/httpcache" "github.com/syncthing/syncthing/lib/httpcache"
"github.com/syncthing/syncthing/lib/upgrade" "github.com/syncthing/syncthing/lib/upgrade"
) )
@@ -45,8 +44,6 @@ func main() {
Level: slog.LevelInfo, Level: slog.LevelInfo,
}))) })))
slog.Info(build.LongVersionFor("stupgrades"))
if err := server(&params); err != nil { if err := server(&params); err != nil {
fmt.Printf("Error: %v\n", err) fmt.Printf("Error: %v\n", err)
os.Exit(1) os.Exit(1)
@@ -61,10 +58,10 @@ func server(params *cli) error {
if err != nil { if err != nil {
return fmt.Errorf("metrics: %w", err) return fmt.Errorf("metrics: %w", err)
} }
slog.Info("Metrics listener started", slogutil.Address(params.MetricsListen)) slog.Info("Metrics listener started", "addr", params.MetricsListen)
go func() { go func() {
if err := http.Serve(metricsListen, mux); err != nil { if err := http.Serve(metricsListen, mux); err != nil {
slog.Warn("Metrics server returned", slogutil.Error(err)) slog.Warn("Metrics server returned", "error", err)
} }
}() }()
} }
@@ -78,9 +75,9 @@ func server(params *cli) error {
go func() { go func() {
for range time.NewTicker(params.CacheTime).C { for range time.NewTicker(params.CacheTime).C {
slog.Info("Refreshing cached releases", slogutil.URI(params.URL)) slog.Info("Refreshing cached releases", "url", params.URL)
if err := cache.Update(context.Background()); err != nil { if err := cache.Update(context.Background()); err != nil {
slog.Error("Failed to refresh cached releases", slogutil.URI(params.URL), slogutil.Error(err)) slog.Error("Failed to refresh cached releases", "url", params.URL, "error", err)
} }
} }
}() }()
@@ -112,7 +109,7 @@ func server(params *cli) error {
if err != nil { if err != nil {
return fmt.Errorf("listen: %w", err) return fmt.Errorf("listen: %w", err)
} }
slog.Info("Main listener started", slogutil.Address(params.Listen)) slog.Info("Main listener started", "addr", params.Listen)
return srv.Serve(srvListener) return srv.Serve(srvListener)
} }
@@ -140,7 +137,7 @@ func (p *githubReleases) serveReleases(w http.ResponseWriter, req *http.Request)
osv := req.Header.Get("Syncthing-Os-Version") osv := req.Header.Get("Syncthing-Os-Version")
if ua != "" && osv != "" { if ua != "" && osv != "" {
// We should determine the compatibility of the releases. // We should determine the compatibility of the releases.
rels = filterForCompatibility(rels, ua, osv) rels = filterForCompabitility(rels, ua, osv)
} else { } else {
metricFilterCalls.WithLabelValues("no-ua-or-osversion").Inc() metricFilterCalls.WithLabelValues("no-ua-or-osversion").Inc()
} }
@@ -188,7 +185,7 @@ func (p *proxy) ServeHTTP(w http.ResponseWriter, req *http.Request) {
w.WriteHeader(resp.StatusCode) w.WriteHeader(resp.StatusCode)
if strings.HasPrefix(ct, "application/json") { if strings.HasPrefix(ct, "application/json") {
// Special JSON handling; clean it up a bit. // Special JSON handling; clean it up a bit.
var v any var v interface{}
if err := json.NewDecoder(resp.Body).Decode(&v); err != nil { if err := json.NewDecoder(resp.Body).Decode(&v); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError) http.Error(w, err.Error(), http.StatusInternalServerError)
return return
@@ -224,9 +221,9 @@ func filterForLatest(rels []upgrade.Release) []upgrade.Release {
return filtered return filtered
} }
var userAgentOSArchExp = regexp.MustCompile(`^[Ss]yncthing.*\(.+ (\w+)-(\w+)\)$`) var userAgentOSArchExp = regexp.MustCompile(`^syncthing.*\(.+ (\w+)-(\w+)\)$`)
func filterForCompatibility(rels []upgrade.Release, ua, osv string) []upgrade.Release { func filterForCompabitility(rels []upgrade.Release, ua, osv string) []upgrade.Release {
osArch := userAgentOSArchExp.FindStringSubmatch(ua) osArch := userAgentOSArchExp.FindStringSubmatch(ua)
if len(osArch) != 3 { if len(osArch) != 3 {
metricFilterCalls.WithLabelValues("bad-os-arch").Inc() metricFilterCalls.WithLabelValues("bad-os-arch").Inc()
+1 -3
View File
@@ -13,7 +13,7 @@ import (
"github.com/alecthomas/kong" "github.com/alecthomas/kong"
"github.com/syncthing/syncthing/cmd/infra/ursrv/serve" "github.com/syncthing/syncthing/cmd/infra/ursrv/serve"
"github.com/syncthing/syncthing/lib/build" _ "github.com/syncthing/syncthing/lib/automaxprocs"
) )
type CLI struct { type CLI struct {
@@ -25,8 +25,6 @@ func main() {
Level: slog.LevelInfo, Level: slog.LevelInfo,
}))) })))
slog.Info(build.LongVersionFor("ursrv"))
var cli CLI var cli CLI
ctx := kong.Parse(&cli) ctx := kong.Parse(&cli)
if err := ctx.Run(); err != nil { if err := ctx.Run(); err != nil {
-15
View File
@@ -32,21 +32,6 @@ var (
Subsystem: "ursrv_v2", Subsystem: "ursrv_v2",
Name: "collect_seconds_last", Name: "collect_seconds_last",
}) })
metricsRecalcsTotal = promauto.NewCounter(prometheus.CounterOpts{
Namespace: "syncthing",
Subsystem: "ursrv_v2",
Name: "recalcs_total",
})
metricsRecalcSecondsTotal = promauto.NewCounter(prometheus.CounterOpts{
Namespace: "syncthing",
Subsystem: "ursrv_v2",
Name: "recalc_seconds_total",
})
metricsRecalcSecondsLast = promauto.NewGauge(prometheus.GaugeOpts{
Namespace: "syncthing",
Subsystem: "ursrv_v2",
Name: "recalc_seconds_last",
})
metricsWriteSecondsLast = promauto.NewGauge(prometheus.GaugeOpts{ metricsWriteSecondsLast = promauto.NewGauge(prometheus.GaugeOpts{
Namespace: "syncthing", Namespace: "syncthing",
Subsystem: "ursrv_v2", Subsystem: "ursrv_v2",
+31 -68
View File
@@ -7,9 +7,6 @@
package serve package serve
import ( import (
"context"
"fmt"
"log/slog"
"reflect" "reflect"
"slices" "slices"
"strconv" "strconv"
@@ -31,7 +28,7 @@ type metricsSet struct {
gaugeVecLabels map[string][]string gaugeVecLabels map[string][]string
summaries map[string]*metricSummary summaries map[string]*metricSummary
collectMut sync.RWMutex collectMut sync.Mutex
collectCutoff time.Duration collectCutoff time.Duration
} }
@@ -47,7 +44,7 @@ func newMetricsSet(srv *server) *metricsSet {
var initForType func(reflect.Type) var initForType func(reflect.Type)
initForType = func(t reflect.Type) { initForType = func(t reflect.Type) {
for i := range t.NumField() { for i := 0; i < t.NumField(); i++ {
field := t.Field(i) field := t.Field(i)
if field.Type.Kind() == reflect.Struct { if field.Type.Kind() == reflect.Struct {
initForType(field.Type) initForType(field.Type)
@@ -111,60 +108,6 @@ func nameConstLabels(name string) (string, prometheus.Labels) {
return name, m return name, m
} }
func (s *metricsSet) Serve(ctx context.Context) error {
s.recalc()
const recalcInterval = 5 * time.Minute
next := time.Until(time.Now().Truncate(recalcInterval).Add(recalcInterval))
recalcTimer := time.NewTimer(next)
defer recalcTimer.Stop()
for {
select {
case <-recalcTimer.C:
s.recalc()
next := time.Until(time.Now().Truncate(recalcInterval).Add(recalcInterval))
recalcTimer.Reset(next)
case <-ctx.Done():
return ctx.Err()
}
}
}
func (s *metricsSet) recalc() {
s.collectMut.Lock()
defer s.collectMut.Unlock()
t0 := time.Now()
defer func() {
dur := time.Since(t0)
slog.Info("Metrics recalculated", "d", dur.String())
metricsRecalcSecondsLast.Set(dur.Seconds())
metricsRecalcSecondsTotal.Add(dur.Seconds())
metricsRecalcsTotal.Inc()
}()
for _, g := range s.gauges {
g.Set(0)
}
for _, g := range s.gaugeVecs {
g.Reset()
}
for _, g := range s.summaries {
g.Reset()
}
cutoff := time.Now().Add(s.collectCutoff)
s.srv.reports.Range(func(key string, r *contract.Report) bool {
if s.collectCutoff < 0 && r.Received.Before(cutoff) {
s.srv.reports.Delete(key)
return true
}
s.addReport(r)
return true
})
}
func (s *metricsSet) addReport(r *contract.Report) { func (s *metricsSet) addReport(r *contract.Report) {
gaugeVecs := make(map[string][]string) gaugeVecs := make(map[string][]string)
s.addReportStruct(reflect.ValueOf(r).Elem(), gaugeVecs) s.addReportStruct(reflect.ValueOf(r).Elem(), gaugeVecs)
@@ -175,7 +118,7 @@ func (s *metricsSet) addReport(r *contract.Report) {
func (s *metricsSet) addReportStruct(v reflect.Value, gaugeVecs map[string][]string) { func (s *metricsSet) addReportStruct(v reflect.Value, gaugeVecs map[string][]string) {
t := v.Type() t := v.Type()
for i := range v.NumField() { for i := 0; i < v.NumField(); i++ {
field := v.Field(i) field := v.Field(i)
if field.Kind() == reflect.Struct { if field.Kind() == reflect.Struct {
s.addReportStruct(field, gaugeVecs) s.addReportStruct(field, gaugeVecs)
@@ -255,8 +198,8 @@ func (s *metricsSet) Describe(c chan<- *prometheus.Desc) {
} }
func (s *metricsSet) Collect(c chan<- prometheus.Metric) { func (s *metricsSet) Collect(c chan<- prometheus.Metric) {
s.collectMut.RLock() s.collectMut.Lock()
defer s.collectMut.RUnlock() defer s.collectMut.Unlock()
t0 := time.Now() t0 := time.Now()
defer func() { defer func() {
@@ -266,6 +209,26 @@ func (s *metricsSet) Collect(c chan<- prometheus.Metric) {
metricsCollectsTotal.Inc() metricsCollectsTotal.Inc()
}() }()
for _, g := range s.gauges {
g.Set(0)
}
for _, g := range s.gaugeVecs {
g.Reset()
}
for _, g := range s.summaries {
g.Reset()
}
cutoff := time.Now().Add(s.collectCutoff)
s.srv.reports.Range(func(key string, r *contract.Report) bool {
if s.collectCutoff < 0 && r.Received.Before(cutoff) {
s.srv.reports.Delete(key)
return true
}
s.addReport(r)
return true
})
for _, g := range s.gauges { for _, g := range s.gauges {
c <- g c <- g
} }
@@ -336,12 +299,12 @@ func (q *metricSummary) Collect(c chan<- prometheus.Metric) {
} }
slices.Sort(vs) slices.Sort(vs)
c <- prometheus.MustNewConstMetric(q.qDesc, prometheus.GaugeValue, vs[0], append(labelVals, "0")...)
pctiles := []float64{0, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 0.75, 0.9, 0.95, 0.975, 0.99, 1} c <- prometheus.MustNewConstMetric(q.qDesc, prometheus.GaugeValue, vs[len(vs)*5/100], append(labelVals, "0.05")...)
for _, pct := range pctiles { c <- prometheus.MustNewConstMetric(q.qDesc, prometheus.GaugeValue, vs[len(vs)/2], append(labelVals, "0.5")...)
idx := int(float64(len(vs)-1) * pct) c <- prometheus.MustNewConstMetric(q.qDesc, prometheus.GaugeValue, vs[len(vs)*9/10], append(labelVals, "0.9")...)
c <- prometheus.MustNewConstMetric(q.qDesc, prometheus.GaugeValue, vs[idx], append(labelVals, fmt.Sprint(pct))...) c <- prometheus.MustNewConstMetric(q.qDesc, prometheus.GaugeValue, vs[len(vs)*95/100], append(labelVals, "0.95")...)
} c <- prometheus.MustNewConstMetric(q.qDesc, prometheus.GaugeValue, vs[len(vs)-1], append(labelVals, "1")...)
} }
} }
+29 -40
View File
@@ -27,12 +27,11 @@ import (
"github.com/prometheus/client_golang/prometheus/promhttp" "github.com/prometheus/client_golang/prometheus/promhttp"
"github.com/puzpuzpuz/xsync/v3" "github.com/puzpuzpuz/xsync/v3"
"github.com/syncthing/syncthing/internal/blob" "github.com/syncthing/syncthing/internal/blob"
"github.com/syncthing/syncthing/internal/blob/azureblob"
"github.com/syncthing/syncthing/internal/blob/s3" "github.com/syncthing/syncthing/internal/blob/s3"
"github.com/syncthing/syncthing/internal/slogutil"
"github.com/syncthing/syncthing/lib/build" "github.com/syncthing/syncthing/lib/build"
"github.com/syncthing/syncthing/lib/geoip" "github.com/syncthing/syncthing/lib/geoip"
"github.com/syncthing/syncthing/lib/ur/contract" "github.com/syncthing/syncthing/lib/ur/contract"
"github.com/thejerf/suture/v4"
) )
type CLI struct { type CLI struct {
@@ -48,6 +47,10 @@ type CLI struct {
S3Bucket string `name:"s3-bucket" env:"UR_S3_BUCKET"` S3Bucket string `name:"s3-bucket" env:"UR_S3_BUCKET"`
S3AccessKeyID string `name:"s3-access-key-id" env:"UR_S3_ACCESS_KEY_ID"` S3AccessKeyID string `name:"s3-access-key-id" env:"UR_S3_ACCESS_KEY_ID"`
S3SecretKey string `name:"s3-secret-key" env:"UR_S3_SECRET_KEY"` S3SecretKey string `name:"s3-secret-key" env:"UR_S3_SECRET_KEY"`
AzureBlobAccount string `name:"azure-blob-account" env:"UR_AZUREBLOB_ACCOUNT"`
AzureBlobKey string `name:"azure-blob-key" env:"UR_AZUREBLOB_KEY"`
AzureBlobContainer string `name:"azure-blob-container" env:"UR_AZUREBLOB_CONTAINER"`
} }
var ( var (
@@ -74,8 +77,7 @@ var (
{regexp.MustCompile(`\svagrant@bullseye`), "F-Droid"}, {regexp.MustCompile(`\svagrant@bullseye`), "F-Droid"},
{regexp.MustCompile(`\svagrant@bookworm`), "F-Droid"}, {regexp.MustCompile(`\svagrant@bookworm`), "F-Droid"},
{regexp.MustCompile(`\sreproducible-build@Catfriend1-syncthing-android`), "Syncthing-Fork Catfriend1 (3rd party)"}, {regexp.MustCompile(`Anwender@NET2017`), "Syncthing-Fork (3rd party)"},
{regexp.MustCompile(`\sreproducible-build@nel0x-syncthing-android-gplay`), "Syncthing-Fork nel0x (3rd party)"},
{regexp.MustCompile(`\sbuilduser@(archlinux|svetlemodry)`), "Arch (3rd party)"}, {regexp.MustCompile(`\sbuilduser@(archlinux|svetlemodry)`), "Arch (3rd party)"},
{regexp.MustCompile(`\ssyncthing@archlinux`), "Arch (3rd party)"}, {regexp.MustCompile(`\ssyncthing@archlinux`), "Arch (3rd party)"},
@@ -102,23 +104,23 @@ func (cli *CLI) Run() error {
urListener, err := net.Listen("tcp", cli.Listen) urListener, err := net.Listen("tcp", cli.Listen)
if err != nil { if err != nil {
slog.Error("Failed to listen (usage reports)", slogutil.Error(err)) slog.Error("Failed to listen (usage reports)", "error", err)
return err return err
} }
slog.Info("Listening (usage reports)", slogutil.Address(urListener.Addr())) slog.Info("Listening (usage reports)", "address", urListener.Addr())
internalListener, err := net.Listen("tcp", cli.ListenInternal) internalListener, err := net.Listen("tcp", cli.ListenInternal)
if err != nil { if err != nil {
slog.Error("Failed to listen (internal)", slogutil.Error(err)) slog.Error("Failed to listen (internal)", "error", err)
return err return err
} }
slog.Info("Listening (internal)", slogutil.Address(internalListener.Addr())) slog.Info("Listening (internal)", "address", internalListener.Addr())
var geo *geoip.Provider var geo *geoip.Provider
if cli.GeoIPAccountID != 0 && cli.GeoIPLicenseKey != "" { if cli.GeoIPAccountID != 0 && cli.GeoIPLicenseKey != "" {
geo, err = geoip.NewGeoLite2CityProvider(context.Background(), cli.GeoIPAccountID, cli.GeoIPLicenseKey, os.TempDir()) geo, err = geoip.NewGeoLite2CityProvider(context.Background(), cli.GeoIPAccountID, cli.GeoIPLicenseKey, os.TempDir())
if err != nil { if err != nil {
slog.Error("Failed to load GeoIP", slogutil.Error(err)) slog.Error("Failed to load GeoIP", "error", err)
return err return err
} }
go geo.Serve(context.TODO()) go geo.Serve(context.TODO())
@@ -130,14 +132,20 @@ func (cli *CLI) Run() error {
if cli.S3Endpoint != "" { if cli.S3Endpoint != "" {
blobs, err = s3.NewSession(cli.S3Endpoint, cli.S3Region, cli.S3Bucket, cli.S3AccessKeyID, cli.S3SecretKey) blobs, err = s3.NewSession(cli.S3Endpoint, cli.S3Region, cli.S3Bucket, cli.S3AccessKeyID, cli.S3SecretKey)
if err != nil { if err != nil {
slog.Error("Failed to create S3 session", slogutil.Error(err)) slog.Error("Failed to create S3 session", "error", err)
return err
}
} else if cli.AzureBlobAccount != "" {
blobs, err = azureblob.NewBlobStore(cli.AzureBlobAccount, cli.AzureBlobKey, cli.AzureBlobContainer)
if err != nil {
slog.Error("Failed to create Azure blob store", "error", err)
return err return err
} }
} }
if _, err := os.Stat(cli.DumpFile); err != nil && blobs != nil { if _, err := os.Stat(cli.DumpFile); err != nil && blobs != nil {
if err := cli.downloadDumpFile(blobs); err != nil { if err := cli.downloadDumpFile(blobs); err != nil {
slog.Error("Failed to download dump file", slogutil.Error(err)) slog.Error("Failed to download dump file", "error", err)
} }
} }
@@ -159,7 +167,7 @@ func (cli *CLI) Run() error {
go func() { go func() {
for range time.Tick(cli.DumpInterval) { for range time.Tick(cli.DumpInterval) {
if err := cli.saveDumpFile(srv, blobs); err != nil { if err := cli.saveDumpFile(srv, blobs); err != nil {
slog.Error("Failed to write dump file", slogutil.Error(err)) slog.Error("Failed to write dump file", "error", err)
} }
} }
}() }()
@@ -178,12 +186,7 @@ func (cli *CLI) Run() error {
// New external metrics endpoint accepts reports from clients and serves // New external metrics endpoint accepts reports from clients and serves
// aggregated usage reporting metrics. // aggregated usage reporting metrics.
main := suture.NewSimple("main")
main.ServeBackground(context.Background())
ms := newMetricsSet(srv) ms := newMetricsSet(srv)
main.Add(ms)
reg := prometheus.NewRegistry() reg := prometheus.NewRegistry()
reg.MustRegister(ms) reg.MustRegister(ms)
@@ -194,7 +197,7 @@ func (cli *CLI) Run() error {
metricsSrv := http.Server{ metricsSrv := http.Server{
ReadTimeout: 5 * time.Second, ReadTimeout: 5 * time.Second,
WriteTimeout: 60 * time.Second, WriteTimeout: 15 * time.Second,
Handler: mux, Handler: mux,
} }
@@ -223,11 +226,6 @@ func (cli *CLI) downloadDumpFile(blobs blob.Store) error {
} }
func (cli *CLI) saveDumpFile(srv *server, blobs blob.Store) error { func (cli *CLI) saveDumpFile(srv *server, blobs blob.Store) error {
t0 := time.Now()
defer func() {
metricsWriteSecondsLast.Set(float64(time.Since(t0)))
}()
fd, err := os.Create(cli.DumpFile + ".tmp") fd, err := os.Create(cli.DumpFile + ".tmp")
if err != nil { if err != nil {
return fmt.Errorf("creating dump file: %w", err) return fmt.Errorf("creating dump file: %w", err)
@@ -246,10 +244,9 @@ func (cli *CLI) saveDumpFile(srv *server, blobs blob.Store) error {
if err := os.Rename(cli.DumpFile+".tmp", cli.DumpFile); err != nil { if err := os.Rename(cli.DumpFile+".tmp", cli.DumpFile); err != nil {
return fmt.Errorf("renaming dump file: %w", err) return fmt.Errorf("renaming dump file: %w", err)
} }
slog.Info("Dump file saved", "d", time.Since(t0).String()) slog.Info("Dump file saved")
if blobs != nil { if blobs != nil {
t1 := time.Now()
key := fmt.Sprintf("reports-%s.jsons.gz", time.Now().UTC().Format("2006-01-02")) key := fmt.Sprintf("reports-%s.jsons.gz", time.Now().UTC().Format("2006-01-02"))
fd, err := os.Open(cli.DumpFile) fd, err := os.Open(cli.DumpFile)
if err != nil { if err != nil {
@@ -259,7 +256,7 @@ func (cli *CLI) saveDumpFile(srv *server, blobs blob.Store) error {
return fmt.Errorf("uploading dump file: %w", err) return fmt.Errorf("uploading dump file: %w", err)
} }
_ = fd.Close() _ = fd.Close()
slog.Info("Dump file uploaded", "d", time.Since(t1).String()) slog.Info("Dump file uploaded")
} }
return nil return nil
@@ -310,8 +307,8 @@ func (s *server) handleNewData(w http.ResponseWriter, r *http.Request) {
lr := &io.LimitedReader{R: r.Body, N: 40 * 1024} lr := &io.LimitedReader{R: r.Body, N: 40 * 1024}
bs, _ := io.ReadAll(lr) bs, _ := io.ReadAll(lr)
if err := json.Unmarshal(bs, &rep); err != nil { if err := json.Unmarshal(bs, &rep); err != nil {
log.Error("Failed to decode JSON", slogutil.Error(err)) log.Error("Failed to decode JSON", "error", err)
http.Error(w, "JSON Decode Error", http.StatusBadRequest) http.Error(w, "JSON Decode Error", http.StatusInternalServerError)
return return
} }
@@ -320,8 +317,8 @@ func (s *server) handleNewData(w http.ResponseWriter, r *http.Request) {
rep.Address = addr rep.Address = addr
if err := rep.Validate(); err != nil { if err := rep.Validate(); err != nil {
log.Error("Failed to validate report", slogutil.Error(err)) log.Error("Failed to validate report", "error", err)
http.Error(w, "Validation Error", http.StatusBadRequest) http.Error(w, "Validation Error", http.StatusInternalServerError)
return return
} }
@@ -371,13 +368,6 @@ func (s *server) addReport(rep *contract.Report) bool {
rep.DistOS = rep.OS rep.DistOS = rep.OS
rep.DistArch = rep.Arch rep.DistArch = rep.Arch
if strings.HasPrefix(rep.Version, "v2.") {
rep.Database.ModernCSQLite = strings.Contains(rep.LongVersion, "modernc-sqlite")
rep.Database.MattnSQLite = !rep.Database.ModernCSQLite
} else {
rep.Database.LevelDB = true
}
_, loaded := s.reports.LoadAndStore(rep.UniqueID, rep) _, loaded := s.reports.LoadAndStore(rep.UniqueID, rep)
return loaded return loaded
} }
@@ -397,7 +387,6 @@ func (s *server) save(w io.Writer) error {
} }
func (s *server) load(r io.Reader) { func (s *server) load(r io.Reader) {
t0 := time.Now()
dec := json.NewDecoder(r) dec := json.NewDecoder(r)
s.reports.Clear() s.reports.Clear()
for { for {
@@ -405,12 +394,12 @@ func (s *server) load(r io.Reader) {
if err := dec.Decode(&rep); errors.Is(err, io.EOF) { if err := dec.Decode(&rep); errors.Is(err, io.EOF) {
break break
} else if err != nil { } else if err != nil {
slog.Error("Failed to load record", slogutil.Error(err)) slog.Error("Failed to load record", "error", err)
break break
} }
s.addReport(&rep) s.addReport(&rep)
} }
slog.Info("Loaded reports", "count", s.reports.Size(), "d", time.Since(t0).String()) slog.Info("Loaded reports", "count", s.reports.Size())
} }
var ( var (
+2 -3
View File
@@ -10,7 +10,7 @@ import (
"context" "context"
"fmt" "fmt"
"io" "io"
"log/slog" "log"
amqp "github.com/rabbitmq/amqp091-go" amqp "github.com/rabbitmq/amqp091-go"
"github.com/thejerf/suture/v4" "github.com/thejerf/suture/v4"
@@ -23,7 +23,6 @@ import (
type amqpReplicator struct { type amqpReplicator struct {
suture.Service suture.Service
broker string broker string
sender *amqpSender sender *amqpSender
receiver *amqpReceiver receiver *amqpReceiver
@@ -173,7 +172,7 @@ func (s *amqpReceiver) Serve(ctx context.Context) error {
id, err = protocol.DeviceIDFromString(string(rec.Key)) id, err = protocol.DeviceIDFromString(string(rec.Key))
} }
if err != nil { if err != nil {
slog.Warn("Failed to parse replication device ID", "error", err) log.Println("Replication device ID:", err)
replicationRecvsTotal.WithLabelValues("error").Inc() replicationRecvsTotal.WithLabelValues("error").Inc()
continue continue
} }
+36 -37
View File
@@ -18,7 +18,6 @@ import (
"fmt" "fmt"
io "io" io "io"
"log" "log"
"log/slog"
"math/rand" "math/rand"
"net" "net"
"net/http" "net/http"
@@ -67,7 +66,7 @@ type contextKey int
const idKey contextKey = iota const idKey contextKey = iota
func newAPISrv(addr string, cert tls.Certificate, db database, repl replicator, useHTTP, compression bool, desiredUnseenNotFoundRate, desiredSeenNotFoundRate float64) *apiSrv { func newAPISrv(addr string, cert tls.Certificate, db database, repl replicator, useHTTP, compression bool, desiredNotFoundRate float64) *apiSrv {
return &apiSrv{ return &apiSrv{
addr: addr, addr: addr,
cert: cert, cert: cert,
@@ -78,13 +77,13 @@ func newAPISrv(addr string, cert tls.Certificate, db database, repl replicator,
seenTracker: &retryAfterTracker{ seenTracker: &retryAfterTracker{
name: "seenTracker", name: "seenTracker",
bucketStarts: time.Now(), bucketStarts: time.Now(),
desiredRate: desiredSeenNotFoundRate, desiredRate: desiredNotFoundRate / 2,
currentDelay: notFoundRetryUnknownMinSeconds, currentDelay: notFoundRetryUnknownMinSeconds,
}, },
notSeenTracker: &retryAfterTracker{ notSeenTracker: &retryAfterTracker{
name: "notSeenTracker", name: "notSeenTracker",
bucketStarts: time.Now(), bucketStarts: time.Now(),
desiredRate: desiredUnseenNotFoundRate, desiredRate: desiredNotFoundRate / 2,
currentDelay: notFoundRetryUnknownMaxSeconds / 2, currentDelay: notFoundRetryUnknownMaxSeconds / 2,
}, },
} }
@@ -94,7 +93,7 @@ func (s *apiSrv) Serve(ctx context.Context) error {
if s.useHTTP { if s.useHTTP {
listener, err := net.Listen("tcp", s.addr) listener, err := net.Listen("tcp", s.addr)
if err != nil { if err != nil {
slog.ErrorContext(ctx, "Failed to listen", "error", err) log.Println("Listen:", err)
return err return err
} }
s.listener = listener s.listener = listener
@@ -108,7 +107,7 @@ func (s *apiSrv) Serve(ctx context.Context) error {
tlsListener, err := tls.Listen("tcp", s.addr, tlsCfg) tlsListener, err := tls.Listen("tcp", s.addr, tlsCfg)
if err != nil { if err != nil {
slog.ErrorContext(ctx, "Failed to listen", "error", err) log.Println("Listen:", err)
return err return err
} }
s.listener = tlsListener s.listener = tlsListener
@@ -133,7 +132,7 @@ func (s *apiSrv) Serve(ctx context.Context) error {
err := srv.Serve(s.listener) err := srv.Serve(s.listener)
if err != nil { if err != nil {
slog.ErrorContext(ctx, "Failed to serve", "error", err) log.Println("Serve:", err)
} }
return err return err
} }
@@ -152,7 +151,9 @@ func (s *apiSrv) handler(w http.ResponseWriter, req *http.Request) {
reqID := requestID(rand.Int63()) reqID := requestID(rand.Int63())
req = req.WithContext(context.WithValue(req.Context(), idKey, reqID)) req = req.WithContext(context.WithValue(req.Context(), idKey, reqID))
slog.Debug("Handling request", "id", reqID, "method", req.Method, "url", req.URL, "proto", req.Proto) if debug {
log.Println(reqID, req.Method, req.URL, req.Proto)
}
remoteAddr := &net.TCPAddr{ remoteAddr := &net.TCPAddr{
IP: nil, IP: nil,
@@ -173,7 +174,7 @@ func (s *apiSrv) handler(w http.ResponseWriter, req *http.Request) {
var err error var err error
remoteAddr, err = net.ResolveTCPAddr("tcp", req.RemoteAddr) remoteAddr, err = net.ResolveTCPAddr("tcp", req.RemoteAddr)
if err != nil { if err != nil {
slog.Warn("Failed to resolve remote address", "address", req.RemoteAddr, "error", err) log.Println("remoteAddr:", err)
lw.Header().Set("Retry-After", errorRetryAfterString()) lw.Header().Set("Retry-After", errorRetryAfterString())
http.Error(lw, "Internal Server Error", http.StatusInternalServerError) http.Error(lw, "Internal Server Error", http.StatusInternalServerError)
apiRequestsTotal.WithLabelValues("no_remote_addr").Inc() apiRequestsTotal.WithLabelValues("no_remote_addr").Inc()
@@ -196,7 +197,9 @@ func (s *apiSrv) handleGET(w http.ResponseWriter, req *http.Request) {
deviceID, err := protocol.DeviceIDFromString(req.URL.Query().Get("device")) deviceID, err := protocol.DeviceIDFromString(req.URL.Query().Get("device"))
if err != nil { if err != nil {
slog.Debug("Request with bad device param", "id", reqID, "error", err) if debug {
log.Println(reqID, "bad device param:", err)
}
lookupRequestsTotal.WithLabelValues("bad_request").Inc() lookupRequestsTotal.WithLabelValues("bad_request").Inc()
w.Header().Set("Retry-After", errorRetryAfterString()) w.Header().Set("Retry-After", errorRetryAfterString())
http.Error(w, "Bad Request", http.StatusBadRequest) http.Error(w, "Bad Request", http.StatusBadRequest)
@@ -206,7 +209,6 @@ func (s *apiSrv) handleGET(w http.ResponseWriter, req *http.Request) {
rec, err := s.db.get(&deviceID) rec, err := s.db.get(&deviceID)
if err != nil { if err != nil {
// some sort of internal error // some sort of internal error
slog.Warn("Failed to handle GET request", "id", reqID, "error", err)
lookupRequestsTotal.WithLabelValues("internal_error").Inc() lookupRequestsTotal.WithLabelValues("internal_error").Inc()
w.Header().Set("Retry-After", errorRetryAfterString()) w.Header().Set("Retry-After", errorRetryAfterString())
http.Error(w, "Internal Server Error", http.StatusInternalServerError) http.Error(w, "Internal Server Error", http.StatusInternalServerError)
@@ -255,9 +257,11 @@ func (s *apiSrv) handleGET(w http.ResponseWriter, req *http.Request) {
func (s *apiSrv) handlePOST(remoteAddr *net.TCPAddr, w http.ResponseWriter, req *http.Request) { func (s *apiSrv) handlePOST(remoteAddr *net.TCPAddr, w http.ResponseWriter, req *http.Request) {
reqID := req.Context().Value(idKey).(requestID) reqID := req.Context().Value(idKey).(requestID)
rawCert, err := s.certificateBytes(req) rawCert, err := certificateBytes(req)
if err != nil { if err != nil {
slog.Debug("Request without certificates", "id", reqID, "error", err) if debug {
log.Println(reqID, "no certificates:", err)
}
announceRequestsTotal.WithLabelValues("no_certificate").Inc() announceRequestsTotal.WithLabelValues("no_certificate").Inc()
w.Header().Set("Retry-After", errorRetryAfterString()) w.Header().Set("Retry-After", errorRetryAfterString())
http.Error(w, "Forbidden", http.StatusForbidden) http.Error(w, "Forbidden", http.StatusForbidden)
@@ -266,7 +270,9 @@ func (s *apiSrv) handlePOST(remoteAddr *net.TCPAddr, w http.ResponseWriter, req
var ann announcement var ann announcement
if err := json.NewDecoder(req.Body).Decode(&ann); err != nil { if err := json.NewDecoder(req.Body).Decode(&ann); err != nil {
slog.Debug("Failed to decode request", "id", reqID, "error", err) if debug {
log.Println(reqID, "decode:", err)
}
announceRequestsTotal.WithLabelValues("bad_request").Inc() announceRequestsTotal.WithLabelValues("bad_request").Inc()
w.Header().Set("Retry-After", errorRetryAfterString()) w.Header().Set("Retry-After", errorRetryAfterString())
http.Error(w, "Bad Request", http.StatusBadRequest) http.Error(w, "Bad Request", http.StatusBadRequest)
@@ -277,7 +283,9 @@ func (s *apiSrv) handlePOST(remoteAddr *net.TCPAddr, w http.ResponseWriter, req
addresses := fixupAddresses(remoteAddr, ann.Addresses) addresses := fixupAddresses(remoteAddr, ann.Addresses)
if len(addresses) == 0 { if len(addresses) == 0 {
slog.Debug("Request without addresses", "id", reqID, "error", err) if debug {
log.Println(reqID, "no addresses")
}
announceRequestsTotal.WithLabelValues("bad_request").Inc() announceRequestsTotal.WithLabelValues("bad_request").Inc()
w.Header().Set("Retry-After", errorRetryAfterString()) w.Header().Set("Retry-After", errorRetryAfterString())
http.Error(w, "Bad Request", http.StatusBadRequest) http.Error(w, "Bad Request", http.StatusBadRequest)
@@ -285,7 +293,9 @@ func (s *apiSrv) handlePOST(remoteAddr *net.TCPAddr, w http.ResponseWriter, req
} }
if err := s.handleAnnounce(deviceID, addresses); err != nil { if err := s.handleAnnounce(deviceID, addresses); err != nil {
slog.Warn("Failed to handle POST request", "id", reqID, "error", err) if debug {
log.Println(reqID, "handle:", err)
}
announceRequestsTotal.WithLabelValues("internal_error").Inc() announceRequestsTotal.WithLabelValues("internal_error").Inc()
w.Header().Set("Retry-After", errorRetryAfterString()) w.Header().Set("Retry-After", errorRetryAfterString())
http.Error(w, "Internal Server Error", http.StatusInternalServerError) http.Error(w, "Internal Server Error", http.StatusInternalServerError)
@@ -296,7 +306,9 @@ func (s *apiSrv) handlePOST(remoteAddr *net.TCPAddr, w http.ResponseWriter, req
w.Header().Set("Reannounce-After", reannounceAfterString()) w.Header().Set("Reannounce-After", reannounceAfterString())
w.WriteHeader(http.StatusNoContent) w.WriteHeader(http.StatusNoContent)
slog.Debug("Device announced", "id", reqID, "device", deviceID, "addresses", addresses) if debug {
log.Println(reqID, "announced", deviceID, addresses)
}
} }
func (s *apiSrv) Stop() { func (s *apiSrv) Stop() {
@@ -331,17 +343,14 @@ func handlePing(w http.ResponseWriter, _ *http.Request) {
w.WriteHeader(http.StatusNoContent) w.WriteHeader(http.StatusNoContent)
} }
func (s *apiSrv) certificateBytes(req *http.Request) ([]byte, error) { func certificateBytes(req *http.Request) ([]byte, error) {
if req.TLS != nil && len(req.TLS.PeerCertificates) > 0 { if req.TLS != nil && len(req.TLS.PeerCertificates) > 0 {
return req.TLS.PeerCertificates[0].Raw, nil return req.TLS.PeerCertificates[0].Raw, nil
} }
if !s.useHTTP {
return nil, errors.New("no certificate presented")
}
var bs []byte var bs []byte
if hdr := req.Header.Get("X-Ssl-Cert"); hdr != "" { if hdr := req.Header.Get("X-SSL-Cert"); hdr != "" {
if strings.Contains(hdr, "%") { if strings.Contains(hdr, "%") {
// Nginx using $ssl_client_escaped_cert // Nginx using $ssl_client_escaped_cert
// The certificate is in PEM format with url encoding. // The certificate is in PEM format with url encoding.
@@ -403,7 +412,10 @@ func (s *apiSrv) certificateBytes(req *http.Request) ([]byte, error) {
b.WriteByte('\n') b.WriteByte('\n')
for i := 0; i < len(cert); i += 64 { for i := 0; i < len(cert); i += 64 {
end := min(i+64, len(cert)) end := i + 64
if end > len(cert) {
end = len(cert)
}
b.WriteString(cert[i:end]) b.WriteString(cert[i:end])
b.WriteByte('\n') b.WriteByte('\n')
} }
@@ -501,7 +513,6 @@ func fixupAddresses(remote *net.TCPAddr, addresses []string) []string {
type loggingResponseWriter struct { type loggingResponseWriter struct {
http.ResponseWriter http.ResponseWriter
statusCode int statusCode int
} }
@@ -565,17 +576,5 @@ func (t *retryAfterTracker) retryAfterS() int {
} }
t.curCount++ t.curCount++
t.mut.Unlock() t.mut.Unlock()
return t.currentDelay + rand.Intn(t.currentDelay/4)
// Skewed normal distribution with the mean at currentDelay and the
// limits (50% and 150%) at 3 standard deviations
nf := rand.NormFloat64()
minD := max(notFoundRetryUnknownMinSeconds, t.currentDelay/2)
maxD := min(notFoundRetryUnknownMaxSeconds, t.currentDelay*3/2)
intv := float64(maxD - t.currentDelay)
if nf < 0 {
intv = float64(t.currentDelay - minD)
}
nf = min(max(nf*intv/3+float64(t.currentDelay), notFoundRetryUnknownMinSeconds), notFoundRetryUnknownMaxSeconds)
return int(nf)
} }
+5 -48
View File
@@ -7,6 +7,7 @@
package main package main
import ( import (
"context"
"crypto/tls" "crypto/tls"
"fmt" "fmt"
"io" "io"
@@ -17,7 +18,6 @@ import (
"regexp" "regexp"
"strings" "strings"
"testing" "testing"
"time"
"github.com/syncthing/syncthing/lib/protocol" "github.com/syncthing/syncthing/lib/protocol"
"github.com/syncthing/syncthing/lib/tlsutil" "github.com/syncthing/syncthing/lib/tlsutil"
@@ -106,59 +106,16 @@ func addr(host string, port int) *net.TCPAddr {
} }
} }
func TestRetryAfterSHistogram(t *testing.T) {
tracker := &retryAfterTracker{
name: "test",
bucketStarts: time.Now(),
desiredRate: 100,
currentDelay: 1800,
}
const n = 1000
bucketSize := 60 // seconds per histogram bucket
numBuckets := (notFoundRetryUnknownMaxSeconds + bucketSize - 1) / bucketSize
buckets := make([]int, numBuckets)
for range n {
v := tracker.retryAfterS()
if v < notFoundRetryUnknownMinSeconds || v > notFoundRetryUnknownMaxSeconds {
t.Fatalf("retryAfterS() = %d, out of range [%d, %d]", v, notFoundRetryUnknownMinSeconds, notFoundRetryUnknownMaxSeconds)
}
b := (v - 1) / bucketSize
if b >= numBuckets {
b = numBuckets - 1
}
buckets[b]++
}
// Print a horizontal histogram
maxCount := 0
for _, c := range buckets {
if c > maxCount {
maxCount = c
}
}
barWidth := 60
for i, c := range buckets {
lo := i*bucketSize + 1
hi := min((i+1)*bucketSize, notFoundRetryUnknownMaxSeconds)
bar := ""
if maxCount > 0 {
bar = strings.Repeat("#", c*barWidth/maxCount)
}
t.Logf("%4d-%4ds | %-*s %d", lo, hi, barWidth, bar, c)
}
}
func BenchmarkAPIRequests(b *testing.B) { func BenchmarkAPIRequests(b *testing.B) {
db := newInMemoryStore(b.TempDir(), 0, nil) db := newInMemoryStore(b.TempDir(), 0, nil)
ctx := b.Context() ctx, cancel := context.WithCancel(context.Background())
defer cancel()
go db.Serve(ctx) go db.Serve(ctx)
api := newAPISrv("127.0.0.1:0", tls.Certificate{}, db, nil, true, true, 1000, 1000) api := newAPISrv("127.0.0.1:0", tls.Certificate{}, db, nil, true, true, 1000)
srv := httptest.NewServer(http.HandlerFunc(api.handler)) srv := httptest.NewServer(http.HandlerFunc(api.handler))
kf := b.TempDir() + "/cert" kf := b.TempDir() + "/cert"
crt, err := tlsutil.NewCertificate(kf+".crt", kf+".key", "localhost", 7, true) crt, err := tlsutil.NewCertificate(kf+".crt", kf+".key", "localhost", 7)
if err != nil { if err != nil {
b.Fatal(err) b.Fatal(err)
} }
+46 -66
View File
@@ -13,9 +13,9 @@ import (
"encoding/binary" "encoding/binary"
"errors" "errors"
"io" "io"
"log/slog" "log"
"os" "os"
"path/filepath" "path"
"runtime" "runtime"
"slices" "slices"
"strings" "strings"
@@ -74,24 +74,24 @@ func newInMemoryStore(dir string, flushInterval time.Duration, blobs blob.Store)
// Try to read from blob storage // Try to read from blob storage
latestKey, cerr := blobs.LatestKey(context.Background()) latestKey, cerr := blobs.LatestKey(context.Background())
if cerr != nil { if cerr != nil {
slog.Error("Failed to find database in blob storage", "error", cerr) log.Println("Error finding database from blob storage:", cerr)
return s 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 { if cerr != nil {
slog.Error("Failed to create database file", "error", cerr) log.Println("Error creating database file:", cerr)
return s return s
} }
if cerr := blobs.Download(context.Background(), latestKey, fd); cerr != nil { if cerr := blobs.Download(context.Background(), latestKey, fd); cerr != nil {
slog.Error("Failed to download database from blob storage", "error", cerr) log.Printf("Error downloading database from blob storage: %v", cerr)
} }
_ = fd.Close() _ = fd.Close()
nr, err = s.read() nr, err = s.read()
} }
if err != nil { if err != nil {
slog.Error("Failed to read database", "error", err) log.Println("Error reading database:", err)
} }
slog.Info("Loaded database", "records", nr) log.Printf("Read %d records from database", nr)
s.expireAndCalculateStatistics() s.expireAndCalculateStatistics()
return s return s
} }
@@ -113,7 +113,7 @@ func (s *inMemoryStore) merge(key *protocol.DeviceID, addrs []*discosrv.Database
} }
if oldRec, ok := s.m.Load(*key); ok { if oldRec, ok := s.m.Load(*key); ok {
newRec = merge(newRec, oldRec) newRec = merge(oldRec, newRec)
} }
s.m.Store(*key, newRec) s.m.Store(*key, newRec)
@@ -135,13 +135,7 @@ func (s *inMemoryStore) get(key *protocol.DeviceID) (*discosrv.DatabaseRecord, e
return &discosrv.DatabaseRecord{}, nil return &discosrv.DatabaseRecord{}, nil
} }
naddresses, changed := expire(rec.Addresses, s.clock.Now()) rec.Addresses = expire(rec.Addresses, s.clock.Now())
if changed {
rec = &discosrv.DatabaseRecord{
Addresses: naddresses,
Seen: rec.Seen,
}
}
databaseOperations.WithLabelValues(dbOpGet, dbResSuccess).Inc() databaseOperations.WithLabelValues(dbOpGet, dbResSuccess).Inc()
return rec, nil return rec, nil
} }
@@ -159,13 +153,13 @@ loop:
for { for {
select { select {
case <-t.C: case <-t.C:
slog.InfoContext(ctx, "Calculating statistics") log.Println("Calculating statistics")
s.expireAndCalculateStatistics() s.expireAndCalculateStatistics()
slog.InfoContext(ctx, "Flushing database") log.Println("Flushing database")
if err := s.write(); err != nil { if err := s.write(); err != nil {
slog.ErrorContext(ctx, "Failed to write database", "error", err) log.Println("Error writing database:", err)
} }
slog.InfoContext(ctx, "Finished flushing database") log.Println("Finished flushing database")
t.Reset(s.flushInterval) t.Reset(s.flushInterval)
case <-ctx.Done(): case <-ctx.Done():
@@ -190,12 +184,12 @@ func (s *inMemoryStore) expireAndCalculateStatistics() {
} }
n++ n++
addresses, changed := expire(rec.Addresses, now) addresses := expire(rec.Addresses, now)
if changed { if len(addresses) == 0 {
rec = &discosrv.DatabaseRecord{ rec.Addresses = nil
Addresses: addresses, s.m.Store(key, rec)
Seen: rec.Seen, } else if len(addresses) != len(rec.Addresses) {
} rec.Addresses = addresses
s.m.Store(key, rec) s.m.Store(key, rec)
} }
@@ -257,12 +251,12 @@ 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") fd, err := os.Create(dbf + ".tmp")
if err != nil { if err != nil {
return err return err
} }
bw := bufio.NewWriterSize(fd, 1<<20) bw := bufio.NewWriter(fd)
var buf []byte var buf []byte
var rangeErr error var rangeErr error
@@ -306,7 +300,7 @@ func (s *inMemoryStore) write() (err error) {
} }
if err := bw.Flush(); err != nil { if err := bw.Flush(); err != nil {
_ = fd.Close() _ = fd.Close
return err return err
} }
if err := fd.Close(); err != nil { if err := fd.Close(); err != nil {
@@ -316,31 +310,25 @@ func (s *inMemoryStore) write() (err error) {
return err return err
} }
if info, err := os.Lstat(dbf); err == nil {
slog.Info("Saved database", "name", dbf, "size", info.Size(), "modtime", info.ModTime())
} else {
slog.Warn("Failed to stat database after save", "error", err)
}
// Upload to blob storage // Upload to blob storage
if s.blobs != nil { if s.blobs != nil {
fd, err = os.Open(dbf) fd, err = os.Open(dbf)
if err != nil { if err != nil {
slog.Error("Failed to upload database to blob storage", "error", err) log.Printf("Error uploading database to blob storage: %v", err)
return nil return nil
} }
defer fd.Close() defer fd.Close()
if err := s.blobs.Upload(context.Background(), s.objKey, fd); err != nil { if err := s.blobs.Upload(context.Background(), s.objKey, fd); err != nil {
slog.Error("Failed to upload database to blob storage", "error", err) log.Printf("Error uploading database to blob storage: %v", err)
} }
slog.Info("Finished uploading database") log.Println("Finished uploading database")
} }
return nil return nil
} }
func (s *inMemoryStore) read() (int, 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 { if err != nil {
return 0, err return 0, err
} }
@@ -372,14 +360,14 @@ func (s *inMemoryStore) read() (int, error) {
key, err = protocol.DeviceIDFromString(string(rec.Key)) key, err = protocol.DeviceIDFromString(string(rec.Key))
} }
if err != nil { if err != nil {
slog.Error("Got bad device ID while reading database", "error", err) log.Println("Bad device ID:", err)
continue continue
} }
slices.SortFunc(rec.Addresses, Cmp) slices.SortFunc(rec.Addresses, Cmp)
rec.Addresses, _ = expire(slices.CompactFunc(rec.Addresses, Equal), s.clock.Now()) rec.Addresses = slices.CompactFunc(rec.Addresses, Equal)
s.m.Store(key, &discosrv.DatabaseRecord{ s.m.Store(key, &discosrv.DatabaseRecord{
Addresses: rec.Addresses, Addresses: expire(rec.Addresses, s.clock.Now()),
Seen: rec.Seen, Seen: rec.Seen,
}) })
nr++ nr++
@@ -390,7 +378,7 @@ func (s *inMemoryStore) read() (int, error) {
// merge returns the merged result of the two database records a and b. The // merge returns the merged result of the two database records a and b. The
// result is the union of the two address sets, with the newer expiry time // result is the union of the two address sets, with the newer expiry time
// chosen for any duplicates. The address list in a is overwritten and // chosen for any duplicates. The address list in a is overwritten and
// reused for the result; b is not modified. // reused for the result.
func merge(a, b *discosrv.DatabaseRecord) *discosrv.DatabaseRecord { func merge(a, b *discosrv.DatabaseRecord) *discosrv.DatabaseRecord {
// Both lists must be sorted for this to work. // Both lists must be sorted for this to work.
@@ -421,33 +409,25 @@ func merge(a, b *discosrv.DatabaseRecord) *discosrv.DatabaseRecord {
return a return a
} }
// expire returns the list of addresses after removing expired entries. A // expire returns the list of addresses after removing expired entries.
// new slice is allocated if any changes are required, and the changed // Expiration happen in place, so the slice given as the parameter is
// boolean indicates whether that happened or not. // destroyed. Internal order is preserved.
func expire(addrs []*discosrv.DatabaseAddress, now time.Time) (result []*discosrv.DatabaseAddress, changed bool) { func expire(addrs []*discosrv.DatabaseAddress, now time.Time) []*discosrv.DatabaseAddress {
cutoff := now.UnixNano() cutoff := now.UnixNano()
remains := 0 naddrs := addrs[:0]
for _, a := range addrs { for i := range addrs {
if a.Expires < cutoff { if i > 0 && addrs[i].Address == addrs[i-1].Address {
changed = true // Skip duplicates
} else { continue
remains++ }
if addrs[i].Expires >= cutoff {
naddrs = append(naddrs, addrs[i])
} }
} }
if !changed { if len(naddrs) == 0 {
return addrs, false return nil
} }
if remains == 0 { return naddrs
return nil, true
}
naddrs := make([]*discosrv.DatabaseAddress, 0, remains)
for _, a := range addrs {
if a.Expires >= cutoff {
naddrs = append(naddrs, a)
}
}
return naddrs, true
} }
func Cmp(d, other *discosrv.DatabaseAddress) (n int) { func Cmp(d, other *discosrv.DatabaseAddress) (n int) {
+1 -1
View File
@@ -161,7 +161,7 @@ func TestFilter(t *testing.T) {
} }
for _, tc := range cases { for _, tc := range cases {
res, _ := expire(tc.a, time.Unix(0, 10)) res := expire(tc.a, time.Unix(0, 10))
if fmt.Sprint(res) != fmt.Sprint(tc.b) { if fmt.Sprint(res) != fmt.Sprint(tc.b) {
t.Errorf("Incorrect result %v, expected %v", res, tc.b) t.Errorf("Incorrect result %v, expected %v", res, tc.b)
} }
+39 -42
View File
@@ -9,12 +9,12 @@ package main
import ( import (
"context" "context"
"crypto/tls" "crypto/tls"
"fmt" "log"
"log/slog"
"net/http" "net/http"
_ "net/http/pprof"
"os" "os"
"os/signal" "os/signal"
"syscall" "runtime"
"time" "time"
"github.com/alecthomas/kong" "github.com/alecthomas/kong"
@@ -22,8 +22,9 @@ import (
"github.com/thejerf/suture/v4" "github.com/thejerf/suture/v4"
"github.com/syncthing/syncthing/internal/blob" "github.com/syncthing/syncthing/internal/blob"
"github.com/syncthing/syncthing/internal/blob/azureblob"
"github.com/syncthing/syncthing/internal/blob/s3" "github.com/syncthing/syncthing/internal/blob/s3"
"github.com/syncthing/syncthing/internal/slogutil" _ "github.com/syncthing/syncthing/lib/automaxprocs"
"github.com/syncthing/syncthing/lib/build" "github.com/syncthing/syncthing/lib/build"
"github.com/syncthing/syncthing/lib/protocol" "github.com/syncthing/syncthing/lib/protocol"
"github.com/syncthing/syncthing/lib/rand" "github.com/syncthing/syncthing/lib/rand"
@@ -31,14 +32,15 @@ import (
) )
const ( const (
addressExpiryTime = 2 * time.Hour addressExpiryTime = 2 * time.Hour
databaseStatisticsInterval = 5 * time.Minute
// Reannounce-After is set to reannounceAfterSeconds + // Reannounce-After is set to reannounceAfterSeconds +
// random(reannounzeFuzzSeconds), similar for Retry-After // random(reannounzeFuzzSeconds), similar for Retry-After
reannounceAfterSeconds = 2400 reannounceAfterSeconds = 3300
reannounzeFuzzSeconds = 1800 reannounzeFuzzSeconds = 300
errorRetryAfterSeconds = 1800 errorRetryAfterSeconds = 1500
errorRetryFuzzSeconds = 900 errorRetryFuzzSeconds = 300
// Retry for not found is notFoundRetrySeenSeconds for records we have // Retry for not found is notFoundRetrySeenSeconds for records we have
// seen an announcement for (but it's not active right now) and // seen an announcement for (but it's not active right now) and
@@ -58,16 +60,13 @@ const (
var debug = false var debug = false
type CLI struct { type CLI struct {
Cert string `group:"Listen" help:"Certificate file" default:"./cert.pem" env:"DISCOVERY_CERT_FILE"` Cert string `group:"Listen" help:"Certificate file" default:"./cert.pem" env:"DISCOVERY_CERT_FILE"`
Key string `group:"Listen" help:"Key file" default:"./key.pem" env:"DISCOVERY_KEY_FILE"` Key string `group:"Listen" help:"Key file" default:"./key.pem" env:"DISCOVERY_KEY_FILE"`
HTTP bool `group:"Listen" help:"Listen on HTTP (behind an HTTPS proxy)" env:"DISCOVERY_HTTP"` HTTP bool `group:"Listen" help:"Listen on HTTP (behind an HTTPS proxy)" env:"DISCOVERY_HTTP"`
Compression bool `group:"Listen" help:"Enable GZIP compression of responses" env:"DISCOVERY_COMPRESSION"` Compression bool `group:"Listen" help:"Enable GZIP compression of responses" env:"DISCOVERY_COMPRESSION"`
Listen string `group:"Listen" help:"Listen address" default:":8443" env:"DISCOVERY_LISTEN"` Listen string `group:"Listen" help:"Listen address" default:":8443" env:"DISCOVERY_LISTEN"`
MetricsListen string `group:"Listen" help:"Metrics listen address" env:"DISCOVERY_METRICS_LISTEN"` MetricsListen string `group:"Listen" help:"Metrics listen address" env:"DISCOVERY_METRICS_LISTEN"`
DesiredUnseenNotFoundRate float64 `group:"Listen" help:"Desired maximum rate of not-found replies for never seen devices (/s)" default:"1000" env:"DISCOVERY_UNSEEN_RATE"` DesiredNotFoundRate float64 `group:"Listen" help:"Desired maximum rate of not-found replies (/s)" default:"1000"`
DesiredSeenNotFoundRate float64 `group:"Listen" help:"Desired maximum rate of not-found replies for previously seen devices (/s)" default:"1000" env:"DISCOVERY_SEEN_RATE"`
ShutdownDelay float64 `help:"Time to wait before shutdown after receiving a shutdown signal (s)" env:"DISCOVERY_SHUTDOWN_DELAY"`
DBDir string `group:"Database" help:"Database directory" default:"." env:"DISCOVERY_DB_DIR"` DBDir string `group:"Database" help:"Database directory" default:"." env:"DISCOVERY_DB_DIR"`
DBFlushInterval time.Duration `group:"Database" help:"Interval between database flushes" default:"5m" env:"DISCOVERY_DB_FLUSH_INTERVAL"` DBFlushInterval time.Duration `group:"Database" help:"Interval between database flushes" default:"5m" env:"DISCOVERY_DB_FLUSH_INTERVAL"`
@@ -78,6 +77,10 @@ type CLI struct {
DBS3AccessKeyID string `name:"db-s3-access-key-id" group:"Database (S3 backup)" hidden:"true" help:"S3 access key ID for database" env:"DISCOVERY_DB_S3_ACCESS_KEY_ID"` DBS3AccessKeyID string `name:"db-s3-access-key-id" group:"Database (S3 backup)" hidden:"true" help:"S3 access key ID for database" env:"DISCOVERY_DB_S3_ACCESS_KEY_ID"`
DBS3SecretKey string `name:"db-s3-secret-key" group:"Database (S3 backup)" hidden:"true" help:"S3 secret key for database" env:"DISCOVERY_DB_S3_SECRET_KEY"` DBS3SecretKey string `name:"db-s3-secret-key" group:"Database (S3 backup)" hidden:"true" help:"S3 secret key for database" env:"DISCOVERY_DB_S3_SECRET_KEY"`
DBAzureBlobAccount string `name:"db-azure-blob-account" env:"DISCOVERY_DB_AZUREBLOB_ACCOUNT"`
DBAzureBlobKey string `name:"db-azure-blob-key" env:"DISCOVERY_DB_AZUREBLOB_KEY"`
DBAzureBlobContainer string `name:"db-azure-blob-container" env:"DISCOVERY_DB_AZUREBLOB_CONTAINER"`
AMQPAddress string `group:"AMQP replication" hidden:"true" help:"Address to AMQP broker" env:"DISCOVERY_AMQP_ADDRESS"` AMQPAddress string `group:"AMQP replication" hidden:"true" help:"Address to AMQP broker" env:"DISCOVERY_AMQP_ADDRESS"`
Debug bool `short:"d" help:"Print debug output" env:"DISCOVERY_DEBUG"` Debug bool `short:"d" help:"Print debug output" env:"DISCOVERY_DEBUG"`
@@ -85,37 +88,34 @@ type CLI struct {
} }
func main() { func main() {
log.SetOutput(os.Stdout)
var cli CLI var cli CLI
kong.Parse(&cli) kong.Parse(&cli)
debug = cli.Debug
level := slog.LevelInfo log.Println(build.LongVersionFor("stdiscosrv"))
if cli.Debug {
level = slog.LevelDebug
}
slogutil.SetDefaultLevel(level)
if cli.Version { if cli.Version {
fmt.Println(build.LongVersionFor("stdiscosrv"))
return return
} }
slog.Info(build.LongVersionFor("stdiscosrv"))
buildInfo.WithLabelValues(build.Version, runtime.Version(), build.User, build.Date.UTC().Format("2006-01-02T15:04:05Z")).Set(1)
var cert tls.Certificate var cert tls.Certificate
if !cli.HTTP { if !cli.HTTP {
var err error var err error
cert, err = tls.LoadX509KeyPair(cli.Cert, cli.Key) cert, err = tls.LoadX509KeyPair(cli.Cert, cli.Key)
if os.IsNotExist(err) { if os.IsNotExist(err) {
slog.Info("Failed to load keypair. Generating one, this might take a while...") log.Println("Failed to load keypair. Generating one, this might take a while...")
cert, err = tlsutil.NewCertificate(cli.Cert, cli.Key, "stdiscosrv", 20*365, false) cert, err = tlsutil.NewCertificate(cli.Cert, cli.Key, "stdiscosrv", 20*365)
if err != nil { if err != nil {
slog.Error("Failed to generate X509 key pair", "error", err) log.Fatalln("Failed to generate X509 key pair:", err)
os.Exit(1)
} }
} else if err != nil { } else if err != nil {
slog.Error("Failed to load keypair", "error", err) log.Fatalln("Failed to load keypair:", err)
os.Exit(1)
} }
devID := protocol.NewDeviceID(cert.Certificate[0]) devID := protocol.NewDeviceID(cert.Certificate[0])
slog.Info("Loaded certificate keypair", "deviceId", devID.String()) log.Println("Server device ID is", devID)
} }
// Root of the service tree. // Root of the service tree.
@@ -129,10 +129,11 @@ func main() {
var err error var err error
if cli.DBS3Endpoint != "" { if cli.DBS3Endpoint != "" {
blobs, err = s3.NewSession(cli.DBS3Endpoint, cli.DBS3Region, cli.DBS3Bucket, cli.DBS3AccessKeyID, cli.DBS3SecretKey) blobs, err = s3.NewSession(cli.DBS3Endpoint, cli.DBS3Region, cli.DBS3Bucket, cli.DBS3AccessKeyID, cli.DBS3SecretKey)
} else if cli.DBAzureBlobAccount != "" {
blobs, err = azureblob.NewBlobStore(cli.DBAzureBlobAccount, cli.DBAzureBlobKey, cli.DBAzureBlobContainer)
} }
if err != nil { if err != nil {
slog.Error("Failed to create blob store", "error", err) log.Fatalf("Failed to create blob store: %v", err)
os.Exit(1)
} }
// Start the database. // Start the database.
@@ -149,7 +150,7 @@ func main() {
} }
// Start the main API server. // Start the main API server.
qs := newAPISrv(cli.Listen, cert, db, repl, cli.HTTP, cli.Compression, cli.DesiredUnseenNotFoundRate, cli.DesiredSeenNotFoundRate) qs := newAPISrv(cli.Listen, cert, db, repl, cli.HTTP, cli.Compression, cli.DesiredNotFoundRate)
main.Add(qs) main.Add(qs)
// If we have a metrics port configured, start a metrics handler. // If we have a metrics port configured, start a metrics handler.
@@ -157,9 +158,7 @@ func main() {
go func() { go func() {
mux := http.NewServeMux() mux := http.NewServeMux()
mux.Handle("/metrics", promhttp.Handler()) mux.Handle("/metrics", promhttp.Handler())
err := http.ListenAndServe(cli.MetricsListen, mux) log.Fatal(http.ListenAndServe(cli.MetricsListen, mux))
slog.Error("Failed to serve", "error", err)
os.Exit(1)
}() }()
} }
@@ -169,11 +168,9 @@ func main() {
// Cancel on signal // Cancel on signal
signalChan := make(chan os.Signal, 1) signalChan := make(chan os.Signal, 1)
signal.Notify(signalChan, os.Interrupt) signal.Notify(signalChan, os.Interrupt)
signal.Notify(signalChan, syscall.SIGTERM)
go func() { go func() {
sig := <-signalChan sig := <-signalChan
slog.Info("Received signal; shutting down", "signal", sig, "delay", cli.ShutdownDelay) log.Printf("Received signal %s; shutting down", sig)
time.Sleep(time.Duration(float64(time.Second) * cli.ShutdownDelay))
cancel() cancel()
}() }()
+17 -28
View File
@@ -7,12 +7,18 @@
package main package main
import ( import (
"net/http"
"github.com/prometheus/client_golang/prometheus" "github.com/prometheus/client_golang/prometheus"
) )
var ( var (
buildInfo = prometheus.NewGaugeVec(
prometheus.GaugeOpts{
Namespace: "syncthing",
Subsystem: "discovery",
Name: "build_info",
Help: "A metric with a constant '1' value labeled by version, goversion, builduser and builddate from which stdiscosrv was built.",
}, []string{"version", "goversion", "builduser", "builddate"})
apiRequestsTotal = prometheus.NewCounterVec( apiRequestsTotal = prometheus.NewCounterVec(
prometheus.CounterOpts{ prometheus.CounterOpts{
Namespace: "syncthing", Namespace: "syncthing",
@@ -115,15 +121,18 @@ var (
) )
const ( const (
dbOpGet = "get" dbOpGet = "get"
dbOpPut = "put" dbOpPut = "put"
dbOpMerge = "merge" dbOpMerge = "merge"
dbResSuccess = "success" dbOpDelete = "delete"
dbResNotFound = "not_found" dbResSuccess = "success"
dbResNotFound = "not_found"
dbResError = "error"
dbResUnmarshalError = "unmarsh_err"
) )
func init() { func init() {
prometheus.MustRegister( prometheus.MustRegister(buildInfo,
apiRequestsTotal, apiRequestsSeconds, apiRequestsTotal, apiRequestsSeconds,
lookupRequestsTotal, announceRequestsTotal, lookupRequestsTotal, announceRequestsTotal,
replicationSendsTotal, replicationRecvsTotal, replicationSendsTotal, replicationRecvsTotal,
@@ -131,24 +140,4 @@ func init() {
databaseOperations, databaseOperationSeconds, databaseOperations, databaseOperationSeconds,
databaseWriteSeconds, databaseLastWritten, databaseWriteSeconds, databaseLastWritten,
retryAfterLevel) 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")
} }
+4 -4
View File
@@ -18,7 +18,7 @@ import (
var ( var (
outboxesMut = sync.RWMutex{} outboxesMut = sync.RWMutex{}
outboxes = make(map[syncthingprotocol.DeviceID]chan any) outboxes = make(map[syncthingprotocol.DeviceID]chan interface{})
numConnections atomic.Int64 numConnections atomic.Int64
) )
@@ -97,9 +97,9 @@ func protocolConnectionHandler(tcpConn net.Conn, config *tls.Config, token strin
id := syncthingprotocol.NewDeviceID(certs[0].Raw) id := syncthingprotocol.NewDeviceID(certs[0].Raw)
messages := make(chan any) messages := make(chan interface{})
errors := make(chan error, 1) errors := make(chan error, 1)
outbox := make(chan any) outbox := make(chan interface{})
// Read messages from the connection and send them on the messages // Read messages from the connection and send them on the messages
// channel. When there is an error, send it on the error channel and // channel. When there is an error, send it on the error channel and
@@ -364,7 +364,7 @@ func sessionConnectionHandler(conn net.Conn) {
} }
} }
func messageReader(conn net.Conn, messages chan<- any, errors chan<- error) { func messageReader(conn net.Conn, messages chan<- interface{}, errors chan<- error) {
numConnections.Add(1) numConnections.Add(1)
defer numConnections.Add(-1) defer numConnections.Add(-1)
+11 -18
View File
@@ -14,7 +14,6 @@ import (
"os" "os"
"os/signal" "os/signal"
"path/filepath" "path/filepath"
"strconv"
"strings" "strings"
"sync/atomic" "sync/atomic"
"syscall" "syscall"
@@ -22,6 +21,7 @@ import (
"golang.org/x/time/rate" "golang.org/x/time/rate"
_ "github.com/syncthing/syncthing/lib/automaxprocs"
"github.com/syncthing/syncthing/lib/build" "github.com/syncthing/syncthing/lib/build"
"github.com/syncthing/syncthing/lib/config" "github.com/syncthing/syncthing/lib/config"
"github.com/syncthing/syncthing/lib/events" "github.com/syncthing/syncthing/lib/events"
@@ -71,13 +71,9 @@ var (
// httpClient is the HTTP client we use for outbound requests. It has a // httpClient is the HTTP client we use for outbound requests. It has a
// timeout and may get further options set during initialization. // timeout and may get further options set during initialization.
var ( var httpClient = &http.Client{
httpTransport = &http.Transport{} Timeout: 30 * time.Second,
httpClient = &http.Client{ }
Timeout: 30 * time.Second,
Transport: httpTransport,
}
)
func main() { func main() {
log.SetFlags(log.Lshortfile | log.LstdFlags) log.SetFlags(log.Lshortfile | log.LstdFlags)
@@ -136,7 +132,9 @@ func main() {
// also come from that address. // also come from that address.
laddr.Port = 0 laddr.Port = 0
boundDialer := &net.Dialer{LocalAddr: laddr} boundDialer := &net.Dialer{LocalAddr: laddr}
httpTransport.DialContext = boundDialer.DialContext httpClient.Transport = &http.Transport{
DialContext: boundDialer.DialContext,
}
} }
log.Println(longVer) log.Println(longVer)
@@ -159,17 +157,12 @@ func main() {
cert, err := tls.LoadX509KeyPair(certFile, keyFile) cert, err := tls.LoadX509KeyPair(certFile, keyFile)
if err != nil { if err != nil {
log.Println("Failed to load keypair. Generating one, this might take a while...") log.Println("Failed to load keypair. Generating one, this might take a while...")
cert, err = tlsutil.NewCertificate(certFile, keyFile, "strelaysrv", 20*365, false) cert, err = tlsutil.NewCertificate(certFile, keyFile, "strelaysrv", 20*365)
if err != nil { if err != nil {
log.Fatalln("Failed to generate X509 key pair:", err) log.Fatalln("Failed to generate X509 key pair:", err)
} }
} }
// Outgoing HTTPS requests may use our certificate for authentication
httpTransport.TLSClientConfig = &tls.Config{
Certificates: []tls.Certificate{cert},
}
tlsCfg := &tls.Config{ tlsCfg := &tls.Config{
Certificates: []tls.Certificate{cert}, Certificates: []tls.Certificate{cert},
NextProtos: []string{protocol.ProtocolName}, NextProtos: []string{protocol.ProtocolName},
@@ -254,10 +247,10 @@ func main() {
query.Set("pingInterval", pingInterval.String()) query.Set("pingInterval", pingInterval.String())
query.Set("networkTimeout", networkTimeout.String()) query.Set("networkTimeout", networkTimeout.String())
if sessionLimitBps > 0 { if sessionLimitBps > 0 {
query.Set("sessionLimitBps", strconv.Itoa(sessionLimitBps)) query.Set("sessionLimitBps", fmt.Sprint(sessionLimitBps))
} }
if globalLimitBps > 0 { if globalLimitBps > 0 {
query.Set("globalLimitBps", strconv.Itoa(globalLimitBps)) query.Set("globalLimitBps", fmt.Sprint(globalLimitBps))
} }
if statusAddr != "" { if statusAddr != "" {
query.Set("statusAddr", statusAddr) query.Set("statusAddr", statusAddr)
@@ -284,7 +277,7 @@ func main() {
for _, pool := range pools { for _, pool := range pools {
pool = strings.TrimSpace(pool) pool = strings.TrimSpace(pool)
if len(pool) > 0 { if len(pool) > 0 {
go poolHandler(pool, uri, mapping) go poolHandler(pool, uri, mapping, cert)
} }
} }
+20 -2
View File
@@ -4,6 +4,7 @@ package main
import ( import (
"bytes" "bytes"
"crypto/tls"
"encoding/json" "encoding/json"
"io" "io"
"log" "log"
@@ -16,7 +17,7 @@ const (
httpStatusEnhanceYourCalm = 429 httpStatusEnhanceYourCalm = 429
) )
func poolHandler(pool string, uri *url.URL, mapping mapping) { func poolHandler(pool string, uri *url.URL, mapping mapping, ownCert tls.Certificate) {
if debug { if debug {
log.Println("Joining", pool) log.Println("Joining", pool)
} }
@@ -31,7 +32,24 @@ func poolHandler(pool string, uri *url.URL, mapping mapping) {
uriCopy.String(), uriCopy.String(),
}) })
resp, err := httpClient.Post(pool, "application/json", &b) //nolint:noctx poolUrl, err := url.Parse(pool)
if err != nil {
log.Printf("Could not parse pool url '%s': %v", pool, err)
}
client := http.DefaultClient
if poolUrl.Scheme == "https" {
// Sent our certificate in join request
client = &http.Client{
Transport: &http.Transport{
TLSClientConfig: &tls.Config{
Certificates: []tls.Certificate{ownCert},
},
},
}
}
resp, err := client.Post(pool, "application/json", &b)
if err != nil { if err != nil {
log.Printf("Error joining pool %v: HTTP request: %v", pool, err) log.Printf("Error joining pool %v: HTTP request: %v", pool, err)
time.Sleep(time.Minute) time.Sleep(time.Minute)
+13 -3
View File
@@ -158,12 +158,19 @@ func (s *session) Serve() {
} }
wg := sync.WaitGroup{} wg := sync.WaitGroup{}
wg.Add(2)
var err0 error var err0 error
wg.Go(func() { err0 = s.proxy(s.conns[0], s.conns[1]) }) go func() {
err0 = s.proxy(s.conns[0], s.conns[1])
wg.Done()
}()
var err1 error var err1 error
wg.Go(func() { err1 = s.proxy(s.conns[1], s.conns[0]) }) go func() {
err1 = s.proxy(s.conns[1], s.conns[0])
wg.Done()
}()
sessionMut.Lock() sessionMut.Lock()
activeSessions = append(activeSessions, s) activeSessions = append(activeSessions, s)
@@ -330,7 +337,10 @@ func take(tokens int, ls ...*rate.Limiter) {
for tokens > 0 { for tokens > 0 {
// chunk is how many tokens we can consume at a time // chunk is how many tokens we can consume at a time
chunk := min(tokens, minBurst) chunk := tokens
if chunk > minBurst {
chunk = minBurst
}
// maxDelay is the longest delay mandated by any of the limiters for // maxDelay is the longest delay mandated by any of the limiters for
// the chosen chunk size. // the chosen chunk size.
+3 -3
View File
@@ -38,7 +38,7 @@ func statusService(addr string) {
func getStatus(w http.ResponseWriter, _ *http.Request) { func getStatus(w http.ResponseWriter, _ *http.Request) {
w.Header().Set("Access-Control-Allow-Origin", "*") w.Header().Set("Access-Control-Allow-Origin", "*")
status := make(map[string]any) status := make(map[string]interface{})
sessionMut.Lock() sessionMut.Lock()
// This can potentially be double the number of pending sessions, as each session has two keys, one for each side. // This can potentially be double the number of pending sessions, as each session has two keys, one for each side.
@@ -67,7 +67,7 @@ func getStatus(w http.ResponseWriter, _ *http.Request) {
rc.rate(30*60/10) * 8 / 1000, rc.rate(30*60/10) * 8 / 1000,
rc.rate(60*60/10) * 8 / 1000, rc.rate(60*60/10) * 8 / 1000,
} }
status["options"] = map[string]any{ status["options"] = map[string]interface{}{
"network-timeout": networkTimeout / time.Second, "network-timeout": networkTimeout / time.Second,
"ping-interval": pingInterval / time.Second, "ping-interval": pingInterval / time.Second,
"message-timeout": messageTimeout / time.Second, "message-timeout": messageTimeout / time.Second,
@@ -122,7 +122,7 @@ func (r *rateCalculator) updateRates(interval time.Duration) {
func (r *rateCalculator) rate(periods int) int64 { func (r *rateCalculator) rate(periods int) int64 {
var tot int64 var tot int64
for i := range periods { for i := 0; i < periods; i++ {
tot += r.rates[i] tot += r.rates[i]
} }
return tot / int64(periods) return tot / int64(periods)
+2 -3
View File
@@ -6,7 +6,6 @@ import (
"bufio" "bufio"
"context" "context"
"crypto/tls" "crypto/tls"
"errors"
"flag" "flag"
"log" "log"
"net" "net"
@@ -15,6 +14,7 @@ import (
"path/filepath" "path/filepath"
"time" "time"
_ "github.com/syncthing/syncthing/lib/automaxprocs"
syncthingprotocol "github.com/syncthing/syncthing/lib/protocol" syncthingprotocol "github.com/syncthing/syncthing/lib/protocol"
"github.com/syncthing/syncthing/lib/relay/client" "github.com/syncthing/syncthing/lib/relay/client"
"github.com/syncthing/syncthing/lib/relay/protocol" "github.com/syncthing/syncthing/lib/relay/protocol"
@@ -133,8 +133,7 @@ func connectToStdio(stdin <-chan string, conn net.Conn) {
conn.SetReadDeadline(time.Now().Add(time.Millisecond)) conn.SetReadDeadline(time.Now().Add(time.Millisecond))
n, err := conn.Read(buf[0:]) n, err := conn.Read(buf[0:])
if err != nil { if err != nil {
var nerr net.Error nerr, ok := err.(net.Error)
ok := errors.As(err, &nerr)
if !ok || !nerr.Timeout() { if !ok || !nerr.Timeout() {
log.Println(err) log.Println(err)
return return
+1 -1
View File
@@ -10,7 +10,7 @@ import (
func setTCPOptions(conn net.Conn) error { func setTCPOptions(conn net.Conn) error {
tcpConn, ok := conn.(*net.TCPConn) tcpConn, ok := conn.(*net.TCPConn)
if !ok { if !ok {
return errors.New("not a TCP connection") return errors.New("Not a TCP connection")
} }
if err := tcpConn.SetLinger(0); err != nil { if err := tcpConn.SetLinger(0); err != nil {
return err return err
+2 -5
View File
@@ -8,14 +8,11 @@ package main
import ( import (
"fmt" "fmt"
"log/slog"
"os" "os"
"runtime" "runtime"
"runtime/pprof" "runtime/pprof"
"syscall" "syscall"
"time" "time"
"github.com/syncthing/syncthing/internal/slogutil"
) )
func startBlockProfiler() { func startBlockProfiler() {
@@ -23,10 +20,10 @@ func startBlockProfiler() {
if profiler == nil { if profiler == nil {
panic("Couldn't find block profiler") panic("Couldn't find block profiler")
} }
slog.Debug("Starting block profiling") l.Debugln("Starting block profiling")
go func() { go func() {
err := saveBlockingProfiles(profiler) // Only returns on error err := saveBlockingProfiles(profiler) // Only returns on error
slog.Error("Block profiler failed", slogutil.Error(err)) l.Warnln("Block profiler failed:", err)
panic("Block profiler failed") panic("Block profiler failed")
}() }()
} }
+6 -9
View File
@@ -18,7 +18,6 @@ import (
"net/http" "net/http"
"strings" "strings"
"github.com/syncthing/syncthing/lib/build"
"github.com/syncthing/syncthing/lib/config" "github.com/syncthing/syncthing/lib/config"
"github.com/syncthing/syncthing/lib/events" "github.com/syncthing/syncthing/lib/events"
"github.com/syncthing/syncthing/lib/locations" "github.com/syncthing/syncthing/lib/locations"
@@ -28,12 +27,11 @@ import (
type APIClient interface { type APIClient interface {
Get(url string) (*http.Response, error) Get(url string) (*http.Response, error)
Post(url, body string) (*http.Response, error) Post(url, body string) (*http.Response, error)
PutJSON(url string, o any) (*http.Response, error) PutJSON(url string, o interface{}) (*http.Response, error)
} }
type apiClient struct { type apiClient struct {
http.Client http.Client
cfg config.GUIConfiguration cfg config.GUIConfiguration
apikey string apikey string
} }
@@ -93,11 +91,11 @@ func loadGUIConfig() (config.GUIConfiguration, error) {
guiCfg := cfg.GUI() guiCfg := cfg.GUI()
if guiCfg.Address() == "" { if guiCfg.Address() == "" {
return config.GUIConfiguration{}, errors.New("could not find GUI Address") return config.GUIConfiguration{}, errors.New("Could not find GUI Address")
} }
if guiCfg.APIKey == "" { if guiCfg.APIKey == "" {
return config.GUIConfiguration{}, errors.New("could not find GUI API key") return config.GUIConfiguration{}, errors.New("Could not find GUI API key")
} }
return guiCfg, nil return guiCfg, nil
@@ -115,8 +113,7 @@ func (c *apiClient) Endpoint() string {
} }
func (c *apiClient) Do(req *http.Request) (*http.Response, error) { func (c *apiClient) Do(req *http.Request) (*http.Response, error) {
req.Header.Set("X-Api-Key", c.apikey) req.Header.Set("X-API-Key", c.apikey)
req.Header.Set("User-Agent", build.UserAgent())
resp, err := c.Client.Do(req) resp, err := c.Client.Do(req)
if err != nil { if err != nil {
return nil, err return nil, err
@@ -136,7 +133,7 @@ func (c *apiClient) RequestString(url, method, data string) (*http.Response, err
return c.Request(url, method, bytes.NewBufferString(data)) return c.Request(url, method, bytes.NewBufferString(data))
} }
func (c *apiClient) RequestJSON(url, method string, o any) (*http.Response, error) { func (c *apiClient) RequestJSON(url, method string, o interface{}) (*http.Response, error) {
data, err := json.Marshal(o) data, err := json.Marshal(o)
if err != nil { if err != nil {
return nil, err return nil, err
@@ -152,7 +149,7 @@ func (c *apiClient) Post(url, body string) (*http.Response, error) {
return c.RequestString(url, "POST", body) return c.RequestString(url, "POST", body)
} }
func (c *apiClient) PutJSON(url string, o any) (*http.Response, error) { func (c *apiClient) PutJSON(url string, o interface{}) (*http.Response, error) {
return c.RequestJSON(url, "PUT", o) return c.RequestJSON(url, "PUT", o)
} }
+5 -60
View File
@@ -10,7 +10,6 @@ import (
"encoding/json" "encoding/json"
"errors" "errors"
"fmt" "fmt"
"net/http"
"reflect" "reflect"
"github.com/AudriusButkevicius/recli" "github.com/AudriusButkevicius/recli"
@@ -19,53 +18,6 @@ import (
"github.com/urfave/cli" "github.com/urfave/cli"
) )
// Try to mimic the kong output format through custom help templates
var customAppHelpTemplate = `Usage: {{if .UsageText}}{{.UsageText}}{{else}}{{.HelpName}}{{if .Commands}} <command> [flags]{{end}} {{if .ArgsUsage}}{{.ArgsUsage}}{{else}}[arguments...]{{end}}{{end}}
{{.Description}}{{if .VisibleFlags}}
Flags:
{{range $index, $option := .VisibleFlags}}{{if $index}}
{{end}}{{$option}}{{end}}{{end}}{{if .VisibleCommands}}
Commands:{{range .VisibleCategories}}{{if .Name}}
{{.Name}}:{{range .VisibleCommands}}
{{join .Names ", "}}{{"\t"}}{{.Usage}}{{end}}{{else}}{{range .VisibleCommands}}
{{join .Names ", "}}{{"\t"}}{{.Usage}}{{end}}{{end}}{{end}}{{end}}
`
var customCommandHelpTemplate = `Usage: {{if .UsageText}}{{.UsageText}}{{else}}{{.HelpName}}{{if .VisibleFlags}} [flags]{{end}} {{if .ArgsUsage}}{{.ArgsUsage}}{{else}}[arguments...]{{end}}{{end}}
{{.Usage}}{{if .VisibleFlags}}
Flags:
{{range $index, $option := .VisibleFlags}}{{if $index}}
{{end}}{{$option}}{{end}}{{end}}{{if .Category}}
Category:
{{.Category}}{{end}}{{if .Description}}
{{.Description}}{{end}}
`
var customSubcommandHelpTemplate = `Usage: {{if .UsageText}}{{.UsageText}}{{else}}{{.HelpName}} <command>{{if .VisibleFlags}} [flags]{{end}} {{if .ArgsUsage}}{{.ArgsUsage}}{{else}}[arguments...]{{end}}{{end}}{{if .Description}}
{{.Description}}{{else}}{{if .Usage}}
{{.Usage}}{{end}}{{end}}{{if .VisibleFlags}}
Flags:
{{range $index, $option := .VisibleFlags}}{{if $index}}
{{end}}{{$option}}{{end}}{{end}}{{if .VisibleCommands}}
Commands:{{range .VisibleCategories}}{{if .Name}}
{{.Name}}:{{range .VisibleCommands}}
{{join .Names ", "}}{{"\t"}}{{.Usage}}{{end}}{{else}}{{range .VisibleCommands}}
{{join .Names ", "}}{{"\t"}}{{.Usage}}{{end}}{{end}}{{end}}{{end}}
`
type configHandler struct { type configHandler struct {
original, cfg config.Configuration original, cfg config.Configuration
client APIClient client APIClient
@@ -76,18 +28,13 @@ type configCommand struct {
Args []string `arg:"" default:"-h"` Args []string `arg:"" default:"-h"`
} }
func (c *configCommand) Run(ctx Context, outerCtx *kong.Context) error { func (c *configCommand) Run(ctx Context, _ *kong.Context) error {
app := cli.NewApp() app := cli.NewApp()
app.Name = "syncthing cli config" app.Name = "syncthing"
app.HelpName = "syncthing cli config" app.Author = "The Syncthing Authors"
app.Description = outerCtx.Selected().Help app.Metadata = map[string]interface{}{
app.Metadata = map[string]any{
"clientFactory": ctx.clientFactory, "clientFactory": ctx.clientFactory,
} }
app.CustomAppHelpTemplate = customAppHelpTemplate
// Override global templates, as this is out only usage of the package
cli.CommandHelpTemplate = customCommandHelpTemplate
cli.SubcommandHelpTemplate = customSubcommandHelpTemplate
h := new(configHandler) h := new(configHandler)
h.client, h.err = ctx.clientFactory.getClient() h.client, h.err = ctx.clientFactory.getClient()
@@ -108,8 +55,6 @@ func (c *configCommand) Run(ctx Context, outerCtx *kong.Context) error {
app.Commands = commands app.Commands = commands
app.HideHelp = true app.HideHelp = true
// Explicitly re-add help only as flags, not as commands
app.Flags = []cli.Flag{cli.HelpFlag}
app.Before = h.configBefore app.Before = h.configBefore
app.After = h.configAfter app.After = h.configAfter
@@ -141,7 +86,7 @@ func (h *configHandler) configAfter(_ *cli.Context) error {
if err != nil { if err != nil {
return err return err
} }
if resp.StatusCode != http.StatusOK { if resp.StatusCode != 200 {
body, err := responseToBArray(resp) body, err := responseToBArray(resp)
if err != nil { if err != nil {
return err return err
+1
View File
@@ -41,4 +41,5 @@ func (p *profileCommand) Run(ctx Context) error {
type debugCommand struct { type debugCommand struct {
File fileCommand `cmd:"" help:"Show information about a file (or directory/symlink)"` File fileCommand `cmd:"" help:"Show information about a file (or directory/symlink)"`
Profile profileCommand `cmd:"" help:"Save a profile to help figuring out what Syncthing does"` Profile profileCommand `cmd:"" help:"Save a profile to help figuring out what Syncthing does"`
Index indexCommand `cmd:"" help:"Show information about the index (database)"`
} }
+1 -2
View File
@@ -9,7 +9,6 @@ package cli
import ( import (
"errors" "errors"
"fmt" "fmt"
"net/http"
"strings" "strings"
"github.com/alecthomas/kong" "github.com/alecthomas/kong"
@@ -35,7 +34,7 @@ func (e *errorsPushCommand) Run(ctx Context) error {
if err != nil { if err != nil {
return err return err
} }
if response.StatusCode != http.StatusOK { if response.StatusCode != 200 {
errStr = fmt.Sprint("Failed to push error\nStatus code: ", response.StatusCode) errStr = fmt.Sprint("Failed to push error\nStatus code: ", response.StatusCode)
bytes, err := responseToBArray(response) bytes, err := responseToBArray(response)
if err != nil { if err != nil {
+32
View File
@@ -0,0 +1,32 @@
// Copyright (C) 2014 The Syncthing Authors.
//
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this file,
// You can obtain one at https://mozilla.org/MPL/2.0/.
package cli
import (
"github.com/alecthomas/kong"
)
type indexCommand struct {
Dump struct{} `cmd:"" help:"Print the entire db"`
DumpSize struct{} `cmd:"" help:"Print the db size of different categories of information"`
Check struct{} `cmd:"" help:"Check the database for inconsistencies"`
Account struct{} `cmd:"" help:"Print key and value size statistics per key type"`
}
func (*indexCommand) Run(kongCtx *kong.Context) error {
switch kongCtx.Selected().Name {
case "dump":
return indexDump()
case "dump-size":
return indexDumpSize()
case "check":
return indexCheck()
case "account":
return indexAccount()
}
return nil
}
+62
View File
@@ -0,0 +1,62 @@
// Copyright (C) 2020 The Syncthing Authors.
//
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this file,
// You can obtain one at https://mozilla.org/MPL/2.0/.
package cli
import (
"fmt"
"os"
"text/tabwriter"
)
// indexAccount prints key and data size statistics per class
func indexAccount() error {
ldb, err := getDB()
if err != nil {
return err
}
it, err := ldb.NewPrefixIterator(nil)
if err != nil {
return err
}
var ksizes [256]int
var dsizes [256]int
var counts [256]int
var max [256]int
for it.Next() {
key := it.Key()
t := key[0]
ds := len(it.Value())
ks := len(key)
s := ks + ds
counts[t]++
ksizes[t] += ks
dsizes[t] += ds
if s > max[t] {
max[t] = s
}
}
tw := tabwriter.NewWriter(os.Stdout, 1, 1, 1, ' ', tabwriter.AlignRight)
toti, totds, totks := 0, 0, 0
for t := range ksizes {
if ksizes[t] > 0 {
// yes metric kilobytes 🤘
fmt.Fprintf(tw, "0x%02x:\t%d items,\t%d KB keys +\t%d KB data,\t%d B +\t%d B avg,\t%d B max\t\n", t, counts[t], ksizes[t]/1000, dsizes[t]/1000, ksizes[t]/counts[t], dsizes[t]/counts[t], max[t])
toti += counts[t]
totds += dsizes[t]
totks += ksizes[t]
}
}
fmt.Fprintf(tw, "Total\t%d items,\t%d KB keys +\t%d KB data.\t\n", toti, totks/1000, totds/1000)
tw.Flush()
return nil
}
+162
View File
@@ -0,0 +1,162 @@
// Copyright (C) 2015 The Syncthing Authors.
//
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this file,
// You can obtain one at https://mozilla.org/MPL/2.0/.
package cli
import (
"encoding/binary"
"fmt"
"time"
"google.golang.org/protobuf/proto"
"github.com/syncthing/syncthing/internal/gen/bep"
"github.com/syncthing/syncthing/internal/gen/dbproto"
"github.com/syncthing/syncthing/lib/db"
"github.com/syncthing/syncthing/lib/protocol"
)
func indexDump() error {
ldb, err := getDB()
if err != nil {
return err
}
it, err := ldb.NewPrefixIterator(nil)
if err != nil {
return err
}
for it.Next() {
key := it.Key()
switch key[0] {
case db.KeyTypeDevice:
folder := binary.BigEndian.Uint32(key[1:])
device := binary.BigEndian.Uint32(key[1+4:])
name := nulString(key[1+4+4:])
fmt.Printf("[device] F:%d D:%d N:%q", folder, device, name)
var f bep.FileInfo
err := proto.Unmarshal(it.Value(), &f)
if err != nil {
return err
}
fmt.Printf(" V:%v\n", &f)
case db.KeyTypeGlobal:
folder := binary.BigEndian.Uint32(key[1:])
name := nulString(key[1+4:])
var flv dbproto.VersionList
proto.Unmarshal(it.Value(), &flv)
fmt.Printf("[global] F:%d N:%q V:%s\n", folder, name, &flv)
case db.KeyTypeBlock:
folder := binary.BigEndian.Uint32(key[1:])
hash := key[1+4 : 1+4+32]
name := nulString(key[1+4+32:])
fmt.Printf("[block] F:%d H:%x N:%q I:%d\n", folder, hash, name, binary.BigEndian.Uint32(it.Value()))
case db.KeyTypeDeviceStatistic:
fmt.Printf("[dstat] K:%x V:%x\n", key, it.Value())
case db.KeyTypeFolderStatistic:
fmt.Printf("[fstat] K:%x V:%x\n", key, it.Value())
case db.KeyTypeVirtualMtime:
folder := binary.BigEndian.Uint32(key[1:])
name := nulString(key[1+4:])
val := it.Value()
var realTime, virtualTime time.Time
realTime.UnmarshalBinary(val[:len(val)/2])
virtualTime.UnmarshalBinary(val[len(val)/2:])
fmt.Printf("[mtime] F:%d N:%q R:%v V:%v\n", folder, name, realTime, virtualTime)
case db.KeyTypeFolderIdx:
key := binary.BigEndian.Uint32(key[1:])
fmt.Printf("[folderidx] K:%d V:%q\n", key, it.Value())
case db.KeyTypeDeviceIdx:
key := binary.BigEndian.Uint32(key[1:])
val := it.Value()
device := "<nil>"
if len(val) > 0 {
dev, err := protocol.DeviceIDFromBytes(val)
if err != nil {
device = fmt.Sprintf("<invalid %d bytes>", len(val))
} else {
device = dev.String()
}
}
fmt.Printf("[deviceidx] K:%d V:%s\n", key, device)
case db.KeyTypeIndexID:
device := binary.BigEndian.Uint32(key[1:])
folder := binary.BigEndian.Uint32(key[5:])
fmt.Printf("[indexid] D:%d F:%d I:%x\n", device, folder, it.Value())
case db.KeyTypeFolderMeta:
folder := binary.BigEndian.Uint32(key[1:])
fmt.Printf("[foldermeta] F:%d", folder)
var cs dbproto.CountsSet
if err := proto.Unmarshal(it.Value(), &cs); err != nil {
fmt.Printf(" (invalid)\n")
} else {
fmt.Printf(" V:%v\n", &cs)
}
case db.KeyTypeMiscData:
fmt.Printf("[miscdata] K:%q V:%q\n", key[1:], it.Value())
case db.KeyTypeSequence:
folder := binary.BigEndian.Uint32(key[1:])
seq := binary.BigEndian.Uint64(key[5:])
fmt.Printf("[sequence] F:%d S:%d V:%q\n", folder, seq, it.Value())
case db.KeyTypeNeed:
folder := binary.BigEndian.Uint32(key[1:])
file := string(key[5:])
fmt.Printf("[need] F:%d V:%q\n", folder, file)
case db.KeyTypeBlockList:
fmt.Printf("[blocklist] H:%x\n", key[1:])
case db.KeyTypeBlockListMap:
folder := binary.BigEndian.Uint32(key[1:])
hash := key[5:37]
fileName := string(key[37:])
fmt.Printf("[blocklistmap] F:%d H:%x N:%s\n", folder, hash, fileName)
case db.KeyTypeVersion:
fmt.Printf("[version] H:%x", key[1:])
var v bep.Vector
err := proto.Unmarshal(it.Value(), &v)
if err != nil {
fmt.Printf(" (invalid)\n")
} else {
fmt.Printf(" V:%v\n", &v)
}
case db.KeyTypePendingFolder:
device := binary.BigEndian.Uint32(key[1:])
folder := string(key[5:])
var of dbproto.ObservedFolder
proto.Unmarshal(it.Value(), &of)
fmt.Printf("[pendingFolder] D:%d F:%s V:%v\n", device, folder, &of)
case db.KeyTypePendingDevice:
device := "<invalid>"
dev, err := protocol.DeviceIDFromBytes(key[1:])
if err == nil {
device = dev.String()
}
var od dbproto.ObservedDevice
proto.Unmarshal(it.Value(), &od)
fmt.Printf("[pendingDevice] D:%v V:%v\n", device, &od)
default:
fmt.Printf("[??? %d]\n %x\n %x\n", key[0], key, it.Value())
}
}
return nil
}
+89
View File
@@ -0,0 +1,89 @@
// Copyright (C) 2015 The Syncthing Authors.
//
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this file,
// You can obtain one at https://mozilla.org/MPL/2.0/.
package cli
import (
"cmp"
"encoding/binary"
"fmt"
"slices"
"github.com/syncthing/syncthing/lib/db"
)
func indexDumpSize() error {
type sizedElement struct {
key string
size int
}
ldb, err := getDB()
if err != nil {
return err
}
it, err := ldb.NewPrefixIterator(nil)
if err != nil {
return err
}
var elems []sizedElement
for it.Next() {
var ele sizedElement
key := it.Key()
switch key[0] {
case db.KeyTypeDevice:
folder := binary.BigEndian.Uint32(key[1:])
device := binary.BigEndian.Uint32(key[1+4:])
name := nulString(key[1+4+4:])
ele.key = fmt.Sprintf("DEVICE:%d:%d:%s", folder, device, name)
case db.KeyTypeGlobal:
folder := binary.BigEndian.Uint32(key[1:])
name := nulString(key[1+4:])
ele.key = fmt.Sprintf("GLOBAL:%d:%s", folder, name)
case db.KeyTypeBlock:
folder := binary.BigEndian.Uint32(key[1:])
hash := key[1+4 : 1+4+32]
name := nulString(key[1+4+32:])
ele.key = fmt.Sprintf("BLOCK:%d:%x:%s", folder, hash, name)
case db.KeyTypeDeviceStatistic:
ele.key = fmt.Sprintf("DEVICESTATS:%s", key[1:])
case db.KeyTypeFolderStatistic:
ele.key = fmt.Sprintf("FOLDERSTATS:%s", key[1:])
case db.KeyTypeVirtualMtime:
ele.key = fmt.Sprintf("MTIME:%s", key[1:])
case db.KeyTypeFolderIdx:
id := binary.BigEndian.Uint32(key[1:])
ele.key = fmt.Sprintf("FOLDERIDX:%d", id)
case db.KeyTypeDeviceIdx:
id := binary.BigEndian.Uint32(key[1:])
ele.key = fmt.Sprintf("DEVICEIDX:%d", id)
default:
ele.key = fmt.Sprintf("UNKNOWN:%x", key)
}
ele.size = len(it.Value())
elems = append(elems, ele)
}
slices.SortFunc(elems, func(a, b sizedElement) int {
return cmp.Compare(b.size, a.size)
})
for _, ele := range elems {
fmt.Println(ele.key, ele.size)
}
return nil
}
+435
View File
@@ -0,0 +1,435 @@
// Copyright (C) 2018 The Syncthing Authors.
//
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this file,
// You can obtain one at https://mozilla.org/MPL/2.0/.
package cli
import (
"bytes"
"cmp"
"encoding/binary"
"errors"
"fmt"
"slices"
"google.golang.org/protobuf/proto"
"github.com/syncthing/syncthing/internal/gen/bep"
"github.com/syncthing/syncthing/internal/gen/dbproto"
"github.com/syncthing/syncthing/lib/db"
"github.com/syncthing/syncthing/lib/protocol"
)
type fileInfoKey struct {
folder uint32
device uint32
name string
}
type globalKey struct {
folder uint32
name string
}
type sequenceKey struct {
folder uint32
sequence uint64
}
func indexCheck() (err error) {
ldb, err := getDB()
if err != nil {
return err
}
folders := make(map[uint32]string)
devices := make(map[uint32]string)
deviceToIDs := make(map[string]uint32)
fileInfos := make(map[fileInfoKey]*bep.FileInfo)
globals := make(map[globalKey]*dbproto.VersionList)
sequences := make(map[sequenceKey]string)
needs := make(map[globalKey]struct{})
blocklists := make(map[string]struct{})
versions := make(map[string]*bep.Vector)
usedBlocklists := make(map[string]struct{})
usedVersions := make(map[string]struct{})
var localDeviceKey uint32
success := true
defer func() {
if err == nil {
if success {
fmt.Println("Index check completed successfully.")
} else {
err = errors.New("Inconsistencies found in the index")
}
}
}()
it, err := ldb.NewPrefixIterator(nil)
if err != nil {
return err
}
for it.Next() {
key := it.Key()
switch key[0] {
case db.KeyTypeDevice:
folder := binary.BigEndian.Uint32(key[1:])
device := binary.BigEndian.Uint32(key[1+4:])
name := nulString(key[1+4+4:])
var f bep.FileInfo
err := proto.Unmarshal(it.Value(), &f)
if err != nil {
fmt.Println("Unable to unmarshal FileInfo:", err)
success = false
continue
}
fileInfos[fileInfoKey{folder, device, name}] = &f
case db.KeyTypeGlobal:
folder := binary.BigEndian.Uint32(key[1:])
name := nulString(key[1+4:])
var flv dbproto.VersionList
if err := proto.Unmarshal(it.Value(), &flv); err != nil {
fmt.Println("Unable to unmarshal VersionList:", err)
success = false
continue
}
globals[globalKey{folder, name}] = &flv
case db.KeyTypeFolderIdx:
key := binary.BigEndian.Uint32(it.Key()[1:])
folders[key] = string(it.Value())
case db.KeyTypeDeviceIdx:
key := binary.BigEndian.Uint32(it.Key()[1:])
devices[key] = string(it.Value())
deviceToIDs[string(it.Value())] = key
if bytes.Equal(it.Value(), protocol.LocalDeviceID[:]) {
localDeviceKey = key
}
case db.KeyTypeSequence:
folder := binary.BigEndian.Uint32(key[1:])
seq := binary.BigEndian.Uint64(key[5:])
val := it.Value()
sequences[sequenceKey{folder, seq}] = string(val[9:])
case db.KeyTypeNeed:
folder := binary.BigEndian.Uint32(key[1:])
name := nulString(key[1+4:])
needs[globalKey{folder, name}] = struct{}{}
case db.KeyTypeBlockList:
hash := string(key[1:])
blocklists[hash] = struct{}{}
case db.KeyTypeVersion:
hash := string(key[1:])
var v bep.Vector
if err := proto.Unmarshal(it.Value(), &v); err != nil {
fmt.Println("Unable to unmarshal Vector:", err)
success = false
continue
}
versions[hash] = &v
}
}
if localDeviceKey == 0 {
fmt.Println("Missing key for local device in device index (bailing out)")
success = false
return
}
var missingSeq []sequenceKey
for fk, fi := range fileInfos {
if fk.name != fi.Name {
fmt.Printf("Mismatching FileInfo name, %q (key) != %q (actual)\n", fk.name, fi.Name)
success = false
}
folder := folders[fk.folder]
if folder == "" {
fmt.Printf("Unknown folder ID %d for FileInfo %q\n", fk.folder, fk.name)
success = false
continue
}
if devices[fk.device] == "" {
fmt.Printf("Unknown device ID %d for FileInfo %q, folder %q\n", fk.folder, fk.name, folder)
success = false
}
if fk.device == localDeviceKey {
sk := sequenceKey{fk.folder, uint64(fi.Sequence)}
name, ok := sequences[sk]
if !ok {
fmt.Printf("Sequence entry missing for FileInfo %q, folder %q, seq %d\n", fi.Name, folder, fi.Sequence)
missingSeq = append(missingSeq, sk)
success = false
continue
}
if name != fi.Name {
fmt.Printf("Sequence entry refers to wrong name, %q (seq) != %q (FileInfo), folder %q, seq %d\n", name, fi.Name, folder, fi.Sequence)
success = false
}
}
if len(fi.Blocks) == 0 && len(fi.BlocksHash) != 0 {
key := string(fi.BlocksHash)
if _, ok := blocklists[key]; !ok {
fmt.Printf("Missing block list for file %q, block list hash %x\n", fi.Name, fi.BlocksHash)
success = false
} else {
usedBlocklists[key] = struct{}{}
}
}
if fi.VersionHash != nil {
key := string(fi.VersionHash)
if _, ok := versions[key]; !ok {
fmt.Printf("Missing version vector for file %q, version hash %x\n", fi.Name, fi.VersionHash)
success = false
} else {
usedVersions[key] = struct{}{}
}
}
_, ok := globals[globalKey{fk.folder, fk.name}]
if !ok {
fmt.Printf("Missing global for file %q\n", fi.Name)
success = false
continue
}
}
// Aggregate the ranges of missing sequence entries, print them
slices.SortFunc(missingSeq, func(a, b sequenceKey) int {
if a.folder != b.folder {
return cmp.Compare(a.folder, b.folder)
}
return cmp.Compare(a.sequence, b.sequence)
})
var folder uint32
var startSeq, prevSeq uint64
for _, sk := range missingSeq {
if folder != sk.folder || sk.sequence != prevSeq+1 {
if folder != 0 {
fmt.Printf("Folder %d missing %d sequence entries: #%d - #%d\n", folder, prevSeq-startSeq+1, startSeq, prevSeq)
}
startSeq = sk.sequence
folder = sk.folder
}
prevSeq = sk.sequence
}
if folder != 0 {
fmt.Printf("Folder %d missing %d sequence entries: #%d - #%d\n", folder, prevSeq-startSeq+1, startSeq, prevSeq)
}
for gk, vl := range globals {
folder := folders[gk.folder]
if folder == "" {
fmt.Printf("Unknown folder ID %d for VersionList %q\n", gk.folder, gk.name)
success = false
}
checkGlobal := func(i int, device []byte, version protocol.Vector, invalid, deleted bool) {
dev, ok := deviceToIDs[string(device)]
if !ok {
fmt.Printf("VersionList %q, folder %q refers to unknown device %q\n", gk.name, folder, device)
success = false
}
fi, ok := fileInfos[fileInfoKey{gk.folder, dev, gk.name}]
if !ok {
fmt.Printf("VersionList %q, folder %q, entry %d refers to unknown FileInfo\n", gk.name, folder, i)
success = false
}
fiv := fi.Version
if fi.VersionHash != nil {
fiv = versions[string(fi.VersionHash)]
}
if !protocol.VectorFromWire(fiv).Equal(version) {
fmt.Printf("VersionList %q, folder %q, entry %d, FileInfo version mismatch, %v (VersionList) != %v (FileInfo)\n", gk.name, folder, i, version, fi.Version)
success = false
}
ffi := protocol.FileInfoFromDB(fi)
if ffi.IsInvalid() != invalid {
fmt.Printf("VersionList %q, folder %q, entry %d, FileInfo invalid mismatch, %v (VersionList) != %v (FileInfo)\n", gk.name, folder, i, invalid, ffi.IsInvalid())
success = false
}
if ffi.IsDeleted() != deleted {
fmt.Printf("VersionList %q, folder %q, entry %d, FileInfo deleted mismatch, %v (VersionList) != %v (FileInfo)\n", gk.name, folder, i, deleted, ffi.IsDeleted())
success = false
}
}
for i, fv := range vl.Versions {
ver := protocol.VectorFromWire(fv.Version)
for _, device := range fv.Devices {
checkGlobal(i, device, ver, false, fv.Deleted)
}
for _, device := range fv.InvalidDevices {
checkGlobal(i, device, ver, true, fv.Deleted)
}
}
// If we need this file we should have a need entry for it. False
// positives from needsLocally for deleted files, where we might
// legitimately lack an entry if we never had it, and ignored files.
if needsLocally(vl) {
_, ok := needs[gk]
if !ok {
fv, _ := vlGetGlobal(vl)
devB, _ := fvFirstDevice(fv)
dev := deviceToIDs[string(devB)]
fi := protocol.FileInfoFromDB(fileInfos[fileInfoKey{gk.folder, dev, gk.name}])
if !fi.IsDeleted() && !fi.IsIgnored() {
fmt.Printf("Missing need entry for needed file %q, folder %q\n", gk.name, folder)
}
}
}
}
seenSeq := make(map[fileInfoKey]uint64)
for sk, name := range sequences {
folder := folders[sk.folder]
if folder == "" {
fmt.Printf("Unknown folder ID %d for sequence entry %d, %q\n", sk.folder, sk.sequence, name)
success = false
continue
}
if prev, ok := seenSeq[fileInfoKey{folder: sk.folder, name: name}]; ok {
fmt.Printf("Duplicate sequence entry for %q, folder %q, seq %d (prev %d)\n", name, folder, sk.sequence, prev)
success = false
}
seenSeq[fileInfoKey{folder: sk.folder, name: name}] = sk.sequence
fi, ok := fileInfos[fileInfoKey{sk.folder, localDeviceKey, name}]
if !ok {
fmt.Printf("Missing FileInfo for sequence entry %d, folder %q, %q\n", sk.sequence, folder, name)
success = false
continue
}
if fi.Sequence != int64(sk.sequence) {
fmt.Printf("Sequence mismatch for %q, folder %q, %d (key) != %d (FileInfo)\n", name, folder, sk.sequence, fi.Sequence)
success = false
}
}
for nk := range needs {
folder := folders[nk.folder]
if folder == "" {
fmt.Printf("Unknown folder ID %d for need entry %q\n", nk.folder, nk.name)
success = false
continue
}
vl, ok := globals[nk]
if !ok {
fmt.Printf("Missing global for need entry %q, folder %q\n", nk.name, folder)
success = false
continue
}
if !needsLocally(vl) {
fmt.Printf("Need entry for file we don't need, %q, folder %q\n", nk.name, folder)
success = false
}
}
if d := len(blocklists) - len(usedBlocklists); d > 0 {
fmt.Printf("%d block list entries out of %d needs GC\n", d, len(blocklists))
}
if d := len(versions) - len(usedVersions); d > 0 {
fmt.Printf("%d version entries out of %d needs GC\n", d, len(versions))
}
return nil
}
func needsLocally(vl *dbproto.VersionList) bool {
gfv, gok := vlGetGlobal(vl)
if !gok { // That's weird, but we hardly need something non-existent
return false
}
fv, ok := vlGet(vl, protocol.LocalDeviceID[:])
return db.Need(gfv, ok, protocol.VectorFromWire(fv.Version))
}
// Get returns a FileVersion that contains the given device and whether it has
// been found at all.
func vlGet(vl *dbproto.VersionList, device []byte) (*dbproto.FileVersion, bool) {
_, i, _, ok := vlFindDevice(vl, device)
if !ok {
return &dbproto.FileVersion{}, false
}
return vl.Versions[i], true
}
// GetGlobal returns the current global FileVersion. The returned FileVersion
// may be invalid, if all FileVersions are invalid. Returns false only if
// VersionList is empty.
func vlGetGlobal(vl *dbproto.VersionList) (*dbproto.FileVersion, bool) {
i := vlFindGlobal(vl)
if i == -1 {
return nil, false
}
return vl.Versions[i], true
}
// findGlobal returns the first version that isn't invalid, or if all versions are
// invalid just the first version (i.e. 0) or -1, if there's no versions at all.
func vlFindGlobal(vl *dbproto.VersionList) int {
for i := range vl.Versions {
if !fvIsInvalid(vl.Versions[i]) {
return i
}
}
if len(vl.Versions) == 0 {
return -1
}
return 0
}
// findDevice returns whether the device is in InvalidVersions or Versions and
// in InvalidDevices or Devices (true for invalid), the positions in the version
// and device slices and whether it has been found at all.
func vlFindDevice(vl *dbproto.VersionList, device []byte) (bool, int, int, bool) {
for i, v := range vl.Versions {
if j := deviceIndex(v.Devices, device); j != -1 {
return false, i, j, true
}
if j := deviceIndex(v.InvalidDevices, device); j != -1 {
return true, i, j, true
}
}
return false, -1, -1, false
}
func deviceIndex(devices [][]byte, device []byte) int {
for i, dev := range devices {
if bytes.Equal(device, dev) {
return i
}
}
return -1
}
func fvFirstDevice(fv *dbproto.FileVersion) ([]byte, bool) {
if len(fv.Devices) != 0 {
return fv.Devices[0], true
}
if len(fv.InvalidDevices) != 0 {
return fv.InvalidDevices[0], true
}
return nil, false
}
func fvIsInvalid(fv *dbproto.FileVersion) bool {
return fv == nil || len(fv.Devices) == 0
}
+10 -2
View File
@@ -14,12 +14,15 @@ import (
"github.com/alecthomas/kong" "github.com/alecthomas/kong"
"github.com/kballard/go-shellquote" "github.com/kballard/go-shellquote"
"github.com/syncthing/syncthing/cmd/syncthing/cmdutil"
"github.com/syncthing/syncthing/lib/config" "github.com/syncthing/syncthing/lib/config"
) )
type CLI struct { type CLI struct {
GUIAddress string `name:"gui-address" env:"STGUIADDRESS"` cmdutil.CommonOptions
GUIAPIKey string `name:"gui-apikey" env:"STGUIAPIKEY"` DataDir string `name:"data" placeholder:"PATH" env:"STDATADIR" help:"Set data directory (database and logs)"`
GUIAddress string `name:"gui-address"`
GUIAPIKey string `name:"gui-apikey"`
Show showCommand `cmd:"" help:"Show command group"` Show showCommand `cmd:"" help:"Show command group"`
Debug debugCommand `cmd:"" help:"Debug command group"` Debug debugCommand `cmd:"" help:"Debug command group"`
@@ -34,6 +37,11 @@ type Context struct {
} }
func (cli CLI) AfterApply(kongCtx *kong.Context) error { func (cli CLI) AfterApply(kongCtx *kong.Context) error {
err := cmdutil.SetConfigDataLocationsFromFlags(cli.HomeDir, cli.ConfDir, cli.DataDir)
if err != nil {
return fmt.Errorf("command line options: %w", err)
}
clientFactory := &apiClientFactory{ clientFactory := &apiClientFactory{
cfg: config.GUIConfiguration{ cfg: config.GUIConfiguration{
RawAddress: cli.GUIAddress, RawAddress: cli.GUIAddress,
+1 -2
View File
@@ -10,7 +10,6 @@ import (
"bufio" "bufio"
"errors" "errors"
"fmt" "fmt"
"net/http"
"path/filepath" "path/filepath"
"github.com/alecthomas/kong" "github.com/alecthomas/kong"
@@ -64,7 +63,7 @@ func (f *folderOverrideCommand) Run(ctx Context) error {
if err != nil { if err != nil {
return err return err
} }
if response.StatusCode != http.StatusOK { if response.StatusCode != 200 {
errStr := fmt.Sprint("Failed to override changes\nStatus code: ", response.StatusCode) errStr := fmt.Sprint("Failed to override changes\nStatus code: ", response.StatusCode)
bytes, err := responseToBArray(response) bytes, err := responseToBArray(response)
if err != nil { if err != nil {
+17 -2
View File
@@ -17,6 +17,8 @@ import (
"path/filepath" "path/filepath"
"github.com/syncthing/syncthing/lib/config" "github.com/syncthing/syncthing/lib/config"
"github.com/syncthing/syncthing/lib/db/backend"
"github.com/syncthing/syncthing/lib/locations"
) )
func responseToBArray(response *http.Response) ([]byte, error) { func responseToBArray(response *http.Response) ([]byte, error) {
@@ -112,7 +114,7 @@ func getConfig(c APIClient) (config.Configuration, error) {
return cfg, nil return cfg, nil
} }
func prettyPrintJSON(data any) error { func prettyPrintJSON(data interface{}) error {
enc := json.NewEncoder(os.Stdout) enc := json.NewEncoder(os.Stdout)
enc.SetIndent("", " ") enc.SetIndent("", " ")
return enc.Encode(data) return enc.Encode(data)
@@ -123,7 +125,7 @@ func prettyPrintResponse(response *http.Response) error {
if err != nil { if err != nil {
return err return err
} }
var data any var data interface{}
if err := json.Unmarshal(bytes, &data); err != nil { if err := json.Unmarshal(bytes, &data); err != nil {
return err return err
} }
@@ -131,6 +133,19 @@ func prettyPrintResponse(response *http.Response) error {
return prettyPrintJSON(data) return prettyPrintJSON(data)
} }
func getDB() (backend.Backend, error) {
return backend.OpenLevelDBRO(locations.Get(locations.Database))
}
func nulString(bs []byte) string {
for i := range bs {
if bs[i] == 0 {
return string(bs[:i])
}
}
return string(bs)
}
func normalizePath(path string) string { func normalizePath(path string) string {
return filepath.ToSlash(filepath.Clean(path)) return filepath.ToSlash(filepath.Clean(path))
} }
+16
View File
@@ -0,0 +1,16 @@
// Copyright (C) 2021 The Syncthing Authors.
//
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this file,
// You can obtain one at https://mozilla.org/MPL/2.0/.
package cmdutil
// CommonOptions are reused among several subcommands
type CommonOptions struct {
buildCommonOptions
ConfDir string `name:"config" placeholder:"PATH" env:"STCONFDIR" help:"Set configuration directory (config and keys)"`
HomeDir string `name:"home" placeholder:"PATH" env:"STHOMEDIR" help:"Set configuration and data directory"`
NoDefaultFolder bool `env:"STNODEFAULTFOLDER" help:"Don't create the \"default\" folder on first startup"`
SkipPortProbing bool `help:"Don't try to find free ports for GUI and listen addresses on first startup"`
}
@@ -5,9 +5,10 @@
// You can obtain one at https://mozilla.org/MPL/2.0/. // You can obtain one at https://mozilla.org/MPL/2.0/.
//go:build !windows //go:build !windows
// +build !windows
package main package cmdutil
type buildSpecificOptions struct { type buildCommonOptions struct {
HideConsole bool `hidden:""` HideConsole bool `hidden:""`
} }
@@ -4,8 +4,8 @@
// License, v. 2.0. If a copy of the MPL was not distributed with this file, // License, v. 2.0. If a copy of the MPL was not distributed with this file,
// You can obtain one at https://mozilla.org/MPL/2.0/. // You can obtain one at https://mozilla.org/MPL/2.0/.
package main package cmdutil
type buildSpecificOptions struct { type buildCommonOptions struct {
HideConsole bool `name:"no-console" help:"Hide console window (Always enabled on Windows 11 24H2 and later)" env:"STHIDECONSOLE"` HideConsole bool `name:"no-console" help:"Hide console window"`
} }
+35
View File
@@ -0,0 +1,35 @@
// Copyright (C) 2014 The Syncthing Authors.
//
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this file,
// You can obtain one at https://mozilla.org/MPL/2.0/.
package cmdutil
import (
"errors"
"github.com/syncthing/syncthing/lib/locations"
)
func SetConfigDataLocationsFromFlags(homeDir, confDir, dataDir string) error {
homeSet := homeDir != ""
confSet := confDir != ""
dataSet := dataDir != ""
switch {
case dataSet != confSet:
return errors.New("either both or none of --config and --data must be given, use --home to set both at once")
case homeSet && dataSet:
return errors.New("--home must not be used together with --config and --data")
case homeSet:
confDir = homeDir
dataDir = homeDir
fallthrough
case dataSet:
if err := locations.SetBaseDir(locations.ConfigBaseDir, confDir); err != nil {
return err
}
return locations.SetBaseDir(locations.DataBaseDir, dataDir)
}
return nil
}
+5 -11
View File
@@ -11,16 +11,12 @@ import (
"context" "context"
"crypto/sha256" "crypto/sha256"
"fmt" "fmt"
"log/slog"
"net/http" "net/http"
"os" "os"
"path/filepath" "path/filepath"
"slices" "slices"
"strings" "strings"
"time" "time"
"github.com/syncthing/syncthing/internal/slogutil"
"github.com/syncthing/syncthing/lib/build"
) )
const ( const (
@@ -32,12 +28,12 @@ const (
// directory to the crash reporting server as urlBase. Uploads are attempted // directory to the crash reporting server as urlBase. Uploads are attempted
// with the newest log first. // with the newest log first.
// //
// This can block for a long time. The context can set a final deadline // This can can block for a long time. The context can set a final deadline
// for this. // for this.
func uploadPanicLogs(ctx context.Context, urlBase, dir string) { func uploadPanicLogs(ctx context.Context, urlBase, dir string) {
files, err := filepath.Glob(filepath.Join(dir, "panic-*.log")) files, err := filepath.Glob(filepath.Join(dir, "panic-*.log"))
if err != nil { if err != nil {
slog.ErrorContext(ctx, "Failed to list panic logs", slogutil.Error(err)) l.Warnln("Failed to list panic logs:", err)
return return
} }
@@ -52,7 +48,7 @@ func uploadPanicLogs(ctx context.Context, urlBase, dir string) {
} }
if err := uploadPanicLog(ctx, urlBase, file); err != nil { if err := uploadPanicLog(ctx, urlBase, file); err != nil {
slog.ErrorContext(ctx, "Reporting crash", slogutil.Error(err)) l.Warnln("Reporting crash:", err)
} else { } else {
// Rename the log so we don't have to try to report it again. This // Rename the log so we don't have to try to report it again. This
// succeeds, or it does not. There is no point complaining about it. // succeeds, or it does not. There is no point complaining about it.
@@ -75,14 +71,13 @@ func uploadPanicLog(ctx context.Context, urlBase, file string) error {
data = filterLogLines(data) data = filterLogLines(data)
hash := fmt.Sprintf("%x", sha256.Sum256(data)) hash := fmt.Sprintf("%x", sha256.Sum256(data))
slog.InfoContext(ctx, "Reporting crash", slogutil.FilePath(filepath.Base(file)), slog.String("id", hash[:8])) l.Infof("Reporting crash found in %s (report ID %s) ...\n", filepath.Base(file), hash[:8])
url := fmt.Sprintf("%s/%s", urlBase, hash) url := fmt.Sprintf("%s/%s", urlBase, hash)
headReq, err := http.NewRequest(http.MethodHead, url, nil) headReq, err := http.NewRequest(http.MethodHead, url, nil)
if err != nil { if err != nil {
return err return err
} }
headReq.Header.Set("User-Agent", build.UserAgent())
// Set a reasonable timeout on the HEAD request // Set a reasonable timeout on the HEAD request
headCtx, headCancel := context.WithTimeout(ctx, headRequestTimeout) headCtx, headCancel := context.WithTimeout(ctx, headRequestTimeout)
@@ -103,7 +98,6 @@ func uploadPanicLog(ctx context.Context, urlBase, file string) error {
if err != nil { if err != nil {
return err return err
} }
putReq.Header.Set("User-Agent", build.UserAgent())
// Set a reasonable timeout on the PUT request // Set a reasonable timeout on the PUT request
putCtx, putCancel := context.WithTimeout(ctx, putRequestTimeout) putCtx, putCancel := context.WithTimeout(ctx, putRequestTimeout)
@@ -128,7 +122,7 @@ func uploadPanicLog(ctx context.Context, urlBase, file string) error {
func filterLogLines(data []byte) []byte { func filterLogLines(data []byte) []byte {
filtered := data[:0] filtered := data[:0]
matched := false matched := false
for line := range bytes.SplitSeq(data, []byte("\n")) { for _, line := range bytes.Split(data, []byte("\n")) {
switch { switch {
case !matched && bytes.HasPrefix(line, []byte("Panic ")): case !matched && bytes.HasPrefix(line, []byte("Panic ")):
// This begins the panic trace, set the matched flag and append. // This begins the panic trace, set the matched flag and append.
+4 -2
View File
@@ -6,6 +6,8 @@
package main package main
import "github.com/syncthing/syncthing/internal/slogutil" import (
"github.com/syncthing/syncthing/lib/logger"
)
func init() { slogutil.RegisterPackage("Main package") } var l = logger.DefaultLogger.NewFacility("main", "Main package")
+2 -2
View File
@@ -167,7 +167,7 @@ func (c *CLI) process(srcFs fs.Filesystem, dstFs fs.Filesystem, path string) err
var plainFd fs.File var plainFd fs.File
if dstFs != nil { if dstFs != nil {
if err := dstFs.MkdirAll(filepath.Dir(plainFi.Name), fs.ModePerm); err != nil { if err := dstFs.MkdirAll(filepath.Dir(plainFi.Name), 0o700); err != nil {
return fmt.Errorf("%s: %w", plainFi.Name, err) return fmt.Errorf("%s: %w", plainFi.Name, err)
} }
@@ -238,7 +238,7 @@ func (c *CLI) decryptFile(encFi *protocol.FileInfo, plainFi *protocol.FileInfo,
} }
// Verify the hash against the plaintext block info // Verify the hash against the plaintext block info
if !scanner.Validate(dec, plainBlock.Hash) { if !scanner.Validate(dec, plainBlock.Hash, 0) {
// The block decrypted correctly but fails the hash check. This // The block decrypted correctly but fails the hash check. This
// is odd and unexpected, but it it's still a valid block from // is odd and unexpected, but it it's still a valid block from
// the source. The file might have changed while we pulled it? // the source. The file might have changed while we pulled it?
+31 -15
View File
@@ -11,25 +11,42 @@ import (
"bufio" "bufio"
"context" "context"
"crypto/tls" "crypto/tls"
"errors"
"fmt" "fmt"
"log/slog"
"os" "os"
"github.com/syncthing/syncthing/cmd/syncthing/cmdutil"
"github.com/syncthing/syncthing/lib/config" "github.com/syncthing/syncthing/lib/config"
"github.com/syncthing/syncthing/lib/events" "github.com/syncthing/syncthing/lib/events"
"github.com/syncthing/syncthing/lib/fs" "github.com/syncthing/syncthing/lib/fs"
"github.com/syncthing/syncthing/lib/locations" "github.com/syncthing/syncthing/lib/locations"
"github.com/syncthing/syncthing/lib/logger"
"github.com/syncthing/syncthing/lib/osutil"
"github.com/syncthing/syncthing/lib/protocol" "github.com/syncthing/syncthing/lib/protocol"
"github.com/syncthing/syncthing/lib/syncthing" "github.com/syncthing/syncthing/lib/syncthing"
) )
type CLI struct { type CLI struct {
GUIUser string `placeholder:"STRING" help:"Specify new GUI authentication user name"` cmdutil.CommonOptions
GUIPassword string `placeholder:"STRING" help:"Specify new GUI authentication password (use - to read from standard input)"` GUIUser string `placeholder:"STRING" help:"Specify new GUI authentication user name"`
NoPortProbing bool `help:"Don't try to find free ports for GUI and listen addresses on first startup" env:"STNOPORTPROBING"` GUIPassword string `placeholder:"STRING" help:"Specify new GUI authentication password (use - to read from standard input)"`
} }
func (c *CLI) Run() error { func (c *CLI) Run(l logger.Logger) error {
if c.HideConsole {
osutil.HideConsole()
}
if c.HomeDir != "" {
if c.ConfDir != "" {
return errors.New("--home must not be used together with --config")
}
c.ConfDir = c.HomeDir
}
if c.ConfDir == "" {
c.ConfDir = locations.GetBaseDir(locations.ConfigBaseDir)
}
// Support reading the password from a pipe or similar // Support reading the password from a pipe or similar
if c.GUIPassword == "-" { if c.GUIPassword == "-" {
reader := bufio.NewReader(os.Stdin) reader := bufio.NewReader(os.Stdin)
@@ -40,13 +57,13 @@ func (c *CLI) Run() error {
c.GUIPassword = string(password) c.GUIPassword = string(password)
} }
if err := Generate(locations.GetBaseDir(locations.ConfigBaseDir), c.GUIUser, c.GUIPassword, c.NoPortProbing); err != nil { if err := Generate(l, c.ConfDir, c.GUIUser, c.GUIPassword, c.NoDefaultFolder, c.SkipPortProbing); err != nil {
return fmt.Errorf("failed to generate config and keys: %w", err) return fmt.Errorf("failed to generate config and keys: %w", err)
} }
return nil return nil
} }
func Generate(confDir, guiUser, guiPassword string, skipPortProbing bool) error { func Generate(l logger.Logger, confDir, guiUser, guiPassword string, noDefaultFolder, skipPortProbing bool) error {
dir, err := fs.ExpandTilde(confDir) dir, err := fs.ExpandTilde(confDir)
if err != nil { if err != nil {
return err return err
@@ -61,21 +78,20 @@ func Generate(confDir, guiUser, guiPassword string, skipPortProbing bool) error
certFile, keyFile := locations.Get(locations.CertFile), locations.Get(locations.KeyFile) certFile, keyFile := locations.Get(locations.CertFile), locations.Get(locations.KeyFile)
cert, err := tls.LoadX509KeyPair(certFile, keyFile) cert, err := tls.LoadX509KeyPair(certFile, keyFile)
if err == nil { if err == nil {
slog.Warn("Key exists; will not overwrite") l.Warnln("Key exists; will not overwrite.")
} else { } else {
cert, err = syncthing.GenerateCertificate(certFile, keyFile) cert, err = syncthing.GenerateCertificate(certFile, keyFile)
if err != nil { if err != nil {
return fmt.Errorf("create certificate: %w", err) return fmt.Errorf("create certificate: %w", err)
} }
} }
myID = protocol.NewDeviceID(cert.Certificate[0]) myID = protocol.NewDeviceID(cert.Certificate[0])
slog.Info("Calculated device ID", slog.String("device", myID.String())) l.Infoln("Device ID:", myID)
cfgFile := locations.Get(locations.ConfigFile) cfgFile := locations.Get(locations.ConfigFile)
cfg, _, err := config.Load(cfgFile, myID, events.NoopLogger) cfg, _, err := config.Load(cfgFile, myID, events.NoopLogger)
if fs.IsNotExist(err) { if fs.IsNotExist(err) {
if cfg, err = syncthing.DefaultConfig(cfgFile, myID, events.NoopLogger, skipPortProbing); err != nil { if cfg, err = syncthing.DefaultConfig(cfgFile, myID, events.NoopLogger, noDefaultFolder, skipPortProbing); err != nil {
return fmt.Errorf("create config: %w", err) return fmt.Errorf("create config: %w", err)
} }
} else if err != nil { } else if err != nil {
@@ -88,7 +104,7 @@ func Generate(confDir, guiUser, guiPassword string, skipPortProbing bool) error
var updateErr error var updateErr error
waiter, err := cfg.Modify(func(cfg *config.Configuration) { waiter, err := cfg.Modify(func(cfg *config.Configuration) {
updateErr = updateGUIAuthentication(&cfg.GUI, guiUser, guiPassword) updateErr = updateGUIAuthentication(l, &cfg.GUI, guiUser, guiPassword)
}) })
if err != nil { if err != nil {
return fmt.Errorf("modify config: %w", err) return fmt.Errorf("modify config: %w", err)
@@ -104,17 +120,17 @@ func Generate(confDir, guiUser, guiPassword string, skipPortProbing bool) error
return nil return nil
} }
func updateGUIAuthentication(guiCfg *config.GUIConfiguration, guiUser, guiPassword string) error { func updateGUIAuthentication(l logger.Logger, guiCfg *config.GUIConfiguration, guiUser, guiPassword string) error {
if guiUser != "" && guiCfg.User != guiUser { if guiUser != "" && guiCfg.User != guiUser {
guiCfg.User = guiUser guiCfg.User = guiUser
slog.Info("Updated GUI authentication user", "name", guiUser) l.Infoln("Updated GUI authentication user name:", guiUser)
} }
if guiPassword != "" && guiCfg.Password != guiPassword { if guiPassword != "" && guiCfg.Password != guiPassword {
if err := guiCfg.SetPassword(guiPassword); err != nil { if err := guiCfg.SetPassword(guiPassword); err != nil {
return fmt.Errorf("failed to set GUI authentication password: %w", err) return fmt.Errorf("failed to set GUI authentication password: %w", err)
} }
slog.Info("Updated GUI authentication password") l.Infoln("Updated GUI authentication password.")
} }
return nil return nil
} }
+2 -5
View File
@@ -8,21 +8,18 @@ package main
import ( import (
"fmt" "fmt"
"log/slog"
"os" "os"
"runtime" "runtime"
"runtime/pprof" "runtime/pprof"
"syscall" "syscall"
"time" "time"
"github.com/syncthing/syncthing/internal/slogutil"
) )
func startHeapProfiler() { func startHeapProfiler() {
slog.Debug("Starting heap profiling") l.Debugln("Starting heap profiling")
go func() { go func() {
err := saveHeapProfiles(1) // Only returns on error err := saveHeapProfiles(1) // Only returns on error
slog.Error("Heap profiler failed", slogutil.Error(err)) l.Warnln("Heap profiler failed:", err)
panic("Heap profiler failed") panic("Heap profiler failed")
}() }()
} }
+381 -520
View File
File diff suppressed because it is too large Load Diff
+60 -41
View File
@@ -9,31 +9,28 @@ package main
import ( import (
"bufio" "bufio"
"context" "context"
"errors"
"fmt" "fmt"
"io" "io"
"log/slog"
"os" "os"
"os/exec" "os/exec"
"os/signal" "os/signal"
"path/filepath" "path/filepath"
"strings" "strings"
"sync"
"syscall" "syscall"
"time" "time"
"github.com/syncthing/syncthing/internal/slogutil"
"github.com/syncthing/syncthing/lib/build" "github.com/syncthing/syncthing/lib/build"
"github.com/syncthing/syncthing/lib/fs" "github.com/syncthing/syncthing/lib/fs"
"github.com/syncthing/syncthing/lib/locations" "github.com/syncthing/syncthing/lib/locations"
"github.com/syncthing/syncthing/lib/osutil" "github.com/syncthing/syncthing/lib/osutil"
"github.com/syncthing/syncthing/lib/svcutil" "github.com/syncthing/syncthing/lib/svcutil"
"github.com/syncthing/syncthing/lib/sync"
) )
var ( var (
stdoutFirstLines []string // The first 10 lines of stdout stdoutFirstLines []string // The first 10 lines of stdout
stdoutLastLines []string // The last 50 lines of stdout stdoutLastLines []string // The last 50 lines of stdout
stdoutMut sync.Mutex stdoutMut = sync.NewMutex()
) )
const ( const (
@@ -46,7 +43,9 @@ const (
panicUploadNoticeWait = 10 * time.Second panicUploadNoticeWait = 10 * time.Second
) )
func (c *serveCmd) monitorMain() { func monitorMain(options serveOptions) {
l.SetPrefix("[monitor] ")
var dst io.Writer = os.Stdout var dst io.Writer = os.Stdout
logFile := locations.Get(locations.LogFile) logFile := locations.Get(locations.LogFile)
@@ -59,13 +58,13 @@ func (c *serveCmd) monitorMain() {
open := func(name string) (io.WriteCloser, error) { open := func(name string) (io.WriteCloser, error) {
return newAutoclosedFile(name, logFileAutoCloseDelay, logFileMaxOpenTime) return newAutoclosedFile(name, logFileAutoCloseDelay, logFileMaxOpenTime)
} }
if c.LogMaxSize > 0 { if options.LogMaxSize > 0 {
fileDst, err = newRotatedFile(logFile, open, int64(c.LogMaxSize), c.LogMaxFiles) fileDst, err = newRotatedFile(logFile, open, int64(options.LogMaxSize), options.LogMaxFiles)
} else { } else {
fileDst, err = open(logFile) fileDst, err = open(logFile)
} }
if err != nil { if err != nil {
slog.Error("Failed to set up logging to file, proceeding with logging to stdout only", slogutil.Error(err)) l.Warnln("Failed to set up logging to file, proceeding with logging to stdout only:", err)
} else { } else {
if build.IsWindows { if build.IsWindows {
// Translate line breaks to Windows standard // Translate line breaks to Windows standard
@@ -79,14 +78,14 @@ func (c *serveCmd) monitorMain() {
// Log to both stdout and file. // Log to both stdout and file.
dst = io.MultiWriter(dst, fileDst) dst = io.MultiWriter(dst, fileDst)
slog.Info("Saved log output", slogutil.FilePath(logFile)) l.Infof(`Log output saved to file "%s"`, logFile)
} }
} }
args := os.Args args := os.Args
binary, err := getBinary(args[0]) binary, err := getBinary(args[0])
if err != nil { if err != nil {
slog.Error("Failed to start the main Syncthing process", slogutil.Error(err)) l.Warnln("Error starting the main Syncthing process:", err)
panic("Error starting the main Syncthing process") panic("Error starting the main Syncthing process")
} }
var restarts [restartCounts]time.Time var restarts [restartCounts]time.Time
@@ -103,7 +102,7 @@ func (c *serveCmd) monitorMain() {
maybeReportPanics() maybeReportPanics()
if t := time.Since(restarts[0]); t < restartLoopThreshold { if t := time.Since(restarts[0]); t < restartLoopThreshold {
slog.Error("Too many restarts; not retrying further", slog.Int("count", restartCounts), slog.Any("interval", t)) l.Warnf("%d restarts in %v; not retrying further", restartCounts, t)
os.Exit(svcutil.ExitError.AsInt()) os.Exit(svcutil.ExitError.AsInt())
} }
@@ -123,10 +122,10 @@ func (c *serveCmd) monitorMain() {
panic(err) panic(err)
} }
slog.Debug("Starting syncthing") l.Debugln("Starting syncthing")
err = cmd.Start() err = cmd.Start()
if err != nil { if err != nil {
slog.Error("Failed to start the main Syncthing process", slogutil.Error(err)) l.Warnln("Error starting the main Syncthing process:", err)
panic("Error starting the main Syncthing process") panic("Error starting the main Syncthing process")
} }
@@ -135,10 +134,19 @@ func (c *serveCmd) monitorMain() {
stdoutLastLines = make([]string, 0, 50) stdoutLastLines = make([]string, 0, 50)
stdoutMut.Unlock() stdoutMut.Unlock()
var wg sync.WaitGroup wg := sync.NewWaitGroup()
wg.Go(func() { copyStderr(stderr, dst) }) wg.Add(1)
wg.Go(func() { copyStdout(stdout, dst) }) go func() {
copyStderr(stderr, dst)
wg.Done()
}()
wg.Add(1)
go func() {
copyStdout(stdout, dst)
wg.Done()
}()
exit := make(chan error) exit := make(chan error)
@@ -150,13 +158,13 @@ func (c *serveCmd) monitorMain() {
stopped := false stopped := false
select { select {
case s := <-stopSign: case s := <-stopSign:
slog.Info("Received signal; exiting", "signal", s) l.Infof("Signal %d received; exiting", s)
cmd.Process.Signal(sigTerm) cmd.Process.Signal(sigTerm)
err = <-exit err = <-exit
stopped = true stopped = true
case s := <-restartSign: case s := <-restartSign:
slog.Info("Received signal; restarting", "signal", s) l.Infof("Signal %d received; restarting", s)
cmd.Process.Signal(sigHup) cmd.Process.Signal(sigHup)
err = <-exit err = <-exit
@@ -168,33 +176,27 @@ func (c *serveCmd) monitorMain() {
os.Exit(svcutil.ExitSuccess.AsInt()) os.Exit(svcutil.ExitSuccess.AsInt())
} }
exiterr := &exec.ExitError{} if exiterr, ok := err.(*exec.ExitError); ok {
if errors.As(err, &exiterr) {
exitCode := exiterr.ExitCode() exitCode := exiterr.ExitCode()
switch { if stopped || options.NoRestart {
case stopped || c.NoRestart:
os.Exit(exitCode) os.Exit(exitCode)
}
case exitCode == svcutil.ExitUpgrade.AsInt(): if exitCode == svcutil.ExitUpgrade.AsInt() {
// Restart the monitor process to release the .old // Restart the monitor process to release the .old
// binary as part of the upgrade process. // binary as part of the upgrade process.
slog.Info("Restarting monitor...") l.Infoln("Restarting monitor...")
if err = restartMonitor(binary, args); err != nil { if err = restartMonitor(binary, args); err != nil {
slog.Error("Failed to restart monitor", slogutil.Error(err)) l.Warnln("Restart:", err)
} }
os.Exit(exitCode) os.Exit(exitCode)
case exitCode == svcutil.ExitNoRestart.AsInt():
// Requested to not restart the child
os.Exit(exitCode)
} }
} }
if c.NoRestart { if options.NoRestart {
os.Exit(svcutil.ExitError.AsInt()) os.Exit(svcutil.ExitError.AsInt())
} }
slog.Info("Syncthing exited", slogutil.Error(err)) l.Infoln("Syncthing exited:", err)
time.Sleep(restartPause) time.Sleep(restartPause)
if first { if first {
@@ -238,16 +240,32 @@ func copyStderr(stderr io.Reader, dst io.Writer) {
dst.Write([]byte(line)) 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)) panicFd, err = os.Create(locations.GetTimestamped(locations.PanicLog))
if err != nil { if err != nil {
slog.Error("Failed to create panic log", slogutil.Error(err)) l.Warnln("Create panic log:", err)
continue continue
} }
slog.Error("Panic detected, writing to file", slogutil.FilePath(panicFd.Name())) l.Warnf("Panic detected, writing to \"%s\"", panicFd.Name())
slog.Info("Please check for existing issues with similar panic message at https://github.com/syncthing/syncthing/issues/") if strings.Contains(line, "leveldb") && strings.Contains(line, "corrupt") {
slog.Info("If no issue with similar panic message exists, please create a new issue with the panic log attached") l.Warnln(`
*********************************************************************************
* Crash due to corrupt database. *
* *
* This crash usually occurs due to one of the following reasons: *
* - Syncthing being stopped abruptly (killed/loss of power) *
* - Bad hardware (memory/disk issues) *
* - Software that affects disk writes (SSD caching software and similar) *
* *
* Please see the following URL for instructions on how to recover: *
* https://docs.syncthing.net/users/faq.html#my-syncthing-database-is-corrupt *
*********************************************************************************
`)
} else {
l.Warnln("Please check for existing issues with similar panic message at https://github.com/syncthing/syncthing/issues/")
l.Warnln("If no issue with similar panic message exists, please create a new issue with the panic log attached")
}
stdoutMut.Lock() stdoutMut.Lock()
for _, line := range stdoutFirstLines { for _, line := range stdoutFirstLines {
@@ -428,6 +446,7 @@ func newAutoclosedFile(name string, closeDelay, maxOpenTime time.Duration) (*aut
name: name, name: name,
closeDelay: closeDelay, closeDelay: closeDelay,
maxOpenTime: maxOpenTime, maxOpenTime: maxOpenTime,
mut: sync.NewMutex(),
closed: make(chan struct{}), closed: make(chan struct{}),
closeTimer: time.NewTimer(time.Minute), closeTimer: time.NewTimer(time.Minute),
} }
@@ -484,7 +503,7 @@ func (f *autoclosedFile) ensureOpenLocked() error {
// We open the file for write only, and create it if it doesn't exist. // We open the file for write only, and create it if it doesn't exist.
flags := os.O_WRONLY | os.O_CREATE | os.O_APPEND flags := os.O_WRONLY | os.O_CREATE | os.O_APPEND
fd, err := os.OpenFile(f.name, flags, 0o666) fd, err := os.OpenFile(f.name, flags, 0o644)
if err != nil { if err != nil {
return err return err
} }
@@ -535,7 +554,7 @@ func maybeReportPanics() {
// Try to get a config to see if/where panics should be reported. // Try to get a config to see if/where panics should be reported.
cfg, err := loadOrDefaultConfig() cfg, err := loadOrDefaultConfig()
if err != nil { if err != nil {
slog.Error("Couldn't load config; not reporting crash") l.Warnln("Couldn't load config; not reporting crash")
return return
} }
@@ -555,7 +574,7 @@ func maybeReportPanics() {
case <-ctx.Done(): case <-ctx.Done():
return return
case <-time.After(panicUploadNoticeWait): case <-time.After(panicUploadNoticeWait):
slog.Warn("Uploading crash reports is taking a while, please wait") l.Warnln("Uploading crash reports is taking a while, please wait...")
} }
}() }()
+1
View File
@@ -5,6 +5,7 @@
// You can obtain one at https://mozilla.org/MPL/2.0/. // You can obtain one at https://mozilla.org/MPL/2.0/.
//go:build !windows //go:build !windows
// +build !windows
package main package main
+3 -21
View File
@@ -5,30 +5,12 @@
// You can obtain one at https://mozilla.org/MPL/2.0/. // You can obtain one at https://mozilla.org/MPL/2.0/.
//go:build windows //go:build windows
// +build windows
package main package main
import "golang.org/x/sys/windows" import "os/exec"
func openURL(url string) error { func openURL(url string) error {
urlPtr, err := windows.UTF16PtrFromString(url) return exec.Command("cmd.exe", "/C", "start "+url).Run()
if err != nil {
return err
}
verbPtr, err := windows.UTF16PtrFromString("open")
if err != nil {
return err
}
err = windows.ShellExecute(
0, // hwnd
verbPtr, // operation
urlPtr, // file
nil, // parameters
nil, // directory
windows.SW_SHOWNORMAL,
)
return err
} }
+29 -60
View File
@@ -5,6 +5,7 @@
// You can obtain one at https://mozilla.org/MPL/2.0/. // You can obtain one at https://mozilla.org/MPL/2.0/.
//go:build !solaris && !windows //go:build !solaris && !windows
// +build !solaris,!windows
package main package main
@@ -15,82 +16,50 @@ import (
"syscall" "syscall"
"time" "time"
"github.com/syncthing/syncthing/internal/db"
"github.com/syncthing/syncthing/lib/build"
"github.com/syncthing/syncthing/lib/locations"
"github.com/syncthing/syncthing/lib/osutil"
"github.com/syncthing/syncthing/lib/protocol" "github.com/syncthing/syncthing/lib/protocol"
"golang.org/x/exp/constraints"
) )
func startPerfStats(db db.DB) { func startPerfStats() {
go savePerfStats(fmt.Sprintf("perfstats-%d.csv", syscall.Getpid()), db) go savePerfStats(fmt.Sprintf("perfstats-%d.csv", syscall.Getpid()))
} }
func savePerfStats(file string, db db.DB) { func savePerfStats(file string) {
fd, err := os.Create(file) fd, err := os.Create(file)
if err != nil { if err != nil {
panic(err) panic(err)
} }
var prevTime time.Time var prevUsage int64
var curRus, prevRus syscall.Rusage var prevTime int64
var curMem, prevMem runtime.MemStats var rusage syscall.Rusage
var memstats runtime.MemStats
var prevIn, prevOut int64 var prevIn, prevOut int64
t0 := time.Now() t0 := time.Now()
syscall.Getrusage(syscall.RUSAGE_SELF, &prevRus)
runtime.ReadMemStats(&prevMem)
fmt.Fprintf(fd, "TIME_S\tCPU_S\tHEAP_KIB\tRSS_KIB\tNETIN_KBPS\tNETOUT_KBPS\tDBSIZE_KIB\tDBLOCAL\n")
for t := range time.NewTicker(250 * time.Millisecond).C { for t := range time.NewTicker(250 * time.Millisecond).C {
syscall.Getrusage(syscall.RUSAGE_SELF, &curRus) if err := syscall.Getrusage(syscall.RUSAGE_SELF, &rusage); err != nil {
runtime.ReadMemStats(&curMem) continue
}
curTime := time.Now().UnixNano()
timeDiff := curTime - prevTime
curUsage := rusage.Utime.Nano() + rusage.Stime.Nano()
usageDiff := curUsage - prevUsage
cpuUsagePercent := 100 * float64(usageDiff) / float64(timeDiff)
prevTime = curTime
prevUsage = curUsage
in, out := protocol.TotalInOut() in, out := protocol.TotalInOut()
timeDiff := t.Sub(prevTime) var inRate, outRate float64
if timeDiff > 0 {
rss := curRus.Maxrss inRate = float64(in-prevIn) / (float64(timeDiff) / 1e9) // bytes per second
if build.IsDarwin { outRate = float64(out-prevOut) / (float64(timeDiff) / 1e9) // bytes per second
rss /= 1024
} }
folders, _ := db.ListFolders()
var dbLocal int
for _, f := range folders {
local, _ := db.CountLocal(f, protocol.LocalDeviceID)
dbLocal += local.Files + local.Directories + local.Symlinks
}
fmt.Fprintf(
fd, "%.03f\t%f\t%d\t%d\t%.0f\t%.0f\t%d\t%d\n",
t.Sub(t0).Seconds(),
rate(cpusec(&prevRus), cpusec(&curRus), timeDiff, 1),
(curMem.Sys-curMem.HeapReleased)/1024,
rss,
rate(prevIn, in, timeDiff, 1e3),
rate(prevOut, out, timeDiff, 1e3),
osutil.DirSize(locations.Get(locations.Database))/1024,
dbLocal,
)
prevTime = t
prevRus = curRus
prevMem = curMem
prevIn, prevOut = in, out prevIn, prevOut = in, out
runtime.ReadMemStats(&memstats)
startms := int(t.Sub(t0).Seconds() * 1000)
fmt.Fprintf(fd, "%d\t%f\t%d\t%d\t%.0f\t%.0f\n", startms, cpuUsagePercent, memstats.Alloc, memstats.Sys-memstats.HeapReleased, inRate, outRate)
} }
} }
func cpusec(r *syscall.Rusage) float64 {
return float64(r.Utime.Nano()+r.Stime.Nano()) / float64(time.Second)
}
type number interface {
constraints.Float | constraints.Integer
}
func rate[T number](prev, cur T, d time.Duration, div float64) float64 {
diff := cur - prev
rate := float64(diff) / d.Seconds() / div
return rate
}
+2 -3
View File
@@ -5,10 +5,9 @@
// You can obtain one at https://mozilla.org/MPL/2.0/. // You can obtain one at https://mozilla.org/MPL/2.0/.
//go:build solaris || windows //go:build solaris || windows
// +build solaris windows
package main package main
import "github.com/syncthing/syncthing/internal/db" func startPerfStats() {
func startPerfStats(_ db.DB) {
} }
+1
View File
@@ -5,6 +5,7 @@
// You can obtain one at https://mozilla.org/MPL/2.0/. // You can obtain one at https://mozilla.org/MPL/2.0/.
//go:build go1.7 //go:build go1.7
// +build go1.7
package main package main
-19
View File
@@ -32,22 +32,3 @@
darwin: "20" darwin: "20"
linux: "3.2" linux: "3.2"
windows: "10.0" windows: "10.0"
- runtime: go1.25
requirements:
# macOS 12 (Monterey) per https://tip.golang.org/doc/go1.25#darwin
darwin: "21"
linux: "3.2"
windows: "10.0"
- runtime: go1.26 # no changes from 1.25
requirements:
darwin: "21"
linux: "3.2"
windows: "10.0"
- runtime: go1.27
requirements:
darwin: "22" # macOS 13 (Ventura)
linux: "3.2"
windows: "10.0"
+1 -1
View File
@@ -2,7 +2,7 @@
Name=Syncthing Web UI Name=Syncthing Web UI
GenericName=File synchronization UI GenericName=File synchronization UI
Comment=Opens Syncthing's Web UI in the default browser (Syncthing must already be started). Comment=Opens Syncthing's Web UI in the default browser (Syncthing must already be started).
Exec=syncthing browser Exec=syncthing --browser-only
Icon=syncthing Icon=syncthing
Terminal=false Terminal=false
Type=Application Type=Application
+7 -200
View File
@@ -7,215 +7,22 @@ StartLimitBurst=4
[Service] [Service]
User=%i User=%i
Environment="STLOGFORMATTIMESTAMP=" ExecStart=/usr/bin/syncthing serve --no-browser --no-restart --logflags=0
Environment="STLOGFORMATLEVELSTRING=false"
Environment="STLOGFORMATLEVELSYSLOG=true"
ExecStart=/usr/bin/syncthing serve --no-browser --no-restart
Restart=on-failure Restart=on-failure
RestartSec=1 RestartSec=1
SuccessExitStatus=3 4 SuccessExitStatus=3 4
RestartForceExitStatus=3 4 RestartForceExitStatus=3 4
############# # Hardening
# SANDBOXING
#############
#
# This section contains best-effort sandboxing of syncthing. Such sandboxing is
# useful to reduce the blast damage of a syncthing exploit.
#
# The sandboxing is "best-effort" only because some of these options are ignored
# if your systemd or kernel are too old or configured in unusual ways. Systemd
# should (but may not) tell you in the journal logs if that's the case. See the
# logs (after starting the service) with:
#
# journalctl --boot --pager-end --unit syncthing@<user-you-used>.service
#
# See systemd's analysis of syncthing's sandbox with:
#
# systemd-analyze security syncthing@<user-you-used>.service
#
# Most of these sandboxing options are documented in `man systemd.exec`.
#
# NOTE: Some of these options _appear_ redundant with each other... but
# depending on the version and configs of systemd and the kernel, some of the
# "redundant" options may be non-functional while others still work.
# We recommend leaving the "redundant" options in place.
# Makes /usr, /boot, /efi and /etc read-only.
ProtectSystem=full ProtectSystem=full
# Protect several system areas syncthing should not be touching. PrivateTmp=true
ProtectKernelTunables=true
ProtectKernelModules=true
ProtectKernelLogs=true
ProtectControlGroups=true
ProtectHostname=true
ProtectClock=true
# No new privileges through SUID/SGID binaries
NoNewPrivileges=true
# Prevents *setting* SUID/SGID bits on files/dirs
RestrictSUIDSGID=true
# Prevent memory pages that are both writable and executable. This kills JIT
# compilers, but syncthing is precompiled.
MemoryDenyWriteExecute=true
# Prevents creation of unprivileged user namespaces which are a significant
# source of privilege escalation exploits.
#
# (In 2023, Google saw 44% of kernel exploits using unpriv. user namespaces.
# Source: https://ubuntu.com/blog/ubuntu-23-10-restricted-unprivileged-user-namespaces)
#
# The service can still be placed *inside* such user namespaces (and is, through
# other sandboxing options), it just can't create any itself.
RestrictNamespaces=true
# RT task scheduling can be abused for denial-of-service
RestrictRealtime=true
# NOTE: This option is poorly named. It doesn't _restrict_ the listed families,
# it _allows_ the listed families. Unlisted ones are restricted.
#
# Specifically, notice the absence of AF_PACKET (raw packets).
# AF_UNIX is needed to support binding to UNIX sockets.
# AF_NETLINK is needed to support hotplugging of network devices and because
# otherwise we see the following (non-fatal) error on startup:
#
# Failed to list network interfaces (error="route ip+net: netlinkrib:
# address family not supported by protocol" log.pkg=upnp)
#
# This option does NOT affect systemd socket passing using .socket units.
RestrictAddressFamilies=AF_INET AF_INET6 AF_NETLINK AF_UNIX
# The lifetime limit of (superuser) capabilities that syncthing can acquire.
# This option _restricts_ capabilities.
#
# NOTE: This is set to `CAP_CHOWN CAP_FOWNER` to avoid breaking users that have
# set `AmbientCapabilities=CAP_CHOWN CAP_FOWNER` to enable the `syncOwnership`
# option as described in:
# https://docs.syncthing.net/users/autostart.html#permissions
#
# If you do not use the `syncOwnership` option, you can set this to:
# CapabilityBoundingSet=
CapabilityBoundingSet=CAP_CHOWN CAP_FOWNER
# Start with empty (superuser) capabilities.
# This option _expands_ capabilities.
# AmbientCapabilities should equal CapabilityBoundingSet.
#
# NOTE: IFF you wish to use the `syncOwnership` option, you must set this to:
# AmbientCapabilities=CAP_CHOWN CAP_FOWNER
# in a systemd drop-in file. Be aware that this gives syncthing the ability to
# change or ignore file ownership across the entire operating system.
AmbientCapabilities=
# Disables `personality` system call; it can be used for privilege escalation.
LockPersonality=true
# Prevents circumvention of restrictions through the use of x86 syscalls on
# x86-64 systems.
SystemCallArchitectures=native SystemCallArchitectures=native
# Clean up IPC objects after service stops. MemoryDenyWriteExecute=true
RemoveIPC=true NoNewPrivileges=true
# Create private namespace for System V IPC.
# NOTE: This does not apply to AF_UNIX sockets which are more commonly used.
PrivateIPC=true
# Completely isolated /tmp and /var/tmp
PrivateTmp=disconnected
# New /dev with safe virtual devices like /dev/null
PrivateDevices=true
# Allow access to devices explicitly listed with DeviceAllow and pseudo devices
# like /dev/null.
DevicePolicy=closed
# Creates a new PID namespace. /proc now contains only entries for processes
# in this PID namespace.
PrivatePIDs=true
# Make processes owned by other users hidden in /proc/
ProtectProc=invisible
# Prevent access to non-pid interfaces in /proc.
ProcSubset=pid
# System call allow-list. `@system-service` is a systemd-provided category that
# allows common syscalls needed for system services.
SystemCallFilter=@system-service
# Return EPERM when a disallowed syscall is made instead of killing the process.
SystemCallErrorNumber=EPERM
# Digits from left to right; disallow creation of files with:
# - special security-related bits like setuid/setgid
# - (no restrictions on file owner permissions)
# - group-writable access
# - world-readable access
# NOTE: The default value is 0022. We are only restricting special security bits
# and world-readable access.
# NOTE: Syncthing can still _explicitly_ change file permissions using `chmod`.
UMask=7027
# The default HOME folder for system users on Debian-like systems is
# /nonexistent, which should never exist.
# We prevent syncthing from accessing that folder it if was previously created
# through misconfiguration, or from creating it if it's (correctly) missing.
InaccessiblePaths=-/nonexistent
################## # Elevated permissions to sync ownership (disabled by default),
# OPTIONAL CONFIG # see https://docs.syncthing.net/advanced/folder-sync-ownership
##################
#
# Users that want to tweak this service file should add a systemd drop-in
# file to avoid changing the original file.
#
# Documentation describing drop-in files:
# https://www.freedesktop.org/software/systemd/man/latest/systemd.unit.html
#
# Example drop-in file location (assuming user "syncthing"):
# /etc/systemd/system/syncthing@syncthing.service.d/override.conf
#
## Elevated permissions to sync ownership (disabled by default),
## see https://docs.syncthing.net/advanced/folder-sync-ownership
##
## NOTE:
## - Use the same value for *both* of these options.
## - PrivateUsers=false must be set (false is the default, but you might have
## changed it to true in the "extra credit" section below).
#AmbientCapabilities=CAP_CHOWN CAP_FOWNER #AmbientCapabilities=CAP_CHOWN CAP_FOWNER
#CapabilityBoundingSet=CAP_CHOWN CAP_FOWNER
#########################
# EXTRA CREDIT FOR USERS
#########################
#
# Users that want to harden their systems further should set the following
# properties. (Also through a systemd drop-in file; see comments above.)
#
## Makes all of / read-only *except*:
## - /dev/, /proc/ and /sys/ (see other Protect* options)
## - ReadWritePaths=
## - StateDirectory=, LogsDirectory= and similar
##
## This cannot be enabled by default because we don't know which folders you wish to
## share. If enabling this option, enable it along with ReadWritePaths=, e.g.:
## ReadWritePaths=/my/shared/dir1 /my/shared/dir2
#ProtectSystem=strict
#
## When enabled, sets up a new user namespace. Maps the "root" user and group as
## well as the unit's own user and group to themselves and everything else to
## the "nobody" user and group.
## This is useful to securely detach the user and group databases used by the
## unit from the rest of the system, and thus to create an effective sandbox
## environment.
#PrivateUsers=true
#
## Makes /home, /root and /run/user *invisible* while allowing BindPaths= and
## BindReadOnlyPaths= to "carve out" access to parts of those dirs.
## (Use 'true' instead of 'tmpfs' if you don't need to carve out anything.)
##
## "Invisible" is superior to read-only provided by ProtectSystem=strict because
## it prevents information disclosure of private user data in case of service
## compromise.
#ProtectHome=tmpfs
#
## Disallow execution of all binaries. ExecPaths= below carves out exceptions.
## Can't be enabled by default due to the External File Versioning feature:
## https://docs.syncthing.net/users/versioning.html#external-file-versioning
##
## If you do not use that feature, you can enable both NoExecPaths and
## ExecPaths.
## If you do use that featuer, you can still use these options; just add
## the paths to the binaries you invoke to ExecPaths so they can be executed.
#NoExecPaths=/
## Allow execution of syncthing and system shared libraries.
## NOTE: If you are seeing an error like
## "Failed to execute /some/path/to/syncthing: Permission denied", this is the
## option you need to update to use your non-standard install location.
#ExecPaths=/usr/bin/syncthing /usr/lib
[Install] [Install]
WantedBy=multi-user.target WantedBy=multi-user.target
+1 -4
View File
@@ -5,10 +5,7 @@ StartLimitIntervalSec=60
StartLimitBurst=4 StartLimitBurst=4
[Service] [Service]
Environment="STLOGFORMATTIMESTAMP=" ExecStart=/usr/bin/syncthing serve --no-browser --no-restart --logflags=0
Environment="STLOGFORMATLEVELSTRING=false"
Environment="STLOGFORMATLEVELSYSLOG=true"
ExecStart=/usr/bin/syncthing serve --no-browser --no-restart
Restart=on-failure Restart=on-failure
RestartSec=1 RestartSec=1
SuccessExitStatus=3 4 SuccessExitStatus=3 4
+1 -1
View File
@@ -18,4 +18,4 @@ env STNORESTART=yes
respawn respawn
# the syncthing command Upstart is to execute when it is started up # the syncthing command Upstart is to execute when it is started up
exec $SYNCTHING_EXE --no-browser exec $SYNCTHING_EXE -no-browser

Some files were not shown because too many files have changed in this diff Show More