cmd/viewcore: add -p <port> option to 'html' subcommand

In interactive mode, the command returns and resumes
further command processing immediately after starting the
web server.

Change-Id: I2e4768c931213bc9ef1107aff6d7973c0488bfe0
Reviewed-on: https://go-review.googlesource.com/129035
Reviewed-by: Keith Randall <khr@golang.org>
diff --git a/cmd/viewcore/html.go b/cmd/viewcore/html.go
index 564a547..7027cf2 100644
--- a/cmd/viewcore/html.go
+++ b/cmd/viewcore/html.go
@@ -15,7 +15,9 @@
 	"golang.org/x/debug/internal/gocore"
 )
 
-func serveHTML(c *gocore.Process) {
+// serveHTML starts and serves a webserver on the port.
+// If async is true, it returns immediately after starting the server.
+func serveHTML(c *gocore.Process, port int, async bool) {
 	http.HandleFunc("/object", func(w http.ResponseWriter, r *http.Request) {
 		objs, ok := r.URL.Query()["o"]
 		if !ok || len(objs) != 1 {
@@ -199,8 +201,18 @@
 		p(c.Stats(), "")
 		fmt.Fprintf(w, "</table>\n")
 	})
-	fmt.Println("serving on :8080")
-	http.ListenAndServe(":8080", nil)
+
+	if port <= 0 {
+		port = 8080
+	}
+	fmt.Printf("start serving on http://localhost:%d\n", port)
+
+	httpAddr := fmt.Sprintf(":%d", port)
+	if async {
+		go http.ListenAndServe(httpAddr, nil)
+		return
+	}
+	http.ListenAndServe(httpAddr, nil)
 }
 
 func htmlObject(w http.ResponseWriter, c *gocore.Process, name string, a core.Address, t *gocore.Type, live map[core.Address]bool) {
diff --git a/cmd/viewcore/main.go b/cmd/viewcore/main.go
index 050d0ab..b480ffc 100644
--- a/cmd/viewcore/main.go
+++ b/cmd/viewcore/main.go
@@ -15,6 +15,7 @@
 	"sort"
 	"strconv"
 	"strings"
+	"sync"
 	"text/tabwriter"
 
 	"github.com/chzyer/readline" // TODO: vendor
@@ -117,11 +118,9 @@
 
 	cmdHTML = &cobra.Command{
 		Use:   "html",
-		Short: "start an http server on :8080 for browsing core file data",
+		Short: "start an http server for browsing core file data on the port specified with -port",
 		Args:  cobra.ExactArgs(0),
 		Run:   runHTML,
-
-		// TODO: port flag
 	}
 
 	cmdRead = &cobra.Command{
@@ -133,6 +132,8 @@
 )
 
 type config struct {
+	interactive bool
+
 	// Set based on os.Args[1]
 	corefile string
 
@@ -149,6 +150,8 @@
 	cmdRoot.PersistentFlags().StringVar(&cfg.exePath, "exe", "", "main executable file")
 	cmdRoot.PersistentFlags().StringVar(&cfg.cpuprof, "prof", "", "write cpu profile of viewcore to this file for viewcore's developers")
 
+	cmdHTML.Flags().IntP("port", "p", 8080, "port for http server")
+
 	cmdRoot.AddCommand(
 		cmdOverview,
 		cmdMappings,
@@ -272,6 +275,9 @@
 		exitf("%v\n", err)
 	}
 
+	// Interactive mode.
+	cfg.interactive = true
+
 	// Create a dummy root to run in shell.
 	root := &cobra.Command{}
 	// Make all subcommands of viewcore available in the shell.
@@ -454,7 +460,6 @@
 		fmt.Fprintf(t, "%d\t%d\t%d\t %s\n", e.count, e.size, e.count*e.size, e.name)
 	}
 	t.Flush()
-
 }
 
 func runBreakdown(cmd *cobra.Command, args []string) {
@@ -647,12 +652,32 @@
 	}
 }
 
+// httpServer is the singleton http server, initialized by
+// the first call to runHTML.
+var httpServer struct {
+	sync.Mutex
+	port int
+}
+
 func runHTML(cmd *cobra.Command, args []string) {
+	httpServer.Lock()
+	defer httpServer.Unlock()
+	if httpServer.port != 0 {
+		fmt.Printf("already serving on http://localhost:%d\n", httpServer.port)
+		return
+	}
 	_, c, err := readCore()
 	if err != nil {
 		exitf("%v\n", err)
 	}
-	serveHTML(c)
+
+	port, err := cmd.Flags().GetInt("port")
+	if err != nil {
+		exitf("%v\n", err)
+	}
+	serveHTML(c, port, cfg.interactive)
+	httpServer.port = port
+	// TODO: launch web browser
 }
 
 func runRead(cmd *cobra.Command, args []string) {