File: syncmaildir.lua

package info (click to toggle)
syncmaildir 1.2.5-1
  • links: PTS, VCS
  • area: main
  • in suites: wheezy
  • size: 1,104 kB
  • sloc: ansic: 6,643; sh: 1,780; makefile: 247
file content (734 lines) | stat: -rw-r--r-- 19,838 bytes parent folder | download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
-- Released under the terms of GPLv3 or at your option any later version.
-- No warranties.
-- Copyright Enrico Tassi <gares@fettunta.org>
--
-- common code for smd-client/server

local PROTOCOL_VERSION="1.1"

local verbose = false
local dryrun = false
local translator = false

local PREFIX = '@PREFIX@'
local BUGREPORT_ADDRESS = 'syncmaildir-users@lists.sourceforge.net'

local __G = _G
local __error = _G.error

module('syncmaildir',package.seeall)

-- set mddiff path
MDDIFF = ""
if string.sub(PREFIX,1,1) == '@' then
		MDDIFF = './mddiff'
		io.stderr:write('smd-client not installed, assuming mddiff is: ',
			MDDIFF,'\n')
else
		MDDIFF = PREFIX .. '/bin/mddiff'
end

-- set xdelta executable name
XDELTA = '@XDELTA@'
if string.sub(XDELTA,1,1) == '@' then
		XDELTA = 'xdelta'
end

-- set smd version 
SMDVERSION = '@SMDVERSION@'
if string.sub(SMDVERSION,1,1) == '@' then
		SMDVERSION = '0.0.0'
end

-- to call external filter processes without too much pain
function make_slave_filter_process(cmd, seed)
	seed = seed or "no seed"
	local init = function(filter)
		if filter.inf == nil then
			local rc
			local base_dir
			local home = os.getenv('HOME')
			local user = os.getenv('USER') or 'nobody'
			local mangled_name = string.gsub(seed,"[ %./]",'-')
			local attempt = 0
			if home ~= nil then
				base_dir = home ..'/.smd/fifo/'
			else
				base_dir = '/tmp/'
			end
			rc = os.execute(MDDIFF..' --mkdir-p '..quote(base_dir))
			if rc ~= 0 then
				log_internal_error_and_fail('unable to create directory',
					"make_slave_filter_process")
			end
			repeat 
				pipe = base_dir..'smd-'..user..os.time()..mangled_name..attempt
				attempt = attempt + 1
				rc = os.execute(MDDIFF..' --mkfifo '..quote(pipe))
			until rc == 0 or attempt > 10
			if rc ~= 0 then
				log_internal_error_and_fail('unable to create fifo',
					"make_slave_filter_process")
			end
			filter.inf = io.popen(cmd(quote(pipe)),'r')
			filter.outf = io.open(pipe,'w')
			filter.pipe = pipe
		end
	end
	return setmetatable({}, {
		__index = {
			read = function(filter,...)
				if filter.inf == nil then
					-- check already initialized
					log_internal_error_and_fail("read called before write",
						"make_slave_filter_process")
				end
				-- once we known the channel is open, we clean up the fifo
				if not filter.removed and filter.did_write then
					filter.removed = true
					local rc = { filter.inf:read(...) }
					os.remove(filter.pipe)
					return unpack(rc)
				else
					return filter.inf:read(...)
				end
			end,
			write = function(filter,...)
				init(filter)
				filter.did_write = true
				return filter.outf:write(...)
			end,
			flush = function(filter)
				return filter.outf:flush()
			end,
			lines = function(filter)
				return filter.inf:lines()
			end
		}
	})
end

-- you should use logs_tags_and_fail
function error(msg)
	local d = debug.getinfo(1,"nl")
	__error((d.name or '?')..': '..(d.currentline or '?')..
	' :attempt to call error instead of log_tags_and_fail')
end

function log_tags_and_fail(msg,...)
	log_tags(...)
	__error({text=msg})
end

function log_internal_error_and_fail(msg,...)
	log_internal_error_tags(msg,...)
	__error({text=msg})
end

function set_verbose(v)
	verbose = v
end

function set_dry_run(v)
	dryrun = v
	if v then set_verbose(v) end
end

function dry_run() return dryrun end

function set_translator(p)
	local translator_filter = make_slave_filter_process(function(pipe)
		return p .. ' < ' .. pipe
	end, "translate")
	if p == 'cat' then translator = function(x) return x end
	else translator = function(x)
		translator_filter:write(x..'\n')
		translator_filter:flush()
		local rc = translator_filter:read('*l')
		if rc == nil or rc == 'ERROR' then
			log_error("Translator "..p.." on input "..x.." gave an error")
			for l in translator_filter:lines() do log_error(l) end
			log_tags_and_fail('Unable to translate mailbox',
				'translate','bad-translator',true)
		end
		if rc:match('%.%.') then
			log_error("Translator "..p.." on input "..x..
				" returned a path containing ..")
			log_tags_and_fail('Translator returned a path containing ..',
				'translate','bad-translator',true)
		end
		return rc end
	end
end

function is_translator_set() return translator ~= false end

function translate(x)
	if is_translator_set() then return translator(x) else return x end
end

function log(msg)
	if verbose then
		io.stderr:write('INFO: ',msg,'\n')
	end
end

function log_error(msg)
	io.stderr:write('ERROR: ',msg,'\n')
end

function log_tag(tag)
	io.stderr:write('TAGS: ',tag,'\n')
end

function log_progress(msg)
	if verbose then
		for l in msg:gmatch('\t*([^\n]+)') do
			io.stderr:write('PROGRESS: ',l,'\n')
		end
	end
end

-- this function shoud be used only by smd-client leaves
function log_tags(context, cause, human, ...)
	cause = cause or 'unknown'
	context = context or 'unknown'
	if human then human = "necessary" else human = "avoidable" end
	local suggestions = {}
	local suggestions_string = ""
	if select('#',...) > 0 then 
			suggestions_string = 
				"suggested-actions("..table.concat({...}," ")..")"
	else 
			suggestions_string = "" 
	end
	log_tag("error::context("..context..") "..
		"probable-cause("..cause..") "..
		"human-intervention("..human..") ".. suggestions_string)
end

-- ======================== data transmission protocol ======================

