Skip to content
This repository has been archived by the owner on Jan 8, 2024. It is now read-only.

Commit

Permalink
App restart (darwin)
Browse files Browse the repository at this point in the history
This uses ps to find the pid of an app, since app bundles use an executable
like Test.app/Contents/MacOS/Test. It will SIGTERM this process (and SIGKILL
after a second), and then call open.

It doesn't fail to call open if the kill failed.

Bundled is a Test.app that is used in the test.
  • Loading branch information
gabriel committed Apr 29, 2016
1 parent bd62e7d commit 3169c4b
Show file tree
Hide file tree
Showing 14 changed files with 413 additions and 35 deletions.
6 changes: 1 addition & 5 deletions keybase/context.go
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ func NewUpdaterContext(pathToKeybase string, log logging.Logger) (updater.Contex

src := NewUpdateSource(log)
// For testing
//src := sources.NewLocalUpdateSource("/tmp/Keybase.zip", log)
//src := updater.NewLocalUpdateSource("/tmp/Keybase.zip", log)
upd := updater.NewUpdater(src, &cfg, log)
return newContext(&cfg, log), upd
}
Expand Down Expand Up @@ -84,7 +84,3 @@ func (c context) AfterApply(update updater.Update) error {
}
return nil
}

func (c context) Restart() error {
return nil
}
16 changes: 16 additions & 0 deletions keybase/platform_darwin.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import (
"github.com/kardianos/osext"
"github.com/keybase/go-updater"
"github.com/keybase/go-updater/command"
"github.com/keybase/go-updater/process"
)

// destinationPath returns the app bundle path where this executable is located
Expand Down Expand Up @@ -67,3 +68,18 @@ func (c context) UpdatePrompt(update updater.Update, options updater.UpdateOptio
promptPath := filepath.Join(destinationPath, "Contents", "Resources", "Updater.app", "Contents", "MacOS", "Updater")
return c.updatePrompt(promptPath, update, options, promptOptions)
}

func (c context) Restart() error {
appPath := c.config.destinationPath()

keybase := filepath.Join(appPath, "Contents/SharedSupport/bin/keybase")
process.TerminateAll(keybase, c.log)

kbfs := filepath.Join(appPath, "Contents/SharedSupport/bin/kbfs")
process.TerminateAll(kbfs, c.log)

if err := process.RestartAppDarwin(appPath, c.log); err != nil {
c.log.Warningf("Error restarting app: %s", err)
}
return nil
}
4 changes: 4 additions & 0 deletions keybase/platform_linux.go
Original file line number Diff line number Diff line change
Expand Up @@ -45,3 +45,7 @@ func (c context) UpdatePrompt(update updater.Update, options updater.UpdateOptio
// TODO
return nil, fmt.Errorf("Unsupported")
}

func (c context) Restart() error {
return nil
}
4 changes: 4 additions & 0 deletions keybase/platform_windows.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,3 +39,7 @@ func (c context) UpdatePrompt(update updater.Update, options updater.UpdateOptio
// TODO
return nil, fmt.Errorf("Unsupported")
}

func (c context) Restart() error {
return nil
}
134 changes: 134 additions & 0 deletions process/process_darwin.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
// Copyright 2015 Keybase, Inc. All rights reserved. Use of
// this source code is governed by the included BSD license.

package process

import (
"bufio"
"fmt"
"io"
"os"
"path/filepath"
"strconv"
"strings"
"syscall"
"time"

"github.com/keybase/go-logging"
"github.com/keybase/go-updater/command"
)

// RestartAppDarwin restarts an app. We will still call open if the kill fails.
func RestartAppDarwin(appPath string, log logging.Logger) error {
if appPath == "" {
return fmt.Errorf("No app path to restart")
}
procName := filepath.Join(appPath, "Contents/MacOS/")
TerminateAll(procName, log)
return OpenAppDarwin(appPath, log)
}

