source: trunk/lua/chdku.lua @ 38

Revision 38, 11.8 KB checked in by reyalP, 2 years ago (diff)

work-in-progress rlib code + cleanup

  • Property svn:eol-style set to native
Line 
1--[[
2lua helper functions for working with the chdk.* c api
3
4 Copyright (C) 2010-2011 <reyalp (at) gmail dot com>
5  This program is free software; you can redistribute it and/or modify
6  it under the terms of the GNU General Public License version 2 as
7  published by the Free Software Foundation.
8
9  This program is distributed in the hope that it will be useful,
10  but WITHOUT ANY WARRANTY; without even the implied warranty of
11  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12  GNU General Public License for more details.
13
14  You should have received a copy of the GNU General Public License
15  along with this program; if not, write to the Free Software
16  Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
17
18--]]
19local chdku={}
20-- format a script message in a human readable way
21function chdku.format_script_msg(msg)
22        if msg.type == 'none' then
23                return ''
24        end
25        local r=string.format("%d:%s:",msg.script_id,msg.type)
26        -- for user messages, type is clear from value, strings quoted, others not
27        if msg.type == 'user' or msg.type == 'return' then
28                if msg.subtype == 'boolean' or msg.subtype == 'integer' or msg.subtype == 'nil' then
29                        r = r .. tostring(msg.value)
30                elseif msg.subtype == 'string' then
31                        r = r .. string.format("'%s'",msg.value)
32                else
33                        r = r .. msg.subtype .. ':' .. tostring(msg.value)
34                end
35        elseif msg.type == 'error' then
36                r = r .. msg.subtype .. ':' .. tostring(msg.value)
37        end
38        return r
39end
40
41--[[
42chunks of source code to be used remotely
43can be used with chdku.exec
44TODO some of these are duplicated with local code, but we don't yet have an easy way of sharing them
45TODO would be good to minify
46TODO handle order and dependencies
47TODO passing compiled chunks might be better but our lua configurations are different
48]]
49chdku.rlib={
50--[[
51mostly duplicated from util.serialize
52global defaults can be changed from code
53]]
54        serialize=[[
55serialize_r = function(v,opts,seen,depth)
56        local vt = type(v)
57        if vt == 'nil' or  vt == 'boolean' or vt == 'number' then
58                return tostring(v)
59        elseif vt == 'string' then
60                return string.format('%q',v)
61        elseif vt == 'table' then
62                if not depth then
63                        depth = 1
64                end
65                if depth >= opts.maxdepth then
66                        error('serialize: max depth')
67                end
68                if not seen then
69                        seen={}
70                elseif seen[v] then
71                        if opts.err_cycle then
72                                error('serialize: cycle')
73                        else
74                                return '"cycle:'..tostring(v)..'"'
75                        end
76                end
77                seen[v] = true;
78                local r='{'
79                for k,v1 in pairs(v) do
80                        if opts.pretty then
81                                r = r .. '\n' ..  string.rep(' ',depth)
82                        end
83                        if type(k) == 'string' and string.match(k,'^[_%a][%a%d_]*$') then
84                                r = r .. tostring(k)
85                        else
86                                r = r .. '[' .. serialize_r(k,opts,seen,depth+1) .. ']'
87                        end
88                        r = r .. '=' .. serialize_r(v1,opts,seen,depth+1) .. ','
89                end
90                if opts.pretty then
91                        r = r .. '\n' .. string.rep(' ',depth-1)
92                end
93                r = r .. '}'
94                return r
95        elseif opts.err_type then
96                error('serialize: unsupported type ' .. vt, 2)
97        else
98                return '"'..tostring(v)..'"'
99        end
100end
101
102serialize_defaults = {
103        maxdepth=10,
104        err_type=true,
105        err_cycle=true,
106        pretty=false,
107}
108
109function serialize(v,opts)
110        if opts then
111                for k,v in pairs(serialize_defaults) do
112                        if not opts[k] then
113                                opts[k]=v
114                        end
115                end
116        else
117                opts=serialize_defaults
118        end
119        return serialize_r(v,opts)
120end
121
122]],
123-- override default table serialization for messages
124        serialize_msgs=[[
125        usb_msg_table_to_string=serialize
126]],
127--[[
128        status[,err]=dir_iter(path,func,opts)
129general purpose directory iterator
130interates over directory 'path', calling
131func(path,filename,opts.fdata) on each item
132func is called with a nil filename after listing is complete
133]]
134        dir_iter=[[
135function dir_iter(path,func,opts)
136        if not opts then
137                opts = {}
138        end
139               
140        local t,err=os.listdir(path,opts.listall)
141        if not t then
142                return false,err
143        end
144        for i,v in ipairs(t) do
145                local status,err=func(path,v,opts)
146                if not status then
147                        return status,err
148                end
149        end
150        return func(path,nil,opts)
151end
152]],
153--[[
154function to batch stuff in groups of messages
155b=msg_batcher{
156        batchsize=num, -- items per batch
157        timeout=num, -- message timeout
158}
159call
160b:write() adds items and sends when batch size is reached
161b:flush() sends any remaining items
162]]
163        msg_batch=[[
164function msg_batcher(opts_in)
165        local t = {
166                batchsize=50,
167                timeout=100000
168        }
169        if opts_in then
170                for k,v in pairs(opts_in) do
171                        t[k] = v
172                end
173        end
174        t.data = {}
175        t.n = 0
176        t.write=function(self,val)
177                self.n = self.n + 1
178                self.data[self.n] = val
179                if self.n >= self.batchsize then
180                        return self:flush()
181                end
182                return true
183        end
184        t.flush = function(self)
185                if self.n > 0 then
186                        if not write_usb_msg(self.data,self.timeout) then
187                                return false
188                        end
189                        self.data = {}
190                        self.n = 0
191                end
192                return true
193        end
194        return t
195end
196]],
197--[[
198retrieve a directory listing of names, batched in messages
199]]
200        ls_simple=[[
201function ls_simple(path)
202        local b=msg_batcher()
203        local t,err=os.listdir(path)
204        if not t then
205                return false,err
206        end
207        for i,v in ipairs(t) do
208                if not b:write(v) then
209                        return false
210                end
211        end
212        return b:flush()
213end
214]],
215--[[
216TODO rework this to a general iterate over directory function
217sends file listing as serialized tables with write_usb_msg
218returns true, or false,error message
219opts={
220        stat=bool|{table},
221        listall=bool,
222        msglimit=number,
223        match="pattern",
224}
225stat
226        false/nil, return an array of names without stating at all
227        '/' return array of names, with / appended to dirs
228        '*" return all stat fields
229        {table} return stat fields named in table (TODO not implemented)
230msglimit
231        maximum number of items to return in a message
232        each message will contain a table with partial results
233        default 50
234match
235        pattern, file names matching with string.match will be returned
236listall
237        passed as second arg to os.listdir
238
239may run out of memory on very large directories,
240msglimit can help but os.listdir itself could use all memory
241TODO message timeout is not checked
242TODO handle case if 'path' is a file
243]]
244        ls=[[
245function ls(path,opts_in)
246        local opts={
247                msglimit=50,
248                msgtimeout=100000,
249        }
250        if opts_in then
251                for k,v in pairs(opts_in) do
252                        opts[k]=v
253                end
254        end
255        local t,msg=os.listdir(path,opts.listall)
256        if not t then
257                return false,msg
258        end
259        local r={}
260        local count=1
261        for i,v in ipairs(t) do
262                if not opts.match or string.match(v,opts.match) then
263                        if opts.stat then
264                                local st,msg=os.stat(path..'/'..v)
265                                if not st then
266                                        return false,msg
267                                end
268                                if opts.stat == '/' then
269                                        if st.is_dir then
270                                                r[count]=v .. '/'
271                                        else
272                                                r[count]=v
273                                        end
274                                elseif opts.stat == '*' then
275                                        r[v]=st
276                                end
277                        else
278                                r[count]=t[i];
279                        end
280                        if count < opts.msglimit then
281                                count=count+1
282                        else
283                                write_usb_msg(r,opts.msgtimeout)
284                                r={}
285                                count=1
286                        end
287                end
288        end
289        if count > 1 then
290                write_usb_msg(r,opts.msgtimeout)
291        end
292        return true
293end
294]],
295}
296
297--[[
298opts may be a table, or a string containing lua code for a table
299return a list of remote directory contents
300return
301table|false,msg
302note may return an empty table if target is not a directory
303]]
304function chdku.listdir(path,opts) 
305        if type(opts) == 'table' then
306                opts = serialize(opts)
307        end
308        local results={}
309        local status,err=chdku.exec("return ls('"..path.."',"..opts..")",
310                {
311                        wait=true,
312                        libs={'serialize','serialize_msgs','ls'},
313                        msgs=function(msg)
314                                if msg.subtype ~= 'table' then
315                                        return false, 'unexpected message value'
316                                end
317                                local chunk,err=unserialize(msg.value)
318                                if err then
319                                        return false, err
320                                end
321                                for k,v in pairs(chunk) do
322                                        results[k] = v
323                                end
324                                return true
325                        end,
326                })
327        if not status then
328                return false,err
329        end
330
331        return results
332end
333
334--[[
335read pending messages and return error from current script, if available
336]]
337function chdku.get_error_msg()
338        while true do
339                local msg,err = chdk.read_msg()
340                if not msg then
341                        return false
342                end
343                if msg.type == 'none' then
344                        return false
345                end
346                if msg.type == 'error' and msg.script_id == chdk.get_script_id() then
347                        return msg.value
348                end
349                warnf("chdku.get_error_msg: ignoring message %s\n",chdku.format_script_msg(msg))
350        end
351end
352--[[
353wrapper for chdk.execlua, using optional code from rlibs
354status[,err]=chdku.exec("code",opts)
355opts {
356        libs={"rlib name1","rlib name2"...} -- rlib code to be prepended to "code"
357        wait=bool -- wait for script to complete, return values will be returned after status if true
358        -- below only apply if with wait
359        msgs={table|callback} -- table or function to receive user script messages
360        rets={table|callback} -- table or function to receive script return values, instead of returning them
361        fdata={any lua value} -- data to be passed as second argument to callbacks
362}
363callbacks
364        status[,err] = f(message,fdata)
365        processing continues if status is true, otherwise aborts and returns err
366]]
367function chdku.exec(code,opts_in)
368        local opts = extend_table({},opts_in)
369        if opts.libs then
370                local libcode=''
371                for k,v in ipairs(opts.libs) do
372                        if chdku.rlib[v] then
373                                libcode = libcode .. chdku.rlib[v];
374                        else
375                                return false,'unknown rlib'..v
376                        end
377                end
378                code = libcode .. code
379        end
380        local status,err=chdk.execlua(code)
381        if not status then
382                -- syntax error, try to fetch the error message
383                if err == 'syntax' then
384                        local msg = chdku.get_error_msg()
385                        if msg then
386                                return false,msg
387                        end
388                end
389                return false,err
390        end
391        if not opts.wait then
392                return true
393        end
394
395        -- to collect return values
396        -- first result is our status
397        local results = {true}
398        local i=2
399
400        while true do
401                status,err=chdku.wait_status{ msg=true, run=false }
402                if not status then
403                        return false,tostring(err)
404                end
405                if status.msg then
406                        local msg,err=chdk.read_msg()
407                        if not msg then
408                                return false, err
409                        end
410                        if msg.script_id ~= chdk.get_script_id() then
411                                warnf("chdku.exec: message from unexpected script %s\n",msg.script_id,chdku.format_script_msg(msg))
412                        elseif msg.type == 'user' then
413                                if type(opts.msgs) == 'function' then
414                                        local status,err = opts.msgs(msg,opts.fdata)
415                                        if not status then
416                                                return false,err
417                                        end
418                                elseif type(opts.msgs) == 'table' then
419                                        table.insert(opts.msgs,msg)
420                                else
421                                        warnf("chdku.exec: unexpected user message %s\n",chdku.format_script_msg(msg))
422                                end
423                        elseif msg.type == 'return' then
424                                if type(opts.rets) == 'function' then
425                                        local status,err = opts.rets(msg,opts.fdata)
426                                        if not status then
427                                                return false,err
428                                        end
429                                elseif type(opts.rets) == 'table' then
430                                        table.insert(opts.rets,msg)
431                                else
432                                        -- if serialize_msgs is not selected, table return values will be strings
433                                        if msg.subtype == 'table' and in_table(opts.libs,'serialize_msgs') then
434                                                results[i] = unserialize(msg.value)
435                                        else
436                                                results[i] = msg.value
437                                        end
438                                        i=i+1
439                                end
440                        elseif msg.type == 'error' then
441                                return false, msg.value
442                        else
443                                return false, 'unexpected message type'
444                        end
445                elseif status.run == false then
446                        if opts.rets then
447                                return true
448                        else
449                                return unpack(results,1,table.maxn(results)) -- maxn expression preserves nils
450                        end
451                end
452        end
453
454end
455
456--[[
457sleep until specified status is met
458status,errmsg=chdku.wait_status(opts)
459opts:
460{
461        -- bool values cause the function to return when the status matches the given value
462        -- if not set, status of that item is ignored
463        msg=bool
464        run=bool
465        timeout=<number> -- timeout in ms
466        poll=<number> -- polling interval in ms
467}
468status: table with msg and run set to last status, and timeout set if timeout expired, or false,errormessage on error
469TODO for gui, this should yield in lua, resume from timer or something
470]]
471function chdku.wait_status(opts)
472        local timeleft = opts.timeout
473        local sleeptime = opts.poll
474        if not timeleft then
475                timeleft=86400000 -- 1 day
476        end
477        if not sleeptime or sleeptime < 50 then
478                sleeptime=250
479        end
480        while true do
481                local status,msg = chdk.script_status()
482                if not status then
483                        return false,msg
484                end
485                if status.run == opts.run or status.msg == opts.msg then
486                        return status
487                end
488                if timeleft > 0 then
489                        if timeleft < sleeptime then
490                                sleeptime = timeleft
491                        end
492                        sys.sleep(sleeptime)
493                        timeleft =  timeleft - sleeptime
494                else
495                        status.timeout=true
496                        return status
497                end
498        end
499end
500return chdku
Note: See TracBrowser for help on using the repository browser.