Avoid opening games that have crashed.

On each open, increment a counter. And if we're able to close without a
crash intervening, decrement. Once we're trying to open with a non-0
counter we have a bad game. Open only after warning the user.
This commit is contained in:
Eric House 2020-04-18 13:36:21 -07:00
parent c39bede5c1
commit 8c50bb1895
6 changed files with 196 additions and 21 deletions

View file

@ -1106,24 +1106,27 @@ public class DBUtils {
public static byte[] loadGame( Context context, GameLock lock )
{
byte[] result = null;
long rowid = lock.getRowid();
Assert.assertTrue( ROWID_NOTFOUND != rowid );
byte[] result = getCached( rowid );
if ( null == result ) {
String[] columns = { DBHelper.SNAPSHOT };
String selection = String.format( ROW_ID_FMT, rowid );
initDB( context );
synchronized( s_dbHelper ) {
Cursor cursor = query( TABLE_NAMES.SUM, columns, selection );
if ( 1 == cursor.getCount() && cursor.moveToFirst() ) {
result = cursor.getBlob( cursor
.getColumnIndex(DBHelper.SNAPSHOT));
} else {
Log.e( TAG, "loadGame: none for rowid=%d", rowid );
if ( Quarantine.safeToOpen( rowid ) ) {
result = getCached( rowid );
if ( null == result ) {
String[] columns = { DBHelper.SNAPSHOT };
String selection = String.format( ROW_ID_FMT, rowid );
initDB( context );
synchronized( s_dbHelper ) {
Cursor cursor = query( TABLE_NAMES.SUM, columns, selection );
if ( 1 == cursor.getCount() && cursor.moveToFirst() ) {
result = cursor.getBlob( cursor
.getColumnIndex(DBHelper.SNAPSHOT));
} else {
Log.e( TAG, "loadGame: none for rowid=%d", rowid );
}
cursor.close();
}
cursor.close();
setCached( rowid, result );
}
setCached( rowid, result );
}
return result;
}

View file

@ -53,6 +53,7 @@ public class DlgDelegate {
SEND_EMAIL,
WRITE_LOG_DB,
CLEAR_LOG_DB,
CLEAR_QUARANTINE,
// BoardDelegate
UNDO_LAST_ACTION,

View file

@ -1226,6 +1226,25 @@ public class GamesListDelegate extends ListDelegateBase
} );
}
private void openWithChecks( long rowid, GameSummary summary )
{
if ( ! m_launchedGames.contains( rowid ) ) {
if ( Quarantine.safeToOpen( rowid ) ) {
makeNotAgainBuilder( R.string.not_again_newselect,
R.string.key_notagain_newselect,
Action.OPEN_GAME )
.setParams( rowid, summary )
.show();
} else {
makeConfirmThenBuilder( R.string.unsafe_open_warning,
Action.CLEAR_QUARANTINE )
.setPosButton( R.string.unsafe_open_disregard )
.setParams( rowid, summary )
.show();
}
}
}
//////////////////////////////////////////////////////////////////////
// SelectableItem interface
//////////////////////////////////////////////////////////////////////
@ -1237,13 +1256,7 @@ public class GamesListDelegate extends ListDelegateBase
// an empty room name.
if ( clicked instanceof GameListItem ) {
long rowid = ((GameListItem)clicked).getRowID();
if ( ! m_launchedGames.contains( rowid ) ) {
makeNotAgainBuilder( R.string.not_again_newselect,
R.string.key_notagain_newselect,
Action.OPEN_GAME )
.setParams( rowid, summary )
.show();
}
openWithChecks( rowid, summary );
}
}
@ -1344,6 +1357,13 @@ public class GamesListDelegate extends ListDelegateBase
case OPEN_GAME:
doOpenGame( params );
break;
case CLEAR_QUARANTINE:
long rowid = (long)params[0];
Quarantine.clear( rowid );
GameSummary summary = (GameSummary)params[0];
openWithChecks( rowid, summary );
break;
case CLEAR_SELS:
clearSelections();
break;

View file

@ -0,0 +1,143 @@
/* -*- compile-command: "find-and-gradle.sh inXw4dDeb"; -*- */
/*
* Copyright 2009-2020 by Eric House (xwords@eehouse.org). All rights
* reserved.
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License as
* published by the Free Software Foundation; either version 2 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
*/
package org.eehouse.android.xw4;
import android.content.Context;
import java.util.HashMap;
import java.util.Map;
import java.io.Serializable;
public class Quarantine {
private static final String TAG = Quarantine.class.getSimpleName();
private static final String DATA_KEY = TAG + "/key";
public static boolean safeToOpen( long rowid )
{
int count;
synchronized ( sDataRef ) {
count = get().getFor( rowid );
}
boolean result = count == 0; // Not too strict?
Log.d( TAG, "safeToOpen(%d) => %b (count=%d)", rowid, result, count );
return result;
}
public static void clear( long rowid )
{
synchronized ( sDataRef ) {
get().clear( rowid );
store();
}
}
public static void recordOpened( long rowid )
{
synchronized ( sDataRef ) {
get().increment( rowid );
store();
Log.d( TAG, "recordOpened(%d): %s", rowid, sDataRef[0].toString() );
}
}
public static void recordClosed( long rowid )
{
synchronized ( sDataRef ) {
get().decrement( rowid );
store();
Log.d( TAG, "recordClosed(%d): %s", rowid, sDataRef[0].toString() );
}
}
private static class Data implements Serializable {
private HashMap<Long, Integer> mCounts = new HashMap<>();
synchronized void increment( long rowid ) {
if ( ! mCounts.containsKey(rowid) ) {
mCounts.put(rowid, 0);
}
mCounts.put( rowid, mCounts.get(rowid) + 1 );
}
synchronized void decrement( long rowid )
{
Assert.assertTrue( mCounts.containsKey(rowid) );
mCounts.put( rowid, mCounts.get(rowid) - 1 );
Assert.assertTrueNR( mCounts.get(rowid) >= 0 );
}
synchronized int getFor( long rowid )
{
int result = mCounts.containsKey(rowid) ? mCounts.get( rowid ) : 0;
return result;
}
synchronized void clear( long rowid )
{
mCounts.put( rowid, 0 );
}
@Override
synchronized public String toString()
{
StringBuilder sb = new StringBuilder().append("[");
synchronized ( mCounts ) {
for ( long rowid : mCounts.keySet() ) {
int count = mCounts.get(rowid);
sb.append( String.format("{%d: %d}", rowid, count ) );
}
}
return sb.append("]").toString();
}
}
private static Data[] sDataRef = {null};
private static void store()
{
synchronized( sDataRef ) {
DBUtils.setSerializableFor( getContext(), DATA_KEY, sDataRef[0] );
}
}
private static Data get()
{
Data data;
synchronized ( sDataRef ) {
data = sDataRef[0];
if ( null == data ) {
data = (Data)DBUtils.getSerializableFor( getContext(), DATA_KEY );
if ( null == data ) {
data = new Data();
} else {
Log.d( TAG, "loading existing: %s", data );
}
sDataRef[0] = data;
}
}
return data;
}
private static Context getContext()
{
return XWApp.getContext();
}
}

View file

@ -28,6 +28,7 @@ import org.eehouse.android.xw4.Assert;
import org.eehouse.android.xw4.BuildConfig;
import org.eehouse.android.xw4.Log;
import org.eehouse.android.xw4.NetLaunchInfo;
import org.eehouse.android.xw4.Quarantine;
import org.eehouse.android.xw4.Utils;
import org.eehouse.android.xw4.jni.CommsAddrRec.CommsConnType;
@ -46,6 +47,7 @@ public class XwJNI {
m_ptrGame = ptr;
m_rowid = rowid;
mStack = android.util.Log.getStackTraceString(new Exception());
Quarantine.recordOpened( rowid );
}
public synchronized long ptr()
@ -73,6 +75,7 @@ public class XwJNI {
// getClass().getName(), this, m_rowid, m_refCount );
if ( 0 == m_refCount ) {
if ( 0 != m_ptrGame ) {
Quarantine.recordClosed( m_rowid );
if ( haveEnv( getJNI().m_ptrGlobals ) ) {
game_dispose( this ); // will crash if haveEnv fails
} else {

View file

@ -2520,6 +2520,11 @@
<string name="history_msg_fmt"> Message: %1$s.</string>
<string name="history_autopause">Auto-paused.</string>
<string name="unsafe_open_warning">This game has caused CrossWords
to crash recently and is likely corrupt. Do you want to open it
anyway?</string>
<string name="unsafe_open_disregard">Open anyway</string>
<string name="gamel_menu_logs">Debug logs</string>
<!-- Debug-build-only menu to turn on saving of logs -->
<string name="gamel_menu_logstore_on">Enable log storage</string>