-
Notifications
You must be signed in to change notification settings - Fork 3
Improve the arduino-app-cli version command by adding the "server version" #31 #49
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from 2 commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -16,34 +16,132 @@ | |||||||||||
| package version | ||||||||||||
|
|
||||||||||||
| import ( | ||||||||||||
| "encoding/json" | ||||||||||||
| "fmt" | ||||||||||||
| "io" | ||||||||||||
| "net" | ||||||||||||
| "net/http" | ||||||||||||
| "net/url" | ||||||||||||
| "time" | ||||||||||||
|
|
||||||||||||
| "github.com/spf13/cobra" | ||||||||||||
|
|
||||||||||||
| "github.com/arduino/arduino-app-cli/cmd/feedback" | ||||||||||||
| "github.com/arduino/arduino-app-cli/cmd/i18n" | ||||||||||||
| ) | ||||||||||||
|
|
||||||||||||
| func NewVersionCmd(version string) *cobra.Command { | ||||||||||||
| // The actual listening address for the daemon | ||||||||||||
| // is defined in the installation package | ||||||||||||
| const ( | ||||||||||||
| DefaultHostname = "localhost" | ||||||||||||
| DefaultPort = "8800" | ||||||||||||
| ) | ||||||||||||
|
|
||||||||||||
| func NewVersionCmd(clientVersion string) *cobra.Command { | ||||||||||||
| cmd := &cobra.Command{ | ||||||||||||
| Use: "version", | ||||||||||||
| Short: "Print the version number of Arduino App CLI", | ||||||||||||
| Short: "Print the client and server version numbers for the Arduino App CLI.", | ||||||||||||
| Run: func(cmd *cobra.Command, args []string) { | ||||||||||||
| feedback.PrintResult(versionResult{ | ||||||||||||
| AppName: "Arduino App CLI", | ||||||||||||
| Version: version, | ||||||||||||
| }) | ||||||||||||
| host, _ := cmd.Flags().GetString("host") | ||||||||||||
|
|
||||||||||||
| versionHandler(clientVersion, host) | ||||||||||||
| }, | ||||||||||||
| } | ||||||||||||
| cmd.Flags().String("host", fmt.Sprintf("%s:%s", DefaultHostname, DefaultPort), | ||||||||||||
| "The daemon network address [host]:[port]") | ||||||||||||
| return cmd | ||||||||||||
| } | ||||||||||||
|
|
||||||||||||
| type versionResult struct { | ||||||||||||
| AppName string `json:"appName"` | ||||||||||||
| func versionHandler(clientVersion string, host string) { | ||||||||||||
| httpClient := http.Client{ | ||||||||||||
| Timeout: time.Second, | ||||||||||||
| } | ||||||||||||
| result := doVersionHandler(httpClient, clientVersion, host) | ||||||||||||
| feedback.PrintResult(result) | ||||||||||||
| } | ||||||||||||
|
||||||||||||
|
|
||||||||||||
| func doVersionHandler(httpClient http.Client, clientVersion string, host string) versionResult { | ||||||||||||
| url, err := getValidOrDefaultUrl(host) | ||||||||||||
| if err != nil { | ||||||||||||
| feedback.Fatal(i18n.Tr("Error: invalid host:port format"), feedback.ErrBadArgument) | ||||||||||||
| } | ||||||||||||
|
||||||||||||
|
|
||||||||||||
| serverVersion, err := getServerVersion(httpClient, url) | ||||||||||||
| if err != nil { | ||||||||||||
| serverVersion = fmt.Sprintf("n/a (cannot connect to the server %s://%s)", url.Scheme, url.Host) | ||||||||||||
| } | ||||||||||||
|
|
||||||||||||
| return versionResult{ | ||||||||||||
| ClientVersion: clientVersion, | ||||||||||||
| ServerVersion: serverVersion, | ||||||||||||
| } | ||||||||||||
| } | ||||||||||||
|
|
||||||||||||
| func getValidOrDefaultUrl(hostPort string) (url.URL, error) { | ||||||||||||
| host := DefaultHostname | ||||||||||||
| port := DefaultPort | ||||||||||||
|
|
||||||||||||
| if hostPort != "" { | ||||||||||||
| h, p, err := net.SplitHostPort(hostPort) | ||||||||||||
| if err != nil { | ||||||||||||
| return url.URL{}, err | ||||||||||||
| } | ||||||||||||
| if h != "" { | ||||||||||||
| host = h | ||||||||||||
| } | ||||||||||||
| if p != "" { | ||||||||||||
| port = p | ||||||||||||
| } | ||||||||||||
|
|
||||||||||||
| } | ||||||||||||
|
|
||||||||||||
| hostAndPort := net.JoinHostPort(host, port) | ||||||||||||
|
|
||||||||||||
| u := url.URL{ | ||||||||||||
| Scheme: "http", | ||||||||||||
| Host: hostAndPort, | ||||||||||||
| Path: "/v1/version", | ||||||||||||
| } | ||||||||||||
|
|
||||||||||||
| return u, nil | ||||||||||||
| } | ||||||||||||
|
|
||||||||||||
| func getServerVersion(httpClient http.Client, url url.URL) (string, error) { | ||||||||||||
| resp, err := httpClient.Get(url.String()) | ||||||||||||
| if err != nil { | ||||||||||||
| return "", err | ||||||||||||
| } | ||||||||||||
| defer resp.Body.Close() | ||||||||||||
|
|
||||||||||||
| if resp.StatusCode != http.StatusOK { | ||||||||||||
| return "", fmt.Errorf("request failed with status %d", resp.StatusCode) | ||||||||||||
| } | ||||||||||||
|
|
||||||||||||
| body, err := io.ReadAll(resp.Body) | ||||||||||||
| if err != nil { | ||||||||||||
| return "", err | ||||||||||||
| } | ||||||||||||
|
|
||||||||||||
| var serverResponse serverVersionResponse | ||||||||||||
| if err := json.Unmarshal(body, &serverResponse); err != nil { | ||||||||||||
|
||||||||||||
| if err := json.Unmarshal(body, &serverResponse); err != nil { | |
| var serverResponse struct { | |
| Version string `json:"version"` | |
| } | |
| if err := json.NewDecoder(resp.Body).Decode(&serverResponse); err != nil { |
Outdated
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Usually structs used only for parsing JSON are defined directly inside the function that uses them
Outdated
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I would probably return those info
| ClientVersion string `json:"version"` | |
| ServerVersion string `json:"serverVersion"` | |
| AppName string `json:"app_name"` | |
| Version string `json:"version"` | |
| DaemonVersion string `json:"daemon_version"` |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We could print the name with the arduino-flasher-cli output https://github.com/arduino/arduino-flasher-cli/blob/1899741a4e890a2ff0727669d23d25b6499e0522/main.go#L63
Arduino App CLI
BUT I would avoid theapp_name that can be misleading (the term "app" is for the Arduino Apps).
maybe only "name" in the json field.
Outdated
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I am wondering if we should make a more text-friendly line like
Arduino App CLI v0.6.6
daemon server at "localhost:8800" v0.6.6
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,115 @@ | ||
| // This file is part of arduino-app-cli. | ||
| // | ||
| // Copyright 2025 ARDUINO SA (http://www.arduino.cc/) | ||
| // | ||
| // This software is released under the GNU General Public License version 3, | ||
| // which covers the main part of arduino-app-cli. | ||
| // The terms of this license can be found at: | ||
| // https://www.gnu.org/licenses/gpl-3.0.en.html | ||
| // | ||
| // You can be released from the requirements of the above licenses by purchasing | ||
| // a commercial license. Buying such a license is mandatory if you want to | ||
| // modify or otherwise use the software for commercial activities involving the | ||
| // Arduino software without disclosing the source code of your own applications. | ||
| // To purchase a commercial license, send an email to license@arduino.cc. | ||
|
|
||
| package version | ||
|
|
||
| import ( | ||
| "errors" | ||
| "fmt" | ||
| "io" | ||
| "net/http" | ||
| "strings" | ||
| "testing" | ||
|
|
||
| "github.com/stretchr/testify/require" | ||
| ) | ||
|
|
||
| func TestServerVersion(t *testing.T) { | ||
| clientVersion := "5.1-dev" | ||
|
|
||
| testCases := []struct { | ||
| name string | ||
| serverStub Tripper | ||
| expectedResult versionResult | ||
| host string | ||
| }{ | ||
| { | ||
| name: "return the server version when the server is up", | ||
| serverStub: successServer, | ||
| expectedResult: versionResult{ | ||
| ClientVersion: "5.1-dev", | ||
| ServerVersion: "3.0", | ||
| }, | ||
| host: "", | ||
| }, | ||
| { | ||
| name: "return error if default server is not listening", | ||
| serverStub: failureServer, | ||
| expectedResult: versionResult{ | ||
| ClientVersion: "5.1-dev", | ||
| ServerVersion: fmt.Sprintf("n/a (cannot connect to the server http://%s:%s)", DefaultHostname, DefaultPort), | ||
| }, | ||
| host: "", | ||
| }, | ||
| { | ||
| name: "return error if provided server is not listening", | ||
| serverStub: failureServer, | ||
| expectedResult: versionResult{ | ||
| ClientVersion: "5.1-dev", | ||
| ServerVersion: "n/a (cannot connect to the server http://unreacheable:123)", | ||
| }, | ||
| host: "unreacheable:123", | ||
| }, | ||
| { | ||
| name: "return error for server resopnse 500 Internal Server Error", | ||
| serverStub: failureInternalServerError, | ||
| expectedResult: versionResult{ | ||
| ClientVersion: "5.1-dev", | ||
| ServerVersion: "n/a (cannot connect to the server http://unreacheable:123)", | ||
| }, | ||
| host: "unreacheable:123", | ||
| }, | ||
| } | ||
| for _, tc := range testCases { | ||
| t.Run(tc.name, func(t *testing.T) { | ||
| // arrange | ||
| httpClient := http.Client{} | ||
| httpClient.Transport = tc.serverStub | ||
|
|
||
| // act | ||
| result := doVersionHandler(httpClient, clientVersion, tc.host) | ||
|
|
||
| // assert | ||
| require.Equal(t, tc.expectedResult, result) | ||
| }) | ||
| } | ||
| } | ||
|
|
||
| // Leverage the http.Client's RoundTripper | ||
| // to return a canned response and bypass network calls. | ||
| type Tripper func(*http.Request) (*http.Response, error) | ||
|
|
||
| func (t Tripper) RoundTrip(request *http.Request) (*http.Response, error) { | ||
| return t(request) | ||
| } | ||
|
|
||
| var successServer = Tripper(func(*http.Request) (*http.Response, error) { | ||
| body := io.NopCloser(strings.NewReader(`{"version":"3.0"}`)) | ||
| return &http.Response{ | ||
| StatusCode: http.StatusOK, | ||
| Body: body, | ||
| }, nil | ||
| }) | ||
|
|
||
| var failureServer = Tripper(func(*http.Request) (*http.Response, error) { | ||
| return nil, errors.New("connetion refused") | ||
| }) | ||
|
|
||
| var failureInternalServerError = Tripper(func(*http.Request) (*http.Response, error) { | ||
| return &http.Response{ | ||
| StatusCode: http.StatusInternalServerError, | ||
| Body: io.NopCloser(strings.NewReader("")), | ||
| }, nil | ||
| }) |
Uh oh!
There was an error while loading. Please reload this page.