No Description
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

syncmaildir.lua 19KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734
  1. -- Released under the terms of GPLv3 or at your option any later version.
  2. -- No warranties.
  3. -- Copyright Enrico Tassi <gares@fettunta.org>
  4. --
  5. -- common code for smd-client/server
  6. local PROTOCOL_VERSION="1.2"
  7. local verbose = false
  8. local dryrun = false
  9. local translator = false
  10. local PREFIX = '@PREFIX@'
  11. local BUGREPORT_ADDRESS = 'syncmaildir-users@lists.sourceforge.net'
  12. local __G = _G
  13. local __error = _G.error
  14. module('syncmaildir',package.seeall)
  15. -- set mddiff path
  16. MDDIFF = ""
  17. if string.sub(PREFIX,1,1) == '@' then
  18. MDDIFF = './mddiff'
  19. io.stderr:write('smd-client not installed, assuming mddiff is: ',
  20. MDDIFF,'\n')
  21. else
  22. MDDIFF = PREFIX .. '/bin/mddiff'
  23. end
  24. -- set xdelta executable name
  25. XDELTA = '@XDELTA@'
  26. if string.sub(XDELTA,1,1) == '@' then
  27. XDELTA = 'xdelta'
  28. end
  29. -- set smd version
  30. SMDVERSION = '@SMDVERSION@'
  31. if string.sub(SMDVERSION,1,1) == '@' then
  32. SMDVERSION = '0.0.0'
  33. end
  34. -- to call external filter processes without too much pain
  35. function make_slave_filter_process(cmd, seed)
  36. seed = seed or "no seed"
  37. local init = function(filter)
  38. if filter.inf == nil then
  39. local rc
  40. local base_dir
  41. local home = os.getenv('HOME')
  42. local user = os.getenv('USER') or 'nobody'
  43. local mangled_name = string.gsub(seed,"[ %./]",'-')
  44. local attempt = 0
  45. if home ~= nil then
  46. base_dir = home ..'/.smd/fifo/'
  47. else
  48. base_dir = '/tmp/'
  49. end
  50. rc = os.execute(MDDIFF..' --mkdir-p '..quote(base_dir))
  51. if rc ~= 0 then
  52. log_internal_error_and_fail('unable to create directory',
  53. "make_slave_filter_process")
  54. end
  55. repeat
  56. pipe = base_dir..'smd-'..user..os.time()..mangled_name..attempt
  57. attempt = attempt + 1
  58. rc = os.execute(MDDIFF..' --mkfifo '..quote(pipe))
  59. until rc == 0 or attempt > 10
  60. if rc ~= 0 then
  61. log_internal_error_and_fail('unable to create fifo',
  62. "make_slave_filter_process")
  63. end
  64. filter.inf = io.popen(cmd(quote(pipe)),'r')
  65. filter.outf = io.open(pipe,'w')
  66. filter.pipe = pipe
  67. end
  68. end
  69. return setmetatable({}, {
  70. __index = {
  71. read = function(filter,...)
  72. if filter.inf == nil then
  73. -- check already initialized
  74. log_internal_error_and_fail("read called before write",
  75. "make_slave_filter_process")
  76. end
  77. -- once we known the channel is open, we clean up the fifo
  78. if not filter.removed and filter.did_write then
  79. filter.removed = true
  80. local rc = { filter.inf:read(...) }
  81. os.remove(filter.pipe)
  82. return unpack(rc)
  83. else
  84. return filter.inf:read(...)
  85. end
  86. end,
  87. write = function(filter,...)
  88. init(filter)
  89. filter.did_write = true
  90. return filter.outf:write(...)
  91. end,
  92. flush = function(filter)
  93. return filter.outf:flush()
  94. end,
  95. lines = function(filter)
  96. return filter.inf:lines()
  97. end
  98. }
  99. })
  100. end
  101. -- you should use logs_tags_and_fail
  102. function error(msg)
  103. local d = debug.getinfo(1,"nl")
  104. __error((d.name or '?')..': '..(d.currentline or '?')..
  105. ' :attempt to call error instead of log_tags_and_fail')
  106. end
  107. function log_tags_and_fail(msg,...)
  108. log_tags(...)
  109. __error({text=msg})
  110. end
  111. function log_internal_error_and_fail(msg,...)
  112. log_internal_error_tags(msg,...)
  113. __error({text=msg})
  114. end
  115. function set_verbose(v)
  116. verbose = v
  117. end
  118. function set_dry_run(v)
  119. dryrun = v
  120. if v then set_verbose(v) end
  121. end
  122. function dry_run() return dryrun end
  123. function set_translator(p)
  124. local translator_filter = make_slave_filter_process(function(pipe)
  125. return p .. ' < ' .. pipe
  126. end, "translate")
  127. if p == 'cat' then translator = function(x) return x end
  128. else translator = function(x)
  129. translator_filter:write(x..'\n')
  130. translator_filter:flush()
  131. local rc = translator_filter:read('*l')
  132. if rc == nil or rc == 'ERROR' then
  133. log_error("Translator "..p.." on input "..x.." gave an error")
  134. for l in translator_filter:lines() do log_error(l) end
  135. log_tags_and_fail('Unable to translate mailbox',
  136. 'translate','bad-translator',true)
  137. end
  138. if rc:match('%.%.') then
  139. log_error("Translator "..p.." on input "..x..
  140. " returned a path containing ..")
  141. log_tags_and_fail('Translator returned a path containing ..',
  142. 'translate','bad-translator',true)
  143. end
  144. return rc end
  145. end
  146. end
  147. function is_translator_set() return translator ~= false end
  148. function translate(x)
  149. if is_translator_set() then return translator(x) else return x end
  150. end
  151. function log(msg)
  152. if verbose then
  153. io.stderr:write('INFO: ',msg,'\n')
  154. end
  155. end
  156. function log_error(msg)
  157. io.stderr:write('ERROR: ',msg,'\n')
  158. end
  159. function log_tag(tag)
  160. io.stderr:write('TAGS: ',tag,'\n')
  161. end
  162. function log_progress(msg)
  163. if verbose then
  164. for l in msg:gmatch('\t*([^\n]+)') do
  165. io.stderr:write('PROGRESS: ',l,'\n')
  166. end
  167. end
  168. end
  169. -- this function shoud be used only by smd-client leaves
  170. function log_tags(context, cause, human, ...)
  171. cause = cause or 'unknown'
  172. context = context or 'unknown'
  173. if human then human = "necessary" else human = "avoidable" end
  174. local suggestions = {}
  175. local suggestions_string = ""
  176. if select('#',...) > 0 then
  177. suggestions_string =
  178. "suggested-actions("..table.concat({...}," ")..")"
  179. else
  180. suggestions_string = ""
  181. end
  182. log_tag("error::context("..context..") "..
  183. "probable-cause("..cause..") "..
  184. "human-intervention("..human..") ".. suggestions_string)
  185. end
  186. -- ======================== data transmission protocol ======================
  187. function transmit(out, path, what)
  188. what = what or "all"
  189. local f, err = io.open(path,"r")
  190. if not f then
  191. log_error("Unable to open "..path..": "..(err or "no error"))
  192. log_error("The problem should be transient, please retry.")
  193. log_tags_and_fail('Unable to open requested file.',
  194. "transmit", "simultaneous-mailbox-edit",false,"retry")
  195. end
  196. local size, err = f:seek("end")
  197. if not size then
  198. log_error("Unable to calculate the size of "..path)
  199. log_error("If it is not a regular file, please move it away.")
  200. log_error("If it is a regular file, please report the problem.")
  201. log_tags_and_fail('Unable to calculate the size of the requested file.',
  202. "transmit", "non-regular-file",true,
  203. mk_act('permission', path))
  204. end
  205. f:seek("set")
  206. if what == "header" then
  207. local line
  208. local header = {}
  209. size = 0
  210. while line ~= "" do
  211. line = assert(f:read("*l"))
  212. header[#header+1] = line
  213. header[#header+1] = "\n"
  214. size = size + 1 + string.len(line)
  215. end
  216. f:close()
  217. out:write("chunk " .. size .. "\n")
  218. out:write(unpack(header))
  219. out:flush()
  220. return
  221. end
  222. if what == "body" then
  223. local line
  224. while line ~= "" do
  225. line = assert(f:read("*l"))
  226. size = size -1 -string.len(line)
  227. end
  228. end
  229. out:write("chunk " .. size .. "\n")
  230. while true do
  231. local data = f:read(16384)
  232. if data == nil then break end
  233. out:write(data)
  234. end
  235. out:flush()
  236. f:close()
  237. end
  238. function receive(inf,outfile)
  239. local outf = io.open(outfile,"w")
  240. if not outf then
  241. log_error("Unable to open "..outfile.." for writing.")
  242. log_error('It may be caused by bad directory permissions, '..
  243. 'please check.')
  244. log_tags_and_fail("Unable to write incoming data",
  245. "receive", "non-writeable-file",true,
  246. mk_act('permission', outfile))
  247. end
  248. local line = inf:read("*l")
  249. if line == nil or line == "ABORT" then
  250. log_error("Data transmission failed.")
  251. log_error("This problem is transient, please retry.")
  252. log_tags_and_fail('server sent ABORT or connection died',
  253. "receive","network",false,"retry")
  254. end
  255. local len = tonumber(line:match('^chunk (%d+)'))
  256. local total = len
  257. while len > 0 do
  258. local next_chunk = 16384
  259. if len < next_chunk then next_chunk = len end
  260. local data = inf:read(next_chunk)
  261. if data == nil then
  262. log_error("Data transmission failed.")
  263. log_error("This problem is transient, please retry.")
  264. log_tags_and_fail('connection died',
  265. "receive","network",false,"retry")
  266. end
  267. len = len - data:len()
  268. outf:write(data)
  269. end
  270. outf:close()
  271. return total
  272. end
  273. function handshake(dbfile)
  274. -- send the protocol version and the dbfile sha1 sum
  275. io.write('protocol ',PROTOCOL_VERSION,'\n')
  276. -- if true the db file is deleted after SHA1 computation
  277. local kill_db_file_ASAP = false
  278. -- if the db file was not there and --dry-run, we schedule its deletion
  279. if dry_run() and not exists(dbfile) then kill_db_file_ASAP = true end
  280. -- we must have at least an empty file to compute its SHA1 sum
  281. touch(dbfile)
  282. local inf = io.popen(MDDIFF..' --sha1sum '.. quote(dbfile),'r')
  283. local db_sha, errmsg = inf:read('*a'):match('^(%S+)(.*)$')
  284. inf:close()
  285. if db_sha == 'ERROR' then
  286. log_internal_error_and_fail('unreadable db file: '.. quote(dbfile),'handshake')
  287. end
  288. io.write('dbfile ',db_sha,'\n')
  289. io.flush()
  290. -- but if the file was not there and --dry-run, we should not create it
  291. if kill_db_file_ASAP then os.remove(dbfile) end
  292. -- check protocol version and dbfile sha
  293. local line = io.read('*l')
  294. if line == nil then
  295. log_error("Network error.")
  296. log_error("Unable to get any data from the other endpoint.")
  297. log_error("This problem may be transient, please retry.")
  298. log_error("Hint: did you correctly setup the SERVERNAME variable")
  299. log_error("on your client? Did you add an entry for it in your ssh")
  300. log_error("configuration file?")
  301. log_tags_and_fail('Network error',"handshake", "network",false,"retry")
  302. end
  303. local protocol = line:match('^protocol (.+)$')
  304. if protocol ~= PROTOCOL_VERSION then
  305. log_error('Wrong protocol version.')
  306. log_error('The same version of syncmaildir must be used on '..
  307. 'both endpoints')
  308. log_tags_and_fail('Protocol version mismatch',
  309. "handshake", "protocol-mismatch",true)
  310. end
  311. line = io.read('*l')
  312. if line == nil then
  313. log_error "The client disconnected during handshake"
  314. log_tags_and_fail('Network error',"handshake", "network",false,"retry")
  315. end
  316. local sha = line:match('^dbfile (%S+)$')
  317. if sha ~= db_sha then
  318. log_error('Local dbfile and remote db file differ.')
  319. log_error('Remove both files and push/pull again.')
  320. log_tags_and_fail('Database mismatch',
  321. "handshake", "db-mismatch",true, mk_act('rm',dbfile))
  322. end
  323. end
  324. function dbfile_name(endpoint, mailboxes)
  325. local HOME = os.getenv('HOME')
  326. os.execute(MDDIFF..' --mkdir-p '..quote(HOME..'/.smd/'))
  327. local dbfile = HOME..'/.smd/' ..endpoint:gsub('/$',''):gsub('/','_').. '__'
  328. ..table.concat(mailboxes,'__'):gsub('/$',''):gsub('[/%%]','_')..
  329. '.db.txt'
  330. return dbfile
  331. end
  332. -- =================== fast/maildir aware mkdir -p ==========================
  333. local mddiff_mkdirln_handler = make_slave_filter_process(function(pipe)
  334. return MDDIFF .. ' -s ' .. pipe
  335. end, "mk_link_wa")
  336. -- create a link from the workarea to the real mailbox using mddiff
  337. function mk_link_wa(src, target)
  338. mddiff_mkdirln_handler:write(src,'\n',target,'\n')
  339. mddiff_mkdirln_handler:flush()
  340. local data = mddiff_mkdirln_handler:read('*l')
  341. if data:match('^ERROR') or not data:match('^OK') then
  342. log_tags_and_fail('Failed to mddiff -s',
  343. 'mddiff-s','wrong-permissions',true)
  344. end
  345. end
  346. local mkdir_p_cache = {}
  347. -- function to create the dir calling the real mkdir command
  348. -- pieces is a list components of the patch, they are concatenated
  349. -- separated by '/' and if absolute is true prefixed by '/'
  350. function make_dir_aux(absolute, pieces)
  351. local root = ""
  352. if absolute then root = '/' end
  353. local dir = root .. table.concat(pieces,'/')
  354. if not mkdir_p_cache[dir] then
  355. local rc = 0
  356. local last = pieces[#pieces]
  357. if is_translator_set() and not absolute and
  358. (last == 'cur' or last == 'new' or last == 'tmp')
  359. then
  360. local lfn = translate(dir)
  361. local abs_lfn = homefy(lfn)
  362. if not dry_run() then
  363. rc = os.execute(MDDIFF..' --mkdir-p '..quote(abs_lfn))
  364. end
  365. if dir ~= lfn then
  366. log('translating: '..dir..' -> '..lfn)
  367. end
  368. mk_link_wa(abs_lfn, dir)
  369. else
  370. if not dry_run() then
  371. rc = os.execute(MDDIFF..' --mkdir-p '..quote(dir))
  372. end
  373. end
  374. if rc ~= 0 then
  375. log_error("Unable to create directory "..dir)
  376. log_error('It may be caused by bad directory permissions, '..
  377. 'please check.')
  378. log_tags_and_fail("Directory creation failed",
  379. "mkdir", "wrong-permissions",true,
  380. mk_act('permission',dir))
  381. end
  382. mkdir_p_cache[dir] = true
  383. end
  384. end
  385. function tokenize_path(path)
  386. local t = {}
  387. local absolute = false
  388. local file = ""
  389. if string.byte(path,1) == string.byte('/',1) then absolute = true end
  390. -- tokenization
  391. for m in path:gmatch('([^/]+)') do t[#t+1] = m end
  392. -- strip last component if not ending with '/'
  393. if string.byte(path,string.len(path)) ~= string.byte('/',1) then
  394. file=t[#t]
  395. table.remove(t,#t)
  396. end
  397. return absolute, t, file
  398. end
  399. -- creates a directory that can contains a path, should be equivalent
  400. -- to mkdir -p `dirname path`. moreover, if the last component is 'tmp',
  401. -- 'cur' or 'new', they are all are created too. exampels:
  402. -- mkdir_p('/foo/bar') creates /foo
  403. -- mkdir_p('/foo/bar/') creates /foo/bar/
  404. -- mkdir_p('/foo/tmp/baz') creates /foo/tmp/, /foo/cur/ and /foo/new/
  405. function mkdir_p(path)
  406. local absolute, t, _ = tokenize_path(path)
  407. make_dir_aux(absolute, t)
  408. -- ensure new, tmp and cur are there
  409. local todo = { ["new"] = true, ["cur"] = true, ["tmp"]=true }
  410. if todo[t[#t]] == true then
  411. todo[t[#t]] = nil
  412. for x, _ in pairs(todo) do
  413. t[#t] = x
  414. make_dir_aux(absolute, t)
  415. end
  416. end
  417. end
  418. -- ============== maildir aware tempfile name generator =====================
  419. -- complex function to generate a valid tempfile name for path, possibly using
  420. -- the tmp directory if a subdir of path is new or cur and use_tmp is true
  421. --
  422. -- we want something that changes, so we keep a local variable and increment it
  423. local smd_pid = 1
  424. function tmp_for(path,use_tmp)
  425. if use_tmp == nil then use_tmp = true end
  426. local t = {}
  427. local absolute = ""
  428. if string.byte(path,1) == string.byte('/',1) then absolute = '/' end
  429. for m in path:gmatch('([^/]+)') do t[#t+1] = m end
  430. local fname = t[#t]
  431. local time, pid, host, tags = fname:match('^(%d+)%.([%d_]+)%.([^:]+)(.*)$')
  432. time = time or os.date("%s")
  433. pid = pid or smd_pid
  434. smd_pid = smd_pid + 1
  435. host = host or "localhost"
  436. tags = tags or ""
  437. table.remove(t,#t)
  438. local i, found = 0, false
  439. if use_tmp then
  440. for i=#t,1,-1 do
  441. if t[i] == 'cur' or t[i] == 'new' then
  442. t[i] = 'tmp'
  443. found = true
  444. break
  445. end
  446. end
  447. end
  448. make_dir_aux(absolute == '/', t)
  449. local newpath
  450. if not found then
  451. time = os.date("%s")
  452. t[#t+1] = time..'.'..pid..'.'..host..tags
  453. else
  454. t[#t+1] = fname
  455. end
  456. newpath = absolute .. table.concat(t,'/')
  457. local attempts = 0
  458. while exists(newpath) do
  459. if attempts > 10 then
  460. log_internal_error_and_fail('unable to generate a fresh tmp name: last attempt was '..newpath,
  461. "tmp_for")
  462. else
  463. time = os.date("%s")
  464. host = host .. 'x'
  465. t[#t] = time..'.'..pid..'.'..host..tags
  466. newpath = absolute .. table.concat(t,'/')
  467. attempts = attempts + 1
  468. end
  469. end
  470. return newpath
  471. end
  472. -- =========================== misc helpers =================================
  473. -- like s:match(spec) but chencks no captures are nil
  474. function parse(s,spec)
  475. local res = {s:match(spec)}
  476. local _,expected = spec:gsub('%b()','')
  477. if #res ~= expected then
  478. log_internal_error_and_fail('Error parsing "'..s..'"', "protocol")
  479. end
  480. return unpack(res)
  481. end
  482. local mddiff_sha_handler = make_slave_filter_process(function(pipe)
  483. return MDDIFF .. ' ' .. pipe
  484. end, "sha_file")
  485. function sha_file(name)
  486. mddiff_sha_handler:write(name,'\n')
  487. mddiff_sha_handler:flush()
  488. local data = mddiff_sha_handler:read('*l')
  489. if data:match('^ERROR') then
  490. log_tags_and_fail("Failed to sha1 message: "..(name or "nil"),
  491. 'sha_file','modify-while-update',false,'retry')
  492. end
  493. local hsha, bsha = data:match('(%S+) (%S+)')
  494. if hsha == nil or bsha == nil then
  495. log_internal_error_and_fail('mddiff incorrect behaviour', "mddiff")
  496. end
  497. return hsha, bsha
  498. end
  499. function exists(name)
  500. local f = io.open(name,'r')
  501. if f ~= nil then
  502. f:close()
  503. return true
  504. else
  505. return false
  506. end
  507. end
  508. function exists_and_sha(name)
  509. if exists(name) then
  510. local h, b = sha_file(name)
  511. return true, h, b
  512. else
  513. return false
  514. end
  515. end
  516. function cp(src,tgt)
  517. local s,err = io.open(src,'r')
  518. if not s then return 1, err end
  519. local t,err = io.open(tgt,'w+')
  520. if not t then return 1, err end
  521. local data
  522. repeat
  523. data = s:read(4096)
  524. if data then t:write(data) end
  525. until data == nil
  526. t:close()
  527. s:close()
  528. return 0
  529. end
  530. function touch(f)
  531. local h = io.open(f,'r')
  532. if h == nil then
  533. h = io.open(f,'w')
  534. if h == nil then
  535. log_error('Unable to touch '..quote(f))
  536. log_tags_and_fail("Unable to touch a file",
  537. "touch","bad-permissions",true,
  538. mk_act('permission', f))
  539. else
  540. h:close()
  541. end
  542. else
  543. h:close()
  544. end
  545. end
  546. function quote(s)
  547. return '"' .. s:gsub('"','\\"'):gsub("%)","\\)").. '"'
  548. end
  549. function homefy(s)
  550. if string.byte(s,1) == string.byte('/',1) then
  551. return s
  552. else
  553. return os.getenv('HOME')..'/'..s
  554. end
  555. end
  556. function mk_act(kind, name)
  557. local homefy = function(x)
  558. if is_translator_set() and string.byte(x,1) ~= string.byte('/',1) then
  559. return homefy(".smd/workarea/"..x)
  560. else
  561. return homefy(x)
  562. end
  563. end
  564. if kind == "display" then
  565. return "display-mail("..quote(homefy(name))..")"
  566. elseif kind == "rm" then
  567. return "run(rm "..quote(homefy(name))..")"
  568. elseif kind == "mv" then
  569. return "run(mv -n "..quote(homefy(name)).." "..
  570. quote(tmp_for(homefy(name),true)) ..")"
  571. elseif kind == "permission" then
  572. return "display-permissions("..quote(homefy(name))..")"
  573. else
  574. return kind .. (name or '')
  575. end
  576. end
  577. function assert_exists(name)
  578. local name = name:match('^([^ ]+)')
  579. local rc = os.execute('type '..name..' >/dev/null 2>&1')
  580. assert(rc == 0,'Not found: "'..name..'"')
  581. end
  582. -- a bit brutal, but correct
  583. function url_quote(txt)
  584. return string.gsub(txt,'.',
  585. function(x) return string.format("%%%02X",string.byte(x)) end)
  586. end
  587. -- the one used by mddiff
  588. function url_decode(s)
  589. return string.gsub(s,'%%([0-9A-Za-z][0-9A-Za-z])',
  590. function(x) return string.char(tonumber(x,16)) end)
  591. end
  592. function url_encode(s)
  593. return string.gsub(s,'[%% ]',
  594. function(x) return string.format("%%%2X",string.byte(x)) end)
  595. end
  596. function log_internal_error_tags(msg,ctx)
  597. log_tags("internal-error",ctx,true,
  598. 'run(gnome-open "mailto:'..BUGREPORT_ADDRESS..'?'..
  599. 'subject='..url_quote("[smd-bug] internal error")..'&'..
  600. 'body='..url_quote(
  601. 'This email reports an internal error, '..
  602. 'something that should never happen.\n'..
  603. 'To help the developers to find and solve the issue, please '..
  604. 'send this email.\n'..
  605. 'If you are able to reproduce the bug, please attach a '..
  606. 'detailed description\n'..
  607. 'of what you do to help the developers to experience the '..
  608. 'same malfunctioning.'..
  609. '\n\n'..
  610. 'smd-version: '..SMDVERSION..'\n'..
  611. 'error-message: '..tostring(msg)..'\n'..
  612. 'backtrace:\n'..debug.traceback()
  613. )..'")')
  614. end
  615. -- parachute
  616. function parachute(f,rc)
  617. xpcall(f,function(msg)
  618. if type(msg) == "table" then
  619. log_error(tostring(msg.text))
  620. else
  621. log_internal_error_tags("unknown","unknown")
  622. log_error(tostring(msg))
  623. log_error(debug.traceback())
  624. end
  625. os.exit(rc)
  626. end)
  627. end
  628. -- prints the stack trace. idiom is 'return(trance(x))' so that
  629. -- we have in the log the path for the leaf that computed x
  630. function trace(x)
  631. if verbose then
  632. local t = {}
  633. local n = 2
  634. while true do
  635. local d = debug.getinfo(n,"nl")
  636. if not d or not d.name then break end
  637. t[#t+1] = d.name ..":".. (d.currentline or "?")
  638. n=n+1
  639. end
  640. io.stderr:write('TRACE: ',table.concat(t," | "),'\n')
  641. end
  642. return x
  643. end
  644. -- strict access to the global environment
  645. function set_strict()
  646. setmetatable(__G,{
  647. __newindex = function (t,k,v)
  648. local d = debug.getinfo(2,"nl")
  649. __error((d.name or '?')..': '..(d.currentline or '?')..
  650. ' :attempt to create new global '..k)
  651. end;
  652. __index = function(t,k)
  653. local d = debug.getinfo(2,"nl")
  654. __error((d.name or '?')..': '..(d.currentline or '?')..
  655. ' :attempt to read undefined global '..k)
  656. end;
  657. })
  658. end
  659. -- vim:set ts=4: