/* $Id$ Copyright (C) 2004-2011 tooar Portions copyright (C) 2004 Florian Zaehringer Portions copyright (C) 1999 Michael Clark 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_mvbar.c @brief plugin for moving selected items, with a progress-bar */ #include "emelfm2.h" #include #include #include #include #include "e2_plugins.h" #include "e2_dialog.h" #include "e2_option.h" #include "e2_filelist.h" #include "e2_task.h" //max. no. of _bytes_, at the end of a copied item's source-path //and dest-path, shown in the progress dialog //paths longer than this will be 'ellipsized' #define MAX_CHAR 55 //size of buffer for progress dialog //make this >= (MAX_CHAR+4)*2 + space for rest of message text //#define MAX_MSG 220 //interval (usec) between progress window updates //for items < and > 10MB resepcively //#define MIN_UPDATE_INTERVAL 100000 //#define MAX_UPDATE_INTERVAL 200000 typedef enum { E2_BARTASK_STOPPED = 1, E2_BARTASK_PAUSEREQ = 1 << 1, //pause requested E2_BARTASK_PAUSED = 1 << 2, //actually paused E2_BARTASK_COMPLETED = 1 << 3, E2_BARTASK_SUCCEEDED = 1 << 4, } E2_BarFlags; typedef struct _E2_BarData { guint64 count; //count of items to be processed, in active pane guint64 totalsize; //aggregate apparent size of items to be processed } E2_BarData; typedef struct _E2_ProgressData { VPATH *dlocal; //includes localised destination path (maybe temp) of item moved guint64 done_size; } E2_ProgressData; typedef struct _E2_BarWindowData { GtkWidget *dialog; GtkWidget *label; GtkWidget *progbar; GtkWidget *pause_btn; GtkWidget *resume_btn; GtkWidget *stop_btn; E2_BarFlags bflags; E2_MainLoop *loop; } E2_BarWindowData; static gboolean _e2p_mvbarQ (E2_ActionTaskData *qed); /** @brief update items' count and total size This is a callback for a treewalk function Error message expects BGL to be open @param localpath absolute path of item reported by the walker, localised string @param statptr pointer to struct stat with data about @a localpath @param status code from the walker, indicating what type of report it is @param twdata pointer to tw data struct @return E2TW_CONTINUE always */ static E2_TwResult _e2p_mvbar_twcb (VPATH *localpath, const struct stat *statptr, E2_TwStatus status, E2_BarData *twdata) { switch (status) { case E2TW_F: //not dir or link case E2TW_SL: //symbolic link case E2TW_SLN: //symbolic link naming non-existing file twdata->totalsize += statptr->st_size; case E2TW_DL: //dir, not opened due to tree-depth limit (reported upstream) case E2TW_DM: //dir, not opened due to different file system (reported upstream) case E2TW_D: //dir (don't care about its reported size) case E2TW_DRR: //dir now readable case E2TW_DNR: //unreadable dir (reported upstream) case E2TW_NS: //un-statable item (reported upstream) twdata->count++; // case E2TW_DP: //dir, finished default: break; } return E2TW_CONTINUE; } /** @brief determine how much progress has been made on the current item This provides data for progressive updates during the course of moving an item. BGL will be open (though no UI change here) @param data pointer to struct with data which is needed here @return */ static void _e2p_mvbar_progress (E2_ProgressData *data) { gchar *localpath; struct stat sb; #ifdef E2_VFS VPATH ddata; ddata.spacedata = data->dlocal->spacedata; #endif localpath = VPSTR (data->dlocal); //determine how much of current item has been moved E2_BarData pdata; pdata.totalsize = 0; #ifdef E2_VFS ddata.path = localpath; if (!e2_fs_lstat (&ddata, &sb E2_ERR_NONE())) #else if (!e2_fs_lstat (localpath, &sb E2_ERR_NONE())) #endif { if (S_ISDIR (sb.st_mode)) { //if (! #ifdef E2_VFS e2_fs_tw (&ddata, #else e2_fs_tw (localpath, #endif _e2p_mvbar_twcb, &pdata, -1, E2TW_PHYS E2_ERR_NONE()); //) //{ //FIXME handle error //} } else pdata.totalsize = sb.st_size; } data->done_size = pdata.totalsize; // printd (DEBUG, "progress is %llu", pdata.totalsize); } /** @brief cleanup after this task's thread is aborted @param pid pointer to store for pid of child process doing the copying @return */ static void _e2p_mvbar_abort_clean_process (pid_t *pid) { if (*pid != 0) kill (*pid, SIGSTOP); //FIXME cleanup shared memory, temp file } /*static void _e2p_mvbar_abort_clean_memory (gboolean **shared) { if (*shared != NULL && shmdt (*shared) == 0) shmctl (shmid, IPC_RMID, NULL); } */ /** @brief cleanup after this task's thread is aborted @param dialog the progress dialog @return */ static void _e2p_mvbar_abort_clean_dialog (GtkWidget **dialog) { if (GTK_IS_DIALOG (*dialog)) { gdk_threads_enter (); gtk_widget_destroy (*dialog); gdk_threads_leave(); } } /** @brief move item @a slocal to @a dlocal, while updating progress-window details This is called with BGL open @param slocal path of item to be moved, localised string @param dlocal new path of moved item, localised string @param realmove TRUE if this is a move between devices or partititions @param flags bitflags indicating task parameters @param bdata pointer to bar data struct holding progress data @param tdata pointer to bar data struct holding totals data @param wdata pointer to info-window data struct @return enumerator of user's request, if any, to abort */ static DialogButtons _e2p_mvbar_exec (VPATH *slocal, VPATH *dlocal, gboolean realmove, E2_FileTaskMode flags, E2_BarData *bdata, E2_BarData *tdata, E2_BarWindowData *wdata) { gchar progresstext[64]; //utf-8 string, middle line of progress dialog //localised strings gchar *src = F_FILENAME_FROM_LOCALE (VPSTR (slocal)); // gchar *dest = F_FILENAME_FROM_LOCALE (VPSTR (dlocal)); gchar *dest_dir = g_path_get_dirname (VPSTR (dlocal)); //before move starts, get size of item to be moved E2_BarData pdata = { 0,0 }; //if (! e2_fs_tw (slocal, _e2p_mvbar_twcb, &pdata, -1, E2TW_PHYS E2_ERR_NONE()); //) //{ //FIXME handle error //} //work with a temp name so that the backend doesn't create its own //(which would prevent destination-monitoring) gchar *templocal = e2_utils_get_tempname (VPSTR (dlocal)); #ifdef E2_VFS VPATH tempdata = { templocal, dlocal->spacedata }; #endif key_t key = ftok (templocal, pthread_self ()); gint shmid = shmget (key, 2*sizeof(gboolean), 0600 | IPC_CREAT); // gboolean *results = NULL; // pthread_cleanup_push ((gpointer)_e2p_mvbar_abort_clean_memory, &results); FIXME gboolean *results = shmat (shmid, NULL, 0); results[0] = FALSE; //result-value returned by backend task function results[1] = FALSE; //set TRUE when the backend task is completed (valid or not) pid_t pid = fork (); if (pid == 0) { //child //CHECKME BGL mutex here ? //threads enter/leave don't hang, at least, but any message printed here //generates a SIGCHILD but no actual message ? results[0] = e2_task_backend_move (slocal, #ifdef E2_VFS &tempdata); #else templocal); #endif results[1] = TRUE; //finished now _exit (0); } if (pid < 0) { //fork error printd (WARN,"action-process-create error!"); g_free (templocal); //cleanup shared memory if (shmdt (results) == 0) shmctl (shmid, IPC_RMID, NULL); return CANCEL; } //parent process //for very quick operation, don't bother with anything fancy guint waitup; if (!realmove) waitup = 1000; //shorter delay if the move is logical (same device) else waitup = 50000; //longer delay if the move needs to be physical usleep (waitup); if (!results[1]) { //task not finished already, do progress reporting //to reduce race-risk, this push should be before the fork, but in that case //we can't pop in case of fork error pthread_cleanup_push ((gpointer)_e2p_mvbar_abort_clean_process, &pid); gchar *shortsrc = e2_utils_str_shorten (src, MAX_CHAR, E2_DOTS_START); gchar *shortdest = e2_utils_str_shorten (dest_dir, MAX_CHAR, E2_DOTS_START); gchar *num1 = g_strdup_printf ("%"PRIu64, bdata->count); //gettext workaround gchar *num2 = g_strdup_printf ("%"PRIu64, tdata->count); gchar *labeltext = g_strdup_printf ( _("moving %s\nto %s\nthis is item %s of %s"), shortsrc, shortdest, num1, num2); gdk_threads_enter (); gtk_label_set_text (GTK_LABEL (wdata->label), labeltext); #ifdef USE_GTK2_18 if (!gtk_widget_get_visible (wdata->dialog)) #else if (!GTK_WIDGET_VISIBLE (wdata->dialog)) #endif gtk_widget_show (wdata->dialog); gdk_threads_leave (); g_free (shortsrc); g_free (shortdest); g_free (num1); g_free (num2); g_free (labeltext); gchar *progress_format = _("%.2f MB of %.2f MB (%.0f\%%)"); gfloat fraction; guint64 progress; E2_ProgressData m_data; #ifdef E2_VFS m_data.dlocal = &tempdata; #else m_data.dlocal = templocal; #endif m_data.done_size = 0; /* //rough approach to setting the reporting interval (usec) guint refresh_interval; if (pdata.totalsize < 10000000) refresh_interval = MIN_UPDATE_INTERVAL; else refresh_interval = MAX_UPDATE_INTERVAL; //with a cap */ while (!results[1]) //loop until the action is completed { if (wdata->bflags & E2_BARTASK_STOPPED) //user wants to abort { //cancel the task process kill (pid, SIGKILL); //CHECKME properly abort the whole process gdk_threads_enter (); gtk_widget_destroy (wdata->dialog); gdk_threads_leave (); //cleanup anything part-copied #ifdef E2_VFS e2_task_backend_delete (&tempdata); #else e2_task_backend_delete (templocal); #endif g_free (templocal); //FIXME cleanup cancellation push(es) //cleanup shared memory if (shmdt (results) == 0) shmctl (shmid, IPC_RMID, NULL); return NO_TO_ALL; } _e2p_mvbar_progress (&m_data); progress = m_data.done_size + bdata->totalsize; fraction = (gdouble) progress / tdata->totalsize; //deal with rounding errors if (fraction > 1.0) fraction = 1.0; g_snprintf (progresstext, sizeof (progresstext), progress_format, progress / 1048576.0, tdata->totalsize / 1048576.0, fraction * 100.0); gdk_threads_enter (); gtk_progress_bar_set_text (GTK_PROGRESS_BAR (wdata->progbar), progresstext); gtk_progress_bar_set_fraction (GTK_PROGRESS_BAR (wdata->progbar), fraction); gdk_threads_leave (); if (wdata->bflags & E2_BARTASK_PAUSEREQ //pause was requested && GTK_IS_DIALOG (wdata->dialog) && #ifdef USE_GTK2_18 gtk_widget_get_visible (wdata->dialog)) #else GTK_WIDGET_VISIBLE (wdata->dialog)) #endif { wdata->bflags &= ~E2_BARTASK_PAUSEREQ; wdata->loop = e2_main_loop_new (FALSE); if (wdata->loop != NULL) { kill (pid, SIGSTOP); wdata->bflags |= E2_BARTASK_PAUSED; e2_filelist_enable_refresh (); pthread_cleanup_push ((gpointer)gdk_threads_leave, NULL); gdk_threads_enter (); e2_main_loop_run (wdata->loop); pthread_cleanup_pop (1); kill (pid, SIGCONT); //don't get to here if stopped or aborted } } else usleep (100000); //no need to hog CPU by refreshing too fast (cancellation point) } //show the full-time score ASAP if (results[0] //move succeeded && GTK_IS_WIDGET (wdata->dialog) && #ifdef USE_GTK2_18 gtk_widget_get_visible (wdata->dialog)) #else GTK_WIDGET_VISIBLE (wdata->dialog)) #endif { progress = pdata.totalsize + bdata->totalsize; g_snprintf (progresstext, sizeof (progresstext), progress_format, progress / 1048576.0, tdata->totalsize / 1048576.0, 100.0); gdk_threads_enter (); gtk_progress_bar_set_text (GTK_PROGRESS_BAR (wdata->progbar), progresstext); gtk_progress_bar_set_fraction (GTK_PROGRESS_BAR (wdata->progbar), (gdouble) progress / tdata->totalsize); gdk_threads_leave (); //update progressive total bdata->totalsize = progress; } //CHECKME some sort of wait here ?? usleep (100000); //small delay to (sort of) show the completion status ? //need to slow down ... //this approach is empirical, needs to be validated for various systems // if (realmove && bdata->count > 300 && bdata->count%10 == 0) // g_usleep (50000); //small delay to allow filesystem to complete its business // if (!(wdata->flags & E2_BARTASK_PAUSED)) // gtk_widget_hide (wdata->dialog); pthread_cleanup_pop (0); } else bdata->totalsize += pdata.totalsize; if (results[0]) //move succeeded e2_task_backend_rename ( #ifdef E2_VFS &tempdata, dlocal); #else templocal, dlocal); #endif else //cleanup anything part-moved e2_task_backend_delete ( #ifdef E2_VFS &tempdata); #else templocal); #endif // pthread_cleanup_pop (0); g_free (templocal); //cleanup shared memory if (shmdt (results) == 0) shmctl (shmid, IPC_RMID, NULL); return OK; } /** @brief "response" signal callback for progress dialog @param dialog UNUSED the dialog where the response was generated @param response the response returned from the dialog @param wdata pointer to data struct for the dialog @return */ static void _e2p_mvbar_response_cb (GtkDialog *dialog, gint response, E2_BarWindowData *wdata) { switch (response) { case E2_RESPONSE_USER1: //pause if ((wdata->bflags & E2_BARTASK_PAUSED) == 0) { //not paused already gtk_widget_set_sensitive (wdata->pause_btn, FALSE); gtk_widget_set_sensitive (wdata->resume_btn, TRUE); gtk_widget_grab_focus (wdata->resume_btn); wdata->bflags |= E2_BARTASK_PAUSEREQ; //actual pause is started near end of _e2p_cpbar_exec() } break; case E2_RESPONSE_USER2: //resume wdata->bflags &= ~E2_BARTASK_PAUSEREQ; //insurance if (wdata->bflags & E2_BARTASK_PAUSED) { gtk_widget_set_sensitive (wdata->resume_btn, FALSE); gtk_widget_set_sensitive (wdata->pause_btn, TRUE); gtk_widget_grab_focus (wdata->pause_btn); wdata->bflags &= ~E2_BARTASK_PAUSED; e2_filelist_disable_refresh (); e2_main_loop_quit (wdata->loop); wdata->loop = NULL; } break; case E2_RESPONSE_NOTOALL: //cancel case GTK_RESPONSE_NONE: //dialog hidden or destroyed case GTK_RESPONSE_DELETE_EVENT: wdata->bflags |= E2_BARTASK_STOPPED; wdata->bflags &= ~E2_BARTASK_PAUSEREQ; //insurance //handle paused copying if (wdata->bflags & E2_BARTASK_PAUSED) { wdata->bflags &= ~E2_BARTASK_PAUSED; e2_filelist_disable_refresh (); e2_main_loop_quit (wdata->loop); wdata->loop = NULL; } default: break; } } /** @brief mvbar action This sets up progress display window, and processes each selected item @art ->data may contain pointerised flags indicating how the copy is to be done @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_mvbar (gpointer from, E2_ActionRuntime *art) { return (e2_task_enqueue_task (E2_TASK_MOVE, art, from, _e2p_mvbarQ, e2_task_refresh_lists)); } static gboolean _e2p_mvbarQ (E2_ActionTaskData *qed) { //handle case of specific data instead of selection?? if (!strcmp (qed->currdir, qed->othrdir)) { //display some message ?? return FALSE; } E2_ERR_DECLARE //FIXME change dir permission if possible ? #ifdef E2_VFS VPATH sdata = { qed->currdir, qed->currspace }; VPATH ddata = { qed->othrdir, qed->othrspace }; if (e2_fs_access (&ddata, W_OK E2_ERR_PTR())) #else if (e2_fs_access (qed->othrdir, W_OK E2_ERR_PTR())) #endif { e2_fs_error_local (_("Cannot put anything in %s"), #ifdef E2_VFS &ddata E2_ERR_MSGL()); #else qed->othrdir E2_ERR_MSGL()); #endif E2_ERR_CLEAR return FALSE; } GPtrArray *names = qed->names; GtkWidget *dialog_vbox; //, *hbox; GString *src = g_string_sized_new (1024); GString *dest = g_string_sized_new (1024); //determine whether move will be physical or logical gboolean realmove; #ifdef E2_VFS if (qed->currspace != qed->othrspace) realmove = TRUE; else { #endif dev_t device; struct stat statbuf; #ifdef E2_VFS sdata.spacedata = qed->currspace; if (!e2_fs_stat (&sdata, &statbuf E2_ERR_NONE())) #else if (!e2_fs_stat (qed->currdir, &statbuf E2_ERR_NONE())) #endif device = statbuf.st_dev; else device = -1; #ifdef E2_VFS if (!e2_fs_stat (&ddata, &statbuf E2_ERR_NONE())) #else if (!e2_fs_stat (qed->othrdir, &statbuf E2_ERR_NONE())) #endif realmove = (statbuf.st_dev != device); else realmove = TRUE; #ifdef E2_VFS } #endif printd (DEBUG, (realmove) ? "physical move":"logical move"); //setup the information window E2_BarWindowData windowdata; windowdata.bflags = 0; windowdata.loop = NULL; //no pause yet windowdata.dialog = NULL; //setup for abort pthread_cleanup_push ((gpointer)_e2p_mvbar_abort_clean_dialog, &windowdata.dialog); gdk_threads_enter (); windowdata.dialog = e2_dialog_create (NULL, NULL, _("moving"), (ResponseFunc)_e2p_mvbar_response_cb, &windowdata); e2_dialog_setup (windowdata.dialog, app.main_window); gdk_threads_leave (); #ifndef USE_GTK3_0 gtk_dialog_set_has_separator (GTK_DIALOG (windowdata.dialog), FALSE); #endif dialog_vbox = #ifdef USE_GTK2_14 gtk_dialog_get_content_area (GTK_DIALOG (windowdata.dialog)); #else GTK_DIALOG (windowdata.dialog)->vbox; #endif windowdata.label = e2_widget_add_mid_label (dialog_vbox, "", 0.0, FALSE, 0); // hbox = e2_widget_add_box (dialog_vbox, FALSE, 0, TRUE, FALSE, 0); // windowdata.label2 = e2_widget_add_mid_label (hbox, "", 0.5, TRUE, 0); windowdata.progbar = gtk_progress_bar_new (); gtk_box_pack_start (GTK_BOX (dialog_vbox), windowdata.progbar, TRUE, TRUE, E2_PADDING_LARGE); //buttons E2_Button local_btn = { _("_Pause"), GTK_STOCK_MEDIA_PAUSE, //_("Suspend moving"), E2_BTN_TIPPED, E2_BTN_TIPPED, NULL, 0, 0, E2_RESPONSE_USER1 }; windowdata.pause_btn = e2_dialog_add_defined_button (windowdata.dialog, &local_btn); local_btn.label = _("_Resume"); local_btn.stock = GTK_STOCK_MEDIA_PLAY; //local_btn.tip = _("Resume moving after pause"); local_btn.response = E2_RESPONSE_USER2; windowdata.resume_btn = e2_dialog_add_defined_button (windowdata.dialog, &local_btn); //this one is disabled for now gtk_widget_set_sensitive (windowdata.resume_btn, FALSE); local_btn = E2_BUTTON_CANCEL; local_btn.showflags |= E2_BTN_DEFAULT; windowdata.stop_btn = e2_dialog_add_defined_button (windowdata.dialog, &local_btn); //show everything but the dialog itself gtk_widget_show_all (dialog_vbox); e2_dialog_set_negative_response (windowdata.dialog, E2_BUTTON_CANCEL.response); //accumulate non-recursed count and total size of src item(s) E2_BarData totaldata = { 0,0 }; guint count; E2_SelectedItemInfo **iterator = (E2_SelectedItemInfo **) names->pdata; for (count=0; count < names->len; count++, iterator++) { g_string_printf (src, "%s%s", qed->currdir, (*iterator)->filename); //separator comes with dir #ifdef E2_VFS sdata.path = src->str; #endif //if (! e2_fs_tw ( #ifdef E2_VFS &sdata, #else src->str, #endif _e2p_mvbar_twcb, &totaldata, -1, E2TW_PHYS E2_ERR_NONE()); //) //{ //FIXME handle error //} } totaldata.count = names->len; //not interested in nested count gboolean check = e2_option_bool_get ("confirm-overwrite") #ifdef E2_VFS && qed->currspace == qed->othrspace #endif ; E2_BarData progressdata = { 1,0 }; OW_ButtonFlags extras = (totaldata.count > 1) ? BOTHALL : NONE; iterator = (E2_SelectedItemInfo **) names->pdata; e2_filelist_disable_refresh (); for (count=0; count < names->len; count++, iterator++) { if (windowdata.bflags & E2_BARTASK_STOPPED) break; //user pressed stop btn or closed info window //src_dir, dest_dir have trailing "/" g_string_printf (src, "%s%s", qed->currdir, (*iterator)->filename); //separator comes with dir g_string_printf (dest, "%s%s", qed->othrdir, (*iterator)->filename); DialogButtons result; #ifdef E2_VFS sdata.path = src->str; ddata.path = dest->str; if (check && e2_fs_access2 (&ddata E2_ERR_NONE()) == 0) #else if (check && e2_fs_access2 (dest->str E2_ERR_NONE()) == 0) #endif { *qed->status = E2_TASK_PAUSED; result = e2_dialog_ow_check ( #ifdef E2_VFS &sdata, &ddata, #else src->str, dest->str, #endif extras); *qed->status = E2_TASK_RUNNING; switch (result) { case YES_TO_ALL: check = FALSE; case OK: result = _e2p_mvbar_exec ( #ifdef E2_VFS &sdata, &ddata, #else src->str, dest->str, #endif realmove, GPOINTER_TO_INT (qed->action->data), &progressdata, &totaldata, &windowdata); break; case CANCEL: break; default: result = NO_TO_ALL; break; } } else //no overwrite, or don't care { result = _e2p_mvbar_exec ( #ifdef E2_VFS &sdata, &ddata, #else src->str, dest->str, #endif realmove, GPOINTER_TO_INT (qed->action->data), &progressdata, &totaldata, &windowdata); } if (result == NO_TO_ALL) { break; } progressdata.count++; } pthread_cleanup_pop (1); //always cleanup any dialog g_string_free (src, TRUE); g_string_free (dest, TRUE); #ifdef E2_FAM e2_filelist_request_refresh (other_view->dir, FALSE); e2_filelist_request_refresh (curr_view->dir, TRUE); #else e2_filelist_check_dirty (GINT_TO_POINTER (1)); #endif e2_filelist_enable_refresh (); return TRUE; } //aname must be confined to this module static gchar *aname; /** @brief initialize this plugin @param p pointer to plugin data struct to be populated @return TRUE if the intialization succeeded */ gboolean init_plugin (Plugin *p) { #define ANAME "mvbar" aname = _("mvbar"); p->signature = ANAME VERSION; p->menu_name = _("_Move"); p->description = _("Move selected item(s), with displayed progress details"); p->icon = "plugin_move"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_mvbar,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) { gchar *action_name = g_strconcat (_A(6),".",aname,NULL); gboolean ret = e2_plugins_action_unregister (action_name); g_free (action_name); return ret; }