1/*
2 * JavaAppLauncher: a simple Java application launcher for Mac OS X.
3 * JavaParamBlock.mm
4 *
5 * Paul J. Lucas [paul@lightcrafts.com]
6 *
7 * This code is heavily based on:
8 * http://developer.apple.com/samplecode/JavaSplashScreen/JavaSplashScreen.html
9 */
10
11#import <stdlib.h>                      /* for getenv(3) */
12#import <string.h>
13#import <unistd.h>                      /* for chdir(2) */
14#import <Cocoa/Cocoa.h>
15#import <mach-o/arch.h>                 /* for NXGetLocalArchInfo(3) */
16#import <sys/sysctl.h>                  /* for sysctl(3) */
17
18#import "JavaParamBlock.h"
19#import "LC_CocoaUtils.h"
20#import "SetJVMVersion.h"
21#import "UI.h"
22
23using namespace std;
24using namespace LightCrafts;
25
26/**
27 * If there is no previously set user preference for the maximum amount of
28 * memory to use, then this is the percentage of the physical memory that
29 * should be used.
30 */
31float const DefaultMaxMemoryPercentage = 0.3;
32
33/**
34 * Insist on at least this much RAM.
35 */
36int const   JavaMinMemoryInMB = 512;
37
38/**
39 * Limit the JVM heap size because we now use non-heap memory for things like
40 * tile caches.
41 */
42#ifdef __LP64__
43int const   JavaMaxMemoryInMB = 32768;
44#else
45int const   JavaMaxMemoryInMB = 2048;
46#endif
47
48extern cpu_type_t lc_cpuType;
49
50/**
51 * Copy a C string to a newly allocated C buffer.
52 */
53static void new_strcpy( char **dest, char const *src ) {
54    if ( src ) {
55        *dest = new char[ ::strlen( src ) + 1 ];
56        if ( !*dest )
57            LC_die( @"Unexpected", @"Could not allocate memory" );
58        ::strcpy( *dest, src );
59    } else {
60        *dest = new char[1];
61        if ( !*dest )
62            LC_die( @"Unexpected", @"Could not allocate memory" );
63        **dest = '\0';
64    }
65}
66
67/**
68 * Convert/copy an NSString to a newly allocated C string.
69 */
70static void new_strcpy( char **dest, NSString const *src ) {
71    if ( src )
72        new_strcpy( dest, [src UTF8String] );
73    else
74        new_strcpy( dest, static_cast<char const*>( 0 ) );
75}
76
77/**
78 * Allocate and copy a JVM option string.
79 */
80inline void newJVMOption( JavaVMOption *jvm_opt, char const *s ) {
81    new_strcpy( &jvm_opt->optionString, s );
82    jvm_opt->extraInfo = 0;
83}
84
85/**
86 * Allocate and copy a JVM option string.
87 */
88static void newJVMOption( JavaVMOption *jvm_opt, NSString const *s ) {
89    if ( s )
90        newJVMOption( jvm_opt, [s UTF8String] );
91    else
92        newJVMOption( jvm_opt, static_cast<char const*>( 0 ) );
93}
94
95/**
96 * Resolve all references to $APP_PACKAGE, $JAVAROOT, and $LC_JVM_VERSION.
97 */
98static NSString* resolveString( NSString *in, NSDictionary const *javaDict ) {
99    if ( in == nil )
100        return in;
101
102    // Make a mutable copy of the string to work on.
103    NSMutableString *const temp = [NSMutableString string];
104    [temp appendString:in];
105
106    //
107    // First do $APP_PACKAGE.
108    //
109    NSString *const appPackage = [[NSBundle mainBundle] bundlePath];
110    if ( appPackage )
111        [temp replaceOccurrencesOfString:@"$APP_PACKAGE" withString:appPackage
112            options:NSLiteralSearch range:NSMakeRange( 0, [temp length] )];
113
114    //
115    // Next do $JAVAROOT.
116    //
117    NSMutableString *const javaRoot = [NSMutableString string];
118    NSString *const javaRootProp = [javaDict objectForKey:@"$JAVAROOT"];
119    if ( javaRootProp )
120        [javaRoot appendString:javaRootProp];
121    else
122        [javaRoot appendString:@"Contents/Resources/Java"];
123    [temp replaceOccurrencesOfString:@"$JAVAROOT" withString:javaRoot
124        options:NSLiteralSearch range:NSMakeRange( 0, [temp length] )];
125
126    //
127    // Finally, $LC_JVM_VERSION.
128    //
129    NSString *const jvmVersion =
130        [NSString stringWithUTF8String: ::getenv( "JAVA_JVM_VERSION" )];
131    [temp replaceOccurrencesOfString:@"$LC_JVM_VERSION" withString:jvmVersion
132        options:NSLiteralSearch range:NSMakeRange( 0, [temp length] )];
133
134    return temp;
135}
136
137/**
138 * Set the given JVM option string.
139 */
140inline void setJVMOption( JavaVMOption *jvm_opt, char const *s ) {
141    jvm_opt->optionString = const_cast<char*>( s );
142    jvm_opt->extraInfo = 0;
143}
144
145/**
146 * Set the given JVM option string.
147 */
148static void setJVMOption( JavaVMOption *jvm_opt, NSString const *s ) {
149    if ( s )
150        setJVMOption( jvm_opt, [s UTF8String] );
151    else
152        setJVMOption( jvm_opt, static_cast<char const*>( 0 ) );
153}
154
155/**
156 * Gets the class path from the "ClassPath" key of the Java dictionary in the
157 * application's Info.plist file.
158 */
159static NSString* getClassPath( NSDictionary const *javaDict ) {
160    id classPathProp = [javaDict objectForKey:@"ClassPath"];
161    if ( classPathProp == nil )
162        return nil;
163
164    //
165    // The class path must be passed to the JVM using the java.class.path
166    // property.  The JVM doesn't accept either the -cp or -classpath options.
167    //
168    NSMutableString *const javaClassPath = [NSMutableString string];
169    [javaClassPath appendString:@"-Djava.class.path="];
170
171    if ( [classPathProp isKindOfClass:[NSString class]] ) {
172        //
173        // It's just a single string.
174        //
175        [javaClassPath appendString:classPathProp];
176    } else if ( [classPathProp isKindOfClass:[NSArray class]] ) {
177        //
178        // It's an array of strings.
179        //
180        int const n = [classPathProp count];
181        for ( int i = 0; i < n; ++i ) {
182            if ( i > 0 )
183                [javaClassPath appendString:@":"];
184            [javaClassPath appendString:[classPathProp objectAtIndex:i]];
185        }
186    } else
187        LC_die( @"Corrupted", @"Bad ClassPath" );
188
189    return resolveString( javaClassPath, javaDict );
190}
191
192/**
193 * Gets the working directory from the "WorkingDirectory" key, if any, of the
194 * Java dictionary in the application's Info.plist file.
195 *
196 * Also sets the current working directory to "WorkingDirectory" if specified
197 * or the application bundle's path if not.
198 */
199static NSString* getCWD( NSDictionary const *javaDict ) {
200    //
201    // First check to see if the key WorkingDirectory is defined in the Java
202    // dictionary.
203    //
204    NSString *cwd = [javaDict objectForKey:@"WorkingDirectory"];
205
206    if ( cwd )
207        cwd = resolveString( cwd, javaDict );
208    else // Default to the path to the application's bundle.
209        cwd = [[NSBundle mainBundle] bundlePath];
210
211    if ( ::chdir( [cwd fileSystemRepresentation] ) != 0 )
212        LC_die( @"Unexpected", @"Set CWD failed" );
213
214    return cwd;
215}
216
217/**
218 * Add the VM options for the given key.
219 */
220static void addVMOptions( NSMutableArray *options,
221                          NSDictionary const *javaDict,
222                          NSString const *vmOptionsKey ) {
223    if ( id const vmArgs = [javaDict objectForKey:vmOptionsKey] )
224        if ( [vmArgs isKindOfClass:[NSString class]] )
225            [options addObject:vmArgs];
226        else if ( [vmArgs isKindOfClass:[NSArray class]] )
227            [options addObjectsFromArray:vmArgs];
228        else
229            LC_die( @"Corrupted", @"Bad VMOptions" );
230}
231
232/**
233 * Allocate and initialize an array of JavaVMOption data structures from the
234 * Java dictionary in the application's Info.plist file.
235 */
236static int getJVMOptions( JavaVMOption **jvm_options,
237                          NSDictionary const *javaDict ) {
238    NSMutableArray *const options = [NSMutableArray arrayWithCapacity:1];
239
240    //
241    // Process the VMOptions.
242    //
243    addVMOptions( options, javaDict, @"VMOptions" );
244    switch ( lc_cpuType ) {
245        case CPU_TYPE_I386:
246            addVMOptions( options, javaDict, @"LC_VMOptionsX86" );
247            break;
248        case CPU_TYPE_POWERPC:
249            addVMOptions( options, javaDict, @"LC_VMOptionsPPC" );
250            break;
251    }
252
253    //
254    // Add the java.class.path property.
255    //
256    NSString *const classPath = getClassPath( javaDict );
257    if ( classPath )
258        [options addObject:classPath];
259
260    //
261    // Set the working directory (pwd).
262    //
263    NSMutableString *const userDir = [NSMutableString string];
264    [userDir appendString:@"-Duser.dir="];
265    [userDir appendString:getCWD( javaDict )];
266    [userDir appendString:@"/"];
267    [options addObject:userDir];
268
269    //
270    // Add the properties defined in Properties dictionary.
271    //
272    NSDictionary const *const propDict = [javaDict objectForKey:@"Properties"];
273    if ( propDict ) {
274        NSArray const *const keys = [propDict allKeys];
275        int const n = [keys count];
276        for ( int i = 0; i < n; ++i ) {
277            NSString *const key = [keys objectAtIndex:i];
278            NSMutableString *const prop = [NSMutableString string];
279            [prop appendString:@"-D"];
280            [prop appendString:key];
281            [prop appendString:@"="];
282            [prop appendString:
283                resolveString( [propDict objectForKey:key], javaDict )];
284            [options addObject:prop];
285        }
286    }
287
288    //
289    // Convert the NSMutableArray into an array of JavaVMOptions.
290    //
291    int const n = [options count];
292    if ( n > 0 ) {
293        *jvm_options = new JavaVMOption[ n ];
294        for ( int i = 0; i < n; ++i )
295            newJVMOption( &(*jvm_options)[i], [options objectAtIndex:i] );
296    } else
297        *jvm_options = 0;
298
299    return n;
300}
301
302/**
303 * Gets the desired Java version from the "JVMVersion" key of the Java
304 * dictionary in the application's Info.plist file.
305 */
306static void getJVMVersion( char **dest, NSDictionary const *javaDict ) {
307    NSString const *const jvmVersion = [javaDict objectForKey:@"JVMVersion"];
308    if ( !jvmVersion )
309        LC_die( @"Corrupted", @"Missing JVMVersion" );
310    new_strcpy( dest, jvmVersion );
311}
312
313/**
314 * Get the arguments to main() from the "Arguments" key of the Java dictionary
315 * in the application's Info.plist file.
316 */
317static int getMainArgs( char ***main_argv, NSDictionary const *javaDict ) {
318    int n;
319    id const args = [javaDict objectForKey:@"Arguments"];
320    if ( args ) {
321        if ( [args isKindOfClass:[NSString class]] ) {
322            //
323            // The "Arguments" key has only a single string value.
324            //
325            n = 1;
326            *main_argv = new char*[1];
327            if ( !*main_argv )
328                LC_die( @"Unexpected", @"Could not allocate main() array" );
329            new_strcpy( &(*main_argv)[0], args );
330        } else if ( [args isKindOfClass:[NSArray class]] ) {
331            //
332            // The "Arguments" key is an array of strings.
333            //
334            n = [args count];
335            *main_argv = new char*[n];
336            if ( !*main_argv )
337                LC_die( @"Unexpected", @"Could not allocate main() array" );
338            for ( int i = 0; i < n; ++i ) {
339                id const arg = [args objectAtIndex:i];
340                if ( ![arg isKindOfClass:[NSString class]] )
341                    LC_die( @"Corrupted", @"Bad argument array" );
342                new_strcpy( &(*main_argv)[i], arg );
343            }
344        } else
345            LC_die( @"Corrupted", @"Bad Arguments" );
346    } else {
347        *main_argv = 0;
348        n = 0;
349    }
350    return n;
351}
352
353/**
354 * Get the name of the class whose main() is to be executed from the Java
355 * dictionary in the application's Info.plist file.
356 */
357static void getMainClassName( char **dest, NSDictionary const *javaDict ) {
358    NSString const *const mainClassName = [javaDict objectForKey:@"MainClass"];
359    if ( !mainClassName )
360        LC_die( @"Corrupted", @"Missing MainClass" );
361    new_strcpy( dest, mainClassName );
362    //
363    // Convert a class name of the form com.foo.bar to com/foo/bar because the
364    // latter is how FindClass() wants it.  Curiously, this step isn't in
365    // Apple's sample code.  Hmmm....
366    //
367    for ( char *c = *dest; *c; ++c )
368        if ( *c == '.' )
369            *c = '/';
370}
371
372/**
373 * Read the maxmemory preference from Java.
374 */
375static int getMaxMemoryFromPreference() {
376    //
377    // TODO: this should be replaced with the proper Cocoa API for accessing
378    // user preferences since the path ~/Library/Preferences is probably in the
379    // user's native language.
380    //
381    char const *const home = ::getenv( "HOME" );
382    if ( !home )
383        return 0;
384    NSMutableString *const prefFile =
385        [NSMutableString stringWithUTF8String:home];
386    [prefFile appendString:@"/Library/Preferences/com.lightcrafts.app.plist"];
387    NSDictionary const *const plistDict =
388        [NSDictionary dictionaryWithContentsOfFile:prefFile];
389    if ( !plistDict )
390        return 0;
391    NSDictionary const *const appDict =
392        [plistDict objectForKey:@"/com/lightcrafts/app/"];
393    if ( !appDict )
394        return 0;
395    NSString const *const maxMemory = [appDict objectForKey:@"MaxMemory"];
396    if ( !maxMemory )
397        return 0;
398    return (int)::strtol( [maxMemory UTF8String], NULL, 10 );
399}
400
401/**
402 * Get the value of the MaxMemory user preference in megabytes; if none,
403 * default to some percentage of physical memory (but do not exceed what the
404 * JVM can handle).
405 */
406static int getMaxMemory() {
407    int memInMB = getMaxMemoryFromPreference();
408    if ( !memInMB ) {
409        int sysParam[] = { CTL_HW, HW_MEMSIZE };
410        //
411        // Be defensive and allow for the possibility that sysctl(3) might
412        // return either a 32- or 64-bit result by using a union and checking
413        // the size of the result and using the correct union member.
414        //
415        // See:
416        // http://www.cocoabuilder.com/archive/message/cocoa/2004/5/6/106388
417        //
418        union {
419            uint32_t ui32;
420            uint64_t ui64;
421        } result;
422        size_t resultSize = sizeof( result );
423
424        ::sysctl( sysParam, 2, &result, &resultSize, NULL, 0 );
425        memInMB = (int)(
426            ( resultSize == sizeof( result.ui32 ) ?
427                (result.ui32 / 1048576) : (result.ui64 / 1048576)
428            ) * DefaultMaxMemoryPercentage
429        );
430    }
431
432    if ( memInMB < JavaMinMemoryInMB )
433        return JavaMinMemoryInMB;
434    if ( memInMB > JavaMaxMemoryInMB )
435        return JavaMaxMemoryInMB;
436    return memInMB;
437}
438
439/**
440 * Replace any -Xmx option that may have been specified in the Info.plist file
441 * with one generated from the user preference.
442 */
443static void replaceXmxOption( JavaVMInitArgs *jvm_args ) {
444    NSString const *const jvmXmxOption =
445        [NSString stringWithFormat:@"-Xmx%dm", getMaxMemory()];
446    if ( jvm_args->nOptions ) {
447        //
448        // There is at lease one JVM option: loop through them all looking for
449        // an -Xmx option.  If found, replace it.
450        //
451        for ( int i = 0; i < jvm_args->nOptions; ++i ) {
452            char **const optionString = &jvm_args->options[i].optionString;
453            if ( ::strncmp( *optionString, "-Xmx", 4 ) == 0 ) {
454                delete[] *optionString;
455                new_strcpy( optionString, jvmXmxOption );
456                return;
457            }
458        }
459        //
460        // No existing -Xmx option was found: we need to append one.
461        //
462        int const new_nOptions = jvm_args->nOptions + 1;
463        JavaVMOption *const new_options = new JavaVMOption[ new_nOptions ];
464        int i;
465        for ( i = 0; i < jvm_args->nOptions; ++i )
466            setJVMOption( &new_options[i], jvm_args->options[i].optionString );
467        newJVMOption( &new_options[i], jvmXmxOption );
468        delete[] jvm_args->options;
469        jvm_args->options = new_options;
470        jvm_args->nOptions = new_nOptions;
471    } else {
472        //
473        // There are no JVM options: create one for the -Xmx option.
474        //
475        jvm_args->nOptions = 1;
476        jvm_args->options = new JavaVMOption[1];
477        newJVMOption( &jvm_args->options[0], jvmXmxOption );
478    }
479}
480
481/**
482 * Initialize the given JavaParamBlock from the application's Info.plist file.
483 */
484void initJavaParamBlock( JavaParamBlock *jpb ) {
485    auto_obj<NSAutoreleasePool> pool;
486
487    NSDictionary const *const javaDict =
488        [[[NSBundle mainBundle] infoDictionary] objectForKey:@"Java"];
489    if ( !javaDict )
490        LC_die( @"Corrupted", @"Missing Java dictionary" );
491
492    //
493    // Set-up the desired JVM version.
494    //
495    getJVMVersion( &jpb->jvm_version, javaDict );
496    setJVMVersion( jpb->jvm_version );
497
498    //
499    // Set-up the JVM initialization options.
500    //
501    jpb->jvm_args.version = JNI_VERSION_1_4;
502    jpb->jvm_args.nOptions = getJVMOptions( &jpb->jvm_args.options, javaDict );
503    jpb->jvm_args.ignoreUnrecognized = JNI_TRUE;
504
505    //
506    // We need to replace any -Xmx option that may have been specified in the
507    // Info.plist file with one generated from the user preference.
508    //
509    replaceXmxOption( &jpb->jvm_args );
510
511    //
512    // Set-up the main class name and main()'s arguments.
513    //
514    getMainClassName( &jpb->main_className, javaDict );
515    jpb->main_argc = getMainArgs( &jpb->main_argv, javaDict );
516}
517
518/* vim:set et sw=4 ts=4: */
519