123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439(*
* lTerm_history.ml
* ----------------
* Copyright : (c) 2012, Jeremie Dimino <jeremie@dimino.org>
* Licence : BSD3
*
* This file is a part of Lambda-Term.
*)openCamomileLibraryDefault.Camomileletreturn,(>>=)=Lwt.return,Lwt.(>>=)(* A node contains an entry of the history. *)typenode={mutabledata:Zed_utf8.t;mutablesize:int;mutableprev:node;}typet={mutableentries:node;(* Points to the first entry (the most recent). Its [prev] is a fake
node used as marker, is after the oldest entry. *)mutablefull_size:int;mutablelength:int;mutablemax_size:int;mutablemax_entries:int;mutableold_count:int;mutablecache:Zed_utf8.tlistoption;(* When set, the cache is equal to the list of entries, from the
most recent to the oldest. *)}letentry_sizestr=letsize=ref0infori=0toString.lengthstr-1domatchString.unsafe_getstriwith|'\n'|'\\'->size:=!size+2|_->size:=!size+1done;!size+1(* Check that [size1 + size2 < limit], handling overflow. *)letsize_oksize1size2limit=letsum=size1+size2insum>=0&&sum<=limitletcreate?(max_size=max_int)?(max_entries=max_int)init=ifmax_size<0theninvalid_arg"LTerm_history.create: negative maximum size";ifmax_entries<0theninvalid_arg"LTerm_history.create: negative maximum number of entries";letrecauxsizecountnodeentries=matchentrieswith|[]->(size,count,node)|entry::entries->letentry_size=entry_sizeentryinifsize_oksizeentry_sizemax_size&&count+1<max_entriesthenbeginletnext={data="";prev=node;size=0}innode.data<-entry;node.size<-entry_size;aux(size+entry_size)(count+1)nextentriesendelse(size,count,node)inletrecnode={data="";size=0;prev=node}inletsize,count,marker=aux00nodeinitinnode.prev<-marker;{entries=node;full_size=size;length=count;max_size=max_size;max_entries=max_entries;old_count=count;cache=None;}letspaces=UCharInfo.load_property_tbl`White_Spaceletis_spacech=UCharTbl.Bool.getspaceschletis_emptystr=Zed_utf8.for_allis_spacestrletis_duphistoryentry=history.length>0&&history.entries.data=entry(* Remove the oldest entry of history, precondition: the history
contains at least one entry. *)letdrop_oldesthistory=letlast=history.entries.prev.previn(* Make [last] become the end of entries marker. *)history.entries.prev<-last;(* Update counters. *)history.length<-history.length-1;history.full_size<-history.full_size-last.size;ifhistory.old_count>0thenhistory.old_count<-history.old_count-1;(* Clear the marker so its contents can be garbage collected. *)last.data<-"";last.size<-0letadd_auxhistorydatasize=ifsize<=history.max_sizethenbegin(* Check length. *)ifhistory.length=history.max_entriesthenbeginhistory.cache<-None;(* We know that [max_entries > 0], so the precondition is
verified. *)drop_oldesthistoryend;(* Check size. *)ifnot(size_okhistory.full_sizesizehistory.max_size)thenbeginhistory.cache<-None;(* We know that size <= max_size, so we are here only if there
is at least one other entry in the history, so the
precondition is verified. *)drop_oldesthistory;whilenot(size_okhistory.full_sizesizehistory.max_size)do(* Same here. *)drop_oldesthistorydoneend;(* Add the entry. *)letnode={data=data;size=size;prev=history.entries.prev}inhistory.entries.prev<-node;history.entries<-node;history.length<-history.length+1;history.full_size<-history.full_size+size;matchhistory.cachewith|None->()|Somel->history.cache<-Some(data::l)endletaddhistory?(skip_empty=true)?(skip_dup=true)entry=ifhistory.max_entries>0&&history.max_size>0&¬(skip_empty&&is_emptyentry)&¬(skip_dup&&is_duphistoryentry)thenadd_auxhistoryentry(entry_sizeentry)letreclist_of_nodesmarkeraccnode=ifnode==markerthenaccelselist_of_nodesmarker(node.data::acc)node.prevletcontentshistory=matchhistory.cachewith|Somel->l|None->letmarker=history.entries.previnletl=list_of_nodesmarker[]marker.previnhistory.cache<-Somel;lletsizehistory=history.full_sizeletlengthhistory=history.lengthletold_counthistory=history.old_countletmax_sizehistory=history.max_sizeletmax_entrieshistory=history.max_entriesletset_old_counthistoryn=ifn<0theninvalid_arg"LTerm_history.set_old_count: negative old count";ifn>history.lengththeninvalid_arg"LTerm_history.set_old_count: old count greater than the length of the history";history.old_count<-nletset_max_sizehistorysize=ifsize<0theninvalid_arg"LTerm_history.set_max_size: negative maximum size";ifsize<history.full_sizethenbeginhistory.cache<-None;(* 0 <= size < full_size so there is at least one element. *)drop_oldesthistory;whilesize<history.full_sizedo(* Same here. *)drop_oldesthistorydoneend;history.max_size<-sizeletset_max_entrieshistoryn=ifn<0theninvalid_arg"LTerm_history.set_max_entries: negative maximum number of entries";ifn<history.lengththenbeginhistory.cache<-None;(* 0 <= n < length so there is at least one element. *)drop_oldesthistory;whilen<history.lengthdo(* Same here. *)drop_oldesthistorydoneend;history.max_entries<-nletescapeentry=letlen=String.lengthentryinletbuf=Buffer.createleninletrecloopofs=ifofs=lenthenBuffer.contentsbufelsematchString.unsafe_getentryofswith|'\n'->Buffer.add_stringbuf"\\n";loop(ofs+1)|'\\'->Buffer.add_stringbuf"\\\\";loop(ofs+1)|chwhenChar.codech<=127->Buffer.add_charbufch;loop(ofs+1)|_->letofs'=Zed_utf8.unsafe_nextentryofsinBuffer.add_substringbufentryofs(ofs'-ofs);loopofs'inloop0letunescapeline=letlen=String.lengthlineinletbuf=Buffer.createleninletrecloopofssize=ifofs=lenthen(Buffer.contentsbuf,size+1)elsematchString.unsafe_getlineofswith|'\\'->ifofs=lenthenbeginBuffer.add_charbuf'\\';(Buffer.contentsbuf,size+3)endelsebeginmatchString.unsafe_getline(ofs+1)with|'n'->Buffer.add_charbuf'\n';loop(ofs+2)(size+2)|'\\'->Buffer.add_charbuf'\\';loop(ofs+2)(size+2)|_->Buffer.add_charbuf'\\';loop(ofs+1)(size+2)end|chwhenChar.codech<=127->Buffer.add_charbufch;loop(ofs+1)(size+1)|_->letofs'=Zed_utf8.unsafe_nextlineofsinBuffer.add_substringbuflineofs(ofs'-ofs);loopofs'(size+ofs'-ofs)inloop00letsection=Lwt_log.Section.make"lambda-term(history)"letrecsafe_lockffnfdcmdofs=Lwt.catch(fun()->Lwt_unix.lockffdcmdofs>>=fun()->returntrue)(function|Unix.Unix_error(Unix.EINTR,_,_)->safe_lockffnfdcmdofs|Unix.Unix_error(error,_,_)->Lwt_log.ign_warning_f~section"failed to lock file '%s': %s"fn(Unix.error_messageerror);returnfalse|exn->Lwt.failexn)letopen_historyfn=Lwt.catch(fun()->Lwt_unix.openfilefn[Unix.O_RDWR]0>>=funfd->safe_lockffnfdLwt_unix.F_LOCK0>>=funlocked->return(Some(fd,locked)))(function|Unix.Unix_error(Unix.ENOENT,_,_)->returnNone|Unix.Unix_error(Unix.EACCES,_,_)->Lwt_log.ign_info_f"cannot open file '%s' in read and write mode: %s"fn(Unix.error_messageUnix.EACCES);(* If the file cannot be openned in read & write mode,
open it in read only mode but do not lock it. *)Lwt.catch(fun()->Lwt_unix.openfilefn[Unix.O_RDONLY]0>>=funfd->return(Some(fd,false)))(function|Unix.Unix_error(Unix.ENOENT,_,_)->returnNone|exn->Lwt.failexn)|exn->Lwt.failexn)letloadhistory?log?(skip_empty=true)?(skip_dup=true)fn=(* In case we do not load anything. *)history.old_count<-history.length;ifhistory.max_entries=0||history.max_size=0then(* Do not bother loading the file for nothing... *)return()elsebeginletlog=matchlogwith|Somefunc->func|None->funlinemsg->Lwt_log.ign_error_f~section"File %S, at line %d: %s"fnlinemsgin(* File opening. *)open_historyfn>>=funhistory_file->matchhistory_filewith|None->return()|Some(fd,locked)->(* File loading. *)letic=Lwt_io.of_fd~mode:Lwt_io.inputfdinLwt.finalize(fun()->letrecauxnum=Lwt_io.read_line_optic>>=funline->matchlinewith|None->return()|Someline->(tryletentry,size=unescapelineinifnot(skip_empty&&is_emptyentry)&¬(skip_dup&&is_duphistoryentry)thenbeginadd_auxhistoryentrysize;history.old_count<-history.lengthendwithZed_utf8.Invalid(msg,_)->lognummsg);aux(num+1)inaux1)(fun()->(* Cleanup. *)(iflockedthensafe_lockffnfdLwt_unix.F_ULOCK0elsereturntrue)>>=fun_->Lwt_unix.closefd)endletrecskip_nodesnodecount=ifcount=0thennodeelseskip_nodesnode.prev(count-1)letreccopyhistorymarkernodeskip_emptyskip_dup=ifnode!=markerthenbeginletline=escapenode.datainifnot(skip_empty&&is_emptyline)&¬(skip_dup&&is_duphistoryline)thenadd_auxhistorylinenode.size;copyhistorymarkernode.prevskip_emptyskip_dupendletrecdump_entriesocmarkernode=ifnode==markerthenreturn()elsebeginLwt_io.write_lineocnode.data>>=fun()->dump_entriesocmarkernode.prevendletsavehistory?max_size?max_entries?(skip_empty=true)?(skip_dup=true)?(append=true)?(perm=0o666)fn=letmax_size=matchmax_sizewith|Somem->m|None->history.max_sizeandmax_entries=matchmax_entrieswith|Somem->m|None->history.max_entriesinlethistory_save=create~max_size~max_entries[]inifhistory_save.max_size=0||history_save.max_entries=0||(notappend&&history.old_count=history.length)then(* Just empty the history. *)Lwt_unix.openfilefn[Unix.O_CREAT;Unix.O_TRUNC]perm>>=Lwt_unix.closeelseifappend&&history.old_count=history.lengththen(* Do not touch the file. *)return()elsebeginLwt_unix.openfilefn[Unix.O_CREAT;Unix.O_RDWR]perm>>=funfd->(* Lock the entire file. *)safe_lockffnfdUnix.F_LOCK0>>=funlocked->Lwt.finalize(fun()->beginifappendthenbegin(* Load existing entries into [history_save].
We return the number of entries read. This may be greater
than the number of entries stored in [history_save]:
- because of limits
- because the history files contains duplicated lines
and/or empty lines and [skip_dup] and/or [skip_empty]
have been specified. *)letic=Lwt_io.of_fd~mode:Lwt_io.input~close:returnfdinletrecauxcount=Lwt_io.read_line_optic>>=funline->matchlinewith|None->history_save.old_count<-history_save.length;Lwt_io.closeic>>=fun()->returncount|Someline->(* Do not bother unescaping. Tests remain the same
on the unescaped version. *)ifnot(skip_empty&&is_emptyline)&¬(skip_dup&&is_duphistory_saveline)thenadd_auxhistory_saveline(String.lengthline+1);aux(count+1)inaux0endelsereturn0end>>=funcount->letmarker=history.entries.previn(* Copy new entries into the saving history. *)copyhistory_savemarker(skip_nodesmarker.prevhistory.old_count)skip_emptyskip_dup;beginifappend&&history_save.old_count=countthen(* We are in append mode and no old entries were removed: do
not modify the file and append new entries at the end of
the file. *)returncountelse(* Otherwise truncate the file and save everything. *)Lwt_unix.lseekfd0Unix.SEEK_SET>>=fun_->Lwt_unix.ftruncatefd0>>=fun()->return0end>>=funto_skip->(* Save entries to the temporary file. *)letoc=Lwt_io.of_fd~mode:Lwt_io.output~close:returnfdinletmarker=history_save.entries.previndump_entriesocmarker(skip_nodesmarker.prevto_skip)>>=fun()->Lwt_io.closeoc>>=fun()->(* Done! *)history.old_count<-history.length;return())(fun()->(iflockedthensafe_lockffnfdLwt_unix.F_ULOCK0elsereturntrue)>>=fun_->Lwt_unix.closefd)end