comparison persistent-tags.sl @ 0:8eeb70d3d1ce

Initial revision
author Guido Berhoerster <guido+slrn@berhoerster.name>
date Sat, 14 Mar 2015 11:43:52 +0100
parents
children 49f639bc9bd9
comparison
equal deleted inserted replaced
-1:000000000000 0:8eeb70d3d1ce
1 % persistent-tags.sl - keep persistent tags across sessions
2 %
3 % Copyright (C) 2009 Guido Berhoerster <guido+slrn@berhoerster.name>
4 %
5 % Permission is hereby granted, free of charge, to any person obtaining
6 % a copy of this software and associated documentation files (the
7 % "Software"), to deal in the Software without restriction, including
8 % without limitation the rights to use, copy, modify, merge, publish,
9 % distribute, sublicense, and/or sell copies of the Software, and to
10 % permit persons to whom the Software is furnished to do so, subject to
11 % the following conditions:
12 %
13 % The above copyright notice and this permission notice shall be included
14 % in all copies or substantial portions of the Software.
15 %
16 % THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17 % EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18 % MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
19 % IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
20 % CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
21 % TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
22 % SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
23
24 %open_log_file(make_home_filename("slrn-debug.log"));
25 %_traceback = 1;
26
27 implements("PersistentTags");
28
29 private variable rand_next = 1;
30
31 % implementation of rand based on an example in IEEE Std 1003.1, 2004 Edition
32 static define myrand() {
33 rand_next = rand_next * 1103515245 + 12345;
34 % RAND_MAX is hardcoded to 32767
35 return ((rand_next / 65536U) mod 32768U);
36 }
37
38 static define mysrand(seed) {
39 rand_next = seed;
40 }
41
42 private variable URL_SAFE_CHARS =
43 "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ.-+";
44
45 private define urldecode(str)
46 {
47 variable decoded_str = ""B;
48 variable char;
49 variable pos = 1;
50 variable opos = 1;
51
52 forever {
53 pos = string_match(str, "%[0-9a-fA-F][0-9a-fA-F]", opos);
54 if (pos == 0)
55 break;
56
57 % add characters between the last match and the current match
58 decoded_str += substr(str, opos, pos - opos);
59 % convert the hex representation of a byte to a byte and append it to
60 % the string
61 char = integer("0x" + substr(str, pos + 1, 2));
62 decoded_str += pack("C", char);
63 opos = pos + 3;
64 }
65 % add remaining charcters
66 decoded_str += substr(str, opos, -1);
67 return typecast(decoded_str, String_Type);
68 }
69
70 private define urlencode(str)
71 {
72 variable char;
73 variable encoded_str = "";
74 variable i;
75 variable j;
76
77 for (i = 0; i < strlen(str); ++i) {
78 char = substr(str, i + 1, 1);
79 ifnot (is_substr(URL_SAFE_CHARS, char)) {
80 for (j = 0; j < strbytelen(char); ++j) {
81 encoded_str += sprintf("%%%02X", char[j]);
82 }
83 } else {
84 encoded_str += sprintf("%s", char);
85 }
86 }
87 return encoded_str;
88 }
89
90 private variable FILENAME_CHARS =
91 "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";
92 private variable FILENAME_CHARS_LEN = strlen(FILENAME_CHARS);
93
94 private define mkstemp(template)
95 {
96 variable fd;
97 variable tmp_filename;
98 variable len_template = strlen(@template);
99 variable c;
100
101 if (len_template < 6)
102 return NULL;
103 c = len_template - 6;
104 ifnot (substr(@template, c + 1, len_template) == "XXXXXX")
105 return NULL;
106
107 loop (10000) {
108 tmp_filename = substr(@template, 1, c);
109 loop(6) {
110 tmp_filename += FILENAME_CHARS[[myrand() mod FILENAME_CHARS_LEN]];
111 }
112 fd = open(tmp_filename, O_CREAT|O_EXCL|O_RDWR, 0600);
113 ifnot (fd == NULL) {
114 @template = tmp_filename;
115 break;
116 }
117 }
118
119 return fd;
120 }
121
122 private define lock_file(filename)
123 {
124 variable fd;
125 variable st;
126 variable timeout = qualifier("timeout", 30);
127 variable stale_timeout = qualifier("stale_timeout", 360);
128 variable lockfile = filename + ".lock";
129 variable tmp_lockfile = lockfile + ".XXXXXX";
130 variable time_timeout = _time() + timeout;
131 variable time_stale_timeout = _time() - stale_timeout;
132
133 fd = mkstemp(&tmp_lockfile);
134 if (fd == NULL)
135 return NULL;
136 () = close(fd);
137 try {
138 % attempt to acquire a lock until time_timeout is reached
139 while (_time() < time_timeout) {
140 % try to link lockfile to the previously created temporary file,
141 % link(2) is atomic even on NFSv2 if the lockfile exists link(2)
142 % will fail, this is either detected if EEXIST is returned or the
143 % link count of the temporary file is not 2 in this case try to
144 % remove a stale lockfile, then wait and try again
145 ifnot ((hardlink(tmp_lockfile, lockfile) == 0) ||
146 (errno == EEXIST))
147 return NULL;
148
149 st = stat_file(tmp_lockfile);
150 if (st == NULL)
151 return NULL;
152 if (st.st_nlink == 2)
153 return lockfile;
154
155 st = stat_file(lockfile);
156 if (st == NULL) {
157 ifnot (errno == ENOENT)
158 return NULL;
159 else
160 continue;
161 }
162
163 % remove a stale lockfile after stale_timeout seconds have passed
164 if (st.st_mtime < time_stale_timeout)
165 () = remove(lockfile);
166
167 sleep(2);
168 }
169
170 return NULL;
171 } finally {
172 () = remove(tmp_lockfile);
173 }
174 }
175
176 static variable config = struct
177 {
178 tag_path = ".slrn-tags",
179 autosave = 1
180 };
181
182 private variable tag_list = Assoc_Type[Null_Type];
183
184 private define update_tag_list(ref_tag_list)
185 {
186 variable new_tag_list = Assoc_Type[Null_Type];
187 variable needs_update = 0;
188 variable msgid = NULL;
189
190 call("header_bob");
191 % the very first article must be treated specially so it will not be missed
192 % when doing next_tagged_header()
193 while (((msgid == NULL) && (get_header_flags() & HEADER_TAGGED)) ||
194 next_tagged_header()) {
195 msgid = extract_article_header("Message-ID");
196 if (strlen(msgid) == 0)
197 continue;
198 new_tag_list[msgid] = NULL;
199
200 % check if a new element which will go into new_tag_list also exists
201 % in tag_list in order to find any difference between both lists
202 ifnot (needs_update || assoc_key_exists(@ref_tag_list, msgid))
203 needs_update = 1;
204 }
205
206 % if all elements of new_tag_list are also contained in tag_lists
207 % check whether all elements of tag_lists are also contained in
208 % new_tag_lists in order to find any difference between both lists
209 ifnot (needs_update) {
210 foreach msgid(@ref_tag_list) using("keys") {
211 ifnot (assoc_key_exists(new_tag_list, msgid)) {
212 needs_update = 1;
213 break;
214 }
215 }
216 }
217
218 % replace tag_list with new_tag_list
219 @ref_tag_list = new_tag_list;
220 return needs_update;
221 }
222
223 static define save_tags()
224 {
225 variable line, fp;
226 variable dir = path_concat(make_home_filename(config.tag_path),
227 urlencode(server_name()));
228 variable filename = path_concat(dir, urlencode(current_newsgroup()));
229 variable lockfile;
230
231 ifnot (update_tag_list(&tag_list))
232 return;
233
234 if (mkdir(dir) == -1) {
235 ifnot (errno == EEXIST)
236 throw IOError, "mkdir $dir failed: "$ + errno_string(errno);
237 }
238
239 lockfile = lock_file(filename);
240 if (lockfile == NULL)
241 throw IOError, "failed to lock $filename"$;
242
243 try {
244 % if tag_list is empty remove the file
245 if (length(tag_list) == 0) {
246 () = remove(filename);
247 return;
248 }
249
250 fp = fopen(filename, "w");
251 if (fp == NULL)
252 throw OpenError, "opening $filename failed: "$ +
253 errno_string(errno);
254 foreach line(tag_list) using("keys") {
255 if (fputs(line + "\n", fp) == -1)
256 throw WriteError, "writing to $filename failed: "$ +
257 errno_string(errno);
258 }
259 () = fclose(fp);
260 } finally {
261 () = remove(lockfile);
262 }
263 return;
264 }
265
266 static define load_tags()
267 {
268 variable fp, buf, msgid, pos;
269 variable dir = path_concat(make_home_filename(config.tag_path),
270 urlencode(server_name()));
271 variable filename = path_concat(dir, urlencode(current_newsgroup()));
272
273 % re-ininitalize tag_list
274 tag_list = Assoc_Type[Null_Type];
275
276 fp = fopen(filename, "r");
277 if (fp == NULL) {
278 if (errno == ENOENT)
279 return;
280 throw OpenError, "opening $filename failed: "$ + errno_string(errno);
281 }
282
283 % save position to restore after applying tags
284 pos = extract_article_header("Message-ID");
285 while (fgets(&buf, fp) != -1) {
286 msgid = strtrim(buf);
287 if (strlen(msgid)) {
288 tag_list[msgid] = NULL;
289 if (locate_header_by_msgid(msgid, 1))
290 set_header_flags(get_header_flags() | HEADER_TAGGED);
291 }
292 }
293 () = fclose(fp);
294 () = locate_header_by_msgid(pos, 0);
295
296 return;
297 }
298
299 mysrand(_time() * getpid());
300
301 if (config.autosave)
302 {
303 () = register_hook("article_mode_hook", "PersistentTags->load_tags");
304 () = register_hook("article_mode_quit_hook", "PersistentTags->save_tags");
305 }