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