2005-09-20 13:26:39 +00:00
/** \file history.c
2007-01-20 02:33:47 +00:00
History functions , part of the user interface .
2005-09-20 13:26:39 +00:00
*/
2006-08-11 01:18:35 +00:00
# include "config.h"
2005-09-20 13:26:39 +00:00
# include <stdlib.h>
# include <stdio.h>
# include <wchar.h>
# include <errno.h>
# include <dirent.h>
# include <sys/types.h>
# include <sys/stat.h>
# include <unistd.h>
2006-10-20 22:36:49 +00:00
# include <sys/mman.h>
# include <fcntl.h>
# include <string.h>
# include <time.h>
# include <assert.h>
2005-09-20 13:26:39 +00:00
2006-02-28 13:17:16 +00:00
# include "fallback.h"
2005-09-20 13:26:39 +00:00
# include "util.h"
2012-02-06 18:52:13 +00:00
# include "sanity.h"
2006-02-28 13:17:16 +00:00
2005-09-20 13:26:39 +00:00
# include "wutil.h"
# include "history.h"
# include "common.h"
2006-09-08 12:34:55 +00:00
# include "halloc.h"
# include "halloc_util.h"
2006-10-20 22:36:49 +00:00
# include "intern.h"
2006-10-19 11:50:23 +00:00
# include "path.h"
2006-10-20 22:36:49 +00:00
# include "signal.h"
2012-02-06 04:54:41 +00:00
# include "autoload.h"
2012-02-06 00:42:24 +00:00
# include <map>
2012-02-06 04:54:41 +00:00
# include <algorithm>
/** When we rewrite the history, the number of items we keep */
# define HISTORY_SAVE_MAX (1024 * 256)
/** Interval in seconds between automatic history save */
# define SAVE_INTERVAL (5*60)
/** Number of new history entries to add before automatic history save */
# define SAVE_COUNT 5
/* Our LRU cache is used for restricting the amount of history we have, and limiting how long we order it. */
class history_lru_node_t : public lru_node_t {
public :
time_t timestamp ;
2012-02-06 06:30:42 +00:00
history_lru_node_t ( const history_item_t & item ) : lru_node_t ( item . str ( ) ) , timestamp ( item . timestamp ( ) ) { }
bool write_to_file ( FILE * f ) const ;
2012-02-06 04:54:41 +00:00
} ;
class history_lru_cache_t : public lru_cache_t < history_lru_node_t > {
protected :
/* Override to delete evicted nodes */
virtual void node_was_evicted ( history_lru_node_t * node ) {
delete node ;
}
public :
history_lru_cache_t ( size_t max ) : lru_cache_t < history_lru_node_t > ( max ) { }
/* Function to add a history item */
void add_item ( const history_item_t & item ) {
/* Skip empty items */
if ( item . empty ( ) )
return ;
/* See if it's in the cache. If it is, update the timestamp. If not, we create a new node and add it. Note that calling get_node promotes the node to the front. */
history_lru_node_t * node = this - > get_node ( item . str ( ) ) ;
if ( node ! = NULL ) {
node - > timestamp = std : : max ( node - > timestamp , item . timestamp ( ) ) ;
} else {
node = new history_lru_node_t ( item ) ;
this - > add_node ( node ) ;
}
}
} ;
2005-09-20 13:26:39 +00:00
2012-02-06 00:42:24 +00:00
static pthread_mutex_t hist_lock = PTHREAD_MUTEX_INITIALIZER ;
static std : : map < wcstring , history_t * > histories ;
static wcstring history_filename ( const wcstring & name , const wcstring & suffix ) ;
/* Unescapes newlines in-place */
static void unescape_newlines ( wcstring & str ) ;
2012-02-06 04:54:41 +00:00
/* Escapes newlines in-place */
static void escape_newlines ( wcstring & str ) ;
2012-02-06 00:42:24 +00:00
/* Custom deleter for our shared_ptr */
class history_item_data_deleter_t {
private :
const bool free_it ;
public :
history_item_data_deleter_t ( bool flag ) : free_it ( flag ) { }
void operator ( ) ( const wchar_t * data ) {
if ( free_it )
free ( ( void * ) data ) ;
}
} ;
2012-02-06 04:54:41 +00:00
history_item_t : : history_item_t ( const wcstring & str ) : contents ( str ) , creation_timestamp ( time ( NULL ) )
2012-02-06 00:42:24 +00:00
{
}
2012-02-06 04:54:41 +00:00
history_item_t : : history_item_t ( const wcstring & str , time_t when ) : contents ( str ) , creation_timestamp ( when )
2012-02-06 00:42:24 +00:00
{
}
2012-02-06 18:52:13 +00:00
bool history_item_t : : matches_search ( const wcstring & term , enum history_search_type_t type ) const {
switch ( type ) {
case HISTORY_SEARCH_TYPE_CONTAINS :
/* We consider equal strings to NOT match a contains search (so that you don't have to see history equal to what you typed). The length check ensures that. */
return contents . size ( ) > term . size ( ) & & contents . find ( term ) ! = wcstring : : npos ;
case HISTORY_SEARCH_TYPE_PREFIX :
/* We consider equal strings to match a prefix search, so that autosuggest will allow suggesting what you've typed */
return string_prefixes_string ( term , contents ) ;
default :
sanity_lose ( ) ;
return false ;
}
}
2012-02-06 06:30:42 +00:00
bool history_lru_node_t : : write_to_file ( FILE * f ) const {
wcstring escaped = key ;
2012-02-06 04:54:41 +00:00
escape_newlines ( escaped ) ;
2012-02-06 06:30:42 +00:00
return fwprintf ( f , L " # %d \n %ls \n " , timestamp , escaped . c_str ( ) ) ;
2012-02-06 04:54:41 +00:00
}
2012-02-06 00:42:24 +00:00
history_t & history_t : : history_with_name ( const wcstring & name ) {
/* Note that histories are currently never deleted, so we can return a reference to them without using something like shared_ptr */
scoped_lock locker ( hist_lock ) ;
history_t * & current = histories [ name ] ;
if ( current = = NULL )
current = new history_t ( name ) ;
return * current ;
}
history_t : : history_t ( const wcstring & pname ) :
name ( pname ) ,
mmap_start ( NULL ) ,
mmap_length ( 0 ) ,
save_timestamp ( 0 ) ,
loaded_old ( false )
{
pthread_mutex_init ( & lock , NULL ) ;
}
history_t : : ~ history_t ( )
{
pthread_mutex_destroy ( & lock ) ;
}
void history_t : : add ( const wcstring & str )
{
scoped_lock locker ( lock ) ;
2012-02-06 06:30:42 +00:00
2012-02-06 00:42:24 +00:00
new_items . push_back ( history_item_t ( str . c_str ( ) , true ) ) ;
2012-02-06 04:54:41 +00:00
/* This might be a good candidate for moving to a background thread */
if ( ( time ( 0 ) > save_timestamp + SAVE_INTERVAL ) | | ( new_items . size ( ) > = SAVE_COUNT ) )
this - > save_internal ( ) ;
2012-02-06 00:42:24 +00:00
}
history_item_t history_t : : item_at_index ( size_t idx ) {
scoped_lock locker ( lock ) ;
/* 0 is considered an invalid index */
assert ( idx > 0 ) ;
idx - - ;
/* idx=0 corresponds to last item in new_items */
size_t new_item_count = new_items . size ( ) ;
if ( idx < new_item_count ) {
return new_items . at ( new_item_count - idx - 1 ) ;
}
/* Now look in our old items */
idx - = new_item_count ;
load_old_if_needed ( ) ;
size_t old_item_count = old_item_offsets . size ( ) ;
if ( idx < old_item_count ) {
/* idx=0 corresponds to last item in old_item_offsets */
size_t offset = old_item_offsets . at ( old_item_count - idx - 1 ) ;
return history_t : : decode_item ( mmap_start + offset , mmap_length - offset ) ;
}
/* Index past the valid range, so return an empty history item */
return history_item_t ( wcstring ( ) , 0 ) ;
}
history_item_t history_t : : decode_item ( const char * begin , size_t len )
{
const char * pos = begin , * end = begin + len ;
int was_backslash = 0 ;
wcstring output ;
time_t timestamp = 0 ;
int first_char = 1 ;
bool timestamp_mode = false ;
while ( 1 )
{
wchar_t c ;
mbstate_t state ;
bzero ( & state , sizeof state ) ;
size_t res ;
res = mbrtowc ( & c , pos , end - pos , & state ) ;
if ( res = = ( size_t ) - 1 )
{
pos + + ;
continue ;
}
else if ( res = = ( size_t ) - 2 )
{
break ;
}
else if ( res = = ( size_t ) 0 )
{
pos + + ;
continue ;
}
pos + = res ;
if ( c = = L ' \n ' )
{
if ( timestamp_mode )
{
const wchar_t * time_string = output . c_str ( ) ;
while ( * time_string & & ! iswdigit ( * time_string ) )
time_string + + ;
errno = 0 ;
if ( * time_string )
{
time_t tm ;
wchar_t * end ;
errno = 0 ;
tm = ( time_t ) wcstol ( time_string , & end , 10 ) ;
if ( tm & & ! errno & & ! * end )
{
timestamp = tm ;
}
}
output . clear ( ) ;
timestamp_mode = 0 ;
continue ;
}
if ( ! was_backslash )
break ;
}
if ( first_char )
{
if ( c = = L ' # ' )
timestamp_mode = 1 ;
}
first_char = 0 ;
output . push_back ( c ) ;
was_backslash = ( ( c = = L ' \\ ' ) & & ! was_backslash ) ;
}
unescape_newlines ( output ) ;
return history_item_t ( output , timestamp ) ;
}
void history_t : : populate_from_mmap ( void )
{
const char * begin = mmap_start ;
const char * end = begin + mmap_length ;
const char * pos ;
int ignore_newline = 0 ;
int do_push = 1 ;
for ( pos = begin ; pos < end ; pos + + )
{
if ( do_push )
{
ignore_newline = * pos = = ' # ' ;
/* Need to unique-ize */
old_item_offsets . push_back ( pos - begin ) ;
do_push = 0 ;
}
switch ( * pos )
{
case ' \\ ' :
{
pos + + ;
break ;
}
case ' \n ' :
{
if ( ignore_newline )
{
ignore_newline = 0 ;
}
else
{
do_push = 1 ;
}
break ;
}
}
}
}
void history_t : : load_old_if_needed ( void )
{
if ( loaded_old ) return ;
loaded_old = true ;
int fd ;
int ok = 0 ;
signal_block ( ) ;
wcstring filename = history_filename ( name , L " " ) ;
if ( ! filename . empty ( ) )
{
if ( ( fd = wopen ( filename . c_str ( ) , O_RDONLY ) ) > 0 )
{
off_t len = lseek ( fd , 0 , SEEK_END ) ;
if ( len ! = ( off_t ) - 1 )
{
mmap_length = ( size_t ) len ;
if ( lseek ( fd , 0 , SEEK_SET ) = = 0 )
{
if ( ( mmap_start = ( char * ) mmap ( 0 , mmap_length , PROT_READ , MAP_PRIVATE , fd , 0 ) ) ! = MAP_FAILED )
{
ok = 1 ;
this - > populate_from_mmap ( ) ;
}
}
}
close ( fd ) ;
}
}
signal_unblock ( ) ;
}
2012-02-06 19:52:24 +00:00
void history_search_t : : skip_matches ( const wcstring_list_t & skips ) {
external_skips = skips ;
std : : sort ( external_skips . begin ( ) , external_skips . end ( ) ) ;
}
bool history_search_t : : should_skip_match ( const wcstring & str ) const {
return std : : binary_search ( external_skips . begin ( ) , external_skips . end ( ) , str ) ;
}
2012-02-06 00:42:24 +00:00
bool history_search_t : : go_forwards ( ) {
2012-02-06 06:48:43 +00:00
/* Pop the top index (if more than one) and return if we have any left */
if ( prev_matches . size ( ) > 1 ) {
2012-02-06 06:30:42 +00:00
prev_matches . pop_back ( ) ;
2012-02-06 06:48:43 +00:00
return true ;
}
return false ;
2012-02-06 00:42:24 +00:00
}
bool history_search_t : : go_backwards ( ) {
/* Backwards means increasing our index */
const size_t max_idx = ( size_t ) ( - 1 ) ;
2012-02-06 06:30:42 +00:00
size_t idx = 0 ;
if ( ! prev_matches . empty ( ) )
idx = prev_matches . back ( ) . first ;
2012-02-06 00:42:24 +00:00
if ( idx = = max_idx )
return false ;
2012-02-06 06:30:42 +00:00
while ( + + idx < max_idx ) {
const history_item_t item = history - > item_at_index ( idx ) ;
2012-02-06 00:42:24 +00:00
/* We're done if it's empty */
if ( item . empty ( ) ) {
return false ;
}
2012-02-06 06:30:42 +00:00
/* Look for a term that matches and that we haven't seen before */
2012-02-06 19:52:24 +00:00
const wcstring & str = item . str ( ) ;
if ( item . matches_search ( term , search_type ) & & ! match_already_made ( str ) & & ! should_skip_match ( str ) ) {
2012-02-06 06:30:42 +00:00
prev_matches . push_back ( prev_match_t ( idx , item . str ( ) ) ) ;
2012-02-06 00:42:24 +00:00
return true ;
}
}
return false ;
}
/** Goes to the end (forwards) */
void history_search_t : : go_to_end ( void ) {
2012-02-06 06:30:42 +00:00
prev_matches . clear ( ) ;
2012-02-06 00:42:24 +00:00
}
2012-02-06 07:22:18 +00:00
/** Returns if we are at the end, which is where we start. */
bool history_search_t : : is_at_end ( void ) const {
return prev_matches . empty ( ) ;
}
2012-02-06 00:42:24 +00:00
/** Goes to the beginning (backwards) */
void history_search_t : : go_to_beginning ( void ) {
2012-02-06 06:30:42 +00:00
/* Just go backwards as far as we can */
while ( go_backwards ( ) )
;
2012-02-06 00:42:24 +00:00
}
2005-09-20 13:26:39 +00:00
2012-02-06 06:30:42 +00:00
wcstring history_search_t : : current_item ( ) const {
assert ( ! prev_matches . empty ( ) ) ;
return prev_matches . back ( ) . second ;
2006-10-22 01:21:02 +00:00
}
2012-02-06 06:30:42 +00:00
bool history_search_t : : match_already_made ( const wcstring & match ) const {
for ( std : : deque < prev_match_t > : : const_iterator iter = prev_matches . begin ( ) ; iter ! = prev_matches . end ( ) ; iter + + ) {
if ( iter - > second = = match )
return true ;
}
return false ;
2006-10-22 01:21:02 +00:00
}
2006-10-08 13:50:46 +00:00
2012-02-06 04:54:41 +00:00
static void replace_all ( wcstring & str , const wchar_t * needle , const wchar_t * replacement )
2012-02-06 00:42:24 +00:00
{
size_t needle_len = wcslen ( needle ) ;
size_t offset = 0 ;
while ( ( offset = str . find ( needle , offset ) ) ! = wcstring : : npos )
{
str . replace ( offset , needle_len , replacement ) ;
offset + = needle_len ;
}
}
2012-02-06 04:54:41 +00:00
static void unescape_newlines ( wcstring & str )
{
/* Replace instances of backslash + newline with just the newline */
replace_all ( str , L " \\ \n " , L " \n " ) ;
}
static void escape_newlines ( wcstring & str )
{
/* Replace instances of newline with backslash + newline with newline */
replace_all ( str , L " \\ \n " , L " \n " ) ;
/* If the string ends with a backslash, we'll combine it with the next line. Hack around that by appending a newline. */
if ( ! str . empty ( ) & & str . at ( str . size ( ) - 1 ) = = L ' \\ ' )
str . push_back ( ' \n ' ) ;
}
2012-02-06 00:42:24 +00:00
static wcstring history_filename ( const wcstring & name , const wcstring & suffix )
{
wcstring path ;
if ( ! path_get_config ( path ) )
return L " " ;
wcstring result = path ;
result . append ( L " / " ) ;
result . append ( name ) ;
result . append ( L " _history " ) ;
result . append ( suffix ) ;
return result ;
}
2012-02-06 04:54:41 +00:00
/** Save the specified mode to file */
void history_t : : save_internal ( )
2012-02-06 00:42:24 +00:00
{
2012-02-06 04:54:41 +00:00
/* This must be called while locked */
2012-02-06 00:42:24 +00:00
/* Nothing to do if there's no new items */
if ( new_items . empty ( ) )
return ;
2012-02-06 04:54:41 +00:00
bool ok = true ;
2012-02-06 00:42:24 +00:00
signal_block ( ) ;
wcstring tmp_name = history_filename ( name , L " .tmp " ) ;
if ( ! tmp_name . empty ( ) )
{
2012-02-06 04:54:41 +00:00
FILE * out ;
if ( ( out = wfopen ( tmp_name . c_str ( ) , " w " ) ) )
2012-02-06 00:42:24 +00:00
{
2012-02-06 04:54:41 +00:00
/* Load old */
load_old_if_needed ( ) ;
/* Make an LRU cache to save only the last N elements */
history_lru_cache_t lru ( HISTORY_SAVE_MAX ) ;
/* Insert old items in, from old to new */
for ( std : : vector < size_t > : : iterator iter = old_item_offsets . begin ( ) ; iter ! = old_item_offsets . end ( ) ; iter + + ) {
size_t offset = * iter ;
history_item_t item = history_t : : decode_item ( mmap_start + offset , mmap_length - offset ) ;
lru . add_item ( item ) ;
}
/* Insert new items */
for ( std : : vector < history_item_t > : : iterator iter = new_items . begin ( ) ; iter ! = new_items . end ( ) ; iter + + ) {
lru . add_item ( * iter ) ;
}
/* Write them out */
for ( history_lru_cache_t : : iterator iter = lru . begin ( ) ; iter ! = lru . end ( ) ; iter + + ) {
if ( ! ( * iter ) - > write_to_file ( out ) ) {
ok = false ;
break ;
}
}
2012-02-06 00:42:24 +00:00
if ( fclose ( out ) | | ! ok )
{
/*
This message does not have high enough priority to
be shown by default .
*/
debug ( 2 , L " Error when writing history file " ) ;
}
else
{
2012-02-06 04:54:41 +00:00
wcstring new_name = history_filename ( name , wcstring ( ) ) ;
wrename ( tmp_name . c_str ( ) , new_name . c_str ( ) ) ;
2012-02-06 00:42:24 +00:00
}
}
}
if ( ok )
{
2012-02-06 04:54:41 +00:00
/* Our history has been written to the file, so clear our state so we can re-reference the file. */
if ( mmap_start ! = NULL & & mmap_start ! = MAP_FAILED ) {
munmap ( ( void * ) mmap_start , mmap_length ) ;
}
mmap_start = 0 ;
mmap_length = 0 ;
loaded_old = false ;
new_items . clear ( ) ;
old_item_offsets . clear ( ) ;
save_timestamp = time ( 0 ) ;
2012-02-06 00:42:24 +00:00
}
signal_unblock ( ) ;
}
2012-02-06 04:54:41 +00:00
void history_t : : save ( void ) {
scoped_lock locker ( lock ) ;
this - > save_internal ( ) ;
}
2012-02-06 00:42:24 +00:00
2006-10-20 22:36:49 +00:00
void history_init ( )
2006-10-21 10:30:35 +00:00
{
2006-10-20 22:36:49 +00:00
}
2005-09-20 13:26:39 +00:00
2006-10-20 22:36:49 +00:00
void history_destroy ( )
{
2012-02-06 04:54:41 +00:00
/* Save all histories */
for ( std : : map < wcstring , history_t * > : : iterator iter = histories . begin ( ) ; iter ! = histories . end ( ) ; iter + + ) {
iter - > second - > save ( ) ;
}
2006-10-20 22:36:49 +00:00
}
2005-09-20 13:26:39 +00:00
2006-10-20 22:36:49 +00:00
void history_sanity_check ( )
{
2006-10-22 01:21:02 +00:00
/*
No sanity checking implemented yet . . .
*/
2005-09-20 13:26:39 +00:00
}