Merge remote-tracking branch 'origin/android_translate' into android_translate

This commit is contained in:
Weblate 2017-12-01 04:25:46 +01:00
commit 6ffd4d4a37
77 changed files with 2168 additions and 437 deletions

40
.travis.yml Normal file
View file

@ -0,0 +1,40 @@
language: android
jdk: oraclejdk8
env:
global:
- ANDROID_TARGET=android-15
- ANDROID_ABI=armeabi-v7a,x86
- secure: d8PwteM+xp1IRU3QkvmHtxh+1Ta9n/kl/SJ3EZa3iColVVXY1etzjY3cKrEGKKMJuI4be30kPzvNw9/BVTawDpnU9/NtWqykJ8QHXNWnZIvUQ/kxHBS1DbcstmcYU9gvR83EFb8BT+Y9frpNfMcZDlSvBpEGqDQEPmxiDzSmjdUmJJQWStncxL9pE+lCdM6lHBgtfYoMMiqCQF/DxkQisjyUVF4mbTGuT9JOOWjVsTGPA7ehzsWDHoJ3p2ai8UKHAYucUWZcTt4rkq9l35ExvgKd3L8luk8U3X3Fk9yzVhPJC56T0XNbNrsQ2W7/7oGRv6EQFV3aKDZimJ7CVjBcEjZmPxeUVvCsMW8XB41ZvYcy6xsjF96oyjn1gb0r/2mZbTaWP0izSTwMYZ5vFNKUamDtRZgrneD0lfvXgfTzirrCU7FqO2RH7ZK5PQpSgSoZxKsKyeyFPEa2ihivc95rz1MS6mamle9wrIlSAgEGcaZMIYvKiOnCLk7CZCKuwm2dhYPgzCHW3PUopay59BBwMsSqWpxsiHEr5jYGpb0pHGbzPTJNUpg1LNQX5eMQOMlEt7rfpoC7JG24hR9vxl4Yf9LhxYlSwUiPy7TYHdbA0kUS68skfzxU6+ekWZF2QFM+L4vWCYmEHDy7n+I0df+PavycgNW989ROlAKhQjtMyqM=
android:
components:
- tools
- platform-tools
- build-tools-23.0.3
- android-23
before_script:
- export TERM=dumb
- curl -L http://dl.google.com/android/ndk/android-ndk-r10e-linux-x86_64.bin -O
- chmod u+x android-ndk-r10e-linux-x86_64.bin
- "./android-ndk-r10e-linux-x86_64.bin > /dev/null"
- rm android-ndk-r10e-linux-x86_64.bin
- export ANDROID_NDK_HOME=`pwd`/android-ndk-r10e
- export LOCAL_ANDROID_NDK_HOME="$ANDROID_NDK_HOME"
- export LOCAL_ANDROID_NDK_HOST_PLATFORM="linux-x86_64"
- export PATH=$PATH:${ANDROID_NDK_HOME}
- cd xwords4/android/
before_install:
- openssl aes-256-cbc -K $encrypted_8436f2891714_key -iv $encrypted_8436f2891714_iv
-in id_rsa_uploader.enc -out /tmp/id_rsa_uploader -d
- chmod 600 \/tmp\/id_rsa_uploader
- sudo apt-get -qq update
- sudo apt-get install -y python-lxml imagemagick
script:
- "./gradlew -PuseCrashlytics assXw4dDeb"
- scp -o "StrictHostKeyChecking no" -i /tmp/id_rsa_uploader -d app/build/outputs/apk/*.apk
uploader@eehouse.org:XW4D_UPLOAD
notifications:
email:
recipients:
- xwords@eehouse.org
on_success: always
on_failure: always

BIN
id_rsa_uploader.enc Normal file

Binary file not shown.

View file

@ -1,7 +1,9 @@
def INITIAL_CLIENT_VERS = 8 def INITIAL_CLIENT_VERS = 8
def VERSION_CODE_BASE = 121 def VERSION_CODE_BASE = 126
def VERSION_NAME = '4.4.125' def VERSION_NAME = '4.4.130'
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 BUILD_INFO_NAME = "build-info.txt"
boolean forFDroid = hasProperty('forFDroid') boolean forFDroid = hasProperty('forFDroid')
@ -37,8 +39,6 @@ android {
applicationVariants.all { variant -> applicationVariants.all { variant ->
// renameArtifact(variant) // renameArtifact(variant)
// variant.buildConfigField "String", "FIELD_NAME", "\"my String\"" // variant.buildConfigField "String", "FIELD_NAME", "\"my String\""
def GCM_SENDER_ID = System.getenv("GCM_SENDER_ID")
variant.buildConfigField "String", "SENDER_ID", "\"$GCM_SENDER_ID\""
variant.buildConfigField "String", "FABRIC_API_KEY", "\"$FABRIC_API_KEY\"" variant.buildConfigField "String", "FABRIC_API_KEY", "\"$FABRIC_API_KEY\""
resValue "string", "git_rev", "$GITREV" resValue "string", "git_rev", "$GITREV"
@ -51,9 +51,6 @@ android {
// FIX ME // FIX ME
variant.buildConfigField "String", "STRINGS_HASH", "\"00000\"" variant.buildConfigField "String", "STRINGS_HASH", "\"00000\""
def senderID = System.getenv("GCM_SENDER_ID")
variant.buildConfigField "String", "GCM_SENDER_ID", "\"$senderID\""
variant.buildConfigField "short", "CLIENT_VERS_RELAY", "$INITIAL_CLIENT_VERS" variant.buildConfigField "short", "CLIENT_VERS_RELAY", "$INITIAL_CLIENT_VERS"
variant.buildConfigField "boolean", "FOR_FDROID", "$forFDroid" variant.buildConfigField "boolean", "FOR_FDROID", "$forFDroid"
@ -61,6 +58,10 @@ android {
flavorDimensions "variant"//, "abi" flavorDimensions "variant"//, "abi"
productFlavors { productFlavors {
all {
buildConfigField "String", "BUILD_INFO_NAME", "\"${BUILD_INFO_NAME}\""
}
xw4 { xw4 {
dimension "variant" dimension "variant"
applicationId "org.eehouse.android.xw4" applicationId "org.eehouse.android.xw4"
@ -70,6 +71,8 @@ android {
resValue "string", "invite_prefix", "/and/" resValue "string", "invite_prefix", "/and/"
buildConfigField "boolean", "WIDIR_ENABLED", "false" buildConfigField "boolean", "WIDIR_ENABLED", "false"
buildConfigField "boolean", "RELAYINVITE_SUPPORTED", "false" buildConfigField "boolean", "RELAYINVITE_SUPPORTED", "false"
buildConfigField "String", "GCM_SENDER_ID", "\"$GCM_SENDER_ID\""
} }
xw4d { xw4d {
dimension "variant" dimension "variant"
@ -81,6 +84,8 @@ android {
resValue "string", "invite_prefix", "/anddbg/" resValue "string", "invite_prefix", "/anddbg/"
buildConfigField "boolean", "WIDIR_ENABLED", "true" buildConfigField "boolean", "WIDIR_ENABLED", "true"
buildConfigField "boolean", "RELAYINVITE_SUPPORTED", "true" buildConfigField "boolean", "RELAYINVITE_SUPPORTED", "true"
buildConfigField "String", "GCM_SENDER_ID", "\"\""
} }
// WARNING: "all" breaks things. Seems to be a keyword. Need // WARNING: "all" breaks things. Seems to be a keyword. Need
@ -254,9 +259,14 @@ afterEvaluate {
task makeBuildAssets() { task makeBuildAssets() {
def assetsDir = android.sourceSets.main.assets.srcDirs.toArray()[0] def assetsDir = android.sourceSets.main.assets.srcDirs.toArray()[0]
String path = new File(assetsDir, 'build-info.txt').getAbsolutePath() String path = new File(assetsDir, BUILD_INFO_NAME).getAbsolutePath()
File file = new File(path); String out = "git: ${GITREV}\n"
file.write("git: ${GITREV}\n");
String diff = "git diff".execute().text.trim()
if (diff) {
out += "\n" + diff
}
new File(path).write(out)
} }
gradle.projectsEvaluated { gradle.projectsEvaluated {

View file

@ -34,13 +34,7 @@
/> />
<uses-feature android:name="android.hardware.nfc" android:required="false" /> <uses-feature android:name="android.hardware.nfc" android:required="false" />
<!-- GCM stuff -->
<permission android:name="${APP_ID}.permission.C2D_MESSAGE"
android:protectionLevel="signature" />
<uses-permission android:name="${APP_ID}.permission.C2D_MESSAGE" />
<uses-permission android:name="com.google.android.c2dm.permission.RECEIVE" />
<uses-permission android:name="android.permission.GET_ACCOUNTS" /> <uses-permission android:name="android.permission.GET_ACCOUNTS" />
<uses-permission android:name="android.permission.WAKE_LOCK" />
<uses-permission android:name="android.permission.NFC" /> <uses-permission android:name="android.permission.NFC" />
<application android:icon="@drawable/icon48x48" <application android:icon="@drawable/icon48x48"
@ -208,16 +202,5 @@
</intent-filter> </intent-filter>
</receiver> </receiver>
<receiver android:name="com.google.android.gcm.GCMBroadcastReceiver"
android:permission="com.google.android.c2dm.permission.SEND" >
<intent-filter>
<action android:name="com.google.android.c2dm.intent.RECEIVE" />
<action android:name="com.google.android.c2dm.intent.REGISTRATION" />
<category android:name="${APP_ID}" />
</intent-filter>
</receiver>
<service android:name=".GCMIntentService" />
</application> </application>
</manifest> </manifest>

View file

@ -13,10 +13,9 @@
</style> </style>
</head> </head>
<body> <body>
<h2>CrossWords 4.4.125 release</h2> <h2>CrossWords 4.4.130 release</h2>
<p>This release fixes a problem inviting to new networked games, and <p>This release makes a couple of small UI tweaks.</p>
with title bars on some Samsung devices.</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
@ -26,10 +25,12 @@
<h3>New with this release</h3> <h3>New with this release</h3>
<ul> <ul>
<li>Fix delays bringing up the Invite dialog for new games</li> <li>Offer to &quot;Archive&quot; finished games</li>
<li>Explicitly specify application "theme" to fix a Samsung <li>Make tap on thumbnail toggle whether game's selected rather
"upgrade" turning the titlebar white and so making menu than open it. (Tap to the right still opens)</li>
icons disappear</li> <li>Bug fix: don't allow duplicate group names</li>
<li>Fix battery-hogging behavior on non-Google-play
installs</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,10 +573,8 @@ 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 );
if ( nRead == len ) {
BluetoothDevice host = socket.getRemoteDevice(); BluetoothDevice host = socket.getRemoteDevice();
addAddr( host ); addAddr( host );
@ -589,10 +587,6 @@ public class BTService extends XWService {
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

@ -322,28 +322,37 @@ public class BoardCanvas extends Canvas implements DrawCtx {
} }
} }
public void drawTimer( Rect rect, int player, int secondsLeft ) public void drawTimer( Rect rect, final int player,
int secondsLeft )
{ {
if ( null != m_jniThread && if ( m_lastSecsLeft != secondsLeft || m_lastTimerPlayer != player ) {
(m_lastSecsLeft != secondsLeft || m_lastTimerPlayer != player) ) { final Rect rectCopy = new Rect(rect);
m_lastSecsLeft = secondsLeft; final int secondsLeftCopy = secondsLeft;
m_activity.runOnUiThread( new Runnable() {
@Override
public void run() {
if ( null != m_jniThread ) {
m_lastSecsLeft = secondsLeftCopy;
m_lastTimerPlayer = player; m_lastTimerPlayer = player;
String negSign = secondsLeft < 0? "-":""; String negSign = secondsLeftCopy < 0? "-":"";
secondsLeft = Math.abs( secondsLeft ); int secondsLeft = Math.abs( secondsLeftCopy );
String time = String.format( "%s%d:%02d", negSign, secondsLeft/60, String time =
secondsLeft%60 ); String.format( "%s%d:%02d", negSign,
secondsLeft/60, secondsLeft%60 );
fillRectOther( rect, CommonPrefs.COLOR_BACKGRND ); fillRectOther( rectCopy, CommonPrefs.COLOR_BACKGRND );
m_fillPaint.setColor( m_playerColors[player] ); m_fillPaint.setColor( m_playerColors[player] );
Rect shorter = new Rect( rect ); rectCopy.inset( 0, rectCopy.height() / 5 );
shorter.inset( 0, shorter.height() / 5 ); drawCentered( time, rectCopy, null );
drawCentered( time, shorter, null );
m_jniThread.handle( JNIThread.JNICmd.CMD_DRAW ); m_jniThread.handle( JNIThread.JNICmd.CMD_DRAW );
} }
} }
} );
}
}
public boolean drawCell( Rect rect, String text, int tile, int value, public boolean drawCell( Rect rect, String text, int tile, int value,
int owner, int bonus, int hintAtts, int flags ) int owner, int bonus, int hintAtts, int flags )

View file

@ -104,7 +104,6 @@ public class BoardDelegate extends DelegateBase
private Button m_exchCancelButton; private Button m_exchCancelButton;
private SentInvitesInfo m_sentInfo; private SentInvitesInfo m_sentInfo;
private Perms23.PermCbck m_permCbck; private Perms23.PermCbck m_permCbck;
private ArrayList<String> m_pendingChats;
private CommsConnTypeSet m_connTypes = null; private CommsConnTypeSet m_connTypes = null;
private String[] m_missingDevs; private String[] m_missingDevs;
@ -205,6 +204,25 @@ 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
final String archiveName = LocUtils
.getString( m_activity, R.string.group_name_archive );
final long archiveGroup = DBUtils.getGroup( m_activity, archiveName );
long curGroup = DBUtils.getGroupForGame( m_activity, m_rowid );
if ( curGroup != archiveGroup ) {
lstnr = new OnClickListener() {
public void onClick( DialogInterface dlg,
int whichButton ) {
makeNotAgainBuilder( R.string.not_again_archive,
R.string.key_na_archive,
Action.ARCHIVE_ACTION )
.setParams( archiveName, archiveGroup )
.show();
}
};
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 )
@ -553,8 +571,6 @@ public class BoardDelegate extends DelegateBase
m_isFirstLaunch = null == savedInstanceState; m_isFirstLaunch = null == savedInstanceState;
getBundledData( savedInstanceState ); getBundledData( savedInstanceState );
m_pendingChats = new ArrayList<String>();
m_utils = new BoardUtilCtxt(); m_utils = new BoardUtilCtxt();
m_timers = new TimerRunnable[4]; // needs to be in sync with m_timers = new TimerRunnable[4]; // needs to be in sync with
// XWTimerReason // XWTimerReason
@ -843,7 +859,7 @@ public class BoardDelegate extends DelegateBase
Utils.setItemVisible( menu, R.id.board_menu_game_invites, enable ); Utils.setItemVisible( menu, R.id.board_menu_game_invites, enable );
enable = XWPrefs.getStudyEnabled( m_activity ); enable = XWPrefs.getStudyEnabled( m_activity );
Utils.setItemVisible( menu, R.id.games_menu_study, enable ); Utils.setItemVisible( menu, R.id.board_menu_study, enable );
return true; return true;
} // onPrepareOptionsMenu } // onPrepareOptionsMenu
@ -913,7 +929,7 @@ public class BoardDelegate extends DelegateBase
case R.id.board_menu_tray: case R.id.board_menu_tray:
cmd = JNICmd.CMD_TOGGLE_TRAY; cmd = JNICmd.CMD_TOGGLE_TRAY;
break; break;
case R.id.games_menu_study: case R.id.board_menu_study:
StudyListDelegate.launchOrAlert( getDelegator(), m_gi.dictLang, this ); StudyListDelegate.launchOrAlert( getDelegator(), m_gi.dictLang, this );
break; break;
case R.id.board_menu_game_netstats: case R.id.board_menu_game_netstats:
@ -1095,6 +1111,12 @@ public class BoardDelegate extends DelegateBase
makeOkOnlyBuilder( R.string.after_restart ).show(); makeOkOnlyBuilder( R.string.after_restart ).show();
break; break;
case ARCHIVE_ACTION:
String archiveName = (String)params[0];
long archiveGroup = (Long)params[1];
archiveAndClose( archiveName, archiveGroup );
break;
case ENABLE_SMS_DO: case ENABLE_SMS_DO:
post( new Runnable() { post( new Runnable() {
public void run() { public void run() {
@ -2144,7 +2166,6 @@ public class BoardDelegate extends DelegateBase
if ( m_gi.serverRole != DeviceRole.SERVER_STANDALONE ) { if ( m_gi.serverRole != DeviceRole.SERVER_STANDALONE ) {
warnIfNoTransport(); warnIfNoTransport();
trySendChats();
tickle( isStart ); tickle( isStart );
tryInvites(); tryInvites();
} }
@ -2407,15 +2428,6 @@ public class BoardDelegate extends DelegateBase
} }
} }
private void trySendChats()
{
Iterator<String> iter = m_pendingChats.iterator();
while ( iter.hasNext() ) {
handleViaThread( JNICmd.CMD_SENDCHAT, iter.next() );
}
m_pendingChats.clear();
}
private void tryInvites() private void tryInvites()
{ {
if ( 0 < m_mySIS.nMissing && m_summary.hasRematchInfo() ) { if ( 0 < m_mySIS.nMissing && m_summary.hasRematchInfo() ) {
@ -2588,6 +2600,16 @@ public class BoardDelegate extends DelegateBase
return wordsArray; return wordsArray;
} }
private void archiveAndClose( String archiveName, long groupID )
{
if ( DBUtils.GROUPID_UNSPEC == groupID ) {
groupID = DBUtils.addGroup( m_activity, archiveName );
}
DBUtils.moveGame( m_activity, m_rowid, groupID );
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

@ -158,8 +158,7 @@ public class BoardView extends View implements BoardHandler, SyncedDraw {
if ( null != m_dims ) { if ( null != m_dims ) {
if ( BoardContainer.getIsPortrait() != (m_dims.height > m_dims.width) ) { if ( BoardContainer.getIsPortrait() != (m_dims.height > m_dims.width) ) {
// square possible; will break above! // square possible; will break above! No. tested by forceing square
Assert.assertTrue( m_dims.height != m_dims.width );
Log.d( TAG, "onMeasure: discarding m_dims" ); Log.d( TAG, "onMeasure: discarding m_dims" );
if ( ++m_dimsTossCount < 4 ) { if ( ++m_dimsTossCount < 4 ) {
m_dims = null; m_dims = null;

View file

@ -52,9 +52,6 @@ public class ConnStatusHandler {
public Handler getHandler(); public Handler getHandler();
} }
private static final int GREEN = 0xFF00AF00;
private static final int RED = 0xFFAF0000;
private static final int BLACK = 0xFF000000;
private static final int SUCCESS_IN = 0; private static final int SUCCESS_IN = 0;
private static final int SUCCESS_OUT = 1; private static final int SUCCESS_OUT = 1;
private static final int SHOW_SUCCESS_INTERVAL = 1000; private static final int SHOW_SUCCESS_INTERVAL = 1000;
@ -340,7 +337,7 @@ public class ConnStatusHandler {
boolean isIn ) boolean isIn )
{ {
enabled = enabled && null != newestSuccess( connTypes, isIn ); enabled = enabled && null != newestSuccess( connTypes, isIn );
s_fillPaint.setColor( enabled ? GREEN : RED ); s_fillPaint.setColor( enabled ? XWApp.GREEN : XWApp.RED );
canvas.drawRect( rect, s_fillPaint ); canvas.drawRect( rect, s_fillPaint );
} }

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 );
@ -1616,21 +1618,34 @@ public class DBUtils {
return result; return result;
} }
// ORDER BY clause that governs display of games in main GamesList view
private static final String s_getGroupGamesOrderBy =
TextUtils.join(",", new String[] {
// Ended games at bottom
DBHelper.GAME_OVER,
// games with unread chat messages at top
"(" + DBHelper.HASMSGS + " & " + GameSummary.MSG_FLAGS_CHAT + ") IS NOT 0 DESC",
// Games not yet connected at top
DBHelper.TURN + " is -1 DESC",
// Games where it's a local player's turn at top
DBHelper.TURN_LOCAL + " DESC",
// finally, sort by timestamp of last-made move
DBHelper.LASTMOVE,
});
public static long[] getGroupGames( Context context, long groupID ) public static long[] getGroupGames( Context context, long groupID )
{ {
long[] result = null; long[] result = null;
initDB( context ); initDB( context );
String[] columns = { ROW_ID }; String[] columns = { ROW_ID, DBHelper.HASMSGS };
String selection = String.format( "%s=%d", DBHelper.GROUPID, groupID ); String selection = String.format( "%s=%d", DBHelper.GROUPID, groupID );
String orderBy = String.format( "%s,%s DESC,%s", DBHelper.GAME_OVER,
DBHelper.TURN_LOCAL, DBHelper.LASTMOVE );
synchronized( s_dbHelper ) { synchronized( s_dbHelper ) {
Cursor cursor = s_db.query( DBHelper.TABLE_NAME_SUM, columns, Cursor cursor = s_db.query( DBHelper.TABLE_NAME_SUM, columns,
selection, // selection selection, // selection
null, // args null, // args
null, // groupBy null, // groupBy
null, // having null, // having
orderBy s_getGroupGamesOrderBy
); );
int index = cursor.getColumnIndex( ROW_ID ); int index = cursor.getColumnIndex( ROW_ID );
result = new long[ cursor.getCount() ]; result = new long[ cursor.getCount() ];
@ -1688,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;
@ -1746,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

@ -163,6 +163,7 @@ public class DelegateBase implements DlgClickNotify,
} }
if ( this != result ) { if ( this != result ) {
Log.d( TAG, "%s.curThis() => " + result, this.toString() ); Log.d( TAG, "%s.curThis() => " + result, this.toString() );
Assert.fail();
} }
return result; return result;
} }

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

@ -22,7 +22,6 @@ package org.eehouse.android.xw4;
import android.content.Context; import android.content.Context;
import android.graphics.Bitmap; import android.graphics.Bitmap;
import android.graphics.Canvas; import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint; import android.graphics.Paint;
import android.graphics.Rect; import android.graphics.Rect;
import android.graphics.drawable.BitmapDrawable; import android.graphics.drawable.BitmapDrawable;
@ -214,13 +213,13 @@ public class ExpiringDelegate {
int offset = 0; int offset = 0;
int count = s_points.length; int count = s_points.length;
if ( 0 < redWidth ) { if ( 0 < redWidth ) {
s_paint.setColor( Color.RED ); s_paint.setColor( XWApp.RED );
canvas.drawLines( s_points, offset, count / 2, s_paint ); canvas.drawLines( s_points, offset, count / 2, s_paint );
count /= 2; count /= 2;
offset += count; offset += count;
} }
if ( redWidth < width ) { if ( redWidth < width ) {
s_paint.setColor( Color.GREEN ); s_paint.setColor( XWApp.GREEN );
} }
canvas.drawLines( s_points, offset, count, s_paint ); canvas.drawLines( s_points, offset, count, s_paint );
} }
@ -256,7 +255,7 @@ public class ExpiringDelegate {
Paint paint = new Paint(); Paint paint = new Paint();
paint.setStyle(Paint.Style.FILL); paint.setStyle(Paint.Style.FILL);
paint.setColor( Color.RED ); paint.setColor( XWApp.RED );
canvas.drawRect( 0, 0, pct, 1, paint ); canvas.drawRect( 0, 0, pct, 1, paint );
paint.setColor( Utils.TURN_COLOR ); paint.setColor( Utils.TURN_COLOR );
canvas.drawRect( pct, 0, 100, 1, paint ); canvas.drawRect( pct, 0, 100, 1, paint );

View file

@ -64,7 +64,7 @@ public class GameListItem extends LinearLayout
private LinearLayout m_list; private LinearLayout m_list;
private TextView m_state; private TextView m_state;
private TextView m_modTime; private TextView m_modTime;
private ImageView m_marker; private ImageView m_gameTypeImage;
private TextView m_role; private TextView m_role;
private boolean m_expanded, m_haveTurn, m_haveTurnLocal; private boolean m_expanded, m_haveTurn, m_haveTurnLocal;
@ -90,16 +90,6 @@ public class GameListItem extends LinearLayout
m_lastMoveTime = 0; m_lastMoveTime = 0;
m_loadingCount = 0; m_loadingCount = 0;
m_dsdel = new DrawSelDelegate( this ); m_dsdel = new DrawSelDelegate( this );
setOnClickListener( new View.OnClickListener() {
@Override
public void onClick( View v ) {
// if selected, just un-select
if ( null != m_summary ) {
m_cb.itemClicked( GameListItem.this, m_summary );
}
}
} );
} }
public GameSummary getSummary() public GameSummary getSummary()
@ -174,13 +164,32 @@ public class GameListItem extends LinearLayout
} }
// View.OnClickListener interface // View.OnClickListener interface
public void onClick( View view ) { public void onClick( View view )
{
int id = view.getId();
switch ( id ) {
case R.id.expander:
m_expanded = !m_expanded; m_expanded = !m_expanded;
DBUtils.setExpanded( m_rowid, m_expanded ); DBUtils.setExpanded( m_rowid, m_expanded );
makeThumbnailIf( m_expanded ); makeThumbnailIf( m_expanded );
showHide(); showHide();
break;
case R.id.view_loaded:
toggleSelected();
break;
case R.id.right_side:
if ( null != m_summary ) {
m_cb.itemClicked( GameListItem.this, m_summary );
}
break;
default:
Assert.assertFalse(BuildConfig.DEBUG);
break;
}
} }
private void findViews() private void findViews()
@ -191,12 +200,15 @@ public class GameListItem extends LinearLayout
m_expandButton.setOnClickListener( this ); m_expandButton.setOnClickListener( this );
m_viewUnloaded = (TextView)findViewById( R.id.view_unloaded ); m_viewUnloaded = (TextView)findViewById( R.id.view_unloaded );
m_viewLoaded = findViewById( R.id.view_loaded ); m_viewLoaded = findViewById( R.id.view_loaded );
m_viewLoaded.setOnClickListener( this );
m_list = (LinearLayout)findViewById( R.id.player_list ); m_list = (LinearLayout)findViewById( R.id.player_list );
m_state = (TextView)findViewById( R.id.state ); m_state = (TextView)findViewById( R.id.state );
m_modTime = (TextView)findViewById( R.id.modtime ); m_modTime = (TextView)findViewById( R.id.modtime );
m_marker = (ImageView)findViewById( R.id.msg_marker ); m_gameTypeImage = (ImageView)findViewById( R.id.game_type_marker );
m_thumb = (ImageView)findViewById( R.id.thumbnail ); m_thumb = (ImageView)findViewById( R.id.thumbnail );
m_role = (TextView)findViewById( R.id.role ); m_role = (TextView)findViewById( R.id.role );
findViewById( R.id.right_side ).setOnClickListener( this );
} }
private void setLoaded( boolean loaded ) private void setLoaded( boolean loaded )
@ -316,19 +328,20 @@ public class GameListItem extends LinearLayout
int iconID = summary.isMultiGame() ? int iconID = summary.isMultiGame() ?
R.drawable.multigame__gen : R.drawable.sologame__gen; R.drawable.multigame__gen : R.drawable.sologame__gen;
m_marker.setImageResource( iconID ); m_gameTypeImage.setImageResource( iconID );
m_marker.setOnClickListener( new View.OnClickListener() {
@Override boolean hasChat = summary.isMultiGame();
public void onClick( View view ) { if ( hasChat ) {
toggleSelected(); int flags = DBUtils.getMsgFlags( m_context, m_rowid );
hasChat = 0 != (flags & GameSummary.MSG_FLAGS_CHAT);
} }
} ); findViewById( R.id.has_chat_marker )
.setVisibility( hasChat ? View.VISIBLE : View.GONE );
String roleSummary = summary.summarizeRole( m_context, m_rowid ); String roleSummary = summary.summarizeRole( m_context, m_rowid );
m_role.setVisibility( null == roleSummary ? View.GONE : View.VISIBLE );
if ( null != roleSummary ) { if ( null != roleSummary ) {
m_role.setText( roleSummary ); m_role.setText( roleSummary );
} else {
m_role.setVisibility( View.GONE );
} }
update( expanded, summary.lastMoveTime, haveATurn, update( expanded, summary.lastMoveTime, haveATurn,
@ -420,6 +433,7 @@ public class GameListItem extends LinearLayout
// } // }
// GameListAdapter.ClickHandler interface // GameListAdapter.ClickHandler interface
@Override
public void longClicked() public void longClicked()
{ {
toggleSelected(); toggleSelected();

View file

@ -1196,7 +1196,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 +1211,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();
} }
} }

View file

@ -564,6 +564,7 @@ public class GamesListDelegate extends ListDelegateBase
private static final int[] DEBUG_ITEMS = { private static final int[] DEBUG_ITEMS = {
// R.id.games_menu_loaddb, // R.id.games_menu_loaddb,
R.id.games_menu_storedb, R.id.games_menu_storedb,
R.id.games_menu_writegit,
}; };
private static final int[] NOSEL_ITEMS = { private static final int[] NOSEL_ITEMS = {
R.id.games_menu_newgroup, R.id.games_menu_newgroup,
@ -754,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();
long hasName = DBUtils.getGroup( m_activity, name );
if ( DBUtils.GROUPID_UNSPEC == hasName ) {
DBUtils.addGroup( m_activity, name ); DBUtils.addGroup( m_activity, name );
mkListAdapter(); mkListAdapter();
showNewGroupIf(); showNewGroupIf();
} else {
String msg = LocUtils
.getString( m_activity,
R.string.duplicate_group_name_fmt,
name );
makeOkOnlyBuilder( msg ).show();
}
} }
}; };
lstnr2 = new OnClickListener() { lstnr2 = new OnClickListener() {
@ -1059,8 +1069,6 @@ public class GamesListDelegate extends ListDelegateBase
invalidateOptionsMenuIf(); invalidateOptionsMenuIf();
setTitle(); setTitle();
} }
mkListAdapter();
} }
public void invalidateOptionsMenuIf() public void invalidateOptionsMenuIf()
@ -1132,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;
@ -1539,10 +1550,10 @@ public class GamesListDelegate extends ListDelegateBase
GameUtils.resendAllIf( m_activity, null, true, true ); GameUtils.resendAllIf( m_activity, null, true, true );
break; break;
case R.id.games_menu_newgame_solo: case R.id.games_menu_newgame_solo:
handleNewGame( true ); handleNewGameButton( true );
break; break;
case R.id.games_menu_newgame_net: case R.id.games_menu_newgame_net:
handleNewGame( false ); handleNewGameButton( false );
break; break;
case R.id.games_menu_newgroup: case R.id.games_menu_newgroup:
@ -1597,6 +1608,10 @@ public class GamesListDelegate extends ListDelegateBase
Action.STORAGE_CONFIRMED, itemID ); Action.STORAGE_CONFIRMED, itemID );
break; break;
case R.id.games_menu_writegit:
Utils.gitInfoToClip( m_activity );
break;
default: default:
handled = handleSelGamesItem( itemID, selRowIDs ) handled = handleSelGamesItem( itemID, selRowIDs )
|| handleSelGroupsItem( itemID, getSelGroupIDs() ); || handleSelGroupsItem( itemID, getSelGroupIDs() );

View file

@ -184,7 +184,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;
} }
} }

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

@ -736,7 +736,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 +756,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();
@ -995,9 +994,8 @@ 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;
} }

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

@ -50,6 +50,7 @@ public class StudyListDelegate extends ListDelegateBase
implements OnItemSelectedListener, SelectableItem, implements OnItemSelectedListener, SelectableItem,
View.OnLongClickListener, View.OnClickListener, View.OnLongClickListener, View.OnClickListener,
DBUtils.StudyListListener { DBUtils.StudyListListener {
private static final String TAG = StudyListDelegate.class.getSimpleName();
protected static final int NO_LANG = -1; protected static final int NO_LANG = -1;
@ -220,7 +221,8 @@ public class StudyListDelegate extends ListDelegateBase
showToast( msg ); showToast( msg );
break; break;
default: default:
Assert.assertFalse( BuildConfig.DEBUG ); Log.d( TAG, "not handling: %s", action );
handled = false;
break; break;
} }
return handled; return handled;

View file

@ -32,7 +32,9 @@ import android.content.Intent;
import android.content.SharedPreferences; import android.content.SharedPreferences;
import android.content.pm.PackageManager; import android.content.pm.PackageManager;
import android.content.pm.ResolveInfo; import android.content.pm.ResolveInfo;
import android.content.res.AssetManager;
import android.content.res.Configuration; import android.content.res.Configuration;
import android.text.ClipboardManager;
import android.database.Cursor; import android.database.Cursor;
import android.media.Ringtone; import android.media.Ringtone;
@ -55,9 +57,12 @@ import android.widget.Toast;
import org.json.JSONException; import org.json.JSONException;
import org.json.JSONObject; import org.json.JSONObject;
import java.io.BufferedReader;
import java.io.ByteArrayInputStream; import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream; import java.io.ByteArrayOutputStream;
import java.io.File; import java.io.File;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.ObjectInputStream; import java.io.ObjectInputStream;
import java.io.ObjectOutputStream; import java.io.ObjectOutputStream;
import java.io.Serializable; import java.io.Serializable;
@ -187,6 +192,33 @@ public class Utils {
context.startActivity( Intent.createChooser( intent, chooserMsg ) ); context.startActivity( Intent.createChooser( intent, chooserMsg ) );
} }
static void gitInfoToClip( Context context )
{
StringBuilder sb;
try {
InputStream is = context.getAssets().open( BuildConfig.BUILD_INFO_NAME,
AssetManager.ACCESS_BUFFER );
BufferedReader reader = new BufferedReader(new InputStreamReader(is));
sb = new StringBuilder();
for ( ; ; ) {
String line = reader.readLine();
if ( null == line ) {
break;
}
sb.append( line ).append( "\n" );
}
reader.close();
} catch ( Exception ex ) {
sb = null;
}
if ( null != sb ) {
ClipboardManager clipboard = (ClipboardManager)
context.getSystemService(Context.CLIPBOARD_SERVICE);
clipboard.setText( sb.toString() );
}
}
public static void postNotification( Context context, Intent intent, public static void postNotification( Context context, Intent intent,
int titleID, int bodyID, int id ) int titleID, int bodyID, int id )
{ {

View file

@ -36,7 +36,6 @@ public class XWApp extends Application {
private static final String TAG = XWApp.class.getSimpleName(); private static final String TAG = XWApp.class.getSimpleName();
public static final boolean BTSUPPORTED = true; public static final boolean BTSUPPORTED = true;
public static final boolean GCMSUPPORTED = true;
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;
@ -53,6 +52,9 @@ public class XWApp extends Application {
public static final int MAX_TRAY_TILES = 7; // comtypes.h public static final int MAX_TRAY_TILES = 7; // comtypes.h
public static final int SEL_COLOR = Color.argb( 0xFF, 0x09, 0x70, 0x93 ); public static final int SEL_COLOR = Color.argb( 0xFF, 0x09, 0x70, 0x93 );
public static final int GREEN = 0xFF00AF00;
public static final int RED = 0xFFAF0000;
private static UUID s_UUID = null; private static UUID s_UUID = null;
private static Boolean s_onEmulator = null; private static Boolean s_onEmulator = null;
private static Context s_context = null; private static Context s_context = null;

View file

@ -1 +1,2 @@
values-??/strings.xml values-??/strings.xml
**/*__gen.png

