]> git.sesse.net Git - vlc/blob - share/lua/intf/modules/host.lua
0d299cbfe0e33e46af81049002dabf89b4ca47ea
[vlc] / share / lua / intf / modules / host.lua
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 --[==========================================================================[
25 Example 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 not vlc.misc.should_die() 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
62 For complete examples see existing VLC Lua interface modules (ie cli.lua)
63 --]==========================================================================]
64
65 module("host",package.seeall)
66
67 status = { init = 0, read = 1, write = 2, password = 3 }
68 client_type = { net = 1, stdio = 2, fifo = 3, telnet = 4 }
69
70 function is_flag_set(val, flag)
71     return (((val - (val % flag)) / flag) % 2 ~= 0)
72 end
73
74 function host()
75     -- private data
76     local clients = {}
77     local listeners = {}
78     local status_callbacks = {}
79
80     -- private methods
81     local function fd_client( client )
82         if client.status == status.read then
83             return client.rfd
84         else -- status.write
85             return client.wfd
86         end
87     end
88
89     local function send( client, data, len )
90         if len then
91             return vlc.net.send( client.wfd, data, len )
92         else
93             return vlc.net.send( client.wfd, data or client.buffer )
94         end
95     end
96
97     local function recv( client, len )
98         if len then
99             return vlc.net.recv( client.rfd, len )
100         else
101             return vlc.net.recv( client.rfd )
102         end
103     end
104
105     local function write( client, data )
106         return vlc.net.write( client.wfd, data or client.buffer )
107     end
108
109     local function read( client, len )
110         if len then
111             return vlc.net.read( client.rfd, len )
112         else
113             return vlc.net.read( client.rfd )
114         end
115     end
116
117         local function write_console( client, data )
118                 -- FIXME: this method shouldn't be needed. vlc.net.write should just work
119                 io.write(data or client.buffer)
120                 return string.len(data or client.buffer)
121         end
122
123     local function read_console( client, len )
124         -- Read stdin from a windows console (beware: select/poll doesn't work!)
125         return vlc.win.console_read()
126     end
127
128     local function del_client( client, nostdioerror )
129         --client:send("Cleaning up.\r\n")
130         if client.type == client_type.stdio then
131             if not nostdioerror then
132                 client:send( "Cannot delete stdin/stdout client.\n" )
133             end
134         elseif clients[client] then
135             if client.type == client_type.net
136             or client.type == client_type.telnet then
137                 if client.wfd ~= client.rfd then
138                     vlc.net.close( client.rfd )
139                 end
140                 vlc.net.close( client.wfd )
141             end
142             clients[client] = nil
143         else
144             vlc.msg.err("couldn't find client to remove.")
145         end
146     end
147
148     local function switch_status( client, s )
149         if client.status == s then return end
150         client.status = s
151         if status_callbacks[s] then
152             status_callbacks[s]( client )
153         end
154     end
155
156     -- append a line to a client's (output) buffer
157     local function append( client, string )
158         client.buffer = client.buffer .. string .. "\r\n"
159     end
160
161     local function new_client( h, fd, wfd, t )
162         if fd < 0 then return end
163         local w, r
164         if t == client_type.net or t == client_type.telnet then
165             w = send
166             r = recv
167         else if t == client_type.stdio or t == client_type.fifo then
168             if vlc.win and t == client_type.stdio then
169                 vlc.win.console_init()
170                                 w = write_console
171                 r = read_console
172             else
173                                 w = write
174                 r = read
175             end
176         else
177             error("Unknown client type", t )
178         end end
179
180         local client = { -- data
181                          rfd = fd,
182                          wfd = wfd or fd,
183                          status = status.init,
184                          buffer = "",
185                          cmds = "",
186                          type = t,
187                          -- methods
188                          fd = fd_client,
189                          send = w,
190                          recv = r,
191                          del = del_client,
192                          switch_status = switch_status,
193                          append = append,
194                        }
195         client:send( "VLC media player "..vlc.misc.version().."\n" )
196         clients[client] = client
197         client:switch_status(status.password)
198     end
199
200     -- public methods
201     local function _listen_tcp( h, host, port, telnet )
202         if listeners.tcp and listeners.tcp[host]
203                          and listeners.tcp[host][port] then
204             error("Already listening on tcp host `"..host..":"..tostring(port).."'")
205         end
206         if listeners.stdio and vlc.win then
207             error("Cannot listen on console and sockets concurrently on Windows")
208         end
209         if not listeners.tcp then
210             listeners.tcp = {}
211         end
212         if not listeners.tcp[host] then
213             listeners.tcp[host] = {}
214         end
215         listeners.tcp[host][port] = true
216         if not listeners.tcp.list then
217             -- FIXME: if host == "list" we'll have a problem
218             listeners.tcp.list = {}
219         end
220         local listener = vlc.net.listen_tcp( host, port )
221         local type = telnet and client_type.telnet or client_type.net;
222         table.insert( listeners.tcp.list, { data = listener,
223                                             type = type,
224                                           } )
225     end
226
227     local function _listen_stdio( h )
228         if listeners.stdio then
229             error("Already listening on stdio")
230         end
231         if listeners.tcp and vlc.win then
232             error("Cannot listen on console and sockets concurrently on Windows")
233         end
234         new_client( h, 0, 1, client_type.stdio )
235         listeners.stdio = true
236     end
237
238     local function _listen( h, url )
239         if type(url)==type({}) then
240             for _,u in pairs(url) do
241                 h:listen( u )
242             end
243         else
244             vlc.msg.info( "Listening on host \""..url.."\"." )
245             if url == "*console" then
246                 h:listen_stdio()
247             else
248                 u = vlc.net.url_parse( url )
249                 h:listen_tcp( u.host, u.port, (u.protocol == "telnet") )
250             end
251         end
252     end
253
254     local function _accept_and_select( h )
255         local wclients = {}
256         local rclients = {}
257         if not (vlc.win and listeners.stdio) then
258             local function filter_client( fds, status, event )
259                 for _, client in pairs(clients) do
260                     if client.status == status then
261                         fds[client:fd()] = event
262                     end
263                 end
264             end
265
266             local pollfds = {}
267             filter_client( pollfds, status.read, vlc.net.POLLIN )
268             filter_client( pollfds, status.password, vlc.net.POLLIN )
269             filter_client( pollfds, status.write, vlc.net.POLLOUT )
270             if listeners.tcp then
271                 for _, listener in pairs(listeners.tcp.list) do
272                     for _, fd in pairs({listener.data:fds()}) do
273                         pollfds[fd] = vlc.net.POLLIN
274                     end
275                 end
276             end
277
278             local ret = vlc.net.poll( pollfds )
279             if ret > 0 then
280                 for _, client in pairs(clients) do
281                     if is_flag_set(pollfds[client:fd()], vlc.net.POLLIN) then
282                         table.insert(rclients, client)
283                     elseif is_flag_set(pollfds[client:fd()], vlc.net.POLLERR)
284                     or is_flag_set(pollfds[client:fd()], vlc.net.POLLHUP)
285                     or is_flag_set(pollfds[client:fd()], vlc.net.POLLNVAL) then
286                         client:del()
287                     elseif is_flag_set(pollfds[client:fd()], vlc.net.POLLOUT) then
288                         table.insert(wclients, client)
289                     end
290                 end
291                 if listeners.tcp then
292                     for _, listener in pairs(listeners.tcp.list) do
293                         for _, fd in pairs({listener.data:fds()}) do
294                             if is_flag_set(pollfds[fd], vlc.net.POLLIN) then
295                                 local afd = listener.data:accept()
296                                 new_client( h, afd, afd, listener.type )
297                                 break
298                             end
299                         end
300                     end
301                 end
302             end
303         else
304             for _, client in pairs(clients) do
305                 if client.type == client_type.stdio then
306                     if client.status == status.read or client.status == status.password then
307                         if vlc.win.console_wait(50) then
308                             table.insert(rclients, client)
309                         end
310                     else
311                         table.insert(wclients, client)
312                     end
313                 end
314             end
315         end
316         return wclients, rclients
317     end
318
319     local function destructor( h )
320         for _,client in pairs(clients) do
321             client:del(true)
322         end
323     end
324
325     local function _broadcast( h, msg )
326         for _,client in pairs(clients) do
327             client:send( msg )
328         end
329     end
330
331     if setfenv then
332         -- We're running Lua 5.1
333         -- See http://lua-users.org/wiki/HiddenFeatures for more info.
334         local proxy = newproxy(true)
335         getmetatable(proxy).__gc = destructor
336         destructor = proxy
337     end
338
339     -- the instance
340     local h = setmetatable(
341               { -- data
342                 status_callbacks = status_callbacks,
343                 -- methods
344                 listen = _listen,
345                 listen_tcp = _listen_tcp,
346                 listen_stdio = _listen_stdio,
347                 accept_and_select = _accept_and_select,
348                 broadcast = _broadcast,
349               },
350               { -- metatable
351                 __gc = destructor, -- Should work in Lua 5.2 without the new proxytrick as __gc is also called on tables (needs to be tested)
352                 __metatable = "",
353               })
354     return h
355 end