Skip to content

Commit

Permalink
Removed socket binding, check for other instances with DB locks. (#269)
Browse files Browse the repository at this point in the history
Signed-off-by: Jeffrey Han <itdelatrisu@gmail.com>
  • Loading branch information
itdelatrisu committed Apr 6, 2017
1 parent 8766ab5 commit 9074d43
Show file tree
Hide file tree
Showing 5 changed files with 82 additions and 134 deletions.
64 changes: 23 additions & 41 deletions src/itdelatrisu/opsu/Opsu.java
Expand Up @@ -40,9 +40,6 @@
import java.io.IOException;
import java.io.PrintStream;
import java.lang.reflect.Field;
import java.net.InetAddress;
import java.net.ServerSocket;
import java.net.UnknownHostException;

import org.newdawn.slick.GameContainer;
import org.newdawn.slick.Input;
Expand All @@ -54,6 +51,8 @@
import org.newdawn.slick.util.FileSystemLocation;
import org.newdawn.slick.util.Log;
import org.newdawn.slick.util.ResourceLoader;
import org.sqlite.SQLiteErrorCode;
import org.sqlite.SQLiteException;

/**
* Main class.
Expand All @@ -72,9 +71,6 @@ public class Opsu extends StateBasedGame {
STATE_GAMERANKING = 6,
STATE_DOWNLOADSMENU = 7;

/** Server socket for restricting the program to a single instance. */
private static ServerSocket SERVER_SOCKET;

/**
* Constructor.
* @param name the program name
Expand Down Expand Up @@ -119,26 +115,28 @@ public void uncaughtException(Thread t, Throwable e) {
// parse configuration file
Options.parseOptions();

// only allow a single instance
// initialize databases
try {
SERVER_SOCKET = new ServerSocket(Options.getPort(), 1, InetAddress.getLocalHost());
} catch (UnknownHostException e) {
// shouldn't happen
} catch (IOException e) {
errorAndExit(
null,
String.format(
"%s could not be launched for one of these reasons:\n" +
"- An instance of %s is already running.\n" +
"- Another program is bound to port %d. " +
"You can change the port %s uses by editing the \"Port\" field in the configuration file.",
OpsuConstants.PROJECT_NAME,
OpsuConstants.PROJECT_NAME,
Options.getPort(),
OpsuConstants.PROJECT_NAME
),
false
);
DBController.init();
} catch (SQLiteException e) {
// probably locked by another instance
if (e.getErrorCode() == SQLiteErrorCode.SQLITE_BUSY.code) {
Log.error(e);
errorAndExit(
null,
String.format(
"%s could not be launched for one of these reasons:\n" +
"- An instance of %s is already running.\n" +
"- A database is locked for another reason (unlikely). ",
OpsuConstants.PROJECT_NAME,
OpsuConstants.PROJECT_NAME
),
false
);
} else
errorAndExit(e, "The databases could not be initialized.", true);
} catch (Exception e) {
errorAndExit(e, "The databases could not be initialized.", true);
}

// load natives
Expand Down Expand Up @@ -171,13 +169,6 @@ public void uncaughtException(Thread t, Throwable e) {
// set the resource paths
ResourceLoader.addResourceLocation(new FileSystemLocation(new File("./res/")));

// initialize databases
try {
DBController.init();
} catch (Exception e) {
errorAndExit(e, "The databases could not be initialized.", true);
}

// check if just updated
if (args.length >= 2)
Updater.get().setUpdateInfo(args[0], args[1]);
Expand Down Expand Up @@ -277,15 +268,6 @@ public static void close() {

// cancel all downloads
DownloadList.get().cancelAllDownloads();

// close server socket
if (SERVER_SOCKET != null) {
try {
SERVER_SOCKET.close();
} catch (IOException e) {
ErrorHandler.error("Failed to close server socket.", e, false);
}
}
}

/**
Expand Down
58 changes: 26 additions & 32 deletions src/itdelatrisu/opsu/db/BeatmapDB.java
Expand Up @@ -131,7 +131,7 @@ private BeatmapDB() {}
/**
* Initializes the database connection.
*/
public static void init() {
public static void init() throws SQLException {
// create a database connection
connection = DBController.createConnection(Options.BEATMAP_DB.getPath());
if (connection == null)
Expand All @@ -144,40 +144,32 @@ public static void init() {
createDatabase();

// prepare sql statements (used below)
try {
updateSizeStmt = connection.prepareStatement("REPLACE INTO info (key, value) VALUES ('size', ?)");
} catch (SQLException e) {
ErrorHandler.error("Failed to prepare beatmap statements.", e, true);
}
updateSizeStmt = connection.prepareStatement("REPLACE INTO info (key, value) VALUES ('size', ?)");

// retrieve the cache size
getCacheSize();

// prepare sql statements (not used here)
try {
insertStmt = connection.prepareStatement(
"INSERT INTO beatmaps VALUES (" +
"?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?," +
"?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?," +
"?, ?, ?, ?, ?, ?, ?, ?, ?" +
")"
);
selectStmt = connection.prepareStatement("SELECT * FROM beatmaps WHERE dir = ? AND file = ?");
deleteMapStmt = connection.prepareStatement("DELETE FROM beatmaps WHERE dir = ? AND file = ?");
deleteGroupStmt = connection.prepareStatement("DELETE FROM beatmaps WHERE dir = ?");
setStarsStmt = connection.prepareStatement("UPDATE beatmaps SET stars = ? WHERE dir = ? AND file = ?");
updatePlayStatsStmt = connection.prepareStatement("UPDATE beatmaps SET playCount = ?, lastPlayed = ? WHERE dir = ? AND file = ?");
setFavoriteStmt = connection.prepareStatement("UPDATE beatmaps SET favorite = ? WHERE dir = ? AND file = ?");
setLocalOffsetStmt = connection.prepareStatement("UPDATE beatmaps SET localOffset = ? WHERE dir = ? AND file = ?");
} catch (SQLException e) {
ErrorHandler.error("Failed to prepare beatmap statements.", e, true);
}
insertStmt = connection.prepareStatement(
"INSERT INTO beatmaps VALUES (" +
"?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?," +
"?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?," +
"?, ?, ?, ?, ?, ?, ?, ?, ?" +
")"
);
selectStmt = connection.prepareStatement("SELECT * FROM beatmaps WHERE dir = ? AND file = ?");
deleteMapStmt = connection.prepareStatement("DELETE FROM beatmaps WHERE dir = ? AND file = ?");
deleteGroupStmt = connection.prepareStatement("DELETE FROM beatmaps WHERE dir = ?");
setStarsStmt = connection.prepareStatement("UPDATE beatmaps SET stars = ? WHERE dir = ? AND file = ?");
updatePlayStatsStmt = connection.prepareStatement("UPDATE beatmaps SET playCount = ?, lastPlayed = ? WHERE dir = ? AND file = ?");
setFavoriteStmt = connection.prepareStatement("UPDATE beatmaps SET favorite = ? WHERE dir = ? AND file = ?");
setLocalOffsetStmt = connection.prepareStatement("UPDATE beatmaps SET localOffset = ? WHERE dir = ? AND file = ?");
}

/**
* Creates the database, if it does not exist.
*/
private static void createDatabase() {
private static void createDatabase() throws SQLException {
try (Statement stmt = connection.createStatement()) {
String sql =
"CREATE TABLE IF NOT EXISTS beatmaps (" +
Expand Down Expand Up @@ -208,16 +200,14 @@ private static void createDatabase() {
// set the version key, if empty
sql = String.format("INSERT OR IGNORE INTO info(key, value) VALUES('version', '%s')", DATABASE_VERSION);
stmt.executeUpdate(sql);
} catch (SQLException e) {
ErrorHandler.error("Could not create beatmap database.", e, true);
}
}

/**
* Applies any database updates by comparing the current version to the
* stored version. Does nothing if tables have not been created.
*/
private static void updateDatabase() {
private static void updateDatabase() throws SQLException {
try (Statement stmt = connection.createStatement()) {
int version = 0;

Expand Down Expand Up @@ -260,8 +250,6 @@ private static void updateDatabase() {
ps.executeUpdate();
ps.close();
}
} catch (SQLException e) {
ErrorHandler.error("Failed to update beatmap database.", e, true);
}
}

Expand Down Expand Up @@ -305,7 +293,7 @@ public static void clearDatabase() {
if (connection == null)
return;

// drop the table, then recreate it
// drop the table
try (Statement stmt = connection.createStatement()) {
String sql = "DROP TABLE beatmaps";
stmt.executeUpdate(sql);
Expand All @@ -314,7 +302,13 @@ public static void clearDatabase() {
} catch (SQLException e) {
ErrorHandler.error("Could not drop beatmap database.", e, true);
}
createDatabase();

// recreate it
try {
createDatabase();
} catch (SQLException e) {
ErrorHandler.error("Could not create beatmap database.", e, true);
}
}

/**
Expand Down
2 changes: 1 addition & 1 deletion src/itdelatrisu/opsu/db/DBController.java
Expand Up @@ -34,7 +34,7 @@ private DBController() {}
/**
* Initializes all databases.
*/
public static void init() {
public static void init() throws SQLException {
// load the sqlite-JDBC driver using the current class loader
try {
Class.forName("org.sqlite.JDBC");
Expand Down
72 changes: 32 additions & 40 deletions src/itdelatrisu/opsu/db/ScoreDB.java
Expand Up @@ -90,7 +90,7 @@ private ScoreDB() {}
/**
* Initializes the database connection.
*/
public static void init() {
public static void init() throws SQLException {
// create a database connection
connection = DBController.createConnection(Options.SCORE_DB.getPath());
if (connection == null)
Expand All @@ -103,45 +103,41 @@ public static void init() {
createDatabase();

// prepare sql statements
try {
insertStmt = connection.prepareStatement(
// TODO: There will be problems if multiple replays have the same
// timestamp (e.g. when imported) due to timestamp being the primary key.
"INSERT OR IGNORE INTO scores VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)"
);
selectMapStmt = connection.prepareStatement(
"SELECT * FROM scores WHERE " +
"MID = ? AND title = ? AND artist = ? AND creator = ? AND version = ?"
);
selectMapSetStmt = connection.prepareStatement(
"SELECT * FROM scores WHERE " +
"MSID = ? AND title = ? AND artist = ? AND creator = ? ORDER BY version DESC"
);
deleteSongStmt = connection.prepareStatement(
"DELETE FROM scores WHERE " +
"MID = ? AND title = ? AND artist = ? AND creator = ? AND version = ?"
);
deleteScoreStmt = connection.prepareStatement(
"DELETE FROM scores WHERE " +
"timestamp = ? AND MID = ? AND MSID = ? AND title = ? AND artist = ? AND " +
"creator = ? AND version = ? AND hit300 = ? AND hit100 = ? AND hit50 = ? AND " +
"geki = ? AND katu = ? AND miss = ? AND score = ? AND combo = ? AND perfect = ? AND mods = ? AND " +
"(replay = ? OR (replay IS NULL AND ? IS NULL)) AND " +
"(playerName = ? OR (playerName IS NULL AND ? IS NULL))"
// TODO: extra playerName checks not needed if name is guaranteed not null
);
setCurrentUserStmt = connection.prepareStatement("INSERT OR REPLACE INTO info VALUES ('user', ?)");
insertUserStmt = connection.prepareStatement("INSERT OR REPLACE INTO users VALUES (?, ?, ?, ?, ?, ?)");
deleteUserStmt = connection.prepareStatement("DELETE FROM users WHERE name = ?");
} catch (SQLException e) {
ErrorHandler.error("Failed to prepare score statements.", e, true);
}
insertStmt = connection.prepareStatement(
// TODO: There will be problems if multiple replays have the same
// timestamp (e.g. when imported) due to timestamp being the primary key.
"INSERT OR IGNORE INTO scores VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)"
);
selectMapStmt = connection.prepareStatement(
"SELECT * FROM scores WHERE " +
"MID = ? AND title = ? AND artist = ? AND creator = ? AND version = ?"
);
selectMapSetStmt = connection.prepareStatement(
"SELECT * FROM scores WHERE " +
"MSID = ? AND title = ? AND artist = ? AND creator = ? ORDER BY version DESC"
);
deleteSongStmt = connection.prepareStatement(
"DELETE FROM scores WHERE " +
"MID = ? AND title = ? AND artist = ? AND creator = ? AND version = ?"
);
deleteScoreStmt = connection.prepareStatement(
"DELETE FROM scores WHERE " +
"timestamp = ? AND MID = ? AND MSID = ? AND title = ? AND artist = ? AND " +
"creator = ? AND version = ? AND hit300 = ? AND hit100 = ? AND hit50 = ? AND " +
"geki = ? AND katu = ? AND miss = ? AND score = ? AND combo = ? AND perfect = ? AND mods = ? AND " +
"(replay = ? OR (replay IS NULL AND ? IS NULL)) AND " +
"(playerName = ? OR (playerName IS NULL AND ? IS NULL))"
// TODO: extra playerName checks not needed if name is guaranteed not null
);
setCurrentUserStmt = connection.prepareStatement("INSERT OR REPLACE INTO info VALUES ('user', ?)");
insertUserStmt = connection.prepareStatement("INSERT OR REPLACE INTO users VALUES (?, ?, ?, ?, ?, ?)");
deleteUserStmt = connection.prepareStatement("DELETE FROM users WHERE name = ?");
}

/**
* Creates the database, if it does not exist.
*/
private static void createDatabase() {
private static void createDatabase() throws SQLException {
try (Statement stmt = connection.createStatement()) {
String sql =
"CREATE TABLE IF NOT EXISTS scores (" +
Expand Down Expand Up @@ -172,16 +168,14 @@ private static void createDatabase() {
// set the version key, if empty
sql = String.format("INSERT OR IGNORE INTO info(key, value) VALUES('version', %d)", DATABASE_VERSION);
stmt.executeUpdate(sql);
} catch (SQLException e) {
ErrorHandler.error("Could not create score database.", e, true);
}
}

/**
* Applies any database updates by comparing the current version to the
* stored version. Does nothing if tables have not been created.
*/
private static void updateDatabase() {
private static void updateDatabase() throws SQLException {
try (Statement stmt = connection.createStatement()) {
int version = 0;

Expand Down Expand Up @@ -224,8 +218,6 @@ private static void updateDatabase() {
ps.executeUpdate();
ps.close();
}
} catch (SQLException e) {
ErrorHandler.error("Failed to update score database.", e, true);
}
}

Expand Down
20 changes: 0 additions & 20 deletions src/itdelatrisu/opsu/options/Options.java
Expand Up @@ -138,9 +138,6 @@ public class Options {
/** The custom FFmpeg location (or null for the default). */
private static File FFmpegPath;

/** Port binding. */
private static int port = 49250;

/** The theme song string: {@code filename,title,artist,length(ms)} */
private static String themeString = "theme.mp3,Rainbows,Kevin MacLeod,219350";

Expand Down Expand Up @@ -303,17 +300,6 @@ public void read(String s) {
}
}
},
PORT ("Port") {
@Override
public String write() { return Integer.toString(port); }

@Override
public void read(String s) {
int i = Integer.parseInt(s);
if (i > 0 && i <= 65535)
port = i;
}
},

// in-game options
SCREEN_RESOLUTION ("Resolution", "ScreenResolution", "") {
Expand Down Expand Up @@ -1114,12 +1100,6 @@ public static void setDisplayMode(Container app) {
*/
public static boolean isComboBurstEnabled() { return GameOption.SHOW_COMBO_BURSTS.getBooleanValue(); }

/**
* Returns the port number to bind to.
* @return the port
*/
public static int getPort() { return port; }

/**
* Returns the cursor scale.
* @return the scale [0.5, 2]
Expand Down

0 comments on commit 9074d43

Please sign in to comment.