Skip to content

Commit

Permalink
ipn/ipnlocal: add logging and locking to c2n /update
Browse files Browse the repository at this point in the history
Log some progress info to make updates more debuggable. Also, track
whether an active update is already started and return an error if
a concurrent update is attempted.

Some planned future PRs:
* add JSON output to `tailscale update`
* use JSON output from `tailscale update` to provide a more detailed
  status of in-progress update (stage, download progress, etc)

Updates #6907

Signed-off-by: Andrew Lytvynov <awly@tailscale.com>
  • Loading branch information
awly committed Sep 8, 2023
1 parent 50990f8 commit bb39e0a
Show file tree
Hide file tree
Showing 4 changed files with 45 additions and 16 deletions.
4 changes: 2 additions & 2 deletions clientupdate/clientupdate.go
Original file line number Diff line number Diff line change
Expand Up @@ -128,8 +128,8 @@ func NewUpdater(args Arguments) (*Updater, error) {
return nil, err
}
}
if args.PkgsAddr == "" {
args.PkgsAddr = "https://pkgs.tailscale.com"
if up.Arguments.PkgsAddr == "" {
up.Arguments.PkgsAddr = "https://pkgs.tailscale.com"
}
return &up, nil
}
Expand Down
44 changes: 32 additions & 12 deletions ipn/ipnlocal/c2n.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
package ipnlocal

import (
"bytes"
"encoding/json"
"errors"
"fmt"
Expand All @@ -14,6 +15,7 @@ import (
"path/filepath"
"runtime"
"strconv"
"strings"
"time"

"tailscale.com/clientupdate"
Expand Down Expand Up @@ -112,8 +114,7 @@ func (b *LocalBackend) handleC2N(w http.ResponseWriter, r *http.Request) {
}

func (b *LocalBackend) handleC2NUpdate(w http.ResponseWriter, r *http.Request) {
// TODO(bradfitz): add some sort of semaphore that prevents two concurrent
// updates, or if one happened in the past 5 minutes, or something.
b.logf("c2n: %s /update received", r.Method)

// GET returns the current status, and POST actually begins an update.
if r.Method != "GET" && r.Method != "POST" {
Expand All @@ -129,19 +130,31 @@ func (b *LocalBackend) handleC2NUpdate(w http.ResponseWriter, r *http.Request) {
// invoke it here. For this purpose, it is ok to pass it a zero Arguments.
prefs := b.Prefs().AutoUpdate()
_, err := clientupdate.NewUpdater(clientupdate.Arguments{})
b.c2nUpdateStatusMu.Lock()
defer b.c2nUpdateStatusMu.Unlock()
res := tailcfg.C2NUpdateResponse{
Enabled: envknob.AllowsRemoteUpdate() || prefs.Apply,
Supported: err == nil && !version.IsMacSysExt(),
Started: b.c2nUpdateStatus.started,
}

defer func() {
resb, err := json.Marshal(res)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
b.logf("c2n: %s /update response: %s", r.Method, resb)
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(res)
w.Write(resb)
}()

if r.Method == "GET" {
return
}
if b.c2nUpdateStatus.started {
res.Err = "update already in progress"
}
if !res.Enabled {
res.Err = "not enabled"
return
Expand Down Expand Up @@ -173,21 +186,28 @@ func (b *LocalBackend) handleC2NUpdate(w http.ResponseWriter, r *http.Request) {
return
}
cmd := exec.Command(cmdTS, "update", "--yes")
buf := new(bytes.Buffer)
cmd.Stdout = buf
cmd.Stderr = buf
b.logf("c2n: running %q", strings.Join(cmd.Args, " "))
if err := cmd.Start(); err != nil {
res.Err = fmt.Sprintf("failed to start cmd/tailscale update: %v", err)
return
}
res.Started = true
b.c2nUpdateStatus.started = true

// TODO(bradfitz,andrew): There might be a race condition here on Windows:
// * We start the update process.
// * tailscale.exe copies itself and kicks off the update process
// * msiexec stops this process during the update before the selfCopy exits(?)
// * This doesn't return because the process is dead.
//
// This seems fairly unlikely, but worth checking.
defer cmd.Wait()
return
// Run update asynchronously and respond that it started.
go func() {
if err := cmd.Wait(); err != nil {
b.logf("c2n: update command failed: %v, output: %s", err, buf)
} else {
b.logf("c2n: update complete")
}
b.c2nUpdateStatusMu.Lock()
b.c2nUpdateStatus.started = false
b.c2nUpdateStatusMu.Unlock()
}()
}

// findCmdTailscale looks for the cmd/tailscale that corresponds to the
Expand Down
9 changes: 9 additions & 0 deletions ipn/ipnlocal/local.go
Original file line number Diff line number Diff line change
Expand Up @@ -266,6 +266,15 @@ type LocalBackend struct {
// at the moment that tkaSyncLock is taken).
tkaSyncLock sync.Mutex
clock tstime.Clock

// c2nUpdateStatus is the status of c2n-triggered client update, protected
// by c2nUpdateStatusMu.
c2nUpdateStatusMu sync.Mutex
c2nUpdateStatus updateStatus
}

type updateStatus struct {
started bool
}

// clientGen is a func that creates a control plane client.
Expand Down
4 changes: 2 additions & 2 deletions tailcfg/c2ntypes.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ type C2NSSHUsernamesResponse struct {
// its Tailscale installation.
type C2NUpdateResponse struct {
// Err is the error message, if any.
Err string
Err string `json:",omitempty"`

// Enabled indicates whether the user has opted in to updates triggered from
// control.
Expand All @@ -50,5 +50,5 @@ type C2NUpdateResponse struct {
Supported bool

// Started indicates whether the update has started.
Started bool
Started bool `json:",omitempty"`
}

0 comments on commit bb39e0a

Please sign in to comment.