// findPS finds PIDs for processes with prefix using ps.
// The command `ps ax -o pid,comm` returns process list in 2 columns, pid and executable name.
// For example:
//
// 67846 /Applications/Keybase.app/Contents/SharedSupport/bin/keybase
// 67847 /Applications/Keybase.app/Contents/SharedSupport/bin/keybase
// 53852 /Applications/Keybase.app/Contents/SharedSupport/bin/kbfs
// 3915 /Applications/Keybase.app/Contents/SharedSupport/bin/updater
// 67833 /Applications/Keybase.app/Contents/MacOS/Keybase
//
func findPS(prefix string, log logging.Logger) ([]int, error) {
log.Debugf("Finding process with prefix: %q", prefix)
result, err := command.Exec("ps", []string{"ax", "-o", "pid,comm"}, time.Minute, log)
if err != nil {
return nil, err
}
return parsePS(&result.Stdout, prefix, log)
}

func parsePS(reader io.Reader, prefix string, log logging.Logger) ([]int, error) {
if reader == nil {
return nil, fmt.Errorf("Nothing to parse")
}
if prefix == "" {
return nil, fmt.Errorf("No prefix")
}
pids := []int{}
scanner := bufio.NewScanner(reader)
for scanner.Scan() {
line := strings.TrimSpace(scanner.Text())
fields := strings.Fields(line)
if len(fields) >= 2 && strings.HasPrefix(fields[1], prefix) {
pid, err := strconv.Atoi(fields[0])
if err != nil {
log.Warningf("Invalid pid for %s", fields)
} else if pid > 0 {
pids = append(pids, pid)
}
}
}
return pids, nil
}

// TerminateAll stops processes with executable names that start with prefix
func TerminateAll(prefix string, log logging.Logger) {
pids, err := findPS(prefix, log)
if err != nil {
log.Warningf("Error finding process: %s", err)
}
if pids == nil {
log.Warningf("No processes found with prefix %q", prefix)
return
}
for _, pid := range pids {
if err := TerminatePid(pid, log); err != nil {
log.Warningf("Error terminating %d: %s", pid, err)
}
}
}

// TerminatePid calls SIGTERM, then waits a second and then calls SIGKILL.
// We don't mind if we call SIGKILL on an already terminated process.
// If SIGKILL failed then we've got bigger problems.
func TerminatePid(pid int, log logging.Logger) error {
log.Debugf("Searching OS for %d", pid)
process, err := os.FindProcess(pid)
if err != nil {
return fmt.Errorf("Error finding OS process: %s", err)
}
if process == nil {
return fmt.Errorf("No process found with pid %d", pid)
}

log.Debugf("Terminating: %#v", process)
err = process.Signal(syscall.SIGTERM)
if err != nil {
log.Warningf("Error sending terminate: %s", err)
}
time.Sleep(time.Second)
_ = process.Kill()
return nil
}

// OpenAppDarwin starts an app
func OpenAppDarwin(appPath string, log logging.Logger) error {
tryOpen := func() error {
result, err := command.Exec("/usr/bin/open", []string{appPath}, time.Minute, log)
if err != nil {
return fmt.Errorf("Open error: %s; %s", err, result.CombinedOutput())
}
return nil
}
// We need to try 10 times because Gatekeeper has some issues, for example,
// http://www.openradar.me/23614087
for i := 0; i < 10; i++ {
err := tryOpen()
if err == nil {
break
}
log.Errorf("Open error (trying again in a second): %s", err)
time.Sleep(1 * time.Second)
}
return nil
}
72 changes: 72 additions & 0 deletions process/process_darwin_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
// Copyright 2015 Keybase, Inc. All rights reserved. Use of
// this source code is governed by the included BSD license.

// +build darwin

package process

import (
"bytes"
"os"
"path/filepath"
"testing"

"github.com/keybase/go-logging"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)

var log = logging.Logger{Module: "test"}

