/** * @file mce-io.c * Generic I/O functionality for the Mode Control Entity *

* Copyright © 2006-2011 Nokia Corporation and/or its subsidiary(-ies). * Copyright (C) 2012-2019 Jolla Ltd. *

* @author David Weinehall * @author Santtu Lakkala * @author Jukka Turunen * @author Simo Piiroinen * * mce is free software; you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License * version 2.1 as published by the Free Software Foundation. * * mce 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 * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with mce. If not, see . */ #include "mce-io.h" #include "mce.h" #include "mce-log.h" #include "mce-lib.h" #include "mce-wakelock.h" #ifdef ENABLE_WAKELOCKS # include "libwakelock.h" #endif #include #include #include #include #include #include #include #ifndef TFD_TIMER_CANCELON_SET # define TFD_TIMER_CANCELON_SET (1<<1) #endif /* ========================================================================= * * CONSTANTS * ========================================================================= */ /** Suffix used for temporary files */ #define TMP_SUFFIX ".tmp" /* ========================================================================= * * TYPES * ========================================================================= */ /** I/O monitor type */ typedef enum { IOMON_UNSET = -1, /**< I/O monitor type unset */ IOMON_STRING = 0, /**< String I/O monitor */ IOMON_CHUNK = 1, /**< Chunk I/O monitor */ } iomon_type; /** I/O monitor structure */ struct mce_io_mon_t { gchar *path; /**< Monitored file */ iomon_type type; /**< Monitor type */ gulong chunk_size; /**< Read-chunk size */ gboolean seekable; /**< is the I/O channel seekable */ gboolean suspended; /**< Is the I/O monitor suspended? */ GIOChannel *iochan; /**< I/O channel */ guint iowatch_id; /**< GSource ID for input */ mce_io_mon_notify_cb nofity_cb; /**< Input handling callback */ mce_io_mon_delete_cb delete_cb; /**< Iomon delete callback */ error_policy_t error_policy; /**< Error policy */ gboolean rewind_policy; /**< Rewind policy */ void *user_data; /**< Attached user data block */ mce_io_mon_free_cb user_free_cb;/**< Callback for freeing user_data */ }; /* ========================================================================= * * STATE_DATA * ========================================================================= */ /** List of all file monitors */ static GSList *file_monitors = NULL; /* ========================================================================= * * PROTOTYPES * ========================================================================= */ // SUSPEND_DETECTION static void io_detect_resume (void); static gboolean mce_io_resume_timer_cb (GIOChannel *chn, GIOCondition cnd, gpointer aptr); static bool mce_io_prime_resume_timer (void); void mce_io_init_resume_timer (void); void mce_io_quit_resume_timer (void); // GLIB_IO_HELPERS const char *mce_io_condition_repr (GIOCondition cond); const char *mce_io_status_name (GIOStatus io_status); // IO_MONITOR static mce_io_mon_t *mce_io_mon_create (const char *path, mce_io_mon_delete_cb delete_cb); static void mce_io_mon_delete (mce_io_mon_t *self); static void mce_io_mon_probe_seekable (mce_io_mon_t *self); static gboolean mce_io_mon_read_chunks (GIOChannel *source, GIOCondition condition, gpointer data); static gboolean mce_io_mon_read_string (GIOChannel *source, GIOCondition condition, gpointer data); static gboolean mce_io_mon_input_cb (GIOChannel *source, GIOCondition condition, gpointer data); static mce_io_mon_t *mce_io_mon_register (gint fd, const gchar *path, error_policy_t error_policy, gboolean rewind_policy, mce_io_mon_notify_cb callback, mce_io_mon_delete_cb delete_cb); mce_io_mon_t *mce_io_mon_register_string (const gint fd, const gchar *const file, error_policy_t error_policy, gboolean rewind_policy, mce_io_mon_notify_cb callback, mce_io_mon_delete_cb delete_cb); mce_io_mon_t *mce_io_mon_register_chunk (const gint fd, const gchar *const file, error_policy_t error_policy, gboolean rewind_policy, mce_io_mon_notify_cb callback, mce_io_mon_delete_cb delete_cb, gulong chunk_size); void mce_io_mon_unregister (mce_io_mon_t *iomon); void mce_io_mon_unregister_list (GSList *list); void mce_io_mon_unregister_at_path (const char *path); void mce_io_mon_suspend (mce_io_mon_t *iomon); void mce_io_mon_resume (mce_io_mon_t *iomon); const gchar *mce_io_mon_get_path (const mce_io_mon_t *iomon); int mce_io_mon_get_fd (const mce_io_mon_t *iomon); // MISC_UTILS guint mce_io_add_watch (int fd, bool close_on_unref, GIOCondition cnd, GIOFunc io_cb, gpointer aptr); gboolean mce_close_file (const gchar *const file, FILE **fp); gboolean mce_read_chunk_from_file (const gchar *const file, void **data, gssize *len, int flags); gboolean mce_read_string_from_file (const gchar *const file, gchar **string); gboolean mce_read_number_string_from_file (const gchar *const file, gulong *number, FILE **fp, gboolean rewind_file, gboolean close_on_exit); gboolean mce_write_string_to_file (const gchar *const file, const gchar *const string); void mce_close_output (output_state_t *output); gboolean mce_write_number_string_to_file (output_state_t *output, const gulong number); gboolean mce_write_number_string_to_file_atomic (const gchar *const file, const gulong number); gboolean mce_are_settings_locked (void); gboolean mce_unlock_settings (void); static gboolean mce_io_read_all (int fd, void *buff, size_t size, size_t *pdone); static gboolean mce_io_write_all (int fd, const void *buff, size_t size, size_t *pdone); void *mce_io_load_file (const char *path, size_t *psize); void *mce_io_load_file_until_eof (const char *path, size_t *psize); gboolean mce_io_save_file (const char *path, const void *data, size_t size, mode_t mode); gboolean mce_io_save_to_existing_file (const char *path, const void *data, size_t size); gboolean mce_io_save_file_atomic (const char *path, const void *data, size_t size, mode_t mode, gboolean keep_backup); gboolean mce_io_update_file_atomic (const char *path, const void *data, size_t size, mode_t mode, gboolean keep_backup); /* ========================================================================= * * SUSPEND_DETECTION * ========================================================================= */ /** Detect suspend/resume cycle from CLOCK_MONOTONIC vs CLOCK_BOOTTIME */ static void io_detect_resume(void) { static int64_t prev = 0; int64_t boot = mce_lib_get_boot_tick(); int64_t mono = mce_lib_get_mono_tick(); int64_t diff = boot - mono; int64_t skip = diff - prev; // small jitter can be due to scheduling too if( skip < 100 ) goto EXIT; prev = diff; // no logging from the 1st time skip if( prev == skip ) goto EXIT; mce_log(LL_DEVEL, "time skip: assume %"PRId64".%03"PRId64"s suspend", skip / 1000, skip % 1000); // notify in case some timers need re-evaluating datapipe_exec_full(&resume_detected_event_pipe, &prev); EXIT: return; } /** Timerfd we use for detecting resume */ static int mce_io_resume_timer_fd = -1; /** Glib io watch for mce_io_resume_timer_fd */ static guint mce_io_resume_timer_id = 0; /** Virtual wakelock used for protecting resume timer wakeups */ static const char mce_io_resume_timer_wakelock[] = "mce_io_resume_timer"; /** Glib io callback for mce_io_resume_timer_fd */ static gboolean mce_io_resume_timer_cb(GIOChannel *chn, GIOCondition cnd, gpointer aptr) { (void)chn; (void)aptr; /* Deny suspending while handling timer wakeup */ mce_wakelock_obtain(mce_io_resume_timer_wakelock, -1); gboolean result = G_SOURCE_REMOVE; if( mce_io_resume_timer_fd == -1 || mce_io_resume_timer_id == 0 ) { mce_log(LL_WARN, "stray resume timer wakeup"); goto EXIT; } if( cnd & ~G_IO_IN ) { mce_log(LL_CRIT, "unexpected resume timer wakeup: %s", mce_io_condition_repr(cnd)); goto EXIT; } /* Read trigger count. Expected result is ECANCELED */ uint64_t cnt = 0; int res = read(mce_io_resume_timer_fd, &cnt, sizeof cnt); if( res == -1 ) { if( errno == EAGAIN || errno == EINTR ) { /* Silentry ignore temporary errors */ } else if( errno == ECANCELED ) { /* The expected error */ mce_log(LL_DEBUG, "resume timer wakeup"); io_detect_resume(); if( !mce_io_prime_resume_timer() ) goto EXIT; } else { /* Some unexpected error */ mce_log(LL_CRIT, "can't read resume timer: %m"); goto EXIT; } } else { /* Silentry ignore timer actually triggering */ if( !mce_io_prime_resume_timer() ) goto EXIT; } result = G_SOURCE_CONTINUE; EXIT: if( result != G_SOURCE_CONTINUE && mce_io_resume_timer_id ) { mce_log(LL_CRIT, "disabling resume timer"); mce_io_resume_timer_id = 0; mce_io_quit_resume_timer(); } mce_wakelock_release(mce_io_resume_timer_wakelock); return result; } /** Program resume timer fd */ static bool mce_io_prime_resume_timer(void) { bool ack = false; if( mce_io_resume_timer_fd == -1 ) goto EXIT; /* Use dummy interval as we are basically only interested in * getting ECANCELED when CLOCK_REALTIME is adjusted due to * device resuming from suspend. */ struct itimerspec its = { }; int flags = TFD_TIMER_ABSTIME | TFD_TIMER_CANCELON_SET; if( timerfd_settime(mce_io_resume_timer_fd, flags, &its, 0) == -1 ) { mce_log(LL_WARN, "can't program resume timer: %m"); goto EXIT; } ack = true; EXIT: return ack; } /** Create resume timer fd */ void mce_io_init_resume_timer(void) { bool ack = false; /* Create realtime clock timerfd */ int clockid = CLOCK_REALTIME; int flags = O_NONBLOCK | O_CLOEXEC; mce_io_resume_timer_fd = timerfd_create(clockid, flags); if( mce_io_resume_timer_fd == -1 ) { mce_log(LL_WARN, "timerfd_create: %m"); goto EXIT; } /* Program dummy wakeup time */ if( !mce_io_prime_resume_timer() ) goto EXIT; /* Add io watch for the timerfd */ mce_io_resume_timer_id = mce_io_add_watch(mce_io_resume_timer_fd, false, G_IO_IN, mce_io_resume_timer_cb, 0); if( !mce_io_resume_timer_id ) goto EXIT; ack = true; EXIT: if( !ack ) { mce_io_quit_resume_timer(); mce_log(LL_WARN, "detect resume via timerfd not in use"); } } /** Delete resume timer fd */ void mce_io_quit_resume_timer(void) { if( mce_io_resume_timer_id ) { g_source_remove(mce_io_resume_timer_id), mce_io_resume_timer_id = 0; } if( mce_io_resume_timer_fd != -1 ) { close(mce_io_resume_timer_fd), mce_io_resume_timer_fd = -1; } } /* ========================================================================= * * GLIB_IO_HELPERS * ========================================================================= */ /** * Get glib io condition as human readable string * * @param cond Bitmap of glib io conditions * * @return Names of bits set, separated with " | " */ const char *mce_io_condition_repr(GIOCondition cond) { static const struct { GIOCondition bit; const char *name; } lut[] = { { .bit = G_IO_IN, .name = "IN" }, { .bit = G_IO_OUT, .name = "OUT" }, { .bit = G_IO_PRI, .name = "PRI" }, { .bit = G_IO_ERR, .name = "ERR" }, { .bit = G_IO_HUP, .name = "HUP" }, { .bit = G_IO_NVAL, .name = "NVAL" }, // sentinel { .bit = 0, .name = 0 } }; static char buf[64]; char *end = buf + sizeof buf - 1; char *pos = buf; auto void add(const char *s); auto void add(const char *s) { while( pos < end && *s ) *pos++ = *s++; } for( size_t i = 0; lut[i].bit; ++i ) { if( cond & lut[i].bit ) { cond ^= lut[i].bit; if( pos > buf ) add("|"); add(lut[i].name); } } *pos = 0; if( cond ) { if( pos > buf ) add("|"); snprintf(pos, end - pos, "0x%x", cond); } return buf; } /** * Get glib io status as human readable string * * @param io_status as returned from g_io_channel_read_chars() * * @return Name of the status enum, without the common prefix */ const char *mce_io_status_name(GIOStatus io_status) { const char *status_name = "UNKNOWN"; switch (io_status) { case G_IO_STATUS_NORMAL: status_name = "NORMAL"; break; case G_IO_STATUS_ERROR: status_name = "ERROR"; break; case G_IO_STATUS_EOF: status_name = "EOF"; break; case G_IO_STATUS_AGAIN: status_name = "AGAIN"; break; default: break; // ... just to keep static analysis happy } return status_name; } /* ========================================================================= * * IO_MONITOR * ========================================================================= */ /** Create I/O monitor object * * Allocates I/O monitor object and does all initialization that * does not need monitoring type information. * * Specifically the io watch is not activated from within this * function, it needs to be done separately. * * @param path File path * @param delete_cb I/O monitor object delete notification callback * * @return I/O monitor object */ static mce_io_mon_t *mce_io_mon_create(const char *path, mce_io_mon_delete_cb delete_cb) { mce_io_mon_t *self = 0; if( !path ) { mce_log(LL_ERR, "path == NULL!"); goto EXIT; } if( !delete_cb ) { mce_log(LL_ERR, "delete_cb == NULL!"); goto EXIT; } if( !(self = g_slice_new(mce_io_mon_t)) ) goto EXIT; memset(self, 0, sizeof *self); /* Fill in sane default values */ self->path = g_strdup(path); self->type = IOMON_UNSET; self->chunk_size = 0; self->seekable = FALSE; self->suspended = TRUE; self->iochan = 0; self->iowatch_id = 0; self->nofity_cb = 0; self->delete_cb = delete_cb; self->error_policy = MCE_IO_ERROR_POLICY_WARN; self->rewind_policy = FALSE; self->user_data = 0; self->user_free_cb = 0; mce_log(LL_DEBUG, "adding monitor for: %s", self->path); EXIT: return self; } /** Delete I/O monitor object * * Calls delete notification callback to allow upper level * logic to perform cleanup. * * Then removes io watch, closes io channel and releases * all dynamic resources associated with the I/O monitor object. * * @param self I/O monitor object */ static void mce_io_mon_delete(mce_io_mon_t *self) { if( !self ) goto EXIT; mce_log(LL_NOTICE, "removing monitor for: %s", self->path); /* Call the about to delete callback */ if( self->delete_cb ) { self->delete_cb(self); } /* Free attached user data */ if( self->user_data && self->user_free_cb ) self->user_free_cb(self->user_data); /* Unlink from monitor list */ if( !g_slist_find(file_monitors, self) ) { mce_log(LL_WARN, "Trying to unregister non-registered" " file monitor"); } else { file_monitors = g_slist_remove(file_monitors, self); } /* Remove I/O watch */ mce_io_mon_suspend(self); /* Close the I/O channel */ if( self->iochan ) { GError *error = NULL; GIOStatus iostatus = g_io_channel_shutdown(self->iochan, TRUE, &error); if( iostatus != G_IO_STATUS_NORMAL ) { loglevel_t loglevel = LL_ERR; /* If we get ENODEV, only log a debug message, * since this happens for hotpluggable * /dev/input files */ if( (error->code == G_IO_CHANNEL_ERROR_FAILED) && (errno == ENODEV) ) loglevel = LL_DEBUG; mce_log(loglevel, "Cannot close `%s'; %s", self->path, error->message); } g_clear_error(&error); g_io_channel_unref(self->iochan); self->iochan = 0; } /* Forget file path */ g_free(self->path), self->path = 0; /* Reset to something that is likely to generate segfaults * if it ends up used after freeing ... */ memset(self, 0xff, sizeof *self); g_slice_free(mce_io_mon_t, self); EXIT: return; } /** * Check if the monitored io channel is truly seekable * * Glib seems to be making guesses based on file type and * gets it massively wrong for the files MCE needs to read. */ static void mce_io_mon_probe_seekable(mce_io_mon_t *self) { gboolean glib = FALSE, kernel = FALSE; /* glib assumes ... */ if (g_io_channel_get_flags(self->iochan) & G_IO_FLAG_IS_SEEKABLE) { glib = TRUE; } /* ... kernel knows */ if (lseek64(g_io_channel_unix_get_fd(self->iochan), 0, SEEK_CUR) != -1) { kernel = TRUE; } /* report the difference */ if (kernel != glib) { mce_log(LL_DEBUG, "%s: is %sseekable, while glib thinks it is %sseekable", self->path, kernel ? "" : "NOT ", glib ? "" : "NOT "); } self->seekable = kernel; } /** Process input for chunked io monitor * * For use from mce_io_mon_input_cb() only. * * @param source The source of the activity * @param condition The I/O condition * @param data The iomon structure * * @return TRUE on success, FALSE on failure */ static gboolean mce_io_mon_read_chunks(GIOChannel *source, GIOCondition condition, gpointer data) { gboolean status = FALSE; mce_io_mon_t *iomon = data; gchar *buffer = NULL; gsize bytes_want = 4096; gsize bytes_have = 0; gsize chunks_have = 0; gsize chunks_done = 0; GError *error = NULL; GIOStatus io_status = G_IO_STATUS_NORMAL; #ifdef ENABLE_WAKELOCKS /* Since the locks on kernel side are released once all * events are read, we must obtain the userspace lock * before reading the available data */ wakelock_lock("mce_input_handler", -1); #endif /* We get input from evdev nodes at resume, handle that 1st */ io_detect_resume(); // paranoia mode: upper levels should take care of these if( !(condition & G_IO_IN) ) goto EXIT; if( !iomon ) goto EXIT; /* Seek to the beginning of the file before reading if needed */ if( iomon->rewind_policy ) { g_io_channel_seek_position(source, 0, G_SEEK_SET, &error); if( error ) { mce_log(LL_ERR, "%s: seek error: %s", iomon->path, error->message); g_clear_error(&error); } } /* Adjust read size to multiples of small sized chunks, * or size of one larger chunk */ if( iomon->chunk_size < bytes_want ) bytes_want -= bytes_want % iomon->chunk_size; else bytes_want = iomon->chunk_size; /* Allocate read buffer */ buffer = g_malloc(bytes_want); io_status = g_io_channel_read_chars(source, buffer, bytes_want, &bytes_have, &error); /* If the read was interrupted, ignore */ if( io_status == G_IO_STATUS_AGAIN ) { status = TRUE; goto EXIT; } if( error ) { mce_log(LL_ERR, "Error when reading from %s: %s", iomon->path, error->message); g_clear_error(&error); goto EXIT; } if( bytes_have % iomon->chunk_size ) { mce_log(LL_WARN, "Incomplete chunks read from: %s", iomon->path); } /* Process the data, and optionally ignore some of it */ chunks_have = bytes_have / iomon->chunk_size; if( !chunks_have ) { mce_log(LL_ERR, "Empty read from %s", iomon->path); } else { gchar *chunk = buffer; for( ; chunks_done < chunks_have ; chunk += iomon->chunk_size ) { ++chunks_done; if( !iomon->nofity_cb(iomon, chunk, iomon->chunk_size) ) { continue; } /* Ignore rest of the data already read */ if( !iomon->seekable ) break; /* Try to seek to end of the file */ g_io_channel_seek_position(iomon->iochan, 0, G_SEEK_END, &error); if( error ) { mce_log(LL_ERR, "Error when reading from %s: %s", iomon->path, error->message); g_clear_error(&error); } break; } } mce_log(LL_INFO, "%s: status=%s, data=%ld/%ld=%ld+%ld, skipped=%ld", iomon->path, mce_io_status_name(io_status), (long)bytes_have, (long)iomon->chunk_size, (long)chunks_have, (long)(bytes_have % iomon->chunk_size), (long)(chunks_have - chunks_done)); status = TRUE; EXIT: g_clear_error(&error); g_free(buffer); #ifdef ENABLE_WAKELOCKS /* Release the lock after we're done with processing it */ wakelock_unlock("mce_input_handler"); #endif return status; } /** Process input for string io monitor * * For use from mce_io_mon_input_cb() only. * * @param source The source of the activity * @param condition The I/O condition * @param data The iomon structure * * @return TRUE on success, FALSE on failure */ static gboolean mce_io_mon_read_string(GIOChannel *source, GIOCondition condition, gpointer data) { gboolean status = FALSE; mce_io_mon_t *iomon = data; gchar *str = NULL; gsize bytes_read = 0; GError *error = NULL; // paranoia mode: upper levels should take care of these if( !(condition & G_IO_IN) ) goto EXIT; if( !iomon ) goto EXIT; /* Seek to the beginning of the file before reading if needed */ if( iomon->rewind_policy ) { g_io_channel_seek_position(source, 0, G_SEEK_SET, &error); if( error ) { mce_log(LL_ERR, "%s: seek error: %s", iomon->path, error->message); g_clear_error(&error); } } g_io_channel_read_line(source, &str, &bytes_read, NULL, &error); if( error ) { mce_log(LL_ERR, "Error when reading from %s: %s", iomon->path, error->message); goto EXIT; } if( !bytes_read || !str || !*str ) mce_log(LL_ERR, "Empty read from %s",iomon->path); else iomon->nofity_cb(iomon, str, bytes_read); status = TRUE; EXIT: g_free(str); g_clear_error(&error); return status; } /** Callback for I/O watch * * Handles error conditions first; then does input monitor * type specific input processing. * * The I/O monitor will be disabled and deleted on errors. * Additionally the whole process can be terminated if * error policy requires it. * * @param source Unused * @param condition The GIOCondition for the error * @param data The iomon structure * * @return TRUE to keep iomon active, FALSE to disable it; * may also exit depending on error policy for iomon */ static gboolean mce_io_mon_input_cb(GIOChannel *source, GIOCondition condition, gpointer data) { (void)source; // unused mce_io_mon_t *iomon = data; gboolean keep_going = TRUE; gboolean terminate = FALSE; loglevel_t loglevel = LL_DEBUG; // sanity checks if( !iomon ) { mce_log(LL_ERR, "iomon == NULL!"); keep_going = FALSE; goto EXIT; } // error conditions if( condition & (G_IO_ERR | G_IO_HUP | G_IO_NVAL) ) { mce_log(LL_ERR, "iomon '%s' got %s", iomon->path, mce_io_condition_repr(condition)); keep_going = FALSE; goto EXIT; } // input processing if( condition & G_IO_IN ) { switch (iomon->type) { case IOMON_STRING: if( !mce_io_mon_read_string(source, condition, data) ) { mce_log(LL_WARN, "mce_io_mon_read_string failed"); } break; case IOMON_CHUNK: if( !mce_io_mon_read_chunks(source, condition, data) ) { mce_log(LL_WARN, "mce_io_mon_read_chunks failed"); } break; default: case IOMON_UNSET: mce_log(LL_WARN, "unknown iomon type"); keep_going = FALSE; break; } } EXIT: // cancel io monitor if( !keep_going && iomon ) { /* Mark error watch as removed */ iomon->iowatch_id = 0; /* Adjust actions based on error policy */ switch (iomon->error_policy) { case MCE_IO_ERROR_POLICY_EXIT: terminate = TRUE; loglevel = LL_CRIT; break; case MCE_IO_ERROR_POLICY_WARN: loglevel = LL_WARN; break; default: case MCE_IO_ERROR_POLICY_IGNORE: loglevel = LL_DEBUG; break; } /* Write log */ mce_log(loglevel, "disabling io monitor for: %s", iomon->path); /* Remove IO monitor */ mce_io_mon_unregister(iomon); } // terminate process if( terminate ) { mce_log(LL_CRIT, "terminating due to error policy"); mce_quit_mainloop(); } return keep_going; } /* ========================================================================= * * I/O MONITOR API * ========================================================================= */ /** * Register an I/O monitor; reads and returns data * * @param fd File Descriptor; this takes priority over file; -1 if not used * @param file Path to the file * @param error_policy MCE_IO_ERROR_POLICY_EXIT to exit on error, * MCE_IO_ERROR_POLICY_WARN to warn about errors * but ignore them, * MCE_IO_ERROR_POLICY_IGNORE to silently ignore errors * @param callback Function to call with result * @return An I/O monitor pointer on success, NULL on failure */ static mce_io_mon_t *mce_io_mon_register(gint fd, const gchar *path, error_policy_t error_policy, gboolean rewind_policy, mce_io_mon_notify_cb callback, mce_io_mon_delete_cb delete_cb) { bool success = false; mce_io_mon_t *iomon = 0; GError *error = NULL; /* Sanity checks */ if( !path ) { mce_log(LL_ERR, "path == NULL!"); goto EXIT; } if( !callback ) { mce_log(LL_ERR, "callback == NULL!"); goto EXIT; } /* Silently ignore non-existing files */ if( fd == -1 && access(path, F_OK) == -1 ) goto EXIT; /* Allocate monitor object */ if( !(iomon = mce_io_mon_create(path, delete_cb)) ) goto EXIT; /* Add to monitor list */ file_monitors = g_slist_prepend(file_monitors, iomon); /* Set custom props */ iomon->nofity_cb = callback; iomon->error_policy = error_policy; /* Set up io channel */ if( fd != -1 ) iomon->iochan = g_io_channel_unix_new(fd); else iomon->iochan = g_io_channel_new_file(path, "r", &error); if( !iomon->iochan ) { mce_log(LL_ERR, "Failed to open `%s'; %s", path, error ? error->message : "unknown error"); goto EXIT; } /* Transfer fd ownership to io channel */ g_io_channel_set_close_on_unref(iomon->iochan, TRUE), fd = -1; /* Glib seekability is broken, probe via syscall */ mce_io_mon_probe_seekable(iomon); /* Set rewind policy */ if( iomon->seekable ) { iomon->rewind_policy = rewind_policy; } else if( rewind_policy ) { mce_log(LL_ERR, "Attempting to set rewind policy to TRUE " "on non-seekable I/O channel `%s'", path); } success = true; EXIT: if( fd != -1 ) close(fd); if( !success ) mce_io_mon_delete(iomon), iomon = 0; g_clear_error(&error); return iomon; } /** Unregister an I/O monitor * * @param io_monitor A pointer to the I/O monitor to unregister */ void mce_io_mon_unregister(mce_io_mon_t *iomon) { mce_io_mon_delete(iomon); } /** Remove all touch device I/O monitors in a list * * @param list A list of I/O monitors */ void mce_io_mon_unregister_list(GSList *list) { GSList *now, *zen; for( now = list; now; now = zen ) { zen = now->next; mce_io_mon_unregister(now->data); } } /** Unregister I/O monitors for the given path * * @param path Path to file for which all monitors should be unregistered */ void mce_io_mon_unregister_at_path(const char *path) { GSList *now, *zen; if( !path ) goto EXIT; for( now = file_monitors; now; now = zen ) { zen = now->next; mce_io_mon_t *self = now->data; if( !self->path || strcmp(self->path, path) ) continue; mce_io_mon_unregister(self); } EXIT: return; } /** * Suspend an I/O monitor * * @param io_monitor A pointer to the I/O monitor to suspend */ void mce_io_mon_suspend(mce_io_mon_t *iomon) { if( !iomon ) { mce_log(LL_ERR, "iomon == NULL!"); goto EXIT; } /* Remove I/O watches */ if( iomon->iowatch_id ) { g_source_remove(iomon->iowatch_id), iomon->iowatch_id = 0; } iomon->suspended = TRUE; EXIT: return; } /** * Resume an I/O monitor * * @param io_monitor A pointer to the I/O monitor to resume */ void mce_io_mon_resume(mce_io_mon_t *iomon) { if( !iomon ) { mce_log(LL_ERR, "iomon == NULL!"); goto EXIT; } if( !iomon->suspended ) goto EXIT; /* Seek to the end of the file if the file is seekable, * and rewind policy is not requested */ if( iomon->seekable && !iomon->rewind_policy ) { GError *error = NULL; g_io_channel_seek_position(iomon->iochan, 0, G_SEEK_END, &error); if( error ) { mce_log(LL_ERR, "%s: seek error: %s", iomon->path, error->message); } g_clear_error(&error); } /* Set up input monitor */ if( iomon->iowatch_id ) g_source_remove(iomon->iowatch_id); iomon->iowatch_id = g_io_add_watch(iomon->iochan, G_IO_IN | G_IO_ERR | G_IO_HUP | G_IO_NVAL, mce_io_mon_input_cb, iomon); /* Mark as not-suspended */ iomon->suspended = FALSE; EXIT: return; } /** * Register an I/O monitor; reads and returns a string * * @param fd File Descriptor; this takes priority over file; -1 if not used * @param file Path to the file * @param error_policy MCE_IO_ERROR_POLICY_EXIT to exit on error, * MCE_IO_ERROR_POLICY_WARN to warn about errors * but ignore them, * MCE_IO_ERROR_POLICY_IGNORE to silently ignore errors * @param rewind_policy TRUE to seek to the beginning, * FALSE to stay at current position * @param callback Function to call with result * @return An I/O monitor cookie on success, NULL on failure */ mce_io_mon_t *mce_io_mon_register_string(const gint fd, const gchar *const file, error_policy_t error_policy, gboolean rewind_policy, mce_io_mon_notify_cb callback, mce_io_mon_delete_cb delete_cb) { mce_io_mon_t *iomon = NULL; iomon = mce_io_mon_register(fd, file, error_policy, rewind_policy, callback, delete_cb); if (iomon == NULL) goto EXIT; /* Set the I/O monitor type and call resume to add an I/O watch */ iomon->type = IOMON_STRING; mce_io_mon_resume(iomon); EXIT: return iomon; } /** * Register an I/O monitor; reads and returns a chunk of specified size * * @param fd File Descriptor; this takes priority over file; -1 if not used * @param file Path to the file * @param error_policy MCE_IO_ERROR_POLICY_EXIT to exit on error, * MCE_IO_ERROR_POLICY_WARN to warn about errors * but ignore them, * MCE_IO_ERROR_POLICY_IGNORE to silently ignore errors * @param rewind_policy TRUE to seek to the beginning, * FALSE to stay at current position * @param callback Function to call with result * @param chunk_size The number of bytes to read in each chunk * @return An I/O monitor cookie on success, NULL on failure */ mce_io_mon_t *mce_io_mon_register_chunk(const gint fd, const gchar *const file, error_policy_t error_policy, gboolean rewind_policy, mce_io_mon_notify_cb callback, mce_io_mon_delete_cb delete_cb, gulong chunk_size) { mce_io_mon_t *iomon = NULL; GError *error = NULL; iomon = mce_io_mon_register(fd, file, error_policy, rewind_policy, callback, delete_cb); if( !iomon ) goto EXIT; /* We only read this file in binary form */ g_io_channel_set_encoding(iomon->iochan, NULL, &error); g_clear_error(&error); /* No buffering since we're using this for reading data from * device drivers and need to keep the i/o state in sync * between kernel and user space for the automatic suspend * prevention via wakelocks to work */ g_io_channel_set_buffered(iomon->iochan, FALSE); /* Don't block */ g_io_channel_set_flags(iomon->iochan, G_IO_FLAG_NONBLOCK, &error); g_clear_error(&error); /* Set the I/O monitor type and call resume to add an I/O watch */ iomon->type = IOMON_CHUNK; iomon->chunk_size = chunk_size; mce_io_mon_resume(iomon); EXIT: return iomon; } /** * Return the name of the monitored file * * @param io_monitor An opaque pointer to the I/O monitor structure * @return The name of the monitored file */ const gchar *mce_io_mon_get_path(const mce_io_mon_t *iomon) { const gchar *path = 0; if( iomon ) path = iomon->path; return path; } /** * Return the file descriptor of the monitored file; * if the file being monitored was opened from a path * rather than a file descriptor, -1 is returned * * @param io_monitor An opaque pointer to the I/O monitor structure * @return The file descriptor of the monitored file */ int mce_io_mon_get_fd(const mce_io_mon_t *iomon) { int fd = -1; if( iomon && iomon->iochan ) fd = g_io_channel_unix_get_fd(iomon->iochan); return fd; } /** Attach user data block to io monitor * * If non-null free_cb callback is given, the user_data block will * be released using it when io-monitor itself is deleted. * * Note: The delete notification callback is called before the * user data is released, i.e. user data is still available * at that point. * * @param io_monitor An opaque pointer to the I/O monitor structure * @param user_data Data block to attach to io monitor, or NULL * @param free_cb Free function to release data block or NULL */ void mce_io_mon_set_user_data(mce_io_mon_t *iomon, void *user_data, mce_io_mon_free_cb free_cb) { if( !iomon ) goto EXIT; /* Clear already existing user data */ if( iomon->user_data && iomon->user_free_cb ) iomon->user_free_cb(iomon->user_data); /* Set user data */ iomon->user_data = user_data; iomon->user_free_cb = free_cb; EXIT: return; } /** Get user data block attached to io monitor * * @param io_monitor An opaque pointer to the I/O monitor structure * * @return user data block, or NULL if not set */ void *mce_io_mon_get_user_data(const mce_io_mon_t *iomon) { return iomon ? iomon->user_data : 0; } /* ========================================================================= * * MISC_UTILS * ========================================================================= */ /** Helper for creating I/O watch for file descriptor * * @param fd File descriptor for which to add glib io watch * @param close_on_unref True to transfer fd owhership to io watch * @param cnd Condition bitmask * @param io_cb Callback function * @param aptr User data to pass to the callback * * @return wath id on success, or 0 on failure */ guint mce_io_add_watch(int fd, bool close_on_unref, GIOCondition cnd, GIOFunc io_cb, gpointer aptr) { guint wid = 0; GIOChannel *chn = 0; if( !(chn = g_io_channel_unix_new(fd)) ) goto cleanup; g_io_channel_set_close_on_unref(chn, close_on_unref); cnd |= G_IO_ERR | G_IO_HUP | G_IO_NVAL; if( !(wid = g_io_add_watch(chn, cnd, io_cb, aptr)) ) goto cleanup; cleanup: if( chn != 0 ) g_io_channel_unref(chn); return wid; } /** * Helper function for closing files that checks for NULL, * prints proper error messages and NULLs the file pointer after close * * @param file The name of the file to close; only used by error messages * @param fp A pointer to the file pointer to close * @return TRUE on success, FALSE on failure */ gboolean mce_close_file(const gchar *const file, FILE **fp) { gboolean status = FALSE; if (fp == NULL) { mce_log(LL_CRIT, "fp == NULL!"); goto EXIT; } if (*fp == NULL) { status = TRUE; goto EXIT; } if (fclose(*fp) == EOF) { mce_log(LL_ERR, "Failed to close `%s'; %s", file ? file : "", g_strerror(errno)); status = FALSE; /* Ignore error */ errno = 0; goto EXIT; } *fp = NULL; status = TRUE; EXIT: return status; } /** * Read a chunk from a file * * @param file Path to the file, or NULL to use an already open fd instead * @param[out] data A newly allocated buffer with the first chunk from the file * @param[in,out] len [in] The length of the buffer to read * [out] The number of bytes read * @param flags Additional flags to pass to open(); * by default O_RDONLY is always passed -- this is mainly * to allow passing O_NONBLOCK * @return TRUE on success, FALSE on failure */ gboolean mce_read_chunk_from_file(const gchar *const file, void **data, gssize *len, int flags) { gboolean status = FALSE; gint again_count = 0; gssize result = -1; gint fd; if (file == NULL) { mce_log(LL_CRIT, "file == NULL!"); goto EXIT; } if (len == NULL) { mce_log(LL_CRIT, "len == NULL!"); goto EXIT; } if (*len <= 0) { mce_log(LL_CRIT, "*len <= 0!"); goto EXIT; } /* If we cannot open the file, abort */ if ((fd = open(file, O_RDONLY | flags)) == -1) { mce_log(LL_ERR, "Cannot open `%s' for reading; %s", file, g_strerror(errno)); /* Ignore error */ errno = 0; goto EXIT; } if ((*data = g_try_malloc(*len)) == NULL) { mce_log(LL_CRIT, "Failed to allocate memory (%zd bytes)!", *len); goto EXIT2; } while (again_count++ < 10) { /* Clear errors from earlier iterations */ errno = 0; result = read(fd, *data, *len); if ((result == -1) && ((errno == EAGAIN) || (errno == EWOULDBLOCK))) { continue; } else { break; } } if (result == -1) { mce_log(LL_ERR, "Failed to read from `%s'; %s", file, g_strerror(errno)); /* Ignore error */ errno = 0; goto EXIT2; } status = TRUE; EXIT2: if (close(fd) == -1) { mce_log(LL_ERR, "Failed to close `%s'; %s", file, g_strerror(errno)); errno = 0; } /* Ignore error */ errno = 0; *len = result; EXIT: return status; } /** * Read a string from a file * * @param file Path to the file * @param[out] string A newly allocated string with the first line of the file * @return TRUE on success, FALSE on failure */ gboolean mce_read_string_from_file(const gchar *const file, gchar **string) { GError *error = NULL; gboolean status = FALSE; if (file == NULL) { mce_log(LL_CRIT, "file == NULL!"); goto EXIT; } if (g_file_get_contents(file, string, NULL, &error) == FALSE) { mce_log(LL_ERR, "Cannot open `%s' for reading; %s", file, error->message); goto EXIT; } status = TRUE; EXIT: /* Reset errno, * to avoid false positives down the line */ errno = 0; g_clear_error(&error); return status; } /** * Read a number representation of a string from a file * * @param file Path to the file, or NULL to user an already open FILE * instead * @param[out] number A number representation of the first line of the file * @param fp A pointer to a FILE *; set the FILE * to NULL to use the file * path instead * @param rewind_file TRUE to seek to the beginning of the file, * FALSE to read from the current position; * only affects already open files * @param close_on_exit TRUE to close the file on exit, * FALSE to leave the file open * @return TRUE on success, FALSE on failure */ gboolean mce_read_number_string_from_file(const gchar *const file, gulong *number, FILE **fp, gboolean rewind_file, gboolean close_on_exit) { gboolean status = FALSE; gint again_count = 0; FILE *new_fp = NULL; gint retval; if ((file == NULL) && ((fp == NULL) || (*fp == NULL))) { mce_log(LL_CRIT, "(file == NULL) && ((fp == NULL) || (*fp == NULL))!"); goto EXIT; } if ((fp == NULL) && (close_on_exit == FALSE)) { mce_log(LL_CRIT, "(fp == NULL) && (close_on_exit == FALSE)!"); goto EXIT; } /* If we cannot open the file, abort */ if ((fp == NULL) || (*fp == NULL)) { if ((new_fp = fopen(file, "r")) == NULL) { mce_log(LL_ERR, "Cannot open `%s' for reading; %s", file, g_strerror(errno)); /* Ignore error */ errno = 0; goto EXIT; } } else { new_fp = *fp; } /* Rewind file if we already have one */ if ((fp != NULL) && (*fp != NULL) && (rewind_file == TRUE)) { if (fseek(*fp, 0L, SEEK_SET) == -1) { mce_log(LL_ERR, "Failed to rewind `%s'; %s", file, g_strerror(errno)); /* Ignore error */ errno = 0; goto EXIT2; } } if ((fp != NULL) && (*fp == NULL)) *fp = new_fp; while (again_count++ < 10) { /* Clear errors from earlier iterations */ clearerr(new_fp); errno = 0; retval = fscanf(new_fp, "%lu", number); if ((retval == EOF) && (ferror(new_fp) != 0) && (errno == EAGAIN)) { continue; } else { break; } } /* Was the read successful? */ if ((retval == EOF) && (ferror(new_fp) != 0)) { mce_log(LL_ERR, "Failed to read from `%s'; %s", file, g_strerror(errno)); clearerr(new_fp); /* Ignore error */ errno = 0; goto EXIT2; } if (retval != 1) { mce_log(LL_ERR, "Could not match any values when reading from `%s'", file); goto EXIT2; } status = TRUE; EXIT2: /* XXX: improve close policy? */ if ((status == FALSE) || (close_on_exit == TRUE)) { mce_close_file(file, &new_fp); if (fp != NULL) *fp = NULL; } /* Ignore error */ errno = 0; EXIT: return status; } /** * Write a string to a file * * @param file Path to the file * @param string The string to write * @return TRUE on success, FALSE on failure */ gboolean mce_write_string_to_file(const gchar *const file, const gchar *const string) { gboolean status = FALSE; FILE *fp = NULL; gint retval; if (file == NULL) { mce_log(LL_CRIT, "file == NULL!"); goto EXIT; } if (string == NULL) { mce_log(LL_CRIT, "string == NULL!"); goto EXIT; } if ((fp = fopen(file, "w")) == NULL) { mce_log(LL_ERR, "Cannot open `%s' for %s; %s", file, "writing", g_strerror(errno)); /* Ignore error */ errno = 0; goto EXIT; } retval = fprintf(fp, "%s", string); /* Was the write successful? */ if (retval < 0) { mce_log(LL_ERR, "Failed to write to `%s'; %s", file, g_strerror(errno)); /* Ignore error */ errno = 0; goto EXIT2; } status = TRUE; EXIT2: mce_close_file(file, &fp); EXIT: return status; } /** * Cleanup function for output file control structures * * Closes file stream associated with output if it is open * * It is explicitly permitted to call this function: * 1) with NULL output parameter * 2) without open stream in ouput * 3) more than one times * * @param output control structure for writing to a file */ void mce_close_output(output_state_t *output) { if( output && output->file ) { if( fclose(output->file) == EOF ) { mce_log(LL_WARN,"%s: can't close %s: %m", output->context, output->path); } output->file = 0; } } /** * Write a string representation of a number to a file * * Note: this variant uses in-place rewrites when truncating. * It should thus not be used in cases where atomicity is expected. * For atomic replace, use mce_write_number_string_to_file_atomic() * * @param output control structure for writing to a file * @param number The number to write * * @return TRUE on success, FALSE on failure */ gboolean mce_write_number_string_to_file(output_state_t *output, const gulong number) { gboolean status = FALSE; // assume failure if( !output ) { mce_log(LL_CRIT, "NULL output passed, terminating"); mce_abort(); } if( !output->context ) { mce_log(LL_CRIT, "output->context missing, terminating"); mce_abort(); } if( !output->path ) { if( !output->invalid_config_reported ) { output->invalid_config_reported = TRUE; mce_log(LL_ERR, "%s: output->path not configured", output->context); } goto EXIT; } if( !output->file ) { output->file = fopen(output->path, output->truncate_file ? "w" : "a"); if( !output->file ) { mce_log(LL_ERR,"%s: can't open %s: %m", output->context, output->path); goto EXIT; } } else if( output->truncate_file ) { rewind(output->file); if( ftruncate(fileno(output->file), 0) == -1 ) { mce_log(LL_WARN,"%s: can't truncate %s: %m", output->context, output->path); } } // from now on assume success status = TRUE; if( fprintf(output->file, "%lu", number) < 0 ) { mce_log(LL_WARN,"%s: can't write %s: %m", output->context, output->path); status = FALSE; } if( fflush(output->file) == EOF ) { mce_log(output->reported_errno != errno ? LL_WARN : LL_DEBUG, "%s: can't flush %s: %m", output->context, output->path); output->reported_errno = errno; status = FALSE; } else { output->reported_errno = 0; } EXIT: if( output->close_on_exit && output->file ) { if( fclose(output->file) == EOF ) { mce_log(LL_WARN,"%s: can't close %s: %m", output->context, output->path); } output->file = 0; } return status; } /** * Write a string representation of a number to a file * in an atomic manner * * @param file Path to the file to write to * @param number The number to write * @return TRUE on success, FALSE on failure */ gboolean mce_write_number_string_to_file_atomic(const gchar *const file, const gulong number) { gboolean status = FALSE; gchar *tmpname = NULL; FILE *fp = NULL; gint retval; int fd; if (file == NULL) { mce_log(LL_CRIT, "file == NULL"); goto EXIT; } if ((tmpname = g_strconcat(file, TMP_SUFFIX, NULL)) == NULL) { mce_log(LL_ERR, "Failed to allocate memory for `%s%s'", file, TMP_SUFFIX); goto EXIT; } /* If we cannot open the file, abort */ if ((fp = fopen(tmpname, "w")) == NULL) { mce_log(LL_ERR, "Cannot open `%s' for writing; %s", tmpname, g_strerror(errno)); /* Ignore error */ errno = 0; goto EXIT; } retval = fprintf(fp, "%lu", number); /* Was the write successful? */ if (retval < 0) { mce_log(LL_ERR, "Failed to write to `%s'; %s", tmpname, g_strerror(errno)); /* Ignore error */ errno = 0; goto EXIT2; } if ((fd = fileno(fp)) == -1) { mce_log(LL_ERR, "Failed to convert *fp to fd; %s", g_strerror(errno)); /* Ignore error */ errno = 0; goto EXIT2; } /* Ensure that the data makes it to disk */ if (fsync(fd) == -1) { mce_log(LL_ERR, "Failed to fsync `%s'; %s", tmpname, g_strerror(errno)); /* Ignore error */ errno = 0; goto EXIT2; } status = TRUE; EXIT2: /* Close the temporary file */ if (mce_close_file(tmpname, &fp) == FALSE) { status = FALSE; goto EXIT; } /* And if everything has been successful so far, * rename the temporary file over the old file */ if (status == TRUE) { if (rename(tmpname, file) == -1) { mce_log(LL_ERR, "Failed to rename `%s' to `%s'; %s", tmpname, file, g_strerror(errno)); status = FALSE; /* Ignore error */ errno = 0; goto EXIT; } } EXIT: g_free(tmpname); return status; } /** * Test whether there's a settings lock due to pending * backup/restore or device clear/factory reset operation * * @return TRUE if the settings lock file is in place, * FALSE if the settings lock file is not in place */ gboolean mce_are_settings_locked(void) { gboolean status = (g_access(MCE_SETTINGS_LOCK_FILE_PATH, F_OK) == 0); errno = 0; return status; } /** * Remove the settings lock file * * @return TRUE on success, FALSE on failure */ gboolean mce_unlock_settings(void) { gboolean status = (g_unlink(MCE_SETTINGS_LOCK_FILE_PATH) == 0); errno = 0; return status; } /** Helper for dealing with partially successful reads * * @param fd file descriptor to read from * @param buff address where to read to * @param size number of bytes to read * @param pdone where to store actual read count, or NULL * * @return TRUE if all bytes could be read, otherwise FALSE */ static gboolean mce_io_read_all(int fd, void *buff, size_t size, size_t *pdone) { size_t done = 0; char *data = buff; while( done < size ) { ssize_t rc = TEMP_FAILURE_RETRY(read(fd, data+done, size)); if( rc < 0 ) break; if( rc == 0 ) { // clear errno if returning prematurely due to eof errno = 0; break; } done += (size_t)rc; } if( pdone ) *pdone = done; return (done == size); } /** Helper for dealing with partially successful writes * * @param fd file descriptor to write to * @param buff address where to write from * @param size number of bytes to write * @param pdone where to store actual write count, or NULL * * @return TRUE if all bytes could be written, otherwise FALSE */ static gboolean mce_io_write_all(int fd, const void *buff, size_t size, size_t *pdone) { size_t done = 0; const char *data = buff; while( done < size ) { ssize_t rc = TEMP_FAILURE_RETRY(write(fd, data+done, size)); if( rc < 0 ) break; done += (size_t)rc; } if( pdone ) *pdone = done; return (done == size); } /** Load contents of a file * * @param path file to read from * @param psize where to store size of the loaded file, or NULL if not needed * * @return Zero terminated contents of the file, or NULL in case of errors */ void *mce_io_load_file(const char *path, size_t *psize) { void *res = 0; char *data = 0; size_t size = 0; int fd = -1; struct stat st; if( (fd = TEMP_FAILURE_RETRY(open(path, O_RDONLY))) == -1 ) { if( errno != ENOENT ) mce_log(LL_WARN, "open(%s): %m", path); goto EXIT; } if( fstat(fd, &st) == -1 ) { mce_log(LL_WARN, "stat(%s): %m", path); goto EXIT; } size = st.st_size; data = g_malloc(size + 1); if( !mce_io_read_all(fd, data, size, 0) ) { mce_log(LL_WARN, "read(%s): %m", path); goto EXIT; } data[size] = 0; res = data, data = 0; EXIT: g_free(data); if( fd != -1 && TEMP_FAILURE_RETRY(close(fd)) == -1 ) mce_log(LL_WARN, "close(%s): %m", path); if( psize ) *psize = res ? size : 0; return res; } /** Load contents of a file, ignoring the reported file size * * @param path file to read from * @param psize where to store size of the loaded file, or NULL if not needed * * @return Zero terminated contents of the file, or NULL in case of errors */ void *mce_io_load_file_until_eof(const char *path, size_t *psize) { void *res = 0; char *data = 0; size_t used = 0; size_t size = 0; int fd = -1; ssize_t rc; if( (fd = TEMP_FAILURE_RETRY(open(path, O_RDONLY))) == -1 ) { if( errno != ENOENT ) mce_log(LL_WARN, "open(%s): %m", path); goto EXIT; } size = 1024; data = g_malloc(size); for( ;; ) { rc = TEMP_FAILURE_RETRY(read(fd, data + used, size - used));; if( rc == 0 ) break; if( rc == -1 ) { mce_log(LL_WARN, "read(%s): %m", path); goto EXIT; } used += (size_t)rc; if( size - used < 512 ) { size = size * 2; data = g_realloc(data, size); } } data = g_realloc(data, used + 1); data[used] = 0; res = data, data = 0; EXIT: g_free(data); if( fd != -1 && TEMP_FAILURE_RETRY(close(fd)) == -1 ) mce_log(LL_WARN, "close(%s): %m", path); if( psize ) *psize = res ? used : 0; return res; } /** Write datablock to a file * * @param path file to write to * @param data start of the data to write * @param size length of the data to write * @param mode protection bits to apply * * @return TRUE on success, FALSE on errors */ gboolean mce_io_save_file(const char *path, const void *data, size_t size, mode_t mode) { gboolean res = FALSE; int fd = -1; if( mode <= 0 ) mode = 0664; fd = TEMP_FAILURE_RETRY(open(path, O_WRONLY|O_CREAT|O_TRUNC, 0600)); if( fd == -1 ) { mce_log(LL_WARN, "open(%s): %m", path); goto EXIT; } if( !mce_io_write_all(fd, data, size, 0) ) { mce_log(LL_WARN, "write(%s): %m", path); goto EXIT; } if( fchmod(fd, mode) == -1 ) { mce_log(LL_WARN, "chmod(%s, %03o): %m", path, (int)mode); goto EXIT; } res = TRUE; EXIT: if( fd != -1 && TEMP_FAILURE_RETRY(close(fd)) == -1 ) mce_log(LL_WARN, "close(%s): %m", path); return res; } /** Write datablock to an existing file * * @param path file to write to * @param data start of the data to write * @param size length of the data to write * * @return TRUE on success, FALSE on errors */ gboolean mce_io_save_to_existing_file(const char *path, const void *data, size_t size) { gboolean res = FALSE; int fd = -1; fd = TEMP_FAILURE_RETRY(open(path, O_WRONLY|O_TRUNC)); if( fd == -1 ) { mce_log(LL_WARN, "open(%s): %m", path); goto EXIT; } if( !mce_io_write_all(fd, data, size, 0) ) { mce_log(LL_WARN, "write(%s): %m", path); goto EXIT; } res = TRUE; EXIT: if( fd != -1 && TEMP_FAILURE_RETRY(close(fd)) == -1 ) mce_log(LL_WARN, "close(%s): %m", path); return res; } /** Atomically replace a file contents * * First writes to a temp file, then duplicates the original as * a backup and atomically replaces the original with freshly * written data. * * @param path file to write to * @param data start of the data to write * @param size length of the data to write * @param mode protection bits to apply * @param keep_backup whether to keep backup file on successful update * * @return TRUE on success, FALSE on errors */ gboolean mce_io_save_file_atomic(const char *path, const void *data, size_t size, mode_t mode, gboolean keep_backup) { gboolean res = FALSE; gchar *temp = g_strdup_printf("%s.tmp", path); gchar *back = g_strdup_printf("%s.bak", path); if( !mce_io_save_file(temp, data, size, mode) ) goto EXIT; if( unlink(back) == -1 && errno != ENOENT ) { mce_log(LL_WARN, "unlink(%s): %m", back); goto EXIT; } if( link(path, back) == -1 && errno != ENOENT ) { mce_log(LL_WARN, "link(%s, %s): %m", path, back); goto EXIT; } if( rename(temp, path) == -1 ) { mce_log(LL_WARN, "rename(%s, %s): %m", temp, path); goto EXIT; } if( !keep_backup && unlink(back) == -1 && errno != ENOENT ) { mce_log(LL_WARN, "unlink(%s): %m", back); goto EXIT; } res = TRUE; EXIT: if( temp && unlink(temp) == -1 && errno != ENOENT ) mce_log(LL_WARN, "unlink(%s): %m", temp); g_free(back); g_free(temp); return res; } /** Atomically replace a file if existing file content differs from wanted * * The purpose of this function is to avoid unnecessary writes when * writing to filesystem is slow or otherwise undesired (flash wear out). * * @param path file to write to * @param data start of the data to write * @param size length of the data to write * @param mode protection bits to apply * @param keep_backup whether to keep backup file on successful update * * @return TRUE on success, FALSE on errors */ gboolean mce_io_update_file_atomic(const char *path, const void *data, size_t size, mode_t mode, gboolean keep_backup) { gboolean res = FALSE; size_t old_size = 0; void *old_data = 0; /* Skip write if the content would not change */ if( (old_data = mce_io_load_file(path, &old_size)) ) { if( old_size == size && !memcmp(old_data, data, size) ) { res = TRUE; goto EXIT; } } res = mce_io_save_file_atomic(path, data, size, mode, keep_backup); EXIT: g_free(old_data); return res; }