Merge branch 'android_branch' into android_translate

This commit is contained in:
Eric House 2017-11-20 06:53:34 -08:00
commit 3216ef8a5c
76 changed files with 2163 additions and 432 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 VERSION_CODE_BASE = 121
def VERSION_NAME = '4.4.125'
def VERSION_CODE_BASE = 126
def VERSION_NAME = '4.4.130'
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')
@ -37,8 +39,6 @@ android {
applicationVariants.all { variant ->
// renameArtifact(variant)
// 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\""
resValue "string", "git_rev", "$GITREV"
@ -51,9 +51,6 @@ android {
// FIX ME
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 "boolean", "FOR_FDROID", "$forFDroid"
@ -61,6 +58,10 @@ android {
flavorDimensions "variant"//, "abi"
productFlavors {
all {
buildConfigField "String", "BUILD_INFO_NAME", "\"${BUILD_INFO_NAME}\""
}
xw4 {
dimension "variant"
applicationId "org.eehouse.android.xw4"
@ -70,6 +71,8 @@ android {
resValue "string", "invite_prefix", "/and/"
buildConfigField "boolean", "WIDIR_ENABLED", "false"
buildConfigField "boolean", "RELAYINVITE_SUPPORTED", "false"
buildConfigField "String", "GCM_SENDER_ID", "\"$GCM_SENDER_ID\""
}
xw4d {
dimension "variant"
@ -81,6 +84,8 @@ android {
resValue "string", "invite_prefix", "/anddbg/"
buildConfigField "boolean", "WIDIR_ENABLED", "true"
buildConfigField "boolean", "RELAYINVITE_SUPPORTED", "true"
buildConfigField "String", "GCM_SENDER_ID", "\"\""
}
// WARNING: "all" breaks things. Seems to be a keyword. Need
@ -254,9 +259,14 @@ afterEvaluate {
task makeBuildAssets() {
def assetsDir = android.sourceSets.main.assets.srcDirs.toArray()[0]
String path = new File(assetsDir, 'build-info.txt').getAbsolutePath()
File file = new File(path);
file.write("git: ${GITREV}\n");
String path = new File(assetsDir, BUILD_INFO_NAME).getAbsolutePath()
String out = "git: ${GITREV}\n"
String diff = "git diff".execute().text.trim()
if (diff) {
out += "\n" + diff
}
new File(path).write(out)
}
gradle.projectsEvaluated {

View file

@ -34,13 +34,7 @@
/>
<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.WAKE_LOCK" />
<uses-permission android:name="android.permission.NFC" />
<application android:icon="@drawable/icon48x48"
@ -208,16 +202,5 @@
</intent-filter>
</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>
</manifest>

View file

@ -13,10 +13,9 @@
</style>
</head>
<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
with title bars on some Samsung devices.</p>
<p>This release makes a couple of small UI tweaks.</p>
<div id="survey">
<p>Please <a href="https://www.surveymonkey.com/s/GX3XLHR">take
@ -26,10 +25,12 @@
<h3>New with this release</h3>
<ul>
<li>Fix delays bringing up the Invite dialog for new games</li>
<li>Explicitly specify application "theme" to fix a Samsung
"upgrade" turning the titlebar white and so making menu
icons disappear</li>
<li>Offer to &quot;Archive&quot; finished games</li>
<li>Make tap on thumbnail toggle whether game's selected rather
than open it. (Tap to the right still opens)</li>
<li>Bug fix: don't allow duplicate group names</li>
<li>Fix battery-hogging behavior on non-Google-play
installs</li>
</ul>
<p>(The full changelog

View file

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

View file

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

View file

@ -322,26 +322,35 @@ 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 &&
(m_lastSecsLeft != secondsLeft || m_lastTimerPlayer != player) ) {
m_lastSecsLeft = secondsLeft;
m_lastTimerPlayer = player;
if ( m_lastSecsLeft != secondsLeft || m_lastTimerPlayer != player ) {
final Rect rectCopy = new Rect(rect);
final int secondsLeftCopy = secondsLeft;
m_activity.runOnUiThread( new Runnable() {
@Override
public void run() {
if ( null != m_jniThread ) {
m_lastSecsLeft = secondsLeftCopy;
m_lastTimerPlayer = player;
String negSign = secondsLeft < 0? "-":"";
secondsLeft = Math.abs( secondsLeft );
String time = String.format( "%s%d:%02d", negSign, secondsLeft/60,
secondsLeft%60 );
String negSign = secondsLeftCopy < 0? "-":"";
int secondsLeft = Math.abs( secondsLeftCopy );
String time =
String.format( "%s%d:%02d", negSign,
secondsLeft/60, secondsLeft%60 );
fillRectOther( rect, CommonPrefs.COLOR_BACKGRND );
m_fillPaint.setColor( m_playerColors[player] );
fillRectOther( rectCopy, CommonPrefs.COLOR_BACKGRND );
m_fillPaint.setColor( m_playerColors[player] );
Rect shorter = new Rect( rect );
shorter.inset( 0, shorter.height() / 5 );
drawCentered( time, shorter, null );
rectCopy.inset( 0, rectCopy.height() / 5 );
drawCentered( time, rectCopy, null );
m_jniThread.handle( JNIThread.JNICmd.CMD_DRAW );
m_jniThread.handle( JNIThread.JNICmd.CMD_DRAW );
}
}
} );
}
}

View file

@ -104,7 +104,6 @@ public class BoardDelegate extends DelegateBase
private Button m_exchCancelButton;
private SentInvitesInfo m_sentInfo;
private Perms23.PermCbck m_permCbck;
private ArrayList<String> m_pendingChats;
private CommsConnTypeSet m_connTypes = null;
private String[] m_missingDevs;
@ -205,6 +204,25 @@ public class BoardDelegate extends DelegateBase
}
};
ab.setNegativeButton( R.string.button_rematch, lstnr );
// If we're not already in the "archive" group, offer to move
final String archiveName = LocUtils
.getString( m_activity, R.string.group_name_archive );
final long archiveGroup = DBUtils.getGroup( m_activity, archiveName );
long curGroup = DBUtils.getGroupForGame( m_activity, m_rowid );
if ( curGroup != archiveGroup ) {
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
&& BuildConfig.DEBUG && null != m_connTypes
&& (m_connTypes.contains( CommsConnType.COMMS_CONN_RELAY )
@ -553,8 +571,6 @@ public class BoardDelegate extends DelegateBase
m_isFirstLaunch = null == savedInstanceState;
getBundledData( savedInstanceState );
m_pendingChats = new ArrayList<String>();
m_utils = new BoardUtilCtxt();
m_timers = new TimerRunnable[4]; // needs to be in sync with
// XWTimerReason
@ -843,7 +859,7 @@ public class BoardDelegate extends DelegateBase
Utils.setItemVisible( menu, R.id.board_menu_game_invites, enable );
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;
} // onPrepareOptionsMenu
@ -913,7 +929,7 @@ public class BoardDelegate extends DelegateBase
case R.id.board_menu_tray:
cmd = JNICmd.CMD_TOGGLE_TRAY;
break;
case R.id.games_menu_study:
case R.id.board_menu_study:
StudyListDelegate.launchOrAlert( getDelegator(), m_gi.dictLang, this );
break;
case R.id.board_menu_game_netstats:
@ -1095,6 +1111,12 @@ public class BoardDelegate extends DelegateBase
makeOkOnlyBuilder( R.string.after_restart ).show();
break;
case ARCHIVE_ACTION:
String archiveName = (String)params[0];
long archiveGroup = (Long)params[1];
archiveAndClose( archiveName, archiveGroup );
break;
case ENABLE_SMS_DO:
post( new Runnable() {
public void run() {
@ -2144,7 +2166,6 @@ public class BoardDelegate extends DelegateBase
if ( m_gi.serverRole != DeviceRole.SERVER_STANDALONE ) {
warnIfNoTransport();
trySendChats();
tickle( isStart );
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()
{
if ( 0 < m_mySIS.nMissing && m_summary.hasRematchInfo() ) {
@ -2588,6 +2600,16 @@ public class BoardDelegate extends DelegateBase
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
private boolean rematchSupported( boolean showMulti )
{

View file

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

View file

@ -52,9 +52,6 @@ public class ConnStatusHandler {
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_OUT = 1;
private static final int SHOW_SUCCESS_INTERVAL = 1000;
@ -340,7 +337,7 @@ public class ConnStatusHandler {
boolean 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 );
}

View file

@ -81,7 +81,9 @@ public class DBUtils {
private static long s_cachedRowID = ROWID_NOTFOUND;
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 void gameSaved( long rowid, GameChangeType change );
@ -1616,21 +1618,34 @@ public class DBUtils {
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 )
{
long[] result = null;
initDB( context );
String[] columns = { ROW_ID };
String[] columns = { ROW_ID, DBHelper.HASMSGS };
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 ) {
Cursor cursor = s_db.query( DBHelper.TABLE_NAME_SUM, columns,
selection, // selection
null, // args
null, // groupBy
null, // having
orderBy
s_getGroupGamesOrderBy
);
int index = cursor.getColumnIndex( ROW_ID );
result = new long[ cursor.getCount() ];
@ -1688,6 +1703,29 @@ public class DBUtils {
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 )
{
long rowid = GROUPID_UNSPEC;
@ -1746,13 +1784,14 @@ public class DBUtils {
}
// 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 );
ContentValues values = new ContentValues();
values.put( DBHelper.GROUPID, groupID );
updateRow( context, DBHelper.TABLE_NAME_SUM, gameid, values );
updateRow( context, DBHelper.TABLE_NAME_SUM, rowid, values );
invalGroupsCache();
notifyListeners( rowid, GameChangeType.GAME_MOVED );
}
private static String getChatHistoryStr( Context context, long rowid )

View file

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

View file

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

View file

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

View file

@ -64,7 +64,7 @@ public class GameListItem extends LinearLayout
private LinearLayout m_list;
private TextView m_state;
private TextView m_modTime;
private ImageView m_marker;
private ImageView m_gameTypeImage;
private TextView m_role;
private boolean m_expanded, m_haveTurn, m_haveTurnLocal;
@ -90,16 +90,6 @@ public class GameListItem extends LinearLayout
m_lastMoveTime = 0;
m_loadingCount = 0;
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()
@ -174,13 +164,32 @@ public class GameListItem extends LinearLayout
}
// View.OnClickListener interface
public void onClick( View view ) {
m_expanded = !m_expanded;
DBUtils.setExpanded( m_rowid, m_expanded );
public void onClick( View view )
{
int id = view.getId();
switch ( id ) {
case R.id.expander:
m_expanded = !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()
@ -191,12 +200,15 @@ public class GameListItem extends LinearLayout
m_expandButton.setOnClickListener( this );
m_viewUnloaded = (TextView)findViewById( R.id.view_unloaded );
m_viewLoaded = findViewById( R.id.view_loaded );
m_viewLoaded.setOnClickListener( this );
m_list = (LinearLayout)findViewById( R.id.player_list );
m_state = (TextView)findViewById( R.id.state );
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_role = (TextView)findViewById( R.id.role );
findViewById( R.id.right_side ).setOnClickListener( this );
}
private void setLoaded( boolean loaded )
@ -316,19 +328,20 @@ public class GameListItem extends LinearLayout
int iconID = summary.isMultiGame() ?
R.drawable.multigame__gen : R.drawable.sologame__gen;
m_marker.setImageResource( iconID );
m_marker.setOnClickListener( new View.OnClickListener() {
@Override
public void onClick( View view ) {
toggleSelected();
}
} );
m_gameTypeImage.setImageResource( iconID );
boolean hasChat = summary.isMultiGame();
if ( hasChat ) {
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 );
m_role.setVisibility( null == roleSummary ? View.GONE : View.VISIBLE );
if ( null != roleSummary ) {
m_role.setText( roleSummary );
} else {
m_role.setVisibility( View.GONE );
}
update( expanded, summary.lastMoveTime, haveATurn,
@ -420,6 +433,7 @@ public class GameListItem extends LinearLayout
// }
// GameListAdapter.ClickHandler interface
@Override
public void longClicked()
{
toggleSelected();

View file

@ -1196,7 +1196,7 @@ public class GameUtils {
for ( CommsConnType typ : conTypes ) {
switch ( typ ) {
case COMMS_CONN_RELAY:
tellRelayDied( context, summary, informNow );
// see below
break;
case COMMS_CONN_BT:
BTService.gameDied( context, addr.bt_btAddr, gameID );
@ -1210,6 +1210,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();
}

View file

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

View file

@ -184,7 +184,7 @@ public class NetUtils {
short len = dis.readShort();
if ( len > 0 ) {
byte[] packet = new byte[len];
dis.read( packet );
dis.readFully( 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
// from DataInputStream so parse it myself.
byte[] bytes = new byte[len];
dis.read( bytes );
dis.readFully( bytes );
int index = -1;
for ( int ii = 0; ii < nRooms; ++ii ) {

View file

@ -736,7 +736,7 @@ public class RelayService extends XWService
case XWPDEV_MSG:
int token = dis.readInt();
byte[] msg = new byte[dis.available()];
dis.read( msg );
dis.readFully( msg );
postData( this, token, msg );
// game-related packets only count
@ -756,9 +756,8 @@ public class RelayService extends XWService
resetBackoff = true;
intent = getIntentTo( this, MsgCmds.GOT_INVITE );
int srcDevID = dis.readInt();
short len = dis.readShort();
byte[] nliData = new byte[len];
dis.read( nliData );
byte[] nliData = new byte[dis.readShort()];
dis.readFully( nliData );
NetLaunchInfo nli = XwJNI.nliFromStream( nliData );
intent.putExtra( INVITE_FROM, srcDevID );
String asStr = nli.toString();
@ -995,9 +994,8 @@ public class RelayService extends XWService
private String getVLIString( DataInputStream dis )
throws java.io.IOException
{
int len = vli2un( dis );
byte[] tmp = new byte[len];
dis.read( tmp );
byte[] tmp = new byte[vli2un( dis )];
dis.readFully( tmp );
String result = new String( tmp );
return result;
}

View file

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

View file

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

View file

@ -32,7 +32,9 @@ import android.content.Intent;
import android.content.SharedPreferences;
import android.content.pm.PackageManager;
import android.content.pm.ResolveInfo;
import android.content.res.AssetManager;
import android.content.res.Configuration;
import android.text.ClipboardManager;
import android.database.Cursor;
import android.media.Ringtone;
@ -55,9 +57,12 @@ import android.widget.Toast;
import org.json.JSONException;
import org.json.JSONObject;
import java.io.BufferedReader;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.Serializable;
@ -187,6 +192,33 @@ public class Utils {
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,
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();
public static final boolean BTSUPPORTED = true;
public static final boolean GCMSUPPORTED = true;
public static final boolean ATTACH_SUPPORTED = false;
public static final boolean LOG_LIFECYLE = 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 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 Boolean s_onEmulator = null;
private static Context s_context = null;

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -120,5 +120,8 @@
<item android:id="@+id/games_menu_loaddb"
android:title="@string/gamel_menu_loaddb"
/>
<item android:id="@+id/games_menu_writegit"
android:title="@string/gamel_menu_writegit"
/>
</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_hidenewgamebuttons">key_notagain_hidenewgamebuttons</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_browseall">key_na_browseall</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,
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_newgroup">New group</string>
@ -2123,7 +2131,7 @@
<string name="newgroup_label">Name your new 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_moveup">Move up</string>
<string name="list_group_movedown">Move down</string>
@ -2158,6 +2166,10 @@
game with the same players and parameters as the one that
just ended. -->
<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>
@ -2419,11 +2431,11 @@
<string name="set_pref">Hide buttons</string>
<string name="not_again_hidenewgamebuttons">These two buttons do
the same thing as the first two items in this window\'s Action Bar
(or menu). If you like you can hide the buttons to make more games
visible.\n\n(If you later want to unhide them go to the Appearance
section of App settings).
<string name="not_again_hidenewgamebuttons">The two buttons at the
bottom of this screen and the first two items in its Action Bar
(or menu) do the same thing. If you like you can hide the buttons
to make more games visible.\n\n(If you later want to unhide the
buttons go to the Appearance section of App settings).
</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="gamel_menu_storedb">Write games to 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="xlations_locale">Fake locale for translation</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"?>
<resources>
<style name="AppTheme" parent="android:Theme.Material"/>
<style name="config_separator">
<item name="android:layout_height">wrap_content</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:capitalize="words"
android:defaultValue=""
android:maxLines="1"
android:maxLength="32"
android:inputType="text"
/>
<org.eehouse.android.xw4.XWEditTextPreference
android:key="@string/key_robot_name"
android:title="@string/robot_label"
android:capitalize="words"
android:defaultValue="@string/button_default_robot"
android:maxLines="1"
android:maxLength="32"
android:inputType="text"
/>
</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 junit.framework.Assert;
public class GCMIntentService extends GCMBaseIntentService {
private static final String TAG = GCMIntentService.class.getSimpleName();
public GCMIntentService()
{
super( BuildConfig.GCM_SENDER_ID );
Assert.assertTrue( BuildConfig.GCM_SENDER_ID.length() > 0 );
}
@Override
@ -120,20 +123,22 @@ public class GCMIntentService extends GCMBaseIntentService {
public static void init( Application app )
{
int sdkVersion = Integer.valueOf( android.os.Build.VERSION.SDK );
if ( 8 <= sdkVersion && 0 < BuildConfig.GCM_SENDER_ID.length() ) {
try {
GCMRegistrar.checkDevice( app );
// GCMRegistrar.checkManifest( app );
String regId = DevID.getGCMDevID( app );
if ( regId.equals("") ) {
GCMRegistrar.register( app, BuildConfig.GCM_SENDER_ID );
if ( 0 < BuildConfig.GCM_SENDER_ID.length() ) {
int sdkVersion = Integer.valueOf( android.os.Build.VERSION.SDK );
if ( 8 <= sdkVersion ) {
try {
GCMRegistrar.checkDevice( app );
// GCMRegistrar.checkManifest( app );
String regId = DevID.getGCMDevID( app );
if ( regId.equals("") ) {
GCMRegistrar.register( app, BuildConfig.GCM_SENDER_ID );
}
} catch ( UnsupportedOperationException uoe ) {
Log.w( TAG, "Device can't do GCM." );
} catch ( Exception whatever ) {
// funky devices could do anything
Log.ex( TAG, whatever );
}
} catch ( UnsupportedOperationException uoe ) {
Log.w( TAG, "Device can't do GCM." );
} catch ( Exception whatever ) {
// funky devices could do anything
Log.ex( TAG, whatever );
}
}
}

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

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

View file

@ -10,7 +10,7 @@ function printHead() {
<html>
<head>
<link rel="stylesheet" type="text/css" href="/xw4mobile.css" />
<title>Crosswords Invite redirect</title>
<title>CrossWords Invite redirect</title>
</head>
<body>
<div class="center">
@ -51,32 +51,31 @@ function printAndroid() {
print <<<EOF
<div>
<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>
<p>If you got this page on your device, it means either
<ul>
<li>The copy of Crosswords you have is NOT beta 56 or newer (dating from about Dec. 1, 2012).</li>
<li> OR </li>
<li> that your copy of Crosswords is new enough <em>BUT</em> that
when you clicked on the link and were asked to choose between a
browser and Crosswords you chose the browser.</li>
<li>You don't have CrossWords installed</li>
<li>OR</li>
<li>that when you clicked on the link and were asked to choose between a
browser and CrossWords you chose the browser.</li>
</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
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
invite email (or text) and tap the link again.</p>
<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
Crosswords handle it.</p>
CrossWords handle it.</p>
<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
Crosswords will be given control of all URLs that start with
will allow you to make CrossWords the default. If you do that
CrossWords will be given control of all URLs that start with
"http://eehouse.org/and/" -- not all URLs of any type.)</p>
<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
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_shelfFile = k_filebase + 'xw4/info_shelf_2'
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
g_langs = {'English' : 'en',
@ -127,7 +114,7 @@ def md5Checksums( sums, filePath ):
if filePath in sums:
result = sums[filePath]
else:
logging.debug( "opening %s" % (k_filebase + "and_wordlists/" + filePath))
# logging.debug( "opening %s" % (k_filebase + "and_wordlists/" + filePath))
try:
file = open( k_filebase + "and_wordlists/" + filePath, 'rb' )
md5 = hashlib.md5()
@ -158,7 +145,7 @@ def openShelf():
if not k_SUMS in s_shelf: s_shelf[k_SUMS] = {}
if not k_COUNT in s_shelf: s_shelf[k_COUNT] = 0
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():
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]))
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 ):
result = ''
splits = string.split( name, '.' )
@ -271,10 +286,10 @@ def dictVersion( req, name, lang, md5sum ):
closeShelf()
return json.dumps( result )
def getApp( params, name ):
def getApp( params, name = None, debug = False):
result = None
if k_NAME in params:
name = params[k_NAME]
if k_DEBUG in params: debug = params[k_DEBUG]
if k_NAME in params: name = params[k_NAME]
if name:
variantDir = getVariantDir( name )
# If we're a dev device, always push the latest
@ -303,18 +318,21 @@ def getApp( params, name ):
result = {k_URL: url}
logging.debug( result )
elif k_GVERS in params:
gvers = params[k_GVERS]
elif k_AVERS in params:
vers = params[k_AVERS]
if k_INSTALLER in params: installer = params[k_INSTALLER]
else: installer = ''
logging.debug( "name: %s; installer: %s; gvers: %s"
% (name, installer, gvers) )
if name in k_versions:
if k_GVERS in versForName and not gvers == versForName[k_GVERS]:
result = {k_URL: k_urlbase + '/' + versForName[k_URL]}
else:
logging.debug(name + " is up-to-date")
% (name, installer, vers) )
print "name: %s; installer: %s; vers: %s" % (name, installer, vers)
dir = k_filebase + k_apkDir + 'rel/'
apk = getNextAfter( dir, name, vers, debug )
if apk:
apk = apk[len(k_filebase):] # strip fs path
result = {k_URL: k_urlbase + '/' + apk}
else:
logging.debug(name + " is up-to-date")
else:
logging.debug( 'Error: bad name ' + name )
else:
@ -542,15 +560,13 @@ def getUpdates( req, params ):
result[k_DICTS] = dictsResult
# Let's not upgrade strings at the same time as we're upgrading the app
if appResult:
logging.debug( 'skipping xlation upgrade because app being updated' )
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] )
if xlateResult:
logging.debug( xlateResult )
result[k_XLATEINFO] = xlateResult;
else:
logging.debug( "NOT FOUND xlate info" )
# if appResult:
# logging.debug( 'skipping xlation upgrade because app being updated' )
# 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] )
# if xlateResult:
# logging.debug( xlateResult )
# result[k_XLATEINFO] = xlateResult;
result = json.dumps( result )
# logging.debug( result )
@ -564,7 +580,7 @@ def clearShelf():
def usage(msg=None):
if msg: print "ERROR:", msg
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 ' | --list-apks [--path <path/to/apks>] [--debug] --appID org.something'
print ' | --list-dicts'
@ -574,8 +590,9 @@ def usage(msg=None):
def main():
argc = len(sys.argv)
if 1 >= argc: usage();
if 1 >= argc: usage('too few args')
arg = sys.argv[1]
args = sys.argv[2:]
if arg == '--clear-shelf':
clearShelf()
elif arg == '--list-dicts':
@ -589,12 +606,24 @@ def main():
print arg, md5Checksums(dictSums, arg)
s_shelf[k_SUMS] = dictSums
closeShelf()
elif arg == '--test-get-app':
if not 4 == argc: usage()
params = { k_NAME: sys.argv[2],
k_GVERS: sys.argv[3],
elif arg == '--get-app':
appID = None
vers = 0
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':
if not 5 == argc: usage()
params = { k_NAME: sys.argv[2],
@ -607,7 +636,6 @@ def main():
path = ""
debug = False
appID = ''
args = sys.argv[2:]
while len(args):
arg = args.pop(0)
if arg == '--appID': appID = args.pop(0)

View file

@ -43,3 +43,8 @@ for SVG in img_src/*.svg; do
fi
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 )
# endif
/* For debugging the special case of square board */
// #define FORCE_SQUARE
void
board_figureLayout( BoardCtxt* board, const CurGameInfo* gi,
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 scoreWidth, XP_U16 fontWidth, XP_U16 fontHt,
XP_Bool squareTiles, BoardDims* dimsp )
@ -465,6 +468,14 @@ board_figureLayout( BoardCtxt* board, const CurGameInfo* gi,
XP_U16 wantHt;
XP_U16 nToScroll;
#ifdef FORCE_SQUARE
if ( bWidth > bHeight ) {
bWidth = bHeight;
} else {
bHeight = bWidth;
}
#endif
ldims.left = bLeft;
ldims.top = bTop;
ldims.width = bWidth;
@ -552,7 +563,13 @@ board_figureLayout( BoardCtxt* board, const CurGameInfo* gi,
ldims.boardHt = cellSize * nCells;
ldims.trayTop = ldims.top + scoreHt + (cellSize * (nCells-nToScroll));
ldims.height = heightUsed;
ldims.height =
#ifdef FORCE_SQUARE
ldims.width
#else
heightUsed
#endif
;
ldims.cellSize = cellSize;
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. */
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 ) {
db_store( pDb, KEY_RDEVID, devID );
} else {
XP_ASSERT( 0 == strcmp( buf, devID ) );
}
(void)g_timeout_add_seconds( maxInterval, keepalive_timer, globals );
} else {

View file

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

View file

@ -1577,7 +1577,7 @@ parsePair( const char* optarg, XP_U16* min, XP_U16* max )
} else {
int intmin, intmax;
if ( 2 == sscanf( optarg, "%d:%d", &intmin, &intmax ) ) {
if ( intmin <= intmin ) {
if ( intmin <= intmax ) {
*min = intmin;
*max = intmax;
success = true;
@ -2284,6 +2284,7 @@ main( int argc, char** argv )
break;
case CMD_PLAYERNAME:
index = mainParams.pgi.nPlayers++;
XP_ASSERT( index < MAX_NUM_PLAYERS );
++mainParams.nLocalPlayers;
mainParams.pgi.players[index].robotIQ = 0; /* means human */
mainParams.pgi.players[index].isLocal = XP_TRUE;
@ -2292,6 +2293,7 @@ main( int argc, char** argv )
break;
case CMD_REMOTEPLAYER:
index = mainParams.pgi.nPlayers++;
XP_ASSERT( index < MAX_NUM_PLAYERS );
mainParams.pgi.players[index].isLocal = XP_FALSE;
++mainParams.info.serverInfo.nRemotePlayers;
break;
@ -2302,6 +2304,7 @@ main( int argc, char** argv )
case CMD_ROBOTNAME:
++robotCount;
index = mainParams.pgi.nPlayers++;
XP_ASSERT( index < MAX_NUM_PLAYERS );
++mainParams.nLocalPlayers;
mainParams.pgi.players[index].robotIQ = 1; /* real smart by default */
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,
((0 <= nRead)? nRead : 0) );
XP_LOGF( "%s: read %zd bytes ('%s')", __func__, nRead, b64 );
g_free( b64 );
#ifdef COMMS_CHECKSUM
gchar* sum = g_compute_checksum_for_data( G_CHECKSUM_MD5, buf, nRead );
XP_LOGF( "%s: read %zd bytes ('%s')(sum=%s)", __func__, nRead, b64, sum );
g_free( sum );
#endif
g_free( b64 );
if ( 0 <= nRead ) {
const XP_U8* ptr = buf;
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
GITINFO = gitversion.txt
HASH=$(shell git describe)
HASH=$(shell git rev-parse --verify HEAD)
OBJ = $(patsubst %.cpp,obj/%.o,$(SRC))
#LDFLAGS += -pthread -g -lmcheck $(STATIC)
@ -70,6 +70,24 @@ endif
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
# required by something Ubuntu did upgrading natty to oneiric
xwrelay: $(OBJ)

View file

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

View file

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

View file

@ -337,7 +337,7 @@ CRefMgr::getMakeCookieRef( const char* connName, const char* cookie,
} /* getMakeCookieRef */
CidInfo*
CRefMgr::getMakeCookieRef( const char* const connName, bool* isDead )
CRefMgr::getMakeCookieRef( const char* const connName, HostID hid, bool* isDead )
{
CookieRef* cref = NULL;
CidInfo* cinfo = NULL;
@ -347,7 +347,7 @@ CRefMgr::getMakeCookieRef( const char* const connName, bool* isDead )
int nAlreadyHere = 0;
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,
isDead );
if ( 0 != cid ) { /* already open */
@ -375,6 +375,48 @@ CRefMgr::getMakeCookieRef( const char* const connName, bool* isDead )
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
CRefMgr::RemoveSocketRefs( const AddrInfo* addr )
{
@ -649,10 +691,14 @@ SafeCref::SafeCref( const char* connName, const char* cookie, HostID hid,
nPlayersS, gameSeed, langCode,
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 ) {
logf( XW_LOGINFO, "%s: taking a second crack", __func__ );
m_hid = HOST_ID_NONE;
logf( XW_LOGINFO, "%s: taking a second crack; (cur hid: %d)",
__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,
langCode, gameSeed, clientIndx,
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 */
SafeCref::SafeCref( const char* const connName )
SafeCref::SafeCref( const char* const connName, HostID hid )
: m_cinfo( NULL )
, m_mgr( CRefMgr::Get() )
, m_isValid( 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() ) {
assert( cinfo->GetCid() == cinfo->GetRef()->GetCid() );
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()
{
if ( m_cinfo != NULL ) {

View file

@ -128,7 +128,8 @@ class CRefMgr {
int nPlayersS, int seed, int langCode,
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( const AddrInfo* addr );
@ -179,9 +180,10 @@ class SafeCref {
const AddrInfo* addr, int clientVersion, DevID* devID,
int nPlayersH, int nPlayersS, unsigned short gameSeed,
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( const AddrInfo* addr );
SafeCref( const AddrInfo::ClientToken clientToken, HostID srcID );
/* SafeCref( CookieRef* cref ); */
~SafeCref();

View file

@ -70,20 +70,6 @@ DBMgr::DBMgr()
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 ) );
}
@ -107,7 +93,7 @@ DBMgr::AddNew( const char* cookie, const char* connName, CookieID cid,
qb.appendQueryf( "INSERT INTO " GAMES_TABLE
" (cid, room, connName, nTotal, lang, pub)"
" VALUES( $$, $$, $$, $$, $$, $$ )" )
.appendParam(cid)
.appendParam(cid)
.appendParam(cookie)
.appendParam(connName)
.appendParam(nPlayersT)
@ -136,7 +122,7 @@ DBMgr::FindGameFor( const char* connName, char* cookieBuf, int bufLen,
{
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 "
"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 ) );
found = 1 == PQntuples( result );
if ( found ) {
*cidp = atoi( PQgetvalue( result, 0, 0 ) );
snprintf( cookieBuf, bufLen, "%s", PQgetvalue( result, 0, 1 ) );
*langP = atoi( PQgetvalue( result, 0, 2 ) );
*isDead = 't' == PQgetvalue( result, 0, 4 )[0];
int col = 0;
*cidp = atoi( PQgetvalue( result, 0, col++ ) );
snprintf( cookieBuf, bufLen, "%s", PQgetvalue( result, 0, col++ ) );
*langP = atoi( PQgetvalue( result, 0, col++ ) );
*isDead = 't' == PQgetvalue( result, 0, col++ )[0];
}
PQclear( result );
@ -160,28 +147,29 @@ DBMgr::FindGameFor( const char* connName, char* cookieBuf, int bufLen,
} /* FindGameFor */
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 )
{
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'"
// " LIMIT 1"
;
StrWPF query;
query.catf( fmt, connName );
query.catf( fmt, hid, connName );
logf( XW_LOGINFO, "query: %s", query.c_str() );
PGresult* result = PQexec( getThreadConn(), query.c_str() );
assert( 1 >= PQntuples( result ) );
if ( 1 == PQntuples( result ) ) {
cid = atoi( PQgetvalue( result, 0, 0 ) );
snprintf( cookieBuf, bufLen, "%s", PQgetvalue( result, 0, 1 ) );
*langP = atoi( PQgetvalue( result, 0, 2 ) );
*nPlayersTP = atoi( PQgetvalue( result, 0, 3 ) );
*nPlayersHP = atoi( PQgetvalue( result, 0, 4 ) );
*isDead = 't' == PQgetvalue( result, 0, 5 )[0];
int col = 0;
cid = atoi( PQgetvalue( result, 0, col++ ) );
snprintf( roomBuf, roomBufLen, "%s", PQgetvalue( result, 0, col++ ) );
*langP = atoi( PQgetvalue( result, 0, col++ ) );
*nPlayersTP = atoi( PQgetvalue( result, 0, col++ ) );
*nPlayersHP = atoi( PQgetvalue( result, 0, col++ ) );
*isDead = 't' == PQgetvalue( result, 0, col++ )[0];
}
PQclear( result );
@ -189,6 +177,40 @@ DBMgr::FindGame( const char* connName, char* cookieBuf, int bufLen,
return cid;
} /* 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
DBMgr::FindPlayer( DevIDRelay relayID, AddrInfo::ClientToken token,
string& connName, HostID* hidp, unsigned short* seed )
@ -294,11 +316,13 @@ DBMgr::SeenSeed( const char* cookie, unsigned short seed,
NULL, NULL, 0 );
bool found = 1 == PQntuples( result );
if ( found ) {
*cid = atoi( PQgetvalue( result, 0, 0 ) );
*nPlayersHP = here_less_seed( PQgetvalue( result, 0, 2 ),
atoi( PQgetvalue( result, 0, 3 ) ),
seed );
snprintf( connNameBuf, bufLen, "%s", PQgetvalue( result, 0, 1 ) );
int col = 0;
*cid = atoi( PQgetvalue( result, 0, col++ ) );
snprintf( connNameBuf, bufLen, "%s", PQgetvalue( result, 0, col++ ) );
const char* seeds = PQgetvalue( result, 0, col++ );
int perDeviceSum = atoi( PQgetvalue( result, 0, col++ ) );
*nPlayersHP = here_less_seed( seeds, perDeviceSum, seed );
}
PQclear( result );
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 );
CookieID cid = 0;
if ( 1 == PQntuples( result ) ) {
cid = atoi( PQgetvalue( result, 0, 0 ) );
snprintf( connNameBuf, bufLen, "%s", PQgetvalue( result, 0, 1 ) );
*nPlayersHP = atoi( PQgetvalue( result, 0, 2 ) );
int col = 0;
cid = atoi( PQgetvalue( result, 0, col++ ) );
snprintf( connNameBuf, bufLen, "%s", PQgetvalue( result, 0, col++ ) );
*nPlayersHP = atoi( PQgetvalue( result, 0, col++ ) );
/* cid may be 0, but should use game anyway */
}
PQclear( result );
@ -699,9 +724,11 @@ DBMgr::RecordSent( const int* msgIDs, int nMsgIDs )
if ( PGRES_TUPLES_OK == PQresultStatus( result ) ) {
int ntuples = PQntuples( result );
for ( int ii = 0; ii < ntuples; ++ii ) {
RecordSent( PQgetvalue( result, ii, 0 ),
atoi( PQgetvalue( result, ii, 1 ) ),
atoi( PQgetvalue( result, ii, 2 ) ) );
int col = 0;
const char* const connName = PQgetvalue( result, ii, col++ );
HostID hid = atoi( PQgetvalue( result, ii, col++ ) );
int nBytes = atoi( PQgetvalue( result, ii, col++ ) );
RecordSent( connName, hid, nBytes );
}
}
PQclear( result );
@ -1014,43 +1041,51 @@ DBMgr::CountStoredMessages( DevIDRelay relayID )
return getCountWhere( MSGS_TABLE, test );
}
void
DBMgr::StoreMessage( DevIDRelay devID, const uint8_t* const buf,
int
DBMgr::StoreMessage( DevIDRelay destDevID, const uint8_t* const buf,
int len )
{
clearHasNoMessages( devID );
int msgID = 0;
clearHasNoMessages( destDevID );
size_t newLen;
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;
if ( m_useB64 ) {
gchar* b64 = g_base64_encode( buf, len );
query.catf( fmt, "msg64", devID, "", b64, len );
query.catf( fmt, "msg64", destDevID, "", b64, len );
g_free( b64 );
} else {
uint8_t* bytes = PQescapeByteaConn( getThreadConn(), buf,
len, &newLen );
assert( NULL != bytes );
query.catf( fmt, "msg", devID, "E", bytes, len );
query.catf( fmt, "msg", destDevID, "E", bytes, len );
PQfreemem( bytes );
}
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
DBMgr::StoreMessage( const char* const connName, int hid,
int
DBMgr::StoreMessage( const char* const connName, int destHid,
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 ) {
logf( XW_LOGERROR, "%s: warning: devid not found for connName=%s, "
"hid=%d", __func__, connName, hid );
"hid=%d", __func__, connName, destHid );
} else {
clearHasNoMessages( devID );
}
@ -1066,7 +1101,7 @@ DBMgr::StoreMessage( const char* const connName, int hid,
StrWPF query;
if ( m_useB64 ) {
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 );
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
" AND stime='epoch'"
#endif
" );", connName, hid, b64 );
" )", connName, destHid, b64 );
g_free( b64 );
} else {
uint8_t* bytes = PQescapeByteaConn( getThreadConn(), buf,
len, &newLen );
assert( NULL != bytes );
query.catf( fmt, "msg", connName, hid, devID, hid, connName,
query.catf( fmt, "msg", connName, destHid, devID, destHid, connName,
"E", bytes, len );
PQfreemem( bytes );
}
query.catf(" RETURNING id;");
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

View file

@ -75,9 +75,13 @@ class DBMgr {
bool FindRelayIDFor( const char* connName, HostID hid, unsigned short seed,
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,
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,
unsigned short seed, HostID hid,
@ -137,9 +141,10 @@ class DBMgr {
/* message storage -- different DB */
int CountStoredMessages( const char* const connName );
int CountStoredMessages( DevIDRelay relayID );
void StoreMessage( DevIDRelay relayID, const uint8_t* const buf, int len );
void StoreMessage( const char* const connName, int hid,
const uint8_t* const buf, int len );
int StoreMessage( DevIDRelay destRelayID, const uint8_t* const buf,
int len );
int StoreMessage( const char* const connName, int destHid,
const uint8_t* const buf, int len );
void GetStoredMessages( DevIDRelay relayID, vector<MsgInfo>& msgs );
void GetStoredMessages( const char* const connName, HostID hid,
vector<DBMgr::MsgInfo>& msgs );
@ -170,6 +175,7 @@ class DBMgr {
int clientVersion, const char* const model,
const char* const osVers, DevIDRelay relayID );
PGconn* getThreadConn( void );
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;")
# 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;" \
| psql xwgames
# Messages
echo "SELECT * "\
"FROM msgs WHERE connname IN (SELECT connname from games $QUERY) "\
echo "Unack'd msgs count:" $(psql -t xwgames -c "select count(*) FROM msgs where stime = 'epoch' AND 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;" \
| psql xwgames

View file

@ -550,18 +550,18 @@ assemble_packet( vector<uint8_t>& packet, uint32_t* packetIDP, XWRelayReg cmd,
}
#ifdef LOG_UDP_PACKETS
gsize size = 0;
gint state = 0;
gint save = 0;
gchar out[1024];
for ( unsigned int ii = 0; ii < iocount; ++ii ) {
size += g_base64_encode_step( (const guchar*)vec[ii].iov_base,
vec[ii].iov_len,
FALSE, &out[size], &state, &save );
}
size += g_base64_encode_close( FALSE, &out[size], &state, &save );
assert( size < sizeof(out) );
out[size] = '\0';
// gsize size = 0;
// gint state = 0;
// gint save = 0;
// gchar out[1024];
// for ( unsigned int ii = 0; ii < iocount; ++ii ) {
// size += g_base64_encode_step( (const guchar*)vec[ii].iov_base,
// vec[ii].iov_len,
// FALSE, &out[size], &state, &save );
// }
// size += g_base64_encode_close( FALSE, &out[size], &state, &save );
// assert( size < sizeof(out) );
// out[size] = '\0';
#endif
}
@ -640,8 +640,10 @@ send_via_udp_impl( int sock, const struct sockaddr* dest_addr,
#ifdef LOG_UDP_PACKETS
gchar* b64 = g_base64_encode( (uint8_t*)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,
b64, out );
g_free( out );
g_free( b64 );
#else
logf( XW_LOGINFO, "%s()=>%d", __func__, nSent );
@ -760,15 +762,19 @@ send_havemsgs( const AddrInfo* addr )
class MsgClosure {
public:
MsgClosure( DevIDRelay devid, const vector<uint8_t>* packet,
OnMsgAckProc proc, void* procClosure )
MsgClosure( DevIDRelay dest, const vector<uint8_t>* packet,
int msgID, OnMsgAckProc proc, void* procClosure )
{
m_devid = devid;
assert(m_msgID != 0);
m_destDevID = dest;
m_packet = *packet;
m_proc = proc;
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;
OnMsgAckProc m_proc;
void* m_procClosure;
@ -778,22 +784,29 @@ static void
onPostedMsgAcked( bool acked, uint32_t packetID, void* data )
{
MsgClosure* mc = (MsgClosure*)data;
if ( !acked ) {
DBMgr::Get()->StoreMessage( mc->m_devid, mc->m_packet.data(),
mc->m_packet.size() );
int msgID = mc->getMsgID();
if ( acked ) {
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 ) {
(*mc->m_proc)( acked, mc->m_devid, packetID, mc->m_procClosure );
(*mc->m_proc)( acked, mc->m_destDevID, packetID, mc->m_procClosure );
}
delete mc;
}
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 )
{
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 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 ) ) {
sent = 0 < send_packet_via_udp_impl( packet, sock, dest_addr );
if ( sent ) {
MsgClosure* mc = new MsgClosure( devid, &packet,
if ( sent && msgID != 0 ) {
MsgClosure* mc = new MsgClosure( destDevID, &packet, msgID,
proc, procClosure );
UDPAckTrack::setOnAck( onPostedMsgAcked, packetID, (void*)mc );
}
}
}
if ( !sent ) {
DBMgr::Get()->StoreMessage( devid, packet.data(), packet.size() );
}
return sent;
}
bool
post_message( DevIDRelay devid, const char* message, OnMsgAckProc proc,
post_message( DevIDRelay destDevID, const char* message, OnMsgAckProc proc,
void* procClosure )
{
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,
message, len, NULL );
return post_or_store( devid, packet, packetID, proc, procClosure );
return post_or_store( destDevID, packet, packetID, proc, procClosure );
}
void
@ -988,13 +998,13 @@ processReconnect( const uint8_t* bufp, int bufLen, const AddrInfo* addr )
} /* processReconnect */
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;
const uint8_t* end = bufp + bufLen;
HostID srcID;
if ( getNetByte( &bufp, end, &srcID ) ) {
SafeCref scr( addr );
SafeCref scr( clientToken, srcID );
success = scr.HandleAck( srcID );
}
return success;
@ -1084,7 +1094,8 @@ forwardMessage( const uint8_t* buf, int buflen, const AddrInfo* addr )
} /* forwardMessage */
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 */
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 );
break;
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;
case XWRELAY_GAME_DISCONNECT:
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 );
if ( sendFull && nwritten >= 0 && (size_t)nwritten == out.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 );
}
}
@ -1438,7 +1456,7 @@ handleProxyMsgs( int sock, const AddrInfo* addr, const uint8_t* bufp,
}
unsigned short nMsgs;
if ( getNetShort( &bufp, end, &nMsgs ) ) {
SafeCref scr( connName );
SafeCref scr( connName, hid );
while ( scr.IsValid() && nMsgs-- > 0 ) {
unsigned short len;
if ( getNetShort( &bufp, end, &len ) ) {
@ -1460,7 +1478,7 @@ handleProxyMsgs( int sock, const AddrInfo* addr, const uint8_t* bufp,
static void
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() );
}
}
@ -1528,7 +1546,7 @@ proxy_thread_proc( UdpThreadClosure* utc )
sizeof( connName ), &hid ) ) {
break;
}
SafeCref scr( connName );
SafeCref scr( connName, hid );
scr.DeviceGone( hid, seed );
}
}
@ -1748,7 +1766,7 @@ handle_udp_packet( UdpThreadClosure* utc )
clientToken = ntohl( clientToken );
if ( AddrInfo::NULL_TOKEN != clientToken ) {
AddrInfo addr( g_udpsock, clientToken, utc->saddr() );
(void)processMessage( ptr, end - ptr, &addr );
(void)processMessage( ptr, end - ptr, &addr, clientToken );
} else {
logf( XW_LOGERROR, "%s: dropping packet with token of 0",
__func__ );
@ -1766,7 +1784,7 @@ handle_udp_packet( UdpThreadClosure* utc )
logf( XW_LOGERROR, "parse failed!!!" );
break;
}
SafeCref scr( connName );
SafeCref scr( connName, hid );
if ( scr.IsValid() ) {
AddrInfo addr( g_udpsock, clientToken, utc->saddr() );
handlePutMessage( scr, hid, &addr, end - ptr, &ptr, end );
@ -1833,7 +1851,7 @@ handle_udp_packet( UdpThreadClosure* utc )
string connName;
if ( DBMgr::Get()->FindPlayer( devID.asRelayID(), clientToken,
connName, &hid, &seed ) ) {
SafeCref scr( connName.c_str() );
SafeCref scr( connName.c_str(), hid );
scr.DeviceGone( hid, seed );
}
}
@ -1980,7 +1998,7 @@ maint_str_loop( int udpsock, const char* str )
} // maint_str_loop
static uint32_t
getIPAddr( void )
getUDPIPAddr( void )
{
uint32_t result = INADDR_ANY;
char iface[16] = {0};
@ -2215,7 +2233,7 @@ main( int argc, char** argv )
struct sockaddr_in saddr;
g_udpsock = socket( AF_INET, SOCK_DGRAM, IPPROTO_UDP );
saddr.sin_family = PF_INET;
saddr.sin_addr.s_addr = getIPAddr();
saddr.sin_addr.s_addr = getUDPIPAddr();
saddr.sin_port = htons(udpport);
int err = bind( g_udpsock, (struct sockaddr*)&saddr, sizeof(saddr) );
if ( 0 == err ) {

View file

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

View file

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