diff --git a/internal/app/base/base.go b/internal/app/base/base.go index f1427f49..92fe43e8 100644 --- a/internal/app/base/base.go +++ b/internal/app/base/base.go @@ -76,6 +76,7 @@ const ( flagRestart = "restart" FlagLauncher = "launcher" FlagNoWindow = "no-window" + FlagParentPID = "parent-pid" ) type Base struct { @@ -324,6 +325,12 @@ func (b *Base) NewApp(mainLoop func(*Base, *cli.Context) error) *cli.App { Usage: "The launcher to use to restart the application", Hidden: true, }, + &cli.IntFlag{ + Name: FlagParentPID, + Usage: "The PID of the process that started the application. Ignored if frontend is not gRPC", + Hidden: true, + Value: -1, + }, } return app diff --git a/internal/app/bridge/bridge.go b/internal/app/bridge/bridge.go index 38984d4a..f6e04147 100644 --- a/internal/app/bridge/bridge.go +++ b/internal/app/bridge/bridge.go @@ -86,6 +86,7 @@ func main(b *base.Base, c *cli.Context) error { //nolint:funlen b.Updater, b, b.Locations, + c.Int(base.FlagParentPID), ) cache, cacheErr := loadMessageCache(b) diff --git a/internal/frontend/bridge-gui/bridge-gui/main.cpp b/internal/frontend/bridge-gui/bridge-gui/main.cpp index b4ea36f6..d75c56b5 100644 --- a/internal/frontend/bridge-gui/bridge-gui/main.cpp +++ b/internal/frontend/bridge-gui/bridge-gui/main.cpp @@ -220,7 +220,12 @@ void launchBridge(QStringList const &args) else app().log().debug(QString("Bridge executable path: %1").arg(QDir::toNativeSeparators(bridgeExePath))); - overseer = std::make_unique(new ProcessMonitor(bridgeExePath, QStringList("--grpc") + args, nullptr), nullptr); + + + qint64 const pid = qApp->applicationPid(); + QStringList const params = QStringList { "--grpc", "--parent-pid", QString::number(pid) } + args ; + app().log().info(QString("Launching bridge process with command \"%1\" %2").arg(bridgeExePath, params.join(" "))); + overseer = std::make_unique(new ProcessMonitor(bridgeExePath, params , nullptr), nullptr); overseer->startWorker(true); } diff --git a/internal/frontend/frontend.go b/internal/frontend/frontend.go index de370e53..91e294bb 100644 --- a/internal/frontend/frontend.go +++ b/internal/frontend/frontend.go @@ -56,6 +56,7 @@ func New( updater types.Updater, restarter types.Restarter, locations *locations.Locations, + parentPID int, ) Frontend { switch frontendType { case GRPC: @@ -66,6 +67,7 @@ func New( updater, restarter, locations, + parentPID, ) case CLI: diff --git a/internal/frontend/grpc/service.go b/internal/frontend/grpc/service.go index ed5c918d..85395dd6 100644 --- a/internal/frontend/grpc/service.go +++ b/internal/frontend/grpc/service.go @@ -40,6 +40,9 @@ 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/bradenaw/juniper/xslices" + "github.com/elastic/go-sysinfo" + sysinfotypes "github.com/elastic/go-sysinfo/types" "github.com/google/uuid" "github.com/pkg/errors" "github.com/sirupsen/logrus" @@ -85,6 +88,8 @@ type Service struct { // nolint:structcheck locations *locations.Locations token string pemCert string + parentPID int + parentPIDDoneCh chan struct{} } // NewService returns a new instance of the service. @@ -95,6 +100,7 @@ func NewService( updater types.Updater, restarter types.Restarter, locations *locations.Locations, + parentPID int, ) *Service { s := Service{ UnimplementedBridgeServer: UnimplementedBridgeServer{}, @@ -110,6 +116,8 @@ func NewService( firstTimeAutostart: sync.Once{}, locations: locations, token: uuid.NewString(), + parentPID: parentPID, + parentPIDDoneCh: make(chan struct{}), } // Initializing.Done is only called sync.Once. Please keep the increment @@ -177,6 +185,12 @@ func (s *Service) Loop(b types.Bridger) error { s.initAutostart() s.startGRPCServer() + if s.parentPID < 0 { + s.log.Info("Not monitoring parent PID") + } else { + go s.monitorParentPID() + } + defer func() { s.bridge.SetBool(settings.FirstStartGUIKey, false) }() @@ -520,3 +534,36 @@ func (s *Service) validateStreamServerToken( return handler(srv, ss) } + +// monitorParentPID check at regular intervals that the parent process is still alive, and if not shuts down the server +// and the applications. +func (s *Service) monitorParentPID() { + s.log.Infof("Starting to monitor parent PID %v", s.parentPID) + ticker := time.NewTicker(5 * time.Second) + + for { + select { + case <-ticker.C: + if s.parentPID < 0 { + continue + } + + processes, err := sysinfo.Processes() // sysinfo.Process(pid) does not seem to work on Windows. + if err != nil { + s.log.Debug("Could not retrieve process list") + continue + } + + if !xslices.Any(processes, func(p sysinfotypes.Process) bool { return p != nil && p.PID() == s.parentPID }) { + s.log.Info("Parent process does not exist anymore. Initiating shutdown") + go s.quit() // quit will write to the parentPIDDoneCh, so we launch a goroutine. + } else { + s.log.Tracef("Parent process %v is still alive", s.parentPID) + } + + case <-s.parentPIDDoneCh: + s.log.Infof("Stopping process monitoring for PID %v", s.parentPID) + return + } + } +} diff --git a/internal/frontend/grpc/service_methods.go b/internal/frontend/grpc/service_methods.go index 283537e9..b4cc062f 100644 --- a/internal/frontend/grpc/service_methods.go +++ b/internal/frontend/grpc/service_methods.go @@ -95,15 +95,19 @@ func (s *Service) GuiReady(ctx context.Context, _ *emptypb.Empty) (*emptypb.Empt // Quit implement the Quit gRPC service call. func (s *Service) Quit(ctx context.Context, empty *emptypb.Empty) (*emptypb.Empty, error) { s.log.Debug("Quit") - return &emptypb.Empty{}, s.quit() + s.quit() + return &emptypb.Empty{}, nil } -func (s *Service) quit() error { +func (s *Service) quit() { // Windows is notably slow at Quitting. We do it in a goroutine to speed things up a bit. go func() { - var err error + if s.parentPID >= 0 { + s.parentPIDDoneCh <- struct{}{} + } + if s.isStreamingEvents() { - if err = s.stopEventStream(); err != nil { + if err := s.stopEventStream(); err != nil { s.log.WithError(err).Error("Quit failed.") } } @@ -111,8 +115,6 @@ func (s *Service) quit() error { // The following call is launched as a goroutine, as it will wait for current calls to end, including this one. s.grpcServer.GracefulStop() }() - - return nil } // Restart implement the Restart gRPC service call. diff --git a/internal/frontend/grpc/service_stream.go b/internal/frontend/grpc/service_stream.go index 4d079619..ec63ef93 100644 --- a/internal/frontend/grpc/service_stream.go +++ b/internal/frontend/grpc/service_stream.go @@ -71,8 +71,9 @@ func (s *Service) RunEventStream(request *EventStreamRequest, server Bridge_RunE return err } case <-server.Context().Done(): - s.log.Debug("Client closed the stream, exiting") - return s.quit() + s.log.Info("Client closed the stream, initiating shutdown") + s.quit() + return nil } } }