summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorKaz Kyheku <kaz@kylheku.com>2020-02-18 06:40:15 -0800
committerKaz Kylheku <kaz@kylheku.com>2020-02-18 06:40:15 -0800
commite41669c53b57fa19f614e22a328fdf181d9c8f46 (patch)
treeab774f89fafa02a26ae4cacddde504fa85962fbd
parent3080fb5f28e0337b5859f4d6eb19bda9207a65fa (diff)
downloadtxr-e41669c53b57fa19f614e22a328fdf181d9c8f46.tar.gz
txr-e41669c53b57fa19f614e22a328fdf181d9c8f46.tar.bz2
txr-e41669c53b57fa19f614e22a328fdf181d9c8f46.zip
listener: append to .txr_history instead of clobbering.
This patch addresses the problem of history loss that occurs when a user juggles multiple TXR sessions that all clobber the same history file. * linenoise/linenoise.c (struct lino_state): New member, loaded_lines, keeping track of how many of the lines in the history came from loading the history file. Lines between [0] and [loaded_lines - 1] are loaded lines. New lines occur between [loaded_lines] and [history_len - 1]. (lino_hist_add): Reset loaded_lines to zero when creating history for the first time. Not really necessary since the structure starts zero-filled. When a line of history is erased, then it must be a loaded line, unless loaded_lines is zero. Thus, then decrement loaded_lines to account for a loss of a loaded line, but don't decrement below zero. (lino_hist_set_max_len): Setting the max length can cause history to be trimmed, so we must adjust loaded_lines to account for any loaded lines that get discarded. (lino_hist_save): Takes a new parameter which indicates whether to just save the new history by appending it to the given file, or to overwrite the file with the entire history. In either case, once we save the history, we assume that all of our lines are loaded lines and set loaded_lines to hist_len. In the future, this last step will help implement incremental saving mid-way through a sesssion. (lino_hist_load): Error out if there is already a history. With this loaded_lines logic, it really wouldn't make sense to read history more than once. After loading, set loaded_lines to hist_len. * linenoise/linenoise.h (enum lino_file_mode): New enumeration lino_append. (lino_hist_save): Declaration updated. * parser.c (repl): Implement new history saving protocol. The history file is read using a temporary instance of linenoise, which has the effect of trimming it to the required number of lines. This is written to a temporary file, to which the newly entered lines are appended, and which is finally renamed to replace the history file. (lino_mode_str): Add "a" entry corresponding to lino_append. (lino_open): Do the fchmod in the lino_append case also. * txr.1: Documented the new handling of the history file.
-rw-r--r--linenoise/linenoise.c25
-rw-r--r--linenoise/linenoise.h3
-rw-r--r--parser.c15
-rw-r--r--txr.127
4 files changed, 60 insertions, 10 deletions
diff --git a/linenoise/linenoise.c b/linenoise/linenoise.c
index bdb0fedc..92a5b0e7 100644
--- a/linenoise/linenoise.c
+++ b/linenoise/linenoise.c
@@ -106,6 +106,7 @@ struct lino_state {
int mlmode; /* Multi line mode. Default is single line. */
int history_max_len;
int history_len;
+ int loaded_lines; /* How many lines come from load. */
wchar_t **history;
wchar_t *clip; /* Selection */
wchar_t *result; /* Previous command result. */
@@ -2680,6 +2681,7 @@ int lino_hist_add(lino_t *ls, const wchar_t *line) {
ls->history = coerce(wchar_t **, lino_os.alloc_fn(size));
if (ls->history == NULL) return 0;
memset(ls->history, 0, size);
+ ls->loaded_lines = 0;
}
/* Don't add duplicated lines, unless we are resubmitting historic lines. */
@@ -2695,6 +2697,8 @@ int lino_hist_add(lino_t *ls, const wchar_t *line) {
lino_os.free_fn(ls->history[0]);
memmove(ls->history,ls->history+1,(ls->history_max_len-1)*sizeof *ls->history);
ls->history_len--;
+ if (ls->loaded_lines > 0)
+ ls->loaded_lines--;
}
ls->history[ls->history_len] = linecopy;
ls->history_len++;
@@ -2723,6 +2727,10 @@ int lino_hist_set_max_len(lino_t *ls, int len) {
for (j = 0; j < tocopy-len; j++)
lino_os.free_fn(ls->history[j]);
tocopy = len;
+
+ ls->loaded_lines -= (tocopy - len);
+ if (ls->loaded_lines < 0)
+ ls->loaded_lines = 0;
}
memset(nsv, 0, sizeof *nsv * len);
memcpy(nsv, ls->history+(ls->history_len-tocopy), sizeof *ls->history * tocopy);
@@ -2736,8 +2744,10 @@ int lino_hist_set_max_len(lino_t *ls, int len) {
/* Save the history in the specified file. On success 0 is returned
* otherwise -1 is returned. */
-int lino_hist_save(lino_t *ls, const wchar_t *filename) {
- mem_t *fp = lino_os.open_fn(filename, lino_overwrite);
+int lino_hist_save(lino_t *ls, const wchar_t *filename, int new_only) {
+ int from = new_only ? ls->loaded_lines : 0;
+ mem_t *fp = lino_os.open_fn(filename,
+ new_only ? lino_append : lino_overwrite);
int j;
if (fp == NULL) {
@@ -2745,11 +2755,13 @@ int lino_hist_save(lino_t *ls, const wchar_t *filename) {
return -1;
}
- for (j = 0; j < ls->history_len; j++) {
+ for (j = from; j < ls->history_len; j++) {
lino_os.puts_file_fn(fp, ls->history[j]);
lino_os.puts_file_fn(fp, L"\n");
}
+ ls->loaded_lines = ls->history_len;
+
lino_os.close_fn(fp);
return 0;
}
@@ -2768,6 +2780,12 @@ int lino_hist_load(lino_t *ls, const wchar_t *filename) {
return -1;
}
+ if (ls->history) {
+ ls->error = lino_error;
+ lino_os.close_fn(fp);
+ return -1;
+ }
+
while (lino_os.getl_fn(fp, buf, LINENOISE_MAX_LINE) != NULL) {
wchar_t *p = wcschr(buf, '\n');
if (p)
@@ -2776,6 +2794,7 @@ int lino_hist_load(lino_t *ls, const wchar_t *filename) {
}
lino_os.close_fn(fp);
+ ls->loaded_lines = ls->history_len;
return 0;
}
diff --git a/linenoise/linenoise.h b/linenoise/linenoise.h
index 178df11c..05f66632 100644
--- a/linenoise/linenoise.h
+++ b/linenoise/linenoise.h
@@ -58,6 +58,7 @@ typedef unsigned char mem_t;
typedef enum lino_file_mode {
lino_read,
lino_overwrite,
+ lino_append,
} lino_file_mode_t;
typedef struct lino_os {
@@ -112,7 +113,7 @@ lino_error_t lino_get_error(lino_t *);
lino_error_t lino_set_error(lino_t *, lino_error_t); /* returns old */
int lino_hist_add(lino_t *, const wchar_t *line);
int lino_hist_set_max_len(lino_t *, int len);
-int lino_hist_save(lino_t *, const wchar_t *filename);
+int lino_hist_save(lino_t *, const wchar_t *filename, int new_only);
int lino_hist_load(lino_t *, const wchar_t *filename);
void lino_set_result(lino_t *, wchar_t *); /* takes ownership of malloced mem; modifies it */
int lino_clear_screen(lino_t *);
diff --git a/parser.c b/parser.c
index 8746f4c8..0c6067e7 100644
--- a/parser.c
+++ b/parser.c
@@ -1505,12 +1505,19 @@ val repl(val bindings, val in_stream, val out_stream, val env)
dyn_env = saved_dyn_env;
if (histfile_w) {
- val histfile_tmp = format(nil, lit("~a/.txr_history.tmp"), home, nao);
- if (lino_hist_save(ls, c_str(histfile_tmp)) == 0)
+ val histfile_tmp = format(nil, lit("~a.tmp"), histfile, nao);
+ const wchar_t *histfile_tmp_w = c_str(histfile_tmp);
+ lino_t *ltmp = lino_make(coerce(mem_t *, in_stream),
+ coerce(mem_t *, out_stream));
+ lino_hist_set_max_len(ltmp, c_num(cdr(hist_len_var)));
+ lino_hist_load(ltmp, histfile_w);
+ lino_hist_save(ltmp, histfile_tmp_w, 0);
+ if (lino_hist_save(ls, histfile_tmp_w, 1) == 0)
rename_path(histfile_tmp, histfile);
else
put_line(lit("** unable to save history file"), out_stream);
gc_hint(histfile_tmp);
+ lino_free(ltmp);
}
free(line_w);
@@ -1649,7 +1656,7 @@ static int lino_feof(mem_t *stream_in)
}
static const wchli_t *lino_mode_str[] = {
- wli("r"), wli("w")
+ wli("r"), wli("w"), wli("a")
};
static mem_t *lino_open(const wchar_t *name_in, lino_file_mode_t mode_in)
@@ -1660,7 +1667,7 @@ static mem_t *lino_open(const wchar_t *name_in, lino_file_mode_t mode_in)
ignerr_begin;
ret = open_file(name, mode);
#if HAVE_CHMOD
- if (mode_in == lino_overwrite)
+ if (mode_in == lino_overwrite || mode_in == lino_append)
(void) fchmod(c_num(stream_fd(ret)), S_IRUSR | S_IWUSR);
#endif
ignerr_end;
diff --git a/txr.1 b/txr.1
index 6c032c05..0d2c3c09 100644
--- a/txr.1
+++ b/txr.1
@@ -72025,9 +72025,32 @@ option isn't present.
The history is maintained in a text file called
.code .txr_history
in the user's home directory. Whenever the interactive listener terminates,
-this file is overwritten with the history contents stored in the listener's
+this file is updated with the history contents stored in the listener's
memory. The next time the listener starts, it first re-loads the history from
-this file, making the commands of a previous session available for recall.
+this file, making the most recent
+.code *listener-hist-len*
+expressions of a previous session available for recall.
+
+The history file is maintained in a way that is somewhat
+robust against the loss of history arising from the situation that a user
+manages multiple simultaneous \*(TX sessions. When a session terminates, it
+doesn't blindly overwrite the history file, which may have already been updated
+with new history produced by another session. Rather, it appends new entries
+to the history file. New entries are those that had not been previously read
+from the history file, but have been newly entered into the listener.
+
+An effort is made to keep the history file trimmed to no more than
+twice the number of entries specified in
+.codn *listener-hist-len* .
+The terminating session first makes a temporary copy of the existing
+history, which is trimmed to the most recent
+.code *listener-hist-len*
+entries. New entries are then appended to this temporary file.
+Finally, the actual history file is replaced with this temporary file by a
+.code rename-path
+a rename operation. This algorithm doesn't use locking, and is therefore not
+robust against the situation when a two or more multiple interactive \*(TX
+sessions belonging to the same user terminate at around the same time.
The home directory is determined from the
contents of the