1--[==========================================================================[
2 host.lua: VLC Lua interface command line host module
3--[==========================================================================[
4 Copyright (C) 2007-2012 the VideoLAN team
5 $Id$
6
7 Authors: Antoine Cellerier <dionoea at videolan dot org>
8
9 This program is free software; you can redistribute it and/or modify
10 it under the terms of the GNU General Public License as published by
11 the Free Software Foundation; either version 2 of the License, or
12 (at your option) any later version.
13
14 This program is distributed in the hope that it will be useful,
15 but WITHOUT ANY WARRANTY; without even the implied warranty of
16 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
17 GNU General Public License for more details.
18
19 You should have received a copy of the GNU General Public License
20 along with this program; if not, write to the Free Software
21 Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston MA 02110-1301, USA.
22--]==========================================================================]
23
24--[==========================================================================[
25Example use:
26
27    require "host"
28    h = host.host()
29
30    -- Bypass any authentication
31    function on_password( client )
32        client:switch_status( host.status.read )
33    end
34    h.status_callbacks[host.status.password] = on_password
35
36    h:listen( "localhost:4212" )
37    h:listen( "*console" )
38    --or h:listen( { "localhost:4212", "*console" } )
39
40    -- The main loop
41    while true do
42        -- accept new connections and select active clients
43        local write, read = h:accept_and_select()
44
45        -- handle clients in write mode
46        for _, client in pairs(write) do
47            client:send()
48            client.buffer = ""
49            client:switch_status( host.status.read )
50        end
51
52        -- handle clients in read mode
53        for _, client in pairs(read) do
54            local str = client:recv(1000)
55            if not str then break end
56            str = string.gsub(str,"\r?\n$","")
57            client.buffer = "Got `"..str.."'.\r\n"
58            client:switch_status( host.status.write )
59        end
60    end
61
62For complete examples see existing VLC Lua interface modules (ie cli.lua)
63--]==========================================================================]
64
65module("host",package.seeall)
66
67status = { init = 0, read = 1, write = 2, password = 3 }
68client_type = { net = 1, stdio = 2, fifo = 3, telnet = 4 }
69
70function is_flag_set(val, flag)
71    local bit = 65536
72    while bit > 1 do
73        val = val % bit
74        flag = flag % bit
75        bit = bit / 2
76        if val >= bit and flag >= bit then return true end
77    end
78    return false
79end
80
81function host()
82    -- private data
83    local clients = {}
84    local listeners = {}
85    local status_callbacks = {}
86
87    -- private methods
88    local function fd_client( client )
89        if client.status == status.read then
90            return client.rfd
91        else -- status.write
92            return client.wfd
93        end
94    end
95
96    local function send( client, data, len )
97        if len then
98            return vlc.net.send( client.wfd, data, len )
99        else
100            return vlc.net.send( client.wfd, data or client.buffer )
101        end
102    end
103
104    local function recv( client, len )
105        if len then
106            return vlc.net.recv( client.rfd, len )
107        else
108            return vlc.net.recv( client.rfd )
109        end
110    end
111
112    local function write( client, data )
113        return vlc.net.write( client.wfd, data or client.buffer )
114    end
115
116    local function read( client, len )
117        if len then
118            return vlc.net.read( client.rfd, len )
119        else
120            return vlc.net.read( client.rfd )
121        end
122    end
123
124    local function write_console( client, data )
125        -- FIXME: this method shouldn't be needed. vlc.net.write should
126        -- just work
127        vlc.win.console_write(data or client.buffer)
128        return string.len(data or client.buffer)
129    end
130
131    local function read_console( client, len )
132        -- Read stdin from a windows console (beware: select/poll doesn't work!)
133        return vlc.win.console_read()
134    end
135
136    local function del_client( client )
137        if not clients[client] then
138            vlc.msg.err("couldn't find client to remove.")
139            return
140        end
141
142        if client.type == client_type.stdio then
143            h:broadcast("Shutting down.\r\n")
144            vlc.msg.info("Requested shutdown.")
145            vlc.misc.quit()
146        elseif client.type == client_type.net
147        or client.type == client_type.telnet then
148            if client.wfd ~= client.rfd then
149                vlc.net.close( client.rfd )
150            end
151            vlc.net.close( client.wfd )
152        end
153        clients[client] = nil
154    end
155
156    local function switch_status( client, s )
157        if client.status == s then return end
158        client.status = s
159        if status_callbacks[s] then
160            status_callbacks[s]( client )
161        end
162    end
163
164    -- append a line to a client's (output) buffer
165    local function append( client, string )
166        client.buffer = client.buffer .. string .. "\r\n"
167    end
168
169    local function new_client( h, fd, wfd, t )
170        if fd < 0 then return end
171        local w, r
172        if t == client_type.net or t == client_type.telnet then
173            w = send
174            r = recv
175        elseif t == client_type.stdio or t == client_type.fifo then
176            if vlc.win and t == client_type.stdio then
177                vlc.win.console_init()
178                w = write_console
179                r = read_console
180            else
181                w = write
182                r = read
183            end
184        else
185            error("Unknown client type", t )
186        end
187
188        local client = { -- data
189                         rfd = fd,
190                         wfd = wfd or fd,
191                         status = status.init,
192                         buffer = "",
193                         cmds = "",
194                         type = t,
195                         -- methods
196                         fd = fd_client,
197                         send = w,
198                         recv = r,
199                         del = del_client,
200                         switch_status = switch_status,
201                         append = append,
202                       }
203        client:send( "VLC media player "..vlc.misc.version().."\n" )
204        clients[client] = client
205        client:switch_status(status.password)
206    end
207
208    -- public methods
209    local function _listen_tcp( h, host, port, telnet )
210        if listeners.tcp and listeners.tcp[host]
211                         and listeners.tcp[host][port] then
212            error("Already listening on tcp host `"..host..":"..tostring(port).."'")
213        end
214        if listeners.stdio and vlc.win then
215            error("Cannot listen on console and sockets concurrently on Windows")
216        end
217        if not listeners.tcp then
218            listeners.tcp = {}
219        end
220        if not listeners.tcp[host] then
221            listeners.tcp[host] = {}
222        end
223        listeners.tcp[host][port] = true
224        if not listeners.tcp.list then
225            -- FIXME: if host == "list" we'll have a problem
226            listeners.tcp.list = {}
227        end
228        local listener = vlc.net.listen_tcp( host, port )
229        local type = telnet and client_type.telnet or client_type.net;
230        table.insert( listeners.tcp.list, { data = listener,
231                                            type = type,
232                                          } )
233    end
234
235    local function _listen_stdio( h )
236        if listeners.stdio then
237            error("Already listening on stdio")
238        end
239        if listeners.tcp and vlc.win then
240            error("Cannot listen on console and sockets concurrently on Windows")
241        end
242        new_client( h, 0, 1, client_type.stdio )
243        listeners.stdio = true
244    end
245
246    local function _listen( h, url )
247        if type(url)==type({}) then
248            for _,u in pairs(url) do
249                h:listen( u )
250            end
251        else
252            vlc.msg.info( "Listening on host \""..url.."\"." )
253            if url == "*console" then
254                h:listen_stdio()
255            else
256                u = vlc.strings.url_parse( url )
257                if u.host == nil then
258                    u = vlc.strings.url_parse( "//" .. url )
259                end
260                h:listen_tcp( u.host, u.port, (u.protocol == "telnet") )
261            end
262        end
263    end
264
265    local function _accept_and_select( h )
266        local wclients = {}
267        local rclients = {}
268        if not (vlc.win and listeners.stdio) then
269            local function filter_client( fds, status, event )
270                for _, client in pairs(clients) do
271                    if client.status == status then
272                        fds[client:fd()] = event
273                    end
274                end
275            end
276
277            local pollfds = {}
278            filter_client( pollfds, status.read, vlc.net.POLLIN )
279            filter_client( pollfds, status.password, vlc.net.POLLIN )
280            filter_client( pollfds, status.write, vlc.net.POLLOUT )
281            if listeners.tcp then
282                for _, listener in pairs(listeners.tcp.list) do
283                    for _, fd in pairs({listener.data:fds()}) do
284                        pollfds[fd] = vlc.net.POLLIN
285                    end
286                end
287            end
288
289            local ret = vlc.net.poll( pollfds )
290            if ret > 0 then
291                for _, client in pairs(clients) do
292                    if is_flag_set(pollfds[client:fd()], vlc.net.POLLIN) then
293                        table.insert(rclients, client)
294                    elseif is_flag_set(pollfds[client:fd()], vlc.net.POLLERR)
295                    or is_flag_set(pollfds[client:fd()], vlc.net.POLLHUP)
296                    or is_flag_set(pollfds[client:fd()], vlc.net.POLLNVAL) then
297                        client:del()
298                    elseif is_flag_set(pollfds[client:fd()], vlc.net.POLLOUT) then
299                        table.insert(wclients, client)
300                    end
301                end
302                if listeners.tcp then
303                    for _, listener in pairs(listeners.tcp.list) do
304                        for _, fd in pairs({listener.data:fds()}) do
305                            if is_flag_set(pollfds[fd], vlc.net.POLLIN) then
306                                local afd = listener.data:accept()
307                                new_client( h, afd, afd, listener.type )
308                                break
309                            end
310                        end
311                    end
312                end
313            end
314        else
315            for _, client in pairs(clients) do
316                if client.type == client_type.stdio then
317                    if client.status == status.read or client.status == status.password then
318                        if vlc.win.console_wait(50) then
319                            table.insert(rclients, client)
320                        end
321                    else
322                        table.insert(wclients, client)
323                    end
324                end
325            end
326        end
327        return wclients, rclients
328    end
329
330    local function destructor( h )
331        for _,client in pairs(clients) do
332            if client.type ~= client_type.stdio then
333                client:del()
334            end
335        end
336    end
337
338    local function _broadcast( h, msg )
339        for _,client in pairs(clients) do
340            client:send( msg )
341        end
342    end
343
344    if setfenv then
345        -- We're running Lua 5.1
346        -- See http://lua-users.org/wiki/HiddenFeatures for more info.
347        local proxy = newproxy(true)
348        getmetatable(proxy).__gc = destructor
349        destructor = proxy
350    end
351
352    -- the instance
353    local h = setmetatable(
354              { -- data
355                status_callbacks = status_callbacks,
356                -- methods
357                listen = _listen,
358                listen_tcp = _listen_tcp,
359                listen_stdio = _listen_stdio,
360                accept_and_select = _accept_and_select,
361                broadcast = _broadcast,
362              },
363              { -- metatable
364                __gc = destructor, -- Should work in Lua 5.2 without the new proxytrick as __gc is also called on tables (needs to be tested)
365                __metatable = "",
366              })
367    return h
368end
369