@@ -5,10 +5,12 @@ import (
5
5
"encoding/json"
6
6
"fmt"
7
7
"os"
8
+ "regexp"
8
9
"strings"
9
10
"time"
10
11
11
12
"github.com/adrg/xdg"
13
+ "github.com/gofrs/flock"
12
14
"github.com/google/uuid"
13
15
"golang.org/x/mod/semver"
14
16
@@ -47,7 +49,13 @@ func NewUpdateChecker(versionClient VersionClient) (UpdateChecker, error) {
47
49
var contents updateFile
48
50
err = json .Unmarshal (rawContents , & contents )
49
51
if err != nil {
50
- return nil , fmt .Errorf ("failed to deserialize update file: %w" , err )
52
+ // If the file is corrupted, attempt to recover
53
+ if recoveredFile , recoverErr := recoverCorruptedJSON (rawContents ); recoverErr == nil {
54
+ contents = recoveredFile
55
+ // Note: Update file is corrupted, attempting to preserve instance ID
56
+ } else {
57
+ return nil , fmt .Errorf ("failed to deserialize update file: %w" , err )
58
+ }
51
59
}
52
60
instanceID = contents .InstanceID
53
61
previousVersion = contents .LatestVersion
@@ -106,7 +114,13 @@ func (d *defaultUpdateChecker) CheckLatestVersion() error {
106
114
}
107
115
} else {
108
116
if err := json .Unmarshal (rawContents , & currentFile ); err != nil {
109
- return fmt .Errorf ("failed to deserialize update file: %w" , err )
117
+ // If the file is corrupted, attempt to recover
118
+ if recoveredFile , recoverErr := recoverCorruptedJSON (rawContents ); recoverErr == nil {
119
+ currentFile = recoveredFile
120
+ // Note: Recovered corrupted update file, preserving instance ID
121
+ } else {
122
+ return fmt .Errorf ("failed to deserialize update file: %w" , err )
123
+ }
110
124
}
111
125
112
126
// Initialize components map if it doesn't exist (for backward compatibility)
@@ -151,6 +165,15 @@ func (d *defaultUpdateChecker) CheckLatestVersion() error {
151
165
return fmt .Errorf ("failed to marshal updated data: %w" , err )
152
166
}
153
167
168
+ // Acquire lock just before writing to minimize lock time
169
+ lockFile := flock .New (d .updateFilePath + ".lock" )
170
+ if err := lockFile .Lock (); err != nil {
171
+ return fmt .Errorf ("failed to acquire lock on update file: %w" , err )
172
+ }
173
+ defer func () {
174
+ _ = lockFile .Unlock ()
175
+ }()
176
+
154
177
if err := os .WriteFile (d .updateFilePath , updatedData , 0600 ); err != nil {
155
178
return fmt .Errorf ("failed to write updated file: %w" , err )
156
179
}
@@ -181,3 +204,20 @@ func notifyIfUpdateAvailable(current, latest string) {
181
204
fmt .Fprintf (os .Stderr , "A new version of ToolHive is available: %s\n Currently running: %s\n " , latest , current )
182
205
}
183
206
}
207
+
208
+ // recoverCorruptedJSON attempts to recover from common JSON corruption issues
209
+ // while preserving the instance_id to avoid regenerating it.
210
+ func recoverCorruptedJSON (rawContents []byte ) (updateFile , error ) {
211
+ content := string (rawContents )
212
+
213
+ // Extract the instance_id from the corrupted JSON and regenerate the file
214
+ instanceIDRegex := regexp .MustCompile (`"instance_id":"([^"]+)"` )
215
+ if matches := instanceIDRegex .FindStringSubmatch (content ); len (matches ) > 1 {
216
+ return updateFile {
217
+ InstanceID : matches [1 ],
218
+ Components : make (map [string ]componentInfo ),
219
+ }, nil
220
+ }
221
+
222
+ return updateFile {}, fmt .Errorf ("unable to recover corrupted JSON" )
223
+ }
0 commit comments