1 /*
2  * Copyright (c) 2004-2010, P. Simon Tuffs (simon@simontuffs.com)
3  * All rights reserved.
4  *
5  * See the full license at http://one-jar.sourceforge.net/one-jar-license.html
6  * This license is also included in the distributions of this software
7  * under doc/one-jar-license.txt
8  */
9 
10 package com.simontuffs.onejar;
11 
12 import java.io.BufferedReader;
13 import java.io.File;
14 import java.io.FileInputStream;
15 import java.io.IOException;
16 import java.io.InputStream;
17 import java.io.InputStreamReader;
18 import java.lang.reflect.Constructor;
19 import java.lang.reflect.Method;
20 import java.net.MalformedURLException;
21 import java.net.URL;
22 import java.security.AccessController;
23 import java.security.PrivilegedAction;
24 import java.util.ArrayList;
25 import java.util.Arrays;
26 import java.util.Enumeration;
27 import java.util.Properties;
28 import java.util.jar.Attributes;
29 import java.util.jar.JarEntry;
30 import java.util.jar.JarFile;
31 import java.util.jar.JarInputStream;
32 import java.util.jar.Manifest;
33 import java.util.zip.ZipEntry;
34 import java.util.zip.ZipFile;
35 
36 /**
37  * Run a java application which requires multiple support jars from inside
38  * a single jar file.
39  *
40  * <p>
41  * Developer time JVM properties:
42  * <pre>
43  *   -Done-jar.main.class={name}  Use named class as main class to run.
44  *   -Done-jar.record[=recording] Record loaded classes into "recording" directory.
45  *                                Flatten jar.names into directory tree suitable
46  * 								  for use as a classpath.
47  *   -Done-jar.jar.names          Record loaded classes, preserve jar structure
48  *   -Done-jar.verbose            Run the JarClassLoader in verbose mode.
49  * </pre>
50  * @author simon@simontuffs.com (<a href="http://www.simontuffs.com">http://www.simontuffs.com</a>)
51  */
52 public class Boot {
53 
54     /**
55 	 * The name of the manifest attribute which controls which class
56 	 * to bootstrap from the jar file.  The boot class can
57 	 * be in any of the contained jar files.
58 	 */
59 	public final static String BOOT_CLASS = "Boot-Class";
60     public final static String ONE_JAR_CLASSLOADER = "One-Jar-Class-Loader";
61     public final static String ONE_JAR_MAIN_CLASS = "One-Jar-Main-Class";
62     public final static String ONE_JAR_DEFAULT_MAIN_JAR = "One-Jar-Default-Main-Jar";
63     public final static String ONE_JAR_MAIN_ARGS = "One-Jar-Main-Args";
64     public final static String ONE_JAR_URL_FACTORY = "One-Jar-URL-Factory";
65 
66 	public final static String MANIFEST = "META-INF/MANIFEST.MF";
67 	public final static String MAIN_JAR = "main/main.jar";
68 
69 	public final static String WRAP_CLASS_LOADER = "Wrap-Class-Loader";
70     public final static String WRAP_DIR = "wrap";
71 	public final static String WRAP_JAR = "/" + WRAP_DIR + "/wraploader.jar";
72 
73     // System properties.
74 	public final static String PROPERTY_PREFIX = "one-jar.";
75 	public final static String P_MAIN_CLASS = PROPERTY_PREFIX + "main.class";
76 	public final static String P_MAIN_JAR = PROPERTY_PREFIX + "main.jar";
77     public final static String P_MAIN_APP = PROPERTY_PREFIX + "main.app";
78 	public final static String P_RECORD = PROPERTY_PREFIX + "record";
79 	public final static String P_JARNAMES = PROPERTY_PREFIX + "jar.names";
80 	public final static String P_VERBOSE = PROPERTY_PREFIX + "verbose";
81 	public final static String P_INFO = PROPERTY_PREFIX + "info";
82 	public final static String P_WARNING = PROPERTY_PREFIX + "warning";
83     public final static String P_STATISTICS = PROPERTY_PREFIX + "statistics";
84     public final static String P_SHOW_PROPERTIES = PROPERTY_PREFIX + "show.properties";
85     public final static String P_JARPATH = PROPERTY_PREFIX + "jar.path";
86     public final static String P_ONE_JAR_CLASS_PATH = PROPERTY_PREFIX + "class.path";
87     public final static String P_JAVA_CLASS_PATH = "java.class.path";
88     public final static String P_PATH_SEPARATOR = "|";
89     public final static String P_EXPAND_DIR = PROPERTY_PREFIX + "expand.dir";
90 
91     // Command-line arguments
92     public final static String A_HELP    = "--one-jar-help";
93     public final static String A_VERSION = "--one-jar-version";
94 
95     public final static String[] HELP_PROPERTIES = {
96         P_MAIN_CLASS, "Specifies the name of the class which should be executed \n(via public static void main(String[])",
97         P_MAIN_APP,   "Specifies the name of the main/<app>.jar to be executed",
98         P_RECORD,     "true:  Enables recording of the classes loaded by the application",
99         P_JARNAMES,   "true:  Recorded classes are kept in directories corresponding to their jar names.\n" +
100                       "false: Recorded classes are flattened into a single directory.  \nDuplicates are ignored (first wins)",
101         P_VERBOSE,    "true:  Print verbose classloading information",
102         P_INFO,       "true:  Print informative classloading information",
103         P_WARNING,    "true:  Print serious classloading warnings",
104         P_STATISTICS, "true:  Shows statistics about the One-Jar Classloader",
105         P_JARPATH,    "Full path of the one-jar file being executed.  \nOnly needed if java.class.path does not contain the path to the jar, e.g. on Max OS/X.",
106         P_ONE_JAR_CLASS_PATH,    "Extra classpaths to be added to the execution environment.  \nUse platform independent path separator '" + P_PATH_SEPARATOR + "'",
107         P_EXPAND_DIR, "Directory to use for expanded files.",
108         P_SHOW_PROPERTIES, "true:  Shows the JVM system properties.",
109     };
110 
111     public final static String[] HELP_ARGUMENTS = {
112         A_HELP,       "Shows this message, then exits.",
113         A_VERSION,    "Shows the version of One-JAR, then exits.",
114     };
115 
116     protected static String mainJar;
117 
118 	protected static boolean warning = true, info, verbose, statistics;
119     protected static String myJarPath;
120 
121     protected static long startTime = System.currentTimeMillis();
122     protected static long endTime = 0;
123 
124 
125 	// Singleton loader.  This must not be changed once it is set, otherwise all
126     // sorts of nasty class-cast exceptions will ensue.  Hence we control
127     // access to it strongly.
128 	private static JarClassLoader loader = null;
129 
130 
131 	/**
132      * This method provides access to the bootstrap One-JAR classloader which
133      * is needed in the URL connection Handler when opening streams relative
134      * to classes.
135      * @return
136 	 */
getClassLoader()137     public synchronized static JarClassLoader getClassLoader() {
138 		return loader;
139 	}
140 
141     /**
142      * This is the single point of entry for setting the "loader" member.  It checks to
143      * make sure programming errors don't call it more than once.
144      * @param $loader
145      */
setClassLoader(JarClassLoader $loader)146     public synchronized static void setClassLoader(JarClassLoader $loader) {
147         if (loader != null) throw new RuntimeException("Attempt to set a second Boot loader");
148         loader = $loader;
149     }
150 
VERBOSE(String message)151 	protected static void VERBOSE(String message) {
152 		if (verbose) System.out.println("Boot: " + message);
153 	}
154 
WARNING(String message)155 	protected static void WARNING(String message) {
156 		System.err.println("Boot: Warning: " + message);
157 	}
158 
INFO(String message)159 	protected static void INFO(String message) {
160 		if (info) System.out.println("Boot: Info: " + message);
161 	}
162 
PRINTLN(String message)163     protected static void PRINTLN(String message) {
164         System.out.println("Boot: " + message);
165     }
166 
main(String[] args)167     public static void main(String[] args) throws Exception {
168     	run(args);
169     }
170 
run(String args[])171     public static void run(String args[]) throws Exception {
172 
173         args = processArgs(args);
174 
175     	// Is the main class specified on the command line?  If so, boot it.
176     	// Otherwise, read the main class out of the manifest.
177 		String mainClass = null;
178 
179 		{
180 			// Default properties are in resource 'one-jar.properties'.
181 			Properties properties = new Properties();
182 			String props = "one-jar.properties";
183 			InputStream is = Boot.class.getResourceAsStream("/" + props);
184 			if (is != null) {
185 				INFO("loading properties from " + props);
186 				properties.load(is);
187 			}
188 
189 			// Merge in anything in a local file with the same name.
190 			if (new File(props).exists()) {
191     			is = new FileInputStream(props);
192     			if (is != null) {
193     				INFO("merging properties from " + props);
194     				properties.load(is);
195     			}
196 			}
197 
198 			// Set system properties only if not already specified.
199 			Enumeration _enum = properties.propertyNames();
200 			while (_enum.hasMoreElements()) {
201 				String name = (String)_enum.nextElement();
202 				if (System.getProperty(name) == null) {
203 					System.setProperty(name, properties.getProperty(name));
204 				}
205 			}
206 		}
207         if (Boolean.valueOf(System.getProperty(P_SHOW_PROPERTIES, "false")).booleanValue()) {
208             // What are the system properties.
209             Properties props = System.getProperties();
210             String keys[] = (String[])props.keySet().toArray(new String[]{});
211             Arrays.sort(keys);
212 
213             for (int i=0; i<keys.length; i++) {
214                 String key = keys[i];
215                 System.out.println(key + "=" + props.get(key));
216             }
217         }
218 
219         // Process developer properties:
220         if (mainClass == null) {
221         	mainClass = System.getProperty(P_MAIN_CLASS);
222         }
223 
224 		if (mainJar == null) {
225 			String app = System.getProperty(P_MAIN_APP);
226 			if (app != null) {
227 				mainJar = "main/" + app + ".jar";
228 			} else {
229 				mainJar = System.getProperty(P_MAIN_JAR, MAIN_JAR);
230 			}
231         }
232 
233         // Pick some things out of the top-level JAR file.
234         String jar = getMyJarPath();
235         JarFile jarFile = new JarFile(jar);
236         Manifest manifest = jarFile.getManifest();
237         Attributes attributes = manifest.getMainAttributes();
238         String bootLoaderName = attributes.getValue(ONE_JAR_CLASSLOADER);
239 
240         if (mainJar == null) {
241             mainJar = attributes.getValue(ONE_JAR_DEFAULT_MAIN_JAR);
242         }
243 
244         String mainargs = attributes.getValue(ONE_JAR_MAIN_ARGS);
245         if (mainargs != null && args.length == 0) {
246             // Replace the args with built-in.  Support escaped whitespace.
247             args = mainargs.split("[^\\\\]\\s");
248             for (int i=0; i<args.length; i++) {
249                 args[i] = args[i].replaceAll("\\\\(\\s)", "$1");
250             }
251         }
252 
253 		// If no main-class specified, check the manifest of the main jar for
254 		// a Boot-Class attribute.
255 		if (mainClass == null) {
256             mainClass = attributes.getValue(ONE_JAR_MAIN_CLASS);
257             if (mainClass == null) {
258                 mainClass = attributes.getValue(BOOT_CLASS);
259                 if (mainClass != null) {
260                     WARNING("The manifest attribute " + BOOT_CLASS + " is deprecated in favor of the attribute " + ONE_JAR_MAIN_CLASS);
261                 }
262             }
263 		}
264 
265 		if (mainClass == null) {
266 			// Still don't have one (default).  One final try: look for a jar file in a
267 			// main directory.  There should be only one, and it's manifest
268 			// Main-Class attribute is the main class.  The JarClassLoader will take
269 			// care of finding it.
270 			InputStream is = Boot.class.getResourceAsStream("/" + mainJar);
271 			if (is != null) {
272 				JarInputStream jis = new JarInputStream(is);
273 				Manifest mainmanifest = jis.getManifest();
274                 jis.close();
275 				mainClass = mainmanifest.getMainAttributes().getValue(Attributes.Name.MAIN_CLASS);
276 			} else {
277 			    // There is no main jar. Info unless mainJar is empty string.
278 			    // The load(mainClass) will scan for main jars anyway.
279 				if (!"".equals(mainJar)){
280                     INFO("Unable to locate main jar '" + mainJar + "' in the JAR file " + getMyJarPath());
281 				}
282 			}
283 		}
284 
285 		// Do we need to create a wrapping classloader?  Check for the
286 		// presence of a "wrap" directory at the top of the jar file.
287 		URL url = Boot.class.getResource(WRAP_JAR);
288 
289 		if (url != null) {
290 			// Wrap class loaders.
291             final JarClassLoader bootLoader = getBootLoader(bootLoaderName);
292 			bootLoader.load(null);
293 
294 			// Read the "Wrap-Class-Loader" property from the wraploader jar file.
295 			// This is the class to use as a wrapping class-loader.
296             InputStream is = Boot.class.getResourceAsStream(WRAP_JAR);
297             if (is != null) {
298     			JarInputStream jis = new JarInputStream(is);
299     			final String wrapLoader = jis.getManifest().getMainAttributes().getValue(WRAP_CLASS_LOADER);
300                 jis.close();
301     			if (wrapLoader == null) {
302     				WARNING(url + " did not contain a " + WRAP_CLASS_LOADER + " attribute, unable to load wrapping classloader");
303     			} else {
304     				INFO("using " + wrapLoader);
305                     JarClassLoader wrapped = getWrapLoader(bootLoader, wrapLoader);
306                     if (wrapped == null) {
307                         WARNING("Unable to instantiate " + wrapLoader + " from " + WRAP_DIR + ": using default JarClassLoader");
308                         wrapped = getBootLoader(null);
309                     }
310                     setClassLoader(wrapped);
311     			}
312             }
313 		} else {
314             setClassLoader(getBootLoader(bootLoaderName, Boot.class.getClassLoader()));
315             INFO("using JarClassLoader: " + getClassLoader().getClass().getName());
316 		}
317 
318 		// Allow injection of the URL factory.
319 		String urlfactory = attributes.getValue(ONE_JAR_URL_FACTORY);
320 		if (urlfactory != null) {
321 		    loader.setURLFactory(urlfactory);
322 		}
323 
324 		mainClass = loader.load(mainClass);
325 
326         if (mainClass == null && !loader.isExpanded())
327             throw new Exception(getMyJarName() + " main class was not found (fix: add main/main.jar with a Main-Class manifest attribute, or specify -D" + P_MAIN_CLASS + "=<your.class.name>), or use " + ONE_JAR_MAIN_CLASS + " in the manifest");
328 
329         if (mainClass != null) {
330         	// Guard against the main.jar pointing back to this
331         	// class, and causing an infinite recursion.
332             String bootClass = Boot.class.getName();
333         	if (bootClass.equals(mainClass))
334         		throw new Exception(getMyJarName() + " main class (" + mainClass + ") would cause infinite recursion: check main.jar/META-INF/MANIFEST.MF/Main-Class attribute: " + mainClass);
335 
336         	Class cls = loader.loadClass(mainClass);
337 
338             endTime = System.currentTimeMillis();
339             showTime();
340 
341         	Method main = cls.getMethod("main", new Class[]{String[].class});
342         	main.invoke(null, new Object[]{args});
343         }
344     }
345 
showTime()346     public static void showTime() {
347         long endtime = System.currentTimeMillis();
348         if (statistics) {
349             PRINTLN("Elapsed time: " + (endtime - startTime) + "ms");
350         }
351     }
352 
setProperties(IProperties jarloader)353     public static void setProperties(IProperties jarloader) {
354         INFO("setProperties(" + jarloader + ")");
355         if (getProperty(P_RECORD, "false")) {
356             jarloader.setRecord(true);
357             jarloader.setRecording(System.getProperty(P_RECORD));
358         }
359         if (getProperty(P_JARNAMES, "false")) {
360             jarloader.setRecord(true);
361             jarloader.setFlatten(false);
362         }
363         // TODO: clean up the use of one-jar.{verbose,info,warning} properties.
364         if (verbose = getProperty(P_VERBOSE, "false")) {
365             jarloader.setVerbose(true);
366             jarloader.setInfo(true);
367         }
368         jarloader.setInfo(info=getProperty(P_INFO, "false"));
369         jarloader.setWarning(warning=getProperty(P_WARNING, "true"));
370 
371         statistics = getProperty(P_STATISTICS, "false");
372     }
373 
getProperty(String key, String $default)374     public static boolean getProperty(String key, String $default) {
375         return Boolean.valueOf(System.getProperty(key, "false")).booleanValue();
376     }
377 
getMyJarName()378     public static String getMyJarName() {
379         String name = getMyJarPath();
380         int last = name.lastIndexOf("/");
381         if (last >= 0) {
382             name = name.substring(last+1);
383         }
384         return name;
385     }
386 
getMyJarPath()387     public static String getMyJarPath() {
388         if (myJarPath != null) {
389             return myJarPath;
390         }
391         myJarPath = System.getProperty(P_JARPATH);
392         if (myJarPath == null) {
393             try {
394                 // Hack to obtain the name of this jar file.
395                 String jarname = System.getProperty(P_JAVA_CLASS_PATH);
396                 // Open each Jar file looking for this class name.  This allows for
397                 // JVM's that place more than the jar file on the classpath.
398                 String jars[] =jarname.split(System.getProperty("path.separator"));
399                 for (int i=0; i<jars.length; i++) {
400                     jarname = jars[i];
401                     VERBOSE("Checking " + jarname + " as One-Jar file");
402                     // Allow for URL based paths, as well as file-based paths.  File
403                     InputStream is = null;
404                     try {
405                         is = new URL(jarname).openStream();
406                     } catch (MalformedURLException mux) {
407                         // Try a local file.
408                         try {
409                             is = new FileInputStream(jarname);
410                         } catch (IOException iox) {
411                             // Ignore..., but it isn't good to have bad entries on the classpath.
412                             continue;
413                         }
414                     }
415                     ZipEntry entry = findJarEntry(new JarInputStream(is), Boot.class.getName().replace('.', '/') + ".class");
416                     if (entry != null) {
417                         myJarPath = jarname;
418                         break;
419                     } else {
420                         // One more try as a Zip file: supports launch4j on Windows.
421                         entry = findZipEntry(new ZipFile(jarname), Boot.class.getName().replace('.', '/') + ".class");
422                         if (entry != null) {
423                             myJarPath = jarname;
424                             break;
425                         }
426                     }
427                 }
428             } catch (Exception x) {
429                 x.printStackTrace();
430                 WARNING("jar=" + myJarPath + " loaded from " + P_JAVA_CLASS_PATH + " (" + System.getProperty(P_JAVA_CLASS_PATH) + ")");
431             }
432         }
433         if (myJarPath == null) {
434             throw new IllegalArgumentException("Unable to locate " + Boot.class.getName() + " in the java.class.path: consider using -D" + P_JARPATH + " to specify the one-jar filename.");
435         }
436         // Normalize those annoying DOS backslashes.
437         myJarPath = myJarPath.replace('\\', '/');
438         return myJarPath;
439     }
440 
findJarEntry(JarInputStream jis, String name)441     public static JarEntry findJarEntry(JarInputStream jis, String name) throws IOException {
442         JarEntry entry;
443         while ((entry = jis.getNextJarEntry()) != null) {
444             if (entry.getName().equals(name)) {
445                 return entry;
446             }
447         }
448         return null;
449     }
450 
findZipEntry(ZipFile zip, String name)451     public static ZipEntry findZipEntry(ZipFile zip, String name) throws IOException {
452         Enumeration entries = zip.entries();
453         while (entries.hasMoreElements()) {
454             ZipEntry entry = (ZipEntry) entries.nextElement();
455             VERBOSE(("findZipEntry(): entry=" + entry.getName()));
456             if (entry.getName().equals(name))
457                 return entry;
458         }
459         return null;
460     }
461 
firstWidth(String[] table)462     public static int firstWidth(String[] table) {
463         int width = 0;
464         for (int i=0; i<table.length; i+=2) {
465             if (table[i].length() > width) width = table[i].length();
466         }
467         return width;
468     }
469 
pad(String indent, String string, int width)470     public static String pad(String indent, String string, int width) {
471         StringBuffer buf = new StringBuffer();
472         buf.append(indent);
473         buf.append(string);
474         for (int i=0; i<width-string.length(); i++) {
475             buf.append(" ");
476         }
477         return buf.toString();
478     }
479 
wrap(String indent, String string, int width)480     public static String wrap(String indent, String string, int width) {
481         String padding = pad(indent, "", width);
482         string = string.replaceAll("\n", "\n" + padding);
483         return string;
484     }
485 
processArgs(String args[])486     public static String[] processArgs(String args[]) throws Exception {
487         // Check for arguments which matter to us, and strip them.
488     	VERBOSE("processArgs(" + Arrays.asList(args) + ")");
489     	ArrayList list = new ArrayList();
490     	for (int a=0; a<args.length; a++) {
491         	String argument = args[a];
492 	        if (argument.startsWith(A_HELP)) {
493 	            int width = firstWidth(HELP_ARGUMENTS);
494 	            // Width of first column
495 
496 	            System.out.println("One-Jar uses the following command-line arguments");
497 	            for (int i=0; i<HELP_ARGUMENTS.length; i++) {
498 	                System.out.print(pad("    ", HELP_ARGUMENTS[i++], width+1));
499 	                System.out.println(wrap("    ", HELP_ARGUMENTS[i], width+1));
500 	            }
501 	            System.out.println();
502 
503 	            width = firstWidth(HELP_PROPERTIES);
504 	            System.out.println("One-Jar uses the following VM properties (-D<property>=<true|false|string>)");
505 	            for (int i=0; i<HELP_PROPERTIES.length; i++) {
506 	                System.out.print(pad("    ", HELP_PROPERTIES[i++], width+1));
507 	                System.out.println(wrap("    ", HELP_PROPERTIES[i], width+1));
508 	            }
509 	            System.out.println();
510 	            System.exit(0);
511 	        } else if (argument.startsWith(A_VERSION)) {
512 	            InputStream is = Boot.class.getResourceAsStream("/.version");
513 	            if (is != null) {
514 	                BufferedReader br = new BufferedReader(new InputStreamReader(is));
515 	                String version = br.readLine();
516 	                br.close();
517 	                System.out.println("One-JAR version " + version);
518 	            } else {
519 	                System.out.println("Unable to determine One-JAR version (missing /.version resource in One-JAR archive)");
520 	            }
521 	            System.exit(0);
522 	        } else {
523 	        	list.add(argument);
524 	        }
525         }
526     	return (String[])list.toArray(new String[0]);
527     }
528 
getBootLoader(final String loader)529     protected static JarClassLoader getBootLoader(final String loader) {
530         JarClassLoader bootLoader = (JarClassLoader)AccessController.doPrivileged(
531                 new PrivilegedAction() {
532                     public Object run() {
533                         if (loader != null) {
534                             try {
535                                 Class cls = Class.forName(loader);
536                                 Constructor ctor = cls.getConstructor(new Class[]{String.class});
537                                 return ctor.newInstance(new Object[]{WRAP_DIR});
538                             } catch (Exception x) {
539                                 WARNING("Unable to instantiate " + loader + ": " + x + " continuing using default " + JarClassLoader.class.getName());
540                             }
541                         }
542                         return new JarClassLoader(WRAP_DIR);
543                     }
544                 }
545             );
546         return bootLoader;
547     }
548 
getBootLoader(final String loader, ClassLoader parent)549     protected static JarClassLoader getBootLoader(final String loader, ClassLoader parent) {
550         return (JarClassLoader)AccessController.doPrivileged(
551             new PrivilegedAction() {
552                 public Object run() {
553                     if (loader != null) {
554                         try {
555                             Class cls = Class.forName(loader);
556                             Constructor ctor = cls.getConstructor(new Class[]{ClassLoader.class});
557                             return ctor.newInstance(new Object[]{Boot.class.getClassLoader()});
558                         } catch (Exception x) {
559                             WARNING("Unable to instantiate " + loader + ": " + x + " continuing using default " + JarClassLoader.class.getName());
560                         }
561                     }
562                     return new JarClassLoader(Boot.class.getClassLoader());
563                 }
564             }
565         );
566     }
567 
568     protected static JarClassLoader getWrapLoader(final ClassLoader bootLoader, final String wrapLoader) {
569         return ((JarClassLoader)AccessController.doPrivileged(
570             new PrivilegedAction() {
571                 public Object run() {
572                     try {
573                         Class jarLoaderClass = bootLoader.loadClass(wrapLoader);
574                         Constructor ctor = jarLoaderClass.getConstructor(new Class[]{ClassLoader.class});
575                         return ctor.newInstance(new Object[]{bootLoader});
576                     } catch (Throwable t) {
577                         WARNING(t.toString());
578                     }
579                     return null;
580                 }
581             }));
582     }
583 
584     public static long getEndTime() {
585         return endTime;
586     }
587 
588     public static long getStartTime() {
589         return startTime;
590     }
591 
592 }
593