Skip to content

Commit c928cc8

Browse files
committed
Merge pull request #9 from cniemira/issue-4
introduces a (somewhat flimsy) save-on-quit mechanism
2 parents 7f21181 + 12ec6ff commit c928cc8

File tree

1 file changed

+152
-7
lines changed

1 file changed

+152
-7
lines changed

src/net/rptools/maptool/server/StandaloneServer.java

+152-7
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,13 @@
66
import java.util.Collection;
77
import java.util.Set;
88

9+
import net.rptools.lib.FileUtil;
910
import net.rptools.lib.MD5Key;
1011
import net.rptools.lib.ModelVersionManager;
1112
import net.rptools.lib.io.PackedFile;
1213
import net.rptools.maptool.client.AppUtil;
1314
import net.rptools.maptool.client.AssetTransferHandler;
15+
import net.rptools.maptool.client.MapTool;
1416
import net.rptools.maptool.client.MapToolRegistry;
1517
import net.rptools.maptool.client.ServerCommandClientImpl;
1618
import net.rptools.maptool.model.Asset;
@@ -22,14 +24,14 @@
2224
import net.rptools.maptool.transfer.AssetTransferManager;
2325
import net.rptools.maptool.util.PersistenceUtil.PersistedCampaign;
2426
import net.rptools.maptool.util.StringUtil;
27+
2528
import org.apache.commons.cli.CommandLineParser;
2629
import org.apache.commons.cli.BasicParser;
2730
import org.apache.commons.cli.CommandLine;
2831
import org.apache.commons.cli.HelpFormatter;
2932
import org.apache.commons.cli.Option;
3033
import org.apache.commons.cli.Options;
3134
import org.apache.commons.cli.ParseException;
32-
3335
import org.apache.commons.io.IOUtils;
3436
import org.apache.log4j.Level;
3537
import org.apache.log4j.Logger;
@@ -42,6 +44,137 @@ public class StandaloneServer {
4244

4345
private static MapToolServer server;
4446
private static ServerCommand serverCommand;
47+
private static ShutDownHandler shutDownHandler;
48+
49+
private static class ShutDownHandler extends Thread {
50+
private static final String PROP_VERSION = "version"; //$NON-NLS-1$
51+
private static final String PROP_CAMPAIGN_VERSION = "campaignVersion"; //$NON-NLS-1$
52+
private static final String ASSET_DIR = "assets/"; //$NON-NLS-1$
53+
private static final String CAMPAIGN_VERSION = "1.3.85";
54+
55+
private Campaign campaign;
56+
private File file;
57+
58+
public void run() {
59+
try {
60+
if ((this.campaign != null) && (this.file != null)) {
61+
log.info("Saving campaign");
62+
//This would have been too easy, wouldn't it?
63+
//PersistenceUtil.saveCampaign(this.campaign, this.file);
64+
this.saveCampaign();
65+
}
66+
} catch (IOException e) {
67+
log.error("Failed to save campaign!");
68+
}
69+
}
70+
71+
private void saveCampaign() throws IOException {
72+
File tmpDir = AppUtil.getTmpDir();
73+
File tmpFile = new File(tmpDir.getAbsolutePath(), this.file.getName());
74+
if (tmpFile.exists()) {
75+
tmpFile.delete();
76+
}
77+
78+
PackedFile pakFile = null;
79+
try {
80+
pakFile = new PackedFile(tmpFile);
81+
// Configure the meta file (this is for legacy support)
82+
PersistedCampaign persistedCampaign = new PersistedCampaign();
83+
persistedCampaign.campaign = this.campaign;
84+
85+
Set<MD5Key> allAssetIds = this.campaign.getAllAssetIds();
86+
87+
// Special handling of assets: XML file to describe the Asset, but binary file for the image data
88+
pakFile.getXStream().processAnnotations(Asset.class);
89+
90+
// Store the assets
91+
for (MD5Key assetId : allAssetIds) {
92+
if (assetId == null)
93+
continue;
94+
95+
/*
96+
* This is a PITA. The way the Asset class determines the image extension when creating an 'Asset'
97+
* object is by opening up the file and passing it through a stream reader. Unfortunately that
98+
* tries to register a shutdown hook which you can't do when processing a shutdown hook. The workaround
99+
* is disabling the persistent cache at save time. This should cause us to pull assets directly out
100+
* of the assetMap (memory). The down side is that we've created a race condition. If we exit before
101+
* all of the assets are loaded into memory, they're not going to be available and we'll "corrupt" the
102+
* campaign file.
103+
*
104+
* Alternatively, we can wedge a bogus remote repo into the campaign properties object via
105+
* Campaign.getCampaignProperties and Campaign.mergeCampaignProperties
106+
* and then try to use AssetManager.findAllAssetsNotInRepositories
107+
* I'm not sure this would work, though.
108+
*/
109+
AssetManager.setUsePersistentCache(false);
110+
111+
Asset asset = AssetManager.getAsset(assetId);
112+
if (asset == null) {
113+
log.error("Asset " + assetId + " not found while saving");
114+
continue;
115+
}
116+
persistedCampaign.assetMap.put(assetId, null);
117+
pakFile.putFile(ASSET_DIR + assetId + "." + asset.getImageExtension(), asset.getImage());
118+
pakFile.putFile(ASSET_DIR + assetId, asset); // Does not write the image
119+
log.debug("Asset " + assetId + " saved correctly.");
120+
}
121+
122+
// Write the actual pakfile out
123+
try {
124+
pakFile.setContent(persistedCampaign);
125+
pakFile.setProperty(PROP_VERSION, MapTool.getVersion());
126+
pakFile.setProperty(PROP_CAMPAIGN_VERSION, CAMPAIGN_VERSION);
127+
128+
pakFile.save();
129+
} catch (OutOfMemoryError oom) {
130+
/*
131+
* This error is normally because the heap space has been
132+
* exceeded while trying to save the campaign. It's not recoverable the way we're handling
133+
* the standalone server right now.
134+
*/
135+
pakFile.close(); // Have to close the tmpFile first on some OSes
136+
pakFile = null;
137+
tmpFile.delete(); // Delete the temporary file
138+
log.error("Failed to save campaign due to OOM");
139+
return;
140+
}
141+
} finally {
142+
try {
143+
if (pakFile != null)
144+
pakFile.close();
145+
} catch (Exception e) {
146+
log.error("Failed to write cmpgn file");
147+
}
148+
pakFile = null;
149+
}
150+
151+
// Copy to the new location
152+
// Not the fastest solution in the world if renameTo() fails, but worth the safety net it provides
153+
File bakFile = new File(tmpDir.getAbsolutePath(), this.file.getName() + ".bak");
154+
bakFile.delete();
155+
if (this.file.exists()) {
156+
if (!this.file.renameTo(bakFile)) {
157+
FileUtil.copyFile(this.file, bakFile);
158+
this.file.delete();
159+
}
160+
}
161+
if (!tmpFile.renameTo(this.file)) {
162+
FileUtil.copyFile(tmpFile, this.file);
163+
tmpFile.delete();
164+
}
165+
if (bakFile.exists()) {
166+
bakFile.delete();
167+
}
168+
}
169+
170+
public void setCampaign(Campaign campaign) {
171+
this.campaign = campaign;
172+
}
173+
174+
public void setFile(File file) {
175+
this.file = file;
176+
}
177+
}
45178

46179
static {
47180
PackedFile.init(AppUtil.getAppHome("tmp"));
@@ -52,7 +185,7 @@ public class StandaloneServer {
52185
private static void loadAssets(Collection<MD5Key> assetIds, PackedFile pakFile) throws IOException {
53186
pakFile.getXStream().processAnnotations(Asset.class);
54187
String campVersion = (String)pakFile.getProperty("campaignVersion");
55-
188+
56189
for (MD5Key key : assetIds) {
57190
if (key == null) {
58191
continue;
@@ -67,6 +200,7 @@ private static void loadAssets(Collection<MD5Key> assetIds, PackedFile pakFile)
67200
asset = (Asset) pakFile.getFileObject(pathname);
68201
} catch (Exception e) {
69202
log.info("Exception while handling asset '" + pathname + "'", e);
203+
continue;
70204
}
71205

72206
if (asset == null) {
@@ -92,8 +226,7 @@ private static void loadAssets(Collection<MD5Key> assetIds, PackedFile pakFile)
92226
}
93227
}
94228

95-
public static void loadCampaignFile(String filename) throws IOException {
96-
File campaignFile = new File(filename);
229+
public static Campaign loadCampaignFile(File campaignFile) throws IOException {
97230
PackedFile pakfile = new PackedFile(campaignFile);
98231
pakfile.setModelVersionManager(campaignVersionManager);
99232

@@ -107,6 +240,8 @@ public static void loadCampaignFile(String filename) throws IOException {
107240
Set<MD5Key> allAssetIds = persistedCampaign.assetMap.keySet();
108241
loadAssets(allAssetIds, pakfile);
109242
}
243+
244+
return persistedCampaign.campaign;
110245
}
111246

112247
private static void startServer(String id, ServerConfig config, ServerPolicy policy, Campaign campaign) throws IOException {
@@ -164,6 +299,7 @@ public static void main(String [] args) {
164299
options.addOption("t", "useToolTipsForDefaultRollFormat", false, "");
165300
options.addOption("r", "restrictedImpersonation", false, "Restrict the impersonation of a token to only one user");
166301
options.addOption("c", "campaign", true, "The campaign file to load");
302+
options.addOption("o", "saveOnExit", false, "Save the campaign file when exiting");
167303
options.addOption("d", "logDebug", false, "Show debug information");
168304
CommandLineParser parser = new BasicParser();
169305
CommandLine cmd = null;
@@ -227,14 +363,23 @@ public static void main(String [] args) {
227363
}
228364

229365
log.info("Started on port " + port);
230-
366+
367+
shutDownHandler = new ShutDownHandler();
368+
Runtime.getRuntime().addShutdownHook(shutDownHandler);
369+
231370
if (cmd.hasOption("c")) {
371+
File campaignFile = new File(cmd.getOptionValue("c"));
372+
if (cmd.hasOption("o")) {
373+
shutDownHandler.setFile(campaignFile);
374+
log.warn("Save support is experimental!");
375+
}
232376
try {
233-
loadCampaignFile(cmd.getOptionValue("c"));
234-
log.info("Loaded campaign " + cmd.getOptionValue("c"));
377+
campaign = loadCampaignFile(campaignFile);
378+
log.info("Loaded campaign " + campaignFile.getAbsolutePath());
235379
} catch (IOException e) {
236380
log.error("Unable to load campaign", e);
237381
}
238382
}
383+
shutDownHandler.setCampaign(campaign);
239384
}
240385
}

0 commit comments

Comments
 (0)