Watch cookie file, so we can refresh cookies with limited expiry
times.

Change-Id: I2d692e7552d53c390a1ba6126cc8dd30c5c959f3
diff --git a/cmd/slothfs-expand-manifest/main.go b/cmd/slothfs-expand-manifest/main.go
index 294c43c..642798d 100644
--- a/cmd/slothfs-expand-manifest/main.go
+++ b/cmd/slothfs-expand-manifest/main.go
@@ -20,7 +20,6 @@
 	"log"
 	"os"
 
-	"github.com/google/slothfs/cookie"
 	"github.com/google/slothfs/gitiles"
 	"github.com/google/slothfs/manifest"
 
@@ -42,16 +41,11 @@
 	opts := gitiles.Options{
 		UserAgent: *agent,
 	}
-	if *cookieJarPath != "" {
-		var err error
-		opts.CookieJar, err = cookie.NewJar(*cookieJarPath)
-		if err != nil {
-			log.Fatal(err)
-		}
+
+	if err := opts.LoadCookieJar(*cookieJarPath); err != nil {
+		log.Fatalf("LoadCookieJar(%s): %v", *cookieJarPath, err)
 	}
 
-	// SustainedQPS is a little high, but since this is a one-shot
-	// program let's try to get away with it.
 	service, err := gitiles.NewService(*gitilesURL, opts)
 	if err != nil {
 		log.Fatalf("NewService: %v", err)
diff --git a/cmd/slothfs-gitiles-test/main.go b/cmd/slothfs-gitiles-test/main.go
index 092acb8..4262998 100644
--- a/cmd/slothfs-gitiles-test/main.go
+++ b/cmd/slothfs-gitiles-test/main.go
@@ -25,7 +25,6 @@
 	"os"
 	"sync"
 
-	"github.com/google/slothfs/cookie"
 	"github.com/google/slothfs/gitiles"
 )
 
@@ -46,12 +45,8 @@
 	opts := gitiles.Options{
 		UserAgent: *agent,
 	}
-	if *cookieJarPath != "" {
-		var err error
-		opts.CookieJar, err = cookie.NewJar(*cookieJarPath)
-		if err != nil {
-			log.Fatal(err)
-		}
+	if err := opts.LoadCookieJar(*cookieJarPath); err != nil {
+		log.Fatalf("LoadCookieJar(%s): %v", *cookieJarPath, err)
 	}
 
 	service, err := gitiles.NewService(*gitilesURL, opts)
diff --git a/cmd/slothfs-multifs/main.go b/cmd/slothfs-multifs/main.go
index 1c25fcb..03e9214 100644
--- a/cmd/slothfs-multifs/main.go
+++ b/cmd/slothfs-multifs/main.go
@@ -23,7 +23,6 @@
 	"time"
 
 	"github.com/google/slothfs/cache"
-	"github.com/google/slothfs/cookie"
 	"github.com/google/slothfs/fs"
 	"github.com/google/slothfs/gitiles"
 	"github.com/hanwen/go-fuse/fuse/nodefs"
@@ -59,12 +58,8 @@
 	gitilesOpts := gitiles.Options{
 		UserAgent: *agent,
 	}
-	if *cookieJarPath != "" {
-		var err error
-		gitilesOpts.CookieJar, err = cookie.NewJar(*cookieJarPath)
-		if err != nil {
-			log.Fatal(err)
-		}
+	if err := gitilesOpts.LoadCookieJar(*cookieJarPath); err != nil {
+		log.Fatalf("LoadCookieJar(%s): %v", *cookieJarPath, err)
 	}
 
 	service, err := gitiles.NewService(*gitilesURL, gitilesOpts)
diff --git a/cookie/cookie.go b/cookie/cookie.go
index d5c8e5c..f25e884 100644
--- a/cookie/cookie.go
+++ b/cookie/cookie.go
@@ -19,13 +19,17 @@
 	"bufio"
 	"fmt"
 	"io"
+	"log"
 	"net/http"
 	"net/http/cookiejar"
 	"net/url"
 	"os"
+	"path/filepath"
 	"strconv"
 	"strings"
 	"time"
+
+	"github.com/fsnotify/fsnotify"
 )
 
 // ParseCookieJar parses a cURL/Mozilla/Netscape cookie jar text file.
@@ -74,21 +78,56 @@
 	return result, nil
 }
 
-func NewJar(path string) (http.CookieJar, error) {
+// WatchJar starts watching the given path for changes, and loads new
+// data from the file whenever it is available.
+func WatchJar(jar http.CookieJar, path string) error {
+	w, err := fsnotify.NewWatcher()
+	if err != nil {
+		return err
+	}
+
+	// We watch the dir, so we catch creation + rename events too.
+	if err := w.Add(filepath.Dir(path)); err != nil {
+		return err
+	}
+
+	go func() {
+		var lastMod time.Time
+		for {
+			select {
+			case <-w.Events:
+				fi, err := os.Stat(path)
+				if err != nil {
+					log.Printf("Stat(%s): %v", path, err)
+					continue
+				}
+				if fi.ModTime().Equal(lastMod) {
+					continue
+				}
+				lastMod = fi.ModTime()
+				if err := updateJar(jar, path); err != nil {
+					log.Printf("updateJar(%s): %v", path, err)
+				}
+
+			case <-w.Errors:
+				log.Printf("notify: %v", path, err)
+			}
+		}
+	}()
+	return nil
+}
+
+// updateJar reads the path into the jar.
+func updateJar(jar http.CookieJar, path string) error {
 	f, err := os.Open(path)
 	if err != nil {
-		return nil, err
+		return err
 	}
 	defer f.Close()
 
 	cs, err := ParseCookieJar(f)
 	if err != nil {
-		return nil, err
-	}
-
-	jar, err := cookiejar.New(nil)
-	if err != nil {
-		return nil, err
+		return err
 	}
 
 	for _, c := range cs {
@@ -98,5 +137,20 @@
 		}, []*http.Cookie{c})
 	}
 
+	return nil
+}
+
+// NewJar reads cookies in the Mozilla/Netscape cookie file format,
+// and returns them as a CookieJar
+func NewJar(path string) (http.CookieJar, error) {
+	jar, err := cookiejar.New(nil)
+	if err != nil {
+		return nil, err
+	}
+
+	if err := updateJar(jar, path); err != nil {
+		return nil, err
+	}
+
 	return jar, nil
 }
diff --git a/gitiles/client.go b/gitiles/client.go
index 2cf859f..5da2ea4 100644
--- a/gitiles/client.go
+++ b/gitiles/client.go
@@ -26,6 +26,7 @@
 	"net/url"
 	"path"
 
+	"github.com/google/slothfs/cookie"
 	"golang.org/x/net/context"
 	"golang.org/x/time/rate"
 )
@@ -55,6 +56,23 @@
 	UserAgent string
 }
 
+func (o *Options) LoadCookieJar(nm string) error {
+	if nm == "" {
+		return nil
+	}
+
+	jar, err := cookie.NewJar(nm)
+	if err != nil {
+		return err
+	}
+	if err := cookie.WatchJar(jar, nm); err != nil {
+		return err
+	}
+
+	o.CookieJar = jar
+	return nil
+}
+
 // NewService returns a new Gitiles JSON client.
 func NewService(addr string, opts Options) (*Service, error) {
 	if opts.BurstQPS == 0 {