From 6e27e7c003fedb968cb24e93c3719520a0990db0 Mon Sep 17 00:00:00 2001 From: Kleissner Date: Mon, 15 Nov 2021 03:28:17 +0100 Subject: [PATCH] Add exit codes documentation. Add documentation to reduce binary file size. Implement /shutdown API endpoint for a graceful shutdown. --- API.go | 74 ++++++++++++++++++++++++++++++++++++++++++++++++------- Main.go | 9 +++++-- README.md | 26 ++++++++++++++++++- go.mod | 2 +- go.sum | 4 +-- 5 files changed, 100 insertions(+), 15 deletions(-) diff --git a/API.go b/API.go index 8619785..a702c46 100644 --- a/API.go +++ b/API.go @@ -8,11 +8,15 @@ package main import ( "bytes" + "encoding/json" "flag" "net/http" + "os" + "strconv" "strings" "time" + "github.com/PeernetOfficial/core" "github.com/PeernetOfficial/core/webapi" "github.com/gorilla/websocket" ) @@ -28,12 +32,25 @@ func startAPI() { } else if len(config.APIListen) != 0 { // API settings via config file. webapi.Start(config.APIListen, config.APIUseSSL, config.APICertificateFile, config.APICertificateKey, parseDuration(config.APITimeoutRead), parseDuration(config.APITimeoutWrite)) - return } else { return } webapi.Router.HandleFunc("/console", apiConsole).Methods("GET") + webapi.Router.HandleFunc("/shutdown", apiShutdown).Methods("GET") +} + +// parseCmdParamWebapi parses a "-webapi=" command line parameter +func parseCmdParamWebapi() (apiListen []string) { + var param string + flag.StringVar(¶m, "webapi", "", "Specify the list of IP:Ports for the webapi to listen. Example: -webapi=127.0.0.1:1234") + flag.Parse() + + if len(param) == 0 { + return nil + } + + return strings.Split(param, ",") } // parseDuration is the same as time.ParseDuration without returning an error. Valid units are ms, s, m, h. For example "10s". @@ -44,6 +61,7 @@ func parseDuration(input string) (result time.Duration) { /* apiConsole provides a websocket to send/receive internal commands + Request: GET /console Result: 200 with JSON structure apiResponsePeerSelf */ @@ -100,15 +118,53 @@ func apiConsole(w http.ResponseWriter, r *http.Request) { } } -// parseCmdParamWebapi parses a "-webapi=" command line parameter -func parseCmdParamWebapi() (apiListen []string) { - var param string - flag.StringVar(¶m, "webapi", "", "Specify the list of IP:Ports for the webapi to listen. Example: -webapi=127.0.0.1:1234") - flag.Parse() +/* +apiShutdown gracefully shuts down the application. Actions: 0 = Shutdown. - if len(param) == 0 { - return nil +Request: GET /shutdown?action=[action] +Result: 200 with JSON structure apiShutdownStatus +*/ +func apiShutdown(w http.ResponseWriter, r *http.Request) { + r.ParseForm() + action, err := strconv.Atoi(r.Form.Get("action")) + if err != nil || action != 0 { + http.Error(w, "", http.StatusBadRequest) + return } - return strings.Split(param, ",") + if action == 0 { + // Later: Initiate shutdown signal to core library and wait for all requests to complete. + + core.Filters.LogError("apiShutdown", "Graceful shutdown via API requested from '%s'\n", r.RemoteAddr) + + EncodeJSONFlush(w, r, &apiShutdownStatus{Status: 0}) + + os.Exit(core.ExitGraceful) + } +} + +type apiShutdownStatus struct { + Status int `json:"status"` // Status of the API call. 0 = Success. +} + +// EncodeJSONFlush encodes the data as JSON and flushes the writer. It sets the Content-Length header so no subsequent writes should be made. +func EncodeJSONFlush(w http.ResponseWriter, r *http.Request, data interface{}) (err error) { + response, err := json.Marshal(data) + if err != nil { + core.Filters.LogError("EncodeJSONFlush", "Error marshalling data for route '%s': %v\n", r.URL.Path, err) + return err + } + + w.Header().Set("Content-Type", "application/json") + w.Header().Set("Content-Length", strconv.Itoa(len(response))) + w.WriteHeader(http.StatusOK) + + _, err = w.Write(response) + + // Flushing the buffer immediately is needed in case the application exits immediately after this call. + if f, ok := w.(http.Flusher); ok { + f.Flush() + } + + return } diff --git a/Main.go b/Main.go index 97d51ca..db99d14 100644 --- a/Main.go +++ b/Main.go @@ -31,22 +31,27 @@ var config struct { func init() { if status, err := core.LoadConfigOut(configFile, &config); err != nil { + var exitCode int switch status { case 0: + exitCode = core.ExitErrorConfigAccess fmt.Printf("Unknown error accessing config file '%s': %s\n", configFile, err.Error()) case 1: + exitCode = core.ExitErrorConfigRead fmt.Printf("Error reading config file '%s': %s\n", configFile, err.Error()) case 2: + exitCode = core.ExitErrorConfigParse fmt.Printf("Error parsing config file '%s' (make sure it is valid YAML format): %s\n", configFile, err.Error()) default: + exitCode = core.ExitErrorConfigAccess fmt.Printf("Unknown error loading config file '%s': %s\n", configFile, err.Error()) } - os.Exit(1) + os.Exit(exitCode) } if err := core.InitLog(); err != nil { fmt.Printf("Error opening log file: %s\n", err.Error()) - os.Exit(1) + os.Exit(core.ExitErrorLogInit) } monitorKeys = make(map[string]struct{}) diff --git a/README.md b/README.md index 11692ad..c8411e7 100644 --- a/README.md +++ b/README.md @@ -6,12 +6,18 @@ This client can be used as root peer to help speed up discovery of peers and dat ## Compile -To build: +Download the [latest version of Go](https://golang.org/dl/). To build: ``` go build ``` +To reduce the binary size provide the linker switch -s which will "Omit the symbol table and debug information". This reduced the Windows binary by 26% in a test. + +``` +go build -ldflags "-s" +``` + ## Use The config filename is hard-coded to `Config.yaml` and is created on the first run. Please see the core library for individual settings to change. @@ -70,3 +76,21 @@ APICertificateKey: "certificate.key" # Private Key APITimeoutRead: "10m" # The maximum duration for reading the entire request, including the body. In this example 10 minutes. APITimeoutWrite: "10m" # The maximum duration before timing out writes of the response. This includes processing time and is therefore the max time any HTTP function may take. ``` + +## Error Handling + +The application exits in case of the errors listed below and uses the specified exit code. Applications that launch this application can monitor for those exit codes. End users should look into the log file for additional information in case any of these errors occur, although some of them are pre log file initialization. + +| Exit Code | Constant | Info | +| ---------- | ---------------------- | --------------------------------------------------- | +| 0 | ExitSuccess | This is actually never used. | +| 1 | ExitErrorConfigAccess | Error accessing the config file. | +| 2 | ExitErrorConfigRead | Error reading the config file. | +| 3 | ExitErrorConfigParse | Error parsing the config file. | +| 4 | ExitErrorLogInit | Error initializing log file. | +| 5 | ExitParamWebapiInvalid | Parameter for webapi is invalid. | +| 6 | ExitPrivateKeyCorrupt | Private key is corrupt. | +| 7 | ExitPrivateKeyCreate | Cannot create a new private key. | +| 8 | ExitBlockchainCorrupt | Blockchain is corrupt. | +| 9 | ExitGraceful | Graceful shutdown. | +| 0xC000013A | STATUS_CONTROL_C_EXIT | The application terminated as a result of a CTRL+C. | diff --git a/go.mod b/go.mod index c0a418d..ea5082c 100644 --- a/go.mod +++ b/go.mod @@ -3,6 +3,6 @@ module github.com/PeernetOfficial/Cmd go 1.16 require ( - github.com/PeernetOfficial/core v0.0.0-20211114213707-9626fa80882d + github.com/PeernetOfficial/core v0.0.0-20211115020024-d06c2e0b1a65 github.com/gorilla/websocket v1.4.3-0.20210424162022-e8629af678b7 ) diff --git a/go.sum b/go.sum index 7af27a5..a136447 100644 --- a/go.sum +++ b/go.sum @@ -1,5 +1,5 @@ -github.com/PeernetOfficial/core v0.0.0-20211114213707-9626fa80882d h1:PzpGyK69XH6j+lRVVB173jz3i7sU+Q+lx59vw43UGxM= -github.com/PeernetOfficial/core v0.0.0-20211114213707-9626fa80882d/go.mod h1:0D/jIDYdV0WXg5WACS+fR5keafLhYB89WUs6ZEE8t4Y= +github.com/PeernetOfficial/core v0.0.0-20211115020024-d06c2e0b1a65 h1:WD3/HFSv4+z/hxD5fAXrShgwbtcC8tigre3tQN9Ck8c= +github.com/PeernetOfficial/core v0.0.0-20211115020024-d06c2e0b1a65/go.mod h1:0D/jIDYdV0WXg5WACS+fR5keafLhYB89WUs6ZEE8t4Y= github.com/akrylysov/pogreb v0.10.1 h1:FqlR8VR7uCbJdfUob916tPM+idpKgeESDXOA1K0DK4w= github.com/akrylysov/pogreb v0.10.1/go.mod h1:pNs6QmpQ1UlTJKDezuRWmaqkgUE2TuU0YTWyqJZ7+lI= github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=