libbb: make history saving/loading concurrent-safe

* all history writers always append (not overwrite) history files
* they reload history if they detect that file length has changed since last
write
* they trim history file only when it grows 4 times longer than MAXLINES
* they do this atomically by creating new file and renaming it to old

Unfortunately, this comes at a price:

function                                             old     new   delta
load_history                                           -     346    +346
read_line_input                                     3155    3358    +203
new_line_input_t                                      17      31     +14
...irrelevant small jitter...
------------------------------------------------------------------------------
(add/remove: 1/0 grow/shrink: 5/5 up/down: 573/-13)           Total: 560 bytes
This commit is contained in:
Denis Vlasenko 2009-03-22 19:00:05 +00:00
parent 3fd1046300
commit 57abf9e947
2 changed files with 117 additions and 35 deletions

View File

@ -1197,7 +1197,11 @@ typedef struct line_input_t {
#if MAX_HISTORY #if MAX_HISTORY
int cnt_history; int cnt_history;
int cur_history; int cur_history;
USE_FEATURE_EDITING_SAVEHISTORY(const char *hist_file;) #if ENABLE_FEATURE_EDITING_SAVEHISTORY
unsigned cnt_history_in_file;
off_t last_history_end;
const char *hist_file;
#endif
char *history[MAX_HISTORY + 1]; char *history[MAX_HISTORY + 1];
#endif #endif
} line_input_t; } line_input_t;

View File

