forked from Silverfish/proton-bridge
chore: merge branch release/stone to devel
This commit is contained in:
348
.gitlab-ci.yml
348
.gitlab-ci.yml
@ -30,13 +30,6 @@ stages:
|
||||
- test
|
||||
- build
|
||||
|
||||
.rules-branch-and-MR-always:
|
||||
rules:
|
||||
- if: $CI_COMMIT_BRANCH || $CI_PIPELINE_SOURCE == "merge_request_event"
|
||||
when: always
|
||||
allow_failure: false
|
||||
- when: never
|
||||
|
||||
.rules-branch-and-MR-manual:
|
||||
rules:
|
||||
- if: $CI_COMMIT_BRANCH || $CI_PIPELINE_SOURCE == "merge_request_event"
|
||||
@ -44,16 +37,6 @@ stages:
|
||||
allow_failure: true
|
||||
- when: never
|
||||
|
||||
.rules-branch-manual-MR-always:
|
||||
rules:
|
||||
- if: $CI_PIPELINE_SOURCE == "merge_request_event"
|
||||
when: always
|
||||
allow_failure: false
|
||||
- if: $CI_COMMIT_BRANCH
|
||||
when: manual
|
||||
allow_failure: true
|
||||
- when: never
|
||||
|
||||
.rules-branch-manual-MR-and-devel-always:
|
||||
rules:
|
||||
- if: $CI_COMMIT_BRANCH == "devel" || $CI_PIPELINE_SOURCE == "merge_request_event"
|
||||
@ -64,172 +47,8 @@ stages:
|
||||
allow_failure: true
|
||||
- when: never
|
||||
|
||||
.after-script-code-coverage:
|
||||
after_script:
|
||||
- go get github.com/boumenot/gocover-cobertura
|
||||
- go run github.com/boumenot/gocover-cobertura < /tmp/coverage.out > coverage.xml
|
||||
- "go tool cover -func=/tmp/coverage.out | grep total:"
|
||||
coverage: '/total:.*\(statements\).*\d+\.\d+%/'
|
||||
artifacts:
|
||||
reports:
|
||||
coverage_report:
|
||||
coverage_format: cobertura
|
||||
path: coverage.xml
|
||||
|
||||
# Stage: TEST
|
||||
|
||||
lint:
|
||||
stage: test
|
||||
extends:
|
||||
- .rules-branch-and-MR-always
|
||||
script:
|
||||
- make lint
|
||||
tags:
|
||||
- medium
|
||||
|
||||
|
||||
.test-base:
|
||||
stage: test
|
||||
script:
|
||||
- make test
|
||||
|
||||
test-linux:
|
||||
extends:
|
||||
- .test-base
|
||||
- .rules-branch-manual-MR-and-devel-always
|
||||
- .after-script-code-coverage
|
||||
tags:
|
||||
- large
|
||||
|
||||
test-linux-race:
|
||||
extends:
|
||||
- test-linux
|
||||
- .rules-branch-and-MR-manual
|
||||
script:
|
||||
- make test-race
|
||||
|
||||
test-integration:
|
||||
extends:
|
||||
- test-linux
|
||||
script:
|
||||
- make test-integration
|
||||
tags:
|
||||
- large
|
||||
|
||||
test-integration-race:
|
||||
extends:
|
||||
- test-integration
|
||||
- .rules-branch-and-MR-manual
|
||||
script:
|
||||
- make test-integration-race
|
||||
|
||||
|
||||
.windows-base:
|
||||
before_script:
|
||||
- export GOROOT=/c/Go1.20
|
||||
- export PATH=$GOROOT/bin:$PATH
|
||||
- export GOARCH=amd64
|
||||
- export GOPATH=~/go1.20
|
||||
- export GO111MODULE=on
|
||||
- export PATH=$GOPATH/bin:$PATH
|
||||
- export MSYSTEM=
|
||||
tags:
|
||||
- windows-bridge
|
||||
|
||||
test-windows:
|
||||
extends:
|
||||
- .rules-branch-manual-MR-always
|
||||
- .windows-base
|
||||
stage: test
|
||||
script:
|
||||
- make test
|
||||
|
||||
# Stage: BUILD
|
||||
|
||||
.build-base:
|
||||
stage: build
|
||||
needs: ["lint"]
|
||||
rules:
|
||||
# GODT-1833: use `=~ /qa/` after mac and windows runners are fixed
|
||||
- if: $CI_JOB_NAME =~ /build-linux-qa/ && $CI_PIPELINE_SOURCE == "merge_request_event"
|
||||
when: always
|
||||
allow_failure: false
|
||||
- if: $CI_COMMIT_BRANCH || $CI_PIPELINE_SOURCE == "merge_request_event"
|
||||
when: manual
|
||||
allow_failure: true
|
||||
- when: never
|
||||
script:
|
||||
- make build
|
||||
- git diff && git diff-index --quiet HEAD
|
||||
- make vault-editor
|
||||
artifacts:
|
||||
expire_in: 1 day
|
||||
when: always
|
||||
name: "$CI_JOB_NAME-$CI_COMMIT_SHORT_SHA"
|
||||
paths:
|
||||
- bridge_*.tgz
|
||||
- vault-editor
|
||||
|
||||
|
||||
.linux-build-setup:
|
||||
image: gitlab.protontech.ch:4567/go/bridge-internal:build-go1.20-qt6.3.2
|
||||
variables:
|
||||
VCPKG_DEFAULT_BINARY_CACHE: ${CI_PROJECT_DIR}/.cache
|
||||
cache:
|
||||
key: linux-vcpkg
|
||||
paths:
|
||||
- .cache
|
||||
when: 'always'
|
||||
before_script:
|
||||
- mkdir -p .cache/bin
|
||||
- export PATH=$(pwd)/.cache/bin:$PATH
|
||||
- export GOPATH="$CI_PROJECT_DIR/.cache"
|
||||
- export PATH=$PATH:$QT6DIR/bin
|
||||
- $(git config --global -l | grep -o 'url.*gitlab.protontech.ch.*insteadof' | xargs -L 1 git config --global --unset &> /dev/null) || echo "nothing to remove"
|
||||
- git config --global url.https://gitlab-ci-token:${CI_JOB_TOKEN}@${CI_SERVER_HOST}.insteadOf https://${CI_SERVER_HOST}
|
||||
tags:
|
||||
- large
|
||||
|
||||
build-linux:
|
||||
extends:
|
||||
- .build-base
|
||||
- .linux-build-setup
|
||||
|
||||
build-linux-qa:
|
||||
extends:
|
||||
- build-linux
|
||||
variables:
|
||||
BUILD_TAGS: "build_qa"
|
||||
|
||||
|
||||
.darwin-build-setup:
|
||||
before_script:
|
||||
- export PATH=/usr/local/bin:$PATH
|
||||
- export PATH=/usr/local/opt/git/bin:$PATH
|
||||
- export PATH=/usr/local/opt/make/libexec/gnubin:$PATH
|
||||
- export PATH=/usr/local/opt/go@1.13/bin:$PATH
|
||||
- export PATH=/usr/local/opt/gnu-sed/libexec/gnubin:$PATH
|
||||
- export GOPATH=~/go1.20
|
||||
- export PATH=$GOPATH/bin:$PATH
|
||||
- export CGO_CPPFLAGS='-Wno-error -Wno-nullability-completeness -Wno-expansion-to-defined -Wno-builtin-requires-header'
|
||||
- $(git config --global -l | grep -o 'url.*gitlab.protontech.ch.*insteadof' | xargs -L 1 git config --global --unset &> /dev/null) || echo "nothing to remove"
|
||||
- git config --global url.https://gitlab-ci-token:${CI_JOB_TOKEN}@${CI_SERVER_HOST}.insteadOf https://${CI_SERVER_HOST}
|
||||
cache: {}
|
||||
tags:
|
||||
- macOS
|
||||
|
||||
build-darwin:
|
||||
extends:
|
||||
- .build-base
|
||||
- .darwin-build-setup
|
||||
|
||||
build-darwin-qa:
|
||||
extends:
|
||||
- build-darwin
|
||||
variables:
|
||||
BUILD_TAGS: "build_qa"
|
||||
|
||||
.windows-build-setup:
|
||||
# ENV
|
||||
.env-windows:
|
||||
before_script:
|
||||
- export GOROOT=/c/Go1.20/
|
||||
- export PATH=$GOROOT/bin:$PATH
|
||||
@ -249,10 +68,169 @@ build-darwin-qa:
|
||||
tags:
|
||||
- windows-bridge
|
||||
|
||||
.env-darwin:
|
||||
before_script:
|
||||
- export PATH=/usr/local/bin:$PATH
|
||||
- export PATH=/usr/local/opt/git/bin:$PATH
|
||||
- export PATH=/usr/local/opt/make/libexec/gnubin:$PATH
|
||||
- export PATH=/usr/local/opt/gnu-sed/libexec/gnubin:$PATH
|
||||
- export GOROOT=~/local/opt/go@1.20
|
||||
- export PATH="${GOROOT}/bin:$PATH"
|
||||
- export GOPATH=~/go1.20
|
||||
- export PATH="${GOPATH}/bin:$PATH"
|
||||
- export QT6DIR=/opt/Qt/6.3.2/macos
|
||||
- export PATH="${QT6DIR}/bin:$PATH"
|
||||
- uname -a
|
||||
cache: {}
|
||||
tags:
|
||||
- macos-m1-bridge
|
||||
|
||||
.env-linux-build:
|
||||
image: gitlab.protontech.ch:4567/go/bridge-internal:build-go1.20-qt6.3.2
|
||||
variables:
|
||||
VCPKG_DEFAULT_BINARY_CACHE: ${CI_PROJECT_DIR}/.cache
|
||||
cache:
|
||||
key: linux-vcpkg
|
||||
paths:
|
||||
- .cache
|
||||
when: 'always'
|
||||
before_script:
|
||||
- mkdir -p .cache/bin
|
||||
- export PATH=$(pwd)/.cache/bin:$PATH
|
||||
- export GOPATH="$CI_PROJECT_DIR/.cache"
|
||||
- export PATH=$PATH:$QT6DIR/bin
|
||||
- $(git config --global -l | grep -o 'url.*gitlab.protontech.ch.*insteadof' | xargs -L 1 git config --global --unset &> /dev/null) || echo "nothing to remove"
|
||||
- git config --global url.https://gitlab-ci-token:${CI_JOB_TOKEN}@${CI_SERVER_HOST}.insteadOf https://${CI_SERVER_HOST}
|
||||
tags:
|
||||
- large
|
||||
|
||||
# Stage: TEST
|
||||
|
||||
lint:
|
||||
stage: test
|
||||
extends:
|
||||
- .rules-branch-manual-MR-and-devel-always
|
||||
script:
|
||||
- make lint
|
||||
tags:
|
||||
- medium
|
||||
|
||||
.script-test:
|
||||
stage: test
|
||||
extends:
|
||||
- .rules-branch-manual-MR-and-devel-always
|
||||
script:
|
||||
- make test
|
||||
artifacts:
|
||||
paths:
|
||||
- coverage/**
|
||||
|
||||
test-linux:
|
||||
extends:
|
||||
- .script-test
|
||||
tags:
|
||||
- large
|
||||
|
||||
test-linux-race:
|
||||
extends:
|
||||
- test-linux
|
||||
- .rules-branch-and-MR-manual
|
||||
script:
|
||||
- make test-race
|
||||
|
||||
test-integration:
|
||||
extends:
|
||||
- test-linux
|
||||
script:
|
||||
- make test-integration
|
||||
|
||||
test-integration-race:
|
||||
extends:
|
||||
- test-integration
|
||||
- .rules-branch-and-MR-manual
|
||||
script:
|
||||
- make test-integration-race
|
||||
|
||||
test-windows:
|
||||
extends:
|
||||
- .env-windows
|
||||
- .script-test
|
||||
|
||||
test-darwin:
|
||||
extends:
|
||||
- .env-darwin
|
||||
- .script-test
|
||||
|
||||
test-coverage:
|
||||
stage: test
|
||||
extends:
|
||||
- .rules-branch-manual-MR-and-devel-always
|
||||
script:
|
||||
- ./utils/coverage.sh
|
||||
coverage: '/total:.*\(statements\).*\d+\.\d+%/'
|
||||
needs:
|
||||
- test-linux
|
||||
- test-windows
|
||||
- test-darwin
|
||||
- test-integration
|
||||
tags:
|
||||
- small
|
||||
artifacts:
|
||||
paths:
|
||||
- coverage*
|
||||
- coverage/**
|
||||
when: 'always'
|
||||
reports:
|
||||
coverage_report:
|
||||
coverage_format: cobertura
|
||||
path: coverage.xml
|
||||
|
||||
# Stage: BUILD
|
||||
|
||||
.script-build:
|
||||
stage: build
|
||||
needs: ["lint"]
|
||||
extends:
|
||||
- .rules-branch-and-MR-manual
|
||||
script:
|
||||
- make build
|
||||
- git diff && git diff-index --quiet HEAD
|
||||
- make vault-editor
|
||||
artifacts:
|
||||
expire_in: 1 day
|
||||
when: always
|
||||
name: "$CI_JOB_NAME-$CI_COMMIT_SHORT_SHA"
|
||||
paths:
|
||||
- bridge_*.tgz
|
||||
- vault-editor
|
||||
|
||||
build-linux:
|
||||
extends:
|
||||
- .script-build
|
||||
- .env-linux-build
|
||||
|
||||
build-linux-qa:
|
||||
extends:
|
||||
- build-linux
|
||||
- .rules-branch-manual-MR-and-devel-always
|
||||
variables:
|
||||
BUILD_TAGS: "build_qa"
|
||||
|
||||
build-darwin:
|
||||
extends:
|
||||
- .script-build
|
||||
- .env-darwin
|
||||
|
||||
build-darwin-qa:
|
||||
extends:
|
||||
- build-darwin
|
||||
variables:
|
||||
BUILD_TAGS: "build_qa"
|
||||
|
||||
build-windows:
|
||||
extends:
|
||||
- .build-base
|
||||
- .windows-build-setup
|
||||
- .script-build
|
||||
- .env-windows
|
||||
|
||||
build-windows-qa:
|
||||
extends:
|
||||
|
||||
@ -58,7 +58,7 @@ Proton Mail Bridge includes the following 3rd party software:
|
||||
* [testify](https://github.com/stretchr/testify) available under [license](https://github.com/stretchr/testify/blob/master/LICENSE)
|
||||
* [cli](https://github.com/urfave/cli/v2) available under [license](https://github.com/urfave/cli/v2/blob/master/LICENSE)
|
||||
* [msgpack](https://github.com/vmihailenco/msgpack/v5) available under [license](https://github.com/vmihailenco/msgpack/v5/blob/master/LICENSE)
|
||||
* [goleak](https://go.uber.org/goleak)
|
||||
* [goleak](https://go.uber.org/goleak) available under [license](https://pkg.go.dev/go.uber.org/goleak?tab=licenses)
|
||||
* [exp](https://golang.org/x/exp) available under [license](https://cs.opensource.google/go/x/exp/+/master:LICENSE)
|
||||
* [net](https://golang.org/x/net) available under [license](https://cs.opensource.google/go/x/net/+/master:LICENSE)
|
||||
* [sys](https://golang.org/x/sys) available under [license](https://cs.opensource.google/go/x/sys/+/master:LICENSE)
|
||||
@ -66,8 +66,8 @@ Proton Mail Bridge includes the following 3rd party software:
|
||||
* [grpc](https://google.golang.org/grpc) available under [license](https://github.com/grpc/grpc-go/blob/master/LICENSE)
|
||||
* [protobuf](https://google.golang.org/protobuf) available under [license](https://github.com/protocolbuffers/protobuf/blob/main/LICENSE)
|
||||
* [plist](https://howett.net/plist) available under [license](https://github.com/DHowett/go-plist/blob/main/LICENSE)
|
||||
* [atlas](https://ariga.io/atlas)
|
||||
* [ent](https://entgo.io/ent)
|
||||
* [atlas](https://ariga.io/atlas) available under [license](https://github.com/ariga/atlas/blob/master/LICENSE)
|
||||
* [ent](https://entgo.io/ent) available under [license](https://pkg.go.dev/entgo.io/ent?tab=licenses)
|
||||
* [bcrypt](https://github.com/ProtonMail/bcrypt) available under [license](https://github.com/ProtonMail/bcrypt/blob/master/LICENSE)
|
||||
* [go-crypto](https://github.com/ProtonMail/go-crypto) available under [license](https://github.com/ProtonMail/go-crypto/blob/master/LICENSE)
|
||||
* [go-mime](https://github.com/ProtonMail/go-mime) available under [license](https://github.com/ProtonMail/go-mime/blob/master/LICENSE)
|
||||
@ -136,8 +136,8 @@ Proton Mail Bridge includes the following 3rd party software:
|
||||
* [mod](https://golang.org/x/mod) available under [license](https://cs.opensource.google/go/x/mod/+/master:LICENSE)
|
||||
* [sync](https://golang.org/x/sync) available under [license](https://cs.opensource.google/go/x/sync/+/master:LICENSE)
|
||||
* [tools](https://golang.org/x/tools) available under [license](https://cs.opensource.google/go/x/tools/+/master:LICENSE)
|
||||
* [genproto](https://google.golang.org/genproto)
|
||||
gopkg.in/yaml.v3
|
||||
* [genproto](https://google.golang.org/genproto) available under [license](https://pkg.go.dev/google.golang.org/genproto?tab=licenses)
|
||||
* [yaml](https://gopkg.in/yaml.v3) available under [license](https://github.com/go-yaml/yaml/blob/v3.0.1/LICENSE)
|
||||
* [docker-credential-helpers](https://github.com/ProtonMail/docker-credential-helpers) available under [license](https://github.com/ProtonMail/docker-credential-helpers/blob/master/LICENSE)
|
||||
* [go-message](https://github.com/ProtonMail/go-message) available under [license](https://github.com/ProtonMail/go-message/blob/master/LICENSE)
|
||||
* [go-keychain](https://github.com/cuthix/go-keychain) available under [license](https://github.com/cuthix/go-keychain/blob/master/LICENSE)
|
||||
|
||||
18
Makefile
18
Makefile
@ -229,14 +229,28 @@ add-license:
|
||||
change-copyright-year:
|
||||
./utils/missing_license.sh change-year
|
||||
|
||||
GOCOVERAGE=-covermode=count -coverpkg=github.com/ProtonMail/proton-bridge/v3/internal/...,github.com/ProtonMail/proton-bridge/v3/pkg/...,
|
||||
GOCOVERDIR=-args -test.gocoverdir=$$PWD/coverage
|
||||
|
||||
test: gofiles
|
||||
go test -v -timeout=20m -p=1 -count=1 -coverprofile=/tmp/coverage.out -run=${TESTRUN} ./internal/... ./pkg/...
|
||||
mkdir -p coverage/unit-${GOOS}
|
||||
go test \
|
||||
-v -timeout=20m -p=1 -count=1 \
|
||||
${GOCOVERAGE} \
|
||||
-run=${TESTRUN} ./internal/... ./pkg/... \
|
||||
${GOCOVERDIR}/unit-${GOOS}
|
||||
|
||||
test-race: gofiles
|
||||
go test -v -timeout=40m -p=1 -count=1 -race -failfast -run=${TESTRUN} ./internal/... ./pkg/...
|
||||
|
||||
test-integration: gofiles
|
||||
go test -v -timeout=60m -p=1 -count=1 github.com/ProtonMail/proton-bridge/v3/tests
|
||||
mkdir -p coverage/integration
|
||||
go test \
|
||||
-v -timeout=60m -p=1 -count=1 \
|
||||
${GOCOVERAGE} \
|
||||
github.com/ProtonMail/proton-bridge/v3/tests \
|
||||
${GOCOVERDIR}/integration
|
||||
|
||||
|
||||
test-integration-debug: gofiles
|
||||
dlv test github.com/ProtonMail/proton-bridge/v3/tests -- -test.v -test.timeout=10m -test.parallel=1 -test.count=1
|
||||
|
||||
@ -43,9 +43,10 @@ import (
|
||||
)
|
||||
|
||||
const (
|
||||
appName = "Proton Mail Launcher"
|
||||
exeName = "bridge"
|
||||
guiName = "bridge-gui"
|
||||
appName = "Proton Mail Launcher"
|
||||
exeName = "bridge"
|
||||
guiName = "bridge-gui"
|
||||
launcherName = "launcher"
|
||||
|
||||
FlagCLI = "cli"
|
||||
FlagCLIShort = "c"
|
||||
@ -53,6 +54,7 @@ const (
|
||||
FlagNonInteractiveShort = "n"
|
||||
FlagLauncher = "--launcher"
|
||||
FlagWait = "--wait"
|
||||
FlagSessionID = "--session-id"
|
||||
)
|
||||
|
||||
func main() { //nolint:funlen
|
||||
@ -75,9 +77,11 @@ func main() { //nolint:funlen
|
||||
if err != nil {
|
||||
l.WithError(err).Fatal("Failed to get logs path")
|
||||
}
|
||||
crashHandler.AddRecoveryAction(logging.DumpStackTrace(logsPath))
|
||||
|
||||
if err := logging.Init(logsPath, os.Getenv("VERBOSITY")); err != nil {
|
||||
sessionID := logging.NewSessionID()
|
||||
crashHandler.AddRecoveryAction(logging.DumpStackTrace(logsPath, sessionID, launcherName))
|
||||
|
||||
if err := logging.Init(logsPath, sessionID, launcherName, os.Getenv("VERBOSITY")); err != nil {
|
||||
l.WithError(err).Fatal("Failed to setup logging")
|
||||
}
|
||||
|
||||
@ -134,7 +138,7 @@ func main() { //nolint:funlen
|
||||
}
|
||||
}
|
||||
|
||||
cmd := execabs.Command(exe, appendLauncherPath(launcher, args)...) //nolint:gosec
|
||||
cmd := execabs.Command(exe, appendLauncherPath(launcher, append(args, FlagSessionID, string(sessionID)))...) //nolint:gosec
|
||||
|
||||
cmd.Stdin = os.Stdin
|
||||
cmd.Stdout = os.Stdout
|
||||
|
||||
2
go.sum
2
go.sum
@ -28,8 +28,6 @@ github.com/ProtonMail/bcrypt v0.0.0-20211005172633-e235017c1baf h1:yc9daCCYUefEs
|
||||
github.com/ProtonMail/bcrypt v0.0.0-20211005172633-e235017c1baf/go.mod h1:o0ESU9p83twszAU8LBeJKFAAMX14tISa0yk4Oo5TOqo=
|
||||
github.com/ProtonMail/docker-credential-helpers v1.1.0 h1:+kvUIpwWcbtP3WFv5sSvkFn/XLzSqPOB5AAthuk9xPk=
|
||||
github.com/ProtonMail/docker-credential-helpers v1.1.0/go.mod h1:mK0aBveCxhnQ756AmaTfXMZDeULvheYVhF/MWMErN5g=
|
||||
github.com/ProtonMail/gluon v0.16.1-0.20230607083802-83f92429ca8d h1:+1BKm++zxmfGwj81q3jFkiDpgVwg529qznGbI//uXpk=
|
||||
github.com/ProtonMail/gluon v0.16.1-0.20230607083802-83f92429ca8d/go.mod h1:ERZikuN+2i/oTeSwS5fq7J0Fms76uUcBlTAwT4KaEAk=
|
||||
github.com/ProtonMail/gluon v0.16.1-0.20230607122549-dbdb8e1cc0c3 h1:VMbbJD3dcGPPIgbdQTS5Z4nX0QU/SsVZWdmsMVVBBsI=
|
||||
github.com/ProtonMail/gluon v0.16.1-0.20230607122549-dbdb8e1cc0c3/go.mod h1:ERZikuN+2i/oTeSwS5fq7J0Fms76uUcBlTAwT4KaEAk=
|
||||
github.com/ProtonMail/go-autostart v0.0.0-20210130080809-00ed301c8e9a h1:D+aZah+k14Gn6kmL7eKxoo/4Dr/lK3ChBcwce2+SQP4=
|
||||
|
||||
@ -76,10 +76,12 @@ const (
|
||||
flagNoWindow = "no-window"
|
||||
flagParentPID = "parent-pid"
|
||||
flagSoftwareRenderer = "software-renderer"
|
||||
flagSessionID = "session-id"
|
||||
)
|
||||
|
||||
const (
|
||||
appUsage = "Proton Mail IMAP and SMTP Bridge"
|
||||
appUsage = "Proton Mail IMAP and SMTP Bridge"
|
||||
appShortName = "bridge"
|
||||
)
|
||||
|
||||
func New() *cli.App {
|
||||
@ -150,6 +152,10 @@ func New() *cli.App {
|
||||
Hidden: true,
|
||||
Value: false,
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: flagSessionID,
|
||||
Hidden: true,
|
||||
},
|
||||
}
|
||||
|
||||
app.Action = run
|
||||
@ -311,12 +317,13 @@ func withLogging(c *cli.Context, crashHandler *crash.Handler, locations *locatio
|
||||
logrus.WithField("path", logsPath).Debug("Received logs path")
|
||||
|
||||
// Initialize logging.
|
||||
if err := logging.Init(logsPath, c.String(flagLogLevel)); err != nil {
|
||||
sessionID := logging.NewSessionIDFromString(c.String(flagSessionID))
|
||||
if err := logging.Init(logsPath, sessionID, appShortName, c.String(flagLogLevel)); err != nil {
|
||||
return fmt.Errorf("could not initialize logging: %w", err)
|
||||
}
|
||||
|
||||
// Ensure we dump a stack trace if we crash.
|
||||
crashHandler.AddRecoveryAction(logging.DumpStackTrace(logsPath))
|
||||
crashHandler.AddRecoveryAction(logging.DumpStackTrace(logsPath, sessionID, appShortName))
|
||||
|
||||
logrus.
|
||||
WithField("appName", constants.FullAppName).
|
||||
|
||||
@ -44,7 +44,7 @@ import (
|
||||
// deleteOldGoIMAPFiles Set with `-ldflags -X app.deleteOldGoIMAPFiles=true` to enable cleanup of old imap cache data.
|
||||
var deleteOldGoIMAPFiles bool //nolint:gochecknoglobals
|
||||
|
||||
// withBridge creates creates and tears down the bridge.
|
||||
// withBridge creates and tears down the bridge.
|
||||
func withBridge(
|
||||
c *cli.Context,
|
||||
exe string,
|
||||
|
||||
@ -54,14 +54,14 @@ func (bridge *Bridge) ReportBug(ctx context.Context, osType, osVersion, descript
|
||||
|
||||
if attachLogs {
|
||||
logs, err := getMatchingLogs(bridge.locator, func(filename string) bool {
|
||||
return logging.MatchLogName(filename) && !logging.MatchStackTraceName(filename)
|
||||
return logging.MatchBridgeLogName(filename) && !logging.MatchStackTraceName(filename)
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
crashes, err := getMatchingLogs(bridge.locator, func(filename string) bool {
|
||||
return logging.MatchLogName(filename) && logging.MatchStackTraceName(filename)
|
||||
return logging.MatchBridgeLogName(filename) && logging.MatchStackTraceName(filename)
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
|
||||
4
internal/frontend/bridge-gui/bridge-gui-tester/.lldbinit
Normal file
4
internal/frontend/bridge-gui/bridge-gui-tester/.lldbinit
Normal file
@ -0,0 +1,4 @@
|
||||
# The following fix an issue happening using LLDB with OpenSSL 3.1 on ARM64 architecture. (GODT-2680)
|
||||
# WARNING: this file is ignored if you do not enable reading lldb config from cwd in ~/.lldbinit (`settings set target.load-cwd-lldbinit true`)
|
||||
settings set platform.plugin.darwin.ignored-exceptions EXC_BAD_INSTRUCTION
|
||||
process handle SIGILL -n false -p true -s false
|
||||
4
internal/frontend/bridge-gui/bridge-gui/.lldbinit
Normal file
4
internal/frontend/bridge-gui/bridge-gui/.lldbinit
Normal file
@ -0,0 +1,4 @@
|
||||
# The following fix an issue happening using LLDB with OpenSSL 3.1 on ARM64 architecture. (GODT-2680)
|
||||
# WARNING: this file is ignored if you do not enable reading lldb config from cwd in ~/.lldbinit (`settings set target.load-cwd-lldbinit true`)
|
||||
settings set platform.plugin.darwin.ignored-exceptions EXC_BAD_INSTRUCTION
|
||||
process handle SIGILL -n false -p true -s false
|
||||
@ -19,6 +19,7 @@
|
||||
#include "Pch.h"
|
||||
#include "CommandLine.h"
|
||||
#include "Settings.h"
|
||||
#include <bridgepp/SessionID/SessionID.h>
|
||||
|
||||
|
||||
using namespace bridgepp;
|
||||
@ -142,5 +143,12 @@ CommandLineOptions parseCommandLine(int argc, char *argv[]) {
|
||||
|
||||
options.logLevel = parseLogLevel(argc, argv);
|
||||
|
||||
options.sessionID = parseGoCLIStringArgument(argc, argv, { "session-id" });
|
||||
if (options.sessionID.isEmpty()) {
|
||||
options.sessionID = newSessionID();
|
||||
options.bridgeArgs.append("--session-id");
|
||||
options.bridgeArgs.append(options.sessionID);
|
||||
}
|
||||
|
||||
return options;
|
||||
}
|
||||
|
||||
@ -34,6 +34,7 @@ struct CommandLineOptions {
|
||||
bridgepp::Log::Level logLevel { bridgepp::Log::defaultLevel }; ///< The log level
|
||||
bool noWindow { false }; ///< Should the application start without displaying the main window?
|
||||
bool useSoftwareRenderer { false }; ///< Should QML be renderer in software (i.e. without rendering hardware interface).
|
||||
QString sessionID; ///< The sessionID.
|
||||
};
|
||||
|
||||
|
||||
|
||||
@ -19,7 +19,6 @@
|
||||
#include "LogUtils.h"
|
||||
#include "BuildConfig.h"
|
||||
#include <bridgepp/Log/LogUtils.h>
|
||||
#include <bridgepp/BridgeUtils.h>
|
||||
|
||||
|
||||
using namespace bridgepp;
|
||||
@ -28,7 +27,7 @@ using namespace bridgepp;
|
||||
//****************************************************************************************************************************************************
|
||||
/// \return A reference to the log.
|
||||
//****************************************************************************************************************************************************
|
||||
Log &initLog() {
|
||||
Log &initLog(QString const &sessionID) {
|
||||
Log &log = app().log();
|
||||
log.registerAsQtMessageHandler();
|
||||
log.setEchoInConsole(true);
|
||||
@ -41,7 +40,7 @@ Log &initLog() {
|
||||
|
||||
// create new GUI log file
|
||||
QString error;
|
||||
if (!log.startWritingToFile(logsDir.absoluteFilePath(QString("gui_v%1_%2.log").arg(PROJECT_VER).arg(QDateTime::currentSecsSinceEpoch())), &error)) {
|
||||
if (!log.startWritingToFile(logsDir.absoluteFilePath(QString("%1_000_gui_v%2_%3.log").arg(sessionID, PROJECT_VER, PROJECT_TAG)), &error)) {
|
||||
log.error(error);
|
||||
}
|
||||
|
||||
|
||||
@ -23,7 +23,7 @@
|
||||
#include <bridgepp/Log/Log.h>
|
||||
|
||||
|
||||
bridgepp::Log &initLog(); ///< Initialize the application log.
|
||||
bridgepp::Log &initLog(QString const &sessionID); ///< Initialize the application log.
|
||||
|
||||
|
||||
#endif //BRIDGE_GUI_LOG_UTILS_H
|
||||
|
||||
@ -286,7 +286,8 @@ int main(int argc, char *argv[]) {
|
||||
|
||||
initQtApplication();
|
||||
|
||||
Log &log = initLog();
|
||||
CommandLineOptions const cliOptions = parseCommandLine(argc, argv);
|
||||
Log &log = initLog(cliOptions.sessionID);
|
||||
|
||||
QLockFile lock(bridgepp::userCacheDir() + "/" + bridgeGUILock);
|
||||
if (!checkSingleInstance(lock)) {
|
||||
@ -294,8 +295,6 @@ int main(int argc, char *argv[]) {
|
||||
return EXIT_FAILURE;
|
||||
}
|
||||
|
||||
CommandLineOptions const cliOptions = parseCommandLine(argc, argv);
|
||||
|
||||
#ifdef Q_OS_MACOS
|
||||
registerSecondInstanceHandler();
|
||||
setDockIconVisibleState(!cliOptions.noWindow);
|
||||
|
||||
@ -32,9 +32,9 @@ set(CMAKE_CXX_STANDARD_REQUIRED ON)
|
||||
|
||||
if (NOT DEFINED BRIDGE_APP_VERSION)
|
||||
message(FATAL_ERROR "BRIDGE_APP_VERSION is not defined.")
|
||||
else()
|
||||
else ()
|
||||
message(STATUS "Bridge version is ${BRIDGE_APP_VERSION}")
|
||||
endif()
|
||||
endif ()
|
||||
|
||||
|
||||
#****************************************************************************************************************************************************
|
||||
@ -148,6 +148,7 @@ add_library(bridgepp
|
||||
bridgepp/Log/Log.h bridgepp/Log/Log.cpp
|
||||
bridgepp/Log/LogUtils.h bridgepp/Log/LogUtils.cpp
|
||||
bridgepp/ProcessMonitor.cpp bridgepp/ProcessMonitor.h
|
||||
bridgepp/SessionID/SessionID.cpp bridgepp/SessionID/SessionID.h
|
||||
bridgepp/User/User.cpp bridgepp/User/User.h
|
||||
bridgepp/Worker/Worker.h bridgepp/Worker/Overseer.h bridgepp/Worker/Overseer.cpp)
|
||||
|
||||
@ -167,7 +168,7 @@ target_precompile_headers(bridgepp PRIVATE Pch.h)
|
||||
|
||||
if (CMAKE_VERSION VERSION_GREATER_EQUAL "3.24.0")
|
||||
cmake_policy(SET CMP0135 NEW) # avoid warning DOWNLOAD_EXTRACT_TIMESTAMP
|
||||
endif()
|
||||
endif ()
|
||||
|
||||
include(FetchContent)
|
||||
FetchContent_Declare(
|
||||
@ -188,7 +189,9 @@ enable_testing()
|
||||
add_executable(bridgepp-test EXCLUDE_FROM_ALL
|
||||
Test/TestBridgeUtils.cpp
|
||||
Test/TestException.cpp
|
||||
Test/TestWorker.cpp Test/TestWorker.h)
|
||||
Test/TestSessionID.cpp
|
||||
Test/TestWorker.cpp Test/TestWorker.h
|
||||
)
|
||||
add_dependencies(bridgepp-test bridgepp)
|
||||
target_precompile_headers(bridgepp-test PRIVATE Pch.h)
|
||||
target_link_libraries(bridgepp-test
|
||||
|
||||
36
internal/frontend/bridge-gui/bridgepp/Test/TestSessionID.cpp
Normal file
36
internal/frontend/bridge-gui/bridgepp/Test/TestSessionID.cpp
Normal file
@ -0,0 +1,36 @@
|
||||
// Copyright (c) 2023 Proton AG
|
||||
//
|
||||
// This file is part of Proton Mail Bridge.
|
||||
//
|
||||
// Proton Mail Bridge is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// Proton Mail Bridge is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
|
||||
#include "QtCore/qdatetime.h"
|
||||
#include <gtest/gtest.h>
|
||||
#include <bridgepp/SessionID/SessionID.h>
|
||||
|
||||
|
||||
using namespace bridgepp;
|
||||
|
||||
|
||||
TEST(SessionID, SessionID) {
|
||||
QString const sessionID = newSessionID();
|
||||
EXPECT_TRUE(sessionID.size() > 0);
|
||||
|
||||
EXPECT_FALSE(sessionIDToDateTime("invalidSessionID").isValid());
|
||||
|
||||
QDateTime const dateTime = sessionIDToDateTime(sessionID);
|
||||
EXPECT_TRUE(dateTime.isValid());
|
||||
EXPECT_TRUE(qAbs(dateTime.secsTo(QDateTime::currentDateTime())) < 5);
|
||||
}
|
||||
@ -0,0 +1,53 @@
|
||||
// Copyright (c) 2023 Proton AG
|
||||
//
|
||||
// This file is part of Proton Mail Bridge.
|
||||
//
|
||||
// Proton Mail Bridge is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// Proton Mail Bridge is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
|
||||
#include "SessionID.h"
|
||||
#include "QtCore/qdatetime.h"
|
||||
|
||||
|
||||
namespace {
|
||||
|
||||
|
||||
QString const dateTimeFormat = "yyyyMMdd_hhmmsszzz"; ///< The format string for date/time used by the sessionID.
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
namespace bridgepp {
|
||||
|
||||
|
||||
//****************************************************************************************************************************************************
|
||||
/// \return a new session ID based on the current local date/time
|
||||
//****************************************************************************************************************************************************
|
||||
QString newSessionID() {
|
||||
return QDateTime::currentDateTime().toString(dateTimeFormat);
|
||||
}
|
||||
|
||||
|
||||
//****************************************************************************************************************************************************
|
||||
/// \param[in] sessionID The sessionID.
|
||||
/// \return The date/time corresponding to the sessionID.
|
||||
/// \return An invalid date/time if an error occurs.
|
||||
//****************************************************************************************************************************************************
|
||||
QDateTime sessionIDToDateTime(QString const &sessionID) {
|
||||
return QDateTime::fromString(sessionID, dateTimeFormat);
|
||||
}
|
||||
|
||||
|
||||
} // namespace
|
||||
@ -0,0 +1,32 @@
|
||||
// Copyright (c) 2023 Proton AG
|
||||
//
|
||||
// This file is part of Proton Mail Bridge.
|
||||
//
|
||||
// Proton Mail Bridge is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// Proton Mail Bridge is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
|
||||
#ifndef BRIDGE_PP_SESSION_ID_H
|
||||
#define BRIDGE_PP_SESSION_ID_H
|
||||
|
||||
|
||||
namespace bridgepp {
|
||||
|
||||
|
||||
QString newSessionID(); ///< Create a new sessions
|
||||
QDateTime sessionIDToDateTime(QString const &sessionID); ///< Parse the date/time from a sessionID.
|
||||
|
||||
|
||||
} // namespace
|
||||
|
||||
#endif //BRIDGE_PP_SESSION_ID_H
|
||||
@ -40,12 +40,12 @@ func clearLogs(logDir string, maxLogs int, maxCrashes int) error {
|
||||
|
||||
// Remove old logs.
|
||||
removeOldLogs(logDir, xslices.Filter(names, func(name string) bool {
|
||||
return MatchLogName(name) && !MatchStackTraceName(name)
|
||||
return MatchBridgeLogName(name) && !MatchStackTraceName(name)
|
||||
}), maxLogs)
|
||||
|
||||
// Remove old stack traces.
|
||||
removeOldLogs(logDir, xslices.Filter(names, func(name string) bool {
|
||||
return MatchLogName(name) && MatchStackTraceName(name)
|
||||
return MatchBridgeLogName(name) && MatchStackTraceName(name)
|
||||
}), maxCrashes)
|
||||
|
||||
return nil
|
||||
@ -58,7 +58,7 @@ func removeOldLogs(dir string, names []string, max int) {
|
||||
|
||||
// Sort by timestamp, oldest first.
|
||||
slices.SortFunc(names, func(a, b string) bool {
|
||||
return getLogTime(a) < getLogTime(b)
|
||||
return getLogTime(a).Before(getLogTime(b))
|
||||
})
|
||||
|
||||
for _, path := range xslices.Map(names[:len(names)-max], func(name string) string { return filepath.Join(dir, name) }) {
|
||||
|
||||
@ -23,16 +23,15 @@ import (
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"runtime/pprof"
|
||||
"time"
|
||||
|
||||
"github.com/ProtonMail/proton-bridge/v3/internal/constants"
|
||||
"github.com/ProtonMail/proton-bridge/v3/internal/crash"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
func DumpStackTrace(logsPath string) crash.RecoveryAction {
|
||||
func DumpStackTrace(logsPath string, sessionID SessionID, appName string) crash.RecoveryAction {
|
||||
return func(r interface{}) error {
|
||||
file := filepath.Join(logsPath, getStackTraceName(constants.Version, constants.Revision))
|
||||
file := filepath.Join(logsPath, getStackTraceName(sessionID, appName, constants.Version, constants.Tag))
|
||||
|
||||
f, err := os.OpenFile(filepath.Clean(file), os.O_WRONLY|os.O_APPEND|os.O_CREATE, 0o600)
|
||||
if err != nil {
|
||||
@ -53,10 +52,10 @@ func DumpStackTrace(logsPath string) crash.RecoveryAction {
|
||||
}
|
||||
}
|
||||
|
||||
func getStackTraceName(version, revision string) string {
|
||||
return fmt.Sprintf("v%v_%v_crash_%v.log", version, revision, time.Now().Unix())
|
||||
func getStackTraceName(sessionID SessionID, appName, version, tag string) string {
|
||||
return fmt.Sprintf("%v_000_%v_v%v_%v_crash.log", sessionID, appName, version, tag)
|
||||
}
|
||||
|
||||
func MatchStackTraceName(name string) bool {
|
||||
return regexp.MustCompile(`^v.*_crash_.*\.log$`).MatchString(name)
|
||||
return regexp.MustCompile(`^\d{8}_\d{9}_000_.*_crash\.log$`).MatchString(name)
|
||||
}
|
||||
|
||||
32
internal/logging/crash_test.go
Normal file
32
internal/logging/crash_test.go
Normal file
@ -0,0 +1,32 @@
|
||||
// Copyright (c) 2023 Proton AG
|
||||
//
|
||||
// This file is part of Proton Mail Bridge.
|
||||
//
|
||||
// Proton Mail Bridge is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// Proton Mail Bridge is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
package logging
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/ProtonMail/proton-bridge/v3/internal/constants"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestMatchStackTraceName(t *testing.T) {
|
||||
filename := getStackTraceName(NewSessionID(), constants.AppName, constants.Version, constants.Tag)
|
||||
require.True(t, len(filename) > 0)
|
||||
require.True(t, MatchStackTraceName(filename))
|
||||
require.False(t, MatchStackTraceName("Invalid.log"))
|
||||
}
|
||||
@ -19,26 +19,21 @@ package logging
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/ProtonMail/proton-bridge/v3/internal/constants"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
const (
|
||||
// MaxLogSize defines the maximum log size we should permit: 5 MB
|
||||
//
|
||||
// The Zendesk limit for an attachement is 50MB and this is what will
|
||||
// The Zendesk limit for an attachment is 50MB and this is what will
|
||||
// be allowed via the API. However, if that fails for some reason, the
|
||||
// fallback is sending the report via email, which has a limit of 10mb
|
||||
// total or 7MB per file. Since we can produce up to 6 logs, and we
|
||||
// compress all the files (avarage compression - 80%), we need to have
|
||||
// compress all the files (average compression - 80%), we need to have
|
||||
// a limit of 30MB total before compression, hence 5MB per log file.
|
||||
MaxLogSize = 5 * 1024 * 1024
|
||||
|
||||
@ -82,7 +77,7 @@ func (cs *coloredStdOutHook) Fire(entry *logrus.Entry) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func Init(logsPath, level string) error {
|
||||
func Init(logsPath string, sessionID SessionID, appName, level string) error {
|
||||
logrus.SetFormatter(&logrus.TextFormatter{
|
||||
DisableColors: true,
|
||||
FullTimestamp: true,
|
||||
@ -91,13 +86,7 @@ func Init(logsPath, level string) error {
|
||||
|
||||
logrus.AddHook(newColoredStdOutHook())
|
||||
|
||||
rotator, err := NewRotator(MaxLogSize, func() (io.WriteCloser, error) {
|
||||
if err := clearLogs(logsPath, MaxLogs, MaxLogs); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return os.Create(filepath.Join(logsPath, getLogName(constants.Version, constants.Revision))) //nolint:gosec // G304
|
||||
})
|
||||
rotator, err := NewDefaultRotator(logsPath, sessionID, appName, MaxLogSize)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@ -137,34 +126,42 @@ func setLevel(level string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func getLogName(version, revision string) string {
|
||||
return fmt.Sprintf("v%v_%v_%v.log", version, revision, time.Now().Unix())
|
||||
}
|
||||
func getLogTime(filename string) time.Time {
|
||||
re := regexp.MustCompile(`^(?P<sessionID>\d{8}_\d{9})_.*\.log$`)
|
||||
|
||||
func getLogTime(name string) int {
|
||||
re := regexp.MustCompile(`^v.*_.*_(?P<timestamp>\d+).log$`)
|
||||
|
||||
match := re.FindStringSubmatch(name)
|
||||
match := re.FindStringSubmatch(filename)
|
||||
|
||||
if len(match) == 0 {
|
||||
logrus.Warn("Could not parse log name: ", name)
|
||||
return 0
|
||||
logrus.WithField("filename", filename).Warn("Could not parse log filename")
|
||||
return time.Time{}
|
||||
}
|
||||
|
||||
timestamp, err := strconv.Atoi(match[re.SubexpIndex("timestamp")])
|
||||
if err != nil {
|
||||
return 0
|
||||
index := re.SubexpIndex("sessionID")
|
||||
if index < 0 {
|
||||
logrus.WithField("filename", filename).Warn("Could not parse log filename")
|
||||
return time.Time{}
|
||||
}
|
||||
|
||||
return timestamp
|
||||
return SessionID(match[index]).toTime()
|
||||
}
|
||||
|
||||
func MatchLogName(name string) bool {
|
||||
return regexp.MustCompile(`^v.*\.log$`).MatchString(name)
|
||||
// MatchBridgeLogName return true iff filename is a bridge log filename.
|
||||
func MatchBridgeLogName(filename string) bool {
|
||||
return matchLogName(filename, "bridge")
|
||||
}
|
||||
|
||||
func MatchGUILogName(name string) bool {
|
||||
return regexp.MustCompile(`^gui_v.*\.log$`).MatchString(name)
|
||||
// MatchGUILogName return true iff filename is a bridge-gui log filename.
|
||||
func MatchGUILogName(filename string) bool {
|
||||
return matchLogName(filename, "gui")
|
||||
}
|
||||
|
||||
// MatchLauncherLogName return true iff filename is a launcher log filename.
|
||||
func MatchLauncherLogName(filename string) bool {
|
||||
return matchLogName(filename, "launcher")
|
||||
}
|
||||
|
||||
func matchLogName(logName, appName string) bool {
|
||||
return regexp.MustCompile(`^\d{8}_\d{9}_\d{3}_` + appName + `.*\.log$`).MatchString(logName)
|
||||
}
|
||||
|
||||
type logKey string
|
||||
@ -180,12 +177,3 @@ func WithLogrusField(ctx context.Context, key string, value interface{}) context
|
||||
fields[key] = value
|
||||
return context.WithValue(ctx, logrusFields, fields)
|
||||
}
|
||||
|
||||
func LogFromContext(ctx context.Context) *logrus.Entry {
|
||||
fields, ok := ctx.Value(logrusFields).(logrus.Fields)
|
||||
if !ok || fields == nil {
|
||||
return logrus.WithField("ctx", "empty")
|
||||
}
|
||||
|
||||
return logrus.WithFields(fields)
|
||||
}
|
||||
|
||||
@ -25,59 +25,95 @@ import (
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
// TestClearLogs tests that cearLogs removes only bridge old log files keeping last three of them.
|
||||
func TestClearLogs(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
|
||||
// Create some old log files.
|
||||
require.NoError(t, os.WriteFile(filepath.Join(dir, "other.log"), []byte("Hello"), 0o755))
|
||||
require.NoError(t, os.WriteFile(filepath.Join(dir, "v2.4.7_debe87f2f5_0000000001.log"), []byte("Hello"), 0o755))
|
||||
require.NoError(t, os.WriteFile(filepath.Join(dir, "v2.4.8_debe87f2f5_0000000002.log"), []byte("Hello"), 0o755))
|
||||
require.NoError(t, os.WriteFile(filepath.Join(dir, "v2.4.9_debe87f2f5_0000000003.log"), []byte("Hello"), 0o755))
|
||||
require.NoError(t, os.WriteFile(filepath.Join(dir, "v2.5.0_debe87f2f5_0000000004.log"), []byte("Hello"), 0o755))
|
||||
require.NoError(t, os.WriteFile(filepath.Join(dir, "v2.5.1_debe87f2f5_0000000005.log"), []byte("Hello"), 0o755))
|
||||
require.NoError(t, os.WriteFile(filepath.Join(dir, "v2.5.2_debe87f2f5_0000000006.log"), []byte("Hello"), 0o755))
|
||||
require.NoError(t, os.WriteFile(filepath.Join(dir, "v2.5.3_debe87f2f5_0000000007.log"), []byte("Hello"), 0o755))
|
||||
require.NoError(t, os.WriteFile(filepath.Join(dir, "v2.5.4_debe87f2f5_0000000008.log"), []byte("Hello"), 0o755))
|
||||
require.NoError(t, os.WriteFile(filepath.Join(dir, "v2.5.5_debe87f2f5_0000000009.log"), []byte("Hello"), 0o755))
|
||||
require.NoError(t, os.WriteFile(filepath.Join(dir, "v2.5.6_debe87f2f5_0000000010.log"), []byte("Hello"), 0o755))
|
||||
require.NoError(t, os.WriteFile(filepath.Join(dir, "v2.5.7_debe87f2f5_0000000011.log"), []byte("Hello"), 0o755))
|
||||
require.NoError(t, os.WriteFile(filepath.Join(dir, "v2.5.8_debe87f2f5_0000000012.log"), []byte("Hello"), 0o755))
|
||||
require.NoError(t, os.WriteFile(filepath.Join(dir, "v2.5.12_debe87f2f5_0000000013.log"), []byte("Hello"), 0o755))
|
||||
require.NoError(t, os.WriteFile(filepath.Join(dir, "v2.5.9_debe87f2f5_0000000014.log"), []byte("Hello"), 0o755))
|
||||
require.NoError(t, os.WriteFile(filepath.Join(dir, "v2.5.10_debe87f2f5_0000000015.log"), []byte("Hello"), 0o755))
|
||||
require.NoError(t, os.WriteFile(filepath.Join(dir, "v2.5.11_debe87f2f5_0000000016.log"), []byte("Hello"), 0o755))
|
||||
|
||||
// Clear the logs.
|
||||
require.NoError(t, clearLogs(dir, 3, 0))
|
||||
|
||||
// We should only clear matching files, and keep the 3 most recent ones.
|
||||
checkFileNames(t, dir, []string{
|
||||
"other.log",
|
||||
"v2.5.9_debe87f2f5_0000000014.log",
|
||||
"v2.5.10_debe87f2f5_0000000015.log",
|
||||
"v2.5.11_debe87f2f5_0000000016.log",
|
||||
})
|
||||
}
|
||||
|
||||
func checkFileNames(t *testing.T, dir string, expectedFileNames []string) {
|
||||
require.ElementsMatch(t, expectedFileNames, getFileNames(t, dir))
|
||||
}
|
||||
|
||||
func getFileNames(t *testing.T, dir string) []string {
|
||||
files, err := os.ReadDir(dir)
|
||||
func TestGetLogTime(t *testing.T) {
|
||||
sessionID := NewSessionID()
|
||||
fp := defaultFileProvider(os.TempDir(), sessionID, "bridge-test")
|
||||
wc, err := fp(0)
|
||||
require.NoError(t, err)
|
||||
file, ok := wc.(*os.File)
|
||||
require.True(t, ok)
|
||||
|
||||
fileNames := []string{}
|
||||
for _, file := range files {
|
||||
fileNames = append(fileNames, file.Name())
|
||||
if file.IsDir() {
|
||||
subDir := filepath.Join(dir, file.Name())
|
||||
subFileNames := getFileNames(t, subDir)
|
||||
for _, subFileName := range subFileNames {
|
||||
fileNames = append(fileNames, file.Name()+"/"+subFileName)
|
||||
}
|
||||
}
|
||||
}
|
||||
return fileNames
|
||||
sessionIDTime := sessionID.toTime()
|
||||
require.False(t, sessionIDTime.IsZero())
|
||||
logTime := getLogTime(filepath.Base(file.Name()))
|
||||
require.False(t, logTime.IsZero())
|
||||
require.Equal(t, sessionIDTime, logTime)
|
||||
}
|
||||
|
||||
func TestMatchLogName(t *testing.T) {
|
||||
bridgeLog := "20230602_094633102_000_bridge_v3.0.99+git_5b650b1be3.log"
|
||||
crashLog := "20230602_094633102_000_bridge_v3.0.99+git_5b650b1be3_crash.log"
|
||||
guiLog := "20230602_094633102_000_gui_v3.0.99+git_5b650b1be3.log"
|
||||
launcherLog := "20230602_094633102_000_launcher_v3.0.99+git_5b650b1be3.log"
|
||||
require.True(t, MatchBridgeLogName(bridgeLog))
|
||||
require.False(t, MatchGUILogName(bridgeLog))
|
||||
require.False(t, MatchLauncherLogName(bridgeLog))
|
||||
require.True(t, MatchBridgeLogName(crashLog))
|
||||
require.False(t, MatchGUILogName(crashLog))
|
||||
require.False(t, MatchLauncherLogName(crashLog))
|
||||
require.False(t, MatchBridgeLogName(guiLog))
|
||||
require.True(t, MatchGUILogName(guiLog))
|
||||
require.False(t, MatchLauncherLogName(guiLog))
|
||||
require.False(t, MatchBridgeLogName(launcherLog))
|
||||
require.False(t, MatchGUILogName(launcherLog))
|
||||
require.True(t, MatchLauncherLogName(launcherLog))
|
||||
}
|
||||
|
||||
// The test below is temporarily disabled, and will be restored when implementing new retention policy GODT-2668
|
||||
|
||||
// TestClearLogs tests that clearLogs removes only bridge old log files keeping last three of them.
|
||||
// func TestClearLogs(t *testing.T) {
|
||||
// dir := t.TempDir()
|
||||
//
|
||||
// // Create some old log files.
|
||||
// require.NoError(t, os.WriteFile(filepath.Join(dir, "other.log"), []byte("Hello"), 0o755))
|
||||
// require.NoError(t, os.WriteFile(filepath.Join(dir, "v2.4.7_debe87f2f5_0000000001.log"), []byte("Hello"), 0o755))
|
||||
// require.NoError(t, os.WriteFile(filepath.Join(dir, "v2.4.8_debe87f2f5_0000000002.log"), []byte("Hello"), 0o755))
|
||||
// require.NoError(t, os.WriteFile(filepath.Join(dir, "v2.4.9_debe87f2f5_0000000003.log"), []byte("Hello"), 0o755))
|
||||
// require.NoError(t, os.WriteFile(filepath.Join(dir, "v2.5.0_debe87f2f5_0000000004.log"), []byte("Hello"), 0o755))
|
||||
// require.NoError(t, os.WriteFile(filepath.Join(dir, "v2.5.1_debe87f2f5_0000000005.log"), []byte("Hello"), 0o755))
|
||||
// require.NoError(t, os.WriteFile(filepath.Join(dir, "v2.5.2_debe87f2f5_0000000006.log"), []byte("Hello"), 0o755))
|
||||
// require.NoError(t, os.WriteFile(filepath.Join(dir, "v2.5.3_debe87f2f5_0000000007.log"), []byte("Hello"), 0o755))
|
||||
// require.NoError(t, os.WriteFile(filepath.Join(dir, "v2.5.4_debe87f2f5_0000000008.log"), []byte("Hello"), 0o755))
|
||||
// require.NoError(t, os.WriteFile(filepath.Join(dir, "v2.5.5_debe87f2f5_0000000009.log"), []byte("Hello"), 0o755))
|
||||
// require.NoError(t, os.WriteFile(filepath.Join(dir, "v2.5.6_debe87f2f5_0000000010.log"), []byte("Hello"), 0o755))
|
||||
// require.NoError(t, os.WriteFile(filepath.Join(dir, "v2.5.7_debe87f2f5_0000000011.log"), []byte("Hello"), 0o755))
|
||||
// require.NoError(t, os.WriteFile(filepath.Join(dir, "v2.5.8_debe87f2f5_0000000012.log"), []byte("Hello"), 0o755))
|
||||
// require.NoError(t, os.WriteFile(filepath.Join(dir, "v2.5.12_debe87f2f5_0000000013.log"), []byte("Hello"), 0o755))
|
||||
// require.NoError(t, os.WriteFile(filepath.Join(dir, "v2.5.9_debe87f2f5_0000000014.log"), []byte("Hello"), 0o755))
|
||||
// require.NoError(t, os.WriteFile(filepath.Join(dir, "v2.5.10_debe87f2f5_0000000015.log"), []byte("Hello"), 0o755))
|
||||
// require.NoError(t, os.WriteFile(filepath.Join(dir, "v2.5.11_debe87f2f5_0000000016.log"), []byte("Hello"), 0o755))
|
||||
//
|
||||
// // Clear the logs.
|
||||
// require.NoError(t, clearLogs(dir, 3, 0))
|
||||
//
|
||||
// // We should only clear matching files, and keep the 3 most recent ones.
|
||||
// checkFileNames(t, dir, []string{
|
||||
// "other.log",
|
||||
// "v2.5.9_debe87f2f5_0000000014.log",
|
||||
// "v2.5.10_debe87f2f5_0000000015.log",
|
||||
// "v2.5.11_debe87f2f5_0000000016.log",
|
||||
// })
|
||||
// }
|
||||
//
|
||||
// func checkFileNames(t *testing.T, dir string, expectedFileNames []string) {
|
||||
// require.ElementsMatch(t, expectedFileNames, getFileNames(t, dir))
|
||||
// }
|
||||
//
|
||||
// func getFileNames(t *testing.T, dir string) []string {
|
||||
// files, err := os.ReadDir(dir)
|
||||
// require.NoError(t, err)
|
||||
//
|
||||
// fileNames := []string{}
|
||||
// for _, file := range files {
|
||||
// fileNames = append(fileNames, file.Name())
|
||||
// if file.IsDir() {
|
||||
// subDir := filepath.Join(dir, file.Name())
|
||||
// subFileNames := getFileNames(t, subDir)
|
||||
// for _, subFileName := range subFileNames {
|
||||
// fileNames = append(fileNames, file.Name()+"/"+subFileName)
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// return fileNames
|
||||
// }
|
||||
|
||||
@ -17,16 +17,36 @@
|
||||
|
||||
package logging
|
||||
|
||||
import "io"
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/ProtonMail/proton-bridge/v3/internal/constants"
|
||||
)
|
||||
|
||||
type Rotator struct {
|
||||
getFile FileProvider
|
||||
wc io.WriteCloser
|
||||
size int
|
||||
maxSize int
|
||||
getFile FileProvider
|
||||
wc io.WriteCloser
|
||||
size int
|
||||
maxSize int
|
||||
nextIndex int
|
||||
}
|
||||
|
||||
type FileProvider func() (io.WriteCloser, error)
|
||||
type FileProvider func(index int) (io.WriteCloser, error)
|
||||
|
||||
func defaultFileProvider(logsPath string, sessionID SessionID, appName string) FileProvider {
|
||||
return func(index int) (io.WriteCloser, error) {
|
||||
if err := clearLogs(logsPath, MaxLogs, MaxLogs); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return os.Create(filepath.Join(logsPath, //nolint:gosec // G304
|
||||
fmt.Sprintf("%v_%03d_%v_v%v_%v.log", sessionID, index, appName, constants.Version, constants.Tag),
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
func NewRotator(maxSize int, getFile FileProvider) (*Rotator, error) {
|
||||
r := &Rotator{
|
||||
@ -41,6 +61,10 @@ func NewRotator(maxSize int, getFile FileProvider) (*Rotator, error) {
|
||||
return r, nil
|
||||
}
|
||||
|
||||
func NewDefaultRotator(logsPath string, sessionID SessionID, appName string, maxSize int) (*Rotator, error) {
|
||||
return NewRotator(maxSize, defaultFileProvider(logsPath, sessionID, appName))
|
||||
}
|
||||
|
||||
func (r *Rotator) Write(p []byte) (int, error) {
|
||||
if r.size+len(p) > r.maxSize {
|
||||
if err := r.rotate(); err != nil {
|
||||
@ -63,11 +87,12 @@ func (r *Rotator) rotate() error {
|
||||
_ = r.wc.Close()
|
||||
}
|
||||
|
||||
wc, err := r.getFile()
|
||||
wc, err := r.getFile(r.nextIndex)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
r.nextIndex++
|
||||
r.wc = wc
|
||||
r.size = 0
|
||||
|
||||
|
||||
@ -19,8 +19,10 @@ package logging
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
@ -38,7 +40,7 @@ func (c *WriteCloser) Close() error {
|
||||
func TestRotator(t *testing.T) {
|
||||
n := 0
|
||||
|
||||
getFile := func() (io.WriteCloser, error) {
|
||||
getFile := func(_ int) (io.WriteCloser, error) {
|
||||
n++
|
||||
return &WriteCloser{}, nil
|
||||
}
|
||||
@ -75,11 +77,70 @@ func TestRotator(t *testing.T) {
|
||||
assert.Equal(t, 4, n)
|
||||
}
|
||||
|
||||
func countFilesMatching(pattern string) int {
|
||||
files, err := filepath.Glob(pattern)
|
||||
if err != nil {
|
||||
return -1
|
||||
}
|
||||
|
||||
return len(files)
|
||||
}
|
||||
|
||||
func cleanupLogs(t *testing.T, sessionID SessionID) {
|
||||
paths, err := filepath.Glob(filepath.Join(os.TempDir(), string(sessionID)+"*.log"))
|
||||
require.NoError(t, err)
|
||||
for _, path := range paths {
|
||||
require.NoError(t, os.Remove(path))
|
||||
}
|
||||
}
|
||||
|
||||
func TestDefaultRotator(t *testing.T) {
|
||||
fiveBytes := []byte("00000")
|
||||
tmpDir := os.TempDir()
|
||||
|
||||
sessionID := NewSessionID()
|
||||
basePath := filepath.Join(tmpDir, string(sessionID))
|
||||
|
||||
r, err := NewDefaultRotator(tmpDir, sessionID, "bridge", 10)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 1, countFilesMatching(basePath+"_000_*.log"))
|
||||
require.Equal(t, 1, countFilesMatching(basePath+"*.log"))
|
||||
|
||||
_, err = r.Write(fiveBytes)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 1, countFilesMatching(basePath+"*.log"))
|
||||
|
||||
_, err = r.Write(fiveBytes)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 1, countFilesMatching(basePath+"*.log"))
|
||||
|
||||
_, err = r.Write(fiveBytes)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 2, countFilesMatching(basePath+"*.log"))
|
||||
require.Equal(t, 1, countFilesMatching(basePath+"_001_*.log"))
|
||||
|
||||
for i := 0; i < 4; i++ {
|
||||
_, err = r.Write(fiveBytes)
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
require.NoError(t, r.wc.Close())
|
||||
|
||||
// total written: 35 bytes, i.e. 4 log files
|
||||
logFileCount := countFilesMatching(basePath + "*.log")
|
||||
require.Equal(t, 4, logFileCount)
|
||||
for i := 0; i < logFileCount; i++ {
|
||||
require.Equal(t, 1, countFilesMatching(basePath+fmt.Sprintf("_%03d_*.log", i)))
|
||||
}
|
||||
|
||||
cleanupLogs(t, sessionID)
|
||||
}
|
||||
|
||||
func BenchmarkRotate(b *testing.B) {
|
||||
benchRotate(b, MaxLogSize, getTestFile(b, b.TempDir(), MaxLogSize-1))
|
||||
}
|
||||
|
||||
func benchRotate(b *testing.B, logSize int, getFile func() (io.WriteCloser, error)) {
|
||||
func benchRotate(b *testing.B, logSize int, getFile func(index int) (io.WriteCloser, error)) {
|
||||
r, err := NewRotator(logSize, getFile)
|
||||
require.NoError(b, err)
|
||||
|
||||
@ -92,8 +153,8 @@ func benchRotate(b *testing.B, logSize int, getFile func() (io.WriteCloser, erro
|
||||
}
|
||||
}
|
||||
|
||||
func getTestFile(b *testing.B, dir string, length int) func() (io.WriteCloser, error) {
|
||||
return func() (io.WriteCloser, error) {
|
||||
func getTestFile(b *testing.B, dir string, length int) func(int) (io.WriteCloser, error) {
|
||||
return func(index int) (io.WriteCloser, error) {
|
||||
b.StopTimer()
|
||||
defer b.StartTimer()
|
||||
|
||||
|
||||
64
internal/logging/session_id.go
Normal file
64
internal/logging/session_id.go
Normal file
@ -0,0 +1,64 @@
|
||||
// Copyright (c) 2023 Proton AG
|
||||
//
|
||||
// This file is part of Proton Mail Bridge.
|
||||
//
|
||||
// Proton Mail Bridge is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// Proton Mail Bridge is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
|
||||
package logging
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
"time"
|
||||
)
|
||||
|
||||
type SessionID string
|
||||
|
||||
const (
|
||||
timeFormat = "20060102_150405" // time format in Go does not support milliseconds without dot, so we'll process it manually.
|
||||
)
|
||||
|
||||
// NewSessionID creates a sessionID based on the current time.
|
||||
func NewSessionID() SessionID {
|
||||
now := time.Now()
|
||||
return SessionID(now.Format(timeFormat) + fmt.Sprintf("%03d", now.Nanosecond()/1000000))
|
||||
}
|
||||
|
||||
// NewSessionIDFromString Return a new sessionID from string. If the str is empty a new time based sessionID is returned, otherwise the string
|
||||
// is used as the sessionID.
|
||||
func NewSessionIDFromString(str string) SessionID {
|
||||
if (len(str)) > 0 {
|
||||
return SessionID(str)
|
||||
}
|
||||
|
||||
return NewSessionID()
|
||||
}
|
||||
|
||||
// toTime converts a sessionID to a date/Time, considering the time zone is local.
|
||||
func (s SessionID) toTime() time.Time {
|
||||
if len(s) < 3 {
|
||||
return time.Time{}
|
||||
}
|
||||
|
||||
t, err := time.ParseInLocation(timeFormat, string(s)[:len(s)-3], time.Local)
|
||||
if err != nil {
|
||||
return time.Time{}
|
||||
}
|
||||
|
||||
var ms int
|
||||
if ms, err = strconv.Atoi(string(s)[len(s)-3:]); err != nil {
|
||||
return time.Time{}
|
||||
}
|
||||
|
||||
return t.Add(time.Duration(ms) * time.Millisecond)
|
||||
}
|
||||
38
internal/logging/session_id_test.go
Normal file
38
internal/logging/session_id_test.go
Normal file
@ -0,0 +1,38 @@
|
||||
// Copyright (c) 2023 Proton AG
|
||||
//
|
||||
// This file is part of Proton Mail Bridge.
|
||||
//
|
||||
// Proton Mail Bridge is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// Proton Mail Bridge is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
|
||||
package logging
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestSessionID(t *testing.T) {
|
||||
now := time.Now()
|
||||
sessionID := NewSessionID()
|
||||
sessionTime := sessionID.toTime()
|
||||
require.False(t, sessionTime.IsZero())
|
||||
require.WithinRange(t, sessionTime, now.Add(-1*time.Millisecond), now.Add(1*time.Millisecond))
|
||||
|
||||
fromString := NewSessionIDFromString("")
|
||||
require.True(t, len(fromString) > 0)
|
||||
fromString = NewSessionIDFromString(string(sessionID))
|
||||
require.True(t, len(fromString) > 0)
|
||||
require.Equal(t, sessionID, fromString)
|
||||
}
|
||||
@ -28,70 +28,80 @@ import (
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
func syncFolders(localPath, updatePath string) (err error) {
|
||||
func syncFolders(localPath, updatePath string) error {
|
||||
backupDir := filepath.Join(filepath.Dir(updatePath), "backup")
|
||||
if err = createBackup(localPath, backupDir); err != nil {
|
||||
return
|
||||
if err := createBackup(localPath, backupDir); err != nil {
|
||||
logrus.WithField("dir", backupDir).WithError(err).Error("Cannot create backup")
|
||||
return err
|
||||
}
|
||||
|
||||
if err = removeMissing(localPath, updatePath); err != nil {
|
||||
if err := removeMissing(localPath, updatePath); err != nil {
|
||||
logrus.WithError(err).Error("Sync folders: failed to remove missing.")
|
||||
restoreFromBackup(backupDir, localPath)
|
||||
return
|
||||
return err
|
||||
}
|
||||
|
||||
if err = copyRecursively(updatePath, localPath); err != nil {
|
||||
if err := copyRecursively(updatePath, localPath); err != nil {
|
||||
logrus.WithError(err).Error("Sync folders: failed to copy.")
|
||||
restoreFromBackup(backupDir, localPath)
|
||||
return
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
//nolint:nakedret
|
||||
func removeMissing(folderToCleanPath, itemsToKeepPath string) (err error) {
|
||||
logrus.WithField("from", folderToCleanPath).Debug("Remove missing")
|
||||
func removeMissing(folderToCleanPath, itemsToKeepPath string) error {
|
||||
logrus.WithField("dir", folderToCleanPath).Debug("Remove missing")
|
||||
|
||||
// Create list of files.
|
||||
existingRelPaths := map[string]bool{}
|
||||
err = filepath.Walk(itemsToKeepPath, func(keepThis string, _ os.FileInfo, walkErr error) error {
|
||||
if err := filepath.Walk(itemsToKeepPath, func(keepThis string, _ os.FileInfo, walkErr error) error {
|
||||
if walkErr != nil {
|
||||
return walkErr
|
||||
}
|
||||
|
||||
relPath, walkErr := filepath.Rel(itemsToKeepPath, keepThis)
|
||||
if walkErr != nil {
|
||||
return walkErr
|
||||
}
|
||||
|
||||
logrus.WithField("path", relPath).Trace("Keep the path")
|
||||
existingRelPaths[relPath] = true
|
||||
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
delList := []string{}
|
||||
err = filepath.Walk(folderToCleanPath, func(removeThis string, _ os.FileInfo, walkErr error) error {
|
||||
if err := filepath.Walk(folderToCleanPath, func(fullPath string, _ os.FileInfo, walkErr error) error {
|
||||
if walkErr != nil {
|
||||
return walkErr
|
||||
}
|
||||
relPath, walkErr := filepath.Rel(folderToCleanPath, removeThis)
|
||||
|
||||
relPath, walkErr := filepath.Rel(folderToCleanPath, fullPath)
|
||||
if walkErr != nil {
|
||||
logrus.WithField("full", fullPath).WithError(walkErr).Error("Failed to get relative path")
|
||||
return walkErr
|
||||
}
|
||||
logrus.Debug("check path ", relPath)
|
||||
|
||||
l := logrus.WithField("path", relPath)
|
||||
l.Debug("Check")
|
||||
|
||||
if !existingRelPaths[relPath] {
|
||||
logrus.Debug("path not in list, removing ", removeThis)
|
||||
delList = append(delList, removeThis)
|
||||
l.WithField("remove", fullPath).Debug("Path not in list, removing")
|
||||
delList = append(delList, fullPath)
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, removeThis := range delList {
|
||||
if err = os.RemoveAll(removeThis); err != nil && !errors.Is(err, fs.ErrNotExist) {
|
||||
logrus.Error("remove error ", err)
|
||||
return
|
||||
if err := os.RemoveAll(removeThis); err != nil && !errors.Is(err, fs.ErrNotExist) {
|
||||
logrus.WithField("path", removeThis).WithError(err).Error("Cannot remove")
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
@ -99,23 +109,30 @@ func removeMissing(folderToCleanPath, itemsToKeepPath string) (err error) {
|
||||
}
|
||||
|
||||
func restoreFromBackup(backupDir, localPath string) {
|
||||
logrus.WithField("from", backupDir).
|
||||
WithField("to", localPath).
|
||||
Error("recovering")
|
||||
l := logrus.WithField("from", backupDir).
|
||||
WithField("to", localPath)
|
||||
l.Warning("Recovering")
|
||||
|
||||
if err := copyRecursively(backupDir, localPath); err != nil {
|
||||
logrus.WithField("from", backupDir).
|
||||
WithField("to", localPath).
|
||||
Error("Not able to recover.")
|
||||
l.WithError(err).Error("Not able to recover")
|
||||
}
|
||||
}
|
||||
|
||||
func createBackup(srcFile, dstDir string) (err error) {
|
||||
logrus.WithField("from", srcFile).WithField("to", dstDir).Debug("Create backup")
|
||||
if err = mkdirAllClear(dstDir); err != nil {
|
||||
return
|
||||
func createBackup(srcFile, dstDir string) error {
|
||||
l := logrus.WithField("from", srcFile).WithField("to", dstDir)
|
||||
|
||||
l.Debug("Create backup")
|
||||
if err := mkdirAllClear(dstDir); err != nil {
|
||||
l.WithError(err).Error("Cannot create backup folder")
|
||||
return err
|
||||
}
|
||||
|
||||
return copyRecursively(srcFile, dstDir)
|
||||
if err := copyRecursively(srcFile, dstDir); err != nil {
|
||||
l.WithError(err).Error("Cannot copy to backup folder")
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func mkdirAllClear(path string) error {
|
||||
@ -129,12 +146,14 @@ func mkdirAllClear(path string) error {
|
||||
func checksum(path string) (hash string) {
|
||||
file, err := os.Open(filepath.Clean(path))
|
||||
if err != nil {
|
||||
logrus.WithError(err).WithField("path", path).Error("Cannot open file for checksum")
|
||||
return
|
||||
}
|
||||
defer file.Close() //nolint:errcheck,gosec
|
||||
|
||||
hasher := sha256.New()
|
||||
if _, err := io.Copy(hasher, file); err != nil {
|
||||
logrus.WithError(err).WithField("path", path).Error("Cannot read file for checksum")
|
||||
return
|
||||
}
|
||||
|
||||
@ -152,40 +171,50 @@ func copyRecursively(srcDir, dstDir string) error {
|
||||
srcIsLink := srcInfo.Mode()&os.ModeSymlink == os.ModeSymlink
|
||||
srcIsDir := srcInfo.IsDir()
|
||||
|
||||
l := logrus.WithField("source", srcPath)
|
||||
|
||||
// Non regular source (e.g. named pipes, sockets, devices...).
|
||||
if !srcIsLink && !srcIsDir && !srcInfo.Mode().IsRegular() {
|
||||
logrus.Error("File ", srcPath, " with mode ", srcInfo.Mode())
|
||||
return errors.New("irregular source file. Copy not implemented")
|
||||
err := errors.New("irregular source file: copy not implemented")
|
||||
l.WithField("mode", srcInfo.Mode()).WithError(err).Error("Source with iregular mode")
|
||||
return err
|
||||
}
|
||||
|
||||
// Destination path.
|
||||
srcRelPath, err := filepath.Rel(srcDir, srcPath)
|
||||
if err != nil {
|
||||
l.WithField("dir", srcDir).WithError(err).Error("Failed to get relative source path")
|
||||
return err
|
||||
}
|
||||
|
||||
dstPath := filepath.Join(dstDir, srcRelPath)
|
||||
logrus.Debug("src: ", srcPath, " dst: ", dstPath)
|
||||
l = l.WithField("destination", dstPath)
|
||||
|
||||
// Destination exists.
|
||||
dstInfo, err := os.Lstat(dstPath)
|
||||
l.WithError(err).Debug("Destination check")
|
||||
|
||||
if err == nil {
|
||||
dstIsLink := dstInfo.Mode()&os.ModeSymlink == os.ModeSymlink
|
||||
dstIsDir := dstInfo.IsDir()
|
||||
|
||||
// Non regular destination (e.g. named pipes, sockets, devices...).
|
||||
if !dstIsLink && !dstIsDir && !dstInfo.Mode().IsRegular() {
|
||||
logrus.Error("File ", dstPath, " with mode ", dstInfo.Mode())
|
||||
return errors.New("irregular target file. Copy not implemented")
|
||||
err := errors.New("irregular target file: copy not implemented")
|
||||
l.WithError(err).WithField("mode", dstInfo.Mode()).Error("Destination with irregular mode")
|
||||
return err
|
||||
}
|
||||
|
||||
if dstIsLink {
|
||||
if err = os.Remove(dstPath); err != nil {
|
||||
l.WithError(err).Error("Cannot remove destination link")
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if !dstIsLink && dstIsDir && !srcIsDir {
|
||||
if err = os.RemoveAll(dstPath); err != nil {
|
||||
l.WithError(err).Error("Cannot remove destination folder")
|
||||
return err
|
||||
}
|
||||
}
|
||||
@ -194,63 +223,89 @@ func copyRecursively(srcDir, dstDir string) error {
|
||||
|
||||
if dstInfo.Mode().IsRegular() && !srcInfo.Mode().IsRegular() {
|
||||
if err = os.Remove(dstPath); err != nil {
|
||||
l.WithError(err).Error("Cannot remove destination file")
|
||||
return err
|
||||
}
|
||||
}
|
||||
} else if !errors.Is(err, fs.ErrNotExist) {
|
||||
l.WithError(err).Error("Destination error")
|
||||
return err
|
||||
}
|
||||
|
||||
// Create symbolic link and return.
|
||||
if srcIsLink {
|
||||
logrus.Debug("It is a symlink")
|
||||
l.Debug("Source is a symlink")
|
||||
linkPath, err := os.Readlink(srcPath)
|
||||
if err != nil {
|
||||
l.WithError(err).Error("Failed to read link")
|
||||
return err
|
||||
}
|
||||
logrus.Debug("link to ", linkPath)
|
||||
l.WithField("linkPath", linkPath).Debug("Creating symlink")
|
||||
return os.Symlink(linkPath, dstPath)
|
||||
}
|
||||
|
||||
// Create dir and return.
|
||||
if srcIsDir {
|
||||
logrus.Debug("It is a dir")
|
||||
return os.MkdirAll(dstPath, srcInfo.Mode())
|
||||
l.Debug("Source is a dir")
|
||||
err := os.MkdirAll(dstPath, srcInfo.Mode())
|
||||
if err != nil {
|
||||
l.WithError(err).Error("Failed to create dir")
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
// Regular files only.
|
||||
// If files are same return.
|
||||
if os.SameFile(srcInfo, dstInfo) || checksum(srcPath) == checksum(dstPath) {
|
||||
logrus.Debug("Same files, skip copy")
|
||||
l.Debug("Same files, skip copy")
|
||||
return nil
|
||||
}
|
||||
|
||||
// Create/overwrite regular file.
|
||||
srcReader, err := os.Open(filepath.Clean(srcPath))
|
||||
if err != nil {
|
||||
l.WithError(err).Error("Failed to open source")
|
||||
return err
|
||||
}
|
||||
defer srcReader.Close() //nolint:errcheck,gosec
|
||||
|
||||
return copyToTmpFileRename(srcReader, dstPath, srcInfo.Mode())
|
||||
})
|
||||
}
|
||||
|
||||
func copyToTmpFileRename(srcReader io.Reader, dstPath string, dstMode os.FileMode) error {
|
||||
logrus.Debug("Tmp and rename ", dstPath)
|
||||
tmpPath := dstPath + ".tmp"
|
||||
l := logrus.WithField("dstPath", dstPath)
|
||||
l.Debug("Create tmp and rename")
|
||||
|
||||
if err := copyToFileTruncate(srcReader, tmpPath, dstMode); err != nil {
|
||||
l.WithError(err).Error("Failed to copy and truncate")
|
||||
return err
|
||||
}
|
||||
return os.Rename(tmpPath, dstPath)
|
||||
|
||||
if err := os.Rename(tmpPath, dstPath); err != nil {
|
||||
l.WithError(err).Error("Failed to rename")
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func copyToFileTruncate(srcReader io.Reader, dstPath string, dstMode os.FileMode) error {
|
||||
logrus.Debug("Copy and truncate ", dstPath)
|
||||
l := logrus.WithField("dstPath", dstPath)
|
||||
l.Debug("Copy and truncate")
|
||||
|
||||
dstWriter, err := os.OpenFile(filepath.Clean(dstPath), os.O_WRONLY|os.O_CREATE|os.O_TRUNC, dstMode) //nolint:gosec // Cannot guess the safe part of path
|
||||
if err != nil {
|
||||
l.WithError(err).Error("Failed to open destination")
|
||||
return err
|
||||
}
|
||||
defer dstWriter.Close() //nolint:errcheck,gosec
|
||||
_, err = io.Copy(dstWriter, srcReader)
|
||||
return err
|
||||
|
||||
if _, err := io.Copy(dstWriter, srcReader); err != nil {
|
||||
l.WithError(err).Error("Failed to open destination")
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@ -27,6 +27,7 @@ import (
|
||||
"github.com/Masterminds/semver/v3"
|
||||
"github.com/ProtonMail/gopenpgp/v2/crypto"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
var (
|
||||
@ -101,6 +102,7 @@ func (u *Updater) InstallUpdate(ctx context.Context, downloader Downloader, upda
|
||||
}
|
||||
|
||||
if err := u.installer.InstallUpdate(update.Version, bytes.NewReader(b)); err != nil {
|
||||
logrus.WithError(err).Error("Failed to install update")
|
||||
return ErrInstall
|
||||
}
|
||||
|
||||
|
||||
@ -77,7 +77,7 @@ func (restarter *Restarter) Restart() {
|
||||
//nolint:gosec
|
||||
cmd := execabs.Command(
|
||||
restarter.exe,
|
||||
xslices.Join(removeFlagWithValue(removeFlag(os.Args[1:], "no-window"), "parent-pid"), restarter.flags)...,
|
||||
xslices.Join(removeFlagsWithValue(removeFlag(os.Args[1:], "no-window"), "parent-pid", "session-id"), restarter.flags)...,
|
||||
)
|
||||
|
||||
l := logrus.WithFields(logrus.Fields{
|
||||
@ -157,6 +157,16 @@ func removeFlagWithValue(argList []string, flag string) []string {
|
||||
return result
|
||||
}
|
||||
|
||||
// removeFlagWithValue removes flags that require a value from a list of command line parameters.
|
||||
// The flags can be of the following form `-flag value`, `--flag value`, `-flag=value` or `--flags=value`.
|
||||
func removeFlagsWithValue(argList []string, flags ...string) []string {
|
||||
for _, flag := range flags {
|
||||
argList = removeFlagWithValue(argList, flag)
|
||||
}
|
||||
|
||||
return argList
|
||||
}
|
||||
|
||||
func removeFlag(argList []string, flag string) []string {
|
||||
return xslices.Filter(argList, func(arg string) bool { return (arg != "-"+flag) && (arg != "--"+flag) })
|
||||
}
|
||||
|
||||
@ -42,6 +42,23 @@ func TestRemoveFlagWithValue(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestRemoveFlagsWithValue(t *testing.T) {
|
||||
tests := []struct {
|
||||
argList []string
|
||||
flags []string
|
||||
expected []string
|
||||
}{
|
||||
{[]string{}, []string{"a", "b"}, nil},
|
||||
{[]string{"-a", "-b=value", "-c"}, []string{"b"}, []string{"-a", "-c"}},
|
||||
{[]string{"-a", "--b=value", "-c"}, []string{"b", "c"}, []string{"-a"}},
|
||||
{[]string{"-a", "-b", "value", "-c"}, []string{"c", "b"}, []string{"-a"}},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
require.Equal(t, removeFlagsWithValue(tt.argList, tt.flags...), tt.expected)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRemoveFlag(t *testing.T) {
|
||||
tests := []struct {
|
||||
argList []string
|
||||
|
||||
52
utils/coverage.sh
Executable file
52
utils/coverage.sh
Executable file
@ -0,0 +1,52 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
# Copyright (c) 2023 Proton AG
|
||||
#
|
||||
# This file is part of Proton Mail Bridge.
|
||||
#
|
||||
# Proton Mail Bridge is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# Proton Mail Bridge is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
|
||||
# This script calculates coverage for bridge project
|
||||
#
|
||||
# Output:
|
||||
# stdout : total coverage (to be parsed by Gitlab pipeline)
|
||||
# coverage.xml : Cobertura format of covered lines for coverage visualization in Gitlab
|
||||
|
||||
# Assuming that test coverages from all jobs were put into `./coverage` folder
|
||||
# and passed as artifacts. The flags are:
|
||||
# -covermode=count
|
||||
# -coverpkg=github.com/ProtonMail/proton-bridge/v3/internal/...,github.com/ProtonMail/proton-bridge/v3/pkg/...,
|
||||
# -args -test.gocoverdir=$$PWD/coverage/${TOPIC}
|
||||
|
||||
|
||||
ALLINPUTS="coverage$(printf ",%s" coverage/*)"
|
||||
|
||||
go tool covdata textfmt \
|
||||
-i "$ALLINPUTS" \
|
||||
-o coverage_withGen.out
|
||||
|
||||
# Filter out auto-generated code
|
||||
grep -v '\.pb\.go' coverage_withGen.out > coverage.out
|
||||
|
||||
# Print out coverage
|
||||
go tool cover -func=./coverage.out | grep total:
|
||||
|
||||
# Convert to Cobertura
|
||||
#
|
||||
# NOTE: We are not using the latest `github.com/boumenot/gocover-cobertura`
|
||||
# because it does not support multiplatform coverage in one profile. See
|
||||
# https://github.com/boumenot/gocover-cobertura/pull/3#issuecomment-1571586099
|
||||
go get github.com/t-yuki/gocover-cobertura
|
||||
go run github.com/t-yuki/gocover-cobertura < ./coverage.out > coverage.xml
|
||||
@ -49,6 +49,8 @@ generate_dep_licenses(){
|
||||
sed -i -r '/^github.com\/therecipe\/qt\/internal\/binding\/files\/docs\//d;' "$tmpDepLicenses"
|
||||
sed -i -r 's|^(.*)/([[:alnum:]-]+)/(v[[:digit:]]+)$|* [\2](https://\1/\2/\3)|g' "$tmpDepLicenses"
|
||||
sed -i -r 's|^(.*)/([[:alnum:]-]+)$|* [\2](https://\1/\2)|g' "$tmpDepLicenses"
|
||||
sed -i -r 's|^(.*)/([[:alnum:]-]+).(v[[:digit:]]+)$|* [\2](https://\1/\2.\3)|g' "$tmpDepLicenses"
|
||||
|
||||
|
||||
## add license file to github links, and others
|
||||
sed -i -r '/github.com/s|^(.*(https://[^)]+).*)$|\1 available under [license](\2/blob/master/LICENSE) |g' "$tmpDepLicenses"
|
||||
@ -57,8 +59,14 @@ generate_dep_licenses(){
|
||||
sed -i -r '/golang.org\/x/s|^(.*golang.org/x/([^)]+).*)$|\1 available under [license](https://cs.opensource.google/go/x/\2/+/master:LICENSE) |g' "$tmpDepLicenses"
|
||||
sed -i -r '/google.golang.org\/grpc/s|^(.*)$|\1 available under [license](https://github.com/grpc/grpc-go/blob/master/LICENSE) |g' "$tmpDepLicenses"
|
||||
sed -i -r '/google.golang.org\/protobuf/s|^(.*)$|\1 available under [license](https://github.com/protocolbuffers/protobuf/blob/main/LICENSE) |g' "$tmpDepLicenses"
|
||||
sed -i -r '/go.uber.org\/goleak/s|^(.*)$|\1 available under [license](https://pkg.go.dev/go.uber.org/goleak?tab=licenses) |g' "$tmpDepLicenses"
|
||||
sed -i -r '/ariga.io\/atlas/s|^(.*)$|\1 available under [license](https://github.com/ariga/atlas/blob/master/LICENSE) |g' "$tmpDepLicenses"
|
||||
sed -i -r '/entgo.io\/ent/s|^(.*)$|\1 available under [license](https://pkg.go.dev/entgo.io/ent?tab=licenses) |g' "$tmpDepLicenses"
|
||||
sed -i -r '/google.golang.org\/genproto/s|^(.*)$|\1 available under [license](https://pkg.go.dev/google.golang.org/genproto?tab=licenses) |g' "$tmpDepLicenses"
|
||||
sed -i -r '/gopkg.in\/yaml\.v3/s|^(.*)$|\1 available under [license](https://github.com/go-yaml/yaml/blob/v3.0.1/LICENSE) |g' "$tmpDepLicenses"
|
||||
}
|
||||
|
||||
|
||||
check_dependecies(){
|
||||
generate_dep_licenses
|
||||
|
||||
|
||||
Reference in New Issue
Block a user