Merge branch 'master' of /Users/jsimsa/Code/vanadium/release/go/src/v.io/apps
diff --git a/rps/common/common.go b/rps/common/common.go
new file mode 100644
index 0000000..4ab7bb4
--- /dev/null
+++ b/rps/common/common.go
@@ -0,0 +1,111 @@
+// Package common factors out common utility functions that both the
+// rock paper scissors clients and servers invoke.
+package common
+
+import (
+ "bytes"
+ "errors"
+ "fmt"
+ "math/rand"
+ "os"
+ "time"
+
+ "v.io/apps/rps"
+
+ "v.io/v23"
+ "v.io/v23/context"
+ "v.io/v23/naming"
+ "v.io/x/lib/vlog"
+)
+
+// CreateName creates a name using the username and hostname.
+func CreateName() string {
+ hostname, err := os.Hostname()
+ if err != nil {
+ vlog.Fatalf("os.Hostname failed: %v", err)
+ }
+ return os.Getenv("USER") + "@" + hostname
+}
+
+// FindJudge returns a random rock-paper-scissors judge from the mount table.
+func FindJudge(ctx *context.T) (string, error) {
+ judges, err := findAll(ctx, "judge")
+ if err != nil {
+ return "", err
+ }
+ if len(judges) > 0 {
+ return judges[rand.Intn(len(judges))], nil
+ }
+ return "", errors.New("no judges")
+}
+
+// FindPlayer returns a random rock-paper-scissors player from the mount table.
+func FindPlayer(ctx *context.T) (string, error) {
+ players, err := findAll(ctx, "player")
+ if err != nil {
+ return "", err
+ }
+ if len(players) > 0 {
+ return players[rand.Intn(len(players))], nil
+ }
+ return "", errors.New("no players")
+}
+
+// FindScoreKeepers returns all the rock-paper-scissors score keepers from the
+// mount table.
+func FindScoreKeepers(ctx *context.T) ([]string, error) {
+ sKeepers, err := findAll(ctx, "scorekeeper")
+ if err != nil {
+ return nil, err
+ }
+ return sKeepers, nil
+}
+
+func findAll(ctx *context.T, t string) ([]string, error) {
+ start := time.Now()
+ ns := v23.GetNamespace(ctx)
+ c, err := ns.Glob(ctx, "rps/"+t+"/*")
+ if err != nil {
+ vlog.Infof("mt.Glob failed: %v", err)
+ return nil, err
+ }
+ var servers []string
+ for e := range c {
+ switch v := e.(type) {
+ case *naming.GlobError:
+ vlog.VI(1).Infof("findAll(%q) error for %q: %v", t, v.Name, v.Error)
+ case *naming.MountEntry:
+ servers = append(servers, v.Name)
+ }
+ }
+ vlog.VI(1).Infof("findAll(%q) elapsed: %s", t, time.Now().Sub(start))
+ return servers, nil
+}
+
+// FormatScoreCard returns a string representation of a score card.
+func FormatScoreCard(score rps.ScoreCard) string {
+ buf := bytes.NewBufferString("")
+ var gameType string
+ switch score.Opts.GameType {
+ case rps.Classic:
+ gameType = "Classic"
+ case rps.LizardSpock:
+ gameType = "LizardSpock"
+ default:
+ gameType = "Unknown"
+ }
+ fmt.Fprintf(buf, "Game Type: %s\n", gameType)
+ fmt.Fprintf(buf, "Number of rounds: %d\n", score.Opts.NumRounds)
+ fmt.Fprintf(buf, "Judge: %s\n", score.Judge)
+ fmt.Fprintf(buf, "Player 1: %s\n", score.Players[0])
+ fmt.Fprintf(buf, "Player 2: %s\n", score.Players[1])
+ for i, r := range score.Rounds {
+ roundOffset := time.Duration(r.StartTimeNS - score.StartTimeNS)
+ roundTime := time.Duration(r.EndTimeNS - r.StartTimeNS)
+ fmt.Fprintf(buf, "Round %2d: Player 1 played %-10q. Player 2 played %-10q. Winner: %d %-28s [%-10s/%-10s]\n",
+ i+1, r.Moves[0], r.Moves[1], r.Winner, r.Comment, roundOffset, roundTime)
+ }
+ fmt.Fprintf(buf, "Winner: %d\n", score.Winner)
+ fmt.Fprintf(buf, "Time: %s\n", time.Duration(score.EndTimeNS-score.StartTimeNS))
+ return buf.String()
+}
diff --git a/rps/rpsbot/impl.go b/rps/rpsbot/impl.go
new file mode 100644
index 0000000..1942094
--- /dev/null
+++ b/rps/rpsbot/impl.go
@@ -0,0 +1,69 @@
+package main
+
+import (
+ "errors"
+
+ "v.io/apps/rps"
+ "v.io/v23/context"
+ "v.io/v23/ipc"
+ "v.io/v23/vtrace"
+ "v.io/x/lib/vlog"
+)
+
+// RPS implements rps.RockPaperScissorsServerMethods
+type RPS struct {
+ player *Player
+ judge *Judge
+ scoreKeeper *ScoreKeeper
+ ctx *context.T
+}
+
+func NewRPS(ctx *context.T) *RPS {
+ return &RPS{player: NewPlayer(), judge: NewJudge(), scoreKeeper: NewScoreKeeper(), ctx: ctx}
+}
+
+func (r *RPS) Judge() *Judge {
+ return r.judge
+}
+
+func (r *RPS) Player() *Player {
+ return r.player
+}
+
+func (r *RPS) ScoreKeeper() *ScoreKeeper {
+ return r.scoreKeeper
+}
+
+func (r *RPS) CreateGame(ctx ipc.ServerContext, opts rps.GameOptions) (rps.GameID, error) {
+ if vlog.V(1) {
+ b, _ := ctx.RemoteBlessings().ForContext(ctx)
+ vlog.Infof("CreateGame %+v from %v", opts, b)
+ }
+ names, _ := ctx.LocalBlessings().ForContext(ctx)
+ if len(names) == 0 {
+ return rps.GameID{}, errors.New("no names provided for context")
+ }
+ return r.judge.createGame(names[0], opts)
+}
+
+func (r *RPS) Play(ctx rps.JudgePlayContext, id rps.GameID) (rps.PlayResult, error) {
+ names, _ := ctx.RemoteBlessings().ForContext(ctx)
+ vlog.VI(1).Infof("Play %+v from %v", id, names)
+ if len(names) == 0 {
+ return rps.PlayResult{}, errors.New("no names provided for context")
+ }
+ return r.judge.play(ctx, names[0], id)
+}
+
+func (r *RPS) Challenge(ctx ipc.ServerContext, address string, id rps.GameID, opts rps.GameOptions) error {
+ b, _ := ctx.RemoteBlessings().ForContext(ctx)
+ vlog.VI(1).Infof("Challenge (%q, %+v, %+v) from %v", address, id, opts, b)
+ newctx, _ := vtrace.SetNewTrace(r.ctx)
+ return r.player.challenge(newctx, address, id, opts)
+}
+
+func (r *RPS) Record(ctx ipc.ServerContext, score rps.ScoreCard) error {
+ b, _ := ctx.RemoteBlessings().ForContext(ctx)
+ vlog.VI(1).Infof("Record (%+v) from %v", score, b)
+ return r.scoreKeeper.Record(ctx, score)
+}
diff --git a/rps/rpsbot/impl_test.go b/rps/rpsbot/impl_test.go
new file mode 100644
index 0000000..e80fe67
--- /dev/null
+++ b/rps/rpsbot/impl_test.go
@@ -0,0 +1,180 @@
+package main
+
+import (
+ "bufio"
+ "bytes"
+ "io"
+ "io/ioutil"
+ "os"
+ "regexp"
+ "runtime"
+ "runtime/pprof"
+ "strconv"
+ "testing"
+
+ "v.io/apps/rps"
+
+ "v.io/core/veyron/lib/testutil"
+ mtlib "v.io/core/veyron/services/mounttable/lib"
+
+ "v.io/v23"
+ "v.io/v23/context"
+ "v.io/v23/ipc"
+ "v.io/v23/naming"
+ "v.io/v23/options"
+ "v.io/x/lib/vlog"
+)
+
+var spec = ipc.ListenSpec{Addrs: ipc.ListenAddrs{{"tcp", "127.0.0.1:0"}}}
+
+func startMountTable(t *testing.T, ctx *context.T) (string, func()) {
+ server, err := v23.NewServer(ctx, options.ServesMountTable(true))
+ if err != nil {
+ t.Fatalf("NewServer() failed: %v", err)
+ }
+ dispatcher, err := mtlib.NewMountTableDispatcher("")
+ if err != nil {
+ t.Fatalf("NewMountTableDispatcher() failed: %v", err)
+ }
+ endpoints, err := server.Listen(spec)
+ if err != nil {
+ t.Fatalf("Listen(%v) failed: %v", spec, err)
+ }
+ if err := server.ServeDispatcher("", dispatcher); err != nil {
+ t.Fatalf("ServeDispatcher(%v) failed: %v", dispatcher, err)
+ }
+ address := naming.JoinAddressName(endpoints[0].String(), "")
+ vlog.VI(1).Infof("Mount table running at endpoint: %s", address)
+ return address, func() {
+ if err := server.Stop(); err != nil {
+ t.Fatalf("Stop() failed: %v", err)
+ }
+ }
+}
+
+func startRockPaperScissors(t *testing.T, ctx *context.T, mtAddress string) (*RPS, func()) {
+ ns := v23.GetNamespace(ctx)
+ ns.SetRoots(mtAddress)
+ server, err := v23.NewServer(ctx)
+ if err != nil {
+ t.Fatalf("NewServer failed: %v", err)
+ }
+ rpsService := NewRPS(ctx)
+
+ if _, err = server.Listen(spec); err != nil {
+ t.Fatalf("Listen failed: %v", err)
+ }
+ names := []string{"rps/judge/test", "rps/player/test", "rps/scorekeeper/test"}
+ if err := server.Serve(names[0], rps.RockPaperScissorsServer(rpsService), nil); err != nil {
+ t.Fatalf("Serve(%v) failed: %v", names[0], err)
+ }
+ for _, n := range names[1:] {
+ server.AddName(n)
+ }
+ return rpsService, func() {
+ if err := server.Stop(); err != nil {
+ t.Fatalf("Stop() failed: %v", err)
+ }
+ }
+}
+
+// TestRockPaperScissorsImpl runs one rock-paper-scissors game and verifies
+// that all the counters are consistent.
+func TestRockPaperScissorsImpl(t *testing.T) {
+ ctx, shutdown := testutil.InitForTest()
+ defer shutdown()
+
+ mtAddress, mtStop := startMountTable(t, ctx)
+ defer mtStop()
+ rpsService, rpsStop := startRockPaperScissors(t, ctx, mtAddress)
+ defer rpsStop()
+
+ const numGames = 100
+ for x := 0; x < numGames; x++ {
+ if err := rpsService.Player().InitiateGame(ctx); err != nil {
+ t.Errorf("Failed to initiate game: %v", err)
+ }
+ }
+ rpsService.Player().WaitUntilIdle()
+
+ // For each game, the player plays twice. So, we expect the player to
+ // show that it played 2×numGames, and won numGames.
+ played, won := rpsService.Player().Stats()
+ if want, got := int64(2*numGames), played; want != got {
+ t.Errorf("Unexpected number of played games. Got %d, want %d", got, want)
+ }
+ if want, got := int64(numGames), won; want != got {
+ t.Errorf("Unexpected number of won games. Got %d, want %d", got, want)
+ }
+
+ // The Judge ran every game.
+ if want, got := int64(numGames), rpsService.Judge().Stats(); want != got {
+ t.Errorf("Unexpected number of games run. Got %d, want %d", got, want)
+ }
+
+ // The Score Keeper received one score card per game.
+ if want, got := int64(numGames), rpsService.ScoreKeeper().Stats(); want != got {
+ t.Errorf("Unexpected number of score cards. Got %d, want %d", got, want)
+ }
+
+ // Check for leaked goroutines.
+ if n := runtime.NumGoroutine(); n > 100 {
+ t.Logf("Num goroutines: %d", n)
+ if leak := reportLeakedGoroutines(t, n/3); len(leak) != 0 {
+ t.Errorf("Potentially leaked goroutines:\n%s", leak)
+ }
+ }
+}
+
+// reportLeakedGoroutines reads the goroutine pprof profile and returns the
+// goroutines that have more than 'threshold' instances.
+func reportLeakedGoroutines(t *testing.T, threshold int) string {
+ f, err := ioutil.TempFile("", "test-profile-")
+ if err != nil {
+ t.Fatalf("Failed to create temp file: %v", err)
+ }
+ defer f.Close()
+ defer os.Remove(f.Name())
+ p := pprof.Lookup("goroutine")
+ if p == nil {
+ t.Fatalf("Failed to lookup the goroutine profile")
+ }
+ if err := p.WriteTo(f, 1); err != nil {
+ t.Fatalf("Failed to write profile: %v", err)
+ }
+
+ re, err := regexp.Compile(`^([0-9]+) @`)
+ if err != nil {
+ t.Fatalf("Failed to compile regexp")
+ }
+ f.Seek(0, 0)
+ r := bufio.NewReader(f)
+
+ var buf bytes.Buffer
+ var printing bool
+ for {
+ line, err := r.ReadBytes('\n')
+ if err == io.EOF {
+ break
+ }
+ if err != nil {
+ t.Fatalf("Failed to read bytes: %v", err)
+ }
+ if len(line) <= 1 {
+ printing = false
+ continue
+ }
+ if !printing {
+ if m := re.FindSubmatch(line); len(m) == 2 {
+ count, _ := strconv.Atoi(string(m[1]))
+ if count > threshold {
+ printing = true
+ }
+ }
+ }
+ if printing {
+ buf.Write(line)
+ }
+ }
+ return buf.String()
+}
diff --git a/rps/rpsbot/judge.go b/rps/rpsbot/judge.go
new file mode 100644
index 0000000..c7240dd
--- /dev/null
+++ b/rps/rpsbot/judge.go
@@ -0,0 +1,356 @@
+package main
+
+import (
+ "errors"
+ "fmt"
+ "strconv"
+ "sync"
+ "time"
+
+ "v.io/apps/rps"
+ "v.io/apps/rps/common"
+ "v.io/core/veyron/lib/stats"
+ "v.io/core/veyron/lib/stats/counter"
+ "v.io/v23/context"
+ "v.io/x/lib/vlog"
+)
+
+var (
+ errBadGameID = errors.New("requested gameID doesn't exist")
+ errTooManyPlayers = errors.New("all players are already seated")
+)
+
+type Judge struct {
+ lock sync.Mutex
+ games map[rps.GameID]*gameInfo
+ gamesRun *counter.Counter
+}
+
+type sendStream interface {
+ Send(item rps.JudgeAction) error
+}
+
+type gameInfo struct {
+ id rps.GameID
+ startTime time.Time
+ score rps.ScoreCard
+ streams []sendStream
+ playerIn chan playerInput
+ playerOut []chan rps.JudgeAction
+ scoreChan chan scoreData
+}
+
+func (g *gameInfo) moveOptions() []string {
+ switch g.score.Opts.GameType {
+ case rps.LizardSpock:
+ return []string{"rock", "paper", "scissors", "lizard", "spock"}
+ default:
+ return []string{"rock", "paper", "scissors"}
+ }
+}
+
+func (g *gameInfo) validMove(m string) bool {
+ for _, x := range g.moveOptions() {
+ if x == m {
+ return true
+ }
+ }
+ return false
+}
+
+type playerInput struct {
+ player int
+ action rps.PlayerAction
+}
+
+type scoreData struct {
+ err error
+ score rps.ScoreCard
+}
+
+func NewJudge() *Judge {
+ return &Judge{
+ games: make(map[rps.GameID]*gameInfo),
+ gamesRun: stats.NewCounter("judge/games-run"),
+ }
+}
+
+func (j *Judge) Stats() int64 {
+ return j.gamesRun.Value()
+}
+
+// createGame handles a request to create a new game.
+func (j *Judge) createGame(ownName string, opts rps.GameOptions) (rps.GameID, error) {
+ vlog.VI(1).Infof("createGame called")
+ score := rps.ScoreCard{Opts: opts, Judge: ownName}
+ now := time.Now()
+ id := rps.GameID{ID: strconv.FormatInt(now.UnixNano(), 16)}
+
+ j.lock.Lock()
+ defer j.lock.Unlock()
+ for k, v := range j.games {
+ if now.Sub(v.startTime) > 1*time.Hour {
+ vlog.Infof("Removing stale game ID %v", k)
+ delete(j.games, k)
+ }
+ }
+ j.games[id] = &gameInfo{
+ id: id,
+ startTime: now,
+ score: score,
+ playerIn: make(chan playerInput),
+ playerOut: []chan rps.JudgeAction{
+ make(chan rps.JudgeAction),
+ make(chan rps.JudgeAction),
+ },
+ scoreChan: make(chan scoreData),
+ }
+ return id, nil
+}
+
+// play interacts with a player for the duration of a game.
+func (j *Judge) play(ctx rps.JudgePlayContext, name string, id rps.GameID) (rps.PlayResult, error) {
+ vlog.VI(1).Infof("play from %q for %v", name, id)
+ nilResult := rps.PlayResult{}
+
+ pIn, pOut, s, err := j.gameChannels(id)
+ if err != nil {
+ return nilResult, err
+ }
+ playerNum, err := j.addPlayer(name, id, ctx)
+ if err != nil {
+ return nilResult, err
+ }
+ vlog.VI(1).Infof("This is player %d", playerNum)
+
+ // Send all user input to the player input channel.
+ done := make(chan struct{})
+ defer close(done)
+ go func() {
+ rStream := ctx.RecvStream()
+ for rStream.Advance() {
+ action := rStream.Value()
+ select {
+ case pIn <- playerInput{player: playerNum, action: action}:
+ case <-done:
+ return
+ }
+ }
+ select {
+ case pIn <- playerInput{player: playerNum, action: rps.PlayerActionQuit{}}:
+ case <-done:
+ }
+ }()
+ // Send all the output to the user.
+ go func() {
+ for packet := range pOut[playerNum-1] {
+ if err := ctx.SendStream().Send(packet); err != nil {
+ vlog.Infof("error sending to player stream: %v", err)
+ }
+ }
+ }()
+ defer close(pOut[playerNum-1])
+
+ pOut[playerNum-1] <- rps.JudgeActionPlayerNum{int32(playerNum)}
+
+ // When the second player connects, we start the game.
+ if playerNum == 2 {
+ go j.manageGame(ctx.Context(), id)
+ }
+ // Wait for the ScoreCard.
+ scoreData := <-s
+ if scoreData.err != nil {
+ return rps.PlayResult{}, scoreData.err
+ }
+ return rps.PlayResult{YouWon: scoreData.score.Winner == rps.WinnerTag(playerNum)}, nil
+}
+
+func (j *Judge) manageGame(ctx *context.T, id rps.GameID) {
+ j.gamesRun.Incr(1)
+ j.lock.Lock()
+ info, exists := j.games[id]
+ if !exists {
+ e := scoreData{err: errBadGameID}
+ info.scoreChan <- e
+ info.scoreChan <- e
+ return
+ }
+ delete(j.games, id)
+ j.lock.Unlock()
+
+ // Inform each player of their opponent's name.
+ for p := 0; p < 2; p++ {
+ opp := 1 - p
+ info.playerOut[p] <- rps.JudgeActionOpponentName{info.score.Players[opp]}
+ }
+
+ win1, win2 := 0, 0
+ goal := int(info.score.Opts.NumRounds)
+ // Play until one player has won 'goal' times.
+ for win1 < goal && win2 < goal {
+ round, err := j.playOneRound(info)
+ if err != nil {
+ err := scoreData{err: err}
+ info.scoreChan <- err
+ info.scoreChan <- err
+ return
+ }
+ if round.Winner == rps.Player1 {
+ win1++
+ } else if round.Winner == rps.Player2 {
+ win2++
+ }
+ info.score.Rounds = append(info.score.Rounds, round)
+ }
+ if win1 > win2 {
+ info.score.Winner = rps.Player1
+ } else {
+ info.score.Winner = rps.Player2
+ }
+
+ info.score.StartTimeNS = info.startTime.UnixNano()
+ info.score.EndTimeNS = time.Now().UnixNano()
+
+ // Send the score card to the players.
+ action := rps.JudgeActionScore{info.score}
+ for _, p := range info.playerOut {
+ p <- action
+ }
+
+ // Send the score card to the score keepers.
+ scoreCtx, cancel := context.WithTimeout(ctx, 30*time.Second)
+ defer cancel()
+ keepers, err := common.FindScoreKeepers(scoreCtx)
+ if err != nil || len(keepers) == 0 {
+ vlog.Infof("No score keepers: %v", err)
+ return
+ }
+ var wg sync.WaitGroup
+ wg.Add(len(keepers))
+ for _, k := range keepers {
+ go j.sendScore(scoreCtx, k, info.score, &wg)
+ }
+ wg.Wait()
+
+ info.scoreChan <- scoreData{score: info.score}
+ info.scoreChan <- scoreData{score: info.score}
+}
+
+func (j *Judge) playOneRound(info *gameInfo) (rps.Round, error) {
+ round := rps.Round{StartTimeNS: time.Now().UnixNano()}
+ var action rps.JudgeAction
+ action = rps.JudgeActionMoveOptions{info.moveOptions()}
+ for _, p := range info.playerOut {
+ p <- action
+ }
+ for x := 0; x < 2; x++ {
+ in := <-info.playerIn
+ switch v := in.action.(type) {
+ case rps.PlayerActionQuit:
+ return round, fmt.Errorf("player %d quit the game", in.player)
+ case rps.PlayerActionMove:
+ move := v.Value
+ if !info.validMove(move) {
+ return round, fmt.Errorf("player %d made an invalid move: %s", in.player, move)
+ }
+ if len(round.Moves[in.player-1]) > 0 {
+ return round, fmt.Errorf("player %d played twice in the same round!", in.player)
+ }
+ round.Moves[in.player-1] = move
+ default:
+ vlog.Infof("unexpected message type: %T", in.action)
+ }
+ }
+ round.Winner, round.Comment = j.compareMoves(round.Moves[0], round.Moves[1])
+ vlog.VI(1).Infof("Player 1 played %q. Player 2 played %q. Winner: %d %s", round.Moves[0], round.Moves[1], round.Winner, round.Comment)
+
+ action = rps.JudgeActionRoundResult{round}
+ for _, p := range info.playerOut {
+ p <- action
+ }
+ round.EndTimeNS = time.Now().UnixNano()
+ return round, nil
+}
+
+func (j *Judge) addPlayer(name string, id rps.GameID, stream rps.JudgePlayServerStream) (int, error) {
+ j.lock.Lock()
+ defer j.lock.Unlock()
+
+ info, exists := j.games[id]
+ if !exists {
+ return 0, errBadGameID
+ }
+ if len(info.streams) == 2 {
+ return 0, errTooManyPlayers
+ }
+ info.score.Players = append(info.score.Players, name)
+ info.streams = append(info.streams, stream.SendStream())
+ return len(info.streams), nil
+}
+
+func (j *Judge) gameChannels(id rps.GameID) (chan playerInput, []chan rps.JudgeAction, chan scoreData, error) {
+ j.lock.Lock()
+ defer j.lock.Unlock()
+ info, exists := j.games[id]
+ if !exists {
+ return nil, nil, nil, errBadGameID
+ }
+ return info.playerIn, info.playerOut, info.scoreChan, nil
+}
+
+func (j *Judge) sendScore(ctx *context.T, address string, score rps.ScoreCard, wg *sync.WaitGroup) error {
+ defer wg.Done()
+ k := rps.RockPaperScissorsClient(address)
+ if err := k.Record(ctx, score); err != nil {
+ vlog.Infof("Record: %v", err)
+ return err
+ }
+ return nil
+}
+
+var moveComments = map[string]string{
+ "lizard-paper": "lizard eats paper",
+ "lizard-rock": "rock crushes lizard",
+ "lizard-scissors": "scissors decapitates lizard",
+ "lizard-spock": "lizard poisons spock",
+ "paper-rock": "paper covers rock",
+ "paper-scissors": "scissors cuts paper",
+ "paper-spock": "paper disproves spock",
+ "rock-scissors": "rock crushes scissors",
+ "rock-spock": "spock vaporizes rock",
+ "scissors-spock": "spock smashes scissors",
+}
+
+func (j *Judge) compareMoves(m1, m2 string) (winner rps.WinnerTag, comment string) {
+ if m1 < m2 {
+ comment = moveComments[m1+"-"+m2]
+ } else {
+ comment = moveComments[m2+"-"+m1]
+ }
+ if m1 == m2 {
+ winner = rps.Draw
+ return
+ }
+ if m1 == "rock" && (m2 == "scissors" || m2 == "lizard") {
+ winner = rps.Player1
+ return
+ }
+ if m1 == "paper" && (m2 == "rock" || m2 == "spock") {
+ winner = rps.Player1
+ return
+ }
+ if m1 == "scissors" && (m2 == "paper" || m2 == "lizard") {
+ winner = rps.Player1
+ return
+ }
+ if m1 == "lizard" && (m2 == "paper" || m2 == "spock") {
+ winner = rps.Player1
+ return
+ }
+ if m1 == "spock" && (m2 == "scissors" || m2 == "rock") {
+ winner = rps.Player1
+ return
+ }
+ winner = rps.Player2
+ return
+}
diff --git a/rps/rpsbot/main.go b/rps/rpsbot/main.go
new file mode 100644
index 0000000..56b7c8e
--- /dev/null
+++ b/rps/rpsbot/main.go
@@ -0,0 +1,79 @@
+// Command rpsbot is a binary that runs the fully automated
+// implementation of the RockPaperScissors service, which includes all
+// three roles involved in the game of rock-paper-scissors. It
+// publishes itself as player, judge, and scorekeeper. Then, it
+// initiates games with other players, in a loop. As soon as one game
+// is over, it starts a new one.
+package main
+
+import (
+ "flag"
+ "fmt"
+ "math/rand"
+ "time"
+
+ "v.io/v23"
+ "v.io/v23/context"
+ "v.io/x/lib/vlog"
+
+ "v.io/core/veyron/lib/signals"
+ _ "v.io/core/veyron/profiles/roaming"
+ sflag "v.io/core/veyron/security/flag"
+
+ "v.io/apps/rps"
+ "v.io/apps/rps/common"
+)
+
+var (
+ name = flag.String("name", "", "identifier to publish itself as (defaults to user@hostname)")
+ numGames = flag.Int("num-games", -1, "number of games to play (-1 means unlimited)")
+)
+
+func main() {
+ ctx, shutdown := v23.Init()
+ defer shutdown()
+
+ auth := sflag.NewAuthorizerOrDie()
+ server, err := v23.NewServer(ctx)
+ if err != nil {
+ vlog.Fatalf("NewServer failed: %v", err)
+ }
+
+ rand.Seed(time.Now().UnixNano())
+ rpsService := NewRPS(ctx)
+
+ listenSpec := v23.GetListenSpec(ctx)
+ eps, err := server.Listen(listenSpec)
+ if err != nil {
+ vlog.Fatalf("Listen(%v) failed: %v", listenSpec, err)
+ }
+ if *name == "" {
+ *name = common.CreateName()
+ }
+ names := []string{
+ fmt.Sprintf("rps/judge/%s", *name),
+ fmt.Sprintf("rps/player/%s", *name),
+ fmt.Sprintf("rps/scorekeeper/%s", *name),
+ }
+ if err := server.Serve(names[0], rps.RockPaperScissorsServer(rpsService), auth); err != nil {
+ vlog.Fatalf("Serve(%v) failed: %v", names[0], err)
+ }
+ for _, n := range names[1:] {
+ if err := server.AddName(n); err != nil {
+ vlog.Fatalf("(%v) failed: %v", n, err)
+ }
+ }
+ vlog.Infof("Listening on endpoint %s (published as %v)", eps, names)
+
+ go initiateGames(ctx, rpsService)
+ <-signals.ShutdownOnSignals(ctx)
+}
+
+func initiateGames(ctx *context.T, rpsService *RPS) {
+ for i := 0; i < *numGames || *numGames == -1; i++ {
+ if err := rpsService.Player().InitiateGame(ctx); err != nil {
+ vlog.Infof("Failed to initiate game: %v", err)
+ }
+ time.Sleep(5 * time.Second)
+ }
+}
diff --git a/rps/rpsbot/player.go b/rps/rpsbot/player.go
new file mode 100644
index 0000000..1181d25
--- /dev/null
+++ b/rps/rpsbot/player.go
@@ -0,0 +1,155 @@
+package main
+
+import (
+ "math/rand"
+ "time"
+
+ "v.io/apps/rps"
+ "v.io/apps/rps/common"
+ "v.io/core/veyron/lib/stats"
+ "v.io/core/veyron/lib/stats/counter"
+ "v.io/v23/context"
+ "v.io/x/lib/vlog"
+)
+
+type Player struct {
+ gamesPlayed *counter.Counter
+ gamesWon *counter.Counter
+ gamesInProgress *stats.Integer
+}
+
+func NewPlayer() *Player {
+ return &Player{
+ gamesPlayed: stats.NewCounter("player/games-played"),
+ gamesWon: stats.NewCounter("player/games-won"),
+ gamesInProgress: stats.NewInteger("player/games-in-progress"),
+ }
+}
+
+func (p *Player) Stats() (played, won int64) {
+ played = p.gamesPlayed.Value()
+ won = p.gamesWon.Value()
+ return
+}
+
+// only used by tests.
+func (p *Player) WaitUntilIdle() {
+ for p.gamesInProgress.Value() != int64(0) {
+ time.Sleep(10 * time.Millisecond)
+ }
+}
+
+func (p *Player) InitiateGame(ctx *context.T) error {
+ judge, err := common.FindJudge(ctx)
+ if err != nil {
+ vlog.Infof("FindJudge: %v", err)
+ return err
+ }
+ gameID, gameOpts, err := p.createGame(ctx, judge)
+ if err != nil {
+ vlog.Infof("createGame: %v", err)
+ return err
+ }
+ vlog.VI(1).Infof("Created gameID %q on %q", gameID, judge)
+
+ for {
+ opponent, err := common.FindPlayer(ctx)
+ if err != nil {
+ vlog.Infof("FindPlayer: %v", err)
+ return err
+ }
+ vlog.VI(1).Infof("chosen opponent is %q", opponent)
+ if err = p.sendChallenge(ctx, opponent, judge, gameID, gameOpts); err == nil {
+ break
+ }
+ vlog.Infof("sendChallenge: %v", err)
+ }
+ result, err := p.playGame(ctx, judge, gameID)
+ if err != nil {
+ vlog.Infof("playGame: %v", err)
+ return err
+ }
+ if result.YouWon {
+ vlog.VI(1).Info("Game result: I won! :)")
+ } else {
+ vlog.VI(1).Info("Game result: I lost :(")
+ }
+ return nil
+}
+
+func (p *Player) createGame(ctx *context.T, server string) (rps.GameID, rps.GameOptions, error) {
+ j := rps.RockPaperScissorsClient(server)
+ numRounds := 3 + rand.Intn(3)
+ gameType := rps.Classic
+ if rand.Intn(2) == 1 {
+ gameType = rps.LizardSpock
+ }
+ gameOpts := rps.GameOptions{NumRounds: int32(numRounds), GameType: gameType}
+ gameId, err := j.CreateGame(ctx, gameOpts)
+ return gameId, gameOpts, err
+}
+
+func (p *Player) sendChallenge(ctx *context.T, opponent, judge string, gameID rps.GameID, gameOpts rps.GameOptions) error {
+ o := rps.RockPaperScissorsClient(opponent)
+ return o.Challenge(ctx, judge, gameID, gameOpts)
+}
+
+// challenge receives an incoming challenge and starts to play a new game.
+// Note that the new game will occur in a new context.
+func (p *Player) challenge(ctx *context.T, judge string, gameID rps.GameID, _ rps.GameOptions) error {
+ vlog.VI(1).Infof("challenge received: %s %v", judge, gameID)
+ go p.playGame(ctx, judge, gameID)
+ return nil
+}
+
+// playGame plays an entire game, which really only consists of reading
+// commands from the server, and picking a random "move" when asked to.
+func (p *Player) playGame(outer *context.T, judge string, gameID rps.GameID) (rps.PlayResult, error) {
+ ctx, cancel := context.WithTimeout(outer, 10*time.Minute)
+ defer cancel()
+ p.gamesInProgress.Incr(1)
+ defer p.gamesInProgress.Incr(-1)
+ j := rps.RockPaperScissorsClient(judge)
+ game, err := j.Play(ctx, gameID)
+ if err != nil {
+ return rps.PlayResult{}, err
+ }
+ rStream := game.RecvStream()
+ sender := game.SendStream()
+ for rStream.Advance() {
+ in := rStream.Value()
+ switch v := in.(type) {
+ case rps.JudgeActionPlayerNum:
+ vlog.VI(1).Infof("I'm player %d", v.Value)
+ case rps.JudgeActionOpponentName:
+ vlog.VI(1).Infof("My opponent is %q", v.Value)
+ case rps.JudgeActionMoveOptions:
+ opts := v.Value
+ n := rand.Intn(len(opts))
+ vlog.VI(1).Infof("My turn to play. Picked %q from %v", opts[n], opts)
+ if err := sender.Send(rps.PlayerActionMove{opts[n]}); err != nil {
+ return rps.PlayResult{}, err
+ }
+ case rps.JudgeActionRoundResult:
+ rr := v.Value
+ vlog.VI(1).Infof("Player 1 played %q. Player 2 played %q. Winner: %v %s",
+ rr.Moves[0], rr.Moves[1], rr.Winner, rr.Comment)
+ case rps.JudgeActionScore:
+ vlog.VI(1).Infof("Score card: %s", common.FormatScoreCard(v.Value))
+ default:
+ vlog.Infof("unexpected message type: %T", in)
+ }
+ }
+
+ if err := rStream.Err(); err != nil {
+ vlog.Infof("stream error: %v", err)
+ } else {
+ vlog.VI(1).Infof("Game Ended")
+ }
+ result, err := game.Finish()
+ p.gamesPlayed.Incr(1)
+ if err == nil && result.YouWon {
+ p.gamesWon.Incr(1)
+ }
+ return result, err
+}
diff --git a/rps/rpsbot/scorekeeper.go b/rps/rpsbot/scorekeeper.go
new file mode 100644
index 0000000..6fad0aa
--- /dev/null
+++ b/rps/rpsbot/scorekeeper.go
@@ -0,0 +1,32 @@
+package main
+
+import (
+ "v.io/apps/rps"
+ "v.io/apps/rps/common"
+ "v.io/core/veyron/lib/stats"
+ "v.io/core/veyron/lib/stats/counter"
+ "v.io/v23/ipc"
+ "v.io/x/lib/vlog"
+)
+
+type ScoreKeeper struct {
+ numRecords *counter.Counter
+}
+
+func NewScoreKeeper() *ScoreKeeper {
+ return &ScoreKeeper{
+ numRecords: stats.NewCounter("scorekeeper/num-records"),
+ }
+}
+
+func (k *ScoreKeeper) Stats() int64 {
+ return k.numRecords.Value()
+}
+
+func (k *ScoreKeeper) Record(ctx ipc.ServerContext, score rps.ScoreCard) error {
+ b, _ := ctx.RemoteBlessings().ForContext(ctx)
+ vlog.VI(1).Infof("Received ScoreCard from %v:", b)
+ vlog.VI(1).Info(common.FormatScoreCard(score))
+ k.numRecords.Incr(1)
+ return nil
+}
diff --git a/rps/rpsplayer/main.go b/rps/rpsplayer/main.go
new file mode 100644
index 0000000..2bba584
--- /dev/null
+++ b/rps/rpsplayer/main.go
@@ -0,0 +1,308 @@
+// Command rpsplayer is a command-line implementation of the Player
+// service that allows a human player to join the game.
+package main
+
+import (
+ "errors"
+ "flag"
+ "fmt"
+ "sort"
+ "strings"
+ "sync"
+ "time"
+
+ "v.io/v23"
+ "v.io/v23/context"
+ "v.io/v23/ipc"
+ "v.io/v23/naming"
+ "v.io/v23/vtrace"
+ "v.io/x/lib/vlog"
+
+ _ "v.io/core/veyron/profiles/roaming"
+ sflag "v.io/core/veyron/security/flag"
+
+ "v.io/apps/rps"
+ "v.io/apps/rps/common"
+)
+
+var (
+ name = flag.String("name", "", "identifier to publish itself as (defaults to user@hostname)")
+)
+
+func main() {
+ rootctx, shutdown := v23.Init()
+ defer shutdown()
+
+ for {
+ ctx, _ := vtrace.SetNewTrace(rootctx)
+ if selectOne([]string{"Initiate Game", "Wait For Challenge"}) == 0 {
+ initiateGame(ctx)
+ } else {
+ fmt.Println("Waiting to receive a challenge...")
+ game := recvChallenge(ctx)
+ playGame(ctx, game.address, game.id)
+ }
+ if selectOne([]string{"Play Again", "Quit"}) == 1 {
+ break
+ }
+ }
+}
+
+type gameChallenge struct {
+ address string
+ id rps.GameID
+ opts rps.GameOptions
+}
+
+// impl is a PlayerServerMethods implementation that prompts the user to accept
+// or decline challenges. While waiting for a reply from the user, any incoming
+// challenges are auto-declined.
+type impl struct {
+ ch chan gameChallenge
+ decline bool
+ lock sync.Mutex
+}
+
+func (i *impl) setDecline(v bool) bool {
+ i.lock.Lock()
+ defer i.lock.Unlock()
+ prev := i.decline
+ i.decline = v
+ return prev
+}
+
+func (i *impl) Challenge(ctx ipc.ServerContext, address string, id rps.GameID, opts rps.GameOptions) error {
+ remote, _ := ctx.RemoteBlessings().ForContext(ctx)
+ vlog.VI(1).Infof("Challenge (%q, %+v) from %v", address, id, remote)
+ // When setDecline(true) returns, future challenges will be declined.
+ // Whether the current challenge should be considered depends on the
+ // previous state. If 'decline' was already true, we need to decline
+ // this challenge. It 'decline' was false, this is the first challenge
+ // that we should process.
+ if i.setDecline(true) {
+ return errors.New("player is busy")
+ }
+ fmt.Println()
+ fmt.Printf("Challenge received from %v for a %d-round ", remote, opts.NumRounds)
+ switch opts.GameType {
+ case rps.Classic:
+ fmt.Print("Classic ")
+ case rps.LizardSpock:
+ fmt.Print("Lizard-Spock ")
+ default:
+ }
+ fmt.Println("Game.")
+ if selectOne([]string{"Accept", "Decline"}) == 0 {
+ i.ch <- gameChallenge{address, id, opts}
+ return nil
+ }
+ // Start considering challenges again.
+ i.setDecline(false)
+ return errors.New("player declined challenge")
+}
+
+// recvChallenge runs a server until a game challenge is accepted by the user.
+// The server is stopped afterwards.
+func recvChallenge(ctx *context.T) gameChallenge {
+ server, err := v23.NewServer(ctx)
+ if err != nil {
+ vlog.Fatalf("NewServer failed: %v", err)
+ }
+ ch := make(chan gameChallenge)
+
+ listenSpec := v23.GetListenSpec(ctx)
+ ep, err := server.Listen(listenSpec)
+ if err != nil {
+ vlog.Fatalf("Listen(%v) failed: %v", listenSpec, err)
+ }
+ if *name == "" {
+ *name = common.CreateName()
+ }
+ if err := server.Serve(fmt.Sprintf("rps/player/%s", *name), rps.PlayerServer(&impl{ch: ch}), sflag.NewAuthorizerOrDie()); err != nil {
+ vlog.Fatalf("Serve failed: %v", err)
+ }
+ vlog.Infof("Listening on endpoint /%s", ep)
+ result := <-ch
+ server.Stop()
+ return result
+}
+
+// initiateGame initiates a new game by getting a list of judges and players,
+// and asking the user to select one of each, to select the game options, what
+// to play, etc.
+func initiateGame(ctx *context.T) error {
+ jChan := make(chan []string)
+ oChan := make(chan []string)
+ go findAll(ctx, "judge", jChan)
+ go findAll(ctx, "player", oChan)
+
+ fmt.Println("Looking for available participants...")
+ judges := <-jChan
+ opponents := <-oChan
+ fmt.Println()
+ if len(judges) == 0 || len(opponents) == 0 {
+ return errors.New("no one to play with")
+ }
+
+ fmt.Println("Choose a judge:")
+ j := selectOne(judges)
+ fmt.Println("Choose an opponent:")
+ o := selectOne(opponents)
+ fmt.Println("Choose the type of rock-paper-scissors game would you like to play:")
+ gameType := selectOne([]string{"Classic", "LizardSpock"})
+ fmt.Println("Choose the number of rounds required to win:")
+ numRounds := selectOne([]string{"1", "2", "3", "4", "5", "6"}) + 1
+ gameOpts := rps.GameOptions{NumRounds: int32(numRounds), GameType: rps.GameTypeTag(gameType)}
+
+ gameID, err := createGame(ctx, judges[j], gameOpts)
+ if err != nil {
+ vlog.Infof("createGame: %v", err)
+ return err
+ }
+ for {
+ err := sendChallenge(ctx, opponents[o], judges[j], gameID, gameOpts)
+ if err == nil {
+ break
+ }
+ fmt.Printf("Challenge was declined by %s (%v)\n", opponents[o], err)
+ fmt.Println("Choose another opponent:")
+ o = selectOne(opponents)
+ }
+ fmt.Println("Joining the game...")
+
+ if _, err = playGame(ctx, judges[j], gameID); err != nil {
+ vlog.Infof("playGame: %v", err)
+ return err
+ }
+ return nil
+}
+
+func createGame(ctx *context.T, server string, opts rps.GameOptions) (rps.GameID, error) {
+ j := rps.RockPaperScissorsClient(server)
+ return j.CreateGame(ctx, opts)
+}
+
+func sendChallenge(ctx *context.T, opponent, judge string, gameID rps.GameID, gameOpts rps.GameOptions) error {
+ o := rps.RockPaperScissorsClient(opponent)
+ return o.Challenge(ctx, judge, gameID, gameOpts)
+}
+
+func playGame(outer *context.T, judge string, gameID rps.GameID) (rps.PlayResult, error) {
+ ctx, cancel := context.WithTimeout(outer, 10*time.Minute)
+ defer cancel()
+ fmt.Println()
+ j := rps.RockPaperScissorsClient(judge)
+ game, err := j.Play(ctx, gameID)
+ if err != nil {
+ return rps.PlayResult{}, err
+ }
+ var playerNum int32
+ rStream := game.RecvStream()
+ for rStream.Advance() {
+ in := rStream.Value()
+ switch v := in.(type) {
+ case rps.JudgeActionPlayerNum:
+ playerNum = v.Value
+ fmt.Printf("You are player %d\n", playerNum)
+ case rps.JudgeActionOpponentName:
+ fmt.Printf("Your opponent is %q\n", v.Value)
+ case rps.JudgeActionRoundResult:
+ rr := v.Value
+ if playerNum != 1 && playerNum != 2 {
+ vlog.Fatalf("invalid playerNum: %d", playerNum)
+ }
+ fmt.Printf("You played %q\n", rr.Moves[playerNum-1])
+ fmt.Printf("Your opponent played %q\n", rr.Moves[2-playerNum])
+ if len(rr.Comment) > 0 {
+ fmt.Printf(">>> %s <<<\n", strings.ToUpper(rr.Comment))
+ }
+ if rr.Winner == 0 {
+ fmt.Println("----- It's a draw -----")
+ } else if rps.WinnerTag(playerNum) == rr.Winner {
+ fmt.Println("***** You WIN *****")
+ } else {
+ fmt.Println("##### You LOSE #####")
+ }
+ case rps.JudgeActionMoveOptions:
+ opts := v.Value
+ fmt.Println()
+ fmt.Println("Choose your weapon:")
+ m := selectOne(opts)
+ if err := game.SendStream().Send(rps.PlayerActionMove{opts[m]}); err != nil {
+ return rps.PlayResult{}, err
+ }
+ case rps.JudgeActionScore:
+ score := v.Value
+ fmt.Println()
+ fmt.Println("==== GAME SUMMARY ====")
+ fmt.Print(common.FormatScoreCard(score))
+ fmt.Println("======================")
+ if rps.WinnerTag(playerNum) == score.Winner {
+ fmt.Println("You won! :)")
+ } else {
+ fmt.Println("You lost! :(")
+ }
+ default:
+ vlog.Infof("unexpected message type: %T", in)
+ }
+ }
+ if err := rStream.Err(); err == nil {
+ fmt.Println("Game Ended")
+ } else {
+ vlog.Infof("stream error: %v", err)
+ }
+
+ return game.Finish()
+}
+
+func selectOne(choices []string) (choice int) {
+ if len(choices) == 0 {
+ vlog.Fatal("No options to choose from!")
+ }
+ fmt.Println()
+ for i, x := range choices {
+ fmt.Printf(" %d. %s\n", i+1, x)
+ }
+ fmt.Println()
+ for {
+ if len(choices) == 1 {
+ fmt.Print("Select one [1] --> ")
+ } else {
+ fmt.Printf("Select one [1-%d] --> ", len(choices))
+ }
+ fmt.Scanf("%d", &choice)
+ if choice >= 1 && choice <= len(choices) {
+ choice -= 1
+ break
+ }
+ }
+ fmt.Println()
+ return
+}
+
+func findAll(ctx *context.T, t string, out chan []string) {
+ ns := v23.GetNamespace(ctx)
+ var result []string
+ c, err := ns.Glob(ctx, "rps/"+t+"/*")
+ if err != nil {
+ vlog.Infof("ns.Glob failed: %v", err)
+ out <- result
+ return
+ }
+ for e := range c {
+ switch v := e.(type) {
+ case *naming.GlobError:
+ fmt.Print("E")
+ case *naming.MountEntry:
+ fmt.Print(".")
+ result = append(result, v.Name)
+ }
+ }
+ if len(result) == 0 {
+ vlog.Infof("found no %ss", t)
+ out <- result
+ return
+ }
+ sort.Strings(result)
+ out <- result
+}
diff --git a/rps/rpsscorekeeper/main.go b/rps/rpsscorekeeper/main.go
new file mode 100644
index 0000000..4533eb5
--- /dev/null
+++ b/rps/rpsscorekeeper/main.go
@@ -0,0 +1,63 @@
+// Command rpsscorekeeper is a simple implementation of the
+// ScoreKeeper service. It publishes itself as a score keeper for the
+// rock-paper-scissors game and prints out all the score cards it
+// receives to stdout.
+package main
+
+import (
+ "fmt"
+ "os"
+
+ "v.io/v23"
+ "v.io/v23/ipc"
+ "v.io/x/lib/vlog"
+
+ _ "v.io/core/veyron/profiles/roaming"
+ sflag "v.io/core/veyron/security/flag"
+
+ "v.io/apps/rps"
+ "v.io/apps/rps/common"
+)
+
+type impl struct {
+ ch chan rps.ScoreCard
+}
+
+func (i *impl) Record(ctx ipc.ServerContext, score rps.ScoreCard) error {
+ b, _ := ctx.RemoteBlessings().ForContext(ctx)
+ vlog.VI(1).Infof("Record (%+v) from %v", score, b)
+ i.ch <- score
+ return nil
+}
+
+func main() {
+ ctx, shutdown := v23.Init()
+ defer shutdown()
+
+ server, err := v23.NewServer(ctx)
+ if err != nil {
+ vlog.Fatalf("NewServer failed: %v", err)
+ }
+ defer server.Stop()
+
+ ch := make(chan rps.ScoreCard)
+ rpsService := &impl{ch}
+
+ listenSpec := v23.GetListenSpec(ctx)
+ ep, err := server.Listen(listenSpec)
+ if err != nil {
+ vlog.Fatalf("Listen(%v) failed: %v", listenSpec, err)
+ }
+ hostname, err := os.Hostname()
+ if err != nil {
+ vlog.Fatalf("os.Hostname failed: %v", err)
+ }
+ if err := server.Serve(fmt.Sprintf("rps/scorekeeper/%s", hostname), rps.ScoreKeeperServer(rpsService), sflag.NewAuthorizerOrDie()); err != nil {
+ vlog.Fatalf("Serve failed: %v", err)
+ }
+ vlog.Infof("Listening on endpoint /%s", ep)
+
+ for score := range ch {
+ fmt.Print("======================\n", common.FormatScoreCard(score))
+ }
+}
diff --git a/rps/service.vdl b/rps/service.vdl
new file mode 100644
index 0000000..708cb85
--- /dev/null
+++ b/rps/service.vdl
@@ -0,0 +1,110 @@
+// Package rps is an example of veyron service for playing the game of
+// Rock-Paper-Scissors. (http://en.wikipedia.org/wiki/Rock-paper-scissors)
+//
+// There are three different roles in the game:
+//
+// 1. Judge: A judge enforces the rules of the game and decides who
+// the winner is. At the end of the game, the judge reports the
+// final score to all the score keepers.
+//
+// 2. Player: A player can ask a judge to start a new game, it can
+// challenge another player, and it can play a game.
+//
+// 3. ScoreKeeper: A score keeper receives the final score for a game
+// after it ended.
+package rps
+
+import "v.io/v23/services/security/access"
+
+type RockPaperScissors interface {
+ Judge
+ Player
+ ScoreKeeper
+}
+
+type Judge interface {
+ // CreateGame creates a new game with the given game options and returns a game
+ // identifier that can be used by the players to join the game.
+ CreateGame(Opts GameOptions) (GameID | error) {access.Write}
+ // Play lets a player join an existing game and play.
+ Play(ID GameID) stream<PlayerAction,JudgeAction> (PlayResult | error) {access.Write}
+}
+
+// A GameID is used to uniquely identify a game within one Judge.
+type GameID struct {
+ ID string
+}
+
+// GameOptions specifies the parameters of a game.
+type GameOptions struct {
+ NumRounds int32 // The number of rounds that a player must win to win the game.
+ GameType GameTypeTag // The type of game to play: Classic or LizardSpock.
+}
+
+type GameTypeTag byte
+const (
+ Classic = GameTypeTag(0) // Rock-Paper-Scissors
+ LizardSpock = GameTypeTag(1) // Rock-Paper-Scissors-Lizard-Spock
+)
+
+type PlayerAction union {
+ Move string // The move that the player wants to make.
+ Quit unused // Indicates that the player is quitting the game.
+}
+
+type unused struct{}
+
+type JudgeAction union {
+ PlayerNum int32 // The player's number.
+ OpponentName string // The name of the opponent.
+ MoveOptions []string // A list of allowed moves that the player must choose from.
+ RoundResult Round // The result of the previous round.
+ Score ScoreCard // The result of the game.
+}
+
+type PlayersMoves [2]string
+
+// Round represents the state of a round.
+type Round struct {
+ Moves PlayersMoves // Each player's move.
+ Comment string // A text comment from judge about the round.
+ Winner WinnerTag // Who won the round.
+ StartTimeNS int64 // The time at which the round started.
+ EndTimeNS int64 // The time at which the round ended.
+}
+
+// WinnerTag is a type used to indicate whether a round or a game was a draw,
+// was won by player 1 or was won by player 2.
+type WinnerTag byte
+const (
+ Draw = WinnerTag(0)
+ Player1 = WinnerTag(1)
+ Player2 = WinnerTag(2)
+)
+
+// PlayResult is the value returned by the Play method. It indicates the outcome of the game.
+type PlayResult struct {
+ YouWon bool // True if the player receiving the result won the game.
+}
+
+// Player can receive challenges from other players.
+type Player interface {
+ // Challenge is used by other players to challenge this player to a game. If
+ // the challenge is accepted, the method returns nil.
+ Challenge(Address string, ID GameID, Opts GameOptions) error {access.Write}
+}
+
+// ScoreKeeper receives the outcome of games from Judges.
+type ScoreKeeper interface {
+ Record(Score ScoreCard) error {access.Write}
+}
+
+type ScoreCard struct {
+ Opts GameOptions // The game options.
+ Judge string // The name of the judge.
+ Players []string // The name of the players.
+ Rounds []Round // The outcome of each round.
+ StartTimeNS int64 // The time at which the game started.
+ EndTimeNS int64 // The time at which the game ended.
+ Winner WinnerTag // Who won the game.
+}
diff --git a/rps/service.vdl.go b/rps/service.vdl.go
new file mode 100644
index 0000000..49d8f17
--- /dev/null
+++ b/rps/service.vdl.go
@@ -0,0 +1,955 @@
+// This file was auto-generated by the veyron vdl tool.
+// Source: service.vdl
+
+// Package rps is an example of veyron service for playing the game of
+// Rock-Paper-Scissors. (http://en.wikipedia.org/wiki/Rock-paper-scissors)
+//
+// There are three different roles in the game:
+//
+// 1. Judge: A judge enforces the rules of the game and decides who
+// the winner is. At the end of the game, the judge reports the
+// final score to all the score keepers.
+//
+// 2. Player: A player can ask a judge to start a new game, it can
+// challenge another player, and it can play a game.
+//
+// 3. ScoreKeeper: A score keeper receives the final score for a game
+// after it ended.
+package rps
+
+import (
+ // VDL system imports
+ "io"
+ "v.io/v23"
+ "v.io/v23/context"
+ "v.io/v23/ipc"
+ "v.io/v23/vdl"
+
+ // VDL user imports
+ "v.io/v23/services/security/access"
+)
+
+// A GameID is used to uniquely identify a game within one Judge.
+type GameID struct {
+ ID string
+}
+
+func (GameID) __VDLReflect(struct {
+ Name string "v.io/apps/rps.GameID"
+}) {
+}
+
+// GameOptions specifies the parameters of a game.
+type GameOptions struct {
+ NumRounds int32 // The number of rounds that a player must win to win the game.
+ GameType GameTypeTag // The type of game to play: Classic or LizardSpock.
+}
+
+func (GameOptions) __VDLReflect(struct {
+ Name string "v.io/apps/rps.GameOptions"
+}) {
+}
+
+type GameTypeTag byte
+
+func (GameTypeTag) __VDLReflect(struct {
+ Name string "v.io/apps/rps.GameTypeTag"
+}) {
+}
+
+type (
+ // PlayerAction represents any single field of the PlayerAction union type.
+ PlayerAction interface {
+ // Index returns the field index.
+ Index() int
+ // Interface returns the field value as an interface.
+ Interface() interface{}
+ // Name returns the field name.
+ Name() string
+ // __VDLReflect describes the PlayerAction union type.
+ __VDLReflect(__PlayerActionReflect)
+ }
+ // PlayerActionMove represents field Move of the PlayerAction union type.
+ PlayerActionMove struct{ Value string } // The move that the player wants to make.
+ // PlayerActionQuit represents field Quit of the PlayerAction union type.
+ PlayerActionQuit struct{ Value unused } // Indicates that the player is quitting the game.
+ // __PlayerActionReflect describes the PlayerAction union type.
+ __PlayerActionReflect struct {
+ Name string "v.io/apps/rps.PlayerAction"
+ Type PlayerAction
+ Union struct {
+ Move PlayerActionMove
+ Quit PlayerActionQuit
+ }
+ }
+)
+
+func (x PlayerActionMove) Index() int { return 0 }
+func (x PlayerActionMove) Interface() interface{} { return x.Value }
+func (x PlayerActionMove) Name() string { return "Move" }
+func (x PlayerActionMove) __VDLReflect(__PlayerActionReflect) {}
+
+func (x PlayerActionQuit) Index() int { return 1 }
+func (x PlayerActionQuit) Interface() interface{} { return x.Value }
+func (x PlayerActionQuit) Name() string { return "Quit" }
+func (x PlayerActionQuit) __VDLReflect(__PlayerActionReflect) {}
+
+type unused struct {
+}
+
+func (unused) __VDLReflect(struct {
+ Name string "v.io/apps/rps.unused"
+}) {
+}
+
+type (
+ // JudgeAction represents any single field of the JudgeAction union type.
+ JudgeAction interface {
+ // Index returns the field index.
+ Index() int
+ // Interface returns the field value as an interface.
+ Interface() interface{}
+ // Name returns the field name.
+ Name() string
+ // __VDLReflect describes the JudgeAction union type.
+ __VDLReflect(__JudgeActionReflect)
+ }
+ // JudgeActionPlayerNum represents field PlayerNum of the JudgeAction union type.
+ JudgeActionPlayerNum struct{ Value int32 } // The player's number.
+ // JudgeActionOpponentName represents field OpponentName of the JudgeAction union type.
+ JudgeActionOpponentName struct{ Value string } // The name of the opponent.
+ // JudgeActionMoveOptions represents field MoveOptions of the JudgeAction union type.
+ JudgeActionMoveOptions struct{ Value []string } // A list of allowed moves that the player must choose from.
+ // JudgeActionRoundResult represents field RoundResult of the JudgeAction union type.
+ JudgeActionRoundResult struct{ Value Round } // The result of the previous round.
+ // JudgeActionScore represents field Score of the JudgeAction union type.
+ JudgeActionScore struct{ Value ScoreCard } // The result of the game.
+ // __JudgeActionReflect describes the JudgeAction union type.
+ __JudgeActionReflect struct {
+ Name string "v.io/apps/rps.JudgeAction"
+ Type JudgeAction
+ Union struct {
+ PlayerNum JudgeActionPlayerNum
+ OpponentName JudgeActionOpponentName
+ MoveOptions JudgeActionMoveOptions
+ RoundResult JudgeActionRoundResult
+ Score JudgeActionScore
+ }
+ }
+)
+
+func (x JudgeActionPlayerNum) Index() int { return 0 }
+func (x JudgeActionPlayerNum) Interface() interface{} { return x.Value }
+func (x JudgeActionPlayerNum) Name() string { return "PlayerNum" }
+func (x JudgeActionPlayerNum) __VDLReflect(__JudgeActionReflect) {}
+
+func (x JudgeActionOpponentName) Index() int { return 1 }
+func (x JudgeActionOpponentName) Interface() interface{} { return x.Value }
+func (x JudgeActionOpponentName) Name() string { return "OpponentName" }
+func (x JudgeActionOpponentName) __VDLReflect(__JudgeActionReflect) {}
+
+func (x JudgeActionMoveOptions) Index() int { return 2 }
+func (x JudgeActionMoveOptions) Interface() interface{} { return x.Value }
+func (x JudgeActionMoveOptions) Name() string { return "MoveOptions" }
+func (x JudgeActionMoveOptions) __VDLReflect(__JudgeActionReflect) {}
+
+func (x JudgeActionRoundResult) Index() int { return 3 }
+func (x JudgeActionRoundResult) Interface() interface{} { return x.Value }
+func (x JudgeActionRoundResult) Name() string { return "RoundResult" }
+func (x JudgeActionRoundResult) __VDLReflect(__JudgeActionReflect) {}
+
+func (x JudgeActionScore) Index() int { return 4 }
+func (x JudgeActionScore) Interface() interface{} { return x.Value }
+func (x JudgeActionScore) Name() string { return "Score" }
+func (x JudgeActionScore) __VDLReflect(__JudgeActionReflect) {}
+
+type PlayersMoves [2]string
+
+func (PlayersMoves) __VDLReflect(struct {
+ Name string "v.io/apps/rps.PlayersMoves"
+}) {
+}
+
+// Round represents the state of a round.
+type Round struct {
+ Moves PlayersMoves // Each player's move.
+ Comment string // A text comment from judge about the round.
+ Winner WinnerTag // Who won the round.
+ StartTimeNS int64 // The time at which the round started.
+ EndTimeNS int64 // The time at which the round ended.
+}
+
+func (Round) __VDLReflect(struct {
+ Name string "v.io/apps/rps.Round"
+}) {
+}
+
+// WinnerTag is a type used to indicate whether a round or a game was a draw,
+// was won by player 1 or was won by player 2.
+type WinnerTag byte
+
+func (WinnerTag) __VDLReflect(struct {
+ Name string "v.io/apps/rps.WinnerTag"
+}) {
+}
+
+// PlayResult is the value returned by the Play method. It indicates the outcome of the game.
+type PlayResult struct {
+ YouWon bool // True if the player receiving the result won the game.
+}
+
+func (PlayResult) __VDLReflect(struct {
+ Name string "v.io/apps/rps.PlayResult"
+}) {
+}
+
+type ScoreCard struct {
+ Opts GameOptions // The game options.
+ Judge string // The name of the judge.
+ Players []string // The name of the players.
+ Rounds []Round // The outcome of each round.
+ StartTimeNS int64 // The time at which the game started.
+ EndTimeNS int64 // The time at which the game ended.
+ Winner WinnerTag // Who won the game.
+}
+
+func (ScoreCard) __VDLReflect(struct {
+ Name string "v.io/apps/rps.ScoreCard"
+}) {
+}
+
+func init() {
+ vdl.Register((*GameID)(nil))
+ vdl.Register((*GameOptions)(nil))
+ vdl.Register((*GameTypeTag)(nil))
+ vdl.Register((*PlayerAction)(nil))
+ vdl.Register((*unused)(nil))
+ vdl.Register((*JudgeAction)(nil))
+ vdl.Register((*PlayersMoves)(nil))
+ vdl.Register((*Round)(nil))
+ vdl.Register((*WinnerTag)(nil))
+ vdl.Register((*PlayResult)(nil))
+ vdl.Register((*ScoreCard)(nil))
+}
+
+const Classic = GameTypeTag(0) // Rock-Paper-Scissors
+
+const LizardSpock = GameTypeTag(1) // Rock-Paper-Scissors-Lizard-Spock
+
+const Draw = WinnerTag(0)
+
+const Player1 = WinnerTag(1)
+
+const Player2 = WinnerTag(2)
+
+// JudgeClientMethods is the client interface
+// containing Judge methods.
+type JudgeClientMethods interface {
+ // CreateGame creates a new game with the given game options and returns a game
+ // identifier that can be used by the players to join the game.
+ CreateGame(ctx *context.T, Opts GameOptions, opts ...ipc.CallOpt) (GameID, error)
+ // Play lets a player join an existing game and play.
+ Play(ctx *context.T, ID GameID, opts ...ipc.CallOpt) (JudgePlayCall, error)
+}
+
+// JudgeClientStub adds universal methods to JudgeClientMethods.
+type JudgeClientStub interface {
+ JudgeClientMethods
+ ipc.UniversalServiceMethods
+}
+
+// JudgeClient returns a client stub for Judge.
+func JudgeClient(name string, opts ...ipc.BindOpt) JudgeClientStub {
+ var client ipc.Client
+ for _, opt := range opts {
+ if clientOpt, ok := opt.(ipc.Client); ok {
+ client = clientOpt
+ }
+ }
+ return implJudgeClientStub{name, client}
+}
+
+type implJudgeClientStub struct {
+ name string
+ client ipc.Client
+}
+
+func (c implJudgeClientStub) c(ctx *context.T) ipc.Client {
+ if c.client != nil {
+ return c.client
+ }
+ return v23.GetClient(ctx)
+}
+
+func (c implJudgeClientStub) CreateGame(ctx *context.T, i0 GameOptions, opts ...ipc.CallOpt) (o0 GameID, err error) {
+ var call ipc.Call
+ if call, err = c.c(ctx).StartCall(ctx, c.name, "CreateGame", []interface{}{i0}, opts...); err != nil {
+ return
+ }
+ err = call.Finish(&o0)
+ return
+}
+
+func (c implJudgeClientStub) Play(ctx *context.T, i0 GameID, opts ...ipc.CallOpt) (ocall JudgePlayCall, err error) {
+ var call ipc.Call
+ if call, err = c.c(ctx).StartCall(ctx, c.name, "Play", []interface{}{i0}, opts...); err != nil {
+ return
+ }
+ ocall = &implJudgePlayCall{Call: call}
+ return
+}
+
+// JudgePlayClientStream is the client stream for Judge.Play.
+type JudgePlayClientStream interface {
+ // RecvStream returns the receiver side of the Judge.Play client stream.
+ RecvStream() interface {
+ // Advance stages an item so that it may be retrieved via Value. Returns
+ // true iff there is an item to retrieve. Advance must be called before
+ // Value is called. May block if an item is not available.
+ Advance() bool
+ // Value returns the item that was staged by Advance. May panic if Advance
+ // returned false or was not called. Never blocks.
+ Value() JudgeAction
+ // Err returns any error encountered by Advance. Never blocks.
+ Err() error
+ }
+ // SendStream returns the send side of the Judge.Play client stream.
+ SendStream() interface {
+ // Send places the item onto the output stream. Returns errors
+ // encountered while sending, or if Send is called after Close or
+ // the stream has been canceled. Blocks if there is no buffer
+ // space; will unblock when buffer space is available or after
+ // the stream has been canceled.
+ Send(item PlayerAction) error
+ // Close indicates to the server that no more items will be sent;
+ // server Recv calls will receive io.EOF after all sent items.
+ // This is an optional call - e.g. a client might call Close if it
+ // needs to continue receiving items from the server after it's
+ // done sending. Returns errors encountered while closing, or if
+ // Close is called after the stream has been canceled. Like Send,
+ // blocks if there is no buffer space available.
+ Close() error
+ }
+}
+
+// JudgePlayCall represents the call returned from Judge.Play.
+type JudgePlayCall interface {
+ JudgePlayClientStream
+ // Finish performs the equivalent of SendStream().Close, then blocks until
+ // the server is done, and returns the positional return values for the call.
+ //
+ // Finish returns immediately if the call has been canceled; depending on the
+ // timing the output could either be an error signaling cancelation, or the
+ // valid positional return values from the server.
+ //
+ // Calling Finish is mandatory for releasing stream resources, unless the call
+ // has been canceled or any of the other methods return an error. Finish should
+ // be called at most once.
+ Finish() (PlayResult, error)
+}
+
+type implJudgePlayCall struct {
+ ipc.Call
+ valRecv JudgeAction
+ errRecv error
+}
+
+func (c *implJudgePlayCall) RecvStream() interface {
+ Advance() bool
+ Value() JudgeAction
+ Err() error
+} {
+ return implJudgePlayCallRecv{c}
+}
+
+type implJudgePlayCallRecv struct {
+ c *implJudgePlayCall
+}
+
+func (c implJudgePlayCallRecv) Advance() bool {
+ c.c.errRecv = c.c.Recv(&c.c.valRecv)
+ return c.c.errRecv == nil
+}
+func (c implJudgePlayCallRecv) Value() JudgeAction {
+ return c.c.valRecv
+}
+func (c implJudgePlayCallRecv) Err() error {
+ if c.c.errRecv == io.EOF {
+ return nil
+ }
+ return c.c.errRecv
+}
+func (c *implJudgePlayCall) SendStream() interface {
+ Send(item PlayerAction) error
+ Close() error
+} {
+ return implJudgePlayCallSend{c}
+}
+
+type implJudgePlayCallSend struct {
+ c *implJudgePlayCall
+}
+
+func (c implJudgePlayCallSend) Send(item PlayerAction) error {
+ return c.c.Send(item)
+}
+func (c implJudgePlayCallSend) Close() error {
+ return c.c.CloseSend()
+}
+func (c *implJudgePlayCall) Finish() (o0 PlayResult, err error) {
+ err = c.Call.Finish(&o0)
+ return
+}
+
+// JudgeServerMethods is the interface a server writer
+// implements for Judge.
+type JudgeServerMethods interface {
+ // CreateGame creates a new game with the given game options and returns a game
+ // identifier that can be used by the players to join the game.
+ CreateGame(ctx ipc.ServerContext, Opts GameOptions) (GameID, error)
+ // Play lets a player join an existing game and play.
+ Play(ctx JudgePlayContext, ID GameID) (PlayResult, error)
+}
+
+// JudgeServerStubMethods is the server interface containing
+// Judge methods, as expected by ipc.Server.
+// The only difference between this interface and JudgeServerMethods
+// is the streaming methods.
+type JudgeServerStubMethods interface {
+ // CreateGame creates a new game with the given game options and returns a game
+ // identifier that can be used by the players to join the game.
+ CreateGame(ctx ipc.ServerContext, Opts GameOptions) (GameID, error)
+ // Play lets a player join an existing game and play.
+ Play(ctx *JudgePlayContextStub, ID GameID) (PlayResult, error)
+}
+
+// JudgeServerStub adds universal methods to JudgeServerStubMethods.
+type JudgeServerStub interface {
+ JudgeServerStubMethods
+ // Describe the Judge interfaces.
+ Describe__() []ipc.InterfaceDesc
+}
+
+// JudgeServer returns a server stub for Judge.
+// It converts an implementation of JudgeServerMethods into
+// an object that may be used by ipc.Server.
+func JudgeServer(impl JudgeServerMethods) JudgeServerStub {
+ stub := implJudgeServerStub{
+ impl: impl,
+ }
+ // Initialize GlobState; always check the stub itself first, to handle the
+ // case where the user has the Glob method defined in their VDL source.
+ if gs := ipc.NewGlobState(stub); gs != nil {
+ stub.gs = gs
+ } else if gs := ipc.NewGlobState(impl); gs != nil {
+ stub.gs = gs
+ }
+ return stub
+}
+
+type implJudgeServerStub struct {
+ impl JudgeServerMethods
+ gs *ipc.GlobState
+}
+
+func (s implJudgeServerStub) CreateGame(ctx ipc.ServerContext, i0 GameOptions) (GameID, error) {
+ return s.impl.CreateGame(ctx, i0)
+}
+
+func (s implJudgeServerStub) Play(ctx *JudgePlayContextStub, i0 GameID) (PlayResult, error) {
+ return s.impl.Play(ctx, i0)
+}
+
+func (s implJudgeServerStub) Globber() *ipc.GlobState {
+ return s.gs
+}
+
+func (s implJudgeServerStub) Describe__() []ipc.InterfaceDesc {
+ return []ipc.InterfaceDesc{JudgeDesc}
+}
+
+// JudgeDesc describes the Judge interface.
+var JudgeDesc ipc.InterfaceDesc = descJudge
+
+// descJudge hides the desc to keep godoc clean.
+var descJudge = ipc.InterfaceDesc{
+ Name: "Judge",
+ PkgPath: "v.io/apps/rps",
+ Methods: []ipc.MethodDesc{
+ {
+ Name: "CreateGame",
+ Doc: "// CreateGame creates a new game with the given game options and returns a game\n// identifier that can be used by the players to join the game.",
+ InArgs: []ipc.ArgDesc{
+ {"Opts", ``}, // GameOptions
+ },
+ OutArgs: []ipc.ArgDesc{
+ {"", ``}, // GameID
+ },
+ Tags: []*vdl.Value{vdl.ValueOf(access.Tag("Write"))},
+ },
+ {
+ Name: "Play",
+ Doc: "// Play lets a player join an existing game and play.",
+ InArgs: []ipc.ArgDesc{
+ {"ID", ``}, // GameID
+ },
+ OutArgs: []ipc.ArgDesc{
+ {"", ``}, // PlayResult
+ },
+ Tags: []*vdl.Value{vdl.ValueOf(access.Tag("Write"))},
+ },
+ },
+}
+
+// JudgePlayServerStream is the server stream for Judge.Play.
+type JudgePlayServerStream interface {
+ // RecvStream returns the receiver side of the Judge.Play server stream.
+ RecvStream() interface {
+ // Advance stages an item so that it may be retrieved via Value. Returns
+ // true iff there is an item to retrieve. Advance must be called before
+ // Value is called. May block if an item is not available.
+ Advance() bool
+ // Value returns the item that was staged by Advance. May panic if Advance
+ // returned false or was not called. Never blocks.
+ Value() PlayerAction
+ // Err returns any error encountered by Advance. Never blocks.
+ Err() error
+ }
+ // SendStream returns the send side of the Judge.Play server stream.
+ SendStream() interface {
+ // Send places the item onto the output stream. Returns errors encountered
+ // while sending. Blocks if there is no buffer space; will unblock when
+ // buffer space is available.
+ Send(item JudgeAction) error
+ }
+}
+
+// JudgePlayContext represents the context passed to Judge.Play.
+type JudgePlayContext interface {
+ ipc.ServerContext
+ JudgePlayServerStream
+}
+
+// JudgePlayContextStub is a wrapper that converts ipc.StreamServerCall into
+// a typesafe stub that implements JudgePlayContext.
+type JudgePlayContextStub struct {
+ ipc.StreamServerCall
+ valRecv PlayerAction
+ errRecv error
+}
+
+// Init initializes JudgePlayContextStub from ipc.StreamServerCall.
+func (s *JudgePlayContextStub) Init(call ipc.StreamServerCall) {
+ s.StreamServerCall = call
+}
+
+// RecvStream returns the receiver side of the Judge.Play server stream.
+func (s *JudgePlayContextStub) RecvStream() interface {
+ Advance() bool
+ Value() PlayerAction
+ Err() error
+} {
+ return implJudgePlayContextRecv{s}
+}
+
+type implJudgePlayContextRecv struct {
+ s *JudgePlayContextStub
+}
+
+func (s implJudgePlayContextRecv) Advance() bool {
+ s.s.errRecv = s.s.Recv(&s.s.valRecv)
+ return s.s.errRecv == nil
+}
+func (s implJudgePlayContextRecv) Value() PlayerAction {
+ return s.s.valRecv
+}
+func (s implJudgePlayContextRecv) Err() error {
+ if s.s.errRecv == io.EOF {
+ return nil
+ }
+ return s.s.errRecv
+}
+
+// SendStream returns the send side of the Judge.Play server stream.
+func (s *JudgePlayContextStub) SendStream() interface {
+ Send(item JudgeAction) error
+} {
+ return implJudgePlayContextSend{s}
+}
+
+type implJudgePlayContextSend struct {
+ s *JudgePlayContextStub
+}
+
+func (s implJudgePlayContextSend) Send(item JudgeAction) error {
+ return s.s.Send(item)
+}
+
+// PlayerClientMethods is the client interface
+// containing Player methods.
+//
+// Player can receive challenges from other players.
+type PlayerClientMethods interface {
+ // Challenge is used by other players to challenge this player to a game. If
+ // the challenge is accepted, the method returns nil.
+ Challenge(ctx *context.T, Address string, ID GameID, Opts GameOptions, opts ...ipc.CallOpt) error
+}
+
+// PlayerClientStub adds universal methods to PlayerClientMethods.
+type PlayerClientStub interface {
+ PlayerClientMethods
+ ipc.UniversalServiceMethods
+}
+
+// PlayerClient returns a client stub for Player.
+func PlayerClient(name string, opts ...ipc.BindOpt) PlayerClientStub {
+ var client ipc.Client
+ for _, opt := range opts {
+ if clientOpt, ok := opt.(ipc.Client); ok {
+ client = clientOpt
+ }
+ }
+ return implPlayerClientStub{name, client}
+}
+
+type implPlayerClientStub struct {
+ name string
+ client ipc.Client
+}
+
+func (c implPlayerClientStub) c(ctx *context.T) ipc.Client {
+ if c.client != nil {
+ return c.client
+ }
+ return v23.GetClient(ctx)
+}
+
+func (c implPlayerClientStub) Challenge(ctx *context.T, i0 string, i1 GameID, i2 GameOptions, opts ...ipc.CallOpt) (err error) {
+ var call ipc.Call
+ if call, err = c.c(ctx).StartCall(ctx, c.name, "Challenge", []interface{}{i0, i1, i2}, opts...); err != nil {
+ return
+ }
+ err = call.Finish()
+ return
+}
+
+// PlayerServerMethods is the interface a server writer
+// implements for Player.
+//
+// Player can receive challenges from other players.
+type PlayerServerMethods interface {
+ // Challenge is used by other players to challenge this player to a game. If
+ // the challenge is accepted, the method returns nil.
+ Challenge(ctx ipc.ServerContext, Address string, ID GameID, Opts GameOptions) error
+}
+
+// PlayerServerStubMethods is the server interface containing
+// Player methods, as expected by ipc.Server.
+// There is no difference between this interface and PlayerServerMethods
+// since there are no streaming methods.
+type PlayerServerStubMethods PlayerServerMethods
+
+// PlayerServerStub adds universal methods to PlayerServerStubMethods.
+type PlayerServerStub interface {
+ PlayerServerStubMethods
+ // Describe the Player interfaces.
+ Describe__() []ipc.InterfaceDesc
+}
+
+// PlayerServer returns a server stub for Player.
+// It converts an implementation of PlayerServerMethods into
+// an object that may be used by ipc.Server.
+func PlayerServer(impl PlayerServerMethods) PlayerServerStub {
+ stub := implPlayerServerStub{
+ impl: impl,
+ }
+ // Initialize GlobState; always check the stub itself first, to handle the
+ // case where the user has the Glob method defined in their VDL source.
+ if gs := ipc.NewGlobState(stub); gs != nil {
+ stub.gs = gs
+ } else if gs := ipc.NewGlobState(impl); gs != nil {
+ stub.gs = gs
+ }
+ return stub
+}
+
+type implPlayerServerStub struct {
+ impl PlayerServerMethods
+ gs *ipc.GlobState
+}
+
+func (s implPlayerServerStub) Challenge(ctx ipc.ServerContext, i0 string, i1 GameID, i2 GameOptions) error {
+ return s.impl.Challenge(ctx, i0, i1, i2)
+}
+
+func (s implPlayerServerStub) Globber() *ipc.GlobState {
+ return s.gs
+}
+
+func (s implPlayerServerStub) Describe__() []ipc.InterfaceDesc {
+ return []ipc.InterfaceDesc{PlayerDesc}
+}
+
+// PlayerDesc describes the Player interface.
+var PlayerDesc ipc.InterfaceDesc = descPlayer
+
+// descPlayer hides the desc to keep godoc clean.
+var descPlayer = ipc.InterfaceDesc{
+ Name: "Player",
+ PkgPath: "v.io/apps/rps",
+ Doc: "// Player can receive challenges from other players.",
+ Methods: []ipc.MethodDesc{
+ {
+ Name: "Challenge",
+ Doc: "// Challenge is used by other players to challenge this player to a game. If\n// the challenge is accepted, the method returns nil.",
+ InArgs: []ipc.ArgDesc{
+ {"Address", ``}, // string
+ {"ID", ``}, // GameID
+ {"Opts", ``}, // GameOptions
+ },
+ Tags: []*vdl.Value{vdl.ValueOf(access.Tag("Write"))},
+ },
+ },
+}
+
+// ScoreKeeperClientMethods is the client interface
+// containing ScoreKeeper methods.
+//
+// ScoreKeeper receives the outcome of games from Judges.
+type ScoreKeeperClientMethods interface {
+ Record(ctx *context.T, Score ScoreCard, opts ...ipc.CallOpt) error
+}
+
+// ScoreKeeperClientStub adds universal methods to ScoreKeeperClientMethods.
+type ScoreKeeperClientStub interface {
+ ScoreKeeperClientMethods
+ ipc.UniversalServiceMethods
+}
+
+// ScoreKeeperClient returns a client stub for ScoreKeeper.
+func ScoreKeeperClient(name string, opts ...ipc.BindOpt) ScoreKeeperClientStub {
+ var client ipc.Client
+ for _, opt := range opts {
+ if clientOpt, ok := opt.(ipc.Client); ok {
+ client = clientOpt
+ }
+ }
+ return implScoreKeeperClientStub{name, client}
+}
+
+type implScoreKeeperClientStub struct {
+ name string
+ client ipc.Client
+}
+
+func (c implScoreKeeperClientStub) c(ctx *context.T) ipc.Client {
+ if c.client != nil {
+ return c.client
+ }
+ return v23.GetClient(ctx)
+}
+
+func (c implScoreKeeperClientStub) Record(ctx *context.T, i0 ScoreCard, opts ...ipc.CallOpt) (err error) {
+ var call ipc.Call
+ if call, err = c.c(ctx).StartCall(ctx, c.name, "Record", []interface{}{i0}, opts...); err != nil {
+ return
+ }
+ err = call.Finish()
+ return
+}
+
+// ScoreKeeperServerMethods is the interface a server writer
+// implements for ScoreKeeper.
+//
+// ScoreKeeper receives the outcome of games from Judges.
+type ScoreKeeperServerMethods interface {
+ Record(ctx ipc.ServerContext, Score ScoreCard) error
+}
+
+// ScoreKeeperServerStubMethods is the server interface containing
+// ScoreKeeper methods, as expected by ipc.Server.
+// There is no difference between this interface and ScoreKeeperServerMethods
+// since there are no streaming methods.
+type ScoreKeeperServerStubMethods ScoreKeeperServerMethods
+
+// ScoreKeeperServerStub adds universal methods to ScoreKeeperServerStubMethods.
+type ScoreKeeperServerStub interface {
+ ScoreKeeperServerStubMethods
+ // Describe the ScoreKeeper interfaces.
+ Describe__() []ipc.InterfaceDesc
+}
+
+// ScoreKeeperServer returns a server stub for ScoreKeeper.
+// It converts an implementation of ScoreKeeperServerMethods into
+// an object that may be used by ipc.Server.
+func ScoreKeeperServer(impl ScoreKeeperServerMethods) ScoreKeeperServerStub {
+ stub := implScoreKeeperServerStub{
+ impl: impl,
+ }
+ // Initialize GlobState; always check the stub itself first, to handle the
+ // case where the user has the Glob method defined in their VDL source.
+ if gs := ipc.NewGlobState(stub); gs != nil {
+ stub.gs = gs
+ } else if gs := ipc.NewGlobState(impl); gs != nil {
+ stub.gs = gs
+ }
+ return stub
+}
+
+type implScoreKeeperServerStub struct {
+ impl ScoreKeeperServerMethods
+ gs *ipc.GlobState
+}
+
+func (s implScoreKeeperServerStub) Record(ctx ipc.ServerContext, i0 ScoreCard) error {
+ return s.impl.Record(ctx, i0)
+}
+
+func (s implScoreKeeperServerStub) Globber() *ipc.GlobState {
+ return s.gs
+}
+
+func (s implScoreKeeperServerStub) Describe__() []ipc.InterfaceDesc {
+ return []ipc.InterfaceDesc{ScoreKeeperDesc}
+}
+
+// ScoreKeeperDesc describes the ScoreKeeper interface.
+var ScoreKeeperDesc ipc.InterfaceDesc = descScoreKeeper
+
+// descScoreKeeper hides the desc to keep godoc clean.
+var descScoreKeeper = ipc.InterfaceDesc{
+ Name: "ScoreKeeper",
+ PkgPath: "v.io/apps/rps",
+ Doc: "// ScoreKeeper receives the outcome of games from Judges.",
+ Methods: []ipc.MethodDesc{
+ {
+ Name: "Record",
+ InArgs: []ipc.ArgDesc{
+ {"Score", ``}, // ScoreCard
+ },
+ Tags: []*vdl.Value{vdl.ValueOf(access.Tag("Write"))},
+ },
+ },
+}
+
+// RockPaperScissorsClientMethods is the client interface
+// containing RockPaperScissors methods.
+type RockPaperScissorsClientMethods interface {
+ JudgeClientMethods
+ // Player can receive challenges from other players.
+ PlayerClientMethods
+ // ScoreKeeper receives the outcome of games from Judges.
+ ScoreKeeperClientMethods
+}
+
+// RockPaperScissorsClientStub adds universal methods to RockPaperScissorsClientMethods.
+type RockPaperScissorsClientStub interface {
+ RockPaperScissorsClientMethods
+ ipc.UniversalServiceMethods
+}
+
+// RockPaperScissorsClient returns a client stub for RockPaperScissors.
+func RockPaperScissorsClient(name string, opts ...ipc.BindOpt) RockPaperScissorsClientStub {
+ var client ipc.Client
+ for _, opt := range opts {
+ if clientOpt, ok := opt.(ipc.Client); ok {
+ client = clientOpt
+ }
+ }
+ return implRockPaperScissorsClientStub{name, client, JudgeClient(name, client), PlayerClient(name, client), ScoreKeeperClient(name, client)}
+}
+
+type implRockPaperScissorsClientStub struct {
+ name string
+ client ipc.Client
+
+ JudgeClientStub
+ PlayerClientStub
+ ScoreKeeperClientStub
+}
+
+func (c implRockPaperScissorsClientStub) c(ctx *context.T) ipc.Client {
+ if c.client != nil {
+ return c.client
+ }
+ return v23.GetClient(ctx)
+}
+
+// RockPaperScissorsServerMethods is the interface a server writer
+// implements for RockPaperScissors.
+type RockPaperScissorsServerMethods interface {
+ JudgeServerMethods
+ // Player can receive challenges from other players.
+ PlayerServerMethods
+ // ScoreKeeper receives the outcome of games from Judges.
+ ScoreKeeperServerMethods
+}
+
+// RockPaperScissorsServerStubMethods is the server interface containing
+// RockPaperScissors methods, as expected by ipc.Server.
+// The only difference between this interface and RockPaperScissorsServerMethods
+// is the streaming methods.
+type RockPaperScissorsServerStubMethods interface {
+ JudgeServerStubMethods
+ // Player can receive challenges from other players.
+ PlayerServerStubMethods
+ // ScoreKeeper receives the outcome of games from Judges.
+ ScoreKeeperServerStubMethods
+}
+
+// RockPaperScissorsServerStub adds universal methods to RockPaperScissorsServerStubMethods.
+type RockPaperScissorsServerStub interface {
+ RockPaperScissorsServerStubMethods
+ // Describe the RockPaperScissors interfaces.
+ Describe__() []ipc.InterfaceDesc
+}
+
+// RockPaperScissorsServer returns a server stub for RockPaperScissors.
+// It converts an implementation of RockPaperScissorsServerMethods into
+// an object that may be used by ipc.Server.
+func RockPaperScissorsServer(impl RockPaperScissorsServerMethods) RockPaperScissorsServerStub {
+ stub := implRockPaperScissorsServerStub{
+ impl: impl,
+ JudgeServerStub: JudgeServer(impl),
+ PlayerServerStub: PlayerServer(impl),
+ ScoreKeeperServerStub: ScoreKeeperServer(impl),
+ }
+ // Initialize GlobState; always check the stub itself first, to handle the
+ // case where the user has the Glob method defined in their VDL source.
+ if gs := ipc.NewGlobState(stub); gs != nil {
+ stub.gs = gs
+ } else if gs := ipc.NewGlobState(impl); gs != nil {
+ stub.gs = gs
+ }
+ return stub
+}
+
+type implRockPaperScissorsServerStub struct {
+ impl RockPaperScissorsServerMethods
+ JudgeServerStub
+ PlayerServerStub
+ ScoreKeeperServerStub
+ gs *ipc.GlobState
+}
+
+func (s implRockPaperScissorsServerStub) Globber() *ipc.GlobState {
+ return s.gs
+}
+
+func (s implRockPaperScissorsServerStub) Describe__() []ipc.InterfaceDesc {
+ return []ipc.InterfaceDesc{RockPaperScissorsDesc, JudgeDesc, PlayerDesc, ScoreKeeperDesc}
+}
+
+// RockPaperScissorsDesc describes the RockPaperScissors interface.
+var RockPaperScissorsDesc ipc.InterfaceDesc = descRockPaperScissors
+
+// descRockPaperScissors hides the desc to keep godoc clean.
+var descRockPaperScissors = ipc.InterfaceDesc{
+ Name: "RockPaperScissors",
+ PkgPath: "v.io/apps/rps",
+ Embeds: []ipc.EmbedDesc{
+ {"Judge", "v.io/apps/rps", ``},
+ {"Player", "v.io/apps/rps", "// Player can receive challenges from other players."},
+ {"ScoreKeeper", "v.io/apps/rps", "// ScoreKeeper receives the outcome of games from Judges."},
+ },
+}
diff --git a/tunnel/tunnel.vdl b/tunnel/tunnel.vdl
new file mode 100644
index 0000000..73d3e7e
--- /dev/null
+++ b/tunnel/tunnel.vdl
@@ -0,0 +1,49 @@
+// Package tunnel describes a service that can be used to create a
+// network tunnel from the client to the server.
+package tunnel
+
+import "v.io/v23/services/security/access"
+
+type Tunnel interface {
+ // The Forward method is used for network forwarding. All the data sent over
+ // the byte stream is forwarded to the requested network address and all the
+ // data received from that network connection is sent back in the reply
+ // stream.
+ Forward(network, address string) stream<[]byte, []byte> error {access.Admin}
+
+ // The Shell method is used to either run shell commands remotely, or to open
+ // an interactive shell. The data received over the byte stream is sent to the
+ // shell's stdin, and the data received from the shell's stdout and stderr is
+ // sent back in the reply stream. It returns the exit status of the shell
+ // command.
+ Shell(command string, shellOpts ShellOpts) stream<ClientShellPacket, ServerShellPacket> (int32 | error) {access.Admin}
+}
+
+type ShellOpts struct {
+ UsePty bool // Whether to open a pseudo-terminal.
+ Environment []string // Environment variables to pass to the remote shell.
+ WinSize WindowSize // The size of the window.
+}
+
+type WindowSize struct {
+ Rows, Cols uint16
+}
+
+type ClientShellPacket union {
+ // Bytes going to the shell's stdin.
+ Stdin []byte
+ // Indicates that stdin should be closed. The presence of this field indicates
+ // EOF. Its actual value is ignored.
+ EOF unused
+ // A dynamic update of the window size.
+ WinSize WindowSize
+}
+
+type unused struct {}
+
+type ServerShellPacket union {
+ // Bytes coming from the shell's stdout.
+ Stdout []byte
+ // Bytes coming from the shell's stderr.
+ Stderr []byte
+}
diff --git a/tunnel/tunnel.vdl.go b/tunnel/tunnel.vdl.go
new file mode 100644
index 0000000..e1cbb01
--- /dev/null
+++ b/tunnel/tunnel.vdl.go
@@ -0,0 +1,694 @@
+// This file was auto-generated by the veyron vdl tool.
+// Source: tunnel.vdl
+
+// Package tunnel describes a service that can be used to create a
+// network tunnel from the client to the server.
+package tunnel
+
+import (
+ // VDL system imports
+ "io"
+ "v.io/v23"
+ "v.io/v23/context"
+ "v.io/v23/ipc"
+ "v.io/v23/vdl"
+
+ // VDL user imports
+ "v.io/v23/services/security/access"
+)
+
+type ShellOpts struct {
+ UsePty bool // Whether to open a pseudo-terminal.
+ Environment []string // Environment variables to pass to the remote shell.
+ WinSize WindowSize // The size of the window.
+}
+
+func (ShellOpts) __VDLReflect(struct {
+ Name string "v.io/apps/tunnel.ShellOpts"
+}) {
+}
+
+type WindowSize struct {
+ Rows uint16
+ Cols uint16
+}
+
+func (WindowSize) __VDLReflect(struct {
+ Name string "v.io/apps/tunnel.WindowSize"
+}) {
+}
+
+type (
+ // ClientShellPacket represents any single field of the ClientShellPacket union type.
+ ClientShellPacket interface {
+ // Index returns the field index.
+ Index() int
+ // Interface returns the field value as an interface.
+ Interface() interface{}
+ // Name returns the field name.
+ Name() string
+ // __VDLReflect describes the ClientShellPacket union type.
+ __VDLReflect(__ClientShellPacketReflect)
+ }
+ // ClientShellPacketStdin represents field Stdin of the ClientShellPacket union type.
+ //
+ // Bytes going to the shell's stdin.
+ ClientShellPacketStdin struct{ Value []byte }
+ // ClientShellPacketEOF represents field EOF of the ClientShellPacket union type.
+ //
+ // Indicates that stdin should be closed. The presence of this field indicates
+ // EOF. Its actual value is ignored.
+ ClientShellPacketEOF struct{ Value unused }
+ // ClientShellPacketWinSize represents field WinSize of the ClientShellPacket union type.
+ //
+ // A dynamic update of the window size.
+ ClientShellPacketWinSize struct{ Value WindowSize }
+ // __ClientShellPacketReflect describes the ClientShellPacket union type.
+ __ClientShellPacketReflect struct {
+ Name string "v.io/apps/tunnel.ClientShellPacket"
+ Type ClientShellPacket
+ Union struct {
+ Stdin ClientShellPacketStdin
+ EOF ClientShellPacketEOF
+ WinSize ClientShellPacketWinSize
+ }
+ }
+)
+
+func (x ClientShellPacketStdin) Index() int { return 0 }
+func (x ClientShellPacketStdin) Interface() interface{} { return x.Value }
+func (x ClientShellPacketStdin) Name() string { return "Stdin" }
+func (x ClientShellPacketStdin) __VDLReflect(__ClientShellPacketReflect) {}
+
+func (x ClientShellPacketEOF) Index() int { return 1 }
+func (x ClientShellPacketEOF) Interface() interface{} { return x.Value }
+func (x ClientShellPacketEOF) Name() string { return "EOF" }
+func (x ClientShellPacketEOF) __VDLReflect(__ClientShellPacketReflect) {}
+
+func (x ClientShellPacketWinSize) Index() int { return 2 }
+func (x ClientShellPacketWinSize) Interface() interface{} { return x.Value }
+func (x ClientShellPacketWinSize) Name() string { return "WinSize" }
+func (x ClientShellPacketWinSize) __VDLReflect(__ClientShellPacketReflect) {}
+
+type unused struct {
+}
+
+func (unused) __VDLReflect(struct {
+ Name string "v.io/apps/tunnel.unused"
+}) {
+}
+
+type (
+ // ServerShellPacket represents any single field of the ServerShellPacket union type.
+ ServerShellPacket interface {
+ // Index returns the field index.
+ Index() int
+ // Interface returns the field value as an interface.
+ Interface() interface{}
+ // Name returns the field name.
+ Name() string
+ // __VDLReflect describes the ServerShellPacket union type.
+ __VDLReflect(__ServerShellPacketReflect)
+ }
+ // ServerShellPacketStdout represents field Stdout of the ServerShellPacket union type.
+ //
+ // Bytes coming from the shell's stdout.
+ ServerShellPacketStdout struct{ Value []byte }
+ // ServerShellPacketStderr represents field Stderr of the ServerShellPacket union type.
+ //
+ // Bytes coming from the shell's stderr.
+ ServerShellPacketStderr struct{ Value []byte }
+ // __ServerShellPacketReflect describes the ServerShellPacket union type.
+ __ServerShellPacketReflect struct {
+ Name string "v.io/apps/tunnel.ServerShellPacket"
+ Type ServerShellPacket
+ Union struct {
+ Stdout ServerShellPacketStdout
+ Stderr ServerShellPacketStderr
+ }
+ }
+)
+
+func (x ServerShellPacketStdout) Index() int { return 0 }
+func (x ServerShellPacketStdout) Interface() interface{} { return x.Value }
+func (x ServerShellPacketStdout) Name() string { return "Stdout" }
+func (x ServerShellPacketStdout) __VDLReflect(__ServerShellPacketReflect) {}
+
+func (x ServerShellPacketStderr) Index() int { return 1 }
+func (x ServerShellPacketStderr) Interface() interface{} { return x.Value }
+func (x ServerShellPacketStderr) Name() string { return "Stderr" }
+func (x ServerShellPacketStderr) __VDLReflect(__ServerShellPacketReflect) {}
+
+func init() {
+ vdl.Register((*ShellOpts)(nil))
+ vdl.Register((*WindowSize)(nil))
+ vdl.Register((*ClientShellPacket)(nil))
+ vdl.Register((*unused)(nil))
+ vdl.Register((*ServerShellPacket)(nil))
+}
+
+// TunnelClientMethods is the client interface
+// containing Tunnel methods.
+type TunnelClientMethods interface {
+ // The Forward method is used for network forwarding. All the data sent over
+ // the byte stream is forwarded to the requested network address and all the
+ // data received from that network connection is sent back in the reply
+ // stream.
+ Forward(ctx *context.T, network string, address string, opts ...ipc.CallOpt) (TunnelForwardCall, error)
+ // The Shell method is used to either run shell commands remotely, or to open
+ // an interactive shell. The data received over the byte stream is sent to the
+ // shell's stdin, and the data received from the shell's stdout and stderr is
+ // sent back in the reply stream. It returns the exit status of the shell
+ // command.
+ Shell(ctx *context.T, command string, shellOpts ShellOpts, opts ...ipc.CallOpt) (TunnelShellCall, error)
+}
+
+// TunnelClientStub adds universal methods to TunnelClientMethods.
+type TunnelClientStub interface {
+ TunnelClientMethods
+ ipc.UniversalServiceMethods
+}
+
+// TunnelClient returns a client stub for Tunnel.
+func TunnelClient(name string, opts ...ipc.BindOpt) TunnelClientStub {
+ var client ipc.Client
+ for _, opt := range opts {
+ if clientOpt, ok := opt.(ipc.Client); ok {
+ client = clientOpt
+ }
+ }
+ return implTunnelClientStub{name, client}
+}
+
+type implTunnelClientStub struct {
+ name string
+ client ipc.Client
+}
+
+func (c implTunnelClientStub) c(ctx *context.T) ipc.Client {
+ if c.client != nil {
+ return c.client
+ }
+ return v23.GetClient(ctx)
+}
+
+func (c implTunnelClientStub) Forward(ctx *context.T, i0 string, i1 string, opts ...ipc.CallOpt) (ocall TunnelForwardCall, err error) {
+ var call ipc.Call
+ if call, err = c.c(ctx).StartCall(ctx, c.name, "Forward", []interface{}{i0, i1}, opts...); err != nil {
+ return
+ }
+ ocall = &implTunnelForwardCall{Call: call}
+ return
+}
+
+func (c implTunnelClientStub) Shell(ctx *context.T, i0 string, i1 ShellOpts, opts ...ipc.CallOpt) (ocall TunnelShellCall, err error) {
+ var call ipc.Call
+ if call, err = c.c(ctx).StartCall(ctx, c.name, "Shell", []interface{}{i0, i1}, opts...); err != nil {
+ return
+ }
+ ocall = &implTunnelShellCall{Call: call}
+ return
+}
+
+// TunnelForwardClientStream is the client stream for Tunnel.Forward.
+type TunnelForwardClientStream interface {
+ // RecvStream returns the receiver side of the Tunnel.Forward client stream.
+ RecvStream() interface {
+ // Advance stages an item so that it may be retrieved via Value. Returns
+ // true iff there is an item to retrieve. Advance must be called before
+ // Value is called. May block if an item is not available.
+ Advance() bool
+ // Value returns the item that was staged by Advance. May panic if Advance
+ // returned false or was not called. Never blocks.
+ Value() []byte
+ // Err returns any error encountered by Advance. Never blocks.
+ Err() error
+ }
+ // SendStream returns the send side of the Tunnel.Forward client stream.
+ SendStream() interface {
+ // Send places the item onto the output stream. Returns errors
+ // encountered while sending, or if Send is called after Close or
+ // the stream has been canceled. Blocks if there is no buffer
+ // space; will unblock when buffer space is available or after
+ // the stream has been canceled.
+ Send(item []byte) error
+ // Close indicates to the server that no more items will be sent;
+ // server Recv calls will receive io.EOF after all sent items.
+ // This is an optional call - e.g. a client might call Close if it
+ // needs to continue receiving items from the server after it's
+ // done sending. Returns errors encountered while closing, or if
+ // Close is called after the stream has been canceled. Like Send,
+ // blocks if there is no buffer space available.
+ Close() error
+ }
+}
+
+// TunnelForwardCall represents the call returned from Tunnel.Forward.
+type TunnelForwardCall interface {
+ TunnelForwardClientStream
+ // Finish performs the equivalent of SendStream().Close, then blocks until
+ // the server is done, and returns the positional return values for the call.
+ //
+ // Finish returns immediately if the call has been canceled; depending on the
+ // timing the output could either be an error signaling cancelation, or the
+ // valid positional return values from the server.
+ //
+ // Calling Finish is mandatory for releasing stream resources, unless the call
+ // has been canceled or any of the other methods return an error. Finish should
+ // be called at most once.
+ Finish() error
+}
+
+type implTunnelForwardCall struct {
+ ipc.Call
+ valRecv []byte
+ errRecv error
+}
+
+func (c *implTunnelForwardCall) RecvStream() interface {
+ Advance() bool
+ Value() []byte
+ Err() error
+} {
+ return implTunnelForwardCallRecv{c}
+}
+
+type implTunnelForwardCallRecv struct {
+ c *implTunnelForwardCall
+}
+
+func (c implTunnelForwardCallRecv) Advance() bool {
+ c.c.errRecv = c.c.Recv(&c.c.valRecv)
+ return c.c.errRecv == nil
+}
+func (c implTunnelForwardCallRecv) Value() []byte {
+ return c.c.valRecv
+}
+func (c implTunnelForwardCallRecv) Err() error {
+ if c.c.errRecv == io.EOF {
+ return nil
+ }
+ return c.c.errRecv
+}
+func (c *implTunnelForwardCall) SendStream() interface {
+ Send(item []byte) error
+ Close() error
+} {
+ return implTunnelForwardCallSend{c}
+}
+
+type implTunnelForwardCallSend struct {
+ c *implTunnelForwardCall
+}
+
+func (c implTunnelForwardCallSend) Send(item []byte) error {
+ return c.c.Send(item)
+}
+func (c implTunnelForwardCallSend) Close() error {
+ return c.c.CloseSend()
+}
+func (c *implTunnelForwardCall) Finish() (err error) {
+ err = c.Call.Finish()
+ return
+}
+
+// TunnelShellClientStream is the client stream for Tunnel.Shell.
+type TunnelShellClientStream interface {
+ // RecvStream returns the receiver side of the Tunnel.Shell client stream.
+ RecvStream() interface {
+ // Advance stages an item so that it may be retrieved via Value. Returns
+ // true iff there is an item to retrieve. Advance must be called before
+ // Value is called. May block if an item is not available.
+ Advance() bool
+ // Value returns the item that was staged by Advance. May panic if Advance
+ // returned false or was not called. Never blocks.
+ Value() ServerShellPacket
+ // Err returns any error encountered by Advance. Never blocks.
+ Err() error
+ }
+ // SendStream returns the send side of the Tunnel.Shell client stream.
+ SendStream() interface {
+ // Send places the item onto the output stream. Returns errors
+ // encountered while sending, or if Send is called after Close or
+ // the stream has been canceled. Blocks if there is no buffer
+ // space; will unblock when buffer space is available or after
+ // the stream has been canceled.
+ Send(item ClientShellPacket) error
+ // Close indicates to the server that no more items will be sent;
+ // server Recv calls will receive io.EOF after all sent items.
+ // This is an optional call - e.g. a client might call Close if it
+ // needs to continue receiving items from the server after it's
+ // done sending. Returns errors encountered while closing, or if
+ // Close is called after the stream has been canceled. Like Send,
+ // blocks if there is no buffer space available.
+ Close() error
+ }
+}
+
+// TunnelShellCall represents the call returned from Tunnel.Shell.
+type TunnelShellCall interface {
+ TunnelShellClientStream
+ // Finish performs the equivalent of SendStream().Close, then blocks until
+ // the server is done, and returns the positional return values for the call.
+ //
+ // Finish returns immediately if the call has been canceled; depending on the
+ // timing the output could either be an error signaling cancelation, or the
+ // valid positional return values from the server.
+ //
+ // Calling Finish is mandatory for releasing stream resources, unless the call
+ // has been canceled or any of the other methods return an error. Finish should
+ // be called at most once.
+ Finish() (int32, error)
+}
+
+type implTunnelShellCall struct {
+ ipc.Call
+ valRecv ServerShellPacket
+ errRecv error
+}
+
+func (c *implTunnelShellCall) RecvStream() interface {
+ Advance() bool
+ Value() ServerShellPacket
+ Err() error
+} {
+ return implTunnelShellCallRecv{c}
+}
+
+type implTunnelShellCallRecv struct {
+ c *implTunnelShellCall
+}
+
+func (c implTunnelShellCallRecv) Advance() bool {
+ c.c.errRecv = c.c.Recv(&c.c.valRecv)
+ return c.c.errRecv == nil
+}
+func (c implTunnelShellCallRecv) Value() ServerShellPacket {
+ return c.c.valRecv
+}
+func (c implTunnelShellCallRecv) Err() error {
+ if c.c.errRecv == io.EOF {
+ return nil
+ }
+ return c.c.errRecv
+}
+func (c *implTunnelShellCall) SendStream() interface {
+ Send(item ClientShellPacket) error
+ Close() error
+} {
+ return implTunnelShellCallSend{c}
+}
+
+type implTunnelShellCallSend struct {
+ c *implTunnelShellCall
+}
+
+func (c implTunnelShellCallSend) Send(item ClientShellPacket) error {
+ return c.c.Send(item)
+}
+func (c implTunnelShellCallSend) Close() error {
+ return c.c.CloseSend()
+}
+func (c *implTunnelShellCall) Finish() (o0 int32, err error) {
+ err = c.Call.Finish(&o0)
+ return
+}
+
+// TunnelServerMethods is the interface a server writer
+// implements for Tunnel.
+type TunnelServerMethods interface {
+ // The Forward method is used for network forwarding. All the data sent over
+ // the byte stream is forwarded to the requested network address and all the
+ // data received from that network connection is sent back in the reply
+ // stream.
+ Forward(ctx TunnelForwardContext, network string, address string) error
+ // The Shell method is used to either run shell commands remotely, or to open
+ // an interactive shell. The data received over the byte stream is sent to the
+ // shell's stdin, and the data received from the shell's stdout and stderr is
+ // sent back in the reply stream. It returns the exit status of the shell
+ // command.
+ Shell(ctx TunnelShellContext, command string, shellOpts ShellOpts) (int32, error)
+}
+
+// TunnelServerStubMethods is the server interface containing
+// Tunnel methods, as expected by ipc.Server.
+// The only difference between this interface and TunnelServerMethods
+// is the streaming methods.
+type TunnelServerStubMethods interface {
+ // The Forward method is used for network forwarding. All the data sent over
+ // the byte stream is forwarded to the requested network address and all the
+ // data received from that network connection is sent back in the reply
+ // stream.
+ Forward(ctx *TunnelForwardContextStub, network string, address string) error
+ // The Shell method is used to either run shell commands remotely, or to open
+ // an interactive shell. The data received over the byte stream is sent to the
+ // shell's stdin, and the data received from the shell's stdout and stderr is
+ // sent back in the reply stream. It returns the exit status of the shell
+ // command.
+ Shell(ctx *TunnelShellContextStub, command string, shellOpts ShellOpts) (int32, error)
+}
+
+// TunnelServerStub adds universal methods to TunnelServerStubMethods.
+type TunnelServerStub interface {
+ TunnelServerStubMethods
+ // Describe the Tunnel interfaces.
+ Describe__() []ipc.InterfaceDesc
+}
+
+// TunnelServer returns a server stub for Tunnel.
+// It converts an implementation of TunnelServerMethods into
+// an object that may be used by ipc.Server.
+func TunnelServer(impl TunnelServerMethods) TunnelServerStub {
+ stub := implTunnelServerStub{
+ impl: impl,
+ }
+ // Initialize GlobState; always check the stub itself first, to handle the
+ // case where the user has the Glob method defined in their VDL source.
+ if gs := ipc.NewGlobState(stub); gs != nil {
+ stub.gs = gs
+ } else if gs := ipc.NewGlobState(impl); gs != nil {
+ stub.gs = gs
+ }
+ return stub
+}
+
+type implTunnelServerStub struct {
+ impl TunnelServerMethods
+ gs *ipc.GlobState
+}
+
+func (s implTunnelServerStub) Forward(ctx *TunnelForwardContextStub, i0 string, i1 string) error {
+ return s.impl.Forward(ctx, i0, i1)
+}
+
+func (s implTunnelServerStub) Shell(ctx *TunnelShellContextStub, i0 string, i1 ShellOpts) (int32, error) {
+ return s.impl.Shell(ctx, i0, i1)
+}
+
+func (s implTunnelServerStub) Globber() *ipc.GlobState {
+ return s.gs
+}
+
+func (s implTunnelServerStub) Describe__() []ipc.InterfaceDesc {
+ return []ipc.InterfaceDesc{TunnelDesc}
+}
+
+// TunnelDesc describes the Tunnel interface.
+var TunnelDesc ipc.InterfaceDesc = descTunnel
+
+// descTunnel hides the desc to keep godoc clean.
+var descTunnel = ipc.InterfaceDesc{
+ Name: "Tunnel",
+ PkgPath: "v.io/apps/tunnel",
+ Methods: []ipc.MethodDesc{
+ {
+ Name: "Forward",
+ Doc: "// The Forward method is used for network forwarding. All the data sent over\n// the byte stream is forwarded to the requested network address and all the\n// data received from that network connection is sent back in the reply\n// stream.",
+ InArgs: []ipc.ArgDesc{
+ {"network", ``}, // string
+ {"address", ``}, // string
+ },
+ Tags: []*vdl.Value{vdl.ValueOf(access.Tag("Admin"))},
+ },
+ {
+ Name: "Shell",
+ Doc: "// The Shell method is used to either run shell commands remotely, or to open\n// an interactive shell. The data received over the byte stream is sent to the\n// shell's stdin, and the data received from the shell's stdout and stderr is\n// sent back in the reply stream. It returns the exit status of the shell\n// command.",
+ InArgs: []ipc.ArgDesc{
+ {"command", ``}, // string
+ {"shellOpts", ``}, // ShellOpts
+ },
+ OutArgs: []ipc.ArgDesc{
+ {"", ``}, // int32
+ },
+ Tags: []*vdl.Value{vdl.ValueOf(access.Tag("Admin"))},
+ },
+ },
+}
+
+// TunnelForwardServerStream is the server stream for Tunnel.Forward.
+type TunnelForwardServerStream interface {
+ // RecvStream returns the receiver side of the Tunnel.Forward server stream.
+ RecvStream() interface {
+ // Advance stages an item so that it may be retrieved via Value. Returns
+ // true iff there is an item to retrieve. Advance must be called before
+ // Value is called. May block if an item is not available.
+ Advance() bool
+ // Value returns the item that was staged by Advance. May panic if Advance
+ // returned false or was not called. Never blocks.
+ Value() []byte
+ // Err returns any error encountered by Advance. Never blocks.
+ Err() error
+ }
+ // SendStream returns the send side of the Tunnel.Forward server stream.
+ SendStream() interface {
+ // Send places the item onto the output stream. Returns errors encountered
+ // while sending. Blocks if there is no buffer space; will unblock when
+ // buffer space is available.
+ Send(item []byte) error
+ }
+}
+
+// TunnelForwardContext represents the context passed to Tunnel.Forward.
+type TunnelForwardContext interface {
+ ipc.ServerContext
+ TunnelForwardServerStream
+}
+
+// TunnelForwardContextStub is a wrapper that converts ipc.StreamServerCall into
+// a typesafe stub that implements TunnelForwardContext.
+type TunnelForwardContextStub struct {
+ ipc.StreamServerCall
+ valRecv []byte
+ errRecv error
+}
+
+// Init initializes TunnelForwardContextStub from ipc.StreamServerCall.
+func (s *TunnelForwardContextStub) Init(call ipc.StreamServerCall) {
+ s.StreamServerCall = call
+}
+
+// RecvStream returns the receiver side of the Tunnel.Forward server stream.
+func (s *TunnelForwardContextStub) RecvStream() interface {
+ Advance() bool
+ Value() []byte
+ Err() error
+} {
+ return implTunnelForwardContextRecv{s}
+}
+
+type implTunnelForwardContextRecv struct {
+ s *TunnelForwardContextStub
+}
+
+func (s implTunnelForwardContextRecv) Advance() bool {
+ s.s.errRecv = s.s.Recv(&s.s.valRecv)
+ return s.s.errRecv == nil
+}
+func (s implTunnelForwardContextRecv) Value() []byte {
+ return s.s.valRecv
+}
+func (s implTunnelForwardContextRecv) Err() error {
+ if s.s.errRecv == io.EOF {
+ return nil
+ }
+ return s.s.errRecv
+}
+
+// SendStream returns the send side of the Tunnel.Forward server stream.
+func (s *TunnelForwardContextStub) SendStream() interface {
+ Send(item []byte) error
+} {
+ return implTunnelForwardContextSend{s}
+}
+
+type implTunnelForwardContextSend struct {
+ s *TunnelForwardContextStub
+}
+
+func (s implTunnelForwardContextSend) Send(item []byte) error {
+ return s.s.Send(item)
+}
+
+// TunnelShellServerStream is the server stream for Tunnel.Shell.
+type TunnelShellServerStream interface {
+ // RecvStream returns the receiver side of the Tunnel.Shell server stream.
+ RecvStream() interface {
+ // Advance stages an item so that it may be retrieved via Value. Returns
+ // true iff there is an item to retrieve. Advance must be called before
+ // Value is called. May block if an item is not available.
+ Advance() bool
+ // Value returns the item that was staged by Advance. May panic if Advance
+ // returned false or was not called. Never blocks.
+ Value() ClientShellPacket
+ // Err returns any error encountered by Advance. Never blocks.
+ Err() error
+ }
+ // SendStream returns the send side of the Tunnel.Shell server stream.
+ SendStream() interface {
+ // Send places the item onto the output stream. Returns errors encountered
+ // while sending. Blocks if there is no buffer space; will unblock when
+ // buffer space is available.
+ Send(item ServerShellPacket) error
+ }
+}
+
+// TunnelShellContext represents the context passed to Tunnel.Shell.
+type TunnelShellContext interface {
+ ipc.ServerContext
+ TunnelShellServerStream
+}
+
+// TunnelShellContextStub is a wrapper that converts ipc.StreamServerCall into
+// a typesafe stub that implements TunnelShellContext.
+type TunnelShellContextStub struct {
+ ipc.StreamServerCall
+ valRecv ClientShellPacket
+ errRecv error
+}
+
+// Init initializes TunnelShellContextStub from ipc.StreamServerCall.
+func (s *TunnelShellContextStub) Init(call ipc.StreamServerCall) {
+ s.StreamServerCall = call
+}
+
+// RecvStream returns the receiver side of the Tunnel.Shell server stream.
+func (s *TunnelShellContextStub) RecvStream() interface {
+ Advance() bool
+ Value() ClientShellPacket
+ Err() error
+} {
+ return implTunnelShellContextRecv{s}
+}
+
+type implTunnelShellContextRecv struct {
+ s *TunnelShellContextStub
+}
+
+func (s implTunnelShellContextRecv) Advance() bool {
+ s.s.errRecv = s.s.Recv(&s.s.valRecv)
+ return s.s.errRecv == nil
+}
+func (s implTunnelShellContextRecv) Value() ClientShellPacket {
+ return s.s.valRecv
+}
+func (s implTunnelShellContextRecv) Err() error {
+ if s.s.errRecv == io.EOF {
+ return nil
+ }
+ return s.s.errRecv
+}
+
+// SendStream returns the send side of the Tunnel.Shell server stream.
+func (s *TunnelShellContextStub) SendStream() interface {
+ Send(item ServerShellPacket) error
+} {
+ return implTunnelShellContextSend{s}
+}
+
+type implTunnelShellContextSend struct {
+ s *TunnelShellContextStub
+}
+
+func (s implTunnelShellContextSend) Send(item ServerShellPacket) error {
+ return s.s.Send(item)
+}
diff --git a/tunnel/tunneld/impl.go b/tunnel/tunneld/impl.go
new file mode 100644
index 0000000..96dda4e
--- /dev/null
+++ b/tunnel/tunneld/impl.go
@@ -0,0 +1,169 @@
+package main
+
+import (
+ "github.com/kr/pty"
+
+ "errors"
+ "fmt"
+ "io"
+ "io/ioutil"
+ "net"
+ "os"
+ "os/exec"
+ "syscall"
+
+ "v.io/apps/tunnel"
+ "v.io/apps/tunnel/tunnelutil"
+ "v.io/x/lib/vlog"
+)
+
+// T implements tunnel.TunnelServerMethods
+type T struct {
+}
+
+const nonShellErrorCode = 255
+
+func (t *T) Forward(ctx tunnel.TunnelForwardContext, network, address string) error {
+ conn, err := net.Dial(network, address)
+ if err != nil {
+ return err
+ }
+ b, _ := ctx.RemoteBlessings().ForContext(ctx)
+ name := fmt.Sprintf("RemoteBlessings:%v LocalAddr:%v RemoteAddr:%v", b, conn.LocalAddr(), conn.RemoteAddr())
+ vlog.Infof("TUNNEL START: %v", name)
+ err = tunnelutil.Forward(conn, ctx.SendStream(), ctx.RecvStream())
+ vlog.Infof("TUNNEL END : %v (%v)", name, err)
+ return err
+}
+
+func (t *T) Shell(ctx tunnel.TunnelShellContext, command string, shellOpts tunnel.ShellOpts) (int32, error) {
+ b, _ := ctx.RemoteBlessings().ForContext(ctx)
+ vlog.Infof("SHELL START for %v: %q", b, command)
+ shell, err := findShell()
+ if err != nil {
+ return nonShellErrorCode, err
+ }
+ var c *exec.Cmd
+ // An empty command means that we need an interactive shell.
+ if len(command) == 0 {
+ c = exec.Command(shell, "-i")
+ sendMotd(ctx)
+ } else {
+ c = exec.Command(shell, "-c", command)
+ }
+
+ c.Env = []string{
+ fmt.Sprintf("HOME=%s", os.Getenv("HOME")),
+ fmt.Sprintf("PATH=%s", os.Getenv("PATH")),
+ }
+ c.Env = append(c.Env, shellOpts.Environment...)
+ vlog.Infof("Shell environment: %v", c.Env)
+
+ c.Dir = os.Getenv("HOME")
+ vlog.Infof("Shell CWD: %v", c.Dir)
+
+ var (
+ stdin io.WriteCloser // We write to stdin.
+ stdout, stderr io.ReadCloser // We read from stdout and stderr.
+ ptyFd uintptr // File descriptor for pty.
+ )
+
+ if shellOpts.UsePty {
+ f, err := pty.Start(c)
+ if err != nil {
+ return nonShellErrorCode, err
+ }
+ stdin = f
+ stdout = f
+ stderr = nil
+ ptyFd = f.Fd()
+
+ defer f.Close()
+
+ setWindowSize(ptyFd, shellOpts.WinSize.Rows, shellOpts.WinSize.Cols)
+ } else {
+ var err error
+ if stdin, err = c.StdinPipe(); err != nil {
+ return nonShellErrorCode, err
+ }
+ defer stdin.Close()
+
+ if stdout, err = c.StdoutPipe(); err != nil {
+ return nonShellErrorCode, err
+ }
+ defer stdout.Close()
+
+ if stderr, err = c.StderrPipe(); err != nil {
+ return nonShellErrorCode, err
+ }
+ defer stderr.Close()
+
+ if err = c.Start(); err != nil {
+ vlog.Infof("Cmd.Start failed: %v", err)
+ return nonShellErrorCode, err
+ }
+ }
+
+ defer c.Process.Kill()
+
+ select {
+ case runErr := <-runIOManager(stdin, stdout, stderr, ptyFd, ctx):
+ b, _ := ctx.RemoteBlessings().ForContext(ctx)
+ vlog.Infof("SHELL END for %v: %q (%v)", b, command, runErr)
+ return harvestExitcode(c.Process, runErr)
+ case <-ctx.Context().Done():
+ return nonShellErrorCode, fmt.Errorf("remote end exited")
+ }
+}
+
+// harvestExitcode returns the (exitcode, error) pair to be returned for the Shell RPC
+// based on the status of the process and the error returned from runIOManager
+func harvestExitcode(process *os.Process, ioerr error) (int32, error) {
+ // Check the exit status.
+ var status syscall.WaitStatus
+ if _, err := syscall.Wait4(process.Pid, &status, syscall.WNOHANG, nil); err != nil {
+ return nonShellErrorCode, err
+ }
+ if status.Signaled() {
+ return int32(status), fmt.Errorf("process killed by signal %u (%v)", status.Signal(), status.Signal())
+ }
+ if status.Exited() {
+ if status.ExitStatus() == 0 {
+ return 0, nil
+ }
+ return int32(status.ExitStatus()), fmt.Errorf("process exited with exit status %d", status.ExitStatus())
+ }
+ // The process has not exited. Use the error from ForwardStdIO.
+ return nonShellErrorCode, ioerr
+}
+
+// findShell returns the path to the first usable shell binary.
+func findShell() (string, error) {
+ shells := []string{"/bin/bash", "/bin/sh"}
+ for _, s := range shells {
+ if _, err := os.Stat(s); err == nil {
+ return s, nil
+ }
+ }
+ return "", errors.New("could not find any shell binary")
+}
+
+// sendMotd sends the content of the MOTD file to the stream, if it exists.
+func sendMotd(s tunnel.TunnelShellServerStream) {
+ data, err := ioutil.ReadFile("/etc/motd")
+ if err != nil {
+ // No MOTD. That's OK.
+ return
+ }
+ packet := tunnel.ServerShellPacketStdout{[]byte(data)}
+ if err = s.SendStream().Send(packet); err != nil {
+ vlog.Infof("Send failed: %v", err)
+ }
+}
+
+func setWindowSize(fd uintptr, row, col uint16) {
+ ws := tunnelutil.Winsize{Row: row, Col: col}
+ if err := tunnelutil.SetWindowSize(fd, ws); err != nil {
+ vlog.Infof("Failed to set window size: %v", err)
+ }
+}
diff --git a/tunnel/tunneld/iomanager.go b/tunnel/tunneld/iomanager.go
new file mode 100644
index 0000000..9f12ce2
--- /dev/null
+++ b/tunnel/tunneld/iomanager.go
@@ -0,0 +1,187 @@
+package main
+
+import (
+ "fmt"
+ "io"
+ "sync"
+
+ "v.io/apps/tunnel"
+ "v.io/x/lib/vlog"
+)
+
+func runIOManager(stdin io.WriteCloser, stdout, stderr io.Reader, ptyFd uintptr, stream tunnel.TunnelShellServerStream) <-chan error {
+ m := ioManager{stdin: stdin, stdout: stdout, stderr: stderr, ptyFd: ptyFd, stream: stream}
+ c := make(chan error, 1) // buffered channel so that the goroutine spawned below is not leaked if the channel is not read from.
+ go func() { c <- m.run() }()
+ return c
+}
+
+// ioManager manages the forwarding of all the data between the shell and the
+// stream.
+type ioManager struct {
+ stdin io.WriteCloser
+ stdout, stderr io.Reader
+ ptyFd uintptr
+ stream tunnel.TunnelShellServerStream
+
+ // streamError receives errors coming from stream operations.
+ streamError chan error
+ // stdioError receives errors coming from stdio operations.
+ stdioError chan error
+}
+
+func (m *ioManager) run() error {
+ m.streamError = make(chan error, 1)
+ m.stdioError = make(chan error, 1)
+
+ var pendingShellOutput sync.WaitGroup
+ pendingShellOutput.Add(1)
+ var pendingStreamInput sync.WaitGroup
+ pendingStreamInput.Add(1)
+
+ // Forward data between the shell's stdio and the stream.
+ go func() {
+ defer pendingShellOutput.Done()
+ // outchan is used to serialize the output to the stream.
+ // chan2stream() receives data sent by stdout2outchan() and
+ // stderr2outchan() and sends it to the stream.
+ outchan := make(chan tunnel.ServerShellPacket)
+ var wgStream sync.WaitGroup
+ wgStream.Add(1)
+ go m.chan2stream(outchan, &wgStream)
+ var wgStdio sync.WaitGroup
+ wgStdio.Add(1)
+ go m.stdout2outchan(outchan, &wgStdio)
+ if m.stderr != nil {
+ wgStdio.Add(1)
+ go m.stderr2outchan(outchan, &wgStdio)
+ }
+ // When both stdout2outchan and stderr2outchan are done, close
+ // outchan to signal chan2stream to exit.
+ wgStdio.Wait()
+ close(outchan)
+ wgStream.Wait()
+ }()
+ go m.stream2stdin(&pendingStreamInput)
+
+ // Block until something reports an error.
+ //
+ // If there is any stream error, we assume that both ends of the stream
+ // have an error, e.g. if stream.Reader.Advance fails then
+ // stream.Sender.Send will fail. We process any remaining input from
+ // the stream and then return.
+ //
+ // If there is any stdio error, we assume all 3 io channels will fail
+ // (if stdout.Read fails then stdin.Write and stderr.Read will also
+ // fail). We process is remaining output from the shell and then
+ // return.
+ select {
+ case err := <-m.streamError:
+ // Process remaining input from the stream before exiting.
+ vlog.VI(2).Infof("run stream error: %v", err)
+ pendingStreamInput.Wait()
+ return err
+ case err := <-m.stdioError:
+ // Process remaining output from the shell before exiting.
+ vlog.VI(2).Infof("run stdio error: %v", err)
+ pendingShellOutput.Wait()
+ return err
+ }
+}
+
+func (m *ioManager) sendStreamError(err error) {
+ select {
+ case m.streamError <- err:
+ default:
+ }
+}
+
+func (m *ioManager) sendStdioError(err error) {
+ select {
+ case m.stdioError <- err:
+ default:
+ }
+}
+
+// chan2stream receives ServerShellPacket from outchan and sends it to stream.
+func (m *ioManager) chan2stream(outchan <-chan tunnel.ServerShellPacket, wg *sync.WaitGroup) {
+ defer wg.Done()
+ sender := m.stream.SendStream()
+ for packet := range outchan {
+ vlog.VI(3).Infof("chan2stream packet: %+v", packet)
+ if err := sender.Send(packet); err != nil {
+ vlog.VI(2).Infof("chan2stream: %v", err)
+ m.sendStreamError(err)
+ }
+ }
+}
+
+// stdout2stream reads data from the shell's stdout and sends it to the outchan.
+func (m *ioManager) stdout2outchan(outchan chan<- tunnel.ServerShellPacket, wg *sync.WaitGroup) {
+ defer wg.Done()
+ for {
+ buf := make([]byte, 2048)
+ n, err := m.stdout.Read(buf[:])
+ if err != nil {
+ vlog.VI(2).Infof("stdout2outchan: %v", err)
+ m.sendStdioError(err)
+ return
+ }
+ outchan <- tunnel.ServerShellPacketStdout{buf[:n]}
+ }
+}
+
+// stderr2stream reads data from the shell's stderr and sends it to the outchan.
+func (m *ioManager) stderr2outchan(outchan chan<- tunnel.ServerShellPacket, wg *sync.WaitGroup) {
+ defer wg.Done()
+ for {
+ buf := make([]byte, 2048)
+ n, err := m.stderr.Read(buf[:])
+ if err != nil {
+ vlog.VI(2).Infof("stderr2outchan: %v", err)
+ m.sendStdioError(err)
+ return
+ }
+ outchan <- tunnel.ServerShellPacketStderr{buf[:n]}
+ }
+}
+
+// stream2stdin reads data from the stream and sends it to the shell's stdin.
+func (m *ioManager) stream2stdin(wg *sync.WaitGroup) {
+ defer wg.Done()
+ rStream := m.stream.RecvStream()
+ for rStream.Advance() {
+ packet := rStream.Value()
+ vlog.VI(3).Infof("stream2stdin packet: %+v", packet)
+ switch v := packet.(type) {
+ case tunnel.ClientShellPacketStdin:
+ if n, err := m.stdin.Write(v.Value); n != len(v.Value) || err != nil {
+ m.sendStdioError(fmt.Errorf("stdin.Write returned (%d, %v) want (%d, nil)", n, err, len(v.Value)))
+ return
+ }
+ case tunnel.ClientShellPacketEOF:
+ if err := m.stdin.Close(); err != nil {
+ m.sendStdioError(fmt.Errorf("stdin.Close: %v", err))
+ return
+ }
+ case tunnel.ClientShellPacketWinSize:
+ size := v.Value
+ if size.Rows > 0 && size.Cols > 0 && m.ptyFd != 0 {
+ setWindowSize(m.ptyFd, size.Rows, size.Cols)
+ }
+ default:
+ vlog.Infof("unexpected message type: %T", packet)
+ }
+ }
+
+ err := rStream.Err()
+ if err == nil {
+ err = io.EOF
+ }
+
+ vlog.VI(2).Infof("stream2stdin: %v", err)
+ m.sendStreamError(err)
+ if err := m.stdin.Close(); err != nil {
+ m.sendStdioError(fmt.Errorf("stdin.Close: %v", err))
+ }
+}
diff --git a/tunnel/tunneld/main.go b/tunnel/tunneld/main.go
new file mode 100644
index 0000000..4e5b199
--- /dev/null
+++ b/tunnel/tunneld/main.go
@@ -0,0 +1,84 @@
+// Command tunneld is an implementation of the tunnel service.
+package main
+
+import (
+ "errors"
+ "fmt"
+ "net"
+ "os"
+ "strings"
+
+ "v.io/v23"
+ "v.io/x/lib/vlog"
+
+ "v.io/core/veyron/lib/signals"
+ _ "v.io/core/veyron/profiles/roaming"
+ sflag "v.io/core/veyron/security/flag"
+
+ "v.io/apps/tunnel"
+)
+
+// firstHardwareAddrInUse returns the hwaddr of the first network interface
+// that is up, excluding loopback.
+func firstHardwareAddrInUse() (string, error) {
+ interfaces, err := net.Interfaces()
+ if err != nil {
+ return "", err
+ }
+ for _, i := range interfaces {
+ if !strings.HasPrefix(i.Name, "lo") && i.Flags&net.FlagUp != 0 {
+ name := i.HardwareAddr.String()
+ if len(name) == 0 {
+ continue
+ }
+ vlog.Infof("Using %q (from %v)", name, i.Name)
+ return name, nil
+ }
+ }
+ return "", errors.New("No usable network interfaces")
+}
+
+func main() {
+ ctx, shutdown := v23.Init()
+ defer shutdown()
+
+ auth := sflag.NewAuthorizerOrDie()
+ server, err := v23.NewServer(ctx)
+ if err != nil {
+ vlog.Fatalf("NewServer failed: %v", err)
+ }
+ defer server.Stop()
+
+ listenSpec := v23.GetListenSpec(ctx)
+ eps, err := server.Listen(listenSpec)
+ if err != nil {
+ vlog.Fatalf("Listen(%v) failed: %v", listenSpec, err)
+ }
+ vlog.Infof("Listening on: %v", eps)
+ hwaddr, err := firstHardwareAddrInUse()
+ if err != nil {
+ vlog.Fatalf("Couldn't find a good hw address: %v", err)
+ }
+ hostname, err := os.Hostname()
+ if err != nil {
+ vlog.Fatalf("os.Hostname failed: %v", err)
+ }
+ names := []string{
+ fmt.Sprintf("tunnel/hostname/%s", hostname),
+ fmt.Sprintf("tunnel/hwaddr/%s", hwaddr),
+ }
+ published := false
+ if err := server.Serve(names[0], tunnel.TunnelServer(&T{}), auth); err != nil {
+ vlog.Infof("Serve(%v) failed: %v", names[0], err)
+ }
+ published = true
+ for _, n := range names[1:] {
+ server.AddName(n)
+ }
+ if !published {
+ vlog.Fatalf("Failed to publish with any of %v", names)
+ }
+ vlog.Infof("Published as %v", names)
+
+ <-signals.ShutdownOnSignals(ctx)
+}
diff --git a/tunnel/tunneld/tunneld_v23_test.go b/tunnel/tunneld/tunneld_v23_test.go
new file mode 100644
index 0000000..2b8638a
--- /dev/null
+++ b/tunnel/tunneld/tunneld_v23_test.go
@@ -0,0 +1,91 @@
+package main_test
+
+//go:generate v23 test generate .
+
+import (
+ "bytes"
+ "fmt"
+ "io/ioutil"
+ "os"
+ "path/filepath"
+ "regexp"
+ "strings"
+
+ "v.io/core/veyron/lib/testutil"
+ "v.io/core/veyron/lib/testutil/v23tests"
+)
+
+func V23TestTunneld(t *v23tests.T) {
+ v23tests.RunRootMT(t, "--veyron.tcp.address=127.0.0.1:0")
+
+ tunneldBin := t.BuildGoPkg("v.io/apps/tunnel/tunneld")
+ vsh := t.BuildGoPkg("v.io/apps/tunnel/vsh")
+ mounttableBin := t.BuildGoPkg("v.io/core/veyron/tools/mounttable")
+
+ port, err := testutil.FindUnusedPort()
+ if err != nil {
+ t.Fatalf("FindUnusedPort failed: %v", err)
+ }
+
+ tunnelAddress := fmt.Sprintf("127.0.0.1:%d", port)
+ tunnelEndpoint := "/" + tunnelAddress
+
+ // Start tunneld with a known endpoint.
+ tunneldBin.Start("--veyron.tcp.address=" + tunnelAddress)
+
+ // Run remote command with the endpoint.
+ if want, got := "HELLO ENDPOINT\n", vsh.Start(tunnelEndpoint, "echo", "HELLO", "ENDPOINT").Output(); want != got {
+ t.Fatalf("unexpected output, got %s, want %s", got, want)
+ }
+
+ // Run remote command with the object name.
+ hostname, err := os.Hostname()
+ if err != nil {
+ t.Fatalf("Hostname() failed: %v", err)
+ }
+
+ if want, got := "HELLO NAME\n", vsh.Start("tunnel/hostname/"+hostname, "echo", "HELLO", "NAME").Output(); want != got {
+ t.Fatalf("unexpected output, got %s, want %s", got, want)
+ }
+
+ // Send input to remote command.
+ want := "HELLO SERVER"
+ if got := vsh.WithStdin(bytes.NewBufferString(want)).Start(tunnelEndpoint, "cat").Output(); want != got {
+ t.Fatalf("unexpected output, got %s, want %s", got, want)
+ }
+
+ // And again with a file redirection this time.
+ outDir := t.NewTempDir()
+ outPath := filepath.Join(outDir, "hello.txt")
+
+ // TODO(sjr): instead of using Output() here, we'd really rather do
+ // WaitOrDie(os.Stdout, os.Stderr). There is currently a race caused by
+ // WithStdin that makes this flaky.
+ vsh.WithStdin(bytes.NewBufferString(want)).Start(tunnelEndpoint, "cat > "+outPath).Output()
+ if got, err := ioutil.ReadFile(outPath); err != nil || string(got) != want {
+ if err != nil {
+ t.Fatalf("ReadFile(%v) failed: %v", outPath, err)
+ } else {
+ t.Fatalf("unexpected output, got %s, want %s", got, want)
+ }
+ }
+
+ // Verify that all published names are there.
+ root, _ := t.GetVar("NAMESPACE_ROOT")
+ inv := mounttableBin.Start("glob", root, "tunnel/*/*")
+
+ // Expect two entries: one for the tunnel hostname and one for its hwaddr.
+ matches := inv.ExpectSetEventuallyRE(
+ "tunnel/hostname/"+regexp.QuoteMeta(hostname)+" (.*) \\(TTL .*\\)",
+ "tunnel/hwaddr/.* (.*) \\(TTL .*\\)")
+
+ // The full endpoint should contain the address we initially specified for the tunnel.
+ if want = "@" + tunnelAddress + "@"; !strings.Contains(matches[0][1], want) {
+ t.Fatalf("expected tunnel endpoint %s to contain %s, but it did not", matches[0][1], want)
+ }
+
+ // The hwaddr endpoint should be the same as the hostname endpoint.
+ if matches[0][1] != matches[1][1] {
+ t.Fatalf("expected hwaddr and hostname tunnel endpoints to match, but they did not (%s != %s)", matches[0][1], matches[1][1])
+ }
+}
diff --git a/tunnel/tunneld/v23_test.go b/tunnel/tunneld/v23_test.go
new file mode 100644
index 0000000..d45c99d
--- /dev/null
+++ b/tunnel/tunneld/v23_test.go
@@ -0,0 +1,25 @@
+// Copyright 2015 The Vanadium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+// This file was auto-generated via go generate.
+// DO NOT UPDATE MANUALLY
+package main_test
+
+import "testing"
+import "os"
+
+import "v.io/core/veyron/lib/testutil"
+import "v.io/core/veyron/lib/testutil/v23tests"
+
+func TestMain(m *testing.M) {
+ testutil.Init()
+ cleanup := v23tests.UseSharedBinDir()
+ r := m.Run()
+ cleanup()
+ os.Exit(r)
+}
+
+func TestV23Tunneld(t *testing.T) {
+ v23tests.RunTest(t, V23TestTunneld)
+}
diff --git a/tunnel/tunnelutil/forward.go b/tunnel/tunnelutil/forward.go
new file mode 100644
index 0000000..bc046d3
--- /dev/null
+++ b/tunnel/tunnelutil/forward.go
@@ -0,0 +1,64 @@
+package tunnelutil
+
+import (
+ "fmt"
+ "io"
+ "net"
+)
+
+type sender interface {
+ Send([]uint8) error
+}
+type receiver interface {
+ Advance() bool
+
+ Value() []uint8
+
+ Err() error
+}
+
+// Forward forwards data read from net.Conn to a TunnelForwardClientStream or a
+// TunnelForwardServerStream.
+func Forward(conn net.Conn, s sender, r receiver) error {
+ defer conn.Close()
+ // Both conn2stream and stream2conn will write to the channel exactly
+ // once.
+ // Forward reads from the channel exactly once.
+ // A buffered channel is used to prevent the other write to the channel
+ // from blocking.
+ done := make(chan error, 1)
+ go conn2stream(conn, s, done)
+ go stream2conn(r, conn, done)
+ return <-done
+}
+
+func conn2stream(r io.Reader, s sender, done chan error) {
+ var buf [2048]byte
+ for {
+ n, err := r.Read(buf[:])
+ if err == io.EOF {
+ done <- nil
+ return
+ }
+ if err != nil {
+ done <- err
+ return
+ }
+ if err := s.Send(buf[:n]); err != nil {
+ done <- err
+ return
+ }
+ }
+}
+
+func stream2conn(r receiver, w io.Writer, done chan error) {
+ for r.Advance() {
+ buf := r.Value()
+
+ if n, err := w.Write(buf); n != len(buf) || err != nil {
+ done <- fmt.Errorf("conn.Write returned (%d, %v) want (%d, nil)", n, err, len(buf))
+ return
+ }
+ }
+ done <- r.Err()
+}
diff --git a/tunnel/tunnelutil/terminal.go b/tunnel/tunnelutil/terminal.go
new file mode 100644
index 0000000..92f178f
--- /dev/null
+++ b/tunnel/tunnelutil/terminal.go
@@ -0,0 +1,95 @@
+// Package tunnelutil contains a set of common types and functions
+// used by both tunnel service clients and servers.
+package tunnelutil
+
+import (
+ "errors"
+ "os/exec"
+ "strings"
+ "syscall"
+ "unsafe"
+
+ "v.io/x/lib/vlog"
+)
+
+// Used with ioctl TIOCGWINSZ and TIOCSWINSZ.
+type Winsize struct {
+ Row uint16
+ Col uint16
+ Xpixel uint16
+ Ypixel uint16
+}
+
+// SetWindowSize sets the terminal's window size.
+func SetWindowSize(fd uintptr, ws Winsize) error {
+ vlog.Infof("Setting window size: %v", ws)
+ ret, _, _ := syscall.Syscall(
+ syscall.SYS_IOCTL,
+ fd,
+ uintptr(syscall.TIOCSWINSZ),
+ uintptr(unsafe.Pointer(&ws)))
+ if int(ret) == -1 {
+ return errors.New("ioctl(TIOCSWINSZ) failed")
+ }
+ return nil
+}
+
+// GetWindowSize gets the terminal's window size.
+func GetWindowSize() (*Winsize, error) {
+ ws := &Winsize{}
+ ret, _, _ := syscall.Syscall(
+ syscall.SYS_IOCTL,
+ uintptr(syscall.Stdin),
+ uintptr(syscall.TIOCGWINSZ),
+ uintptr(unsafe.Pointer(ws)))
+ if int(ret) == -1 {
+ return nil, errors.New("ioctl(TIOCGWINSZ) failed")
+ }
+ return ws, nil
+}
+
+func EnterRawTerminalMode() string {
+ var savedBytes []byte
+ var err error
+ if savedBytes, err = exec.Command("stty", "-F", "/dev/tty", "-g").Output(); err != nil {
+ vlog.Infof("Failed to save terminal settings: %q (%v)", savedBytes, err)
+ }
+ saved := strings.TrimSpace(string(savedBytes))
+
+ args := []string{
+ "-F", "/dev/tty",
+ // Don't buffer stdin. Read characters as they are typed.
+ "-icanon", "min", "1", "time", "0",
+ // Turn off local echo of input characters.
+ "-echo", "-echoe", "-echok", "-echonl",
+ // Disable interrupt, quit, and suspend special characters.
+ "-isig",
+ // Ignore characters with parity errors.
+ "ignpar",
+ // Disable translate newline to carriage return.
+ "-inlcr",
+ // Disable ignore carriage return.
+ "-igncr",
+ // Disable translate carriage return to newline.
+ "-icrnl",
+ // Disable flow control.
+ "-ixon", "-ixany", "-ixoff",
+ // Disable non-POSIX special characters.
+ "-iexten",
+ }
+ if out, err := exec.Command("stty", args...).CombinedOutput(); err != nil {
+ vlog.Infof("stty failed (%v) (%q)", err, out)
+ }
+
+ return string(saved)
+}
+
+func RestoreTerminalSettings(saved string) {
+ args := []string{
+ "-F", "/dev/tty",
+ saved,
+ }
+ if out, err := exec.Command("stty", args...).CombinedOutput(); err != nil {
+ vlog.Infof("stty failed (%v) (%q)", err, out)
+ }
+}
diff --git a/tunnel/vsh/iomanager.go b/tunnel/vsh/iomanager.go
new file mode 100644
index 0000000..9effbd9
--- /dev/null
+++ b/tunnel/vsh/iomanager.go
@@ -0,0 +1,182 @@
+package main
+
+import (
+ "fmt"
+ "io"
+ "os"
+ "os/signal"
+ "sync"
+ "syscall"
+
+ "v.io/apps/tunnel"
+ "v.io/apps/tunnel/tunnelutil"
+ "v.io/x/lib/vlog"
+)
+
+func runIOManager(stdin io.Reader, stdout, stderr io.Writer, stream tunnel.TunnelShellCall) error {
+ m := ioManager{stdin: stdin, stdout: stdout, stderr: stderr, stream: stream}
+ return m.run()
+}
+
+// ioManager manages the forwarding of all the data between the shell and the
+// stream.
+type ioManager struct {
+ stdin io.Reader
+ stdout, stderr io.Writer
+ stream tunnel.TunnelShellCall
+
+ // streamError receives errors coming from stream operations.
+ streamError chan error
+ // stdioError receives errors coming from stdio operations.
+ stdioError chan error
+}
+
+func (m *ioManager) run() error {
+ m.streamError = make(chan error, 1)
+ m.stdioError = make(chan error, 1)
+
+ var pendingUserInput sync.WaitGroup
+ pendingUserInput.Add(1)
+ var pendingStreamOutput sync.WaitGroup
+ pendingStreamOutput.Add(1)
+
+ // Forward data between the user and the remote shell.
+ go func() {
+ defer pendingUserInput.Done()
+ // outchan is used to serialize the output to the stream.
+ // chan2stream() receives data sent by handleWindowResize() and
+ // user2outchan() and sends it to the stream.
+ outchan := make(chan tunnel.ClientShellPacket)
+ var wgStream sync.WaitGroup
+ wgStream.Add(1)
+ go m.chan2stream(outchan, &wgStream)
+
+ // When the terminal window is resized, we receive a SIGWINCH. Then we
+ // send the new window size to the server.
+ winch := make(chan os.Signal, 1)
+ signal.Notify(winch, syscall.SIGWINCH)
+
+ var wgUser sync.WaitGroup
+ wgUser.Add(2)
+ go func() {
+ m.user2outchan(outchan, &wgUser)
+ signal.Stop(winch)
+ close(winch)
+ }()
+ go m.handleWindowResize(winch, outchan, &wgUser)
+ // When both user2outchan and handleWindowResize are done,
+ // close outchan to signal chan2stream to exit.
+ wgUser.Wait()
+ close(outchan)
+ wgStream.Wait()
+ }()
+ go m.stream2user(&pendingStreamOutput)
+ // Block until something reports an error.
+ select {
+ case err := <-m.streamError:
+ // When we receive an error from the stream, wait for any
+ // remaining stream output to be sent to the user before
+ // exiting.
+ vlog.VI(2).Infof("run stream error: %v", err)
+ pendingStreamOutput.Wait()
+ return err
+ case err := <-m.stdioError:
+ // When we receive an error from the user, wait for any
+ // remaining input from the user to be sent to the stream
+ // before exiting.
+ vlog.VI(2).Infof("run stdio error: %v", err)
+ pendingUserInput.Wait()
+ return err
+ }
+}
+
+func (m *ioManager) sendStreamError(err error) {
+ select {
+ case m.streamError <- err:
+ default:
+ }
+}
+
+func (m *ioManager) sendStdioError(err error) {
+ select {
+ case m.stdioError <- err:
+ default:
+ }
+}
+
+// chan2stream receives ClientShellPacket from outchan and sends it to stream.
+func (m *ioManager) chan2stream(outchan <-chan tunnel.ClientShellPacket, wg *sync.WaitGroup) {
+ defer wg.Done()
+ sender := m.stream.SendStream()
+ for packet := range outchan {
+ vlog.VI(3).Infof("chan2stream packet: %+v", packet)
+ if err := sender.Send(packet); err != nil {
+ vlog.VI(2).Infof("chan2stream: %v", err)
+ m.sendStreamError(err)
+ }
+ }
+ m.sendStreamError(io.EOF)
+}
+
+func (m *ioManager) handleWindowResize(winch <-chan os.Signal, outchan chan<- tunnel.ClientShellPacket, wg *sync.WaitGroup) {
+ defer wg.Done()
+ for _ = range winch {
+ ws, err := tunnelutil.GetWindowSize()
+ if err != nil {
+ vlog.Infof("GetWindowSize failed: %v", err)
+ continue
+ }
+ outchan <- tunnel.ClientShellPacketWinSize{tunnel.WindowSize{ws.Row, ws.Col}}
+ }
+}
+
+// user2stream reads input from stdin and sends it to the outchan.
+func (m *ioManager) user2outchan(outchan chan<- tunnel.ClientShellPacket, wg *sync.WaitGroup) {
+ defer wg.Done()
+ for {
+ buf := make([]byte, 2048)
+ n, err := m.stdin.Read(buf[:])
+ if err == io.EOF {
+ vlog.VI(2).Infof("user2outchan: EOF, closing stdin")
+ outchan <- tunnel.ClientShellPacketEOF{}
+ return
+ }
+ if err != nil {
+ vlog.VI(2).Infof("user2outchan: %v", err)
+ m.sendStdioError(err)
+ return
+ }
+ outchan <- tunnel.ClientShellPacketStdin{buf[:n]}
+ }
+}
+
+// stream2user reads data from the stream and sends it to either stdout or stderr.
+func (m *ioManager) stream2user(wg *sync.WaitGroup) {
+ defer wg.Done()
+ rStream := m.stream.RecvStream()
+ for rStream.Advance() {
+ packet := rStream.Value()
+ vlog.VI(3).Infof("stream2user packet: %+v", packet)
+
+ switch v := packet.(type) {
+ case tunnel.ServerShellPacketStdout:
+ if n, err := m.stdout.Write(v.Value); n != len(v.Value) || err != nil {
+ m.sendStdioError(fmt.Errorf("stdout.Write returned (%d, %v) want (%d, nil)", n, err, len(v.Value)))
+ return
+ }
+ case tunnel.ServerShellPacketStderr:
+ if n, err := m.stderr.Write(v.Value); n != len(v.Value) || err != nil {
+ m.sendStdioError(fmt.Errorf("stderr.Write returned (%d, %v) want (%d, nil)", n, err, len(v.Value)))
+ return
+ }
+ default:
+ vlog.Infof("unexpected message type: %T", packet)
+ }
+ }
+ err := rStream.Err()
+ if err == nil {
+ err = io.EOF
+ }
+ vlog.VI(2).Infof("stream2user: %v", err)
+ m.sendStreamError(err)
+}
diff --git a/tunnel/vsh/main.go b/tunnel/vsh/main.go
new file mode 100644
index 0000000..a66ea11
--- /dev/null
+++ b/tunnel/vsh/main.go
@@ -0,0 +1,210 @@
+// Command vsh is a tunnel service client that can be used to start a
+// shell on the server.
+package main
+
+import (
+ "errors"
+ "flag"
+ "fmt"
+ "net"
+ "os"
+ "path"
+ "strings"
+
+ "v.io/v23"
+ "v.io/v23/context"
+ "v.io/x/lib/vlog"
+
+ _ "v.io/core/veyron/profiles"
+
+ "v.io/apps/tunnel"
+ "v.io/apps/tunnel/tunnelutil"
+ "v.io/core/veyron/lib/signals"
+)
+
+var (
+ disablePty = flag.Bool("T", false, "Disable pseudo-terminal allocation.")
+ forcePty = flag.Bool("t", false, "Force allocation of pseudo-terminal.")
+
+ portforward = flag.String("L", "", "localaddr,remoteaddr Forward local 'localaddr' to 'remoteaddr'")
+ lprotocol = flag.String("local_protocol", "tcp", "Local network protocol for port forwarding")
+ rprotocol = flag.String("remote_protocol", "tcp", "Remote network protocol for port forwarding")
+
+ noshell = flag.Bool("N", false, "Do not execute a shell. Only do port forwarding.")
+)
+
+func init() {
+ flag.Usage = func() {
+ bname := path.Base(os.Args[0])
+ fmt.Fprintf(os.Stderr, `%s: Veyron SHell.
+
+This tool is used to run shell commands or an interactive shell on a remote
+tunneld service.
+
+To open an interactive shell, use:
+ %s <object name>
+
+To run a shell command, use:
+ %s <object name> <command to run>
+
+The -L flag will forward connections from a local port to a remote address
+through the tunneld service. The flag value is localaddr,remoteaddr. E.g.
+ -L :14141,www.google.com:80
+
+%s can't be used directly with tools like rsync because veyron object names
+don't look like traditional hostnames, which rsync doesn't understand. For
+compatibility with such tools, %s has a special feature that allows passing the
+veyron object name via the VSH_NAME environment variable.
+
+ $ VSH_NAME=<object name> rsync -avh -e %s /foo/* veyron:/foo/
+
+In this example, the "veyron" host will be substituted with $VSH_NAME by %s
+and rsync will work as expected.
+
+Full flags:
+`, os.Args[0], bname, bname, bname, bname, os.Args[0], bname)
+ flag.PrintDefaults()
+ }
+}
+
+func main() {
+ // Work around the fact that os.Exit doesn't run deferred functions.
+ os.Exit(realMain())
+}
+
+func realMain() int {
+ ctx, shutdown := v23.Init()
+ defer shutdown()
+
+ oname, cmd, err := objectNameAndCommandLine()
+ if err != nil {
+ flag.Usage()
+ fmt.Fprintf(os.Stderr, "\n%v\n", err)
+ return 1
+ }
+
+ t := tunnel.TunnelClient(oname)
+
+ if len(*portforward) > 0 {
+ go runPortForwarding(ctx, t, oname)
+ }
+
+ if *noshell {
+ <-signals.ShutdownOnSignals(ctx)
+ return 0
+ }
+
+ opts := shellOptions(cmd)
+
+ stream, err := t.Shell(ctx, cmd, opts)
+ if err != nil {
+ fmt.Fprintf(os.Stderr, "Error: %v\n", err)
+ return 1
+ }
+ if opts.UsePty {
+ saved := tunnelutil.EnterRawTerminalMode()
+ defer tunnelutil.RestoreTerminalSettings(saved)
+ }
+ runIOManager(os.Stdin, os.Stdout, os.Stderr, stream)
+
+ exitMsg := fmt.Sprintf("Connection to %s closed.", oname)
+ exitStatus, err := stream.Finish()
+ if err != nil {
+ exitMsg += fmt.Sprintf(" (%v)", err)
+ }
+ vlog.VI(1).Info(exitMsg)
+ // Only show the exit message on stdout for interactive shells.
+ // Otherwise, the exit message might get confused with the output
+ // of the command that was run.
+ if err != nil {
+ fmt.Fprintln(os.Stderr, exitMsg)
+ } else if len(cmd) == 0 {
+ fmt.Println(exitMsg)
+ }
+ return int(exitStatus)
+}
+
+func shellOptions(cmd string) (opts tunnel.ShellOpts) {
+ opts.UsePty = (len(cmd) == 0 || *forcePty) && !*disablePty
+ opts.Environment = environment()
+ ws, err := tunnelutil.GetWindowSize()
+ if err != nil {
+ vlog.VI(1).Infof("GetWindowSize failed: %v", err)
+ } else {
+ opts.WinSize.Rows = ws.Row
+ opts.WinSize.Cols = ws.Col
+ }
+ return
+}
+
+func environment() []string {
+ env := []string{}
+ for _, name := range []string{"TERM", "COLORTERM"} {
+ if value := os.Getenv(name); value != "" {
+ env = append(env, fmt.Sprintf("%s=%s", name, value))
+ }
+ }
+ return env
+}
+
+// objectNameAndCommandLine extracts the object name and the remote command to
+// send to the server. The object name is the first non-flag argument.
+// The command line is the concatenation of all non-flag arguments excluding
+// the object name.
+func objectNameAndCommandLine() (string, string, error) {
+ args := flag.Args()
+ if len(args) < 1 {
+ return "", "", errors.New("object name missing")
+ }
+ name := args[0]
+ args = args[1:]
+ // For compatibility with tools like rsync. Because object names
+ // don't look like traditional hostnames, tools that work with rsh and
+ // ssh can't work directly with vsh. This trick makes the following
+ // possible:
+ // $ VSH_NAME=<object name> rsync -avh -e vsh /foo/* veyron:/foo/
+ // The "veyron" host will be substituted with <object name>.
+ if envName := os.Getenv("VSH_NAME"); len(envName) > 0 && name == "veyron" {
+ name = envName
+ }
+ cmd := strings.Join(args, " ")
+ return name, cmd, nil
+}
+
+func runPortForwarding(ctx *context.T, t tunnel.TunnelClientMethods, oname string) {
+ // *portforward is localaddr,remoteaddr
+ parts := strings.Split(*portforward, ",")
+ var laddr, raddr string
+ if len(parts) != 2 {
+ vlog.Fatalf("-L flag expects 2 values separated by a comma")
+ }
+ laddr = parts[0]
+ raddr = parts[1]
+
+ ln, err := net.Listen(*lprotocol, laddr)
+ if err != nil {
+ vlog.Fatalf("net.Listen(%q, %q) failed: %v", *lprotocol, laddr, err)
+ }
+ defer ln.Close()
+ vlog.VI(1).Infof("Listening on %q", ln.Addr())
+ for {
+ conn, err := ln.Accept()
+ if err != nil {
+ vlog.Infof("Accept failed: %v", err)
+ continue
+ }
+ stream, err := t.Forward(ctx, *rprotocol, raddr)
+ if err != nil {
+ vlog.Infof("Tunnel(%q, %q) failed: %v", *rprotocol, raddr, err)
+ conn.Close()
+ continue
+ }
+ name := fmt.Sprintf("%v-->%v-->(%v)-->%v", conn.RemoteAddr(), conn.LocalAddr(), oname, raddr)
+ go func() {
+ vlog.VI(1).Infof("TUNNEL START: %v", name)
+ errf := tunnelutil.Forward(conn, stream.SendStream(), stream.RecvStream())
+ err := stream.Finish()
+ vlog.VI(1).Infof("TUNNEL END : %v (%v, %v)", name, errf, err)
+ }()
+ }
+}