xwords/xwords4/linux/linuxsms.c
Eric House 87418d63d1 fix linux "sms" and modify script to test it
My linux sms hack used inotify and didn't check for messages that were
there when the app launched. Replace inotify with a simple glib periodic
timer. A bit of latency mimics SMS better anyway. Update test script to
support SMS, and add params to and otherwise fix linux client so
everything works.
2018-07-05 21:22:20 -07:00

458 lines
14 KiB
C

/* -*- compile-command: "make -j MEMDEBUG=TRUE";-*- */
/*
* Copyright 2006 - 2018 by Eric House (xwords@eehouse.org). All rights
* reserved.
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License
* as published by the Free Software Foundation; either version 2
* of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
*/
#ifdef XWFEATURE_SMS
#include <errno.h>
#include <glib.h>
#include <glib/gstdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include "linuxsms.h"
#include "strutils.h"
#include "linuxmain.h"
#define SMS_DIR "/tmp/xw_sms"
#define LOCK_FILE ".lock"
/* The idea here is to mimic an SMS-based transport using files. We'll use a
* directory in /tmp to hold "messages", in files with names based on phone
* number and port. Servers and clients will listen for changes in that
* directory and "receive" messages when a change is noted to a file with
* their phone number and on the port they listen to.
*
* Server's phone number is known to both server and client and passed on the
* commandline. Client's "phone number" is the PID of the client process.
* When the client sends to the server the phone number is passed in and its
* return (own) number is included in the "message".
*
* If I'm the server, I create an empty file for each port I listen on. (Only
* one now....). Clients will append to that. Likewise, a client creates a
* file that it will listen on.
*
* Data is encoded using the same mechanism so size constraints can be
* checked.
*/
#define ADDR_FMT "from: %s %d\n"
typedef struct _LinSMSData {
XP_UCHAR myQueue[256];
XP_U16 myPort;
FILE* lock;
const gchar* myPhone;
const SMSProcs* procs;
void* procClosure;
} LinSMSData;
typedef enum { NONE, INVITE, DATA, DEATH, ACK, } SMS_CMD;
#define SMS_PROTO_VERSION 0
static LinSMSData* getStorage( LaunchParams* params );
static void writeHeader( XWStreamCtxt* stream, SMS_CMD cmd );
static gint check_for_files( gpointer data );
static gint check_for_files_once( gpointer data );
static void
formatQueuePath( const XP_UCHAR* phone, XP_U16 port, XP_UCHAR* path,
XP_U16 pathlen )
{
XP_ASSERT( 0 != port );
snprintf( path, pathlen, "%s/%s_%d", SMS_DIR, phone, port );
}
static void
lock_queue( LinSMSData* storage )
{
gchar* lock = g_strdup_printf( "%s/%s", storage->myQueue, LOCK_FILE );
FILE* fp = fopen( lock, "w" );
XP_ASSERT( !!fp );
XP_ASSERT( NULL == storage->lock );
storage->lock = fp;
g_free( lock );
}
static void
unlock_queue( LinSMSData* storage )
{
XP_ASSERT( NULL != storage->lock );
FILE* lock = storage->lock;
storage->lock = NULL;
fclose( lock );
}
static XP_S16
send_sms( LinSMSData* storage, XWStreamCtxt* stream,
const XP_UCHAR* phone, XP_U16 port )
{
const XP_U8* buf = stream_getPtr( stream );
XP_U16 buflen = stream_getSize( stream );
XP_LOGF( "%s(phone=%s, port=%d, len=%d)", __func__, phone,
port, buflen );
XP_S16 nSent = -1;
XP_ASSERT( !!storage );
char path[256];
lock_queue( storage );
#ifdef DEBUG
gchar* str64 = g_base64_encode( buf, buflen );
#endif
formatQueuePath( phone, port, path, sizeof(path) );
/* Random-number-based name is fine, as we pick based on age. */
int rint = makeRandomInt();
g_mkdir_with_parents( path, 0777 ); /* just in case */
int len = strlen( path );
snprintf( &path[len], sizeof(path)-len, "/%u", rint );
XP_UCHAR sms[buflen*2]; /* more like (buflen*4/3) */
XP_U16 smslen = sizeof(sms);
binToSms( sms, &smslen, buf, buflen );
XP_ASSERT( smslen == strlen(sms) );
XP_LOGF( "%s: writing msg to %s", __func__, path );
#ifdef DEBUG
XP_ASSERT( !strcmp( str64, sms ) );
g_free( str64 );
XP_U8 testout[buflen];
XP_U16 lenout = sizeof( testout );
XP_ASSERT( smsToBin( testout, &lenout, sms, smslen ) );
XP_ASSERT( lenout == buflen );
// valgrind doesn't like this; punting on figuring out
// XP_ASSERT( XP_MEMCMP( testout, buf, smslen ) );
#endif
FILE* fp = fopen( path, "w" );
XP_ASSERT( !!fp );
(void)fprintf( fp, ADDR_FMT, storage->myPhone, storage->myPort );
(void)fprintf( fp, "%s\n", sms );
fclose( fp );
sync();
unlock_queue( storage );
nSent = buflen;
return nSent;
} /* linux_sms_send */
static XP_S16
decodeAndDelete( LinSMSData* storage, const gchar* name,
XP_U8* buf, XP_U16 buflen, CommsAddrRec* addr )
{
LOG_FUNC();
XP_S16 nRead = -1;
gchar* path = g_strdup_printf( "%s/%s", storage->myQueue, name );
gchar* contents;
gsize length;
#ifdef DEBUG
gboolean success =
#endif
g_file_get_contents( path, &contents, &length, NULL );
XP_ASSERT( success );
unlink( path );
g_free( path );
char phone[32];
int port;
int matched = sscanf( contents, ADDR_FMT, phone, &port );
if ( 2 == matched ) {
gchar* eol = strstr( contents, "\n" );
*eol = '\0';
XP_ASSERT( !*eol );
++eol; /* skip NULL */
*strstr(eol, "\n" ) = '\0';
XP_U16 inlen = strlen(eol); /* skip \n */
XP_U8 out[inlen];
XP_U16 outlen = sizeof(out);
XP_Bool valid = smsToBin( out, &outlen, eol, inlen );
if ( valid && outlen <= buflen ) {
XP_MEMCPY( buf, out, outlen );
nRead = outlen;
addr_setType( addr, COMMS_CONN_SMS );
XP_STRNCPY( addr->u.sms.phone, phone, sizeof(addr->u.sms.phone) );
XP_LOGF( "%s: message came from phone: %s, port: %d", __func__, phone, port );
addr->u.sms.port = port;
}
} else {
XP_ASSERT(0);
}
g_free( contents );
LOG_RETURNF( "%d", nRead );
return nRead;
} /* decodeAndDelete */
static void
dispatch_invite( LinSMSData* storage, XP_U16 XP_UNUSED(proto),
XWStreamCtxt* stream, CommsAddrRec* addr )
{
XP_UCHAR gameName[256];
XP_UCHAR dictName[256];
XP_U32 gameID = stream_getU32( stream );
XP_LOGF( "%s: got gameID %d", __func__, gameID );
stringFromStreamHere( stream, gameName, VSIZE(gameName) );
XP_U32 dictLang = stream_getU32( stream );
stringFromStreamHere( stream, dictName, VSIZE(dictName) );
XP_U8 nMissing = stream_getU8( stream );
XP_U8 nPlayers = stream_getU8( stream );
XP_U16 forceChannel = stream_getU8( stream );
addrFromStream( addr, stream );
(*storage->procs->inviteReceived)( storage->procClosure, gameName,
gameID, dictLang, dictName, nPlayers,
nMissing, forceChannel, addr );
}
static void
dispatch_data( LinSMSData* storage, XP_U16 XP_UNUSED(proto),
XWStreamCtxt* stream, const CommsAddrRec* addr )
{
XP_U32 gameID = stream_getU32( stream );
XP_U16 len = stream_getSize( stream );
XP_U8 data[len];
stream_getBytes( stream, data, len );
(*storage->procs->msgReceived)( storage->procClosure, addr, gameID,
data, len );
}
static void
parseAndDispatch( LaunchParams* params, uint8_t* buf, int len,
CommsAddrRec* addr )
{
LinSMSData* storage = getStorage( params );
XWStreamCtxt* stream = mem_stream_make_raw( MPPARM(params->mpool)
params->vtMgr );
stream_setVersion( stream, CUR_STREAM_VERS );
stream_putBytes( stream, buf, len );
XP_U8 proto = stream_getU8( stream );
XP_ASSERT( SMS_PROTO_VERSION == proto );
XP_U8 cmd = stream_getU8( stream );
switch( cmd ) {
case INVITE:
dispatch_invite( storage, proto, stream, addr );
break;
case DATA:
dispatch_data( storage, proto, stream, addr );
break;
case DEATH:
case ACK:
break;
default:
XP_ASSERT( 0 );
}
stream_destroy( stream );
}
void
linux_sms_init( LaunchParams* params, const gchar* myPhone, XP_U16 myPort,
const SMSProcs* procs, void* procClosure )
{
LOG_FUNC();
XP_ASSERT( !!myPhone );
LinSMSData* storage = getStorage( params );
XP_ASSERT( !!storage );
storage->myPhone = myPhone;
storage->myPort = myPort;
storage->procs = procs;
storage->procClosure = procClosure;
formatQueuePath( myPhone, myPort, storage->myQueue, sizeof(storage->myQueue) );
XP_LOGF( "%s: my queue: %s", __func__, storage->myQueue );
storage->myPort = params->connInfo.sms.port;
(void)g_mkdir_with_parents( storage->myQueue, 0777 );
/* Look for preexisting or new files every half second. Easier than
inotify, especially when you add the need to handle files written while
not running. */
(void)g_idle_add( check_for_files_once, params );
(void)g_timeout_add( 500, check_for_files, params );
} /* linux_sms_init */
void
linux_sms_invite( LaunchParams* params, const CurGameInfo* gi,
const CommsAddrRec* addr, const gchar* gameName,
XP_U16 nMissing, int forceChannel,
const gchar* toPhone, int toPort )
{
LOG_FUNC();
XWStreamCtxt* stream;
stream = mem_stream_make_raw( MPPARM(params->mpool) params->vtMgr );
writeHeader( stream, INVITE );
stream_putU32( stream, gi->gameID );
stringToStream( stream, gameName );
stream_putU32( stream, gi->dictLang );
stringToStream( stream, gi->dictName );
stream_putU8( stream, nMissing );
stream_putU8( stream, gi->nPlayers );
stream_putU8( stream, forceChannel );
addrToStream( stream, addr );
LinSMSData* storage = getStorage( params );
send_sms( storage, stream, toPhone, toPort );
stream_destroy( stream );
}
XP_S16
linux_sms_send( LaunchParams* params, const XP_U8* buf,
XP_U16 buflen, const XP_UCHAR* phone, XP_U16 port,
XP_U32 gameID )
{
XP_LOGF( "%s(len=%d)", __func__, buflen );
XWStreamCtxt* stream = mem_stream_make_raw( MPPARM(params->mpool)
params->vtMgr );
writeHeader( stream, DATA );
stream_putU32( stream, gameID );
stream_putBytes( stream, buf, buflen );
LinSMSData* storage = getStorage( params );
if ( 0 >= send_sms( storage, stream, phone, port ) ) {
buflen = -1;
}
stream_destroy( stream );
return buflen;
}
static gint
check_for_files( gpointer data )
{
check_for_files_once( data );
return TRUE;
}
static gint
check_for_files_once( gpointer data )
{
LOG_FUNC();
LaunchParams* params = (LaunchParams*)data;
LinSMSData* storage = getStorage( params );
for ( ; ; ) {
lock_queue( storage );
char oldestFile[256] = { '\0' };
struct timespec oldestModTime;
GDir* dir = g_dir_open( storage->myQueue, 0, NULL );
XP_LOGF( "%s: opening queue %s", __func__, storage->myQueue );
for ( ; ; ) {
const gchar* name = g_dir_read_name( dir );
if ( NULL == name ) {
break;
} else if ( 0 == strcmp( name, LOCK_FILE ) ) {
continue;
}
/* We want the oldest file first. Timestamp comes from stat(). */
struct stat statbuf;
char fullPath[500];
snprintf( fullPath, sizeof(fullPath), "%s/%s", storage->myQueue, name );
int err = stat( fullPath, &statbuf );
if ( err != 0 ) {
XP_LOGF( "%s(); %d from stat (error: %s)", __func__,
err, strerror(errno) );
XP_ASSERT( 0 );
} else {
XP_Bool replace = !oldestFile[0]; /* always replace empty/unset :-) */
if ( !replace ) {
if (statbuf.st_mtim.tv_sec == oldestModTime.tv_sec ) {
replace = statbuf.st_mtim.tv_nsec < oldestModTime.tv_nsec;
} else {
replace = statbuf.st_mtim.tv_sec < oldestModTime.tv_sec;
}
}
if ( replace ) {
oldestModTime = statbuf.st_mtim;
if ( !!oldestFile[0] ) {
XP_LOGF( "%s(): replacing %s with older %s", __func__, oldestFile, name );
}
snprintf( oldestFile, sizeof(oldestFile), "%s", name );
}
}
}
g_dir_close( dir );
uint8_t buf[256];
CommsAddrRec fromAddr = {0};
XP_S16 nRead = -1;
if ( !!oldestFile[0] ) {
nRead = decodeAndDelete( storage, oldestFile, buf,
sizeof(buf), &fromAddr );
} else {
XP_LOGF( "%s: no file found", __func__ );
}
unlock_queue( storage );
if ( 0 >= nRead ) {
break;
}
parseAndDispatch( params, buf, nRead, &fromAddr );
}
return FALSE;
} /* check_for_files_once */
void
linux_sms_cleanup( LaunchParams* params )
{
XP_FREEP( params->mpool, &params->smsStorage );
}
static LinSMSData*
getStorage( LaunchParams* params )
{
LinSMSData* storage = (LinSMSData*)params->smsStorage;
if ( NULL == storage ) {
storage = XP_CALLOC( params->mpool, sizeof(*storage) );
params->smsStorage = storage;
}
return storage;
}
static void
writeHeader( XWStreamCtxt* stream, SMS_CMD cmd )
{
stream_putU8( stream, SMS_PROTO_VERSION );
stream_putU8( stream, cmd );
}
#endif /* XWFEATURE_SMS */