ascii-chat 0.8.38
Real-time terminal-based video chat with ASCII art conversion
Loading...
Searching...
No Matches
update_checker.c File Reference

Update checker implementation with GitHub API and cache management. More...

Go to the source code of this file.

Macros

#define UPDATE_CHECK_CACHE_FILENAME   "last_update_check"
 
#define UPDATE_CHECK_CACHE_MAX_AGE_SECONDS   (7 * 24 * 60 * 60)
 
#define DNS_TIMEOUT_SECONDS   2
 
#define GITHUB_API_HOSTNAME   "api.github.com"
 
#define GITHUB_RELEASES_PATH   "/repos/zfogg/ascii-chat/releases/latest"
 

Functions

asciichat_error_t update_check_load_cache (update_check_result_t *result)
 
asciichat_error_t update_check_save_cache (const update_check_result_t *result)
 
bool update_check_is_cache_fresh (const update_check_result_t *result)
 
asciichat_error_t update_check_perform (update_check_result_t *result)
 
install_method_t update_check_detect_install_method (void)
 
void update_check_get_upgrade_suggestion (install_method_t method, const char *latest_version, char *buffer, size_t buffer_size)
 
void update_check_format_notification (const update_check_result_t *result, char *buffer, size_t buffer_size)
 
asciichat_error_t update_check_startup (update_check_result_t *result)
 

Detailed Description

Update checker implementation with GitHub API and cache management.

Definition in file update_checker.c.

Macro Definition Documentation

◆ DNS_TIMEOUT_SECONDS

#define DNS_TIMEOUT_SECONDS   2

Definition at line 34 of file update_checker.c.

◆ GITHUB_API_HOSTNAME

#define GITHUB_API_HOSTNAME   "api.github.com"

Definition at line 37 of file update_checker.c.

◆ GITHUB_RELEASES_PATH

#define GITHUB_RELEASES_PATH   "/repos/zfogg/ascii-chat/releases/latest"

Definition at line 38 of file update_checker.c.

◆ UPDATE_CHECK_CACHE_FILENAME

#define UPDATE_CHECK_CACHE_FILENAME   "last_update_check"

Definition at line 28 of file update_checker.c.

◆ UPDATE_CHECK_CACHE_MAX_AGE_SECONDS

#define UPDATE_CHECK_CACHE_MAX_AGE_SECONDS   (7 * 24 * 60 * 60)

Definition at line 31 of file update_checker.c.

Function Documentation

◆ update_check_detect_install_method()

install_method_t update_check_detect_install_method ( void  )

Definition at line 387 of file update_checker.c.

387 {
388 if (is_homebrew_install()) {
389 return INSTALL_METHOD_HOMEBREW;
390 }
391
392 if (is_arch_linux()) {
393 return INSTALL_METHOD_ARCH_AUR;
394 }
395
396 // Default to GitHub releases
397 return INSTALL_METHOD_GITHUB;
398}

Referenced by update_check_format_notification().

◆ update_check_format_notification()

void update_check_format_notification ( const update_check_result_t *  result,
char *  buffer,
size_t  buffer_size 
)

Definition at line 429 of file update_checker.c.

429 {
430 if (!result || !buffer || buffer_size == 0) {
431 return;
432 }
433
434 // Get upgrade suggestion
435 install_method_t method = update_check_detect_install_method();
436 char suggestion[512];
437 update_check_get_upgrade_suggestion(method, result->latest_version, suggestion, sizeof(suggestion));
438
439 // Format: "Update available: v0.8.1 (f8dc35e1) → v0.9.0 (a1b2c3d4). Run: brew upgrade ascii-chat"
440 snprintf(buffer, buffer_size, "Update available: %s (%.8s) → %s (%.8s). %s%s", result->current_version,
441 result->current_sha, result->latest_version, result->latest_sha,
442 (method == INSTALL_METHOD_GITHUB || method == INSTALL_METHOD_UNKNOWN) ? "Download: " : "Run: ", suggestion);
443}
int buffer_size
Size of circular buffer.
Definition grep.c:84
void update_check_get_upgrade_suggestion(install_method_t method, const char *latest_version, char *buffer, size_t buffer_size)
install_method_t update_check_detect_install_method(void)

