6
6
import java .util .Collection ;
7
7
import java .util .Set ;
8
8
9
+ import net .rptools .lib .FileUtil ;
9
10
import net .rptools .lib .MD5Key ;
10
11
import net .rptools .lib .ModelVersionManager ;
11
12
import net .rptools .lib .io .PackedFile ;
12
13
import net .rptools .maptool .client .AppUtil ;
13
14
import net .rptools .maptool .client .AssetTransferHandler ;
15
+ import net .rptools .maptool .client .MapTool ;
14
16
import net .rptools .maptool .client .MapToolRegistry ;
15
17
import net .rptools .maptool .client .ServerCommandClientImpl ;
16
18
import net .rptools .maptool .model .Asset ;
22
24
import net .rptools .maptool .transfer .AssetTransferManager ;
23
25
import net .rptools .maptool .util .PersistenceUtil .PersistedCampaign ;
24
26
import net .rptools .maptool .util .StringUtil ;
27
+
25
28
import org .apache .commons .cli .CommandLineParser ;
26
29
import org .apache .commons .cli .BasicParser ;
27
30
import org .apache .commons .cli .CommandLine ;
28
31
import org .apache .commons .cli .HelpFormatter ;
29
32
import org .apache .commons .cli .Option ;
30
33
import org .apache .commons .cli .Options ;
31
34
import org .apache .commons .cli .ParseException ;
32
-
33
35
import org .apache .commons .io .IOUtils ;
34
36
import org .apache .log4j .Level ;
35
37
import org .apache .log4j .Logger ;
@@ -42,6 +44,137 @@ public class StandaloneServer {
42
44
43
45
private static MapToolServer server ;
44
46
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
+ }
45
178
46
179
static {
47
180
PackedFile .init (AppUtil .getAppHome ("tmp" ));
@@ -52,7 +185,7 @@ public class StandaloneServer {
52
185
private static void loadAssets (Collection <MD5Key > assetIds , PackedFile pakFile ) throws IOException {
53
186
pakFile .getXStream ().processAnnotations (Asset .class );
54
187
String campVersion = (String )pakFile .getProperty ("campaignVersion" );
55
-
188
+
56
189
for (MD5Key key : assetIds ) {
57
190
if (key == null ) {
58
191
continue ;
@@ -67,6 +200,7 @@ private static void loadAssets(Collection<MD5Key> assetIds, PackedFile pakFile)
67
200
asset = (Asset ) pakFile .getFileObject (pathname );
68
201
} catch (Exception e ) {
69
202
log .info ("Exception while handling asset '" + pathname + "'" , e );
203
+ continue ;
70
204
}
71
205
72
206
if (asset == null ) {
@@ -92,8 +226,7 @@ private static void loadAssets(Collection<MD5Key> assetIds, PackedFile pakFile)
92
226
}
93
227
}
94
228
95
- public static void loadCampaignFile (String filename ) throws IOException {
96
- File campaignFile = new File (filename );
229
+ public static Campaign loadCampaignFile (File campaignFile ) throws IOException {
97
230
PackedFile pakfile = new PackedFile (campaignFile );
98
231
pakfile .setModelVersionManager (campaignVersionManager );
99
232
@@ -107,6 +240,8 @@ public static void loadCampaignFile(String filename) throws IOException {
107
240
Set <MD5Key > allAssetIds = persistedCampaign .assetMap .keySet ();
108
241
loadAssets (allAssetIds , pakfile );
109
242
}
243
+
244
+ return persistedCampaign .campaign ;
110
245
}
111
246
112
247
private static void startServer (String id , ServerConfig config , ServerPolicy policy , Campaign campaign ) throws IOException {
@@ -164,6 +299,7 @@ public static void main(String [] args) {
164
299
options .addOption ("t" , "useToolTipsForDefaultRollFormat" , false , "" );
165
300
options .addOption ("r" , "restrictedImpersonation" , false , "Restrict the impersonation of a token to only one user" );
166
301
options .addOption ("c" , "campaign" , true , "The campaign file to load" );
302
+ options .addOption ("o" , "saveOnExit" , false , "Save the campaign file when exiting" );
167
303
options .addOption ("d" , "logDebug" , false , "Show debug information" );
168
304
CommandLineParser parser = new BasicParser ();
169
305
CommandLine cmd = null ;
@@ -227,14 +363,23 @@ public static void main(String [] args) {
227
363
}
228
364
229
365
log .info ("Started on port " + port );
230
-
366
+
367
+ shutDownHandler = new ShutDownHandler ();
368
+ Runtime .getRuntime ().addShutdownHook (shutDownHandler );
369
+
231
370
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
+ }
232
376
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 ( ));
235
379
} catch (IOException e ) {
236
380
log .error ("Unable to load campaign" , e );
237
381
}
238
382
}
383
+ shutDownHandler .setCampaign (campaign );
239
384
}
240
385
}
0 commit comments