Improve update integration diagnostics
This commit is contained in:
parent
88d0aeebdf
commit
bb55144637
8 changed files with 95 additions and 17 deletions
|
|
@ -140,7 +140,7 @@ function GlobalUpdateProgressWatcher() {
|
|||
} else if (!inProgress && hasAutoOpened()) {
|
||||
setHasAutoOpened(false);
|
||||
}
|
||||
} catch (error) {
|
||||
} catch (_error) {
|
||||
// Silently ignore polling errors
|
||||
}
|
||||
};
|
||||
|
|
|
|||
|
|
@ -2061,7 +2061,7 @@ const Settings: Component<SettingsProps> = (props) => {
|
|||
showError('Invalid backup file format. Expected encrypted data in "data" field.');
|
||||
return;
|
||||
}
|
||||
} catch (parseError) {
|
||||
} catch (_parseError) {
|
||||
// Not JSON - treat entire contents as raw base64 from CLI export
|
||||
encryptedData = fileContent.trim();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@ const getInitialViewMode = (): MetricsViewMode => {
|
|||
if (stored === 'sparklines' || stored === 'bars') {
|
||||
return stored;
|
||||
}
|
||||
} catch (err) {
|
||||
} catch (_err) {
|
||||
// Ignore localStorage errors
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -188,6 +188,13 @@ func (rw *responseWriter) Hijack() (net.Conn, *bufio.ReadWriter, error) {
|
|||
return hijacker.Hijack()
|
||||
}
|
||||
|
||||
// Flush implements http.Flusher when the underlying writer supports it.
|
||||
func (rw *responseWriter) Flush() {
|
||||
if flusher, ok := rw.ResponseWriter.(http.Flusher); ok {
|
||||
flusher.Flush()
|
||||
}
|
||||
}
|
||||
|
||||
// NewAPIError creates a new API error
|
||||
func NewAPIError(statusCode int, code, message string) error {
|
||||
return &APIError{
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@ import (
|
|||
"os/exec"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
|
@ -58,7 +59,11 @@ type UpdateInfo struct {
|
|||
Warning string `json:"warning,omitempty"`
|
||||
}
|
||||
|
||||
var errGitHubRateLimited = errors.New("GitHub API rate limit exceeded")
|
||||
var (
|
||||
errGitHubRateLimited = errors.New("GitHub API rate limit exceeded")
|
||||
stageDelayOnce sync.Once
|
||||
stageDelayValue time.Duration
|
||||
)
|
||||
|
||||
// Manager handles update operations
|
||||
type Manager struct {
|
||||
|
|
@ -1137,6 +1142,10 @@ func (m *Manager) updateStatus(status string, progress int, message string, err
|
|||
if m.sseBroadcast != nil {
|
||||
m.sseBroadcast.Broadcast(statusCopy)
|
||||
}
|
||||
|
||||
if delay := statusDelayForStage(status); delay > 0 {
|
||||
time.Sleep(delay)
|
||||
}
|
||||
}
|
||||
|
||||
// sseHeartbeatLoop sends periodic heartbeats to SSE clients
|
||||
|
|
@ -1168,6 +1177,37 @@ func sanitizeError(err error) string {
|
|||
return errMsg
|
||||
}
|
||||
|
||||
func statusDelayForStage(status string) time.Duration {
|
||||
delay := configuredStageDelay()
|
||||
if delay == 0 {
|
||||
return 0
|
||||
}
|
||||
|
||||
switch status {
|
||||
case "downloading", "verifying", "extracting", "backing-up", "applying":
|
||||
return delay
|
||||
default:
|
||||
return 0
|
||||
}
|
||||
}
|
||||
|
||||
func configuredStageDelay() time.Duration {
|
||||
stageDelayOnce.Do(func() {
|
||||
value := strings.TrimSpace(os.Getenv("PULSE_UPDATE_STAGE_DELAY_MS"))
|
||||
if value == "" {
|
||||
return
|
||||
}
|
||||
ms, err := strconv.Atoi(value)
|
||||
if err != nil || ms <= 0 {
|
||||
log.Warn().Str("value", value).Msg("Invalid PULSE_UPDATE_STAGE_DELAY_MS, ignoring")
|
||||
return
|
||||
}
|
||||
stageDelayValue = time.Duration(ms) * time.Millisecond
|
||||
})
|
||||
|
||||
return stageDelayValue
|
||||
}
|
||||
|
||||
// cleanupOldTempDirs removes old pulse-update-* temp directories from previous runs
|
||||
func (m *Manager) cleanupOldTempDirs() {
|
||||
// Check multiple locations where temp dirs might exist
|
||||
|
|
|
|||
|
|
@ -181,7 +181,7 @@ func waitForCompletion(t *testing.T, client *http.Client, baseURL string, timeou
|
|||
if time.Now().After(deadline) {
|
||||
t.Fatalf("update did not complete within %s (last status: %+v)", timeout, status)
|
||||
}
|
||||
time.Sleep(500 * time.Millisecond)
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -41,6 +41,7 @@ services:
|
|||
# Mock mode for faster testing
|
||||
- PULSE_MOCK_MODE=true
|
||||
- PULSE_ALLOW_DOCKER_UPDATES=true
|
||||
- PULSE_UPDATE_STAGE_DELAY_MS=250
|
||||
# Pre-configure authentication to bypass first-run setup
|
||||
- PULSE_AUTH_USER=admin
|
||||
- PULSE_AUTH_PASS=admin
|
||||
|
|
|
|||
|
|
@ -57,6 +57,15 @@ func getenvDefault(key, fallback string) string {
|
|||
return fallback
|
||||
}
|
||||
|
||||
func isChecksumFilename(name string) bool {
|
||||
switch strings.ToLower(name) {
|
||||
case "checksums.txt", "sha256sums", "sha256sums.txt":
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func (rl *rateLimiter) cleanup() {
|
||||
rl.mu.Lock()
|
||||
defer rl.mu.Unlock()
|
||||
|
|
@ -122,6 +131,7 @@ func main() {
|
|||
// In-memory storage for tarballs and checksums
|
||||
tarballs := make(map[string][]byte)
|
||||
checksums := make(map[string]string)
|
||||
checksumEntries := make(map[string][]string)
|
||||
|
||||
latestTag := getenvDefault("MOCK_LATEST_VERSION", "v99.0.0")
|
||||
prevTag := getenvDefault("MOCK_PREVIOUS_VERSION", "v98.5.0")
|
||||
|
|
@ -169,6 +179,9 @@ func main() {
|
|||
}
|
||||
|
||||
checksums[filename] = checksum
|
||||
entry := fmt.Sprintf("%s %s", checksum, filename)
|
||||
checksumEntries[version] = append(checksumEntries[version], entry)
|
||||
checksumEntries["v"+version] = append(checksumEntries["v"+version], entry)
|
||||
|
||||
// Add download URLs to release
|
||||
rel.Assets = []struct {
|
||||
|
|
@ -242,27 +255,44 @@ func main() {
|
|||
version := parts[0]
|
||||
file := parts[1]
|
||||
|
||||
if file == "checksums.txt" {
|
||||
if isChecksumFilename(file) {
|
||||
// Generate checksums.txt
|
||||
var buf bytes.Buffer
|
||||
for fname, chksum := range checksums {
|
||||
if strings.Contains(fname, version) {
|
||||
buf.WriteString(fmt.Sprintf("%s %s\n", chksum, fname))
|
||||
entries, ok := checksumEntries[version]
|
||||
if !ok {
|
||||
trimmed := strings.TrimPrefix(version, "v")
|
||||
entries = checksumEntries[trimmed]
|
||||
}
|
||||
if len(entries) == 0 {
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
log.Printf("No checksums found for version %s", version)
|
||||
return
|
||||
}
|
||||
for _, line := range entries {
|
||||
buf.WriteString(line)
|
||||
if !strings.HasSuffix(line, "\n") {
|
||||
buf.WriteByte('\n')
|
||||
}
|
||||
}
|
||||
w.Header().Set("Content-Type", "text/plain")
|
||||
w.Write(buf.Bytes())
|
||||
log.Printf("Served checksums for version %s", version)
|
||||
log.Printf("Served checksums for version %s (requested %s)", version, file)
|
||||
return
|
||||
}
|
||||
|
||||
// Serve tarball
|
||||
filename := fmt.Sprintf("pulse-%s-linux-amd64.tar.gz", version)
|
||||
tarball, ok := tarballs[filename]
|
||||
// Serve tarball (strictly match requested filename first)
|
||||
tarball, ok := tarballs[file]
|
||||
if !ok {
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
log.Printf("Tarball not found: %s", filename)
|
||||
return
|
||||
// Fallback to canonical filename derived from version (without leading v)
|
||||
trimmedVersion := strings.TrimPrefix(version, "v")
|
||||
canonical := fmt.Sprintf("pulse-%s-linux-amd64.tar.gz", trimmedVersion)
|
||||
tarball, ok = tarballs[canonical]
|
||||
if !ok {
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
log.Printf("Tarball not found: %s (canonical %s)", file, canonical)
|
||||
return
|
||||
}
|
||||
file = canonical
|
||||
}
|
||||
|
||||
// Mark as stale if requested
|
||||
|
|
@ -274,7 +304,7 @@ func main() {
|
|||
w.Header().Set("Content-Type", "application/gzip")
|
||||
w.Header().Set("Content-Length", strconv.Itoa(len(tarball)))
|
||||
w.Write(tarball)
|
||||
log.Printf("Served tarball: %s", filename)
|
||||
log.Printf("Served tarball: %s (version %s)", file, version)
|
||||
})
|
||||
|
||||
// Health check
|
||||
|
|
|
|||
Loading…
Reference in a new issue