References buffer_size, update_check_detect_install_method(), and update_check_get_upgrade_suggestion().

Referenced by action_check_update_immediate(), and main().

◆ update_check_get_upgrade_suggestion()

void update_check_get_upgrade_suggestion ( install_method_t  method,
const char *  latest_version,
char *  buffer,
size_t  buffer_size 
)

Definition at line 400 of file update_checker.c.

401 {
402 if (!buffer || buffer_size == 0) {
403 return;
404 }
405
406 switch (method) {
407 case INSTALL_METHOD_HOMEBREW:
408 snprintf(buffer, buffer_size, "brew upgrade ascii-chat");
409 break;
410
411 case INSTALL_METHOD_ARCH_AUR:
412 // Check if paru is available, otherwise suggest yay
413 if (system("command -v paru >/dev/null 2>&1") == 0) {
414 snprintf(buffer, buffer_size, "paru -S ascii-chat");
415 } else {
416 snprintf(buffer, buffer_size, "yay -S ascii-chat");
417 }
418 break;
419
420 case INSTALL_METHOD_GITHUB:
421 case INSTALL_METHOD_UNKNOWN:
422 default:
423 snprintf(buffer, buffer_size, "https://github.com/zfogg/ascii-chat/releases/tag/%s",
424 latest_version ? latest_version : "latest");
425 break;
426 }
427}

References buffer_size.

Referenced by update_check_format_notification().

◆ update_check_is_cache_fresh()

bool update_check_is_cache_fresh ( const update_check_result_t *  result)

Definition at line 172 of file update_checker.c.

172 {
173 if (!result || result->last_check_time == 0) {
174 return false;
175 }
176
177 time_t now = time(NULL);
178 time_t age = now - result->last_check_time;
179
181}
#define UPDATE_CHECK_CACHE_MAX_AGE_SECONDS

References UPDATE_CHECK_CACHE_MAX_AGE_SECONDS.

Referenced by update_check_startup().

◆ update_check_load_cache()

asciichat_error_t update_check_load_cache ( update_check_result_t *  result)

Definition at line 61 of file update_checker.c.

61 {
62 if (!result) {
63 return SET_ERRNO(ERROR_INVALID_PARAM, "NULL result pointer");
64 }
65
66 memset(result, 0, sizeof(*result));
67
68 char *cache_path = get_cache_file_path();
69 if (!cache_path) {
70 return SET_ERRNO(ERROR_FILE_OPERATION, "Could not determine cache file path");
71 }
72
73 FILE *f = platform_fopen(cache_path, "r");
74 if (!f) {
75 const char *error_msg = file_read_error_message(cache_path);
76 SAFE_FREE(cache_path);
77 return SET_ERRNO(ERROR_FILE_OPERATION, "%s", error_msg);
78 }
79
80 // Read line 1: timestamp
81 char line[512];
82 if (!fgets(line, sizeof(line), f)) {
83 fclose(f);
84 SAFE_FREE(cache_path);
85 return SET_ERRNO(ERROR_FILE_OPERATION, "Failed to read timestamp from cache");
86 }
87 result->last_check_time = (time_t)atoll(line);
88
89 // Read line 2: latest version (may be empty if check failed)
90 if (fgets(line, sizeof(line), f)) {
91 // Remove newline
92 size_t len = strlen(line);
93 if (len > 0 && line[len - 1] == '\n') {
94 line[len - 1] = '\0';
95 }
96 SAFE_STRNCPY(result->latest_version, line, sizeof(result->latest_version));
97 }
98
99 // Read line 3: latest SHA (may be empty if check failed)
100 if (fgets(line, sizeof(line), f)) {
101 // Remove newline
102 size_t len = strlen(line);
103 if (len > 0 && line[len - 1] == '\n') {
104 line[len - 1] = '\0';
105 }
106 SAFE_STRNCPY(result->latest_sha, line, sizeof(result->latest_sha));
107 }
108
109 fclose(f);
110
111 // Fill in current version/SHA
112 SAFE_STRNCPY(result->current_version, ASCII_CHAT_VERSION_STRING, sizeof(result->current_version));
113 SAFE_STRNCPY(result->current_sha, ASCII_CHAT_GIT_COMMIT_HASH, sizeof(result->current_sha));
114
115 // Determine if update is available using version comparison (if we have cached data)
116 if (result->latest_version[0] != '\0') {
117 semantic_version_t current_ver = version_parse(result->current_version);
118 semantic_version_t latest_ver = version_parse(result->latest_version);
119
120 if (current_ver.valid && latest_ver.valid) {
121 int cmp = version_compare(latest_ver, current_ver);
122 result->update_available = (cmp > 0); // Update available if latest > current
123 result->check_succeeded = true;
124 } else {
125 // Cache contains invalid version data - delete it
126 log_warn("Update cache contains invalid version data (current:%s, latest:%s) - deleting corrupted cache",
127 result->current_version, result->latest_version);
128 if (remove(cache_path) != 0) {
129 log_warn("Failed to delete corrupted cache file: %s", cache_path);
130 }
131 SAFE_FREE(cache_path);
132 return SET_ERRNO(ERROR_FORMAT, "Corrupted cache file deleted");
133 }
134 }
135
136 SAFE_FREE(cache_path);
137 return ASCIICHAT_OK;
138}
const char * file_read_error_message(const char *path)
Definition filesystem.c:15
int version_compare(semantic_version_t a, semantic_version_t b)
Definition version.c:112
semantic_version_t version_parse(const char *version_string)
Definition version.c:49
FILE * platform_fopen(const char *filename, const char *mode)

