Merge "services/device/interna/impl: split app service state"
diff --git a/lib/apilog/log.go b/lib/apilog/apilog.go
similarity index 75%
rename from lib/apilog/log.go
rename to lib/apilog/apilog.go
index 3eb4f92..83c11b0 100644
--- a/lib/apilog/log.go
+++ b/lib/apilog/apilog.go
@@ -2,16 +2,25 @@
 // Use of this source code is governed by a BSD-style
 // license that can be found in the LICENSE file.
 
-// Package apilog provides a function to be used in conjunction with vtrace
-// and logcop automatically injected logging calls. These are called on
-// entry/exit for methods that implements a v23 API call.
+// Package apilog provides functions to be used in conjunction with logcop.
+// In particular, logcop will inject calls to these functions as the first
+// statement in methods that implement the v23 API. The output can
+// be controlled by vlog verbosity or vtrace.
+// The log lines generated have a header that refers to this file
+// and the message includes the file and line # of the caller. This is
+// currently the best we can do given the functionality of the underlying
+// vlog package. It has the advantage the api logging can be selectively
+// enabled/disabled globally using --vmodule=apilog=<level>, but the
+// disadvantage that it can't be controlled at the per file level.
 package apilog
 
 import (
 	"fmt"
 	"path"
+	"path/filepath"
 	"reflect"
 	"runtime"
+	"strconv"
 	"sync/atomic"
 
 	"v.io/x/lib/vlog"
@@ -22,17 +31,23 @@
 // logCallLogLevel is the log level beyond which calls are logged.
 const logCallLogLevel = 1
 
-func callerFuncName() string {
+var logger vlog.Logger
+
+func init() {
+	logger = vlog.Log
+}
+
+func callerLocation() string {
 	var funcName string
 	const stackSkip = 1
-	pc, _, _, ok := runtime.Caller(stackSkip + 1)
+	pc, file, line, ok := runtime.Caller(stackSkip + 1)
 	if ok {
 		function := runtime.FuncForPC(pc)
 		if function != nil {
 			funcName = path.Base(function.Name())
 		}
 	}
-	return funcName
+	return filepath.Base(file) + ":" + strconv.Itoa(line) + " " + funcName
 }
 
 // TODO(cnicolaou): remove LogCall from vlog.
@@ -89,27 +104,28 @@
 //     }
 //
 func LogCall(ctx *context.T, v ...interface{}) func(*context.T, ...interface{}) {
-	if !vlog.V(logCallLogLevel) { // TODO(mattr): add call to vtrace.
+	if !logger.V(logCallLogLevel) { // TODO(mattr): add call to vtrace.
 		return func(*context.T, ...interface{}) {}
 	}
-	callerFuncName := callerFuncName()
+	callerLocation := callerLocation()
 	invocationId := newInvocationIdentifier()
 	var output string
 	if len(v) > 0 {
-		output = fmt.Sprintf("call[%s %s]: args:%v", callerFuncName, invocationId, v)
+		output = fmt.Sprintf("call[%s %s]: args:%v", callerLocation, invocationId, v)
 	} else {
-		output = fmt.Sprintf("call[%s %s]", callerFuncName, invocationId)
+		output = fmt.Sprintf("call[%s %s]", callerLocation, invocationId)
 	}
-	vlog.Info(output)
+	logger.Info(output)
+
 	// TODO(mattr): annotate vtrace span.
 	return func(ctx *context.T, v ...interface{}) {
 		var output string
 		if len(v) > 0 {
-			output = fmt.Sprintf("return[%s %s]: %v", callerFuncName, invocationId, derefSlice(v))
+			output = fmt.Sprintf("return[%s %s]: %v", callerLocation, invocationId, derefSlice(v))
 		} else {
-			output = fmt.Sprintf("return[%s %s]", callerFuncName, invocationId)
+			output = fmt.Sprintf("return[%s %s]", callerLocation, invocationId)
 		}
-		vlog.Info(output)
+		logger.Info(output)
 		// TODO(mattr): annotate vtrace span.
 	}
 }
@@ -124,17 +140,17 @@
 //     }
 //
 func LogCallf(ctx *context.T, format string, v ...interface{}) func(*context.T, string, ...interface{}) {
-	if !vlog.V(logCallLogLevel) { // TODO(mattr): add call to vtrace.
+	if !logger.V(logCallLogLevel) { // TODO(mattr): add call to vtrace.
 		return func(*context.T, string, ...interface{}) {}
 	}
-	callerFuncName := callerFuncName()
+	callerLocation := callerLocation()
 	invocationId := newInvocationIdentifier()
-	output := fmt.Sprintf("call[%s %s]: %s", callerFuncName, invocationId, fmt.Sprintf(format, v...))
-	vlog.Info(output)
+	output := fmt.Sprintf("call[%s %s]: %s", callerLocation, invocationId, fmt.Sprintf(format, v...))
+	logger.Info(output)
 	// TODO(mattr): annotate vtrace span.
 	return func(ctx *context.T, format string, v ...interface{}) {
-		output := fmt.Sprintf("return[%s %s]: %v", callerFuncName, invocationId, fmt.Sprintf(format, derefSlice(v)...))
-		vlog.Info(output)
+		output := fmt.Sprintf("return[%s %s]: %v", callerLocation, invocationId, fmt.Sprintf(format, derefSlice(v)...))
+		logger.Info(output)
 		// TODO(mattr): annotate vtrace span.
 	}
 }
diff --git a/lib/apilog/apilog_test.go b/lib/apilog/apilog_test.go
new file mode 100644
index 0000000..d085efc
--- /dev/null
+++ b/lib/apilog/apilog_test.go
@@ -0,0 +1,93 @@
+// Copyright 2015 The Vanadium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package apilog_test
+
+import (
+	"bufio"
+	"io/ioutil"
+	"os"
+	"path/filepath"
+	"regexp"
+	"strings"
+	"testing"
+
+	"v.io/x/lib/vlog"
+
+	"v.io/x/ref/lib/apilog"
+)
+
+func readLogFiles(dir string) ([]string, error) {
+	files, err := ioutil.ReadDir(dir)
+	if err != nil {
+		return nil, err
+	}
+	var contents []string
+	for _, fi := range files {
+		// Skip symlinks to avoid double-counting log lines.
+		if !fi.Mode().IsRegular() {
+			continue
+		}
+		file, err := os.Open(filepath.Join(dir, fi.Name()))
+		if err != nil {
+			return nil, err
+		}
+		scanner := bufio.NewScanner(file)
+		for scanner.Scan() {
+			if line := scanner.Text(); len(line) > 0 && line[0] == 'I' {
+				contents = append(contents, line)
+			}
+		}
+	}
+	return contents, nil
+}
+
+func myLoggedFunc() {
+	f := apilog.LogCall(nil, "entry")
+	f(nil, "exit")
+}
+
+func TestLogCall(t *testing.T) {
+	dir, err := ioutil.TempDir("", "logtest")
+	defer os.RemoveAll(dir)
+	if err != nil {
+		t.Fatalf("unexpected error: %s", err)
+	}
+	logger := vlog.NewLogger("testHeader")
+	logger.Configure(vlog.LogDir(dir), vlog.Level(2))
+	saveLog := apilog.Log()
+	defer func() { apilog.SetLog(saveLog) }()
+	apilog.SetLog(logger)
+	myLoggedFunc()
+	logger.FlushLog()
+	contents, err := readLogFiles(dir)
+	if err != nil {
+		t.Fatalf("unexpected error: %s", err)
+	}
+	if want, got := 2, len(contents); want != got {
+		t.Errorf("Expected %d info lines, got %d instead", want, got)
+	}
+	logCallLineRE := regexp.MustCompile(`\S+ \S+ \S+ ([^:]*):.*(call|return)\[(\S*) (\S*)`)
+	for _, line := range contents {
+		match := logCallLineRE.FindStringSubmatch(line)
+		if len(match) != 5 {
+			t.Errorf("failed to match %s", line)
+			continue
+		}
+		fileName, callType, location, funcName := match[1], match[2], match[3], match[4]
+		if fileName != "apilog.go" {
+			t.Errorf("unexpected file name: %s", fileName)
+			continue
+		}
+		if callType != "call" && callType != "return" {
+			t.Errorf("unexpected call type: %s", callType)
+		}
+		if !strings.HasPrefix(location, "apilog_test.go:") {
+			t.Errorf("unexpected location: %s", location)
+		}
+		if funcName != "apilog_test.myLoggedFunc" {
+			t.Errorf("unexpected func name: %s", funcName)
+		}
+	}
+}
diff --git a/lib/apilog/util_test.go b/lib/apilog/util_test.go
new file mode 100644
index 0000000..9d2ea7d
--- /dev/null
+++ b/lib/apilog/util_test.go
@@ -0,0 +1,15 @@
+// Copyright 2015 The Vanadium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package apilog
+
+import "v.io/x/lib/vlog"
+
+func SetLog(l vlog.Logger) {
+	logger = l
+}
+
+func Log() vlog.Logger {
+	return logger
+}
diff --git a/lib/vdl/codegen/java/file_client_impl.go b/lib/vdl/codegen/java/file_client_impl.go
index 36b5e20..be2d92c 100644
--- a/lib/vdl/codegen/java/file_client_impl.go
+++ b/lib/vdl/codegen/java/file_client_impl.go
@@ -70,7 +70,7 @@
         // Start the call.
         java.lang.Object[] _args = new java.lang.Object[]{ {{ $method.CallingArgs }} };
         java.lang.reflect.Type[] _argTypes = new java.lang.reflect.Type[]{ {{ $method.CallingArgTypes }} };
-        final io.v.v23.rpc.Client.Call _call = getClient(context).startCall(context, this.vName, "{{ $method.Name }}", _args, _argTypes, vOpts);
+        final io.v.v23.rpc.ClientCall _call = getClient(context).startCall(context, this.vName, "{{ $method.Name }}", _args, _argTypes, vOpts);
 
         // Finish the call.
         {{/* Now handle returning from the function. */}}
@@ -99,7 +99,7 @@
         {{ end }} {{/* end if $method.IsVoid */}}
 
         {{else }} {{/* else $method.NotStreaming */}}
-        return new io.v.v23.vdl.ClientStream<{{ $method.SendType }}, {{ $method.RecvType }}, {{ $method.DeclaredObjectRetType }}>() {
+        return new io.v.v23.vdl.TypedClientStream<{{ $method.SendType }}, {{ $method.RecvType }}, {{ $method.DeclaredObjectRetType }}>() {
             @Override
             public void send({{ $method.SendType }} item) throws io.v.v23.verror.VException {
                 java.lang.reflect.Type type = new com.google.common.reflect.TypeToken<{{ $method.SendType }}>() {}.getType();
diff --git a/lib/vdl/codegen/java/file_client_interface.go b/lib/vdl/codegen/java/file_client_interface.go
index 83ee6f1..f4f8c32 100644
--- a/lib/vdl/codegen/java/file_client_interface.go
+++ b/lib/vdl/codegen/java/file_client_interface.go
@@ -71,7 +71,7 @@
 
 func clientInterfaceOutArg(iface *compile.Interface, method *compile.Method, env *compile.Env) string {
 	if isStreamingMethod(method) {
-		return fmt.Sprintf("io.v.v23.vdl.ClientStream<%s, %s, %s>", javaType(method.InStream, true, env), javaType(method.OutStream, true, env), clientInterfaceNonStreamingOutArg(iface, method, true, env))
+		return fmt.Sprintf("io.v.v23.vdl.TypedClientStream<%s, %s, %s>", javaType(method.InStream, true, env), javaType(method.OutStream, true, env), clientInterfaceNonStreamingOutArg(iface, method, true, env))
 	}
 	return clientInterfaceNonStreamingOutArg(iface, method, false, env)
 }
diff --git a/lib/vdl/codegen/java/file_server_interface.go b/lib/vdl/codegen/java/file_server_interface.go
index 0b0d96c..e978a4e 100644
--- a/lib/vdl/codegen/java/file_server_interface.go
+++ b/lib/vdl/codegen/java/file_server_interface.go
@@ -73,7 +73,7 @@
 func processServerInterfaceMethod(method *compile.Method, iface *compile.Interface, env *compile.Env) serverInterfaceMethod {
 	args := javaDeclarationArgStr(method.InArgs, env, true)
 	if isStreamingMethod(method) {
-		args += fmt.Sprintf(", io.v.v23.vdl.Stream<%s, %s> stream", javaType(method.OutStream, true, env), javaType(method.InStream, true, env))
+		args += fmt.Sprintf(", io.v.v23.vdl.TypedStream<%s, %s> stream", javaType(method.OutStream, true, env), javaType(method.InStream, true, env))
 	}
 	retArgs := make([]serverInterfaceArg, len(method.OutArgs))
 	for i := 0; i < len(method.OutArgs); i++ {
diff --git a/lib/vdl/codegen/java/file_server_wrapper.go b/lib/vdl/codegen/java/file_server_wrapper.go
index cfca39c..6889463 100644
--- a/lib/vdl/codegen/java/file_server_wrapper.go
+++ b/lib/vdl/codegen/java/file_server_wrapper.go
@@ -114,7 +114,7 @@
     {{ $method.JavaDoc }}
     public {{ $method.RetType }} {{ $method.Name }}(io.v.v23.context.VContext ctx, final io.v.v23.rpc.StreamServerCall call{{ $method.DeclarationArgs }}) throws io.v.v23.verror.VException {
         {{ if $method.IsStreaming }}
-        io.v.v23.vdl.Stream<{{ $method.SendType }}, {{ $method.RecvType }}> _stream = new io.v.v23.vdl.Stream<{{ $method.SendType }}, {{ $method.RecvType }}>() {
+        io.v.v23.vdl.TypedStream<{{ $method.SendType }}, {{ $method.RecvType }}> _stream = new io.v.v23.vdl.TypedStream<{{ $method.SendType }}, {{ $method.RecvType }}>() {
             @Override
             public void send({{ $method.SendType }} item) throws io.v.v23.verror.VException {
                 java.lang.reflect.Type type = new com.google.common.reflect.TypeToken< {{ $method.SendType }} >() {}.getType();
diff --git a/services/application/application/impl_test.go b/services/application/application/impl_test.go
index 4227acd..8f7a084 100644
--- a/services/application/application/impl_test.go
+++ b/services/application/application/impl_test.go
@@ -37,6 +37,8 @@
 				File: "/path/to/package1",
 			},
 		},
+		Restarts:          0,
+		RestartTimeWindow: 0,
 	}
 	jsonEnv = `{
   "Title": "fifa world cup",
@@ -70,7 +72,9 @@
         "S": null
       }
     }
-  }
+  },
+  "Restarts": 0,
+  "RestartTimeWindow": 0
 }`
 )
 
diff --git a/services/device/internal/impl/applife/app_life_test.go b/services/device/internal/impl/applife/app_life_test.go
index f336bb2..8484495 100644
--- a/services/device/internal/impl/applife/app_life_test.go
+++ b/services/device/internal/impl/applife/app_life_test.go
@@ -89,7 +89,7 @@
 	utiltest.Resolve(t, ctx, "pingserver", 1)
 
 	// Create an envelope for a first version of the app.
-	*envelope = utiltest.EnvelopeFromShell(sh, []string{utiltest.TestEnvVarName + "=env-val-envelope"}, utiltest.AppCmd, "google naps", fmt.Sprintf("--%s=flag-val-envelope", utiltest.TestFlagName), "appV1")
+	*envelope = utiltest.EnvelopeFromShell(sh, []string{utiltest.TestEnvVarName + "=env-val-envelope"}, utiltest.AppCmd, "google naps", 0, 0, fmt.Sprintf("--%s=flag-val-envelope", utiltest.TestFlagName), "appV1")
 
 	// Install the app.  The config-specified flag value for testFlagName
 	// should override the value specified in the envelope above, and the
@@ -185,12 +185,12 @@
 	utiltest.UpdateAppExpectError(t, ctx, appID, impl.ErrUpdateNoOp.ID)
 
 	// Updating the installation should not work with a mismatched title.
-	*envelope = utiltest.EnvelopeFromShell(sh, nil, utiltest.AppCmd, "bogus")
+	*envelope = utiltest.EnvelopeFromShell(sh, nil, utiltest.AppCmd, "bogus", 0, 0)
 
 	utiltest.UpdateAppExpectError(t, ctx, appID, impl.ErrAppTitleMismatch.ID)
 
 	// Create a second version of the app and update the app to it.
-	*envelope = utiltest.EnvelopeFromShell(sh, []string{utiltest.TestEnvVarName + "=env-val-envelope"}, utiltest.AppCmd, "google naps", "appV2")
+	*envelope = utiltest.EnvelopeFromShell(sh, []string{utiltest.TestEnvVarName + "=env-val-envelope"}, utiltest.AppCmd, "google naps", 0, 0, "appV2")
 
 	utiltest.UpdateApp(t, ctx, appID)
 
@@ -303,7 +303,7 @@
 	// cleanly Do this by installing, instantiating, running, and killing
 	// hangingApp, which sleeps (rather than exits) after being asked to
 	// Stop()
-	*envelope = utiltest.EnvelopeFromShell(sh, nil, utiltest.HangingAppCmd, "hanging ap", "hAppV1")
+	*envelope = utiltest.EnvelopeFromShell(sh, nil, utiltest.HangingAppCmd, "hanging ap", 0, 0, "hAppV1")
 	hAppID := utiltest.InstallApp(t, ctx)
 	hInstanceID := utiltest.LaunchApp(t, ctx, hAppID)
 	hangingPid := pingCh.WaitForPingArgs(t).Pid
diff --git a/services/device/internal/impl/applife/instance_reaping_test.go b/services/device/internal/impl/applife/instance_reaping_test.go
index ef023ca..dfe3592 100644
--- a/services/device/internal/impl/applife/instance_reaping_test.go
+++ b/services/device/internal/impl/applife/instance_reaping_test.go
@@ -34,7 +34,7 @@
 	utiltest.Resolve(t, ctx, "pingserver", 1)
 
 	// Create an envelope for a first version of the app.
-	*envelope = utiltest.EnvelopeFromShell(sh, nil, utiltest.AppCmd, "google naps", "appV1")
+	*envelope = utiltest.EnvelopeFromShell(sh, nil, utiltest.AppCmd, "google naps", 0, 0, "appV1")
 
 	// Install the app.  The config-specified flag value for testFlagName
 	// should override the value specified in the envelope above.
diff --git a/services/device/internal/impl/daemonreap/daemon_reaping_test.go b/services/device/internal/impl/daemonreap/daemon_reaping_test.go
new file mode 100644
index 0000000..84d0602
--- /dev/null
+++ b/services/device/internal/impl/daemonreap/daemon_reaping_test.go
@@ -0,0 +1,87 @@
+// Copyright 2015 The Vanadium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package daemonreap_test
+
+import (
+	"syscall"
+	"testing"
+	"time"
+
+	"v.io/v23/naming"
+	"v.io/v23/services/device"
+	"v.io/v23/services/stats"
+	"v.io/v23/vdl"
+
+	"v.io/x/ref/services/device/internal/impl/utiltest"
+	"v.io/x/ref/services/internal/servicetest"
+)
+
+func TestDaemonRestart(t *testing.T) {
+	cleanup, ctx, sh, envelope, root, helperPath, _ := utiltest.StartupHelper(t)
+	defer cleanup()
+
+	// Set up the device manager.  Since we won't do device manager updates,
+	// don't worry about its application envelope and current link.
+	dmh := servicetest.RunCommand(t, sh, nil, utiltest.DeviceManagerCmd, "dm", root, helperPath, "unused_app_repo_name", "unused_curr_link")
+	servicetest.ReadPID(t, dmh)
+	utiltest.ClaimDevice(t, ctx, "claimable", "dm", "mydevice", utiltest.NoPairingToken)
+
+	// Create the local server that the app uses to let us know it's ready.
+	pingCh, cleanup := utiltest.SetupPingServer(t, ctx)
+	defer cleanup()
+
+	utiltest.Resolve(t, ctx, "pingserver", 1)
+
+	// Create an envelope for a first version of the app that will be restarted once.
+	*envelope = utiltest.EnvelopeFromShell(sh, nil, utiltest.AppCmd, "google naps", 1, 10*time.Second, "appV1")
+	appID := utiltest.InstallApp(t, ctx)
+
+	// Start an instance of the app.
+	instance1ID := utiltest.LaunchApp(t, ctx, appID)
+
+	// Wait until the app pings us that it's ready.
+	pingCh.VerifyPingArgs(t, utiltest.UserName(t), "default", "")
+
+	// Get application pid.
+	name := naming.Join("dm", "apps/"+appID+"/"+instance1ID+"/stats/system/pid")
+	c := stats.StatsClient(name)
+	v, err := c.Value(ctx)
+	if err != nil {
+		t.Fatalf("Value() failed: %v\n", err)
+	}
+	var pid int
+	if err := vdl.Convert(&pid, v); err != nil {
+		t.Fatalf("pid returned from stats interface is not an int: %v", err)
+	}
+
+	utiltest.VerifyState(t, ctx, device.InstanceStateRunning, appID, instance1ID)
+	syscall.Kill(int(pid), 9)
+
+	// Start a second instance of the app which will force polling to happen.
+	instance2ID := utiltest.LaunchApp(t, ctx, appID)
+	pingCh.VerifyPingArgs(t, utiltest.UserName(t), "default", "")
+
+	// TODO(rjkroege): Because there is no daemon mode, instance1ID is not running even
+	// though it should be.
+	utiltest.VerifyState(t, ctx, device.InstanceStateNotRunning, appID, instance1ID)
+
+	// TODO(rjkroege): Demonstrate that the device manager will only restart the app the
+	// configured number of times (1)
+
+	// instance2ID is still running though.
+	utiltest.VerifyState(t, ctx, device.InstanceStateRunning, appID, instance2ID)
+
+	// Cleanup.
+	utiltest.TerminateApp(t, ctx, appID, instance2ID)
+
+	// TODO(rjkroege): instance1ID isn't running but should be.
+	// utiltest.TerminateApp(t, ctx, appID, instance1ID)
+
+	// Cleanly shut down the device manager.
+	utiltest.VerifyNoRunningProcesses(t)
+	syscall.Kill(dmh.Pid(), syscall.SIGINT)
+	dmh.Expect("dm terminated")
+	dmh.ExpectEOF()
+}
diff --git a/services/device/internal/impl/daemonreap/doc.go b/services/device/internal/impl/daemonreap/doc.go
new file mode 100644
index 0000000..c0def53
--- /dev/null
+++ b/services/device/internal/impl/daemonreap/doc.go
@@ -0,0 +1,8 @@
+// Copyright 2015 The Vanadium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package daemonreap
+
+// Test code for the device manager's facility to run daemon
+// processes and reconcile processes via kill.
diff --git a/services/device/internal/impl/daemonreap/impl_test.go b/services/device/internal/impl/daemonreap/impl_test.go
new file mode 100644
index 0000000..dd72081
--- /dev/null
+++ b/services/device/internal/impl/daemonreap/impl_test.go
@@ -0,0 +1,19 @@
+// Copyright 2015 The Vanadium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package daemonreap_test
+
+import (
+	"testing"
+
+	"v.io/x/ref/services/device/internal/impl/utiltest"
+)
+
+func TestMain(m *testing.M) {
+	utiltest.TestMainImpl(m)
+}
+
+func TestSuidHelper(t *testing.T) {
+	utiltest.TestSuidHelperImpl(t)
+}
diff --git a/services/device/internal/impl/instance_reaping_kill_test.go b/services/device/internal/impl/daemonreap/instance_reaping_kill_test.go
similarity index 98%
rename from services/device/internal/impl/instance_reaping_kill_test.go
rename to services/device/internal/impl/daemonreap/instance_reaping_kill_test.go
index 8e6c68b..c04f8b0 100644
--- a/services/device/internal/impl/instance_reaping_kill_test.go
+++ b/services/device/internal/impl/daemonreap/instance_reaping_kill_test.go
@@ -2,7 +2,7 @@
 // Use of this source code is governed by a BSD-style
 // license that can be found in the LICENSE file.
 
-package impl_test
+package daemonreap_test
 
 import (
 	"fmt"
@@ -42,7 +42,7 @@
 	utiltest.Resolve(t, ctx, "pingserver", 1)
 
 	// Create an envelope for the app.
-	*envelope = utiltest.EnvelopeFromShell(sh, nil, utiltest.AppCmd, "google naps", "appV1")
+	*envelope = utiltest.EnvelopeFromShell(sh, nil, utiltest.AppCmd, "google naps", 0, 0, "appV1")
 
 	// Install the app.
 	appID := utiltest.InstallApp(t, ctx)
diff --git a/services/device/internal/impl/globsuid/glob_test.go b/services/device/internal/impl/globsuid/glob_test.go
index f9becde..30fdc23 100644
--- a/services/device/internal/impl/globsuid/glob_test.go
+++ b/services/device/internal/impl/globsuid/glob_test.go
@@ -45,7 +45,7 @@
 	defer cleanup()
 
 	// Create the envelope for the first version of the app.
-	*envelope = utiltest.EnvelopeFromShell(sh, nil, utiltest.AppCmd, "google naps", "appV1")
+	*envelope = utiltest.EnvelopeFromShell(sh, nil, utiltest.AppCmd, "google naps", 0, 0, "appV1")
 
 	// Device must be claimed before applications can be installed.
 	utiltest.ClaimDevice(t, ctx, "claimable", "dm", "mydevice", utiltest.NoPairingToken)
diff --git a/services/device/internal/impl/globsuid/suid_test.go b/services/device/internal/impl/globsuid/suid_test.go
index 83ae185..12afe73 100644
--- a/services/device/internal/impl/globsuid/suid_test.go
+++ b/services/device/internal/impl/globsuid/suid_test.go
@@ -82,7 +82,7 @@
 	defer cleanup()
 
 	// Create an envelope for a first version of the app.
-	*envelope = utiltest.EnvelopeFromShell(sh, []string{utiltest.TestEnvVarName + "=env-var"}, utiltest.AppCmd, "google naps", fmt.Sprintf("--%s=flag-val-envelope", utiltest.TestFlagName), "appV1")
+	*envelope = utiltest.EnvelopeFromShell(sh, []string{utiltest.TestEnvVarName + "=env-var"}, utiltest.AppCmd, "google naps", 0, 0, fmt.Sprintf("--%s=flag-val-envelope", utiltest.TestFlagName), "appV1")
 
 	// Install and start the app as root/self.
 	appID := utiltest.InstallApp(t, selfCtx)
diff --git a/services/device/internal/impl/impl_test.go b/services/device/internal/impl/impl_test.go
index 14425d8..4a7743d 100644
--- a/services/device/internal/impl/impl_test.go
+++ b/services/device/internal/impl/impl_test.go
@@ -137,7 +137,7 @@
 	// Brand new device manager must be claimed first.
 	utiltest.ClaimDevice(t, ctx, "claimable", "factoryDM", "mydevice", utiltest.NoPairingToken)
 	// Simulate an invalid envelope in the application repository.
-	*envelope = utiltest.EnvelopeFromShell(sh, dmPauseBeforeStopEnv, utiltest.DeviceManagerCmd, "bogus", dmArgs...)
+	*envelope = utiltest.EnvelopeFromShell(sh, dmPauseBeforeStopEnv, utiltest.DeviceManagerCmd, "bogus", 0, 0, dmArgs...)
 
 	utiltest.UpdateDeviceExpectError(t, ctx, "factoryDM", impl.ErrAppTitleMismatch.ID)
 	utiltest.RevertDeviceExpectError(t, ctx, "factoryDM", impl.ErrUpdateNoOp.ID)
@@ -145,7 +145,7 @@
 	// Set up a second version of the device manager. The information in the
 	// envelope will be used by the device manager to stage the next
 	// version.
-	*envelope = utiltest.EnvelopeFromShell(sh, dmEnv, utiltest.DeviceManagerCmd, application.DeviceManagerTitle, "v2DM")
+	*envelope = utiltest.EnvelopeFromShell(sh, dmEnv, utiltest.DeviceManagerCmd, application.DeviceManagerTitle, 0, 0, "v2DM")
 	utiltest.UpdateDevice(t, ctx, "factoryDM")
 
 	// Current link should have been updated to point to v2.
@@ -189,7 +189,7 @@
 	// Try issuing an update with a binary that has a different major version
 	// number. It should fail.
 	utiltest.ResolveExpectNotFound(t, ctx, "v2.5DM") // Ensure a clean slate.
-	*envelope = utiltest.EnvelopeFromShell(sh, dmEnv, utiltest.DeviceManagerV10Cmd, application.DeviceManagerTitle, "v2.5DM")
+	*envelope = utiltest.EnvelopeFromShell(sh, dmEnv, utiltest.DeviceManagerV10Cmd, application.DeviceManagerTitle, 0, 0, "v2.5DM")
 	utiltest.UpdateDeviceExpectError(t, ctx, "v2DM", impl.ErrOperationFailed.ID)
 
 	if evalLink() != scriptPathV2 {
@@ -197,7 +197,7 @@
 	}
 
 	// Create a third version of the device manager and issue an update.
-	*envelope = utiltest.EnvelopeFromShell(sh, dmEnv, utiltest.DeviceManagerCmd, application.DeviceManagerTitle, "v3DM")
+	*envelope = utiltest.EnvelopeFromShell(sh, dmEnv, utiltest.DeviceManagerCmd, application.DeviceManagerTitle, 0, 0, "v3DM")
 	utiltest.UpdateDevice(t, ctx, "v2DM")
 
 	scriptPathV3 := evalLink()
@@ -404,7 +404,7 @@
 	defer cleanup()
 
 	// Create the envelope for the first version of the app.
-	*envelope = utiltest.EnvelopeFromShell(sh, nil, utiltest.AppCmd, "google naps", "appV1")
+	*envelope = utiltest.EnvelopeFromShell(sh, nil, utiltest.AppCmd, "google naps", 0, 0, "appV1")
 	envelope.Packages = map[string]application.SignedFile{
 		"test": application.SignedFile{
 			File: "realbin/testpkg",
diff --git a/services/device/internal/impl/perms/debug_perms_test.go b/services/device/internal/impl/perms/debug_perms_test.go
index 9c6e757..296c3b8 100644
--- a/services/device/internal/impl/perms/debug_perms_test.go
+++ b/services/device/internal/impl/perms/debug_perms_test.go
@@ -62,7 +62,7 @@
 	// TODO(rjkroege): Set AccessLists here that conflict with the one provided by the device
 	// manager and show that the one set here is overridden.
 	// Create the envelope for the first version of the app.
-	*envelope = utiltest.EnvelopeFromShell(sh, nil, utiltest.AppCmd, "google naps", "appV1")
+	*envelope = utiltest.EnvelopeFromShell(sh, nil, utiltest.AppCmd, "google naps", 0, 0, "appV1")
 
 	// Install the app.
 	appID := utiltest.InstallApp(t, ctx)
diff --git a/services/device/internal/impl/perms/perms_test.go b/services/device/internal/impl/perms/perms_test.go
index 83bd086..69fe3c1 100644
--- a/services/device/internal/impl/perms/perms_test.go
+++ b/services/device/internal/impl/perms/perms_test.go
@@ -59,7 +59,7 @@
 	pid := servicetest.ReadPID(t, dmh)
 	defer syscall.Kill(pid, syscall.SIGINT)
 
-	*envelope = utiltest.EnvelopeFromShell(sh, nil, utiltest.AppCmd, "google naps", "trapp")
+	*envelope = utiltest.EnvelopeFromShell(sh, nil, utiltest.AppCmd, "google naps", 0, 0, "trapp")
 
 	claimantCtx := utiltest.CtxWithNewPrincipal(t, ctx, idp, "claimant")
 	octx, err := v23.WithPrincipal(ctx, testutil.NewPrincipal("other"))
@@ -142,7 +142,7 @@
 	defer utiltest.VerifyNoRunningProcesses(t)
 
 	// Create an envelope for an app.
-	*envelope = utiltest.EnvelopeFromShell(sh, nil, utiltest.AppCmd, "google naps")
+	*envelope = utiltest.EnvelopeFromShell(sh, nil, utiltest.AppCmd, "google naps", 0, 0)
 
 	// On an unclaimed device manager, there will be no AccessLists.
 	if _, _, err := device.DeviceClient("claimable").GetPermissions(selfCtx); err == nil {
diff --git a/services/device/internal/impl/reaping/instance_reaping_test.go b/services/device/internal/impl/reaping/instance_reaping_test.go
index 7dd0c55..d4c1aeb 100644
--- a/services/device/internal/impl/reaping/instance_reaping_test.go
+++ b/services/device/internal/impl/reaping/instance_reaping_test.go
@@ -43,7 +43,7 @@
 	utiltest.Resolve(t, ctx, "pingserver", 1)
 
 	// Create an envelope for the app.
-	*envelope = utiltest.EnvelopeFromShell(sh, nil, utiltest.AppCmd, "google naps", "appV1")
+	*envelope = utiltest.EnvelopeFromShell(sh, nil, utiltest.AppCmd, "google naps", 0, 0, "appV1")
 
 	// Install the app.
 	appID := utiltest.InstallApp(t, ctx)
diff --git a/services/device/internal/impl/utiltest/helpers.go b/services/device/internal/impl/utiltest/helpers.go
index 0f2305a..b92adf6 100644
--- a/services/device/internal/impl/utiltest/helpers.go
+++ b/services/device/internal/impl/utiltest/helpers.go
@@ -69,15 +69,17 @@
 	}
 }
 
-func EnvelopeFromShell(sh *modules.Shell, env []string, cmd, title string, args ...string) application.Envelope {
+func EnvelopeFromShell(sh *modules.Shell, env []string, cmd, title string, retries int, window time.Duration, args ...string) application.Envelope {
 	args, nenv := sh.CommandEnvelope(cmd, env, args...)
 	return application.Envelope{
 		Title: title,
 		Args:  args[1:],
 		// TODO(caprita): revisit how the environment is sanitized for arbirary
 		// apps.
-		Env:    impl.VanadiumEnvironment(nenv),
-		Binary: application.SignedFile{File: MockBinaryRepoName},
+		Env:               impl.VanadiumEnvironment(nenv),
+		Binary:            application.SignedFile{File: MockBinaryRepoName},
+		Restarts:          int32(retries),
+		RestartTimeWindow: window,
 	}
 }
 
diff --git a/services/identity/internal/blesser/macaroon.go b/services/identity/internal/blesser/macaroon.go
index c96c7cf..1cada67 100644
--- a/services/identity/internal/blesser/macaroon.go
+++ b/services/identity/internal/blesser/macaroon.go
@@ -5,7 +5,9 @@
 package blesser
 
 import (
+	"errors"
 	"fmt"
+	"reflect"
 	"time"
 
 	"v.io/x/ref/services/identity"
@@ -45,6 +47,13 @@
 	if secCall.LocalPrincipal() == nil {
 		return empty, fmt.Errorf("server misconfiguration: no authentication happened")
 	}
+	macaroonPublicKey, err := security.UnmarshalPublicKey(m.PublicKey)
+	if err != nil {
+		return empty, fmt.Errorf("failed to unmarshal public key in macaroon: %v", err)
+	}
+	if !reflect.DeepEqual(secCall.RemoteBlessings().PublicKey(), macaroonPublicKey) {
+		return empty, errors.New("remote end's public key does not match public key in macaroon")
+	}
 	if len(m.Caveats) == 0 {
 		m.Caveats = []security.Caveat{security.UnconstrainedUse()}
 	}
diff --git a/services/identity/internal/blesser/macaroon_test.go b/services/identity/internal/blesser/macaroon_test.go
index eee0dab..541f903 100644
--- a/services/identity/internal/blesser/macaroon_test.go
+++ b/services/identity/internal/blesser/macaroon_test.go
@@ -22,6 +22,7 @@
 	var (
 		key            = make([]byte, 16)
 		provider, user = testutil.NewPrincipal(), testutil.NewPrincipal()
+		userKey, _     = user.PublicKey().MarshalBinary()
 		cOnlyMethodFoo = newCaveat(security.NewMethodCaveat("Foo"))
 		ctx, call      = fakeContextAndCall(provider, user)
 	)
@@ -30,12 +31,20 @@
 	}
 	blesser := NewMacaroonBlesserServer(key)
 
-	m := oauth.BlessingMacaroon{Creation: time.Now().Add(-1 * time.Hour), Name: "foo"}
+	m := oauth.BlessingMacaroon{Creation: time.Now().Add(-1 * time.Hour), Name: "foo", PublicKey: userKey}
 	wantErr := "macaroon has expired"
 	if _, err := blesser.Bless(ctx, call, newMacaroon(t, key, m)); err == nil || err.Error() != wantErr {
 		t.Errorf("Bless(...) failed with error: %v, want: %v", err, wantErr)
 	}
-	m = oauth.BlessingMacaroon{Creation: time.Now(), Name: "bugsbunny", Caveats: []security.Caveat{cOnlyMethodFoo}}
+
+	otherKey, _ := testutil.NewPrincipal().PublicKey().MarshalBinary()
+	m = oauth.BlessingMacaroon{Creation: time.Now(), Name: "foo", PublicKey: otherKey}
+	wantErr = "remote end's public key does not match public key in macaroon"
+	if _, err := blesser.Bless(ctx, call, newMacaroon(t, key, m)); err == nil || err.Error() != wantErr {
+		t.Errorf("Bless(...) failed with error: %v, want: %v", err, wantErr)
+	}
+
+	m = oauth.BlessingMacaroon{Creation: time.Now(), PublicKey: userKey, Name: "bugsbunny", Caveats: []security.Caveat{cOnlyMethodFoo}}
 	b, err := blesser.Bless(ctx, call, newMacaroon(t, key, m))
 	if err != nil {
 		t.Errorf("Bless failed: %v", err)
diff --git a/services/identity/internal/oauth/handler.go b/services/identity/internal/oauth/handler.go
index 97e962c..6d339b6 100644
--- a/services/identity/internal/oauth/handler.go
+++ b/services/identity/internal/oauth/handler.go
@@ -89,9 +89,10 @@
 
 // BlessingMacaroon contains the data that is encoded into the macaroon for creating blessings.
 type BlessingMacaroon struct {
-	Creation time.Time
-	Caveats  []security.Caveat
-	Name     string
+	Creation  time.Time
+	Caveats   []security.Caveat
+	Name      string
+	PublicKey []byte // Marshaled public key of the principal tool.
 }
 
 func redirectURL(baseURL, suffix string) string {
@@ -313,6 +314,7 @@
 
 type seekBlessingsMacaroon struct {
 	RedirectURL, State string
+	PublicKey          []byte // Marshaled public key of the principal tool.
 }
 
 func validLoopbackURL(u string) (*url.URL, error) {
@@ -340,9 +342,16 @@
 		util.HTTPBadRequest(w, r, fmt.Errorf("invalid redirect_url: %v", err))
 		return
 	}
+	pubKeyBytes, err := base64.URLEncoding.DecodeString(r.FormValue("public_key"))
+	if err != nil {
+		vlog.Infof("seekBlessings failed: invalid public_key: %v", err)
+		util.HTTPBadRequest(w, r, fmt.Errorf("invalid public_key: %v", err))
+		return
+	}
 	outputMacaroon, err := h.csrfCop.NewToken(w, r, clientIDCookie, seekBlessingsMacaroon{
 		RedirectURL: redirect,
 		State:       r.FormValue("state"),
+		PublicKey:   pubKeyBytes,
 	})
 	if err != nil {
 		vlog.Infof("Failed to create CSRF token[%v] for request %#v", err, r)
@@ -354,6 +363,7 @@
 
 type addCaveatsMacaroon struct {
 	ToolRedirectURL, ToolState, Email string
+	ToolPublicKey                     []byte // Marshaled public key of the principal tool.
 }
 
 func (h *handler) addCaveats(w http.ResponseWriter, r *http.Request) {
@@ -370,6 +380,7 @@
 	outputMacaroon, err := h.csrfCop.NewToken(w, r, clientIDCookie, addCaveatsMacaroon{
 		ToolRedirectURL: inputMacaroon.RedirectURL,
 		ToolState:       inputMacaroon.State,
+		ToolPublicKey:   inputMacaroon.PublicKey,
 		Email:           email,
 	})
 	if err != nil {
@@ -426,9 +437,10 @@
 		return
 	}
 	m := BlessingMacaroon{
-		Creation: time.Now(),
-		Caveats:  caveats,
-		Name:     strings.Join(parts, security.ChainSeparator),
+		Creation:  time.Now(),
+		Caveats:   caveats,
+		Name:      strings.Join(parts, security.ChainSeparator),
+		PublicKey: inputMacaroon.ToolPublicKey,
 	}
 	macBytes, err := vom.Encode(m)
 	if err != nil {
diff --git a/services/internal/fs/simplestore.go b/services/internal/fs/simplestore.go
index 8ae5457..2211ecd 100644
--- a/services/internal/fs/simplestore.go
+++ b/services/internal/fs/simplestore.go
@@ -15,6 +15,7 @@
 	"sort"
 	"strings"
 	"sync"
+	"time"
 
 	"v.io/x/ref/services/profile"
 
@@ -86,12 +87,14 @@
 // implementation by removing support for loading files in the
 // now legacy GOB format.
 type applicationEnvelope struct {
-	Title     string
-	Args      []string
-	Binary    application.SignedFile
-	Publisher security.WireBlessings
-	Env       []string
-	Packages  application.Packages
+	Title             string
+	Args              []string
+	Binary            application.SignedFile
+	Publisher         security.WireBlessings
+	Env               []string
+	Packages          application.Packages
+	Restarts          int32
+	RestartTimeWindow time.Duration
 }
 
 // This function is needed only to support existing serialized data and
@@ -114,12 +117,14 @@
 		return nil, err
 	}
 	return application.Envelope{
-		Title:     env.Title,
-		Args:      env.Args,
-		Binary:    env.Binary,
-		Publisher: publisher,
-		Env:       env.Env,
-		Packages:  env.Packages,
+		Title:             env.Title,
+		Args:              env.Args,
+		Binary:            env.Binary,
+		Publisher:         publisher,
+		Env:               env.Env,
+		Packages:          env.Packages,
+		Restarts:          env.Restarts,
+		RestartTimeWindow: env.RestartTimeWindow,
 	}, nil
 }
 
@@ -131,7 +136,7 @@
 	// Ensure that no fields have been added to application.Envelope,
 	// because if so, then applicationEnvelope defined in this package
 	// needs to change
-	if n := reflect.TypeOf(application.Envelope{}).NumField(); n != 6 {
+	if n := reflect.TypeOf(application.Envelope{}).NumField(); n != 8 {
 		panic(fmt.Sprintf("It appears that fields have been added to or removed from application.Envelope before the hack in this file around gob-encodeability was removed. Please also update applicationEnvelope, translateToGobEncodeable and translateToGobDecodeable in this file"))
 	}
 }