View file

@ -30,7 +30,6 @@
<EditText android:id="@+id/chat_edit" <EditText android:id="@+id/chat_edit"
android:layout_width="fill_parent" android:layout_width="fill_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:singleLine="false"
android:inputType="textCapSentences|textMultiLine" android:inputType="textCapSentences|textMultiLine"
android:layout_weight="1" android:layout_weight="1"
android:scrollHorizontally="false" android:scrollHorizontally="false"

View file

@ -10,9 +10,9 @@
android:layout_width="fill_parent" android:layout_width="fill_parent"
android:layout_marginLeft="30dip" android:layout_marginLeft="30dip"
android:layout_marginRight="30dip" android:layout_marginRight="30dip"
android:autoText="false" android:maxLines="1"
android:capitalize="words" android:maxLength="32"
android:singleLine="true" android:inputType="textCapWords"
android:selectAllOnFocus="true" android:selectAllOnFocus="true"
android:textAppearance="?android:attr/textAppearanceMedium" android:textAppearance="?android:attr/textAppearanceMedium"
/> />

View file

@ -26,7 +26,8 @@
<EditText android:id="@+id/word_edit" <EditText android:id="@+id/word_edit"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:singleLine="true" android:maxLines="1"
android:inputType="text"
android:layout_weight="1" android:layout_weight="1"
android:hint="@string/word_search_hint" android:hint="@string/word_search_hint"
android:capitalize="characters" android:capitalize="characters"

