diff --git a/xwords4/android/app/build.gradle b/xwords4/android/app/build.gradle index 563cce10e..b170c5e9a 100644 --- a/xwords4/android/app/build.gradle +++ b/xwords4/android/app/build.gradle @@ -1,6 +1,6 @@ def INITIAL_CLIENT_VERS = 8 -def VERSION_CODE_BASE = 126 -def VERSION_NAME = '4.4.130' +def VERSION_CODE_BASE = 127 +def VERSION_NAME = '4.4.131' def FABRIC_API_KEY = System.getenv("FABRIC_API_KEY") def GCM_SENDER_ID = System.getenv("GCM_SENDER_ID") def BUILD_INFO_NAME = "build-info.txt" @@ -74,6 +74,19 @@ android { buildConfigField "String", "GCM_SENDER_ID", "\"$GCM_SENDER_ID\"" } + + xw4fdroid { + dimension "variant" + applicationId "org.eehouse.android.xw4" + manifestPlaceholders = [ APP_ID: applicationId ] + resValue "string", "app_name", "CrossWords" + resValue "string", "nbs_port", "3344" + resValue "string", "invite_prefix", "/and/" + buildConfigField "boolean", "WIDIR_ENABLED", "false" + buildConfigField "boolean", "RELAYINVITE_SUPPORTED", "false" + + buildConfigField "String", "GCM_SENDER_ID", "\"\"" + } xw4d { dimension "variant" minSdkVersion 8 @@ -158,6 +171,14 @@ android { jniLibs.srcDir "../libs-xw4dDebug" } } + xw4fdroid { + release { + jniLibs.srcDir "../libs-xw4fdroidRelease" + } + debug { + jniLibs.srcDir "../libs-xw4fdroidDebug" + } + } } lintOptions { @@ -176,7 +197,7 @@ android { dependencies { // Look into replacing this with a fetch too PENDING - compile files('../libs/gcm.jar') + xw4Compile files('../libs/gcm.jar') compile 'com.android.support:support-v4:23.4.0' diff --git a/xwords4/android/app/src/main/assets/changes.html b/xwords4/android/app/src/main/assets/changes.html index 09d30d22e..5ee93b9c0 100644 --- a/xwords4/android/app/src/main/assets/changes.html +++ b/xwords4/android/app/src/main/assets/changes.html @@ -13,9 +13,9 @@ -

CrossWords 4.4.130 release

+

CrossWords 4.4.131 release

-

This release makes a couple of small UI tweaks.

+

An F-Droid-only release meeting new requirements

Please take @@ -25,12 +25,10 @@

New with this release

(The full changelog diff --git a/xwords4/android/app/src/main/java/org/eehouse/android/xw4/BoardDelegate.java b/xwords4/android/app/src/main/java/org/eehouse/android/xw4/BoardDelegate.java index b90febc40..7a498c8b1 100644 --- a/xwords4/android/app/src/main/java/org/eehouse/android/xw4/BoardDelegate.java +++ b/xwords4/android/app/src/main/java/org/eehouse/android/xw4/BoardDelegate.java @@ -206,23 +206,16 @@ public class BoardDelegate extends DelegateBase ab.setNegativeButton( R.string.button_rematch, lstnr ); // If we're not already in the "archive" group, offer to move - final String archiveName = LocUtils - .getString( m_activity, R.string.group_name_archive ); - final long archiveGroup = DBUtils.getGroup( m_activity, archiveName ); - long curGroup = DBUtils.getGroupForGame( m_activity, m_rowid ); - if ( curGroup != archiveGroup ) { + if ( !inArchiveGroup() ) { lstnr = new OnClickListener() { public void onClick( DialogInterface dlg, int whichButton ) { - makeNotAgainBuilder( R.string.not_again_archive, - R.string.key_na_archive, - Action.ARCHIVE_ACTION ) - .setParams( archiveName, archiveGroup ) - .show(); + showArchiveNA(); } }; ab.setNeutralButton( R.string.button_archive, lstnr ); } + } else if ( DlgID.DLG_CONNSTAT == dlgID && BuildConfig.DEBUG && null != m_connTypes && (m_connTypes.contains( CommsConnType.COMMS_CONN_RELAY ) @@ -847,6 +840,9 @@ public class BoardDelegate extends DelegateBase enable = m_gameOver && rematchSupported( false ); Utils.setItemVisible( menu, R.id.board_menu_rematch, enable ); + enable = m_gameOver && !inArchiveGroup(); + Utils.setItemVisible( menu, R.id.board_menu_archive, enable ); + boolean netGame = null != m_gi && DeviceRole.SERVER_STANDALONE != m_gi.serverRole; Utils.setItemVisible( menu, R.id.gamel_menu_checkmoves, netGame ); @@ -890,6 +886,10 @@ public class BoardDelegate extends DelegateBase doRematchIf(); break; + case R.id.board_menu_archive: + showArchiveNA(); + break; + case R.id.board_menu_trade_commit: cmd = JNICmd.CMD_COMMIT; break; @@ -1112,9 +1112,7 @@ public class BoardDelegate extends DelegateBase break; case ARCHIVE_ACTION: - String archiveName = (String)params[0]; - long archiveGroup = (Long)params[1]; - archiveAndClose( archiveName, archiveGroup ); + archiveAndClose(); break; case ENABLE_SMS_DO: @@ -2600,12 +2598,33 @@ public class BoardDelegate extends DelegateBase return wordsArray; } - private void archiveAndClose( String archiveName, long groupID ) + private boolean inArchiveGroup() { - if ( DBUtils.GROUPID_UNSPEC == groupID ) { - groupID = DBUtils.addGroup( m_activity, archiveName ); + String archiveName = LocUtils + .getString( m_activity, R.string.group_name_archive ); + long archiveGroup = DBUtils.getGroup( m_activity, archiveName ); + long curGroup = DBUtils.getGroupForGame( m_activity, m_rowid ); + return curGroup == archiveGroup; + } + + private void showArchiveNA() + { + makeNotAgainBuilder( R.string.not_again_archive, + R.string.key_na_archive, + Action.ARCHIVE_ACTION ) + .show(); + } + + private void archiveAndClose() + { + String archiveName = LocUtils + .getString( m_activity, R.string.group_name_archive ); + long archiveGroupID = DBUtils.getGroup( m_activity, archiveName ); + + if ( DBUtils.GROUPID_UNSPEC == archiveGroupID ) { + archiveGroupID = DBUtils.addGroup( m_activity, archiveName ); } - DBUtils.moveGame( m_activity, m_rowid, groupID ); + DBUtils.moveGame( m_activity, m_rowid, archiveGroupID ); waitCloseGame( false ); finish(); } diff --git a/xwords4/android/app/src/main/java/org/eehouse/android/xw4/DevID.java b/xwords4/android/app/src/main/java/org/eehouse/android/xw4/DevID.java index 2ba88cb9d..8a39bac03 100644 --- a/xwords4/android/app/src/main/java/org/eehouse/android/xw4/DevID.java +++ b/xwords4/android/app/src/main/java/org/eehouse/android/xw4/DevID.java @@ -31,8 +31,6 @@ package org.eehouse.android.xw4; import android.content.Context; -import com.google.android.gcm.GCMRegistrar; - public class DevID { private static final String TAG = DevID.class.getSimpleName(); @@ -137,7 +135,7 @@ public class DevID { if ( 0 != storedVers && storedVers < curVers ) { result = ""; // Don't trust what registrar has } else { - result = GCMRegistrar.getRegistrationId( context ); + result = GCMStub.getRegistrationId( context ); } return result; } diff --git a/xwords4/android/app/src/main/java/org/eehouse/android/xw4/DictsDelegate.java b/xwords4/android/app/src/main/java/org/eehouse/android/xw4/DictsDelegate.java index 726fe693a..7f1832fe5 100644 --- a/xwords4/android/app/src/main/java/org/eehouse/android/xw4/DictsDelegate.java +++ b/xwords4/android/app/src/main/java/org/eehouse/android/xw4/DictsDelegate.java @@ -1238,7 +1238,7 @@ public class DictsDelegate extends ListDelegateBase // parse less data String name = null; String proc = String.format( "listDicts?lc=%s", m_lc ); - HttpURLConnection conn = NetUtils.makeHttpConn( m_context, proc ); + HttpURLConnection conn = NetUtils.makeHttpUpdateConn( m_context, proc ); if ( null != conn ) { JSONObject theOne = null; String langName = null; @@ -1320,7 +1320,7 @@ public class DictsDelegate extends ListDelegateBase public Boolean doInBackground( Void... unused ) { boolean success = false; - HttpURLConnection conn = NetUtils.makeHttpConn( m_context, "listDicts" ); + HttpURLConnection conn = NetUtils.makeHttpUpdateConn( m_context, "listDicts" ); if ( null != conn ) { String json = NetUtils.runConn( conn, new JSONObject() ); if ( !isCancelled() ) { diff --git a/xwords4/android/app/src/main/java/org/eehouse/android/xw4/NetUtils.java b/xwords4/android/app/src/main/java/org/eehouse/android/xw4/NetUtils.java index 78c43b68e..acf6ac4be 100644 --- a/xwords4/android/app/src/main/java/org/eehouse/android/xw4/NetUtils.java +++ b/xwords4/android/app/src/main/java/org/eehouse/android/xw4/NetUtils.java @@ -25,6 +25,8 @@ import android.text.TextUtils; import junit.framework.Assert; +import org.json.JSONArray; +import org.json.JSONException; import org.json.JSONObject; import java.io.BufferedInputStream; @@ -88,49 +90,29 @@ public class NetUtils { m_obits = obits; } - public void run() { - Socket socket = makeProxySocket( m_context, 10000 ); - if ( null != socket ) { - int strLens = 0; - int nObits = 0; + @Override + public void run() + { + try { + JSONArray params = new JSONArray(); for ( int ii = 0; ii < m_obits.length; ++ii ) { - String relayID = m_obits[ii].m_relayID; - if ( null != relayID ) { - ++nObits; - strLens += relayID.length() + 1; // 1 for /n - } + JSONObject one = new JSONObject(); + one.put( "relayID", m_obits[ii].m_relayID ); + one.put( "seed", m_obits[ii].m_seed ); + params.put( one ); } + HttpURLConnection conn = makeHttpRelayConn( m_context, "kill" ); + String resStr = runConn( conn, params ); + Log.d( TAG, "runViaWeb(): kill(%s) => %s", params, resStr ); - try { - DataOutputStream outStream = - new DataOutputStream( socket.getOutputStream() ); - outStream.writeShort( 2 + 2 + (2*nObits) + strLens ); - outStream.writeByte( NetUtils.PROTOCOL_VERSION ); - outStream.writeByte( NetUtils.PRX_DEVICE_GONE ); - outStream.writeShort( m_obits.length ); - - for ( int ii = 0; ii < m_obits.length; ++ii ) { - String relayID = m_obits[ii].m_relayID; - if ( null != relayID ) { - outStream.writeShort( m_obits[ii].m_seed ); - outStream.writeBytes( relayID ); - outStream.write( '\n' ); - } - } - - outStream.flush(); - - DataInputStream dis = - new DataInputStream( socket.getInputStream() ); - short resLen = dis.readShort(); - socket.close(); - - if ( resLen == 0 ) { + if ( null != resStr ) { + JSONObject result = new JSONObject( resStr ); + if ( 0 == result.optInt( "err", -1 ) ) { DBUtils.clearObits( m_context, m_obits ); } - } catch ( java.io.IOException ioe ) { - Log.ex( TAG, ioe ); } + } catch ( JSONException ex ) { + Assert.assertFalse( BuildConfig.DEBUG ); } } } @@ -139,8 +121,7 @@ public class NetUtils { { DBUtils.Obit[] obits = DBUtils.listObits( context ); if ( null != obits && 0 < obits.length ) { - InformThread thread = new InformThread( context, obits ); - thread.start(); + new InformThread( context, obits ).start(); } } @@ -214,14 +195,26 @@ public class NetUtils { return host; } - protected static HttpURLConnection makeHttpConn( Context context, - String proc ) + protected static HttpURLConnection makeHttpRelayConn( Context context, + String proc ) + { + String url = XWPrefs.getDefaultRelayUrl( context ); + return makeHttpConn( context, url, proc ); + } + + protected static HttpURLConnection makeHttpUpdateConn( Context context, + String proc ) + { + String url = XWPrefs.getDefaultUpdateUrl( context ); + return makeHttpConn( context, url, proc ); + } + + private static HttpURLConnection makeHttpConn( Context context, + String path, String proc ) { HttpURLConnection result = null; try { - String url = String.format( "%s/%s", - XWPrefs.getDefaultUpdateUrl( context ), - proc ); + String url = String.format( "%s/%s", path, proc ); result = (HttpURLConnection)new URL(url).openConnection(); } catch ( java.net.MalformedURLException mue ) { Assert.assertNull( result ); @@ -233,11 +226,21 @@ public class NetUtils { return result; } + protected static String runConn( HttpURLConnection conn, JSONArray param ) + { + return runConn( conn, param.toString() ); + } + protected static String runConn( HttpURLConnection conn, JSONObject param ) + { + return runConn( conn, param.toString() ); + } + + private static String runConn( HttpURLConnection conn, String param ) { String result = null; Map params = new HashMap(); - params.put( k_PARAMS, param.toString() ); + params.put( k_PARAMS, param ); String paramsString = getPostDataString( params ); if ( null != paramsString ) { @@ -273,7 +276,8 @@ public class NetUtils { } result = new String( bas.toByteArray() ); } else { - Log.w( TAG, "runConn: responseCode: %d", responseCode ); + Log.w( TAG, "runConn: responseCode: %d for url: %s", + responseCode, conn.getURL() ); } } catch ( java.net.ProtocolException pe ) { Log.ex( TAG, pe ); @@ -285,17 +289,18 @@ public class NetUtils { return result; } + // This handles multiple params but only every gets passed one! private static String getPostDataString( Map params ) { String result = null; try { ArrayList pairs = new ArrayList(); // StringBuilder sb = new StringBuilder(); - String[] pair = { null, null }; + // String[] pair = { null, null }; for ( Map.Entry entry : params.entrySet() ){ - pair[0] = URLEncoder.encode( entry.getKey(), "UTF-8" ); - pair[1] = URLEncoder.encode( entry.getValue(), "UTF-8" ); - pairs.add( TextUtils.join( "=", pair ) ); + pairs.add( URLEncoder.encode( entry.getKey(), "UTF-8" ) + + "=" + + URLEncoder.encode( entry.getValue(), "UTF-8" ) ); } result = TextUtils.join( "&", pairs ); } catch ( java.io.UnsupportedEncodingException uee ) { diff --git a/xwords4/android/app/src/main/java/org/eehouse/android/xw4/RelayService.java b/xwords4/android/app/src/main/java/org/eehouse/android/xw4/RelayService.java index 89c24ebb4..984e57840 100644 --- a/xwords4/android/app/src/main/java/org/eehouse/android/xw4/RelayService.java +++ b/xwords4/android/app/src/main/java/org/eehouse/android/xw4/RelayService.java @@ -26,6 +26,7 @@ import android.content.Intent; import android.os.AsyncTask; import android.os.Build; import android.os.Handler; +import android.text.TextUtils; import junit.framework.Assert; @@ -38,6 +39,10 @@ import org.eehouse.android.xw4.jni.UtilCtxt.DevIDType; import org.eehouse.android.xw4.jni.XwJNI; import org.eehouse.android.xw4.loc.LocUtils; +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; + import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.DataInputStream; @@ -46,13 +51,16 @@ import java.io.InputStream; import java.io.OutputStream; import java.net.DatagramPacket; import java.net.DatagramSocket; +import java.net.HttpURLConnection; import java.net.InetAddress; import java.net.Socket; import java.util.ArrayList; import java.util.HashMap; -import java.util.HashSet; import java.util.Iterator; +import java.util.List; +import java.util.Map; import java.util.concurrent.LinkedBlockingQueue; +import java.util.concurrent.atomic.AtomicInteger; public class RelayService extends XWService implements NetStateCache.StateChangedIf { @@ -60,6 +68,7 @@ public class RelayService extends XWService private static final int MAX_SEND = 1024; private static final int MAX_BUF = MAX_SEND - 2; private static final int REG_WAIT_INTERVAL = 10; + private static final int INITIAL_BACKOFF = 5; // One day, in seconds. Probably should be configurable. private static final long MAX_KEEPALIVE_SECS = 24 * 60 * 60; @@ -90,8 +99,9 @@ public class RelayService extends XWService private static final String ROWID = "ROWID"; private static final String BINBUFFER = "BINBUFFER"; - private static HashSet s_packetsSent = new HashSet(); - private static int s_nextPacketID = 1; + private static Map s_packetsSentUDP = new HashMap<>(); + private static Map s_packetsSentWeb = new HashMap<>(); + private static AtomicInteger s_nextPacketID = new AtomicInteger(); private static boolean s_gcmWorking = false; private static boolean s_registered = false; private static CommsAddrRec s_addr = @@ -110,6 +120,8 @@ public class RelayService extends XWService private Runnable m_onInactivity; private int m_maxIntervalSeconds = 0; private long m_lastGamePacketReceived; + // m_nativeNotWorking: set to true if too many acks missed? + private boolean m_nativeNotWorking = false; private static DevIDType s_curType = DevIDType.ID_TYPE_NONE; private static long s_regStartTime = 0; @@ -160,7 +172,7 @@ public class RelayService extends XWService { boolean enabled = ! XWPrefs .getPrefsBoolean( context, R.string.key_disable_relay, false ); - Log.d( TAG, "relayEnabled() => %b", enabled ); + // Log.d( TAG, "relayEnabled() => %b", enabled ); return enabled; } @@ -403,7 +415,7 @@ public class RelayService extends XWService byte[][][] msgss = expandMsgsArray( intent ); for ( byte[][] msgs : msgss ) { for ( byte[] msg : msgs ) { - gotPacket( msg, true ); + gotPacket( msg, true, false ); } } break; @@ -449,7 +461,7 @@ public class RelayService extends XWService case TIMER_FIRED: if ( !NetStateCache.netAvail( this ) ) { Log.w( TAG, "not connecting: no network" ); - } else if ( startFetchThreadIf() ) { + } else if ( startFetchThreadIfNotUDP() ) { // do nothing } else if ( registerWithRelayIfNot() ) { requestMessages(); @@ -510,9 +522,9 @@ public class RelayService extends XWService } } - private boolean startFetchThreadIf() + private boolean startFetchThreadIfNotUDP() { - // DbgUtils.logf( "startFetchThreadIf()" ); + // DbgUtils.logf( "startFetchThreadIfNotUDP()" ); boolean handled = relayEnabled( this ) && !XWApp.UDP_ENABLED; if ( handled && null == m_fetchThread ) { m_fetchThread = new Thread( null, new Runnable() { @@ -601,6 +613,15 @@ public class RelayService extends XWService } } + private boolean skipNativeSend() + { + boolean skip = m_nativeNotWorking; + if ( ! skip ) { + skip = XWPrefs.getSkipToWebAPI( RelayService.this ); + } + return skip; + } + private void startWriteThread() { if ( null == m_UDPWriteThread ) { @@ -608,46 +629,38 @@ public class RelayService extends XWService public void run() { Log.i( TAG, "write thread starting" ); for ( ; ; ) { - PacketData outData; + boolean exitNow = false; + boolean useWeb = skipNativeSend(); + List dataListUDP = new ArrayList<>(); + List dataListWeb = new ArrayList<>(); try { - outData = m_queue.take(); + for ( PacketData outData = m_queue.take(); // blocks + null != outData; + outData = m_queue.poll() ) { // doesn't block + if ( outData.isEOQ() ) { + exitNow = true; + break; + } + if ( useWeb || outData.getForWeb() ) { + dataListWeb.add(outData); + } else { + dataListUDP.add(outData); + } + } } catch ( InterruptedException ie ) { Log.w( TAG, "write thread killed" ); break; } - if ( null == outData - || 0 == outData.getLength() ) { + if ( exitNow ) { Log.i( TAG, "stopping write thread" ); break; } - try { - DatagramPacket outPacket = outData.assemble(); - m_UDPSocket.send( outPacket ); - int pid = outData.m_packetID; - Log.d( TAG, "Sent udp packet, cmd=%s, id=%d," - + " of length %d", - outData.m_cmd.toString(), - pid, outPacket.getLength()); - synchronized( s_packetsSent ) { - s_packetsSent.add( pid ); - } - resetExitTimer(); - ConnStatusHandler.showSuccessOut(); - } catch ( java.net.SocketException se ) { - Log.ex( TAG, se ); - Log.i( TAG, "Restarting threads to force" - + " new socket" ); - m_handler.post( new Runnable() { - public void run() { - stopUDPThreadsIf(); - } - } ); - } catch ( java.io.IOException ioe ) { - Log.ex( TAG, ioe ); - } catch ( NullPointerException npe ) { - Log.w( TAG, "network problem; dropping packet" ); - } + sendViaWeb( dataListWeb ); + sendViaUDP( dataListUDP ); + + resetExitTimer(); + ConnStatusHandler.showSuccessOut(); } Log.i( TAG, "write thread exiting" ); } @@ -659,8 +672,138 @@ public class RelayService extends XWService } } + private int sendViaWeb( List packets ) + { + Log.d( TAG, "sendViaWeb(): sending %d at once", packets.size() ); + int sentLen = 0; + if ( packets.size() > 0 ) { + HttpURLConnection conn = NetUtils.makeHttpRelayConn( this, "post" ); + if ( null == conn ) { + Log.e( TAG, "sendViaWeb(): null conn for POST" ); + } else { + try { + JSONArray dataArray = new JSONArray(); + for ( PacketData packet : packets ) { + Assert.assertFalse( packet.isEOQ() ); + byte[] datum = packet.assemble(); + dataArray.put( Utils.base64Encode(datum) ); + sentLen += datum.length; + } + JSONObject params = new JSONObject(); + params.put( "data", dataArray ); + + String result = NetUtils.runConn(conn, params); + if ( null != result ) { + Log.d( TAG, "sendViaWeb(): POST(%s) => %s", params, result ); + JSONObject resultObj = new JSONObject( result ); + JSONArray resData = resultObj.getJSONArray( "data" ); + int nReplies = resData.length(); + Log.d( TAG, "sendViaWeb(): got %d replies", nReplies ); + + noteSent( packets, s_packetsSentWeb ); // before we process the acks below :-) + + for ( int ii = 0; ii < nReplies; ++ii ) { + byte[] datum = Utils.base64Decode( resData.getString( ii ) ); + // PENDING: skip ack or not + gotPacket( datum, false, false ); + } + } else { + Log.e( TAG, "sendViaWeb(): failed result for POST" ); + } + } catch ( JSONException ex ) { + Assert.assertFalse( BuildConfig.DEBUG ); + } + } + } + return sentLen; + } + + private int sendViaUDP( List packets ) + { + int sentLen = 0; + for ( PacketData packet : packets ) { + boolean getOut = true; + byte[] data = packet.assemble(); + try { + DatagramPacket udpPacket = new DatagramPacket( data, data.length ); + m_UDPSocket.send( udpPacket ); + + sentLen += udpPacket.getLength(); + noteSent( packet, s_packetsSentUDP ); + getOut = false; + } catch ( java.net.SocketException se ) { + Log.ex( TAG, se ); + Log.i( TAG, "Restarting threads to force" + + " new socket" ); + m_handler.post( new Runnable() { + public void run() { + stopUDPThreadsIf(); + } + } ); + } catch ( java.io.IOException ioe ) { + Log.ex( TAG, ioe ); + } catch ( NullPointerException npe ) { + Log.w( TAG, "network problem; dropping packet" ); + } + if ( getOut ) { + break; + } + } + + if ( sentLen > 0 ) { + startAckTimer( packets ); + } + + return sentLen; + } + + private void startAckTimer( final List packets ) + { + Runnable ackTimer = new Runnable() { + @Override + public void run() { + List forResend = new ArrayList<>(); + Log.d( TAG, "ackTimer.run() called" ); + synchronized ( s_packetsSentUDP ) { + for ( PacketData packet : packets ) { + PacketData stillThere = s_packetsSentUDP.remove(packet.m_packetID); + if ( stillThere != null ) { + Log.d( TAG, "packed %d not yet acked; resending", + stillThere.m_packetID ); + stillThere.setForWeb(); + forResend.add( stillThere ); + } + } + } + m_queue.addAll( forResend ); + } + }; + m_handler.postDelayed( ackTimer, 10 * 1000 ); + } + + private void noteSent( PacketData packet, Map map ) + { + int pid = packet.m_packetID; + Log.d( TAG, "Sent [udp?] packet: cmd=%s, id=%d", + packet.m_cmd.toString(), pid ); + if ( packet.m_cmd != XWRelayReg.XWPDEV_ACK ) { + synchronized( map ) { + map.put( pid, packet ); + } + } + } + + private void noteSent( List packets, Map map ) + { + for ( PacketData packet : packets ) { + noteSent( packet, map ); + } + } + private void stopUDPThreadsIf() { + DbgUtils.assertOnUIThread(); + if ( null != m_UDPWriteThread ) { // can't add null m_queue.add( new PacketData() ); @@ -687,7 +830,7 @@ public class RelayService extends XWService } // MIGHT BE Running on reader thread - private void gotPacket( byte[] data, boolean skipAck ) + private void gotPacket( byte[] data, boolean skipAck, boolean fromUDP ) { boolean resetBackoff = false; ByteArrayInputStream bis = new ByteArrayInputStream( data ); @@ -766,7 +909,7 @@ public class RelayService extends XWService startService( intent ); break; case XWPDEV_ACK: - noteAck( vli2un( dis ) ); + noteAck( vli2un( dis ), fromUDP ); break; // case XWPDEV_MSGFWDOTHERS: // Assert.assertTrue( 0 == dis.readByte() ); // protocol; means "invite", I guess. @@ -795,7 +938,7 @@ public class RelayService extends XWService byte[] data = new byte[packetLen]; System.arraycopy( packet.getData(), 0, data, 0, packetLen ); // DbgUtils.logf( "RelayService::gotPacket: %d bytes of data", packetLen ); - gotPacket( data, false ); + gotPacket( data, false, true ); } // gotPacket private boolean shouldRegister() @@ -873,11 +1016,15 @@ public class RelayService extends XWService { ByteArrayOutputStream bas = new ByteArrayOutputStream(); try { - String devid = getDevID( null ); + DevIDType[] typp = new DevIDType[1]; + String devid = getDevID( typp ); if ( null != devid ) { DataOutputStream out = new DataOutputStream( bas ); writeVLIString( out, devid ); + Log.d(TAG, "requestMessagesImpl(): devid: %s; type: " + typp[0], devid ); postPacket( bas, reg ); + } else { + Log.d(TAG, "requestMessagesImpl(): devid is null" ); } } catch ( java.io.IOException ioe ) { Log.ex( TAG, ioe ); @@ -1080,6 +1227,7 @@ public class RelayService extends XWService @Override protected Void doInBackground( Void... ignored ) { + Assert.assertFalse( XWPrefs.getSkipToWebAPI( m_context ) ); // format: total msg lenth: 2 // number-of-relayIDs: 2 // for-each-relayid: relayid + '\n': varies @@ -1127,6 +1275,8 @@ public class RelayService extends XWService } // Now open a real socket, write size and proto, and // copy in the formatted buffer + + Assert.assertFalse( XWPrefs.getSkipToWebAPI( m_context ) ); Socket socket = NetUtils.makeProxySocket( m_context, 8000 ); if ( null != socket ) { DataOutputStream outStream = @@ -1203,23 +1353,31 @@ public class RelayService extends XWService { int nextPacketID = 0; if ( XWRelayReg.XWPDEV_ACK != cmd ) { - synchronized( s_packetsSent ) { - nextPacketID = ++s_nextPacketID; - } + nextPacketID = s_nextPacketID.incrementAndGet(); } return nextPacketID; } - private static void noteAck( int packetID ) + private static void noteAck( int packetID, boolean fromUDP ) { - synchronized( s_packetsSent ) { - if ( s_packetsSent.contains( packetID ) ) { - s_packetsSent.remove( packetID ); + PacketData packet; + Map map = fromUDP ? s_packetsSentUDP : s_packetsSentWeb; + synchronized( map ) { + packet = map.remove( packetID ); + if ( packet != null ) { + Log.d( TAG, "noteAck(fromUDP=%b): removed for id %d: %s", + fromUDP, packetID, packet ); } else { Log.w( TAG, "Weird: got ack %d but never sent", packetID ); } - Log.d( TAG, "noteAck(): Got ack for %d; there are %d unacked packets", - packetID, s_packetsSent.size() ); + if ( BuildConfig.DEBUG ) { + ArrayList pstrs = new ArrayList<>(); + for ( Integer pkid : map.keySet() ) { + pstrs.add( map.get(pkid).toString() ); + } + Log.d( TAG, "noteAck(fromUDP=%b): Got ack for %d; there are %d unacked packets: %s", + fromUDP, packetID, map.size(), TextUtils.join( ",", pstrs ) ); + } } } @@ -1245,7 +1403,7 @@ public class RelayService extends XWService registerWithRelay(); } else { stopUDPThreadsIf(); - startFetchThreadIf(); + startFetchThreadIfNotUDP(); } } @@ -1394,18 +1552,19 @@ public class RelayService extends XWService long now = Utils.getCurSeconds(); if ( s_curNextTimer <= now ) { if ( 0 == s_curBackoff ) { - s_curBackoff = 15; + s_curBackoff = INITIAL_BACKOFF; + } else { + s_curBackoff = Math.min( 2 * s_curBackoff, result ); } - s_curBackoff = Math.min( 2 * s_curBackoff, result ); s_curNextTimer += s_curBackoff; } diff = s_curNextTimer - now; } Assert.assertTrue( diff < Integer.MAX_VALUE ); - Log.d( TAG, "figureBackoffSeconds() => %d", diff ); - result = (int)diff; + result = (int)diff; } + Log.d( TAG, "figureBackoffSeconds() => %d", result ); return result; } @@ -1419,14 +1578,37 @@ public class RelayService extends XWService } private class PacketData { - public PacketData() { m_bas = null; } + public ByteArrayOutputStream m_bas; + public XWRelayReg m_cmd; + public byte[] m_header; + public int m_packetID; + private long m_created; + private boolean m_useWeb; + + public PacketData() { + m_bas = null; + m_created = System.currentTimeMillis(); + } public PacketData( ByteArrayOutputStream bas, XWRelayReg cmd ) { + this(); m_bas = bas; m_cmd = cmd; } + @Override + public String toString() + { + return String.format( "{cmd: %s; age: %d ms}", m_cmd, + System.currentTimeMillis() - m_created ); + } + + void setForWeb() { m_useWeb = true; } + boolean getForWeb() { return m_useWeb; } + + public boolean isEOQ() { return 0 == getLength(); } + public int getLength() { int result = 0; @@ -1439,13 +1621,13 @@ public class RelayService extends XWService return result; } - public DatagramPacket assemble() + public byte[] assemble() { - byte[] dest = new byte[getLength()]; - System.arraycopy( m_header, 0, dest, 0, m_header.length ); + byte[] data = new byte[getLength()]; + System.arraycopy( m_header, 0, data, 0, m_header.length ); byte[] basData = m_bas.toByteArray(); - System.arraycopy( basData, 0, dest, m_header.length, basData.length ); - return new DatagramPacket( dest, dest.length ); + System.arraycopy( basData, 0, data, m_header.length, basData.length ); + return data; } private void makeHeader() @@ -1464,10 +1646,5 @@ public class RelayService extends XWService Log.ex( TAG, ioe ); } } - - public ByteArrayOutputStream m_bas; - public XWRelayReg m_cmd; - public byte[] m_header; - public int m_packetID; } } diff --git a/xwords4/android/app/src/main/java/org/eehouse/android/xw4/UpdateCheckReceiver.java b/xwords4/android/app/src/main/java/org/eehouse/android/xw4/UpdateCheckReceiver.java index 1dbd678bc..75efe5abe 100644 --- a/xwords4/android/app/src/main/java/org/eehouse/android/xw4/UpdateCheckReceiver.java +++ b/xwords4/android/app/src/main/java/org/eehouse/android/xw4/UpdateCheckReceiver.java @@ -258,7 +258,8 @@ public class UpdateCheckReceiver extends BroadcastReceiver { @Override protected String doInBackground( Void... unused ) { - HttpURLConnection conn = NetUtils.makeHttpConn( m_context, "getUpdates" ); + HttpURLConnection conn + = NetUtils.makeHttpUpdateConn( m_context, "getUpdates" ); String json = null; if ( null != conn ) { json = NetUtils.runConn( conn, m_params ); diff --git a/xwords4/android/app/src/main/java/org/eehouse/android/xw4/XWPrefs.java b/xwords4/android/app/src/main/java/org/eehouse/android/xw4/XWPrefs.java index 80d16b1d2..6747f05a2 100644 --- a/xwords4/android/app/src/main/java/org/eehouse/android/xw4/XWPrefs.java +++ b/xwords4/android/app/src/main/java/org/eehouse/android/xw4/XWPrefs.java @@ -115,6 +115,16 @@ public class XWPrefs { return getPrefsString( context, R.string.key_update_url ); } + public static String getDefaultRelayUrl( Context context ) + { + return getPrefsString( context, R.string.key_relay_url ); + } + + public static boolean getSkipToWebAPI( Context context ) + { + return getPrefsBoolean( context, R.string.key_relay_via_http_first, false ); + } + public static int getDefaultProxyPort( Context context ) { String val = getPrefsString( context, R.string.key_proxy_port ); diff --git a/xwords4/android/app/src/main/java/org/eehouse/android/xw4/XWService.java b/xwords4/android/app/src/main/java/org/eehouse/android/xw4/XWService.java index c1f305fb1..fc2e666ae 100644 --- a/xwords4/android/app/src/main/java/org/eehouse/android/xw4/XWService.java +++ b/xwords4/android/app/src/main/java/org/eehouse/android/xw4/XWService.java @@ -82,7 +82,7 @@ class XWService extends Service { s_seen.add( inviteID ); } } - Log.d( TAG, "checkNotDupe(%s) => %b", inviteID, !isDupe ); + Log.d( TAG, "checkNotDupe('%s') => %b", inviteID, !isDupe ); return !isDupe; } diff --git a/xwords4/android/app/src/main/res/menu-small/board_menu.xml b/xwords4/android/app/src/main/res/menu-small/board_menu.xml index 49835d6a7..b553c5711 100644 --- a/xwords4/android/app/src/main/res/menu-small/board_menu.xml +++ b/xwords4/android/app/src/main/res/menu-small/board_menu.xml @@ -6,6 +6,13 @@ android:title="@string/board_menu_invite" /> + + + - + - + - - - + + + diff --git a/xwords4/android/app/src/main/res/menu/board_menu.xml b/xwords4/android/app/src/main/res/menu/board_menu.xml index f49b85f19..a027b9aaf 100644 --- a/xwords4/android/app/src/main/res/menu/board_menu.xml +++ b/xwords4/android/app/src/main/res/menu/board_menu.xml @@ -5,9 +5,15 @@ + diff --git a/xwords4/android/app/src/main/res/values/common_rsrc.xml b/xwords4/android/app/src/main/res/values/common_rsrc.xml index d029cb00f..50ab6a10b 100644 --- a/xwords4/android/app/src/main/res/values/common_rsrc.xml +++ b/xwords4/android/app/src/main/res/values/common_rsrc.xml @@ -36,7 +36,9 @@ key_relay_host key_relay_port2 + key_relay_via_http_first key_update_url + key_relay_url key_update_prerel key_proxy_port key_sms_port @@ -150,6 +152,7 @@ http://eehouse.org/and_wordlists http://eehouse.org/xw4/info.py + http://eehouse.org/xw4/relay.py diff --git a/xwords4/android/app/src/main/res/values/strings.xml b/xwords4/android/app/src/main/res/values/strings.xml index 713fd3211..57aa7015b 100644 --- a/xwords4/android/app/src/main/res/values/strings.xml +++ b/xwords4/android/app/src/main/res/values/strings.xml @@ -1709,7 +1709,9 @@ - Toolbar icons by Sarah Chu. + Toolbar icons by Sarah Chu. Navbar + icons from the Noun Project: \"archive\" by Trendy; \"rematch\" by + Becris; and \"swap\" by iconomania. @@ -2486,6 +2488,8 @@ For debugging You should never need these... Relay host + Use Web APIs first + (instead of as fallback for custom protocol) Wordlist download URL Enable logging (release builds only) @@ -2525,6 +2529,7 @@ gameid Pending packet count Update checks URL + URL for relay web API Fetch default wordlist for language Don\'t try a second time diff --git a/xwords4/android/app/src/main/res/xml/xwprefs.xml b/xwords4/android/app/src/main/res/xml/xwprefs.xml index 6b30a70fb..43dc96048 100644 --- a/xwords4/android/app/src/main/res/xml/xwprefs.xml +++ b/xwords4/android/app/src/main/res/xml/xwprefs.xml @@ -415,11 +415,29 @@ + + + + + + - + + + + + image/svg+xml + + 01 + + + + + + + + 01 + + + + + diff --git a/xwords4/android/img_src/rematch.svg b/xwords4/android/img_src/rematch.svg new file mode 100644 index 000000000..01cd1dd04 --- /dev/null +++ b/xwords4/android/img_src/rematch.svg @@ -0,0 +1,54 @@ + +image/svg+xml \ No newline at end of file diff --git a/xwords4/android/img_src/trade.svg b/xwords4/android/img_src/trade.svg new file mode 100644 index 000000000..7d1752d97 --- /dev/null +++ b/xwords4/android/img_src/trade.svg @@ -0,0 +1,50 @@ + +image/svg+xml \ No newline at end of file diff --git a/xwords4/android/jni/andutils.c b/xwords4/android/jni/andutils.c index c3e26f02e..a05931d71 100644 --- a/xwords4/android/jni/andutils.c +++ b/xwords4/android/jni/andutils.c @@ -754,7 +754,7 @@ android_debugf( const char* format, ... ) } (void)__android_log_write( ANDROID_LOG_DEBUG, -# if defined VARIANT_xw4 +# if defined VARIANT_xw4 || defined VARIANT_xw4fdroid "xw4" # elif defined VARIANT_xw4d "x4bg" diff --git a/xwords4/android/jni/xwjni.c b/xwords4/android/jni/xwjni.c index 6aaf43f31..9d09568a9 100644 --- a/xwords4/android/jni/xwjni.c +++ b/xwords4/android/jni/xwjni.c @@ -648,7 +648,7 @@ Java_org_eehouse_android_xw4_jni_XwJNI_comms_1getUUID { jstring jstr = #ifdef XWFEATURE_BLUETOOTH -# if defined VARIANT_xw4 +# if defined VARIANT_xw4 || defined VARIANT_xw4fdroid (*env)->NewStringUTF( env, XW_BT_UUID ) # elif defined VARIANT_xw4d (*env)->NewStringUTF( env, XW_BT_UUID_DBG ) diff --git a/xwords4/android/res_src/values-nb-rNO/strings.xml b/xwords4/android/res_src/values-nb-rNO/strings.xml index 6f27c4bcc..3c6613f48 100644 --- a/xwords4/android/res_src/values-nb-rNO/strings.xml +++ b/xwords4/android/res_src/values-nb-rNO/strings.xml @@ -284,7 +284,6 @@ SMS (tekstmelding) E-post Blåtann - Invitasjon av spillere: Hvordan? "Meg: " "Ikke meg: " @@ -445,7 +444,6 @@ Nedlasting mislyktes Lagre ordlister internt - Nedlastingsmappe @@ -594,7 +592,6 @@ Skru på offentlige rom Rom andre kan se og ta del i - Skjul knapper @@ -612,7 +609,6 @@ Skru på feilrettingsfunksjoner Nettverksstatistikk Vis invitasjoner - %1$s/%2$s Skriv spill til SD-kort Last spill fra SD-kort diff --git a/xwords4/android/scripts/adb-pull-apk.sh b/xwords4/android/scripts/adb-pull-apk.sh new file mode 100755 index 000000000..106eb9ea3 --- /dev/null +++ b/xwords4/android/scripts/adb-pull-apk.sh @@ -0,0 +1,9 @@ +#!/bin/bash + +set -e -u + +APP_ID=org.eehouse.android.xw4 + +APK_PATH=$(adb shell pm path $APP_ID) +APK_PATH=${APK_PATH/package:/} +adb pull $APK_PATH diff --git a/xwords4/android/scripts/copy-strings.py b/xwords4/android/scripts/copy-strings.py index 2ba15fb79..8dcd73644 100755 --- a/xwords4/android/scripts/copy-strings.py +++ b/xwords4/android/scripts/copy-strings.py @@ -141,7 +141,7 @@ def writeDoc(doc, src, dest): def checkOrConvertString(engNames, elem, verbose): name = elem.get('name') if not elem.text: - print "elem", name, "is empty" + print "ERROR: elem", name, "is empty" sys.exit(1) elif not name in engNames or elem.text.startswith(s_prefix): ok = False diff --git a/xwords4/android/scripts/info.py b/xwords4/android/scripts/info.py index 936a4dd32..80ad70d6f 100755 --- a/xwords4/android/scripts/info.py +++ b/xwords4/android/scripts/info.py @@ -7,7 +7,10 @@ import mk_for_download, mygit import xwconfig # I'm not checking my key in... -import mykey +try : + import mykey +except: + print('unable to load mykey') from stat import ST_CTIME try: diff --git a/xwords4/common/comms.c b/xwords4/common/comms.c index c641fc98e..19a0c7986 100644 --- a/xwords4/common/comms.c +++ b/xwords4/common/comms.c @@ -274,6 +274,9 @@ CommsRelayState2Str( CommsRelayState state ) CASE_STR(COMMS_RELAYSTATE_CONNECTED); CASE_STR(COMMS_RELAYSTATE_RECONNECTED); CASE_STR(COMMS_RELAYSTATE_ALLCONNECTED); +#ifdef RELAY_VIA_HTTP + CASE_STR(COMMS_RELAYSTATE_USING_HTTP); +#endif default: XP_ASSERT(0); } @@ -459,7 +462,10 @@ reset_internal( CommsCtxt* comms, XP_Bool isServer, if ( 0 != comms->nextChannelNo ) { XP_LOGF( "%s: comms->nextChannelNo: %d", __func__, comms->nextChannelNo ); } - XP_ASSERT( 0 == comms->nextChannelNo ); /* firing... */ + /* This tends to fire when games reconnect to the relay after the DB's + been wiped and connect in a different order from that in which they did + originally. So comment it out. */ + // XP_ASSERT( 0 == comms->nextChannelNo ); // comms->nextChannelNo = 0; if ( resetRelay ) { comms->channelSeed = 0; @@ -1773,7 +1779,7 @@ relayPreProcess( CommsCtxt* comms, XWStreamCtxt* stream, XWHostID* senderID ) } if ( consumed ) { - XP_LOGF( "%s: rejecting data message", __func__ ); + XP_LOGF( "%s: rejecting data message (consumed)", __func__ ); } else { *senderID = srcID; } @@ -2375,6 +2381,19 @@ comms_isConnected( const CommsCtxt* const comms ) return result; } +#ifdef RELAY_VIA_HTTP +void +comms_gameJoined( CommsCtxt* comms, const XP_UCHAR* connname, XWHostID hid ) +{ + LOG_FUNC(); + XP_ASSERT( XP_STRLEN( connname ) + 1 < sizeof(comms->rr.connName) ); + XP_STRNCPY( comms->rr.connName, connname, sizeof(comms->rr.connName) ); + comms->rr.myHostID = hid; + comms->forceChannel = hid; + set_relay_state( comms, COMMS_RELAYSTATE_USING_HTTP ); +} +#endif + #if defined COMMS_HEARTBEAT || defined XWFEATURE_COMMSACK static void sendEmptyMsg( CommsCtxt* comms, AddressRecord* rec ) @@ -3097,14 +3116,34 @@ sendNoConn( CommsCtxt* comms, const MsgQueueElem* elem, XWHostID destID ) static XP_Bool relayConnect( CommsCtxt* comms ) { - XP_Bool success = XP_TRUE; LOG_FUNC(); - if ( addr_hasType( &comms->addr, COMMS_CONN_RELAY ) && !comms->rr.connecting ) { - comms->rr.connecting = XP_TRUE; - success = send_via_relay( comms, comms->rr.connName[0]? - XWRELAY_GAME_RECONNECT : XWRELAY_GAME_CONNECT, - comms->rr.myHostID, NULL, 0, NULL ); - comms->rr.connecting = XP_FALSE; + XP_Bool success = XP_TRUE; + if ( addr_hasType( &comms->addr, COMMS_CONN_RELAY ) ) { + if ( 0 ) { +#ifdef RELAY_VIA_HTTP + } else if ( comms->rr.connName[0] ) { + set_relay_state( comms, COMMS_RELAYSTATE_USING_HTTP ); + } else { + CommsAddrRec addr; + comms_getAddr( comms, &addr ); + DevIDType ignored; /* but should it be? */ + (*comms->procs.requestJoin)( comms->procs.closure, + util_getDevID( comms->util, &ignored ), + addr.u.ip_relay.invite, /* room */ + comms->rr.nPlayersHere, + comms->rr.nPlayersTotal, + comms_getChannelSeed(comms), + comms->util->gameInfo->dictLang ); + success = XP_FALSE; +#else + } else if ( !comms->rr.connecting ) { + comms->rr.connecting = XP_TRUE; + success = send_via_relay( comms, comms->rr.connName[0]? + XWRELAY_GAME_RECONNECT : XWRELAY_GAME_CONNECT, + comms->rr.myHostID, NULL, 0, NULL ); + comms->rr.connecting = XP_FALSE; +#endif + } } return success; } /* relayConnect */ diff --git a/xwords4/common/comms.h b/xwords4/common/comms.h index e767b6b92..31ddfae82 100644 --- a/xwords4/common/comms.h +++ b/xwords4/common/comms.h @@ -32,7 +32,7 @@ EXTERN_C_START #define CONN_ID_NONE 0L typedef XP_U32 MsgID; /* this is too big!!! PENDING */ -typedef XP_U8 XWHostID; +typedef XP_U8 XWHostID; typedef enum { COMMS_CONN_NONE /* I want errors on uninited case */ @@ -56,6 +56,9 @@ typedef enum { , COMMS_RELAYSTATE_CONNECTED , COMMS_RELAYSTATE_RECONNECTED , COMMS_RELAYSTATE_ALLCONNECTED +#ifdef RELAY_VIA_HTTP + , COMMS_RELAYSTATE_USING_HTTP /* connection state doesn't matter */ +#endif } CommsRelayState; #ifdef XWFEATURE_BLUETOOTH @@ -90,7 +93,7 @@ typedef struct _CommsAddrRec { XP_U16 port_ip; } ip; struct { - XP_UCHAR invite[MAX_INVITE_LEN + 1]; + XP_UCHAR invite[MAX_INVITE_LEN + 1]; /* room!!!! */ XP_UCHAR hostName[MAX_HOSTNAME_LEN + 1]; XP_U32 ipAddr; /* looked up from above */ XP_U16 port; @@ -135,6 +138,12 @@ typedef void (*RelayErrorProc)( void* closure, XWREASON relayErr ); typedef XP_Bool (*RelayNoConnProc)( const XP_U8* buf, XP_U16 len, const XP_UCHAR* msgNo, const XP_UCHAR* relayID, void* closure ); +# ifdef RELAY_VIA_HTTP +typedef void (*RelayRequestJoinProc)( void* closure, const XP_UCHAR* devID, + const XP_UCHAR* room, XP_U16 nPlayersHere, + XP_U16 nPlayersTotal, XP_U16 seed, + XP_U16 lang ); +# endif #endif typedef enum { @@ -161,6 +170,9 @@ typedef struct _TransportProcs { RelayConndProc rconnd; RelayErrorProc rerror; RelayNoConnProc sendNoConn; +# ifdef RELAY_VIA_HTTP + RelayRequestJoinProc requestJoin; +# endif #endif void* closure; } TransportProcs; @@ -248,6 +260,10 @@ XP_Bool comms_checkComplete( const CommsAddrRec* const addr ); XP_Bool comms_canChat( const CommsCtxt* comms ); XP_Bool comms_isConnected( const CommsCtxt* const comms ); +#ifdef RELAY_VIA_HTTP +void comms_gameJoined( CommsCtxt* comms, const XP_UCHAR* connname, XWHostID hid ); +#endif + CommsConnType addr_getType( const CommsAddrRec* addr ); void addr_setType( CommsAddrRec* addr, CommsConnType type ); void addr_addType( CommsAddrRec* addr, CommsConnType type ); diff --git a/xwords4/common/game.c b/xwords4/common/game.c index a9412b2d8..9bb55135f 100644 --- a/xwords4/common/game.c +++ b/xwords4/common/game.c @@ -338,6 +338,34 @@ game_saveSucceeded( const XWGame* game, XP_U16 saveToken ) } } +XP_Bool +game_receiveMessage( XWGame* game, XWStreamCtxt* stream, CommsAddrRec* retAddr ) +{ + ServerCtxt* server = game->server; + CommsMsgState commsState; + XP_Bool result = comms_checkIncomingStream( game->comms, stream, retAddr, + &commsState ); + if ( result ) { + (void)server_do( server ); + + result = server_receiveMessage( server, stream ); + } + comms_msgProcessed( game->comms, &commsState, !result ); + + if ( result ) { + /* in case MORE work's pending. Multiple calls are required in at + least one case, where I'm a host handling client registration *AND* + I'm a robot. Only one server_do and I'll never make that first + robot move. That's because comms can't detect a duplicate initial + packet (in validateInitialMessage()). */ + for ( int ii = 0; ii < 5; ++ii ) { + (void)server_do( server ); + } + } + + return result; +} + void game_getState( const XWGame* game, GameStateInfo* gsi ) { diff --git a/xwords4/common/game.h b/xwords4/common/game.h index d79cbc20f..6a68a9bf0 100644 --- a/xwords4/common/game.h +++ b/xwords4/common/game.h @@ -82,6 +82,10 @@ void game_saveNewGame( MPFORMAL const CurGameInfo* gi, XW_UtilCtxt* util, void game_saveToStream( const XWGame* game, const CurGameInfo* gi, XWStreamCtxt* stream, XP_U16 saveToken ); void game_saveSucceeded( const XWGame* game, XP_U16 saveToken ); + +XP_Bool game_receiveMessage( XWGame* game, XWStreamCtxt* stream, + CommsAddrRec* retAddr ); + void game_dispose( XWGame* game ); void game_getState( const XWGame* game, GameStateInfo* gsi ); diff --git a/xwords4/common/nli.c b/xwords4/common/nli.c index e54c72a8f..29ea12285 100644 --- a/xwords4/common/nli.c +++ b/xwords4/common/nli.c @@ -61,6 +61,13 @@ nli_setDevID( NetLaunchInfo* nli, XP_U32 devID ) types_addType( &nli->_conTypes, COMMS_CONN_RELAY ); } +void +nli_setInviteID( NetLaunchInfo* nli, const XP_UCHAR* inviteID ) +{ + nli->inviteID[0] = '\0'; + XP_STRCAT( nli->inviteID, inviteID ); +} + void nli_saveToStream( const NetLaunchInfo* nli, XWStreamCtxt* stream ) { diff --git a/xwords4/common/nli.h b/xwords4/common/nli.h index 721a839c4..47f572585 100644 --- a/xwords4/common/nli.h +++ b/xwords4/common/nli.h @@ -76,6 +76,7 @@ void nli_saveToStream( const NetLaunchInfo* invit, XWStreamCtxt* stream ); void nli_makeAddrRec( const NetLaunchInfo* invit, CommsAddrRec* addr ); void nli_setDevID( NetLaunchInfo* invit, XP_U32 devID ); +void nli_setInviteID( NetLaunchInfo* invit, const XP_UCHAR* inviteID ); #endif diff --git a/xwords4/common/xwlist.c b/xwords4/common/xwlist.c new file mode 100644 index 000000000..31a85b127 --- /dev/null +++ b/xwords4/common/xwlist.c @@ -0,0 +1,127 @@ +/* -*-mode: C; fill-column: 78; c-basic-offset: 4; -*- */ +/* + * Copyright 2009 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. + */ + +#include "xwlist.h" + +#define MAX_HERE 16 + +typedef struct XWList { + XP_U16 len; + XP_U16 size; + elem* list; + MPSLOT +} XWList; + +XWList* +mk_list(MPFORMAL XP_U16 XP_UNUSED(sizeHint)) +{ + XWList* list = XP_CALLOC( mpool, sizeof(*list)); + MPASSIGN( list->mpool, mpool); + return list; +} + +void +list_append( XWList* self, elem one ) +{ + if ( self->size == 0 ) { /* initial case */ + self->size = 2; + self->list = XP_MALLOC( self->mpool, self->size * sizeof(self->list[0]) ); + } + if ( self->len == self->size ) { /* need to grow? */ + self->size *= 2; + self->list = XP_REALLOC( self->mpool, self->list, self->size * sizeof(self->list[0]) ); + } + + self->list[self->len++] = one; + XP_LOGF( "%s(): put %p at position %d (size: %d)", __func__, one, self->len-1, self->size ); +} + +XP_U16 +list_get_len( const XWList* list ) +{ + return list->len; +} + +void +list_remove_front( XWList* self, elem* out, XP_U16* countp ) +{ + const XP_U16 nMoved = XP_MIN( *countp, self->len ); + XP_MEMCPY( out, self->list, nMoved * sizeof(out[0]) ); + *countp = nMoved; + + // Now copy the survivors down + self->len -= nMoved; + XP_MEMMOVE( &self->list[0], &self->list[nMoved], self->len * sizeof(self->list[0])); +} + +void +list_remove_back(XWList* XP_UNUSED(self), elem* XP_UNUSED(here), XP_U16* XP_UNUSED(count)) +{ +} + +void +list_free( XWList* self, destructor proc, void* closure ) +{ + if ( !!proc ) { + for ( XP_U16 ii = 0; ii < self->len; ++ii ) { + (*proc)(self->list[ii], closure); + } + } + + if ( !!self->list ) { + XP_FREE( self->mpool, self->list ); + } + XP_FREE( self->mpool, self ); +} + +#ifdef DEBUG + +static void +dest(elem elem, void* XP_UNUSED(closure)) +{ + XP_LOGF( "%s(%p)", __func__, elem); +} + +void +list_test_lists(MPFORMAL_NOCOMMA) +{ + XWList* list = mk_list( mpool, 16 ); + for ( char* ii = 0; ii < (char*)100; ++ii ) { + (void)list_append( list, ii ); + } + + XP_ASSERT( list_get_len(list) == 100 ); + + char* prev = 0; + while ( 0 < list_get_len( list ) ) { + elem here; + XP_U16 count = 1; + list_remove_front( list, &here, &count ); + XP_LOGF( "%s(): got here: %p", __func__, here ); + XP_ASSERT( count == 1 ); + XP_ASSERT( prev++ == here ); + } + + for ( char* ii = 0; ii < (char*)10; ++ii ) { + (void)list_append( list, ii ); + } + + list_free( list, dest, NULL ); +} +#endif diff --git a/xwords4/common/xwlist.h b/xwords4/common/xwlist.h new file mode 100644 index 000000000..d404590e6 --- /dev/null +++ b/xwords4/common/xwlist.h @@ -0,0 +1,44 @@ +/* -*-mode: C; fill-column: 78; c-basic-offset: 4; -*- */ +/* + * Copyright 2017 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. + */ + +#ifndef _XWLIST_H_ +#define _XWLIST_H_ + +#include "comtypes.h" +#include "mempool.h" + +#include "xptypes.h" + +typedef void* elem; +typedef struct XWList XWList; +typedef void (*destructor)(elem one, void* closure); + +XWList* mk_list(MPFORMAL XP_U16 sizeHint); +void list_free(XWList* list, destructor proc, void* closure); + +void list_append(XWList* list, elem one); +XP_U16 list_get_len(const XWList* list); +void list_remove_front(XWList* list, elem* here, XP_U16* count); +void list_remove_back(XWList* list, elem* here, XP_U16* count); + +#ifdef DEBUG +void list_test_lists(MPFORMAL_NOCOMMA); +#endif + +#endif diff --git a/xwords4/linux/dict.xwd b/xwords4/linux/CollegeEng_2to8.xwd similarity index 100% rename from xwords4/linux/dict.xwd rename to xwords4/linux/CollegeEng_2to8.xwd diff --git a/xwords4/linux/Makefile b/xwords4/linux/Makefile index 8b5499b5c..30d0c59dc 100644 --- a/xwords4/linux/Makefile +++ b/xwords4/linux/Makefile @@ -130,6 +130,7 @@ DEFINES += -DCOMMS_XPORT_FLAGSPROC DEFINES += -DINITIAL_CLIENT_VERS=3 DEFINES += -DCOMMON_LAYOUT DEFINES += -DNATIVE_NLI +# DEFINES += -DRELAY_VIA_HTTP # MAX_ROWS controls STREAM_VERS_BIGBOARD and with it move hashing DEFINES += -DMAX_ROWS=32 @@ -226,7 +227,7 @@ OBJ = \ $(BUILD_PLAT_DIR)/relaycon.o \ $(CURSES_OBJS) $(GTK_OBJS) $(MAIN_OBJS) -LIBS = -lm -luuid $(GPROFFLAG) +LIBS = -lm -lpthread -luuid -lcurl -ljson-c $(GPROFFLAG) ifdef USE_SQLITE LIBS += -lsqlite3 DEFINES += -DUSE_SQLITE @@ -242,7 +243,7 @@ endif ifneq (,$(findstring DPLATFORM_GTK,$(DEFINES))) LIBS += `pkg-config --libs gtk+-3.0` CFLAGS += `pkg-config --cflags gtk+-3.0` -# CFLAGS += -DGDK_DISABLE_DEPRECATED + CFLAGS += -DGDK_DISABLE_DEPRECATED POINTER_SUPPORT = -DPOINTER_SUPPORT endif diff --git a/xwords4/linux/cursesmain.c b/xwords4/linux/cursesmain.c index ea02def79..c02e5fe60 100644 --- a/xwords4/linux/cursesmain.c +++ b/xwords4/linux/cursesmain.c @@ -279,35 +279,57 @@ curses_util_userError( XW_UtilCtxt* uc, UtilErrID id ) } } /* curses_util_userError */ +static gint +ask_move( gpointer data ) +{ + CursesAppGlobals* globals = (CursesAppGlobals*)data; + CommonGlobals* cGlobals = &globals->cGlobals; + const char* answers[] = {"Ok", "Cancel", NULL}; + + if (0 == cursesask(globals, cGlobals->question, VSIZE(answers)-1, answers) ) { + BoardCtxt* board = cGlobals->game.board; + if ( board_commitTurn( board, XP_TRUE, XP_TRUE, NULL ) ) { + board_draw( board ); + } + } + + return FALSE; +} + +/* this needs to change!!! */ static void curses_util_notifyMove( XW_UtilCtxt* uc, XWStreamCtxt* stream ) { CursesAppGlobals* globals = (CursesAppGlobals*)uc->closure; - char* question; - const char* answers[3] = {NULL}; - short numAnswers = 0; - XP_Bool freeMe = XP_FALSE; - - question = strFromStream( stream ); - freeMe = XP_TRUE; - answers[numAnswers++] = "Cancel"; - answers[numAnswers++] = "Ok"; - - // result = okIndex == - cursesask( globals, question, numAnswers, answers ); - - if ( freeMe ) { - free( question ); - } + CommonGlobals* cGlobals = &globals->cGlobals; + XP_U16 len = stream_getSize( stream ); + XP_ASSERT( len <= VSIZE(cGlobals->question) ); + stream_getBytes( stream, cGlobals->question, len ); + (void)g_idle_add( ask_move, globals ); } /* curses_util_userQuery */ +static gint +ask_trade( gpointer data ) +{ + CursesAppGlobals* globals = (CursesAppGlobals*)data; + CommonGlobals* cGlobals = &globals->cGlobals; + + const char* buttons[] = { "Ok", "Cancel" }; + if (0 == cursesask( globals, cGlobals->question, VSIZE(buttons), buttons ) ) { + BoardCtxt* board = cGlobals->game.board; + if ( board_commitTurn( board, XP_TRUE, XP_TRUE, NULL ) ) { + board_draw( board ); + } + } + return FALSE; +} + static void curses_util_notifyTrade( XW_UtilCtxt* uc, const XP_UCHAR** tiles, XP_U16 nTiles ) { CursesAppGlobals* globals = (CursesAppGlobals*)uc->closure; formatConfirmTrade( &globals->cGlobals, tiles, nTiles ); - /* const char* buttons[] = { "Cancel", "Ok" }; */ - /* cursesask( globals, question, VSIZE(buttons), buttons ); */ + (void)g_idle_add( ask_trade, globals ); } static void @@ -1001,7 +1023,7 @@ curses_socket_added( void* closure, int newSock, GIOFunc func ) /* XP_ASSERT( !globals->cGlobals.relaySocket ); */ /* globals->cGlobals.relaySocket = newSock; */ #endif -} /* curses_socket_changed */ +} /* curses_socket_added */ static void curses_onGameSaved( void* closure, sqlite3_int64 rowid, @@ -1591,6 +1613,27 @@ relay_sendNoConn_curses( const XP_U8* msg, XP_U16 len, return storeNoConnMsg( &globals->cGlobals, msg, len, relayID ); } /* relay_sendNoConn_curses */ +#ifdef RELAY_VIA_HTTP +static void +onJoined( void* closure, const XP_UCHAR* connname, XWHostID hid ) +{ + LOG_FUNC(); + CursesAppGlobals* globals = (CursesAppGlobals*)closure; + CommsCtxt* comms = globals->cGlobals.game.comms; + comms_gameJoined( comms, connname, hid ); +} + +static void +relay_requestJoin_curses( void* closure, const XP_UCHAR* devID, const XP_UCHAR* room, + XP_U16 nPlayersHere, XP_U16 nPlayersTotal, + XP_U16 seed, XP_U16 lang ) +{ + CursesAppGlobals* globals = (CursesAppGlobals*)closure; + relaycon_join( globals->cGlobals.params, devID, room, nPlayersHere, nPlayersTotal, + seed, lang, onJoined, globals ); +} +#endif + static void relay_status_curses( void* closure, CommsRelayState state ) { @@ -1659,6 +1702,7 @@ static void cursesGotBuf( void* closure, const CommsAddrRec* addr, const XP_U8* buf, XP_U16 len ) { + LOG_FUNC(); CursesAppGlobals* globals = (CursesAppGlobals*)closure; XP_U32 clientToken; XP_ASSERT( sizeof(clientToken) < len ); @@ -1676,6 +1720,19 @@ cursesGotBuf( void* closure, const CommsAddrRec* addr, XP_LOGF( "%s: dropping packet; meant for a different device", __func__ ); } + LOG_RETURN_VOID(); +} + +static void +cursesGotForRow( void* closure, const CommsAddrRec* from, + sqlite3_int64 rowid, const XP_U8* buf, + XP_U16 len ) +{ + LOG_FUNC(); + CursesAppGlobals* globals = (CursesAppGlobals*)closure; + XP_ASSERT( globals->cGlobals.selRow == rowid ); + gameGotBuf( &globals->cGlobals, XP_TRUE, buf, len, from ); + LOG_RETURN_VOID(); } static gint @@ -1913,6 +1970,10 @@ cursesmain( XP_Bool isServer, LaunchParams* params ) .rconnd = relay_connd_curses, .rerror = relay_error_curses, .sendNoConn = relay_sendNoConn_curses, +#ifdef RELAY_VIA_HTTP + .requestJoin = relay_requestJoin_curses, +#endif + # ifdef COMMS_XPORT_FLAGSPROC .getFlags = curses_getFlags, # endif @@ -1949,6 +2010,7 @@ cursesmain( XP_Bool isServer, LaunchParams* params ) if ( params->useUdp ) { RelayConnProcs procs = { .msgReceived = cursesGotBuf, + .msgForRow = cursesGotForRow, .msgNoticeReceived = cursesNoticeRcvd, .devIDReceived = cursesDevIDReceived, .msgErrorMsg = cursesErrorMsgRcvd, diff --git a/xwords4/linux/cursesmain.h b/xwords4/linux/cursesmain.h index 1d859e254..59443e652 100644 --- a/xwords4/linux/cursesmain.h +++ b/xwords4/linux/cursesmain.h @@ -71,6 +71,7 @@ struct CursesAppGlobals { gchar* lastErr; XP_U16 nChatsSent; + XP_U16 nextQueryTimeSecs; union { struct { diff --git a/xwords4/linux/gamesdb.c b/xwords4/linux/gamesdb.c index 6b599e2bc..377a0dac0 100644 --- a/xwords4/linux/gamesdb.c +++ b/xwords4/linux/gamesdb.c @@ -52,6 +52,7 @@ openGamesDB( const char* dbName ) ",inviteInfo BLOB" ",room VARCHAR(32)" ",connvia VARCHAR(32)" + ",relayid VARCHAR(32)" ",ended INT(1)" ",turn INT(2)" ",local INT(1)" @@ -128,13 +129,14 @@ writeBlobColumnData( const XP_U8* data, gsize len, XP_U16 strVersion, sqlite3* p assertPrintResult( pDb, result, SQLITE_OK ); result = sqlite3_blob_close( blob ); assertPrintResult( pDb, result, SQLITE_OK ); + if ( !!stmt ) { sqlite3_finalize( stmt ); } LOG_RETURNF( "%lld", curRow ); return curRow; -} +} /* writeBlobColumnData */ static sqlite3_int64 writeBlobColumnStream( XWStreamCtxt* stream, sqlite3* pDb, sqlite3_int64 curRow, @@ -199,11 +201,12 @@ addSnapshot( CommonGlobals* cGlobals ) void summarize( CommonGlobals* cGlobals ) { - XP_S16 nMoves = model_getNMoves( cGlobals->game.model ); - XP_Bool gameOver = server_getGameIsOver( cGlobals->game.server ); + const XWGame* game = &cGlobals->game; + XP_S16 nMoves = model_getNMoves( game->model ); + XP_Bool gameOver = server_getGameIsOver( game->server ); XP_Bool isLocal; - XP_S16 turn = server_getCurrentTurn( cGlobals->game.server, &isLocal ); - XP_U32 lastMoveTime = server_getLastMoveTime( cGlobals->game.server ); + XP_S16 turn = server_getCurrentTurn( game->server, &isLocal ); + XP_U32 lastMoveTime = server_getLastMoveTime( game->server ); XP_U16 seed = 0; XP_S16 nMissing = 0; XP_U16 nTotal = cGlobals->gi->nPlayers; @@ -214,10 +217,11 @@ summarize( CommonGlobals* cGlobals ) // gchar* connvia = "local"; gchar connvia[128] = {0}; + XP_UCHAR relayID[32] = {0}; - if ( !!cGlobals->game.comms ) { - nMissing = server_getMissingPlayers( cGlobals->game.server ); - comms_getAddr( cGlobals->game.comms, &addr ); + if ( !!game->comms ) { + nMissing = server_getMissingPlayers( game->server ); + comms_getAddr( game->comms, &addr ); CommsConnType typ; for ( XP_U32 st = 0; addr_iter( &addr, &typ, &st ); ) { if ( !!connvia[0] ) { @@ -242,18 +246,21 @@ summarize( CommonGlobals* cGlobals ) break; } } - seed = comms_getChannelSeed( cGlobals->game.comms ); + seed = comms_getChannelSeed( game->comms ); + XP_U16 len = VSIZE(relayID); + (void)comms_getRelayID( game->comms, relayID, &len ); } else { strcat( connvia, "local" ); } const char* fmt = "UPDATE games " - " SET room='%s', ended=%d, turn=%d, local=%d, ntotal=%d, nmissing=%d, " - " nmoves=%d, seed=%d, gameid=%d, connvia='%s', lastMoveTime=%d" + " SET room='%s', ended=%d, turn=%d, local=%d, ntotal=%d, " + " nmissing=%d, nmoves=%d, seed=%d, gameid=%d, connvia='%s', " + " relayid='%s', lastMoveTime=%d" " WHERE rowid=%lld"; XP_UCHAR buf[256]; snprintf( buf, sizeof(buf), fmt, room, gameOver?1:0, turn, isLocal?1:0, - nTotal, nMissing, nMoves, seed, gameID, connvia, lastMoveTime, + nTotal, nMissing, nMoves, seed, gameID, connvia, relayID, lastMoveTime, cGlobals->selRow ); XP_LOGF( "query: %s", buf ); sqlite3_stmt* stmt = NULL; @@ -305,12 +312,46 @@ listGames( sqlite3* pDb ) return list; } +GHashTable* +getRelayIDsToRowsMap( sqlite3* pDb ) +{ + GHashTable* table = g_hash_table_new( g_str_hash, g_str_equal ); + sqlite3_stmt *ppStmt; + int result = sqlite3_prepare_v2( pDb, "SELECT relayid, rowid FROM games " + "where NOT relayid = ''", -1, &ppStmt, NULL ); + assertPrintResult( pDb, result, SQLITE_OK ); + while ( result == SQLITE_OK && NULL != ppStmt ) { + switch( sqlite3_step( ppStmt ) ) { + case SQLITE_ROW: /* have data */ + { + XP_UCHAR relayID[32]; + getColumnText( ppStmt, 0, relayID, VSIZE(relayID) ); + gpointer key = g_strdup( relayID ); + sqlite3_int64* value = g_malloc( sizeof( value ) ); + *value = sqlite3_column_int64( ppStmt, 1 ); + g_hash_table_insert( table, key, value ); + /* XP_LOGF( "%s(): added map %s => %lld", __func__, (char*)key, *value ); */ + } + break; + case SQLITE_DONE: + sqlite3_finalize( ppStmt ); + ppStmt = NULL; + break; + default: + XP_ASSERT( 0 ); + break; + } + } + + return table; +} + XP_Bool getGameInfo( sqlite3* pDb, sqlite3_int64 rowid, GameInfo* gib ) { XP_Bool success = XP_FALSE; const char* fmt = "SELECT room, ended, turn, local, nmoves, ntotal, nmissing, " - "seed, connvia, gameid, lastMoveTime, snap " + "seed, connvia, gameid, lastMoveTime, relayid, snap " "FROM games WHERE rowid = %lld"; XP_UCHAR query[256]; snprintf( query, sizeof(query), fmt, rowid ); @@ -321,25 +362,28 @@ getGameInfo( sqlite3* pDb, sqlite3_int64 rowid, GameInfo* gib ) result = sqlite3_step( ppStmt ); if ( SQLITE_ROW == result ) { success = XP_TRUE; - getColumnText( ppStmt, 0, gib->room, sizeof(gib->room) ); - gib->gameOver = 1 == sqlite3_column_int( ppStmt, 1 ); - gib->turn = sqlite3_column_int( ppStmt, 2 ); - gib->turnLocal = 1 == sqlite3_column_int( ppStmt, 3 ); - gib->nMoves = sqlite3_column_int( ppStmt, 4 ); - gib->nTotal = sqlite3_column_int( ppStmt, 5 ); - gib->nMissing = sqlite3_column_int( ppStmt, 6 ); - gib->seed = sqlite3_column_int( ppStmt, 7 ); - getColumnText( ppStmt, 8, gib->conn, sizeof(gib->conn) ); - gib->gameID = sqlite3_column_int( ppStmt, 9 ); - gib->lastMoveTime = sqlite3_column_int( ppStmt, 10 ); + int col = 0; + getColumnText( ppStmt, col++, gib->room, sizeof(gib->room) ); + gib->gameOver = 1 == sqlite3_column_int( ppStmt, col++ ); + gib->turn = sqlite3_column_int( ppStmt, col++ ); + gib->turnLocal = 1 == sqlite3_column_int( ppStmt, col++ ); + gib->nMoves = sqlite3_column_int( ppStmt, col++ ); + gib->nTotal = sqlite3_column_int( ppStmt, col++ ); + gib->nMissing = sqlite3_column_int( ppStmt, col++ ); + gib->seed = sqlite3_column_int( ppStmt, col++ ); + getColumnText( ppStmt, col++, gib->conn, sizeof(gib->conn) ); + gib->gameID = sqlite3_column_int( ppStmt, col++ ); + gib->lastMoveTime = sqlite3_column_int( ppStmt, col++ ); + getColumnText( ppStmt, col++, gib->relayID, sizeof(gib->relayID) ); snprintf( gib->name, sizeof(gib->name), "Game %lld", rowid ); #ifdef PLATFORM_GTK /* Load the snapshot */ GdkPixbuf* snap = NULL; - const XP_U8* ptr = sqlite3_column_blob( ppStmt, 11 ); + int snapCol = col++; + const XP_U8* ptr = sqlite3_column_blob( ppStmt, snapCol ); if ( !!ptr ) { - int size = sqlite3_column_bytes( ppStmt, 11 ); + int size = sqlite3_column_bytes( ppStmt, snapCol ); /* Skip the version that's written in */ ptr += sizeof(XP_U16); size -= sizeof(XP_U16); GInputStream* istr = g_memory_input_stream_new_from_data( ptr, size, NULL ); diff --git a/xwords4/linux/gamesdb.h b/xwords4/linux/gamesdb.h index b7744cdc8..086301570 100644 --- a/xwords4/linux/gamesdb.h +++ b/xwords4/linux/gamesdb.h @@ -31,6 +31,7 @@ typedef struct _GameInfo { XP_UCHAR name[128]; XP_UCHAR room[128]; XP_UCHAR conn[128]; + XP_UCHAR relayID[32]; #ifdef PLATFORM_GTK GdkPixbuf* snap; #endif @@ -55,6 +56,9 @@ void summarize( CommonGlobals* cGlobals ); /* Return GSList whose data is (ptrs to) rowids */ GSList* listGames( sqlite3* dbp ); +/* Mapping of relayID -> rowid */ +GHashTable* getRelayIDsToRowsMap( sqlite3* pDb ); + XP_Bool getGameInfo( sqlite3* dbp, sqlite3_int64 rowid, GameInfo* gib ); void getRowsForGameID( sqlite3* dbp, XP_U32 gameID, sqlite3_int64* rowids, int* nRowIDs ); diff --git a/xwords4/linux/gtkboard.c b/xwords4/linux/gtkboard.c index 564ce31ac..0930e3935 100644 --- a/xwords4/linux/gtkboard.c +++ b/xwords4/linux/gtkboard.c @@ -341,6 +341,8 @@ relay_connd_gtk( void* closure, XP_UCHAR* const room, char buf[256]; if ( allHere ) { + /* disable for now. Seeing this too often */ + skip = XP_TRUE; snprintf( buf, sizeof(buf), "All expected players have joined in %s. Play!", room ); } else { @@ -428,13 +430,57 @@ relay_sendNoConn_gtk( const XP_U8* msg, XP_U16 len, return success; } /* relay_sendNoConn_gtk */ +static void +tryConnectToServer(CommonGlobals* cGlobals) +{ + LaunchParams* params = cGlobals->params; + XWStreamCtxt* stream = + mem_stream_make( cGlobals->util->mpool, params->vtMgr, + cGlobals, CHANNEL_NONE, + sendOnClose ); + (void)server_initClientConnection( cGlobals->game.server, + stream ); +} + +#ifdef RELAY_VIA_HTTP +static void +onJoined( void* closure, const XP_UCHAR* connname, XWHostID hid ) +{ + GtkGameGlobals* globals = (GtkGameGlobals*)closure; + XWGame* game = &globals->cGlobals.game; + CommsCtxt* comms = game->comms; + comms_gameJoined( comms, connname, hid ); + if ( hid > 1 ) { + globals->cGlobals.gi->serverRole = SERVER_ISCLIENT; + server_reset( game->server, game->comms ); + tryConnectToServer( &globals->cGlobals ); + } +} + +static void +relay_requestJoin_gtk( void* closure, const XP_UCHAR* devID, const XP_UCHAR* room, + XP_U16 nPlayersHere, XP_U16 nPlayersTotal, + XP_U16 seed, XP_U16 lang ) +{ + GtkGameGlobals* globals = (GtkGameGlobals*)closure; + LaunchParams* params = globals->cGlobals.params; + relaycon_join( params, devID, room, nPlayersHere, nPlayersTotal, seed, lang, + onJoined, globals ); +} +#endif + #ifdef COMMS_XPORT_FLAGSPROC static XP_U32 gtk_getFlags( void* closure ) { GtkGameGlobals* globals = (GtkGameGlobals*)closure; +# ifdef RELAY_VIA_HTTP + XP_USE( globals ); + return COMMS_XPORT_FLAGS_HASNOCONN; +# else return (!!globals->draw) ? COMMS_XPORT_FLAGS_NONE : COMMS_XPORT_FLAGS_HASNOCONN; +# endif } #endif @@ -454,6 +500,9 @@ setTransportProcs( TransportProcs* procs, GtkGameGlobals* globals ) procs->rconnd = relay_connd_gtk; procs->rerror = relay_error_gtk; procs->sendNoConn = relay_sendNoConn_gtk; +# ifdef RELAY_VIA_HTTP + procs->requestJoin = relay_requestJoin_gtk; +# endif #endif } @@ -663,12 +712,7 @@ createOrLoadObjects( GtkGameGlobals* globals ) } else { DeviceRole serverRole = cGlobals->gi->serverRole; if ( serverRole == SERVER_ISCLIENT ) { - XWStreamCtxt* stream = - mem_stream_make( MEMPOOL params->vtMgr, - cGlobals, CHANNEL_NONE, - sendOnClose ); - (void)server_initClientConnection( cGlobals->game.server, - stream ); + tryConnectToServer( cGlobals ); } #endif } @@ -1014,12 +1058,7 @@ new_game_impl( GtkGameGlobals* globals, XP_Bool fireConnDlg ) } if ( isClient ) { - XWStreamCtxt* stream = - mem_stream_make( MEMPOOL cGlobals->params->vtMgr, - cGlobals, CHANNEL_NONE, - sendOnClose ); - (void)server_initClientConnection( cGlobals->game.server, - stream ); + tryConnectToServer( cGlobals ); } #endif (void)server_do( cGlobals->game.server ); /* assign tiles, etc. */ @@ -1175,6 +1214,7 @@ handle_memstats( GtkWidget* XP_UNUSED(widget), GtkGameGlobals* globals ) stream_destroy( stream ); } /* handle_memstats */ + #endif #ifdef XWFEATURE_ACTIVERECT @@ -1199,15 +1239,15 @@ frame_active( GtkWidget* XP_UNUSED(widget), GtkGameGlobals* globals ) } #endif -static GtkWidget* +GtkWidget* createAddItem( GtkWidget* parent, gchar* label, - GCallback handlerFunc, GtkGameGlobals* globals ) + GCallback handlerFunc, gpointer closure ) { GtkWidget* item = gtk_menu_item_new_with_label( label ); if ( handlerFunc != NULL ) { g_signal_connect( item, "activate", G_CALLBACK(handlerFunc), - globals ); + closure ); } gtk_menu_shell_append( GTK_MENU_SHELL(parent), item ); @@ -1302,7 +1342,7 @@ static void disenable_buttons( GtkGameGlobals* globals ) { XP_U16 nPending = server_getPendingRegs( globals->cGlobals.game.server ); - if ( !globals->invite_button && 0 < nPending ) { + if ( !globals->invite_button && 0 < nPending && !!globals->buttons_hbox ) { globals->invite_button = addButton( globals->buttons_hbox, "Invite", G_CALLBACK(handle_invite_button), globals ); @@ -1600,6 +1640,9 @@ send_invites( CommonGlobals* cGlobals, XP_U16 nPlayers, NetLaunchInfo nli = {0}; nli_init( &nli, cGlobals->gi, &addr, nPlayers, forceChannel ); + XP_UCHAR buf[32]; + snprintf( buf, sizeof(buf), "%X", makeRandomInt() ); + nli_setInviteID( &nli, buf ); nli_setDevID( &nli, linux_getDevIDRelay( cGlobals->params ) ); #ifdef DEBUG diff --git a/xwords4/linux/gtkboard.h b/xwords4/linux/gtkboard.h index d46f26ba6..d2abde645 100644 --- a/xwords4/linux/gtkboard.h +++ b/xwords4/linux/gtkboard.h @@ -46,6 +46,10 @@ typedef struct GtkDrawCtx { /* GdkDrawable* pixmap; */ GtkWidget* drawing_area; cairo_surface_t* surface; +#ifdef GDK_AVAILABLE_IN_3_22 + GdkDrawingContext* dc; +#endif + struct GtkGameGlobals* globals; #ifdef USE_CAIRO @@ -187,6 +191,10 @@ XP_Bool loadGameNoDraw( GtkGameGlobals* globals, LaunchParams* params, sqlite3_int64 rowid ); void destroy_board_window( GtkWidget* widget, GtkGameGlobals* globals ); +GtkWidget* makeAddSubmenu( GtkWidget* menubar, gchar* label ); +GtkWidget* createAddItem( GtkWidget* parent, gchar* label, + GCallback handlerFunc, gpointer closure ); + #endif /* PLATFORM_GTK */ #endif diff --git a/xwords4/linux/gtkdraw.c b/xwords4/linux/gtkdraw.c index aeab1f824..fa04326ad 100644 --- a/xwords4/linux/gtkdraw.c +++ b/xwords4/linux/gtkdraw.c @@ -1,6 +1,6 @@ -/* -*- compile-command: "make MEMDEBUG=TRUE -j3"; -*- */ +/* -*- compile-command: "make MEMDEBUG=TRUE -j5"; -*- */ /* - * Copyright 1997-2011 by Eric House (xwords@eehouse.org). All rights + * Copyright 1997 - 2017 by Eric House (xwords@eehouse.org). All rights * reserved. * * This program is free software; you can redistribute it and/or @@ -86,7 +86,14 @@ initCairo( GtkDrawCtx* dctx ) if ( !!dctx->surface ) { cairo = cairo_create( dctx->surface ); } else if ( !!dctx->drawing_area ) { +#ifdef GDK_AVAILABLE_IN_3_22 + GdkWindow* window = gtk_widget_get_window( dctx->drawing_area ); + const cairo_region_t* region = gdk_window_get_visible_region( window ); + dctx->dc = gdk_window_begin_draw_frame( window, region ); + cairo = gdk_drawing_context_get_cairo_context( dctx->dc ); +#else cairo = gdk_cairo_create( gtk_widget_get_window(dctx->drawing_area) ); +#endif } else { XP_ASSERT( 0 ); } @@ -108,7 +115,12 @@ destroyCairo( GtkDrawCtx* dctx ) { /* XP_LOGF( "%s(dctx=%p)", __func__, dctx ); */ XP_ASSERT( !!dctx->_cairo ); - cairo_destroy(dctx->_cairo); +#ifdef GDK_AVAILABLE_IN_3_22 + GdkWindow* window = gtk_widget_get_window( dctx->drawing_area ); + gdk_window_end_draw_frame( window, dctx->dc ); +#else + cairo_destroy( dctx->_cairo ); +#endif dctx->_cairo = NULL; } diff --git a/xwords4/linux/gtkmain.c b/xwords4/linux/gtkmain.c index 63ec72f3a..9058ad5fe 100644 --- a/xwords4/linux/gtkmain.c +++ b/xwords4/linux/gtkmain.c @@ -76,7 +76,7 @@ findOpenGame( const GtkAppGlobals* apg, sqlite3_int64 rowid ) } enum { ROW_ITEM, ROW_THUMB, NAME_ITEM, ROOM_ITEM, GAMEID_ITEM, SEED_ITEM, - CONN_ITEM, OVER_ITEM, TURN_ITEM, LOCAL_ITEM, NMOVES_ITEM, NTOTAL_ITEM, + CONN_ITEM, RELAYID_ITEM, OVER_ITEM, TURN_ITEM, LOCAL_ITEM, NMOVES_ITEM, NTOTAL_ITEM, MISSING_ITEM, LASTTURN_ITEM, N_ITEMS }; static void @@ -167,6 +167,7 @@ init_games_list( GtkAppGlobals* apg ) addTextColumn( list, "GameID", GAMEID_ITEM ); addTextColumn( list, "Seed", SEED_ITEM ); addTextColumn( list, "Conn. via", CONN_ITEM ); + addTextColumn( list, "RelayID", RELAYID_ITEM ); addTextColumn( list, "Ended", OVER_ITEM ); addTextColumn( list, "Turn", TURN_ITEM ); addTextColumn( list, "Local", LOCAL_ITEM ); @@ -183,6 +184,7 @@ init_games_list( GtkAppGlobals* apg ) G_TYPE_INT, /* GAMEID_ITEM */ G_TYPE_INT, /* SEED_ITEM */ G_TYPE_STRING, /* CONN_ITEM */ + G_TYPE_STRING, /*RELAYID_ITEM */ G_TYPE_BOOLEAN, /* OVER_ITEM */ G_TYPE_INT, /* TURN_ITEM */ G_TYPE_STRING, /* LOCAL_ITEM */ @@ -239,6 +241,7 @@ add_to_list( GtkWidget* list, sqlite3_int64 rowid, XP_Bool isNew, GAMEID_ITEM, gib->gameID, SEED_ITEM, gib->seed, CONN_ITEM, gib->conn, + RELAYID_ITEM, gib->relayID, TURN_ITEM, gib->turn, OVER_ITEM, gib->gameOver, LOCAL_ITEM, localString, @@ -506,6 +509,13 @@ trySetWinConfig( GtkAppGlobals* apg ) gtk_window_move (GTK_WINDOW(apg->window), xx, yy ); } +static void +handle_movescheck( GtkWidget* XP_UNUSED(widget), GtkAppGlobals* apg ) +{ + LaunchParams* params = apg->params; + relaycon_checkMsgs( params ); +} + static void makeGamesWindow( GtkAppGlobals* apg ) { @@ -529,6 +539,17 @@ makeGamesWindow( GtkAppGlobals* apg ) GtkWidget* vbox = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0); gtk_container_add( GTK_CONTAINER(swin), vbox ); gtk_widget_show( vbox ); + + // add menubar here + GtkWidget* menubar = gtk_menu_bar_new(); + GtkWidget* netMenu = makeAddSubmenu( menubar, "Network" ); + if ( params->useHTTP ) { + (void)createAddItem( netMenu, "Check for moves", + (GCallback)handle_movescheck, apg ); + } + gtk_widget_show( menubar ); + gtk_box_pack_start( GTK_BOX(vbox), menubar, FALSE, TRUE, 0 ); + GtkWidget* list = init_games_list( apg ); gtk_container_add( GTK_CONTAINER(vbox), list ); @@ -693,6 +714,17 @@ gtkGotBuf( void* closure, const CommsAddrRec* from, XP_USE( seed ); } +static void +gtkGotMsgForRow( void* closure, const CommsAddrRec* from, + sqlite3_int64 rowid, const XP_U8* buf, XP_U16 len ) +{ + XP_LOGF( "%s(): got msg of len %d for row %lld", __func__, len, rowid ); + GtkAppGlobals* apg = (GtkAppGlobals*)closure; + // LaunchParams* params = apg->params; + (void)feedBufferGTK( apg, rowid, buf, len, from ); + LOG_RETURN_VOID(); +} + static gint requestMsgs( gpointer data ) { @@ -847,6 +879,7 @@ gtkmain( LaunchParams* params ) if ( params->useUdp ) { RelayConnProcs procs = { .msgReceived = gtkGotBuf, + .msgForRow = gtkGotMsgForRow, .msgNoticeReceived = gtkNoticeRcvd, .devIDReceived = gtkDevIDReceived, .msgErrorMsg = gtkErrorMsgRcvd, diff --git a/xwords4/linux/linuxmain.c b/xwords4/linux/linuxmain.c index 124321ea0..ffdc68917 100644 --- a/xwords4/linux/linuxmain.c +++ b/xwords4/linux/linuxmain.c @@ -634,6 +634,8 @@ typedef enum { ,CMD_CHAT ,CMD_USEUDP ,CMD_NOUDP + ,CMD_USEHTTP + ,CMD_NOHTTPAUTO ,CMD_DROPSENDRELAY ,CMD_DROPRCVRELAY ,CMD_DROPSENDSMS @@ -752,6 +754,8 @@ static CmdInfoRec CmdInfoRecs[] = { ,{ CMD_CHAT, true, "send-chat", "send a chat every seconds" } ,{ CMD_USEUDP, false, "use-udp", "connect to relay new-style, via udp not tcp (on by default)" } ,{ CMD_NOUDP, false, "no-use-udp", "connect to relay old-style, via tcp not udp" } + ,{ CMD_USEHTTP, false, "use-http", "use relay's new http interfaces rather than sockets" } + ,{ CMD_NOHTTPAUTO, false, "no-http-auto", "When http's on, don't periodically connect to relay (manual only)" } ,{ CMD_DROPSENDRELAY, false, "drop-send-relay", "start new games with relay send disabled" } ,{ CMD_DROPRCVRELAY, false, "drop-receive-relay", "start new games with relay receive disabled" } @@ -973,6 +977,7 @@ linux_setupDevidParams( LaunchParams* params ) static int linux_init_relay_socket( CommonGlobals* cGlobals, const CommsAddrRec* addrRec ) { + XP_ASSERT( !cGlobals->params->useHTTP ); struct sockaddr_in to_sock; struct hostent* host; int sock = cGlobals->relaySocket; @@ -1174,6 +1179,7 @@ linux_relay_send( CommonGlobals* cGlobals, const XP_U8* buf, XP_U16 buflen, result = relaycon_send( cGlobals->params, buf, buflen, clientToken, addrRec ); } else { + XP_ASSERT( !cGlobals->params->useHTTP ); int sock = cGlobals->relaySocket; if ( sock == -1 ) { @@ -1552,8 +1558,8 @@ linuxChangeRoles( CommonGlobals* cGlobals ) } #endif -static unsigned int -defaultRandomSeed() +unsigned int +makeRandomInt() { /* use kernel device rather than time() so can run multiple times/second without getting the same results. */ @@ -2028,7 +2034,7 @@ main( int argc, char** argv ) XP_Bool isServer = XP_FALSE; // char* portNum = NULL; // char* hostName = "localhost"; - unsigned int seed = defaultRandomSeed(); + unsigned int seed = makeRandomInt(); LaunchParams mainParams; XP_U16 nPlayerDicts = 0; XP_U16 robotCount = 0; @@ -2401,6 +2407,12 @@ main( int argc, char** argv ) case CMD_NOUDP: mainParams.useUdp = false; break; + case CMD_USEHTTP: + mainParams.useHTTP = true; + break; + case CMD_NOHTTPAUTO: + mainParams.noHTTPAuto = true; + break; case CMD_DROPSENDRELAY: mainParams.commsDisableds[COMMS_CONN_RELAY][1] = XP_TRUE; @@ -2490,10 +2502,10 @@ main( int argc, char** argv ) mainParams.dictDirs = g_slist_append( mainParams.dictDirs, "./" ); } - if ( isServer ) { - if ( mainParams.info.serverInfo.nRemotePlayers == 0 ) { - mainParams.pgi.serverRole = SERVER_STANDALONE; - } else { + if ( mainParams.info.serverInfo.nRemotePlayers == 0 ) { + mainParams.pgi.serverRole = SERVER_STANDALONE; + } else if ( isServer ) { + if ( mainParams.info.serverInfo.nRemotePlayers > 0 ) { mainParams.pgi.serverRole = SERVER_ISSERVER; } } else { @@ -2649,7 +2661,8 @@ main( int argc, char** argv ) if ( mainParams.useCurses ) { if ( mainParams.needsNewGame ) { /* curses doesn't have newgame dialog */ - usage( argv[0], "game params required for curses version" ); + usage( argv[0], "game params required for curses version, e.g. --name Eric --room MyRoom" + " --remote-player --dict-dir ../ --game-dict CollegeEng_2to8.xwd"); } else { #if defined PLATFORM_NCURSES cursesmain( isServer, &mainParams ); diff --git a/xwords4/linux/linuxmain.h b/xwords4/linux/linuxmain.h index 5f902c810..a3622257e 100644 --- a/xwords4/linux/linuxmain.h +++ b/xwords4/linux/linuxmain.h @@ -111,6 +111,8 @@ const XP_UCHAR* linux_getDevID( LaunchParams* params, DevIDType* typ ); void linux_doInitialReg( LaunchParams* params, XP_Bool idIsNew ); XP_Bool linux_setupDevidParams( LaunchParams* params ); +unsigned int makeRandomInt(); + /* void initParams( LaunchParams* params ); */ /* void freeParams( LaunchParams* params ); */ diff --git a/xwords4/linux/linuxutl.c b/xwords4/linux/linuxutl.c index 95f99666d..fbab06504 100644 --- a/xwords4/linux/linuxutl.c +++ b/xwords4/linux/linuxutl.c @@ -41,7 +41,7 @@ void linux_debugf( const char* format, ... ) { - char buf[1000]; + char buf[1024*8]; va_list ap; struct tm* timp; struct timeval tv; @@ -50,14 +50,17 @@ linux_debugf( const char* format, ... ) gettimeofday( &tv, &tz ); timp = localtime( &tv.tv_sec ); - snprintf( buf, sizeof(buf), "<%d>%.2d:%.2d:%.2d:", getpid(), - timp->tm_hour, timp->tm_min, timp->tm_sec ); + size_t len = snprintf( buf, sizeof(buf), "<%d:%lx>%.2d:%.2d:%.2d:", getpid(), + pthread_self(), timp->tm_hour, timp->tm_min, timp->tm_sec ); + XP_ASSERT( len < sizeof(buf) ); va_start(ap, format); - - vsprintf(buf+strlen(buf), format, ap); - + len = vsprintf(buf+strlen(buf), format, ap); va_end(ap); + + if ( len >= sizeof(buf) ) { + buf[sizeof(buf)-1] = '\0'; + } fprintf( stderr, "%s\n", buf ); } diff --git a/xwords4/linux/main.h b/xwords4/linux/main.h index 93605dc93..5f90e6999 100644 --- a/xwords4/linux/main.h +++ b/xwords4/linux/main.h @@ -105,6 +105,8 @@ typedef struct LaunchParams { XP_Bool closeStdin; XP_Bool useCurses; XP_Bool useUdp; + XP_Bool useHTTP; + XP_Bool noHTTPAuto; XP_U16 splitPackets; XP_U16 chatsInterval; /* 0 means disabled */ XP_U16 askTimeout; diff --git a/xwords4/linux/relaycon.c b/xwords4/linux/relaycon.c index 0519177dd..6f0c2a534 100644 --- a/xwords4/linux/relaycon.c +++ b/xwords4/linux/relaycon.c @@ -20,12 +20,29 @@ #include #include #include +#include +#include + #include "relaycon.h" #include "linuxmain.h" #include "comtypes.h" +#include "gamesdb.h" + +#define MAX_MOVE_CHECK_MS ((XP_U16)(1000 * 60 * 60 * 24)) +#define RELAY_API_PROTO "http" typedef struct _RelayConStorage { + pthread_t mainThread; + guint moveCheckerID; + XP_U32 nextMoveCheckMS; + pthread_cond_t relayCondVar; + pthread_mutex_t relayMutex; + GSList* relayTaskList; + + pthread_mutex_t gotDataMutex; + GSList* gotDataTaskList; + int socket; RelayConnProcs procs; void* procsClosure; @@ -33,6 +50,8 @@ typedef struct _RelayConStorage { uint32_t nextID; XWPDevProto proto; LaunchParams* params; + XP_UCHAR host[64]; + int nextTaskID; } RelayConStorage; typedef struct _MsgHeader { @@ -41,10 +60,16 @@ typedef struct _MsgHeader { } MsgHeader; static RelayConStorage* getStorage( LaunchParams* params ); +static XP_Bool onMainThread( RelayConStorage* storage ); static XP_U32 hostNameToIP( const XP_UCHAR* name ); static gboolean relaycon_receive( GIOChannel *source, GIOCondition condition, gpointer data ); -static ssize_t sendIt( RelayConStorage* storage, const XP_U8* msgbuf, XP_U16 len ); +static void schedule_next_check( RelayConStorage* storage ); +static void reset_schedule_check_interval( RelayConStorage* storage ); +static void checkForMovesOnce( RelayConStorage* storage ); +static gboolean gotDataTimer(gpointer user_data); + +static ssize_t sendIt( RelayConStorage* storage, const XP_U8* msgbuf, XP_U16 len, float timeoutSecs ); static size_t addVLIStr( XP_U8* buf, size_t len, const XP_UCHAR* str ); static void getNetString( const XP_U8** ptr, XP_U16 len, XP_UCHAR* buf ); static XP_U16 getNetShort( const XP_U8** ptr ); @@ -59,7 +84,160 @@ static size_t writeBytes( XP_U8* buf, size_t len, const XP_U8* bytes, static size_t writeVLI( XP_U8* out, uint32_t nn ); static size_t un2vli( int nn, uint8_t* buf ); static bool vli2un( const uint8_t** inp, uint32_t* outp ); +#ifdef DEBUG +static const char* msgToStr( XWRelayReg msg ); +#endif +static void* relayThread( void* arg ); + +typedef struct _WriteState { + gchar* ptr; + size_t curSize; +} WriteState; + +typedef enum { +#ifdef RELAY_VIA_HTTP + JOIN, +#endif + POST, QUERY, } TaskType; + +typedef struct _RelayTask { + TaskType typ; + int id; + RelayConStorage* storage; + WriteState ws; + XP_U32 ctime; + union { +#ifdef RELAY_VIA_HTTP + struct { + XP_U16 lang; + XP_U16 nHere; + XP_U16 nTotal; + XP_U16 seed; + XP_UCHAR devID[64]; + XP_UCHAR room[MAX_INVITE_LEN + 1]; + OnJoinedProc proc; + void* closure; + } join; +#endif + struct { + XP_U8* msgbuf; + XP_U16 len; + float timeoutSecs; + } post; + struct { + GHashTable* map; + } query; + } u; +} RelayTask; + +static RelayTask* makeRelayTask( RelayConStorage* storage, TaskType typ ); +static void freeRelayTask(RelayTask* task); +#ifdef RELAY_VIA_HTTP +static void handleJoin( RelayTask* task ); +#endif +static void handlePost( RelayTask* task ); +static void handleQuery( RelayTask* task ); +static void addToGotData( RelayTask* task ); +static RelayTask* getFromGotData( RelayConStorage* storage ); + +static size_t +write_callback(void *contents, size_t size, size_t nmemb, void* data) +{ + WriteState* ws = (WriteState*)data; + + if ( !ws->ptr ) { + ws->ptr = g_malloc0(1); + ws->curSize = 1L; + } + + XP_LOGF( "%s(size=%ld, nmemb=%ld)", __func__, size, nmemb ); + size_t oldLen = ws->curSize; + const size_t newLength = size * nmemb; + XP_ASSERT( (oldLen + newLength) > 0 ); + ws->ptr = g_realloc( ws->ptr, oldLen + newLength ); + memcpy( ws->ptr + oldLen - 1, contents, newLength ); + ws->ptr[oldLen + newLength - 1] = '\0'; + // XP_LOGF( "%s() => %ld: (passed: \"%s\")", __func__, result, *strp ); + return newLength; +} + +static gchar* +mkJsonParams( CURL* curl, va_list ap ) +{ + json_object* params = json_object_new_object(); + for ( ; ; ) { + const char* name = va_arg(ap, const char*); + if ( !name ) { + break; + } + json_object* param = va_arg(ap, json_object*); + XP_ASSERT( !!param ); + + json_object_object_add( params, name, param ); + // XP_LOGF( "%s: adding param (with name %s): %s", __func__, name, json_object_get_string(param) ); + } + + const char* asStr = json_object_get_string( params ); + char* curl_params = curl_easy_escape( curl, asStr, strlen(asStr) ); + gchar* result = g_strdup_printf( "params=%s", curl_params ); + XP_LOGF( "%s: adding: params=%s (%s)", __func__, asStr, curl_params ); + curl_free( curl_params ); + json_object_put( params ); + return result; +} + +/* relay.py's methods all take one json object param "param" So we wrap + everything else in that then send it. */ +static XP_Bool +runWitCurl( RelayTask* task, const gchar* proc, ...) +{ + CURLcode res = curl_global_init(CURL_GLOBAL_DEFAULT); + XP_ASSERT(res == CURLE_OK); + CURL* curl = curl_easy_init(); + + char url[128]; + snprintf( url, sizeof(url), "%s://%s/xw4/relay.py/%s", + RELAY_API_PROTO, task->storage->host, proc ); + curl_easy_setopt( curl, CURLOPT_URL, url ); + curl_easy_setopt( curl, CURLOPT_POST, 1L ); + + va_list ap; + va_start( ap, proc ); + gchar* params = mkJsonParams( curl, ap ); + va_end( ap ); + + curl_easy_setopt( curl, CURLOPT_POSTFIELDS, params ); + curl_easy_setopt( curl, CURLOPT_POSTFIELDSIZE, (long)strlen(params) ); + + curl_easy_setopt( curl, CURLOPT_WRITEFUNCTION, write_callback ); + curl_easy_setopt( curl, CURLOPT_WRITEDATA, &task->ws ); + // curl_easy_setopt( curl, CURLOPT_VERBOSE, 1L ); + + res = curl_easy_perform(curl); + XP_Bool success = res == CURLE_OK; + XP_LOGF( "%s(): curl_easy_perform(%s) => %d", __func__, proc, res ); + /* Check for errors */ + if ( ! success ) { + XP_LOGF( "curl_easy_perform() failed: %s", curl_easy_strerror(res)); + } else { + XP_LOGF( "%s(): got for %s: \"%s\"", __func__, proc, task->ws.ptr ); + } + /* always cleanup */ + curl_easy_cleanup(curl); + curl_global_cleanup(); + g_free( params ); + return success; +} + +void +relaycon_checkMsgs( LaunchParams* params ) +{ + LOG_FUNC(); + RelayConStorage* storage = getStorage( params ); + XP_ASSERT( onMainThread(storage) ); + checkForMovesOnce( storage ); +} void relaycon_init( LaunchParams* params, const RelayConnProcs* procs, @@ -70,17 +248,36 @@ relaycon_init( LaunchParams* params, const RelayConnProcs* procs, XP_MEMCPY( &storage->procs, procs, sizeof(storage->procs) ); storage->procsClosure = procsClosure; - storage->socket = socket( AF_INET, SOCK_DGRAM, IPPROTO_UDP ); - (*procs->socketAdded)( storage, storage->socket, relaycon_receive ); + if ( params->useHTTP ) { + storage->mainThread = pthread_self(); + pthread_mutex_init( &storage->relayMutex, NULL ); + pthread_cond_init( &storage->relayCondVar, NULL ); + pthread_t thread; + (void)pthread_create( &thread, NULL, relayThread, storage ); + pthread_detach( thread ); - XP_MEMSET( &storage->saddr, 0, sizeof(storage->saddr) ); - storage->saddr.sin_family = PF_INET; - storage->saddr.sin_addr.s_addr = htonl( hostNameToIP(host) ); - storage->saddr.sin_port = htons(port); + pthread_mutex_init( &storage->gotDataMutex, NULL ); + g_timeout_add( 50, gotDataTimer, storage ); + XP_ASSERT( XP_STRLEN(host) < VSIZE(storage->host) ); + XP_MEMCPY( storage->host, host, XP_STRLEN(host) + 1 ); + } else { + storage->socket = socket( AF_INET, SOCK_DGRAM, IPPROTO_UDP ); + (*procs->socketAdded)( storage, storage->socket, relaycon_receive ); + + XP_MEMSET( &storage->saddr, 0, sizeof(storage->saddr) ); + storage->saddr.sin_family = PF_INET; + storage->saddr.sin_addr.s_addr = htonl( hostNameToIP(host) ); + storage->saddr.sin_port = htons(port); + + } storage->params = params; storage->proto = XWPDEV_PROTO_VERSION_1; + + if ( params->useHTTP ) { + schedule_next_check( storage ); + } } /* Send existing relay-assigned rDevID to relay, or empty string if we have @@ -109,7 +306,7 @@ relaycon_reg( LaunchParams* params, const XP_UCHAR* rDevID, indx += addVLIStr( &tmpbuf[indx], sizeof(tmpbuf) - indx, "linux box" ); indx += addVLIStr( &tmpbuf[indx], sizeof(tmpbuf) - indx, "linux version" ); - sendIt( storage, tmpbuf, indx ); + sendIt( storage, tmpbuf, indx, 0.5 ); } void @@ -146,7 +343,7 @@ relaycon_invite( LaunchParams* params, XP_U32 destDevID, indx += writeBytes( &tmpbuf[indx], sizeof(tmpbuf) - indx, ptr, len ); stream_destroy( stream ); - sendIt( storage, tmpbuf, indx ); + sendIt( storage, tmpbuf, indx, 0.5 ); LOG_RETURN_VOID(); } @@ -163,7 +360,7 @@ relaycon_send( LaunchParams* params, const XP_U8* buf, XP_U16 buflen, indx += writeHeader( storage, tmpbuf, XWPDEV_MSG ); indx += writeLong( &tmpbuf[indx], sizeof(tmpbuf) - indx, gameToken ); indx += writeBytes( &tmpbuf[indx], sizeof(tmpbuf) - indx, buf, buflen ); - nSent = sendIt( storage, tmpbuf, indx ); + nSent = sendIt( storage, tmpbuf, indx, 0.5 ); if ( nSent > buflen ) { nSent = buflen; } @@ -191,7 +388,7 @@ relaycon_sendnoconn( LaunchParams* params, const XP_U8* buf, XP_U16 buflen, (const XP_U8*)relayID, idLen ); tmpbuf[indx++] = '\n'; indx += writeBytes( &tmpbuf[indx], sizeof(tmpbuf) - indx, buf, buflen ); - nSent = sendIt( storage, tmpbuf, indx ); + nSent = sendIt( storage, tmpbuf, indx, 0.5 ); if ( nSent > buflen ) { nSent = buflen; } @@ -210,7 +407,7 @@ relaycon_requestMsgs( LaunchParams* params, const XP_UCHAR* devID ) indx += writeHeader( storage, tmpbuf, XWPDEV_RQSTMSGS ); indx += addVLIStr( &tmpbuf[indx], sizeof(tmpbuf) - indx, devID ); - sendIt( storage, tmpbuf, indx ); + sendIt( storage, tmpbuf, indx, 0.5 ); } void @@ -225,9 +422,170 @@ relaycon_deleted( LaunchParams* params, const XP_UCHAR* devID, indx += writeDevID( &tmpbuf[indx], sizeof(tmpbuf) - indx, devID ); indx += writeLong( &tmpbuf[indx], sizeof(tmpbuf) - indx, gameToken ); - sendIt( storage, tmpbuf, indx ); + sendIt( storage, tmpbuf, indx, 0.1 ); } +static XP_Bool +onMainThread( RelayConStorage* storage ) +{ + return storage->mainThread = pthread_self(); +} + +static const gchar* +taskName( const RelayTask* task ) +{ + const char* str; +# define CASE_STR(c) case c: str = #c; break + switch (task->typ) { + CASE_STR(POST); + CASE_STR(QUERY); +#ifdef RELAY_VIA_HTTP + CASE_STR(JOIN); +#endif + default: XP_ASSERT(0); + str = NULL; + } +#undef CASE_STR + return str; +} + +static gchar* +listTasks( GSList* tasks ) +{ + XP_U32 now = (XP_U32)time(NULL); + gchar* names[1 + g_slist_length(tasks)]; + int len = g_slist_length(tasks); + names[len] = NULL; + for ( int ii = 0; !!tasks; ++ii ) { + RelayTask* task = (RelayTask*)tasks->data; + names[ii] = g_strdup_printf( "{%s:id:%d;age:%ds}", taskName(task), + task->id, now - task->ctime ); + tasks = tasks->next; + } + + gchar* result = g_strjoinv( ",", names ); + for ( int ii = 0; ii < len; ++ii ) { + g_free( names[ii] ); + } + return result; +} + +static void* +relayThread( void* arg ) +{ + LOG_FUNC(); + RelayConStorage* storage = (RelayConStorage*)arg; + for ( ; ; ) { + pthread_mutex_lock( &storage->relayMutex ); + while ( !storage->relayTaskList ) { + pthread_cond_wait( &storage->relayCondVar, &storage->relayMutex ); + } + + int len = g_slist_length( storage->relayTaskList ); + gchar* strs = listTasks( storage->relayTaskList ); + GSList* head = storage->relayTaskList; + storage->relayTaskList = g_slist_remove_link( storage->relayTaskList, + storage->relayTaskList ); + RelayTask* task = head->data; + g_slist_free( head ); + + + pthread_mutex_unlock( &storage->relayMutex ); + + XP_LOGF( "%s(): processing first of %d (%s)", __func__, len, strs ); + g_free( strs ); + + switch ( task->typ ) { +#ifdef RELAY_VIA_HTTP + case JOIN: + handleJoin( task ); + break; +#endif + case POST: + handlePost( task ); + break; + case QUERY: + handleQuery( task ); + break; + default: + XP_ASSERT(0); + } + } + return NULL; +} + +static XP_Bool +didCombine( const RelayTask* one, const RelayTask* two ) +{ + /* For now.... */ + XP_Bool result = one->typ == QUERY && two->typ == QUERY; + return result; +} + +static void +addTask( RelayConStorage* storage, RelayTask* task ) +{ + pthread_mutex_lock( &storage->relayMutex ); + + /* Let's see if the current last task is the same. */ + GSList* last = g_slist_last( storage->relayTaskList ); + if ( !!last && didCombine( last->data, task ) ) { + freeRelayTask( task ); + } else { + storage->relayTaskList = g_slist_append( storage->relayTaskList, task ); + } + gchar* strs = listTasks( storage->relayTaskList ); + pthread_cond_signal( &storage->relayCondVar ); + pthread_mutex_unlock( &storage->relayMutex ); + XP_LOGF( "%s(): task list now: %s", __func__, strs ); + g_free( strs ); +} + +static RelayTask* +makeRelayTask( RelayConStorage* storage, TaskType typ ) +{ + XP_ASSERT( onMainThread(storage) ); + RelayTask* task = (RelayTask*)g_malloc0(sizeof(*task)); + task->typ = typ; + task->id = ++storage->nextTaskID; + task->ctime = (XP_U32)time(NULL); + task->storage = storage; + return task; +} + +static void +freeRelayTask( RelayTask* task ) +{ + GSList faker = { .next = NULL, .data = task }; + gchar* str = listTasks(&faker); + XP_LOGF( "%s(): deleting %s", __func__, str ); + g_free( str ); + g_free( task->ws.ptr ); + g_free( task ); +} + +#ifdef RELAY_VIA_HTTP +void +relaycon_join( LaunchParams* params, const XP_UCHAR* devID, const XP_UCHAR* room, + XP_U16 nPlayersHere, XP_U16 nPlayersTotal, XP_U16 seed, XP_U16 lang, + OnJoinedProc proc, void* closure ) +{ + LOG_FUNC(); + RelayConStorage* storage = getStorage( params ); + XP_ASSERT( onMainThread(storage) ); + RelayTask* task = makeRelayTask( storage, JOIN ); + task->u.join.nHere = nPlayersHere; + XP_STRNCPY( task->u.join.devID, devID, sizeof(task->u.join.devID) ); + XP_STRNCPY( task->u.join.room, room, sizeof(task->u.join.room) ); + task->u.join.nTotal = nPlayersTotal; + task->u.join.lang = lang; + task->u.join.seed = seed; + task->u.join.proc = proc; + task->u.join.closure = closure; + addTask( storage, task ); +} +#endif + static void sendAckIf( RelayConStorage* storage, const MsgHeader* header ) { @@ -235,41 +593,22 @@ sendAckIf( RelayConStorage* storage, const MsgHeader* header ) XP_U8 tmpbuf[16]; int indx = writeHeader( storage, tmpbuf, XWPDEV_ACK ); indx += writeVLI( &tmpbuf[indx], header->packetID ); - sendIt( storage, tmpbuf, indx ); + sendIt( storage, tmpbuf, indx, 0.1 ); } } -static gboolean -relaycon_receive( GIOChannel* source, GIOCondition XP_UNUSED_DBG(condition), gpointer data ) +static gboolean +process( RelayConStorage* storage, XP_U8* buf, ssize_t nRead ) { - XP_ASSERT( 0 != (G_IO_IN & condition) ); /* FIX ME */ - RelayConStorage* storage = (RelayConStorage*)data; - XP_U8 buf[512]; - struct sockaddr_in from; - socklen_t fromlen = sizeof(from); - - int socket = g_io_channel_unix_get_fd( source ); - XP_LOGF( "%s: calling recvfrom on socket %d", __func__, socket ); - - ssize_t nRead = recvfrom( socket, buf, sizeof(buf), 0, /* flags */ - (struct sockaddr*)&from, &fromlen ); - - gchar* b64 = g_base64_encode( (const guchar*)buf, - ((0 <= nRead)? nRead : 0) ); - XP_LOGF( "%s: read %zd bytes ('%s')", __func__, nRead, b64 ); -#ifdef COMMS_CHECKSUM - gchar* sum = g_compute_checksum_for_data( G_CHECKSUM_MD5, buf, nRead ); - XP_LOGF( "%s: read %zd bytes ('%s')(sum=%s)", __func__, nRead, b64, sum ); - g_free( sum ); -#endif - g_free( b64 ); - if ( 0 <= nRead ) { const XP_U8* ptr = buf; const XP_U8* end = buf + nRead; MsgHeader header; if ( readHeader( &ptr, &header ) ) { sendAckIf( storage, &header ); + + XP_LOGF( "%s(): got %s", __func__, msgToStr(header.cmd) ); + switch( header.cmd ) { case XWPDEV_REGRSP: { uint32_t len; @@ -319,7 +658,7 @@ relaycon_receive( GIOChannel* source, GIOCondition XP_UNUSED_DBG(condition), gpo assert( 0 ); } XP_USE( packetID ); - XP_LOGF( "got ack for packetID %d", packetID ); + XP_LOGF( "%s(): got ack for packetID %d", __func__, packetID ); break; } case XWPDEV_ALERT: { @@ -367,9 +706,55 @@ relaycon_receive( GIOChannel* source, GIOCondition XP_UNUSED_DBG(condition), gpo return TRUE; } +static gboolean +relaycon_receive( GIOChannel* source, GIOCondition XP_UNUSED_DBG(condition), gpointer data ) +{ + XP_ASSERT( 0 != (G_IO_IN & condition) ); /* FIX ME */ + RelayConStorage* storage = (RelayConStorage*)data; + XP_ASSERT( !storage->params->useHTTP ); + XP_U8 buf[512]; + struct sockaddr_in from; + socklen_t fromlen = sizeof(from); + + int socket = g_io_channel_unix_get_fd( source ); + XP_LOGF( "%s: calling recvfrom on socket %d", __func__, socket ); + + ssize_t nRead = recvfrom( socket, buf, sizeof(buf), 0, /* flags */ + (struct sockaddr*)&from, &fromlen ); + + gchar* b64 = g_base64_encode( (const guchar*)buf, + ((0 <= nRead)? nRead : 0) ); + XP_LOGF( "%s: read %zd bytes ('%s')", __func__, nRead, b64 ); +#ifdef COMMS_CHECKSUM + gchar* sum = g_compute_checksum_for_data( G_CHECKSUM_MD5, buf, nRead ); + XP_LOGF( "%s: read %zd bytes ('%s')(sum=%s)", __func__, nRead, b64, sum ); + g_free( sum ); +#endif + g_free( b64 ); + return process( storage, buf, nRead ); +} + void relaycon_cleanup( LaunchParams* params ) { + RelayConStorage* storage = (RelayConStorage*)params->relayConStorage; + if ( storage->params->useHTTP ) { + pthread_mutex_lock( &storage->relayMutex ); + int nRelayTasks = g_slist_length( storage->relayTaskList ); + gchar* taskStrs = listTasks( storage->relayTaskList ); + pthread_mutex_unlock( &storage->relayMutex ); + + pthread_mutex_lock( &storage->gotDataMutex ); + int nDataTasks = g_slist_length( storage->gotDataTaskList ); + gchar* gotStrs = listTasks( storage->gotDataTaskList ); + pthread_mutex_unlock( &storage->gotDataMutex ); + + XP_LOGF( "%s(): sends pending: %d (%s); data tasks pending: %d (%s)", __func__, + nRelayTasks, gotStrs, nDataTasks, taskStrs ); + + g_free( gotStrs ); + g_free( taskStrs ); + } XP_FREEP( params->mpool, ¶ms->relayConStorage ); } @@ -403,12 +788,322 @@ hostNameToIP( const XP_UCHAR* name ) return ip; } -static ssize_t -sendIt( RelayConStorage* storage, const XP_U8* msgbuf, XP_U16 len ) +#ifdef RELAY_VIA_HTTP +static void +onGotJoinData( RelayTask* task ) { - ssize_t nSent = sendto( storage->socket, msgbuf, len, 0, /* flags */ - (struct sockaddr*)&storage->saddr, - sizeof(storage->saddr) ); + LOG_FUNC(); + RelayConStorage* storage = task->storage; + XP_ASSERT( onMainThread(storage) ); + if ( !!task->ws.ptr ) { + XP_LOGF( "%s(): got json? %s", __func__, task->ws.ptr ); + json_object* reply = json_tokener_parse( task->ws.ptr ); + json_object* jConnname = NULL; + json_object* jHID = NULL; + if ( json_object_object_get_ex( reply, "connname", &jConnname ) + && json_object_object_get_ex( reply, "hid", &jHID ) ) { + const char* connname = json_object_get_string( jConnname ); + XWHostID hid = json_object_get_int( jHID ); + (*task->u.join.proc)( task->u.join.closure, connname, hid ); + } + json_object_put( jConnname ); + json_object_put( jHID ); + } + freeRelayTask( task ); +} +#endif + +static gboolean +onGotPostData( RelayTask* task ) +{ + RelayConStorage* storage = task->storage; + /* Now pull any data from the reply */ + // got "{"status": "ok", "dataLen": 14, "data": "AYQDiDAyMUEzQ0MyADw=", "err": "none"}" + if ( !!task->ws.ptr ) { + json_object* reply = json_tokener_parse( task->ws.ptr ); + json_object* replyData; + if ( json_object_object_get_ex( reply, "data", &replyData ) && !!replyData ) { + const int len = json_object_array_length(replyData); + for ( int ii = 0; ii < len; ++ii ) { + json_object* datum = json_object_array_get_idx( replyData, ii ); + const char* str = json_object_get_string( datum ); + gsize out_len; + guchar* buf = g_base64_decode( (const gchar*)str, &out_len ); + process( storage, buf, out_len ); + g_free( buf ); + } + (void)json_object_put( replyData ); + } + (void)json_object_put( reply ); + } + + g_free( task->u.post.msgbuf ); + + freeRelayTask( task ); + + return FALSE; +} + +#ifdef RELAY_VIA_HTTP +static void +handleJoin( RelayTask* task ) +{ + LOG_FUNC(); + runWitCurl( task, "join", + "devID", json_object_new_string( task->u.join.devID ), + "room", json_object_new_string( task->u.join.room ), + "seed", json_object_new_int( task->u.join.seed ), + "lang", json_object_new_int( task->u.join.lang ), + "nInGame", json_object_new_int( task->u.join.nTotal ), + "nHere", json_object_new_int( task->u.join.nHere ), + NULL ); + addToGotData( task ); +} +#endif + +static void +handlePost( RelayTask* task ) +{ + XP_LOGF( "%s(task.post.len=%d)", __func__, task->u.post.len ); + XP_ASSERT( !onMainThread(task->storage) ); + char* data = g_base64_encode( task->u.post.msgbuf, task->u.post.len ); + struct json_object* jstr = json_object_new_string(data); + g_free( data ); + + /* The protocol takes an array of messages so they can be combined. Do + that soon. */ + json_object* dataArr = json_object_new_array(); + json_object_array_add( dataArr, jstr); + + json_object* jTimeout = json_object_new_double( task->u.post.timeoutSecs ); + runWitCurl( task, "post", "data", dataArr, "timeoutSecs", jTimeout, NULL ); + + // Put the data on the main thread for processing + addToGotData( task ); +} /* handlePost */ + +static ssize_t +post( RelayConStorage* storage, const XP_U8* msgbuf, XP_U16 len, float timeout ) +{ + XP_LOGF( "%s(len=%d)", __func__, len ); + RelayTask* task = makeRelayTask( storage, POST ); + task->u.post.msgbuf = g_malloc(len); + task->u.post.timeoutSecs = timeout; + XP_MEMCPY( task->u.post.msgbuf, msgbuf, len ); + task->u.post.len = len; + addTask( storage, task ); + return len; +} + +static gboolean +onGotQueryData( RelayTask* task ) +{ + RelayConStorage* storage = task->storage; + XP_Bool foundAny = false; + if ( !!task->ws.ptr ) { + json_object* reply = json_tokener_parse( task->ws.ptr ); + if ( !!reply ) { + CommsAddrRec addr = {0}; + addr_addType( &addr, COMMS_CONN_RELAY ); + + GList* ids = g_hash_table_get_keys( task->u.query.map ); + const char* xxx = ids->data; + + json_object* jMsgs; + if ( json_object_object_get_ex( reply, "msgs", &jMsgs ) ) { + /* Currently there's an array of arrays for each relayID (value) */ + XP_LOGF( "%s: got result of len %d", __func__, json_object_object_length(jMsgs) ); + XP_ASSERT( json_object_object_length(jMsgs) <= 1 ); + json_object_object_foreach(jMsgs, relayID, arrOfArrOfMoves) { + XP_ASSERT( 0 == strcmp( relayID, xxx ) ); + int len1 = json_object_array_length( arrOfArrOfMoves ); + if ( len1 > 0 ) { + sqlite3_int64 rowid = *(sqlite3_int64*)g_hash_table_lookup( task->u.query.map, relayID ); + XP_LOGF( "%s(): got row %lld for relayID %s", __func__, rowid, relayID ); + for ( int ii = 0; ii < len1; ++ii ) { + json_object* forGameArray = json_object_array_get_idx( arrOfArrOfMoves, ii ); + int len2 = json_object_array_length( forGameArray ); + for ( int jj = 0; jj < len2; ++jj ) { + json_object* oneMove = json_object_array_get_idx( forGameArray, jj ); + const char* asStr = json_object_get_string( oneMove ); + gsize out_len; + guchar* buf = g_base64_decode( asStr, &out_len ); + (*storage->procs.msgForRow)( storage->procsClosure, &addr, + rowid, buf, out_len ); + g_free(buf); + foundAny = XP_TRUE; + } + } + } + } + json_object_put( jMsgs ); + } + json_object_put( reply ); + } + } + + if ( foundAny ) { + /* Reschedule. If we got anything this time, check again sooner! */ + reset_schedule_check_interval( storage ); + } + schedule_next_check( storage ); + + g_hash_table_destroy( task->u.query.map ); + freeRelayTask(task); + + return FALSE; +} + +static void +handleQuery( RelayTask* task ) +{ + XP_ASSERT( !onMainThread(task->storage) ); + + if ( g_hash_table_size( task->u.query.map ) > 0 ) { + GList* ids = g_hash_table_get_keys( task->u.query.map ); + + json_object* jIds = json_object_new_array(); + for ( GList* iter = ids; !!iter; iter = iter->next ) { + json_object* idstr = json_object_new_string( iter->data ); + json_object_array_add(jIds, idstr); + XP_ASSERT( !iter->next ); /* for curses case there should be only one */ + } + g_list_free( ids ); + + runWitCurl( task, "query", "ids", jIds, NULL ); + } + /* Put processing back on the main thread */ + addToGotData( task ); +} /* handleQuery */ + +static void +checkForMovesOnce( RelayConStorage* storage ) +{ + LOG_FUNC(); + XP_ASSERT( onMainThread(storage) ); + + RelayTask* task = makeRelayTask( storage, QUERY ); + sqlite3* dbp = storage->params->pDb; + task->u.query.map = getRelayIDsToRowsMap( dbp ); + addTask( storage, task ); +} + +static gboolean +checkForMoves( gpointer user_data ) +{ + RelayConStorage* storage = (RelayConStorage*)user_data; + checkForMovesOnce( storage ); + schedule_next_check( storage ); + return FALSE; +} + +static gboolean +gotDataTimer(gpointer user_data) +{ + RelayConStorage* storage = (RelayConStorage*)user_data; + assert( onMainThread(storage) ); + + for ( ; ; ) { + RelayTask* task = getFromGotData( storage ); + + if ( !task ) { + break; + } else { + switch ( task->typ ) { +#ifdef RELAY_VIA_HTTP + case JOIN: + onGotJoinData( task ); + break; +#endif + case POST: + onGotPostData( task ); + break; + case QUERY: + onGotQueryData( task ); + break; + default: + XP_ASSERT(0); + } + } + } + return TRUE; +} + +static void +addToGotData( RelayTask* task ) +{ + RelayConStorage* storage = task->storage; + pthread_mutex_lock( &storage->gotDataMutex ); + storage->gotDataTaskList = g_slist_append( storage->gotDataTaskList, task ); + XP_LOGF( "%s(): added id %d; len now %d", __func__, task->id, + g_slist_length(storage->gotDataTaskList) ); + pthread_mutex_unlock( &storage->gotDataMutex ); +} + +static RelayTask* +getFromGotData( RelayConStorage* storage ) +{ + RelayTask* task = NULL; + XP_ASSERT( onMainThread(storage) ); + pthread_mutex_lock( &storage->gotDataMutex ); + int len = g_slist_length( storage->gotDataTaskList ); + // XP_LOGF( "%s(): before: len: %d", __func__, len ); + if ( len > 0 ) { + GSList* head = storage->gotDataTaskList; + storage->gotDataTaskList + = g_slist_remove_link( storage->gotDataTaskList, + storage->gotDataTaskList ); + task = head->data; + g_slist_free( head ); + XP_LOGF( "%s(): got task id %d", __func__, task->id ); + } + // XP_LOGF( "%s(): len now %d", __func__, g_slist_length(storage->gotDataTaskList) ); + pthread_mutex_unlock( &storage->gotDataMutex ); + return task; +} + +static void +reset_schedule_check_interval( RelayConStorage* storage ) +{ + XP_ASSERT( onMainThread(storage) ); + storage->nextMoveCheckMS = 500; +} + +static void +schedule_next_check( RelayConStorage* storage ) +{ + XP_ASSERT( onMainThread(storage) ); + XP_ASSERT( !storage->params->noHTTPAuto ); + if ( !storage->params->noHTTPAuto ) { + if ( storage->moveCheckerID != 0 ) { + g_source_remove( storage->moveCheckerID ); + storage->moveCheckerID = 0; + } + + storage->nextMoveCheckMS *= 2; + if ( storage->nextMoveCheckMS > MAX_MOVE_CHECK_MS ) { + storage->nextMoveCheckMS = MAX_MOVE_CHECK_MS; + } else if ( storage->nextMoveCheckMS == 0 ) { + storage->nextMoveCheckMS = 1000; + } + + storage->moveCheckerID = g_timeout_add( storage->nextMoveCheckMS, + checkForMoves, storage ); + XP_ASSERT( storage->moveCheckerID != 0 ); + } +} + +static ssize_t +sendIt( RelayConStorage* storage, const XP_U8* msgbuf, XP_U16 len, float timeoutSecs ) +{ + ssize_t nSent; + if ( storage->params->useHTTP ) { + nSent = post( storage, msgbuf, len, timeoutSecs ); + } else { + nSent = sendto( storage->socket, msgbuf, len, 0, /* flags */ + (struct sockaddr*)&storage->saddr, + sizeof(storage->saddr) ); + } #ifdef COMMS_CHECKSUM gchar* sum = g_compute_checksum_for_data( G_CHECKSUM_MD5, msgbuf, len ); XP_LOGF( "%s: sent %d bytes with sum %s", __func__, len, sum ); @@ -602,3 +1297,33 @@ vli2un( const uint8_t** inp, uint32_t* outp ) } return success; } + +#ifdef DEBUG +static const char* +msgToStr( XWRelayReg msg ) +{ + const char* str; +# define CASE_STR(c) case c: str = #c; break + switch( msg ) { + CASE_STR(XWPDEV_UNAVAIL); + CASE_STR(XWPDEV_REG); + CASE_STR(XWPDEV_REGRSP); + CASE_STR(XWPDEV_INVITE); + CASE_STR(XWPDEV_KEEPALIVE); + CASE_STR(XWPDEV_HAVEMSGS); + CASE_STR(XWPDEV_RQSTMSGS); + CASE_STR(XWPDEV_MSG); + CASE_STR(XWPDEV_MSGNOCONN); + CASE_STR(XWPDEV_MSGRSP); + CASE_STR(XWPDEV_BADREG); + CASE_STR(XWPDEV_ALERT); // should not receive this.... + CASE_STR(XWPDEV_ACK); + CASE_STR(XWPDEV_DELGAME); + default: + str = ""; + break; + } +# undef CASE_STR + return str; +} +#endif diff --git a/xwords4/linux/relaycon.h b/xwords4/linux/relaycon.h index a8b0ef424..aafab5133 100644 --- a/xwords4/linux/relaycon.h +++ b/xwords4/linux/relaycon.h @@ -27,6 +27,8 @@ typedef struct _Procs { void (*msgReceived)( void* closure, const CommsAddrRec* from, const XP_U8* buf, XP_U16 len ); + void (*msgForRow)( void* closure, const CommsAddrRec* from, + sqlite3_int64 rowid, const XP_U8* buf, XP_U16 len ); void (*msgNoticeReceived)( void* closure ); void (*devIDReceived)( void* closure, const XP_UCHAR* devID, XP_U16 maxInterval ); @@ -56,4 +58,14 @@ void relaycon_cleanup( LaunchParams* params ); XP_U32 makeClientToken( sqlite3_int64 rowid, XP_U16 seed ); void rowidFromToken( XP_U32 clientToken, sqlite3_int64* rowid, XP_U16* seed ); + +void relaycon_checkMsgs( LaunchParams* params ); + +# ifdef RELAY_VIA_HTTP +typedef void (*OnJoinedProc)( void* closure, const XP_UCHAR* connname, XWHostID hid ); +void relaycon_join( LaunchParams* params, const XP_UCHAR* devID, const XP_UCHAR* room, + XP_U16 nPlayersHere, XP_U16 nPlayersTotal, XP_U16 seed, + XP_U16 lang, OnJoinedProc proc, void* closure ); +# endif + #endif diff --git a/xwords4/linux/scripts/discon_ok2.py b/xwords4/linux/scripts/discon_ok2.py index 87c1d5c52..127eba615 100755 --- a/xwords4/linux/scripts/discon_ok2.py +++ b/xwords4/linux/scripts/discon_ok2.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 import re, os, sys, getopt, shutil, threading, requests, json, glob -import argparse, datetime, random, subprocess, time +import argparse, datetime, random, signal, subprocess, time # LOGDIR=./$(basename $0)_logs # APP_NEW="" @@ -161,7 +161,8 @@ class Device(): sTilesLeftPat = re.compile('.*pool_removeTiles: (\d+) tiles left in pool') sRelayIDPat = re.compile('.*UPDATE games.*seed=(\d+),.*relayid=\'([^\']+)\'.*') - def __init__(self, args, indx, app, params, room, db, log, nInGame): + def __init__(self, args, game, indx, app, params, room, db, log, nInGame): + self.game = game self.indx = indx self.args = args self.pid = 0 @@ -178,7 +179,7 @@ class Device(): self.devID = '' self.launchCount = 0 self.allDone = False # when true, can be killed - self.nTilesLeft = -1 # negative means don't know + self.nTilesLeft = None self.relayID = None self.relaySeed = 0 @@ -257,6 +258,12 @@ class Device(): self.proc = None self.check_game() + def handleAllDone(self): + if self.allDone: + self.moveFiles() + self.send_dead() + return self.allDone + def moveFiles(self): assert not self.running() shutil.move(self.logPath, self.args.LOGDIR + '/done') @@ -268,10 +275,9 @@ class Device(): req = requests.get(url, params = {'params' : JSON}) def getTilesCount(self): - result = None - if self.nTilesLeft != -1: - result = '%.2d:%.2d' % (self.indx, self.nTilesLeft) - return result + return {'index': self.indx, 'nTilesLeft': self.nTilesLeft, + 'launchCount': self.launchCount, 'game': self.game, + } def update_ldevid(self): if not self.app in Device.sHasLDevIDMap: @@ -306,6 +312,7 @@ class Device(): if allDone: for dev in Device.sConnnameMap[self.connname]: + assert self.game == dev.game dev.allDone = True # print('Closing', self.connname, datetime.datetime.now()) @@ -343,7 +350,6 @@ def build_cmds(args): for GAME in range(1, args.NGAMES + 1): ROOM = 'ROOM_%.3d' % (GAME % args.NROOMS) - # check_room $ROOM NDEVS = pick_ndevs(args) LOCALS = figure_locals(args, NDEVS) # as array NPLAYERS = sum(LOCALS) @@ -355,12 +361,9 @@ def build_cmds(args): DEV = 0 for NLOCALS in LOCALS: DEV += 1 - FILE="%s/GAME_%d_%d.sql3" % (args.LOGDIR, GAME, DEV) - LOG='%s/%d_%d_LOG.txt' % (args.LOGDIR, GAME, DEV) - # os.system("rm -f $LOG") # clear the log + DB = '{}/{:02d}_{:02d}_DB.sql3'.format(args.LOGDIR, GAME, DEV) + LOG = '{}/{:02d}_{:02d}_LOG.txt'.format(args.LOGDIR, GAME, DEV) - # APPS[$COUNTER]="$APP_NEW" - # NEW_ARGS[$COUNTER]="$APP_NEW_PARAMS" BOARD_SIZE = ['--board-size', '15'] # if [ 0 -lt ${#APPS_OLD[@]} ]; then # # 50% chance of starting out with old app @@ -379,7 +382,7 @@ def build_cmds(args): PARAMS += ['--undo-pct', args.UNDO_PCT] PARAMS += [ '--game-dict', DICT, '--relay-port', args.PORT, '--host', args.HOST] PARAMS += ['--slow-robot', '1:3', '--skip-confirm'] - PARAMS += ['--db', FILE] + PARAMS += ['--db', DB] if random.randint(0,100) % 100 < g_UDP_PCT_START: PARAMS += ['--use-udp'] @@ -409,7 +412,7 @@ def build_cmds(args): # print('PARAMS:', PARAMS) - dev = Device(args, COUNTER, args.APP_NEW, PARAMS, ROOM, FILE, LOG, len(LOCALS)) + dev = Device(args, GAME, COUNTER, args.APP_NEW, PARAMS, ROOM, DB, LOG, len(LOCALS)) dev.update_ldevid() devs.append(dev) @@ -627,22 +630,75 @@ def build_cmds(args): # fi # } -def summarizeTileCounts(devs): - nDevs = len(devs) - strs = [dev.getTilesCount() for dev in devs] - strs = [s for s in strs if s] - nWithTiles = len(strs) - print('%s %d/%d %s' % (datetime.datetime.now().strftime("%H:%M:%S"), nDevs, nWithTiles, ' '.join(strs))) +def summarizeTileCounts(devs, endTime, state): + shouldGoOn = True + data = [dev.getTilesCount() for dev in devs] + nDevs = len(data) + totalTiles = 0 + colWidth = max(2, len(str(nDevs))) + headWidth = 0 + fmtData = [{'head' : 'dev', }, + {'head' : 'launches', }, + {'head' : 'tls left', }, + ] + for datum in fmtData: + headWidth = max(headWidth, len(datum['head'])) + datum['data'] = [] + + # Group devices by game + games = [] + prev = -1 + for datum in data: + gameNo = datum['game'] + if gameNo != prev: + games.append([]) + prev = gameNo + games[-1].append('{:0{width}d}'.format(datum['index'], width=colWidth)) + fmtData[0]['data'] = ['+'.join(game) for game in games] + + nLaunches = 0 + for datum in data: + launchCount = datum['launchCount'] + nLaunches += launchCount + fmtData[1]['data'].append('{:{width}d}'.format(launchCount, width=colWidth)) + + nTiles = datum['nTilesLeft'] + fmtData[2]['data'].append(nTiles is None and ('-' * colWidth) or '{:{width}d}'.format(nTiles, width=colWidth)) + if not nTiles is None: totalTiles += int(nTiles) + + + print('') + print('devs left: {}; tiles left: {}; total launches: {}; {}/{}' + .format(nDevs, totalTiles, nLaunches, datetime.datetime.now(), endTime )) + fmt = '{head:>%d} {data}' % headWidth + for datum in fmtData: datum['data'] = ' '.join(datum['data']) + for datum in fmtData: + print(fmt.format(**datum)) + + # Now let's see if things are stuck: if the tile string hasn't + # changed in two minutes bail. Note that the count of tiles left + # isn't enough because it's zero for a long time as devices are + # using up what's left in their trays and getting killed. + now = datetime.datetime.now() + tilesStr = fmtData[2]['data'] + if not 'tilesStr' in state or state['tilesStr'] != tilesStr: + state['lastChange'] = now + state['tilesStr'] = tilesStr + + return now - state['lastChange'] < datetime.timedelta(minutes = 1) def countCores(): return len(glob.glob1('/tmp',"core*")) +gDone = False + def run_cmds(args, devs): nCores = countCores() - endTime = datetime.datetime.now() + datetime.timedelta(seconds = args.TIMEOUT) + endTime = datetime.datetime.now() + datetime.timedelta(minutes = args.TIMEOUT_MINS) LOOPCOUNT = 0 + printState = {} - while len(devs) > 0: + while len(devs) > 0 and not gDone: if countCores() > nCores: print('core file count increased; exiting') break @@ -651,13 +707,14 @@ def run_cmds(args, devs): break LOOPCOUNT += 1 - if 0 == LOOPCOUNT % 20: summarizeTileCounts(devs) + if 0 == LOOPCOUNT % 20: + if not summarizeTileCounts(devs, endTime, printState): + print('no change in too long; exiting') + break dev = random.choice(devs) if not dev.running(): - if dev.allDone: - dev.moveFiles() - dev.send_dead() + if dev.handleAllDone(): devs.remove(dev) else: # if [ -n "$ONE_PER_ROOM" -a 0 -ne ${ROOM_PIDS[$ROOM]} ]; then @@ -674,9 +731,11 @@ def run_cmds(args, devs): # MINEND[$KEY]=$(($NOW + $MINRUN)) elif not dev.minTimeExpired(): # print('sleeping...') - time.sleep(2) + time.sleep(1.0) else: dev.kill() + if dev.handleAllDone(): + devs.remove(dev) # if g_DROP_N >= 0: dev.increment_drop() # update_ldevid $KEY @@ -739,8 +798,8 @@ def mkParser(): parser.add_argument('--num-games', dest = 'NGAMES', type = int, default = 1, help = 'number of games') parser.add_argument('--num-rooms', dest = 'NROOMS', type = int, default = 0, help = 'number of roooms (default to --num-games)') - parser.add_argument('--no-timeout', dest = 'TIMEOUT', default = False, action = 'store_true', - help = 'run forever (default proportional to number of games') + parser.add_argument('--timeout-mins', dest = 'TIMEOUT_MINS', default = 10000, type = int, + help = 'minutes after which to timeout') parser.add_argument('--log-root', dest='LOGROOT', default = '.', help = 'where logfiles go') parser.add_argument('--dup-packets', dest = 'DUP_PACKETS', default = False, help = 'send all packet twice') parser.add_argument('--use-gtk', dest = 'USE_GTK', default = False, action = 'store_true', @@ -768,7 +827,6 @@ def mkParser(): help = 'Port relay\'s on') parser.add_argument('--resign-pct', dest = 'RESIGN_PCT', default = 0, type = int, \ help = 'Odds of resigning [0..100]') - # # echo " [--no-timeout] # run until all games done \\" >&2 parser.add_argument('--seed', type = int, dest = 'SEED', default = random.randint(1, 1000000000)) # # echo " [--send-chat \\" >&2 @@ -900,7 +958,6 @@ def parseArgs(): def assignDefaults(args): if not args.NROOMS: args.NROOMS = args.NGAMES - args.TIMEOUT = not args.TIMEOUT and (args.NGAMES * 60 + 500) or 100000000000 if len(args.DICTS) == 0: args.DICTS.append('CollegeEng_2to8.xwd') args.LOGDIR = os.path.basename(sys.argv[0]) + '_logs' # Move an existing logdir aside @@ -975,10 +1032,20 @@ def assignDefaults(args): # SECONDS=$((SECONDS%60)) # echo "*********$0 finished: $(date) (took $HOURS:$MINUTES:$SECONDS)**************" +def termHandler(signum, frame): + global gDone + print('termHandler() called') + gDone = True + def main(): + startTime = datetime.datetime.now() + signal.signal(signal.SIGINT, termHandler) + args = parseArgs() devs = build_cmds(args) + nDevs = len(devs) run_cmds(args, devs) + print('{} finished; took {} for {} devices'.format(sys.argv[0], datetime.datetime.now() - startTime, nDevs)) ############################################################################## if __name__ == '__main__': diff --git a/xwords4/linux/scripts/discon_ok2.sh b/xwords4/linux/scripts/discon_ok2.sh index 19d914e7c..39c2c3b0b 100755 --- a/xwords4/linux/scripts/discon_ok2.sh +++ b/xwords4/linux/scripts/discon_ok2.sh @@ -1,7 +1,7 @@ #!/bin/bash set -u -e -LOGDIR=$(basename $0)_logs +LOGDIR=./$(basename $0)_logs APP_NEW="" DO_CLEAN="" APP_NEW_PARAMS="" @@ -17,9 +17,9 @@ SAVE_GOOD="" MINDEVS="" MAXDEVS="" ONEPER="" -RESIGN_RATIO="" +RESIGN_PCT=0 DROP_N="" -MINRUN=2 +MINRUN=2 # seconds ONE_PER_ROOM="" # don't run more than one device at a time per room USE_GTK="" UNDO_PCT=0 @@ -31,6 +31,7 @@ NAMES=(UNUSED Brynn Ariela Kati Eric) SEND_CHAT='' CORE_COUNT=$(ls core.* 2>/dev/null | wc -l) DUP_PACKETS='' +HTTP_PCT=0 declare -A PIDS declare -A APPS @@ -43,7 +44,7 @@ declare -A LOGS declare -A MINEND declare -A ROOM_PIDS declare -a APPS_OLD=() -declare -a DICTS= # wants to be =() too? +declare -a DICTS=() # wants to be =() too? declare -A CHECKED_ROOMS function cleanup() { @@ -194,9 +195,6 @@ build_cmds() { for NLOCALS in ${LOCALS[@]}; do DEV=$((DEV + 1)) FILE="${LOGDIR}/GAME_${GAME}_${DEV}.sql3" - if [ $((RANDOM % 100)) -lt $UDP_PCT_START ]; then - FILE="$FILE --use-udp" - fi LOG=${LOGDIR}/${GAME}_${DEV}_LOG.txt > $LOG # clear the log @@ -219,7 +217,13 @@ build_cmds() { PARAMS="$PARAMS --game-dict $DICT --relay-port $PORT --host $HOST " PARAMS="$PARAMS --slow-robot 1:3 --skip-confirm" PARAMS="$PARAMS --db $FILE" + if [ $((RANDOM % 100)) -lt $UDP_PCT_START ]; then + PARAMS="$PARAMS --use-udp" + fi PARAMS="$PARAMS --drop-nth-packet $DROP_N $PLAT_PARMS" + if [ $((${RANDOM}%100)) -lt $HTTP_PCT ]; then + PARAMS="$PARAMS --use-http" + fi # PARAMS="$PARAMS --split-packets 2" if [ -n "$SEND_CHAT" ]; then PARAMS="$PARAMS --send-chat $SEND_CHAT" @@ -304,6 +308,21 @@ launch() { # exec $CMD >/dev/null 2>>$LOG # } +send_dead() { + ID=$1 + DB=${FILES[$ID]} + while :; do + [ -f $DB ] || break # it's gone + RES=$(echo 'select relayid, seed from games limit 1;' | sqlite3 -separator ' ' $DB || /bin/true) + [ -n "$RES" ] && break + sleep 0.2 + done + RELAYID=$(echo $RES | awk '{print $1}') + SEED=$(echo $RES | awk '{print $2}') + JSON="[{\"relayID\":\"$RELAYID\", \"seed\":$SEED}]" + curl -G --data-urlencode params="$JSON" http://$HOST/xw4/relay.py/kill >/dev/null 2>&1 +} + close_device() { ID=$1 MVTO=$2 @@ -353,11 +372,11 @@ kill_from_log() { } maybe_resign() { - if [ "$RESIGN_RATIO" -gt 0 ]; then + if [ "$RESIGN_PCT" -gt 0 ]; then KEY=$1 LOG=${LOGS[$KEY]} if grep -aq XWRELAY_ALLHERE $LOG; then - if [ 0 -eq $(($RANDOM % $RESIGN_RATIO)) ]; then + if [ $((${RANDOM}%100)) -lt $RESIGN_PCT ]; then echo "making $LOG $(connName $LOG) resign..." kill_from_log $LOG && close_device $KEY $DEADDIR "resignation forced" || /bin/true fi @@ -419,6 +438,7 @@ check_game() { for ID in $OTHERS $KEY; do echo -n "${ID}:${LOGS[$ID]}, " kill_from_log ${LOGS[$ID]} || /bin/true + send_dead $ID close_device $ID $DONEDIR "game over" done echo "" @@ -458,10 +478,8 @@ update_ldevid() { if [ $RNUM -lt 30 ]; then # upgrade or first run CMD="--ldevid LINUX_TEST_$(printf %.5d ${KEY})_" fi - else - if [ $RNUM -lt 10 ]; then - CMD="${CMD}x" # give it a new local ID - fi + elif [ $RNUM -lt 10 ]; then + CMD="${CMD}x" # give it a new local ID fi ARGS_DEVID[$KEY]="$CMD" fi @@ -508,7 +526,8 @@ run_cmds() { local KEYS=( ${!ARGS[*]} ) KEY=${KEYS[$INDX]} ROOM=${ROOMS[$KEY]} - if [ 0 -eq ${PIDS[$KEY]} ]; then + PID=${PIDS[$KEY]} + if [ 0 -eq ${PID} ]; then if [ -n "$ONE_PER_ROOM" -a 0 -ne ${ROOM_PIDS[$ROOM]} ]; then continue fi @@ -522,10 +541,12 @@ run_cmds() { ROOM_PIDS[$ROOM]=$PID MINEND[$KEY]=$(($NOW + $MINRUN)) else - PID=${PIDS[$KEY]} if [ -d /proc/$PID ]; then SLEEP=$((${MINEND[$KEY]} - $NOW)) - [ $SLEEP -gt 0 ] && sleep $SLEEP + if [ $SLEEP -gt 0 ]; then + sleep 1 + continue + fi kill $PID || /bin/true wait $PID fi @@ -594,6 +615,7 @@ function getArg() { function usage() { [ $# -gt 0 ] && echo "Error: $1" >&2 echo "Usage: $(basename $0) \\" >&2 + echo " [--log-root] # default: . \\" >&2 echo " [--dup-packets] # send all packets twice \\" >&2 echo " [--clean-start] \\" >&2 echo " [--game-dict ]* \\" >&2 @@ -601,6 +623,7 @@ function usage() { echo " [--host ] \\" >&2 echo " [--max-devs ] \\" >&2 echo " [--min-devs ] \\" >&2 + echo " [--min-run ] # run each at least this long \\" >&2 echo " [--new-app &2 echo " [--new-app-args [arg*]] # passed only to new app \\" >&2 echo " [--num-games ] \\" >&2 @@ -608,12 +631,14 @@ function usage() { echo " [--old-app &2 echo " [--one-per] # force one player per device \\" >&2 echo " [--port ] \\" >&2 - echo " [--resign-ratio <0 <= n <=1000 > \\" >&2 + echo " [--resign-pct <0 <= n <=100 > \\" >&2 + echo " [--no-timeout] # run until all games done \\" >&2 echo " [--seed ] \\" >&2 echo " [--send-chat \\" >&2 echo " [--udp-incr ] \\" >&2 echo " [--udp-start ] # default: $UDP_PCT_START \\" >&2 echo " [--undo-pct ] \\" >&2 + echo " [--http-pct <0 <= n <=100>] \\" >&2 exit 1 } @@ -647,6 +672,11 @@ while [ "$#" -gt 0 ]; do APPS_OLD[${#APPS_OLD[@]}]=$(getArg $*) shift ;; + --log-root) + [ -d $2 ] || usage "$1: no such directory $2" + LOGDIR=$2/$(basename $0)_logs + shift + ;; --dup-packets) DUP_PACKETS=1 ;; @@ -671,6 +701,11 @@ while [ "$#" -gt 0 ]; do MAXDEVS=$(getArg $*) shift ;; + --min-run) + MINRUN=$(getArg $*) + [ $MINRUN -ge 2 -a $MINRUN -le 60 ] || usage "$1: n must be 2 <= n <= 60" + shift + ;; --one-per) ONEPER=TRUE ;; @@ -690,14 +725,23 @@ while [ "$#" -gt 0 ]; do UNDO_PCT=$(getArg $*) shift ;; + --http-pct) + HTTP_PCT=$(getArg $*) + [ $HTTP_PCT -ge 0 -a $HTTP_PCT -le 100 ] || usage "$1: n must be 0 <= n <= 100" + shift + ;; --send-chat) SEND_CHAT=$(getArg $*) shift ;; - --resign-ratio) - RESIGN_RATIO=$(getArg $*) + --resign-pct) + RESIGN_PCT=$(getArg $*) + [ $RESIGN_PCT -ge 0 -a $RESIGN_PCT -le 100 ] || usage "$1: n must be 0 <= n <= 100" shift ;; + --no-timeout) + TIMEOUT=0x7FFFFFFF + ;; --help) usage ;; @@ -709,7 +753,7 @@ done # Assign defaults #[ 0 -eq ${#DICTS[@]} ] && DICTS=(dict.xwd) -[ 0 -eq ${#DICTS} ] && DICTS=(dict.xwd) +[ 0 -eq ${#DICTS} ] && DICTS=(CollegeEng_2to8.xwd) [ -z "$APP_NEW" ] && APP_NEW=./obj_linux_memdbg/xwords [ -z "$MINDEVS" ] && MINDEVS=2 [ -z "$MAXDEVS" ] && MAXDEVS=4 @@ -719,7 +763,7 @@ done [ -z "$PORT" ] && PORT=10997 [ -z "$TIMEOUT" ] && TIMEOUT=$((NGAMES*60+500)) [ -z "$SAVE_GOOD" ] && SAVE_GOOD=YES -[ -z "$RESIGN_RATIO" -a "$NGAMES" -gt 1 ] && RESIGN_RATIO=1000 || RESIGN_RATIO=0 +# [ -z "$RESIGN_PCT" -a "$NGAMES" -gt 1 ] && RESIGN_RATIO=1000 || RESIGN_RATIO=0 [ -z "$DROP_N" ] && DROP_N=0 [ -z "$USE_GTK" ] && USE_GTK=FALSE [ -z "$UPGRADE_ODDS" ] && UPGRADE_ODDS=10 @@ -747,7 +791,8 @@ for FILE in $(ls $LOGDIR/*.{xwg,txt} 2>/dev/null); do done if [ -z "$RESUME" -a -d $LOGDIR ]; then - mv $LOGDIR /tmp/${LOGDIR}_$$ + NEWNAME="$(basename $LOGDIR)_$$" + (cd $(dirname $LOGDIR) && mv $(basename $LOGDIR) /tmp/${NEWNAME}) fi mkdir -p $LOGDIR @@ -759,7 +804,7 @@ DEADDIR=$LOGDIR/dead mkdir -p $DEADDIR for VAR in NGAMES NROOMS USE_GTK TIMEOUT HOST PORT SAVE_GOOD \ - MINDEVS MAXDEVS ONEPER RESIGN_RATIO DROP_N ALL_VIA_RQ SEED \ + MINDEVS MAXDEVS ONEPER RESIGN_PCT DROP_N ALL_VIA_RQ SEED \ APP_NEW; do echo "$VAR:" $(eval "echo \$${VAR}") 1>&2 done diff --git a/xwords4/linux/scripts/list-message-flow.py b/xwords4/linux/scripts/list-message-flow.py new file mode 100755 index 000000000..92dbd08a8 --- /dev/null +++ b/xwords4/linux/scripts/list-message-flow.py @@ -0,0 +1,100 @@ +#!/usr/bin/python3 + +import getopt, re, sys +import json, psycopg2 + +""" + +I want to understand why some messages linger on the database so +long. So given one or more logfiles that track a linux client's +interaction, look at what it sends and receives and compare that with +what's in the relay's msgs table. + +""" + +DEVID_PAT = re.compile('.*linux_getDevIDRelay => (\d+)$') +QUERY_GOT_PAT = re.compile('.*>(\d+:\d+:\d+):runWitCurl\(\): got for query: \"({.*})\"$') +# <26828:7f03b7fff700>07:47:20:runWitCurl(): got for post: "{"data": ["AR03ggcAH2gwBwESbnVja3k6NTlmYTFjZmM6MTEw", "AR43ggcAH2gwDQBvAgEAAAAAvdAAAAAAAAAAAJIGUGxheWVyGg==", "AYALgw=="], "err": "timeout"}" +POST_GOT_PAT = re.compile('.*>(\d+:\d+:\d+):runWitCurl\(\): got for post: \"({.*})\"$') +def usage(msg = None): + if msg: sys.stderr.write('ERROR:' + msg + '\n') + sys.stderr.write('usage: ' + sys.argv[0] + ': (-l logfile)+ \n') + sys.exit(1) + +def parseLog(log, data): + devIDs = [] + msgMap = {} + for line in open(log): + line = line.strip() + aMatch = DEVID_PAT.match(line) + if aMatch: + devID = int(aMatch.group(1)) + if devID and (len(devIDs) == 0 or devIDs[-1] != devID): + devIDs.append(devID) + + aMatch = QUERY_GOT_PAT.match(line) + if aMatch: + rtime = aMatch.group(1) + jobj = json.loads(aMatch.group(2)) + for relayID in jobj: + msgs = jobj[relayID] + for msgarr in msgs: + for msg in msgarr: + if not msg in msgMap: msgMap[msg] = [] + msgMap[msg].append({'rtime' : rtime,}) + if len(msgMap[msg]) > 1: print('big case') + + aMatch = POST_GOT_PAT.match(line) + if aMatch: + jobj = json.loads(aMatch.group(2)) + for datum in jobj['data']: + data.add(datum) + + return devIDs, msgMap + +def fetchMsgs(devIDs, msgMaps, data): + foundCount = 0 + notFoundCount = 0 + + con = psycopg2.connect(database='xwgames') + cur = con.cursor() + query = "SELECT ctime, stime, stime-ctime as age, msg64 FROM msgs WHERE devid in (%s) order by ctime" \ + % (','.join([str(id) for id in devIDs])) + # print(query) + cur.execute(query) + for row in cur: + msg64 = row[3] + for msgMap in msgMaps: + if msg64 in msgMap: + print('added:', row[0], 'sent:', row[1], 'received:', msgMap[msg64][0]['rtime'], 'age:', row[2]) + if msg64 in data: + foundCount += 1 + else: + notFoundCount += 1 + print('found:', foundCount, 'not found:', notFoundCount); + + +def main(): + logs = [] + opts, args = getopt.getopt(sys.argv[1:], "l:") + for option, value in opts: + if option == '-l': logs.append(value) + else: usage("unknown option" + option) + + if len(logs) == 0: usage('at least one -l requried') + + msgMaps = [] + devIDs = set() + data = set() + for log in logs: + ids, msgMap = parseLog(log, data) + msgMaps.append(msgMap) + for id in ids: devIDs.add(id) + + print(msgMaps) + print(devIDs) + fetchMsgs(devIDs, msgMaps, data) + +############################################################################## +if __name__ == '__main__': + main() diff --git a/xwords4/linux/scripts/start-pair.sh b/xwords4/linux/scripts/start-pair.sh new file mode 100755 index 000000000..c579123a1 --- /dev/null +++ b/xwords4/linux/scripts/start-pair.sh @@ -0,0 +1,82 @@ +#!/bin/sh + +set -e -u + +IN_SEQ='' +HTTP='--use-http' +CURSES='--curses' +SLEEP_SEC=10000 + +usage() { + [ $# -gt 0 ] && echo "ERROR: $1" + echo "usage: $0 --in-sequence|--at-once [--no-use-http] [--gtk]" + cat </dev/null 2>>$LOG & + PID=$! + echo "launched $PID" + if [ $IN_SEQ -eq 1 ]; then + sleep 9 + kill $PID + sleep 1 + elif [ $IN_SEQ -eq 0 ]; then + PIDS="$PIDS $PID" + fi + done +done + +[ -n "${PIDS}" ] && sleep $SLEEP_SEC +for PID in $PIDS; do + kill $PID +done diff --git a/xwords4/newrelay/nr.py b/xwords4/newrelay/nr.py new file mode 100755 index 000000000..4700dd534 --- /dev/null +++ b/xwords4/newrelay/nr.py @@ -0,0 +1,61 @@ +#!/usr/bin/env python3 + +import json, shelve + +"""This will be a prototype of a simple store-and-forward message +passing server. Target clients are peer-to-peer apps like turn-based +games and maybe chat apps. It's expected that they depend on the +server for nothing but message passing and any group-formation that it +depends on (e.g three devices agreeing to participate in a single game +of Fish, a process that gets them a token that can be used to address +messages that are part of that game.) Clients can use this server as +one of several means of communicating, depending on it to deliver +messages e.g. when the devices are out of range of bluetooth. + +""" + +# register: a device is meant to call this once to get from the server +# an identifier that will identify it from then on. Other APIs will +# require this identifier. +# +# @param clientID: a String the client can optionally provide to link +# this registration to an earlier one. For example, if a client app +# wants state to survive a hard reset of the device but there are IDs +# like serial numbers or a user's email address that will survive that +# process, such an id could be used. +def register(req, clientID = None): + shelf = openShelf() + obj = {'deviceID' : shelf['nextID']} + shelf['nextID'] += 1 + shelf.close() + return json.dumps(obj) + +# Associate attributes with a device that can be used for indirectly +# related purposes. The one I have in mind is GCM (Google Cloud +# Messaging), where the device provides a server an ID that the server +# can use to ask google's servers to forward a push message to the +# device. +def setAttr(req, deviceID, attrKey, attrValue): + pass + +# joinRoom: called when a device wants to start a new game to which +# other devices will also connect. Returns a gameID that internally +# refers to the game joined and the device's position in it. +# @param +def joinRoom(deviceID, room, lang, nTotal, nHere = 1, position = 0): + pass + +def forward(req, deviceID, msg, roomID, positions): + pass + +def openShelf(): + shelf = shelve.open("/tmp/nr.shelf") + if not 'nextID' in shelf: shelf['nextID'] = 0; + return shelf + +def main(): + pass + +############################################################################## +if __name__ == '__main__': + main() diff --git a/xwords4/relay/Makefile b/xwords4/relay/Makefile index 06fc29fa8..82cfaeae5 100644 --- a/xwords4/relay/Makefile +++ b/xwords4/relay/Makefile @@ -67,6 +67,7 @@ endif # turn on semaphore debugging # CPPFLAGS += -DDEBUG_LOCKS +# CPPFLAGS += -DLOG_POLL memdebug all: xwrelay rq diff --git a/xwords4/relay/addrinfo.cpp b/xwords4/relay/addrinfo.cpp index 32839d1fd..546b828e6 100644 --- a/xwords4/relay/addrinfo.cpp +++ b/xwords4/relay/addrinfo.cpp @@ -20,13 +20,16 @@ */ #include +#include #include #include +#include #include "addrinfo.h" #include "xwrelay_priv.h" #include "tpool.h" #include "udpager.h" +#include "mlock.h" // static uint32_t s_prevCreated = 0L; @@ -68,7 +71,7 @@ AddrInfo::equals( const AddrInfo& other ) const if ( isTCP() ) { equal = m_socket == other.m_socket; if ( equal && created() != other.created() ) { - logf( XW_LOGINFO, "%s: rejecting on time mismatch (%lx vs %lx)", + logf( XW_LOGINFO, "%s(): rejecting on time mismatch (%lx vs %lx)", __func__, created(), other.created() ); equal = false; } @@ -82,3 +85,40 @@ AddrInfo::equals( const AddrInfo& other ) const return equal; } +static pthread_mutex_t s_refMutex = PTHREAD_MUTEX_INITIALIZER; +static map s_socketRefs; + +void AddrInfo::ref() const +{ + // logf( XW_LOGVERBOSE0, "%s(socket=%d)", __func__, m_socket ); + MutexLock ml( &s_refMutex ); + ++s_socketRefs[m_socket]; + printRefMap(); +} + +void +AddrInfo::unref() const +{ + // logf( XW_LOGVERBOSE0, "%s(socket=%d)", __func__, m_socket ); + + MutexLock ml( &s_refMutex ); + assert( s_socketRefs[m_socket] > 0 ); + --s_socketRefs[m_socket]; + if ( s_socketRefs[m_socket] == 0 ) { + XWThreadPool::GetTPool()->CloseSocket( this ); + } + printRefMap(); +} + +/* private, and assumes have mutex */ +void +AddrInfo::printRefMap() const +{ + /* for ( map::const_iterator iter = s_socketRefs.begin(); */ + /* iter != s_socketRefs.end(); ++iter ) { */ + /* int count = iter->second; */ + /* if ( count > 0 ) { */ + /* logf( XW_LOGVERBOSE0, "socket: %d; count: %d", iter->first, count ); */ + /* } */ + /* } */ +} diff --git a/xwords4/relay/addrinfo.h b/xwords4/relay/addrinfo.h index d92e70a1b..94b04a816 100644 --- a/xwords4/relay/addrinfo.h +++ b/xwords4/relay/addrinfo.h @@ -81,12 +81,18 @@ class AddrInfo { bool equals( const AddrInfo& other ) const; + /* refcount the underlying socket (doesn't modify instance) */ + void ref() const; + void unref() const; + int getref() const; + private: void construct( int sock, const AddrUnion* saddr, bool isTCP ); void init( int sock, ClientToken clientToken, const AddrUnion* saddr ) { construct( sock, saddr, false ); m_clientToken = clientToken; } + void printRefMap() const; // AddrInfo& operator=(const AddrInfo&); // Prevent assignment int m_socket; diff --git a/xwords4/relay/configs.cpp b/xwords4/relay/configs.cpp index 90b222149..fc50fd84e 100644 --- a/xwords4/relay/configs.cpp +++ b/xwords4/relay/configs.cpp @@ -84,12 +84,13 @@ RelayConfigs::GetValueFor( const char* key, time_t* value ) bool RelayConfigs::GetValueFor( const char* key, char* buf, int len ) { - MutexLock ml( &m_values_mutex ); + pthread_mutex_lock( &m_values_mutex ); map::const_iterator iter = m_values.find(key); bool found = iter != m_values.end(); if ( found ) { snprintf( buf, len, "%s", iter->second ); } + pthread_mutex_unlock( &m_values_mutex ); return found; } @@ -125,7 +126,7 @@ RelayConfigs::GetValueFor( const char* key, vector& ints ) void RelayConfigs::SetValueFor( const char* key, const char* value ) { - MutexLock ml( &m_values_mutex ); + pthread_mutex_lock( &m_values_mutex ); /* Remove any entry already there */ map::iterator iter = m_values.find(key); @@ -136,6 +137,7 @@ RelayConfigs::SetValueFor( const char* key, const char* value ) pair::iterator,bool> result = m_values.insert( pair(strdup(key),strdup(value) ) ); assert( result.second ); + pthread_mutex_unlock( &m_values_mutex ); } ino_t diff --git a/xwords4/relay/scripts/relay.py b/xwords4/relay/scripts/relay.py index 7ca6c2fa6..5cf1c615c 100755 --- a/xwords4/relay/scripts/relay.py +++ b/xwords4/relay/scripts/relay.py @@ -171,13 +171,13 @@ def query(req, params): print('params', params) params = json.loads(params) ids = params['ids'] - timeoutSecs = 'timeoutSecs' in params and float(params['timeoutSecs']) or 2.0 + # timeoutSecs = 'timeoutSecs' in params and float(params['timeoutSecs']) or 2.0 idsLen = 0 for id in ids: idsLen += len(id) tcpSock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) - tcpSock.settimeout(timeoutSecs) + # tcpSock.settimeout(timeoutSecs) tcpSock.connect(('127.0.0.1', 10998)) lenShort = 2 + idsLen + len(ids) + 2 @@ -188,8 +188,9 @@ def query(req, params): for id in ids: tcpSock.send(id + '\n') - msgsLists = {} + result = {'ids':ids} try: + msgsLists = {} shortUnpacker = struct.Struct('!H') resLen, = shortUnpacker.unpack(tcpSock.recv(shortUnpacker.size)) # not getting all bytes nameCount, = shortUnpacker.unpack(tcpSock.recv(shortUnpacker.size)) @@ -212,10 +213,14 @@ def query(req, params): msgs.append(msg) perGame.append(msgs) msgsLists[ids[ii]] = perGame + + result['msgs'] = msgsLists except: + # Anything but a timeout should mean we abort/send nothing + result['err'] = 'hit exception' None - return json.dumps(msgsLists) + return json.dumps(result) def main(): result = None diff --git a/xwords4/relay/scripts/showinplay.sh b/xwords4/relay/scripts/showinplay.sh index 8a593ea02..51e7c6ca3 100755 --- a/xwords4/relay/scripts/showinplay.sh +++ b/xwords4/relay/scripts/showinplay.sh @@ -51,7 +51,8 @@ fi echo -n "Device (pid) count: $(pidof xwords | wc | awk '{print $2}')" echo "; relay pid[s]: $(pidof xwrelay)" -echo "Row count:" $(psql -t xwgames -c "select count(*) FROM games $QUERY;") +echo -n "Row count:" $(psql -t xwgames -c "select count(*) FROM games $QUERY;") +echo "; Relay sockets: $(for PID in $(pidof xwrelay); do ls /proc/$PID/fd; done | sort -un | tr '\n' ' ')" # Games echo "SELECT dead as d,connname,cid,room,lang as lg,clntVers as cv ,ntotal as t,nperdevice as npd,nsents as snts, seeds,devids,tokens,ack, mtimes "\ diff --git a/xwords4/relay/tpool.cpp b/xwords4/relay/tpool.cpp index 9e402c744..8abd16277 100644 --- a/xwords4/relay/tpool.cpp +++ b/xwords4/relay/tpool.cpp @@ -119,13 +119,17 @@ XWThreadPool::Stop() void XWThreadPool::AddSocket( SockType stype, QueueCallback proc, const AddrInfo* from ) { + from->ref(); + + int sock = from->getSocket(); + logf( XW_LOGVERBOSE0, "%s(sock=%d, isTCP=%d)", __func__, sock, from->isTCP() ); + SockInfo si = { .m_type = stype, + .m_proc = proc, + .m_addr = *from + }; { - int sock = from->getSocket(); RWWriteLock ml( &m_activeSocketsRWLock ); - SockInfo si; - si.m_type = stype; - si.m_proc = proc; - si.m_addr = *from; + assert( m_activeSockets.find( sock ) == m_activeSockets.end() ); m_activeSockets.insert( pair( sock, si ) ); } interrupt_poll(); @@ -158,13 +162,14 @@ XWThreadPool::RemoveSocket( const AddrInfo* addr ) size_t prevSize = m_activeSockets.size(); - map::iterator iter = m_activeSockets.find( addr->getSocket() ); + int sock = addr->getSocket(); + map::iterator iter = m_activeSockets.find( sock ); if ( m_activeSockets.end() != iter && iter->second.m_addr.equals( *addr ) ) { m_activeSockets.erase( iter ); found = true; } - logf( XW_LOGINFO, "%s: AFTER: %d sockets active (was %d)", __func__, - m_activeSockets.size(), prevSize ); + logf( XW_LOGINFO, "%s(): AFTER closing %d: %d sockets active (was %d)", __func__, + sock, m_activeSockets.size(), prevSize ); } return found; } /* RemoveSocket */ @@ -184,8 +189,14 @@ XWThreadPool::CloseSocket( const AddrInfo* addr ) ++iter; } } - logf( XW_LOGINFO, "CLOSING socket %d", addr->getSocket() ); - close( addr->getSocket() ); + int sock = addr->getSocket(); + int err = close( sock ); + if ( 0 != err ) { + logf( XW_LOGERROR, "%s(): close(socket=%d) => %d/%s", __func__, + sock, errno, strerror(errno) ); + } else { + logf( XW_LOGINFO, "%s(): close(socket=%d) succeeded", __func__, sock ); + } /* We always need to interrupt the poll because the socket we're closing will be in the list being listened to. That or we need to drop sockets @@ -198,7 +209,7 @@ XWThreadPool::CloseSocket( const AddrInfo* addr ) void XWThreadPool::EnqueueKill( const AddrInfo* addr, const char* const why ) { - logf( XW_LOGINFO, "%s(%d) reason: %s", __func__, addr->getSocket(), why ); + logf( XW_LOGINFO, "%s(socket = %d) reason: %s", __func__, addr->getSocket(), why ); if ( addr->isTCP() ) { SockInfo si; si.m_type = STYPE_UNKNOWN; @@ -265,7 +276,6 @@ XWThreadPool::real_tpool_main( ThreadInfo* tip ) if ( gotOne ) { sock = pr.m_info.m_addr.getSocket(); - logf( XW_LOGINFO, "worker thread got socket %d from queue", socket ); switch ( pr.m_act ) { case Q_READ: assert( 0 ); @@ -275,8 +285,9 @@ XWThreadPool::real_tpool_main( ThreadInfo* tip ) // } break; case Q_KILL: + logf( XW_LOGINFO, "worker thread got socket %d from queue (to close it)", sock ); (*m_kFunc)( &pr.m_info.m_addr ); - CloseSocket( &pr.m_info.m_addr ); + pr.m_info.m_addr.unref(); break; } } else { @@ -392,35 +403,40 @@ XWThreadPool::real_listener() curfd = 1; int ii; - for ( ii = 0; ii < nSockets && nEvents > 0; ++ii ) { + for ( ii = 0; ii < nSockets && nEvents > 0; ++ii, ++curfd ) { if ( fds[curfd].revents != 0 ) { // int socket = fds[curfd].fd; SockInfo* sinfo = &sinfos[curfd]; const AddrInfo* addr = &sinfo->m_addr; - assert( fds[curfd].fd == addr->getSocket() ); + int sock = addr->getSocket(); + assert( fds[curfd].fd == sock ); if ( !SocketFound( addr ) ) { + logf( XW_LOGINFO, "%s(): dropping socket %d: not found", + __func__, addr->getSocket() ); /* no further processing if it's been removed while - we've been sleeping in poll */ + we've been sleeping in poll. BUT: shouldn't curfd + be incremented?? */ --nEvents; continue; } if ( 0 != (fds[curfd].revents & (POLLIN | POLLPRI)) ) { if ( !UdpQueue::get()->handle( addr, sinfo->m_proc ) ) { + // This is likely wrong!!! return of 0 means + // remote closed, not error. RemoveSocket( addr ); - EnqueueKill( addr, "bad packet" ); + EnqueueKill( addr, "got EOF" ); } } else { - logf( XW_LOGERROR, "odd revents: %x", - fds[curfd].revents ); + logf( XW_LOGERROR, "%s(): odd revents: %x; bad socket %d", + __func__, fds[curfd].revents, sock ); RemoveSocket( addr ); - EnqueueKill( addr, "error/hup in poll()" ); + EnqueueKill( addr, "error/hup in poll()" ); } --nEvents; } - ++curfd; } assert( nEvents == 0 ); } diff --git a/xwords4/relay/udpqueue.cpp b/xwords4/relay/udpqueue.cpp index 675c28ec3..bb59a0892 100644 --- a/xwords4/relay/udpqueue.cpp +++ b/xwords4/relay/udpqueue.cpp @@ -28,7 +28,7 @@ static UdpQueue* s_instance = NULL; void -UdpThreadClosure::logStats() +PacketThreadClosure::logStats() { time_t now = time( NULL ); if ( 1 < now - m_created ) { @@ -48,6 +48,7 @@ PartialPacket::stillGood() const bool PartialPacket::readAtMost( int len ) { + assert( len > 0 ); bool success = false; uint8_t tmp[len]; ssize_t nRead = recv( m_sock, tmp, len, 0 ); @@ -57,10 +58,12 @@ PartialPacket::readAtMost( int len ) logf( XW_LOGERROR, "%s(len=%d, socket=%d): recv failed: %d (%s)", __func__, len, m_sock, m_errno, strerror(m_errno) ); } - } else if ( 0 == nRead ) { // remote socket closed - logf( XW_LOGINFO, "%s: remote closed (socket=%d)", __func__, m_sock ); + } else if ( 0 == nRead ) { // remote socket half-closed + logf( XW_LOGINFO, "%s(): remote closed (socket=%d)", __func__, m_sock ); m_errno = -1; // so stillGood will fail } else { + // logf( XW_LOGVERBOSE0, "%s(): read %d bytes on socket %d", __func__, + // nRead, m_sock ); m_errno = 0; success = len == nRead; int curSize = m_buf.size(); @@ -100,7 +103,11 @@ UdpQueue::get() return s_instance; } -// return false if socket should no longer be used +// If we're already assembling data from this socket, continue. Otherwise +// create a new parital packet and store data there. If we wind up with a +// complete packet, dispatch it and delete since the data's been delivered. +// +// Return false if socket should no longer be used. bool UdpQueue::handle( const AddrInfo* addr, QueueCallback cb ) { @@ -145,6 +152,7 @@ UdpQueue::handle( const AddrInfo* addr, QueueCallback cb ) } success = success && (NULL == packet || packet->stillGood()); + logf( XW_LOGVERBOSE0, "%s(sock=%d) => %d", __func__, sock, success ); return success; } @@ -152,17 +160,21 @@ void UdpQueue::handle( const AddrInfo* addr, const uint8_t* buf, int len, QueueCallback cb ) { - UdpThreadClosure* utc = new UdpThreadClosure( addr, buf, len, cb ); + // addr->ref(); + PacketThreadClosure* ptc = new PacketThreadClosure( addr, buf, len, cb ); MutexLock ml( &m_queueMutex ); int id = ++m_nextID; - utc->setID( id ); - logf( XW_LOGINFO, "%s: enqueuing packet %d (socket %d, len %d)", + ptc->setID( id ); + logf( XW_LOGINFO, "%s(): enqueuing packet %d (socket %d, len %d)", __func__, id, addr->getSocket(), len ); - m_queue.push_back( utc ); + m_queue.push_back( ptc ); pthread_cond_signal( &m_queueCondVar ); } +// Remove any PartialPacket record with the same socket/fd. This makes sense +// when the socket's being reused or when we have just dealt with a single +// packet and might be getting more. void UdpQueue::newSocket_locked( int sock ) { @@ -194,25 +206,26 @@ UdpQueue::thread_main() while ( m_queue.size() == 0 ) { pthread_cond_wait( &m_queueCondVar, &m_queueMutex ); } - UdpThreadClosure* utc = m_queue.front(); + PacketThreadClosure* ptc = m_queue.front(); m_queue.pop_front(); pthread_mutex_unlock( &m_queueMutex ); - utc->noteDequeued(); + ptc->noteDequeued(); - time_t age = utc->ageInSeconds(); + time_t age = ptc->ageInSeconds(); if ( 30 > age ) { logf( XW_LOGINFO, "%s: dispatching packet %d (socket %d); " - "%d seconds old", __func__, utc->getID(), - utc->addr()->getSocket(), age ); - (*utc->cb())( utc ); - utc->logStats(); + "%d seconds old", __func__, ptc->getID(), + ptc->addr()->getSocket(), age ); + (*ptc->cb())( ptc ); + ptc->logStats(); } else { logf( XW_LOGINFO, "%s: dropping packet %d; it's %d seconds old!", __func__, age ); } - delete utc; + // ptc->addr()->unref(); + delete ptc; } return NULL; } diff --git a/xwords4/relay/udpqueue.h b/xwords4/relay/udpqueue.h index cc467af75..befa99893 100644 --- a/xwords4/relay/udpqueue.h +++ b/xwords4/relay/udpqueue.h @@ -30,13 +30,13 @@ using namespace std; -class UdpThreadClosure; +class PacketThreadClosure; -typedef void (*QueueCallback)( UdpThreadClosure* closure ); +typedef void (*QueueCallback)( PacketThreadClosure* closure ); -class UdpThreadClosure { +class PacketThreadClosure { public: - UdpThreadClosure( const AddrInfo* addr, const uint8_t* buf, + PacketThreadClosure( const AddrInfo* addr, const uint8_t* buf, int len, QueueCallback cb ) : m_buf(new uint8_t[len]) , m_len(len) @@ -44,10 +44,14 @@ public: , m_cb(cb) , m_created(time( NULL )) { - memcpy( m_buf, buf, len ); + memcpy( m_buf, buf, len ); + m_addr.ref(); } - ~UdpThreadClosure() { delete[] m_buf; } + ~PacketThreadClosure() { + m_addr.unref(); + delete[] m_buf; + } const uint8_t* buf() const { return m_buf; } int len() const { return m_len; } @@ -109,8 +113,8 @@ class UdpQueue { pthread_mutex_t m_partialsMutex; pthread_mutex_t m_queueMutex; pthread_cond_t m_queueCondVar; - deque m_queue; - // map > m_bySocket; + deque m_queue; + // map > m_bySocket; int m_nextID; map m_partialPackets; }; diff --git a/xwords4/relay/xwrelay.conf_tmplate b/xwords4/relay/xwrelay.conf_tmplate index 2b6862ecc..fce4469fd 100644 --- a/xwords4/relay/xwrelay.conf_tmplate +++ b/xwords4/relay/xwrelay.conf_tmplate @@ -28,8 +28,12 @@ DEVICE_PORTS=10998 # Port for per-device UDP interface (experimental) UDP_PORT=10997 -# interface to listen on -- may get dup packets if not specified -UDP_IFACE=eth0 + +# interface to listen on -- may get dup packets if not specified. BUT: +# at least on Linode specifying this leads to an socket that can't be +# reached from localhost, e.g. by python scripts, and local tests pass +# fine without it. So the dup packets thing may no longer apply. +# UDP_IFACE=eth0 # How long after we've read from an address before we assume it's # recycled. Also sent to clients as a suggested ping interval diff --git a/xwords4/relay/xwrelay.cpp b/xwords4/relay/xwrelay.cpp index def2b44f1..e94fd7666 100644 --- a/xwords4/relay/xwrelay.cpp +++ b/xwords4/relay/xwrelay.cpp @@ -124,8 +124,6 @@ logf( XW_LogLevel level, const char* format, ... ) va_end(ap); #else FILE* where = NULL; - struct tm* timp; - struct timeval tv; bool useFile; char logFile[256]; @@ -143,13 +141,14 @@ logf( XW_LogLevel level, const char* format, ... ) if ( !!where ) { static int tm_yday = 0; + struct timeval tv; gettimeofday( &tv, NULL ); struct tm result; - timp = localtime_r( &tv.tv_sec, &result ); + struct tm* timp = localtime_r( &tv.tv_sec, &result ); char timeBuf[64]; - sprintf( timeBuf, "%.2d:%.2d:%.2d", timp->tm_hour, - timp->tm_min, timp->tm_sec ); + sprintf( timeBuf, "%.2d:%.2d:%.2d.%03ld", timp->tm_hour, + timp->tm_min, timp->tm_sec, tv.tv_usec / 1000 ); /* log the date once/day. This isn't threadsafe so may be repeated but that's harmless. */ @@ -1031,7 +1030,7 @@ processDisconnect( const uint8_t* bufp, int bufLen, const AddrInfo* addr ) } /* processDisconnect */ static void -killSocket( const AddrInfo* addr ) +rmSocketRefs( const AddrInfo* addr ) { logf( XW_LOGINFO, "%s(addr.socket=%d)", __func__, addr->getSocket() ); CRefMgr::Get()->RemoveSocketRefs( addr ); @@ -1304,14 +1303,17 @@ handleMsgsMsg( const AddrInfo* addr, bool sendFull, const uint8_t* bufp, const uint8_t* end ) { unsigned short nameCount; - int ii; if ( getNetShort( &bufp, end, &nameCount ) ) { + assert( nameCount == 1 ); // Don't commit this!!! DBMgr* dbmgr = DBMgr::Get(); vector out(4); /* space for len and n_msgs */ assert( out.size() == 4 ); vector msgIDs; - for ( ii = 0; ii < nameCount && bufp < end; ++ii ) { - + for ( int ii = 0; ii < nameCount; ++ii ) { + if ( bufp >= end ) { + logf( XW_LOGERROR, "%s(): ran off the end", __func__ ); + break; + } // See NetUtils.java for reply format // message-length: 2 // nameCount: 2 @@ -1329,6 +1331,7 @@ handleMsgsMsg( const AddrInfo* addr, bool sendFull, break; } + logf( XW_LOGVERBOSE0, "%s(): connName: %s", __func__, connName ); dbmgr->RecordAddress( connName, hid, addr ); /* For each relayID, write the number of messages and then @@ -1345,14 +1348,21 @@ handleMsgsMsg( const AddrInfo* addr, bool sendFull, memcpy( &out[0], &tmp, sizeof(tmp) ); tmp = htons( nameCount ); memcpy( &out[2], &tmp, sizeof(tmp) ); - ssize_t nwritten = write( addr->getSocket(), &out[0], out.size() ); - logf( XW_LOGVERBOSE0, "%s: wrote %d bytes", __func__, nwritten ); - if ( sendFull && nwritten >= 0 && (size_t)nwritten == out.size() ) { + int sock = addr->getSocket(); + ssize_t nWritten = write( sock, &out[0], out.size() ); + if ( nWritten < 0 ) { + logf( XW_LOGERROR, "%s(): write to socket %d failed: %d/%s", __func__, + sock, errno, strerror(errno) ); + } else if ( sendFull && (size_t)nWritten == out.size() ) { + logf( XW_LOGVERBOSE0, "%s(): wrote %d bytes to socket %d", __func__, + nWritten, sock ); dbmgr->RecordSent( &msgIDs[0], msgIDs.size() ); // This is wrong: should be removed when ACK returns and not // before. But for some reason if I make that change apps wind up // stalling. dbmgr->RemoveStoredMessages( msgIDs ); + } else { + assert(0); } } } // handleMsgsMsg @@ -1476,23 +1486,24 @@ handleProxyMsgs( int sock, const AddrInfo* addr, const uint8_t* bufp, } // handleProxyMsgs static void -game_thread_proc( UdpThreadClosure* utc ) +game_thread_proc( PacketThreadClosure* ptc ) { - if ( !processMessage( utc->buf(), utc->len(), utc->addr(), 0 ) ) { - XWThreadPool::GetTPool()->CloseSocket( utc->addr() ); + logf( XW_LOGVERBOSE0, "%s()", __func__ ); + if ( !processMessage( ptc->buf(), ptc->len(), ptc->addr(), 0 ) ) { + // XWThreadPool::GetTPool()->CloseSocket( ptc->addr() ); } } static void -proxy_thread_proc( UdpThreadClosure* utc ) +proxy_thread_proc( PacketThreadClosure* ptc ) { - const int len = utc->len(); - const AddrInfo* addr = utc->addr(); + const int len = ptc->len(); + const AddrInfo* addr = ptc->addr(); if ( len > 0 ) { assert( addr->isTCP() ); int sock = addr->getSocket(); - const uint8_t* bufp = utc->buf(); + const uint8_t* bufp = ptc->buf(); const uint8_t* end = bufp + len; if ( (0 == *bufp++) ) { /* protocol */ XWPRXYCMD cmd = (XWPRXYCMD)*bufp++; @@ -1561,7 +1572,8 @@ proxy_thread_proc( UdpThreadClosure* utc ) } } } - XWThreadPool::GetTPool()->CloseSocket( addr ); + // Should I remove this, or make it into more of an unref() call? + // XWThreadPool::GetTPool()->CloseSocket( addr ); } // proxy_thread_proc static size_t @@ -1726,10 +1738,10 @@ ackPacketIf( const UDPHeader* header, const AddrInfo* addr ) } static void -handle_udp_packet( UdpThreadClosure* utc ) +handle_udp_packet( PacketThreadClosure* ptc ) { - const uint8_t* ptr = utc->buf(); - const uint8_t* end = ptr + utc->len(); + const uint8_t* ptr = ptc->buf(); + const uint8_t* end = ptr + ptc->len(); UDPHeader header; if ( getHeader( &ptr, end, &header ) ) { @@ -1752,7 +1764,7 @@ handle_udp_packet( UdpThreadClosure* utc ) if ( 3 >= clientVers ) { checkAllAscii( model, "bad model" ); } - registerDevice( relayID, &devID, utc->addr(), + registerDevice( relayID, &devID, ptc->addr(), clientVers, devDesc, model, osVers ); } } @@ -1765,7 +1777,7 @@ handle_udp_packet( UdpThreadClosure* utc ) ptr += sizeof(clientToken); clientToken = ntohl( clientToken ); if ( AddrInfo::NULL_TOKEN != clientToken ) { - AddrInfo addr( g_udpsock, clientToken, utc->saddr() ); + AddrInfo addr( g_udpsock, clientToken, ptc->saddr() ); (void)processMessage( ptr, end - ptr, &addr, clientToken ); } else { logf( XW_LOGERROR, "%s: dropping packet with token of 0", @@ -1786,7 +1798,7 @@ handle_udp_packet( UdpThreadClosure* utc ) } SafeCref scr( connName, hid ); if ( scr.IsValid() ) { - AddrInfo addr( g_udpsock, clientToken, utc->saddr() ); + AddrInfo addr( g_udpsock, clientToken, ptc->saddr() ); handlePutMessage( scr, hid, &addr, end - ptr, &ptr, end ); assert( ptr == end ); // DON'T CHECK THIS IN!!! } else { @@ -1821,7 +1833,7 @@ handle_udp_packet( UdpThreadClosure* utc ) case XWPDEV_RQSTMSGS: { DevID devID( ID_TYPE_RELAY ); if ( getVLIString( &ptr, end, devID.m_devIDString ) ) { - const AddrInfo* addr = utc->addr(); + const AddrInfo* addr = ptc->addr(); DevMgr::Get()->rememberDevice( devID.asRelayID(), addr ); if ( XWPDEV_RQSTMSGS == header.cmd ) { @@ -1862,7 +1874,7 @@ handle_udp_packet( UdpThreadClosure* utc ) } // Do this after the device and address are registered - ackPacketIf( &header, utc->addr() ); + ackPacketIf( &header, ptc->addr() ); } } @@ -2335,7 +2347,7 @@ main( int argc, char** argv ) (void)sigaction( SIGINT, &act, NULL ); XWThreadPool* tPool = XWThreadPool::GetTPool(); - tPool->Setup( nWorkerThreads, killSocket ); + tPool->Setup( nWorkerThreads, rmSocketRefs ); /* set up select call */ fd_set rfds;