Merge branch 'android_branch' of github.com:eehouse/xwords into android_branch

This commit is contained in:
Eric House 2018-01-16 20:31:49 -08:00
commit 0045de92f3
102 changed files with 5456 additions and 894 deletions

View file

@ -1,29 +1,23 @@
Here's how I'm building crosswords for Android. (Updated Dec 2017)
First, cd into the directory xwords4/android/XWords4. Everything Here's how I'm building CrossWords for Android.
First, cd into the directory xwords4/android. Everything
happens there. happens there.
IF (and it's a big if) you have all the necessary tools installed (the To build and install the debug version of CrossWords:
Android SDK and NDK, apache ant, and probably other stuff I've
forgotten), two commands will build it, the first a one-time deal and
the second repeated every time you made a change.
The build process requires a file called local.properties that must be # ./gradlew clean insXw4Deb
generated locally. Generate it before your first build by running
# ../scripts/setup_local_props.sh To build and install the debug version of CrossDbg (a variant meant
for development that can co-exist with CrossWords):
Then build using this command: # ./gradlew -PuseCrashlytics insXw4dDeb
# ant debug I do all development on Debian and Ubuntu Linux systems. I have built
on MacOS, where once you get all the necessary tools installed via
Or, if you have a device or emulator attached (listed by 'adb homebrew there's only one problem I'm aware of: the parameter 'white'
devices'), try that's passed to convert by android/scripts/images.mk on Linux systems
needs to be 'black' on MacOS. I have no clue why. If you don't make
# ant debug install this change the subset of actionbar icons that are generated from .svg
files will be black-on-black.
Making a release build requires that you've set up your keys. (I did
this too long ago to remember how but the info's easy to find). Once
that's done, just tether your device and type
# ant release install

View file

@ -1,6 +1,6 @@
def INITIAL_CLIENT_VERS = 8 def INITIAL_CLIENT_VERS = 8
def VERSION_CODE_BASE = 125 def VERSION_CODE_BASE = 128
def VERSION_NAME = '4.4.129' def VERSION_NAME = '4.4.132'
def FABRIC_API_KEY = System.getenv("FABRIC_API_KEY") def FABRIC_API_KEY = System.getenv("FABRIC_API_KEY")
def GCM_SENDER_ID = System.getenv("GCM_SENDER_ID") def GCM_SENDER_ID = System.getenv("GCM_SENDER_ID")
def BUILD_INFO_NAME = "build-info.txt" def BUILD_INFO_NAME = "build-info.txt"
@ -74,6 +74,19 @@ android {
buildConfigField "String", "GCM_SENDER_ID", "\"$GCM_SENDER_ID\"" buildConfigField "String", "GCM_SENDER_ID", "\"$GCM_SENDER_ID\""
} }
xw4fdroid {
dimension "variant"
applicationId "org.eehouse.android.xw4"
manifestPlaceholders = [ APP_ID: applicationId ]
resValue "string", "app_name", "CrossWords"
resValue "string", "nbs_port", "3344"
resValue "string", "invite_prefix", "/and/"
buildConfigField "boolean", "WIDIR_ENABLED", "false"
buildConfigField "boolean", "RELAYINVITE_SUPPORTED", "false"
buildConfigField "String", "GCM_SENDER_ID", "\"\""
}
xw4d { xw4d {
dimension "variant" dimension "variant"
minSdkVersion 8 minSdkVersion 8
@ -158,6 +171,14 @@ android {
jniLibs.srcDir "../libs-xw4dDebug" jniLibs.srcDir "../libs-xw4dDebug"
} }
} }
xw4fdroid {
release {
jniLibs.srcDir "../libs-xw4fdroidRelease"
}
debug {
jniLibs.srcDir "../libs-xw4fdroidDebug"
}
}
} }
lintOptions { lintOptions {
@ -176,10 +197,12 @@ android {
dependencies { dependencies {
// Look into replacing this with a fetch too PENDING // Look into replacing this with a fetch too PENDING
compile files('../libs/gcm.jar') xw4Compile files('../libs/gcm.jar')
compile 'com.android.support:support-v4:23.4.0' compile 'com.android.support:support-v4:23.4.0'
// 2.6.8 is probably as far forward as I can go without upping my
// min-supported SDK version
xw4dCompile('com.crashlytics.sdk.android:crashlytics:2.6.8@aar') { xw4dCompile('com.crashlytics.sdk.android:crashlytics:2.6.8@aar') {
transitive = true; transitive = true;
} }

View file

@ -13,9 +13,9 @@
</style> </style>
</head> </head>
<body> <body>
<h2>CrossWords 4.4.129 release</h2> <h2>CrossWords 4.4.132 release</h2>
<p>Quick fix for a another crash reported via the Play Store</p> <p>This release makes communication with the relay more robust.</p>
<div id="survey"> <div id="survey">
<p>Please <a href="https://www.surveymonkey.com/s/GX3XLHR">take <p>Please <a href="https://www.surveymonkey.com/s/GX3XLHR">take
@ -25,7 +25,14 @@
<h3>New with this release</h3> <h3>New with this release</h3>
<ul> <ul>
<li>Fix crash that only showed up in games using timers</li> <li>Communicate with relay via http when the custom protocol
isn't working (e.g. when a wifi router or firewall blocks
it.)</li>
<li>Improved translations for French, Japanese and Norwegian
(by Weblate volunteers)</li>
<li>Add a few menubar icons (thanks to The Noun Project)</li>
<li>Fix taps on left end of items in the Games List not always
selecting/unselecting them</li>
</ul> </ul>
<p>(The full changelog <p>(The full changelog

View file

@ -550,7 +550,7 @@ public class BTService extends XWService {
} else { } else {
short len = is.readShort(); short len = is.readShort();
byte[] nliData = new byte[len]; byte[] nliData = new byte[len];
is.read( nliData ); is.readFully( nliData );
nli = XwJNI.nliFromStream( nliData ); nli = XwJNI.nliFromStream( nliData );
} }
@ -573,26 +573,20 @@ public class BTService extends XWService {
int gameID = dis.readInt(); int gameID = dis.readInt();
switch ( cmd ) { switch ( cmd ) {
case MESG_SEND: case MESG_SEND:
short len = dis.readShort(); byte[] buffer = new byte[dis.readShort()];
byte[] buffer = new byte[len]; dis.readFully( buffer );
int nRead = dis.read( buffer, 0, len ); BluetoothDevice host = socket.getRemoteDevice();
if ( nRead == len ) { addAddr( host );
BluetoothDevice host = socket.getRemoteDevice();
addAddr( host );
CommsAddrRec addr = new CommsAddrRec( host.getName(), CommsAddrRec addr = new CommsAddrRec( host.getName(),
host.getAddress() ); host.getAddress() );
ReceiveResult rslt ReceiveResult rslt
= BTService.this.receiveMessage( BTService.this, = BTService.this.receiveMessage( BTService.this,
gameID, m_btMsgSink, gameID, m_btMsgSink,
buffer, addr ); buffer, addr );
result = rslt == ReceiveResult.GAME_GONE ? result = rslt == ReceiveResult.GAME_GONE ?
BTCmd.MESG_GAMEGONE : BTCmd.MESG_ACCPT; BTCmd.MESG_GAMEGONE : BTCmd.MESG_ACCPT;
} else {
Log.e( TAG, "receiveMessage(): read only %d of %d bytes",
nRead, len );
}
break; break;
case MESG_GAMEGONE: case MESG_GAMEGONE:
postEvent( MultiEvent.MESSAGE_NOGAME, gameID ); postEvent( MultiEvent.MESSAGE_NOGAME, gameID );

View file

@ -188,10 +188,8 @@ public class BiDiSockWrap {
DataInputStream inStream DataInputStream inStream
= new DataInputStream( mSocket.getInputStream() ); = new DataInputStream( mSocket.getInputStream() );
while ( mRunThreads ) { while ( mRunThreads ) {
short len = inStream.readShort(); byte[] packet = new byte[inStream.readShort()];
Log.d( TAG, "got len: %d", len ); inStream.readFully( packet );
byte[] packet = new byte[len];
inStream.read( packet );
mIface.gotPacket( BiDiSockWrap.this, packet ); mIface.gotPacket( BiDiSockWrap.this, packet );
} }
} catch( IOException ioe ) { } catch( IOException ioe ) {

View file

@ -204,6 +204,18 @@ public class BoardDelegate extends DelegateBase
} }
}; };
ab.setNegativeButton( R.string.button_rematch, lstnr ); ab.setNegativeButton( R.string.button_rematch, lstnr );
// If we're not already in the "archive" group, offer to move
if ( !inArchiveGroup() ) {
lstnr = new OnClickListener() {
public void onClick( DialogInterface dlg,
int whichButton ) {
showArchiveNA();
}
};
ab.setNeutralButton( R.string.button_archive, lstnr );
}
} else if ( DlgID.DLG_CONNSTAT == dlgID } else if ( DlgID.DLG_CONNSTAT == dlgID
&& BuildConfig.DEBUG && null != m_connTypes && BuildConfig.DEBUG && null != m_connTypes
&& (m_connTypes.contains( CommsConnType.COMMS_CONN_RELAY ) && (m_connTypes.contains( CommsConnType.COMMS_CONN_RELAY )
@ -828,6 +840,9 @@ public class BoardDelegate extends DelegateBase
enable = m_gameOver && rematchSupported( false ); enable = m_gameOver && rematchSupported( false );
Utils.setItemVisible( menu, R.id.board_menu_rematch, enable ); 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 boolean netGame = null != m_gi
&& DeviceRole.SERVER_STANDALONE != m_gi.serverRole; && DeviceRole.SERVER_STANDALONE != m_gi.serverRole;
Utils.setItemVisible( menu, R.id.gamel_menu_checkmoves, netGame ); Utils.setItemVisible( menu, R.id.gamel_menu_checkmoves, netGame );
@ -871,6 +886,10 @@ public class BoardDelegate extends DelegateBase
doRematchIf(); doRematchIf();
break; break;
case R.id.board_menu_archive:
showArchiveNA();
break;
case R.id.board_menu_trade_commit: case R.id.board_menu_trade_commit:
cmd = JNICmd.CMD_COMMIT; cmd = JNICmd.CMD_COMMIT;
break; break;
@ -1092,6 +1111,10 @@ public class BoardDelegate extends DelegateBase
makeOkOnlyBuilder( R.string.after_restart ).show(); makeOkOnlyBuilder( R.string.after_restart ).show();
break; break;
case ARCHIVE_ACTION:
archiveAndClose();
break;
case ENABLE_SMS_DO: case ENABLE_SMS_DO:
post( new Runnable() { post( new Runnable() {
public void run() { public void run() {
@ -2575,6 +2598,37 @@ public class BoardDelegate extends DelegateBase
return wordsArray; return wordsArray;
} }
private boolean inArchiveGroup()
{
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, archiveGroupID );
waitCloseGame( false );
finish();
}
// For now, supported if standalone or either BT or SMS used for transport // For now, supported if standalone or either BT or SMS used for transport
private boolean rematchSupported( boolean showMulti ) private boolean rematchSupported( boolean showMulti )
{ {

View file

@ -387,32 +387,6 @@ public class CommsTransport implements TransportProcs,
return nSent; return nSent;
} }
public void relayStatus( CommsRelayState newState )
{
Log.i( TAG, "relayStatus called; state=%s", newState.toString() );
switch( newState ) {
case COMMS_RELAYSTATE_UNCONNECTED:
case COMMS_RELAYSTATE_DENIED:
case COMMS_RELAYSTATE_CONNECT_PENDING:
ConnStatusHandler.updateStatus( m_context, null,
CommsConnType.COMMS_CONN_RELAY,
false );
break;
case COMMS_RELAYSTATE_CONNECTED:
case COMMS_RELAYSTATE_RECONNECTED:
ConnStatusHandler.updateStatusOut( m_context, null,
CommsConnType.COMMS_CONN_RELAY,
true );
break;
case COMMS_RELAYSTATE_ALLCONNECTED:
ConnStatusHandler.updateStatusIn( m_context, null,
CommsConnType.COMMS_CONN_RELAY,
true );
break;
}
}
public void relayConnd( String room, int devOrder, boolean allHere, public void relayConnd( String room, int devOrder, boolean allHere,
int nMissing ) int nMissing )
{ {

View file

@ -81,7 +81,9 @@ public class DBUtils {
private static long s_cachedRowID = ROWID_NOTFOUND; private static long s_cachedRowID = ROWID_NOTFOUND;
private static byte[] s_cachedBytes = null; private static byte[] s_cachedBytes = null;
public static enum GameChangeType { GAME_CHANGED, GAME_CREATED, GAME_DELETED }; public static enum GameChangeType { GAME_CHANGED, GAME_CREATED,
GAME_DELETED, GAME_MOVED,
};
public static interface DBChangeListener { public static interface DBChangeListener {
public void gameSaved( long rowid, GameChangeType change ); public void gameSaved( long rowid, GameChangeType change );
@ -1701,6 +1703,29 @@ public class DBUtils {
return result; return result;
} }
public static long getGroup( Context context, String name )
{
long result = GROUPID_UNSPEC;
String[] columns = { ROW_ID };
String selection = DBHelper.GROUPNAME + " = ?";
String[] selArgs = { name };
initDB( context );
synchronized( s_dbHelper ) {
Cursor cursor = s_db.query( DBHelper.TABLE_NAME_GROUPS, columns,
selection, selArgs,
null, // groupBy
null, // having
null // orderby
);
if ( cursor.moveToNext() ) {
result = cursor.getLong( cursor.getColumnIndex( ROW_ID ) );
}
cursor.close();
}
return result;
}
public static long addGroup( Context context, String name ) public static long addGroup( Context context, String name )
{ {
long rowid = GROUPID_UNSPEC; long rowid = GROUPID_UNSPEC;
@ -1759,13 +1784,14 @@ public class DBUtils {
} }
// Change group id of a game // Change group id of a game
public static void moveGame( Context context, long gameid, long groupID ) public static void moveGame( Context context, long rowid, long groupID )
{ {
Assert.assertTrue( GROUPID_UNSPEC != groupID ); Assert.assertTrue( GROUPID_UNSPEC != groupID );
ContentValues values = new ContentValues(); ContentValues values = new ContentValues();
values.put( DBHelper.GROUPID, groupID ); values.put( DBHelper.GROUPID, groupID );
updateRow( context, DBHelper.TABLE_NAME_SUM, gameid, values ); updateRow( context, DBHelper.TABLE_NAME_SUM, rowid, values );
invalGroupsCache(); invalGroupsCache();
notifyListeners( rowid, GameChangeType.GAME_MOVED );
} }
private static String getChatHistoryStr( Context context, long rowid ) private static String getChatHistoryStr( Context context, long rowid )

View file

@ -31,8 +31,6 @@ package org.eehouse.android.xw4;
import android.content.Context; import android.content.Context;
import com.google.android.gcm.GCMRegistrar;
public class DevID { public class DevID {
private static final String TAG = DevID.class.getSimpleName(); private static final String TAG = DevID.class.getSimpleName();
@ -137,7 +135,7 @@ public class DevID {
if ( 0 != storedVers && storedVers < curVers ) { if ( 0 != storedVers && storedVers < curVers ) {
result = ""; // Don't trust what registrar has result = ""; // Don't trust what registrar has
} else { } else {
result = GCMRegistrar.getRegistrationId( context ); result = GCMStub.getRegistrationId( context );
} }
return result; return result;
} }

View file

@ -1238,7 +1238,7 @@ public class DictsDelegate extends ListDelegateBase
// parse less data // parse less data
String name = null; String name = null;
String proc = String.format( "listDicts?lc=%s", m_lc ); 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 ) { if ( null != conn ) {
JSONObject theOne = null; JSONObject theOne = null;
String langName = null; String langName = null;
@ -1320,7 +1320,7 @@ public class DictsDelegate extends ListDelegateBase
public Boolean doInBackground( Void... unused ) public Boolean doInBackground( Void... unused )
{ {
boolean success = false; boolean success = false;
HttpURLConnection conn = NetUtils.makeHttpConn( m_context, "listDicts" ); HttpURLConnection conn = NetUtils.makeHttpUpdateConn( m_context, "listDicts" );
if ( null != conn ) { if ( null != conn ) {
String json = NetUtils.runConn( conn, new JSONObject() ); String json = NetUtils.runConn( conn, new JSONObject() );
if ( !isCancelled() ) { if ( !isCancelled() ) {

View file

@ -84,6 +84,7 @@ public class DlgDelegate {
TRAY_PICKED, TRAY_PICKED,
INVITE_INFO, INVITE_INFO,
DISABLE_DUALPANE, DISABLE_DUALPANE,
ARCHIVE_ACTION,
// Dict Browser // Dict Browser
FINISH_ACTION, FINISH_ACTION,

View file

@ -482,14 +482,12 @@ public class GameUtils {
if ( force ) { if ( force ) {
HashMap<Long,CommsConnTypeSet> games = HashMap<Long,CommsConnTypeSet> games =
DBUtils.getGamesWithSendsPending( context ); DBUtils.getGamesWithSendsPending( context );
if ( 0 < games.size() ) { new ResendTask( context, games, filter, proc ).execute();
new ResendTask( context, games, filter, proc ).execute();
System.arraycopy( sendTimes, 0, /* src */ System.arraycopy( sendTimes, 0, /* src */
sendTimes, 1, /* dest */ sendTimes, 1, /* dest */
sendTimes.length - 1 ); sendTimes.length - 1 );
sendTimes[0] = now; sendTimes[0] = now;
}
} }
} }
@ -1196,7 +1194,7 @@ public class GameUtils {
for ( CommsConnType typ : conTypes ) { for ( CommsConnType typ : conTypes ) {
switch ( typ ) { switch ( typ ) {
case COMMS_CONN_RELAY: case COMMS_CONN_RELAY:
tellRelayDied( context, summary, informNow ); // see below
break; break;
case COMMS_CONN_BT: case COMMS_CONN_BT:
BTService.gameDied( context, addr.bt_btAddr, gameID ); BTService.gameDied( context, addr.bt_btAddr, gameID );
@ -1211,6 +1209,14 @@ public class GameUtils {
} }
} }
// comms doesn't have a relay address for us until the game's
// in play (all devices registered, at least.) To enable
// deleting on relay half-games that we created but nobody
// joined, special-case this one.
if ( summary.inRelayGame() ) {
tellRelayDied( context, summary, informNow );
}
gamePtr.release(); gamePtr.release();
} }
} }
@ -1251,7 +1257,7 @@ public class GameUtils {
private HashMap<Long,CommsConnTypeSet> m_games; private HashMap<Long,CommsConnTypeSet> m_games;
private ResendDoneProc m_doneProc; private ResendDoneProc m_doneProc;
private CommsConnType m_filter; private CommsConnType m_filter;
private MultiMsgSink m_sink; private int m_nSent = 0;
public ResendTask( Context context, HashMap<Long,CommsConnTypeSet> games, public ResendTask( Context context, HashMap<Long,CommsConnTypeSet> games,
CommsConnType filter, ResendDoneProc proc ) CommsConnType filter, ResendDoneProc proc )
@ -1280,14 +1286,15 @@ public class GameUtils {
GameLock lock = new GameLock( rowid, false ); GameLock lock = new GameLock( rowid, false );
if ( lock.tryLock() ) { if ( lock.tryLock() ) {
CurGameInfo gi = new CurGameInfo( m_context ); CurGameInfo gi = new CurGameInfo( m_context );
m_sink = new MultiMsgSink( m_context, rowid ); MultiMsgSink sink = new MultiMsgSink( m_context, rowid );
GamePtr gamePtr = loadMakeGame( m_context, gi, m_sink, lock ); GamePtr gamePtr = loadMakeGame( m_context, gi, sink, lock );
if ( null != gamePtr ) { if ( null != gamePtr ) {
int nSent = XwJNI.comms_resendAll( gamePtr, true, int nSent = XwJNI.comms_resendAll( gamePtr, true,
m_filter, false ); m_filter, false );
gamePtr.release(); gamePtr.release();
Log.d( TAG, "ResendTask.doInBackground(): sent %d " Log.d( TAG, "ResendTask.doInBackground(): sent %d "
+ "messages for rowid %d", nSent, rowid ); + "messages for rowid %d", nSent, rowid );
m_nSent += sink.numSent();
} else { } else {
Log.d( TAG, "ResendTask.doInBackground(): loadMakeGame()" Log.d( TAG, "ResendTask.doInBackground(): loadMakeGame()"
+ " failed for rowid %d", rowid ); + " failed for rowid %d", rowid );
@ -1312,8 +1319,7 @@ public class GameUtils {
protected void onPostExecute( Void unused ) protected void onPostExecute( Void unused )
{ {
if ( null != m_doneProc ) { if ( null != m_doneProc ) {
int nSent = null == m_sink ? 0 : m_sink.numSent(); m_doneProc.onResendDone( m_context, m_nSent );
m_doneProc.onResendDone( m_context, nSent );
} }
} }
} }

View file

@ -755,9 +755,18 @@ public class GamesListDelegate extends ListDelegateBase
lstnr = new OnClickListener() { lstnr = new OnClickListener() {
public void onClick( DialogInterface dlg, int item ) { public void onClick( DialogInterface dlg, int item ) {
String name = namer.getName(); String name = namer.getName();
DBUtils.addGroup( m_activity, name ); long hasName = DBUtils.getGroup( m_activity, name );
mkListAdapter(); if ( DBUtils.GROUPID_UNSPEC == hasName ) {
showNewGroupIf(); DBUtils.addGroup( m_activity, name );
mkListAdapter();
showNewGroupIf();
} else {
String msg = LocUtils
.getString( m_activity,
R.string.duplicate_group_name_fmt,
name );
makeOkOnlyBuilder( msg ).show();
}
} }
}; };
lstnr2 = new OnClickListener() { lstnr2 = new OnClickListener() {
@ -1060,8 +1069,6 @@ public class GamesListDelegate extends ListDelegateBase
invalidateOptionsMenuIf(); invalidateOptionsMenuIf();
setTitle(); setTitle();
} }
mkListAdapter();
} }
public void invalidateOptionsMenuIf() public void invalidateOptionsMenuIf()
@ -1133,6 +1140,9 @@ public class GamesListDelegate extends ListDelegateBase
mkListAdapter(); mkListAdapter();
setSelGame( rowid ); setSelGame( rowid );
break; break;
case GAME_MOVED:
mkListAdapter();
break;
default: default:
Assert.fail(); Assert.fail();
break; break;

View file

@ -119,10 +119,6 @@ public class MultiMsgSink implements TransportProcs {
return nSent; return nSent;
} }
public void relayStatus( CommsRelayState newState )
{
}
public void relayErrorProc( XWRELAY_ERROR relayErr ) public void relayErrorProc( XWRELAY_ERROR relayErr )
{ {
} }

View file

@ -25,6 +25,8 @@ import android.text.TextUtils;
import junit.framework.Assert; import junit.framework.Assert;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject; import org.json.JSONObject;
import java.io.BufferedInputStream; import java.io.BufferedInputStream;
@ -88,49 +90,29 @@ public class NetUtils {
m_obits = obits; m_obits = obits;
} }
public void run() { @Override
Socket socket = makeProxySocket( m_context, 10000 ); public void run()
if ( null != socket ) { {
int strLens = 0; try {
int nObits = 0; JSONArray params = new JSONArray();
for ( int ii = 0; ii < m_obits.length; ++ii ) { for ( int ii = 0; ii < m_obits.length; ++ii ) {
String relayID = m_obits[ii].m_relayID; JSONObject one = new JSONObject();
if ( null != relayID ) { one.put( "relayID", m_obits[ii].m_relayID );
++nObits; one.put( "seed", m_obits[ii].m_seed );
strLens += relayID.length() + 1; // 1 for /n params.put( one );
}
} }
HttpURLConnection conn = makeHttpRelayConn( m_context, "kill" );
String resStr = runConn( conn, params );
Log.d( TAG, "runViaWeb(): kill(%s) => %s", params, resStr );
try { if ( null != resStr ) {
DataOutputStream outStream = JSONObject result = new JSONObject( resStr );
new DataOutputStream( socket.getOutputStream() ); if ( 0 == result.optInt( "err", -1 ) ) {
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 ) {
DBUtils.clearObits( m_context, m_obits ); 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 ); DBUtils.Obit[] obits = DBUtils.listObits( context );
if ( null != obits && 0 < obits.length ) { if ( null != obits && 0 < obits.length ) {
InformThread thread = new InformThread( context, obits ); new InformThread( context, obits ).start();
thread.start();
} }
} }
@ -184,7 +165,7 @@ public class NetUtils {
short len = dis.readShort(); short len = dis.readShort();
if ( len > 0 ) { if ( len > 0 ) {
byte[] packet = new byte[len]; byte[] packet = new byte[len];
dis.read( packet ); dis.readFully( packet );
msgs[ii][jj] = packet; msgs[ii][jj] = packet;
} }
} }
@ -214,14 +195,26 @@ public class NetUtils {
return host; return host;
} }
protected static HttpURLConnection makeHttpConn( Context context, protected static HttpURLConnection makeHttpRelayConn( Context context,
String proc ) 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; HttpURLConnection result = null;
try { try {
String url = String.format( "%s/%s", String url = String.format( "%s/%s", path, proc );
XWPrefs.getDefaultUpdateUrl( context ),
proc );
result = (HttpURLConnection)new URL(url).openConnection(); result = (HttpURLConnection)new URL(url).openConnection();
} catch ( java.net.MalformedURLException mue ) { } catch ( java.net.MalformedURLException mue ) {
Assert.assertNull( result ); Assert.assertNull( result );
@ -233,11 +226,21 @@ public class NetUtils {
return result; return result;
} }
protected static String runConn( HttpURLConnection conn, JSONArray param )
{
return runConn( conn, param.toString() );
}
protected static String runConn( HttpURLConnection conn, JSONObject param ) protected static String runConn( HttpURLConnection conn, JSONObject param )
{
return runConn( conn, param.toString() );
}
private static String runConn( HttpURLConnection conn, String param )
{ {
String result = null; String result = null;
Map<String, String> params = new HashMap<String, String>(); Map<String, String> params = new HashMap<String, String>();
params.put( k_PARAMS, param.toString() ); params.put( k_PARAMS, param );
String paramsString = getPostDataString( params ); String paramsString = getPostDataString( params );
if ( null != paramsString ) { if ( null != paramsString ) {
@ -273,7 +276,8 @@ public class NetUtils {
} }
result = new String( bas.toByteArray() ); result = new String( bas.toByteArray() );
} else { } 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 ) { } catch ( java.net.ProtocolException pe ) {
Log.ex( TAG, pe ); Log.ex( TAG, pe );
@ -285,17 +289,18 @@ public class NetUtils {
return result; return result;
} }
// This handles multiple params but only every gets passed one!
private static String getPostDataString( Map<String, String> params ) private static String getPostDataString( Map<String, String> params )
{ {
String result = null; String result = null;
try { try {
ArrayList<String> pairs = new ArrayList<String>(); ArrayList<String> pairs = new ArrayList<String>();
// StringBuilder sb = new StringBuilder(); // StringBuilder sb = new StringBuilder();
String[] pair = { null, null }; // String[] pair = { null, null };
for ( Map.Entry<String, String> entry : params.entrySet() ){ for ( Map.Entry<String, String> entry : params.entrySet() ){
pair[0] = URLEncoder.encode( entry.getKey(), "UTF-8" ); pairs.add( URLEncoder.encode( entry.getKey(), "UTF-8" )
pair[1] = URLEncoder.encode( entry.getValue(), "UTF-8" ); + "="
pairs.add( TextUtils.join( "=", pair ) ); + URLEncoder.encode( entry.getValue(), "UTF-8" ) );
} }
result = TextUtils.join( "&", pairs ); result = TextUtils.join( "&", pairs );
} catch ( java.io.UnsupportedEncodingException uee ) { } catch ( java.io.UnsupportedEncodingException uee ) {

View file

@ -94,7 +94,7 @@ public class RefreshNamesTask extends AsyncTask<Void, Void, String[]> {
// Can't figure out how to read a null-terminated string // Can't figure out how to read a null-terminated string
// from DataInputStream so parse it myself. // from DataInputStream so parse it myself.
byte[] bytes = new byte[len]; byte[] bytes = new byte[len];
dis.read( bytes ); dis.readFully( bytes );
int index = -1; int index = -1;
for ( int ii = 0; ii < nRooms; ++ii ) { for ( int ii = 0; ii < nRooms; ++ii ) {

View file

@ -26,6 +26,7 @@ import android.content.Intent;
import android.os.AsyncTask; import android.os.AsyncTask;
import android.os.Build; import android.os.Build;
import android.os.Handler; import android.os.Handler;
import android.text.TextUtils;
import junit.framework.Assert; 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.jni.XwJNI;
import org.eehouse.android.xw4.loc.LocUtils; 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.ByteArrayInputStream;
import java.io.ByteArrayOutputStream; import java.io.ByteArrayOutputStream;
import java.io.DataInputStream; import java.io.DataInputStream;
@ -46,13 +51,18 @@ import java.io.InputStream;
import java.io.OutputStream; import java.io.OutputStream;
import java.net.DatagramPacket; import java.net.DatagramPacket;
import java.net.DatagramSocket; import java.net.DatagramSocket;
import java.net.HttpURLConnection;
import java.net.InetAddress; import java.net.InetAddress;
import java.net.Socket; import java.net.Socket;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.HashMap; import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator; import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.concurrent.LinkedBlockingQueue; import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicReference;
public class RelayService extends XWService public class RelayService extends XWService
implements NetStateCache.StateChangedIf { implements NetStateCache.StateChangedIf {
@ -60,6 +70,8 @@ public class RelayService extends XWService
private static final int MAX_SEND = 1024; private static final int MAX_SEND = 1024;
private static final int MAX_BUF = MAX_SEND - 2; private static final int MAX_BUF = MAX_SEND - 2;
private static final int REG_WAIT_INTERVAL = 10; private static final int REG_WAIT_INTERVAL = 10;
private static final int INITIAL_BACKOFF = 5;
private static final int UDP_FAIL_LIMIT = 5;
// One day, in seconds. Probably should be configurable. // One day, in seconds. Probably should be configurable.
private static final long MAX_KEEPALIVE_SECS = 24 * 60 * 60; private static final long MAX_KEEPALIVE_SECS = 24 * 60 * 60;
@ -90,8 +102,9 @@ public class RelayService extends XWService
private static final String ROWID = "ROWID"; private static final String ROWID = "ROWID";
private static final String BINBUFFER = "BINBUFFER"; private static final String BINBUFFER = "BINBUFFER";
private static HashSet<Integer> s_packetsSent = new HashSet<Integer>(); private static List<PacketData> s_packetsSentUDP = new ArrayList<>();
private static int s_nextPacketID = 1; private static List<PacketData> s_packetsSentWeb = new ArrayList<>();
private static AtomicInteger s_nextPacketID = new AtomicInteger();
private static boolean s_gcmWorking = false; private static boolean s_gcmWorking = false;
private static boolean s_registered = false; private static boolean s_registered = false;
private static CommsAddrRec s_addr = private static CommsAddrRec s_addr =
@ -100,16 +113,14 @@ public class RelayService extends XWService
private static long s_curNextTimer; private static long s_curNextTimer;
static { resetBackoffTimer(); } static { resetBackoffTimer(); }
private Thread m_fetchThread = null; private Thread m_fetchThread = null; // no longer used
private Thread m_UDPReadThread = null; private AtomicReference<UDPThreads> m_UDPThreadsRef = new AtomicReference<>();
private Thread m_UDPWriteThread = null;
private DatagramSocket m_UDPSocket;
private LinkedBlockingQueue<PacketData> m_queue =
new LinkedBlockingQueue<PacketData>();
private Handler m_handler; private Handler m_handler;
private Runnable m_onInactivity; private Runnable m_onInactivity;
private int m_maxIntervalSeconds = 0; private int m_maxIntervalSeconds = 0;
private long m_lastGamePacketReceived; private long m_lastGamePacketReceived;
private int m_nativeFailScore;
private boolean m_skipUPDSet;
private static DevIDType s_curType = DevIDType.ID_TYPE_NONE; private static DevIDType s_curType = DevIDType.ID_TYPE_NONE;
private static long s_regStartTime = 0; private static long s_regStartTime = 0;
@ -160,7 +171,7 @@ public class RelayService extends XWService
{ {
boolean enabled = ! XWPrefs boolean enabled = ! XWPrefs
.getPrefsBoolean( context, R.string.key_disable_relay, false ); .getPrefsBoolean( context, R.string.key_disable_relay, false );
Log.d( TAG, "relayEnabled() => %b", enabled ); // Log.d( TAG, "relayEnabled() => %b", enabled );
return enabled; return enabled;
} }
@ -306,7 +317,7 @@ public class RelayService extends XWService
// Exists to get incoming data onto the main thread // Exists to get incoming data onto the main thread
private static void postData( Context context, long rowid, byte[] msg ) private static void postData( Context context, long rowid, byte[] msg )
{ {
Log.d( TAG, "postData(): packet of length %d for token %d", Log.d( TAG, "postData(): packet of length %d for token (rowid) %d",
msg.length, rowid ); msg.length, rowid );
if ( DBUtils.haveGame( context, rowid ) ) { if ( DBUtils.haveGame( context, rowid ) ) {
Intent intent = getIntentTo( context, MsgCmds.RECEIVE ) Intent intent = getIntentTo( context, MsgCmds.RECEIVE )
@ -374,6 +385,8 @@ public class RelayService extends XWService
} }
} }
}; };
m_skipUPDSet = XWPrefs.getSkipToWebAPI( this );
} }
@Override @Override
@ -388,7 +401,7 @@ public class RelayService extends XWService
cmd = null; cmd = null;
} }
if ( null != cmd ) { if ( null != cmd ) {
Log.d( TAG, "onStartCommand(): cmd=%s", cmd.toString() ); // Log.d( TAG, "onStartCommand(): cmd=%s", cmd.toString() );
switch( cmd ) { switch( cmd ) {
case PROCESS_GAME_MSGS: case PROCESS_GAME_MSGS:
String[] relayIDs = new String[1]; String[] relayIDs = new String[1];
@ -403,7 +416,7 @@ public class RelayService extends XWService
byte[][][] msgss = expandMsgsArray( intent ); byte[][][] msgss = expandMsgsArray( intent );
for ( byte[][] msgs : msgss ) { for ( byte[][] msgs : msgss ) {
for ( byte[] msg : msgs ) { for ( byte[] msg : msgs ) {
gotPacket( msg, true ); gotPacket( msg, true, false );
} }
} }
break; break;
@ -449,7 +462,7 @@ public class RelayService extends XWService
case TIMER_FIRED: case TIMER_FIRED:
if ( !NetStateCache.netAvail( this ) ) { if ( !NetStateCache.netAvail( this ) ) {
Log.w( TAG, "not connecting: no network" ); Log.w( TAG, "not connecting: no network" );
} else if ( startFetchThreadIf() ) { } else if ( startFetchThreadIfNotUDP() ) {
// do nothing // do nothing
} else if ( registerWithRelayIfNot() ) { } else if ( registerWithRelayIfNot() ) {
requestMessages(); requestMessages();
@ -510,9 +523,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; boolean handled = relayEnabled( this ) && !XWApp.UDP_ENABLED;
if ( handled && null == m_fetchThread ) { if ( handled && null == m_fetchThread ) {
m_fetchThread = new Thread( null, new Runnable() { m_fetchThread = new Thread( null, new Runnable() {
@ -542,152 +555,69 @@ public class RelayService extends XWService
private void startUDPThreadsIfNot() private void startUDPThreadsIfNot()
{ {
if ( XWApp.UDP_ENABLED && relayEnabled( this ) ) { if ( XWApp.UDP_ENABLED && relayEnabled( this ) ) {
if ( null == m_UDPReadThread ) { synchronized ( m_UDPThreadsRef ) {
m_UDPReadThread = new Thread( null, new Runnable() { if ( null == m_UDPThreadsRef.get() ) {
public void run() { UDPThreads threads = new UDPThreads();
m_UDPThreadsRef.set( threads );
connectSocket(); // block until this is done threads.start();
startWriteThread(); }
Log.i( TAG, "read thread running" );
byte[] buf = new byte[1024];
for ( ; ; ) {
DatagramPacket packet =
new DatagramPacket( buf, buf.length );
try {
m_UDPSocket.receive( packet );
resetExitTimer();
gotPacket( packet );
} catch ( java.io.InterruptedIOException iioe ) {
// DbgUtils.logf( "FYI: udp receive timeout" );
} catch( java.io.IOException ioe ) {
break;
}
}
Log.i( TAG, "read thread exiting" );
}
}, getClass().getName() );
m_UDPReadThread.start();
} else {
Log.i( TAG, "m_UDPReadThread not null and assumed to be running" );
} }
} else { } else {
Log.i( TAG, "startUDPThreadsIfNot(): UDP disabled" ); Log.i( TAG, "startUDPThreadsIfNot(): UDP disabled" );
} }
} // startUDPThreadsIfNot } // startUDPThreadsIfNot
private void connectSocket() private boolean skipNativeSend()
{ {
if ( null == m_UDPSocket ) { boolean skip = m_nativeFailScore > UDP_FAIL_LIMIT || m_skipUPDSet;
int port = XWPrefs.getDefaultRelayPort( this ); // Log.d( TAG, "skipNativeSend(score=%d)) => %b", m_nativeFailScore, skip );
String host = XWPrefs.getDefaultRelayHost( this ); return skip;
try { }
m_UDPSocket = new DatagramSocket();
m_UDPSocket.setSoTimeout(30 * 1000); // timeout so we can log
InetAddress addr = InetAddress.getByName( host ); // So it's a map. The timer iterates over the whole map, which should
m_UDPSocket.connect( addr, port ); // remember this address // never be *that* big, and pulls everything older than 10 seconds. If
Log.d( TAG, "connectSocket(%s:%d): m_UDPSocket now %H", // anything in that list isn't an ACK (since ACKs will always be there
host, port, m_UDPSocket ); // because they're not ACK'd) then the whole thing gets resent.
} catch( java.net.SocketException se ) {
Log.ex( TAG, se ); private void noteSent( PacketData packet, boolean fromUDP )
Assert.fail(); {
} catch( java.net.UnknownHostException uhe ) { Log.d( TAG, "Sent (fromUDP=%b) packet: cmd=%s, id=%d",
Log.ex( TAG, uhe ); fromUDP, packet.m_cmd.toString(), packet.m_packetID );
if ( fromUDP || packet.m_cmd != XWRelayReg.XWPDEV_ACK ) {
List<PacketData> list = fromUDP ? s_packetsSentUDP : s_packetsSentWeb;
synchronized( list ) {
list.add(packet );
} }
} else {
Assert.assertTrue( m_UDPSocket.isConnected() );
Log.i( TAG, "m_UDPSocket not null" );
} }
} }
private void startWriteThread() private void noteSent( List<PacketData> packets, boolean fromUDP )
{ {
if ( null == m_UDPWriteThread ) { long nowMS = System.currentTimeMillis();
m_UDPWriteThread = new Thread( null, new Runnable() { List<PacketData> map = fromUDP ? s_packetsSentUDP : s_packetsSentWeb;
public void run() { Log.d( TAG, "noteSent(fromUDP=%b): adding %d; size before: %d",
Log.i( TAG, "write thread starting" ); fromUDP, packets.size(), map.size() );
for ( ; ; ) { for ( PacketData packet : packets ) {
PacketData outData; if ( fromUDP ) {
try { packet.setSentMS( nowMS );
outData = m_queue.take(); }
} catch ( InterruptedException ie ) { noteSent( packet, fromUDP );
Log.w( TAG, "write thread killed" );
break;
}
if ( null == outData
|| 0 == outData.getLength() ) {
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" );
}
}
Log.i( TAG, "write thread exiting" );
}
}, getClass().getName() );
m_UDPWriteThread.start();
} else {
Log.i( TAG, "m_UDPWriteThread not null and assumed to "
+ "be running" );
} }
Log.d( TAG, "noteSent(fromUDP=%b): size after: %d", fromUDP, map.size() );
} }
private void stopUDPThreadsIf() private void stopUDPThreadsIf()
{ {
if ( null != m_UDPWriteThread ) { DbgUtils.assertOnUIThread();
// can't add null
m_queue.add( new PacketData() ); UDPThreads threads = m_UDPThreadsRef.getAndSet( null );
try { if ( null != threads ) {
Log.d( TAG, "joining m_UDPWriteThread" ); threads.stop();
m_UDPWriteThread.join();
Log.d( TAG, "SUCCESSFULLY joined m_UDPWriteThread" );
} catch( java.lang.InterruptedException ie ) {
Log.ex( TAG, ie );
}
m_UDPWriteThread = null;
m_queue.clear();
}
if ( null != m_UDPSocket && null != m_UDPReadThread ) {
m_UDPSocket.close();
try {
m_UDPReadThread.join();
} catch( java.lang.InterruptedException ie ) {
Log.ex( TAG, ie );
}
m_UDPReadThread = null;
m_UDPSocket = null;
} }
} }
// MIGHT BE Running on reader thread // 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; boolean resetBackoff = false;
ByteArrayInputStream bis = new ByteArrayInputStream( data ); ByteArrayInputStream bis = new ByteArrayInputStream( data );
@ -736,7 +666,7 @@ public class RelayService extends XWService
case XWPDEV_MSG: case XWPDEV_MSG:
int token = dis.readInt(); int token = dis.readInt();
byte[] msg = new byte[dis.available()]; byte[] msg = new byte[dis.available()];
dis.read( msg ); dis.readFully( msg );
postData( this, token, msg ); postData( this, token, msg );
// game-related packets only count // game-related packets only count
@ -756,9 +686,8 @@ public class RelayService extends XWService
resetBackoff = true; resetBackoff = true;
intent = getIntentTo( this, MsgCmds.GOT_INVITE ); intent = getIntentTo( this, MsgCmds.GOT_INVITE );
int srcDevID = dis.readInt(); int srcDevID = dis.readInt();
short len = dis.readShort(); byte[] nliData = new byte[dis.readShort()];
byte[] nliData = new byte[len]; dis.readFully( nliData );
dis.read( nliData );
NetLaunchInfo nli = XwJNI.nliFromStream( nliData ); NetLaunchInfo nli = XwJNI.nliFromStream( nliData );
intent.putExtra( INVITE_FROM, srcDevID ); intent.putExtra( INVITE_FROM, srcDevID );
String asStr = nli.toString(); String asStr = nli.toString();
@ -767,7 +696,7 @@ public class RelayService extends XWService
startService( intent ); startService( intent );
break; break;
case XWPDEV_ACK: case XWPDEV_ACK:
noteAck( vli2un( dis ) ); noteAck( vli2un( dis ), fromUDP );
break; break;
// case XWPDEV_MSGFWDOTHERS: // case XWPDEV_MSGFWDOTHERS:
// Assert.assertTrue( 0 == dis.readByte() ); // protocol; means "invite", I guess. // Assert.assertTrue( 0 == dis.readByte() ); // protocol; means "invite", I guess.
@ -786,7 +715,7 @@ public class RelayService extends XWService
if ( resetBackoff ) { if ( resetBackoff ) {
resetBackoffTimer(); resetBackoffTimer();
} }
} } // gotPacket()
private void gotPacket( DatagramPacket packet ) private void gotPacket( DatagramPacket packet )
{ {
@ -796,7 +725,7 @@ public class RelayService extends XWService
byte[] data = new byte[packetLen]; byte[] data = new byte[packetLen];
System.arraycopy( packet.getData(), 0, data, 0, packetLen ); System.arraycopy( packet.getData(), 0, data, 0, packetLen );
// DbgUtils.logf( "RelayService::gotPacket: %d bytes of data", packetLen ); // DbgUtils.logf( "RelayService::gotPacket: %d bytes of data", packetLen );
gotPacket( data, false ); gotPacket( data, false, true );
} // gotPacket } // gotPacket
private boolean shouldRegister() private boolean shouldRegister()
@ -872,13 +801,17 @@ public class RelayService extends XWService
private void requestMessagesImpl( XWRelayReg reg ) private void requestMessagesImpl( XWRelayReg reg )
{ {
ByteArrayOutputStream bas = new ByteArrayOutputStream();
try { try {
String devid = getDevID( null ); DevIDType[] typp = new DevIDType[1];
String devid = getDevID( typp );
if ( null != devid ) { if ( null != devid ) {
ByteArrayOutputStream bas = new ByteArrayOutputStream();
DataOutputStream out = new DataOutputStream( bas ); DataOutputStream out = new DataOutputStream( bas );
writeVLIString( out, devid ); writeVLIString( out, devid );
// Log.d( TAG, "requestMessagesImpl(): devid: %s; type: " + typp[0], devid );
postPacket( bas, reg ); postPacket( bas, reg );
} else {
Log.d(TAG, "requestMessagesImpl(): devid is null" );
} }
} catch ( java.io.IOException ioe ) { } catch ( java.io.IOException ioe ) {
Log.ex( TAG, ioe ); Log.ex( TAG, ioe );
@ -995,16 +928,19 @@ public class RelayService extends XWService
private String getVLIString( DataInputStream dis ) private String getVLIString( DataInputStream dis )
throws java.io.IOException throws java.io.IOException
{ {
int len = vli2un( dis ); byte[] tmp = new byte[vli2un( dis )];
byte[] tmp = new byte[len]; dis.readFully( tmp );
dis.read( tmp );
String result = new String( tmp ); String result = new String( tmp );
return result; return result;
} }
private void postPacket( ByteArrayOutputStream bas, XWRelayReg cmd ) private void postPacket( ByteArrayOutputStream bas, XWRelayReg cmd )
{ {
m_queue.add( new PacketData( bas, cmd ) ); startUDPThreadsIfNot();
UDPThreads threads = m_UDPThreadsRef.get();
if ( threads != null ) {
threads.add( new PacketData( bas, cmd ) );
}
// 0 ok; thread will often have sent already! // 0 ok; thread will often have sent already!
// DbgUtils.logf( "postPacket() done; %d in queue", m_queue.size() ); // DbgUtils.logf( "postPacket() done; %d in queue", m_queue.size() );
} }
@ -1068,6 +1004,277 @@ public class RelayService extends XWService
} }
} }
private class UDPThreads {
private DatagramSocket m_UDPSocket;
private LinkedBlockingQueue<PacketData> m_queue =
new LinkedBlockingQueue<PacketData>();
private Thread m_UDPReadThread;
private Thread m_UDPWriteThread;
UDPThreads() {}
void start()
{
m_UDPReadThread = new Thread( null, new Runnable() {
public void run() {
connectSocket(); // block until this is done
startWriteThread();
Log.i( TAG, "read thread running" );
byte[] buf = new byte[1024];
for ( ; ; ) {
DatagramPacket packet =
new DatagramPacket( buf, buf.length );
try {
m_UDPSocket.receive( packet );
resetExitTimer();
gotPacket( packet );
} catch ( java.io.InterruptedIOException iioe ) {
// DbgUtils.logf( "FYI: udp receive timeout" );
} catch( java.io.IOException ioe ) {
break;
}
}
Log.i( TAG, "read thread exiting" );
}
}, getClass().getName() );
m_UDPReadThread.start();
}
void stop()
{
m_queue.add( new EOQPacketData() ); // will kill the writer thread
}
void add( PacketData packet )
{
m_queue.add( packet );
}
private void connectSocket()
{
if ( null == m_UDPSocket ) {
int port = XWPrefs.getDefaultRelayPort( RelayService.this );
String host = XWPrefs.getDefaultRelayHost( RelayService.this );
try {
m_UDPSocket = new DatagramSocket();
m_UDPSocket.setSoTimeout(30 * 1000); // timeout so we can log
InetAddress addr = InetAddress.getByName( host );
m_UDPSocket.connect( addr, port ); // remember this address
Log.d( TAG, "connectSocket(%s:%d): m_UDPSocket now %H",
host, port, m_UDPSocket );
} catch( java.net.SocketException se ) {
Log.ex( TAG, se );
Assert.fail();
} catch( java.net.UnknownHostException uhe ) {
Log.ex( TAG, uhe );
}
} else {
Assert.assertTrue( m_UDPSocket.isConnected() );
Log.i( TAG, "m_UDPSocket not null" );
}
}
private void startWriteThread()
{
Assert.assertNull( m_UDPWriteThread );
m_UDPWriteThread = new Thread( null, new Runnable() {
public void run() {
Log.i( TAG, "write thread starting" );
for ( boolean gotEOQ = false; !gotEOQ; ) {
List<PacketData> dataListUDP = new ArrayList<>();
List<PacketData> dataListWeb = new ArrayList<>();
PacketData outData;
try {
long ts = s_packetsSentUDP.size() > 0 ? 10 : 3600;
Log.d( TAG, "blocking %d sec on poll()", ts );
for ( outData = m_queue.poll(ts, TimeUnit.SECONDS);
null != outData;
outData = m_queue.poll() ) { // doesn't block
if ( outData instanceof EOQPacketData ) {
gotEOQ = true;
break;
} else if ( skipNativeSend() || outData.getForWeb() ) {
dataListWeb.add (outData );
} else {
dataListUDP.add( outData );
}
}
} catch ( InterruptedException ie ) {
Log.w( TAG, "write thread killed" );
break;
}
sendViaWeb( dataListWeb );
sendViaUDP( dataListUDP );
resetExitTimer();
runUDPAckTimer();
ConnStatusHandler.showSuccessOut();
}
Log.i( TAG, "write thread killing read thread" );
// now kill the read thread
m_UDPSocket.close();
try {
m_UDPReadThread.join();
} catch( java.lang.InterruptedException ie ) {
Log.ex( TAG, ie );
}
Log.i( TAG, "write thread exiting" );
}
}, getClass().getName() );
m_UDPWriteThread.start();
}
private int sendViaWeb( List<PacketData> packets )
{
Log.d( TAG, "sendViaWeb(): sending %d at once", packets.size() );
int sentLen = 0;
if ( packets.size() > 0 ) {
HttpURLConnection conn = NetUtils.makeHttpRelayConn( RelayService.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 instanceof EOQPacketData );
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 );
boolean succeeded = null != result;
if ( succeeded ) {
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, false ); // 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" );
}
ConnStatusHandler.updateStatus( RelayService.this, null,
CommsConnType.COMMS_CONN_RELAY,
succeeded );
} catch ( JSONException ex ) {
Assert.assertFalse( BuildConfig.DEBUG );
}
}
}
return sentLen;
}
private int sendViaUDP( List<PacketData> packets )
{
int sentLen = 0;
if ( packets.size() > 0 ) {
noteSent( packets, true );
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();
// packet.setSentMS( nowMS );
getOut = false;
} catch ( java.net.SocketException se ) {
Log.ex( TAG, se );
Log.i( TAG, "Restarting threads to force new socket" );
ConnStatusHandler.updateStatusOut( RelayService.this, null,
CommsConnType.COMMS_CONN_RELAY,
true );
m_handler.post( new Runnable() {
public void run() {
stopUDPThreadsIf();
}
} );
break;
} catch ( java.io.IOException ioe ) {
Log.ex( TAG, ioe );
} catch ( NullPointerException npe ) {
Log.w( TAG, "network problem; dropping packet" );
}
if ( getOut ) {
break;
}
}
ConnStatusHandler.updateStatus( RelayService.this, null,
CommsConnType.COMMS_CONN_RELAY,
sentLen > 0 );
}
return sentLen;
}
private long m_lastRunMS = 0;
private void runUDPAckTimer()
{
long nowMS = System.currentTimeMillis();
if ( m_lastRunMS + 3000 > nowMS ) { // never more frequently than 3 sec.
// Log.d( TAG, "runUDPAckTimer(): too soon, so skipping" );
} else {
m_lastRunMS = nowMS;
long minSentMS = nowMS - 10000; // 10 seconds ago
long prevSentMS = 0;
List<PacketData> forResend = new ArrayList<>();
boolean foundNonAck = false;
synchronized ( s_packetsSentUDP ) {
Iterator<PacketData> iter;
for ( iter = s_packetsSentUDP.iterator(); iter.hasNext(); ) {
PacketData packet = iter.next();
long sentMS = packet.getSentMS();
Assert.assertTrue( prevSentMS <= sentMS );
prevSentMS = sentMS;
if ( sentMS > minSentMS ) {
break;
}
forResend.add( packet );
if ( packet.m_cmd != XWRelayReg.XWPDEV_ACK ) {
foundNonAck = true;
++m_nativeFailScore;
}
iter.remove();
}
Log.d( TAG, "runUDPAckTimer(): %d too-new packets remaining",
s_packetsSentUDP.size() );
}
if ( foundNonAck ) {
Log.d( TAG, "runUDPAckTimer(): reposting %d packets", forResend.size() );
m_queue.addAll( forResend );
}
}
}
}
private static class AsyncSender extends AsyncTask<Void, Void, Void> { private static class AsyncSender extends AsyncTask<Void, Void, Void> {
private Context m_context; private Context m_context;
private HashMap<String,ArrayList<byte[]>> m_msgHash; private HashMap<String,ArrayList<byte[]>> m_msgHash;
@ -1129,6 +1336,7 @@ public class RelayService extends XWService
} }
// Now open a real socket, write size and proto, and // Now open a real socket, write size and proto, and
// copy in the formatted buffer // copy in the formatted buffer
Socket socket = NetUtils.makeProxySocket( m_context, 8000 ); Socket socket = NetUtils.makeProxySocket( m_context, 8000 );
if ( null != socket ) { if ( null != socket ) {
DataOutputStream outStream = DataOutputStream outStream =
@ -1205,24 +1413,52 @@ public class RelayService extends XWService
{ {
int nextPacketID = 0; int nextPacketID = 0;
if ( XWRelayReg.XWPDEV_ACK != cmd ) { if ( XWRelayReg.XWPDEV_ACK != cmd ) {
synchronized( s_packetsSent ) { nextPacketID = s_nextPacketID.incrementAndGet();
nextPacketID = ++s_nextPacketID;
}
} }
return nextPacketID; return nextPacketID;
} }
private static void noteAck( int packetID ) private void noteAck( int packetID, boolean fromUDP )
{ {
synchronized( s_packetsSent ) { Assert.assertTrue( packetID != 0 );
if ( s_packetsSent.contains( packetID ) ) { List<PacketData> map = fromUDP ? s_packetsSentUDP : s_packetsSentWeb;
s_packetsSent.remove( packetID ); synchronized( map ) {
PacketData packet = null;
Iterator<PacketData> iter = map.iterator();
for ( iter = map.iterator(); iter.hasNext(); ) {
PacketData next = iter.next();
if ( next.m_packetID == packetID ) {
packet = next;
iter.remove();
break;
}
}
if ( packet != null ) {
// Log.d( TAG, "noteAck(fromUDP=%b): removed for id %d: %s",
// fromUDP, packetID, packet );
if ( fromUDP ) {
--m_nativeFailScore;
}
} else { } else {
Log.w( TAG, "Weird: got ack %d but never sent", packetID ); Log.w( TAG, "Weird: got ack %d but never sent", packetID );
} }
Log.d( TAG, "noteAck(): Got ack for %d; there are %d unacked packets", if ( BuildConfig.DEBUG ) {
packetID, s_packetsSent.size() ); ArrayList<String> pstrs = new ArrayList<>();
for ( PacketData datum : map ) {
pstrs.add( String.format("%d", datum.m_packetID ) );
}
Log.d( TAG, "noteAck(fromUDP=%b): Got ack for %d; there are %d unacked packets: %s",
fromUDP, packetID, map.size(), TextUtils.join( ",", pstrs ) );
}
} }
// If we get an ACK, things are working, even if it's not found above
// (which would be the case for an ACK sent via web, which we don't
// save.)
ConnStatusHandler.updateStatus( this, null,
CommsConnType.COMMS_CONN_RELAY,
true );
} }
// Called from any thread // Called from any thread
@ -1247,7 +1483,7 @@ public class RelayService extends XWService
registerWithRelay(); registerWithRelay();
} else { } else {
stopUDPThreadsIf(); stopUDPThreadsIf();
startFetchThreadIf(); startFetchThreadIfNotUDP();
} }
} }
@ -1334,7 +1570,7 @@ public class RelayService extends XWService
result = figureBackoffSeconds(); result = figureBackoffSeconds();
} }
Log.d( TAG, "getMaxIntervalSeconds() => %d", result ); Log.d( TAG, "getMaxIntervalSeconds() => %d", result ); // WFT? went from 40 to 1000
return result; return result;
} }
@ -1370,7 +1606,8 @@ public class RelayService extends XWService
private boolean shouldMaintainConnection() private boolean shouldMaintainConnection()
{ {
boolean result = relayEnabled( this ) boolean result = relayEnabled( this )
&& (XWApp.GCM_IGNORED || !s_gcmWorking); && (!s_gcmWorking || XWPrefs.getIgnoreGCM( this ));
if ( result ) { if ( result ) {
long interval = Utils.getCurSeconds() - m_lastGamePacketReceived; long interval = Utils.getCurSeconds() - m_lastGamePacketReceived;
result = interval < MAX_KEEPALIVE_SECS; result = interval < MAX_KEEPALIVE_SECS;
@ -1396,18 +1633,19 @@ public class RelayService extends XWService
long now = Utils.getCurSeconds(); long now = Utils.getCurSeconds();
if ( s_curNextTimer <= now ) { if ( s_curNextTimer <= now ) {
if ( 0 == s_curBackoff ) { 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; s_curNextTimer += s_curBackoff;
} }
diff = s_curNextTimer - now; diff = s_curNextTimer - now;
} }
Assert.assertTrue( diff < Integer.MAX_VALUE ); 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; return result;
} }
@ -1421,7 +1659,14 @@ public class RelayService extends XWService
} }
private class PacketData { 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 long m_sentUDP;
private PacketData() {}
public PacketData( ByteArrayOutputStream bas, XWRelayReg cmd ) public PacketData( ByteArrayOutputStream bas, XWRelayReg cmd )
{ {
@ -1429,6 +1674,17 @@ public class RelayService extends XWService
m_cmd = cmd; m_cmd = cmd;
} }
@Override
public String toString()
{
return String.format( "{cmd: %s; age: %d ms}", m_cmd,
System.currentTimeMillis() - m_created );
}
void setSentMS( long ms ) { m_sentUDP = ms; }
long getSentMS() { return m_sentUDP; }
boolean getForWeb() { return m_sentUDP != 0; }
public int getLength() public int getLength()
{ {
int result = 0; int result = 0;
@ -1441,13 +1697,13 @@ public class RelayService extends XWService
return result; return result;
} }
public DatagramPacket assemble() public byte[] assemble()
{ {
byte[] dest = new byte[getLength()]; byte[] data = new byte[getLength()];
System.arraycopy( m_header, 0, dest, 0, m_header.length ); System.arraycopy( m_header, 0, data, 0, m_header.length );
byte[] basData = m_bas.toByteArray(); byte[] basData = m_bas.toByteArray();
System.arraycopy( basData, 0, dest, m_header.length, basData.length ); System.arraycopy( basData, 0, data, m_header.length, basData.length );
return new DatagramPacket( dest, dest.length ); return data;
} }
private void makeHeader() private void makeHeader()
@ -1466,10 +1722,8 @@ public class RelayService extends XWService
Log.ex( TAG, ioe ); Log.ex( TAG, ioe );
} }
} }
public ByteArrayOutputStream m_bas;
public XWRelayReg m_cmd;
public byte[] m_header;
public int m_packetID;
} }
// Exits only to exist, so instanceof can distinguish
private class EOQPacketData extends PacketData {}
} }

View file

@ -522,7 +522,7 @@ public class SMSService extends XWService {
case DATA: case DATA:
int gameID = dis.readInt(); int gameID = dis.readInt();
byte[] rest = new byte[dis.available()]; byte[] rest = new byte[dis.available()];
dis.read( rest ); dis.readFully( rest );
if ( feedMessage( gameID, rest, new CommsAddrRec( phone ) ) ) { if ( feedMessage( gameID, rest, new CommsAddrRec( phone ) ) ) {
SMSResendReceiver.resetTimer( this ); SMSResendReceiver.resetTimer( this );
} }
@ -618,7 +618,7 @@ public class SMSService extends XWService {
} else { } else {
SMS_CMD cmd = SMS_CMD.values()[dis.readByte()]; SMS_CMD cmd = SMS_CMD.values()[dis.readByte()];
byte[] rest = new byte[dis.available()]; byte[] rest = new byte[dis.available()];
dis.read( rest ); dis.readFully( rest );
receive( cmd, rest, senderPhone ); receive( cmd, rest, senderPhone );
success = true; success = true;
} }

View file

@ -258,7 +258,8 @@ public class UpdateCheckReceiver extends BroadcastReceiver {
@Override protected String doInBackground( Void... unused ) @Override protected String doInBackground( Void... unused )
{ {
HttpURLConnection conn = NetUtils.makeHttpConn( m_context, "getUpdates" ); HttpURLConnection conn
= NetUtils.makeHttpUpdateConn( m_context, "getUpdates" );
String json = null; String json = null;
if ( null != conn ) { if ( null != conn ) {
json = NetUtils.runConn( conn, m_params ); json = NetUtils.runConn( conn, m_params );

View file

@ -443,7 +443,7 @@ public class Utils {
{ {
// Note: an int is big enough for *seconds* (not milliseconds) since 1970 // Note: an int is big enough for *seconds* (not milliseconds) since 1970
// until 2038 // until 2038
long millis = new Date().getTime(); long millis = System.currentTimeMillis();
int result = (int)(millis / 1000); int result = (int)(millis / 1000);
return result; return result;
} }

View file

@ -39,7 +39,6 @@ public class XWApp extends Application {
public static final boolean ATTACH_SUPPORTED = false; public static final boolean ATTACH_SUPPORTED = false;
public static final boolean LOG_LIFECYLE = false; public static final boolean LOG_LIFECYLE = false;
public static final boolean DEBUG_EXP_TIMERS = false; public static final boolean DEBUG_EXP_TIMERS = false;
public static final boolean GCM_IGNORED = false;
public static final boolean UDP_ENABLED = true; public static final boolean UDP_ENABLED = true;
public static final boolean SMS_INVITE_ENABLED = true; public static final boolean SMS_INVITE_ENABLED = true;
public static final boolean LOCUTILS_ENABLED = false; public static final boolean LOCUTILS_ENABLED = false;

View file

@ -67,6 +67,16 @@ public class XWPrefs {
return getPrefsBoolean( context, R.string.key_enable_nfc_toself, false ); return getPrefsBoolean( context, R.string.key_enable_nfc_toself, false );
} }
public static boolean getIgnoreGCM( Context context )
{
return getPrefsBoolean( context, R.string.key_ignore_gcm, false );
}
public static boolean getToastGCM( Context context )
{
return getPrefsBoolean( context, R.string.key_show_gcm, false );
}
public static boolean getRelayInviteToSelfEnabled( Context context ) public static boolean getRelayInviteToSelfEnabled( Context context )
{ {
return getPrefsBoolean( context, R.string.key_enable_relay_toself, false ); return getPrefsBoolean( context, R.string.key_enable_relay_toself, false );
@ -115,6 +125,16 @@ public class XWPrefs {
return getPrefsString( context, R.string.key_update_url ); 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 ) public static int getDefaultProxyPort( Context context )
{ {
String val = getPrefsString( context, R.string.key_proxy_port ); String val = getPrefsString( context, R.string.key_proxy_port );

View file

@ -82,7 +82,7 @@ class XWService extends Service {
s_seen.add( inviteID ); s_seen.add( inviteID );
} }
} }
Log.d( TAG, "checkNotDupe(%s) => %b", inviteID, !isDupe ); Log.d( TAG, "checkNotDupe('%s') => %b", inviteID, !isDupe );
return !isDupe; return !isDupe;
} }

View file

@ -266,15 +266,19 @@ public class JNIThread extends Thread {
} }
public boolean busy() public boolean busy()
{ // synchronize this!!! {
boolean result = false; boolean result = false;
// Docs: The returned iterator is a "weakly consistent" iterator that
// will never throw ConcurrentModificationException, and guarantees to
// traverse elements as they existed upon construction of the
// iterator, and may (but is not guaranteed to) reflect any
// modifications subsequent to construction.
Iterator<QueueElem> iter = m_queue.iterator(); Iterator<QueueElem> iter = m_queue.iterator();
while ( iter.hasNext() ) { while ( iter.hasNext() && !result ) {
if ( iter.next().m_isUIEvent ) { result = iter.next().m_isUIEvent;
result = true;
break;
}
} }
return result; return result;
} }

View file

@ -38,7 +38,6 @@ public interface TransportProcs {
, COMMS_RELAYSTATE_RECONNECTED , COMMS_RELAYSTATE_RECONNECTED
, COMMS_RELAYSTATE_ALLCONNECTED , COMMS_RELAYSTATE_ALLCONNECTED
}; };
void relayStatus( CommsRelayState newState );
void relayConnd( String room, int devOrder, boolean allHere, int nMissing ); void relayConnd( String room, int devOrder, boolean allHere, int nMissing );

View file

@ -64,7 +64,7 @@
<LinearLayout android:id="@+id/right_side" <LinearLayout android:id="@+id/right_side"
android:orientation="vertical" android:orientation="vertical"
android:layout_width="fill_parent" android:layout_width="fill_parent"
android:layout_height="wrap_content" android:layout_height="match_parent"
android:clickable="true" android:clickable="true"
android:longClickable="true" android:longClickable="true"
> >

View file

@ -6,6 +6,13 @@
android:title="@string/board_menu_invite" android:title="@string/board_menu_invite"
/> />
<item android:id="@+id/board_menu_archive"
android:title="@string/button_archive"
/>
<item android:id="@+id/board_menu_rematch"
android:title="@string/button_rematch"
/>
<group android:id="@+id/group_done"> <group android:id="@+id/group_done">
<!-- title set in BoardActivity --> <!-- title set in BoardActivity -->
<item android:id="@+id/board_menu_done" <item android:id="@+id/board_menu_done"
@ -78,21 +85,21 @@
</menu> </menu>
</item> </item>
<item android:id="@+id/games_menu_study" <item android:id="@+id/games_menu_study"
android:title="@string/gamel_menu_study" android:title="@string/gamel_menu_study"
/> />
<item android:id="@+id/gamel_menu_checkmoves" <item android:id="@+id/gamel_menu_checkmoves"
android:title="@string/gamel_menu_checkmoves" android:title="@string/gamel_menu_checkmoves"
/> />
<item android:id="@+id/board_menu_file_prefs" <item android:id="@+id/board_menu_file_prefs"
android:title="@string/menu_prefs" android:title="@string/menu_prefs"
android:alphabeticShortcut="P" android:alphabeticShortcut="P"
/> />
<item android:id="@+id/board_menu_game_netstats" <item android:id="@+id/board_menu_game_netstats"
android:title="@string/board_menu_game_netstats" /> android:title="@string/board_menu_game_netstats" />
<item android:id="@+id/board_menu_game_invites" <item android:id="@+id/board_menu_game_invites"
android:title="@string/board_menu_game_showInvites" /> android:title="@string/board_menu_game_showInvites" />
</menu> </menu>

View file

@ -5,9 +5,15 @@
<group android:id="@+id/group_done"> <group android:id="@+id/group_done">
<!-- title set in BoardActivity --> <!-- title set in BoardActivity -->
<item android:id="@+id/board_menu_archive"
android:title="@string/button_archive"
android:showAsAction="ifRoom"
android:icon="@drawable/archive__gen"
/>
<item android:id="@+id/board_menu_rematch" <item android:id="@+id/board_menu_rematch"
android:title="@string/button_rematch" android:title="@string/button_rematch"
android:showAsAction="ifRoom" android:showAsAction="ifRoom"
android:icon="@drawable/rematch__gen"
/> />
<item android:id="@+id/board_menu_done" <item android:id="@+id/board_menu_done"
android:alphabeticShortcut="D" android:alphabeticShortcut="D"
@ -19,13 +25,14 @@
android:title="@string/board_menu_trade" android:title="@string/board_menu_trade"
android:alphabeticShortcut="T" android:alphabeticShortcut="T"
android:showAsAction="ifRoom" android:showAsAction="ifRoom"
android:icon="@drawable/trade__gen"
/> />
</group> </group>
<item android:id="@+id/board_menu_trade_cancel" <item android:id="@+id/board_menu_trade_cancel"
android:title="@string/button_trade_cancel" android:title="@string/button_trade_cancel"
android:showAsAction="ifRoom" android:showAsAction="ifRoom"
android:icon="@drawable/back__gen" android:icon="@drawable/untrade__gen"
/> />
<item android:id="@+id/board_menu_trade_commit" <item android:id="@+id/board_menu_trade_commit"

View file

@ -36,7 +36,9 @@
<string name="key_relay_host">key_relay_host</string> <string name="key_relay_host">key_relay_host</string>
<string name="key_relay_port">key_relay_port2</string> <string name="key_relay_port">key_relay_port2</string>
<string name="key_relay_via_http_first">key_relay_via_http_first</string>
<string name="key_update_url">key_update_url</string> <string name="key_update_url">key_update_url</string>
<string name="key_relay_url">key_relay_url</string>
<string name="key_update_prerel">key_update_prerel</string> <string name="key_update_prerel">key_update_prerel</string>
<string name="key_proxy_port">key_proxy_port</string> <string name="key_proxy_port">key_proxy_port</string>
<string name="key_sms_port">key_sms_port</string> <string name="key_sms_port">key_sms_port</string>
@ -111,6 +113,7 @@
<string name="key_notagain_trading">key_notagain_trading</string> <string name="key_notagain_trading">key_notagain_trading</string>
<string name="key_notagain_hidenewgamebuttons">key_notagain_hidenewgamebuttons</string> <string name="key_notagain_hidenewgamebuttons">key_notagain_hidenewgamebuttons</string>
<string name="key_na_lookup">key_na_lookup</string> <string name="key_na_lookup">key_na_lookup</string>
<string name="key_na_archive">key_na_archive</string>
<string name="key_na_browse">key_na_browse</string> <string name="key_na_browse">key_na_browse</string>
<string name="key_na_browseall">key_na_browseall</string> <string name="key_na_browseall">key_na_browseall</string>
<string name="key_na_values">key_na_values</string> <string name="key_na_values">key_na_values</string>
@ -123,6 +126,8 @@
<string name="key_enable_nfc_toself">key_enable_nfc_toself</string> <string name="key_enable_nfc_toself">key_enable_nfc_toself</string>
<string name="key_enable_sms_toself">key_enable_sms_toself</string> <string name="key_enable_sms_toself">key_enable_sms_toself</string>
<string name="key_enable_relay_toself">key_enable_relay_toself</string> <string name="key_enable_relay_toself">key_enable_relay_toself</string>
<string name="key_ignore_gcm">key_ignore_gcm</string>
<string name="key_show_gcm">key_show_gcm</string>
<string name="key_nag_intervals">key_nag_intervals</string> <string name="key_nag_intervals">key_nag_intervals</string>
<string name="key_download_path">key_download_path</string> <string name="key_download_path">key_download_path</string>
<string name="key_got_langdict">key_got_langdict</string> <string name="key_got_langdict">key_got_langdict</string>
@ -149,6 +154,7 @@
<string name="dict_url">http://eehouse.org/and_wordlists</string> <string name="dict_url">http://eehouse.org/and_wordlists</string>
<string name="default_update_url">http://eehouse.org/xw4/info.py</string> <string name="default_update_url">http://eehouse.org/xw4/info.py</string>
<string name="default_relay_url">http://eehouse.org/xw4/relay.py</string>
<!--string name="dict_url">http://10.0.2.2/~eehouse/and_dicts</string--> <!--string name="dict_url">http://10.0.2.2/~eehouse/and_dicts</string-->

View file

@ -1692,7 +1692,7 @@
<string name="about_vers_fmt">CrossWords for Android, Version %1$s, <string name="about_vers_fmt">CrossWords for Android, Version %1$s,
rev %2$s, built on %3$s.</string> rev %2$s, built on %3$s.</string>
<!-- copyright info --> <!-- copyright info -->
<string name="about_copyright">Copyright (C) 1998-2017 by Eric <string name="about_copyright">Copyright (C) 1998-2018 by Eric
House. This free/open source software is released under the GNU Public House. This free/open source software is released under the GNU Public
License.</string> License.</string>
@ -1709,7 +1709,9 @@
<!-- Another paragraph giving credit for work done other than by <!-- Another paragraph giving credit for work done other than by
Eric House and translators --> Eric House and translators -->
<string name="about_credits">Toolbar icons by Sarah Chu.</string> <string name="about_credits">Toolbar icons by Sarah Chu. Navbar
icons from the Noun Project: \"archive\" by Trendy; \"rematch\" by
Becris; and \"swap\" by iconomania.</string>
<!-- text of dialog showing the set of changes made since the last <!-- text of dialog showing the set of changes made since the last
release --> release -->
@ -1743,6 +1745,14 @@
<string name="not_again_lookup">This button lets you look up, <string name="not_again_lookup">This button lets you look up,
online, the words just played.</string> online, the words just played.</string>
<string name="not_again_archive">Archiving uses a special group
called \"Archive\" to store finished games you want to keep. And,
since deleting an entire archive is easy, archiving is also a
great way to mark games for deletion if that\'s what you prefer
to do.\n\n(Deleting the Archive group is safe because it will be
created anew when needed.)
</string>
<!-- --> <!-- -->
<string name="button_move">Move</string> <string name="button_move">Move</string>
<string name="button_newgroup">New group</string> <string name="button_newgroup">New group</string>
@ -2158,6 +2168,10 @@
game with the same players and parameters as the one that game with the same players and parameters as the one that
just ended. --> just ended. -->
<string name="button_rematch">Rematch</string> <string name="button_rematch">Rematch</string>
<string name="button_archive">Archive\u200C</string>
<string name="group_name_archive">Archive</string>
<string name="duplicate_group_name_fmt">The group \"%1$s\" already exists.</string>
<string name="button_reconnect">Reconnect</string> <string name="button_reconnect">Reconnect</string>
@ -2474,6 +2488,8 @@
<string name="advanced">For debugging</string> <string name="advanced">For debugging</string>
<string name="advanced_summary">You should never need these...</string> <string name="advanced_summary">You should never need these...</string>
<string name="relay_host">Relay host</string> <string name="relay_host">Relay host</string>
<string name="relay_via_http_first">Use Web APIs first</string>
<string name="relay_via_http_first_summary">(instead of as fallback for custom protocol)</string>
<string name="dict_host">Wordlist download URL</string> <string name="dict_host">Wordlist download URL</string>
<string name="logging_on">Enable logging</string> <string name="logging_on">Enable logging</string>
<string name="logging_on_summary">(release builds only)</string> <string name="logging_on_summary">(release builds only)</string>
@ -2513,6 +2529,7 @@
<string name="game_summary_field_gameid">gameid</string> <string name="game_summary_field_gameid">gameid</string>
<string name="game_summary_field_npackets">Pending packet count</string> <string name="game_summary_field_npackets">Pending packet count</string>
<string name="expl_update_url">Update checks URL</string> <string name="expl_update_url">Update checks URL</string>
<string name="expl_relay_url">URL for relay web API</string>
<string name="got_langdict_title">Fetch default wordlist for language</string> <string name="got_langdict_title">Fetch default wordlist for language</string>
<string name="got_langdict_summary">Don\'t try a second time</string> <string name="got_langdict_summary">Don\'t try a second time</string>
@ -2584,6 +2601,12 @@
<string name="enable_relay_toself_title">Enable relay invites to self</string> <string name="enable_relay_toself_title">Enable relay invites to self</string>
<string name="enable_relay_toself_summary">(To aid testing and debugging)</string> <string name="enable_relay_toself_summary">(To aid testing and debugging)</string>
<string name="ignore_gcm_title">Ignore incoming GCM messages</string>
<string name="ignore_gcm_summary">Mimic life without a google account</string>
<string name="show_sms_title">Show SMS sends, receives</string>
<string name="show_gcm_title">Show GCM receives</string>
<!-- Shown after "resend messages" menuitem chosen --> <!-- Shown after "resend messages" menuitem chosen -->
<plurals name="resent_msgs_fmt"> <plurals name="resent_msgs_fmt">
<item quantity="one">One move sent</item> <item quantity="one">One move sent</item>

View file

@ -390,6 +390,55 @@
android:defaultValue="false" android:defaultValue="false"
/> />
<PreferenceScreen android:title="@string/pref_group_relay_title"
android:summary="@string/pref_group_relay_summary"
>
<CheckBoxPreference android:key="@string/key_enable_relay_toself"
android:title="@string/enable_relay_toself_title"
android:summary="@string/enable_relay_toself_summary"
android:defaultValue="false"
/>
<CheckBoxPreference android:key="@string/key_ignore_gcm"
android:title="@string/ignore_gcm_title"
android:summary="@string/ignore_gcm_summary"
android:defaultValue="false"
/>
<CheckBoxPreference android:key="@string/key_show_gcm"
android:title="@string/show_gcm_title"
android:defaultValue="false"
/>
<org.eehouse.android.xw4.XWEditTextPreference
android:key="@string/key_relay_host"
android:title="@string/relay_host"
android:defaultValue="@string/default_host"
/>
<CheckBoxPreference android:key="@string/key_relay_via_http_first"
android:title="@string/relay_via_http_first"
android:summary="@string/relay_via_http_first_summary"
android:defaultValue="false"
/>
<org.eehouse.android.xw4.XWEditTextPreference
android:key="@string/key_relay_url"
android:title="@string/expl_relay_url"
android:defaultValue="@string/default_relay_url"
/>
<org.eehouse.android.xw4.XWEditTextPreference
android:key="@string/key_relay_port"
android:title="@string/relay_port"
android:defaultValue="10997"
android:numeric="decimal"
/>
<org.eehouse.android.xw4.XWEditTextPreference
android:key="@string/key_proxy_port"
android:title="@string/proxy_port"
android:defaultValue="10998"
android:numeric="decimal"
/>
</PreferenceScreen>
<PreferenceScreen android:title="@string/pref_group_sms_title" <PreferenceScreen android:title="@string/pref_group_sms_title"
android:summary="@string/pref_group_sms_summary" android:summary="@string/pref_group_sms_summary"
> >
@ -407,34 +456,7 @@
/> />
<CheckBoxPreference android:key="@string/key_show_sms" <CheckBoxPreference android:key="@string/key_show_sms"
android:title="Show SMS sends, receives" android:title="@string/show_sms_title"
android:defaultValue="false"
/>
</PreferenceScreen>
<PreferenceScreen android:title="@string/pref_group_relay_title"
android:summary="@string/pref_group_relay_summary"
>
<org.eehouse.android.xw4.XWEditTextPreference
android:key="@string/key_relay_host"
android:title="@string/relay_host"
android:defaultValue="@string/default_host"
/>
<org.eehouse.android.xw4.XWEditTextPreference
android:key="@string/key_relay_port"
android:title="@string/relay_port"
android:defaultValue="10997"
android:numeric="decimal"
/>
<org.eehouse.android.xw4.XWEditTextPreference
android:key="@string/key_proxy_port"
android:title="@string/proxy_port"
android:defaultValue="10998"
android:numeric="decimal"
/>
<CheckBoxPreference android:key="@string/key_enable_relay_toself"
android:title="@string/enable_relay_toself_title"
android:summary="@string/enable_relay_toself_summary"
android:defaultValue="false" android:defaultValue="false"
/> />
</PreferenceScreen> </PreferenceScreen>

View file

@ -34,6 +34,8 @@ import junit.framework.Assert;
public class GCMIntentService extends GCMBaseIntentService { public class GCMIntentService extends GCMBaseIntentService {
private static final String TAG = GCMIntentService.class.getSimpleName(); private static final String TAG = GCMIntentService.class.getSimpleName();
private Boolean m_toastGCM;
public GCMIntentService() public GCMIntentService()
{ {
super( BuildConfig.GCM_SENDER_ID ); super( BuildConfig.GCM_SENDER_ID );
@ -67,14 +69,19 @@ public class GCMIntentService extends GCMBaseIntentService {
protected void onMessage( Context context, Intent intent ) protected void onMessage( Context context, Intent intent )
{ {
Log.d( TAG, "onMessage()" ); Log.d( TAG, "onMessage()" );
notifyRelayService( context, true );
String value; if ( null == m_toastGCM ) {
boolean ignoreIt = XWApp.GCM_IGNORED; m_toastGCM = new Boolean( XWPrefs.getToastGCM( context ) );
if ( ignoreIt ) { }
Log.d( TAG, "received GCM but ignoring it" );
if ( XWPrefs.getIgnoreGCM( context ) ) {
String logMsg = "received GCM but ignoring it";
Log.d( TAG, logMsg );
DbgUtils.showf( context, logMsg );
} else { } else {
value = intent.getStringExtra( "checkUpdates" ); notifyRelayService( context, true );
String value = intent.getStringExtra( "checkUpdates" );
if ( null != value && Boolean.parseBoolean( value ) ) { if ( null != value && Boolean.parseBoolean( value ) ) {
UpdateCheckReceiver.checkVersions( context, true ); UpdateCheckReceiver.checkVersions( context, true );
} }
@ -82,6 +89,9 @@ public class GCMIntentService extends GCMBaseIntentService {
value = intent.getStringExtra( "getMoves" ); value = intent.getStringExtra( "getMoves" );
if ( null != value && Boolean.parseBoolean( value ) ) { if ( null != value && Boolean.parseBoolean( value ) ) {
RelayService.timerFired( context ); RelayService.timerFired( context );
if ( m_toastGCM ) {
DbgUtils.showf( context, "onMessage(): got 'getMoves'" );
}
} }
value = intent.getStringExtra( "msgs64" ); value = intent.getStringExtra( "msgs64" );
@ -90,6 +100,11 @@ public class GCMIntentService extends GCMBaseIntentService {
try { try {
JSONArray msgs64 = new JSONArray( value ); JSONArray msgs64 = new JSONArray( value );
String[] strs64 = new String[msgs64.length()]; String[] strs64 = new String[msgs64.length()];
if ( m_toastGCM ) {
DbgUtils.showf( context, "onMessage(): got %d msgs",
strs64.length );
}
for ( int ii = 0; ii < strs64.length; ++ii ) { for ( int ii = 0; ii < strs64.length; ++ii ) {
strs64[ii] = msgs64.optString(ii); strs64[ii] = msgs64.optString(ii);
} }
@ -100,6 +115,7 @@ public class GCMIntentService extends GCMBaseIntentService {
} }
} catch (org.json.JSONException jse ) { } catch (org.json.JSONException jse ) {
Log.ex( TAG, jse ); Log.ex( TAG, jse );
Assert.assertFalse( BuildConfig.DEBUG );
} }
} }
@ -145,10 +161,8 @@ public class GCMIntentService extends GCMBaseIntentService {
private void notifyRelayService( Context context, boolean working ) private void notifyRelayService( Context context, boolean working )
{ {
if ( working && XWApp.GCM_IGNORED ) { if ( !XWPrefs.getIgnoreGCM( context ) ) {
working = false; RelayService.gcmConfirmed( context, working );
} }
RelayService.gcmConfirmed( context, working );
} }
} }

View file

@ -0,0 +1,32 @@
/* -*- compile-command: "find-and-gradle.sh insXw4Deb"; -*- */
/*
* Copyright 2010 - 2015 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.
*/
package org.eehouse.android.xw4;
import android.content.Context;
import com.google.android.gcm.GCMRegistrar;
class GCMStub {
public static String getRegistrationId( Context context )
{
return GCMRegistrar.getRegistrationId( context );
}
}

View file

@ -0,0 +1,29 @@
/* -*- compile-command: "find-and-gradle.sh insXw4dDeb"; -*- */
/*
* 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.
*/
package org.eehouse.android.xw4;
import android.content.Context;
class GCMStub {
public static String getRegistrationId( Context context )
{
return "";
}
}

View file

@ -0,0 +1 @@
strings.xml

View file

@ -0,0 +1,27 @@
/* -*- compile-command: "find-and-gradle.sh insXw4Deb"; -*- */
/*
* Copyright 2009 - 2012 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.
*/
package org.eehouse.android.xw4;
import android.content.Context;
public class CrashTrack {
public static void init( Context context ) {} // does nothing here
}

View file

@ -0,0 +1,37 @@
/* -*- compile-command: "find-and-gradle.sh -PuseCrashlytics insXw4dDeb"; -*- */
/*
* 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.
*/
package org.eehouse.android.xw4;
import android.app.Application;
/**
* The ancient GCMIntentService I copied from sample code seems to have
* trouble (burns battery using the WAKELOCK, specifically) when used with an
* app that doesn't have a registration ID. So let's not use that code.
*/
public class GCMIntentService {
private static final String TAG = GCMIntentService.class.getSimpleName();
public static void init( Application app )
{
Log.d( TAG, "doing nothing" );
}
}

View file

@ -0,0 +1 @@
../../../../../../xw4d/java/org/eehouse/android/xw4/GCMStub.java

View file

@ -0,0 +1,87 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
data-name="Layer 1"
viewBox="0 0 66 82"
x="0px"
y="0px"
id="svg3396"
version="1.1"
inkscape:version="0.91 r13725"
sodipodi:docname="archive.svg"
width="66"
height="82">
<metadata
id="metadata3416">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title>01</dc:title>
</cc:Work>
</rdf:RDF>
</metadata>
<defs
id="defs3414" />
<sodipodi:namedview
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1"
objecttolerance="10"
gridtolerance="10"
guidetolerance="10"
inkscape:pageopacity="0"
inkscape:pageshadow="2"
inkscape:window-width="1920"
inkscape:window-height="1163"
id="namedview3412"
showgrid="true"
showborder="false"
inkscape:zoom="6.616"
inkscape:cx="-17.332527"
inkscape:cy="66.335551"
inkscape:window-x="1920"
inkscape:window-y="0"
inkscape:window-maximized="1"
inkscape:current-layer="svg3396"
fit-margin-top="0"
fit-margin-left="0"
fit-margin-right="0"
fit-margin-bottom="0">
<inkscape:grid
type="xygrid"
id="grid3510"
originx="-17"
originy="-14" />
</sodipodi:namedview>
<title
id="title3398">01</title>
<path
d="M 66,22 0,22 0,82 66,82 Z M 60,76 6,76 6,28 60,28 Z"
id="path3400"
inkscape:connector-curvature="0" />
<polygon
points="64.3,58.3 64.3,46.7 64.3,46.7 58.3,46.7 58.3,52.3 41.7,52.3 41.7,46.7 35.7,46.7 35.7,58.3 "
id="polygon3402"
transform="translate(-17,-9)" />
<rect
x="8"
y="11"
width="50"
height="6"
id="rect3404" />
<rect
x="13"
y="0"
width="40"
height="6"
id="rect3406" />
</svg>

After

Width:  |  Height:  |  Size: 2.2 KiB

View file

@ -1,11 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="120"
height="120" xml:space="preserve">
<g
id="g12"
transform="matrix(1.25,0,0,-1.25,0,120)">
<g transform='translate(46.03,16.24)' id='g1584'>
<path style='fill:#000000;fill-opacity:1;fill-rule:nonzero;stroke:none' d='M 0,0 5.5,5.502 -16.87,27.87 35.7,27.87 35.7,35.65 -16.87,35.65 5.5,58.02 0,63.52 -31.76,31.76 0,0 z' id='path1586'/>
</g></g>
</svg>

Before

Width:  |  Height:  |  Size: 609 B

View file

@ -1,9 +1,49 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"> <svg
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="120" xmlns:dc="http://purl.org/dc/elements/1.1/"
height="120" xml:space="preserve"> xmlns:cc="http://creativecommons.org/ns#"
<g xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
version="1.1"
width="75.012497"
height="90.025002"
xml:space="preserve"
id="svg2"
inkscape:version="0.91 r13725"
sodipodi:docname="content_copy.svg"><metadata
id="metadata10"><rdf:RDF><cc:Work
rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" /><dc:title></dc:title></cc:Work></rdf:RDF></metadata><defs
id="defs8" /><sodipodi:namedview
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1"
objecttolerance="10"
gridtolerance="10"
guidetolerance="10"
inkscape:pageopacity="0"
inkscape:pageshadow="2"
inkscape:window-width="787"
inkscape:window-height="480"
id="namedview6"
showgrid="false"
fit-margin-top="0"
fit-margin-left="0"
fit-margin-right="0"
fit-margin-bottom="0"
inkscape:zoom="1.9666667"
inkscape:cx="37.5125"
inkscape:cy="45.0125"
inkscape:window-x="0"
inkscape:window-y="27"
inkscape:window-maximized="0"
inkscape:current-layer="svg2" /><g
id="g12" id="g12"
transform="matrix(1.25,0,0,-1.25,0,120)"> transform="matrix(1.25,0,0,-1.25,-22.4875,105.0125)"><path
<path style='fill:#000000;fill-opacity:1;fill-rule:evenodd;stroke:none' d='M 29.99,72.01 78,72.01 78,11.99 29.99,11.99 29.99,72.01 z M 72,66 36,66 36,17.99 72,17.99 72,66 z M 55.34,53 42,53 42,55 55.34,55 55.34,53 z M 66,47 41.99,47 41.99,49 66,49 66,47 z M 59.34,41 42,41 42,43 59.34,43 59.34,41 z M 55.34,35 42,35 42,37 55.34,37 55.34,35 z M 64.67,28.99 41.99,28.99 41.99,30.99 64.67,30.99 64.67,28.99 z M 60.01,78.01 23.99,78.01 23.99,29.99 26.99,29.99 26.99,23.99 23.99,23.99 17.99,23.99 17.99,84.01 66,84.01 66,78.01 66,75.01 60.01,75.01 60.01,78.01 z' id='path1220'/></g> style="fill:#000000;fill-opacity:1;fill-rule:evenodd;stroke:none"
</svg> d="m 29.99,72.01 48.01,0 0,-60.02 -48.01,0 0,60.02 z M 72,66 36,66 36,17.99 72,17.99 72,66 Z M 55.34,53 42,53 l 0,2 13.34,0 0,-2 z M 66,47 l -24.01,0 0,2 24.01,0 0,-2 z M 59.34,41 42,41 l 0,2 17.34,0 0,-2 z m -4,-6 -13.34,0 0,2 13.34,0 0,-2 z m 9.33,-6.01 -22.68,0 0,2 22.68,0 0,-2 z m -4.66,49.02 -36.02,0 0,-48.02 3,0 0,-6 -3,0 -6,0 0,60.02 48.01,0 0,-6 0,-3 -5.99,0 0,3 z"
id="path1220"
inkscape:connector-curvature="0" /></g></svg>

Before

Width:  |  Height:  |  Size: 933 B

After

Width:  |  Height:  |  Size: 2.1 KiB

View file

@ -1,18 +1,59 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"> <svg
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="120" xmlns:dc="http://purl.org/dc/elements/1.1/"
height="120" xml:space="preserve"> xmlns:cc="http://creativecommons.org/ns#"
<g xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
version="1.1"
width="59.5"
height="85.212502"
xml:space="preserve"
id="svg2"
inkscape:version="0.91 r13725"
sodipodi:docname="content_discard.svg"><metadata
id="metadata15"><rdf:RDF><cc:Work
rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" /><dc:title></dc:title></cc:Work></rdf:RDF></metadata><defs
id="defs13" /><sodipodi:namedview
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1"
objecttolerance="10"
gridtolerance="10"
guidetolerance="10"
inkscape:pageopacity="0"
inkscape:pageshadow="2"
inkscape:window-width="1594"
inkscape:window-height="887"
id="namedview11"
showgrid="false"
fit-margin-top="0"
fit-margin-left="0"
fit-margin-right="0"
fit-margin-bottom="0"
inkscape:zoom="1.9666667"
inkscape:cx="18.055085"
inkscape:cy="42.6"
inkscape:window-x="0"
inkscape:window-y="27"
inkscape:window-maximized="0"
inkscape:current-layer="svg2" /><g
id="g12" id="g12"
transform="matrix(1.25,0,0,-1.25,0,120)"> transform="matrix(1.25,0,0,-1.25,-30.25,102.6125)"><g
<g id='g1254'> id="g1254"><g
<g id='g1256'> id="g1256"><g
<g transform='translate(68.92,54.16)' id='g1262'> transform="translate(68.92,54.16)"
<path style='fill:#000000;fill-opacity:1;fill-rule:nonzero;stroke:none' d='M 0,0 C -0.046,0.281 -0.218,0.557 -0.476,0.82 -2.555,-1.292 -10.92,-1.642 -20.92,-1.642 -30.92,-1.642 -39.28,-1.292 -41.37,0.82 -41.62,0.557 -41.78,0.281 -41.84,0 L -41.86,0 -41.86,-0.152 C -41.86,-0.17 -41.86,-0.188 -41.86,-0.206 -41.86,-0.247 -41.85,-0.283 -41.85,-0.322 L -40.22,-33.68 -40.21,-33.68 C -40.07,-36.29 -37.07,-40.24 -20.92,-40.24 -4.766,-40.24 -1.77,-36.29 -1.629,-33.68 L -1.617,-33.68 0.013,-0.322 C 0.016,-0.283 0.028,-0.247 0.028,-0.206 0.028,-0.188 0.016,-0.17 0.016,-0.152 L 0.028,0 0,0 z' id='path1264'/> id="g1262"><path
</g> style="fill:#000000;fill-opacity:1;fill-rule:nonzero;stroke:none"
<g transform='translate(42.16,71.94)' id='g1266'> d="m 0,0 c -0.046,0.281 -0.218,0.557 -0.476,0.82 -2.079,-2.112 -10.444,-2.462 -20.444,-2.462 -10,0 -18.36,0.35 -20.45,2.462 C -41.62,0.557 -41.78,0.281 -41.84,0 l -0.02,0 0,-0.152 c 0,-0.018 0,-0.036 0,-0.054 0,-0.041 0.01,-0.077 0.01,-0.116 l 1.63,-33.358 0.01,0 c 0.14,-2.61 3.14,-6.56 19.29,-6.56 16.154,0 19.15,3.95 19.291,6.56 l 0.012,0 1.63,33.358 c 0.003,0.039 0.015,0.075 0.015,0.116 0,0.018 -0.012,0.036 -0.012,0.054 L 0.028,0 0,0 Z"
<path style='fill:#000000;fill-opacity:1;fill-rule:nonzero;stroke:none' d='M 0,0 -0.657,-0.041 -0.657,3.221 C -0.657,4.647 -0.458,5.815 -0.223,5.815 L 2.809,5.815 8.002,5.815 11.03,5.815 C 11.27,5.815 11.46,4.647 11.46,3.221 L 11.46,0.012 C 9.658,0.105 7.779,0.164 5.835,0.164 3.82,0.164 1.863,0.105 0,0 M 15.79,-0.326 15.79,7.549 C 15.79,8.971 14.63,10.15 13.19,10.15 L -2.392,10.15 C -3.813,10.15 -4.982,8.971 -4.982,7.549 L -4.982,-0.416 C -12.69,-1.29 -17.96,-3.06 -17.96,-5.11 L -17.96,-9.003 C -17.96,-9.802 -17.15,-10.56 -15.7,-11.25 -11.9,-13.03 -3.686,-14.27 5.835,-14.27 15.36,-14.27 23.58,-13.03 27.38,-11.25 28.82,-10.56 29.64,-9.802 29.64,-9.003 L 29.64,-5.11 C 29.64,-2.988 23.96,-1.161 15.79,-0.326' id='path1268'/> id="path1264"
</g> inkscape:connector-curvature="0" /></g><g
</g> transform="translate(42.16,71.94)"
</g></g> id="g1266"><path
</svg> style="fill:#000000;fill-opacity:1;fill-rule:nonzero;stroke:none"
d="m 0,0 -0.657,-0.041 0,3.262 c 0,1.426 0.199,2.594 0.434,2.594 l 3.032,0 5.193,0 3.028,0 c 0.24,0 0.43,-1.168 0.43,-2.594 l 0,-3.209 C 9.658,0.105 7.779,0.164 5.835,0.164 3.82,0.164 1.863,0.105 0,0 m 15.79,-0.326 0,7.875 c 0,1.422 -1.16,2.601 -2.6,2.601 l -15.582,0 c -1.421,0 -2.59,-1.179 -2.59,-2.601 l 0,-7.965 C -12.69,-1.29 -17.96,-3.06 -17.96,-5.11 l 0,-3.893 c 0,-0.799 0.81,-1.557 2.26,-2.247 3.8,-1.78 12.014,-3.02 21.535,-3.02 9.525,0 17.745,1.24 21.545,3.02 1.44,0.69 2.26,1.448 2.26,2.247 l 0,3.893 c 0,2.122 -5.68,3.949 -13.85,4.784"
id="path1268"
inkscape:connector-curvature="0" /></g></g></g></g></svg>

Before

Width:  |  Height:  |  Size: 1.8 KiB

After

Width:  |  Height:  |  Size: 3.1 KiB

View file

@ -0,0 +1,54 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
version="1.1"
x="0px"
y="0px"
viewBox="0 0 64 64"
xml:space="preserve"
id="svg3414"
inkscape:version="0.91 r13725"
sodipodi:docname="noun_945427_cc.svg"
width="64"
height="64"><metadata
id="metadata3432"><rdf:RDF><cc:Work
rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" /><dc:title></dc:title></cc:Work></rdf:RDF></metadata><defs
id="defs3430" /><sodipodi:namedview
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1"
objecttolerance="10"
gridtolerance="10"
guidetolerance="10"
inkscape:pageopacity="0"
inkscape:pageshadow="2"
inkscape:window-width="1920"
inkscape:window-height="1016"
id="namedview3428"
showgrid="false"
inkscape:zoom="10.3375"
inkscape:cx="17.006046"
inkscape:cy="32.261185"
inkscape:window-x="0"
inkscape:window-y="27"
inkscape:window-maximized="1"
inkscape:current-layer="svg3414" /><path
d="m 34.47353,49.135982 0.666,1.885 c 7.458,-2.635 13.285,-8.723 15.587,-16.285 l -1.914,-0.582 c -0.799,2.625 -2.061,5.057 -3.69,7.198 l -6.809,-0.851 -1.746,-2.037 -1.518,1.301 1.662,1.939 -1.818,7.27 c -0.14,0.052 -0.278,0.112 -0.42,0.162 z m 4.088,-6.588 5.015,0.627 c -1.82,1.939 -3.972,3.564 -6.367,4.782 l 1.352,-5.409 z"
id="path3416"
inkscape:connector-curvature="0" /><path
d="m 26.80653,50.444982 c -2.846,0 -5.57,-0.523 -8.088,-1.472 l -1.817,-7.27 5.364,-6.258 9.081,0 0.56,0.653 1.52,-1.301 -0.504,-0.588 2.719,-9.063 7.283,-3.642 6.47,1.617 c 0.266,1.402 0.412,2.846 0.412,4.324 0,0.644 -0.026,1.289 -0.078,1.917 l 1.992,0.166 c 0.058,-0.684 0.086,-1.385 0.086,-2.083 0,-13.785 -11.215,-25.0000002 -25,-25.0000002 -13.785,0 -25.0000003,11.2150002 -25.0000003,25.0000002 0,13.785 11.2150003,25 25.0000003,25 0.698,0 1.399,-0.028 2.083,-0.086 l -0.166,-1.992 c -0.628,0.052 -1.273,0.078 -1.917,0.078 z m 4.256,-17 -8.512,0 -2.586,-8.619 6.842,-5.131 6.842,5.131 -2.586,8.619 z m 13.074,-13.698 1.86,-4.96 c 1.24,1.874 2.212,3.939 2.864,6.141 l -4.724,-1.181 z m 0.444,-6.882 -2.573,6.861 -7.096,3.548 -7.105,-5.329 0,-8.9650002 5.424,-3.616 c 4.505,1.313 8.445,3.965 11.35,7.5010002 z m -14.039,-8.1120002 -3.735,2.49 -3.735,-2.49 c 1.216,-0.2 2.463,-0.308 3.735,-0.308 1.272,0 2.519,0.108 3.735,0.308 z m -10.159,0.611 5.424,3.616 0,8.9650002 -7.105,5.329 -7.096,-3.548 -2.5730003,-6.861 C 11.93753,9.3289818 15.87753,6.6769818 20.38253,5.3639818 Z M 9.4765297,19.746982 l -4.724,1.181 c 0.652,-2.202 1.623,-4.267 2.864,-6.141 l 1.86,4.96 z m -5.257,3.375 6.4700003,-1.617 7.283,3.642 2.719,9.064 -5.392,6.291 -6.7980003,0.85 c -2.943,-3.867 -4.695,-8.685 -4.695,-13.907 0,-1.478 0.146,-2.922 0.413,-4.323 z m 5.8270003,20.051 5.005,-0.625 1.35,5.399 c -2.388,-1.217 -4.536,-2.838 -6.355,-4.774 z"
id="path3418"
inkscape:connector-curvature="0" /><path
d="m 60.47553,40.046982 -1.832,0.801 c 0.771,1.768 1.163,3.652 1.163,5.597 0,7.72 -6.28,14 -14,14 -3.721,0 -7.176,-1.447 -9.775,-4 l 2.775,0 0,-2 -5,0 c -0.553,0 -1,0.447 -1,1 l 0,5 2,0 0,-2.41 c 2.951,2.815 6.828,4.41 11,4.41 8.822,0 16,-7.178 16,-16 0,-2.223 -0.447,-4.375 -1.331,-6.398 z"
id="path3420"
inkscape:connector-curvature="0" /><path
d="m 31.80653,46.444982 c 0,-7.72 6.28,-14 14,-14 3.721,0 7.176,1.447 9.775,4 l -2.775,0 0,2 5,0 c 0.553,0 1,-0.447 1,-1 l 0,-5 -2,0 0,2.41 c -2.951,-2.815 -6.828,-4.41 -11,-4.41 -8.822,0 -16,7.178 -16,16 0,2.222 0.447,4.373 1.328,6.394 l 1.834,-0.801 c -0.771,-1.766 -1.162,-3.648 -1.162,-5.593 z"
id="path3422"
inkscape:connector-curvature="0" /></svg>

After

Width:  |  Height:  |  Size: 4 KiB

View file

@ -0,0 +1,50 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
version="1.1"
x="0px"
y="0px"
viewBox="0 0 53.834 53.583248"
enable-background="new 0 0 100 100"
xml:space="preserve"
id="svg3590"
inkscape:version="0.91 r13725"
sodipodi:docname="trade.svg"
width="53.834"
height="53.583248"><metadata
id="metadata3602"><rdf:RDF><cc:Work
rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" /><dc:title></dc:title></cc:Work></rdf:RDF></metadata><defs
id="defs3600" /><sodipodi:namedview
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1"
objecttolerance="10"
gridtolerance="10"
guidetolerance="10"
inkscape:pageopacity="0"
inkscape:pageshadow="2"
inkscape:window-width="1920"
inkscape:window-height="1016"
id="namedview3598"
showgrid="false"
fit-margin-top="0"
fit-margin-left="0"
fit-margin-right="0"
fit-margin-bottom="0"
inkscape:zoom="5.3400704"
inkscape:cx="46.514458"
inkscape:cy="14.292"
inkscape:window-x="0"
inkscape:window-y="27"
inkscape:window-maximized="1"
inkscape:current-layer="svg3590" /><path
d="m 4.5,22.53725 0,-8.546 40.651,0 -5.652,5.652 c -0.879,0.879 -0.879,2.304 0,3.182 0.439,0.439 1.015,0.659 1.591,0.659 0.576,0 1.151,-0.22 1.591,-0.659 l 9.494,-9.493 c 0.014,-0.014 0.023,-0.031 0.037,-0.044 0.087,-0.092 0.17,-0.189 0.241,-0.295 0.024,-0.036 0.04,-0.077 0.063,-0.115 0.052,-0.088 0.104,-0.176 0.145,-0.271 0.02,-0.047 0.028,-0.097 0.045,-0.144 0.031,-0.091 0.064,-0.181 0.084,-0.276 0.029,-0.145 0.045,-0.294 0.045,-0.445 0,-0.151 -0.016,-0.3 -0.045,-0.445 -0.02,-0.097 -0.054,-0.187 -0.085,-0.278 -0.016,-0.047 -0.024,-0.096 -0.044,-0.142 -0.041,-0.1 -0.097,-0.192 -0.151,-0.284 -0.02,-0.034 -0.033,-0.07 -0.056,-0.103 -0.082,-0.123 -0.176,-0.237 -0.279,-0.34 l -9.491,-9.491 c -0.879,-0.879 -2.303,-0.879 -3.182,0 -0.879,0.878 -0.879,2.303 0,3.182 l 5.651,5.651 -42.903,0 c -1.242,0 -2.25,1.007 -2.25,2.25 l 0,10.796 c 0,1.243 1.008,2.25 2.25,2.25 1.242,0 2.25,-1.008 2.25,-2.251 z m 47.084,6.258 c -1.242,0 -2.25,1.007 -2.25,2.25 l 0,8.546 -40.651,0 5.652,-5.652 c 0.879,-0.879 0.879,-2.304 0,-3.182 -0.879,-0.879 -2.303,-0.878 -3.182,0 l -9.494,9.493 c -0.014,0.013 -0.022,0.03 -0.035,0.043 -0.088,0.092 -0.172,0.189 -0.243,0.296 -0.023,0.035 -0.038,0.074 -0.06,0.11 -0.054,0.09 -0.107,0.179 -0.147,0.276 -0.02,0.046 -0.028,0.095 -0.044,0.141 -0.031,0.092 -0.065,0.183 -0.085,0.279 -0.029,0.146 -0.045,0.294 -0.045,0.445 0,0.151 0.016,0.299 0.045,0.445 0.02,0.097 0.054,0.189 0.085,0.281 0.016,0.046 0.025,0.094 0.044,0.139 0.042,0.102 0.098,0.195 0.153,0.289 0.02,0.032 0.033,0.067 0.054,0.098 0.082,0.123 0.175,0.237 0.279,0.341 l 9.491,9.491 c 0.439,0.439 1.015,0.659 1.591,0.659 0.576,0 1.151,-0.22 1.591,-0.659 0.879,-0.878 0.879,-2.303 0,-3.182 l -5.651,-5.651 42.902,0 c 1.242,0 2.25,-1.007 2.25,-2.25 l 0,-10.796 c 0,-1.243 -1.008,-2.25 -2.25,-2.25 z"
id="path3592"
inkscape:connector-curvature="0" /></svg>

After

Width:  |  Height:  |  Size: 3.4 KiB

View file

@ -0,0 +1,59 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
version="1.1"
x="0px"
y="0px"
viewBox="0 0 70 70"
enable-background="new 0 0 100 100"
xml:space="preserve"
id="svg3590"
inkscape:version="0.91 r13725"
sodipodi:docname="untrade.svg"
width="70"
height="70"><metadata
id="metadata3602"><rdf:RDF><cc:Work
rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" /><dc:title></dc:title></cc:Work></rdf:RDF></metadata><defs
id="defs3600" /><sodipodi:namedview
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1"
objecttolerance="10"
gridtolerance="10"
guidetolerance="10"
inkscape:pageopacity="0"
inkscape:pageshadow="2"
inkscape:window-width="1920"
inkscape:window-height="1163"
id="namedview3598"
showgrid="false"
fit-margin-top="0"
fit-margin-left="0"
fit-margin-right="0"
fit-margin-bottom="0"
inkscape:zoom="5.3400704"
inkscape:cx="-12.866321"
inkscape:cy="22.385961"
inkscape:window-x="1920"
inkscape:window-y="0"
inkscape:window-maximized="1"
inkscape:current-layer="svg3590" /><path
d="m 12.159535,30.485512 0,-8.546 40.651,0 -5.652,5.652 c -0.879,0.879 -0.879,2.304 0,3.182 0.439,0.439 1.015,0.659 1.591,0.659 0.576,0 1.151,-0.22 1.591,-0.659 l 9.494,-9.493 c 0.014,-0.014 0.023,-0.031 0.037,-0.044 0.087,-0.092 0.17,-0.189 0.241,-0.295 0.024,-0.036 0.04,-0.077 0.063,-0.115 0.052,-0.088 0.104,-0.176 0.145,-0.271 0.02,-0.047 0.028,-0.097 0.045,-0.144 0.031,-0.091 0.064,-0.181 0.084,-0.276 0.029,-0.145 0.045,-0.294 0.045,-0.445 0,-0.151 -0.016,-0.3 -0.045,-0.445 -0.02,-0.097 -0.054,-0.187 -0.085,-0.278 -0.016,-0.047 -0.024,-0.096 -0.044,-0.142 -0.041,-0.1 -0.097,-0.192 -0.151,-0.284 -0.02,-0.034 -0.033,-0.07 -0.056,-0.103 -0.082,-0.123 -0.176,-0.237 -0.279,-0.34 l -9.491,-9.4909998 c -0.879,-0.879 -2.303,-0.879 -3.182,0 -0.879,0.878 -0.879,2.3029998 0,3.1819998 l 5.651,5.651 -42.9029996,0 c -1.242,0 -2.25,1.007 -2.25,2.25 l 0,10.796 c 0,1.243 1.008,2.25 2.25,2.25 1.2419996,0 2.2499996,-1.008 2.2499996,-2.251 z m 47.084,6.258 c -1.242,0 -2.25,1.007 -2.25,2.25 l 0,8.546 -40.651,0 5.652,-5.652 c 0.879,-0.879 0.879,-2.304 0,-3.182 -0.879,-0.879 -2.303,-0.878 -3.182,0 l -9.4939996,9.493 c -0.014,0.013 -0.022,0.03 -0.035,0.043 -0.088,0.092 -0.172,0.189 -0.243,0.296 -0.023,0.035 -0.038,0.074 -0.06,0.11 -0.054,0.09 -0.107,0.179 -0.147,0.276 -0.02,0.046 -0.028,0.095 -0.044,0.141 -0.031,0.092 -0.065,0.183 -0.085,0.279 -0.029,0.146 -0.045,0.294 -0.045,0.445 0,0.151 0.016,0.299 0.045,0.445 0.02,0.097 0.054,0.189 0.085,0.281 0.016,0.046 0.025,0.094 0.044,0.139 0.042,0.102 0.098,0.195 0.153,0.289 0.02,0.032 0.033,0.067 0.054,0.098 0.082,0.123 0.175,0.237 0.279,0.341 l 9.4909996,9.491 c 0.439,0.439 1.015,0.659 1.591,0.659 0.576,0 1.151,-0.22 1.591,-0.659 0.879,-0.878 0.879,-2.303 0,-3.182 l -5.651,-5.651 42.902,0 c 1.242,0 2.25,-1.007 2.25,-2.25 l 0,-10.796 c 0,-1.243 -1.008,-2.25 -2.25,-2.25 z"
id="path3592"
inkscape:connector-curvature="0" /><circle
style="fill:none;stroke:#000000;stroke-width:3.17759514;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
id="path4159"
cx="35"
cy="35"
r="33.411201" /><path
style="fill:none;fill-rule:evenodd;stroke:#000000;stroke-width:3.00030446;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
d="m 13.380987,12.086444 44.81032,48.282085"
id="path4171"
inkscape:connector-curvature="0" /></svg>

After

Width:  |  Height:  |  Size: 3.9 KiB

View file

@ -754,7 +754,7 @@ android_debugf( const char* format, ... )
} }
(void)__android_log_write( ANDROID_LOG_DEBUG, (void)__android_log_write( ANDROID_LOG_DEBUG,
# if defined VARIANT_xw4 # if defined VARIANT_xw4 || defined VARIANT_xw4fdroid
"xw4" "xw4"
# elif defined VARIANT_xw4d # elif defined VARIANT_xw4d
"x4bg" "x4bg"

View file

@ -95,19 +95,8 @@ and_xport_send( const XP_U8* buf, XP_U16 len, const XP_UCHAR* msgNo,
} }
static void static void
and_xport_relayStatus( void* closure, CommsRelayState newState ) and_xport_relayStatus( void* XP_UNUSED(closure), CommsRelayState XP_UNUSED(newState) )
{ {
AndTransportProcs* aprocs = (AndTransportProcs*)closure;
if ( NULL != aprocs->jxport ) {
JNIEnv* env = ENVFORME( aprocs->ti );
const char* sig = "(L" PKG_PATH("jni/TransportProcs$CommsRelayState") ";)V";
jmethodID mid = getMethodID( env, aprocs->jxport, "relayStatus", sig );
jobject jenum = intToJEnum( env, newState,
PKG_PATH("jni/TransportProcs$CommsRelayState") );
(*env)->CallVoidMethod( env, aprocs->jxport, mid, jenum );
deleteLocalRef( env, jenum );
}
} }
static void static void

View file

@ -648,7 +648,7 @@ Java_org_eehouse_android_xw4_jni_XwJNI_comms_1getUUID
{ {
jstring jstr = jstring jstr =
#ifdef XWFEATURE_BLUETOOTH #ifdef XWFEATURE_BLUETOOTH
# if defined VARIANT_xw4 # if defined VARIANT_xw4 || defined VARIANT_xw4fdroid
(*env)->NewStringUTF( env, XW_BT_UUID ) (*env)->NewStringUTF( env, XW_BT_UUID )
# elif defined VARIANT_xw4d # elif defined VARIANT_xw4d
(*env)->NewStringUTF( env, XW_BT_UUID_DBG ) (*env)->NewStringUTF( env, XW_BT_UUID_DBG )
@ -1673,8 +1673,6 @@ Java_org_eehouse_android_xw4_jni_XwJNI_game_1receiveMessage
{ {
jboolean result; jboolean result;
XWJNI_START_GLOBALS(); XWJNI_START_GLOBALS();
XP_ASSERT( state->game.comms );
XP_ASSERT( state->game.server );
XWStreamCtxt* stream = streamFromJStream( MPPARM(mpool) env, globals->vtMgr, XWStreamCtxt* stream = streamFromJStream( MPPARM(mpool) env, globals->vtMgr,
jstream ); jstream );
@ -1686,31 +1684,7 @@ Java_org_eehouse_android_xw4_jni_XwJNI_game_1receiveMessage
addrp = &addr; addrp = &addr;
} }
/* pthread_mutex_lock( &state->msgMutex ); */ result = game_receiveMessage( &state->game, stream, addrp );
ServerCtxt* server = state->game.server;
CommsMsgState commsState;
result = comms_checkIncomingStream( state->game.comms, stream, addrp,
&commsState );
if ( result ) {
(void)server_do( server );
result = server_receiveMessage( server, stream );
}
comms_msgProcessed( state->game.comms, &commsState, !result );
/* pthread_mutex_unlock( &state->msgMutex ); */
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 );
}
}
stream_destroy( stream ); stream_destroy( stream );

View file

@ -1,7 +1,5 @@
<?xml version='1.0' encoding='UTF-8'?> <?xml version='1.0' encoding='UTF-8'?>
<resources xmlns:tools="http://schemas.android.com/tools" <resources xmlns:tools="http://schemas.android.com/tools" tools:ignore="MissingTranslation">
tools:ignore="MissingTranslation"
>
<string name="button_new_game">Spiel hinzufügen</string> <string name="button_new_game">Spiel hinzufügen</string>
<string name="button_new_group">Gruppe hinzufügen</string> <string name="button_new_group">Gruppe hinzufügen</string>
@ -11,8 +9,8 @@
<string name="missing_player">(noch nicht hier …)</string> <string name="missing_player">(noch nicht hier …)</string>
<string name="summary_relay_conf_fmt">Konfiguriert für Raum „%1$s</string> <string name="summary_relay_conf_fmt">Konfiguriert für Raum „%1$s</string>
<string name="summary_relay_wait_fmt">Auf Spieler in Raum „%1$s warten</string> <string name="summary_relay_wait_fmt">Auf Spieler in Raum „%1$s warten</string>
<string name="summary_relay_conn_fmt">Spiel läuft in Raum „%1$s“</string> <string name="summary_relay_conn_fmt">Spiel läuft in Raum „%1$s“</string>
<string name="summary_relay_gameover_fmt">Spiel vorbei in Raum „%1$s“</string> <string name="summary_relay_gameover_fmt">Spiel vorbei in Raum „%1$s“</string>

View file

@ -2285,8 +2285,12 @@ seulement des informations sur les jetons. Il n\'y a pas de mots à parcourir.</
vous voulez intégrer dans cette partie. Utilisez le bouton \"%2$s\" si vous ne vous voulez intégrer dans cette partie. Utilisez le bouton \"%2$s\" si vous ne
voyez pas un périphérique que vous attendez.</string>--> voyez pas un périphérique que vous attendez.</string>-->
<plurals name="invite_bt_desc_fmt"> <plurals name="invite_bt_desc_fmt">
<item quantity="one">Sélectionnez l\'appareil que vous voulez intégrer dans cette partie. Utilisez le bouton \"%2$s\" si vous ne voyez pas un appareil que vous attendez.</item> <item quantity="one">Sélectionnez l\'appareil que vous voulez intégrer dans cette partie.
<item quantity="other">Sélectionnez jusqu\'à %1$d appareils que vous voulez intégrer dans cette partie. Utilisez le bouton \"%2$s\" si vous ne voyez pas un appareil que vous attendez.</item> \n
\nUtilisez le bouton \"%2$s\" si vous ne voyez pas un appareil que vous attendez.</item>
<item quantity="other">Sélectionnez jusqu\'à %1$d appareils que vous voulez intégrer dans cette partie.
\n
\nUtilisez le bouton \"%2$s\" si vous ne voyez pas un appareil que vous attendez.</item>
</plurals> </plurals>
<!-- --> <!-- -->
<!--<string name="bt_resend_fmt">Bluetooth send to %1$s failed; retry %3$d in <!--<string name="bt_resend_fmt">Bluetooth send to %1$s failed; retry %3$d in
@ -2354,7 +2358,7 @@ le bouton \"Importer le contact\" pour ajouter des gens que vous voulez
inviter, ou le bouton + pour saisir des numéros directement.</string> inviter, ou le bouton + pour saisir des numéros directement.</string>
<!-- --> <!-- -->
<!--<string name="get_sms_number">Enter phone number:</string>--> <!--<string name="get_sms_number">Enter phone number:</string>-->
<string name="get_sms_number">Saisir un numéro de téléphone :</string> <string name="get_sms_number">Numéro de téléphone d\'appareil :</string>
<!-- --> <!-- -->
<!--<string name="confirm_clear">Are you sure you want to delete the <!--<string name="confirm_clear">Are you sure you want to delete the
checked phone number[s]?</string>--> checked phone number[s]?</string>-->
@ -2498,9 +2502,9 @@ vous faire payer pour chaque message !\n\nLes parties par SMS devraient-elles
désactivées, donc aucun coup ne sera envoyé pour cette partie. (Si vous voulez désactivées, donc aucun coup ne sera envoyé pour cette partie. (Si vous voulez
activer les parties par SMS, allez dans Paramètres->Paramètres des parties en activer les parties par SMS, allez dans Paramètres->Paramètres des parties en
réseau.)</string>--> réseau.)</string>-->
<string name="warn_sms_disabled">Les parties par SMS sont actuellement <string name="warn_sms_disabled">Les parties par SMS sont actuellement désactivées. Aucun coup ne sera envoyé par SMS.
désactivées, donc aucun coup ne sera envoyé pour cette partie. (Si vous voulez \n
activer les parties par SMS, allez dans Paramètres-&gt;Paramètres des parties en réseau.)</string> \nVous pouvez activer les parties par SMS maintenant, ou plus tard.</string>
<!-- --> <!-- -->
<!--<string name="gamel_menu_checkupdates">Check for updates</string>--> <!--<string name="gamel_menu_checkupdates">Check for updates</string>-->
@ -2597,7 +2601,7 @@ mots ?</string>
<!--<string name="list_group_delete">Delete group</string>--> <!--<string name="list_group_delete">Delete group</string>-->
<string name="list_group_delete">Supprimer le groupe</string> <string name="list_group_delete">Supprimer le groupe</string>
<!--<string name="list_group_rename">Rename</string>--> <!--<string name="list_group_rename">Rename</string>-->
<string name="list_group_rename">Renommer</string> <string name="list_group_rename">Renommer le groupe</string>
<!--<string name="list_group_default">Put new games here</string>--> <!--<string name="list_group_default">Put new games here</string>-->
<string name="list_group_default">Mettre les nouvelles parties ici</string> <string name="list_group_default">Mettre les nouvelles parties ici</string>
<!--<string name="list_group_moveup">Move up</string>--> <!--<string name="list_group_moveup">Move up</string>-->
@ -2675,8 +2679,8 @@ effacées.)</string>-->
then act on selected games, e.g. to delete them, using the menu or then act on selected games, e.g. to delete them, using the menu or
\"Actionbar.\"</string>--> \"Actionbar.\"</string>-->
<string name="not_again_newselect">Toucher une partie l\'ouvre. <string name="not_again_newselect">Toucher une partie l\'ouvre.
\n
Vous pouvez aussi toucher les icônes sur la gauche pour sélectionner ou désélectionner des parties, par exemples pour les supprimer, en utilisant le menu ou la \"barre d\'action\".</string> \nVous pouvez aussi toucher les icônes sur la gauche pour sélectionner ou désélectionner des parties, par exemples pour les supprimer, en utilisant le menu ou la \"barre d\'action\".</string>
<!--<string name="not_again_backclears">The back button clears any <!--<string name="not_again_backclears">The back button clears any
selection instead of exiting. Hit it again to exit the selection instead of exiting. Hit it again to exit the
@ -2932,7 +2936,7 @@ autorisés.\n\nCochez la case \"Montrer les listes téléchargeables\" en haut
pour voir ce qui est disponible.</string> pour voir ce qui est disponible.</string>
<!--<string name="force_tablet_title">Force tablet layout</string>--> <!--<string name="force_tablet_title">Force tablet layout</string>-->
<string name="force_tablet_title">Forcer la mise en page de tablette</string> <string name="force_tablet_title">Utiliser la mise en page de tablette (côte à côte) ?</string>
<!--<string name="force_tablet_summary">Even if my screen is too small</string>--> <!--<string name="force_tablet_summary">Even if my screen is too small</string>-->
<string name="force_tablet_summary">Même si mon écran est trop petit</string> <string name="force_tablet_summary">Même si mon écran est trop petit</string>
@ -3046,10 +3050,9 @@ vous doit faire une mise à jour avant de pouvoir continuer.</string>
device. No moves will be sent via Bluetooth.\n\nYou can enable device. No moves will be sent via Bluetooth.\n\nYou can enable
Bluetooth now, or later. Bluetooth now, or later.
</string>--> </string>-->
<string name="warn_bt_disabled">"Le Bluetooth est actuellement éteint sur cet appareil. Aucun coup ne sera envoyé par Bluetooth. <string name="warn_bt_disabled">Le Bluetooth est actuellement éteint sur cet appareil. Aucun coup ne sera envoyé par Bluetooth.
\n
Vous pouvez activer le Bluetooth maintenant, ou plus tard. \nVous pouvez activer le Bluetooth maintenant, ou plus tard.</string>
"</string>
<!--XLATE-ME--> <!--XLATE-ME-->
<!--<string name="button_enable_sms">Enable SMS</string>--> <!--<string name="button_enable_sms">Enable SMS</string>-->
<string name="button_enable_sms">Activer les SMS</string> <string name="button_enable_sms">Activer les SMS</string>
@ -3131,11 +3134,9 @@ voir et rejoindre</string>
visible.\n\n(If you later want to unhide them go to the Appearance visible.\n\n(If you later want to unhide them go to the Appearance
section of App settings). section of App settings).
</string>--> </string>-->
<string name="not_again_hidenewgamebuttons">Ces deux boutons font la même <string name="not_again_hidenewgamebuttons">Les deux boutons au bas de cet écran et les deux premiers items dans sa barre d\'action (ou son menu) font la même chose. Si vous voulez, vous pouvez cacher les boutons pour rendre plus de parties visibles.
chose que les deux premiers items dans la barre d\'action de cette fenêtre (ou \n
du menu). Si vous voulez, vous pouvez cacher les boutons pour rendre plus de \n(Si vous voulez faire réapparaître les boutons, allez dans la section Apparence des paramètres de l\'application.)</string>
parties visibles.\n\n(Si vous voulez les faire réapparaître, allez dans la
section Apparence des paramètres de l\'application.)</string>
<!--XLATE-ME--> <!--XLATE-ME-->
<!--<string name="waiting_title">Waiting for players</string>--> <!--<string name="waiting_title">Waiting for players</string>-->
<string name="waiting_title">En attente de joueurs</string> <string name="waiting_title">En attente de joueurs</string>
@ -3472,11 +3473,11 @@ pour la langue</string>
<string name="fragment_button">Double panneau</string> <string name="fragment_button">Double panneau</string>
<string name="relay_behavior">Paramètres des parties par relai</string> <string name="relay_behavior">Paramètres des parties par relai</string>
<string name="relay_behavior_summary">"Paramètres des parties connectées à Internet "</string> <string name="relay_behavior_summary">Paramètres des parties connectées à Internet</string>
<string name="disable_relay">"Désactiver le jeu par relai "</string> <string name="disable_relay">Désactiver le jeu par relai</string>
<string name="disable_relay_summary">Désactiver toute communication par Internet</string> <string name="disable_relay_summary">Désactiver toute communication par Internet</string>
<string name="warn_relay_disabled">"Le jeu par relai est pour l\'instant désactivé sur cet appareil. Aucun coup ne sera envoyé ou reçu par le relai. "</string> <string name="warn_relay_disabled">Le jeu par relai est pour l\'instant désactivé sur cet appareil. Aucun coup ne sera envoyé ou reçu par le relai.</string>
<string name="warn_relay_later">Vous pouvez activer le jeu par relai maintenant, ou plus tard.</string> <string name="warn_relay_later">Vous pouvez activer le jeu par relai maintenant, ou plus tard.</string>
<string name="warn_relay_remove">Vous pouvez activer le jeu par relai maintenant, ou le retirer de cette partie.</string> <string name="warn_relay_remove">Vous pouvez activer le jeu par relai maintenant, ou le retirer de cette partie.</string>
@ -3516,7 +3517,7 @@ Merci de me faire savoir si vous aimez cette fonctionnalité, de signaler les pl
<string name="post_dualpane_title">Redémarrer CrossWords</string> <string name="post_dualpane_title">Redémarrer CrossWords</string>
<string name="dualpane_restart">Fermeture de l\'application…</string> <string name="dualpane_restart">Fermeture de l\'application…</string>
<string name="after_restart">Ce changement ne s\'appliquera pas tant que vous n\'aurez pas relancé Crosswords.</string> <string name="after_restart">Ce changement s\'appliquera après que vous ayez relancé CrossWords.</string>
<string name="invite_choice_p2p">Wifi Direct</string> <string name="invite_choice_p2p">Wifi Direct</string>
<string name="bt_pair_settings">En appairer d\'autres</string> <string name="bt_pair_settings">En appairer d\'autres</string>
@ -3527,28 +3528,23 @@ Merci de me faire savoir si vous aimez cette fonctionnalité, de signaler les pl
<item quantity="other">Sélectionnez les noms des appareils WiFi Direct que vous souhaitez inviter à votre nouvelle partie, puis touchez \"%2$s\".</item> <item quantity="other">Sélectionnez les noms des appareils WiFi Direct que vous souhaitez inviter à votre nouvelle partie, puis touchez \"%2$s\".</item>
</plurals> </plurals>
<string name="invite_p2p_desc_extra">Seuls les appareils actuellement disponibles sont affichés. Si un appareil à proximité ne s\'affiche pas, assurez-vous que le WiFi est allumé, que Crosswords est installé, et que le WiFi Direct est activé.</string> <string name="invite_p2p_desc_extra">Seuls les appareils actuellement disponibles sont affichés. Si un appareil à proximité ne s\'affiche pas, assurez-vous que le WiFi est allumé, que CrossWords est installé, et que le WiFi Direct est activé.</string>
<string name="empty_p2p_inviter">Il n\'y a actuellement pas d\'appareils accessibles par WiFi Direct qui ont Crosswords installé.</string> <string name="empty_p2p_inviter">Il n\'y a actuellement pas d\'appareils accessibles par WiFi Direct qui ont CrossWords installé.</string>
<string name="not_again_comms_p2p">Utiliser le WiFi Direct pour jouer contre un appareil faisant du WiFi Direct, sur lequel Crosswords est installé.</string> <string name="not_again_comms_p2p">Utiliser le WiFi Direct pour jouer contre un appareil faisant du WiFi Direct, sur lequel CrossWords est installé.</string>
<string name="missing_perms">Cette partie est configurée pour communiquer par SMS mais Crosswords n\'a pas la permission de le faire. Vous pouvez toujours ouvrir cette partie, mais elle pourrait ne pas être en capacité d\'envoyer ou recevoir des coups. <string name="missing_perms">Cette partie est configurée pour communiquer par SMS mais Crosswords n\'a pas la permission de le faire. Vous pouvez toujours ouvrir cette partie, mais elle pourrait ne pas être en capacité d\'envoyer ou recevoir des coups.
Vous pouvez la ré-ouvrir pour que la permission soit redemandée. Ou vous pouvez retirer le réglage de communication par SMS.</string> Vous pouvez la ré-ouvrir pour que la permission soit redemandée. Ou vous pouvez retirer le réglage de communication par SMS.</string>
<string name="download_rationale">"Crosswords a besoin d\'accéder à un stockage temporaire pour conserver ce que vous êtes sur le point de télécharger. <string name="download_rationale">Crosswords a besoin d\'accéder à un stockage temporaire pour conserver ce que vous êtes sur le point de télécharger.</string>
"</string>
<string name="sms_invite_rationale">" <string name="sms_invite_rationale">Crosswords a besoin de la permission d\'envoyer une invitation par SMS.</string>
Crosswords a besoin de la permission d\'envoyer une invitation par SMS.
"</string>
<string name="toast_no_permission">Permission non accordée</string> <string name="toast_no_permission">Permission non accordée</string>
<string name="contacts_rationale">" <string name="contacts_rationale">Crosswords veut accéder à vos contacts pour pouvoir mettre un nom sur les numéros de téléphones qui vous envoient des invitations par SMS. Vous pourrez toujours recevoir des invitations si vous n\'accordez pas cette permission, mais seul le numéro de téléphone de l\'expéditeur s\'affichera.</string>
Crosswords veut accéder à vos contacts pour pouvoir mettre un nom sur les numéros de téléphones qui vous envoient des invitations par SMS. Vous pourrez toujours recevoir des invitations si vous n\'accordez pas cette permission, mais seul le numéro de téléphone de l\'expéditeur s\'affichera.
"</string>
<string name="button_ask_again">Redemander</string> <string name="button_ask_again">Redemander</string>
<string name="contact_not_found">Pas dans les contacts</string> <string name="contact_not_found">Pas dans les contacts</string>
@ -3556,11 +3552,9 @@ Vous pouvez la ré-ouvrir pour que la permission soit redemandée. Ou vous pouve
<string name="button_skip">Sauter</string> <string name="button_skip">Sauter</string>
<string name="perms_rationale_title">Permissions Android</string> <string name="perms_rationale_title">Permissions Android</string>
<string name="phone_state_rationale">" <string name="phone_state_rationale">Certains téléphones peuvent échanger des SMS \"de données\". CrossWords souhaite vous offrir cette possibilité mais a d\'abord besoin d\'interroger votre appareil sur lui-même (pour savoir si c\'est un téléphone et, le cas échéant, de quel type).
Certains téléphones peuvent échanger des SMS \"de données\". Crosswords souhaite vous offrir cette possibilité mais a d\'abord besoin d\'interroger votre appareil sur lui-même (pour savoir si c\'est un téléphone et, le cas échéant, de quel type). \n
\nSi votre appareil ne peut pas envoyer de SMS de données (par ex. parce que ce n\'est pas un téléphone) ou que vous ne souhaiterez jamais jouer par SMS (par ex. parce que vous payez pour chaque message), il est raisonnable que refuser cette permission de manière permanente.</string>
Si votre appareil ne peut pas envoyer de SMS de données (par ex. parce que ce n\'est pas un téléphone) ou que vous ne souhaiterez jamais jouer par SMS (par ex. parce que vous payez pour chaque message), il est raisonnable que refuser cette permission de manière permanente.
"</string>
<string name="title_enable_p2p">Activer Wi-Fi Direct</string> <string name="title_enable_p2p">Activer Wi-Fi Direct</string>
<string name="summary_enable_p2p">Expérimental, utilise beaucoup de batterie</string> <string name="summary_enable_p2p">Expérimental, utilise beaucoup de batterie</string>
@ -3585,4 +3579,25 @@ Si votre appareil ne peut pas envoyer de SMS de données (par ex. parce que ce n
<item quantity="other">Êtes-vous sûr de vouloir effacer les %1$d enregistrements RelayID sélectionnés ?</item> <item quantity="other">Êtes-vous sûr de vouloir effacer les %1$d enregistrements RelayID sélectionnés ?</item>
</plurals> </plurals>
</resources> <string name="not_again_archive">L\'archivage utilise un groupe spécial nommé \"Archive\" pour enregistrer les parties finies que vous souhaitez garder. Et, vu qu\'effacer une archive entière est facile, l\'archivage est aussi une bonne manière de sélectionner des parties à effacer si c\'est ce que vous préférez faire.
\n
\n(Effacer le groupe Archive ne cause pas de problème car il sera recréé de zéro si nécessaire.)</string>
<string name="button_archive">Archive</string>
<string name="group_name_archive">Archive</string>
<string name="duplicate_group_name_fmt">Le groupe \"%1$s\" existe déjà.</string>
<string name="gamel_menu_writegit">Copier l\'info git dans le presse-papier</string>
<string name="dicts_storage_rationale">CrossWords peut enregistrer et accéder à des listes de mots dans l\'espace de Téléchargements de votre appareil mais a besoin de la permission d\'y accéder à cet endroit.
\n
\nVous pouvez refuser cette permission sans crainte si vous ne téléchargerez jamais de listes de mots excepté depuis CrossWords et n\'en avez jamais enregistrées là.</string>
<string name="phone_lookup_rationale">Pour rejouer une revanche qui utilise les SMS, CrossWords a besoin de la permission d\'accéder à votre numéro de téléphone.</string>
<string name="phone_lookup_rationale_drop">Jouer cette revanche par SMS uniquement n\'est pas possible sans cette permission.</string>
<string name="phone_lookup_rationale_others">Sans cette permission, la revanche continuera mais créera une partie qui ne peut pas être jouée par SMS.</string>
<string name="move_dict_rationale">Enregistrer une liste de mots dans l\'espace de Téléchargements nécessite la permission.</string>
</resources>

View file

@ -0,0 +1,645 @@
<?xml version='1.0' encoding='UTF-8'?>
<resources><string name="button_new_group">Legg til gruppe</string>
<string name="str_game_name_fmt">%1$s (%2$s)</string>
<string name="robot_name_fmt">%1$s (maskin)</string>
<string name="missing_player">(ikke her enda…)</string>
<string name="summary_relay_conf_fmt">Satt opp for rom \"%1$s\"</string>
<string name="summary_relay_wait_fmt">Venter for spiller i rom \"%1$s\"</string>
<string name="summary_relay_conn_fmt">Spill underveis i rom \"%1$s\"</string>
<string name="summary_relay_gameover_fmt">Spill over i rom \"%1$s\"</string>
<string name="summary_invites_out">Spillere invitert</string>
<string name="summary_invites_out_fmt">Spillere invitert til rom \"%1$s\"</string>
<string name="gameOver">Spill over</string>
<plurals name="moves_fmt">
<item quantity="one">%1$d trekk gjort</item>
<item quantity="other">%1$d trekk gjort</item>
</plurals>
<string name="button_delete">Slett</string>
<string name="button_reset">Tilbakestill</string>
<string name="gamel_menu_dicts">Ordlister…</string>
<string name="menu_prefs">Programinnstillinger</string>
<string name="gamel_menu_checkmoves">Sjekk trekk</string>
<string name="board_menu_file_about">Om</string>
<string name="list_item_config">Spillinnstillinger…</string>
<string name="list_item_rename">Gi nytt navn…</string>
<string name="list_item_move">Flytt til gruppe…</string>
<string name="list_item_delete">Slett spill</string>
<string name="list_item_reset">Tilbakestill</string>
<string name="list_item_new_from">Nytt fra</string>
<string name="list_item_copy">Kopier</string>
<string name="game_rename_title">Gi nytt navn</string>
<string name="rename_label">Endre navnet på dette spillet til:</string>
<string name="rename_label_caveat">Endre navnet på dette spillet (kun på denne enheten) til:</string>
<string name="title_dicts_list">CrossWords-ordlister</string>
<string name="download_more">Last ned flere…</string>
<string name="loc_builtin">Innebygd</string>
<string name="loc_downloads">Nedlastinger</string>
<string name="dicts_item_move">Endre nedlastingsplassering</string>
<string name="dicts_item_select">Gjør til forvalg</string>
<plurals name="confirm_delete_dict_fmt">
<item quantity="one">Er du sikker på at du vil slette ordlisten %1$s?</item>
<item quantity="other">Er du sikker på at du vil slette ordlistene %1$s?</item>
</plurals>
<string name="confirm_deleteonly_dicts_fmt">Sletting av %1$s vil bety at du står uten noen %2$s-ordlister. Ett eller flere spill vil ikke kunne åpnes (til du laster ned en erstatningsliste).</string>
<string name="button_default_human">Menneske</string>
<string name="button_default_robot">Maskin</string>
<string name="button_default_both">Begge</string>
<string name="move_dict_fmt">Plassering for ordlisten %1$s</string>
<string name="loc_internal">Intern</string>
<string name="loc_external">Ekstern</string>
<string name="title_game_config_fmt">Innstillinger for %1$s</string>
<string name="title_gamenet_config_fmt">%1$s-innstillinger (nettverksbasert)</string>
<string name="title_langs_list">Språk (basert på installerte ordlister)</string>
<string name="game_locked">Lås innstillinger</string>
<string name="players_label_standalone">Spillere (trykk for å redigere)</string>
<string name="button_add_player">Legg til spiller</string>
<string name="button_juggle_players">Veksle spillere</string>
<string name="lang_label">Spillspråk</string>
<string name="langdict_label">Spillspråk/ordliste</string>
<string name="connect_label_fmt">Tilkobling (via %1$s)</string>
<string name="join_room">Ta del i offentlig rom</string>
<string name="new_room_hint">Romnavn</string>
<string name="room_public">Gjør nytt navn offentlig</string>
<string name="room_public_prompt">Sett offentlig rom</string>
<string name="public_names_progress_fmt">Henter offentlige rom for %1$d-spillerrom i %2$s.</string>
<string name="no_name_found_fmt">Ingen offentlige rom funnet for %1$d-spillerrom i %2$s. Prøv å gjenoppfriske eller å opprette ditt eget.</string>
<string name="settings_label">Andre innstillinger</string>
<string name="hints_allowed">Tillat hint</string>
<string name="nethints_allowed">Tillat hint (nettverksbasert)</string>
<string name="use_timer">Skru på spillklokke</string>
<string name="minutes_label">Minutter per spiller</string>
<string name="robot_spinner_prompt">Hvor smart skal maskinen være?</string>
<string name="robot_smart">Smart maskin</string>
<string name="robot_smarter">Smartere maskin</string>
<string name="robot_smartest">Smartest maskin</string>
<string name="confirm_save_title">Bekreft lagring</string>
<string name="play">Ta del i spill</string>
<string name="player_label">Navn:</string>
<string name="dict_lang_label_fmt">Ordliste (i %1$s)</string>
<string name="dicts_list_prompt_fmt">Installerte ordlister (i %1$s)</string>
<string name="robot_label">Maskinspiller</string>
<string name="password_label">Passord</string>
<string name="button_trade_commit">Bekreft bytte</string>
<string name="button_trade_cancel">Avbryt bytte</string>
<string name="entering_trade">Trykk på flisene for å velge…</string>
<string name="no_moves_made">(Ingen trekk enda)</string>
<string name="invit_expl_bt_fmt">Invitasjon sendt via Blåtann til tilknyttet enhet \"%1$s\" på %2$s</string>
<string name="invit_expl_notarget_fmt">Invitasjon sendt via %1$s på %2$s. Ukjent mottaker.</string>
<string name="relay_alert">Tilkoblingsproblem</string>
<string name="msg_no_room">Ingen vert har registrert et rom ved det navnet.</string>
<string name="board_menu_game_history">Spillhistorikk…</string>
<string name="board_menu_game_resign">Kapituler</string>
<string name="board_menu_game_resend">Send trekk på nytt</string>
<string name="str_robot_moved_fmt">"Maskinen %1$s gjorde dette trekket: "</string>
<string name="player_edit_title">Rediger spiller</string>
<string name="badwords_accept">" Ønsker du fremdeles å godta dette trekket?"</string>
<string name="badwords_title">Illegale ord</string>
<string name="board_menu_done">Tur over</string>
<string name="board_menu_trade">Bytt</string>
<string name="board_menu_undo_last">Angre siste</string>
<string name="board_submenu_game">Spill →</string>
<string name="board_menu_game_left">Fliser som gjenstår…</string>
<plurals name="strd_robot_traded_fmt">
<item quantity="one">utvekslet ei flis.</item>
<item quantity="other">utvekslet %1$d flis.</item>
</plurals>
<string name="strs_new_tiles_fmt">Nye fliser: %1$s</string>
<string name="strss_traded_for_fmt">Utvekslet %1$s for %2$s.</string>
<string name="vs_join">" mot "</string>
<string name="str_bonus_all">Bonus for bruk av alle flis: 50
\n</string>
<string name="strd_turn_score_fmt">Poengsum for tur: %1$d
\n</string>
<plurals name="strd_remains_header_fmt">
<item quantity="one">%1$d flis igjen å trekke fra.</item>
<item quantity="other"></item>
</plurals>
<string name="button_revert_colors">Gjenopprett farger</string>
<string name="confirm_revert_all">Er du sikker på at du ønsker å tilbakestille alle innstillingene til forvalg?</string>
<string name="prefs_defaults">Nye spillforvalg</string>
<string name="prefs_defaults_summary">Forvalgte innstillinger for nye spill</string>
<string name="prefs_names">Spillernavn</string>
<string name="prefs_names_summary">Forvalgte spillernavn</string>
<string name="pref_player1_name">Første spiller</string>
<string name="pref_player2_name">Andre spiller</string>
<string name="pref_player3_name">Tredje spiller</string>
<string name="pref_player4_name">Fjerde spiller</string>
<string name="pref_human_name">Menneskelig spiller</string>
<string name="prefs_dicts">Ordlister</string>
<string name="prefs_dicts_summary">Forvalgte ordlister</string>
<string name="default_dict">Ordlister for mennesker</string>
<string name="default_robodict">Ordlister for roboter</string>
<string name="hints_allowed_sum">Skru på hint-funksjonen</string>
<string name="nethints_allowed_sum">Skru på hint for to-enhets -spill</string>
<string name="init_autojuggle">Sjongler spillere</string>
<string name="init_autojuggle_sum">Tilfeldig, for nye spill</string>
<string name="board_size">Brettstørrelse</string>
<string name="prefs_appearance">Utseende</string>
<string name="prefs_appearance_summary">Innstillinger for utseende</string>
<string name="summary_field">Inkluder i spilliste</string>
<string name="game_summary_field_empty">&lt;Ingenting&gt;</string>
<string name="game_summary_field_language">Spillspråk</string>
<string name="game_summary_field_opponents">Motstander[e]</string>
<string name="game_summary_field_state">Spilltilstand</string>
<string name="hide_title">Skjul tittellinje</string>
<string name="hide_title_summary">Å skjule spillnavn levner litt mer plass til utvidelse av brettet</string>
<string name="hide_newgames_title">Skjul knapp for nytt spill</string>
<string name="hide_newgames_summary">Å skjule knappen for nytt spill på hovedskjermen gjør spillet mer synlig</string>
<string name="show_arrow">Vis brettpil</string>
<string name="keep_screenon">Behold skjerm påslått</string>
<string name="keep_screenon_summary">Behold brettskjermen påslått i 10 minutter</string>
<string name="prefs_colors">Individuelle farger</string>
<string name="prefs_colors_summary">Rediger fargene brukt på brettet</string>
<string name="bonus_l2x">Dobbeltbokstav</string>
<string name="bonus_l3x">Trippelbokstav</string>
<string name="bonus_w2x">Dobbelord</string>
<string name="bonus_w3x">Trippelord</string>
<string name="tile_back">Flisbakgrunn</string>
<string name="empty">Tom celle/bakgrunn</string>
<string name="background">Brettbakgrunn</string>
<string name="red">Rød</string>
<string name="green">Grønn</string>
<string name="blue">Blå</string>
<string name="prefs_behavior">Oppførsel</string>
<string name="explain_robot">Forklar andre trekk</string>
<string name="title_sort_tiles">Sorter nye flis</string>
<string name="peek_other_summary">Å trykke noens navn på resultattavla viser vedkommendes flis</string>
<string name="network_behavior">Innstillinger for nettverksspill</string>
<string name="network_behavior_summary">Innstillinger som har innvirkning på nettverksspill</string>
<string name="connect_never">Aldri sjekk</string>
<string name="connect_five_mins">Hvert femte minutt</string>
<string name="connect_fifteen_mins">Hvert kvarter</string>
<string name="connect_thirty_mins">Hver halvtime</string>
<string name="connect_one_hour">Hver time</string>
<string name="connect_six_hours">Hver sjette time</string>
<string name="connect_daily">Én gang om dagen</string>
<string name="notify_sound">Spill lyd</string>
<string name="notify_vibrate">Vibrer</string>
<string name="notify_other_summary">Når motstanderens trekk mottas</string>
<string name="newgame_invite">Inviter nå</string>
<string name="newgame_invite_more">Mer info</string>
<string name="invite_choice_sms">SMS (tekstmelding)</string>
<string name="invite_choice_email">E-post</string>
<string name="invite_choice_bt">Blåtann</string>
<string name="invite_choice_title">Invitasjon av spillere: Hvordan?</string>
<string name="chat_local_id">"Meg: "</string>
<string name="chat_other_id">"Ikke meg: "</string>
<string name="chat_send">Send</string>
<string name="chat_hint">Skriv her…</string>
<string name="chat_menu_clear">Tøm historikk</string>
<string name="no_empty_rooms">Dette spillet kan ikke koble til uten et romnavn.</string>
<string name="str_tiles_not_in_line">Alle spilte flis må være i en linje.</string>
<string name="str_too_few_tiles_left_to_trade">For få flis igjen å bytte.</string>
<string name="downloading_dict_fmt">Laster ned %1$s…</string>
<string name="no_dict_title">Ordliste ikke funnet</string>
<string name="button_close_game">Lukk spill</string>
<string name="no_dict_fmt">Spillet \"%1$s\" krever en %2$s-ordliste. Last ned én før du åpner det.</string>
<string name="button_download">Last ned</string>
<string name="button_substdict">Erstatt</string>
<string name="msg_ask_password_fmt">Passord for \"%1$s\":</string>
<string name="game_fmt">Spill %1$d</string>
<string name="player_fmt">Spiller %1$d</string>
<string name="notify_chat_title_fmt">Sludremelding i spillet %1$s</string>
<string name="notify_chat_body_fmt">%1$s: %2$s</string>
<string name="button_yes">Ja</string>
<string name="button_no">Nei</string>
<string name="button_save">Lagre</string>
<string name="button_discard">Forkast</string>
<string name="button_retry">Prøv igjen</string>
<string name="tile_button_txt_fmt">%1$s (%2$d)</string>
<string name="tile_pick_summary_fmt">Nåværende valg: %1$s</string>
<string name="tiles_left_title">Gjenstående flis</string>
<string name="history_title">Spillhistorikk</string>
<string name="query_title">Et spørsmål…</string>
<string name="newbie_title">Her er et tips</string>
<string name="button_notagain">Ikke vis igjen</string>
<string name="not_again_undo">Denne knappen angrer eller gjentar nåværende trekk.</string>
<string name="default_name_title">Forvalgt spillernavn</string>
<string name="default_name_message">Skriv inn navnet ditt her. Det vil bli brukt til opprettelse av nye spill. (Du kan endre det senere i \"Nytt spillforvalg\"-delen i innstillingene.)</string>
<string name="changes_title">Nylige endringer</string>
<string name="changes_button">Nylige endringer</string>
<string name="button_lookup_fmt">Slå opp %1$s</string>
<string name="button_done">Ferdig</string>
<string name="button_done_fmt">Ferdig med %1$s</string>
<string name="pick_url_title_fmt">Slå opp %1$s i</string>
<string name="board_menu_pass">Hopp over</string>
<string name="not_again_lookup">Denne knappen lar deg slå opp ord som akkurat ble spilt på nett.</string>
<string name="button_move">Flytt</string>
<string name="button_newgroup">Ny gruppe</string>
<string name="button_search">Finn</string>
<string name="word_search_hint">Første bokstaver</string>
<string name="tilepick_all_fmt">Plukk %1$d for meg</string>
<string name="disableds_title">Avskrudde adressetyper</string>
<string name="dict_browse_title_fmt">%1$s (%2$d ord ved bruk av %3$d-%4$d flis)</string>
<string name="dict_browse_title1_fmt">%1$s (%2$d ord ved bruk av %3$d flis</string>
<string name="dict_browse_nowords_fmt">Ingen ord i %1$s starter med %2$s.</string>
<string name="not_again_browse">Denne knappen åpner ordlisteutforskeren i gjeldende spillers ordliste.</string>
<string name="not_again_browseall">Denne knappen åpner ordlisteutforskeren i ordlisten du ønsker.</string>
<string name="alert_empty_dict_fmt">Ordlisten %1$s inneholder kun flisinformasjon. Det er ingen ord å utforske.</string>
<string name="min_len">Min. lengde</string>
<string name="max_len">Maks. lengde</string>
<string name="prompt_min_len">Ord som ikke er kortere enn</string>
<string name="prompt_max_len">Ord som ikke er lengre enn</string>
<string name="email_author_chooser">Send kommentar via</string>
<string name="summary_wait_guest">Ikke tilkoblet</string>
<string name="summary_gameover">Spill over</string>
<string name="summary_conn">Spill underveis</string>
<string name="invite_notice_title">Nytt spill via invitasjon</string>
<string name="new_bt_body_fmt">En spiller på enheten %1$s ønsker å starte et spill</string>
<string name="new_relay_body">Trykk for å åpne et nytt spill</string>
<string name="bt_resend_fmt">"Blåtannsforsendelse til %1$s mislyktes; prøv %3$d i %2$d sekunder."</string>
<string name="bt_fail_fmt">Blåtannsforsendelser til %1$s har mislyktes for mange ganger. Åpne spillet igjen og prøv på nytt.</string>
<string name="bt_invite_title">Blåtannsinvitasjon</string>
<string name="sms_invite_title">SMS-invitasjon</string>
<string name="game_name_label">Nytt spillnavn:</string>
<string name="new_name_body_fmt">%1$s har invitert deg til å spille</string>
<string name="button_sms_add">Importer kontakt</string>
<string name="button_relay_add">Skann spill</string>
<string name="get_sms_number">Enhetens telefonnummer:</string>
<string name="get_sms_name">Kontaktnavn (valgfritt):</string>
<string name="get_relay_name">Enhetsnavn (valgfritt):</string>
<string name="get_relay_number">Skriv inn enhets-ID:</string>
<string name="summary_conn_sms_fmt">Spill underveis med %1$s</string>
<string name="menu_hint_prev">Forrige hint</string>
<string name="menu_hint_next">Neste hint</string>
<string name="board_menu_undo_current">Angre/gjenta</string>
<string name="menu_chat">Sludring</string>
<string name="chat_sender_fmt">%1$s:</string>
<string name="menu_toggle_values">Veksle verdier</string>
<string name="board_menu_dict">Utforsk ordliste</string>
<string name="connstat_lastsend_fmt">Siste forsendelse var %1$s (%2$s)</string>
<string name="connstat_lastreceipt_fmt">Siste kvittering var %1$s</string>
<string name="connstat_noreceipt">Ingen meldinger mottatt.</string>
<string name="connstat_sms">SMS/tekstmelding</string>
<string name="enable_sms">Tillat spill via SMS</string>
<string name="enable_sms_summary">Kun hvis du har ubegrensede tekstmeldinger!</string>
<string name="confirm_sms_title">Bekreft din SMS-plan</string>
<string name="button_enable_sms">Skru på SMS</string>
<string name="button_enable_bt">Skru på Blåtann</string>
<string name="button_later">Senere</string>
<string name="gamel_menu_checkupdates">Se etter oppdateringer</string>
<string name="checkupdates_none_found">Alt er oppdatert.</string>
<string name="new_dict_avail">Ny ordliste tilgjengelig</string>
<string name="new_dict_avail_fmt">Trykk for å oppgradere %1$s</string>
<string name="new_app_avail_fmt">Ny %1$s-versjon</string>
<string name="new_app_avail">Trykk for å laste ned og installere</string>
<string name="inform_dict_diffdict_fmt">Du bruker ordlista %1$s, men spillverten bruker %2$s. Ønsker du å bruke %3$s også?</string>
<string name="inform_dict_download">" (Du må i tilfelle late den ned først.)"</string>
<string name="missing_dict_title">Spillinvitasjon venter</string>
<string name="missing_dict_detail">Trykk for å laste ned manglende ordliste</string>
<string name="invite_dict_missing_title">Manglende ordliste</string>
<string name="button_decline">Avslå</string>
<string name="download_done">Nedlasting fullført</string>
<string name="download_failed">Nedlasting mislyktes</string>
<string name="default_loc">Lagre ordlister internt</string>
<string name="download_path_title">Nedlastingsmappe</string>
<string name="newgroup_label">Navnet på din nye gruppe:</string>
<string name="list_group_delete">Slett gruppe</string>
<string name="list_group_rename">Gi nytt navn</string>
<string name="list_group_default">Putt nye spill her</string>
<string name="list_group_moveup">Flytt oppover</string>
<string name="list_group_movedown">Flytt nedover</string>
<string name="group_cur_games">Mine spill</string>
<string name="group_new_games">Nye spill</string>
<string name="rename_group_label">Endre navnet på denne gruppen til:</string>
<string name="game_name_group_title">Navngi gruppe</string>
<string name="cannot_delete_default_group_fmt">Gruppen for nye spill, %1$s, kan ikke slettes.</string>
<plurals name="group_name_fmt">
<item quantity="one">%1$s (%2$d spill)</item>
<item quantity="other">%1$s (%2$d spill)</item>
</plurals>
<string name="button_reconnect">Koble til igjen</string>
<string name="change_group">Flytt valgte spill til:</string>
<string name="sel_games_fmt">Spill: %1$d</string>
<string name="sel_groups_fmt">Grupper: %1$d</string>
<string name="summary_thumbsize">Miniatyrbildestørrelse</string>
<string name="thumb_off">Avskrudd</string>
<string name="cur_menu_marker_fmt">%1$s (i bruk)</string>
<string name="add_to_study_fmt">Legg til %1$s i studieliste</string>
<string name="title_studyon">Skru på studielister</string>
<string name="summary_studyon">Tilby å legge til og vise lister over ord å huske</string>
<string name="gamel_menu_study">Studieliste…</string>
<string name="slmenu_copy_sel">Kopier til utklippstavle</string>
<string name="slmenu_clear_sel">Slett valgte</string>
<string name="add_done_fmt">%1$s lagt til i %2$s studieliste</string>
<string name="studylist_title_fmt">Studieliste for %1$s</string>
<string name="study_langpick">Dine ord for:</string>
<string name="lookup_title">Ordoppslag</string>
<string name="slmenu_select_all">Velg alle</string>
<string name="slmenu_deselect_all">Fravelt alle</string>
<string name="sel_items_fmt">Valgt: %1$d</string>
<string name="loc_menu_xlate">Oversett</string>
<string name="loc_lang_blessed">%1$s (offisiell)</string>
<string name="loc_lang_local">%1$s (din)</string>
<plurals name="new_xlations_fmt">
<item quantity="one">Installerte én ny oversettelse</item>
<item quantity="other">Installerte %1$d nye oversettelser</item>
</plurals>
<string name="xlations_enabled_title">Skru på lokal oversetting</string>
<string name="xlations_enabled_summary">Legg til valget i hver skjermmeny</string>
<string name="loc_filters_prompt">Filtrer etter:</string>
<string name="loc_search_prompt">Søk etter:</string>
<string name="loc_filters_all">Alle</string>
<string name="loc_filters_screen">Siste skjerm</string>
<string name="loc_filters_menu">Seneste meny</string>
<string name="loc_filters_modified">Endret av meg</string>
<string name="lang_name_english">Engelsk</string>
<string name="lang_name_french">Fransk</string>
<string name="lang_name_german">Tysk</string>
<string name="lang_name_turkish">Tyrkisk</string>
<string name="lang_name_arabic">Arabisk</string>
<string name="lang_name_spanish">Spansk</string>
<string name="lang_name_swedish">Svensk</string>
<string name="lang_name_polish">Polsk</string>
<string name="lang_name_danish">Dansk</string>
<string name="lang_name_italian">Italiensk</string>
<string name="lang_name_dutch">Hollandsk</string>
<string name="lang_name_catalan">Katalansk</string>
<string name="lang_name_portuguese">Portugisisk</string>
<string name="lang_name_russian">Russisk</string>
<string name="lang_name_czech">Tsjekkisk</string>
<string name="lang_name_greek">Gresk</string>
<string name="lang_name_slovak">Slovakisk</string>
<string name="loc_item_clear">Tøm</string>
<string name="loc_item_check">Sjekk</string>
<string name="remote_digesting">Behandler ordlisteinformasjon…</string>
<string name="delete_dicts">Slett ordliste[r]</string>
<string name="show_remote">Vis nedlastbare</string>
<string name="progress_title">Laster ned</string>
<string name="dict_on_server">Trykk for detaljer</string>
<plurals name="lang_name_fmt">
<item quantity="one">%1$s (%2$d ordlister)</item>
<item quantity="other">%1$s (%2$d ordlister)</item>
</plurals>
<string name="dict_desc_fmt">%1$s (%2$s/%3$d ord)</string>
<string name="lang_unknown">Ukjent</string>
<string name="force_tablet_title">Bruk brett- (side-ved-side) -oppsett?</string>
<string name="force_tablet_default">Bruk forvalg for min enhet</string>
<plurals name="nag_hours_fmt">
<item quantity="one">%1$d time</item>
<item quantity="other">%1$d timer</item>
</plurals>
<plurals name="nag_days_fmt">
<item quantity="one">%1$d dag</item>
<item quantity="other">%1$d dager</item>
</plurals>
<string name="nag_body_fmt">%1$s flyttet for mer enn %2$s siden.</string>
<string name="nag_warn_last_fmt">Siste advarsel: %1$s</string>
<string name="prev_player">Din motstander</string>
<plurals name="lmi_move_fmt">
<item quantity="one">%1$s spilte %2$s for %3$d poeng</item>
<item quantity="other"></item>
</plurals>
<string name="lmi_phony_fmt">%1$s mistet en tur</string>
<string name="default_language">Forvalgt språk</string>
<string name="title_addrs_pref">Kommuniser via</string>
<string name="new_game">Ny én-enhets -spill</string>
<string name="new_game_networked">Nytt nettverksbasert spill</string>
<string name="rematch_name_fmt">%1$s</string>
<string name="new_game_message_nodflt">Dette spillet må settes opp før det kan åpnes.</string>
<string name="use_defaults">Bruk forvalg</string>
<plurals name="nplayers_fmt">
<item quantity="one">Én spiller</item>
<item quantity="other">%1$d spillere</item>
</plurals>
<string name="network_advanced_title">Avansert</string>
<string name="network_advanced_summary">For erfarne spillere</string>
<string name="invite_multi_title">Inviter flere</string>
<string name="enable_pubroom_title">Skru på offentlige rom</string>
<string name="enable_pubroom_summary">Rom andre kan se og ta del i</string>
<string name="set_pref">Skjul knapper</string>
<string name="waiting_title">Venter på spillere</string>
<string name="waiting_invite_title">Venter på svar</string>
<string name="waiting_rematch_title">Omkamp underveis</string>
<string name="button_wait">Vent</string>
<string name="button_edit">Rediger</string>
<string name="button_discard_changes">Forkast endringer</string>
<string name="disable_nags_title">Turpåminnelser</string>
<string name="advanced">For feilretting</string>
<string name="advanced_summary">Du skal ikke trenge disse…</string>
<string name="logging_on">Skru på loggføring</string>
<string name="debug_features">Skru på feilrettingsfunksjoner</string>
<string name="board_menu_game_netstats">Nettverksstatistikk</string>
<string name="board_menu_game_showInvites">Vis invitasjoner</string>
<string name="name_dict_fmt">%1$s/%2$s</string>
<string name="gamel_menu_storedb">Skriv spill til SD-kort</string>
<string name="gamel_menu_loaddb">Last spill fra SD-kort</string>
<string name="enable_dupes_summary">Godta invitasjoner mer enn én gang</string>
<string name="enable_sms_toself_title">Kortslutt SMS til deg selv</string>
<string name="force_radio_title">Lat som du har radio</string>
<string name="radio_name_real">Ikke lat som</string>
<string name="radio_name_gsm">GSM</string>
<string name="radio_name_cdma">CDMA</string>
<string name="pref_group_sms_title">SMS-ting</string>
<string name="pref_group_l10n_title">Lokaliserings-ting</string>
<string name="checking_title">Sjekker</string>
<string name="checking_for_fmt">Ser etter ordlister i %1$s…</string>
<string name="button_enable">Skru på</string>
<string name="str_no_hint_found">Finner ingen trekk</string>
<plurals name="resent_msgs_fmt">
<item quantity="one">Ett trekk sendt</item>
<item quantity="other">%1$s trekk sendt</item>
</plurals>
<string name="clip_label">Invitasjonsnettadresse</string>
<string name="list_item_select">Velg</string>
<string name="list_item_deselect">Fravelg</string>
<string name="remove_sms">Fjern SMS</string>
<string name="button_ask_again">Spør igjen</string>
<string name="sms_send_failed">SMS-forsendelse mislyktes</string>
<string name="perms_rationale_title">Android-tilganger</string>
<string name="toast_no_permission">Tilgang ikke gitt</string>
</resources>

View file

@ -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

View file

@ -141,7 +141,7 @@ def writeDoc(doc, src, dest):
def checkOrConvertString(engNames, elem, verbose): def checkOrConvertString(engNames, elem, verbose):
name = elem.get('name') name = elem.get('name')
if not elem.text: if not elem.text:
print "elem", name, "is empty" print "ERROR: elem", name, "is empty"
sys.exit(1) sys.exit(1)
elif not name in engNames or elem.text.startswith(s_prefix): elif not name in engNames or elem.text.startswith(s_prefix):
ok = False ok = False

View file

@ -7,7 +7,10 @@ import mk_for_download, mygit
import xwconfig import xwconfig
# I'm not checking my key in... # I'm not checking my key in...
import mykey try :
import mykey
except:
print('unable to load mykey')
from stat import ST_CTIME from stat import ST_CTIME
try: try:

View file

@ -2128,13 +2128,13 @@ board_requestHint( BoardCtxt* board,
result = nTiles > 0; result = nTiles > 0;
} }
XP_Bool canMove = XP_FALSE;
if ( result ) { if ( result ) {
#ifdef XWFEATURE_SEARCHLIMIT #ifdef XWFEATURE_SEARCHLIMIT
BdHintLimits limits; BdHintLimits limits;
BdHintLimits* lp = NULL; BdHintLimits* lp = NULL;
#endif #endif
XP_Bool wasVisible; XP_Bool wasVisible;
XP_Bool canMove;
wasVisible = setArrowVisible( board, XP_FALSE ); wasVisible = setArrowVisible( board, XP_FALSE );
@ -2194,11 +2194,9 @@ board_requestHint( BoardCtxt* board,
} }
setArrowVisible( board, wasVisible ); setArrowVisible( board, wasVisible );
} }
} else {
util_userError( board->util, ERR_NO_HINT_FOUND );
} }
if ( !result ) { if ( !canMove ) {
util_userError( board->util, ERR_NO_HINT_FOUND ); util_userError( board->util, ERR_NO_HINT_FOUND );
} }
} }

View file

@ -274,6 +274,9 @@ CommsRelayState2Str( CommsRelayState state )
CASE_STR(COMMS_RELAYSTATE_CONNECTED); CASE_STR(COMMS_RELAYSTATE_CONNECTED);
CASE_STR(COMMS_RELAYSTATE_RECONNECTED); CASE_STR(COMMS_RELAYSTATE_RECONNECTED);
CASE_STR(COMMS_RELAYSTATE_ALLCONNECTED); CASE_STR(COMMS_RELAYSTATE_ALLCONNECTED);
#ifdef RELAY_VIA_HTTP
CASE_STR(COMMS_RELAYSTATE_USING_HTTP);
#endif
default: default:
XP_ASSERT(0); XP_ASSERT(0);
} }
@ -459,7 +462,10 @@ reset_internal( CommsCtxt* comms, XP_Bool isServer,
if ( 0 != comms->nextChannelNo ) { if ( 0 != comms->nextChannelNo ) {
XP_LOGF( "%s: comms->nextChannelNo: %d", __func__, 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; // comms->nextChannelNo = 0;
if ( resetRelay ) { if ( resetRelay ) {
comms->channelSeed = 0; comms->channelSeed = 0;
@ -1773,7 +1779,7 @@ relayPreProcess( CommsCtxt* comms, XWStreamCtxt* stream, XWHostID* senderID )
} }
if ( consumed ) { if ( consumed ) {
XP_LOGF( "%s: rejecting data message", __func__ ); XP_LOGF( "%s: rejecting data message (consumed)", __func__ );
} else { } else {
*senderID = srcID; *senderID = srcID;
} }
@ -2375,6 +2381,19 @@ comms_isConnected( const CommsCtxt* const comms )
return result; 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 #if defined COMMS_HEARTBEAT || defined XWFEATURE_COMMSACK
static void static void
sendEmptyMsg( CommsCtxt* comms, AddressRecord* rec ) sendEmptyMsg( CommsCtxt* comms, AddressRecord* rec )
@ -3097,14 +3116,34 @@ sendNoConn( CommsCtxt* comms, const MsgQueueElem* elem, XWHostID destID )
static XP_Bool static XP_Bool
relayConnect( CommsCtxt* comms ) relayConnect( CommsCtxt* comms )
{ {
XP_Bool success = XP_TRUE;
LOG_FUNC(); LOG_FUNC();
if ( addr_hasType( &comms->addr, COMMS_CONN_RELAY ) && !comms->rr.connecting ) { XP_Bool success = XP_TRUE;
comms->rr.connecting = XP_TRUE; if ( addr_hasType( &comms->addr, COMMS_CONN_RELAY ) ) {
success = send_via_relay( comms, comms->rr.connName[0]? if ( 0 ) {
XWRELAY_GAME_RECONNECT : XWRELAY_GAME_CONNECT, #ifdef RELAY_VIA_HTTP
comms->rr.myHostID, NULL, 0, NULL ); } else if ( comms->rr.connName[0] ) {
comms->rr.connecting = XP_FALSE; 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; return success;
} /* relayConnect */ } /* relayConnect */

View file

@ -32,7 +32,7 @@ EXTERN_C_START
#define CONN_ID_NONE 0L #define CONN_ID_NONE 0L
typedef XP_U32 MsgID; /* this is too big!!! PENDING */ typedef XP_U32 MsgID; /* this is too big!!! PENDING */
typedef XP_U8 XWHostID; typedef XP_U8 XWHostID;
typedef enum { typedef enum {
COMMS_CONN_NONE /* I want errors on uninited case */ COMMS_CONN_NONE /* I want errors on uninited case */
@ -56,6 +56,9 @@ typedef enum {
, COMMS_RELAYSTATE_CONNECTED , COMMS_RELAYSTATE_CONNECTED
, COMMS_RELAYSTATE_RECONNECTED , COMMS_RELAYSTATE_RECONNECTED
, COMMS_RELAYSTATE_ALLCONNECTED , COMMS_RELAYSTATE_ALLCONNECTED
#ifdef RELAY_VIA_HTTP
, COMMS_RELAYSTATE_USING_HTTP /* connection state doesn't matter */
#endif
} CommsRelayState; } CommsRelayState;
#ifdef XWFEATURE_BLUETOOTH #ifdef XWFEATURE_BLUETOOTH
@ -90,7 +93,7 @@ typedef struct _CommsAddrRec {
XP_U16 port_ip; XP_U16 port_ip;
} ip; } ip;
struct { struct {
XP_UCHAR invite[MAX_INVITE_LEN + 1]; XP_UCHAR invite[MAX_INVITE_LEN + 1]; /* room!!!! */
XP_UCHAR hostName[MAX_HOSTNAME_LEN + 1]; XP_UCHAR hostName[MAX_HOSTNAME_LEN + 1];
XP_U32 ipAddr; /* looked up from above */ XP_U32 ipAddr; /* looked up from above */
XP_U16 port; 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, typedef XP_Bool (*RelayNoConnProc)( const XP_U8* buf, XP_U16 len,
const XP_UCHAR* msgNo, const XP_UCHAR* msgNo,
const XP_UCHAR* relayID, void* closure ); 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 #endif
typedef enum { typedef enum {
@ -161,6 +170,9 @@ typedef struct _TransportProcs {
RelayConndProc rconnd; RelayConndProc rconnd;
RelayErrorProc rerror; RelayErrorProc rerror;
RelayNoConnProc sendNoConn; RelayNoConnProc sendNoConn;
# ifdef RELAY_VIA_HTTP
RelayRequestJoinProc requestJoin;
# endif
#endif #endif
void* closure; void* closure;
} TransportProcs; } TransportProcs;
@ -248,6 +260,10 @@ XP_Bool comms_checkComplete( const CommsAddrRec* const addr );
XP_Bool comms_canChat( const CommsCtxt* comms ); XP_Bool comms_canChat( const CommsCtxt* comms );
XP_Bool comms_isConnected( const CommsCtxt* const 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 ); CommsConnType addr_getType( const CommsAddrRec* addr );
void addr_setType( CommsAddrRec* addr, CommsConnType type ); void addr_setType( CommsAddrRec* addr, CommsConnType type );
void addr_addType( CommsAddrRec* addr, CommsConnType type ); void addr_addType( CommsAddrRec* addr, CommsConnType type );

View file

@ -528,7 +528,7 @@ engine_findMove( EngineCtxt* engine, const ModelCtxt* model,
newMove->nTiles = 0; newMove->nTiles = 0;
canMove = XP_FALSE; canMove = XP_FALSE;
} }
result = XP_TRUE; XP_ASSERT( result );
} }
util_engineStopping( engine->util ); util_engineStopping( engine->util );

View file

@ -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 void
game_getState( const XWGame* game, GameStateInfo* gsi ) game_getState( const XWGame* game, GameStateInfo* gsi )
{ {

View file

@ -82,6 +82,10 @@ void game_saveNewGame( MPFORMAL const CurGameInfo* gi, XW_UtilCtxt* util,
void game_saveToStream( const XWGame* game, const CurGameInfo* gi, void game_saveToStream( const XWGame* game, const CurGameInfo* gi,
XWStreamCtxt* stream, XP_U16 saveToken ); XWStreamCtxt* stream, XP_U16 saveToken );
void game_saveSucceeded( const XWGame* game, 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_dispose( XWGame* game );
void game_getState( const XWGame* game, GameStateInfo* gsi ); void game_getState( const XWGame* game, GameStateInfo* gsi );

View file

@ -61,6 +61,13 @@ nli_setDevID( NetLaunchInfo* nli, XP_U32 devID )
types_addType( &nli->_conTypes, COMMS_CONN_RELAY ); 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 void
nli_saveToStream( const NetLaunchInfo* nli, XWStreamCtxt* stream ) nli_saveToStream( const NetLaunchInfo* nli, XWStreamCtxt* stream )
{ {

View file

@ -76,6 +76,7 @@ void nli_saveToStream( const NetLaunchInfo* invit, XWStreamCtxt* stream );
void nli_makeAddrRec( const NetLaunchInfo* invit, CommsAddrRec* addr ); void nli_makeAddrRec( const NetLaunchInfo* invit, CommsAddrRec* addr );
void nli_setDevID( NetLaunchInfo* invit, XP_U32 devID ); void nli_setDevID( NetLaunchInfo* invit, XP_U32 devID );
void nli_setInviteID( NetLaunchInfo* invit, const XP_UCHAR* inviteID );
#endif #endif

127
xwords4/common/xwlist.c Normal file
View file

@ -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

44
xwords4/common/xwlist.h Normal file
View file

@ -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

View file

@ -130,6 +130,7 @@ DEFINES += -DCOMMS_XPORT_FLAGSPROC
DEFINES += -DINITIAL_CLIENT_VERS=3 DEFINES += -DINITIAL_CLIENT_VERS=3
DEFINES += -DCOMMON_LAYOUT DEFINES += -DCOMMON_LAYOUT
DEFINES += -DNATIVE_NLI DEFINES += -DNATIVE_NLI
# DEFINES += -DRELAY_VIA_HTTP
# MAX_ROWS controls STREAM_VERS_BIGBOARD and with it move hashing # MAX_ROWS controls STREAM_VERS_BIGBOARD and with it move hashing
DEFINES += -DMAX_ROWS=32 DEFINES += -DMAX_ROWS=32
@ -226,7 +227,7 @@ OBJ = \
$(BUILD_PLAT_DIR)/relaycon.o \ $(BUILD_PLAT_DIR)/relaycon.o \
$(CURSES_OBJS) $(GTK_OBJS) $(MAIN_OBJS) $(CURSES_OBJS) $(GTK_OBJS) $(MAIN_OBJS)
LIBS = -lm -luuid $(GPROFFLAG) LIBS = -lm -lpthread -luuid -lcurl -ljson-c $(GPROFFLAG)
ifdef USE_SQLITE ifdef USE_SQLITE
LIBS += -lsqlite3 LIBS += -lsqlite3
DEFINES += -DUSE_SQLITE DEFINES += -DUSE_SQLITE
@ -242,7 +243,7 @@ endif
ifneq (,$(findstring DPLATFORM_GTK,$(DEFINES))) ifneq (,$(findstring DPLATFORM_GTK,$(DEFINES)))
LIBS += `pkg-config --libs gtk+-3.0` LIBS += `pkg-config --libs gtk+-3.0`
CFLAGS += `pkg-config --cflags gtk+-3.0` CFLAGS += `pkg-config --cflags gtk+-3.0`
# CFLAGS += -DGDK_DISABLE_DEPRECATED CFLAGS += -DGDK_DISABLE_DEPRECATED
POINTER_SUPPORT = -DPOINTER_SUPPORT POINTER_SUPPORT = -DPOINTER_SUPPORT
endif endif
@ -264,6 +265,8 @@ REQUIRED_DEBS = gcc libgtk-3-dev \
libncursesw5-dev \ libncursesw5-dev \
uuid-dev \ uuid-dev \
libsqlite3-dev \ libsqlite3-dev \
libcurl4-openssl-dev \
libjson-c-dev \
.PHONY: debcheck debs_install .PHONY: debcheck debs_install

View file

@ -279,35 +279,57 @@ curses_util_userError( XW_UtilCtxt* uc, UtilErrID id )
} }
} /* curses_util_userError */ } /* 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 static void
curses_util_notifyMove( XW_UtilCtxt* uc, XWStreamCtxt* stream ) curses_util_notifyMove( XW_UtilCtxt* uc, XWStreamCtxt* stream )
{ {
CursesAppGlobals* globals = (CursesAppGlobals*)uc->closure; CursesAppGlobals* globals = (CursesAppGlobals*)uc->closure;
char* question; CommonGlobals* cGlobals = &globals->cGlobals;
const char* answers[3] = {NULL}; XP_U16 len = stream_getSize( stream );
short numAnswers = 0; XP_ASSERT( len <= VSIZE(cGlobals->question) );
XP_Bool freeMe = XP_FALSE; stream_getBytes( stream, cGlobals->question, len );
(void)g_idle_add( ask_move, globals );
question = strFromStream( stream );
freeMe = XP_TRUE;
answers[numAnswers++] = "Cancel";
answers[numAnswers++] = "Ok";
// result = okIndex ==
cursesask( globals, question, numAnswers, answers );
if ( freeMe ) {
free( question );
}
} /* curses_util_userQuery */ } /* 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 static void
curses_util_notifyTrade( XW_UtilCtxt* uc, const XP_UCHAR** tiles, XP_U16 nTiles ) curses_util_notifyTrade( XW_UtilCtxt* uc, const XP_UCHAR** tiles, XP_U16 nTiles )
{ {
CursesAppGlobals* globals = (CursesAppGlobals*)uc->closure; CursesAppGlobals* globals = (CursesAppGlobals*)uc->closure;
formatConfirmTrade( &globals->cGlobals, tiles, nTiles ); formatConfirmTrade( &globals->cGlobals, tiles, nTiles );
/* const char* buttons[] = { "Cancel", "Ok" }; */ (void)g_idle_add( ask_trade, globals );
/* cursesask( globals, question, VSIZE(buttons), buttons ); */
} }
static void static void
@ -1001,7 +1023,7 @@ curses_socket_added( void* closure, int newSock, GIOFunc func )
/* XP_ASSERT( !globals->cGlobals.relaySocket ); */ /* XP_ASSERT( !globals->cGlobals.relaySocket ); */
/* globals->cGlobals.relaySocket = newSock; */ /* globals->cGlobals.relaySocket = newSock; */
#endif #endif
} /* curses_socket_changed */ } /* curses_socket_added */
static void static void
curses_onGameSaved( void* closure, sqlite3_int64 rowid, 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 ); return storeNoConnMsg( &globals->cGlobals, msg, len, relayID );
} /* relay_sendNoConn_curses */ } /* 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 static void
relay_status_curses( void* closure, CommsRelayState state ) relay_status_curses( void* closure, CommsRelayState state )
{ {
@ -1659,6 +1702,7 @@ static void
cursesGotBuf( void* closure, const CommsAddrRec* addr, cursesGotBuf( void* closure, const CommsAddrRec* addr,
const XP_U8* buf, XP_U16 len ) const XP_U8* buf, XP_U16 len )
{ {
LOG_FUNC();
CursesAppGlobals* globals = (CursesAppGlobals*)closure; CursesAppGlobals* globals = (CursesAppGlobals*)closure;
XP_U32 clientToken; XP_U32 clientToken;
XP_ASSERT( sizeof(clientToken) < len ); 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", XP_LOGF( "%s: dropping packet; meant for a different device",
__func__ ); __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 static gint
@ -1913,6 +1970,10 @@ cursesmain( XP_Bool isServer, LaunchParams* params )
.rconnd = relay_connd_curses, .rconnd = relay_connd_curses,
.rerror = relay_error_curses, .rerror = relay_error_curses,
.sendNoConn = relay_sendNoConn_curses, .sendNoConn = relay_sendNoConn_curses,
#ifdef RELAY_VIA_HTTP
.requestJoin = relay_requestJoin_curses,
#endif
# ifdef COMMS_XPORT_FLAGSPROC # ifdef COMMS_XPORT_FLAGSPROC
.getFlags = curses_getFlags, .getFlags = curses_getFlags,
# endif # endif
@ -1949,6 +2010,7 @@ cursesmain( XP_Bool isServer, LaunchParams* params )
if ( params->useUdp ) { if ( params->useUdp ) {
RelayConnProcs procs = { RelayConnProcs procs = {
.msgReceived = cursesGotBuf, .msgReceived = cursesGotBuf,
.msgForRow = cursesGotForRow,
.msgNoticeReceived = cursesNoticeRcvd, .msgNoticeReceived = cursesNoticeRcvd,
.devIDReceived = cursesDevIDReceived, .devIDReceived = cursesDevIDReceived,
.msgErrorMsg = cursesErrorMsgRcvd, .msgErrorMsg = cursesErrorMsgRcvd,

View file

@ -71,6 +71,7 @@ struct CursesAppGlobals {
gchar* lastErr; gchar* lastErr;
XP_U16 nChatsSent; XP_U16 nChatsSent;
XP_U16 nextQueryTimeSecs;
union { union {
struct { struct {

View file

@ -52,6 +52,7 @@ openGamesDB( const char* dbName )
",inviteInfo BLOB" ",inviteInfo BLOB"
",room VARCHAR(32)" ",room VARCHAR(32)"
",connvia VARCHAR(32)" ",connvia VARCHAR(32)"
",relayid VARCHAR(32)"
",ended INT(1)" ",ended INT(1)"
",turn INT(2)" ",turn INT(2)"
",local INT(1)" ",local INT(1)"
@ -128,13 +129,14 @@ writeBlobColumnData( const XP_U8* data, gsize len, XP_U16 strVersion, sqlite3* p
assertPrintResult( pDb, result, SQLITE_OK ); assertPrintResult( pDb, result, SQLITE_OK );
result = sqlite3_blob_close( blob ); result = sqlite3_blob_close( blob );
assertPrintResult( pDb, result, SQLITE_OK ); assertPrintResult( pDb, result, SQLITE_OK );
if ( !!stmt ) { if ( !!stmt ) {
sqlite3_finalize( stmt ); sqlite3_finalize( stmt );
} }
LOG_RETURNF( "%lld", curRow ); LOG_RETURNF( "%lld", curRow );
return curRow; return curRow;
} } /* writeBlobColumnData */
static sqlite3_int64 static sqlite3_int64
writeBlobColumnStream( XWStreamCtxt* stream, sqlite3* pDb, sqlite3_int64 curRow, writeBlobColumnStream( XWStreamCtxt* stream, sqlite3* pDb, sqlite3_int64 curRow,
@ -199,11 +201,12 @@ addSnapshot( CommonGlobals* cGlobals )
void void
summarize( CommonGlobals* cGlobals ) summarize( CommonGlobals* cGlobals )
{ {
XP_S16 nMoves = model_getNMoves( cGlobals->game.model ); const XWGame* game = &cGlobals->game;
XP_Bool gameOver = server_getGameIsOver( cGlobals->game.server ); XP_S16 nMoves = model_getNMoves( game->model );
XP_Bool gameOver = server_getGameIsOver( game->server );
XP_Bool isLocal; XP_Bool isLocal;
XP_S16 turn = server_getCurrentTurn( cGlobals->game.server, &isLocal ); XP_S16 turn = server_getCurrentTurn( game->server, &isLocal );
XP_U32 lastMoveTime = server_getLastMoveTime( cGlobals->game.server ); XP_U32 lastMoveTime = server_getLastMoveTime( game->server );
XP_U16 seed = 0; XP_U16 seed = 0;
XP_S16 nMissing = 0; XP_S16 nMissing = 0;
XP_U16 nTotal = cGlobals->gi->nPlayers; XP_U16 nTotal = cGlobals->gi->nPlayers;
@ -214,10 +217,11 @@ summarize( CommonGlobals* cGlobals )
// gchar* connvia = "local"; // gchar* connvia = "local";
gchar connvia[128] = {0}; gchar connvia[128] = {0};
XP_UCHAR relayID[32] = {0};
if ( !!cGlobals->game.comms ) { if ( !!game->comms ) {
nMissing = server_getMissingPlayers( cGlobals->game.server ); nMissing = server_getMissingPlayers( game->server );
comms_getAddr( cGlobals->game.comms, &addr ); comms_getAddr( game->comms, &addr );
CommsConnType typ; CommsConnType typ;
for ( XP_U32 st = 0; addr_iter( &addr, &typ, &st ); ) { for ( XP_U32 st = 0; addr_iter( &addr, &typ, &st ); ) {
if ( !!connvia[0] ) { if ( !!connvia[0] ) {
@ -242,18 +246,21 @@ summarize( CommonGlobals* cGlobals )
break; 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 { } else {
strcat( connvia, "local" ); strcat( connvia, "local" );
} }
const char* fmt = "UPDATE games " const char* fmt = "UPDATE games "
" SET room='%s', ended=%d, turn=%d, local=%d, ntotal=%d, nmissing=%d, " " SET room='%s', ended=%d, turn=%d, local=%d, ntotal=%d, "
" nmoves=%d, seed=%d, gameid=%d, connvia='%s', lastMoveTime=%d" " nmissing=%d, nmoves=%d, seed=%d, gameid=%d, connvia='%s', "
" relayid='%s', lastMoveTime=%d"
" WHERE rowid=%lld"; " WHERE rowid=%lld";
XP_UCHAR buf[256]; XP_UCHAR buf[256];
snprintf( buf, sizeof(buf), fmt, room, gameOver?1:0, turn, isLocal?1:0, 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 ); cGlobals->selRow );
XP_LOGF( "query: %s", buf ); XP_LOGF( "query: %s", buf );
sqlite3_stmt* stmt = NULL; sqlite3_stmt* stmt = NULL;
@ -305,12 +312,46 @@ listGames( sqlite3* pDb )
return list; 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 XP_Bool
getGameInfo( sqlite3* pDb, sqlite3_int64 rowid, GameInfo* gib ) getGameInfo( sqlite3* pDb, sqlite3_int64 rowid, GameInfo* gib )
{ {
XP_Bool success = XP_FALSE; XP_Bool success = XP_FALSE;
const char* fmt = "SELECT room, ended, turn, local, nmoves, ntotal, nmissing, " 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"; "FROM games WHERE rowid = %lld";
XP_UCHAR query[256]; XP_UCHAR query[256];
snprintf( query, sizeof(query), fmt, rowid ); snprintf( query, sizeof(query), fmt, rowid );
@ -321,25 +362,28 @@ getGameInfo( sqlite3* pDb, sqlite3_int64 rowid, GameInfo* gib )
result = sqlite3_step( ppStmt ); result = sqlite3_step( ppStmt );
if ( SQLITE_ROW == result ) { if ( SQLITE_ROW == result ) {
success = XP_TRUE; success = XP_TRUE;
getColumnText( ppStmt, 0, gib->room, sizeof(gib->room) ); int col = 0;
gib->gameOver = 1 == sqlite3_column_int( ppStmt, 1 ); getColumnText( ppStmt, col++, gib->room, sizeof(gib->room) );
gib->turn = sqlite3_column_int( ppStmt, 2 ); gib->gameOver = 1 == sqlite3_column_int( ppStmt, col++ );
gib->turnLocal = 1 == sqlite3_column_int( ppStmt, 3 ); gib->turn = sqlite3_column_int( ppStmt, col++ );
gib->nMoves = sqlite3_column_int( ppStmt, 4 ); gib->turnLocal = 1 == sqlite3_column_int( ppStmt, col++ );
gib->nTotal = sqlite3_column_int( ppStmt, 5 ); gib->nMoves = sqlite3_column_int( ppStmt, col++ );
gib->nMissing = sqlite3_column_int( ppStmt, 6 ); gib->nTotal = sqlite3_column_int( ppStmt, col++ );
gib->seed = sqlite3_column_int( ppStmt, 7 ); gib->nMissing = sqlite3_column_int( ppStmt, col++ );
getColumnText( ppStmt, 8, gib->conn, sizeof(gib->conn) ); gib->seed = sqlite3_column_int( ppStmt, col++ );
gib->gameID = sqlite3_column_int( ppStmt, 9 ); getColumnText( ppStmt, col++, gib->conn, sizeof(gib->conn) );
gib->lastMoveTime = sqlite3_column_int( ppStmt, 10 ); 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 ); snprintf( gib->name, sizeof(gib->name), "Game %lld", rowid );
#ifdef PLATFORM_GTK #ifdef PLATFORM_GTK
/* Load the snapshot */ /* Load the snapshot */
GdkPixbuf* snap = NULL; 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 ) { if ( !!ptr ) {
int size = sqlite3_column_bytes( ppStmt, 11 ); int size = sqlite3_column_bytes( ppStmt, snapCol );
/* Skip the version that's written in */ /* Skip the version that's written in */
ptr += sizeof(XP_U16); size -= sizeof(XP_U16); ptr += sizeof(XP_U16); size -= sizeof(XP_U16);
GInputStream* istr = g_memory_input_stream_new_from_data( ptr, size, NULL ); GInputStream* istr = g_memory_input_stream_new_from_data( ptr, size, NULL );

View file

@ -31,6 +31,7 @@ typedef struct _GameInfo {
XP_UCHAR name[128]; XP_UCHAR name[128];
XP_UCHAR room[128]; XP_UCHAR room[128];
XP_UCHAR conn[128]; XP_UCHAR conn[128];
XP_UCHAR relayID[32];
#ifdef PLATFORM_GTK #ifdef PLATFORM_GTK
GdkPixbuf* snap; GdkPixbuf* snap;
#endif #endif
@ -55,6 +56,9 @@ void summarize( CommonGlobals* cGlobals );
/* Return GSList whose data is (ptrs to) rowids */ /* Return GSList whose data is (ptrs to) rowids */
GSList* listGames( sqlite3* dbp ); GSList* listGames( sqlite3* dbp );
/* Mapping of relayID -> rowid */
GHashTable* getRelayIDsToRowsMap( sqlite3* pDb );
XP_Bool getGameInfo( sqlite3* dbp, sqlite3_int64 rowid, GameInfo* gib ); XP_Bool getGameInfo( sqlite3* dbp, sqlite3_int64 rowid, GameInfo* gib );
void getRowsForGameID( sqlite3* dbp, XP_U32 gameID, sqlite3_int64* rowids, void getRowsForGameID( sqlite3* dbp, XP_U32 gameID, sqlite3_int64* rowids,
int* nRowIDs ); int* nRowIDs );

View file

@ -341,6 +341,8 @@ relay_connd_gtk( void* closure, XP_UCHAR* const room,
char buf[256]; char buf[256];
if ( allHere ) { if ( allHere ) {
/* disable for now. Seeing this too often */
skip = XP_TRUE;
snprintf( buf, sizeof(buf), snprintf( buf, sizeof(buf),
"All expected players have joined in %s. Play!", room ); "All expected players have joined in %s. Play!", room );
} else { } else {
@ -428,13 +430,57 @@ relay_sendNoConn_gtk( const XP_U8* msg, XP_U16 len,
return success; return success;
} /* relay_sendNoConn_gtk */ } /* 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 #ifdef COMMS_XPORT_FLAGSPROC
static XP_U32 static XP_U32
gtk_getFlags( void* closure ) gtk_getFlags( void* closure )
{ {
GtkGameGlobals* globals = (GtkGameGlobals*)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 return (!!globals->draw) ? COMMS_XPORT_FLAGS_NONE
: COMMS_XPORT_FLAGS_HASNOCONN; : COMMS_XPORT_FLAGS_HASNOCONN;
# endif
} }
#endif #endif
@ -454,6 +500,9 @@ setTransportProcs( TransportProcs* procs, GtkGameGlobals* globals )
procs->rconnd = relay_connd_gtk; procs->rconnd = relay_connd_gtk;
procs->rerror = relay_error_gtk; procs->rerror = relay_error_gtk;
procs->sendNoConn = relay_sendNoConn_gtk; procs->sendNoConn = relay_sendNoConn_gtk;
# ifdef RELAY_VIA_HTTP
procs->requestJoin = relay_requestJoin_gtk;
# endif
#endif #endif
} }
@ -663,12 +712,7 @@ createOrLoadObjects( GtkGameGlobals* globals )
} else { } else {
DeviceRole serverRole = cGlobals->gi->serverRole; DeviceRole serverRole = cGlobals->gi->serverRole;
if ( serverRole == SERVER_ISCLIENT ) { if ( serverRole == SERVER_ISCLIENT ) {
XWStreamCtxt* stream = tryConnectToServer( cGlobals );
mem_stream_make( MEMPOOL params->vtMgr,
cGlobals, CHANNEL_NONE,
sendOnClose );
(void)server_initClientConnection( cGlobals->game.server,
stream );
} }
#endif #endif
} }
@ -1014,12 +1058,7 @@ new_game_impl( GtkGameGlobals* globals, XP_Bool fireConnDlg )
} }
if ( isClient ) { if ( isClient ) {
XWStreamCtxt* stream = tryConnectToServer( cGlobals );
mem_stream_make( MEMPOOL cGlobals->params->vtMgr,
cGlobals, CHANNEL_NONE,
sendOnClose );
(void)server_initClientConnection( cGlobals->game.server,
stream );
} }
#endif #endif
(void)server_do( cGlobals->game.server ); /* assign tiles, etc. */ (void)server_do( cGlobals->game.server ); /* assign tiles, etc. */
@ -1175,6 +1214,7 @@ handle_memstats( GtkWidget* XP_UNUSED(widget), GtkGameGlobals* globals )
stream_destroy( stream ); stream_destroy( stream );
} /* handle_memstats */ } /* handle_memstats */
#endif #endif
#ifdef XWFEATURE_ACTIVERECT #ifdef XWFEATURE_ACTIVERECT
@ -1199,15 +1239,15 @@ frame_active( GtkWidget* XP_UNUSED(widget), GtkGameGlobals* globals )
} }
#endif #endif
static GtkWidget* GtkWidget*
createAddItem( GtkWidget* parent, gchar* label, createAddItem( GtkWidget* parent, gchar* label,
GCallback handlerFunc, GtkGameGlobals* globals ) GCallback handlerFunc, gpointer closure )
{ {
GtkWidget* item = gtk_menu_item_new_with_label( label ); GtkWidget* item = gtk_menu_item_new_with_label( label );
if ( handlerFunc != NULL ) { if ( handlerFunc != NULL ) {
g_signal_connect( item, "activate", G_CALLBACK(handlerFunc), g_signal_connect( item, "activate", G_CALLBACK(handlerFunc),
globals ); closure );
} }
gtk_menu_shell_append( GTK_MENU_SHELL(parent), item ); gtk_menu_shell_append( GTK_MENU_SHELL(parent), item );
@ -1302,7 +1342,7 @@ static void
disenable_buttons( GtkGameGlobals* globals ) disenable_buttons( GtkGameGlobals* globals )
{ {
XP_U16 nPending = server_getPendingRegs( globals->cGlobals.game.server ); 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 = globals->invite_button =
addButton( globals->buttons_hbox, "Invite", addButton( globals->buttons_hbox, "Invite",
G_CALLBACK(handle_invite_button), globals ); G_CALLBACK(handle_invite_button), globals );
@ -1600,6 +1640,9 @@ send_invites( CommonGlobals* cGlobals, XP_U16 nPlayers,
NetLaunchInfo nli = {0}; NetLaunchInfo nli = {0};
nli_init( &nli, cGlobals->gi, &addr, nPlayers, forceChannel ); 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 ) ); nli_setDevID( &nli, linux_getDevIDRelay( cGlobals->params ) );
#ifdef DEBUG #ifdef DEBUG

View file

@ -46,6 +46,10 @@ typedef struct GtkDrawCtx {
/* GdkDrawable* pixmap; */ /* GdkDrawable* pixmap; */
GtkWidget* drawing_area; GtkWidget* drawing_area;
cairo_surface_t* surface; cairo_surface_t* surface;
#ifdef GDK_AVAILABLE_IN_3_22
GdkDrawingContext* dc;
#endif
struct GtkGameGlobals* globals; struct GtkGameGlobals* globals;
#ifdef USE_CAIRO #ifdef USE_CAIRO
@ -187,6 +191,10 @@ XP_Bool loadGameNoDraw( GtkGameGlobals* globals, LaunchParams* params,
sqlite3_int64 rowid ); sqlite3_int64 rowid );
void destroy_board_window( GtkWidget* widget, GtkGameGlobals* globals ); 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 /* PLATFORM_GTK */
#endif #endif

View file

@ -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. * reserved.
* *
* This program is free software; you can redistribute it and/or * This program is free software; you can redistribute it and/or
@ -86,7 +86,14 @@ initCairo( GtkDrawCtx* dctx )
if ( !!dctx->surface ) { if ( !!dctx->surface ) {
cairo = cairo_create( dctx->surface ); cairo = cairo_create( dctx->surface );
} else if ( !!dctx->drawing_area ) { } 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) ); cairo = gdk_cairo_create( gtk_widget_get_window(dctx->drawing_area) );
#endif
} else { } else {
XP_ASSERT( 0 ); XP_ASSERT( 0 );
} }
@ -108,7 +115,12 @@ destroyCairo( GtkDrawCtx* dctx )
{ {
/* XP_LOGF( "%s(dctx=%p)", __func__, dctx ); */ /* XP_LOGF( "%s(dctx=%p)", __func__, dctx ); */
XP_ASSERT( !!dctx->_cairo ); 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; dctx->_cairo = NULL;
} }

View file

@ -76,7 +76,7 @@ findOpenGame( const GtkAppGlobals* apg, sqlite3_int64 rowid )
} }
enum { ROW_ITEM, ROW_THUMB, NAME_ITEM, ROOM_ITEM, GAMEID_ITEM, SEED_ITEM, 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 }; MISSING_ITEM, LASTTURN_ITEM, N_ITEMS };
static void static void
@ -167,6 +167,7 @@ init_games_list( GtkAppGlobals* apg )
addTextColumn( list, "GameID", GAMEID_ITEM ); addTextColumn( list, "GameID", GAMEID_ITEM );
addTextColumn( list, "Seed", SEED_ITEM ); addTextColumn( list, "Seed", SEED_ITEM );
addTextColumn( list, "Conn. via", CONN_ITEM ); addTextColumn( list, "Conn. via", CONN_ITEM );
addTextColumn( list, "RelayID", RELAYID_ITEM );
addTextColumn( list, "Ended", OVER_ITEM ); addTextColumn( list, "Ended", OVER_ITEM );
addTextColumn( list, "Turn", TURN_ITEM ); addTextColumn( list, "Turn", TURN_ITEM );
addTextColumn( list, "Local", LOCAL_ITEM ); addTextColumn( list, "Local", LOCAL_ITEM );
@ -183,6 +184,7 @@ init_games_list( GtkAppGlobals* apg )
G_TYPE_INT, /* GAMEID_ITEM */ G_TYPE_INT, /* GAMEID_ITEM */
G_TYPE_INT, /* SEED_ITEM */ G_TYPE_INT, /* SEED_ITEM */
G_TYPE_STRING, /* CONN_ITEM */ G_TYPE_STRING, /* CONN_ITEM */
G_TYPE_STRING, /*RELAYID_ITEM */
G_TYPE_BOOLEAN, /* OVER_ITEM */ G_TYPE_BOOLEAN, /* OVER_ITEM */
G_TYPE_INT, /* TURN_ITEM */ G_TYPE_INT, /* TURN_ITEM */
G_TYPE_STRING, /* LOCAL_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, GAMEID_ITEM, gib->gameID,
SEED_ITEM, gib->seed, SEED_ITEM, gib->seed,
CONN_ITEM, gib->conn, CONN_ITEM, gib->conn,
RELAYID_ITEM, gib->relayID,
TURN_ITEM, gib->turn, TURN_ITEM, gib->turn,
OVER_ITEM, gib->gameOver, OVER_ITEM, gib->gameOver,
LOCAL_ITEM, localString, LOCAL_ITEM, localString,
@ -506,6 +509,13 @@ trySetWinConfig( GtkAppGlobals* apg )
gtk_window_move (GTK_WINDOW(apg->window), xx, yy ); 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 static void
makeGamesWindow( GtkAppGlobals* apg ) makeGamesWindow( GtkAppGlobals* apg )
{ {
@ -529,6 +539,17 @@ makeGamesWindow( GtkAppGlobals* apg )
GtkWidget* vbox = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0); GtkWidget* vbox = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0);
gtk_container_add( GTK_CONTAINER(swin), vbox ); gtk_container_add( GTK_CONTAINER(swin), vbox );
gtk_widget_show( 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 ); GtkWidget* list = init_games_list( apg );
gtk_container_add( GTK_CONTAINER(vbox), list ); gtk_container_add( GTK_CONTAINER(vbox), list );
@ -693,6 +714,17 @@ gtkGotBuf( void* closure, const CommsAddrRec* from,
XP_USE( seed ); 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 static gint
requestMsgs( gpointer data ) requestMsgs( gpointer data )
{ {
@ -847,6 +879,7 @@ gtkmain( LaunchParams* params )
if ( params->useUdp ) { if ( params->useUdp ) {
RelayConnProcs procs = { RelayConnProcs procs = {
.msgReceived = gtkGotBuf, .msgReceived = gtkGotBuf,
.msgForRow = gtkGotMsgForRow,
.msgNoticeReceived = gtkNoticeRcvd, .msgNoticeReceived = gtkNoticeRcvd,
.devIDReceived = gtkDevIDReceived, .devIDReceived = gtkDevIDReceived,
.msgErrorMsg = gtkErrorMsgRcvd, .msgErrorMsg = gtkErrorMsgRcvd,

View file

@ -634,6 +634,8 @@ typedef enum {
,CMD_CHAT ,CMD_CHAT
,CMD_USEUDP ,CMD_USEUDP
,CMD_NOUDP ,CMD_NOUDP
,CMD_USEHTTP
,CMD_NOHTTPAUTO
,CMD_DROPSENDRELAY ,CMD_DROPSENDRELAY
,CMD_DROPRCVRELAY ,CMD_DROPRCVRELAY
,CMD_DROPSENDSMS ,CMD_DROPSENDSMS
@ -752,6 +754,8 @@ static CmdInfoRec CmdInfoRecs[] = {
,{ CMD_CHAT, true, "send-chat", "send a chat every <n> seconds" } ,{ CMD_CHAT, true, "send-chat", "send a chat every <n> seconds" }
,{ CMD_USEUDP, false, "use-udp", "connect to relay new-style, via udp not tcp (on by default)" } ,{ 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_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_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" } ,{ CMD_DROPRCVRELAY, false, "drop-receive-relay", "start new games with relay receive disabled" }
@ -973,6 +977,7 @@ linux_setupDevidParams( LaunchParams* params )
static int static int
linux_init_relay_socket( CommonGlobals* cGlobals, const CommsAddrRec* addrRec ) linux_init_relay_socket( CommonGlobals* cGlobals, const CommsAddrRec* addrRec )
{ {
XP_ASSERT( !cGlobals->params->useHTTP );
struct sockaddr_in to_sock; struct sockaddr_in to_sock;
struct hostent* host; struct hostent* host;
int sock = cGlobals->relaySocket; 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, result = relaycon_send( cGlobals->params, buf, buflen,
clientToken, addrRec ); clientToken, addrRec );
} else { } else {
XP_ASSERT( !cGlobals->params->useHTTP );
int sock = cGlobals->relaySocket; int sock = cGlobals->relaySocket;
if ( sock == -1 ) { if ( sock == -1 ) {
@ -1552,8 +1558,8 @@ linuxChangeRoles( CommonGlobals* cGlobals )
} }
#endif #endif
static unsigned int unsigned int
defaultRandomSeed() makeRandomInt()
{ {
/* use kernel device rather than time() so can run multiple times/second /* use kernel device rather than time() so can run multiple times/second
without getting the same results. */ without getting the same results. */
@ -2028,7 +2034,7 @@ main( int argc, char** argv )
XP_Bool isServer = XP_FALSE; XP_Bool isServer = XP_FALSE;
// char* portNum = NULL; // char* portNum = NULL;
// char* hostName = "localhost"; // char* hostName = "localhost";
unsigned int seed = defaultRandomSeed(); unsigned int seed = makeRandomInt();
LaunchParams mainParams; LaunchParams mainParams;
XP_U16 nPlayerDicts = 0; XP_U16 nPlayerDicts = 0;
XP_U16 robotCount = 0; XP_U16 robotCount = 0;
@ -2284,6 +2290,7 @@ main( int argc, char** argv )
break; break;
case CMD_PLAYERNAME: case CMD_PLAYERNAME:
index = mainParams.pgi.nPlayers++; index = mainParams.pgi.nPlayers++;
XP_ASSERT( index < MAX_NUM_PLAYERS );
++mainParams.nLocalPlayers; ++mainParams.nLocalPlayers;
mainParams.pgi.players[index].robotIQ = 0; /* means human */ mainParams.pgi.players[index].robotIQ = 0; /* means human */
mainParams.pgi.players[index].isLocal = XP_TRUE; mainParams.pgi.players[index].isLocal = XP_TRUE;
@ -2292,6 +2299,7 @@ main( int argc, char** argv )
break; break;
case CMD_REMOTEPLAYER: case CMD_REMOTEPLAYER:
index = mainParams.pgi.nPlayers++; index = mainParams.pgi.nPlayers++;
XP_ASSERT( index < MAX_NUM_PLAYERS );
mainParams.pgi.players[index].isLocal = XP_FALSE; mainParams.pgi.players[index].isLocal = XP_FALSE;
++mainParams.info.serverInfo.nRemotePlayers; ++mainParams.info.serverInfo.nRemotePlayers;
break; break;
@ -2302,6 +2310,7 @@ main( int argc, char** argv )
case CMD_ROBOTNAME: case CMD_ROBOTNAME:
++robotCount; ++robotCount;
index = mainParams.pgi.nPlayers++; index = mainParams.pgi.nPlayers++;
XP_ASSERT( index < MAX_NUM_PLAYERS );
++mainParams.nLocalPlayers; ++mainParams.nLocalPlayers;
mainParams.pgi.players[index].robotIQ = 1; /* real smart by default */ mainParams.pgi.players[index].robotIQ = 1; /* real smart by default */
mainParams.pgi.players[index].isLocal = XP_TRUE; mainParams.pgi.players[index].isLocal = XP_TRUE;
@ -2398,6 +2407,12 @@ main( int argc, char** argv )
case CMD_NOUDP: case CMD_NOUDP:
mainParams.useUdp = false; mainParams.useUdp = false;
break; break;
case CMD_USEHTTP:
mainParams.useHTTP = true;
break;
case CMD_NOHTTPAUTO:
mainParams.noHTTPAuto = true;
break;
case CMD_DROPSENDRELAY: case CMD_DROPSENDRELAY:
mainParams.commsDisableds[COMMS_CONN_RELAY][1] = XP_TRUE; mainParams.commsDisableds[COMMS_CONN_RELAY][1] = XP_TRUE;
@ -2487,10 +2502,10 @@ main( int argc, char** argv )
mainParams.dictDirs = g_slist_append( mainParams.dictDirs, "./" ); mainParams.dictDirs = g_slist_append( mainParams.dictDirs, "./" );
} }
if ( isServer ) { if ( mainParams.info.serverInfo.nRemotePlayers == 0 ) {
if ( mainParams.info.serverInfo.nRemotePlayers == 0 ) { mainParams.pgi.serverRole = SERVER_STANDALONE;
mainParams.pgi.serverRole = SERVER_STANDALONE; } else if ( isServer ) {
} else { if ( mainParams.info.serverInfo.nRemotePlayers > 0 ) {
mainParams.pgi.serverRole = SERVER_ISSERVER; mainParams.pgi.serverRole = SERVER_ISSERVER;
} }
} else { } else {
@ -2646,7 +2661,8 @@ main( int argc, char** argv )
if ( mainParams.useCurses ) { if ( mainParams.useCurses ) {
if ( mainParams.needsNewGame ) { if ( mainParams.needsNewGame ) {
/* curses doesn't have newgame dialog */ /* 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 { } else {
#if defined PLATFORM_NCURSES #if defined PLATFORM_NCURSES
cursesmain( isServer, &mainParams ); cursesmain( isServer, &mainParams );

View file

@ -111,6 +111,8 @@ const XP_UCHAR* linux_getDevID( LaunchParams* params, DevIDType* typ );
void linux_doInitialReg( LaunchParams* params, XP_Bool idIsNew ); void linux_doInitialReg( LaunchParams* params, XP_Bool idIsNew );
XP_Bool linux_setupDevidParams( LaunchParams* params ); XP_Bool linux_setupDevidParams( LaunchParams* params );
unsigned int makeRandomInt();
/* void initParams( LaunchParams* params ); */ /* void initParams( LaunchParams* params ); */
/* void freeParams( LaunchParams* params ); */ /* void freeParams( LaunchParams* params ); */

View file

@ -41,7 +41,7 @@
void void
linux_debugf( const char* format, ... ) linux_debugf( const char* format, ... )
{ {
char buf[1000]; char buf[1024*8];
va_list ap; va_list ap;
struct tm* timp; struct tm* timp;
struct timeval tv; struct timeval tv;
@ -50,15 +50,18 @@ linux_debugf( const char* format, ... )
gettimeofday( &tv, &tz ); gettimeofday( &tv, &tz );
timp = localtime( &tv.tv_sec ); timp = localtime( &tv.tv_sec );
snprintf( buf, sizeof(buf), "<%d>%.2d:%.2d:%.2d:", getpid(), size_t len = snprintf( buf, sizeof(buf), "<%d:%lx>%.2d:%.2d:%.2d:", getpid(),
timp->tm_hour, timp->tm_min, timp->tm_sec ); pthread_self(), timp->tm_hour, timp->tm_min, timp->tm_sec );
XP_ASSERT( len < sizeof(buf) );
va_start(ap, format); va_start(ap, format);
len = vsprintf(buf+strlen(buf), format, ap);
vsprintf(buf+strlen(buf), format, ap);
va_end(ap); va_end(ap);
if ( len >= sizeof(buf) ) {
buf[sizeof(buf)-1] = '\0';
}
fprintf( stderr, "%s\n", buf ); fprintf( stderr, "%s\n", buf );
} }

View file

@ -105,6 +105,8 @@ typedef struct LaunchParams {
XP_Bool closeStdin; XP_Bool closeStdin;
XP_Bool useCurses; XP_Bool useCurses;
XP_Bool useUdp; XP_Bool useUdp;
XP_Bool useHTTP;
XP_Bool noHTTPAuto;
XP_U16 splitPackets; XP_U16 splitPackets;
XP_U16 chatsInterval; /* 0 means disabled */ XP_U16 chatsInterval; /* 0 means disabled */
XP_U16 askTimeout; XP_U16 askTimeout;

View file

@ -20,12 +20,29 @@
#include <netdb.h> #include <netdb.h>
#include <errno.h> #include <errno.h>
#include <stdbool.h> #include <stdbool.h>
#include <curl/curl.h>
#include <json-c/json.h>
#include "relaycon.h" #include "relaycon.h"
#include "linuxmain.h" #include "linuxmain.h"
#include "comtypes.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 { 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; int socket;
RelayConnProcs procs; RelayConnProcs procs;
void* procsClosure; void* procsClosure;
@ -33,6 +50,8 @@ typedef struct _RelayConStorage {
uint32_t nextID; uint32_t nextID;
XWPDevProto proto; XWPDevProto proto;
LaunchParams* params; LaunchParams* params;
XP_UCHAR host[64];
int nextTaskID;
} RelayConStorage; } RelayConStorage;
typedef struct _MsgHeader { typedef struct _MsgHeader {
@ -41,10 +60,16 @@ typedef struct _MsgHeader {
} MsgHeader; } MsgHeader;
static RelayConStorage* getStorage( LaunchParams* params ); static RelayConStorage* getStorage( LaunchParams* params );
static XP_Bool onMainThread( RelayConStorage* storage );
static XP_U32 hostNameToIP( const XP_UCHAR* name ); static XP_U32 hostNameToIP( const XP_UCHAR* name );
static gboolean relaycon_receive( GIOChannel *source, GIOCondition condition, static gboolean relaycon_receive( GIOChannel *source, GIOCondition condition,
gpointer data ); 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 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 void getNetString( const XP_U8** ptr, XP_U16 len, XP_UCHAR* buf );
static XP_U16 getNetShort( const XP_U8** ptr ); 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 writeVLI( XP_U8* out, uint32_t nn );
static size_t un2vli( int nn, uint8_t* buf ); static size_t un2vli( int nn, uint8_t* buf );
static bool vli2un( const uint8_t** inp, uint32_t* outp ); 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 void
relaycon_init( LaunchParams* params, const RelayConnProcs* procs, 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) ); XP_MEMCPY( &storage->procs, procs, sizeof(storage->procs) );
storage->procsClosure = procsClosure; storage->procsClosure = procsClosure;
storage->socket = socket( AF_INET, SOCK_DGRAM, IPPROTO_UDP ); if ( params->useHTTP ) {
(*procs->socketAdded)( storage, storage->socket, relaycon_receive ); 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) ); pthread_mutex_init( &storage->gotDataMutex, NULL );
storage->saddr.sin_family = PF_INET; g_timeout_add( 50, gotDataTimer, storage );
storage->saddr.sin_addr.s_addr = htonl( hostNameToIP(host) );
storage->saddr.sin_port = htons(port);
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->params = params;
storage->proto = XWPDEV_PROTO_VERSION_1; 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 /* 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 box" );
indx += addVLIStr( &tmpbuf[indx], sizeof(tmpbuf) - indx, "linux version" ); indx += addVLIStr( &tmpbuf[indx], sizeof(tmpbuf) - indx, "linux version" );
sendIt( storage, tmpbuf, indx ); sendIt( storage, tmpbuf, indx, 0.5 );
} }
void void
@ -146,7 +343,7 @@ relaycon_invite( LaunchParams* params, XP_U32 destDevID,
indx += writeBytes( &tmpbuf[indx], sizeof(tmpbuf) - indx, ptr, len ); indx += writeBytes( &tmpbuf[indx], sizeof(tmpbuf) - indx, ptr, len );
stream_destroy( stream ); stream_destroy( stream );
sendIt( storage, tmpbuf, indx ); sendIt( storage, tmpbuf, indx, 0.5 );
LOG_RETURN_VOID(); 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 += writeHeader( storage, tmpbuf, XWPDEV_MSG );
indx += writeLong( &tmpbuf[indx], sizeof(tmpbuf) - indx, gameToken ); indx += writeLong( &tmpbuf[indx], sizeof(tmpbuf) - indx, gameToken );
indx += writeBytes( &tmpbuf[indx], sizeof(tmpbuf) - indx, buf, buflen ); indx += writeBytes( &tmpbuf[indx], sizeof(tmpbuf) - indx, buf, buflen );
nSent = sendIt( storage, tmpbuf, indx ); nSent = sendIt( storage, tmpbuf, indx, 0.5 );
if ( nSent > buflen ) { if ( nSent > buflen ) {
nSent = buflen; nSent = buflen;
} }
@ -191,7 +388,7 @@ relaycon_sendnoconn( LaunchParams* params, const XP_U8* buf, XP_U16 buflen,
(const XP_U8*)relayID, idLen ); (const XP_U8*)relayID, idLen );
tmpbuf[indx++] = '\n'; tmpbuf[indx++] = '\n';
indx += writeBytes( &tmpbuf[indx], sizeof(tmpbuf) - indx, buf, buflen ); indx += writeBytes( &tmpbuf[indx], sizeof(tmpbuf) - indx, buf, buflen );
nSent = sendIt( storage, tmpbuf, indx ); nSent = sendIt( storage, tmpbuf, indx, 0.5 );
if ( nSent > buflen ) { if ( nSent > buflen ) {
nSent = buflen; nSent = buflen;
} }
@ -210,7 +407,7 @@ relaycon_requestMsgs( LaunchParams* params, const XP_UCHAR* devID )
indx += writeHeader( storage, tmpbuf, XWPDEV_RQSTMSGS ); indx += writeHeader( storage, tmpbuf, XWPDEV_RQSTMSGS );
indx += addVLIStr( &tmpbuf[indx], sizeof(tmpbuf) - indx, devID ); indx += addVLIStr( &tmpbuf[indx], sizeof(tmpbuf) - indx, devID );
sendIt( storage, tmpbuf, indx ); sendIt( storage, tmpbuf, indx, 0.5 );
} }
void void
@ -225,9 +422,170 @@ relaycon_deleted( LaunchParams* params, const XP_UCHAR* devID,
indx += writeDevID( &tmpbuf[indx], sizeof(tmpbuf) - indx, devID ); indx += writeDevID( &tmpbuf[indx], sizeof(tmpbuf) - indx, devID );
indx += writeLong( &tmpbuf[indx], sizeof(tmpbuf) - indx, gameToken ); 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 static void
sendAckIf( RelayConStorage* storage, const MsgHeader* header ) sendAckIf( RelayConStorage* storage, const MsgHeader* header )
{ {
@ -235,40 +593,22 @@ sendAckIf( RelayConStorage* storage, const MsgHeader* header )
XP_U8 tmpbuf[16]; XP_U8 tmpbuf[16];
int indx = writeHeader( storage, tmpbuf, XWPDEV_ACK ); int indx = writeHeader( storage, tmpbuf, XWPDEV_ACK );
indx += writeVLI( &tmpbuf[indx], header->packetID ); indx += writeVLI( &tmpbuf[indx], header->packetID );
sendIt( storage, tmpbuf, indx ); sendIt( storage, tmpbuf, indx, 0.1 );
} }
} }
static gboolean static gboolean
relaycon_receive( GIOChannel* source, GIOCondition XP_UNUSED_DBG(condition), gpointer data ) 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 );
g_free( 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
if ( 0 <= nRead ) { if ( 0 <= nRead ) {
const XP_U8* ptr = buf; const XP_U8* ptr = buf;
const XP_U8* end = buf + nRead; const XP_U8* end = buf + nRead;
MsgHeader header; MsgHeader header;
if ( readHeader( &ptr, &header ) ) { if ( readHeader( &ptr, &header ) ) {
sendAckIf( storage, &header ); sendAckIf( storage, &header );
XP_LOGF( "%s(): got %s", __func__, msgToStr(header.cmd) );
switch( header.cmd ) { switch( header.cmd ) {
case XWPDEV_REGRSP: { case XWPDEV_REGRSP: {
uint32_t len; uint32_t len;
@ -318,7 +658,7 @@ relaycon_receive( GIOChannel* source, GIOCondition XP_UNUSED_DBG(condition), gpo
assert( 0 ); assert( 0 );
} }
XP_USE( packetID ); XP_USE( packetID );
XP_LOGF( "got ack for packetID %d", packetID ); XP_LOGF( "%s(): got ack for packetID %d", __func__, packetID );
break; break;
} }
case XWPDEV_ALERT: { case XWPDEV_ALERT: {
@ -366,9 +706,55 @@ relaycon_receive( GIOChannel* source, GIOCondition XP_UNUSED_DBG(condition), gpo
return TRUE; 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 void
relaycon_cleanup( LaunchParams* params ) 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, &params->relayConStorage ); XP_FREEP( params->mpool, &params->relayConStorage );
} }
@ -402,12 +788,322 @@ hostNameToIP( const XP_UCHAR* name )
return ip; return ip;
} }
static ssize_t #ifdef RELAY_VIA_HTTP
sendIt( RelayConStorage* storage, const XP_U8* msgbuf, XP_U16 len ) static void
onGotJoinData( RelayTask* task )
{ {
ssize_t nSent = sendto( storage->socket, msgbuf, len, 0, /* flags */ LOG_FUNC();
(struct sockaddr*)&storage->saddr, RelayConStorage* storage = task->storage;
sizeof(storage->saddr) ); 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 #ifdef COMMS_CHECKSUM
gchar* sum = g_compute_checksum_for_data( G_CHECKSUM_MD5, msgbuf, len ); gchar* sum = g_compute_checksum_for_data( G_CHECKSUM_MD5, msgbuf, len );
XP_LOGF( "%s: sent %d bytes with sum %s", __func__, len, sum ); XP_LOGF( "%s: sent %d bytes with sum %s", __func__, len, sum );
@ -601,3 +1297,33 @@ vli2un( const uint8_t** inp, uint32_t* outp )
} }
return success; 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 = "<unknown>";
break;
}
# undef CASE_STR
return str;
}
#endif

View file

@ -27,6 +27,8 @@
typedef struct _Procs { typedef struct _Procs {
void (*msgReceived)( void* closure, const CommsAddrRec* from, void (*msgReceived)( void* closure, const CommsAddrRec* from,
const XP_U8* buf, XP_U16 len ); 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 (*msgNoticeReceived)( void* closure );
void (*devIDReceived)( void* closure, const XP_UCHAR* devID, void (*devIDReceived)( void* closure, const XP_UCHAR* devID,
XP_U16 maxInterval ); XP_U16 maxInterval );
@ -56,4 +58,14 @@ void relaycon_cleanup( LaunchParams* params );
XP_U32 makeClientToken( sqlite3_int64 rowid, XP_U16 seed ); XP_U32 makeClientToken( sqlite3_int64 rowid, XP_U16 seed );
void rowidFromToken( XP_U32 clientToken, 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 #endif

File diff suppressed because it is too large Load diff

View file

@ -1,7 +1,7 @@
#!/bin/bash #!/bin/bash
set -u -e set -u -e
LOGDIR=$(basename $0)_logs LOGDIR=./$(basename $0)_logs
APP_NEW="" APP_NEW=""
DO_CLEAN="" DO_CLEAN=""
APP_NEW_PARAMS="" APP_NEW_PARAMS=""
@ -17,9 +17,9 @@ SAVE_GOOD=""
MINDEVS="" MINDEVS=""
MAXDEVS="" MAXDEVS=""
ONEPER="" ONEPER=""
RESIGN_RATIO="" RESIGN_PCT=0
DROP_N="" DROP_N=""
MINRUN=2 MINRUN=2 # seconds
ONE_PER_ROOM="" # don't run more than one device at a time per room ONE_PER_ROOM="" # don't run more than one device at a time per room
USE_GTK="" USE_GTK=""
UNDO_PCT=0 UNDO_PCT=0
@ -31,6 +31,7 @@ NAMES=(UNUSED Brynn Ariela Kati Eric)
SEND_CHAT='' SEND_CHAT=''
CORE_COUNT=$(ls core.* 2>/dev/null | wc -l) CORE_COUNT=$(ls core.* 2>/dev/null | wc -l)
DUP_PACKETS='' DUP_PACKETS=''
HTTP_PCT=0
declare -A PIDS declare -A PIDS
declare -A APPS declare -A APPS
@ -43,7 +44,7 @@ declare -A LOGS
declare -A MINEND declare -A MINEND
declare -A ROOM_PIDS declare -A ROOM_PIDS
declare -a APPS_OLD=() declare -a APPS_OLD=()
declare -a DICTS= # wants to be =() too? declare -a DICTS=() # wants to be =() too?
declare -A CHECKED_ROOMS declare -A CHECKED_ROOMS
function cleanup() { function cleanup() {
@ -194,9 +195,6 @@ build_cmds() {
for NLOCALS in ${LOCALS[@]}; do for NLOCALS in ${LOCALS[@]}; do
DEV=$((DEV + 1)) DEV=$((DEV + 1))
FILE="${LOGDIR}/GAME_${GAME}_${DEV}.sql3" 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=${LOGDIR}/${GAME}_${DEV}_LOG.txt
> $LOG # clear the log > $LOG # clear the log
@ -219,7 +217,13 @@ build_cmds() {
PARAMS="$PARAMS --game-dict $DICT --relay-port $PORT --host $HOST " PARAMS="$PARAMS --game-dict $DICT --relay-port $PORT --host $HOST "
PARAMS="$PARAMS --slow-robot 1:3 --skip-confirm" PARAMS="$PARAMS --slow-robot 1:3 --skip-confirm"
PARAMS="$PARAMS --db $FILE" 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" 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" # PARAMS="$PARAMS --split-packets 2"
if [ -n "$SEND_CHAT" ]; then if [ -n "$SEND_CHAT" ]; then
PARAMS="$PARAMS --send-chat $SEND_CHAT" PARAMS="$PARAMS --send-chat $SEND_CHAT"
@ -304,6 +308,21 @@ launch() {
# exec $CMD >/dev/null 2>>$LOG # 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() { close_device() {
ID=$1 ID=$1
MVTO=$2 MVTO=$2
@ -353,11 +372,11 @@ kill_from_log() {
} }
maybe_resign() { maybe_resign() {
if [ "$RESIGN_RATIO" -gt 0 ]; then if [ "$RESIGN_PCT" -gt 0 ]; then
KEY=$1 KEY=$1
LOG=${LOGS[$KEY]} LOG=${LOGS[$KEY]}
if grep -aq XWRELAY_ALLHERE $LOG; then 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..." echo "making $LOG $(connName $LOG) resign..."
kill_from_log $LOG && close_device $KEY $DEADDIR "resignation forced" || /bin/true kill_from_log $LOG && close_device $KEY $DEADDIR "resignation forced" || /bin/true
fi fi
@ -419,6 +438,7 @@ check_game() {
for ID in $OTHERS $KEY; do for ID in $OTHERS $KEY; do
echo -n "${ID}:${LOGS[$ID]}, " echo -n "${ID}:${LOGS[$ID]}, "
kill_from_log ${LOGS[$ID]} || /bin/true kill_from_log ${LOGS[$ID]} || /bin/true
send_dead $ID
close_device $ID $DONEDIR "game over" close_device $ID $DONEDIR "game over"
done done
echo "" echo ""
@ -458,10 +478,8 @@ update_ldevid() {
if [ $RNUM -lt 30 ]; then # upgrade or first run if [ $RNUM -lt 30 ]; then # upgrade or first run
CMD="--ldevid LINUX_TEST_$(printf %.5d ${KEY})_" CMD="--ldevid LINUX_TEST_$(printf %.5d ${KEY})_"
fi fi
else elif [ $RNUM -lt 10 ]; then
if [ $RNUM -lt 10 ]; then CMD="${CMD}x" # give it a new local ID
CMD="${CMD}x" # give it a new local ID
fi
fi fi
ARGS_DEVID[$KEY]="$CMD" ARGS_DEVID[$KEY]="$CMD"
fi fi
@ -508,7 +526,8 @@ run_cmds() {
local KEYS=( ${!ARGS[*]} ) local KEYS=( ${!ARGS[*]} )
KEY=${KEYS[$INDX]} KEY=${KEYS[$INDX]}
ROOM=${ROOMS[$KEY]} 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 if [ -n "$ONE_PER_ROOM" -a 0 -ne ${ROOM_PIDS[$ROOM]} ]; then
continue continue
fi fi
@ -522,10 +541,12 @@ run_cmds() {
ROOM_PIDS[$ROOM]=$PID ROOM_PIDS[$ROOM]=$PID
MINEND[$KEY]=$(($NOW + $MINRUN)) MINEND[$KEY]=$(($NOW + $MINRUN))
else else
PID=${PIDS[$KEY]}
if [ -d /proc/$PID ]; then if [ -d /proc/$PID ]; then
SLEEP=$((${MINEND[$KEY]} - $NOW)) SLEEP=$((${MINEND[$KEY]} - $NOW))
[ $SLEEP -gt 0 ] && sleep $SLEEP if [ $SLEEP -gt 0 ]; then
sleep 1
continue
fi
kill $PID || /bin/true kill $PID || /bin/true
wait $PID wait $PID
fi fi
@ -594,6 +615,7 @@ function getArg() {
function usage() { function usage() {
[ $# -gt 0 ] && echo "Error: $1" >&2 [ $# -gt 0 ] && echo "Error: $1" >&2
echo "Usage: $(basename $0) \\" >&2 echo "Usage: $(basename $0) \\" >&2
echo " [--log-root] # default: . \\" >&2
echo " [--dup-packets] # send all packets twice \\" >&2 echo " [--dup-packets] # send all packets twice \\" >&2
echo " [--clean-start] \\" >&2 echo " [--clean-start] \\" >&2
echo " [--game-dict <path/to/dict>]* \\" >&2 echo " [--game-dict <path/to/dict>]* \\" >&2
@ -601,6 +623,7 @@ function usage() {
echo " [--host <hostname>] \\" >&2 echo " [--host <hostname>] \\" >&2
echo " [--max-devs <int>] \\" >&2 echo " [--max-devs <int>] \\" >&2
echo " [--min-devs <int>] \\" >&2 echo " [--min-devs <int>] \\" >&2
echo " [--min-run <int>] # run each at least this long \\" >&2
echo " [--new-app <path/to/app] \\" >&2 echo " [--new-app <path/to/app] \\" >&2
echo " [--new-app-args [arg*]] # passed only to new app \\" >&2 echo " [--new-app-args [arg*]] # passed only to new app \\" >&2
echo " [--num-games <int>] \\" >&2 echo " [--num-games <int>] \\" >&2
@ -608,12 +631,14 @@ function usage() {
echo " [--old-app <path/to/app]* \\" >&2 echo " [--old-app <path/to/app]* \\" >&2
echo " [--one-per] # force one player per device \\" >&2 echo " [--one-per] # force one player per device \\" >&2
echo " [--port <int>] \\" >&2 echo " [--port <int>] \\" >&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 <int>] \\" >&2 echo " [--seed <int>] \\" >&2
echo " [--send-chat <interval-in-seconds> \\" >&2 echo " [--send-chat <interval-in-seconds> \\" >&2
echo " [--udp-incr <pct>] \\" >&2 echo " [--udp-incr <pct>] \\" >&2
echo " [--udp-start <pct>] # default: $UDP_PCT_START \\" >&2 echo " [--udp-start <pct>] # default: $UDP_PCT_START \\" >&2
echo " [--undo-pct <int>] \\" >&2 echo " [--undo-pct <int>] \\" >&2
echo " [--http-pct <0 <= n <=100>] \\" >&2
exit 1 exit 1
} }
@ -647,6 +672,11 @@ while [ "$#" -gt 0 ]; do
APPS_OLD[${#APPS_OLD[@]}]=$(getArg $*) APPS_OLD[${#APPS_OLD[@]}]=$(getArg $*)
shift shift
;; ;;
--log-root)
[ -d $2 ] || usage "$1: no such directory $2"
LOGDIR=$2/$(basename $0)_logs
shift
;;
--dup-packets) --dup-packets)
DUP_PACKETS=1 DUP_PACKETS=1
;; ;;
@ -671,6 +701,11 @@ while [ "$#" -gt 0 ]; do
MAXDEVS=$(getArg $*) MAXDEVS=$(getArg $*)
shift shift
;; ;;
--min-run)
MINRUN=$(getArg $*)
[ $MINRUN -ge 2 -a $MINRUN -le 60 ] || usage "$1: n must be 2 <= n <= 60"
shift
;;
--one-per) --one-per)
ONEPER=TRUE ONEPER=TRUE
;; ;;
@ -690,14 +725,23 @@ while [ "$#" -gt 0 ]; do
UNDO_PCT=$(getArg $*) UNDO_PCT=$(getArg $*)
shift 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)
SEND_CHAT=$(getArg $*) SEND_CHAT=$(getArg $*)
shift shift
;; ;;
--resign-ratio) --resign-pct)
RESIGN_RATIO=$(getArg $*) RESIGN_PCT=$(getArg $*)
[ $RESIGN_PCT -ge 0 -a $RESIGN_PCT -le 100 ] || usage "$1: n must be 0 <= n <= 100"
shift shift
;; ;;
--no-timeout)
TIMEOUT=0x7FFFFFFF
;;
--help) --help)
usage usage
;; ;;
@ -709,7 +753,7 @@ done
# Assign defaults # Assign defaults
#[ 0 -eq ${#DICTS[@]} ] && DICTS=(dict.xwd) #[ 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 "$APP_NEW" ] && APP_NEW=./obj_linux_memdbg/xwords
[ -z "$MINDEVS" ] && MINDEVS=2 [ -z "$MINDEVS" ] && MINDEVS=2
[ -z "$MAXDEVS" ] && MAXDEVS=4 [ -z "$MAXDEVS" ] && MAXDEVS=4
@ -719,7 +763,7 @@ done
[ -z "$PORT" ] && PORT=10997 [ -z "$PORT" ] && PORT=10997
[ -z "$TIMEOUT" ] && TIMEOUT=$((NGAMES*60+500)) [ -z "$TIMEOUT" ] && TIMEOUT=$((NGAMES*60+500))
[ -z "$SAVE_GOOD" ] && SAVE_GOOD=YES [ -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 "$DROP_N" ] && DROP_N=0
[ -z "$USE_GTK" ] && USE_GTK=FALSE [ -z "$USE_GTK" ] && USE_GTK=FALSE
[ -z "$UPGRADE_ODDS" ] && UPGRADE_ODDS=10 [ -z "$UPGRADE_ODDS" ] && UPGRADE_ODDS=10
@ -747,7 +791,8 @@ for FILE in $(ls $LOGDIR/*.{xwg,txt} 2>/dev/null); do
done done
if [ -z "$RESUME" -a -d $LOGDIR ]; then if [ -z "$RESUME" -a -d $LOGDIR ]; then
mv $LOGDIR /tmp/${LOGDIR}_$$ NEWNAME="$(basename $LOGDIR)_$$"
(cd $(dirname $LOGDIR) && mv $(basename $LOGDIR) /tmp/${NEWNAME})
fi fi
mkdir -p $LOGDIR mkdir -p $LOGDIR
@ -759,7 +804,7 @@ DEADDIR=$LOGDIR/dead
mkdir -p $DEADDIR mkdir -p $DEADDIR
for VAR in NGAMES NROOMS USE_GTK TIMEOUT HOST PORT SAVE_GOOD \ 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 APP_NEW; do
echo "$VAR:" $(eval "echo \$${VAR}") 1>&2 echo "$VAR:" $(eval "echo \$${VAR}") 1>&2
done done

View file

@ -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()

View file

@ -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 <<EOF
Starts a pair of devices meant to get into the same game. Verification
is by looking at the relay, usually with
./relay/scripts/showinplay.sh. Both should have an 'A' in the ACK
column.
EOF
exit 1
}
while [ $# -gt 0 ]; do
case $1 in
--in-sequence)
IN_SEQ=1
;;
--at-once)
IN_SEQ=0
;;
--no-use-http)
HTTP=''
;;
--gtk)
CURSES=''
;;
*)
usage "unexpected param $1"
;;
esac
shift
done
[ -n "$IN_SEQ" ] || usage "missing required param"
DB_TMPLATE=_cursesdb_
LOG_TMPLATE=_curseslog_
ROOM_TMPLATE=cursesRoom
echo "delete from msgs;" | psql xwgames
echo "delete from games where room like '$ROOM_TMPLATE%';" | psql xwgames
rm -f ${DB_TMPLATE}*.sqldb
rm -f ${LOG_TMPLATE}*
PIDS=''
for GAME in $(seq 1); do
ROOM=${ROOM_TMPLATE}${GAME}
for N in $(seq 2); do
# for N in $(seq 1); do
DB=$DB_TMPLATE${GAME}_${N}.sqldb
LOG=$LOG_TMPLATE${GAME}_${N}.log
exec ./obj_linux_memdbg/xwords --server $CURSES --remote-player --robot Player \
--room $ROOM --game-dict dict.xwd $HTTP\
--skip-confirm --db $DB --close-stdin --server \
>/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

61
xwords4/newrelay/nr.py Executable file
View file

@ -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()

View file

@ -42,7 +42,7 @@ SRC = \
# STATIC ?= -static # STATIC ?= -static
GITINFO = gitversion.txt GITINFO = gitversion.txt
HASH=$(shell git describe) HASH=$(shell git rev-parse --verify HEAD)
OBJ = $(patsubst %.cpp,obj/%.o,$(SRC)) OBJ = $(patsubst %.cpp,obj/%.o,$(SRC))
#LDFLAGS += -pthread -g -lmcheck $(STATIC) #LDFLAGS += -pthread -g -lmcheck $(STATIC)
@ -67,10 +67,11 @@ endif
# turn on semaphore debugging # turn on semaphore debugging
# CPPFLAGS += -DDEBUG_LOCKS # CPPFLAGS += -DDEBUG_LOCKS
# CPPFLAGS += -DLOG_POLL
memdebug all: xwrelay rq memdebug all: xwrelay rq
REQUIRED_DEBS = libpq-dev g++ \ REQUIRED_DEBS = libpq-dev g++ libglib2.0-dev postgresql \
.PHONY: debcheck debs_install .PHONY: debcheck debs_install

View file

@ -20,13 +20,16 @@
*/ */
#include <assert.h> #include <assert.h>
#include <errno.h>
#include <string.h> #include <string.h>
#include <time.h> #include <time.h>
#include <unistd.h>
#include "addrinfo.h" #include "addrinfo.h"
#include "xwrelay_priv.h" #include "xwrelay_priv.h"
#include "tpool.h" #include "tpool.h"
#include "udpager.h" #include "udpager.h"
#include "mlock.h"
// static uint32_t s_prevCreated = 0L; // static uint32_t s_prevCreated = 0L;
@ -68,7 +71,7 @@ AddrInfo::equals( const AddrInfo& other ) const
if ( isTCP() ) { if ( isTCP() ) {
equal = m_socket == other.m_socket; equal = m_socket == other.m_socket;
if ( equal && created() != other.created() ) { 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() ); __func__, created(), other.created() );
equal = false; equal = false;
} }
@ -82,3 +85,40 @@ AddrInfo::equals( const AddrInfo& other ) const
return equal; return equal;
} }
static pthread_mutex_t s_refMutex = PTHREAD_MUTEX_INITIALIZER;
static map<int, int > 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<int,int>::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 ); */
/* } */
/* } */
}

View file

@ -81,12 +81,18 @@ class AddrInfo {
bool equals( const AddrInfo& other ) const; bool equals( const AddrInfo& other ) const;
/* refcount the underlying socket (doesn't modify instance) */
void ref() const;
void unref() const;
int getref() const;
private: private:
void construct( int sock, const AddrUnion* saddr, bool isTCP ); void construct( int sock, const AddrUnion* saddr, bool isTCP );
void init( int sock, ClientToken clientToken, const AddrUnion* saddr ) { void init( int sock, ClientToken clientToken, const AddrUnion* saddr ) {
construct( sock, saddr, false ); construct( sock, saddr, false );
m_clientToken = clientToken; m_clientToken = clientToken;
} }
void printRefMap() const;
// AddrInfo& operator=(const AddrInfo&); // Prevent assignment // AddrInfo& operator=(const AddrInfo&); // Prevent assignment
int m_socket; int m_socket;

View file

@ -84,12 +84,13 @@ RelayConfigs::GetValueFor( const char* key, time_t* value )
bool bool
RelayConfigs::GetValueFor( const char* key, char* buf, int len ) RelayConfigs::GetValueFor( const char* key, char* buf, int len )
{ {
MutexLock ml( &m_values_mutex ); pthread_mutex_lock( &m_values_mutex );
map<const char*,const char*>::const_iterator iter = m_values.find(key); map<const char*,const char*>::const_iterator iter = m_values.find(key);
bool found = iter != m_values.end(); bool found = iter != m_values.end();
if ( found ) { if ( found ) {
snprintf( buf, len, "%s", iter->second ); snprintf( buf, len, "%s", iter->second );
} }
pthread_mutex_unlock( &m_values_mutex );
return found; return found;
} }
@ -125,7 +126,7 @@ RelayConfigs::GetValueFor( const char* key, vector<int>& ints )
void void
RelayConfigs::SetValueFor( const char* key, const char* value ) RelayConfigs::SetValueFor( const char* key, const char* value )
{ {
MutexLock ml( &m_values_mutex ); pthread_mutex_lock( &m_values_mutex );
/* Remove any entry already there */ /* Remove any entry already there */
map<const char*,const char*>::iterator iter = m_values.find(key); map<const char*,const char*>::iterator iter = m_values.find(key);
@ -136,6 +137,7 @@ RelayConfigs::SetValueFor( const char* key, const char* value )
pair<map<const char*,const char*>::iterator,bool> result = pair<map<const char*,const char*>::iterator,bool> result =
m_values.insert( pair<const char*,const char*>(strdup(key),strdup(value) ) ); m_values.insert( pair<const char*,const char*>(strdup(key),strdup(value) ) );
assert( result.second ); assert( result.second );
pthread_mutex_unlock( &m_values_mutex );
} }
ino_t ino_t

View file

@ -875,13 +875,13 @@ putNetShort( uint8_t** bufpp, unsigned short s )
*bufpp += sizeof(s); *bufpp += sizeof(s);
} }
void int
CookieRef::store_message( HostID dest, const uint8_t* buf, CookieRef::store_message( HostID dest, const uint8_t* buf,
unsigned int len ) unsigned int len )
{ {
logf( XW_LOGVERBOSE0, "%s: storing msg size %d for dest %d", __func__, logf( XW_LOGVERBOSE0, "%s: storing msg size %d for dest %d", __func__,
len, dest ); len, dest );
DBMgr::Get()->StoreMessage( ConnName(), dest, buf, len ); return DBMgr::Get()->StoreMessage( ConnName(), dest, buf, len );
} }
void void
@ -1044,6 +1044,7 @@ CookieRef::postCheckAllHere()
void void
CookieRef::postDropDevice( HostID hostID ) CookieRef::postDropDevice( HostID hostID )
{ {
logf( XW_LOGINFO, "%s(hostID=%d)", __func__, hostID );
CRefEvent evt( XWE_ACKTIMEOUT ); CRefEvent evt( XWE_ACKTIMEOUT );
evt.u.ack.srcID = hostID; evt.u.ack.srcID = hostID;
m_eventQueue.push_back( evt ); m_eventQueue.push_back( evt );
@ -1192,21 +1193,16 @@ CookieRef::sendAnyStored( const CRefEvent* evt )
} }
typedef struct _StoreData { typedef struct _StoreData {
string connName; int msgID;
HostID dest;
uint8_t* buf;
int buflen;
} StoreData; } StoreData;
void void
CookieRef::storeNoAck( bool acked, uint32_t packetID, void* data ) CookieRef::storeNoAck( bool acked, uint32_t packetID, void* data )
{ {
StoreData* sdata = (StoreData*)data; StoreData* sdata = (StoreData*)data;
if ( !acked ) { if ( acked ) {
DBMgr::Get()->StoreMessage( sdata->connName.c_str(), sdata->dest, DBMgr::Get()->RemoveStoredMessages( &sdata->msgID, 1 );
sdata->buf, sdata->buflen );
} }
free( sdata->buf );
delete sdata; delete sdata;
} }
@ -1237,17 +1233,13 @@ CookieRef::forward_or_store( const CRefEvent* evt )
} }
uint32_t packetID = 0; uint32_t packetID = 0;
int msgID = store_message( dest, buf, buflen );
if ( (NULL == destAddr) if ( (NULL == destAddr)
|| !send_with_length( destAddr, dest, buf, buflen, true, || !send_with_length( destAddr, dest, buf, buflen, true,
&packetID ) ) { &packetID ) ) {
store_message( dest, buf, buflen ); } else if ( 0 != msgID && 0 != packetID ) { // sent via UDP
} else if ( 0 != packetID ) { // sent via UDP
StoreData* data = new StoreData; StoreData* data = new StoreData;
data->connName = m_connName; data->msgID = msgID;
data->dest = dest;
data->buf = (uint8_t*)malloc( buflen );
memcpy( data->buf, buf, buflen );
data->buflen = buflen;
UDPAckTrack::setOnAck( storeNoAck, packetID, data ); UDPAckTrack::setOnAck( storeNoAck, packetID, data );
} }
@ -1376,20 +1368,16 @@ CookieRef::sendAllHere( bool initial )
through the vector each time. */ through the vector each time. */
HostID dest; HostID dest;
for ( dest = 1; dest <= m_nPlayersSought; ++dest ) { for ( dest = 1; dest <= m_nPlayersSought; ++dest ) {
bool sent = false;
*idLoc = dest; /* write in this target's hostId */ *idLoc = dest; /* write in this target's hostId */
{ {
RWReadLock rrl( &m_socketsRWLock ); RWReadLock rrl( &m_socketsRWLock );
HostRec* hr = m_sockets[dest-1]; HostRec* hr = m_sockets[dest-1];
if ( !!hr ) { if ( !!hr ) {
sent = send_with_length( &hr->m_addr, dest, buf, (void)send_with_length( &hr->m_addr, dest, buf, bufp-buf, true );
bufp-buf, true );
} }
} }
if ( !sent ) { (void)store_message( dest, buf, bufp-buf );
store_message( dest, buf, bufp-buf );
}
} }
} /* sendAllHere */ } /* sendAllHere */

View file

@ -275,8 +275,7 @@ class CookieRef {
bool notInUse(void) { return m_cid == 0; } bool notInUse(void) { return m_cid == 0; }
void store_message( HostID dest, const uint8_t* buf, int store_message( HostID dest, const uint8_t* buf, unsigned int len );
unsigned int len );
void send_stored_messages( HostID dest, const AddrInfo* addr ); void send_stored_messages( HostID dest, const AddrInfo* addr );
void printSeeds( const char* caller ); void printSeeds( const char* caller );

View file

@ -337,7 +337,7 @@ CRefMgr::getMakeCookieRef( const char* connName, const char* cookie,
} /* getMakeCookieRef */ } /* getMakeCookieRef */
CidInfo* CidInfo*
CRefMgr::getMakeCookieRef( const char* const connName, bool* isDead ) CRefMgr::getMakeCookieRef( const char* const connName, HostID hid, bool* isDead )
{ {
CookieRef* cref = NULL; CookieRef* cref = NULL;
CidInfo* cinfo = NULL; CidInfo* cinfo = NULL;
@ -347,7 +347,7 @@ CRefMgr::getMakeCookieRef( const char* const connName, bool* isDead )
int nAlreadyHere = 0; int nAlreadyHere = 0;
for ( ; ; ) { /* for: see comment above */ for ( ; ; ) { /* for: see comment above */
CookieID cid = m_db->FindGame( connName, curCookie, sizeof(curCookie), CookieID cid = m_db->FindGame( connName, hid, curCookie, sizeof(curCookie),
&curLangCode, &nPlayersT, &nAlreadyHere, &curLangCode, &nPlayersT, &nAlreadyHere,
isDead ); isDead );
if ( 0 != cid ) { /* already open */ if ( 0 != cid ) { /* already open */
@ -375,6 +375,48 @@ CRefMgr::getMakeCookieRef( const char* const connName, bool* isDead )
return cinfo; return cinfo;
} }
CidInfo*
CRefMgr::getMakeCookieRef( const AddrInfo::ClientToken clientToken, HostID srcID )
{
CookieRef* cref = NULL;
CidInfo* cinfo = NULL;
char curCookie[MAX_INVITE_LEN+1];
int curLangCode;
int nPlayersT = 0;
int nAlreadyHere = 0;
for ( ; ; ) { /* for: see comment above */
char connName[MAX_CONNNAME_LEN+1] = {0};
CookieID cid = m_db->FindGame( clientToken, srcID,
connName, sizeof(connName),
curCookie, sizeof(curCookie),
&curLangCode, &nPlayersT, &nAlreadyHere );
// &seed );
if ( 0 != cid ) { /* already open */
cinfo = m_cidlock->Claim( cid );
if ( NULL == cinfo->GetRef() ) {
m_cidlock->Relinquish( cinfo, true );
continue;
}
} else if ( nPlayersT == 0 ) { /* wasn't in the DB */
/* do nothing; insufficient info to fake it */
} else {
cinfo = m_cidlock->Claim();
if ( !m_db->AddCID( connName, cinfo->GetCid() ) ) {
m_cidlock->Relinquish( cinfo, true );
continue;
}
logf( XW_LOGINFO, "%s(): added cid???", __func__ );
cref = AddNew( curCookie, connName, cinfo->GetCid(), curLangCode,
nPlayersT, nAlreadyHere );
cinfo->SetRef( cref );
}
break;
}
logf( XW_LOGINFO, "%s() => %p", __func__, cinfo );
return cinfo;
}
void void
CRefMgr::RemoveSocketRefs( const AddrInfo* addr ) CRefMgr::RemoveSocketRefs( const AddrInfo* addr )
{ {
@ -672,13 +714,13 @@ SafeCref::SafeCref( const char* connName, const char* cookie, HostID hid,
} }
/* ConnName case -- must exist (unless DB record's been removed */ /* ConnName case -- must exist (unless DB record's been removed */
SafeCref::SafeCref( const char* const connName ) SafeCref::SafeCref( const char* const connName, HostID hid )
: m_cinfo( NULL ) : m_cinfo( NULL )
, m_mgr( CRefMgr::Get() ) , m_mgr( CRefMgr::Get() )
, m_isValid( false ) , m_isValid( false )
{ {
bool isDead = false; bool isDead = false;
CidInfo* cinfo = m_mgr->getMakeCookieRef( connName, &isDead ); CidInfo* cinfo = m_mgr->getMakeCookieRef( connName, hid, &isDead );
if ( NULL != cinfo && NULL != cinfo->GetRef() ) { if ( NULL != cinfo && NULL != cinfo->GetRef() ) {
assert( cinfo->GetCid() == cinfo->GetRef()->GetCid() ); assert( cinfo->GetCid() == cinfo->GetRef()->GetCid() );
m_locked = cinfo->GetRef()->Lock(); m_locked = cinfo->GetRef()->Lock();
@ -722,6 +764,19 @@ SafeCref::SafeCref( const AddrInfo* addr )
} }
} }
SafeCref::SafeCref( const AddrInfo::ClientToken clientToken, HostID srcID )
: m_cinfo( NULL )
, m_mgr( CRefMgr::Get() )
, m_isValid( false )
{
CidInfo* cinfo = m_mgr->getMakeCookieRef( clientToken, srcID );
if ( NULL != cinfo && NULL != cinfo->GetRef() ) {
m_locked = cinfo->GetRef()->Lock();
m_cinfo = cinfo;
m_isValid = true;
}
}
SafeCref::~SafeCref() SafeCref::~SafeCref()
{ {
if ( m_cinfo != NULL ) { if ( m_cinfo != NULL ) {

View file

@ -128,7 +128,8 @@ class CRefMgr {
int nPlayersS, int seed, int langCode, int nPlayersS, int seed, int langCode,
bool isPublic, bool* isDead ); bool isPublic, bool* isDead );
CidInfo* getMakeCookieRef( const char* const connName, bool* isDead ); CidInfo* getMakeCookieRef( const char* const connName, HostID hid, bool* isDead );
CidInfo* getMakeCookieRef( const AddrInfo::ClientToken clientToken, HostID srcID );
CidInfo* getCookieRef( CookieID cid, bool failOk = false ); CidInfo* getCookieRef( CookieID cid, bool failOk = false );
CidInfo* getCookieRef( const AddrInfo* addr ); CidInfo* getCookieRef( const AddrInfo* addr );
@ -179,9 +180,10 @@ class SafeCref {
const AddrInfo* addr, int clientVersion, DevID* devID, const AddrInfo* addr, int clientVersion, DevID* devID,
int nPlayersH, int nPlayersS, unsigned short gameSeed, int nPlayersH, int nPlayersS, unsigned short gameSeed,
int clientIndx, int langCode, bool wantsPublic, bool makePublic ); int clientIndx, int langCode, bool wantsPublic, bool makePublic );
SafeCref( const char* const connName ); SafeCref( const char* const connName, HostID hid );
SafeCref( CookieID cid, bool failOk = false ); SafeCref( CookieID cid, bool failOk = false );
SafeCref( const AddrInfo* addr ); SafeCref( const AddrInfo* addr );
SafeCref( const AddrInfo::ClientToken clientToken, HostID srcID );
/* SafeCref( CookieRef* cref ); */ /* SafeCref( CookieRef* cref ); */
~SafeCref(); ~SafeCref();

View file

@ -70,20 +70,6 @@ DBMgr::DBMgr()
pthread_mutex_init( &m_haveNoMessagesMutex, NULL ); pthread_mutex_init( &m_haveNoMessagesMutex, NULL );
/* Now figure out what the largest cid currently is. There must be a way
to get postgres to do this for me.... */
/* const char* query = "SELECT cid FROM games ORDER BY cid DESC LIMIT 1"; */
/* PGresult* result = PQexec( m_pgconn, query ); */
/* if ( 0 == PQntuples( result ) ) { */
/* m_nextCID = 1; */
/* } else { */
/* char* value = PQgetvalue( result, 0, 0 ); */
/* m_nextCID = 1 + atoi( value ); */
/* } */
/* PQclear(result); */
/* logf( XW_LOGINFO, "%s: m_nextCID=%d", __func__, m_nextCID ); */
// I've seen rand returning the same series several times....
srand( time( NULL ) ); srand( time( NULL ) );
} }
@ -136,7 +122,7 @@ DBMgr::FindGameFor( const char* connName, char* cookieBuf, int bufLen,
{ {
bool found = false; bool found = false;
const char* fmt = "SELECT cid, room, lang, nPerDevice, dead FROM " const char* fmt = "SELECT cid, room, lang, dead FROM "
GAMES_TABLE " WHERE connName = '%s' AND nTotal = %d " GAMES_TABLE " WHERE connName = '%s' AND nTotal = %d "
"AND %d = seeds[%d] AND 'A' = ack[%d] " "AND %d = seeds[%d] AND 'A' = ack[%d] "
; ;
@ -148,10 +134,11 @@ DBMgr::FindGameFor( const char* connName, char* cookieBuf, int bufLen,
assert( 1 >= PQntuples( result ) ); assert( 1 >= PQntuples( result ) );
found = 1 == PQntuples( result ); found = 1 == PQntuples( result );
if ( found ) { if ( found ) {
*cidp = atoi( PQgetvalue( result, 0, 0 ) ); int col = 0;
snprintf( cookieBuf, bufLen, "%s", PQgetvalue( result, 0, 1 ) ); *cidp = atoi( PQgetvalue( result, 0, col++ ) );
*langP = atoi( PQgetvalue( result, 0, 2 ) ); snprintf( cookieBuf, bufLen, "%s", PQgetvalue( result, 0, col++ ) );
*isDead = 't' == PQgetvalue( result, 0, 4 )[0]; *langP = atoi( PQgetvalue( result, 0, col++ ) );
*isDead = 't' == PQgetvalue( result, 0, col++ )[0];
} }
PQclear( result ); PQclear( result );
@ -160,28 +147,29 @@ DBMgr::FindGameFor( const char* connName, char* cookieBuf, int bufLen,
} /* FindGameFor */ } /* FindGameFor */
CookieID CookieID
DBMgr::FindGame( const char* connName, char* cookieBuf, int bufLen, DBMgr::FindGame( const char* connName, HostID hid, char* roomBuf, int roomBufLen,
int* langP, int* nPlayersTP, int* nPlayersHP, bool* isDead ) int* langP, int* nPlayersTP, int* nPlayersHP, bool* isDead )
{ {
CookieID cid = 0; CookieID cid = 0;
const char* fmt = "SELECT cid, room, lang, nTotal, nPerDevice, dead FROM " const char* fmt = "SELECT cid, room, lang, nTotal, nPerDevice[%d], dead FROM "
GAMES_TABLE " WHERE connName = '%s'" GAMES_TABLE " WHERE connName = '%s'"
// " LIMIT 1" // " LIMIT 1"
; ;
StrWPF query; StrWPF query;
query.catf( fmt, connName ); query.catf( fmt, hid, connName );
logf( XW_LOGINFO, "query: %s", query.c_str() ); logf( XW_LOGINFO, "query: %s", query.c_str() );
PGresult* result = PQexec( getThreadConn(), query.c_str() ); PGresult* result = PQexec( getThreadConn(), query.c_str() );
assert( 1 >= PQntuples( result ) ); assert( 1 >= PQntuples( result ) );
if ( 1 == PQntuples( result ) ) { if ( 1 == PQntuples( result ) ) {
cid = atoi( PQgetvalue( result, 0, 0 ) ); int col = 0;
snprintf( cookieBuf, bufLen, "%s", PQgetvalue( result, 0, 1 ) ); cid = atoi( PQgetvalue( result, 0, col++ ) );
*langP = atoi( PQgetvalue( result, 0, 2 ) ); snprintf( roomBuf, roomBufLen, "%s", PQgetvalue( result, 0, col++ ) );
*nPlayersTP = atoi( PQgetvalue( result, 0, 3 ) ); *langP = atoi( PQgetvalue( result, 0, col++ ) );
*nPlayersHP = atoi( PQgetvalue( result, 0, 4 ) ); *nPlayersTP = atoi( PQgetvalue( result, 0, col++ ) );
*isDead = 't' == PQgetvalue( result, 0, 5 )[0]; *nPlayersHP = atoi( PQgetvalue( result, 0, col++ ) );
*isDead = 't' == PQgetvalue( result, 0, col++ )[0];
} }
PQclear( result ); PQclear( result );
@ -189,6 +177,40 @@ DBMgr::FindGame( const char* connName, char* cookieBuf, int bufLen,
return cid; return cid;
} /* FindGame */ } /* FindGame */
CookieID
DBMgr::FindGame( const AddrInfo::ClientToken clientToken, HostID hid,
char* connNameBuf, int connNameBufLen,
char* roomBuf, int roomBufLen,
int* langP, int* nPlayersTP, int* nPlayersHP )
{
CookieID cid = 0;
const char* fmt = "SELECT cid, room, lang, nTotal, nPerDevice[%d], connname FROM "
GAMES_TABLE " WHERE tokens[%d] = %d and NOT dead";
// " LIMIT 1"
;
StrWPF query;
query.catf( fmt, hid, hid, clientToken );
logf( XW_LOGINFO, "query: %s", query.c_str() );
PGresult* result = PQexec( getThreadConn(), query.c_str() );
if ( 1 == PQntuples( result ) ) {
int col = 0;
cid = atoi( PQgetvalue( result, 0, col++ ) );
// room
snprintf( roomBuf, roomBufLen, "%s", PQgetvalue( result, 0, col++ ) );
// lang
*langP = atoi( PQgetvalue( result, 0, col++ ) );
*nPlayersTP = atoi( PQgetvalue( result, 0, col++ ) );
*nPlayersHP = atoi( PQgetvalue( result, 0, col++ ) );
snprintf( connNameBuf, connNameBufLen, "%s", PQgetvalue( result, 0, col++ ) );
}
PQclear( result );
logf( XW_LOGINFO, "%s(ct=%d,hid=%d) => %d (connname=%s)", __func__, clientToken,
hid, cid, connNameBuf );
return cid;
}
bool bool
DBMgr::FindPlayer( DevIDRelay relayID, AddrInfo::ClientToken token, DBMgr::FindPlayer( DevIDRelay relayID, AddrInfo::ClientToken token,
string& connName, HostID* hidp, unsigned short* seed ) string& connName, HostID* hidp, unsigned short* seed )
@ -294,11 +316,13 @@ DBMgr::SeenSeed( const char* cookie, unsigned short seed,
NULL, NULL, 0 ); NULL, NULL, 0 );
bool found = 1 == PQntuples( result ); bool found = 1 == PQntuples( result );
if ( found ) { if ( found ) {
*cid = atoi( PQgetvalue( result, 0, 0 ) ); int col = 0;
*nPlayersHP = here_less_seed( PQgetvalue( result, 0, 2 ), *cid = atoi( PQgetvalue( result, 0, col++ ) );
atoi( PQgetvalue( result, 0, 3 ) ), snprintf( connNameBuf, bufLen, "%s", PQgetvalue( result, 0, col++ ) );
seed );
snprintf( connNameBuf, bufLen, "%s", PQgetvalue( result, 0, 1 ) ); const char* seeds = PQgetvalue( result, 0, col++ );
int perDeviceSum = atoi( PQgetvalue( result, 0, col++ ) );
*nPlayersHP = here_less_seed( seeds, perDeviceSum, seed );
} }
PQclear( result ); PQclear( result );
logf( XW_LOGINFO, "%s(%4X)=>%s", __func__, seed, found?"true":"false" ); logf( XW_LOGINFO, "%s(%4X)=>%s", __func__, seed, found?"true":"false" );
@ -333,9 +357,10 @@ DBMgr::FindOpen( const char* cookie, int lang, int nPlayersT, int nPlayersH,
NULL, NULL, 0 ); NULL, NULL, 0 );
CookieID cid = 0; CookieID cid = 0;
if ( 1 == PQntuples( result ) ) { if ( 1 == PQntuples( result ) ) {
cid = atoi( PQgetvalue( result, 0, 0 ) ); int col = 0;
snprintf( connNameBuf, bufLen, "%s", PQgetvalue( result, 0, 1 ) ); cid = atoi( PQgetvalue( result, 0, col++ ) );
*nPlayersHP = atoi( PQgetvalue( result, 0, 2 ) ); snprintf( connNameBuf, bufLen, "%s", PQgetvalue( result, 0, col++ ) );
*nPlayersHP = atoi( PQgetvalue( result, 0, col++ ) );
/* cid may be 0, but should use game anyway */ /* cid may be 0, but should use game anyway */
} }
PQclear( result ); PQclear( result );
@ -699,9 +724,11 @@ DBMgr::RecordSent( const int* msgIDs, int nMsgIDs )
if ( PGRES_TUPLES_OK == PQresultStatus( result ) ) { if ( PGRES_TUPLES_OK == PQresultStatus( result ) ) {
int ntuples = PQntuples( result ); int ntuples = PQntuples( result );
for ( int ii = 0; ii < ntuples; ++ii ) { for ( int ii = 0; ii < ntuples; ++ii ) {
RecordSent( PQgetvalue( result, ii, 0 ), int col = 0;
atoi( PQgetvalue( result, ii, 1 ) ), const char* const connName = PQgetvalue( result, ii, col++ );
atoi( PQgetvalue( result, ii, 2 ) ) ); HostID hid = atoi( PQgetvalue( result, ii, col++ ) );
int nBytes = atoi( PQgetvalue( result, ii, col++ ) );
RecordSent( connName, hid, nBytes );
} }
} }
PQclear( result ); PQclear( result );
@ -1014,15 +1041,16 @@ DBMgr::CountStoredMessages( DevIDRelay relayID )
return getCountWhere( MSGS_TABLE, test ); return getCountWhere( MSGS_TABLE, test );
} }
void int
DBMgr::StoreMessage( DevIDRelay destDevID, const uint8_t* const buf, DBMgr::StoreMessage( DevIDRelay destDevID, const uint8_t* const buf,
int len ) int len )
{ {
int msgID = 0;
clearHasNoMessages( destDevID ); clearHasNoMessages( destDevID );
size_t newLen; size_t newLen;
const char* fmt = "INSERT INTO " MSGS_TABLE " " const char* fmt = "INSERT INTO " MSGS_TABLE " "
"(devid, %s, msglen) VALUES(%d, %s'%s', %d)"; "(devid, %s, msglen) VALUES(%d, %s'%s', %d) RETURNING id";
StrWPF query; StrWPF query;
if ( m_useB64 ) { if ( m_useB64 ) {
@ -1038,13 +1066,20 @@ DBMgr::StoreMessage( DevIDRelay destDevID, const uint8_t* const buf,
} }
logf( XW_LOGINFO, "%s: query: %s", __func__, query.c_str() ); logf( XW_LOGINFO, "%s: query: %s", __func__, query.c_str() );
execSql( query );
PGresult* result = PQexec( getThreadConn(), query.c_str() );
if ( 1 == PQntuples( result ) ) {
msgID = atoi( PQgetvalue( result, 0, 0 ) );
}
PQclear( result );
return msgID;
} }
void int
DBMgr::StoreMessage( const char* const connName, int destHid, DBMgr::StoreMessage( const char* const connName, int destHid,
const uint8_t* buf, int len ) const uint8_t* buf, int len )
{ {
int msgID = 0;
clearHasNoMessages( connName, destHid ); clearHasNoMessages( connName, destHid );
DevIDRelay devID = getDevID( connName, destHid ); DevIDRelay devID = getDevID( connName, destHid );
@ -1074,7 +1109,7 @@ DBMgr::StoreMessage( const char* const connName, int destHid,
#ifdef HAVE_STIME #ifdef HAVE_STIME
" AND stime='epoch'" " AND stime='epoch'"
#endif #endif
" );", connName, destHid, b64 ); " )", connName, destHid, b64 );
g_free( b64 ); g_free( b64 );
} else { } else {
uint8_t* bytes = PQescapeByteaConn( getThreadConn(), buf, uint8_t* bytes = PQescapeByteaConn( getThreadConn(), buf,
@ -1085,9 +1120,17 @@ DBMgr::StoreMessage( const char* const connName, int destHid,
"E", bytes, len ); "E", bytes, len );
PQfreemem( bytes ); PQfreemem( bytes );
} }
query.catf(" RETURNING id;");
logf( XW_LOGINFO, "%s: query: %s", __func__, query.c_str() ); logf( XW_LOGINFO, "%s: query: %s", __func__, query.c_str() );
execSql( query ); PGresult* result = PQexec( getThreadConn(), query.c_str() );
if ( 1 == PQntuples( result ) ) {
msgID = atoi( PQgetvalue( result, 0, 0 ) );
} else {
logf( XW_LOGINFO, "Not stored; duplicate?" );
}
PQclear( result );
return msgID;
} }
void void

View file

@ -75,9 +75,13 @@ class DBMgr {
bool FindRelayIDFor( const char* connName, HostID hid, unsigned short seed, bool FindRelayIDFor( const char* connName, HostID hid, unsigned short seed,
const DevID* host, DevIDRelay* devID ); const DevID* host, DevIDRelay* devID );
CookieID FindGame( const char* connName, char* cookieBuf, int bufLen, CookieID FindGame( const char* connName, HostID hid, char* cookieBuf, int bufLen,
int* langP, int* nPlayersTP, int* nPlayersHP, int* langP, int* nPlayersTP, int* nPlayersHP,
bool* isDead ); bool* isDead );
CookieID FindGame( const AddrInfo::ClientToken clientToken, HostID hid,
char* connNameBuf, int connNameBufLen,
char* cookieBuf, int cookieBufLen,
int* langP, int* nPlayersTP, int* nPlayersHP );
bool FindGameFor( const char* connName, char* cookieBuf, int bufLen, bool FindGameFor( const char* connName, char* cookieBuf, int bufLen,
unsigned short seed, HostID hid, unsigned short seed, HostID hid,
@ -137,10 +141,10 @@ class DBMgr {
/* message storage -- different DB */ /* message storage -- different DB */
int CountStoredMessages( const char* const connName ); int CountStoredMessages( const char* const connName );
int CountStoredMessages( DevIDRelay relayID ); int CountStoredMessages( DevIDRelay relayID );
void StoreMessage( DevIDRelay destRelayID, const uint8_t* const buf, int StoreMessage( DevIDRelay destRelayID, const uint8_t* const buf,
int len ); int len );
void StoreMessage( const char* const connName, int destHid, int StoreMessage( const char* const connName, int destHid,
const uint8_t* const buf, int len ); const uint8_t* const buf, int len );
void GetStoredMessages( DevIDRelay relayID, vector<MsgInfo>& msgs ); void GetStoredMessages( DevIDRelay relayID, vector<MsgInfo>& msgs );
void GetStoredMessages( const char* const connName, HostID hid, void GetStoredMessages( const char* const connName, HostID hid,
vector<DBMgr::MsgInfo>& msgs ); vector<DBMgr::MsgInfo>& msgs );
@ -171,6 +175,7 @@ class DBMgr {
int clientVersion, const char* const model, int clientVersion, const char* const model,
const char* const osVers, DevIDRelay relayID ); const char* const osVers, DevIDRelay relayID );
PGconn* getThreadConn( void ); PGconn* getThreadConn( void );
void clearThreadConn(); void clearThreadConn();

255
xwords4/relay/scripts/relay.py Executable file
View file

@ -0,0 +1,255 @@
#!/usr/bin/python
import base64, json, mod_python, socket, struct, sys
import psycopg2, random
PROTOCOL_VERSION = 0
PRX_DEVICE_GONE = 3
PRX_GET_MSGS = 4
# try:
# from mod_python import apache
# apacheAvailable = True
# except ImportError:
# apacheAvailable = False
# Joining a game. Basic idea is you have stuff to match on (room,
# number in game, language) and when somebody wants to join you add to
# an existing matching game if there's space otherwise create a new
# one. Problems are the unreliablity of transport: if you give a space
# and the device doesn't get the message you can't hold it forever. So
# device provides a seed that holds the space. If it asks again for a
# space with the same seed it gets the same space. If it never asks
# again (app deleted, say), the space needs eventually to be given to
# somebody else. I think that's done by adding a timestamp array and
# treating the space as available if TIME has expired. Need to think
# about this: what if app fails to ACK for TIME, then returns with
# seed to find it given away. Let's do a 30 minute reservation for
# now? [Note: much of this is PENDING]
def join(req, devID, room, seed, hid = 0, lang = 1, nInGame = 2, nHere = 1, inviteID = None):
assert hid <= 4
seed = int(seed)
assert seed != 0
nInGame = int(nInGame)
nHere = int(nHere)
assert nHere <= nInGame
assert nInGame <= 4
devID = int(devID, 16)
connname = None
logs = [] # for debugging
# logs.append('vers: ' + platform.python_version())
con = psycopg2.connect(database='xwgames')
cur = con.cursor()
# cur.execute('LOCK TABLE games IN ACCESS EXCLUSIVE MODE')
# First see if there's a game with a space for me. Must match on
# room, lang and size. Must have room OR must have already given a
# spot for a seed equal to mine, in which case I get it
# back. Assumption is I didn't ack in time.
query = "SELECT connname, seeds, nperdevice FROM games "
query += "WHERE lang = %s AND nTotal = %s AND room = %s "
query += "AND (njoined + %s <= ntotal OR %s = ANY(seeds)) "
query += "LIMIT 1"
cur.execute( query, (lang, nInGame, room, nHere, seed))
for row in cur:
(connname, seeds, nperdevice) = row
print('found', connname, seeds, nperdevice)
break # should be only one!
# If we've found a match, we either need to UPDATE or, if the
# seeds match, remind the caller of where he belongs. If a hid's
# been specified, we honor it by updating if the slot's available;
# otherwise a new game has to be created.
if connname:
if seed in seeds and nHere == nperdevice[seeds.index(seed)]:
hid = seeds.index(seed) + 1
print('resusing seed case; outta here!')
else:
if hid == 0:
# Any gaps? Assign it
if None in seeds:
hid = seeds.index(None) + 1
else:
hid = len(seeds) + 1
print('set hid to', hid, 'based on ', seeds)
else:
print('hid already', hid)
query = "UPDATE games SET njoined = njoined + %s, "
query += "devids[%d] = %%s, " % hid
query += "seeds[%d] = %%s, " % hid
query += "jtimes[%d] = 'now', " % hid
query += "nperdevice[%d] = %%s " % hid
query += "WHERE connname = %s "
print(query)
params = (nHere, devID, seed, nHere, connname)
cur.execute(query, params)
# If nothing was found, add a new game and add me. Honor my hid
# preference if specified
if not connname:
# This requires python3, which likely requires mod_wsgi
# ts = datetime.datetime.utcnow().timestamp()
# connname = '%s:%d:1' % (xwconfig.k_HOSTNAME, int(ts * 1000))
connname = '%s:%d:1' % (xwconfig.k_HOSTNAME, random.randint(0, 10000000000))
useHid = hid == 0 and 1 or hid
print('not found case; inserting using hid:', useHid)
query = "INSERT INTO games (connname, room, lang, ntotal, njoined, " + \
"devids[%d], seeds[%d], jtimes[%d], nperdevice[%d]) " % (4 * (useHid,))
query += "VALUES (%s, %s, %s, %s, %s, %s, %s, 'now', %s) "
query += "RETURNING connname, array_length(seeds,1); "
cur.execute(query, (connname, room, lang, nInGame, nHere, devID, seed, nHere))
for row in cur:
connname, gothid = row
break
if hid == 0: hid = gothid
con.commit()
con.close()
result = {'connname': connname, 'hid' : hid, 'log' : ':'.join(logs)}
return json.dumps(result)
def kill(req, params):
print(params)
params = json.loads(params)
count = len(params)
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.connect(('127.0.0.1', 10998))
header = struct.Struct('!BBh')
strLens = 0
for ii in range(count):
strLens += len(params[ii]['relayID']) + 1
size = header.size + (2*count) + strLens
sock.send(struct.Struct('!h').pack(size))
sock.send(header.pack(PROTOCOL_VERSION, PRX_DEVICE_GONE, count))
for ii in range(count):
elem = params[ii]
asBytes = bytes(elem['relayID'])
sock.send(struct.Struct('!H%dsc' % (len(asBytes))).pack(elem['seed'], asBytes, '\n'))
sock.close()
result = {'err': 0}
return json.dumps(result)
# winds up in handle_udp_packet() in xwrelay.cpp
def post(req, params):
err = 'none'
params = json.loads(params)
data = params['data']
timeoutSecs = 'timeoutSecs' in params and params['timeoutSecs'] or 1.0
binData = [base64.b64decode(datum) for datum in data]
udpSock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
udpSock.settimeout(float(timeoutSecs)) # seconds
addr = ("127.0.0.1", 10997)
for binDatum in binData:
udpSock.sendto(binDatum, addr)
responses = []
while True:
try:
data, server = udpSock.recvfrom(1024)
responses.append(base64.b64encode(data))
except socket.timeout:
#If data is not received back from server, print it has timed out
err = 'timeout'
break
result = {'err' : err, 'data' : responses}
return json.dumps(result)
def query(req, params):
print('params', params)
params = json.loads(params)
ids = params['ids']
# timeoutSecs = 'timeoutSecs' in params and float(params['timeoutSecs']) or 2.0
idsLen = 0
for id in ids: idsLen += len(id)
tcpSock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
# tcpSock.settimeout(timeoutSecs)
tcpSock.connect(('127.0.0.1', 10998))
lenShort = 2 + idsLen + len(ids) + 2
print(lenShort, PROTOCOL_VERSION, PRX_GET_MSGS, len(ids))
header = struct.Struct('!hBBh')
assert header.size == 6
tcpSock.send(header.pack(lenShort, PROTOCOL_VERSION, PRX_GET_MSGS, len(ids)))
for id in ids: tcpSock.send(id + '\n')
result = {'ids':ids}
try:
msgsLists = {}
shortUnpacker = struct.Struct('!H')
resLen, = shortUnpacker.unpack(tcpSock.recv(shortUnpacker.size)) # not getting all bytes
nameCount, = shortUnpacker.unpack(tcpSock.recv(shortUnpacker.size))
resLen -= shortUnpacker.size
print('resLen:', resLen, 'nameCount:', nameCount)
if nameCount == len(ids) and resLen > 0:
print('nameCount', nameCount)
for ii in range(nameCount):
perGame = []
countsThisGame, = shortUnpacker.unpack(tcpSock.recv(shortUnpacker.size)) # problem
print('countsThisGame:', countsThisGame)
for jj in range(countsThisGame):
msgLen, = shortUnpacker.unpack(tcpSock.recv(shortUnpacker.size))
print('msgLen:', msgLen)
msgs = []
if msgLen > 0:
msg = tcpSock.recv(msgLen)
print('msg len:', len(msg))
msg = base64.b64encode(msg)
msgs.append(msg)
perGame.append(msgs)
msgsLists[ids[ii]] = perGame
result['msgs'] = msgsLists
except:
# Anything but a timeout should mean we abort/send nothing
result['err'] = 'hit exception'
None
return json.dumps(result)
def main():
result = None
if len(sys.argv) > 1:
cmd = sys.argv[1]
args = sys.argv[2:]
if cmd == 'query' and len(args) > 0:
result = query(None, json.dumps({'ids':args}))
elif cmd == 'post':
# Params = { 'data' : 'V2VkIE9jdCAxOCAwNjowNDo0OCBQRFQgMjAxNwo=' }
# params = json.dumps(params)
# print(post(None, params))
pass
elif cmd == 'join':
if len(args) == 6:
result = join(None, 1, args[0], int(args[1]), int(args[2]), int(args[3]), int(args[4]), int(args[5]))
elif cmd == 'kill':
result = kill( None, json.dumps([{'relayID': args[0], 'seed':int(args[1])}]) )
if result:
print '->', result
else:
print 'USAGE: query [connname/hid]*'
print ' join <roomName> <seed> <hid> <lang> <nTotal> <nHere>'
print ' query [connname/hid]*'
# print ' post '
print ' kill <relayID> <seed>'
print ' join <roomName> <seed> <hid> <lang> <nTotal> <nHere>'
##############################################################################
if __name__ == '__main__':
main()

View file

@ -51,16 +51,18 @@ fi
echo -n "Device (pid) count: $(pidof xwords | wc | awk '{print $2}')" echo -n "Device (pid) count: $(pidof xwords | wc | awk '{print $2}')"
echo "; relay pid[s]: $(pidof xwrelay)" echo "; relay pid[s]: $(pidof xwrelay)"
echo "Row count:" $(psql -t xwgames -c "select count(*) FROM games $QUERY;") echo -n "Row count:" $(psql -t xwgames -c "select count(*) FROM games $QUERY;")
echo "; Relay sockets: $(for PID in $(pidof xwrelay); do ls /proc/$PID/fd; done | sort -un | tr '\n' ' ')"
# Games # Games
echo "SELECT dead as d,connname,cid,room,lang as lg,clntVers as cv ,ntotal as t,nperdevice as nPerDev,nsents as snts, seeds,devids,tokens,ack, mtimes "\ echo "SELECT dead as d,connname,cid,room,lang as lg,clntVers as cv ,ntotal as t,nperdevice as npd,nsents as snts, seeds,devids,tokens,ack, mtimes "\
"FROM games $QUERY ORDER BY NOT dead, ctime DESC LIMIT $LIMIT;" \ "FROM games $QUERY ORDER BY NOT dead, ctime DESC LIMIT $LIMIT;" \
| psql xwgames | psql xwgames
# Messages # Messages
echo "SELECT * "\ echo "Unack'd msgs count:" $(psql -t xwgames -c "select count(*) FROM msgs where stime = 'epoch' AND connname IN (SELECT connname from games $QUERY);")
"FROM msgs WHERE connname IN (SELECT connname from games $QUERY) "\ echo "SELECT id,connName,hid as h,token,ctime,stime,devid,msg64 "\
"FROM msgs WHERE stime = 'epoch' AND connname IN (SELECT connname from games $QUERY) "\
"ORDER BY ctime DESC, connname LIMIT $LIMIT;" \ "ORDER BY ctime DESC, connname LIMIT $LIMIT;" \
| psql xwgames | psql xwgames

View file

@ -119,13 +119,17 @@ XWThreadPool::Stop()
void void
XWThreadPool::AddSocket( SockType stype, QueueCallback proc, const AddrInfo* from ) XWThreadPool::AddSocket( SockType stype, QueueCallback proc, const AddrInfo* from )
{ {
from->ref();
int sock = from->getSocket();
logf( XW_LOGVERBOSE0, "%s(sock=%d, isTCP=%d)", __func__, sock, from->isTCP() );
SockInfo si = { .m_type = stype,
.m_proc = proc,
.m_addr = *from
};
{ {
int sock = from->getSocket();
RWWriteLock ml( &m_activeSocketsRWLock ); RWWriteLock ml( &m_activeSocketsRWLock );
SockInfo si; assert( m_activeSockets.find( sock ) == m_activeSockets.end() );
si.m_type = stype;
si.m_proc = proc;
si.m_addr = *from;
m_activeSockets.insert( pair<int, SockInfo>( sock, si ) ); m_activeSockets.insert( pair<int, SockInfo>( sock, si ) );
} }
interrupt_poll(); interrupt_poll();
@ -158,13 +162,14 @@ XWThreadPool::RemoveSocket( const AddrInfo* addr )
size_t prevSize = m_activeSockets.size(); size_t prevSize = m_activeSockets.size();
map<int, SockInfo>::iterator iter = m_activeSockets.find( addr->getSocket() ); int sock = addr->getSocket();
map<int, SockInfo>::iterator iter = m_activeSockets.find( sock );
if ( m_activeSockets.end() != iter && iter->second.m_addr.equals( *addr ) ) { if ( m_activeSockets.end() != iter && iter->second.m_addr.equals( *addr ) ) {
m_activeSockets.erase( iter ); m_activeSockets.erase( iter );
found = true; found = true;
} }
logf( XW_LOGINFO, "%s: AFTER: %d sockets active (was %d)", __func__, logf( XW_LOGINFO, "%s(): AFTER closing %d: %d sockets active (was %d)", __func__,
m_activeSockets.size(), prevSize ); sock, m_activeSockets.size(), prevSize );
} }
return found; return found;
} /* RemoveSocket */ } /* RemoveSocket */
@ -184,8 +189,14 @@ XWThreadPool::CloseSocket( const AddrInfo* addr )
++iter; ++iter;
} }
} }
logf( XW_LOGINFO, "CLOSING socket %d", addr->getSocket() ); int sock = addr->getSocket();
close( addr->getSocket() ); int err = close( sock );
if ( 0 != err ) {
logf( XW_LOGERROR, "%s(): close(socket=%d) => %d/%s", __func__,
sock, errno, strerror(errno) );
} else {
logf( XW_LOGINFO, "%s(): close(socket=%d) succeeded", __func__, sock );
}
/* We always need to interrupt the poll because the socket we're closing /* We always need to interrupt the poll because the socket we're closing
will be in the list being listened to. That or we need to drop sockets will be in the list being listened to. That or we need to drop sockets
@ -198,7 +209,7 @@ XWThreadPool::CloseSocket( const AddrInfo* addr )
void void
XWThreadPool::EnqueueKill( const AddrInfo* addr, const char* const why ) XWThreadPool::EnqueueKill( const AddrInfo* addr, const char* const why )
{ {
logf( XW_LOGINFO, "%s(%d) reason: %s", __func__, addr->getSocket(), why ); logf( XW_LOGINFO, "%s(socket = %d) reason: %s", __func__, addr->getSocket(), why );
if ( addr->isTCP() ) { if ( addr->isTCP() ) {
SockInfo si; SockInfo si;
si.m_type = STYPE_UNKNOWN; si.m_type = STYPE_UNKNOWN;
@ -265,7 +276,6 @@ XWThreadPool::real_tpool_main( ThreadInfo* tip )
if ( gotOne ) { if ( gotOne ) {
sock = pr.m_info.m_addr.getSocket(); sock = pr.m_info.m_addr.getSocket();
logf( XW_LOGINFO, "worker thread got socket %d from queue", socket );
switch ( pr.m_act ) { switch ( pr.m_act ) {
case Q_READ: case Q_READ:
assert( 0 ); assert( 0 );
@ -275,8 +285,9 @@ XWThreadPool::real_tpool_main( ThreadInfo* tip )
// } // }
break; break;
case Q_KILL: case Q_KILL:
logf( XW_LOGINFO, "worker thread got socket %d from queue (to close it)", sock );
(*m_kFunc)( &pr.m_info.m_addr ); (*m_kFunc)( &pr.m_info.m_addr );
CloseSocket( &pr.m_info.m_addr ); pr.m_info.m_addr.unref();
break; break;
} }
} else { } else {
@ -392,35 +403,40 @@ XWThreadPool::real_listener()
curfd = 1; curfd = 1;
int ii; int ii;
for ( ii = 0; ii < nSockets && nEvents > 0; ++ii ) { for ( ii = 0; ii < nSockets && nEvents > 0; ++ii, ++curfd ) {
if ( fds[curfd].revents != 0 ) { if ( fds[curfd].revents != 0 ) {
// int socket = fds[curfd].fd; // int socket = fds[curfd].fd;
SockInfo* sinfo = &sinfos[curfd]; SockInfo* sinfo = &sinfos[curfd];
const AddrInfo* addr = &sinfo->m_addr; const AddrInfo* addr = &sinfo->m_addr;
assert( fds[curfd].fd == addr->getSocket() ); int sock = addr->getSocket();
assert( fds[curfd].fd == sock );
if ( !SocketFound( addr ) ) { if ( !SocketFound( addr ) ) {
logf( XW_LOGINFO, "%s(): dropping socket %d: not found",
__func__, addr->getSocket() );
/* no further processing if it's been removed while /* no further processing if it's been removed while
we've been sleeping in poll */ we've been sleeping in poll. BUT: shouldn't curfd
be incremented?? */
--nEvents; --nEvents;
continue; continue;
} }
if ( 0 != (fds[curfd].revents & (POLLIN | POLLPRI)) ) { if ( 0 != (fds[curfd].revents & (POLLIN | POLLPRI)) ) {
if ( !UdpQueue::get()->handle( addr, sinfo->m_proc ) ) { if ( !UdpQueue::get()->handle( addr, sinfo->m_proc ) ) {
// This is likely wrong!!! return of 0 means
// remote closed, not error.
RemoveSocket( addr ); RemoveSocket( addr );
EnqueueKill( addr, "bad packet" ); EnqueueKill( addr, "got EOF" );
} }
} else { } else {
logf( XW_LOGERROR, "odd revents: %x", logf( XW_LOGERROR, "%s(): odd revents: %x; bad socket %d",
fds[curfd].revents ); __func__, fds[curfd].revents, sock );
RemoveSocket( addr ); RemoveSocket( addr );
EnqueueKill( addr, "error/hup in poll()" ); EnqueueKill( addr, "error/hup in poll()" );
} }
--nEvents; --nEvents;
} }
++curfd;
} }
assert( nEvents == 0 ); assert( nEvents == 0 );
} }

View file

@ -28,7 +28,7 @@ static UdpQueue* s_instance = NULL;
void void
UdpThreadClosure::logStats() PacketThreadClosure::logStats()
{ {
time_t now = time( NULL ); time_t now = time( NULL );
if ( 1 < now - m_created ) { if ( 1 < now - m_created ) {
@ -48,6 +48,7 @@ PartialPacket::stillGood() const
bool bool
PartialPacket::readAtMost( int len ) PartialPacket::readAtMost( int len )
{ {
assert( len > 0 );
bool success = false; bool success = false;
uint8_t tmp[len]; uint8_t tmp[len];
ssize_t nRead = recv( m_sock, tmp, len, 0 ); ssize_t nRead = recv( m_sock, tmp, len, 0 );
@ -57,10 +58,12 @@ PartialPacket::readAtMost( int len )
logf( XW_LOGERROR, "%s(len=%d, socket=%d): recv failed: %d (%s)", __func__, logf( XW_LOGERROR, "%s(len=%d, socket=%d): recv failed: %d (%s)", __func__,
len, m_sock, m_errno, strerror(m_errno) ); len, m_sock, m_errno, strerror(m_errno) );
} }
} else if ( 0 == nRead ) { // remote socket closed } else if ( 0 == nRead ) { // remote socket half-closed
logf( XW_LOGINFO, "%s: remote closed (socket=%d)", __func__, m_sock ); logf( XW_LOGINFO, "%s(): remote closed (socket=%d)", __func__, m_sock );
m_errno = -1; // so stillGood will fail m_errno = -1; // so stillGood will fail
} else { } else {
// logf( XW_LOGVERBOSE0, "%s(): read %d bytes on socket %d", __func__,
// nRead, m_sock );
m_errno = 0; m_errno = 0;
success = len == nRead; success = len == nRead;
int curSize = m_buf.size(); int curSize = m_buf.size();
@ -100,7 +103,11 @@ UdpQueue::get()
return s_instance; return s_instance;
} }
// return false if socket should no longer be used // If we're already assembling data from this socket, continue. Otherwise
// create a new parital packet and store data there. If we wind up with a
// complete packet, dispatch it and delete since the data's been delivered.
//
// Return false if socket should no longer be used.
bool bool
UdpQueue::handle( const AddrInfo* addr, QueueCallback cb ) UdpQueue::handle( const AddrInfo* addr, QueueCallback cb )
{ {
@ -145,6 +152,7 @@ UdpQueue::handle( const AddrInfo* addr, QueueCallback cb )
} }
success = success && (NULL == packet || packet->stillGood()); success = success && (NULL == packet || packet->stillGood());
logf( XW_LOGVERBOSE0, "%s(sock=%d) => %d", __func__, sock, success );
return success; return success;
} }
@ -152,17 +160,21 @@ void
UdpQueue::handle( const AddrInfo* addr, const uint8_t* buf, int len, UdpQueue::handle( const AddrInfo* addr, const uint8_t* buf, int len,
QueueCallback cb ) QueueCallback cb )
{ {
UdpThreadClosure* utc = new UdpThreadClosure( addr, buf, len, cb ); // addr->ref();
PacketThreadClosure* ptc = new PacketThreadClosure( addr, buf, len, cb );
MutexLock ml( &m_queueMutex ); MutexLock ml( &m_queueMutex );
int id = ++m_nextID; int id = ++m_nextID;
utc->setID( id ); ptc->setID( id );
logf( XW_LOGINFO, "%s: enqueuing packet %d (socket %d, len %d)", logf( XW_LOGINFO, "%s(): enqueuing packet %d (socket %d, len %d)",
__func__, id, addr->getSocket(), len ); __func__, id, addr->getSocket(), len );
m_queue.push_back( utc ); m_queue.push_back( ptc );
pthread_cond_signal( &m_queueCondVar ); pthread_cond_signal( &m_queueCondVar );
} }
// Remove any PartialPacket record with the same socket/fd. This makes sense
// when the socket's being reused or when we have just dealt with a single
// packet and might be getting more.
void void
UdpQueue::newSocket_locked( int sock ) UdpQueue::newSocket_locked( int sock )
{ {
@ -194,25 +206,26 @@ UdpQueue::thread_main()
while ( m_queue.size() == 0 ) { while ( m_queue.size() == 0 ) {
pthread_cond_wait( &m_queueCondVar, &m_queueMutex ); pthread_cond_wait( &m_queueCondVar, &m_queueMutex );
} }
UdpThreadClosure* utc = m_queue.front(); PacketThreadClosure* ptc = m_queue.front();
m_queue.pop_front(); m_queue.pop_front();
pthread_mutex_unlock( &m_queueMutex ); pthread_mutex_unlock( &m_queueMutex );
utc->noteDequeued(); ptc->noteDequeued();
time_t age = utc->ageInSeconds(); time_t age = ptc->ageInSeconds();
if ( 30 > age ) { if ( 30 > age ) {
logf( XW_LOGINFO, "%s: dispatching packet %d (socket %d); " logf( XW_LOGINFO, "%s: dispatching packet %d (socket %d); "
"%d seconds old", __func__, utc->getID(), "%d seconds old", __func__, ptc->getID(),
utc->addr()->getSocket(), age ); ptc->addr()->getSocket(), age );
(*utc->cb())( utc ); (*ptc->cb())( ptc );
utc->logStats(); ptc->logStats();
} else { } else {
logf( XW_LOGINFO, "%s: dropping packet %d; it's %d seconds old!", logf( XW_LOGINFO, "%s: dropping packet %d; it's %d seconds old!",
__func__, age ); __func__, age );
} }
delete utc; // ptc->addr()->unref();
delete ptc;
} }
return NULL; return NULL;
} }

View file

@ -30,13 +30,13 @@
using namespace std; using namespace std;
class UdpThreadClosure; class PacketThreadClosure;
typedef void (*QueueCallback)( UdpThreadClosure* closure ); typedef void (*QueueCallback)( PacketThreadClosure* closure );
class UdpThreadClosure { class PacketThreadClosure {
public: public:
UdpThreadClosure( const AddrInfo* addr, const uint8_t* buf, PacketThreadClosure( const AddrInfo* addr, const uint8_t* buf,
int len, QueueCallback cb ) int len, QueueCallback cb )
: m_buf(new uint8_t[len]) : m_buf(new uint8_t[len])
, m_len(len) , m_len(len)
@ -45,9 +45,13 @@ public:
, m_created(time( NULL )) , m_created(time( NULL ))
{ {
memcpy( m_buf, buf, len ); memcpy( m_buf, buf, len );
m_addr.ref();
} }
~UdpThreadClosure() { delete[] m_buf; } ~PacketThreadClosure() {
m_addr.unref();
delete[] m_buf;
}
const uint8_t* buf() const { return m_buf; } const uint8_t* buf() const { return m_buf; }
int len() const { return m_len; } int len() const { return m_len; }
@ -109,8 +113,8 @@ class UdpQueue {
pthread_mutex_t m_partialsMutex; pthread_mutex_t m_partialsMutex;
pthread_mutex_t m_queueMutex; pthread_mutex_t m_queueMutex;
pthread_cond_t m_queueCondVar; pthread_cond_t m_queueCondVar;
deque<UdpThreadClosure*> m_queue; deque<PacketThreadClosure*> m_queue;
// map<int, vector<UdpThreadClosure*> > m_bySocket; // map<int, vector<PacketThreadClosure*> > m_bySocket;
int m_nextID; int m_nextID;
map<int, PartialPacket*> m_partialPackets; map<int, PartialPacket*> m_partialPackets;
}; };

Some files were not shown because too many files have changed in this diff Show more