mudgangster

Log | Files | Refs

commit f8744a29e57d467583fdd0512ca8fd4cd3069a79
parent 2b58783da6e5023c2f2c6d30bb3c7bd3078ceb3f
Author: Michael Savage <mikejsavage@gmail.com>
Date:   Wed,  5 Sep 2018 20:47:11 +0300

Move event loop to C to more closely match how Windows works

This means sockets have to be created/polled on the C side, which means
lua-ev and luasocket are both gone.

Also made script debugging a bit easier.

Diffstat:
.gitignore | 2+-
Makefile | 2+-
make.lua | 2+-
scripts/gen_makefile.lua | 4++--
src/common.h | 29++++++++++++++++++++++++++++-
src/lua/chat.lua | 51+++++++++++++++++++--------------------------------
src/lua/connect.lua | 113-------------------------------------------------------------------------------
src/lua/handlers.lua | 4++--
src/lua/interval.lua | 8++------
src/lua/main.lua | 54+++++++++++++++++-------------------------------------
src/lua/mud.lua | 81+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
src/lua/socket.lua | 36++++++++++++++++++++++++++++++++++++
src/main.cc | 2+-
src/platform_network.cc | 262+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
src/platform_network.h | 60++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
src/platform_time.h | 13+++++++++++++
src/script.cc | 102++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-------
src/script.h | 4++++
src/ui.h | 10+++++++---
src/unix_network.cc | 63+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
src/unix_time.h | 10++++++++++
src/win32_network.cc | 61+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
src/win32_time.h | 13+++++++++++++
src/x11.cc | 130+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
24 files changed, 907 insertions(+), 209 deletions(-)