function transmit(out, path, what)
	what = what or "all"
	local f, err = io.open(path,"r")
	if not f then
		log_error("Unable to open "..path..": "..(err or "no error"))
		log_error("The problem should be transient, please retry.")
		log_tags_and_fail('Unable to open requested file.',
			"transmit", "simultaneous-mailbox-edit",false,"retry")
	end
	local size, err = f:seek("end")
	if not size then
		log_error("Unable to calculate the size of "..path)
		log_error("If it is not a regular file, please move it away.")
		log_error("If it is a regular file, please report the problem.")
		log_tags_and_fail('Unable to calculate the size of the requested file.',
			"transmit", "non-regular-file",true,
			mk_act('permission', path))
	end
	f:seek("set")

	if what == "header" then
		local line
		local header = {}
		size = 0
		while line ~= "" do
			line = assert(f:read("*l"))
			header[#header+1] = line
			header[#header+1] = "\n"
			size = size + 1 + string.len(line)
		end
		f:close()
		out:write("chunk " .. size .. "\n")
		out:write(unpack(header))
		out:flush()
		return
	end

	if what == "body" then
		local line
		while line ~= "" do
			line = assert(f:read("*l"))
			size = size -1 -string.len(line)
		end
	end

	out:write("chunk " .. size .. "\n")
	while true do
		local data = f:read(16384)
		if data == nil then break end
		out:write(data)
	end
	out:flush()

	f:close()
end

function receive(inf,outfile)
	local outf = io.open(outfile,"w")
	if not outf then
			log_error("Unable to open "..outfile.." for writing.")
			log_error('It may be caused by bad directory permissions, '..
				'please check.')
			log_tags_and_fail("Unable to write incoming data",
				"receive", "non-writeable-file",true,
				mk_act('permission', outfile))
	end

	local line = inf:read("*l")
	if line == nil or line == "ABORT" then
		log_error("Data transmission failed.")
		log_error("This problem is transient, please retry.")
		log_tags_and_fail('server sent ABORT or connection died',
			"receive","network",false,"retry")
	end
	local len = tonumber(line:match('^chunk (%d+)'))
	local total = len
	while len > 0 do
		local next_chunk = 16384
		if len < next_chunk then next_chunk = len end
		local data = inf:read(next_chunk)
		if data == nil then
			log_error("Data transmission failed.")
			log_error("This problem is transient, please retry.")
			log_tags_and_fail('connection died',
				"receive","network",false,"retry")
		end
		len = len - data:len()
		outf:write(data)
	end
	outf:close()
	return total
end

function handshake(dbfile)
	-- send the protocol version and the dbfile sha1 sum
	io.write('protocol ',PROTOCOL_VERSION,'\n')

	-- if true the db file is deleted after SHA1 computation
	local kill_db_file_ASAP = false

	-- if the db file was not there and --dry-run, we schedule its deletion
	if dry_run() and not exists(dbfile) then kill_db_file_ASAP = true end
	
	-- we must have at least an empty file to compute its SHA1 sum
	touch(dbfile)
	local inf = io.popen(MDDIFF..' --sha1sum '.. quote(dbfile),'r')
	
	local db_sha, errmsg = inf:read('*a'):match('^(%S+)(.*)$')
	inf:close()
	if db_sha == 'ERROR' then
		log_internal_error_and_fail('unreadable db file: '.. quote(dbfile),'handshake')
	end
	io.write('dbfile ',db_sha,'\n')
	io.flush()

	-- but if the file was not there and --dry-run, we should not create it
	if kill_db_file_ASAP then os.remove(dbfile) end

	-- check protocol version and dbfile sha
	local line = io.read('*l')
	if line == nil then
		log_error("Network error.")
		log_error("Unable to get any data from the other endpoint.")
		log_error("This problem may be transient, please retry.")
		log_error("Hint: did you correctly setup the SERVERNAME variable")
		log_error("on your client? Did you add an entry for it in your ssh")
		log_error("configuration file?")
		log_tags_and_fail('Network error',"handshake", "network",false,"retry")
	end
	local protocol = line:match('^protocol (.+)$')
	if protocol ~= PROTOCOL_VERSION then
		log_error('Wrong protocol version.')
		log_error('The same version of syncmaildir must be used on '..
			'both endpoints')
		log_tags_and_fail('Protocol version mismatch',
			"handshake", "protocol-mismatch",true)
	end
	line = io.read('*l')
	if line == nil then
		log_error "The client disconnected during handshake"
		log_tags_and_fail('Network error',"handshake", "network",false,"retry")
	end
	local sha = line:match('^dbfile (%S+)$')
	if sha ~= db_sha then
		log_error('Local dbfile and remote db file differ.')
		log_error('Remove both files and push/pull again.')
		log_tags_and_fail('Database mismatch',
			"handshake", "db-mismatch",true, mk_act('rm',dbfile))
	end
end

function dbfile_name(endpoint, mailboxes)
	local HOME = os.getenv('HOME')
	os.execute(MDDIFF..' --mkdir-p '..quote(HOME..'/.smd/'))
	local dbfile = HOME..'/.smd/' ..endpoint:gsub('/$',''):gsub('/','_').. '__' 
		..table.concat(mailboxes,'__'):gsub('/$',''):gsub('[/%%]','_')..
		'.db.txt'
	return dbfile
end

-- =================== fast/maildir aware mkdir -p ==========================

local mddiff_mkdirln_handler = make_slave_filter_process(function(pipe)
	return MDDIFF .. ' -s ' .. pipe	
end, "mk_link_wa")

-- create a link from the workarea to the real mailbox using mddiff
function mk_link_wa(src, target)
	mddiff_mkdirln_handler:write(src,'\n',target,'\n')
	mddiff_mkdirln_handler:flush()
	local data = mddiff_mkdirln_handler:read('*l')
	if data:match('^ERROR') or not data:match('^OK') then
		log_tags_and_fail('Failed to mddiff -s',
			'mddiff-s','wrong-permissions',true)
	end
end

local mkdir_p_cache = {}

-- function to create the dir calling the real mkdir command
-- pieces is a list components of the patch, they are concatenated
-- separated by '/' and if absolute is true prefixed by '/'
function make_dir_aux(absolute, pieces)
	local root = ""
	if absolute then root = '/' end
	local dir = root .. table.concat(pieces,'/')
	if not mkdir_p_cache[dir] then
		local rc = 0
		local last = pieces[#pieces]
		if is_translator_set() and not absolute and
		   (last == 'cur' or last == 'new' or last == 'tmp')
		then
			local lfn = translate(dir)
			local abs_lfn = homefy(lfn)
			if not dry_run() then
				rc = os.execute(MDDIFF..' --mkdir-p '..quote(abs_lfn))
			end
			if dir ~= lfn then
				log('translating: '..dir..' -> '..lfn)
			end
			mk_link_wa(abs_lfn, dir)
		else
			if not dry_run() then
				rc = os.execute(MDDIFF..' --mkdir-p '..quote(dir))
			end
		end
		if rc ~= 0 then
			log_error("Unable to create directory "..dir)
			log_error('It may be caused by bad directory permissions, '..
				'please check.')
			log_tags_and_fail("Directory creation failed",
				"mkdir", "wrong-permissions",true,
				mk_act('permission',dir))
		end
		mkdir_p_cache[dir] = true
	end
end

function tokenize_path(path)
	local t = {} 
	local absolute = false
	local file = ""

	if string.byte(path,1) == string.byte('/',1) then absolute = true end

	-- tokenization
	for m in path:gmatch('([^/]+)') do t[#t+1] = m end

	-- strip last component if not ending with '/'
	if string.byte(path,string.len(path)) ~= string.byte('/',1) then
		file=t[#t]
		table.remove(t,#t) 
	end

	return absolute, t, file
end

-- creates a directory that can contains a path, should be equivalent
-- to mkdir -p `dirname path`. moreover, if the last component is 'tmp',
-- 'cur' or 'new', they are all are created too. exampels:
--  mkdir_p('/foo/bar')     creates /foo
--  mkdir_p('/foo/bar/')    creates /foo/bar/
--  mkdir_p('/foo/tmp/baz') creates /foo/tmp/, /foo/cur/ and /foo/new/
function mkdir_p(path)
	local absolute, t, _ = tokenize_path(path)

	make_dir_aux(absolute, t)

	--  ensure new, tmp and cur are there
	local todo = { ["new"] = true, ["cur"] = true, ["tmp"]=true }
	if todo[t[#t]] == true then
		todo[t[#t]] = nil
		for x, _ in pairs(todo) do
			t[#t] = x
			make_dir_aux(absolute, t)
		end
	end
end

-- ============== maildir aware tempfile name generator =====================

-- complex function to generate a valid tempfile name for path, possibly using
-- the tmp directory if a subdir of path is new or cur and use_tmp is true
--
-- we want something that changes, so we keep a local variable and increment it
local smd_pid = 1

function tmp_for(path,use_tmp)
	if use_tmp == nil then use_tmp = true end
	local t = {} 
	local absolute = ""
	if string.byte(path,1) == string.byte('/',1) then absolute = '/' end
	for m in path:gmatch('([^/]+)') do t[#t+1] = m end
	local fname = t[#t]
	local time, pid, host, tags = fname:match('^(%d+)%.([%d_]+)%.([^:]+)(.*)$')
	time = time or os.date("%s")
	pid = pid or smd_pid
	smd_pid = smd_pid + 1
	host = host or "localhost"
	tags = tags or ""
	table.remove(t,#t)
	local i, found = 0, false
	if use_tmp then
		for i=#t,1,-1 do
			if t[i] == 'cur' or t[i] == 'new' then 
				t[i] = 'tmp' 
				found = true
				break
			end
		end
	end
	make_dir_aux(absolute == '/', t)
	local newpath
	if not found then
		time = os.date("%s")
		t[#t+1] = time..'.'..pid..'.'..host..tags
	else
		t[#t+1] = fname
	end
	newpath = absolute .. table.concat(t,'/') 
	local attempts = 0
	while exists(newpath) do 
		if attempts > 10 then
			log_internal_error_and_fail('unable to generate a fresh tmp name: last attempt was '..newpath,
				"tmp_for")
		else 
			time = os.date("%s")
			host = host .. 'x'
			t[#t] = time..'.'..pid..'.'..host..tags
			newpath = absolute .. table.concat(t,'/') 
			attempts = attempts + 1
		end
	end
	return newpath
end

-- =========================== misc helpers =================================

-- like s:match(spec) but chencks no captures are nil
function parse(s,spec)
	local res = {s:match(spec)}
	local _,expected = spec:gsub('%b()','')
	if #res ~= expected then
		log_internal_error_and_fail('Error parsing "'..s..'"', "protocol")
	end
	return unpack(res)
end

local mddiff_sha_handler = make_slave_filter_process(function(pipe)
	return MDDIFF .. ' ' .. pipe
end, "sha_file")

function sha_file(name)
	mddiff_sha_handler:write(name,'\n')
	mddiff_sha_handler:flush()
	local data = mddiff_sha_handler:read('*l')
	if data:match('^ERROR') then
		log_tags_and_fail("Failed to sha1 message: "..(name or "nil"),
			'sha_file','modify-while-update',false,'retry')
	end
	local hsha, bsha = data:match('(%S+) (%S+)') 
	if hsha == nil or bsha == nil then
		log_internal_error_and_fail('mddiff incorrect behaviour', "mddiff")
	end
	return hsha, bsha
end

function exists(name)
	local f = io.open(name,'r')
	if f ~= nil then
		f:close()
		return true
	else
		return false		
	end
end

function exists_and_sha(name)
	if exists(name) then
		local h, b = sha_file(name)
		return true, h, b
	else
		return false
	end
end

function cp(src,tgt)
	local s,err = io.open(src,'r')
	if not s then return 1, err end
	local t,err = io.open(tgt,'w+')
	if not t then return 1, err end
	local data
	repeat
		data = s:read(4096)
		if data then t:write(data) end
	until data == nil
	t:close()
	s:close()
	return 0
end

function touch(f)
	local h = io.open(f,'r')
	if h == nil then
		h = io.open(f,'w')
		if h == nil then
			log_error('Unable to touch '..quote(f))
			log_tags_and_fail("Unable to touch a file",
				"touch","bad-permissions",true,
				mk_act('permission', f))
		else
			h:close()
		end
	else
		h:close()
	end
end

function quote(s)
	return '"' .. s:gsub('"','\\"'):gsub("%)","\\)").. '"'
end

function homefy(s)
	if string.byte(s,1) == string.byte('/',1) then
		return s
	else
		return os.getenv('HOME')..'/'..s
	end
end	

function mk_act(kind, name)
	local homefy = function(x)
		if is_translator_set() and string.byte(x,1) ~= string.byte('/',1) then
			return homefy(".smd/workarea/"..x)
		else
			return homefy(x)
		end
	end
	if kind == "display" then
		return "display-mail("..quote(homefy(name))..")"
	elseif kind == "rm" then
		return "run(rm "..quote(homefy(name))..")"
	elseif kind == "mv" then
		return "run(mv -n "..quote(homefy(name)).." "..
			quote(tmp_for(homefy(name),true)) ..")"
	elseif kind == "permission" then
		return "display-permissions("..quote(homefy(name))..")"
	else
		return kind .. (name or '')
	end
end

function assert_exists(name)
	local name = name:match('^([^ ]+)')
	local rc = os.execute('type '..name..' >/dev/null 2>&1')
	assert(rc == 0,'Not found: "'..name..'"')
end

-- a bit brutal, but correct
function url_quote(txt)
	return string.gsub(txt,'.',
		function(x) return string.format("%%%02X",string.byte(x)) end)
end

-- the one used by mddiff
function url_decode(s)
	return string.gsub(s,'%%([0-9A-Za-z][0-9A-Za-z])',
		function(x) return string.char(tonumber(x,16)) end)
end

function url_encode(s)
	return string.gsub(s,'[%% ]',
		function(x) return string.format("%%%2X",string.byte(x)) end)
end

function log_internal_error_tags(msg,ctx)
	log_tags("internal-error",ctx,true,
	'run(gnome-open "mailto:'..BUGREPORT_ADDRESS..'?'..
		'subject='..url_quote("[smd-bug] internal error")..'&'..
		'body='..url_quote(
			'This email reports an internal error, '..
			'something that should never happen.\n'..
			'To help the developers to find and solve the issue, please '..
			'send this email.\n'..
			'If you are able to reproduce the bug, please attach a '..
			'detailed description\n'..
			'of what you do to help the developers to experience the '..
			'same malfunctioning.'..
			'\n\n'..
			'smd-version: '..SMDVERSION..'\n'..
			'error-message: '..tostring(msg)..'\n'..
			'backtrace:\n'..debug.traceback()
		)..'")')
end

-- parachute
function parachute(f,rc)
	xpcall(f,function(msg)
		if type(msg) == "table" then
			log_error(tostring(msg.text))
		else
			log_internal_error_tags("unknown","unknown")
			log_error(tostring(msg))
			log_error(debug.traceback())
		end
		os.exit(rc)
	end)
end

-- prints the stack trace. idiom is 'return(trance(x))' so that
-- we have in the log the path for the leaf that computed x
function trace(x)
	if verbose then
		local t = {}
		local n = 2
		while true do
			local d = debug.getinfo(n,"nl")
			if not d or not d.name then break end
			t[#t+1] = d.name ..":".. (d.currentline or "?")
			n=n+1
		end
		io.stderr:write('TRACE: ',table.concat(t," | "),'\n')
	end
	return x
end

-- strict access to the global environment
function set_strict()
	setmetatable(__G,{
		__newindex = function (t,k,v)
			local d = debug.getinfo(2,"nl")
			__error((d.name or '?')..': '..(d.currentline or '?')..
				' :attempt to create new global '..k)
		end;
		__index = function(t,k)
			local d = debug.getinfo(2,"nl")
			__error((d.name or '?')..': '..(d.currentline or '?')..
				' :attempt to read undefined global '..k)
		end;
	})
end

-- vim:set ts=4: