Create a library for serving static files
It serves files that are in a given directory, and this could be
extended to html files.
In order to validate the functionality we also added a simple
`ScoutingServer` that you can `Start()` and `Stop()`. The unit test
makes use of this. The `main` binary that ties all the components
together also makes use of this.
See the README for an overview of everything we added in this patch.
Change-Id: Id496b032d6aa70fd8502eeb347895ac52b5d1ddd
Signed-off-by: Het Satasiya <satasiyahet@gmail.com>
Signed-off-by: Philipp Schrader <philipp.schrader@gmail.com>
diff --git a/scouting/webserver/BUILD b/scouting/webserver/BUILD
new file mode 100644
index 0000000..ff91aa8
--- /dev/null
+++ b/scouting/webserver/BUILD
@@ -0,0 +1,20 @@
+load("@io_bazel_rules_go//go:def.bzl", "go_binary", "go_library")
+
+go_library(
+ name = "webserver_lib",
+ srcs = ["main.go"],
+ importpath = "github.com/frc971/971-Robot-Code/scouting/webserver",
+ target_compatible_with = ["@platforms//cpu:x86_64"],
+ visibility = ["//visibility:private"],
+ deps = [
+ "//scouting/webserver/server",
+ "//scouting/webserver/static",
+ ],
+)
+
+go_binary(
+ name = "webserver",
+ embed = [":webserver_lib"],
+ target_compatible_with = ["@platforms//cpu:x86_64"],
+ visibility = ["//visibility:public"],
+)
diff --git a/scouting/webserver/README.md b/scouting/webserver/README.md
new file mode 100644
index 0000000..1c2383b
--- /dev/null
+++ b/scouting/webserver/README.md
@@ -0,0 +1,26 @@
+The scouting web server
+================================================================================
+
+This directory contains the code that combines to make the scouting web server.
+
+`main.go`
+--------------------------------------------------------------------------------
+This is the main application that brings all the pieces together. Run it like
+so:
+```bash
+bazel run //scouting/webserver:webserver
+```
+
+`server/`
+--------------------------------------------------------------------------------
+This directory contains the code that manages the web server itself. It's
+responsible for starting and stopping the server. It also exposes a `Handle()`
+method that lets other libraries enhance the server's functionality.
+
+`static/`
+--------------------------------------------------------------------------------
+This directory contains the code that serves static files at the root of the
+server. Make sure that none of the files you're serving clash with any of the
+paths used by the other libraries that enhance the server's functionality. E.g.
+if POST requests for match data are serviced at `/requests/xyz`, then don't
+serve any files in a `requests` directory.
diff --git a/scouting/webserver/main.go b/scouting/webserver/main.go
new file mode 100644
index 0000000..aebeef9
--- /dev/null
+++ b/scouting/webserver/main.go
@@ -0,0 +1,33 @@
+package main
+
+import (
+ "flag"
+ "fmt"
+ "os"
+ "os/signal"
+ "syscall"
+
+ "github.com/frc971/971-Robot-Code/scouting/webserver/server"
+ "github.com/frc971/971-Robot-Code/scouting/webserver/static"
+)
+
+func main() {
+ portPtr := flag.Int("port", 8080, "The port number to bind to.")
+ dirPtr := flag.String("directory", ".", "The directory to serve at /.")
+ flag.Parse()
+
+ scoutingServer := server.NewScoutingServer()
+ static.ServePages(scoutingServer, *dirPtr)
+ scoutingServer.Start(*portPtr)
+ fmt.Println("Serving", *dirPtr, "on port", *portPtr)
+
+ // Block until the user hits Ctrl-C.
+ sigint := make(chan os.Signal, 1)
+ signal.Notify(sigint, syscall.SIGINT)
+ fmt.Println("Waiting for CTRL-C or SIGINT.")
+ <-sigint
+
+ fmt.Println("Shutting down.")
+ scoutingServer.Stop()
+ fmt.Println("Successfully shut down.")
+}
diff --git a/scouting/webserver/server/BUILD b/scouting/webserver/server/BUILD
new file mode 100644
index 0000000..7ac56db
--- /dev/null
+++ b/scouting/webserver/server/BUILD
@@ -0,0 +1,9 @@
+load("@io_bazel_rules_go//go:def.bzl", "go_library")
+
+go_library(
+ name = "server",
+ srcs = ["server.go"],
+ importpath = "github.com/frc971/971-Robot-Code/scouting/webserver/server",
+ target_compatible_with = ["@platforms//cpu:x86_64"],
+ visibility = ["//visibility:public"],
+)
diff --git a/scouting/webserver/server/server.go b/scouting/webserver/server/server.go
new file mode 100644
index 0000000..ff85794
--- /dev/null
+++ b/scouting/webserver/server/server.go
@@ -0,0 +1,104 @@
+// This file implements a web server that can be used by the main scouting
+// application. It can also be used in unit tests for individual components of
+// the web server.
+
+package server
+
+import (
+ "context"
+ "fmt"
+ "log"
+ "net"
+ "net/http"
+ "time"
+)
+
+type ScoutingServer interface {
+ // Add a handler for a particular path. See upstream docs for this:
+ // https://pkg.go.dev/net/http#ServeMux.Handle
+ Handle(string, http.Handler)
+ // Starts the server on the specified port. Handlers cannot be added
+ // once this function is called.
+ Start(int)
+ // Stops the server.
+ Stop()
+}
+
+// This is a collection of data we'll need to implement the ScoutingServer
+// interface.
+type scoutingServer struct {
+ mux *http.ServeMux
+ httpServer *http.Server
+ doneChan <-chan bool
+ // Denotes whether the server has ever been Start()ed before.
+ started bool
+ // Denotes whether or not the server is currently running.
+ running bool
+}
+
+// Instantiates a new ScoutingServer.
+func NewScoutingServer() ScoutingServer {
+ return &scoutingServer{
+ mux: http.NewServeMux(),
+ httpServer: nil,
+ doneChan: nil,
+ started: false,
+ running: false,
+ }
+}
+
+func (server *scoutingServer) Handle(path string, handler http.Handler) {
+ if server.started {
+ log.Fatal("Cannot add handlers once server has started.")
+ }
+ server.mux.Handle(path, handler)
+}
+
+func (server *scoutingServer) Start(port int) {
+ if server.started {
+ log.Fatal("Cannot Start() a server a second time.")
+ }
+ server.started = true
+ server.running = true
+
+ addressStr := fmt.Sprintf(":%d", port)
+ server.httpServer = &http.Server{
+ Addr: addressStr,
+ Handler: server.mux,
+ }
+
+ doneChan := make(chan bool, 1)
+ server.doneChan = doneChan
+
+ // Start the server in the background since the ListenAndServe() call
+ // blocks.
+ go func() {
+ if err := server.httpServer.ListenAndServe(); err != http.ErrServerClosed {
+ log.Fatalf("Error calling ListenAndServe(): %v", err)
+ }
+ close(doneChan)
+ }()
+
+ // Wait until the server is ready.
+ for {
+ dial, err := net.Dial("tcp", addressStr)
+ if err != nil {
+ time.Sleep(100 * time.Millisecond)
+ } else {
+ dial.Close()
+ break
+ }
+ }
+}
+
+func (server *scoutingServer) Stop() {
+ if !server.running {
+ log.Fatal("Cannot Stop() a server that's not running.")
+ }
+ server.running = false
+
+ if err := server.httpServer.Shutdown(context.Background()); err != nil {
+ log.Fatalf("Error shutting down the server: %v", err)
+ }
+ <-server.doneChan
+}
diff --git a/scouting/webserver/static/BUILD b/scouting/webserver/static/BUILD
new file mode 100644
index 0000000..008d719
--- /dev/null
+++ b/scouting/webserver/static/BUILD
@@ -0,0 +1,21 @@
+load("@io_bazel_rules_go//go:def.bzl", "go_library", "go_test")
+
+go_library(
+ name = "static",
+ srcs = ["static.go"],
+ importpath = "github.com/frc971/971-Robot-Code/scouting/webserver/static",
+ target_compatible_with = ["@platforms//cpu:x86_64"],
+ visibility = ["//visibility:public"],
+ deps = ["//scouting/webserver/server"],
+)
+
+go_test(
+ name = "static_test",
+ srcs = ["static_test.go"],
+ data = [
+ "test_pages/page.txt",
+ "test_pages/root.txt",
+ ],
+ embed = [":static"],
+ deps = ["//scouting/webserver/server"],
+)
diff --git a/scouting/webserver/static/static.go b/scouting/webserver/static/static.go
new file mode 100644
index 0000000..e921b0b
--- /dev/null
+++ b/scouting/webserver/static/static.go
@@ -0,0 +1,14 @@
+package static
+
+// A year agnostic way to serve static http files.
+import (
+ "net/http"
+
+ "github.com/frc971/971-Robot-Code/scouting/webserver/server"
+)
+
+// Serve pages given a port, directory to serve from, and an channel to pass the errors back to the caller.
+func ServePages(scoutingServer server.ScoutingServer, directory string) {
+ // Serve the / endpoint given a folder of pages.
+ scoutingServer.Handle("/", http.FileServer(http.Dir(directory)))
+}
diff --git a/scouting/webserver/static/static_test.go b/scouting/webserver/static/static_test.go
new file mode 100644
index 0000000..15bd872
--- /dev/null
+++ b/scouting/webserver/static/static_test.go
@@ -0,0 +1,57 @@
+package static
+
+import (
+ "fmt"
+ "io/ioutil"
+ "net/http"
+ "testing"
+
+ "github.com/frc971/971-Robot-Code/scouting/webserver/server"
+)
+
+func TestServing(t *testing.T) {
+ cases := []struct {
+ // The path to request from the server.
+ path string
+ // The data that the server is expected to return at the
+ // specified path.
+ expectedData string
+ }{
+ {"/root.txt", "Hello, this is the root page!"},
+ {"/page.txt", "Hello from a page!"},
+ }
+
+ scoutingServer := server.NewScoutingServer()
+ ServePages(scoutingServer, "test_pages")
+ scoutingServer.Start(8080)
+
+ // Go through all the test cases, and run them against the running webserver.
+ for _, c := range cases {
+ dataReceived := getData(c.path, t)
+ if dataReceived != c.expectedData {
+ t.Errorf("Got %q, but expected %q", dataReceived, c.expectedData)
+ }
+ }
+
+ scoutingServer.Stop()
+}
+
+// Retrieves the data at the specified path. If an error occurs, the test case
+// is terminated and failed.
+func getData(path string, t *testing.T) string {
+ resp, err := http.Get(fmt.Sprintf("http://localhost:8080/%s", path))
+ if err != nil {
+ t.Fatalf("Failed to get data ", err)
+ }
+ // Error out if the return status is anything other than 200 OK.
+ if resp.Status != "200 OK" {
+ t.Fatalf("Received a status code other than 200")
+ }
+ // Read the response body.
+ body, err := ioutil.ReadAll(resp.Body)
+ if err != nil {
+ t.Fatalf("Failed to read body")
+ }
+ // Decode the body and return it.
+ return string(body)
+}
diff --git a/scouting/webserver/static/test_pages/page.txt b/scouting/webserver/static/test_pages/page.txt
new file mode 100644
index 0000000..3bb47e8
--- /dev/null
+++ b/scouting/webserver/static/test_pages/page.txt
@@ -0,0 +1 @@
+Hello from a page!
\ No newline at end of file
diff --git a/scouting/webserver/static/test_pages/root.txt b/scouting/webserver/static/test_pages/root.txt
new file mode 100644
index 0000000..8f75b7d
--- /dev/null
+++ b/scouting/webserver/static/test_pages/root.txt
@@ -0,0 +1 @@
+Hello, this is the root page!
\ No newline at end of file