diff --git a/.golangci.yml b/.golangci.yml index 95a1cb8..ccd3c1a 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -1,22 +1,37 @@ ---- +version: "2" linters: enable: - - forbidigo - - goimports - - misspell - # - revive - - whitespace - -issues: - exclude-rules: - - path: _test.go - linters: - - errcheck - -linters-settings: - forbidigo: - forbid: - - p: ^fmt\.Print.*$ - msg: Do not commit print statements. Use logger package. - - p: ^log\.(Fatal|Panic|Print)(f|ln)?.*$ - msg: Do not commit log statements. Use logger package. + - forbidigo + - misspell + - whitespace + settings: + forbidigo: + forbid: + - pattern: ^fmt\.Print.*$ + msg: Do not commit print statements. Use logger package. + - pattern: ^log\.(Fatal|Panic|Print)(f|ln)?.*$ + msg: Do not commit log statements. Use logger package. + exclusions: + generated: lax + presets: + - comments + - common-false-positives + - legacy + - std-error-handling + rules: + - linters: + - errcheck + path: _test.go + paths: + - third_party$ + - builtin$ + - examples$ +formatters: + enable: + - goimports + exclusions: + generated: lax + paths: + - third_party$ + - builtin$ + - examples$ diff --git a/dev_deploy.sh b/dev_deploy.sh index ca627cd..30fb3c7 100755 --- a/dev_deploy.sh +++ b/dev_deploy.sh @@ -91,7 +91,7 @@ cd "${REMOTE_PATH}" chmod +x jetkvm_app_debug # Run the application in the background -PION_LOG_TRACE=jetkvm,cloud,websocket,native ./jetkvm_app_debug +PION_LOG_TRACE=jetkvm,cloud,websocket,native,jsonrpc ./jetkvm_app_debug EOF echo "Deployment complete." diff --git a/internal/timesync/http.go b/internal/timesync/http.go index a6be68c..14b4633 100644 --- a/internal/timesync/http.go +++ b/internal/timesync/http.go @@ -1,18 +1,98 @@ package timesync import ( + "context" + "errors" + "math/rand" "net/http" "time" ) +func (t *TimeSync) queryAllHttpTime() (now *time.Time) { + chunkSize := 4 + httpUrls := t.httpUrls + + // shuffle the http urls to avoid always querying the same servers + rand.Shuffle(len(httpUrls), func(i, j int) { httpUrls[i], httpUrls[j] = httpUrls[j], httpUrls[i] }) + + for i := 0; i < len(httpUrls); i += chunkSize { + chunk := httpUrls[i:min(i+chunkSize, len(httpUrls))] + results := t.queryMultipleHttp(chunk, timeSyncTimeout) + if results != nil { + return results + } + } + + return nil +} + +func (t *TimeSync) queryMultipleHttp(urls []string, timeout time.Duration) (now *time.Time) { + results := make(chan *time.Time, len(urls)) + + ctx, cancel := context.WithTimeout(context.Background(), timeout) + defer cancel() + + for _, url := range urls { + go func(url string) { + scopedLogger := t.l.With(). + Str("http_url", url). + Logger() + + startTime := time.Now() + now, err, response := queryHttpTime( + ctx, + url, + timeout, + ) + duration := time.Since(startTime) + + var status int + if response != nil { + status = response.StatusCode + } + + if err == nil { + requestId := response.Header.Get("X-Request-Id") + if requestId != "" { + requestId = response.Header.Get("X-Msedge-Ref") + } + if requestId == "" { + requestId = response.Header.Get("Cf-Ray") + } + scopedLogger.Info(). + Str("time", now.Format(time.RFC3339)). + Int("status", status). + Str("request_id", requestId). + Str("time_taken", duration.String()). + Msg("HTTP server returned time") + + cancel() + results <- now + } else if !errors.Is(err, context.Canceled) { + scopedLogger.Warn(). + Str("error", err.Error()). + Int("status", status). + Msg("failed to query HTTP server") + } + }(url) + } + + return <-results +} + func queryHttpTime( + ctx context.Context, url string, timeout time.Duration, ) (now *time.Time, err error, response *http.Response) { client := http.Client{ Timeout: timeout, } - resp, err := client.Head(url) + req, err := http.NewRequestWithContext(ctx, "GET", url, nil) + if err != nil { + return nil, err, nil + } + resp, err := client.Do(req) if err != nil { return nil, err, nil } @@ -23,32 +103,3 @@ func queryHttpTime( } return &parsedTime, nil, resp } - -func (t *TimeSync) queryAllHttpTime() (now *time.Time) { - for _, url := range t.httpUrls { - now, err, response := queryHttpTime(url, timeSyncTimeout) - - var status string - if response != nil { - status = response.Status - } - - scopedLogger := t.l.With(). - Str("http_url", url). - Str("status", status). - Logger() - - if err == nil { - scopedLogger.Info(). - Str("time", now.Format(time.RFC3339)). - Msg("HTTP server returned time") - return now - } else { - scopedLogger.Error(). - Str("error", err.Error()). - Msg("failed to query HTTP server") - } - } - - return nil -} diff --git a/internal/timesync/ntp.go b/internal/timesync/ntp.go index 9bc9812..eb15ff9 100644 --- a/internal/timesync/ntp.go +++ b/internal/timesync/ntp.go @@ -1,38 +1,61 @@ package timesync import ( + "math/rand/v2" "time" "github.com/beevik/ntp" ) func (t *TimeSync) queryNetworkTime() (now *time.Time) { - for _, server := range t.ntpServers { - now, err, response := queryNtpServer(server, timeSyncTimeout) + chunkSize := 4 + ntpServers := t.ntpServers - scopedLogger := t.l.With(). - Str("server", server). - Logger() + // shuffle the ntp servers to avoid always querying the same servers + rand.Shuffle(len(ntpServers), func(i, j int) { ntpServers[i], ntpServers[j] = ntpServers[j], ntpServers[i] }) - if err == nil { - scopedLogger.Info(). - Str("time", now.Format(time.RFC3339)). - Str("reference", response.ReferenceString()). - Str("rtt", response.RTT.String()). - Str("clockOffset", response.ClockOffset.String()). - Uint8("stratum", response.Stratum). - Msg("NTP server returned time") - return now - } else { - scopedLogger.Error(). - Str("error", err.Error()). - Msg("failed to query NTP server") + for i := 0; i < len(ntpServers); i += chunkSize { + chunk := ntpServers[i:min(i+chunkSize, len(ntpServers))] + results := t.queryMultipleNTP(chunk, timeSyncTimeout) + if results != nil { + return results } } return nil } +func (t *TimeSync) queryMultipleNTP(servers []string, timeout time.Duration) (now *time.Time) { + results := make(chan *time.Time, len(servers)) + + for _, server := range servers { + go func(server string) { + scopedLogger := t.l.With(). + Str("server", server). + Logger() + + now, err, response := queryNtpServer(server, timeout) + + if err == nil { + scopedLogger.Info(). + Str("time", now.Format(time.RFC3339)). + Str("reference", response.ReferenceString()). + Str("rtt", response.RTT.String()). + Str("clockOffset", response.ClockOffset.String()). + Uint8("stratum", response.Stratum). + Msg("NTP server returned time") + results <- now + } else { + scopedLogger.Warn(). + Str("error", err.Error()). + Msg("failed to query NTP server") + } + }(server) + } + + return <-results +} + func queryNtpServer(server string, timeout time.Duration) (now *time.Time, err error, response *ntp.Response) { resp, err := ntp.QueryWithOptions(server, ntp.QueryOptions{Timeout: timeout}) if err != nil { diff --git a/timesync.go b/timesync.go index e31f908..ed95ffd 100644 --- a/timesync.go +++ b/timesync.go @@ -10,12 +10,24 @@ import ( var ( timeSync *timesync.TimeSync defaultNTPServers = []string{ - "time.cloudflare.com", "time.apple.com", + "time.aws.com", + "time.windows.com", + "time.google.com", + "162.159.200.123", // time.cloudflare.com + "0.pool.ntp.org", + "1.pool.ntp.org", + "2.pool.ntp.org", + "3.pool.ntp.org", } defaultHTTPUrls = []string{ - "http://apple.com", - "http://cloudflare.com", + "http://www.gstatic.com/generate_204", + "http://cp.cloudflare.com/", + "http://edge-http.microsoft.com/captiveportal/generate_204", + // Firefox, Apple, and Microsoft have inconsistent results, so we don't use it + // "http://detectportal.firefox.com/", + // "http://www.apple.com/library/test/success.html", + // "http://www.msftconnecttest.com/connecttest.txt", } builtTimestamp string ) diff --git a/ui/src/routes/devices.$id.settings.network.tsx b/ui/src/routes/devices.$id.settings.network.tsx index c1e8468..c5f4fe4 100644 --- a/ui/src/routes/devices.$id.settings.network.tsx +++ b/ui/src/routes/devices.$id.settings.network.tsx @@ -84,10 +84,9 @@ export default function SettingsNetworkRoute() { notifications.error("Failed to renew lease: " + resp.error.message); } else { notifications.success("DHCP lease renewed"); - getNetworkState(); } }); - }, [send, getNetworkState]); + }, [send]); useEffect(() => { getNetworkState(); @@ -320,7 +319,7 @@ export default function SettingsNetworkRoute() { )} -