/* $Id$ Copyright (C) 2003-2011 tooar This file is part of emelFM2. emelFM2 is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 3, or (at your option) any later version. emelFM2 is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with emelFM2; see the file GPL. If not, see http://www.gnu.org/licenses. */ /** @file plugins/e2p_unpack.c @brief plugin for interfacting with several archive managers, to unpack selected item(s) */ #include "emelfm2.h" #include #include "e2_plugins.h" #include "e2_dialog.h" #include "e2_task.h" #include "e2_filelist.h" //same enum as in pack plugin, though it need not be so enum { TAR_GZ, TAR_BZ2, TAR_LZMA, TAR_XZ, TAR, /*DEB, RPM, */ZIP, Z7Z, RAR, ARJ, ZOO, MAXTYPES }; typedef struct _E2P_Unpackdata { gchar *package; //absolute path of source archive, UTF-8 string gchar *workdir; //absolute path of dir used to unpack this archive, //== unpack_tmp or a tmp variant of that, UTF-8, no trailer gchar *last_dir; //dir to go back to after starting a repack glong thispid; //id of process which is re-packing an archive // guint chdir_id; //id of timer which checks for whether to ask user what to do guint pack_id; //id of timer which checks for repack completion E2_CDType cd_completed; //flag set when cd to temp dir is completed gchar *command; //the upack command to be run, UTF-8 string gboolean departing; //TRUE when the temp dir is waiting to be processed after //the user selects from a what-to-do dialog //also used to block reentrant use of the hook function } E2P_Unpackdata; static gboolean _e2p_unpack_delete_dir (E2P_Unpackdata *data); static gboolean _e2p_unpack_change_dir_hook (gchar *path, E2P_Unpackdata *data); static gchar *unpack_tmp; //absolute path of 'base working' directory (no trailer), utf-8 string /** @brief cleanup plugin data This is executed with BGL open or closed @param data pointer to plugin data @return */ static void _e2p_unpack_cleanup (E2P_Unpackdata *data) { g_free (data->package); g_free (data->workdir); g_free (data->command); if (data->last_dir != NULL) g_free (data->last_dir); DEALLOCATE (E2P_Unpackdata, data); } /** @brief select the relevant archive-enumerator corresponding to to @a localpath @param localpath pointer to archive data, with localised path @return the number, or -1 upon no match */ static gint _e2p_unpack_match_type (VPATH *localpath) { gint typecode, count; /* //FIXME use a better way to distinguish filetypes eg magic number //e.g mimetype file string needs to be localised VPATH * file(1) cannot detect all these mimetypes */ gchar *thismime = e2_utils_get_mimetype (localpath); if (thismime != NULL) { const gchar *mimes [] = { "x-gzip", "x-bzip2", //CHECKME not sure about lzma mimetype yet "x-lzma-compressed-tar", //this may be version-specific i.e. NOT for LZMA_Alone tool in LZMA SDK <= v.4.43 "x-lzma", "x-xz" "x-tar", // "x-deb", // "x-rpm", "zip", "x-7z", //"x-7z-compressed" ? "x-rar", //"x-rar-compressed" ? "x-arj", //"arj" ? "x-zoo" }; gint mimecodes [] = { TAR_GZ, TAR_BZ2, TAR_LZMA, //2 alternates for lzma TAR_LZMA, TAR_XZ, TAR, // DEB, // RPM, ZIP, Z7Z, RAR, ARJ, ZOO, }; if (g_str_has_prefix (thismime, "application/")) { gchar *s = thismime + sizeof (gchar) * 12; count = sizeof (mimes) / sizeof (gchar *); for (typecode = 0; typecode < count; typecode++) { if (strcmp (s, mimes [typecode]) == 0) { typecode = mimecodes [typecode]; break; } } if (typecode == count) typecode = -1; } else typecode = -1; g_free (thismime); } else //mime-check by xdg-utils(1) or file(1) not available, try some other approach typecode = -1; if (typecode == -1) { const gchar *extensions [] = { ".tar.gz", ".tgz", //compress(1) does ".taz", ".tar.z", ".tar.bz2", ".tbz2", ".tar.lzma", ".tlz", ".tar.xz", ".tar", // ".deb", // ".rpm". ".zip", ".7z", ".rar", ".arj", ".zoo", }; //codes corresponding to extensions array, above gint extcodes [] = { TAR_GZ, TAR_GZ, TAR_BZ2, TAR_BZ2, TAR_LZMA, TAR_LZMA, TAR_XZ, TAR, // DEB, // RPM, ZIP, Z7Z, RAR, ARJ, ZOO, }; //CHECKME matched string not at end of name - ok ? count = sizeof (extensions) / sizeof (gchar *); for (typecode = 0; typecode < count; typecode++) { if (g_str_has_suffix (VPSTR (localpath), extensions[typecode])) { typecode = extcodes [typecode]; //must be < count break; } } if (typecode == count) typecode = -1; } return typecode; } /** @brief timer callback to resume idle checking @param data pointer to unpack data struct @return FALSE always */ static gboolean _e2p_unpack_pause (E2P_Unpackdata *data) { data->pack_id = g_idle_add_full (G_PRIORITY_LOW, (GSourceFunc)_e2p_unpack_delete_dir, data, NULL); return FALSE; } /** @brief idle callback to check whether temp-dir deletion can proceed safely We do this check at an idle time to reduce risk of conflict, but interpose a timer between idle callbacks to reduce their frequency @param data pointer to unpack data struct @return FALSE always */ static gboolean _e2p_unpack_delete_dir (E2P_Unpackdata *data) { LISTS_LOCK //conservative approach - no deletion while any chance of the temp-dir being used if (g_atomic_int_get (&curr_view->listcontrols.cd_working) || g_atomic_int_get (&curr_view->listcontrols.refresh_working) || ( #ifdef E2_VFS curr_view->spacedata == NULL && //local temp dirs #endif g_str_has_prefix (curr_view->dir, data->workdir))) { LISTS_UNLOCK //wait before checking for an idle again data->pack_id = g_timeout_add (500, (GSourceFunc)_e2p_unpack_pause, data); return FALSE; } if (g_atomic_int_get (&other_view->listcontrols.cd_working) || g_atomic_int_get (&other_view->listcontrols.refresh_working) || ( #ifdef E2_VFS other_view->spacedata == NULL && //local temp dirs #endif g_str_has_prefix (other_view->dir, data->workdir))) { LISTS_UNLOCK //wait before checking for an idle again data->pack_id = g_timeout_add (500, (GSourceFunc)_e2p_unpack_pause, data); return FALSE; } LISTS_UNLOCK //kill the idle now // if (data->pack_id > 0) // g_source_remove (data->pack_id); // else // while (g_source_remove_by_user_data (data)) {} //now we're ready for cleanup e2_filelist_disable_refresh (); gchar *local = F_FILENAME_TO_LOCALE (data->workdir); #ifdef E2_VFS VPATH ddata = { local, NULL }; //local unpacking only if (e2_fs_access2 (&ddata E2_ERR_NONE()) == 0) e2_task_backend_delete (&ddata); #else if (e2_fs_access2 (local E2_ERR_NONE()) == 0) e2_task_backend_delete (local); #endif e2_filelist_enable_refresh (); F_FREE (local, data->workdir); _e2p_unpack_cleanup (data); //FIXME different refresh approach with E2_ASYNC //in case a pane shows parent of temp dir #ifdef E2_FAM e2_filelist_request_refresh (curr_view->dir, FALSE); e2_filelist_request_refresh (other_view->dir, TRUE); #else e2_filelist_check_dirty (GINT_TO_POINTER (1)); #endif return FALSE; } /** @brief delete the temp dir This is executed with BGL open or closed @param data pointer to plugin data @return */ static void _e2p_unpack_clear (E2P_Unpackdata *data) { //ensure BGL is open and manage any race with refresh function data->pack_id = g_idle_add_full (G_PRIORITY_LOW, (GSourceFunc)_e2p_unpack_delete_dir, data, NULL); } /* * @brief timer callback to periodically check whether a repack is completed Needed if repack command does not include cleanup @param data pointer to plugin data struct @return TRUE if the repack process is still running */ /*static gboolean _e2p_unpack_clean_dir (E2P_Unpackdata *data) { if (e2_command_find_process ((guint)data->thispid)) return TRUE; //wait some more //FIXME only delete dir if repack command succeeded, or else don't delete //original archive until repack succeeded //now we're ready for cleanup // g_source_remove (data->pack_id); // if (_e2p_unpack_delete_dir (data)) //keep trying until the deletion is done g_idle_add_full (G_PRIORITY_LOW, (GSourceFunc)_e2p_unpack_delete_dir, data, NULL); return FALSE; } */ /** @brief repack the temp dir This is executed inside a callback with BGL closed @param data pointer to plugin data @param from the dialog where the action was initiated @return */ static void _e2p_unpack_repack (E2P_Unpackdata *data, gpointer from) { #ifdef E2_VFSTMP if (curr_view->spacedata != NULL) //CHECKME fuse ok? { _e2p_unpack_cleanup (data); return; } #endif const gchar *pack_str [MAXTYPES] = { //these command strings are in same order as enum //they are all designed to be executed from the unpack temp dir, and //on all its contents (recursive if appropriate) //CHECKME "." always remains correct during repack execution ? ">tar cf - . | gzip - > %s", ">tar cf - . | bzip2 - > %s", //default block size -9 is not significantly better, maybe use -3 instead ? ">tar cf - . | lzma - > %s", //default compresssion -7, maybe use -2 instead ? ">tar cf - . | xz - > %s", "tar cf %s .", // deb 'pack' command ? // rpm 'pack' command does not exist ">zip -r - . > %s", ">7za a -t7z -so . > %s", //CHECKME //updates of archives probably don't capture renames,deletes "rar u -as -ol -tl -r %s .", //OK "arj a -u -r -s -a -2s %s .", //CHECKME "zoo unP %s ." //CHECKME replace, not update ? deletions ? }; /* compress ANSI files: 7za a -tzip archive.zip file1 file2 ... fileN compress ANSI dir: 7za a -tzip archive.zip dirnametocompress\ compress UNICODE files: 7za a -t7z archive.7z file1 file2 ... fileN compress UNICODE dir: 7za a -t7z archive.7z dirnametocompress\ To backup directories you must use tar : - to backup a directory : tar cf - directory | 7za a -si directory.tar.7z - to restore your backup : 7za x -so directory.tar.7z | tar xf - ar -x package-file.deb a deb top-level package contains a small file with a version number, and two tar archives - one contains the package description, MD5 checksums and install/uninstall scripts, the other the files to be installed. */ gchar *package = data->package; #ifdef E2_VFSTMP VPATH ddata; ddata.path = F_FILENAME_TO_LOCALE (package); ddata.spacedata = curr_view->spacedata; //FIXME gint index = _e2p_unpack_match_type (&ddata); #else gchar *local = F_FILENAME_TO_LOCALE (package); gint index = _e2p_unpack_match_type ((VPATH*)local); #endif gchar *fmt, *qp; g_free (data->command); switch (index) { case TAR_GZ: case TAR_BZ2: case TAR_LZMA: case TAR_XZ: case TAR: case ZIP: case Z7Z: //these types of archive are overwritten fmt = g_strconcat (pack_str [index], " && mv -f %s %s && rm -rfd %s", NULL); qp = e2_utils_quote_string (package); gchar *tmp = e2_utils_get_tempname #ifdef E2_VFSTMP (ddata.path); #else (local); #endif gchar *utftmp = F_FILENAME_FROM_LOCALE (tmp); //tag E2_BADQUOTES gchar *qpt = e2_utils_quote_string (utftmp); g_free (tmp); F_FREE (utftmp, tmp); data->command = g_strdup_printf (fmt, qpt, qpt, qp, data->workdir); g_free (qpt); break; case RAR: case ARJ: case ZOO: //these types of archive are just updated fmt = g_strconcat (pack_str [index], " && rm -rfd %s", NULL); qp = e2_utils_quote_string (package); data->command = g_strdup_printf (fmt, qp, data->workdir, NULL); // printd (DEBUG, "repack command %s", data->command); break; default: //case -1: //should never happen fmt = qp = NULL; //warning prevention _e2p_unpack_cleanup (data); return; } g_free (fmt); g_free (qp); #ifdef E2_VFSTMP F_FREE (ddata.path, package); #else F_FREE (local, package); #endif gint res = e2_command_run_at (data->command, data->workdir, E2_COMMAND_RANGE_DEFAULT, from #ifdef E2_COMMANDQ , T/F ? #endif ); if (res == 0) { /* use this if command doesn't include cleanups i.e. rm workdir //FIXME race here if something else is run at a bad time, so find the pid //with matching command-string instead E2_TaskRuntime *td = e2_task_find_last_running_child (TRUE); data->thispid = (td != NULL) ? td->pid : 0; //periodically check whether re-build finished, when so, cleanup the temp dir //CHECKME make this timer cancellable at session end data->pack_id = g_timeout_add (500, (GSourceFunc) _e2p_unpack_clean_dir, data); //CHECKME refreshing etc may move CWD away from the temp dir while the repack is underway */ _e2p_unpack_cleanup (data); //or else, just cleanup data } else { // data->thispid = 0; printd (WARN, "repack command cannot be run"); //WARN user ? _e2p_unpack_cleanup (data); } } /** @brief callback for "what-to-do" dialog's "response" signal @param dialog UNUSED the dialog where the response was triggered @param response the response for the clicked button @param rt pointer to data for dialog @return */ static void _e2p_unpack_response_decode_cb (GtkDialog *dialog, gint response, E2P_Unpackdata *data) { gtk_widget_destroy (GTK_WIDGET(dialog)); //do this outside of current hook func, as we need to check both panes and data e2_hook_unregister (&app.pane1.hook_change_dir, (HookFunc)_e2p_unpack_change_dir_hook, data, TRUE); e2_hook_unregister (&app.pane2.hook_change_dir, (HookFunc)_e2p_unpack_change_dir_hook, data, TRUE); switch (response) { case E2_RESPONSE_USER1: //repack the temp dir _e2p_unpack_repack (data, (gpointer)dialog); break; case E2_RESPONSE_USER2: //keep the unpacked archive _e2p_unpack_cleanup (data); // case GTK_RESPONSE_CANCEL: break; //case E2_RESPONSE_REMOVE: default: //this will pick up GTK_RESPONSE_NONE or GTK_RESPONSE_DELETE_EVENT _e2p_unpack_clear (data); break; } } /** @brief hook function for cd in either pane (app.paneX.hook_change_dir) This is initiated from a cd thread, and with BGL open/off @param path UNUSED path of an opened directory, utf-8 string @param data pointer to operation data struct @return TRUE always so hook remains active */ static gboolean _e2p_unpack_change_dir_hook (gchar *path, E2P_Unpackdata *data) { if (data->departing) return TRUE; //a callback has begun, ignore this other cd data->departing = TRUE; //temp block on nested checking /* this hookfunc is called toward the end of a cd process, there's no need to check for various "busy" flags before proceeding the first cd will be into the temp dir, so path will be that dir with trailing separator */ if ( #ifdef E2_VFSTMP //FIXME dirs when not mounted local curr_view->spacedata == NULL //local temp dirs other_view->spacedata == NULL //local temp dirs #else ( g_str_has_prefix (curr_view->dir, data->workdir) || g_str_has_prefix (other_view->dir, data->workdir)) #endif ) { data->departing = FALSE; //unblock return TRUE; } //user changed dir, now neither pane is for anywhere in unpack-dir tree printd (DEBUG, "ready to cleanup unpack dir"); //ask user what to do with the unpacked items gdk_threads_enter (); GtkWidget *dialog = e2_dialog_create (GTK_STOCK_DIALOG_QUESTION, _("What do you want to do with the unpacked item(s) ?"), NULL, (ResponseFunc)_e2p_unpack_response_decode_cb, data); gdk_threads_leave (); e2_dialog_add_simple_button (dialog, GTK_STOCK_CLEAR, _("Re_pack"), E2_RESPONSE_USER1); e2_dialog_add_simple_button (dialog, GTK_STOCK_APPLY, _("_Retain"), E2_RESPONSE_USER2); GtkWidget *button = e2_dialog_add_simple_button (dialog, GTK_STOCK_DELETE, _("_Delete"), E2_RESPONSE_REMOVE); gdk_threads_enter (); e2_dialog_setup (dialog, app.main_window); e2_dialog_run (dialog, NULL, 0); gtk_widget_grab_focus (button); gtk_window_present (GTK_WINDOW (dialog)); gdk_threads_leave (); return TRUE; //no hook cleanup here //done in response cb, as we need to check both panes and data } /** @brief unpack plugin action : unpack a supported archive into a temp dir Best if refreshing is disabled when this action is initiated @param from the button, menu item etc which was activated @param art action runtime data @return TRUE if action completed successfully, else FALSE */ static gboolean _e2p_unpack (gpointer from, E2_ActionRuntime *art) { //these unpack-command strings are in same order as enum //all are executed from the temp dir (as some can only do that) //all are ascii (no conversion to utf8 before execution) static gchar *cmd_str [MAXTYPES] = { //each unpack goes into a separate newly-created work dir, so there's //no need to worry manage over-writing when unpacking with these commands #if defined(__FreeBSD__) || defined(__OpenBSD__) || defined(__NetBSD__) || defined(darwin) //tar decompression options -z, -j, --lzma are GNU extensions //ditto for --overwrite, and for -C, if that's wanted ">gzip -cd %s | tar -xpf -", ">bzip2 -cd %s | tar -xpf -", ">lzma -cd %s | tar -xpf -", ">xz -cd %s | tar -xpf -", "tar -xpf %s", #else //note: an --atime-preserve in these tar commands prevents the //file-monitoring process from noticing anything in the temp dir //--overwrite not needed "tar -xpzf %s", "tar -xpjf %s", "tar --lzma -xpf %s", "tar --xz -xpf %s", "tar -xpf %s", #endif //.deb unpack command by Martin Zelaia (has GNU-specific tar options) //">>mkdir ./DEBIAN ./CONTENTS;ar -x %s | tar -xfz control.tar.gz -C ./DEBIAN | tar -xfz data.tar.gz -C ./CONTENTS; rm control.tar.gz data.tar.gz;cp ./DEBIAN/control ./INFO;rm ./debian-binary"; //">rpm2cpio %s | cpio -id" this is ok, but no spec data from a non-src file, and there's no re-pack command available "unzip -o %s", //or "unzip -o -d %s %s" /* decompress ANSI: 7za x archive.zip -odirname -aoa decompress UNICODE: 7za x archive.7z -odirname -aoa */ "7za x %s -aoa", //or ?? "rar x -o+ %s", //rar will only extract to current dir "arj x -y %s", //or "arj x -y "UNPACKPATH" "UNPACKPATH, //NOTE swapped order of archive & path "zoo xO %s" //zoo will only extract to current dir }; //CHECKME e2_filelist_disable_refresh (); FileInfo *info = e2_fileview_get_selected_first_local (curr_view, FALSE); if (info == NULL) { // e2_filelist_enable_refresh (); return FALSE; //nothing selected } #ifdef E2_VFSTMP VPATH ddata; ddata.path = e2_utils_dircat (curr_view, info->filename, TRUE); ddata.spacedata = curr_view->spacedata; gint index = _e2p_unpack_match_type (&ddata); g_free (ddata.path); #else gchar *local = e2_utils_dircat (curr_view, info->filename, TRUE); gint index = _e2p_unpack_match_type ((VPATH*)local); g_free (local); #endif if (index == -1) { e2_output_print_error (_("Selected item is not a supported archive"), FALSE); // e2_filelist_enable_refresh (); return FALSE; } //the current temp dir may be deleted when the user exits that dir, so it //would be very bad to open an archive inside the temp dir //CHECKME recursive unpacking would be ok (with different temp dirs for each //archive) if there's a way to handle (prevent?) repacking when the 'parent' //temp dir is alredy gone #ifdef E2_VFSTMP //FIXME handle space-change too if (curr_view->spacedata == ? || strstr (curr_view->dir, unpack_tmp) != NULL) #else if (strstr (curr_view->dir, unpack_tmp) != NULL) #endif { e2_output_print_error (_("Recursive unpack is not supported"), FALSE); // e2_filelist_enable_refresh (); return FALSE; } gchar *converted = F_FILENAME_TO_LOCALE (unpack_tmp); gchar *workdir = e2_utils_get_tempname (converted); #ifdef E2_VFS VPATH ddata = { workdir, NULL }; //local unpacking only #endif F_FREE (converted, unpack_tmp); //(re)make it #ifdef E2_VFS if (e2_fs_recurse_mkdir (&ddata, 0777 E2_ERR_NONE())) #else if (e2_fs_recurse_mkdir (workdir, 0777 E2_ERR_NONE())) #endif { converted = F_DISPLAYNAME_FROM_LOCALE (workdir); gchar *msg = g_strdup_printf ("Could not create working directory '%s'", converted); e2_output_print_error (msg, TRUE); F_FREE (converted, workdir); g_free (workdir); // e2_filelist_enable_refresh (); return FALSE; } E2P_Unpackdata *data = ALLOCATE0 (E2P_Unpackdata); CHECKALLOCATEDWARN (data, return FALSE;) data->workdir = D_FILENAME_FROM_LOCALE (workdir); g_free (workdir); converted = F_FILENAME_FROM_LOCALE (info->filename); //not much race chance, don't D_... #ifdef E2_VFSTMP FIXME dir when not mounted local #else data->package = e2_utils_strcat (curr_view->dir, converted); //dir has trailing / F_FREE (converted, info->filename); //tag E2_BADQUOTES gchar *qp = e2_utils_quote_string (data->package); #endif //need no conversion of command encoding data->command = g_strdup_printf (cmd_str [index], qp); //CHECKME append 1>/dev/null g_free (qp); e2_window_set_cursor (GDK_WATCH); //unpack the archive into the temp directory gint result = e2_command_run_at (data->command, data->workdir, E2_COMMAND_RANGE_DEFAULT, from #ifdef E2_COMMANDQ , T/F ? #endif ); e2_window_set_cursor (GDK_LEFT_PTR); if (result != 0) { workdir = F_FILENAME_TO_LOCALE (data->workdir); #ifdef E2_VFS ddata.path = workdir; e2_task_backend_delete (&ddata); #else e2_task_backend_delete (workdir); #endif F_FREE (workdir, data->workdir); _e2p_unpack_cleanup (data); // e2_filelist_enable_refresh (); return FALSE; } //now go see it e2_pane_change_dir (NULL, data->workdir); //setup to clean when temp dir not open anymore e2_hook_register (&app.pane1.hook_change_dir, (HookFunc)_e2p_unpack_change_dir_hook, data); e2_hook_register (&app.pane2.hook_change_dir, (HookFunc)_e2p_unpack_change_dir_hook, data); // e2_filelist_enable_refresh (); return TRUE; } //aname must be confined to this module static gchar *aname; /** @brief plugin initialization function, called by main program @param p ptr to plugin data struct @return TRUE if the initialization succeeds, else FALSE */ gboolean init_plugin (Plugin *p) { //once-only, setup the working dir name unpack_tmp = e2_utils_get_temp_path ("-unpack"); //strip the trailing ".tmp~0" as we will append a suffix like that for each unpack gchar *s = strrchr (unpack_tmp, '.'); *s = '\0'; #define ANAME "unpack" #ifdef E2_VFS aname = _("unpack_with_plugin"); #else aname = _A(104); #endif p->signature = ANAME VERSION; p->menu_name = _("_Unpack"); p->description = _("Unpack archive (tar, tar.gz, tar.bz2, zip, 7z, rar, arj, zoo) into a temporary directory"); p->icon = "plugin_"ANAME E2ICONTB; //use icon file pathname if appropriate if (p->action == NULL) { //don't free name string here E2_Action plugact = {g_strconcat (_A(6),".",aname,NULL),_e2p_unpack,FALSE,E2_ACTION_TYPE_ITEM,0,NULL,NULL}; p->action = e2_plugins_action_register (&plugact); if G_LIKELY((p->action != NULL)) return TRUE; g_free (plugact.name); } return FALSE; } /** @brief cleanup transient things for this plugin @param p pointer to data struct for the plugin @return TRUE if all cleanups were completed */ gboolean clean_plugin (Plugin *p) { //FIXME prevent unload when an unpack is underway //clear any current hook(s) while (e2_hook_unregister (&app.pane1.hook_change_dir, (HookFunc)_e2p_unpack_change_dir_hook, NULL, FALSE)) {} while (e2_hook_unregister (&app.pane2.hook_change_dir, (HookFunc)_e2p_unpack_change_dir_hook, NULL, FALSE)) {} gchar *action_name = g_strconcat (_A(6),".",aname,NULL); gboolean ret = e2_plugins_action_unregister (action_name); g_free (action_name); g_free (unpack_tmp); return ret; }