take
@@ -25,12 +25,10 @@
New with this release
- - Offer to "Archive" finished games
- - Make tap on thumbnail toggle whether game's selected rather
- than open it. (Tap to the right still opens)
- - Bug fix: don't allow duplicate group names
- - Fix battery-hogging behavior on non-Google-play
- installs
+ - F-Droid has stiffened their prohibition against including
+ proprietary Google components. This release complies by removing
+ "Google Cloud Messaging" – which never worked on
+ f-droid installs anyway.
(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 @@
+
+
+
+
+
+
-
+
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 @@
+
+
\ 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 @@
+
+
\ 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