@ -990,64 +990,141 @@ static int get_next_history(void)
} }
#if ENABLE_FEATURE_EDITING_SAVEHISTORY #if ENABLE_FEATURE_EDITING_SAVEHISTORY
/* We try to ensure that concurrent additions to the history
* do not overwrite each other, and that additions to the history
* by one user are noticed by others.
* Otherwise shell users get unhappy.
*
* History file is trimmed lazily, when it grows several times longer
* than configured MAX_HISTORY lines.
*/
/* state->flags is already checked to be nonzero */ /* state->flags is already checked to be nonzero */
static void load_history(const char *fromfile) static void load_history(void)
{ {
char *temp_h[MAX_HISTORY];
char *line;
FILE *fp; FILE *fp;
int hi; unsigned idx, i, line_len;
/* NB: do not trash old history if file can't be opened */ /* NB: do not trash old history if file can't be opened */
fp = fopen_for_read(fromfile); fp = fopen_for_read(state->hist_file);
if (fp) { if (fp) {
/* clean up old history */ /* clean up old history */
for (hi = state->cnt_history; hi > 0;) { for (idx = state->cnt_history; idx > 0;) {
hi--; idx--;
free(state->history[hi]); free(state->history[idx]);
state->history[hi] = NULL; state->history[idx] = NULL;
} }
for (hi = 0; hi < MAX_HISTORY;) { /* fill temp_h[], retaining only last MAX_HISTORY lines */
char *hl = xmalloc_fgetline(fp); memset(temp_h, 0, sizeof(temp_h));
int l; state->cnt_history_in_file = idx = 0;
while ((line = xmalloc_fgetline(fp)) != NULL) {
if (!hl) if (line[0] == '\0') {
break; free(line);
l = strlen(hl);
if (l >= MAX_LINELEN)
hl[MAX_LINELEN-1] = '\0';
if (l == 0) {
free(hl);
continue; continue;
} }
state->history[hi++] = hl; free(temp_h[idx]);
temp_h[idx] = line;
state->cnt_history_in_file++;
idx++;
if (idx == MAX_HISTORY)
idx = 0;
} }
state->last_history_end = lseek(fileno(fp), 0, SEEK_CUR);
fclose(fp); fclose(fp);
state->cnt_history = hi;
/* find first non-NULL temp_h[], if any */
if (state->cnt_history_in_file) {
while (temp_h[idx] == NULL) {
idx++;
if (idx == MAX_HISTORY)
idx = 0;
}
}
/* copy temp_h[] to state->history[] */
for (i = 0; i < MAX_HISTORY;) {
line = temp_h[idx];
if (!line)
break;
idx++;
if (idx == MAX_HISTORY)
idx = 0;
line_len = strlen(line);
if (line_len >= MAX_LINELEN)
line[MAX_LINELEN-1] = '\0';
state->history[i++] = line;
}
state->cnt_history = i;
} }
} }
/* state->flags is already checked to be nonzero */ /* state->flags is already checked to be nonzero */
static void save_history(const char *tofile) static void save_history(char *str)
{ {
FILE *fp; off_t end;
int fd;
int len;
fp = fopen_for_write(tofile); len = strlen(str);
again:
fd = open(state->hist_file, O_WRONLY | O_CREAT | O_APPEND, 0666);
if (fd < 0)
return;
end = lseek(fd, 0, SEEK_END);
if (str) {
str[len] = '\n';
full_write(fd, str, len + 1);
str[len] = '\0';
str = NULL;
state->cnt_history_in_file++;
}
close(fd);
/* if it was not a 1st write */
if (state->last_history_end >= 0) {
/* did someone else write anything there? */
if (state->last_history_end != end) {
load_history(); /* note: updates cnt_history_in_file */
state->last_history_end = -1;
goto again;
}
}
state->last_history_end = end + len + 1;
/* did we write so much that history file needs trimming? */
if (state->cnt_history_in_file > MAX_HISTORY * 4) {
FILE *fp;
char *new_name;
new_name = xasprintf("%s.new", state->hist_file);
fp = fopen_for_write(new_name);
if (fp) { if (fp) {
int i; int i;
for (i = 0; i < state->cnt_history; i++) { for (i = 0; i < state->cnt_history; i++) {
fprintf(fp, "%s\n", state->history[i]); fprintf(fp, "%s\n", state->history[i]);
} }
state->cnt_history_in_file = i; /* == cnt_history */
state->last_history_end = lseek(fileno(fp), 0, SEEK_CUR);
fclose(fp); fclose(fp);
/* replace hist_file atomically */
rename(new_name, state->hist_file);
}
free(new_name);
} }
} }
#else #else
#define load_history(a) ((void)0) #define load_history() ((void)0)
#define save_history(a) ((void)0) #define save_history(a) ((void)0)
#endif /* FEATURE_COMMAND_SAVEHISTORY */ #endif /* FEATURE_COMMAND_SAVEHISTORY */
static void remember_in_history(const char *str) static void remember_in_history(char *str)
{ {
int i; int i;
@ -1078,7 +1155,7 @@ static void remember_in_history(const char *str)
state->cnt_history = i; state->cnt_history = i;
#if ENABLE_FEATURE_EDITING_SAVEHISTORY #if ENABLE_FEATURE_EDITING_SAVEHISTORY
if ((state->flags & SAVE_HISTORY) && state->hist_file) if ((state->flags & SAVE_HISTORY) && state->hist_file)
save_history(state->hist_file); save_history(str);
#endif #endif
USE_FEATURE_EDITING_FANCY_PROMPT(num_ok_lines++;) USE_FEATURE_EDITING_FANCY_PROMPT(num_ok_lines++;)
} }
@ -1413,7 +1490,7 @@ int FAST_FUNC read_line_input(const char *prompt, char *command, int maxsize, li
state = st ? st : (line_input_t*) &const_int_0; state = st ? st : (line_input_t*) &const_int_0;
#if ENABLE_FEATURE_EDITING_SAVEHISTORY #if ENABLE_FEATURE_EDITING_SAVEHISTORY
if ((state->flags & SAVE_HISTORY) && state->hist_file) if ((state->flags & SAVE_HISTORY) && state->hist_file)
load_history(state->hist_file); load_history();
#endif #endif
if (state->flags & DO_HISTORY) if (state->flags & DO_HISTORY)
state->cur_history = state->cnt_history; state->cur_history = state->cnt_history;
@ -1875,6 +1952,7 @@ line_input_t* FAST_FUNC new_line_input_t(int flags)
{ {
line_input_t *n = xzalloc(sizeof(*n)); line_input_t *n = xzalloc(sizeof(*n));
n->flags = flags; n->flags = flags;
n->last_history_end = -1;
return n; return n;
} }