func TestRestartDarwin(t *testing.T) {
appPath := filepath.Join(os.Getenv("GOPATH"), "src/github.com/keybase/go-updater/test/Test.app")
defer TerminateAll(appPath, log)

err := OpenAppDarwin(appPath, log)
require.NoError(t, err)

err = RestartAppDarwin(appPath, log)
require.NoError(t, err)
}

func TestFindPS(t *testing.T) {
pids, err := findPS("ps", log)
assert.NoError(t, err)
require.Equal(t, 1, len(pids))
assert.True(t, pids[0] != 0)
}

func TestParsePS(t *testing.T) {
var ps = `
67846 /Applications/Keybase.app/Contents/SharedSupport/bin/keybase
67847 /Applications/Keybase.app/Contents/SharedSupport/bin/keybase
852 /Applications/Keybase.app/Contents/SharedSupport/bin/kbfs
5 /Applications/Keybase.app/Contents/SharedSupport/bin/updater
43 /Applications/Keybase.app/Contents/MacOS/Keybase
67845 /Applications/Keybase.app/Contents/Frameworks/Keybase Helper.app/Contents/MacOS/Keybase Helper
636 login ??
1777 /usr/sbin/distnoted`
pids, err := parsePS(bytes.NewBufferString(ps), "/Applications/Keybase.app/Contents/MacOS", log)
assert.NoError(t, err)
assert.Equal(t, []int{43}, pids)

pids, err = parsePS(bytes.NewBufferString(ps), "/Applications/Keybase.app/Contents/SharedSupport/bin/keybase", log)
assert.NoError(t, err)
assert.Equal(t, []int{67846, 67847}, pids)

pids, err = parsePS(bytes.NewBufferString(ps), "login", log)
assert.NoError(t, err)
assert.Equal(t, []int{636}, pids)
}

func TestParsePSNil(t *testing.T) {
pids, err := parsePS(nil, "", log)
require.EqualError(t, err, "Nothing to parse")
assert.Nil(t, pids)
}

func TestParsePSNoPrefix(t *testing.T) {
pids, err := parsePS(bytes.NewBuffer([]byte{}), "", log)
require.EqualError(t, err, "No prefix")
assert.Nil(t, pids)
}
52 changes: 52 additions & 0 deletions test/Test.app/Contents/Info.plist
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>BuildMachineOSBuild</key>
<string>15E65</string>
<key>CFBundleDevelopmentRegion</key>
<string>en</string>
<key>CFBundleExecutable</key>
<string>Test</string>
<key>CFBundleIdentifier</key>
<string>keybase.Test</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>Test</string>
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>1.0</string>
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleSupportedPlatforms</key>
<array>
<string>MacOSX</string>
</array>
<key>CFBundleVersion</key>
<string>1</string>
<key>DTCompiler</key>
<string>com.apple.compilers.llvm.clang.1_0</string>
<key>DTPlatformBuild</key>
<string>7D175</string>
<key>DTPlatformVersion</key>
<string>GM</string>
<key>DTSDKBuild</key>
<string>15E60</string>
<key>DTSDKName</key>
<string>macosx10.11</string>
<key>DTXcode</key>
<string>0730</string>
<key>DTXcodeBuild</key>
<string>7D175</string>
<key>LSMinimumSystemVersion</key>
<string>10.11</string>
<key>NSHumanReadableCopyright</key>
<string>Copyright © 2016 Keybase. All rights reserved.</string>
<key>NSMainNibFile</key>
<string>MainMenu</string>
<key>NSPrincipalClass</key>
<string>NSApplication</string>
</dict>
</plist>
Binary file added test/Test.app/Contents/MacOS/Test
Binary file not shown.
1 change: 1 addition & 0 deletions test/Test.app/Contents/PkgInfo
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
APPL????
Binary file not shown.
Loading

0 comments on commit 3169c4b

Please sign in to comment.