diff --git a/include/compiler.h b/include/compiler.h index 651ada3..be13bb0 100644 --- a/include/compiler.h +++ b/include/compiler.h @@ -24,7 +24,57 @@ #define COMPILER_H -int compilerRunRecipe(char *recipe, char *input, char *outputPath, void *context); +#include "libtcc.h" +#include "sigsegv.h" + +#if HAVE_SIGSEGV_RECOVERY +#include +#include +#if defined _WIN32 && !defined __CYGWIN__ +// Windows doesn't have sigset_t. +typedef int sigset_t; +#define sigemptyset(set) +#define sigprocmask(how,set,oldset) +#endif +#endif + + +enum CompilerErrorsE { + COMPILER_ERROR_NONE = 0, + COMPILER_ERROR_ALREADY_RUNNING, + COMPILER_ERROR_IN_CODE, + COMPILER_ERROR_CANNOT_RELOCATE, + COMPILER_ERROR_NO_ENTRYPOINT, + COMPILER_ERROR_SIGHANDLER_FAILED, + COMPILER_ERROR_SEGFAULT, + COMPILER_ERROR_COUNT +}; + + +struct CompilerContextS; + + +typedef void (*CompilerCallback)(struct CompilerContextS **context); + + +typedef struct CompilerContextS { + TCCState *s; + int compilerResult; + int programResult; + CompilerCallback callback; + void *userData; +#if HAVE_SIGSEGV_RECOVERY + volatile int runPass; + jmp_buf runJump; + sigset_t runSigSet; +#endif +} CompilerContextT; + + +void compilerDeleteContext(CompilerContextT **context); +gboolean compilerHadError(CompilerContextT **context); +CompilerContextT *compilerNewContext(CompilerCallback callback, void *userData); +void compilerRunRecipe(CompilerContextT *context, char *recipe, char *input, char *outputPath); #endif // COMPILER_H diff --git a/include/messages.h b/include/messages.h index f1e0eac..2300c48 100644 --- a/include/messages.h +++ b/include/messages.h @@ -35,6 +35,8 @@ typedef enum MessageTypesE MessageTypesT; void message(MessageTypesT level, char *format, ...); +void messageShutdown(void); +void messageStartup(void); -#endif //MESSAGES_H +#endif // MESSAGES_H diff --git a/src/compiler.c b/src/compiler.c index 39e9eeb..7bdae7d 100644 --- a/src/compiler.c +++ b/src/compiler.c @@ -20,10 +20,10 @@ */ +#include + #include "common.h" #include "compiler.h" -#include "libtcc.h" -#include "sigsegv.h" #include "project.h" #include "messages.h" #include "utils.h" @@ -31,156 +31,214 @@ #if HAVE_SIGSEGV_RECOVERY -#include -#include - -#if defined _WIN32 && !defined __CYGWIN__ -// Windows doesn't have sigset_t. -typedef int sigset_t; -#define sigemptyset(set) -#define sigprocmask(how,set,oldset) -#endif - -volatile int runPass = 0; -jmp_buf runRecipe; -sigset_t runSigSet; - static void sigHandlerContinuation(void *arg1, void *arg2, void *arg3); int sigHandler(void *faultAddress, int serious); - #endif +static CompilerContextT *_currentRunningContext = NULL; // Right now we can only have one compile running at a time. + char **___recipeTargets = NULL; static void compilerErrorHandler(void *opaque, const char *msg); -void recipeAddTarget(char *target); -void recipeMessage(char *format, ...); + +// Exposed to dynamic code. Not static. +void recipeAddTarget(char *target); +void recipeMessage(char *format, ...); -static void compilerErrorHandler(void *opaque, const char *msg) { - char *isWarning = strstr(msg, " warning: "); - - (void)opaque; - - message(isWarning == NULL ? MSG_ERROR : MSG_WARN, "%s", msg); +void compilerDeleteContext(CompilerContextT **context) { + CompilerContextT *c = *context; + tcc_delete(c->s); + DEL(c); } -// All the paths passed in here are expected to be complete and absolute. -int compilerRunRecipe(char *recipe, char *input, char *outputPath, void *context) { - TCCState *s; - char *oldLocation; - char c; - int x; - int result = -255; - int (*entry)(char *, char *); +static void compilerErrorHandler(void *opaque, const char *msg) { + (void)opaque; - ___recipeTargets = NULL; + message(strstr(msg, " warning: ") == NULL ? MSG_ERROR : MSG_WARN, "%s", msg); +} - s = tcc_new(); - if (!s) { + +gboolean compilerHadError(CompilerContextT **context) { + CompilerContextT *c = *context; + + if (c->compilerResult != COMPILER_ERROR_NONE) { + // Handle errors not handled elsewhere. + switch (c->compilerResult) { + case COMPILER_ERROR_ALREADY_RUNNING: + message(MSG_ERROR, "Compiled code is already running"); + break; + + case COMPILER_ERROR_CANNOT_RELOCATE: + message(MSG_ERROR, "Unable to link compiled code"); + break; + + case COMPILER_ERROR_NO_ENTRYPOINT: + message(MSG_ERROR, "Compiled code is missing entrypoint"); + break; + + case COMPILER_ERROR_SIGHANDLER_FAILED: + message(MSG_ERROR, "Unable to set up sigsegv handler"); + break; + } + return TRUE; + } + + return FALSE; +} + + +CompilerContextT *compilerNewContext(CompilerCallback callback, void *userData) { + CompilerContextT *c; + int x; + char ch; + + c = NEW(CompilerContextT); + + c->compilerResult = COMPILER_ERROR_NONE; + c->programResult = 0; + c->callback = callback; + c->userData = userData; + + c->s = tcc_new(); + if (!c->s) { + DEL(c); // Something bad happened. - return -1; + return NULL; } // __resourcePath comes in with a trailing slash. Temporarily remove it. x = (int)strlen(__resourcePath) - 1; - c = __resourcePath[x]; + ch = __resourcePath[x]; __resourcePath[x] = 0; - tcc_set_lib_path(s, __resourcePath); - tcc_add_sysinclude_path(s, __resourcePath); - __resourcePath[x] = c; + tcc_set_lib_path(c->s, __resourcePath); + tcc_add_sysinclude_path(c->s, __resourcePath); + __resourcePath[x] = ch; - tcc_set_options(s, "-Wall -Wno-write-strings"); - tcc_set_error_func(s, stdout, compilerErrorHandler); - tcc_set_output_type(s, TCC_OUTPUT_MEMORY); + tcc_set_options(c->s, "-Wall -Wno-write-strings"); // Debug options not working right: -g -bt 5 + tcc_set_error_func(c->s, NULL, compilerErrorHandler); + tcc_set_output_type(c->s, TCC_OUTPUT_MEMORY); - if (tcc_add_file(s, recipe) < 0) { + tcc_add_symbol(c->s, "fputc", fputc); + tcc_add_symbol(c->s, "qsort", qsort); + + return c; +} + + +void compilerRunRecipe(CompilerContextT *context, char *recipe, char *input, char *outputPath) { + char *oldLocation; + int (*entry)(char *, char *); + void *self; + + // All the paths passed in here are expected to be complete and absolute. + + //***TODO*** Oh my god this is so bad. The next line depends on 'userData' being set to 'self' when called from project.c. + self = context->userData; + + if (_currentRunningContext != NULL) { + context->compilerResult = COMPILER_ERROR_ALREADY_RUNNING; + context->callback(&context); + return; + } + + ___recipeTargets = NULL; + + if (tcc_add_file(context->s, recipe) < 0) { // Errors in code. - tcc_delete(s); - return -4; + context->compilerResult = COMPILER_ERROR_IN_CODE; + context->callback(&context); + return; } - tcc_add_symbol(s, "fputc", fputc); - tcc_add_symbol(s, "qsort", qsort); + tcc_add_symbol(context->s, "___recipeTargets", ___recipeTargets); + tcc_add_symbol(context->s, "recipeAddTarget", recipeAddTarget); + tcc_add_symbol(context->s, "recipeMessage", recipeMessage); + tcc_add_symbol(context->s, "utilCreateString", utilCreateString); - tcc_add_symbol(s, "___recipeTargets", ___recipeTargets); - tcc_add_symbol(s, "recipeAddTarget", recipeAddTarget); - tcc_add_symbol(s, "recipeMessage", recipeMessage); - tcc_add_symbol(s, "utilCreateString", utilCreateString); - - if (tcc_relocate(s, TCC_RELOCATE_AUTO) < 0) { + if (tcc_relocate(context->s, TCC_RELOCATE_AUTO) < 0) { // Something bad happened. - tcc_delete(s); - return -2; + context->compilerResult = COMPILER_ERROR_CANNOT_RELOCATE; + context->callback(&context); + return; } - entry = tcc_get_symbol(s, "recipe"); + entry = tcc_get_symbol(context->s, "recipe"); if (!entry) { // Something bad happened. - tcc_delete(s); - return -3; + context->compilerResult = COMPILER_ERROR_NO_ENTRYPOINT; + context->callback(&context); + return; } getcwd(__utilFilenameBuffer, FILENAME_MAX); oldLocation = strdup(__utilFilenameBuffer); chdir(outputPath); + _currentRunningContext = context; + #if HAVE_SIGSEGV_RECOVERY sigset_t emptySet; - runPass = 0; + context->runPass = 0; - if (sigsegv_install_handler(&sigHandler) < 0) - return -5; + if (sigsegv_install_handler(&sigHandler) < 0) { + context->compilerResult = COMPILER_ERROR_SIGHANDLER_FAILED; + context->callback(&context); + return; + } sigemptyset(&emptySet); - sigprocmask(SIG_BLOCK, &emptySet, &runSigSet); + sigprocmask(SIG_BLOCK, &emptySet, &context->runSigSet); - switch (setjmp(runRecipe)) { + switch (setjmp(context->runJump)) { case 0: - result = entry(input, outputPath); + context->programResult = entry(input, outputPath); case 1: - sigprocmask(SIG_SETMASK, &runSigSet, NULL); + sigprocmask(SIG_SETMASK, &context->runSigSet, NULL); sigsegv_install_handler(NULL); - if (runPass != 0) { + if (context->runPass != 0) { message(MSG_SEVERE, "%s caused a segmentation fault!", recipe); + context->compilerResult = COMPILER_ERROR_SEGFAULT; } break; } + context->callback(&context); #else - result = entry(input, outputPath); + context->programResult = entry(input, outputPath); + context->callback(&context); #endif + _currentRunningContext = NULL; + chdir(oldLocation); DEL(oldLocation); while (arrlen(___recipeTargets) > 0) { - if (projectAddToTree(context, ___recipeTargets[0])) { + if (projectAddToTree(self, ___recipeTargets[0])) { utilSetDirty((WindowDataT *)context, TRUE); } DEL(___recipeTargets[0]); arrdel(___recipeTargets, 0); } ARRFREE(___recipeTargets); - - tcc_delete(s); - - return result; } #if HAVE_SIGSEGV_RECOVERY static void sigHandlerContinuation(void *arg1, void *arg2, void *arg3) { - (void)arg1; + CompilerContextT *c = (CompilerContextT *)arg1; + (void)arg2; (void)arg3; - longjmp(runRecipe, runPass); + + longjmp(c->runJump, c->runPass); } @@ -188,11 +246,12 @@ int sigHandler(void *faultAddress, int serious) { (void)faultAddress; (void)serious; - runPass++; - return sigsegv_leave_handler(sigHandlerContinuation, NULL, NULL, NULL); + _currentRunningContext->runPass++; + return sigsegv_leave_handler(sigHandlerContinuation, _currentRunningContext, NULL, NULL); } #endif + void recipeAddTarget(char *target) { arrput(___recipeTargets, strdup(target)); } diff --git a/src/main.c b/src/main.c index e51fa11..3114afc 100644 --- a/src/main.c +++ b/src/main.c @@ -28,6 +28,7 @@ #include "ssh.h" #include "utils.h" #include "cwalk.h" +#include "messages.h" char *__resourcePath = NULL; @@ -47,6 +48,7 @@ int main(int argc, char **argv) { gtk_init(&argc, &argv); utilStartup(); + messageStartup(); // Make sure we have a config & resources folder. __resourcePath = utilCreateString("%s%cjoeydev%cresources%c", g_get_user_config_dir(), UTIL_PATH_CHAR, UTIL_PATH_CHAR, UTIL_PATH_CHAR); @@ -79,6 +81,7 @@ int main(int argc, char **argv) { scintilla_release_resources(); sshShutdown(); httpShutdown(); + messageShutdown(); utilShutdown(); DEL(__resourcePath); diff --git a/src/messages.c b/src/messages.c index 50c09b3..74426b8 100644 --- a/src/messages.c +++ b/src/messages.c @@ -20,21 +20,74 @@ */ +#include + #include "common.h" #include "messages.h" #include "utils.h" -static GtkWidget *_lstMessages = NULL; -static int _autoScroll = 0; +static GtkWidget *_lstMessages = NULL; +static char **_pendingMessages = NULL; +static pthread_mutex_t _mtxMessage; -static gboolean messagesScroll(gpointer userData); +static gboolean messagesUpdate(gpointer userData); EVENT gboolean winMessagesClose(GtkWidget *object, gpointer userData); static void winMessagesDelete(gpointer userData); void message(MessageTypesT level, char *format, ...) { + va_list args; + char *string; + char *msg; + char *tok; + char *labels[MSG_COUNT] = { + " Info:", + "Warning:", + " Error:", + " Severe:" + }; + + // Construct message. + va_start(args, format); + msg = utilCreateStringVArgs(format, args); + va_end(args); + + // Break multiline messages down into individual lines. + tok = strtok(msg, "\n"); + while (tok != NULL) { + //***TODO*** Filter out things that could be mistaken as markup tags. + string = utilCreateString("%s %s", labels[level], tok); + + // Save for UI thread. message() is not always called from the UI thread! + pthread_mutex_lock(&_mtxMessage); + arrput(_pendingMessages, string); // Do not free string. + pthread_mutex_unlock(&_mtxMessage); + + tok = strtok(NULL, "\n"); + } +} + + +void messageShutdown(void) { + g_idle_remove_by_data(messagesUpdate); + pthread_mutex_destroy(&_mtxMessage); +} + + +void messageStartup(void) { + if (pthread_mutex_init(&_mtxMessage, NULL) != 0) { + debug("Message mutex init failed!\n"); + exit(1); + } + + // Set up updates on the UI thread. + g_idle_add(messagesUpdate, messagesUpdate); +} + + +static gboolean messagesUpdate(gpointer userData) { WindowDataT *self = NULL; char *widgetNames[] = { "winMessages", @@ -48,83 +101,59 @@ void message(MessageTypesT level, char *format, ...) { GtkWidget *row; GtkWidget *box; GtkWidget *label; - va_list args; - char *string; - char *msg; - char *tok; - char *labels[MSG_COUNT] = { - " Info:", - "Warning:", - " Error:", - " Severe:" - }; + GtkAdjustment *adjustment; + char *string = NULL; - // Do we need to open this window? - if (!_lstMessages) { - // Set up instance data. We only need WindowDataT since this is a "singleton" window. - self = NEW(WindowDataT); - self->closeWindow = winMessagesClose; + (void)userData; - widgets[0] = &self->window; - utilGetWidgetsFromMemory("/com/kangaroopunch/joeydev/Messages.glade", widgetNames, widgets, self); - - // Register window. - utilWindowRegister(self); - - // Show window. - gtk_widget_show_all(self->window); - - // Set up automatic scrolling. - g_idle_add(messagesScroll, messagesScroll); + // Lock and access the pending message list. + pthread_mutex_lock(&_mtxMessage); + if (arrlen(_pendingMessages) > 0) { + string = _pendingMessages[0]; // We just need the pointer. + arrdel(_pendingMessages, 0); } + pthread_mutex_unlock(&_mtxMessage); - // Display message. - va_start(args, format); - msg = utilCreateStringVArgs(format, args); - va_end(args); + // Are there pending messages? + if (string != NULL) { - // Break multiline messages down into individual lines. - tok = strtok(msg, "\n"); - while (tok != NULL) { - //***TODO*** Filter out things that could be mistaken as markup tags. - string = utilCreateString("%s %s", labels[level], tok); + // Do we need to open the message window? + if (!_lstMessages) { + // Set up instance data. We only need WindowDataT since this is a "singleton" window. + self = NEW(WindowDataT); + self->closeWindow = winMessagesClose; + widgets[0] = &self->window; + utilGetWidgetsFromMemory("/com/kangaroopunch/joeydev/Messages.glade", widgetNames, widgets, self); + + // Register window. + utilWindowRegister(self); + + // Show window. + gtk_widget_show_all(self->window); + } + + // Create new row with the message string. row = gtk_list_box_row_new(); box = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 6); gtk_widget_set_hexpand(box, TRUE); label = gtk_label_new(string); gtk_label_set_use_markup(GTK_LABEL(label), TRUE); - DEL(string); - + // Add new row to the message box. gtk_box_pack_start(GTK_BOX(box), label, FALSE, FALSE, 0); gtk_container_add(GTK_CONTAINER(row), box); gtk_list_box_insert(GTK_LIST_BOX(_lstMessages), row, -1); - // Force paint. - gtk_widget_show_all(row); + // Scroll to show new row. + //***TODO*** This doesn't always scroll to the very end. + adjustment = gtk_list_box_get_adjustment(GTK_LIST_BOX(_lstMessages)); + gtk_adjustment_set_value(adjustment, gtk_adjustment_get_upper(adjustment)); + gtk_widget_show_all(_lstMessages); utilForceUpdate(); - _autoScroll = 15; // Try several times to show the new row. - tok = strtok(NULL, "\n"); + DEL(string); // Finally free the string allocated in message(). } -} - - -static gboolean messagesScroll(gpointer userData) { - GtkAdjustment *adjustment; - - (void)userData; - - if (_autoScroll <= 0) return G_SOURCE_CONTINUE; - - // Scroll to show new line. - adjustment = gtk_list_box_get_adjustment(GTK_LIST_BOX(_lstMessages)); - gtk_adjustment_set_value(adjustment, gtk_adjustment_get_upper(adjustment)); - gtk_widget_show_all(_lstMessages); - utilForceUpdate(); - - _autoScroll--; return G_SOURCE_CONTINUE; } @@ -142,7 +171,13 @@ EVENT gboolean winMessagesClose(GtkWidget *object, gpointer userData) { static void winMessagesDelete(gpointer userData) { - g_idle_remove_by_data(messagesScroll); + // Delete any pending messages. + while (arrlen(_pendingMessages) > 0) { + DEL(_pendingMessages[0]); + arrdel(_pendingMessages, 0); + } + ARRFREE(_pendingMessages); + utilWindowUnRegister(userData); _lstMessages = NULL; DEL(userData); diff --git a/src/project.c b/src/project.c index 3923c3f..eacb77d 100644 --- a/src/project.c +++ b/src/project.c @@ -84,7 +84,8 @@ typedef struct ProjectDataS { char *buildUser; char *buildPassword; TargetT **targets; - GtkWidget *tempWidget; // Used to pass data around dialogs. + GtkWidget *tempWidget; // Used to pass data around dialogs. + int tempInteger; // Used during recipe building. } ProjectDataT; typedef struct SectionDataS { @@ -107,7 +108,6 @@ static SectionDataT _sectionData[] = { { NULL, NULL } }; -static ProjectDataT *_cookingProjectData = NULL; static char **_sendList = NULL; @@ -120,6 +120,7 @@ EVENT void btnEditRawClicked(GtkButton *widget, gpointer userData); EVENT void btnEditRecipeClicked(GtkButton *widget, gpointer userData); EVENT void btnNewRecipeClicked(GtkButton *widget, gpointer userData); static void clearRecipeData(ProjectDataT *self); +static void cookFinished(CompilerContextT **context); static void decompressBuild(ArchiveT *archive); static void dialogCookOptions(char *filename, ProjectDataT *self); static int findRecipeData(ProjectDataT *self, char *key); @@ -244,6 +245,50 @@ static void clearRecipeData(ProjectDataT *self) { } +static void cookFinished(CompilerContextT **context) { + CompilerContextT *ctx = *context; + ProjectDataT *self = (ProjectDataT *)ctx->userData; + char *raw; + char *recipe; + + // Was there a compiler error? + if (compilerHadError(context)) { + compilerDeleteContext(context); + return; + } + + // If this is not first call, process context. + if (self->tempInteger != -1) { + if (ctx->programResult != 0) { + //***TODO*** Not all negative returns are severe. + message(ctx->programResult > 0 ? MSG_ERROR : MSG_SEVERE, "Recipe %s returned %d", self->recipes[self->tempInteger]->value, ctx->programResult); + } + } + + compilerDeleteContext(context); + + // Is there something to cook? + self->tempInteger++; + if (self->tempInteger < arrlen(self->recipes)) { + // Build path names. + cwk_path_change_basename(self->windowData.filename, self->recipes[self->tempInteger]->key, __utilFilenameBuffer, sizeof(__utilFilenameBuffer)); + raw = strdup(__utilFilenameBuffer); + cwk_path_change_basename(self->windowData.filename, self->recipes[self->tempInteger]->value, __utilFilenameBuffer, sizeof(__utilFilenameBuffer)); + recipe = strdup(__utilFilenameBuffer); + + // Run it! + message(MSG_INFO, "Cooking %s", self->recipes[self->tempInteger]->key); + ctx = compilerNewContext(cookFinished, self); + compilerRunRecipe(ctx, recipe, raw, self->windowData.path); + DEL(recipe); + DEL(raw); + } else { + // Finished with recipe list. + message(MSG_INFO, "Finished Cooking"); + } +} + + static void decompressBuild(ArchiveT *archive) { ProjectDataT *self = (ProjectDataT *)archive->userData; char *temp; @@ -254,6 +299,7 @@ static void decompressBuild(ArchiveT *archive) { DEL(temp); //***TODO*** Process build results. + message(MSG_INFO, "Processing build results"); } @@ -922,39 +968,15 @@ EVENT void menuProjectBuildTargets(GtkWidget *object, gpointer userData) { EVENT void menuProjectBuildCookRecipes(GtkWidget *object, gpointer userData) { - ProjectDataT *self = (ProjectDataT *)userData; - int i; - char *raw; - char *recipe; - int result; + ProjectDataT *self = (ProjectDataT *)userData; + CompilerContextT *ctx; - // Only one cook at a time. Should not be able to happen. - if (_cookingProjectData) { - message(MSG_ERROR, "Cook currently running"); - return; + // Are there recipes to cook? + if (arrlen(self->recipes) > 0) { + self->tempInteger = -1; // Tell cookFinished() we're just starting. + ctx = compilerNewContext(cookFinished, self); + cookFinished(&ctx); } - - // Remember who started the cook. - _cookingProjectData = self; - - for (i=0; irecipes); i++) { - // Build pathnames. - cwk_path_change_basename(self->windowData.filename, self->recipes[i]->key, __utilFilenameBuffer, sizeof(__utilFilenameBuffer)); - raw = strdup(__utilFilenameBuffer); - cwk_path_change_basename(self->windowData.filename, self->recipes[i]->value, __utilFilenameBuffer, sizeof(__utilFilenameBuffer)); - recipe = strdup(__utilFilenameBuffer); - - // Run it! - message(MSG_INFO, "Cooking %s", self->recipes[i]->key); - result = compilerRunRecipe(recipe, raw, self->windowData.path, self); - if (result != 0) { - //***TODO*** Not all negative returns are severe. - message(result > 0 ? MSG_ERROR : MSG_SEVERE, "Recipe %s returned %d", self->recipes[i]->value, result); - } - message(MSG_INFO, "Finished Cooking"); - } - - _cookingProjectData = NULL; } @@ -975,8 +997,6 @@ EVENT void menuProjectBuildBuild(GtkWidget *object, gpointer userData) { gboolean archPrinted; FILE *out; - menuProjectBuildCookRecipes(object, userData); - ssh = NEW(SSHT); ssh = sshConnect(self->buildHost, self->buildSSHPort, self->buildUser, self->buildPassword); diff --git a/src/ssh.c b/src/ssh.c index 1c2aa1d..3b20704 100644 --- a/src/ssh.c +++ b/src/ssh.c @@ -118,6 +118,8 @@ SSHT *sshConnect(char *hostname, uint16_t port, char *user, char *password) { } } + //***TODO*** Move SFTP session setup here so we don't keep repeating it. + return data; } @@ -125,6 +127,8 @@ SSHT *sshConnect(char *hostname, uint16_t port, char *user, char *password) { void sshDisconnect(SSHT **sshData) { SSHT *data = *sshData; + //***TODO*** Move SFTP session teardown here so we don't keep repeating it. + libssh2_session_disconnect(data->session, "Normal Shutdown."); libssh2_session_free(data->session); socketclose(data->sock); diff --git a/src/utils.c b/src/utils.c index 4d87e4d..307537f 100644 --- a/src/utils.c +++ b/src/utils.c @@ -233,6 +233,7 @@ gboolean utilDecompressUpdate(gpointer userData) { if (toClose >= 0) { a = _activeArchives[toClose]; + arrdel(_activeArchives, toClose); if (a->callback) { a->callback(a); }