View file

@ -138,9 +138,8 @@
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_width="fill_parent" android:layout_width="fill_parent"
android:scrollHorizontally="false" android:scrollHorizontally="false"
android:autoText="false" android:maxLines="1"
android:capitalize="none" android:inputType="text"
android:singleLine="true"
android:selectAllOnFocus="true" android:selectAllOnFocus="true"
android:windowSoftInputMode="stateHidden" android:windowSoftInputMode="stateHidden"
android:maxLength="31" android:maxLength="31"

View file

@ -31,13 +31,25 @@
android:visibility="gone" android:visibility="gone"
> >
<ImageView android:id="@+id/msg_marker" <RelativeLayout android:id="@+id/game_view_container"
android:layout_width="42dp" android:layout_width="42dp"
android:layout_height="fill_parent" android:layout_height="fill_parent"
android:layout_gravity="center_vertical|center_horizontal" android:layout_gravity="center_vertical|center_horizontal"
android:paddingLeft="8dip" android:paddingLeft="8dip"
android:paddingRight="8dip" android:paddingRight="8dip"
>
<ImageView android:id="@+id/game_type_marker"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:src="@drawable/multigame__gen"
/> />
<ImageView android:id="@+id/has_chat_marker"
android:layout_width="wrap_content"
android:layout_height="22dp"
android:src="@drawable/green_chat__gen"
android:layout_alignParentBottom="true"
/>
</RelativeLayout>
<ImageView android:id="@+id/thumbnail" <ImageView android:id="@+id/thumbnail"
android:layout_width="wrap_content" android:layout_width="wrap_content"
@ -49,9 +61,12 @@
<!-- this layout is vertical, holds everything but the status <!-- this layout is vertical, holds everything but the status
icon[s] (plural later) --> icon[s] (plural later) -->
<LinearLayout android:orientation="vertical" <LinearLayout android:id="@+id/right_side"
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:longClickable="true"
> >
<!-- This is the game name and expander --> <!-- This is the game name and expander -->
@ -66,7 +81,7 @@
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:gravity="center" android:gravity="center"
android:layout_weight="1" android:layout_weight="1"
android:singleLine="true" android:maxLines="1"
android:textAppearance="?android:attr/textAppearanceMedium" android:textAppearance="?android:attr/textAppearanceMedium"
/> />

View file

@ -19,7 +19,8 @@
android:layout_marginLeft="30dip" android:layout_marginLeft="30dip"
android:layout_marginRight="30dip" android:layout_marginRight="30dip"
android:autoText="false" android:autoText="false"
android:singleLine="true" android:maxLines="1"
android:inputType="text"
android:selectAllOnFocus="true" android:selectAllOnFocus="true"
android:textAppearance="?android:attr/textAppearanceMedium" android:textAppearance="?android:attr/textAppearanceMedium"
/> />
@ -37,7 +38,8 @@
android:layout_marginLeft="30dip" android:layout_marginLeft="30dip"
android:layout_marginRight="30dip" android:layout_marginRight="30dip"
android:autoText="false" android:autoText="false"
android:singleLine="true" android:maxLines="1"
android:inputType="text"
android:selectAllOnFocus="true" android:selectAllOnFocus="true"
android:textAppearance="?android:attr/textAppearanceMedium" android:textAppearance="?android:attr/textAppearanceMedium"
/> />

View file

@ -19,7 +19,8 @@
android:layout_marginLeft="30dip" android:layout_marginLeft="30dip"
android:layout_marginRight="30dip" android:layout_marginRight="30dip"
android:autoText="false" android:autoText="false"
android:singleLine="true" android:maxLines="1"
android:inputType="text"
android:selectAllOnFocus="true" android:selectAllOnFocus="true"
android:textAppearance="?android:attr/textAppearanceMedium" android:textAppearance="?android:attr/textAppearanceMedium"
/> />
@ -37,7 +38,8 @@
android:layout_marginLeft="30dip" android:layout_marginLeft="30dip"
android:layout_marginRight="30dip" android:layout_marginRight="30dip"
android:autoText="false" android:autoText="false"
android:singleLine="true" android:maxLines="1"
android:inputType="text"
android:selectAllOnFocus="true" android:selectAllOnFocus="true"
android:textAppearance="?android:attr/textAppearanceMedium" android:textAppearance="?android:attr/textAppearanceMedium"
/> />

View file

@ -31,7 +31,7 @@
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_weight="1" android:layout_weight="1"
android:textAppearance="?android:attr/textAppearanceMedium" android:textAppearance="?android:attr/textAppearanceMedium"
android:singleLine="true" android:maxLines="1"
/> />
<TextView android:id="@+id/text_item2" <TextView android:id="@+id/text_item2"
@ -39,7 +39,7 @@
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_weight="1" android:layout_weight="1"
android:gravity="right" android:gravity="right"
android:singleLine="true" android:maxLines="1"
android:visibility="gone" android:visibility="gone"
/> />

View file

@ -9,11 +9,11 @@
<TextView android:id="@+id/english_view" <TextView android:id="@+id/english_view"
style="@style/evenly_spaced_horizontal" style="@style/evenly_spaced_horizontal"
android:singleLine="true" android:maxLines="1"
/> />
<TextView android:id="@+id/xlated_view" <TextView android:id="@+id/xlated_view"
style="@style/evenly_spaced_horizontal" style="@style/evenly_spaced_horizontal"
android:singleLine="true" android:maxLines="1"
/> />
</org.eehouse.android.xw4.loc.LocListItem> </org.eehouse.android.xw4.loc.LocListItem>

View file

@ -37,7 +37,8 @@
<EditText android:id="@+id/loc_search_field" <EditText android:id="@+id/loc_search_field"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:singleLine="true" android:maxLines="1"
android:inputType="text"
android:layout_weight="1" android:layout_weight="1"
/> />

View file

@ -18,8 +18,9 @@
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_width="fill_parent" android:layout_width="fill_parent"
android:scrollHorizontally="true" android:scrollHorizontally="true"
android:autoText="false" android:maxLines="1"
android:capitalize="none" android:inputType="textCapWords"
android:maxLength="32"
android:textAppearance="?android:attr/textAppearanceMedium" android:textAppearance="?android:attr/textAppearanceMedium"
android:selectAllOnFocus="true" android:selectAllOnFocus="true"
/> />

View file

@ -51,11 +51,11 @@
android:layout_marginLeft="20dip" android:layout_marginLeft="20dip"
android:layout_marginRight="20dip" android:layout_marginRight="20dip"
android:scrollHorizontally="true" android:scrollHorizontally="true"
android:autoText="false"
android:capitalize="words"
android:selectAllOnFocus="true" android:selectAllOnFocus="true"
android:gravity="fill_horizontal" android:gravity="fill_horizontal"
android:singleLine="true" android:maxLines="1"
android:maxLength="32"
android:inputType="textCapWords"
android:textAppearance="?android:attr/textAppearanceMedium" android:textAppearance="?android:attr/textAppearanceMedium"
/> />
@ -101,11 +101,9 @@
android:layout_marginLeft="20dip" android:layout_marginLeft="20dip"
android:layout_marginRight="20dip" android:layout_marginRight="20dip"
android:scrollHorizontally="true" android:scrollHorizontally="true"
android:autoText="false"
android:capitalize="none"
android:gravity="fill_horizontal" android:gravity="fill_horizontal"
android:password="true" android:maxLines="1"
android:singleLine="true" android:inputType="textPassword"
android:textAppearance="?android:attr/textAppearanceMedium" android:textAppearance="?android:attr/textAppearanceMedium"
/> />
</LinearLayout> </LinearLayout>

View file

@ -9,13 +9,13 @@
<TextView android:id="@+id/item_name" <TextView android:id="@+id/item_name"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:singleLine="true" android:maxLines="1"
android:gravity="left" android:gravity="left"
/> />
<TextView android:id="@+id/item_score" <TextView android:id="@+id/item_score"
android:layout_width="fill_parent" android:layout_width="fill_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:singleLine="true" android:maxLines="1"
android:gravity="right" android:gravity="right"
/> />
</org.eehouse.android.xw4.ExpiringLinearLayout> </org.eehouse.android.xw4.ExpiringLinearLayout>

View file

@ -20,7 +20,8 @@
android:layout_marginLeft="30dip" android:layout_marginLeft="30dip"
android:layout_marginRight="30dip" android:layout_marginRight="30dip"
android:autoText="false" android:autoText="false"
android:singleLine="true" android:maxLines="1"
android:inputType="text"
android:selectAllOnFocus="true" android:selectAllOnFocus="true"
android:textAppearance="?android:attr/textAppearanceMedium" android:textAppearance="?android:attr/textAppearanceMedium"
/> />

View file

@ -57,7 +57,7 @@
</menu> </menu>
</item> </item>
<item android:id="@+id/games_menu_study" <item android:id="@+id/board_menu_study"
android:title="@string/gamel_menu_study" android:title="@string/gamel_menu_study"
/> />

View file

@ -120,5 +120,8 @@
<item android:id="@+id/games_menu_loaddb" <item android:id="@+id/games_menu_loaddb"
android:title="@string/gamel_menu_loaddb" android:title="@string/gamel_menu_loaddb"
/> />
<item android:id="@+id/games_menu_writegit"
android:title="@string/gamel_menu_writegit"
/>
</menu> </menu>

View file

@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<style name="AppTheme" parent="android:Theme.Holo"/>
</resources>

View file

@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<style name="AppTheme" parent="android:Theme.Material"/>
</resources>

View file

@ -111,6 +111,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>

View file

@ -1743,6 +1743,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>
@ -2123,7 +2131,7 @@
<string name="newgroup_label">Name your new group:</string> <string name="newgroup_label">Name your new group:</string>
<string name="list_group_delete">Delete group</string> <string name="list_group_delete">Delete group</string>
<string name="list_group_rename">Rename</string> <string name="list_group_rename">Rename group</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_moveup">Move up</string> <string name="list_group_moveup">Move up</string>
<string name="list_group_movedown">Move down</string> <string name="list_group_movedown">Move down</string>
@ -2158,6 +2166,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>
@ -2419,11 +2431,11 @@
<string name="set_pref">Hide buttons</string> <string name="set_pref">Hide buttons</string>
<string name="not_again_hidenewgamebuttons">These two buttons do <string name="not_again_hidenewgamebuttons">The two buttons at the
the same thing as the first two items in this window\'s Action Bar bottom of this screen and the first two items in its Action Bar
(or menu). If you like you can hide the buttons to make more games (or menu) do the same thing. If you like you can hide the buttons
visible.\n\n(If you later want to unhide them go to the Appearance to make more games visible.\n\n(If you later want to unhide the
section of App settings). buttons go to the Appearance section of App settings).
</string> </string>
<string name="waiting_title">Waiting for players</string> <string name="waiting_title">Waiting for players</string>
@ -2491,6 +2503,7 @@
<string name="name_dict_fmt">%1$s/%2$s</string> <string name="name_dict_fmt">%1$s/%2$s</string>
<string name="gamel_menu_storedb">Write games to SD card</string> <string name="gamel_menu_storedb">Write games to SD card</string>
<string name="gamel_menu_loaddb">Load games from SD card</string> <string name="gamel_menu_loaddb">Load games from SD card</string>
<string name="gamel_menu_writegit">Copy git info to clipboard</string>
<string name="enable_dupes_title">Accept duplicate invites</string> <string name="enable_dupes_title">Accept duplicate invites</string>
<string name="xlations_locale">Fake locale for translation</string> <string name="xlations_locale">Fake locale for translation</string>
<string name="enable_dupes_summary">Accept invitations more than once</string> <string name="enable_dupes_summary">Accept invitations more than once</string>

View file

@ -1,7 +1,5 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<resources> <resources>
<style name="AppTheme" parent="android:Theme.Material"/>
<style name="config_separator"> <style name="config_separator">
<item name="android:layout_height">wrap_content</item> <item name="android:layout_height">wrap_content</item>
<item name="android:layout_width">fill_parent</item> <item name="android:layout_width">fill_parent</item>

View file

@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<style name="AppTheme" parent="android:Theme"/>
</resources>

View file

@ -16,12 +16,18 @@
android:title="@string/pref_human_name" android:title="@string/pref_human_name"
android:capitalize="words" android:capitalize="words"
android:defaultValue="" android:defaultValue=""
android:maxLines="1"
android:maxLength="32"
android:inputType="text"
/> />
<org.eehouse.android.xw4.XWEditTextPreference <org.eehouse.android.xw4.XWEditTextPreference
android:key="@string/key_robot_name" android:key="@string/key_robot_name"
android:title="@string/robot_label" android:title="@string/robot_label"
android:capitalize="words" android:capitalize="words"
android:defaultValue="@string/button_default_robot" android:defaultValue="@string/button_default_robot"
android:maxLines="1"
android:maxLength="32"
android:inputType="text"
/> />
</PreferenceScreen> </PreferenceScreen>

