Merge "veyron/examples/stfortune: Bug fix to handle same fortune being put repeatedly."
diff --git a/examples/boxes/android/src/boxesp2p/main.go b/examples/boxes/android/src/boxesp2p/main.go
index b157517..8b0b724 100644
--- a/examples/boxes/android/src/boxesp2p/main.go
+++ b/examples/boxes/android/src/boxesp2p/main.go
@@ -68,14 +68,16 @@
"veyron/examples/boxes"
inaming "veyron/runtimes/google/naming"
vsync "veyron/runtimes/google/vsync"
- "veyron/services/store/raw"
- storage "veyron/services/store/server"
+ sstore "veyron/services/store/server"
"veyron2"
"veyron2/ipc"
"veyron2/naming"
"veyron2/rt"
"veyron2/security"
+ istore "veyron2/services/store"
+ iwatch "veyron2/services/watch"
+ "veyron2/storage"
"veyron2/storage/vstore"
"veyron2/storage/vstore/primitives"
"veyron2/vom"
@@ -95,7 +97,6 @@
type goState struct {
runtime veyron2.Runtime
- store *storage.Server
ipc ipc.Server
disp boxesDispatcher
drawStream boxes.DrawInterfaceServiceDrawStream
@@ -192,14 +193,16 @@
func (gs *goState) monitorStore() {
ctx := gs.runtime.NewContext()
+ vst, err := vstore.New(gs.storeEndpoint)
+ if err != nil {
+ panic(fmt.Errorf("Failed to init veyron store:%v", err))
+ }
+ root := vst.Bind("/")
+
// Watch for any box updates from the store
go func() {
- rst, err := raw.BindStore(naming.JoinAddressName(gs.storeEndpoint, raw.RawStoreSuffix))
- if err != nil {
- panic(fmt.Errorf("Failed to raw.Bind Store:%v", err))
- }
- req := raw.Request{}
- stream, err := rst.Watch(ctx, req, veyron2.CallTimeout(ipc.NoTimeout))
+ req := iwatch.GlobRequest{Pattern: "*"}
+ stream, err := root.WatchGlob(ctx, req)
if err != nil {
panic(fmt.Errorf("Can't watch store: %s: %s", gs.storeEndpoint, err))
}
@@ -209,20 +212,16 @@
panic(fmt.Errorf("Can't receive watch event: %s: %s", gs.storeEndpoint, err))
}
for _, change := range cb.Changes {
- if mu, ok := change.Value.(*raw.Mutation); ok && len(mu.Dir) == 0 {
- if box, ok := mu.Value.(boxes.Box); ok && box.DeviceId != gs.myIPAddr {
+ if entry, ok := change.Value.(*storage.Entry); ok {
+ if box, ok := entry.Value.(boxes.Box); ok && box.DeviceId != gs.myIPAddr {
nativeJava.addBox(&box)
}
}
}
}
}()
+
// Send any box updates to the store
- vst, err := vstore.New(gs.storeEndpoint)
- if err != nil {
- panic(fmt.Errorf("Failed to init veyron store:%v", err))
- }
- root := vst.Bind("/")
tr := primitives.NewTransaction(ctx)
if _, err := root.Put(ctx, tr, ""); err != nil {
panic(fmt.Errorf("Put for %s failed:%v", root, err))
@@ -321,10 +320,10 @@
}
// Create a new store server
- var err error
storeDBName := storePath + "/" + storeDatabase
- if gs.store, err = storage.New(storage.ServerConfig{Admin: publicID, DBName: storeDBName}); err != nil {
- panic(fmt.Errorf("storage.New() failed:%v", err))
+ store, err := sstore.New(sstore.ServerConfig{Admin: publicID, DBName: storeDBName})
+ if err != nil {
+ panic(fmt.Errorf("store.New() failed:%v", err))
}
// Create ACL Authorizer with read/write permissions for the identity
@@ -333,7 +332,7 @@
panic(fmt.Errorf("LoadACL failed:%v", err))
}
auth := security.NewACLAuthorizer(acl)
- gs.disp.storeDispatcher = storage.NewStoreDispatcher(gs.store, auth)
+ gs.disp.storeDispatcher = sstore.NewStoreDispatcher(store, auth)
// Create an endpoint and start listening
if _, err = gs.ipc.Listen("tcp", gs.myIPAddr+storeServicePort); err != nil {
@@ -360,7 +359,10 @@
}
func init() {
- vom.Register(&raw.Mutation{})
+ // Register *store.Entry for WatchGlob.
+ // TODO(tilaks): store.Entry is declared in vdl, vom should register the
+ // pointer automatically.
+ vom.Register(&istore.Entry{})
runtime.GOMAXPROCS(runtime.NumCPU())
}
diff --git a/examples/pipetobrowser/Makefile b/examples/pipetobrowser/Makefile
index 2acdaf0..22c9feb 100644
--- a/examples/pipetobrowser/Makefile
+++ b/examples/pipetobrowser/Makefile
@@ -43,9 +43,9 @@
$(VEYRON_IDENT) --name=veyron_p2b_identity > $(VEYRON_IDENTITY_PATH)
export VEYRON_IDENTITY=$(VEYRON_IDENTITY_PATH) ; \
- $(VEYRON_IDENTITYD) --port=$(VEYRON_IDENTITY_PORT) & \
+ $(VEYRON_IDENTITYD) --address=:$(VEYRON_IDENTITY_PORT) & \
$(VEYRON_MOUNTTABLE) --address=:$(VEYRON_MOUNTTABLE_PORT) & \
- export NAMESPACE_ROOT=/localhost:$(VEYRON_MOUNTTABLE_PORT)/mt ; \
+ export NAMESPACE_ROOT=/localhost:$(VEYRON_MOUNTTABLE_PORT) ; \
$(VEYRON_PROXY) -address=$(VEYRON_PROXY_ADDR) & \
$(VEYRON_WSPR) --v=1 -logtostderr=true -vproxy=$(VEYRON_PROXY_ADDR) --port $(VEYRON_WSPR_PORT) & \
$(VEYRON_STORE) --address=:$(VEYRON_STORE_PORT) --name=global/$(USER)/store &
diff --git a/examples/pipetobrowser/browser/actions/add-pipe-viewer.js b/examples/pipetobrowser/browser/actions/add-pipe-viewer.js
index 1919ec2..ae07987 100644
--- a/examples/pipetobrowser/browser/actions/add-pipe-viewer.js
+++ b/examples/pipetobrowser/browser/actions/add-pipe-viewer.js
@@ -14,6 +14,7 @@
import { displayError } from 'actions/display-error'
import { navigatePipesPage } from 'actions/navigate-pipes-page'
+import { redirectPipe } from 'actions/redirect-pipe'
import { LoadingView } from 'views/loading/view'
@@ -52,15 +53,16 @@
var count = (pipesPerNameCounter[name] || 0) + 1;
pipesPerNameCounter[name] = count;
var tabKey = name + count;
- var tabName = name + ' #' + count;
+ var tabName = 'Loading...';
// Get the plugin that can render the stream, ask it to play it and display
// the element returned by the pipeViewer.
getPipeViewer(name).then((pipeViewer) => {
+ tabName = pipeViewer.name + ' #' + count
return pipeViewer.play(stream);
}).then((pipeViewerView) => {
// replace the loading view with the actual viewerView
- pipesViewInstance.replaceTabView(tabKey, pipeViewerView);
+ pipesViewInstance.replaceTabView(tabKey, tabName, pipeViewerView);
}).catch((e) => { displayError(e); });
// Add a new tab and show a loading indicator for now,
@@ -68,6 +70,12 @@
var loadingView = new LoadingView();
pipesViewInstance.addTab(tabKey, tabName, loadingView);
+ // Add the redirect stream action
+ var icon = 'hardware:cast';
+ pipesViewInstance.addToolbarAction(tabKey, icon, () => {
+ redirectPipe(stream, name);
+ });
+
// Take the user to the pipes view.
navigatePipesPage();
-}
\ No newline at end of file
+}
diff --git a/examples/pipetobrowser/browser/actions/navigate-home-page.js b/examples/pipetobrowser/browser/actions/navigate-home-page.js
index 16f6406..ea90138 100644
--- a/examples/pipetobrowser/browser/actions/navigate-home-page.js
+++ b/examples/pipetobrowser/browser/actions/navigate-home-page.js
@@ -7,7 +7,7 @@
import { Logger } from 'libs/logs/logger'
import { register, trigger } from 'libs/mvc/actions'
-import { publish, stopPublishing, state as publishState } from 'services/pipe-to-browser'
+import { publish, stopPublishing, state as publishState } from 'services/pipe-to-browser-server'
import { displayError } from 'actions/display-error'
import { addPipeViewer } from 'actions/add-pipe-viewer'
diff --git a/examples/pipetobrowser/browser/actions/navigate-neighborhood.js b/examples/pipetobrowser/browser/actions/navigate-neighborhood.js
new file mode 100644
index 0000000..998e345
--- /dev/null
+++ b/examples/pipetobrowser/browser/actions/navigate-neighborhood.js
@@ -0,0 +1,49 @@
+/*
+ * Navigates to neighborhood page displaying list of P2B names that are online
+ * @fileoverview
+ */
+
+import { Logger } from 'libs/logs/logger'
+import { register, trigger } from 'libs/mvc/actions'
+
+import { displayError } from 'actions/display-error'
+import { page } from 'runtime/context'
+
+import { NeighborhoodView } from 'views/neighborhood/view'
+import { getAll as getAllPublishedP2BNames } from 'services/pipe-to-browser-namespace'
+
+var log = new Logger('actions/navigate-neighborhood');
+const ACTION_NAME = 'neighborhood';
+
+/*
+ * Registers the action
+ */
+export function registerNavigateNeigbourhoodAction() {
+ register(ACTION_NAME, actionHandler);
+}
+
+/*
+ * Triggers the action
+ */
+export function navigateNeigbourhood() {
+ return trigger(ACTION_NAME);
+}
+
+/*
+ * Handles the action.
+ *
+ * @private
+ */
+function actionHandler() {
+ log.debug('navigate neighborhood triggered');
+
+ // create an neighborhood view
+ var neighborhoodView = new NeighborhoodView();
+
+ // get all the online names and set it on the view
+ getAllPublishedP2BNames().then((allNames) => {
+ neighborhoodView.existingNames = allNames;
+ }).catch((e) => { displayError(e); });
+
+ page.setSubPageView('neighborhood', neighborhoodView);
+}
\ No newline at end of file
diff --git a/examples/pipetobrowser/browser/actions/redirect-pipe.js b/examples/pipetobrowser/browser/actions/redirect-pipe.js
new file mode 100644
index 0000000..5d37832
--- /dev/null
+++ b/examples/pipetobrowser/browser/actions/redirect-pipe.js
@@ -0,0 +1,69 @@
+/*
+ * Redirects a stream to another veyron name. It prompts the user to pick
+ * a Veyron name before redirecting and allows the user to chose between
+ * redirecting all the data or just new incoming data.
+ * @fileoverview
+ */
+import { Logger } from 'libs/logs/logger'
+import { register, trigger } from 'libs/mvc/actions'
+
+import { page } from 'runtime/context'
+
+import { RedirectPipeDialogView } from 'views/redirect-pipe-dialog/view'
+import { pipe } from 'services/pipe-to-browser-client'
+import { getAll as getAllPublishedP2BNames } from 'services/pipe-to-browser-namespace'
+
+var log = new Logger('actions/redirect-pipe');
+const ACTION_NAME = 'redirect-pipe';
+
+/*
+ * Registers the redirect pipe action
+ */
+export function registerRedirectPipeAction() {
+ register(ACTION_NAME, actionHandler);
+}
+
+/*
+ * Triggers the redirect pipe action
+ * @param {stream} stream Stream object to redirect
+ * @param {string} currentPluginName name of the current plugin
+ */
+export function redirectPipe(stream, currentPluginName) {
+ return trigger(ACTION_NAME, stream, currentPluginName);
+}
+
+/*
+ * Handles the redirect pipe action.
+ *
+ * @private
+ */
+function actionHandler(stream, currentPluginName) {
+ log.debug('redirect pipe action triggered');
+
+ // display a dialog asking user where to redirect and whether to redirect
+ // all the data or just new data.
+ var dialog = new RedirectPipeDialogView();
+ dialog.open();
+
+ // if user decides to redirect, copy the stream and pipe it.
+ dialog.onRedirectAction((name, newDataOnly) => {
+ var copyStream = stream.copier.copy(newDataOnly);
+
+ pipe(name, copyStream).then(() => {
+ page.showToast('Redirected successfully to ' + name);
+ }).catch((e) => {
+ page.showToast('FAILED to redirect to ' + name + '. Please see console for error details.');
+ log.debug('FAILED to redirect to', name, e);
+ });
+ });
+
+ // also get the list of all existing P2B names in the namespace and supply it to the dialog
+ getAllPublishedP2BNames().then((allNames) => {
+ // append current plugin name to the veyron names for better UX
+ dialog.existingNames = allNames.map((n) => {
+ return n + '/pipe/' + (currentPluginName || ''); //TODO(aghassemi) publish issue
+ });
+ }).catch((e) => {
+ log.debug('getAllPublishedP2BNames failed', e);
+ });
+}
\ No newline at end of file
diff --git a/examples/pipetobrowser/browser/config.js b/examples/pipetobrowser/browser/config.js
index 0240c14..2fcb266 100644
--- a/examples/pipetobrowser/browser/config.js
+++ b/examples/pipetobrowser/browser/config.js
@@ -11,5 +11,6 @@
proxy: 'http://localhost:5165',
logLevel: veyronLogLevels.INFO
},
+ namespaceRoot: '/localhost:5167',
publishNamePrefix: 'google/p2b'
}
\ No newline at end of file
diff --git a/examples/pipetobrowser/browser/index.css b/examples/pipetobrowser/browser/index.css
new file mode 100644
index 0000000..3b0dd96
--- /dev/null
+++ b/examples/pipetobrowser/browser/index.css
@@ -0,0 +1,4 @@
+body {
+ font-family: 'Roboto', sans-serif;
+ color: rgba(0,0,0,0.87);
+}
\ No newline at end of file
diff --git a/examples/pipetobrowser/browser/index.html b/examples/pipetobrowser/browser/index.html
index 9315525..e37405b 100644
--- a/examples/pipetobrowser/browser/index.html
+++ b/examples/pipetobrowser/browser/index.html
@@ -10,20 +10,24 @@
<script src="libs/vendor/polymer/platform/platform.js"></script>
- <!-- TODO(aghassemi) For Production - Use Volcanize to combine all components into a single import at build time -->
- <link rel="import" href="views/page/component.html">
- <link rel="import" href="views/publish/component.html">
- <link rel="import" href="views/status/component.html">
- <link rel="import" href="views/error/component.html">
- <link rel="import" href="views/loading/component.html">
- <link rel="import" href="views/pipes/component.html">
+ <link rel="stylesheet" type="text/css" href="index.css"/>
- <link rel="import" href="libs/ui-components/blackhole/component.html">
+ <!-- TODO(aghassemi) For Production - Use Volcanize to combine all components into a single import at build time -->
+ <link rel="import" href="views/page/component.html"/>
+ <link rel="import" href="views/publish/component.html"/>
+ <link rel="import" href="views/status/component.html"/>
+ <link rel="import" href="views/error/component.html"/>
+ <link rel="import" href="views/loading/component.html"/>
+ <link rel="import" href="views/pipes/component.html"/>
+ <link rel="import" href="views/redirect-pipe-dialog/component.html"/>
+ <link rel="import" href="views/neighborhood/component.html"/>
+
+ <link rel="import" href="libs/ui-components/blackhole/component.html"/>
<!-- TODO(aghassemi) Dynamic loading of plugins. Plugins should be able to use a provided module to load web components if they need to. We don't want to load all plugins for no reason! There could be hundreds of them -->
- <link rel="import" href="pipe-viewers/builtin/console/component.html">
- <link rel="import" href="pipe-viewers/builtin/git/status/component.html">
- <link rel="import" href="pipe-viewers/builtin/vlog/component.html">
+ <link rel="import" href="pipe-viewers/builtin/console/component.html"/>
+ <link rel="import" href="pipe-viewers/builtin/git/status/component.html"/>
+ <link rel="import" href="pipe-viewers/builtin/vlog/component.html"/>
</head>
<body>
diff --git a/examples/pipetobrowser/browser/libs/css/common-style.css b/examples/pipetobrowser/browser/libs/css/common-style.css
index 62bc38c..cc263ea 100644
--- a/examples/pipetobrowser/browser/libs/css/common-style.css
+++ b/examples/pipetobrowser/browser/libs/css/common-style.css
@@ -2,6 +2,10 @@
display: none !important;
}
+.invisible {
+ visibility: hidden;
+}
+
.secondary-text {
color: rgba(0,0,0,.54);
}
diff --git a/examples/pipetobrowser/browser/libs/ui-components/data-grid/grid/component.css b/examples/pipetobrowser/browser/libs/ui-components/data-grid/grid/component.css
index 34574cf..87cdff3 100644
--- a/examples/pipetobrowser/browser/libs/ui-components/data-grid/grid/component.css
+++ b/examples/pipetobrowser/browser/libs/ui-components/data-grid/grid/component.css
@@ -29,7 +29,8 @@
}
paper-dialog {
- width: 50%;
+ max-width: 80em;
+ width: 80vw;
}
/* hack: wrong z-index in shadow of paper-dialog disables text selection in dialog */
@@ -82,4 +83,18 @@
.more-dialog-content [moreInfoOnly] {
display: initial;
+}
+
+.paginator {
+ display: inline-block;
+ border: solid 1px rgba(0, 0, 0, 0.05);
+ box-shadow: 2px 2px 4px rgba(0, 0, 0, 0.15);
+ color: rgba(0, 0, 0, 0.54);
+ fill: rgba(0, 0, 0, 0.54);
+ margin: 1em;
+ font-size: 0.9em;
+}
+
+.paginator paper-icon-button {
+ vertical-align: middle;
}
\ No newline at end of file
diff --git a/examples/pipetobrowser/browser/libs/ui-components/data-grid/grid/component.html b/examples/pipetobrowser/browser/libs/ui-components/data-grid/grid/component.html
index 55498b0..0824c37 100644
--- a/examples/pipetobrowser/browser/libs/ui-components/data-grid/grid/component.html
+++ b/examples/pipetobrowser/browser/libs/ui-components/data-grid/grid/component.html
@@ -8,13 +8,13 @@
<link rel="import" href="/libs/ui-components/data-grid/grid/cell/renderer.html">
<link rel="import" href="/libs/ui-components/data-grid/grid/column/renderer.html">
-<polymer-element name="p2b-grid" attributes="summary dataSource defaultSortKey defaultSortAscending">
+<polymer-element name="p2b-grid" attributes="summary dataSource defaultSortKey defaultSortAscending pageSize">
<template>
<link rel="stylesheet" href="/libs/css/common-style.css">
<link rel="stylesheet" href="component.css">
<div id="templates"></div>
<core-collapse id="searchTools">
- <div class="result-count">Showing {{ dataSourceResult.length }} items</div>
+ <div class="result-count">Showing {{ dataSourceResult.length }} items of {{totalNumItems}}</div>
<div>
<content select="[grid-search]"></content>
</div>
@@ -43,15 +43,26 @@
<template ref="{{ col.cellTemplateId }}" bind></template>
</td>
<td class="info-column">
- <paper-icon-button on-tap="{{ showMoreInfo }}" class="more-icon" icon="more-vert" title="more info"></paper-icon-button
+ <paper-icon-button on-click="{{ showMoreInfo }}" class="more-icon" icon="more-vert" title="more info"></paper-icon-button
>
</td>
</tr>
</tbody>
</table>
+ <!-- Pagination -->
+ <template if="{{totalNumPages > 1}}">
+ <div class="paginator">
+ <paper-icon-button title="Previous page" icon="hardware:keyboard-arrow-left"
+ class="{{ {invisible : pageNumber == 1} | tokenList }}" on-click="{{ previousPage }}"></paper-icon-button>
+ <span>Page {{ pageNumber }} of {{ totalNumPages }}</span>
+ <paper-icon-button title="Next page" icon="hardware:keyboard-arrow-right"
+ class="{{ {invisible : onLastPage } | tokenList }}" on-click="{{ nextPage }}"></paper-icon-button>
+ </div>
+ </template>
+
<!-- Dialog that displays all columns and their values when more info icon activated -->
- <paper-dialog layered id="dialog" heading="Details" transition="paper-dialog-transition-bottom">
+ <paper-dialog id="dialog" heading="Details" transition="paper-dialog-transition-bottom">
<template id="moreInfoTemplate" bind>
<div class="more-dialog-content">
<template repeat="{{ item in selectedItems }}">
@@ -62,6 +73,7 @@
</template>
</div>
</template>
+ <paper-button label="Close" dismissive></paper-button>
</paper-dialog>
</template>
@@ -143,11 +155,14 @@
*/
defaultSortAscending: false,
- showMoreInfo: function(e) {
- // quirk + hack: cheating here and breaking shadow dom boundaries
- // since paper-dialog does not support backdrop yet
- this.$.dialog.shadowRoot.querySelector('#overlay').backdrop = true;
+ /*
+ * Number if items displayed in each page.
+ * Defaults to 30
+ * @type {integer}
+ */
+ pageSize: 30,
+ showMoreInfo: function(e) {
var item = e.target.templateInstance.model.item;
this.selectedItems = [item];
this.$.dialog.opened = true;
@@ -158,6 +173,7 @@
// private property fields
this.columns = [];
this.shouldRefetchData = true;
+ this.pageNumber = 1;
this.dataSource = null;
this.cachedDataSourceResult = [];
this.gridState = {
@@ -195,7 +211,7 @@
* @private
*/
dataSourceChanged: function() {
- this.refresh();
+ this.refresh(true);
},
/*
@@ -209,8 +225,8 @@
for (key in this.gridState) {
var observer = new ObjectObserver(this.gridState[key])
observer.open(function() {
- // refresh the grid on any mutations
- self.refresh();
+ // refresh the grid on any mutations and go back to page one
+ self.refresh(true);
});
}
},
@@ -283,10 +299,15 @@
/*
* Refreshed the grid by fetching the data again and updating the UI in the next render tick
+ * @param {bool} goBackToPageOne Optional parameter indicating that grid should go back
+ * to page 1 after refresh. false by default
*/
- refresh: function() {
+ refresh: function(goBackToPageOne) {
var self = this;
requestAnimationFrame(function() {
+ if (goBackToPageOne) {
+ self.pageNumber = 1;
+ }
self.shouldRefetchData = true;
});
},
@@ -375,12 +396,27 @@
return this.cachedDataSourceResult;
}
+ // fetch the data
this.cachedDataSourceResult = this.dataSource.fetch(
this.gridState.search,
this.gridState.sort,
this.gridState.filters
);
+ // page the data
+ this.totalNumItems = this.cachedDataSourceResult.length;
+ // if there less data than current page number, go back to page 1
+ if (this.totalNumItems < (this.pageNumber - 1) * this.pageSize) {
+ this.pageNumber = 1;
+ }
+ this.totalNumPages = Math.ceil(this.totalNumItems / this.pageSize);
+ this.onLastPage = this.totalNumPages == this.pageNumber;
+
+ // skip and take
+ var startIndex = (this.pageNumber - 1) * this.pageSize;
+ var endIndex = startIndex + this.pageSize;
+ this.cachedDataSourceResult = this.cachedDataSourceResult.slice(startIndex, endIndex);
+
this.shouldRefetchData = false;
return this.cachedDataSourceResult;
@@ -392,6 +428,20 @@
*/
toggleSearchTools: function() {
this.$.searchTools.toggle();
+ },
+
+ nextPage: function() {
+ if (!this.onLastPage) {
+ this.pageNumber++;
+ this.refresh();
+ }
+ },
+
+ previousPage: function() {
+ if (this.pageNumber > 1) {
+ this.pageNumber--;
+ this.refresh();
+ }
}
});
</script>
diff --git a/examples/pipetobrowser/browser/libs/utils/stream-copy.js b/examples/pipetobrowser/browser/libs/utils/stream-copy.js
new file mode 100644
index 0000000..7e1a38e
--- /dev/null
+++ b/examples/pipetobrowser/browser/libs/utils/stream-copy.js
@@ -0,0 +1,45 @@
+var Transform = require('stream').Transform;
+var PassThrough = require('stream').PassThrough;
+var Buffer = require('buffer').Buffer;
+/*
+ * A through transform stream keep a copy of the data piped to it and provides
+ * functions to create new copies of the stream on-demand
+ * @class
+ */
+export class StreamCopy extends Transform {
+ constructor() {
+ super();
+ this._writableState.objectMode = true;
+ this._readableState.objectMode = true;
+ // TODO(aghassemi) make this a FIFO buffer with reasonable max-size
+ this.buffer = [];
+ this.copies = [];
+ }
+
+ _transform(chunk, encoding, cb) {
+ this.buffer.push(chunk);
+ this.push(chunk);
+ for (var i=0; i < this.copies.length; i++) {
+ this.copies[i].push(chunk);
+ }
+ cb();
+ }
+
+ /*
+ * Create a new copy of the stream
+ * @param {bool} onlyNewData Whether the copy should include
+ * existing data from the stream or just new data.
+ * @return {Stream} Copy of the stream
+ */
+ copy(onlyNewData) {
+ var copy = new PassThrough( { objectMode: true });
+ if (!onlyNewData) {
+ // copy existing data first in the order received
+ for (var i = 0; i < this.buffer.length; i++) {
+ copy.push(this.buffer[i]);
+ }
+ }
+ this.copies.push(copy);
+ return copy;
+ }
+}
diff --git a/examples/pipetobrowser/browser/libs/utils/stream-helpers.js b/examples/pipetobrowser/browser/libs/utils/stream-helpers.js
new file mode 100644
index 0000000..cbf95b9
--- /dev/null
+++ b/examples/pipetobrowser/browser/libs/utils/stream-helpers.js
@@ -0,0 +1,6 @@
+var es = require('event-stream');
+export var streamUtil = {
+ split: es.split,
+ map: es.map,
+ writeArray: es.writeArray
+};
\ No newline at end of file
diff --git a/examples/pipetobrowser/browser/pipe-viewers/builtin/git/status/plugin.js b/examples/pipetobrowser/browser/pipe-viewers/builtin/git/status/plugin.js
index 86db6ce..7ac8c63 100644
--- a/examples/pipetobrowser/browser/pipe-viewers/builtin/git/status/plugin.js
+++ b/examples/pipetobrowser/browser/pipe-viewers/builtin/git/status/plugin.js
@@ -7,14 +7,13 @@
*/
import { View } from 'view';
import { PipeViewer } from 'pipe-viewer';
-import { Logger } from 'logger'
+import { streamUtil } from 'stream-helpers';
+import { Logger } from 'logger';
import { parse } from './parser';
import { gitStatusDataSource } from './data-source';
var log = new Logger('pipe-viewers/builtin/git/status');
-var streamUtil = require('event-stream');
-
class GitStatusPipeViewer extends PipeViewer {
get name() {
return 'git/status';
diff --git a/examples/pipetobrowser/browser/pipe-viewers/builtin/vlog/plugin.js b/examples/pipetobrowser/browser/pipe-viewers/builtin/vlog/plugin.js
index 1891d5e..67b1779 100644
--- a/examples/pipetobrowser/browser/pipe-viewers/builtin/vlog/plugin.js
+++ b/examples/pipetobrowser/browser/pipe-viewers/builtin/vlog/plugin.js
@@ -8,13 +8,12 @@
*/
import { View } from 'view';
import { PipeViewer } from 'pipe-viewer';
+import { streamUtil } from 'stream-helpers';
import { Logger } from 'logger'
import { vLogDataSource } from './data-source';
var log = new Logger('pipe-viewers/builtin/vlog');
-var streamUtil = require('event-stream');
-
class vLogPipeViewer extends PipeViewer {
get name() {
return 'vlog';
diff --git a/examples/pipetobrowser/browser/runtime/app.js b/examples/pipetobrowser/browser/runtime/app.js
index 33072b7..d66ad2b 100644
--- a/examples/pipetobrowser/browser/runtime/app.js
+++ b/examples/pipetobrowser/browser/runtime/app.js
@@ -4,6 +4,8 @@
import { registerDisplayErrorAction } from 'actions/display-error'
import { registerAddPipeViewerAction } from 'actions/add-pipe-viewer'
import { registerNavigatePipesPageAction, navigatePipesPage } from 'actions/navigate-pipes-page'
+import { registerNavigateNeigbourhoodAction, navigateNeigbourhood } from 'actions/navigate-neighborhood'
+import { registerRedirectPipeAction } from 'actions/redirect-pipe'
import { SubPageItem } from 'views/page/view'
@@ -40,6 +42,8 @@
registerDisplayErrorAction();
registerAddPipeViewerAction();
registerNavigatePipesPageAction();
+ registerNavigateNeigbourhoodAction();
+ registerRedirectPipeAction();
}
/*
@@ -62,6 +66,12 @@
pipesSubPageItem.onActivate = navigatePipesPage;
page.subPages.push(pipesSubPageItem);
+ var neighborhoodSubPageItem = new SubPageItem('neighborhood');
+ neighborhoodSubPageItem.name = 'Neighborhood';
+ neighborhoodSubPageItem.icon = 'social:circles-extended';
+ neighborhoodSubPageItem.onActivate = navigateNeigbourhood;
+ page.subPages.push(neighborhoodSubPageItem);
+
var helpSubPageItem = new SubPageItem('help');
helpSubPageItem.name = 'Help';
helpSubPageItem.icon = 'help';
diff --git a/examples/pipetobrowser/browser/runtime/init.js b/examples/pipetobrowser/browser/runtime/init.js
index b5b1cc3..4180417 100644
--- a/examples/pipetobrowser/browser/runtime/init.js
+++ b/examples/pipetobrowser/browser/runtime/init.js
@@ -10,7 +10,8 @@
'pipe-viewer': 'pipe-viewers/pipe-viewer.js',
'pipe-viewer-delegation': 'pipe-viewers/pipe-viewer-delegation.js',
'view': 'libs/mvc/view.js',
- 'logger': 'libs/logs/logger.js'
+ 'logger': 'libs/logs/logger.js',
+ 'stream-helpers': 'libs/utils/stream-helpers.js'
};
System.import('runtime/app').then(function(app) {
diff --git a/examples/pipetobrowser/browser/services/pipe-to-browser-client.js b/examples/pipetobrowser/browser/services/pipe-to-browser-client.js
new file mode 100644
index 0000000..c03e780
--- /dev/null
+++ b/examples/pipetobrowser/browser/services/pipe-to-browser-client.js
@@ -0,0 +1,25 @@
+/*
+ * Implements a veyron client that can talk to a P2B service.
+ * @fileoverview
+ */
+import { Logger } from 'libs/logs/logger'
+import { config } from 'config'
+
+var log = new Logger('services/p2b-client');
+var veyron = new Veyron(config.veyron);
+
+/*
+ * Pipes a stream of data to the P2B service identified
+ * by the given veyron name.
+ * @param {string} name Veyron name of the destination service
+ * @param {Stream} Stream of data to pipe to it.
+ * @return {Promise} Promise indicating if piping was successful or not
+ */
+export function pipe(name, stream) {
+ var client = veyron.newClient();
+ return client.bindTo(name).then((remote) => {
+ var remoteStream = remote.pipe().stream;
+ stream.pipe(remoteStream);
+ return Promise.resolve();
+ });
+}
\ No newline at end of file
diff --git a/examples/pipetobrowser/browser/services/pipe-to-browser-namespace.js b/examples/pipetobrowser/browser/services/pipe-to-browser-namespace.js
new file mode 100644
index 0000000..648a528
--- /dev/null
+++ b/examples/pipetobrowser/browser/services/pipe-to-browser-namespace.js
@@ -0,0 +1,31 @@
+/*
+ * Implements a veyron client that talks to the namespace service and finds all
+ * the P2B services that are available.
+ * @fileoverview
+ */
+import { Logger } from 'libs/logs/logger'
+import { config } from 'config'
+
+var log = new Logger('services/p2b-namespace');
+var veyron = new Veyron(config.veyron);
+var client = veyron.newClient();
+
+/*
+ * Finds all the P2B services that are published by querying the namespace.
+ * @return {Promise} Promise resolving to an array of names for all published
+ * P2B services
+ */
+export function getAll() {
+ return client.bindTo(config.namespaceRoot).then((namespace) => {
+ var globResult = namespace.glob('google/p2b/*');
+ var p2bServices = [];
+ globResult.stream.on('data', (p2bServiceName) => {
+ p2bServices.push(p2bServiceName.name);
+ });
+
+ // wait until all the data arrives then return the collection
+ return globResult.then(() => {
+ return p2bServices;
+ });
+ });
+}
\ No newline at end of file
diff --git a/examples/pipetobrowser/browser/services/pipe-to-browser.js b/examples/pipetobrowser/browser/services/pipe-to-browser-server.js
similarity index 80%
rename from examples/pipetobrowser/browser/services/pipe-to-browser.js
rename to examples/pipetobrowser/browser/services/pipe-to-browser-server.js
index 3bf580a..5f40da4 100644
--- a/examples/pipetobrowser/browser/services/pipe-to-browser.js
+++ b/examples/pipetobrowser/browser/services/pipe-to-browser-server.js
@@ -4,15 +4,14 @@
* It also exposes the state of the service.
* @fileoverview
*/
-
import { Logger } from 'libs/logs/logger'
import { config } from 'config'
import { ByteObjectStreamAdapter } from 'libs/utils/byte-object-stream-adapter'
import { StreamByteCounter } from 'libs/utils/stream-byte-counter'
+import { StreamCopy } from 'libs/utils/stream-copy'
-var log = new Logger('services/p2b');
-var v = new Veyron(config.veyron);
-var server = v.newServer();
+var log = new Logger('services/p2b-server');
+var server;
// State of p2b service
export var state = {
@@ -47,7 +46,8 @@
*/
export function publish(name, pipeRequestHandler) {
log.debug('publishing under name:', name);
-
+ var veyron = new Veyron(config.veyron);
+ server = veyron.newServer();
/*
* Veyron pipe to browser service implementation.
* Implements the p2b IDL.
@@ -55,6 +55,9 @@
var p2b = {
pipe($suffix, $stream) {
return new Promise(function(resolve, reject) {
+ //TODO(aghassemi) publish-issue remove /pipe from the suffix
+ $suffix = $suffix.substr(5);
+
log.debug('received pipe request for:', $suffix);
var numBytesForThisCall = 0;
@@ -65,7 +68,9 @@
state.numBytes += numBytesRead;
});
- var stream = $stream.pipe(bufferStream).pipe(streamByteCounter);
+ var streamCopier = $stream.pipe(new StreamCopy());
+ var stream = streamCopier.pipe(bufferStream).pipe(streamByteCounter);
+ stream.copier = streamCopier;
bufferStream.on('end', () => {
log.debug('end of stream');
@@ -79,7 +84,6 @@
});
state.numPipes++;
-
pipeRequestHandler($suffix, stream);
});
}
@@ -87,13 +91,13 @@
state.publishing = true;
- return server.register(name, p2b).then(() => {
- return server.publish(config.publishNamePrefix).then((endpoint) => {
+ return server.register('pipe', p2b).then(() => { //TODO(aghassemi) publish-issue add pipe for now since we can't register under empty name
+ return server.publish(config.publishNamePrefix + '/' + name).then((endpoint) => { //TODO(aghassemi) publish-issue
log.debug('published with endpoint:', endpoint);
state.published = true;
state.publishing = false;
- state.fullServiceName = config.publishNamePrefix + '/' + name;
+ state.fullServiceName = config.publishNamePrefix + '/' + name + '/pipe'; //TODO(aghassemi) publish-issue
state.date = new Date();
return endpoint;
diff --git a/examples/pipetobrowser/browser/views/common/common.css b/examples/pipetobrowser/browser/views/common/common.css
index a12d237..af47ae2 100644
--- a/examples/pipetobrowser/browser/views/common/common.css
+++ b/examples/pipetobrowser/browser/views/common/common.css
@@ -22,4 +22,10 @@
display: flex;
flex-direction: column;
flex: 1 1 0px;
+}
+
+[page-title] {
+ font-size: 1.5em;
+ color: #4285f4;
+ margin: 0px;
}
\ No newline at end of file
diff --git a/examples/pipetobrowser/browser/views/namespace-list/component.css b/examples/pipetobrowser/browser/views/namespace-list/component.css
new file mode 100644
index 0000000..93ea771
--- /dev/null
+++ b/examples/pipetobrowser/browser/views/namespace-list/component.css
@@ -0,0 +1,19 @@
+[selectable] .selected {
+ background-color: #00e5ff;
+ opacity: 0.8;
+}
+
+[selectable] core-item {
+ cursor: pointer;
+}
+
+core-item {
+ box-sizing: border-box;
+ border-bottom: 1px solid rgba(0,0,0,0.05);
+ margin: 0;
+ padding: 0 1em;
+}
+
+core-item:last-child {
+ border-bottom: none;
+}
diff --git a/examples/pipetobrowser/browser/views/namespace-list/component.html b/examples/pipetobrowser/browser/views/namespace-list/component.html
new file mode 100644
index 0000000..e6faa7d
--- /dev/null
+++ b/examples/pipetobrowser/browser/views/namespace-list/component.html
@@ -0,0 +1,55 @@
+<link rel="import" href="/libs/vendor/polymer/polymer/polymer.html">
+<link rel="import" href="/libs/vendor/polymer/core-list/core-list.html">
+<link rel="import" href="/libs/vendor/polymer/core-item/core-item.html">
+
+<polymer-element name="p2b-namespace-list" attributes="names selectable">
+ <template>
+ <link rel="stylesheet" href="component.css">
+ <template if="{{_items.length > 0}}">
+ <core-list selectable?="{{selectable}}" height="40" on-core-activate="{{fireSelectEvent}}" data="{{_items}}" height="20">
+ <template>
+ <core-item class="{{ {selected: selected} | tokenList }}" label="{{name}}"></core-item>
+ </template>
+ </core-list>
+ </template>
+ </template>
+ <script>
+ Polymer('p2b-namespace-list', {
+ /*
+ * List of names to be displayed
+ * @type {Array<string>}
+ */
+ names: [],
+
+ /*
+ * Whether the names displayed are selectable
+ * if selectable, 'select' event will fire with the name of the selected item
+ * @type {boolean}
+ */
+ selectable: false,
+
+ /*
+ * transformed collection of names to objects
+ * @private
+ */
+ _items: [],
+ namesChanged: function() {
+ // transform from [string] to [object] since core-items expects array of objects
+ this._items = this.names.map( function(n) {
+ return {name: n};
+ });
+ },
+
+ /*
+ * fires the select event pass the name as event argument
+ * @private
+ */
+ fireSelectEvent: function(e) {
+ if (!this.selectable) {
+ return;
+ }
+ this.fire('select', e.detail.data.name);
+ }
+ });
+ </script>
+</polymer-element>
\ No newline at end of file
diff --git a/examples/pipetobrowser/browser/views/namespace-list/view.js b/examples/pipetobrowser/browser/views/namespace-list/view.js
new file mode 100644
index 0000000..e2b0f92
--- /dev/null
+++ b/examples/pipetobrowser/browser/views/namespace-list/view.js
@@ -0,0 +1,25 @@
+import { View } from 'libs/mvc/view'
+
+/*
+ * View showing a list of all p2b services from the namespace
+ * @class
+ * @extends {View}
+ */
+export class NamespaceListView extends View {
+ constructor(items) {
+ var el = document.createElement('p2b-namespace-list');
+ el.items = items;
+ super(el);
+ }
+
+/*
+ * Event that fires when user selects an item from the list.
+ * @event
+ * @type {string} name of the item that was selected
+ */
+ onSelectAction(eventHandler) {
+ this.element.addEventListener('select', (e) => {
+ eventHandler(e.detail);
+ });
+ }
+}
\ No newline at end of file
diff --git a/examples/pipetobrowser/browser/views/neighborhood/component.html b/examples/pipetobrowser/browser/views/neighborhood/component.html
new file mode 100644
index 0000000..7c7b797
--- /dev/null
+++ b/examples/pipetobrowser/browser/views/neighborhood/component.html
@@ -0,0 +1,20 @@
+<link rel="import" href="/libs/vendor/polymer/polymer/polymer.html">
+<link rel="import" href="/views/namespace-list/component.html">
+
+<polymer-element name="p2b-neighborhood">
+ <template>
+ <link rel="stylesheet" href="../common/common.css">
+ <h2 page-title>Neighborhood</h2>
+ <p>List of PipeToBrowser instances currently published</p>
+ <p2b-namespace-list names="{{existingNames}}"></p2b-namespace-list>
+ </template>
+ <script>
+ Polymer('p2b-neighborhood', {
+ /*
+ * List of existing names to show
+ * @type {Array<string>}
+ */
+ existingNames: [],
+ });
+ </script>
+</polymer-element>
\ No newline at end of file
diff --git a/examples/pipetobrowser/browser/views/neighborhood/view.js b/examples/pipetobrowser/browser/views/neighborhood/view.js
new file mode 100644
index 0000000..2106ba2
--- /dev/null
+++ b/examples/pipetobrowser/browser/views/neighborhood/view.js
@@ -0,0 +1,21 @@
+import { View } from 'libs/mvc/view'
+
+/*
+ * View displaying a list of currently published PipeToBrowsers instances
+ * @class
+ * @extends {View}
+ */
+export class NeighborhoodView extends View {
+ constructor() {
+ var el = document.createElement('p2b-neighborhood');
+ super(el);
+ }
+
+ /*
+ * List of existing names to show
+ * @type {Array<string>}
+ */
+ set existingNames(val) {
+ this.element.existingNames = val;
+ }
+}
\ No newline at end of file
diff --git a/examples/pipetobrowser/browser/views/page/component.css b/examples/pipetobrowser/browser/views/page/component.css
index cecfd1c..787f3bb 100644
--- a/examples/pipetobrowser/browser/views/page/component.css
+++ b/examples/pipetobrowser/browser/views/page/component.css
@@ -1,7 +1,3 @@
-:host {
- font-family: 'Roboto', sans-serif;
- color: rgba(0,0,0,0.87);
-}
[drawer] {
background-color: #FAFAFA;
@@ -15,10 +11,12 @@
[sidebar] {
padding: 0.5em;
+ fill: #9e9e9e;
}
[sidebar] .core-selected {
color: #00bcd4;
+ fill: #212121;
}
[main] {
diff --git a/examples/pipetobrowser/browser/views/page/component.html b/examples/pipetobrowser/browser/views/page/component.html
index fdb070b..6e6cc54 100644
--- a/examples/pipetobrowser/browser/views/page/component.html
+++ b/examples/pipetobrowser/browser/views/page/component.html
@@ -7,6 +7,10 @@
<link rel="import" href="/libs/vendor/polymer/core-selector/core-selector.html">
<link rel="import" href="/libs/vendor/polymer/paper-appbar/paper-appbar.html">
<link rel="import" href="/libs/vendor/polymer/paper-icon-button/paper-icon-button.html">
+<link rel="import" href="/libs/vendor/polymer/paper-toast/paper-toast.html">
+<link rel="import" href="/libs/vendor/polymer/core-icons/iconsets/hardware-icons.html">
+<link rel="import" href="/libs/vendor/polymer/core-icons/iconsets/social-icons.html">
+
<link href='http://fonts.googleapis.com/css?family=Roboto' rel='stylesheet' type='text/css'>
<polymer-element name="p2b-page">
@@ -39,6 +43,7 @@
</core-selector>
</core-header-panel>
</core-drawer-panel>
+ <paper-toast id="toast"></paper-toast>
</template>
<script>
Polymer('p2b-page', {
@@ -86,6 +91,15 @@
});
},
+ /*
+ * Displays a message toast for a few seconds e.g. "Saved Successfully"
+ * @param {string} text Text of the toast
+ */
+ showToast: function(text) {
+ this.$.toast.text = text;
+ this.$.toast.show();
+ },
+
/*
* handler for when a sidebar item is clicked
* @param {string} key Key of the page
diff --git a/examples/pipetobrowser/browser/views/page/view.js b/examples/pipetobrowser/browser/views/page/view.js
index 7435950..b2e7aa0 100644
--- a/examples/pipetobrowser/browser/views/page/view.js
+++ b/examples/pipetobrowser/browser/views/page/view.js
@@ -23,6 +23,14 @@
}
/*
+ * Displayed a message toast for a few seconds e.g. "Saved Successfully"
+ * @type {SubPageItem}
+ */
+ showToast(text) {
+ this.element.showToast(text);
+ }
+
+ /*
* Collection of sub pages
* @type {SubPageItem}
*/
diff --git a/examples/pipetobrowser/browser/views/pipes/component.css b/examples/pipetobrowser/browser/views/pipes/component.css
index 5b79a3b..819c741 100644
--- a/examples/pipetobrowser/browser/views/pipes/component.css
+++ b/examples/pipetobrowser/browser/views/pipes/component.css
@@ -26,11 +26,8 @@
}
.empty-message {
- font-size: 1.5em;
- padding: 1.5em;
+ padding: 1em;
position: absolute;
- margin: 0px;
- color: #4285f4;
}
.container {
diff --git a/examples/pipetobrowser/browser/views/pipes/component.html b/examples/pipetobrowser/browser/views/pipes/component.html
index f72aef3..ad47a07 100644
--- a/examples/pipetobrowser/browser/views/pipes/component.html
+++ b/examples/pipetobrowser/browser/views/pipes/component.html
@@ -14,7 +14,7 @@
<link rel="stylesheet" href="component.css">
<div class="container" flex>
<template if="{{ numTabs == 0 }}">
- <p class="empty-message">No pipes to show...</p>
+ <h2 page-title class="empty-message">No pipes to show...</h2>
<div class="no-pipes-bg"></div>
</template>
@@ -94,13 +94,25 @@
this.pipeTabs[key] = {
name: name,
tab: tab,
- tabContent: tabContent
+ tabContent: tabContent,
+ tabToolbar: tabToolbar
};
this.selectedTabKey = key;
},
/*
+ * Adds a new toolbar action for the tab's toolbar
+ * @param {string} tabKey Key of the tab to add action to
+ * @param icon {string} icon Icon name for the action
+ * @param onClick {function} event handler for the action
+ */
+ addToolbarAction: function(tabKey, icon, onClick) {
+ var toolbar = this.pipeTabs[tabKey].tabToolbar;
+ toolbar.add(icon, onClick);
+ },
+
+ /*
* Removes a tab
* @param {string} key Key of the tab to remove
*/
@@ -128,11 +140,16 @@
/*
* Replaces content of a tab
* @param {string} key Key of the tab to replace content for
+ * @param {string} newName new name for the tab
* @param {DOMElement} el New content of the tab
*/
- replaceTabContent: function(key, newEl) {
+ replaceTabContent: function(key, newName, newEl) {
var tabContent = this.pipeTabs[key].tabContent;
tabContent.replaceTabContent(newEl);
+ if (newName) {
+ this.pipeTabs[key].tab.textContent = newName;
+ this.pipeTabs[key].tabToolbar.title = newName;
+ }
},
/*
@@ -147,4 +164,4 @@
}
});
</script>
-</polymer-element>
\ No newline at end of file
+</polymer-element>
diff --git a/examples/pipetobrowser/browser/views/pipes/tab-toolbar/component.html b/examples/pipetobrowser/browser/views/pipes/tab-toolbar/component.html
index 98a930b..f34f1ee 100644
--- a/examples/pipetobrowser/browser/views/pipes/tab-toolbar/component.html
+++ b/examples/pipetobrowser/browser/views/pipes/tab-toolbar/component.html
@@ -1,4 +1,5 @@
<link rel="import" href="/libs/vendor/polymer/polymer/polymer.html">
+<link rel="import" href="/libs/vendor/polymer/paper-icon-button/paper-icon-button.html">
<polymer-element name="p2b-pipes-tab-toolbar">
<template>
@@ -7,6 +8,7 @@
<span flex>
{{ title }}
</span>
+ <span id="customActions"></span>
<paper-icon-button id="fullscreenIcon" icon="fullscreen" on-click="{{ fireFullscreenAction }}"></paper-icon-button>
<paper-icon-button icon="close" on-click="{{ fireCloseAction }}"></paper-icon-button>
</core-toolbar>
@@ -34,6 +36,18 @@
*/
fireFullscreenAction: function() {
this.fire('fullscreen-action');
+ },
+
+ /*
+ * Adds a new action to the toolbar
+ * @param icon {string} icon Icon for the action
+ * @param onClick {function} event handler for the action
+ */
+ add: function(icon, onClick) {
+ var button = document.createElement('paper-icon-button');
+ button.icon = icon;
+ button.addEventListener('click', onClick);
+ this.$.customActions.appendChild(button);
}
});
</script>
diff --git a/examples/pipetobrowser/browser/views/pipes/view.js b/examples/pipetobrowser/browser/views/pipes/view.js
index 7f94b6b..8c9e2b4 100644
--- a/examples/pipetobrowser/browser/views/pipes/view.js
+++ b/examples/pipetobrowser/browser/views/pipes/view.js
@@ -23,12 +23,23 @@
this.element.addTab(key, name, view.element);
}
+ /*
+ * Adds a new toolbar action for the tab's toolbar
+ * @param {string} key Key of the tab to add action to
+ * @param icon {string} icon key for the action
+ * @param onClick {function} event handler for the action
+ */
+ addToolbarAction(tabKey, icon, onClick) {
+ this.element.addToolbarAction(tabKey, icon, onClick);
+ }
+
/*
* Replaces the content of the tab identified via key by the new view.
* @param {string} key A string key identifier for the tab.
+ * @param {string} newName New name for the tab
* @param {View} view View to replace the current tab content
*/
- replaceTabView(key, newView) {
- this.element.replaceTabContent(key, newView.element);
+ replaceTabView(key, newName, newView) {
+ this.element.replaceTabContent(key, newName, newView.element);
}
-}
\ No newline at end of file
+}
diff --git a/examples/pipetobrowser/browser/views/publish/view.js b/examples/pipetobrowser/browser/views/publish/view.js
index bc3843f..66b386d 100644
--- a/examples/pipetobrowser/browser/views/publish/view.js
+++ b/examples/pipetobrowser/browser/views/publish/view.js
@@ -22,57 +22,4 @@
eventHandler(e.detail.publishName);
});
}
-}
-
-
-
-/*veyron/examples/pipetobrowser: Supporting search (in message, file path, threadid),
-filtering (log levels, date), sorting (all columns except message) for
-veyron log viewer. Also adding ability to pause/resume the log viewer.
-
-To support these features for veyron log viewer and other plugins,
-a new reusable data grid component was created (libs/ui-components/data-grid)
-data grid can host search, filters and supports sortable columns and custom cell renderer
-example usage:
-
- <p2b-grid defaultSortKey="firstName"
- defaultSortAscending
- dataSource="{{ myContactsDataSource }}"
- summary="Displays your contacts in a tabular format">
-
- <!-- Search contacts-->
- <p2b-grid-search label="Search Contacts"></p2b-grid-search>
-
- <!-- Filter for circles -->
- <p2b-grid-filter-select multiple key="circle" label="Circles">
- <p2b-grid-filter-select-item checked label="Close Friends" value="close"></p2b-grid-filter-select-item>
- <p2b-grid-filter-select-item label="Colleagues" value="far"></p2b-grid-filter-select-item>
- </p2b-grid-filter-select>
-
- <!-- Toggle to allow filtering by online mode-->
- <p2b-grid-filter-toggle key="online" label="Show online only" checked></p2b-grid-filter-toggle>
-
- <!-- Columns, sorting and cell templates -->
- <p2b-grid-column sortable label="First Name" key="firstName" />
- <template>{{ item.firstName }}</template>
- </p2b-grid-column>
-
- <p2b-grid-column sortable label="Last Name" key="lastName" />
- <template>
- <span style="text-transform:uppercase;">
- {{ item.lastName }}
- </span>
- </template>
- </p2b-grid-column>
-
- <p2b-grid-column label="Circle" key="circle"/>
- <template>
- <img src="images\circls\{{ item.circle }}.jpg" alt="in {{ item.circle }} circle"><img>
- </template>
- </p2b-grid-column>
-
-</p2b-grid>
-Please see documentation in (libs/ui-components/data-grid/grid/component.html) for more details.
-
-still TODO: UI cleanup
-*/
+}
\ No newline at end of file
diff --git a/examples/pipetobrowser/browser/views/redirect-pipe-dialog/component.css b/examples/pipetobrowser/browser/views/redirect-pipe-dialog/component.css
new file mode 100644
index 0000000..a58b136
--- /dev/null
+++ b/examples/pipetobrowser/browser/views/redirect-pipe-dialog/component.css
@@ -0,0 +1,27 @@
+paper-input {
+ width: 90%;
+}
+
+paper-input, paper-checkbox {
+ display: block;
+}
+
+paper-dialog {
+ max-width: 40em;
+ width: 80vw;
+}
+
+/* hack: wrong z-index in shadow of paper-dialog disables text selection in dialog */
+paper-dialog /deep/ #shadow {
+ z-index: -1 !important;
+}
+
+paper-button[affirmative] {
+ color: #4285f4;
+}
+
+.label {
+ margin: 0;
+ margin-top: 1em;
+ font-size: 1.1em;
+}
\ No newline at end of file
diff --git a/examples/pipetobrowser/browser/views/redirect-pipe-dialog/component.html b/examples/pipetobrowser/browser/views/redirect-pipe-dialog/component.html
new file mode 100644
index 0000000..d23a326
--- /dev/null
+++ b/examples/pipetobrowser/browser/views/redirect-pipe-dialog/component.html
@@ -0,0 +1,78 @@
+<link rel="import" href="/libs/vendor/polymer/polymer/polymer.html">
+<link rel="import" href="/libs/vendor/polymer/paper-input/paper-input.html">
+<link rel="import" href="/libs/vendor/polymer/paper-button/paper-button.html">
+<link rel="import" href="/libs/vendor/polymer/paper-dialog/paper-dialog.html">
+<link rel="import" href="/libs/vendor/polymer/paper-dialog/paper-dialog-transition.html">
+<link rel="import" href="/views/namespace-list/component.html">
+
+<polymer-element name="p2b-redirect-pipe-dialog">
+
+ <template id="template">
+ <link rel="stylesheet" href="../common/common.css">
+ <link rel="stylesheet" href="component.css">
+ <paper-dialog id="dialog" heading="Redirect" transition="paper-dialog-transition-bottom">
+ <p>
+ <paper-input focused id="nameInput" label="Name to redirect to" floatinglabel></paper-input>
+ <paper-checkbox id="newDataOnly" label="Only redirect new data"></paper-checkbox>
+ <template if="{{existingNames.length > 0}}">
+ <h2 class="label">Currently online</h2>
+ <p2b-namespace-list selectable on-select="{{updateNameInput}}" names="{{existingNames}}"></p2b-namespace-list>
+ </template>
+ </p>
+ <paper-button label="Cancel" dismissive></paper-button>
+ <paper-button label="Redirect" affirmative default on-tap="{{ fireRedirectActionEvent }}"></paper-button>
+ </paper-dialog>
+ </template>
+ <script>
+ Polymer('p2b-redirect-pipe-dialog', {
+
+ /*
+ * List of existing names to show in the dialog for the user to pick from
+ * @type {Array<string>}
+ */
+ existingNames: [],
+
+ ready: function() {
+ var self = this;
+ var dialog = this.$.dialog;
+ var container = document.querySelector('#redirectDialogContainer');
+ if (!container) {
+ var container = document.createElement('div');
+ container.id = 'redirectDialogContainer';
+ document.body.appendChild(container);
+ }
+ this.container = container;
+ },
+
+ /*
+ * Opens the dialog
+ */
+ open: function() {
+ this.container.innerHTML = ''
+ this.container.appendChild(this);
+ this.$.dialog.toggle();
+ },
+
+ /*
+ * Fires redirect event representing user's intention to redirect
+ * @type {string} Requested name for service to be redirected
+ * @type {boolean} Whether only new data should be redirected
+ */
+ fireRedirectActionEvent: function() {
+ var name = this.$.nameInput.value;
+ this.fire('redirect', {
+ name: name,
+ newDataOnly: this.$.newDataOnly.checked
+ });
+ },
+
+ /*
+ * Updates the input value
+ * @private
+ */
+ updateNameInput: function(e) {
+ this.$.nameInput.value = e.detail;
+ }
+ });
+ </script>
+</polymer-element>
\ No newline at end of file
diff --git a/examples/pipetobrowser/browser/views/redirect-pipe-dialog/view.js b/examples/pipetobrowser/browser/views/redirect-pipe-dialog/view.js
new file mode 100644
index 0000000..d369c49
--- /dev/null
+++ b/examples/pipetobrowser/browser/views/redirect-pipe-dialog/view.js
@@ -0,0 +1,42 @@
+import { View } from 'libs/mvc/view'
+
+/*
+ * View representing a dialog that asks the user where they want to redirect
+ * the current pipe and whether only new data should be redirected
+ * @class
+ * @extends {View}
+ */
+export class RedirectPipeDialogView extends View {
+ constructor() {
+ var el = document.createElement('p2b-redirect-pipe-dialog');
+ super(el);
+ }
+
+ /*
+ * Opens the Redirect Pipe Dialog
+ */
+ open() {
+ this.element.open();
+ }
+
+ /*
+ * List of existing names to show in the dialog for the user to pick from
+ * @type {Array<string>}
+ */
+ set existingNames(val) {
+ this.element.existingNames = val;
+ }
+
+ /*
+ * Event representing user's intention to redirect
+ * @event
+ * @type {string} Requested name for service to be redirected
+ * @type {boolean} Whether only new data should be redirected
+ */
+ onRedirectAction(eventHandler) {
+ this.element.addEventListener('redirect', (e) => {
+ eventHandler(e.detail.name, e.detail.newDataOnly);
+ });
+ }
+
+}
\ No newline at end of file
diff --git a/examples/rockpaperscissors/impl/impl_test.go b/examples/rockpaperscissors/impl/impl_test.go
index 9a2bc32..5a6a856 100644
--- a/examples/rockpaperscissors/impl/impl_test.go
+++ b/examples/rockpaperscissors/impl/impl_test.go
@@ -68,7 +68,7 @@
// that all the counters are consistent.
func TestRockPaperScissorsImpl(t *testing.T) {
runtime := rt.Init()
- defer runtime.Shutdown()
+ defer runtime.Cleanup()
mtAddress, mtStop := startMountTable(t, runtime)
defer mtStop()
rpsService, rpsStop := startRockPaperScissors(t, runtime, mtAddress)
diff --git a/examples/rockpaperscissors/rpsbot/main.go b/examples/rockpaperscissors/rpsbot/main.go
index 9e9a011..a29b8fc 100644
--- a/examples/rockpaperscissors/rpsbot/main.go
+++ b/examples/rockpaperscissors/rpsbot/main.go
@@ -29,7 +29,7 @@
func main() {
r := rt.Init()
- defer r.Shutdown()
+ defer r.Cleanup()
server, err := r.NewServer()
if err != nil {
vlog.Fatalf("NewServer failed: %v", err)
diff --git a/examples/rockpaperscissors/rpsplayercli/main.go b/examples/rockpaperscissors/rpsplayercli/main.go
index 7d0d0d9..074907e 100644
--- a/examples/rockpaperscissors/rpsplayercli/main.go
+++ b/examples/rockpaperscissors/rpsplayercli/main.go
@@ -31,7 +31,7 @@
func main() {
r := rt.Init()
- defer r.Shutdown()
+ defer r.Cleanup()
for {
if selectOne([]string{"Initiate Game", "Wait For Challenge"}) == 0 {
initiateGame()
diff --git a/examples/rockpaperscissors/rpsscorekeeper/main.go b/examples/rockpaperscissors/rpsscorekeeper/main.go
index aca0293..ff0aca9 100644
--- a/examples/rockpaperscissors/rpsscorekeeper/main.go
+++ b/examples/rockpaperscissors/rpsscorekeeper/main.go
@@ -35,7 +35,7 @@
func main() {
r := rt.Init()
- defer r.Shutdown()
+ defer r.Cleanup()
server, err := r.NewServer()
if err != nil {
vlog.Fatalf("NewServer failed: %v", err)
diff --git a/examples/runtime/complex_server.go b/examples/runtime/complex_server.go
index c8e5a01..66a0122 100644
--- a/examples/runtime/complex_server.go
+++ b/examples/runtime/complex_server.go
@@ -17,9 +17,9 @@
func complexServerProgram() {
// Initialize the runtime. This is boilerplate.
r := rt.Init()
- // r.Shutdown is optional, but it's a good idea to clean up, especially
+ // r.Cleanup is optional, but it's a good idea to clean up, especially
// since it takes care of flushing the logs before exiting.
- defer r.Shutdown()
+ defer r.Cleanup()
// Create a couple servers, and start serving.
server1 := makeServer()
diff --git a/examples/runtime/simple_server.go b/examples/runtime/simple_server.go
index 3fc9a85..b459d80 100644
--- a/examples/runtime/simple_server.go
+++ b/examples/runtime/simple_server.go
@@ -16,13 +16,13 @@
// Initialize the runtime. This is boilerplate.
r := rt.Init()
- // r.Shutdown is optional, but it's a good idea to clean up, especially
+ // r.Cleanup is optional, but it's a good idea to clean up, especially
// since it takes care of flushing the logs before exiting.
//
// We use defer to ensure this is the last thing in the program (to
// avoid shutting down the runtime while it may still be in use), and to
// allow it to execute even if a panic occurs down the road.
- defer r.Shutdown()
+ defer r.Cleanup()
// Create a server, and start serving.
server := makeServer()
diff --git a/examples/stfortune/stfortune/main.go b/examples/stfortune/stfortune/main.go
index a5127b5..e65a1b0 100644
--- a/examples/stfortune/stfortune/main.go
+++ b/examples/stfortune/stfortune/main.go
@@ -9,6 +9,7 @@
"fmt"
"log"
"math/rand"
+ "os"
"strings"
"time"
@@ -69,6 +70,52 @@
return
}
+// runAsWatcher monitors updates to the fortunes in the store and
+// prints out that information. It does not return.
+func runAsWatcher(store storage.Store, user string) {
+ // TODO(tilaks): remove this when the store.Entry is auto-registered by VOM.
+ vom.Register(&istore.Entry{})
+ ctx := rt.R().NewContext()
+
+ // Monitor all new fortunes or only those of a specific user.
+ var path string
+ if user == "" {
+ path = fortunePath("")
+ } else {
+ path = userPath(user)
+ }
+ fmt.Printf("Running as a Watcher monitoring new fortunes under %s...\n", path)
+
+ req := iwatch.GlobRequest{Pattern: "*"}
+ stream, err := store.Bind(path).WatchGlob(ctx, req)
+ if err != nil {
+ log.Fatalf("watcher WatchGlob %s failed: %v", path, err)
+ }
+
+ for {
+ batch, err := stream.Recv()
+ if err != nil {
+ log.Fatalf("watcher Recv failed: %v", err)
+ }
+
+ for _, change := range batch.Changes {
+ entry, ok := change.Value.(*storage.Entry)
+ if !ok {
+ log.Printf("watcher change Value not a storage Entry: %#v", change.Value)
+ continue
+ }
+
+ fortune, ok := entry.Value.(schema.FortuneData)
+ if !ok {
+ log.Printf("watcher data not a FortuneData Entry: %#v", entry.Value)
+ continue
+ }
+
+ fmt.Printf("watcher: new fortune: %s\n", fortune.Fortune)
+ }
+ }
+}
+
// pickFortune finds all available fortunes under the input path and
// chooses one randomly.
func pickFortune(store storage.Store, ctx context.T, path string) (string, error) {
@@ -199,6 +246,7 @@
storeAddress = flag.String("store", "", "the address/endpoint of the Veyron Store")
newFortune = flag.String("new_fortune", "", "an optional, new fortune to add to the server's set")
user = flag.String("user_name", "", "an optional username of the fortune creator to get/add to the server's set")
+ watch = flag.Bool("watch", false, "run as a watcher reporting new fortunes")
)
func main() {
@@ -233,4 +281,10 @@
log.Fatal("error adding fortune: ", err)
}
}
+
+ // Run as a watcher if --watch is set.
+ if *watch {
+ runAsWatcher(store, *user)
+ os.Exit(0)
+ }
}
diff --git a/examples/tunnel/tunneld/main.go b/examples/tunnel/tunneld/main.go
index 4520127..687a284 100644
--- a/examples/tunnel/tunneld/main.go
+++ b/examples/tunnel/tunneld/main.go
@@ -41,7 +41,7 @@
func main() {
r := rt.Init()
- defer r.Shutdown()
+ defer r.Cleanup()
server, err := r.NewServer()
if err != nil {
vlog.Fatalf("NewServer failed: %v", err)
@@ -68,9 +68,10 @@
fmt.Sprintf("tunnel/hwaddr/%s", hwaddr),
fmt.Sprintf("tunnel/id/%s", rt.R().Identity().PublicID()),
}
+ dispatcher := ipc.SoloDispatcher(tunnel.NewServerTunnel(&impl.T{}), sflag.NewAuthorizerOrDie())
published := false
for _, n := range names {
- if err := server.Serve(n, ipc.SoloDispatcher(tunnel.NewServerTunnel(&impl.T{}), sflag.NewAuthorizerOrDie())); err != nil {
+ if err := server.Serve(n, dispatcher); err != nil {
vlog.Infof("Serve(%v) failed: %v", n, err)
continue
}
diff --git a/examples/tunnel/tunneld/test.sh b/examples/tunnel/tunneld/test.sh
new file mode 100755
index 0000000..bc0631a
--- /dev/null
+++ b/examples/tunnel/tunneld/test.sh
@@ -0,0 +1,106 @@
+#!/bin/sh
+
+# Test the tunneld binary
+#
+# This test starts a tunnel server and a mounttable server and then verifies
+# that vsh can run commands through it and that all the expected names are
+# in the mounttable.
+
+toplevel=$(git rev-parse --show-toplevel)
+go=${toplevel}/scripts/build/go
+thisscript=$0
+
+echo "Test directory: $(dirname $0)"
+
+builddir=$(mktemp -d --tmpdir=${toplevel}/go)
+trap onexit EXIT
+
+onexit() {
+ cd /
+ exec 2> /dev/null
+ kill -9 $(jobs -p)
+ rm -rf $builddir
+}
+
+FAIL() {
+ [ $# -gt 0 ] && echo "$thisscript $*"
+ echo FAIL
+ exit 1
+}
+
+PASS() {
+ echo PASS
+ exit 0
+}
+
+# Build binaries.
+cd $builddir
+$go build veyron/examples/tunnel/tunneld || FAIL "line $LINENO: failed to build tunneld"
+$go build veyron/examples/tunnel/vsh || FAIL "line $LINENO: failed to build vsh"
+$go build veyron/services/mounttable/mounttabled || FAIL "line $LINENO: failed to build mounttabled"
+$go build veyron/tools/mounttable || FAIL "line $LINENO: failed to build mounttable"
+$go build veyron/tools/identity || FAIL "line $LINENO: failed to build identity"
+
+# Start mounttabled and find its endpoint.
+mtlog=$(mktemp --tmpdir=.)
+./mounttabled --address=localhost:0 > $mtlog 2>&1 &
+
+for i in 1 2 3 4; do
+ ep=$(grep "Mount table service at:" $mtlog | sed -re 's/^.*endpoint: ([^ ]*).*/\1/')
+ if [ -n "$ep" ]; then
+ break
+ fi
+ sleep 1
+done
+[ -z $ep ] && FAIL "line $LINENO: no mounttable server"
+
+tmpid=$(mktemp --tmpdir=.)
+./identity --name=test > $tmpid
+
+export NAMESPACE_ROOT=$ep
+export VEYRON_IDENTITY=$tmpid
+
+# Start tunneld and find its endpoint.
+tunlog=$(mktemp --tmpdir=.)
+./tunneld --address=localhost:0 > $tunlog 2>&1 &
+
+for i in 1 2 3 4; do
+ ep=$(grep "Listening on endpoint" $tunlog | sed -re 's/^.*endpoint ([^ ]*).*/\1/')
+ if [ -n "$ep" ]; then
+ break
+ fi
+ sleep 1
+done
+[ -z $ep ] && FAIL "line $LINENO: no tunnel server"
+
+# Run remote command with the endpoint.
+got=$(./vsh $ep echo HELLO WORLD)
+want="HELLO WORLD"
+
+if [ "$got" != "$want" ]; then
+ FAIL "line $LINENO: unexpected output. Got $got, want $want"
+fi
+
+# Run remote command with the object name.
+got=$(./vsh tunnel/id/test echo HELLO WORLD)
+want="HELLO WORLD"
+
+if [ "$got" != "$want" ]; then
+ FAIL "line $LINENO: unexpected output. Got $got, want $want"
+fi
+
+# Verify that all the published names are there.
+got=$(./mounttable glob $NAMESPACE_ROOT 'tunnel/*/*' | \
+ sed -e 's/TTL .m..s/TTL XmXXs/' \
+ -e 's!hwaddr/[^ ]*!hwaddr/XX:XX:XX:XX:XX:XX!' | \
+ sort)
+want="[$NAMESPACE_ROOT]
+tunnel/hostname/$(hostname) $ep// (TTL XmXXs)
+tunnel/hwaddr/XX:XX:XX:XX:XX:XX $ep// (TTL XmXXs)
+tunnel/id/test $ep// (TTL XmXXs)"
+
+if [ "$got" != "$want" ]; then
+ FAIL "line $LINENO: unexpected output. Got $got, want $want"
+fi
+
+PASS
diff --git a/examples/tunnel/vsh/main.go b/examples/tunnel/vsh/main.go
index 8917472..50f662e 100644
--- a/examples/tunnel/vsh/main.go
+++ b/examples/tunnel/vsh/main.go
@@ -71,7 +71,7 @@
func realMain() int {
r := rt.Init()
- defer r.Shutdown()
+ defer r.Cleanup()
host, cmd, err := veyronNameAndCommandLine()
if err != nil {
diff --git a/examples/unresolve/test_util.go b/examples/unresolve/test_util.go
index 7725c67..031258a 100644
--- a/examples/unresolve/test_util.go
+++ b/examples/unresolve/test_util.go
@@ -21,7 +21,7 @@
)
func initRT(opts ...veyron2.ROpt) func() {
- return rt.Init(opts...).Shutdown
+ return rt.Init(opts...).Cleanup
}
func createServer(name string, dispatcher ipc.Dispatcher, opts ...ipc.ServerOpt) (ipc.Server, string) {
diff --git a/examples/wspr_sample/sampled/main.go b/examples/wspr_sample/sampled/main.go
index fce02eb..0860680 100644
--- a/examples/wspr_sample/sampled/main.go
+++ b/examples/wspr_sample/sampled/main.go
@@ -12,7 +12,7 @@
func main() {
// Create the runtime
r := rt.Init()
- defer r.Shutdown()
+ defer r.Cleanup()
s, endpoint, err := lib.StartServer(r)
if err != nil {
diff --git a/lib/bluetooth/bluetooth.go b/lib/bluetooth/bluetooth.go
index 647eddd..208858b 100644
--- a/lib/bluetooth/bluetooth.go
+++ b/lib/bluetooth/bluetooth.go
@@ -18,7 +18,7 @@
// // Explicitly link libbluetooth and other libraries as "go build" cannot
// // figure out these dependencies..
-// #cgo LDFLAGS: -lbluetooth -ldbus-1 -lusb-1.0 -lusb -lexpat
+// #cgo LDFLAGS: -lbluetooth
// #include <bluetooth/bluetooth.h>
// #include <bluetooth/hci.h>
// #include <bluetooth/hci_lib.h>
diff --git a/lib/bluetooth/listener.go b/lib/bluetooth/listener.go
index 35bad9b..86e99a3 100644
--- a/lib/bluetooth/listener.go
+++ b/lib/bluetooth/listener.go
@@ -11,13 +11,13 @@
// // Explicitly link libbluetooth and other libraries as "go build" cannot
// // figure out these dependencies..
-// #cgo LDFLAGS: -lbluetooth -ldbus-1 -lusb-1.0 -lusb -lexpat
+// #cgo LDFLAGS: -lbluetooth
// #include <stdlib.h>
// #include <unistd.h>
// #include "bt.h"
import "C"
-// listener waits for incoming RFCOMM connections on the providee socket.
+// listener waits for incoming RFCOMM connections on the provided socket.
// It implements the net.Listener interface.
type listener struct {
localAddr *addr
diff --git a/lib/signals/signals_test.go b/lib/signals/signals_test.go
index bbd576c..93480f8 100644
--- a/lib/signals/signals_test.go
+++ b/lib/signals/signals_test.go
@@ -52,7 +52,7 @@
wait := ShutdownOnSignals(signals...)
fmt.Println("ready")
fmt.Println("received signal", <-wait)
- r.Shutdown()
+ r.Cleanup()
<-closeStopLoop
}
@@ -69,7 +69,7 @@
}
func handleDefaultsIgnoreChan([]string) {
- defer rt.Init().Shutdown()
+ defer rt.Init().Cleanup()
closeStopLoop := make(chan struct{})
go stopLoop(closeStopLoop)
ShutdownOnSignals()
@@ -302,7 +302,7 @@
// TestCleanRemoteShutdown verifies that remote shutdown works correctly.
func TestCleanRemoteShutdown(t *testing.T) {
r := rt.Init()
- defer r.Shutdown()
+ defer r.Cleanup()
c := blackbox.HelperCommand(t, "handleDefaults")
defer c.Cleanup()
// This sets up the child's identity to be derived from the parent's (so
diff --git a/lib/testutil/init.go b/lib/testutil/init.go
index 8ba55c7..188291c 100644
--- a/lib/testutil/init.go
+++ b/lib/testutil/init.go
@@ -11,6 +11,7 @@
"os"
"runtime"
"strconv"
+ "sync"
// Need to import all of the packages that could possibly
// define flags that we care about. In practice, this is the
// flags defined by the testing package, the logging library
@@ -28,8 +29,35 @@
SeedEnv = "VEYRON_RNG_SEED"
)
+// Random is a concurrent-access friendly source of randomness.
+type Random struct {
+ mu sync.Mutex
+ rand *rand.Rand
+}
+
+// Int returns a non-negative pseudo-random int.
+func (r *Random) Int() int {
+ r.mu.Lock()
+ defer r.mu.Unlock()
+ return r.rand.Int()
+}
+
+// Intn returns a non-negative pseudo-random int in the range [0, n).
+func (r *Random) Intn(n int) int {
+ r.mu.Lock()
+ defer r.mu.Unlock()
+ return r.rand.Intn(n)
+}
+
+// Int63 returns a non-negative 63-bit pseudo-random integer as an int64.
+func (r *Random) Int63() int64 {
+ r.mu.Lock()
+ defer r.mu.Unlock()
+ return r.rand.Int63()
+}
+
var (
- Rand *rand.Rand
+ Rand *Random
)
func init() {
@@ -54,5 +82,5 @@
}
}
vlog.Infof("Seeding pseudo-random number generator with %v", seed)
- Rand = rand.New(rand.NewSource(seed))
+ Rand = &Random{rand: rand.New(rand.NewSource(seed))}
}
diff --git a/lib/testutil/security/util_test.go b/lib/testutil/security/util_test.go
index 5632aef..3aac88d 100644
--- a/lib/testutil/security/util_test.go
+++ b/lib/testutil/security/util_test.go
@@ -17,7 +17,7 @@
if err != nil {
t.Fatalf("rt.New failed: %v", err)
}
- defer r.Shutdown()
+ defer r.Cleanup()
newID := func(name string) security.PrivateID {
id, err := r.NewIdentity(name)
if err != nil {
@@ -48,7 +48,7 @@
if err != nil {
t.Fatalf("rt.New failed: %v", err)
}
- defer r.Shutdown()
+ defer r.Cleanup()
acl := security.ACL{
"veyron/alice": security.LabelSet(security.ReadLabel | security.WriteLabel),
"veyron/bob": security.LabelSet(security.ReadLabel),
@@ -76,7 +76,7 @@
if err != nil {
t.Fatalf("rt.New failed: %v", err)
}
- defer r.Shutdown()
+ defer r.Cleanup()
id, err := r.NewIdentity("test")
if err != nil {
t.Fatalf("r.NewIdentity failed: %v", err)
diff --git a/runtimes/google/ipc/client.go b/runtimes/google/ipc/client.go
index 49b3915..0aa2458 100644
--- a/runtimes/google/ipc/client.go
+++ b/runtimes/google/ipc/client.go
@@ -40,6 +40,8 @@
vcMapMu sync.Mutex
// TODO(ashankar): Additionally, should vcMap be keyed with other options also?
vcMap map[string]*vcInfo // map from endpoint.String() to vc info
+
+ dischargeCache dischargeCache
}
type vcInfo struct {
@@ -50,10 +52,11 @@
func InternalNewClient(streamMgr stream.Manager, ns naming.Namespace, opts ...ipc.ClientOpt) (ipc.Client, error) {
c := &client{
- streamMgr: streamMgr,
- ns: ns,
- vcMap: make(map[string]*vcInfo),
- callTimeout: defaultCallTimeout,
+ streamMgr: streamMgr,
+ ns: ns,
+ vcMap: make(map[string]*vcInfo),
+ callTimeout: defaultCallTimeout,
+ dischargeCache: dischargeCache{CaveatDischargeMap: make(security.CaveatDischargeMap)},
}
for _, opt := range opts {
// Collect all client opts that are also vc opts.
@@ -152,8 +155,11 @@
flow.Close()
continue
}
+
+ discharges := c.prepareDischarges(ctx, flow.LocalID(), flow.RemoteID(), method, opts)
+
lastErr = nil
- fc := newFlowClient(flow)
+ fc := newFlowClient(flow, &c.dischargeCache, discharges)
if verr := fc.start(suffix, method, args, timeout, blessing); verr != nil {
return nil, verr
}
@@ -172,7 +178,7 @@
if server == nil {
return nil, fmt.Errorf("server identity cannot be nil")
}
- // TODO(ataly): Fetch third-party discharges from the server.
+ // TODO(ataly,andreser): Check the third-party discharges the server presents
// TODO(ataly): What should the label be for the context? Typically the label is the security.Label
// of the method but we don't have that information here at the client.
authID, err := server.Authorize(isecurity.NewContext(isecurity.ContextArgs{
@@ -236,16 +242,21 @@
flow stream.Flow // the underlying flow
response ipc.Response // each decoded response message is kept here
+ discharges []security.ThirdPartyDischarge // discharges used for this request
+ dischargeCache *dischargeCache // client-global discharge cache reference type
+
sendClosedMu sync.Mutex
sendClosed bool // is the send side already closed? GUARDED_BY(sendClosedMu)
}
-func newFlowClient(flow stream.Flow) *flowClient {
+func newFlowClient(flow stream.Flow, dischargeCache *dischargeCache, discharges []security.ThirdPartyDischarge) *flowClient {
return &flowClient{
// TODO(toddw): Support different codecs
- dec: vom.NewDecoder(flow),
- enc: vom.NewEncoder(flow),
- flow: flow,
+ dec: vom.NewDecoder(flow),
+ enc: vom.NewEncoder(flow),
+ flow: flow,
+ discharges: discharges,
+ dischargeCache: dischargeCache,
}
}
@@ -258,11 +269,12 @@
func (fc *flowClient) start(suffix, method string, args []interface{}, timeout time.Duration, blessing security.PublicID) verror.E {
req := ipc.Request{
- Suffix: suffix,
- Method: method,
- NumPosArgs: uint64(len(args)),
- Timeout: int64(timeout),
- HasBlessing: blessing != nil,
+ Suffix: suffix,
+ Method: method,
+ NumPosArgs: uint64(len(args)),
+ Timeout: int64(timeout),
+ HasBlessing: blessing != nil,
+ NumDischarges: uint64(len(fc.discharges)),
}
if err := fc.enc.Encode(req); err != nil {
return fc.close(verror.BadProtocolf("ipc: request encoding failed: %v", err))
@@ -272,6 +284,11 @@
return fc.close(verror.BadProtocolf("ipc: blessing encoding failed: %v", err))
}
}
+ for _, d := range fc.discharges {
+ if err := fc.enc.Encode(d); err != nil {
+ return fc.close(verror.BadProtocolf("ipc: failed to encode discharge for %x: %v", d.CaveatID(), err))
+ }
+ }
for ix, arg := range args {
if err := fc.enc.Encode(arg); err != nil {
return fc.close(verror.BadProtocolf("ipc: arg %d encoding failed: %v", ix, err))
@@ -374,6 +391,14 @@
}
}
if fc.response.Error != nil {
+ if verror.Is(fc.response.Error, verror.NotAuthorized) {
+ // In case the error was caused by a bad discharge, we do not want to get stuck
+ // with retrying again and again with this discharge. As there is no direct way
+ // to detect it, we conservatively flush all discharges we used from the cache.
+ // TODO(ataly,andreser): add verror.BadDischarge and handle it explicitly?
+ vlog.VI(3).Infof("Discarding %d discharges as RPC failed with %v", len(fc.discharges), fc.response.Error)
+ fc.dischargeCache.Invalidate(fc.discharges...)
+ }
return fc.close(verror.ConvertWithDefault(verror.Internal, fc.response.Error))
}
if got, want := fc.response.NumPosResults, uint64(len(resultptrs)); got != want {
diff --git a/runtimes/google/ipc/discharges.go b/runtimes/google/ipc/discharges.go
new file mode 100644
index 0000000..75d99f3
--- /dev/null
+++ b/runtimes/google/ipc/discharges.go
@@ -0,0 +1,179 @@
+package ipc
+
+import (
+ "sync"
+ "veyron2"
+ "veyron2/context"
+ "veyron2/ipc"
+ "veyron2/security"
+ "veyron2/vdl"
+ "veyron2/vlog"
+)
+
+// prepareDischarges retrieves the caveat discharges required for using blessing
+// at server. The discharges are either found in the client cache, in the call
+// options, or requested from the discharge issuer indicated on the caveat.
+// Note that requesting a discharge is an ipc call, so one copy of this
+// function must be able to successfully terminate while another is blocked.
+func (c *client) prepareDischarges(ctx context.T, blessing, server security.PublicID,
+ method string, opts []ipc.CallOpt) (ret []security.ThirdPartyDischarge) {
+ // TODO(andreser,ataly): figure out whether this should return an error and how that should be handled
+ // Missing discharges do not necessarily mean the blessing is invalid (e.g., SetID)
+ if blessing == nil {
+ return
+ }
+
+ var caveats []security.ThirdPartyCaveat
+ for _, cav := range blessing.ThirdPartyCaveats() {
+ if server.Match(cav.Service) {
+ caveats = append(caveats, cav.Caveat.(security.ThirdPartyCaveat))
+ }
+ }
+ if len(caveats) == 0 {
+ return
+ }
+
+ discharges := make([]security.ThirdPartyDischarge, len(caveats))
+ dischargesFromOpts(caveats, opts, discharges)
+ c.dischargeCache.Discharges(caveats, discharges)
+ if shouldFetchDischarges(opts) {
+ c.fetchDischarges(ctx, caveats, opts, discharges)
+ }
+ for _, d := range discharges {
+ if d != nil {
+ ret = append(ret, d)
+ }
+ }
+ return
+}
+
+// dischargeCache is a concurrency-safe cache for third party caveat discharges.
+type dischargeCache struct {
+ sync.RWMutex
+ security.CaveatDischargeMap // GUARDED_BY(RWMutex)
+}
+
+// Add inserts the argument to the cache, possibly overwriting previous
+// discharges for the same caveat.
+func (dcc *dischargeCache) Add(discharges ...security.ThirdPartyDischarge) {
+ dcc.Lock()
+ for _, d := range discharges {
+ dcc.CaveatDischargeMap[d.CaveatID()] = d
+ }
+ dcc.Unlock()
+}
+
+// Invalidate removes discharges from the cache.
+func (dcc *dischargeCache) Invalidate(discharges ...security.ThirdPartyDischarge) {
+ dcc.Lock()
+ for _, d := range discharges {
+ if dcc.CaveatDischargeMap[d.CaveatID()] == d {
+ delete(dcc.CaveatDischargeMap, d.CaveatID())
+ }
+ }
+ dcc.Unlock()
+}
+
+// Discharges takes a slice of caveats and a slice of discharges of the same
+// length and fills in nil entries in the discharges slice with discharges
+// from the cache (if there are any).
+// REQUIRES: len(caveats) == len(out)
+func (dcc *dischargeCache) Discharges(caveats []security.ThirdPartyCaveat, out []security.ThirdPartyDischarge) {
+ dcc.Lock()
+ for i, d := range out {
+ if d != nil {
+ continue
+ }
+ out[i] = dcc.CaveatDischargeMap[caveats[i].ID()]
+ }
+ dcc.Unlock()
+}
+
+// dischargesFromOpts fills in the nils in the out argument with discharges in
+// opts that match the caveat at the same index in caveats.
+// REQUIRES: len(caveats) == len(out)
+func dischargesFromOpts(caveats []security.ThirdPartyCaveat, opts []ipc.CallOpt,
+ out []security.ThirdPartyDischarge) {
+ for _, opt := range opts {
+ d, ok := opt.(veyron2.DischargeOpt)
+ if !ok {
+ continue
+ }
+ for i, cav := range caveats {
+ if out[i] == nil && d.CaveatID() == cav.ID() {
+ out[i] = d
+ }
+ }
+ }
+}
+
+// fetchDischarges fills in out by fetching discharges for caveats from the
+// appropriate discharge service. Since there may be dependencies in the
+// caveats, fetchDischarges keeps retrying until either all discharges can be
+// fetched or no new discharges are fetched.
+// REQUIRES: len(caveats) == len(out)
+func (c *client) fetchDischarges(ctx context.T, caveats []security.ThirdPartyCaveat, opts []ipc.CallOpt, out []security.ThirdPartyDischarge) {
+ opts = append([]ipc.CallOpt{dontFetchDischarges{}}, opts...)
+ var wg sync.WaitGroup
+ for {
+ type fetched struct {
+ idx int
+ discharge security.ThirdPartyDischarge
+ }
+ discharges := make(chan fetched, len(caveats))
+ for i := range caveats {
+ if out[i] != nil {
+ continue
+ }
+ wg.Add(1)
+ go func(i int, cav security.ThirdPartyCaveat) {
+ defer wg.Done()
+ vlog.VI(3).Infof("Fetching discharge for %T from %v", cav.ID(), cav, cav.Location())
+ call, err := c.StartCall(ctx, cav.Location(), "Discharge", []interface{}{cav}, opts...)
+ if err != nil {
+ vlog.VI(3).Infof("Discharge fetch for caveat %T from %v failed: %v", cav, cav.Location(), err)
+ return
+ }
+ var dAny vdl.Any
+ // TODO(ashankar): Retry on errors like no-route-to-service, name resolution failures etc.
+ ierr := call.Finish(&dAny, &err)
+ if ierr != nil || err != nil {
+ vlog.VI(3).Infof("Discharge fetch for caveat %T from %v failed: (%v, %v)", cav, cav.Location(), err, ierr)
+ return
+ }
+ d, ok := dAny.(security.ThirdPartyDischarge)
+ if !ok {
+ vlog.Errorf("fetchDischarges: server at %s sent a %T (%v) instead of a ThirdPartyDischarge", cav.Location(), dAny, dAny)
+ }
+ discharges <- fetched{i, d}
+ }(i, caveats[i])
+ }
+ wg.Wait()
+ close(discharges)
+ var got int
+ for fetched := range discharges {
+ c.dischargeCache.Add(fetched.discharge)
+ out[fetched.idx] = fetched.discharge
+ got++
+ }
+ vlog.VI(2).Infof("fetchDischarges: got %d discharges", got)
+ if got == 0 {
+ return
+ }
+ }
+}
+
+// dontFetchDischares is an ipc.CallOpt that indicates that no extre ipc-s
+// should be done to fetch discharges for the call with this opt.
+// Discharges in the cache and in the call options are still used.
+type dontFetchDischarges struct{}
+
+func (dontFetchDischarges) IPCCallOpt() {}
+func shouldFetchDischarges(opts []ipc.CallOpt) bool {
+ for _, opt := range opts {
+ if _, ok := opt.(dontFetchDischarges); ok {
+ return false
+ }
+ }
+ return true
+}
diff --git a/runtimes/google/ipc/flow_test.go b/runtimes/google/ipc/flow_test.go
index 7009258..8850b21 100644
--- a/runtimes/google/ipc/flow_test.go
+++ b/runtimes/google/ipc/flow_test.go
@@ -117,7 +117,7 @@
ipcServer := &server{disp: testDisp{newEchoInvoker}}
for _, test := range tests {
clientFlow, serverFlow := newTestFlows()
- client := newFlowClient(clientFlow)
+ client := newFlowClient(clientFlow, nil, nil)
server := newFlowServer(serverFlow, ipcServer)
err := client.start(test.suffix, test.method, test.args, time.Duration(0), nil)
if err != nil {
diff --git a/runtimes/google/ipc/full_test.go b/runtimes/google/ipc/full_test.go
index 86b5a18..66e1cc7 100644
--- a/runtimes/google/ipc/full_test.go
+++ b/runtimes/google/ipc/full_test.go
@@ -30,17 +30,46 @@
"veyron2/ipc/stream"
"veyron2/naming"
"veyron2/security"
+ "veyron2/vdl"
"veyron2/verror"
"veyron2/vlog"
+ "veyron2/vom"
)
var (
+ errAuthorizer = errors.New("ipc: application Authorizer denied access")
errMethod = verror.Abortedf("server returned an error")
clientID security.PrivateID
serverID security.PrivateID
+ clock = new(fakeClock)
)
-var errAuthorizer = errors.New("ipc: application Authorizer denied access")
+type fakeClock struct {
+ sync.Mutex
+ time int
+}
+
+func (c *fakeClock) Now() int {
+ c.Lock()
+ defer c.Unlock()
+ return c.time
+}
+
+func (c *fakeClock) Advance(steps uint) {
+ c.Lock()
+ c.time += int(steps)
+ c.Unlock()
+}
+
+type fakeTimeCaveat int
+
+func (c fakeTimeCaveat) Validate(security.Context) error {
+ now := clock.Now()
+ if now > int(c) {
+ return fmt.Errorf("fakeTimeCaveat expired: now=%d > then=%d", now, c)
+ }
+ return nil
+}
type fakeContext struct{}
@@ -99,6 +128,18 @@
return "UnauthorizedResult", fmt.Errorf("Unauthorized should never be called")
}
+type dischargeServer struct{}
+
+func (*dischargeServer) Discharge(ctx ipc.ServerCall, caveat vdl.Any) (vdl.Any, error) {
+ c, ok := caveat.(security.ThirdPartyCaveat)
+ if !ok {
+ return nil, fmt.Errorf("discharger: unknown caveat(%T)", caveat)
+ }
+ // Add a fakeTimeCaveat to allow the discharge to expire
+ expiry := fakeTimeCaveat(clock.Now())
+ return serverID.MintDischarge(c, ctx, time.Hour, []security.ServiceCaveat{security.UniversalCaveat(expiry)})
+}
+
type testServerAuthorizer struct{}
func (testServerAuthorizer) Authorize(c security.Context) error {
@@ -113,18 +154,22 @@
func (t testServerDisp) Lookup(suffix string) (ipc.Invoker, security.Authorizer, error) {
// If suffix is "nilAuth" we use default authorization, if it is "aclAuth" we
// use an ACL based authorizer, and otherwise we use the custom testServerAuthorizer.
- if suffix == "nilAuth" {
- return ipc.ReflectInvoker(t.server), nil, nil
- }
- if suffix == "aclAuth" {
+ var authorizer security.Authorizer
+ switch suffix {
+ case "discharger":
+ return ipc.ReflectInvoker(&dischargeServer{}), testServerAuthorizer{}, nil
+ case "nilAuth":
+ authorizer = nil
+ case "aclAuth":
// Only authorize clients matching patterns "client" or "server/*".
- acl := security.ACL{
+ authorizer = security.NewACLAuthorizer(security.ACL{
"server/*": security.LabelSet(security.AdminLabel),
"client": security.LabelSet(security.AdminLabel),
- }
- return ipc.ReflectInvoker(t.server), security.NewACLAuthorizer(acl), nil
+ })
+ default:
+ authorizer = testServerAuthorizer{}
}
- return ipc.ReflectInvoker(t.server), testServerAuthorizer{}, nil
+ return ipc.ReflectInvoker(t.server), authorizer, nil
}
// namespace is a simple partial implementation of naming.Namespace. In
@@ -228,6 +273,9 @@
if err := server.Serve("mountpoint/server", disp); err != nil {
t.Errorf("server.Publish failed: %v", err)
}
+ if err := server.Serve("mountpoint/discharger", disp); err != nil {
+ t.Errorf("server.Publish for discharger failed: %v", err)
+ }
return ep, server
}
@@ -315,6 +363,27 @@
return derivedID
}
+// deriveForThirdPartyCaveats creates a SetPrivateID that can be used for
+// 1. talking to the server, if the caveats are fulfilled
+// 2. getting discharges, even if the caveats are not fulfilled
+// As an identity with an unfulfilled caveat is invalid (even for asking for a
+// discharge), this function creates a set of two identities. The first will
+// have the caveats, the second will always be valid, but only for getting
+// discharges. The client presents both blessings in both cases, the discharger
+// ignores the first if it is invalid.
+func deriveForThirdPartyCaveats(blessor security.PrivateID, name string, caveats ...security.ServiceCaveat) security.PrivateID {
+ id := derive(blessor, name, caveats...)
+ dischargeID, err := id.Derive(bless(blessor, id.PublicID(), name, security.UniversalCaveat(caveat.MethodRestriction{"Discharge"})))
+ if err != nil {
+ panic(err)
+ }
+ id, err = isecurity.NewSetPrivateID(id, dischargeID)
+ if err != nil {
+ panic(err)
+ }
+ return id
+}
+
func matchesErrorPattern(err error, pattern string) bool {
if (len(pattern) == 0) != (err == nil) {
return false
@@ -370,20 +439,23 @@
}
func TestStartCall(t *testing.T) {
- authorizeErr := "not authorized because"
- nameErr := "does not match the provided pattern"
+ const (
+ authorizeErr = "not authorized because"
+ nameErr = "does not match the provided pattern"
+ )
+ var (
+ now = time.Now()
+ cavOnlyV1 = security.UniversalCaveat(caveat.PeerIdentity{"client/v1"})
+ cavExpired = security.ServiceCaveat{
+ Service: security.AllPrincipals,
+ Caveat: &caveat.Expiry{IssueTime: now, ExpiryTime: now},
+ }
- cavOnlyV1 := security.UniversalCaveat(caveat.PeerIdentity{"client/v1"})
- now := time.Now()
- cavExpired := security.ServiceCaveat{
- Service: security.AllPrincipals,
- Caveat: &caveat.Expiry{IssueTime: now, ExpiryTime: now},
- }
-
- clientV1ID := derive(clientID, "v1")
- clientV2ID := derive(clientID, "v2")
- serverV1ID := derive(serverID, "v1", cavOnlyV1)
- serverExpiredID := derive(serverID, "expired", cavExpired)
+ clientV1ID = derive(clientID, "v1")
+ clientV2ID = derive(clientID, "v2")
+ serverV1ID = derive(serverID, "v1", cavOnlyV1)
+ serverExpiredID = derive(serverID, "expired", cavExpired)
+ )
tests := []struct {
clientID, serverID security.PrivateID
@@ -546,21 +618,46 @@
}
}
+func mkThirdPartyCaveat(discharger security.PublicID, location string, c security.Caveat) security.ThirdPartyCaveat {
+ tpc, err := caveat.NewPublicKeyCaveat(c, discharger, location)
+ if err != nil {
+ panic(err)
+ }
+ return tpc
+}
+
func TestRPCAuthorization(t *testing.T) {
- cavOnlyEcho := security.ServiceCaveat{
- Service: security.AllPrincipals,
- Caveat: caveat.MethodRestriction{"Echo"},
- }
- now := time.Now()
- cavExpired := security.ServiceCaveat{
- Service: security.AllPrincipals,
- Caveat: &caveat.Expiry{IssueTime: now, ExpiryTime: now},
- }
+ var (
+ now = time.Now()
+ // First-party caveats
+ cavOnlyEcho = security.ServiceCaveat{
+ Service: security.AllPrincipals,
+ Caveat: caveat.MethodRestriction{"Echo"},
+ }
+ cavExpired = security.ServiceCaveat{
+ Service: security.AllPrincipals,
+ Caveat: &caveat.Expiry{IssueTime: now, ExpiryTime: now},
+ }
+ // Third-party caveats
+ // The Discharge service can be run by any identity, but in our tests the same server runs
+ // a Discharge service as well.
+ dischargerID = serverID.PublicID()
+ cavTPValid = security.ServiceCaveat{
+ Service: security.PrincipalPattern(serverID.PublicID().Names()[0]),
+ Caveat: mkThirdPartyCaveat(dischargerID, "mountpoint/server/discharger", &caveat.Expiry{ExpiryTime: now.Add(24 * time.Hour)}),
+ }
+ cavTPExpired = security.ServiceCaveat{
+ Service: security.PrincipalPattern(serverID.PublicID().Names()[0]),
+ Caveat: mkThirdPartyCaveat(dischargerID, "mountpoint/server/discharger", &caveat.Expiry{IssueTime: now, ExpiryTime: now}),
+ }
- blessedByServerOnlyEcho := derive(serverID, "onlyEcho", cavOnlyEcho)
- blessedByServerExpired := derive(serverID, "expired", cavExpired)
- blessedByClient := derive(clientID, "blessed")
-
+ // Client blessings that will be tested
+ blessedByServerOnlyEcho = derive(serverID, "onlyEcho", cavOnlyEcho)
+ blessedByServerExpired = derive(serverID, "expired", cavExpired)
+ blessedByServerTPValid = deriveForThirdPartyCaveats(serverID, "tpvalid", cavTPValid)
+ blessedByServerTPExpired = deriveForThirdPartyCaveats(serverID, "tpexpired", cavTPExpired)
+ blessedByClient = derive(clientID, "blessed")
+ )
const (
expiredIDErr = "forbids credential from being used at this time"
aclAuthErr = "no matching ACL entry found"
@@ -599,7 +696,6 @@
{clientID, "mountpoint/server/aclAuth", "Closure", nil, nil, ""},
{blessedByClient, "mountpoint/server/aclAuth", "Closure", nil, nil, aclAuthErr},
{serverID, "mountpoint/server/aclAuth", "Closure", nil, nil, ""},
-
// All methods except "Unauthorized" are authorized by the custom authorizer.
{clientID, "mountpoint/server/suffix", "Echo", v{"foo"}, v{`method:"Echo",suffix:"suffix",arg:"foo"`}, ""},
{blessedByClient, "mountpoint/server/suffix", "Echo", v{"foo"}, v{`method:"Echo",suffix:"suffix",arg:"foo"`}, ""},
@@ -611,6 +707,9 @@
{clientID, "mountpoint/server/suffix", "Unauthorized", nil, v{""}, "application Authorizer denied access"},
{blessedByClient, "mountpoint/server/suffix", "Unauthorized", nil, v{""}, "application Authorizer denied access"},
{serverID, "mountpoint/server/suffix", "Unauthorized", nil, v{""}, "application Authorizer denied access"},
+ // Third-party caveat discharges should be fetched and forwarded
+ {blessedByServerTPValid, "mountpoint/server/suffix", "Echo", v{"foo"}, v{`method:"Echo",suffix:"suffix",arg:"foo"`}, ""},
+ {blessedByServerTPExpired, "mountpoint/server/suffix", "Echo", v{"foo"}, v{""}, "missing discharge"},
}
name := func(t testcase) string {
return fmt.Sprintf("%q RPCing %s.%s(%v)", t.clientID.PublicID(), t.name, t.method, t.args)
@@ -640,6 +739,49 @@
}
}
+type alwaysValidCaveat struct{}
+
+func (alwaysValidCaveat) Validate(security.Context) error { return nil }
+
+func TestDischargePurgeFromCache(t *testing.T) {
+ var (
+ dischargerID = serverID.PublicID()
+ caveat = mkThirdPartyCaveat(dischargerID, "mountpoint/server/discharger", alwaysValidCaveat{})
+ clientCID = deriveForThirdPartyCaveats(serverID, "client", security.UniversalCaveat(caveat))
+ )
+ b := createBundle(t, clientCID, serverID, &testServer{})
+ defer b.cleanup(t)
+
+ call := func() error {
+ call, err := b.client.StartCall(&fakeContext{}, "mountpoint/server/suffix", "Echo", []interface{}{"batman"})
+ if err != nil {
+ return fmt.Errorf("client.StartCall failed: %v", err)
+ }
+ var got string
+ if err := call.Finish(&got); err != nil {
+ return fmt.Errorf("client.Finish failed: %v", err)
+ }
+ if want := `method:"Echo",suffix:"suffix",arg:"batman"`; got != want {
+ return fmt.Errorf("Got [%v] want [%v]", got, want)
+ }
+ return nil
+ }
+
+ // First call should succeed
+ if err := call(); err != nil {
+ t.Fatal(err)
+ }
+ // Advance virtual clock, which will invalidate the discharge
+ clock.Advance(1)
+ if err := call(); !matchesErrorPattern(err, "fakeTimeCaveat expired") {
+ t.Errorf("Got error [%v] wanted to match pattern 'fakeTimeCaveat expired'", err)
+ }
+ // But retrying will succeed since the discharge should be purged from cache and refreshed
+ if err := call(); err != nil {
+ t.Fatal(err)
+ }
+}
+
type cancelTestServer struct {
started chan struct{}
cancelled chan struct{}
@@ -1095,4 +1237,6 @@
blackbox.CommandTable["runServer"] = runServer
blackbox.CommandTable["runProxy"] = runProxy
+
+ vom.Register(fakeTimeCaveat(0))
}
diff --git a/runtimes/google/ipc/jni/jni.go b/runtimes/google/ipc/jni/jni.go
index 627d828..204a9f0 100644
--- a/runtimes/google/ipc/jni/jni.go
+++ b/runtimes/google/ipc/jni/jni.go
@@ -129,8 +129,8 @@
}
}
-//export Java_com_veyron_runtimes_google_Runtime_00024Server_nativeRegister
-func Java_com_veyron_runtimes_google_Runtime_00024Server_nativeRegister(env *C.JNIEnv, jServer C.jobject, goServerPtr C.jlong, prefix C.jstring, dispatcher C.jobject) {
+//export Java_com_veyron_runtimes_google_Runtime_00024Server_nativeServe
+func Java_com_veyron_runtimes_google_Runtime_00024Server_nativeServe(env *C.JNIEnv, jServer C.jobject, goServerPtr C.jlong, name C.jstring, dispatcher C.jobject) {
s := (*ipc.Server)(ptr(goServerPtr))
if s == nil {
jThrowV(env, fmt.Errorf("Couldn't find Go server with pointer: %d", int(goServerPtr)))
@@ -142,7 +142,10 @@
jThrowV(env, err)
return
}
- (*s).Register(goString(env, prefix), d)
+ if err := (*s).Serve(goString(env, name), d); err != nil {
+ jThrowV(env, err)
+ return
+ }
}
//export Java_com_veyron_runtimes_google_Runtime_00024Server_nativeListen
@@ -160,19 +163,6 @@
return jString(env, ep.String())
}
-//export Java_com_veyron_runtimes_google_Runtime_00024Server_nativePublish
-func Java_com_veyron_runtimes_google_Runtime_00024Server_nativePublish(env *C.JNIEnv, server C.jobject, goServerPtr C.jlong, name C.jstring) {
- s := (*ipc.Server)(ptr(goServerPtr))
- if s == nil {
- jThrowV(env, fmt.Errorf("Couldn't find Go server with pointer: %d", int(goServerPtr)))
- return
- }
- if err := (*s).Publish(goString(env, name)); err != nil {
- jThrowV(env, err)
- return
- }
-}
-
//export Java_com_veyron_runtimes_google_Runtime_00024Server_nativeStop
func Java_com_veyron_runtimes_google_Runtime_00024Server_nativeStop(env *C.JNIEnv, server C.jobject, goServerPtr C.jlong) {
s := (*ipc.Server)(ptr(goServerPtr))
diff --git a/runtimes/google/ipc/server.go b/runtimes/google/ipc/server.go
index f162fdf..1a88570 100644
--- a/runtimes/google/ipc/server.go
+++ b/runtimes/google/ipc/server.go
@@ -332,9 +332,10 @@
server: server,
disp: server.disp,
// TODO(toddw): Support different codecs
- dec: vom.NewDecoder(flow),
- enc: vom.NewEncoder(flow),
- flow: flow,
+ dec: vom.NewDecoder(flow),
+ enc: vom.NewEncoder(flow),
+ flow: flow,
+ discharges: make(security.CaveatDischargeMap),
}
}
@@ -450,7 +451,14 @@
// should servers be able to assume that a blessing is something that does not
// have the authorizations that the server's own identity has?
}
-
+ // Receive third party caveat discharges the client sent
+ for i := uint64(0); i < req.NumDischarges; i++ {
+ var d security.ThirdPartyDischarge
+ if err := fs.dec.Decode(&d); err != nil {
+ return nil, verror.BadProtocolf("ipc: decoding discharge %d of %d failed: %v", i, req.NumDischarges, err)
+ }
+ fs.discharges[d.CaveatID()] = d
+ }
// Lookup the invoker.
invoker, auth, suffix, verr := fs.lookup(req.Suffix)
fs.suffix = suffix // with leading /'s stripped
@@ -461,7 +469,6 @@
numArgs := int(req.NumPosArgs)
argptrs, label, err := invoker.Prepare(req.Method, numArgs)
fs.label = label
- // TODO(ataly, ashankar): Populate the "discharges" field from the request object req.
if err != nil {
return nil, verror.Makef(verror.ErrorID(err), "%s: name: %q", err, req.Suffix)
}
diff --git a/runtimes/google/naming/namespace/all_test.go b/runtimes/google/naming/namespace/all_test.go
index 4db264c..3925350 100644
--- a/runtimes/google/naming/namespace/all_test.go
+++ b/runtimes/google/naming/namespace/all_test.go
@@ -391,7 +391,7 @@
sr := rt.Init()
r, _ := rt.New() // We use a different runtime for the client side.
- defer r.Shutdown()
+ defer r.Cleanup()
root, _, _, stopper := createNamespace(t, sr)
defer stopper()
@@ -455,7 +455,7 @@
t.Skip()
sr := rt.Init()
r, _ := rt.New() // We use a different runtime for the client side.
- defer r.Shutdown()
+ defer r.Cleanup()
root, mts, jokes, stopper := createNamespace(t, sr)
runNestedMountTables(t, sr, mts)
defer stopper()
@@ -472,7 +472,7 @@
t.Skip()
sr := rt.Init()
r, _ := rt.New() // We use a different runtime for the client side.
- defer r.Shutdown()
+ defer r.Cleanup()
_, _, _, stopper := createNamespace(t, sr)
defer func() {
vlog.Infof("%d goroutines:", runtime.NumGoroutine())
@@ -486,7 +486,7 @@
func TestBadRoots(t *testing.T) {
r, _ := rt.New()
- defer r.Shutdown()
+ defer r.Cleanup()
if _, err := namespace.New(r); err != nil {
t.Errorf("namespace.New should not have failed with no roots")
}
diff --git a/runtimes/google/rt/mgmt_test.go b/runtimes/google/rt/mgmt_test.go
index e3dfdf6..bb78645 100644
--- a/runtimes/google/rt/mgmt_test.go
+++ b/runtimes/google/rt/mgmt_test.go
@@ -164,7 +164,7 @@
checkNoProgress(t, ch)
m.AdvanceGoal(0)
checkNoProgress(t, ch)
- m.Shutdown()
+ m.Cleanup()
if _, ok := <-ch; ok {
t.Errorf("Expected channel to be closed")
}
@@ -197,7 +197,7 @@
m.AdvanceGoal(4)
checkProgress(t, ch1, 11, 4)
checkProgress(t, ch2, 11, 4)
- m.Shutdown()
+ m.Cleanup()
if _, ok := <-ch1; ok {
t.Errorf("Expected channel to be closed")
}
@@ -216,7 +216,7 @@
fmt.Printf("Error creating runtime: %v\n", err)
return
}
- defer r.Shutdown()
+ defer r.Cleanup()
ch := make(chan string, 1)
r.WaitForStop(ch)
fmt.Printf("Got %s\n", <-ch)
@@ -279,7 +279,7 @@
configServer.Stop()
c.Cleanup()
os.Remove(idFile)
- // Don't do r.Shutdown() since the runtime needs to be used by
+ // Don't do r.Cleanup() since the runtime needs to be used by
// more than one test case.
}
}
diff --git a/runtimes/google/rt/rt.go b/runtimes/google/rt/rt.go
index a27b496..72470fe 100644
--- a/runtimes/google/rt/rt.go
+++ b/runtimes/google/rt/rt.go
@@ -152,7 +152,7 @@
return nil
}
-func (rt *vrt) Shutdown() {
+func (rt *vrt) Cleanup() {
// TODO(caprita): Consider shutting down mgmt later in the runtime's
// shutdown sequence, to capture some of the runtime internal shutdown
// tasks in the task tracker.
diff --git a/runtimes/google/vsync/vsyncd/main.go b/runtimes/google/vsync/vsyncd/main.go
index 506c49d..c49de82 100644
--- a/runtimes/google/vsync/vsyncd/main.go
+++ b/runtimes/google/vsync/vsyncd/main.go
@@ -30,7 +30,7 @@
// Create the runtime.
r := rt.Init()
- defer r.Shutdown()
+ defer r.Cleanup()
// Create a new server instance.
s, err := r.NewServer()
diff --git a/services/gateway/gatewayd/main.go b/services/gateway/gatewayd/main.go
index 897b542..3025812 100644
--- a/services/gateway/gatewayd/main.go
+++ b/services/gateway/gatewayd/main.go
@@ -37,7 +37,7 @@
// Get the runtime.
r := rt.Init()
- defer r.Shutdown()
+ defer r.Cleanup()
// Create a new instance of the gateway service.
vlog.Info("Connecting to proximity service: ", *proximity)
diff --git a/services/identity/handlers/handlers_test.go b/services/identity/handlers/handlers_test.go
index e540eb8..4793c73 100644
--- a/services/identity/handlers/handlers_test.go
+++ b/services/identity/handlers/handlers_test.go
@@ -37,7 +37,7 @@
if err != nil {
t.Fatal(err)
}
- defer r.Shutdown()
+ defer r.Cleanup()
ts := httptest.NewServer(Random{r})
defer ts.Close()
@@ -55,7 +55,7 @@
if err != nil {
t.Fatal(err)
}
- defer r.Shutdown()
+ defer r.Cleanup()
ts := httptest.NewServer(http.HandlerFunc(Bless))
defer ts.Close()
diff --git a/services/identity/identityd/main.go b/services/identity/identityd/main.go
index ca2a145..b1832e8 100644
--- a/services/identity/identityd/main.go
+++ b/services/identity/identityd/main.go
@@ -33,7 +33,7 @@
// Setup flags and logging
flag.Usage = usage
r := rt.Init()
- defer r.Shutdown()
+ defer r.Cleanup()
if len(*generate) > 0 {
generateAndSaveIdentity()
diff --git a/services/mgmt/application/applicationd/main.go b/services/mgmt/application/applicationd/main.go
index c42bb7d..397e3c3 100644
--- a/services/mgmt/application/applicationd/main.go
+++ b/services/mgmt/application/applicationd/main.go
@@ -23,7 +23,7 @@
vlog.Fatalf("Specify a store using --store=<name>")
}
runtime := rt.Init()
- defer runtime.Shutdown()
+ defer runtime.Cleanup()
server, err := runtime.NewServer()
if err != nil {
vlog.Fatalf("NewServer() failed: %v", err)
diff --git a/services/mgmt/application/impl/impl_test.go b/services/mgmt/application/impl/impl_test.go
index 76a0fd6..3321f58 100644
--- a/services/mgmt/application/impl/impl_test.go
+++ b/services/mgmt/application/impl/impl_test.go
@@ -17,7 +17,7 @@
func TestInterface(t *testing.T) {
runtime := rt.Init()
ctx := runtime.NewContext()
- defer runtime.Shutdown()
+ defer runtime.Cleanup()
// Setup and start the application repository server.
server, err := runtime.NewServer()
diff --git a/services/mgmt/binary/binaryd/main.go b/services/mgmt/binary/binaryd/main.go
index 55b4d6a..bb59019 100644
--- a/services/mgmt/binary/binaryd/main.go
+++ b/services/mgmt/binary/binaryd/main.go
@@ -61,7 +61,7 @@
}
vlog.Infof("Binary repository rooted at %v", root)
runtime := rt.Init()
- defer runtime.Shutdown()
+ defer runtime.Cleanup()
server, err := runtime.NewServer()
if err != nil {
vlog.Errorf("NewServer() failed: %v", err)
diff --git a/services/mgmt/node/impl/impl_test.go b/services/mgmt/node/impl/impl_test.go
index 1e62541..8a4cd47 100644
--- a/services/mgmt/node/impl/impl_test.go
+++ b/services/mgmt/node/impl/impl_test.go
@@ -219,7 +219,7 @@
}
case "parent":
runtime := rt.Init()
- defer runtime.Shutdown()
+ defer runtime.Cleanup()
// Set up a mock binary repository, a mock application repository, and a node manager.
_, crCleanup := startBinaryRepository()
defer crCleanup()
@@ -232,7 +232,7 @@
blackbox.WaitForEOFOnStdin()
case "child":
runtime := rt.Init()
- defer runtime.Shutdown()
+ defer runtime.Cleanup()
// Set up a node manager.
name, nmCleanup := startNodeManager()
defer nmCleanup()
diff --git a/services/mgmt/node/noded/main.go b/services/mgmt/node/noded/main.go
index af7c5d2..b993371 100644
--- a/services/mgmt/node/noded/main.go
+++ b/services/mgmt/node/noded/main.go
@@ -28,7 +28,7 @@
vlog.Fatalf("Specify the node manager origin as environment variable %s=<name>", impl.OriginEnv)
}
runtime := rt.Init()
- defer runtime.Shutdown()
+ defer runtime.Cleanup()
server, err := runtime.NewServer()
if err != nil {
vlog.Fatalf("NewServer() failed: %v", err)
diff --git a/services/mgmt/profile/impl/impl_test.go b/services/mgmt/profile/impl/impl_test.go
index 6822260..bb52ef9 100644
--- a/services/mgmt/profile/impl/impl_test.go
+++ b/services/mgmt/profile/impl/impl_test.go
@@ -26,7 +26,7 @@
// the Profile interface.
func TestInterface(t *testing.T) {
runtime := rt.Init()
- defer runtime.Shutdown()
+ defer runtime.Cleanup()
ctx := runtime.NewContext()
diff --git a/services/mgmt/profile/profiled/main.go b/services/mgmt/profile/profiled/main.go
index c4ab605..61482b2 100644
--- a/services/mgmt/profile/profiled/main.go
+++ b/services/mgmt/profile/profiled/main.go
@@ -23,7 +23,7 @@
vlog.Fatalf("Specify a store using --store=<name>")
}
runtime := rt.Init()
- defer runtime.Shutdown()
+ defer runtime.Cleanup()
server, err := runtime.NewServer()
if err != nil {
vlog.Fatalf("NewServer() failed: %v", err)
diff --git a/services/mgmt/root/rootd/main.go b/services/mgmt/root/rootd/main.go
index ae4b33a..c20a76a 100644
--- a/services/mgmt/root/rootd/main.go
+++ b/services/mgmt/root/rootd/main.go
@@ -10,7 +10,7 @@
func main() {
r := rt.Init()
- defer r.Shutdown()
+ defer r.Cleanup()
server, err := r.NewServer()
if err != nil {
vlog.Errorf("NewServer() failed: %v", err)
diff --git a/services/mounttable/mounttabled/mounttable.go b/services/mounttable/mounttabled/mounttable.go
index 23cf394..658aab0 100644
--- a/services/mounttable/mounttabled/mounttable.go
+++ b/services/mounttable/mounttabled/mounttable.go
@@ -52,7 +52,7 @@
func main() {
flag.Usage = Usage
r := rt.Init()
- defer r.Shutdown()
+ defer r.Cleanup()
mtServer, err := r.NewServer(veyron2.ServesMountTableOpt(true))
if err != nil {
diff --git a/services/proximity/proximityd/main.go b/services/proximity/proximityd/main.go
index fdb3201..43cb723 100644
--- a/services/proximity/proximityd/main.go
+++ b/services/proximity/proximityd/main.go
@@ -30,7 +30,7 @@
func main() {
// Get the runtime.
r := rt.Init()
- defer r.Shutdown()
+ defer r.Cleanup()
// Create a new server.
s, err := r.NewServer()
diff --git a/services/proxy/proxyd/main.go b/services/proxy/proxyd/main.go
index 90fead3..f30b10f 100644
--- a/services/proxy/proxyd/main.go
+++ b/services/proxy/proxyd/main.go
@@ -26,7 +26,7 @@
)
r := rt.Init()
- defer r.Shutdown()
+ defer r.Cleanup()
rid, err := naming.NewRoutingID()
if err != nil {
diff --git a/services/wspr/wsprd/lib/wspr.go b/services/wspr/wsprd/lib/wspr.go
index 4c76bd0..0147432 100644
--- a/services/wspr/wsprd/lib/wspr.go
+++ b/services/wspr/wsprd/lib/wspr.go
@@ -873,7 +873,7 @@
}
func (ctx WSPR) Shutdown() {
- ctx.rt.Shutdown()
+ ctx.rt.Cleanup()
}
// Creates a new WebSocket Proxy object.
diff --git a/tools/application/main.go b/tools/application/main.go
index 2ac2ee8..a510dcc 100644
--- a/tools/application/main.go
+++ b/tools/application/main.go
@@ -7,6 +7,6 @@
)
func main() {
- defer rt.Init().Shutdown()
+ defer rt.Init().Cleanup()
impl.Root().Main()
}
diff --git a/tools/binary/main.go b/tools/binary/main.go
index de795aa..e03d21c 100644
--- a/tools/binary/main.go
+++ b/tools/binary/main.go
@@ -8,7 +8,7 @@
func main() {
r := rt.Init()
- defer r.Shutdown()
+ defer r.Cleanup()
impl.Root().Main()
}
diff --git a/tools/mounttable/main.go b/tools/mounttable/main.go
index a59a17b..61a93d6 100644
--- a/tools/mounttable/main.go
+++ b/tools/mounttable/main.go
@@ -7,6 +7,6 @@
)
func main() {
- defer rt.Init().Shutdown()
+ defer rt.Init().Cleanup()
impl.Root().Main()
}
diff --git a/tools/namespace/main.go b/tools/namespace/main.go
index 4a0ff2d..64f046e 100644
--- a/tools/namespace/main.go
+++ b/tools/namespace/main.go
@@ -7,6 +7,6 @@
)
func main() {
- defer rt.Init().Shutdown()
+ defer rt.Init().Cleanup()
impl.Root().Main()
}
diff --git a/tools/playground/compilerd/main.go b/tools/playground/compilerd/main.go
index 82a4e11..b565ae3 100644
--- a/tools/playground/compilerd/main.go
+++ b/tools/playground/compilerd/main.go
@@ -4,8 +4,12 @@
"bytes"
"encoding/json"
"fmt"
+ "math/rand"
"net/http"
+ "os"
"os/exec"
+ "os/signal"
+ "syscall"
"time"
)
@@ -19,6 +23,19 @@
Events []Event
}
+// This channel is closed when the server begins shutting down.
+// No values are ever sent to it.
+var lameduck chan bool = make(chan bool)
+
+func healthz(w http.ResponseWriter, r *http.Request) {
+ select {
+ case <- lameduck:
+ w.WriteHeader(http.StatusInternalServerError)
+ default:
+ w.Write([]byte("OK"))
+ }
+}
+
func handler(w http.ResponseWriter, r *http.Request) {
if r.Body == nil || r.Method != "POST" {
w.WriteHeader(http.StatusBadRequest)
@@ -68,6 +85,54 @@
}
func main() {
+ limit_min := 60
+ delay_min := limit_min/2 + rand.Intn(limit_min/2)
+
+ // VMs will be periodically killed to prevent any owned vms from
+ // causing damage. We want to shutdown cleanly before then so
+ // we don't cause requests to fail.
+ go WaitForShutdown(time.Minute * time.Duration(delay_min))
+
http.HandleFunc("/compile", handler)
+ http.HandleFunc("/healthz", healthz)
http.ListenAndServe(":8181", nil)
}
+
+func WaitForShutdown(limit time.Duration) {
+ var beforeExit func() error
+
+ // Shutdown if we get a SIGTERM
+ term := make(chan os.Signal, 1)
+ signal.Notify(term, syscall.SIGTERM)
+
+ // Or if the time limit expires
+ deadline := time.After(limit)
+ fmt.Println("Shutting down at", time.Now().Add(limit))
+Loop:
+ for {
+ select {
+ case <-deadline:
+ // Shutdown the vm.
+ fmt.Println("Deadline expired, shutting down.")
+ beforeExit = exec.Command("sudo", "halt").Run
+ break Loop
+ case <-term:
+ fmt.Println("Got SIGTERM, shutting down.")
+ // VM is probably already shutting down, so just exit.
+ break Loop
+ }
+ }
+ // Fail health checks so we stop getting requests.
+ close(lameduck)
+ // Give running requests time to finish.
+ time.Sleep(30 * time.Second)
+
+ // Then go ahead and shutdown.
+ if beforeExit != nil {
+ err := beforeExit()
+ if err != nil {
+ panic(err)
+ }
+ }
+ os.Exit(0)
+}
diff --git a/tools/profile/main.go b/tools/profile/main.go
index 71a015a..da5821b 100644
--- a/tools/profile/main.go
+++ b/tools/profile/main.go
@@ -8,7 +8,7 @@
func main() {
r := rt.Init()
- defer r.Shutdown()
+ defer r.Cleanup()
impl.Root().Main()
}
diff --git a/tools/proximity/main.go b/tools/proximity/main.go
index 07ab805..a5d8826 100644
--- a/tools/proximity/main.go
+++ b/tools/proximity/main.go
@@ -7,6 +7,6 @@
)
func main() {
- defer rt.Init().Shutdown()
+ defer rt.Init().Cleanup()
impl.Root().Main()
}
diff --git a/tools/vrpc/impl/impl_test.go b/tools/vrpc/impl/impl_test.go
index ee67bf0..85c9be6 100644
--- a/tools/vrpc/impl/impl_test.go
+++ b/tools/vrpc/impl/impl_test.go
@@ -170,7 +170,7 @@
func TestVRPC(t *testing.T) {
runtime := rt.Init()
- // Skip defer runtime.Shutdown() to avoid messing up other tests in the
+ // Skip defer runtime.Cleanup() to avoid messing up other tests in the
// same process.
server, endpoint, err := startServer(t, runtime)
if err != nil {
diff --git a/tools/vrpc/main.go b/tools/vrpc/main.go
index 6711130..1cf6021 100644
--- a/tools/vrpc/main.go
+++ b/tools/vrpc/main.go
@@ -8,6 +8,6 @@
func main() {
r := rt.Init()
- defer r.Shutdown()
+ defer r.Cleanup()
impl.Root().Main()
}