References file_read_error_message(), platform_fopen(), version_compare(), and version_parse().

Referenced by update_check_startup().

◆ update_check_perform()

asciichat_error_t update_check_perform ( update_check_result_t *  result)

Definition at line 268 of file update_checker.c.

268 {
269 if (!result) {
270 return SET_ERRNO(ERROR_INVALID_PARAM, "NULL result pointer");
271 }
272
273 memset(result, 0, sizeof(*result));
274
275 // Fill in current version and SHA
276 SAFE_STRNCPY(result->current_version, ASCII_CHAT_VERSION_STRING, sizeof(result->current_version));
277 SAFE_STRNCPY(result->current_sha, ASCII_CHAT_GIT_COMMIT_HASH, sizeof(result->current_sha));
278 result->last_check_time = time(NULL);
279
280 // Test DNS connectivity first
282 log_warn("No internet connectivity detected, skipping update check");
283 // Don't update cache - we'll retry when online
284 return SET_ERRNO(ERROR_NETWORK, "DNS connectivity test failed");
285 }
286
287 // Fetch latest release from GitHub API
288 log_info("Checking for updates from GitHub releases...");
290 if (!response) {
291 log_warn("Failed to fetch GitHub releases API (timeout or network error)");
292 // Mark as checked even though it failed (prevents repeated offline attempts)
293 result->check_succeeded = false;
295 return SET_ERRNO(ERROR_NETWORK, "Failed to fetch GitHub releases");
296 }
297
298 // Parse JSON response
299 char latest_tag[64] = {0};
300 char latest_sha[41] = {0};
301 char release_url[512] = {0};
302
303 if (!parse_github_release_json(response, latest_tag, sizeof(latest_tag), latest_sha, sizeof(latest_sha), release_url,
304 sizeof(release_url))) {
305 log_error("Failed to parse GitHub API response");
306 SAFE_FREE(response);
307 result->check_succeeded = false;
309 return SET_ERRNO(ERROR_FORMAT, "Failed to parse GitHub API JSON");
310 }
311
312 SAFE_FREE(response);
313
314 // Fill in result
315 SAFE_STRNCPY(result->latest_version, latest_tag, sizeof(result->latest_version));
316 SAFE_STRNCPY(result->latest_sha, latest_sha, sizeof(result->latest_sha));
317 SAFE_STRNCPY(result->release_url, release_url, sizeof(result->release_url));
318 result->check_succeeded = true;
319
320 // Compare versions semantically
321 semantic_version_t current_ver = version_parse(result->current_version);
322 semantic_version_t latest_ver = version_parse(result->latest_version);
323
324 if (!current_ver.valid || !latest_ver.valid) {
325 log_warn("Failed to parse version strings for comparison (current: %s, latest: %s)", result->current_version,
326 result->latest_version);
327 result->update_available = false;
328 } else {
329 int cmp = version_compare(latest_ver, current_ver);
330 result->update_available = (cmp > 0); // Update available if latest > current
331 }
332
333 if (result->update_available) {
334 log_info("Update available: %s (%.*s) → %s (%.*s)", result->current_version, 8, result->current_sha,
335 result->latest_version, 8, result->latest_sha);
336 } else {
337 log_info("Already on latest version: %s (%.*s)", result->current_version, 8, result->current_sha);
338 }
339
340 // Save to cache
342
343 return ASCIICHAT_OK;
344}
bool dns_test_connectivity(const char *hostname)
Definition dns.c:11
char * https_get(const char *hostname, const char *path)
#define GITHUB_API_HOSTNAME
#define GITHUB_RELEASES_PATH
asciichat_error_t update_check_save_cache(const update_check_result_t *result)

