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()) {
|
} else if (!inProgress && hasAutoOpened()) {
|
||||||
setHasAutoOpened(false);
|
setHasAutoOpened(false);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (_error) {
|
||||||
// Silently ignore polling errors
|
// Silently ignore polling errors
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -2061,7 +2061,7 @@ const Settings: Component<SettingsProps> = (props) => {
|
||||||
showError('Invalid backup file format. Expected encrypted data in "data" field.');
|
showError('Invalid backup file format. Expected encrypted data in "data" field.');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
} catch (parseError) {
|
} catch (_parseError) {
|
||||||
// Not JSON - treat entire contents as raw base64 from CLI export
|
// Not JSON - treat entire contents as raw base64 from CLI export
|
||||||
encryptedData = fileContent.trim();
|
encryptedData = fileContent.trim();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -19,7 +19,7 @@ const getInitialViewMode = (): MetricsViewMode => {
|
||||||
if (stored === 'sparklines' || stored === 'bars') {
|
if (stored === 'sparklines' || stored === 'bars') {
|
||||||
return stored;
|
return stored;
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (_err) {
|
||||||
// Ignore localStorage errors
|
// Ignore localStorage errors
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -188,6 +188,13 @@ func (rw *responseWriter) Hijack() (net.Conn, *bufio.ReadWriter, error) {
|
||||||
return hijacker.Hijack()
|
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
|
// NewAPIError creates a new API error
|
||||||
func NewAPIError(statusCode int, code, message string) error {
|
func NewAPIError(statusCode int, code, message string) error {
|
||||||
return &APIError{
|
return &APIError{
|
||||||
|
|
|
||||||
|
|
@ -15,6 +15,7 @@ import (
|
||||||
"os/exec"
|
"os/exec"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"runtime"
|
"runtime"
|
||||||
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
@ -58,7 +59,11 @@ type UpdateInfo struct {
|
||||||
Warning string `json:"warning,omitempty"`
|
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
|
// Manager handles update operations
|
||||||
type Manager struct {
|
type Manager struct {
|
||||||
|
|
@ -1137,6 +1142,10 @@ func (m *Manager) updateStatus(status string, progress int, message string, err
|
||||||
if m.sseBroadcast != nil {
|
if m.sseBroadcast != nil {
|
||||||
m.sseBroadcast.Broadcast(statusCopy)
|
m.sseBroadcast.Broadcast(statusCopy)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if delay := statusDelayForStage(status); delay > 0 {
|
||||||
|
time.Sleep(delay)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// sseHeartbeatLoop sends periodic heartbeats to SSE clients
|
// sseHeartbeatLoop sends periodic heartbeats to SSE clients
|
||||||
|
|
@ -1168,6 +1177,37 @@ func sanitizeError(err error) string {
|
||||||
return errMsg
|
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
|
// cleanupOldTempDirs removes old pulse-update-* temp directories from previous runs
|
||||||
func (m *Manager) cleanupOldTempDirs() {
|
func (m *Manager) cleanupOldTempDirs() {
|
||||||
// Check multiple locations where temp dirs might exist
|
// 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) {
|
if time.Now().After(deadline) {
|
||||||
t.Fatalf("update did not complete within %s (last status: %+v)", timeout, status)
|
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
|
# Mock mode for faster testing
|
||||||
- PULSE_MOCK_MODE=true
|
- PULSE_MOCK_MODE=true
|
||||||
- PULSE_ALLOW_DOCKER_UPDATES=true
|
- PULSE_ALLOW_DOCKER_UPDATES=true
|
||||||
|
- PULSE_UPDATE_STAGE_DELAY_MS=250
|
||||||
# Pre-configure authentication to bypass first-run setup
|
# Pre-configure authentication to bypass first-run setup
|
||||||
- PULSE_AUTH_USER=admin
|
- PULSE_AUTH_USER=admin
|
||||||
- PULSE_AUTH_PASS=admin
|
- PULSE_AUTH_PASS=admin
|
||||||
|
|
|
||||||
|
|
@ -57,6 +57,15 @@ func getenvDefault(key, fallback string) string {
|
||||||
return fallback
|
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() {
|
func (rl *rateLimiter) cleanup() {
|
||||||
rl.mu.Lock()
|
rl.mu.Lock()
|
||||||
defer rl.mu.Unlock()
|
defer rl.mu.Unlock()
|
||||||
|
|
@ -122,6 +131,7 @@ func main() {
|
||||||
// In-memory storage for tarballs and checksums
|
// In-memory storage for tarballs and checksums
|
||||||
tarballs := make(map[string][]byte)
|
tarballs := make(map[string][]byte)
|
||||||
checksums := make(map[string]string)
|
checksums := make(map[string]string)
|
||||||
|
checksumEntries := make(map[string][]string)
|
||||||
|
|
||||||
latestTag := getenvDefault("MOCK_LATEST_VERSION", "v99.0.0")
|
latestTag := getenvDefault("MOCK_LATEST_VERSION", "v99.0.0")
|
||||||
prevTag := getenvDefault("MOCK_PREVIOUS_VERSION", "v98.5.0")
|
prevTag := getenvDefault("MOCK_PREVIOUS_VERSION", "v98.5.0")
|
||||||
|
|
@ -169,6 +179,9 @@ func main() {
|
||||||
}
|
}
|
||||||
|
|
||||||
checksums[filename] = checksum
|
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
|
// Add download URLs to release
|
||||||
rel.Assets = []struct {
|
rel.Assets = []struct {
|
||||||
|
|
@ -242,27 +255,44 @@ func main() {
|
||||||
version := parts[0]
|
version := parts[0]
|
||||||
file := parts[1]
|
file := parts[1]
|
||||||
|
|
||||||
if file == "checksums.txt" {
|
if isChecksumFilename(file) {
|
||||||
// Generate checksums.txt
|
// Generate checksums.txt
|
||||||
var buf bytes.Buffer
|
var buf bytes.Buffer
|
||||||
for fname, chksum := range checksums {
|
entries, ok := checksumEntries[version]
|
||||||
if strings.Contains(fname, version) {
|
if !ok {
|
||||||
buf.WriteString(fmt.Sprintf("%s %s\n", chksum, fname))
|
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.Header().Set("Content-Type", "text/plain")
|
||||||
w.Write(buf.Bytes())
|
w.Write(buf.Bytes())
|
||||||
log.Printf("Served checksums for version %s", version)
|
log.Printf("Served checksums for version %s (requested %s)", version, file)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Serve tarball
|
// Serve tarball (strictly match requested filename first)
|
||||||
filename := fmt.Sprintf("pulse-%s-linux-amd64.tar.gz", version)
|
tarball, ok := tarballs[file]
|
||||||
tarball, ok := tarballs[filename]
|
|
||||||
if !ok {
|
if !ok {
|
||||||
w.WriteHeader(http.StatusNotFound)
|
// Fallback to canonical filename derived from version (without leading v)
|
||||||
log.Printf("Tarball not found: %s", filename)
|
trimmedVersion := strings.TrimPrefix(version, "v")
|
||||||
return
|
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
|
// Mark as stale if requested
|
||||||
|
|
@ -274,7 +304,7 @@ func main() {
|
||||||
w.Header().Set("Content-Type", "application/gzip")
|
w.Header().Set("Content-Type", "application/gzip")
|
||||||
w.Header().Set("Content-Length", strconv.Itoa(len(tarball)))
|
w.Header().Set("Content-Length", strconv.Itoa(len(tarball)))
|
||||||
w.Write(tarball)
|
w.Write(tarball)
|
||||||
log.Printf("Served tarball: %s", filename)
|
log.Printf("Served tarball: %s (version %s)", file, version)
|
||||||
})
|
})
|
||||||
|
|
||||||
// Health check
|
// Health check
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue