# HG changeset patch # User Guido Berhoerster # Date 1426329832 -3600 # Node ID 8eeb70d3d1ce0fc04fa0999c4f6f401e71c8f783 Initial revision diff -r 000000000000 -r 8eeb70d3d1ce README --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/README Sat Mar 14 11:43:52 2015 +0100 @@ -0,0 +1,65 @@ +slrn Persistent Tags Macro +========================== + +The slrn persistent tags macro keeps tags persitent across sessions. + +Usage +----- + +The slrn persistent tags macro can be used by including it in the .slrnrc user +initialization file via the `interpret` command, e.g. provided that the file +persistent-tags.sl is located in one of the directories specified by the +macro_directory configuration variable: + + interpret "persistent-tags.sl" + +The persistent tags macro can be configured through the following slang +variables: + +PersistentTags->tag_path +: Specified the path where tag files are stored. + +PersistentTags->autosave +: Determines whether tags are saved automatically. + +Contact +------- + +Please send any feedback, translations or bug reports via email to +. + +Bug Reports +----------- + +When sending bug reports, please always mention the exact version of the +macro with which the issue occurs as well as the version of slrn, slang and +the operating system you are using and make sure that you provide sufficient +information to reproduce the issue and include any input, output, any error +messages and slang stack traces. + +License +------- + +Except otherwise noted, all files are Copyright (C) 2009 Guido Berhoerster and +distributed under the following license terms: + +Copyright (C) 2009 Guido Berhoerster + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be included +in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff -r 000000000000 -r 8eeb70d3d1ce persistent-tags.sl --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/persistent-tags.sl Sat Mar 14 11:43:52 2015 +0100 @@ -0,0 +1,305 @@ +% persistent-tags.sl - keep persistent tags across sessions +% +% Copyright (C) 2009 Guido Berhoerster +% +% Permission is hereby granted, free of charge, to any person obtaining +% a copy of this software and associated documentation files (the +% "Software"), to deal in the Software without restriction, including +% without limitation the rights to use, copy, modify, merge, publish, +% distribute, sublicense, and/or sell copies of the Software, and to +% permit persons to whom the Software is furnished to do so, subject to +% the following conditions: +% +% The above copyright notice and this permission notice shall be included +% in all copies or substantial portions of the Software. +% +% THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +% EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +% MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +% IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +% CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +% TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +% SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +%open_log_file(make_home_filename("slrn-debug.log")); +%_traceback = 1; + +implements("PersistentTags"); + +private variable rand_next = 1; + +% implementation of rand based on an example in IEEE Std 1003.1, 2004 Edition +static define myrand() { + rand_next = rand_next * 1103515245 + 12345; + % RAND_MAX is hardcoded to 32767 + return ((rand_next / 65536U) mod 32768U); +} + +static define mysrand(seed) { + rand_next = seed; +} + +private variable URL_SAFE_CHARS = + "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ.-+"; + +private define urldecode(str) +{ + variable decoded_str = ""B; + variable char; + variable pos = 1; + variable opos = 1; + + forever { + pos = string_match(str, "%[0-9a-fA-F][0-9a-fA-F]", opos); + if (pos == 0) + break; + + % add characters between the last match and the current match + decoded_str += substr(str, opos, pos - opos); + % convert the hex representation of a byte to a byte and append it to + % the string + char = integer("0x" + substr(str, pos + 1, 2)); + decoded_str += pack("C", char); + opos = pos + 3; + } + % add remaining charcters + decoded_str += substr(str, opos, -1); + return typecast(decoded_str, String_Type); +} + +private define urlencode(str) +{ + variable char; + variable encoded_str = ""; + variable i; + variable j; + + for (i = 0; i < strlen(str); ++i) { + char = substr(str, i + 1, 1); + ifnot (is_substr(URL_SAFE_CHARS, char)) { + for (j = 0; j < strbytelen(char); ++j) { + encoded_str += sprintf("%%%02X", char[j]); + } + } else { + encoded_str += sprintf("%s", char); + } + } + return encoded_str; +} + +private variable FILENAME_CHARS = + "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"; +private variable FILENAME_CHARS_LEN = strlen(FILENAME_CHARS); + +private define mkstemp(template) +{ + variable fd; + variable tmp_filename; + variable len_template = strlen(@template); + variable c; + + if (len_template < 6) + return NULL; + c = len_template - 6; + ifnot (substr(@template, c + 1, len_template) == "XXXXXX") + return NULL; + + loop (10000) { + tmp_filename = substr(@template, 1, c); + loop(6) { + tmp_filename += FILENAME_CHARS[[myrand() mod FILENAME_CHARS_LEN]]; + } + fd = open(tmp_filename, O_CREAT|O_EXCL|O_RDWR, 0600); + ifnot (fd == NULL) { + @template = tmp_filename; + break; + } + } + + return fd; +} + +private define lock_file(filename) +{ + variable fd; + variable st; + variable timeout = qualifier("timeout", 30); + variable stale_timeout = qualifier("stale_timeout", 360); + variable lockfile = filename + ".lock"; + variable tmp_lockfile = lockfile + ".XXXXXX"; + variable time_timeout = _time() + timeout; + variable time_stale_timeout = _time() - stale_timeout; + + fd = mkstemp(&tmp_lockfile); + if (fd == NULL) + return NULL; + () = close(fd); + try { + % attempt to acquire a lock until time_timeout is reached + while (_time() < time_timeout) { + % try to link lockfile to the previously created temporary file, + % link(2) is atomic even on NFSv2 if the lockfile exists link(2) + % will fail, this is either detected if EEXIST is returned or the + % link count of the temporary file is not 2 in this case try to + % remove a stale lockfile, then wait and try again + ifnot ((hardlink(tmp_lockfile, lockfile) == 0) || + (errno == EEXIST)) + return NULL; + + st = stat_file(tmp_lockfile); + if (st == NULL) + return NULL; + if (st.st_nlink == 2) + return lockfile; + + st = stat_file(lockfile); + if (st == NULL) { + ifnot (errno == ENOENT) + return NULL; + else + continue; + } + + % remove a stale lockfile after stale_timeout seconds have passed + if (st.st_mtime < time_stale_timeout) + () = remove(lockfile); + + sleep(2); + } + + return NULL; + } finally { + () = remove(tmp_lockfile); + } +} + +static variable config = struct +{ + tag_path = ".slrn-tags", + autosave = 1 +}; + +private variable tag_list = Assoc_Type[Null_Type]; + +private define update_tag_list(ref_tag_list) +{ + variable new_tag_list = Assoc_Type[Null_Type]; + variable needs_update = 0; + variable msgid = NULL; + + call("header_bob"); + % the very first article must be treated specially so it will not be missed + % when doing next_tagged_header() + while (((msgid == NULL) && (get_header_flags() & HEADER_TAGGED)) || + next_tagged_header()) { + msgid = extract_article_header("Message-ID"); + if (strlen(msgid) == 0) + continue; + new_tag_list[msgid] = NULL; + + % check if a new element which will go into new_tag_list also exists + % in tag_list in order to find any difference between both lists + ifnot (needs_update || assoc_key_exists(@ref_tag_list, msgid)) + needs_update = 1; + } + + % if all elements of new_tag_list are also contained in tag_lists + % check whether all elements of tag_lists are also contained in + % new_tag_lists in order to find any difference between both lists + ifnot (needs_update) { + foreach msgid(@ref_tag_list) using("keys") { + ifnot (assoc_key_exists(new_tag_list, msgid)) { + needs_update = 1; + break; + } + } + } + + % replace tag_list with new_tag_list + @ref_tag_list = new_tag_list; + return needs_update; +} + +static define save_tags() +{ + variable line, fp; + variable dir = path_concat(make_home_filename(config.tag_path), + urlencode(server_name())); + variable filename = path_concat(dir, urlencode(current_newsgroup())); + variable lockfile; + + ifnot (update_tag_list(&tag_list)) + return; + + if (mkdir(dir) == -1) { + ifnot (errno == EEXIST) + throw IOError, "mkdir $dir failed: "$ + errno_string(errno); + } + + lockfile = lock_file(filename); + if (lockfile == NULL) + throw IOError, "failed to lock $filename"$; + + try { + % if tag_list is empty remove the file + if (length(tag_list) == 0) { + () = remove(filename); + return; + } + + fp = fopen(filename, "w"); + if (fp == NULL) + throw OpenError, "opening $filename failed: "$ + + errno_string(errno); + foreach line(tag_list) using("keys") { + if (fputs(line + "\n", fp) == -1) + throw WriteError, "writing to $filename failed: "$ + + errno_string(errno); + } + () = fclose(fp); + } finally { + () = remove(lockfile); + } + return; +} + +static define load_tags() +{ + variable fp, buf, msgid, pos; + variable dir = path_concat(make_home_filename(config.tag_path), + urlencode(server_name())); + variable filename = path_concat(dir, urlencode(current_newsgroup())); + + % re-ininitalize tag_list + tag_list = Assoc_Type[Null_Type]; + + fp = fopen(filename, "r"); + if (fp == NULL) { + if (errno == ENOENT) + return; + throw OpenError, "opening $filename failed: "$ + errno_string(errno); + } + + % save position to restore after applying tags + pos = extract_article_header("Message-ID"); + while (fgets(&buf, fp) != -1) { + msgid = strtrim(buf); + if (strlen(msgid)) { + tag_list[msgid] = NULL; + if (locate_header_by_msgid(msgid, 1)) + set_header_flags(get_header_flags() | HEADER_TAGGED); + } + } + () = fclose(fp); + () = locate_header_by_msgid(pos, 0); + + return; +} + +mysrand(_time() * getpid()); + +if (config.autosave) +{ + () = register_hook("article_mode_hook", "PersistentTags->load_tags"); + () = register_hook("article_mode_quit_hook", "PersistentTags->save_tags"); +}