diff --git a/internal/app/base/base.go b/internal/app/base/base.go index 1a11ffa8..372f6c88 100644 --- a/internal/app/base/base.go +++ b/internal/app/base/base.go @@ -57,7 +57,6 @@ import ( "github.com/ProtonMail/proton-bridge/v2/pkg/keychain" "github.com/ProtonMail/proton-bridge/v2/pkg/listener" "github.com/ProtonMail/proton-bridge/v2/pkg/pmapi" - "github.com/allan-simon/go-singleinstance" "github.com/sirupsen/logrus" "github.com/urfave/cli/v2" ) @@ -153,9 +152,9 @@ func New( //nolint:funlen } settingsObj := settings.New(settingsPath) - lock, err := singleinstance.CreateLockFile(locations.GetLockFile()) + lock, err := checkSingleInstance(locations.GetLockFile(), settingsObj) if err != nil { - logrus.Warnf("%v is already running", appName) + logrus.WithError(err).Warnf("%v is already running", appName) return nil, api.CheckOtherInstanceAndFocus(settingsObj.GetInt(settings.APIPortKey)) } diff --git a/internal/app/base/restart_test.go b/internal/app/base/restart_test.go index 419e19d0..9e704f02 100644 --- a/internal/app/base/restart_test.go +++ b/internal/app/base/restart_test.go @@ -21,7 +21,9 @@ import ( "strings" "testing" + "github.com/Masterminds/semver/v3" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) func TestIncrementRestartFlag(t *testing.T) { @@ -47,3 +49,15 @@ func TestIncrementRestartFlag(t *testing.T) { }) } } + +func TestVersionLessThan(t *testing.T) { + r := require.New(t) + + old := semver.MustParse("1.1.0") + current := semver.MustParse("1.1.1") + newer := semver.MustParse("1.1.2") + + r.True(old.LessThan(current)) + r.False(current.LessThan(current)) + r.False(newer.LessThan(current)) +} diff --git a/internal/app/base/singleinstance_unix.go b/internal/app/base/singleinstance_unix.go new file mode 100644 index 00000000..535db8eb --- /dev/null +++ b/internal/app/base/singleinstance_unix.go @@ -0,0 +1,101 @@ +// Copyright (c) 2022 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 . + +//go:build !windows +// +build !windows + +package base + +import ( + "errors" + "os" + "path/filepath" + "strconv" + "strings" + "time" + + "github.com/Masterminds/semver/v3" + "github.com/ProtonMail/proton-bridge/v2/internal/config/settings" + "github.com/ProtonMail/proton-bridge/v2/internal/constants" + "github.com/allan-simon/go-singleinstance" + "golang.org/x/sys/unix" +) + +// checkSingleInstance returns error if a bridge instance is already running +// This instance should be stop and window of running window should be brought +// to focus. +// +// For macOS and Linux when already running version is older than this instance +// it will kill old and continue with this new bridge (i.e. no error returned). +func checkSingleInstance(lockFilePath string, settingsObj *settings.Settings) (*os.File, error) { + if lock, err := singleinstance.CreateLockFile(lockFilePath); err == nil { + // Bridge is not runnig, continue normally + return lock, nil + } + + if err := runningVersionIsOlder(settingsObj); err != nil { + return nil, err + } + + pid, err := getPID(lockFilePath) + if err != nil { + return nil, err + } + + if err := unix.Kill(pid, unix.SIGTERM); err != nil { + return nil, err + } + + // Need to wait some time to release file lock + time.Sleep(time.Second) + + return singleinstance.CreateLockFile(lockFilePath) +} + +func getPID(lockFilePath string) (int, error) { + file, err := os.Open(filepath.Clean(lockFilePath)) + if err != nil { + return 0, err + } + defer func() { _ = file.Close() }() + + rawPID := make([]byte, 10) // PID is probably up to 7 digits long, 10 should be enough + n, err := file.Read(rawPID) + if err != nil { + return 0, err + } + + return strconv.Atoi(strings.TrimSpace(string(rawPID[:n]))) +} + +func runningVersionIsOlder(settingsObj *settings.Settings) error { + currentVer, err := semver.StrictNewVersion(constants.Version) + if err != nil { + return err + } + + runningVer, err := semver.StrictNewVersion(settingsObj.Get(settings.LastVersionKey)) + if err != nil { + return err + } + + if !runningVer.LessThan(currentVer) { + return errors.New("running version is not older") + } + + return nil +} diff --git a/internal/app/base/singleinstance_windows.go b/internal/app/base/singleinstance_windows.go new file mode 100644 index 00000000..d0c34752 --- /dev/null +++ b/internal/app/base/singleinstance_windows.go @@ -0,0 +1,32 @@ +// Copyright (c) 2022 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 . + +//go:build windows +// +build windows + +package base + +import ( + "os" + + "github.com/ProtonMail/proton-bridge/v2/internal/config/settings" + "github.com/allan-simon/go-singleinstance" +) + +func checkSingleInstance(lockFilePath string, _ *settings.Settings) (*os.File, error) { + return singleinstance.CreateLockFile(lockFilePath) +}