References dns_test_connectivity(), GITHUB_API_HOSTNAME, GITHUB_RELEASES_PATH, https_get(), update_check_save_cache(), version_compare(), and version_parse().

Referenced by action_check_update_immediate(), and update_check_startup().

◆ update_check_save_cache()

asciichat_error_t update_check_save_cache ( const update_check_result_t *  result)

Definition at line 140 of file update_checker.c.

140 {
141 if (!result) {
142 return SET_ERRNO(ERROR_INVALID_PARAM, "NULL result pointer");
143 }
144
145 char *cache_path = get_cache_file_path();
146 if (!cache_path) {
147 return SET_ERRNO(ERROR_FILE_OPERATION, "Could not determine cache file path");
148 }
149
150 FILE *f = platform_fopen(cache_path, "w");
151 if (!f) {
152 const char *error_msg = file_write_error_message(cache_path);
153 SAFE_FREE(cache_path);
154 return SET_ERRNO(ERROR_FILE_OPERATION, "%s", error_msg);
155 }
156
157 // Write timestamp
158 fprintf(f, "%lld\n", (long long)result->last_check_time);
159
160 // Write version (may be empty if check failed)
161 fprintf(f, "%s\n", result->latest_version);
162
163 // Write SHA (may be empty if check failed)
164 fprintf(f, "%s\n", result->latest_sha);
165
166 fclose(f);
167 SAFE_FREE(cache_path);
168
169 return ASCIICHAT_OK;
170}
const char * file_write_error_message(const char *path)
Definition filesystem.c:33

References file_write_error_message(), and platform_fopen().

Referenced by update_check_perform().

◆ update_check_startup()

asciichat_error_t update_check_startup ( update_check_result_t *  result)

Definition at line 445 of file update_checker.c.

445 {
446 update_check_result_t local_result;
447 update_check_result_t *target = result ? result : &local_result;
448
449 // Try to load from cache first
450 asciichat_error_t cache_err = update_check_load_cache(target);
451 if (cache_err == ASCIICHAT_OK && update_check_is_cache_fresh(target)) {
452 // Cache is fresh, use it
453 log_debug("Using cached update check result (age: %.1f days)",
454 (time(NULL) - target->last_check_time) / (double)SEC_PER_DAY);
455 return ASCIICHAT_OK;
456 }
457
458 // Cache is stale or missing, perform fresh check
459 log_debug("Performing automatic update check (cache %s)", cache_err == ASCIICHAT_OK ? "stale" : "missing");
460 asciichat_error_t check_err = update_check_perform(target);
461 if (check_err != ASCIICHAT_OK) {
462 // Check failed, but don't fail startup
463 log_debug("Automatic update check failed (continuing startup)");
464 return check_err;
465 }
466
467 return ASCIICHAT_OK;
468}
bool update_check_is_cache_fresh(const update_check_result_t *result)
asciichat_error_t update_check_perform(update_check_result_t *result)
asciichat_error_t update_check_load_cache(update_check_result_t *result)

References update_check_is_cache_fresh(), update_check_load_cache(), and update_check_perform().

Referenced by main().