diff --git a/.gitignore b/.gitignore @@ -1,4 +1,4 @@ -mudGangster +mudgangster build release gen.mk diff --git a/Makefile b/Makefile @@ -1,7 +1,7 @@ all: debug .PHONY: debug asan release clean -build/lua_bytecode.h: src/lua/action.lua src/lua/alias.lua src/lua/chat.lua src/lua/connect.lua src/lua/event.lua src/lua/gag.lua src/lua/handlers.lua src/lua/intercept.lua src/lua/interval.lua src/lua/macro.lua src/lua/main.lua src/lua/script.lua src/lua/serialize.lua src/lua/status.lua src/lua/sub.lua src/lua/utils.lua +build/lua_bytecode.h: src/lua/action.lua src/lua/alias.lua src/lua/chat.lua src/lua/mud.lua src/lua/event.lua src/lua/gag.lua src/lua/handlers.lua src/lua/intercept.lua src/lua/interval.lua src/lua/macro.lua src/lua/main.lua src/lua/script.lua src/lua/serialize.lua src/lua/status.lua src/lua/sub.lua src/lua/utils.lua src/lua/socket.lua @printf "\033[1;33mbuilding $@\033[0m\n" @scripts/pack_lua.sh diff --git a/make.lua b/make.lua @@ -5,5 +5,5 @@ require( "scripts.gen_makefile" ) -- require( "libs.lua" ) -- require( "libs.lpeg" ) -bin( "mudgangster", { "src/main", "src/script", "src/textbox", "src/input", "src/x11" } ) +bin( "mudgangster", { "src/main", "src/script", "src/textbox", "src/input", "src/x11", "src/platform_network" } ) gcc_bin_ldflags( "mudgangster", "-lm -lX11 -llua" ) -- -Wl,-E" ) need to export symbols when vendoring diff --git a/scripts/gen_makefile.lua b/scripts/gen_makefile.lua @@ -14,7 +14,7 @@ local configs = { toolchain = "msvc", - cxxflags = "/I . /c /Oi /Gm- /GR- /EHa- /EHsc /nologo /DNOMINMAX /DWIN32_LEAN_AND_MEAN", + cxxflags = "/I src /c /Oi /Gm- /GR- /EHa- /EHsc /nologo /DNOMINMAX /DWIN32_LEAN_AND_MEAN", ldflags = "user32.lib shell32.lib advapi32.lib dbghelp.lib /nologo", warnings = "/W4 /wd4100 /wd4146 /wd4189 /wd4201 /wd4324 /wd4351 /wd4127 /wd4505 /wd4530 /wd4702 /D_CRT_SECURE_NO_WARNINGS", }, @@ -38,7 +38,7 @@ local configs = { toolchain = "gcc", cxx = "g++", - cxxflags = "-I . -c -x c++ -std=c++11 -msse2 -ffast-math -fno-exceptions -fno-rtti -fno-strict-aliasing -fno-strict-overflow -fdiagnostics-color", + cxxflags = "-I src -c -x c++ -std=c++11 -msse2 -ffast-math -fno-exceptions -fno-rtti -fno-strict-aliasing -fno-strict-overflow -fdiagnostics-color", ldflags = "-lm -lpthread -ldl", warnings = "-Wall -Wextra -Wno-unused-parameter -Wno-unused-function -Wshadow -Wcast-align -Wstrict-overflow -Wvla", -- -Wconversion }, diff --git a/src/common.h b/src/common.h @@ -4,13 +4,28 @@ #include <stdint.h> #include <stddef.h> #include <stdlib.h> +#include <string.h> #include <assert.h> #include "config.h" -#define STRL( x ) ( x ), sizeof( x ) - 1 +typedef uint8_t u8; +typedef uint16_t u16; +typedef uint32_t u32; + +#define FATAL( form, ... ) \ + do { \ + printf( "[FATAL] " form, ##__VA_ARGS__ ); \ + abort(); \ + } while( 0 ) #define STATIC_ASSERT( p ) static_assert( p, #p ) +#define ASSERT assert +#define NONCOPYABLE( T ) T( const T & ) = delete; void operator=( const T & ) = delete; + +template< typename T, size_t N > +char ( &ArrayCountObj( const T ( & )[ N ] ) )[ N ]; +#define ARRAY_COUNT( arr ) ( sizeof( ArrayCountObj( arr ) ) ) template< typename T > constexpr T min( T a, T b ) { @@ -35,3 +50,15 @@ inline To checked_cast( const From & from ) { assert( From( result ) == from ); return result; } + +template< typename T > +inline T * malloc_array( size_t count ) { + ASSERT( SIZE_MAX / count >= sizeof( T ) ); + return ( T * ) malloc( count * sizeof( T ) ); +} + +template< typename T > +inline T * realloc_array( T * old, size_t count ) { + ASSERT( SIZE_MAX / count >= sizeof( T ) ); + return ( T * ) realloc( old, count * sizeof( T ) ); +} diff --git a/src/lua/chat.lua b/src/lua/chat.lua @@ -1,5 +1,3 @@ -local loop = ev.Loop.default - local handleChat local CommandBytes = { @@ -61,11 +59,9 @@ end local Client = { } -function Client:new( socket, address, port ) - socket:setoption( "keepalive", true ) - +function Client:new( sock, address, port ) local client = { - socket = socket, + socket = sock, address = address, port = port, @@ -80,7 +76,7 @@ function Client:new( socket, address, port ) table.insert( Clients, client ) - socket:send( "CHAT:Hirve\n127.0.0.14050 " ) + socket.send( sock, "CHAT:Hirve\n127.0.0.14050 " ) return client end @@ -93,7 +89,7 @@ function Client:kill() mud.print( "\n#s> Disconnected from %s!", self.name ) self.state = "killed" - self.socket:shutdown() + socket.close( self.socket ) for i, client in ipairs( Clients ) do if client == self then @@ -123,7 +119,7 @@ function mud.chatns( form, ... ) for _, client in ipairs( Clients ) do if client.state == "connected" then - client.socket:send( data ) + socket.send( client.socket, data ) end end @@ -138,31 +134,22 @@ end local function call( address, port ) mud.print( "\n#s> Calling %s:%d...", address, port ) - local sock = socket.tcp() - sock:settimeout( 0 ) - - sock:connect( address, port ) - - ev.IO.new( function( loop, watcher ) - local _, err = sock:receive( "*a" ) - - if err ~= "connection refused" then - local client = Client:new( sock, address, port ) - - ev.IO.new( function( loop, watcher ) - dataHandler( client, loop, watcher ) - end, sock:getfd(), ev.READ ):start( loop ) - - ev.IO.new( function( loop, watcher ) - client.socket:send( CommandBytes.version .. "MudGangster" .. "\255" ) - watcher:stop( loop ) - end, sock:getfd(), ev.WRITE ):start( loop ) + local client + local sock, err = socket.connect( address, port, function( sock, data ) + if data then + assert( coroutine.resume( client.handler, data ) ) else - mud.print( "\n#s> Failed to call %s:%d", address, port ) + client:kill() end + end ) - watcher:stop( loop ) - end, sock:getfd(), ev.WRITE ):start( loop ) + if not sock then + mud.print( "\n#s> Connection failed: %s", err ) + return + end + + client = Client:new( sock, address, port ) + socket.send( client.socket, CommandBytes.version .. "MudGangster" .. "\255" ) end mud.alias( "/call", { @@ -187,7 +174,7 @@ local function sendPM( client, message ) local named = "\nHirve chats to you, '" .. message .. "'" local data = CommandBytes.pm .. named .. "\n\255" - client.socket:send( data ) + socket.send( client.socket, data ) end mud.alias( "/silentpm", { diff --git a/src/lua/connect.lua b/src/lua/connect.lua @@ -1,113 +0,0 @@ -local DataHandler - -local LastAddress -local LastPort - -local loop = ev.Loop.default - -function mud.disconnect() - if not mud.connected then - mud.print( "\n#s> You're not connected..." ) - - return - end - - mud.connected = false - - mud.kill() -end - -function mud.connect( address, port ) - if mud.connected then - mud.print( "\n#s> Already connected! (%s:%d)", LastAddress, LastPort ) - - return - end - - if not address then - if not LastAddress then - mud.print( "\n#s> I need an address..." ) - - return - end - - address = LastAddress - port = LastPort - end - - local sock = socket.tcp() - sock:settimeout( 0 ) - sock:connect( address, port ) - - mud.print( "\n#s> Connecting to %s:%d...", address, port ) - - mud.connected = true - mud.lastInput = mud.now() - - mud.send = function( data ) - mud.lastInput = mud.now() - - sock:send( data ) - end - - mud.kill = function() - sock:shutdown() - end - - ev.IO.new( function( loop, watcher ) - local _, err = sock:receive( "*a" ) - - if err ~= "connection refused" then - LastAddress = address - LastPort = port - - ev.IO.new( function( loop, watcher ) - local _, err, data = sock:receive( "*a" ) - - if not data then - data = _ - end - - if data then - DataHandler( data ) - mud.handleXEvents() - end - - if err == "closed" then - mud.print( "\n#s> Disconnected!" ) - - watcher:stop( loop ) - sock:shutdown() - - mud.connected = false - - return - end - end, sock:getfd(), ev.READ ):start( loop ) - else - mud.print( "\n#s> Refused!" ) - - mud.connected = false - end - - watcher:stop( loop ) - end, sock:getfd(), ev.WRITE ):start( loop ) -end - -mud.alias( "/con", { - [ "^$" ] = function() - mud.connect() - end, - - [ "^(%S+)%s+(%d+)$" ] = function( address, port ) - mud.connect( address, port ) - end, -}, "<ip> <port>" ) - -mud.alias( "/dc", mud.disconnect ) - -return { - init = function( dataHandler ) - DataHandler = dataHandler - end, -} diff --git a/src/lua/handlers.lua b/src/lua/handlers.lua @@ -254,6 +254,7 @@ local function handleCommand( input, hide ) end end + print( mud.send ) mud.send( input .. "\n" ) mud.drawMain() end @@ -284,8 +285,6 @@ local function handleMacro( key, shift, ctrl, alt ) end local function handleClose() - ev.Loop.default:unloop() - mud.event( "shutdown" ) end @@ -295,5 +294,6 @@ return { input = handleInput, macro = handleMacro, interval = interval.doIntervals, + socket = handleSocketData, close = handleClose, } diff --git a/src/lua/interval.lua b/src/lua/interval.lua @@ -1,7 +1,7 @@ local Intervals = { } -local function doIntervals( loop, watcher ) - local now = loop:update_now() +local function doIntervals() + local now = mud.now() for i = 1, #Intervals do local event = Intervals[ i ] @@ -12,10 +12,6 @@ local function doIntervals( loop, watcher ) end end -function mud.now() - return ev.Loop.default:update_now() -end - function mud.interval( callback, interval, disabled ) enforce( callback, "callback", "function" ) enforce( interval, "interval", "number" ) diff --git a/src/lua/main.lua b/src/lua/main.lua @@ -2,44 +2,32 @@ mud = { connected = false, } --- local function luarocks_path( opt, orig ) --- local pipe = io.popen( "luarocks path " .. opt, "r" ) --- if not pipe then --- return nil --- end --- --- local contents = pipe:read( "*all" ) --- pipe:close() --- --- if not contents then --- return orig --- end --- --- return contents:gsub( "%s*$", "" ) .. ";" .. orig --- end --- --- package.path = luarocks_path( "--lr-path", package.path ) --- package.cpath = luarocks_path( "--lr-cpath", package.cpath ) - table.unpack = table.unpack or unpack require( "utils" ) -socket = require( "socket" ) -ev = require( "ev" ) - require( "event" ) local script = require( "script" ) local handlers = require( "handlers" ) -require( "chat" ).init( handlers.chat ) -require( "connect" ).init( handlers.data ) - -local xFD, handleXEvents, +local handleXEvents, printMain, newlineMain, drawMain, printChat, newlineChat, drawChat, - setHandlers, urgent, setStatus = ... + setHandlers, urgent, setStatus, + sock_connect, sock_send, sock_close, + get_time = ... + +local socket_api = { + connect = sock_connect, + send = sock_send, + close = sock_close, +} + +local socket_data_handler = require( "socket" ).init( socket_api ) + +require( "mud" ).init( handlers.data ) +require( "chat" ).init( handlers.chat ) mud.printMain = printMain mud.newlineMain = newlineMain @@ -50,20 +38,12 @@ mud.newlineChat = newlineChat mud.drawChat = drawChat mud.urgent = urgent +mud.now = get_time require( "status" ).init( setStatus ) -setHandlers( handlers.input, handlers.macro, handlers.close ) - -local loop = ev.Loop.default +setHandlers( handlers.input, handlers.macro, handlers.close, socket_data_handler, handlers.interval ) mud.handleXEvents = handleXEvents -ev.IO.new( handleXEvents, xFD, ev.READ ):start( loop ) -ev.Timer.new( handlers.interval, 0.5, 0.5 ):start( loop ) - script.load() - -loop:loop() - -script.close() diff --git a/src/lua/mud.lua b/src/lua/mud.lua @@ -0,0 +1,81 @@ +local DataHandler +local mud_socket + +local LastAddress +local LastPort + +function mud.send( data ) + mud.lastInput = mud.now() + socket.send( mud_socket, data ) +end + +function mud.disconnect() + mud.print( "\n#s> Disconnected!" ) + mud.connected = false + socket.close( mud_socket ) + mud_socket = nil +end + +function mud.connect( address, port ) + if mud.connected then + mud.print( "\n#s> Already connected! (%s:%d)", LastAddress, LastPort ) + + return + end + + if not address then + if not LastAddress then + mud.print( "\n#s> I need an address..." ) + + return + end + + address = LastAddress + port = LastPort + end + + mud.print( "\n#s> Connecting to %s:%d...", address, port ) + + local sock, err = socket.connect( address, port, function( sock, data ) + if data then + DataHandler( data ) + else + mud.kill() + end + end ) + + if not sock then + mud.print( "\n#s> Connection failed: %s", err ) + return + end + + LastAddress = address + LastPort = port + + mud.connected = true + mud.lastInput = mud.now() +end + +mud.alias( "/con", { + [ "^$" ] = function() + mud.connect() + end, + + [ "^(%S+)%s+(%d+)$" ] = function( address, port ) + mud.connect( address, port ) + end, +}, "<ip> <port>" ) + +mud.alias( "/dc", function() + if mud.connected then + mud.disconnect() + else + mud.print( "\n#s> You're not connected..." ) + end +end ) + +return { + init = function( dataHandler ) + DataHandler = dataHandler + end, +} diff --git a/src/lua/socket.lua b/src/lua/socket.lua @@ -0,0 +1,36 @@ +local socket_api +local data_callbacks = { } + +local function connect( addr, port, cb ) + local sock, err = socket_api.connect( addr, port ) + if not sock then + return nil, err + end + data_callbacks[ sock ] = cb + return sock +end + +local function close( sock ) + socket_api.close( sock ) + data_callbacks[ sock ] = nil +end + +local function on_socket_data( sock, data ) + -- data could be like { type = "data/failed/close/etc", data = ... } + -- need a failed to handle async connect + data_callbacks[ sock ]( sock, data ) +end + +return { + init = function( api ) + socket_api = api + + socket = { + connect = connect, + send = socket_api.send, + close = close, + } + + return on_socket_data + end, +} diff --git a/src/main.cc b/src/main.cc @@ -7,7 +7,7 @@ int main() { input_init(); script_init(); - // main loop is done in lua + event_loop(); script_term(); input_term(); diff --git a/src/platform_network.cc b/src/platform_network.cc @@ -0,0 +1,262 @@ +#include "common.h" + +#include "platform.h" +#include "platform_network.h" + +struct sockaddr_storage; + +static NetAddress sockaddr_to_netaddress( const struct sockaddr_storage & ss ); +static struct sockaddr_storage netaddress_to_sockaddr( const NetAddress & addr ); +static void setsockoptone( OSSocket fd, int level, int opt ); + +#if PLATFORM_WINDOWS +#include "win32_network.cc" +#elif PLATFORM_UNIX +#include "unix_network.cc" +#else +#error new platform +#endif + +bool operator==( const NetAddress & lhs, const NetAddress & rhs ) { + if( lhs.type != rhs.type ) return false; + if( lhs.port != rhs.port ) return false; + if( lhs.type == NET_IPV4 ) return memcmp( &lhs.ipv4, &rhs.ipv4, sizeof( lhs.ipv4 ) ) == 0; + return memcmp( &lhs.ipv6, &rhs.ipv6, sizeof( lhs.ipv6 ) ) == 0; +} + +bool operator!=( const NetAddress & lhs, const NetAddress & rhs ) { + return !( lhs == rhs ); +} + +static socklen_t sockaddr_size( const struct sockaddr_storage & ss ) { + return ss.ss_family == AF_INET ? sizeof( struct sockaddr_in ) : sizeof( struct sockaddr_in6 ); +} + +static NetAddress sockaddr_to_netaddress( const struct sockaddr_storage & ss ) { + NetAddress addr; + if( ss.ss_family == AF_INET ) { + const struct sockaddr_in & sa4 = ( const struct sockaddr_in & ) ss; + + addr.type = NET_IPV4; + addr.port = ntohs( sa4.sin_port ); + memcpy( &addr.ipv4, &sa4.sin_addr.s_addr, sizeof( addr.ipv4 ) ); + } + else { + const struct sockaddr_in6 & sa6 = ( const struct sockaddr_in6 & ) ss; + + addr.type = NET_IPV6; + addr.port = ntohs( sa6.sin6_port ); + memcpy( &addr.ipv6, &sa6.sin6_addr.s6_addr, sizeof( addr.ipv6 ) ); + } + return addr; +} + +static struct sockaddr_storage netaddress_to_sockaddr( const NetAddress & addr ) { + struct sockaddr_storage ss; + if( addr.type == NET_IPV4 ) { + struct sockaddr_in & sa4 = ( struct sockaddr_in & ) ss; + + ss.ss_family = AF_INET; + sa4.sin_port = htons( addr.port ); + memcpy( &sa4.sin_addr.s_addr, &addr.ipv4, sizeof( addr.ipv4 ) ); + } + else { + struct sockaddr_in6 & sa6 = ( struct sockaddr_in6 & ) ss; + + ss.ss_family = AF_INET6; + sa6.sin6_port = htons( addr.port ); + memcpy( &sa6.sin6_addr.s6_addr, &addr.ipv6, sizeof( addr.ipv6 ) ); + } + return ss; +} + +static void setsockoptone( OSSocket fd, int level, int opt ) { + int one = 1; + int ok = setsockopt( fd, level, opt, ( char * ) &one, sizeof( one ) ); + if( ok == -1 ) { + FATAL( "setsockopt" ); + } +} + +UDPSocket net_new_udp( NonblockingBool nonblocking, u16 port ) { + UDPSocket sock; + + sock.ipv4 = socket( AF_INET, SOCK_DGRAM, IPPROTO_UDP ); + if( sock.ipv4 == INVALID_SOCKET ) { + FATAL( "socket" ); + } + sock.ipv6 = socket( AF_INET6, SOCK_DGRAM, IPPROTO_UDP ); + if( sock.ipv6 == INVALID_SOCKET ) { + FATAL( "socket" ); + } + + if( nonblocking == NET_NONBLOCKING ) { + make_socket_nonblocking( sock.ipv4 ); + make_socket_nonblocking( sock.ipv6 ); + } + + if( port != 0 ) { + setsockoptone( sock.ipv4, SOL_SOCKET, SO_REUSEADDR ); + setsockoptone( sock.ipv6, SOL_SOCKET, SO_REUSEADDR ); + setsockoptone( sock.ipv6, IPPROTO_IPV6, IPV6_V6ONLY ); + } + + platform_init_sock( sock.ipv4 ); + platform_init_sock( sock.ipv6 ); + + { + sockaddr_in my_addr4; + my_addr4.sin_family = AF_INET; + my_addr4.sin_port = htons( port ); + my_addr4.sin_addr.s_addr = htonl( INADDR_ANY ); + int ok = bind( sock.ipv4, ( struct sockaddr * ) &my_addr4, sizeof( my_addr4 ) ); + if( ok == SOCKET_ERROR ) { + FATAL( "bind" ); + } + } + + { + sockaddr_in6 my_addr6; + my_addr6.sin6_family = AF_INET6; + my_addr6.sin6_port = htons( port ); + my_addr6.sin6_addr = in6addr_any; + int ok = bind( sock.ipv6, ( struct sockaddr * ) &my_addr6, sizeof( my_addr6 ) ); + if( ok == SOCKET_ERROR ) { + FATAL( "bind" ); + } + } + + return sock; +} + +void net_send( UDPSocket sock, const void * data, size_t len, const NetAddress & addr ) { + struct sockaddr_storage ss = netaddress_to_sockaddr( addr ); + socklen_t ss_size = sockaddr_size( ss ); + OSSocket fd = addr.type == NET_IPV4 ? sock.ipv4 : sock.ipv6; + ssize_t ok = sendto( fd, ( const char * ) data, checked_cast< int >( len ), NET_SEND_FLAGS, ( struct sockaddr * ) &ss, ss_size ); + if( ok == SOCKET_ERROR ) { + FATAL( "sendto" ); + } +} + +void net_destroy( UDPSocket * sock ) { + int ok4 = closesocket( sock->ipv4 ); + if( ok4 == -1 ) { + FATAL( "closesocket" ); + } + int ok6 = closesocket( sock->ipv6 ); + if( ok6 == -1 ) { + FATAL( "closesocket" ); + } + sock->ipv4 = INVALID_SOCKET; + sock->ipv6 = INVALID_SOCKET; +} + +bool net_new_tcp( TCPSocket * sock, const NetAddress & addr, NonblockingBool nonblocking ) { + struct sockaddr_storage ss = netaddress_to_sockaddr( addr ); + socklen_t ss_size = sockaddr_size( ss ); + + sock->fd = socket( ss.ss_family, SOCK_STREAM, IPPROTO_TCP ); + if( sock->fd == INVALID_SOCKET ) { + FATAL( "socket" ); + } + + int ok = connect( sock->fd, ( const sockaddr * ) &ss, ss_size ); + if( ok == -1 ) { + int ok_close = closesocket( sock->fd ); + if( ok_close == -1 ) { + FATAL( "closesocket" ); + } + // TODO: check for actual coding errors too + return false; + } + + if( nonblocking == NET_NONBLOCKING ) { + make_socket_nonblocking( sock->fd ); + } + + setsockoptone( sock->fd, SOL_SOCKET, SO_KEEPALIVE ); + + platform_init_sock( sock->fd ); + + return true; +} + +bool net_send( TCPSocket sock, const void * data, size_t len ) { + ssize_t sent = send( sock.fd, ( const char * ) data, len, NET_SEND_FLAGS ); + if( sent < 0 ) return false; + return checked_cast< size_t >( sent ) == len; +} + +TCPRecvResult net_recv( TCPSocket sock, void * buf, size_t buf_size, size_t * bytes_read, u32 timeout_ms ) { + while( true ) { + if( timeout_ms > 0 ) { + fd_set fds; + FD_ZERO( &fds ); + FD_SET( sock.fd, &fds ); + + struct timeval tv; + tv.tv_sec = timeout_ms / 1000; + tv.tv_usec = ( timeout_ms % 1000 ) * 1000; + + int ok = select( sock.fd + 1, &fds, NULL, NULL, &tv ); + // TODO: update timeout + if( ok == 0 ) { + return TCP_TIMEOUT; + } + if( ok == -1 ) { + if( errno != EINTR ) continue; + FATAL( "select" ); + } + } + + ssize_t r = recv( sock.fd, ( char * ) buf, buf_size, 0 ); + // TODO: this is not right on windows + if( r == -1 ) { + if( errno == EINTR ) continue; + if( errno == ECONNRESET ) return TCP_ERROR; + FATAL( "recv" ); + } + + *bytes_read = checked_cast< size_t >( r ); + return r == 0 ? TCP_CLOSED : TCP_OK; + } +} + +void net_destroy( TCPSocket * sock ) { + int ok = closesocket( sock->fd ); + if( ok == -1 ) { + FATAL( "closesocket" ); + } + sock->fd = INVALID_SOCKET; +} + +size_t dns( const char * host, NetAddress * out, size_t n ) { + struct addrinfo hints; + memset( &hints, 0, sizeof( struct addrinfo ) ); + hints.ai_family = AF_UNSPEC; + // TODO: figure out why ivp6 doesn't work on windows + hints.ai_family = AF_INET; + hints.ai_socktype = SOCK_STREAM; + hints.ai_protocol = IPPROTO_TCP; + + struct addrinfo * addresses; + int ok = getaddrinfo( host, "http", &hints, &addresses ); + if( ok != 0 ) + return 0; + + struct addrinfo * cursor = addresses; + size_t i = 0; + while( cursor != NULL && i < n ) { + out[ i ] = sockaddr_to_netaddress( *( struct sockaddr_storage * ) cursor->ai_addr ); + cursor = cursor->ai_next; + i++; + } + freeaddrinfo( addresses ); + + return i; +} + +bool dns_first( const char * host, NetAddress * address ) { + return dns( host, address, 1 ) == 1; +} diff --git a/src/platform_network.h b/src/platform_network.h @@ -0,0 +1,60 @@ +#pragma once + +#include "platform.h" + +#if PLATFORM_WINDOWS +typedef s64 OSSocket; +#elif PLATFORM_UNIX +typedef int OSSocket; +#else +#error new platform +#endif + +enum TransportProtocol { NET_UDP, NET_TCP }; +enum IPvX { NET_IPV4, NET_IPV6 }; +enum NonblockingBool { NET_BLOCKING, NET_NONBLOCKING }; + +enum TCPRecvResult { + TCP_OK, + TCP_TIMEOUT, + TCP_CLOSED, + TCP_ERROR, +}; + +struct UDPSocket { + OSSocket ipv4, ipv6; +}; + +struct TCPSocket { + OSSocket fd; +}; + +struct IPv4 { u8 bytes[ 4 ]; }; +struct IPv6 { u8 bytes[ 16 ]; }; + +struct NetAddress { + IPvX type; + union { + IPv4 ipv4; + IPv6 ipv6; + }; + u16 port; +}; + +bool operator==( const NetAddress & lhs, const NetAddress & rhs ); +bool operator!=( const NetAddress & lhs, const NetAddress & rhs ); + +void net_init(); +void net_term(); + +UDPSocket net_new_udp( NonblockingBool nonblocking, u16 port = 0 ); +void net_send( UDPSocket sock, const void * data, size_t len, const NetAddress & addr ); +bool net_tryrecv( UDPSocket sock, void * buf, size_t len, NetAddress * addr, size_t * bytes_received ); +void net_destroy( UDPSocket * sock ); + +bool net_new_tcp( TCPSocket * sock, const NetAddress & addr, NonblockingBool nonblocking ); +bool net_send( TCPSocket sock, const void * data, size_t len ); +TCPRecvResult net_recv( TCPSocket sock, void * buf, size_t buf_size, size_t * bytes_read, u32 timeout_ms = 0 ); +void net_destroy( TCPSocket * sock ); + +bool dns_first( const char * host, NetAddress * address ); diff --git a/src/platform_time.h b/src/platform_time.h @@ -0,0 +1,13 @@ +#pragma once + +#include "platform.h" + +#if PLATFORM_WINDOWS +#include "win32_time.h" +#elif PLATFORM_OSX +#include "darwin_time.h" +#elif PLATFORM_UNIX +#include "unix_time.h" +#else +#error new platform +#endif diff --git a/src/script.cc b/src/script.cc @@ -2,6 +2,8 @@ #include "platform.h" #include "ui.h" +#include "platform_time.h" + #include <lua.hpp> #if LUA_VERSION_NUM < 502 @@ -9,7 +11,7 @@ #endif static const uint8_t lua_bytecode[] = { -#include "build/lua_bytecode.h" +#include "../build/lua_bytecode.h" }; static lua_State * lua; @@ -17,6 +19,17 @@ static lua_State * lua; static int inputHandlerIdx = LUA_NOREF; static int macroHandlerIdx = LUA_NOREF; static int closeHandlerIdx = LUA_NOREF; +static int socketHandlerIdx = LUA_NOREF; +static int intervalHandlerIdx = LUA_NOREF; + +static void pcall( int args, const char * err ) { + if( lua_pcall( lua, args, 0, 1 ) ) { + printf( "%s: %s\n", err, lua_tostring( lua, -1 ) ); + exit( 1 ); + } + + assert( lua_gettop( lua ) == 1 ); +} void script_handleInput( const char * buffer, int len ) { assert( inputHandlerIdx != LUA_NOREF ); @@ -24,10 +37,12 @@ void script_handleInput( const char * buffer, int len ) { lua_rawgeti( lua, LUA_REGISTRYINDEX, inputHandlerIdx ); lua_pushlstring( lua, buffer, len ); - lua_call( lua, 1, 0 ); + pcall( 1, "script_handleInput" ); } void script_doMacro( const char * key, int len, bool shift, bool ctrl, bool alt ) { + assert( macroHandlerIdx != LUA_NOREF ); + lua_rawgeti( lua, LUA_REGISTRYINDEX, macroHandlerIdx ); lua_pushlstring( lua, key, len ); @@ -36,15 +51,35 @@ void script_doMacro( const char * key, int len, bool shift, bool ctrl, bool alt lua_pushboolean( lua, ctrl ); lua_pushboolean( lua, alt ); - lua_call( lua, 4, 0 ); + pcall( 4, "script_doMacro" ); } void script_handleClose() { assert( closeHandlerIdx != LUA_NOREF ); lua_rawgeti( lua, LUA_REGISTRYINDEX, closeHandlerIdx ); + pcall( 0, "script_handleClose" ); +} + +void script_socketData( void * sock, const char * data, size_t len ) { + assert( socketHandlerIdx != LUA_NOREF ); + + lua_rawgeti( lua, LUA_REGISTRYINDEX, socketHandlerIdx ); - lua_call( lua, 0, 0 ); + lua_pushlightuserdata( lua, sock ); + if( data == NULL ) + lua_pushnil( lua ); + else + lua_pushlstring( lua, data, len ); + + pcall( 2, "script_socketData" ); +} + +void script_fire_intervals() { + assert( intervalHandlerIdx != LUA_NOREF ); + + lua_rawgeti( lua, LUA_REGISTRYINDEX, intervalHandlerIdx ); + pcall( 0, "script_fire_intervals" ); } namespace { @@ -66,6 +101,44 @@ static void generic_print( F * f, lua_State * L ) { f( str, len, fg, bg, bold ); } +extern "C" int mud_connect( lua_State * L ) { + const char * host = luaL_checkstring( L, 1 ); + int port = luaL_checkinteger( L, 2 ); + + const char * err; + void * sock = platform_connect( &err, host, port ); + if( sock != NULL ) { + lua_pushlightuserdata( lua, sock ); + return 1; + } + + lua_pushnil( lua ); + lua_pushstring( lua, err ); + + return 2; +} + +extern "C" int mud_send( lua_State * L ) { + luaL_argcheck( L, lua_isuserdata( L, 1 ) == 1, 1, "expected socket" ); + void * sock = lua_touserdata( L, 1 ); + + const char * data = luaL_checkstring( L, 2 ); + size_t len = luaL_len( L, 2 ); + + platform_send( sock, data, len ); + + return 0; +} + +extern "C" int mud_close( lua_State * L ) { + luaL_argcheck( L, lua_isuserdata( L, 1 ) == 1, 1, "expected socket" ); + void * sock = lua_touserdata( L, 1 ); + + platform_close( sock ); + + return 0; +} + extern "C" int mud_printMain( lua_State * L ) { generic_print( ui_main_print, L ); return 0; @@ -136,7 +209,11 @@ extern "C" int mud_setHandlers( lua_State * L ) { luaL_argcheck( L, lua_type( L, 1 ) == LUA_TFUNCTION, 1, "expected function" ); luaL_argcheck( L, lua_type( L, 2 ) == LUA_TFUNCTION, 2, "expected function" ); luaL_argcheck( L, lua_type( L, 3 ) == LUA_TFUNCTION, 3, "expected function" ); + luaL_argcheck( L, lua_type( L, 4 ) == LUA_TFUNCTION, 4, "expected function" ); + luaL_argcheck( L, lua_type( L, 5 ) == LUA_TFUNCTION, 5, "expected function" ); + intervalHandlerIdx = luaL_ref( L, LUA_REGISTRYINDEX ); + socketHandlerIdx = luaL_ref( L, LUA_REGISTRYINDEX ); closeHandlerIdx = luaL_ref( L, LUA_REGISTRYINDEX ); macroHandlerIdx = luaL_ref( L, LUA_REGISTRYINDEX ); inputHandlerIdx = luaL_ref( L, LUA_REGISTRYINDEX ); @@ -149,6 +226,11 @@ extern "C" int mud_urgent( lua_State * L ) { return 0; } +extern "C" int mud_now( lua_State * L ) { + lua_pushnumber( L, get_time() ); + return 1; +} + } // anon namespace #if PLATFORM_WINDOWS @@ -175,7 +257,6 @@ void script_init() { exit( 1 ); } - lua_pushinteger( lua, ui_display_fd() ); lua_pushcfunction( lua, mud_handleXEvents ); lua_pushcfunction( lua, mud_printMain ); @@ -192,10 +273,13 @@ void script_init() { lua_pushcfunction( lua, mud_setStatus ); - if( lua_pcall( lua, 11, 0, -13 ) ) { - printf( "Error running main.lua: %s\n", lua_tostring( lua, -1 ) ); - exit( 1 ); - } + lua_pushcfunction( lua, mud_connect ); + lua_pushcfunction( lua, mud_send ); + lua_pushcfunction( lua, mud_close ); + + lua_pushcfunction( lua, mud_now ); + + pcall( 14, "Error running main.lua" ); } void script_term() { diff --git a/src/script.h b/src/script.h @@ -1,9 +1,13 @@ #pragma once +#include <stddef.h> + // TODO: should be size_t here? void script_handleInput( const char * buffer, int len ); void script_doMacro( const char * key, int len, bool shift, bool ctrl, bool alt ); void script_handleClose(); +void script_socketData( void * sock, const char * data, size_t len ); +void script_fire_intervals(); void script_init(); void script_term(); diff --git a/src/ui.h b/src/ui.h @@ -39,13 +39,17 @@ void ui_chat_print( const char * str, size_t len, Colour fg, Colour bg, bool bol void ui_fill_rect( int left, int top, int width, int height, Colour colour, bool bold ); void ui_draw_char( int left, int top, char c, Colour colour, bool bold, bool bold_font = false ); -void ui_dirty( int left, int top, int right, int bottom ); // TODO: x/y + w/h? +void ui_dirty( int left, int top, int width, int height ); void ui_get_font_size( int * fw, int * fh ); void ui_urgent(); -int ui_display_fd(); // TODO: very x11 specific! - void ui_init(); void ui_term(); + +void * platform_connect( const char ** err, const char * host, int port ); +void platform_send( void * sock, const char * data, size_t len ); +void platform_close( void * sock ); + +void event_loop(); diff --git a/src/unix_network.cc b/src/unix_network.cc @@ -0,0 +1,63 @@ +#include <arpa/inet.h> +#include <sys/types.h> +#include <sys/select.h> +#include <sys/socket.h> +#include <errno.h> +#include <fcntl.h> +#include <netdb.h> +#include <unistd.h> + +#define INVALID_SOCKET ( -1 ) +#define SOCKET_ERROR ( -1 ) + +#define closesocket close + +#if PLATFORM_OSX +static int NET_SEND_FLAGS = 0; +#else +static int NET_SEND_FLAGS = MSG_NOSIGNAL; +#endif + +void net_init() { } +void net_term() { } + +static void make_socket_nonblocking( int fd ) { + int flags = fcntl( fd, F_GETFL, 0 ); + if( flags == -1 ) FATAL( "fcntl F_GETFL" ); + int ok = fcntl( fd, F_SETFL, flags | O_NONBLOCK ); + if( ok == -1 ) FATAL( "fcntl F_SETFL" ); +} + +static void platform_init_sock( int fd ) { +#if PLATFORM_OSX + setsockoptone( fd, SOL_SOCKET, SO_NOSIGPIPE ); +#endif +} + +bool net_tryrecv( UDPSocket sock, void * buf, size_t len, NetAddress * addr, size_t * bytes_received ) { + struct sockaddr_storage sa; + + socklen_t sa_size = sizeof( struct sockaddr_in ); + ssize_t received4 = recvfrom( sock.ipv4, buf, len, 0, ( struct sockaddr * ) &sa, &sa_size ); + if( received4 != -1 ) { + *addr = sockaddr_to_netaddress( sa ); + *bytes_received = size_t( received4 ); + return true; + } + if( received4 == -1 && errno != EAGAIN ) { + FATAL( "recvfrom" ); + } + + sa_size = sizeof( struct sockaddr_in6 ); + ssize_t received6 = recvfrom( sock.ipv6, buf, len, 0, ( struct sockaddr * ) &sa, &sa_size ); + if( received6 != -1 ) { + *addr = sockaddr_to_netaddress( sa ); + *bytes_received = size_t( received6 ); + return true; + } + if( received6 == -1 && errno != EAGAIN ) { + FATAL( "recvfrom" ); + } + + return false; +} diff --git a/src/unix_time.h b/src/unix_time.h @@ -0,0 +1,10 @@ +#pragma once + +#include <time.h> + +inline double get_time() { + struct timespec ts; + clock_gettime( CLOCK_MONOTONIC, &ts ); + + return double( ts.tv_sec ) + ts.tv_nsec / 1e9; +} diff --git a/src/win32_network.cc b/src/win32_network.cc @@ -0,0 +1,61 @@ +#include <winsock2.h> +#include <ws2tcpip.h> + +static int NET_SEND_FLAGS = 0; + +void net_init() { + WSADATA wsa_data; + if( WSAStartup( MAKEWORD( 2, 2 ), &wsa_data ) == SOCKET_ERROR ) { + FATAL( "WSAStartup" ); + } +} + +void net_term() { + if( WSACleanup() == SOCKET_ERROR ) { + FATAL( "WSACleanup" ); + } +} + +static void make_socket_nonblocking( SOCKET fd ) { + u_long one = 1; + int ok = ioctlsocket( fd, FIONBIO, &one ); + if( ok == SOCKET_ERROR ) { + FATAL( "ioctlsocket" ); + } +} + +static void platform_init_sock( SOCKET fd ) { } + +bool net_tryrecv( UDPSocket sock, void * buf, size_t len, NetAddress * addr, size_t * bytes_received ) { + struct sockaddr_storage sa; + + socklen_t sa_size = sizeof( struct sockaddr_in ); + ssize_t received4 = recvfrom( sock.ipv4, ( char * ) buf, len, 0, ( struct sockaddr * ) &sa, &sa_size ); + if( received4 != SOCKET_ERROR ) { + *addr = sockaddr_to_netaddress( sa ); + *bytes_received = size_t( received4 ); + return true; + } + else { + int error = WSAGetLastError(); + if( error != WSAEWOULDBLOCK && error != WSAECONNRESET ) { + FATAL( "recvfrom" ); + } + } + + sa_size = sizeof( struct sockaddr_in6 ); + ssize_t received6 = recvfrom( sock.ipv6, ( char * ) buf, len, 0, ( struct sockaddr * ) &sa, &sa_size ); + if( received6 != SOCKET_ERROR ) { + *addr = sockaddr_to_netaddress( sa ); + *bytes_received = size_t( received6 ); + return true; + } + else { + int error = WSAGetLastError(); + if( error != WSAEWOULDBLOCK && error != WSAECONNRESET ) { + FATAL( "recvfrom" ); + } + } + + return false; +} diff --git a/src/win32_time.h b/src/win32_time.h @@ -0,0 +1,13 @@ +#pragma once + +#include <windows.h> + +inline double get_time() { + LARGE_INTEGER counter; + QueryPerformanceCounter( &counter ); + + LARGE_INTEGER freq; + QueryPerformanceFrequency( &freq ); + + return double( counter.QuadPart ) / double( freq.QuadPart ); +} diff --git a/src/x11.cc b/src/x11.cc @@ -1,4 +1,5 @@ #include <err.h> +#include <poll.h> #include <X11/Xutil.h> #include <X11/XKBlib.h> @@ -10,6 +11,69 @@ #include "script.h" #include "textbox.h" +#include "platform_network.h" + +struct Socket { + TCPSocket sock; + bool in_use; +}; + +static Socket sockets[ 128 ]; + +static bool closing = false; + +void * platform_connect( const char ** err, const char * host, int port ) { + size_t idx; + { + bool ok = false; + for( size_t i = 0; i < ARRAY_COUNT( sockets ); i++ ) { + if( !sockets[ i ].in_use ) { + idx = i; + ok = true; + break; + } + } + + if( !ok ) { + *err = "too many connections"; + return NULL; + } + } + + NetAddress addr; + { + bool ok = dns_first( host, &addr ); + if( !ok ) { + *err = "couldn't resolve hostname"; // TODO: error from dns_first + return NULL; + } + } + addr.port = checked_cast< u16 >( port ); + + TCPSocket sock; + bool ok = net_new_tcp( &sock, addr, NET_BLOCKING ); + if( !ok ) { + *err = "net_new_tcp"; + return NULL; + } + + sockets[ idx ].sock = sock; + sockets[ idx ].in_use = true; + + return &sockets[ idx ]; +} + +void platform_send( void * vsock, const char * data, size_t len ) { + Socket * sock = ( Socket * ) vsock; + net_send( sock->sock, data, len ); +} + +void platform_close( void * vsock ) { + Socket * sock = ( Socket * ) vsock; + net_destroy( &sock->sock ); + sock->in_use = false; +} + struct { Display * display; int screen; @@ -461,6 +525,7 @@ static void event_mouse_move( XEvent * xevent ) { static void event_message( XEvent * xevent ) { if( ( Atom ) xevent->xclient.data.l[ 0 ] == wmDeleteWindow ) { script_handleClose(); + closing = true; } } @@ -736,6 +801,10 @@ static void initStyle() { } void ui_init() { + for( Socket & s : sockets ) { + s.in_use = false; + } + UI = { }; textbox_init( &UI.main_text, SCROLLBACK_SIZE ); @@ -831,3 +900,64 @@ void ui_term() { XDestroyWindow( UI.display, UI.window ); XCloseDisplay( UI.display ); } + +static Socket * socket_from_fd( int fd ) { + for( Socket & sock : sockets ) { + if( sock.in_use && sock.sock.fd == fd ) { + return &sock; + } + } + + return NULL; +} + +void event_loop() { + while( !closing ) { + pollfd fds[ ARRAY_COUNT( sockets ) + 1 ] = { }; + nfds_t num_fds = 1; + + fds[ 0 ].fd = ConnectionNumber( UI.display ); + fds[ 0 ].events = POLLIN; + + for( Socket sock : sockets ) { + if( sock.in_use ) { + fds[ num_fds ].fd = sock.sock.fd; + fds[ num_fds ].events = POLLIN; + num_fds++; + } + } + + int ok = poll( fds, num_fds, 500 ); + if( ok == -1 ) + FATAL( "poll" ); + + if( ok == 0 ) { + script_fire_intervals(); + continue; + } + + for( size_t i = 1; i < ARRAY_COUNT( fds ); i++ ) { + if( fds[ i ].revents & POLLIN ) { + Socket * sock = socket_from_fd( fds[ i ].fd ); + assert( sock != NULL ); + + char buf[ 8192 ]; + size_t n; + TCPRecvResult res = net_recv( sock->sock, buf, sizeof( buf ), &n ); + if( res == TCP_OK ) { + script_socketData( sock, buf, n ); + } + else if( res == TCP_CLOSED ) { + script_socketData( sock, NULL, 0 ); + } + else { + FATAL( "net_recv" ); + } + } + } + + if( fds[ 0 ].events & POLLIN ) { + ui_handleXEvents(); + } + } +}