View file

@ -0,0 +1,26 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="org.eehouse.android.xw4"
>
<!-- GCM stuff -->
<permission android:name="${APP_ID}.permission.C2D_MESSAGE"
android:protectionLevel="signature" />
<uses-permission android:name="${APP_ID}.permission.C2D_MESSAGE" />
<uses-permission android:name="com.google.android.c2dm.permission.RECEIVE" />
<uses-permission android:name="android.permission.WAKE_LOCK" />
<application>
<receiver android:name="com.google.android.gcm.GCMBroadcastReceiver"
android:permission="com.google.android.c2dm.permission.SEND" >
<intent-filter>
<action android:name="com.google.android.c2dm.intent.RECEIVE" />
<action android:name="com.google.android.c2dm.intent.REGISTRATION" />
<category android:name="${APP_ID}" />
</intent-filter>
</receiver>
<service android:name=".GCMIntentService" />
</application>
</manifest>

View file

@ -29,12 +29,15 @@ import com.google.android.gcm.GCMRegistrar;
import org.json.JSONArray; import org.json.JSONArray;
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();
public GCMIntentService() public GCMIntentService()
{ {
super( BuildConfig.GCM_SENDER_ID ); super( BuildConfig.GCM_SENDER_ID );
Assert.assertTrue( BuildConfig.GCM_SENDER_ID.length() > 0 );
} }
@Override @Override
@ -120,8 +123,9 @@ public class GCMIntentService extends GCMBaseIntentService {
public static void init( Application app ) public static void init( Application app )
{ {
if ( 0 < BuildConfig.GCM_SENDER_ID.length() ) {
int sdkVersion = Integer.valueOf( android.os.Build.VERSION.SDK ); int sdkVersion = Integer.valueOf( android.os.Build.VERSION.SDK );
if ( 8 <= sdkVersion && 0 < BuildConfig.GCM_SENDER_ID.length() ) { if ( 8 <= sdkVersion ) {
try { try {
GCMRegistrar.checkDevice( app ); GCMRegistrar.checkDevice( app );
// GCMRegistrar.checkManifest( app ); // GCMRegistrar.checkManifest( app );
@ -137,6 +141,7 @@ public class GCMIntentService extends GCMBaseIntentService {
} }
} }
} }
}
private void notifyRelayService( Context context, boolean working ) private void notifyRelayService( Context context, boolean working )
{ {

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" );
}
}

Binary file not shown.

View file

@ -58,9 +58,9 @@
<plurals name="confirm_delete_dict_fmt"> <plurals name="confirm_delete_dict_fmt">
<item quantity="one">Er du sikker på at du vil slette ordlisten %1$s?</item> <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> <item quantity="other">Er du sikker på at du vil slette ordlistene %1$s?</item>
</plurals> </plurals>
<string name="confirm_deleteonly_dicts_fmt">Sletting av %1$s vil bety at du står uten noen %1$s-ordlister. Ett eller flere spill vil ikke kunne åpnes (til du laster ned en erstatningsliste).</string> <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_human">Menneske</string>
<string name="button_default_robot">Maskin</string> <string name="button_default_robot">Maskin</string>
@ -141,7 +141,7 @@
<string name="no_moves_made">(Ingen trekk enda)</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_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="invit_expl_notarget_fmt">Invitasjon sendt via %1$s på %2$s. Ukjent mottaker.</string>
<string name="relay_alert">Tilkoblingsproblem</string> <string name="relay_alert">Tilkoblingsproblem</string>

View file

@ -27,12 +27,15 @@ getPackage() {
echo $PACK echo $PACK
} }
# FIXME: not all options require a working directory, e.g. --apk
WD=$(pwd) WD=$(pwd)
while :; do while :; do
if [ -e ${WD}/AndroidManifest.xml -a -e ${WD}/build.xml ]; then if [ -e ${WD}/AndroidManifest.xml -a -e ${WD}/build.xml ]; then
break break
elif [ -e ${WD}/app/build.gradle ]; then
break
elif [ ${WD} = '/' ]; then elif [ ${WD} = '/' ]; then
usage "reached / without finding AndroidManifest.xml" usage "reached / without finding AndroidManifest.xml or build.gradle"
else else
WD=$(cd $WD/.. && pwd) WD=$(cd $WD/.. && pwd)
fi fi

View file

@ -10,7 +10,7 @@ function printHead() {
<html> <html>
<head> <head>
<link rel="stylesheet" type="text/css" href="/xw4mobile.css" /> <link rel="stylesheet" type="text/css" href="/xw4mobile.css" />
<title>Crosswords Invite redirect</title> <title>CrossWords Invite redirect</title>
</head> </head>
<body> <body>
<div class="center"> <div class="center">
@ -51,32 +51,31 @@ function printAndroid() {
print <<<EOF print <<<EOF
<div> <div>
<p>You&apos;ll have come here after clicking a link in an email or <p>You&apos;ll have come here after clicking a link in an email or
text inviting you to a Crosswords game. But you should not be seeing text inviting you to a CrossWords game. But you should not be seeing
this page.</p> this page.</p>
<p>If you got this page on your device, it means either <p>If you got this page on your device, it means either
<ul> <ul>
<li>The copy of Crosswords you have is NOT beta 56 or newer (dating from about Dec. 1, 2012).</li> <li>You don't have CrossWords installed</li>
<li> OR </li> <li>OR</li>
<li> that your copy of Crosswords is new enough <em>BUT</em> that <li>that when you clicked on the link and were asked to choose between a
when you clicked on the link and were asked to choose between a browser and CrossWords you chose the browser.</li>
browser and Crosswords you chose the browser.</li>
</ul></p> </ul></p>
<p>In the first case, install the latest Crosswords, <p>In the first case, install the latest CrossWords,
either <a href="market://search?q=pname:org.eehouse.android.xw4">via either <a href="market://search?q=pname:org.eehouse.android.xw4">via
the Google Play store</a> or the Google Play store</a> or
(sideloading) <a href="https://sourceforge.net/projects/xwords/files/xwords_Android/4.4%20beta%2056/XWords4-release_android_beta_56.apk/download">via (sideloading) <a href="https://sourceforge.net/projects/xwords/files/xwords_Android/4.4%20beta%20129/XWords4-release_android_beta_129.apk/download">via
Sourceforge.net</a>. After the install is finished go back to the Sourceforge.net</a>. After the install is finished go back to the
invite email (or text) and tap the link again.</p> invite email (or text) and tap the link again.</p>
<p>In the second case, hit your browser&apos;s back button, click the <p>In the second case, hit your browser&apos;s back button, click the
link in your invite email (or text) again, and this time let link in your invite email (or text) again, and this time let
Crosswords handle it.</p> CrossWords handle it.</p>
<p>(If you get tired of having to having to make that choice, Android <p>(If you get tired of having to having to make that choice, Android
will allow you to make Crosswords the default. If you do that will allow you to make CrossWords the default. If you do that
Crosswords will be given control of all URLs that start with CrossWords will be given control of all URLs that start with
"http://eehouse.org/and/" -- not all URLs of any type.)</p> "http://eehouse.org/and/" -- not all URLs of any type.)</p>
<p>Have fun. And as always, <a href="mailto:xwords@eehouse.org">let <p>Have fun. And as always, <a href="mailto:xwords@eehouse.org">let

View file

@ -19,3 +19,7 @@ $(IMG_DEST)/drawable-mdpi/%__gen.png: $(IMG_SRC)/%.svg
$(IMG_DEST)/drawable-hdpi/%__gen.png: $(IMG_SRC)/%.svg $(IMG_DEST)/drawable-hdpi/%__gen.png: $(IMG_SRC)/%.svg
convert $(PARAMS) -scale 48x48 $< $@ convert $(PARAMS) -scale 48x48 $< $@
# Build have-chat badge using R.color.dull_green
$(IMG_DEST)/drawable/green_chat__gen.png: $(IMG_DEST)/drawable/stat_notify_chat.png
convert -fill '#00AF00' -colorize 50% $< $@

View file

@ -66,20 +66,7 @@ k_filebase = "/var/www/html/"
k_apkDir = "xw4/android/" k_apkDir = "xw4/android/"
k_shelfFile = k_filebase + 'xw4/info_shelf_2' k_shelfFile = k_filebase + 'xw4/info_shelf_2'
k_urlbase = "http://eehouse.org" k_urlbase = "http://eehouse.org"
k_versions = { 'org.eehouse.android.xw4': {
'version' : 91,
k_AVERS : 91,
k_URL : k_apkDir + 'XWords4-release_' + k_REL_REV + '.apk',
},
}
# k_versions_dbg = { 'org.eehouse.android.xw4': {
# 'version' : 74,
# k_AVERS : 74,
# k_GVERS : k_DBG_REV,
# k_URL : k_apkDir + 'XWords4-release_' + k_DBG_REV + '.apk',
# },
# }
s_shelf = None s_shelf = None
g_langs = {'English' : 'en', g_langs = {'English' : 'en',
@ -127,7 +114,7 @@ def md5Checksums( sums, filePath ):
if filePath in sums: if filePath in sums:
result = sums[filePath] result = sums[filePath]
else: else:
logging.debug( "opening %s" % (k_filebase + "and_wordlists/" + filePath)) # logging.debug( "opening %s" % (k_filebase + "and_wordlists/" + filePath))
try: try:
file = open( k_filebase + "and_wordlists/" + filePath, 'rb' ) file = open( k_filebase + "and_wordlists/" + filePath, 'rb' )
md5 = hashlib.md5() md5 = hashlib.md5()
@ -158,7 +145,7 @@ def openShelf():
if not k_SUMS in s_shelf: s_shelf[k_SUMS] = {} if not k_SUMS in s_shelf: s_shelf[k_SUMS] = {}
if not k_COUNT in s_shelf: s_shelf[k_COUNT] = 0 if not k_COUNT in s_shelf: s_shelf[k_COUNT] = 0
s_shelf[k_COUNT] += 1 s_shelf[k_COUNT] += 1
logging.debug( "Count now %d" % s_shelf[k_COUNT] ) # logging.debug( "Count now %d" % s_shelf[k_COUNT] )
def closeShelf(): def closeShelf():
global s_shelf global s_shelf
@ -223,6 +210,34 @@ def getOrderedApks( path, appID, debug ):
result = sorted(apkToCode.keys(), reverse=True, key=lambda file: (apkToCode[file], apkToMtime[file])) result = sorted(apkToCode.keys(), reverse=True, key=lambda file: (apkToCode[file], apkToMtime[file]))
return result return result
# Given a version, find the apk that has the next highest version
def getNextAfter(path, appID, curVers, debug):
# print 'getNextAfter(', path, ')'
apks = getOrderedApks(path, appID, debug)
map = {}
max = 0
for apk in apks:
versionCode = getAAPTInfo(apk)['versionCode']
if versionCode > curVers:
map[versionCode] = apk
if max < versionCode: max = versionCode
# print map
result = None
if map:
print 'looking between', curVers+1, 'and', max
for nextVersion in range(curVers+1, max+1):
if nextVersion in map:
result = map[nextVersion]
break
if result:
print nextVersion, ':', result
return result
# Returns '' for xw4, <variant> for anything else
def getVariantDir( name ): def getVariantDir( name ):
result = '' result = ''
splits = string.split( name, '.' ) splits = string.split( name, '.' )
@ -271,10 +286,10 @@ def dictVersion( req, name, lang, md5sum ):
closeShelf() closeShelf()
return json.dumps( result ) return json.dumps( result )
def getApp( params, name ): def getApp( params, name = None, debug = False):
result = None result = None
if k_NAME in params: if k_DEBUG in params: debug = params[k_DEBUG]
name = params[k_NAME] if k_NAME in params: name = params[k_NAME]
if name: if name:
variantDir = getVariantDir( name ) variantDir = getVariantDir( name )
# If we're a dev device, always push the latest # If we're a dev device, always push the latest
@ -303,16 +318,19 @@ def getApp( params, name ):
result = {k_URL: url} result = {k_URL: url}
logging.debug( result ) logging.debug( result )
elif k_GVERS in params: elif k_AVERS in params:
gvers = params[k_GVERS] vers = params[k_AVERS]
if k_INSTALLER in params: installer = params[k_INSTALLER] if k_INSTALLER in params: installer = params[k_INSTALLER]
else: installer = '' else: installer = ''
logging.debug( "name: %s; installer: %s; gvers: %s" logging.debug( "name: %s; installer: %s; gvers: %s"
% (name, installer, gvers) ) % (name, installer, vers) )
if name in k_versions: print "name: %s; installer: %s; vers: %s" % (name, installer, vers)
if k_GVERS in versForName and not gvers == versForName[k_GVERS]: dir = k_filebase + k_apkDir + 'rel/'
result = {k_URL: k_urlbase + '/' + versForName[k_URL]} apk = getNextAfter( dir, name, vers, debug )
if apk:
apk = apk[len(k_filebase):] # strip fs path
result = {k_URL: k_urlbase + '/' + apk}
else: else:
logging.debug(name + " is up-to-date") logging.debug(name + " is up-to-date")
else: else:
@ -542,15 +560,13 @@ def getUpdates( req, params ):
result[k_DICTS] = dictsResult result[k_DICTS] = dictsResult
# Let's not upgrade strings at the same time as we're upgrading the app # Let's not upgrade strings at the same time as we're upgrading the app
if appResult: # if appResult:
logging.debug( 'skipping xlation upgrade because app being updated' ) # logging.debug( 'skipping xlation upgrade because app being updated' )
elif k_XLATEINFO in asJson and k_NAME in asJson and k_STRINGSHASH in asJson: # elif k_XLATEINFO in asJson and k_NAME in asJson and k_STRINGSHASH in asJson:
xlateResult = getXlate( asJson[k_XLATEINFO], asJson[k_NAME], asJson[k_STRINGSHASH] ) # xlateResult = getXlate( asJson[k_XLATEINFO], asJson[k_NAME], asJson[k_STRINGSHASH] )
if xlateResult: # if xlateResult:
logging.debug( xlateResult ) # logging.debug( xlateResult )
result[k_XLATEINFO] = xlateResult; # result[k_XLATEINFO] = xlateResult;
else:
logging.debug( "NOT FOUND xlate info" )
result = json.dumps( result ) result = json.dumps( result )
# logging.debug( result ) # logging.debug( result )
@ -564,7 +580,7 @@ def clearShelf():
def usage(msg=None): def usage(msg=None):
if msg: print "ERROR:", msg if msg: print "ERROR:", msg
print "usage:", sys.argv[0], '--get-sums [lang/dict]*' print "usage:", sys.argv[0], '--get-sums [lang/dict]*'
print ' | --test-get-app app <org.eehouse.app.name> avers gvers' print ' | --get-app --appID <org.something> --vers <avers> --gvers <gvers> [--debug]'
print ' | --test-get-dicts name lang curSum' print ' | --test-get-dicts name lang curSum'
print ' | --list-apks [--path <path/to/apks>] [--debug] --appID org.something' print ' | --list-apks [--path <path/to/apks>] [--debug] --appID org.something'
print ' | --list-dicts' print ' | --list-dicts'
@ -574,8 +590,9 @@ def usage(msg=None):
def main(): def main():
argc = len(sys.argv) argc = len(sys.argv)
if 1 >= argc: usage(); if 1 >= argc: usage('too few args')
arg = sys.argv[1] arg = sys.argv[1]
args = sys.argv[2:]
if arg == '--clear-shelf': if arg == '--clear-shelf':
clearShelf() clearShelf()
elif arg == '--list-dicts': elif arg == '--list-dicts':
@ -589,12 +606,24 @@ def main():
print arg, md5Checksums(dictSums, arg) print arg, md5Checksums(dictSums, arg)
s_shelf[k_SUMS] = dictSums s_shelf[k_SUMS] = dictSums
closeShelf() closeShelf()
elif arg == '--test-get-app': elif arg == '--get-app':
if not 4 == argc: usage() appID = None
params = { k_NAME: sys.argv[2], vers = 0
k_GVERS: sys.argv[3], debug = False
while len(args):
arg = args.pop(0)
if arg == '--appID': appID = args.pop(0)
elif arg == '--vers': vers = int(args.pop(0))
elif arg == '--debug': debug = True
else: usage('unexpected arg: ' + arg)
if not appID: usage('--appID required')
elif not vers: usage('--vers required')
params = { k_NAME: appID,
k_AVERS: vers,
k_DEBUG: debug,
k_DEVOK: False, # FIX ME
} }
print getApp( params, sys.argv[2] ) print getApp( params )
elif arg == '--test-get-dicts': elif arg == '--test-get-dicts':
if not 5 == argc: usage() if not 5 == argc: usage()
params = { k_NAME: sys.argv[2], params = { k_NAME: sys.argv[2],
@ -607,7 +636,6 @@ def main():
path = "" path = ""
debug = False debug = False
appID = '' appID = ''
args = sys.argv[2:]
while len(args): while len(args):
arg = args.pop(0) arg = args.pop(0)
if arg == '--appID': appID = args.pop(0) if arg == '--appID': appID = args.pop(0)

View file

@ -43,3 +43,8 @@ for SVG in img_src/*.svg; do
fi fi
done done
done done
OTHER_IMAGES="app/src/main/res/drawable/green_chat__gen.png"
for IMAGE in $OTHER_IMAGES; do
make -f $(dirname $0)/images.mk $IMAGE >/dev/null 2>&1
done

View file

@ -447,10 +447,13 @@ printDims( const BoardDims* dimsp )
# define printDims( ldims ) # define printDims( ldims )
# endif # endif
/* For debugging the special case of square board */
// #define FORCE_SQUARE
void void
board_figureLayout( BoardCtxt* board, const CurGameInfo* gi, board_figureLayout( BoardCtxt* board, const CurGameInfo* gi,
XP_U16 bLeft, XP_U16 bTop, XP_U16 bLeft, XP_U16 bTop,
const XP_U16 bWidth, const XP_U16 bHeight, XP_U16 bWidth, XP_U16 bHeight,
XP_U16 colPctMax, XP_U16 scorePct, XP_U16 trayPct, XP_U16 colPctMax, XP_U16 scorePct, XP_U16 trayPct,
XP_U16 scoreWidth, XP_U16 fontWidth, XP_U16 fontHt, XP_U16 scoreWidth, XP_U16 fontWidth, XP_U16 fontHt,
XP_Bool squareTiles, BoardDims* dimsp ) XP_Bool squareTiles, BoardDims* dimsp )
@ -465,6 +468,14 @@ board_figureLayout( BoardCtxt* board, const CurGameInfo* gi,
XP_U16 wantHt; XP_U16 wantHt;
XP_U16 nToScroll; XP_U16 nToScroll;
#ifdef FORCE_SQUARE
if ( bWidth > bHeight ) {
bWidth = bHeight;
} else {
bHeight = bWidth;
}
#endif
ldims.left = bLeft; ldims.left = bLeft;
ldims.top = bTop; ldims.top = bTop;
ldims.width = bWidth; ldims.width = bWidth;
@ -552,7 +563,13 @@ board_figureLayout( BoardCtxt* board, const CurGameInfo* gi,
ldims.boardHt = cellSize * nCells; ldims.boardHt = cellSize * nCells;
ldims.trayTop = ldims.top + scoreHt + (cellSize * (nCells-nToScroll)); ldims.trayTop = ldims.top + scoreHt + (cellSize * (nCells-nToScroll));
ldims.height = heightUsed; ldims.height =
#ifdef FORCE_SQUARE
ldims.width
#else
heightUsed
#endif
;
ldims.cellSize = cellSize; ldims.cellSize = cellSize;
if ( gi->timerEnabled ) { if ( gi->timerEnabled ) {

View file

@ -1721,11 +1721,10 @@ cursesDevIDReceived( void* closure, const XP_UCHAR* devID,
/* If we already have one, make sure it's the same! Else store. */ /* If we already have one, make sure it's the same! Else store. */
gchar buf[64]; gchar buf[64];
XP_Bool have = db_fetch( pDb, KEY_RDEVID, buf, sizeof(buf) ); XP_Bool have = db_fetch( pDb, KEY_RDEVID, buf, sizeof(buf) )
&& 0 == strcmp( buf, devID );
if ( !have ) { if ( !have ) {
db_store( pDb, KEY_RDEVID, devID ); db_store( pDb, KEY_RDEVID, devID );
} else {
XP_ASSERT( 0 == strcmp( buf, devID ) );
} }
(void)g_timeout_add_seconds( maxInterval, keepalive_timer, globals ); (void)g_timeout_add_seconds( maxInterval, keepalive_timer, globals );
} else { } else {

View file

@ -112,6 +112,12 @@ destroyCairo( GtkDrawCtx* dctx )
dctx->_cairo = NULL; dctx->_cairo = NULL;
} }
static XP_Bool
haveCairo( const GtkDrawCtx* dctx )
{
return !!dctx->_cairo;
}
static cairo_t* static cairo_t*
getCairo( const GtkDrawCtx* dctx ) getCairo( const GtkDrawCtx* dctx )
{ {
@ -1231,15 +1237,20 @@ gtk_draw_drawTimer( DrawCtx* p_dctx, const XP_Rect* rInner,
XP_U16 playerNum, XP_S16 secondsLeft ) XP_U16 playerNum, XP_S16 secondsLeft )
{ {
GtkDrawCtx* dctx = (GtkDrawCtx*)p_dctx; GtkDrawCtx* dctx = (GtkDrawCtx*)p_dctx;
XP_Bool hadCairo = haveCairo( dctx );
if ( hadCairo || initCairo( dctx ) ) {
XP_UCHAR buf[10]; XP_UCHAR buf[10];
gtkFormatTimerText( buf, VSIZE(buf), secondsLeft ); gtkFormatTimerText( buf, VSIZE(buf), secondsLeft );
/* gdk_gc_set_clip_rectangle( dctx->drawGC, (GdkRectangle*)rInner ); */
gtkEraseRect( dctx, rInner ); gtkEraseRect( dctx, rInner );
draw_string_at( dctx, NULL, buf, rInner->height-1, draw_string_at( dctx, NULL, buf, rInner->height-1,
rInner, XP_GTK_JUST_CENTER, rInner, XP_GTK_JUST_CENTER,
&dctx->playerColors[playerNum], NULL ); &dctx->playerColors[playerNum], NULL );
if ( !hadCairo ) {
destroyCairo( dctx );
}
}
} /* gtk_draw_drawTimer */ } /* gtk_draw_drawTimer */
#ifdef XWFEATURE_MINIWIN #ifdef XWFEATURE_MINIWIN

View file

@ -1577,7 +1577,7 @@ parsePair( const char* optarg, XP_U16* min, XP_U16* max )
} else { } else {
int intmin, intmax; int intmin, intmax;
if ( 2 == sscanf( optarg, "%d:%d", &intmin, &intmax ) ) { if ( 2 == sscanf( optarg, "%d:%d", &intmin, &intmax ) ) {
if ( intmin <= intmin ) { if ( intmin <= intmax ) {
*min = intmin; *min = intmin;
*max = intmax; *max = intmax;
success = true; success = true;
@ -2284,6 +2284,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 +2293,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 +2304,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;

View file

@ -257,12 +257,13 @@ relaycon_receive( GIOChannel* source, GIOCondition XP_UNUSED_DBG(condition), gpo
gchar* b64 = g_base64_encode( (const guchar*)buf, gchar* b64 = g_base64_encode( (const guchar*)buf,
((0 <= nRead)? nRead : 0) ); ((0 <= nRead)? nRead : 0) );
XP_LOGF( "%s: read %zd bytes ('%s')", __func__, nRead, b64 ); XP_LOGF( "%s: read %zd bytes ('%s')", __func__, nRead, b64 );
g_free( b64 );
#ifdef COMMS_CHECKSUM #ifdef COMMS_CHECKSUM
gchar* sum = g_compute_checksum_for_data( G_CHECKSUM_MD5, buf, nRead ); 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 ); XP_LOGF( "%s: read %zd bytes ('%s')(sum=%s)", __func__, nRead, b64, sum );
g_free( sum ); g_free( sum );
#endif #endif
g_free( b64 );
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;

View file

@ -0,0 +1,985 @@
#!/usr/bin/env python3
import re, os, sys, getopt, shutil, threading, requests, json, glob
import argparse, datetime, random, subprocess, time
# LOGDIR=./$(basename $0)_logs
# APP_NEW=""
# DO_CLEAN=""
# APP_NEW_PARAMS=""
# NGAMES = 1
g_UDP_PCT_START = 100
# UDP_PCT_INCR=10
# UPGRADE_ODDS=""
# NROOMS=""
# HOST=""
# PORT=""
# TIMEOUT=""
# SAVE_GOOD=""
# MINDEVS=""
# MAXDEVS=""
# ONEPER=""
# RESIGN_PCT=0
g_DROP_N=0
# MINRUN=2 # seconds
# ONE_PER_ROOM="" # don't run more than one device at a time per room
# USE_GTK=""
# UNDO_PCT=0
# ALL_VIA_RQ=${ALL_VIA_RQ:-FALSE}
# SEED=""
# BOARD_SIZES_OLD=(15)
# BOARD_SIZES_NEW=(15)
g_NAMES = [None, 'Brynn', 'Ariela', 'Kati', 'Eric']
# SEND_CHAT=''
# CORE_COUNT=$(ls core.* 2>/dev/null | wc -l)
# DUP_PACKETS=''
# HTTP_PCT=0
# declare -A PIDS
# declare -A APPS
# declare -A NEW_ARGS
# declare -a ARGS
# declare -A ARGS_DEVID
# declare -A ROOMS
# declare -A FILES
# declare -A LOGS
# declare -A MINEND
# ROOM_PIDS = {}
# declare -a APPS_OLD=()
# declare -a DICTS= # wants to be =() too?
# declare -A CHECKED_ROOMS
# function cleanup() {
# APP="$(basename $APP_NEW)"
# while pidof $APP; do
# echo "killing existing $APP instances..."
# killall -9 $APP
# sleep 1
# done
# echo "cleaning everything up...."
# if [ -d $LOGDIR ]; then
# mv $LOGDIR /tmp/${LOGDIR}_$$
# fi
# if [ -e $(dirname $0)/../../relay/xwrelay.log ]; then
# mkdir -p /tmp/${LOGDIR}_$$
# mv $(dirname $0)/../../relay/xwrelay.log /tmp/${LOGDIR}_$$
# fi
# echo "DELETE FROM games WHERE room LIKE 'ROOM_%';" | psql -q -t xwgames
# echo "DELETE FROM msgs WHERE NOT devid in (SELECT unnest(devids) from games);" | psql -q -t xwgames
# }
# function connName() {
# LOG=$1
# grep -a 'got_connect_cmd: connName' $LOG | \
# tail -n 1 | \
# sed 's,^.*connName: \"\(.*\)\" (reconnect=.)$,\1,'
# }
# function check_room() {
# ROOM=$1
# if [ -z ${CHECKED_ROOMS[$ROOM]:-""} ]; then
# NUM=$(echo "SELECT COUNT(*) FROM games "\
# "WHERE NOT dead "\
# "AND ntotal!=sum_array(nperdevice) "\
# "AND ntotal != -sum_array(nperdevice) "\
# "AND room='$ROOM'" |
# psql -q -t xwgames)
# NUM=$((NUM+0))
# if [ "$NUM" -gt 0 ]; then
# echo "$ROOM in the DB has unconsummated games. Remove them."
# exit 1
# else
# CHECKED_ROOMS[$ROOM]=1
# fi
# fi
# }
# print_cmdline() {
# local COUNTER=$1
# local LOG=${LOGS[$COUNTER]}
# echo -n "New cmdline: " >> $LOG
# echo "${APPS[$COUNTER]} ${NEW_ARGS[$COUNTER]} ${ARGS[$COUNTER]}" >> $LOG
# }
def pick_ndevs(args):
RNUM = random.randint(0, 99)
if RNUM > 90 and args.MAXDEVS >= 4:
NDEVS = 4
elif RNUM > 75 and args.MAXDEVS >= 3:
NDEVS = 3
else:
NDEVS = 2
if NDEVS < args.MINDEVS:
NDEVS = args.MINDEVS
return NDEVS
# # Given a device count, figure out how many local players per device.
# # "1 1" would be a two-device game with 1 each. "1 2 1" a
# # three-device game with four players total
def figure_locals(args, NDEVS):
NPLAYERS = pick_ndevs(args)
if NPLAYERS < NDEVS: NPLAYERS = NDEVS
EXTRAS = 0
if not args.ONEPER:
EXTRAS = NPLAYERS - NDEVS
LOCALS = []
for IGNORE in range(NDEVS):
COUNT = 1
if EXTRAS > 0:
EXTRA = random.randint(0, EXTRAS)
if EXTRA > 0:
COUNT += EXTRA
EXTRAS -= EXTRA
LOCALS.append(COUNT)
assert 0 < sum(LOCALS) <= 4
return LOCALS
def player_params(args, NLOCALS, NPLAYERS, NAME_INDX):
assert 0 < NPLAYERS <= 4
NREMOTES = NPLAYERS - NLOCALS
PARAMS = []
while NLOCALS > 0 or NREMOTES > 0:
if 0 == random.randint(0, 2) and 0 < NLOCALS:
PARAMS += ['--robot', g_NAMES[NAME_INDX], '--robot-iq', str(random.randint(1,100))]
NLOCALS -= 1
NAME_INDX += 1
elif 0 < NREMOTES:
PARAMS += ['--remote-player']
NREMOTES -= 1
return PARAMS
def logReaderStub(dev): dev.logReaderMain()
class Device():
sConnnameMap = {}
sHasLDevIDMap = {}
sConnNamePat = re.compile('.*got_connect_cmd: connName: "([^"]+)".*$')
sGameOverPat = re.compile('.*\[unused tiles\].*')
sTilesLeftPat = re.compile('.*pool_removeTiles: (\d+) tiles left in pool')
sRelayIDPat = re.compile('.*UPDATE games.*seed=(\d+),.*relayid=\'([^\']+)\'.*')
def __init__(self, args, indx, app, params, room, db, log, nInGame):
self.indx = indx
self.args = args
self.pid = 0
self.gameOver = False
self.app = app
self.params = params
self.room = room
self.db = db
self.logPath = log
self.nInGame = nInGame
# runtime stuff; init now
self.proc = None
self.connname = None
self.devID = ''
self.launchCount = 0
self.allDone = False # when true, can be killed
self.nTilesLeft = -1 # negative means don't know
self.relayID = None
self.relaySeed = 0
with open(self.logPath, "w") as log:
log.write('New cmdline: ' + self.app + ' ' + (' '.join([str(p) for p in self.params])))
log.write(os.linesep)
def logReaderMain(self):
assert self and self.proc
stdout, stderr = self.proc.communicate()
# print('logReaderMain called; opening:', self.logPath, 'flag:', flag)
nLines = 0
with open(self.logPath, 'a') as log:
for line in stderr.splitlines():
nLines += 1
log.write(line + os.linesep)
# check for connname
if not self.connname:
match = Device.sConnNamePat.match(line)
if match:
self.connname = match.group(1)
if not self.connname in Device.sConnnameMap:
Device.sConnnameMap[self.connname] = set()
Device.sConnnameMap[self.connname].add(self)
# check for game over
if not self.gameOver:
match = Device.sGameOverPat.match(line)
if match: self.gameOver = True
# Check every line for tiles left
match = Device.sTilesLeftPat.match(line)
if match: self.nTilesLeft = int(match.group(1))
if not self.relayID:
match = Device.sRelayIDPat.match(line)
if match:
self.relaySeed = int(match.group(1))
self.relayID = match.group(2)
# print('logReaderMain done, wrote lines:', nLines, 'to', self.logPath);
def launch(self):
args = [self.app] + [str(p) for p in self.params]
if self.devID: args.extend( ' '.split(self.devID))
self.launchCount += 1
# self.logStream = open(self.logPath, flag)
self.proc = subprocess.Popen(args, stdout = subprocess.DEVNULL,
stderr = subprocess.PIPE, universal_newlines = True)
self.pid = self.proc.pid
self.minEnd = datetime.datetime.now() + datetime.timedelta(seconds = self.args.MINRUN)
# Now start a thread to read stdio
self.reader = threading.Thread(target = logReaderStub, args=(self,))
self.reader.isDaemon = True
self.reader.start()
def running(self):
return self.proc and not self.proc.poll()
def minTimeExpired(self):
assert self.proc
return self.minEnd < datetime.datetime.now()
def kill(self):
if self.proc.poll() is None:
self.proc.terminate()
self.proc.wait()
assert self.proc.poll() is not None
self.reader.join()
self.reader = None
else:
print('NOT killing')
self.proc = None
self.check_game()
def moveFiles(self):
assert not self.running()
shutil.move(self.logPath, self.args.LOGDIR + '/done')
shutil.move(self.db, self.args.LOGDIR + '/done')
def send_dead(self):
JSON = json.dumps([{'relayID': self.relayID, 'seed': self.relaySeed}])
url = 'http://%s/xw4/relay.py/kill' % (self.args.HOST)
req = requests.get(url, params = {'params' : JSON})
def getTilesCount(self):
result = None
if self.nTilesLeft != -1:
result = '%.2d:%.2d' % (self.indx, self.nTilesLeft)
return result
def update_ldevid(self):
if not self.app in Device.sHasLDevIDMap:
hasLDevID = False
proc = subprocess.Popen([self.app, '--help'], stderr=subprocess.PIPE)
# output, err, = proc.communicate()
for line in proc.stderr.readlines():
if b'--ldevid' in line:
hasLDevID = True
break
print('found --ldevid:', hasLDevID);
Device.sHasLDevIDMap[self.app] = hasLDevID
if Device.sHasLDevIDMap[self.app]:
RNUM = random.randint(0, 99)
if not self.devID:
if RNUM < 30:
self.devID = '--ldevid LINUX_TEST_%.5d_' % (self.indx)
elif RNUM < 10:
self.devID += 'x'
def check_game(self):
if self.gameOver and not self.allDone:
allDone = False
if len(Device.sConnnameMap[self.connname]) == self.nInGame:
allDone = True
for dev in Device.sConnnameMap[self.connname]:
if dev == self: continue
if not dev.gameOver:
allDone = False
break
if allDone:
for dev in Device.sConnnameMap[self.connname]:
dev.allDone = True
# print('Closing', self.connname, datetime.datetime.now())
# for dev in Device.sConnnameMap[self.connname]:
# dev.kill()
# # kill_from_logs $OTHERS $KEY
# for ID in $OTHERS $KEY; do
# echo -n "${ID}:${LOGS[$ID]}, "
# kill_from_log ${LOGS[$ID]} || /bin/true
# send_dead $ID
# close_device $ID $DONEDIR "game over"
# done
# echo ""
# # XWRELAY_ERROR_DELETED may be old
# elif grep -aq 'relay_error_curses(XWRELAY_ERROR_DELETED)' $LOG; then
# echo "deleting $LOG $(connName $LOG) b/c another resigned"
# kill_from_log $LOG || /bin/true
# close_device $KEY $DEADDIR "other resigned"
# elif grep -aq 'relay_error_curses(XWRELAY_ERROR_DEADGAME)' $LOG; then
# echo "deleting $LOG $(connName $LOG) b/c another resigned"
# kill_from_log $LOG || /bin/true
# close_device $KEY $DEADDIR "other resigned"
# else
# maybe_resign $KEY
# fi
# }
def build_cmds(args):
devs = []
COUNTER = 0
PLAT_PARMS = []
if not args.USE_GTK:
PLAT_PARMS += ['--curses', '--close-stdin']
for GAME in range(1, args.NGAMES + 1):
ROOM = 'ROOM_%.3d' % (GAME % args.NROOMS)
# check_room $ROOM
NDEVS = pick_ndevs(args)
LOCALS = figure_locals(args, NDEVS) # as array
NPLAYERS = sum(LOCALS)
assert(len(LOCALS) == NDEVS)
DICT = args.DICTS[GAME % len(args.DICTS)]
# make one in three games public
PUBLIC = []
if random.randint(0, 3) == 0: PUBLIC = ['--make-public', '--join-public']
DEV = 0
for NLOCALS in LOCALS:
DEV += 1
FILE="%s/GAME_%d_%d.sql3" % (args.LOGDIR, GAME, DEV)
LOG='%s/%d_%d_LOG.txt' % (args.LOGDIR, GAME, DEV)
# os.system("rm -f $LOG") # clear the log
# APPS[$COUNTER]="$APP_NEW"
# NEW_ARGS[$COUNTER]="$APP_NEW_PARAMS"
BOARD_SIZE = ['--board-size', '15']
# if [ 0 -lt ${#APPS_OLD[@]} ]; then
# # 50% chance of starting out with old app
# NAPPS=$((1+${#APPS_OLD[*]}))
# if [ 0 -lt $((RANDOM%$NAPPS)) ]; then
# APPS[$COUNTER]=${APPS_OLD[$((RANDOM%${#APPS_OLD[*]}))]}
# BOARD_SIZE="--board-size ${BOARD_SIZES_OLD[$((RANDOM%${#BOARD_SIZES_OLD[*]}))]}"
# NEW_ARGS[$COUNTER]=""
# fi
# fi
PARAMS = player_params(args, NLOCALS, NPLAYERS, DEV)
PARAMS += PLAT_PARMS
PARAMS += BOARD_SIZE + ['--room', ROOM, '--trade-pct', args.TRADE_PCT, '--sort-tiles']
if args.UNDO_PCT > 0:
PARAMS += ['--undo-pct', args.UNDO_PCT]
PARAMS += [ '--game-dict', DICT, '--relay-port', args.PORT, '--host', args.HOST]
PARAMS += ['--slow-robot', '1:3', '--skip-confirm']
PARAMS += ['--db', FILE]
if random.randint(0,100) % 100 < g_UDP_PCT_START:
PARAMS += ['--use-udp']
PARAMS += ['--drop-nth-packet', g_DROP_N]
if random.randint(0, 100) < args.HTTP_PCT:
PARAMS += ['--use-http']
PARAMS += ['--split-packets', '2']
if args.SEND_CHAT:
PARAMS += ['--send-chat', args.SEND_CHAT]
if args.DUP_PACKETS:
PARAMS += ['--dup-packets']
# PARAMS += ['--my-port', '1024']
# PARAMS += ['--savefail-pct', 10]
# With the --seed param passed, games with more than 2
# devices don't get going. No idea why. This param is NOT
# passed in the old bash version of this script, so fixing
# it isn't a priority.
# PARAMS += ['--seed', args.SEED]
PARAMS += PUBLIC
if DEV > 1:
PARAMS += ['--force-channel', DEV - 1]
else:
PARAMS += ['--server']
# print('PARAMS:', PARAMS)
dev = Device(args, COUNTER, args.APP_NEW, PARAMS, ROOM, FILE, LOG, len(LOCALS))
dev.update_ldevid()
devs.append(dev)
COUNTER += 1
return devs
# read_resume_cmds() {
# COUNTER=0
# for LOG in $(ls $LOGDIR/*.txt); do
# echo "need to parse cmd and deal with changes"
# exit 1
# CMD=$(head -n 1 $LOG)
# ARGS[$COUNTER]=$CMD
# LOGS[$COUNTER]=$LOG
# PIDS[$COUNTER]=0
# set $CMD
# while [ $# -gt 0 ]; do
# case $1 in
# --file)
# FILES[$COUNTER]=$2
# shift
# ;;
# --room)
# ROOMS[$COUNTER]=$2
# shift
# ;;
# esac
# shift
# done
# COUNTER=$((COUNTER+1))
# done
# ROOM_PIDS[$ROOM]=0
# }
# launch() {
# KEY=$1
# LOG=${LOGS[$KEY]}
# APP="${APPS[$KEY]}"
# if [ -z "$APP" ]; then
# echo "error: no app set"
# exit 1
# fi
# PARAMS="${NEW_ARGS[$KEY]} ${ARGS[$KEY]} ${ARGS_DEVID[$KEY]}"
# exec $APP $PARAMS >/dev/null 2>>$LOG
# }
# # launch_via_rq() {
# # KEY=$1
# # RELAYID=$2
# # PIPE=${PIPES[$KEY]}
# # ../relay/rq -f $RELAYID -o $PIPE &
# # CMD="${CMDS[$KEY]}"
# # exec $CMD >/dev/null 2>>$LOG
# # }
# send_dead() {
# ID=$1
# DB=${FILES[$ID]}
# while :; do
# [ -f $DB ] || break # it's gone
# RES=$(echo 'select relayid, seed from games limit 1;' | sqlite3 -separator ' ' $DB || /bin/true)
# [ -n "$RES" ] && break
# sleep 0.2
# done
# RELAYID=$(echo $RES | awk '{print $1}')
# SEED=$(echo $RES | awk '{print $2}')
# JSON="[{\"relayID\":\"$RELAYID\", \"seed\":$SEED}]"
# curl -G --data-urlencode params="$JSON" http://$HOST/xw4/relay.py/kill >/dev/null 2>&1
# }
# close_device() {
# ID=$1
# MVTO=$2
# REASON="$3"
# PID=${PIDS[$ID]}
# if [ $PID -ne 0 ]; then
# kill ${PIDS[$ID]} 2>/dev/null
# wait ${PIDS[$ID]}
# ROOM=${ROOMS[$ID]}
# [ ${ROOM_PIDS[$ROOM]} -eq $PID ] && ROOM_PIDS[$ROOM]=0
# fi
# unset PIDS[$ID]
# unset ARGS[$ID]
# echo "closing game: $REASON" >> ${LOGS[$ID]}
# if [ -n "$MVTO" ]; then
# [ -f "${FILES[$ID]}" ] && mv ${FILES[$ID]} $MVTO
# mv ${LOGS[$ID]} $MVTO
# else
# rm -f ${FILES[$ID]}
# rm -f ${LOGS[$ID]}
# fi
# unset FILES[$ID]
# unset LOGS[$ID]
# unset ROOMS[$ID]
# unset APPS[$ID]
# unset ARGS_DEVID[$ID]
# COUNT=${#ARGS[*]}
# echo "$COUNT devices left playing..."
# }
# OBITS=""
# kill_from_log() {
# LOG=$1
# RELAYID=$(./scripts/relayID.sh --long $LOG)
# if [ -n "$RELAYID" ]; then
# OBITS="$OBITS -d $RELAYID"
# if [ 0 -eq $(($RANDOM%2)) ]; then
# ../relay/rq -a $HOST $OBITS 2>/dev/null || /bin/true
# OBITS=""
# fi
# return 0 # success
# fi
# echo "unable to send kill command for $LOG"
# return 1
# }
# maybe_resign() {
# if [ "$RESIGN_PCT" -gt 0 ]; then
# KEY=$1
# LOG=${LOGS[$KEY]}
# if grep -aq XWRELAY_ALLHERE $LOG; then
# if [ $((${RANDOM}%100)) -lt $RESIGN_PCT ]; then
# echo "making $LOG $(connName $LOG) resign..."
# kill_from_log $LOG && close_device $KEY $DEADDIR "resignation forced" || /bin/true
# fi
# fi
# fi
# }
# try_upgrade() {
# KEY=$1
# if [ 0 -lt ${#APPS_OLD[@]} ]; then
# if [ $APP_NEW != "${APPS[$KEY]}" ]; then
# # one in five chance of upgrading
# if [ 0 -eq $((RANDOM % UPGRADE_ODDS)) ]; then
# APPS[$KEY]=$APP_NEW
# NEW_ARGS[$KEY]="$APP_NEW_PARAMS"
# print_cmdline $KEY
# fi
# fi
# fi
# }
# try_upgrade_upd() {
# KEY=$1
# CMD=${ARGS[$KEY]}
# if [ "${CMD/--use-udp/}" = "${CMD}" ]; then
# if [ $((RANDOM % 100)) -lt $UDP_PCT_INCR ]; then
# ARGS[$KEY]="$CMD --use-udp"
# echo -n "$(date +%r): "
# echo "upgrading key $KEY to use UDP"
# fi
# fi
# }
# check_game() {
# KEY=$1
# LOG=${LOGS[$KEY]}
# CONNNAME="$(connName $LOG)"
# OTHERS=""
# if [ -n "$CONNNAME" ]; then
# if grep -aq '\[unused tiles\]' $LOG ; then
# for INDX in ${!LOGS[*]}; do
# [ $INDX -eq $KEY ] && continue
# ALOG=${LOGS[$INDX]}
# CONNNAME2="$(connName $ALOG)"
# if [ "$CONNNAME2" = "$CONNNAME" ]; then
# if ! grep -aq '\[unused tiles\]' $ALOG; then
# OTHERS=""
# break
# fi
# OTHERS="$OTHERS $INDX"
# fi
# done
# fi
# fi
# if [ -n "$OTHERS" ]; then
# echo -n "Closing $CONNNAME [$(date)]: "
# # kill_from_logs $OTHERS $KEY
# for ID in $OTHERS $KEY; do
# echo -n "${ID}:${LOGS[$ID]}, "
# kill_from_log ${LOGS[$ID]} || /bin/true
# send_dead $ID
# close_device $ID $DONEDIR "game over"
# done
# echo ""
# # XWRELAY_ERROR_DELETED may be old
# elif grep -aq 'relay_error_curses(XWRELAY_ERROR_DELETED)' $LOG; then
# echo "deleting $LOG $(connName $LOG) b/c another resigned"
# kill_from_log $LOG || /bin/true
# close_device $KEY $DEADDIR "other resigned"
# elif grep -aq 'relay_error_curses(XWRELAY_ERROR_DEADGAME)' $LOG; then
# echo "deleting $LOG $(connName $LOG) b/c another resigned"
# kill_from_log $LOG || /bin/true
# close_device $KEY $DEADDIR "other resigned"
# else
# maybe_resign $KEY
# fi
# }
# increment_drop() {
# KEY=$1
# CMD=${ARGS[$KEY]}
# if [ "$CMD" != "${CMD/drop-nth-packet//}" ]; then
# DROP_N=$(echo $CMD | sed 's,^.*drop-nth-packet \(-*[0-9]*\) .*$,\1,')
# if [ $DROP_N -gt 0 ]; then
# NEXT_N=$((DROP_N+1))
# ARGS[$KEY]=$(echo $CMD | sed "s,^\(.*drop-nth-packet \)$DROP_N\(.*\)$,\1$NEXT_N\2,")
# fi
# fi
# }
def summarizeTileCounts(devs):
nDevs = len(devs)
strs = [dev.getTilesCount() for dev in devs]
strs = [s for s in strs if s]
nWithTiles = len(strs)
print('%s %d/%d %s' % (datetime.datetime.now().strftime("%H:%M:%S"), nDevs, nWithTiles, ' '.join(strs)))
def countCores():
return len(glob.glob1('/tmp',"core*"))
def run_cmds(args, devs):
nCores = countCores()
endTime = datetime.datetime.now() + datetime.timedelta(seconds = args.TIMEOUT)
LOOPCOUNT = 0
while len(devs) > 0:
if countCores() > nCores:
print('core file count increased; exiting')
break
if datetime.datetime.now() > endTime:
print('outta time; outta here')
break
LOOPCOUNT += 1
if 0 == LOOPCOUNT % 20: summarizeTileCounts(devs)
dev = random.choice(devs)
if not dev.running():
if dev.allDone:
dev.moveFiles()
dev.send_dead()
devs.remove(dev)
else:
# if [ -n "$ONE_PER_ROOM" -a 0 -ne ${ROOM_PIDS[$ROOM]} ]; then
# continue
# fi
# try_upgrade $KEY
# try_upgrade_upd $KEY
dev.launch()
# PID=$!
# # renice doesn't work on one of my machines...
# renice -n 1 -p $PID >/dev/null 2>&1 || /bin/true
# PIDS[$KEY]=$PID
# ROOM_PIDS[$ROOM]=$PID
# MINEND[$KEY]=$(($NOW + $MINRUN))
elif not dev.minTimeExpired():
# print('sleeping...')
time.sleep(2)
else:
dev.kill()
# if g_DROP_N >= 0: dev.increment_drop()
# update_ldevid $KEY
# if we get here via a break, kill any remaining games
if devs:
print('stopping %d remaining games' % (len(devs)))
for dev in devs:
if dev.running(): dev.kill()
# run_via_rq() {
# # launch then kill all games to give chance to hook up
# for KEY in ${!ARGS[*]}; do
# echo "launching $KEY"
# launch $KEY &
# PID=$!
# sleep 1
# kill $PID
# wait $PID
# # add_pipe $KEY
# done
# echo "now running via rq"
# # then run them
# while :; do
# COUNT=${#ARGS[*]}
# [ 0 -ge $COUNT ] && break
# INDX=$(($RANDOM%COUNT))
# KEYS=( ${!ARGS[*]} )
# KEY=${KEYS[$INDX]}
# CMD=${ARGS[$KEY]}
# RELAYID=$(./scripts/relayID.sh --short ${LOGS[$KEY]})
# MSG_COUNT=$(../relay/rq -a $HOST -m $RELAYID 2>/dev/null | sed 's,^.*-- ,,')
# if [ $MSG_COUNT -gt 0 ]; then
# launch $KEY &
# PID=$!
# sleep 2
# kill $PID || /bin/true
# wait $PID
# fi
# [ "$DROP_N" -ge 0 ] && increment_drop $KEY
# check_game $KEY
# done
# } # run_via_rq
# function getArg() {
# [ 1 -lt "$#" ] || usage "$1 requires an argument"
# echo $2
# }
def mkParser():
parser = argparse.ArgumentParser()
parser.add_argument('--send-chat', dest = 'SEND_CHAT', type = str, default = None,
help = 'the message to send')
parser.add_argument('--app-new', dest = 'APP_NEW', default = './obj_linux_memdbg/xwords',
help = 'the app we\'ll use')
parser.add_argument('--num-games', dest = 'NGAMES', type = int, default = 1, help = 'number of games')
parser.add_argument('--num-rooms', dest = 'NROOMS', type = int, default = 0,
help = 'number of roooms (default to --num-games)')
parser.add_argument('--no-timeout', dest = 'TIMEOUT', default = False, action = 'store_true',
help = 'run forever (default proportional to number of games')
parser.add_argument('--log-root', dest='LOGROOT', default = '.', help = 'where logfiles go')
parser.add_argument('--dup-packets', dest = 'DUP_PACKETS', default = False, help = 'send all packet twice')
parser.add_argument('--use-gtk', dest = 'USE_GTK', default = False, action = 'store_true',
help = 'run games using gtk instead of ncurses')
# #
# # echo " [--clean-start] \\" >&2
parser.add_argument('--game-dict', dest = 'DICTS', action = 'append', default = [])
# # echo " [--help] \\" >&2
parser.add_argument('--host', dest = 'HOST', default = 'localhost',
help = 'relay hostname')
# # echo " [--max-devs <int>] \\" >&2
parser.add_argument('--min-devs', dest = 'MINDEVS', type = int, default = 2,
help = 'No game will have fewer devices than this')
parser.add_argument('--max-devs', dest = 'MAXDEVS', type = int, default = 4,
help = 'No game will have more devices than this')
parser.add_argument('--min-run', dest = 'MINRUN', type = int, default = 2,
help = 'Keep each run alive at least this many seconds')
# # echo " [--new-app <path/to/app] \\" >&2
# # echo " [--new-app-args [arg*]] # passed only to new app \\" >&2
# # echo " [--num-rooms <int>] \\" >&2
# # echo " [--old-app <path/to/app]* \\" >&2
parser.add_argument('--one-per', dest = 'ONEPER', default = False,
action = 'store_true', help = 'force one player per device')
parser.add_argument('--port', dest = 'PORT', default = 10997, type = int, \
help = 'Port relay\'s on')
parser.add_argument('--resign-pct', dest = 'RESIGN_PCT', default = 0, type = int, \
help = 'Odds of resigning [0..100]')
# # echo " [--no-timeout] # run until all games done \\" >&2
parser.add_argument('--seed', type = int, dest = 'SEED',
default = random.randint(1, 1000000000))
# # echo " [--send-chat <interval-in-seconds> \\" >&2
# # echo " [--udp-incr <pct>] \\" >&2
# # echo " [--udp-start <pct>] # default: $UDP_PCT_START \\" >&2
# # echo " [--undo-pct <int>] \\" >&2
parser.add_argument('--http-pct', dest = 'HTTP_PCT', default = 0, type = int,
help = 'pct of games to be using web api')
parser.add_argument('--undo-pct', dest = 'UNDO_PCT', default = 0, type = int)
parser.add_argument('--trade-pct', dest = 'TRADE_PCT', default = 0, type = int)
return parser
# #######################################################
# ##################### MAIN begins #####################
# #######################################################
def parseArgs():
args = mkParser().parse_args()
assignDefaults(args)
print(args)
return args
# print(options)
# while [ "$#" -gt 0 ]; do
# case $1 in
# --udp-start)
# UDP_PCT_START=$(getArg $*)
# shift
# ;;
# --udp-incr)
# UDP_PCT_INCR=$(getArg $*)
# shift
# ;;
# --clean-start)
# DO_CLEAN=1
# ;;
# --num-games)
# NGAMES=$(getArg $*)
# shift
# ;;
# --num-rooms)
# NROOMS=$(getArg $*)
# shift
# ;;
# --old-app)
# APPS_OLD[${#APPS_OLD[@]}]=$(getArg $*)
# shift
# ;;
# --log-root)
# [ -d $2 ] || usage "$1: no such directory $2"
# LOGDIR=$2/$(basename $0)_logs
# shift
# ;;
# --dup-packets)
# DUP_PACKETS=1
# ;;
# --new-app)
# APP_NEW=$(getArg $*)
# shift
# ;;
# --new-app-args)
# APP_NEW_PARAMS="${2}"
# echo "got $APP_NEW_PARAMS"
# shift
# ;;
# --game-dict)
# DICTS[${#DICTS[@]}]=$(getArg $*)
# shift
# ;;
# --min-devs)
# MINDEVS=$(getArg $*)
# shift
# ;;
# --max-devs)
# MAXDEVS=$(getArg $*)
# shift
# ;;
# --min-run)
# MINRUN=$(getArg $*)
# [ $MINRUN -ge 2 -a $MINRUN -le 60 ] || usage "$1: n must be 2 <= n <= 60"
# shift
# ;;
# --one-per)
# ONEPER=TRUE
# ;;
# --host)
# HOST=$(getArg $*)
# shift
# ;;
# --port)
# PORT=$(getArg $*)
# shift
# ;;
# --seed)
# SEED=$(getArg $*)
# shift
# ;;
# --undo-pct)
# UNDO_PCT=$(getArg $*)
# shift
# ;;
# --http-pct)
# HTTP_PCT=$(getArg $*)
# [ $HTTP_PCT -ge 0 -a $HTTP_PCT -le 100 ] || usage "$1: n must be 0 <= n <= 100"
# shift
# ;;
# --send-chat)
# SEND_CHAT=$(getArg $*)
# shift
# ;;
# --resign-pct)
# RESIGN_PCT=$(getArg $*)
# [ $RESIGN_PCT -ge 0 -a $RESIGN_PCT -le 100 ] || usage "$1: n must be 0 <= n <= 100"
# shift
# ;;
# --no-timeout)
# TIMEOUT=0x7FFFFFFF
# ;;
# --help)
# usage
# ;;
# *) usage "unrecognized option $1"
# ;;
# esac
# shift
# done
def assignDefaults(args):
if not args.NROOMS: args.NROOMS = args.NGAMES
args.TIMEOUT = not args.TIMEOUT and (args.NGAMES * 60 + 500) or 100000000000
if len(args.DICTS) == 0: args.DICTS.append('CollegeEng_2to8.xwd')
args.LOGDIR = os.path.basename(sys.argv[0]) + '_logs'
# Move an existing logdir aside
if os.path.exists(args.LOGDIR):
shutil.move(args.LOGDIR, '/tmp/' + args.LOGDIR + '_' + str(random.randint(0, 100000)))
for d in ['', 'done', 'dead',]:
os.mkdir(args.LOGDIR + '/' + d)
# [ -z "$SAVE_GOOD" ] && SAVE_GOOD=YES
# # [ -z "$RESIGN_PCT" -a "$NGAMES" -gt 1 ] && RESIGN_RATIO=1000 || RESIGN_RATIO=0
# [ -z "$DROP_N" ] && DROP_N=0
# [ -z "$USE_GTK" ] && USE_GTK=FALSE
# [ -z "$UPGRADE_ODDS" ] && UPGRADE_ODDS=10
# #$((NGAMES/50))
# [ 0 -eq $UPGRADE_ODDS ] && UPGRADE_ODDS=1
# [ -n "$SEED" ] && RANDOM=$SEED
# [ -z "$ONEPER" -a $NROOMS -lt $NGAMES ] && usage "use --one-per if --num-rooms < --num-games"
# [ -n "$DO_CLEAN" ] && cleanup
# RESUME=""
# for FILE in $(ls $LOGDIR/*.{xwg,txt} 2>/dev/null); do
# if [ -e $FILE ]; then
# echo "Unfinished games found in $LOGDIR; continue with them (or discard)?"
# read -p "<yes/no> " ANSWER
# case "$ANSWER" in
# y|yes|Y|YES)
# RESUME=1
# ;;
# *)
# ;;
# esac
# fi
# break
# done
# if [ -z "$RESUME" -a -d $LOGDIR ]; then
# NEWNAME="$(basename $LOGDIR)_$$"
# (cd $(dirname $LOGDIR) && mv $(basename $LOGDIR) /tmp/${NEWNAME})
# fi
# mkdir -p $LOGDIR
# if [ "$SAVE_GOOD" = YES ]; then
# DONEDIR=$LOGDIR/done
# mkdir -p $DONEDIR
# fi
# DEADDIR=$LOGDIR/dead
# mkdir -p $DEADDIR
# for VAR in NGAMES NROOMS USE_GTK TIMEOUT HOST PORT SAVE_GOOD \
# MINDEVS MAXDEVS ONEPER RESIGN_PCT DROP_N ALL_VIA_RQ SEED \
# APP_NEW; do
# echo "$VAR:" $(eval "echo \$${VAR}") 1>&2
# done
# echo "DICTS: ${DICTS[*]}"
# echo -n "APPS_OLD: "; [ xx = "${APPS_OLD[*]+xx}" ] && echo "${APPS_OLD[*]}" || echo ""
# echo "*********$0 starting: $(date)**************"
# STARTTIME=$(date +%s)
# [ -z "$RESUME" ] && build_cmds || read_resume_cmds
# if [ TRUE = "$ALL_VIA_RQ" ]; then
# run_via_rq
# else
# run_cmds
# fi
# wait
# SECONDS=$(($(date +%s)-$STARTTIME))
# HOURS=$((SECONDS/3600))
# SECONDS=$((SECONDS%3600))
# MINUTES=$((SECONDS/60))
# SECONDS=$((SECONDS%60))
# echo "*********$0 finished: $(date) (took $HOURS:$MINUTES:$SECONDS)**************"
def main():
args = parseArgs()
devs = build_cmds(args)
run_cmds(args, devs)
##############################################################################
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)
@ -70,6 +70,24 @@ endif
memdebug all: xwrelay rq memdebug all: xwrelay rq
REQUIRED_DEBS = libpq-dev g++ libglib2.0-dev postgresql \
.PHONY: debcheck debs_install
debs_install:
sudo apt-get install $(REQUIRED_DEBS)
debcheck:
@if which dpkg; then \
for DEB in $(REQUIRED_DEBS); do \
if ! dpkg -l $$DEB >/dev/null 2>&1; then \
echo "$$DEB not installed"; \
echo "try running 'make debs_install'"; \
break; \
fi \
done; \
fi
# Manual config in order to place -lpq after the .obj files as # Manual config in order to place -lpq after the .obj files as
# required by something Ubuntu did upgrading natty to oneiric # required by something Ubuntu did upgrading natty to oneiric
xwrelay: $(OBJ) xwrelay: $(OBJ)

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 )
{ {
@ -649,10 +691,14 @@ SafeCref::SafeCref( const char* connName, const char* cookie, HostID hid,
nPlayersS, gameSeed, langCode, nPlayersS, gameSeed, langCode,
wantsPublic || makePublic, &isDead ); wantsPublic || makePublic, &isDead );
/* If the reconnect doesn't check out, treat it as a connect */ /* If the reconnect doesn't check out, treat it as a connect. But
preserve the existing hid. If the DB was deleted it's important
that devices keep their places (hids) */
if ( NULL == cinfo ) { if ( NULL == cinfo ) {
logf( XW_LOGINFO, "%s: taking a second crack", __func__ ); logf( XW_LOGINFO, "%s: taking a second crack; (cur hid: %d)",
m_hid = HOST_ID_NONE; __func__, hid );
assert( m_hid == hid );
// m_hid = HOST_ID_NONE; /* wrong; but why was I doing it? */
cinfo = m_mgr->getMakeCookieRef( cookie, nPlayersH, nPlayersS, cinfo = m_mgr->getMakeCookieRef( cookie, nPlayersH, nPlayersS,
langCode, gameSeed, clientIndx, langCode, gameSeed, clientIndx,
wantsPublic, makePublic, &m_seenSeed ); wantsPublic, makePublic, &m_seenSeed );
@ -668,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();
@ -718,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,43 +1041,51 @@ DBMgr::CountStoredMessages( DevIDRelay relayID )
return getCountWhere( MSGS_TABLE, test ); return getCountWhere( MSGS_TABLE, test );
} }
void int
DBMgr::StoreMessage( DevIDRelay devID, const uint8_t* const buf, DBMgr::StoreMessage( DevIDRelay destDevID, const uint8_t* const buf,
int len ) int len )
{ {
clearHasNoMessages( devID ); int msgID = 0;
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 ) {
gchar* b64 = g_base64_encode( buf, len ); gchar* b64 = g_base64_encode( buf, len );
query.catf( fmt, "msg64", devID, "", b64, len ); query.catf( fmt, "msg64", destDevID, "", b64, len );
g_free( b64 ); g_free( b64 );
} else { } else {
uint8_t* bytes = PQescapeByteaConn( getThreadConn(), buf, uint8_t* bytes = PQescapeByteaConn( getThreadConn(), buf,
len, &newLen ); len, &newLen );
assert( NULL != bytes ); assert( NULL != bytes );
query.catf( fmt, "msg", devID, "E", bytes, len ); query.catf( fmt, "msg", destDevID, "E", bytes, len );
PQfreemem( bytes ); PQfreemem( bytes );
} }
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 hid, DBMgr::StoreMessage( const char* const connName, int destHid,
const uint8_t* buf, int len ) const uint8_t* buf, int len )
{ {
clearHasNoMessages( connName, hid ); int msgID = 0;
clearHasNoMessages( connName, destHid );
DevIDRelay devID = getDevID( connName, hid ); DevIDRelay devID = getDevID( connName, destHid );
if ( DEVID_NONE == devID ) { if ( DEVID_NONE == devID ) {
logf( XW_LOGERROR, "%s: warning: devid not found for connName=%s, " logf( XW_LOGERROR, "%s: warning: devid not found for connName=%s, "
"hid=%d", __func__, connName, hid ); "hid=%d", __func__, connName, destHid );
} else { } else {
clearHasNoMessages( devID ); clearHasNoMessages( devID );
} }
@ -1066,7 +1101,7 @@ DBMgr::StoreMessage( const char* const connName, int hid,
StrWPF query; StrWPF query;
if ( m_useB64 ) { if ( m_useB64 ) {
gchar* b64 = g_base64_encode( buf, len ); gchar* b64 = g_base64_encode( buf, len );
query.catf( fmt, "msg64", connName, hid, devID, hid, connName, query.catf( fmt, "msg64", connName, destHid, devID, destHid, connName,
"", b64, len ); "", b64, len );
query.catf( " WHERE NOT EXISTS (SELECT 1 FROM " MSGS_TABLE query.catf( " WHERE NOT EXISTS (SELECT 1 FROM " MSGS_TABLE
@ -1074,20 +1109,28 @@ DBMgr::StoreMessage( const char* const connName, int hid,
#ifdef HAVE_STIME #ifdef HAVE_STIME
" AND stime='epoch'" " AND stime='epoch'"
#endif #endif
" );", connName, hid, b64 ); " )", connName, destHid, b64 );
g_free( b64 ); g_free( b64 );
} else { } else {
uint8_t* bytes = PQescapeByteaConn( getThreadConn(), buf, uint8_t* bytes = PQescapeByteaConn( getThreadConn(), buf,
len, &newLen ); len, &newLen );
assert( NULL != bytes ); assert( NULL != bytes );
query.catf( fmt, "msg", connName, hid, devID, hid, connName, query.catf( fmt, "msg", connName, destHid, devID, destHid, connName,
"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,8 +141,9 @@ 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 relayID, const uint8_t* const buf, int len ); int StoreMessage( DevIDRelay destRelayID, const uint8_t* const buf,
void StoreMessage( const char* const connName, int hid, int len );
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,
@ -170,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();

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

@ -0,0 +1,250 @@
#!/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')
msgsLists = {}
try:
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
except:
None
return json.dumps(msgsLists)
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

@ -54,13 +54,14 @@ echo "; relay pid[s]: $(pidof xwrelay)"
echo "Row count:" $(psql -t xwgames -c "select count(*) FROM games $QUERY;") echo "Row count:" $(psql -t xwgames -c "select count(*) FROM games $QUERY;")
# 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

@ -550,18 +550,18 @@ assemble_packet( vector<uint8_t>& packet, uint32_t* packetIDP, XWRelayReg cmd,
} }
#ifdef LOG_UDP_PACKETS #ifdef LOG_UDP_PACKETS
gsize size = 0; // gsize size = 0;
gint state = 0; // gint state = 0;
gint save = 0; // gint save = 0;
gchar out[1024]; // gchar out[1024];
for ( unsigned int ii = 0; ii < iocount; ++ii ) { // for ( unsigned int ii = 0; ii < iocount; ++ii ) {
size += g_base64_encode_step( (const guchar*)vec[ii].iov_base, // size += g_base64_encode_step( (const guchar*)vec[ii].iov_base,
vec[ii].iov_len, // vec[ii].iov_len,
FALSE, &out[size], &state, &save ); // FALSE, &out[size], &state, &save );
} // }
size += g_base64_encode_close( FALSE, &out[size], &state, &save ); // size += g_base64_encode_close( FALSE, &out[size], &state, &save );
assert( size < sizeof(out) ); // assert( size < sizeof(out) );
out[size] = '\0'; // out[size] = '\0';
#endif #endif
} }
@ -640,8 +640,10 @@ send_via_udp_impl( int sock, const struct sockaddr* dest_addr,
#ifdef LOG_UDP_PACKETS #ifdef LOG_UDP_PACKETS
gchar* b64 = g_base64_encode( (uint8_t*)dest_addr, gchar* b64 = g_base64_encode( (uint8_t*)dest_addr,
sizeof(*dest_addr) ); sizeof(*dest_addr) );
gchar* out = g_base64_encode( packet.data(), packet.size() );
logf( XW_LOGINFO, "%s()=>%d; addr='%s'; msg='%s'", __func__, nSent, logf( XW_LOGINFO, "%s()=>%d; addr='%s'; msg='%s'", __func__, nSent,
b64, out ); b64, out );
g_free( out );
g_free( b64 ); g_free( b64 );
#else #else
logf( XW_LOGINFO, "%s()=>%d", __func__, nSent ); logf( XW_LOGINFO, "%s()=>%d", __func__, nSent );
@ -760,15 +762,19 @@ send_havemsgs( const AddrInfo* addr )
class MsgClosure { class MsgClosure {
public: public:
MsgClosure( DevIDRelay devid, const vector<uint8_t>* packet, MsgClosure( DevIDRelay dest, const vector<uint8_t>* packet,
OnMsgAckProc proc, void* procClosure ) int msgID, OnMsgAckProc proc, void* procClosure )
{ {
m_devid = devid; assert(m_msgID != 0);
m_destDevID = dest;
m_packet = *packet; m_packet = *packet;
m_proc = proc; m_proc = proc;
m_procClosure = procClosure; m_procClosure = procClosure;
m_msgID = msgID;
} }
DevIDRelay m_devid; int getMsgID() { return m_msgID; }
int m_msgID;
DevIDRelay m_destDevID;
vector<uint8_t> m_packet; vector<uint8_t> m_packet;
OnMsgAckProc m_proc; OnMsgAckProc m_proc;
void* m_procClosure; void* m_procClosure;
@ -778,22 +784,29 @@ static void
onPostedMsgAcked( bool acked, uint32_t packetID, void* data ) onPostedMsgAcked( bool acked, uint32_t packetID, void* data )
{ {
MsgClosure* mc = (MsgClosure*)data; MsgClosure* mc = (MsgClosure*)data;
if ( !acked ) { int msgID = mc->getMsgID();
DBMgr::Get()->StoreMessage( mc->m_devid, mc->m_packet.data(), if ( acked ) {
mc->m_packet.size() ); DBMgr::Get()->RemoveStoredMessages( &msgID, 1 );
} else {
assert( msgID != 0 );
// So we only store after ack fails? Change that!!!
// DBMgr::Get()->StoreMessage( mc->m_destDevID, mc->m_packet.data(),
// mc->m_packet.size() );
} }
if ( NULL != mc->m_proc ) { if ( NULL != mc->m_proc ) {
(*mc->m_proc)( acked, mc->m_devid, packetID, mc->m_procClosure ); (*mc->m_proc)( acked, mc->m_destDevID, packetID, mc->m_procClosure );
} }
delete mc; delete mc;
} }
static bool static bool
post_or_store( DevIDRelay devid, vector<uint8_t>& packet, uint32_t packetID, post_or_store( DevIDRelay destDevID, vector<uint8_t>& packet, uint32_t packetID,
OnMsgAckProc proc, void* procClosure ) OnMsgAckProc proc, void* procClosure )
{ {
const AddrInfo::AddrUnion* addru = DevMgr::Get()->get( devid ); int msgID = DBMgr::Get()->StoreMessage( destDevID, packet.data(), packet.size() );
const AddrInfo::AddrUnion* addru = DevMgr::Get()->get( destDevID );
bool canSendNow = !!addru; bool canSendNow = !!addru;
bool sent = false; bool sent = false;
@ -804,21 +817,18 @@ post_or_store( DevIDRelay devid, vector<uint8_t>& packet, uint32_t packetID,
if ( get_addr_info_if( &addr, &sock, &dest_addr ) ) { if ( get_addr_info_if( &addr, &sock, &dest_addr ) ) {
sent = 0 < send_packet_via_udp_impl( packet, sock, dest_addr ); sent = 0 < send_packet_via_udp_impl( packet, sock, dest_addr );
if ( sent ) { if ( sent && msgID != 0 ) {
MsgClosure* mc = new MsgClosure( devid, &packet, MsgClosure* mc = new MsgClosure( destDevID, &packet, msgID,
proc, procClosure ); proc, procClosure );
UDPAckTrack::setOnAck( onPostedMsgAcked, packetID, (void*)mc ); UDPAckTrack::setOnAck( onPostedMsgAcked, packetID, (void*)mc );
} }
} }
} }
if ( !sent ) {
DBMgr::Get()->StoreMessage( devid, packet.data(), packet.size() );
}
return sent; return sent;
} }
bool bool
post_message( DevIDRelay devid, const char* message, OnMsgAckProc proc, post_message( DevIDRelay destDevID, const char* message, OnMsgAckProc proc,
void* procClosure ) void* procClosure )
{ {
vector<uint8_t> packet; vector<uint8_t> packet;
@ -830,7 +840,7 @@ post_message( DevIDRelay devid, const char* message, OnMsgAckProc proc,
assemble_packet( packet, &packetID, XWPDEV_ALERT, lenbuf, lenlen, assemble_packet( packet, &packetID, XWPDEV_ALERT, lenbuf, lenlen,
message, len, NULL ); message, len, NULL );
return post_or_store( devid, packet, packetID, proc, procClosure ); return post_or_store( destDevID, packet, packetID, proc, procClosure );
} }
void void
@ -988,13 +998,13 @@ processReconnect( const uint8_t* bufp, int bufLen, const AddrInfo* addr )
} /* processReconnect */ } /* processReconnect */
static bool static bool
processAck( const uint8_t* bufp, int bufLen, const AddrInfo* addr ) processAck( const uint8_t* bufp, int bufLen, AddrInfo::ClientToken clientToken )
{ {
bool success = false; bool success = false;
const uint8_t* end = bufp + bufLen; const uint8_t* end = bufp + bufLen;
HostID srcID; HostID srcID;
if ( getNetByte( &bufp, end, &srcID ) ) { if ( getNetByte( &bufp, end, &srcID ) ) {
SafeCref scr( addr ); SafeCref scr( clientToken, srcID );
success = scr.HandleAck( srcID ); success = scr.HandleAck( srcID );
} }
return success; return success;
@ -1084,7 +1094,8 @@ forwardMessage( const uint8_t* buf, int buflen, const AddrInfo* addr )
} /* forwardMessage */ } /* forwardMessage */
static bool static bool
processMessage( const uint8_t* buf, int bufLen, const AddrInfo* addr ) processMessage( const uint8_t* buf, int bufLen, const AddrInfo* addr,
AddrInfo::ClientToken clientToken )
{ {
bool success = false; /* default is failure */ bool success = false; /* default is failure */
XWRELAY_Cmd cmd = *buf; XWRELAY_Cmd cmd = *buf;
@ -1099,7 +1110,11 @@ processMessage( const uint8_t* buf, int bufLen, const AddrInfo* addr )
success = processReconnect( buf+1, bufLen-1, addr ); success = processReconnect( buf+1, bufLen-1, addr );
break; break;
case XWRELAY_ACK: case XWRELAY_ACK:
success = processAck( buf+1, bufLen-1, addr ); if ( clientToken != 0 ) {
success = processAck( buf+1, bufLen-1, clientToken );
} else {
logf( XW_LOGERROR, "%s(): null client token", __func__ );
}
break; break;
case XWRELAY_GAME_DISCONNECT: case XWRELAY_GAME_DISCONNECT:
success = processDisconnect( buf+1, bufLen-1, addr ); success = processDisconnect( buf+1, bufLen-1, addr );
@ -1334,6 +1349,9 @@ handleMsgsMsg( const AddrInfo* addr, bool sendFull,
logf( XW_LOGVERBOSE0, "%s: wrote %d bytes", __func__, nwritten ); logf( XW_LOGVERBOSE0, "%s: wrote %d bytes", __func__, nwritten );
if ( sendFull && nwritten >= 0 && (size_t)nwritten == out.size() ) { if ( sendFull && nwritten >= 0 && (size_t)nwritten == out.size() ) {
dbmgr->RecordSent( &msgIDs[0], msgIDs.size() ); dbmgr->RecordSent( &msgIDs[0], msgIDs.size() );
// This is wrong: should be removed when ACK returns and not
// before. But for some reason if I make that change apps wind up
// stalling.
dbmgr->RemoveStoredMessages( msgIDs ); dbmgr->RemoveStoredMessages( msgIDs );
} }
} }
@ -1438,7 +1456,7 @@ handleProxyMsgs( int sock, const AddrInfo* addr, const uint8_t* bufp,
} }
unsigned short nMsgs; unsigned short nMsgs;
if ( getNetShort( &bufp, end, &nMsgs ) ) { if ( getNetShort( &bufp, end, &nMsgs ) ) {
SafeCref scr( connName ); SafeCref scr( connName, hid );
while ( scr.IsValid() && nMsgs-- > 0 ) { while ( scr.IsValid() && nMsgs-- > 0 ) {
unsigned short len; unsigned short len;
if ( getNetShort( &bufp, end, &len ) ) { if ( getNetShort( &bufp, end, &len ) ) {
@ -1460,7 +1478,7 @@ handleProxyMsgs( int sock, const AddrInfo* addr, const uint8_t* bufp,
static void static void
game_thread_proc( UdpThreadClosure* utc ) game_thread_proc( UdpThreadClosure* utc )
{ {
if ( !processMessage( utc->buf(), utc->len(), utc->addr() ) ) { if ( !processMessage( utc->buf(), utc->len(), utc->addr(), 0 ) ) {
XWThreadPool::GetTPool()->CloseSocket( utc->addr() ); XWThreadPool::GetTPool()->CloseSocket( utc->addr() );
} }
} }
@ -1528,7 +1546,7 @@ proxy_thread_proc( UdpThreadClosure* utc )
sizeof( connName ), &hid ) ) { sizeof( connName ), &hid ) ) {
break; break;
} }
SafeCref scr( connName ); SafeCref scr( connName, hid );
scr.DeviceGone( hid, seed ); scr.DeviceGone( hid, seed );
} }
} }
@ -1748,7 +1766,7 @@ handle_udp_packet( UdpThreadClosure* utc )
clientToken = ntohl( clientToken ); clientToken = ntohl( clientToken );
if ( AddrInfo::NULL_TOKEN != clientToken ) { if ( AddrInfo::NULL_TOKEN != clientToken ) {
AddrInfo addr( g_udpsock, clientToken, utc->saddr() ); AddrInfo addr( g_udpsock, clientToken, utc->saddr() );
(void)processMessage( ptr, end - ptr, &addr ); (void)processMessage( ptr, end - ptr, &addr, clientToken );
} else { } else {
logf( XW_LOGERROR, "%s: dropping packet with token of 0", logf( XW_LOGERROR, "%s: dropping packet with token of 0",
__func__ ); __func__ );
@ -1766,7 +1784,7 @@ handle_udp_packet( UdpThreadClosure* utc )
logf( XW_LOGERROR, "parse failed!!!" ); logf( XW_LOGERROR, "parse failed!!!" );
break; break;
} }
SafeCref scr( connName ); SafeCref scr( connName, hid );
if ( scr.IsValid() ) { if ( scr.IsValid() ) {
AddrInfo addr( g_udpsock, clientToken, utc->saddr() ); AddrInfo addr( g_udpsock, clientToken, utc->saddr() );
handlePutMessage( scr, hid, &addr, end - ptr, &ptr, end ); handlePutMessage( scr, hid, &addr, end - ptr, &ptr, end );
@ -1833,7 +1851,7 @@ handle_udp_packet( UdpThreadClosure* utc )
string connName; string connName;
if ( DBMgr::Get()->FindPlayer( devID.asRelayID(), clientToken, if ( DBMgr::Get()->FindPlayer( devID.asRelayID(), clientToken,
connName, &hid, &seed ) ) { connName, &hid, &seed ) ) {
SafeCref scr( connName.c_str() ); SafeCref scr( connName.c_str(), hid );
scr.DeviceGone( hid, seed ); scr.DeviceGone( hid, seed );
} }
} }
@ -1980,7 +1998,7 @@ maint_str_loop( int udpsock, const char* str )
} // maint_str_loop } // maint_str_loop
static uint32_t static uint32_t
getIPAddr( void ) getUDPIPAddr( void )
{ {
uint32_t result = INADDR_ANY; uint32_t result = INADDR_ANY;
char iface[16] = {0}; char iface[16] = {0};
@ -2215,7 +2233,7 @@ main( int argc, char** argv )
struct sockaddr_in saddr; struct sockaddr_in saddr;
g_udpsock = socket( AF_INET, SOCK_DGRAM, IPPROTO_UDP ); g_udpsock = socket( AF_INET, SOCK_DGRAM, IPPROTO_UDP );
saddr.sin_family = PF_INET; saddr.sin_family = PF_INET;
saddr.sin_addr.s_addr = getIPAddr(); saddr.sin_addr.s_addr = getUDPIPAddr();
saddr.sin_port = htons(udpport); saddr.sin_port = htons(udpport);
int err = bind( g_udpsock, (struct sockaddr*)&saddr, sizeof(saddr) ); int err = bind( g_udpsock, (struct sockaddr*)&saddr, sizeof(saddr) );
if ( 0 == err ) { if ( 0 == err ) {

View file

@ -14,7 +14,7 @@ LOGFILE=/tmp/xwrelay_log_$$.txt
date > $LOGFILE date > $LOGFILE
usage() { usage() {
echo "usage: $0 start | stop | restart | mkdb" echo "usage: $0 start | stop | restart | mkdb | debs_install"
} }
make_db() { make_db() {
@ -28,7 +28,7 @@ make_db() {
exit 1 exit 1
fi fi
createdb $DBNAME createdb $DBNAME
cat | psql $DBNAME --file - <<EOF cat <<-EOF | psql $DBNAME --file -
create or replace function sum_array( DECIMAL [] ) create or replace function sum_array( DECIMAL [] )
returns decimal returns decimal
as \$\$ as \$\$
@ -40,7 +40,7 @@ from generate_series(
\$\$ language sql immutable; \$\$ language sql immutable;
EOF EOF
cat | psql $DBNAME --file - <<EOF cat <<-EOF | psql $DBNAME --file -
CREATE TABLE games ( CREATE TABLE games (
cid integer cid integer
,room VARCHAR(32) ,room VARCHAR(32)
@ -62,7 +62,7 @@ cid integer
); );
EOF EOF
cat | psql $DBNAME --file - <<EOF cat <<-EOF | psql $DBNAME --file -
CREATE TABLE msgs ( CREATE TABLE msgs (
id SERIAL id SERIAL
,connName VARCHAR(64) ,connName VARCHAR(64)
@ -78,7 +78,7 @@ id SERIAL
); );
EOF EOF
cat | psql $DBNAME --file - <<EOF cat <<-EOF | psql $DBNAME --file -
CREATE TABLE devices ( CREATE TABLE devices (
id INTEGER UNIQUE PRIMARY KEY id INTEGER UNIQUE PRIMARY KEY
,devTypes INTEGER[] ,devTypes INTEGER[]
@ -114,6 +114,10 @@ do_start() {
fi fi
} }
install_debs() {
sudo apt-get install postgresql-client postgresql
}
case $1 in case $1 in
stop) stop)
@ -149,6 +153,9 @@ case $1 in
make_db make_db
;; ;;
debs_install)
install_debs
;;
*) *)
usage usage
exit 0 exit 0

View file

@ -52,8 +52,8 @@ void send_havemsgs( const AddrInfo* addr );
typedef void (*OnMsgAckProc)( bool acked, DevIDRelay devid, uint32_t packetID, typedef void (*OnMsgAckProc)( bool acked, DevIDRelay devid, uint32_t packetID,
void* data ); void* data );
bool post_message( DevIDRelay devid, const char* message, OnMsgAckProc proc, bool post_message( DevIDRelay destDevID, const char* message,
void* data ); OnMsgAckProc proc, void* data );
void post_upgrade( DevIDRelay devid ); void post_upgrade( DevIDRelay devid );
time_t uptime(void); time_t uptime(void);