1/* Copyright (C) 2003 Vladimir Roubtsov. All rights reserved.
2 *
3 * This program and the accompanying materials are made available under
4 * the terms of the Common Public License v1.0 which accompanies this distribution,
5 * and is available at http://www.eclipse.org/legal/cpl-v10.html
6 *
7 * $Id: InstrProcessorST.java,v 1.1.1.1.2.3 2004/07/16 23:32:28 vlad_r Exp $
8 */
9package com.vladium.emma.instr;
10
11import java.io.File;
12import java.io.FileInputStream;
13import java.io.FileNotFoundException;
14import java.io.FileOutputStream;
15import java.io.IOException;
16import java.io.InputStream;
17import java.io.OutputStream;
18import java.io.RandomAccessFile;
19import java.util.Date;
20import java.util.jar.Attributes;
21import java.util.jar.JarFile;
22import java.util.jar.JarInputStream;
23import java.util.jar.JarOutputStream;
24import java.util.jar.Manifest;
25import java.util.zip.CRC32;
26import java.util.zip.ZipEntry;
27import java.util.zip.ZipInputStream;
28import java.util.zip.ZipOutputStream;
29
30import com.vladium.jcd.cls.ClassDef;
31import com.vladium.jcd.compiler.ClassWriter;
32import com.vladium.jcd.parser.ClassDefParser;
33import com.vladium.logging.Logger;
34import com.vladium.util.ByteArrayOStream;
35import com.vladium.util.Descriptors;
36import com.vladium.util.Files;
37import com.vladium.util.IPathEnumerator;
38import com.vladium.util.IProperties;
39//import com.vladium.util.Profiler;
40import com.vladium.util.Property;
41import com.vladium.util.asserts.$assert;
42import com.vladium.util.exception.Exceptions;
43//import com.vladium.utils.ObjectSizeProfiler;
44import com.vladium.emma.IAppConstants;
45import com.vladium.emma.IAppErrorCodes;
46import com.vladium.emma.EMMAProperties;
47import com.vladium.emma.EMMARuntimeException;
48import com.vladium.emma.data.CoverageOptions;
49import com.vladium.emma.data.CoverageOptionsFactory;
50import com.vladium.emma.data.DataFactory;
51import com.vladium.emma.data.IMetaData;
52
53// ----------------------------------------------------------------------------
54/**
55 * @author Vlad Roubtsov, (C) 2003
56 */
57final class InstrProcessorST extends InstrProcessor
58                             implements IAppErrorCodes
59{
60    // public: ................................................................
61
62    // TODO: performance of 'copy' mode could be improved by pushing dir filtering
63    // all the way to the path enumerator [although dir listing is reasonably fast]
64
65
66    // IPathEnumerator.IPathHandler:
67
68
69    public final void handleArchiveStart (final File parentDir, final File archive, final Manifest manifest)
70    {
71        final Logger log = m_log;
72        if (log.atTRACE2 ()) log.trace2 ("handleArchiveStart", "[" + parentDir + "] [" + archive + "]");
73
74        // TODO: pass manifest into this callback, if any
75        // TODO: detect if manifest corresonds to a previously intrumented archive already ?
76
77        if (DO_DEPENDS_CHECKING)
78        {
79            final File fullArchiveFile = Files.newFile (parentDir, archive);
80            m_currentArchiveTS = fullArchiveFile.lastModified ();
81
82            if ($assert.ENABLED) $assert.ASSERT (m_currentArchiveTS > 0, "invalid ts: " + m_currentArchiveTS);
83        }
84
85        if ((m_outMode == OutMode.OUT_MODE_FULLCOPY) || (m_outMode == OutMode.OUT_MODE_OVERWRITE))
86        {
87            final Manifest outManifest = manifest != null
88                ? new Manifest (manifest) // shallow copy
89                : new Manifest ();
90
91            // set some basic main attributes:
92
93            final Attributes mainAttrs = outManifest.getMainAttributes ();
94            if (manifest == null) mainAttrs.put (Attributes.Name.MANIFEST_VERSION,  "1.0");
95            mainAttrs.put (new Attributes.Name ("Created-By"), IAppConstants.APP_NAME + " v" + IAppConstants.APP_VERSION_WITH_BUILD_ID_AND_TAG);
96
97            // note: Manifest makes these 72-char-safe
98
99            mainAttrs.put (Attributes.Name.IMPLEMENTATION_TITLE,  "instrumented version of [" + archive.getAbsolutePath () + "]");
100            mainAttrs.put (Attributes.Name.SPECIFICATION_TITLE,  "instrumented on " + new Date (m_timeStamp) + " [" + Property.getSystemFingerprint () + "]");
101
102            // TODO: remove entries related to signing
103
104            if (m_outMode == OutMode.OUT_MODE_FULLCOPY)
105            {
106                // create an identically named artive in outdir/lib [the stream is
107                // closed in the archive end event handler]:
108
109                try
110                {
111                    final OutputStream out = new FileOutputStream (getFullOutFile (parentDir, archive, IN_LIB));
112
113                    m_archiveOut = outManifest != null ? new JarOutputStream (out, outManifest) : new JarOutputStream (out);
114                }
115                catch (IOException ioe)
116                {
117                    // TODO: error code
118                    throw new EMMARuntimeException (ioe);
119                }
120            }
121            else if (m_outMode == OutMode.OUT_MODE_OVERWRITE)
122            {
123                // create a temp file in the same dir [moved into the original one
124                // in the archive end event handler]:
125
126                m_origArchiveFile = Files.newFile (parentDir, archive);
127
128                // length > 3:
129                final String archiveName = Files.getFileName (archive) + IAppConstants.APP_NAME_LC;
130                final String archiveExt = EMMAProperties.PROPERTY_TEMP_FILE_EXT;
131
132                try
133                {
134                    m_tempArchiveFile = Files.createTempFile (parentDir, archiveName, archiveExt);
135                    if (log.atTRACE2 ()) log.trace2 ("handleArchiveStart", "created temp archive [" + m_tempArchiveFile.getAbsolutePath () + "]");
136
137                    final OutputStream out = new FileOutputStream (m_tempArchiveFile);
138
139                    m_archiveOut = outManifest != null ? new JarOutputStream (out, outManifest) : new JarOutputStream (out);
140                }
141                catch (IOException ioe)
142                {
143                    // TODO: error code
144                    throw new EMMARuntimeException (ioe);
145                }
146            }
147        }
148    }
149
150    public final void handleArchiveEntry (final JarInputStream in, final ZipEntry entry)
151    {
152        final Logger log = m_log;
153        if (log.atTRACE2 ()) log.trace2 ("handleArchiveEntry", "[" + entry.getName () + "]");
154
155        final String name = entry.getName ();
156        final String lcName = name.toLowerCase ();
157
158        final boolean notcopymode = (m_outMode == OutMode.OUT_MODE_FULLCOPY) || (m_outMode == OutMode.OUT_MODE_OVERWRITE);
159
160        boolean copyEntry = false;
161
162        if (lcName.endsWith (".class"))
163        {
164            final String className = name.substring (0, name.length () - 6).replace ('/', '.');
165
166            // it is possible that a class with this name has already been processed;
167            // however, we can't skip it here because there is no guarantee that
168            // the runtime classpath will be identical to the instrumentation path
169
170            // [the metadata will still contain only a single entry for a class with
171            // this name: it is the responsibility of the user to ensure that both
172            // files represent the same class; in the future I might use a more
173            // robust internal strategy that uses something other than a class name
174            // as a metadata key]
175
176            if ((m_coverageFilter == null) || m_coverageFilter.included (className))
177            {
178                InputStream clsin = null;
179                try
180                {
181                    File outFile = null;
182                    File fullOutFile = null;
183
184                    if (DO_DEPENDS_CHECKING)
185                    {
186                        // in 'copy' mode
187
188                        if (m_outMode == OutMode.OUT_MODE_COPY)
189                        {
190                            outFile = new File (className.replace ('.', File.separatorChar).concat (".class"));
191                            fullOutFile = getFullOutFile (null, outFile, IN_CLASSES);
192
193                            // if we already processed this class name within this instrumentor
194                            // run, skip duplicates in copy mode:
195
196                            if (m_mdata.hasDescriptor (Descriptors.javaNameToVMName (className)))
197                                return;
198
199                            // BUG_SF989071: using outFile here instead resulted in
200                            // a zero result regardless of whether the target existed or not
201                            final long outTimeStamp = fullOutFile.lastModified (); // 0 if 'fullOutFile' does not exist or if an I/O error occurs
202
203                            if (outTimeStamp > 0)
204                            {
205                                long inTimeStamp = entry.getTime (); // can return -1
206                                if (inTimeStamp < 0) inTimeStamp = m_currentArchiveTS; // default to the archive file timestamp
207
208                                if ($assert.ENABLED) $assert.ASSERT (inTimeStamp > 0);
209
210                                if (inTimeStamp <= outTimeStamp)
211                                {
212                                    if (log.atVERBOSE ()) log.verbose ("destination file [" + outFile + "] skipped: more recent than the source");
213                                    return;
214                                }
215                            }
216                        }
217                    }
218
219                    readZipEntry (in, entry);
220
221                    final ClassDef clsDef = ClassDefParser.parseClass (m_readbuf, m_readpos);
222
223                    m_visitor.process (clsDef, m_outMode == OutMode.OUT_MODE_OVERWRITE, true, true, m_instrResult);
224                    if (m_instrResult.m_instrumented)
225                    {
226                        if ($assert.ENABLED) $assert.ASSERT (m_instrResult.m_descriptor != null, "no descriptor created for an instrumented class");
227
228                        ++ m_classInstrs;
229
230                        // update metadata [if this class has not been seen before]:
231
232                        m_mdata.add (m_instrResult.m_descriptor, false);
233
234                        // class def modified: write it to an array and submit a write job
235
236                        m_baos.reset ();
237                        ClassWriter.writeClassTable (clsDef, m_baos);
238
239                        if (notcopymode)
240                        {
241                            // [destination is a zip entry]
242
243                            entry.setTime (m_timeStamp);
244                            addJob (new EntryWriteJob (m_archiveOut, m_baos.copyByteArray (), entry, false));
245                        }
246                        else // copy mode
247                        {
248                            // [destination is a file]
249
250                            if (! DO_DEPENDS_CHECKING) // this block is just a complement to the one above (where fullOutFile is inited)
251                            {
252                                outFile = new File (className.replace ('.', File.separatorChar).concat (".class"));
253                                fullOutFile = getFullOutFile (null, outFile, IN_CLASSES);
254                            }
255
256                            addJob (new FileWriteJob (fullOutFile, m_baos.copyByteArray (), true));
257                        }
258                    }
259                    else if (notcopymode)
260                    {
261                        // original class def already read into m_readbuf:
262                        // clone the array and submit an entry write job
263
264                        final byte [] data = new byte [m_readpos];
265                        System.arraycopy (m_readbuf, 0, data, 0, data.length);
266                        ++ m_classCopies;
267
268                        entry.setTime (m_timeStamp);
269                        addJob (new EntryWriteJob (m_archiveOut, data, entry, true));
270                    }
271                }
272                catch (FileNotFoundException fnfe)
273                {
274                    // ignore: this should never happen
275                    if ($assert.ENABLED)
276                    {
277                        fnfe.printStackTrace (System.out);
278                    }
279                }
280                catch (IOException ioe)
281                {
282                    // TODO: error code
283                    throw new EMMARuntimeException (ioe);
284                }
285                finally
286                {
287                    if (clsin != null)
288                        try
289                        {
290                            clsin.close ();
291                        }
292                        catch (Exception e)
293                        {
294                            // TODO: error code
295                            throw new EMMARuntimeException (e);
296                        }
297                }
298            }
299            else
300            {
301                // copy excluded .class entries in full copy and overwrite modes:
302                copyEntry = notcopymode;
303            }
304        }
305        else
306        {
307            // copy non-.class entries in full copy and overwrite modes:
308            copyEntry = notcopymode;
309
310            // skipping these entries here is important: this is done as a complement
311            // to Sun jar API workarounds as detailed in PathEnumerator.enumeratePathArchive():
312
313            if (copyEntry && name.equalsIgnoreCase ("META-INF/"))
314                copyEntry = false;
315            if (copyEntry && name.equalsIgnoreCase (JarFile.MANIFEST_NAME))
316                copyEntry = false;
317
318            // TODO: skip signature-related entries (.SF and .RSA/.DSA/.PGP)
319        }
320
321        if (copyEntry)
322        {
323            try
324            {
325                readZipEntry (in, entry);
326
327                final byte [] data = new byte [m_readpos];
328                System.arraycopy (m_readbuf, 0, data, 0, data.length);
329                ++ m_classCopies;
330
331                entry.setTime (m_timeStamp);
332                addJob (new EntryWriteJob (m_archiveOut, data, entry, true));
333            }
334            catch (IOException ioe)
335            {
336                // TODO: error code
337                throw new EMMARuntimeException (ioe);
338            }
339        }
340    }
341
342    public final void handleArchiveEnd (final File parentDir, final File archive)
343    {
344        final Logger log = m_log;
345        if (log.atTRACE2 ()) log.trace2 ("handleArchiveEnd", "[" + parentDir + "] [" + archive + "]");
346
347        m_currentArchiveTS = Long.MAX_VALUE;
348
349        if ((m_outMode == OutMode.OUT_MODE_FULLCOPY) || (m_outMode == OutMode.OUT_MODE_OVERWRITE))
350        {
351            try
352            {
353                drainJobQueue (); // drain the queue before closing the archive
354
355                m_archiveOut.flush ();
356                m_archiveOut.close ();
357                m_archiveOut = null;
358            }
359            catch (IOException ioe)
360            {
361                // TODO: error code
362                throw new EMMARuntimeException (ioe);
363            }
364
365            // in overwrite mode replace the original archive with the temp archive:
366
367            if (m_outMode == OutMode.OUT_MODE_OVERWRITE)
368            {
369                if (! Files.renameFile (m_tempArchiveFile, m_origArchiveFile, true)) // overwrite the original archive
370                {
371                    // TODO: disable temp file cleanup in this case so that the user
372                    // could do it manually later?
373
374                    // error code
375                    throw new EMMARuntimeException ("could not rename temporary file [" + m_tempArchiveFile + "] to [" + m_origArchiveFile + "]: make sure the original file is not locked and can be deleted");
376                }
377                else
378                {
379                    if (log.atTRACE2 ()) log.trace2 ("handleArchiveEnd", "renamed temp archive [" + m_tempArchiveFile.getAbsolutePath () + "] to [" + m_origArchiveFile + "]");
380                    m_origArchiveFile = m_tempArchiveFile = null;
381                }
382            }
383        }
384    }
385
386
387    public final void handleDirStart (final File pathDir, final File dir)
388    {
389        final Logger log = m_log;
390        if (log.atTRACE2 ()) log.trace2 ("handleDirStart", "[" + pathDir + "] [" + dir + "]");
391
392        // in full copy mode, create all dirs here; in copy mode, do it as part
393        // of writing each individual file:
394
395        if (m_outMode == OutMode.OUT_MODE_FULLCOPY)
396        {
397            final File saveDir = new File (getFullOutDir (pathDir, IN_CLASSES), dir.getPath ());
398            createDir (saveDir, true);
399        }
400    }
401
402    public final void handleFile (final File pathDir, final File file)
403    {
404        final Logger log = m_log;
405        if (log.atTRACE2 ()) log.trace2 ("handleFile", "[" + pathDir + "] [" + file + "]");
406
407        final String name = file.getPath ();
408        final String lcName = name.toLowerCase ();
409
410        final boolean fullcopymode = (m_outMode == OutMode.OUT_MODE_FULLCOPY);
411        final boolean mkdir = (m_outMode == OutMode.OUT_MODE_COPY);
412
413
414        boolean copyFile = false;
415
416        if (lcName.endsWith (".class"))
417        {
418            final String className = name.substring (0, name.length () - 6).replace (File.separatorChar, '.');
419
420            // it is possible that a class with this name has already been processed;
421            // however, we can't skip it here because there is no guarantee that
422            // the runtime classpath will be identical to the instrumentation path
423
424            // [the metadata will still contain only a single entry for a class with
425            // this name: it is the responsibility of the user to ensure that both
426            // files represent the same class; in the future I might use a more
427            // robust internal strategy that uses something other than a class name
428            // as a metadata key]
429
430            if ((m_coverageFilter == null) || m_coverageFilter.included (className))
431            {
432                InputStream clsin = null;
433                try
434                {
435                    final File inFile = Files.newFile (pathDir, file.getPath ());
436                    final File fullOutFile = getFullOutFile (pathDir, file, IN_CLASSES);
437
438                    if (DO_DEPENDS_CHECKING)
439                    {
440                        if (m_outMode == OutMode.OUT_MODE_COPY)
441                        {
442                            // if we already processed this class name within this instrumentor
443                            // run, skip duplicates in copy mode:
444
445                            if (m_mdata.hasDescriptor (Descriptors.javaNameToVMName (className)))
446                                return;
447
448                            // otherwise, instrument only if the dest file is out of date
449                            // wrt to the source file:
450
451                            final long outTimeStamp = fullOutFile.lastModified (); // 0 if 'fullOutFile' does not exist or if an I/O error occurs
452
453                            if (outTimeStamp > 0)
454                            {
455                                final long inTimeStamp = inFile.lastModified ();
456
457                                if (inTimeStamp <= outTimeStamp)
458                                {
459                                    if (log.atVERBOSE ()) log.verbose ("destination file [" + fullOutFile + "] skipped: more recent that the source file");
460                                    return;
461                                }
462                            }
463                        }
464                    }
465
466                    readFile (inFile);
467
468                    ClassDef clsDef = ClassDefParser.parseClass (m_readbuf, m_readpos);
469
470                    // in non-overwrite modes, bail if src file already instrumented:
471                    m_visitor.process (clsDef, m_outMode == OutMode.OUT_MODE_OVERWRITE, true, true, m_instrResult);
472                    if (m_instrResult.m_instrumented)
473                    {
474                        if ($assert.ENABLED) $assert.ASSERT (m_instrResult.m_descriptor != null, "no descriptor created for an instrumented class");
475
476                        ++ m_classInstrs;
477
478                        // update metadata [if this class has not been seen before]:
479
480//       ObjectSizeProfiler.SizeProfile profile = ObjectSizeProfiler.profile (m_instrResult.m_descriptor, true);
481//       System.out.println (clsDef.getName () + " metadata:");
482//       System.out.println (profile.root ().dump (0.2));
483
484                        m_mdata.add (m_instrResult.m_descriptor, false);
485
486                        // class def modified: write it to an array and submit a write job
487
488                        m_baos.reset ();
489                        ClassWriter.writeClassTable (clsDef, m_baos);
490                        clsDef = null;
491
492                        final byte [] outdata = m_baos.copyByteArray ();
493
494                        addJob (new FileWriteJob (fullOutFile, outdata, mkdir));
495                    }
496                    else if (fullcopymode)
497                    {
498                        // original class def already read into m_readbuf:
499                        // clone the array and submit a file write job
500
501                        clsDef = null;
502
503                        final byte [] outdata = new byte [m_readpos];
504                        System.arraycopy (m_readbuf, 0, outdata, 0, m_readpos);
505                        ++ m_classCopies;
506
507                        addJob (new FileWriteJob (fullOutFile, outdata, mkdir));
508                    }
509                }
510                catch (FileNotFoundException fnfe)
511                {
512                    // ignore: this should never happen
513                    if ($assert.ENABLED)
514                    {
515                        fnfe.printStackTrace (System.out);
516                    }
517                }
518                catch (IOException ioe)
519                {
520                    // TODO: error code
521                    throw new EMMARuntimeException (ioe);
522                }
523                finally
524                {
525                    if (clsin != null)
526                        try
527                        {
528                            clsin.close ();
529                        }
530                        catch (Exception e)
531                        {
532                            // TODO: error code
533                            throw new EMMARuntimeException (e);
534                        }
535                }
536            }
537            else
538            {
539                // copy excluded .class files in full copy mode:
540                copyFile = fullcopymode;
541            }
542        }
543        else
544        {
545            // copy non-.class files in full copy mode:
546            copyFile = fullcopymode;
547        }
548
549        if (copyFile)
550        {
551            try
552            {
553                final File inFile = Files.newFile (pathDir, file.getPath ());
554                readFile (inFile);
555
556                final byte [] data = new byte [m_readpos];
557                System.arraycopy (m_readbuf, 0, data, 0, data.length);
558                ++ m_classCopies;
559
560                final File outFile = getFullOutFile (pathDir, file, IN_CLASSES);
561
562                addJob (new FileWriteJob (outFile, data, mkdir));
563            }
564            catch (IOException ioe)
565            {
566                // TODO: error code
567                throw new EMMARuntimeException (ioe);
568            }
569        }
570    }
571
572    public final void handleDirEnd (final File pathDir, final File dir)
573    {
574        final Logger log = m_log;
575        if (log.atTRACE2 ()) log.trace2 ("handleDirEnd", "[" + pathDir + "] [" + dir + "]");
576
577        // in overwrite mode, flush the job queue before going to the next directory:
578
579        if (m_outMode == OutMode.OUT_MODE_OVERWRITE)
580        {
581            try
582            {
583                drainJobQueue ();
584            }
585            catch (IOException ioe)
586            {
587                // TODO: error code
588                throw new EMMARuntimeException (ioe);
589            }
590        }
591    }
592
593    // protected: .............................................................
594
595
596    protected void reset ()
597    {
598        m_visitor = null;
599        m_mdata = null;
600        m_readbuf = null;
601        m_baos = null;
602
603        for (int j = 0; j < m_jobs.length; ++ j) m_jobs [j] = null;
604
605        if (CLEANUP_TEMP_ARCHIVE_ON_ERRORS)
606        {
607            if (m_archiveOut != null)
608                try { m_archiveOut.close (); } catch (Exception ignore) {} // unlock the file descriptor for deletion
609
610            if (m_tempArchiveFile != null)
611                m_tempArchiveFile.delete ();
612        }
613
614        m_archiveOut = null;
615        m_origArchiveFile = null;
616        m_tempArchiveFile = null;
617
618        super.reset ();
619    }
620
621    protected void _run (final IProperties toolProperties)
622    {
623        final Logger log = m_log;
624
625        final boolean verbose = log.atVERBOSE ();
626        if (verbose)
627        {
628            log.verbose (IAppConstants.APP_VERBOSE_BUILD_ID);
629
630            // [assertion: m_instrPath != null]
631            log.verbose ("instrumentation path:");
632            log.verbose ("{");
633            for (int p = 0; p < m_instrPath.length; ++ p)
634            {
635                final File f = m_instrPath [p];
636                final String nonexistent = f.exists () ? "" : "{nonexistent} ";
637
638                log.verbose ("  " + nonexistent + f.getAbsolutePath ());
639            }
640            log.verbose ("}");
641
642            // [assertion: m_outMode != null]
643            log.verbose ("instrumentation output mode: " + m_outMode);
644        }
645        else
646        {
647            log.info ("processing instrumentation path ...");
648        }
649
650        RuntimeException failure = null;
651        try
652        {
653            long start = System.currentTimeMillis ();
654            m_timeStamp = start;
655
656            // construct instr path enumerator [throws on illegal input only]:
657            final IPathEnumerator enumerator = IPathEnumerator.Factory.create (m_instrPath, m_canonical, this);
658
659            // create out dir(s):
660            {
661                if (m_outMode != OutMode.OUT_MODE_OVERWRITE) createDir (m_outDir, true);
662
663                if ((m_outMode == OutMode.OUT_MODE_FULLCOPY))
664                {
665                    final File classesDir = Files.newFile (m_outDir, CLASSES);
666                    createDir (classesDir, false); // note: not using mkdirs() here
667
668                    final File libDir = Files.newFile (m_outDir, LIB);
669                    createDir (libDir, false); // note: not using mkdirs() here
670                }
671            }
672
673            // get the data out settings [note: this is not conditioned on m_dumpRawData]:
674            File mdataOutFile = m_mdataOutFile;
675            Boolean mdataOutMerge = m_mdataOutMerge;
676            {
677                if (mdataOutFile == null)
678                    mdataOutFile = new File (toolProperties.getProperty (EMMAProperties.PROPERTY_META_DATA_OUT_FILE,
679                                                                         EMMAProperties.DEFAULT_META_DATA_OUT_FILE));
680
681                if (mdataOutMerge == null)
682                {
683                    final String _dataOutMerge = toolProperties.getProperty (EMMAProperties.PROPERTY_META_DATA_OUT_MERGE,
684                                                                             EMMAProperties.DEFAULT_META_DATA_OUT_MERGE.toString ());
685                    mdataOutMerge = Property.toBoolean (_dataOutMerge) ? Boolean.TRUE : Boolean.FALSE;
686                }
687            }
688
689            if (verbose)
690            {
691                log.verbose ("metadata output file: " + mdataOutFile.getAbsolutePath ());
692                log.verbose ("metadata output merge mode: " + mdataOutMerge);
693            }
694
695            // TODO: can also register an exit hook to clean up temp files, but this is low value
696
697            // allocate I/O buffers:
698            m_readbuf = new byte [BUF_SIZE]; // don't reuse this across run() calls to reset it to the original size
699            m_readpos = 0;
700            m_baos = new ByteArrayOStream (BUF_SIZE); // don't reuse this across run() calls to reset it to the original size
701
702            // reset job queue position:
703            m_jobPos = 0;
704
705            m_currentArchiveTS = Long.MAX_VALUE;
706
707            final CoverageOptions options = CoverageOptionsFactory.create (toolProperties);
708            m_visitor = new InstrVisitor (options); // TODO: reuse this?
709
710            m_mdata = DataFactory.newMetaData (options);
711
712            // actual work is driven by the path enumerator:
713            try
714            {
715                enumerator.enumerate ();
716                drainJobQueue ();
717            }
718            catch (IOException ioe)
719            {
720                throw new EMMARuntimeException (INSTR_IO_FAILURE, ioe);
721            }
722
723            if (log.atINFO ())
724            {
725                final long end = System.currentTimeMillis ();
726
727                log.info ("instrumentation path processed in " + (end - start) + " ms");
728                log.info ("[" + m_classInstrs + " class(es) instrumented, " + m_classCopies + " resource(s) copied]");
729            }
730
731            // persist metadata:
732            try
733            {
734                // TODO: create an empty file earlier to catch any errors sooner? [to avoid scenarios where a user waits throught the entire instr run to find out the file could not be written to]
735
736                if ($assert.ENABLED) $assert.ASSERT (mdataOutFile != null, "m_metadataOutFile is null");
737
738                if (verbose)
739                {
740                    if (m_mdata != null)
741                    {
742                        log.verbose ("metadata contains " + m_mdata.size () + " entries");
743                    }
744                }
745
746                if (m_mdata.isEmpty ())
747                {
748                    log.info ("no output created: metadata is empty");
749                }
750                else
751                {
752                    start = System.currentTimeMillis ();
753                    DataFactory.persist (m_mdata, mdataOutFile, mdataOutMerge.booleanValue ());
754                    final long end = System.currentTimeMillis ();
755
756                    if (log.atINFO ())
757                    {
758                        log.info ("metadata " + (mdataOutMerge.booleanValue () ? "merged into" : "written to") + " [" + mdataOutFile.getAbsolutePath () + "] {in " + (end - start) + " ms}");
759                    }
760                }
761            }
762            catch (IOException ioe)
763            {
764                throw new EMMARuntimeException (OUT_IO_FAILURE, new Object [] {mdataOutFile.getAbsolutePath ()}, ioe);
765            }
766        }
767        catch (SecurityException se)
768        {
769            failure = new EMMARuntimeException (SECURITY_RESTRICTION, new String [] {IAppConstants.APP_NAME}, se);
770        }
771        catch (RuntimeException re)
772        {
773            failure = re;
774        }
775        finally
776        {
777            reset ();
778        }
779
780        if (failure != null)
781        {
782            if (Exceptions.unexpectedFailure (failure, EXPECTED_FAILURES))
783            {
784                throw new EMMARuntimeException (UNEXPECTED_FAILURE,
785                                                new Object [] {failure.toString (), IAppConstants.APP_BUG_REPORT_LINK},
786                                                failure);
787            }
788            else
789                throw failure;
790        }
791    }
792
793    // package: ...............................................................
794
795
796    InstrProcessorST ()
797    {
798        m_jobs = new Job [JOB_QUEUE_SIZE];
799        m_instrResult = new InstrVisitor.InstrResult ();
800    }
801
802
803    static void writeFile (final byte [] data, final File outFile, final boolean mkdirs)
804        throws IOException
805    {
806        RandomAccessFile raf = null;
807        try
808        {
809            if (mkdirs)
810            {
811                final File parent = outFile.getParentFile ();
812                if (parent != null) parent.mkdirs (); // no error checking here [errors will be throw below]
813            }
814
815            raf = new RandomAccessFile (outFile, "rw");
816            if (DO_RAF_EXTENSION) raf.setLength (data.length);
817
818            raf.write (data);
819        }
820        finally
821        {
822            if (raf != null) raf.close (); // note: intentionally letting the exception percolate up
823        }
824    }
825
826    static void writeZipEntry (final byte [] data, final ZipOutputStream out, final ZipEntry entry, final boolean isCopy)
827        throws IOException
828    {
829        if (isCopy)
830        {
831            out.putNextEntry (entry); // reusing ' entry' is ok here because we are not changing the data
832            try
833            {
834                out.write (data);
835            }
836            finally
837            {
838                out.closeEntry ();
839            }
840        }
841        else
842        {
843            // need to compute the checksum which slows things down quite a bit:
844
845            final ZipEntry entryCopy = new ZipEntry (entry.getName ());
846            entryCopy.setTime (entry.getTime ()); // avoid repeated calls to System.currentTimeMillis() inside the zip stream
847            entryCopy.setMethod (ZipOutputStream.STORED);
848            // [directory status is implicit in the name]
849            entryCopy.setSize (data.length);
850            entryCopy.setCompressedSize (data.length);
851
852            final CRC32 crc = new CRC32 ();
853            crc.update (data);
854            entryCopy.setCrc (crc.getValue ());
855
856            out.putNextEntry (entryCopy);
857            try
858            {
859                out.write (data);
860            }
861            finally
862            {
863                out.closeEntry ();
864            }
865        }
866    }
867
868    // private: ...............................................................
869
870
871    private static abstract class Job
872    {
873        protected abstract void run () throws IOException;
874
875    } // end of nested class
876
877
878    private static final class FileWriteJob extends Job
879    {
880        protected void run () throws IOException
881        {
882            writeFile (m_data, m_outFile, m_mkdirs);
883            m_data = null;
884        }
885
886        FileWriteJob (final File outFile, final byte [] data, final boolean mkdirs)
887        {
888            m_outFile = outFile;
889            m_data = data;
890            m_mkdirs = mkdirs;
891        }
892
893
894        final File m_outFile;
895        final boolean m_mkdirs;
896        byte [] m_data;
897
898    } // end of nested class
899
900
901    private static final class EntryWriteJob extends Job
902    {
903        protected void run () throws IOException
904        {
905            writeZipEntry (m_data, m_out, m_entry, m_isCopy);
906            m_data = null;
907        }
908
909        EntryWriteJob (final ZipOutputStream out, final byte [] data, final ZipEntry entry, final boolean isCopy)
910        {
911            m_out = out;
912            m_data = data;
913            m_entry = entry;
914            m_isCopy = isCopy;
915        }
916
917
918        final ZipOutputStream m_out;
919        byte [] m_data;
920        final ZipEntry m_entry;
921        final boolean m_isCopy;
922
923    } // end of nested class
924
925
926    private void addJob (final Job job)
927        throws FileNotFoundException, IOException
928    {
929        if (m_jobPos == JOB_QUEUE_SIZE) drainJobQueue ();
930
931        m_jobs [m_jobPos ++] = job;
932    }
933
934    private void drainJobQueue ()
935        throws IOException
936    {
937        for (int j = 0; j < m_jobPos; ++ j)
938        {
939            final Job job = m_jobs [j];
940            if (job != null) // a guard just in case
941            {
942                m_jobs [j] = null;
943                job.run ();
944            }
945        }
946
947        m_jobPos = 0;
948    }
949
950    /*
951     * Reads into m_readbuf (m_readpos is updated correspondingly)
952     */
953    private void readFile (final File file)
954        throws IOException
955    {
956        final int length = (int) file.length ();
957
958        ensureReadCapacity (length);
959
960        InputStream in = null;
961        try
962        {
963            in = new FileInputStream (file);
964
965            int totalread = 0;
966            for (int read;
967                 (totalread < length) && (read = in.read (m_readbuf, totalread, length - totalread)) >= 0;
968                 totalread += read);
969            m_readpos = totalread;
970        }
971        finally
972        {
973            if (in != null) try { in.close (); } catch (Exception ignore) {}
974        }
975    }
976
977    /*
978     * Reads into m_readbuf (m_readpos is updated correspondingly)
979     */
980    private void readZipEntry (final ZipInputStream in, final ZipEntry entry)
981        throws IOException
982    {
983        final int length = (int) entry.getSize (); // can be -1 if unknown
984
985        if (length >= 0)
986        {
987            ensureReadCapacity (length);
988
989            int totalread = 0;
990            for (int read;
991                 (totalread < length) && (read = in.read (m_readbuf, totalread, length - totalread)) >= 0;
992                 totalread += read);
993            m_readpos = totalread;
994        }
995        else
996        {
997            ensureReadCapacity (BUF_SIZE);
998
999            m_baos.reset ();
1000            for (int read; (read = in.read (m_readbuf)) >= 0; m_baos.write (m_readbuf, 0, read));
1001
1002            m_readbuf = m_baos.copyByteArray ();
1003            m_readpos = m_readbuf.length;
1004        }
1005    }
1006
1007    private void ensureReadCapacity (final int capacity)
1008    {
1009        if (m_readbuf.length < capacity)
1010        {
1011            final int readbuflen = m_readbuf.length;
1012            m_readbuf = null;
1013            m_readbuf = new byte [Math.max (readbuflen << 1, capacity)];
1014        }
1015    }
1016
1017
1018    // internal run()-scoped state:
1019
1020    private final Job [] m_jobs;
1021    private final InstrVisitor.InstrResult m_instrResult;
1022
1023    private InstrVisitor m_visitor;
1024    private IMetaData m_mdata;
1025    private byte [] m_readbuf;
1026    private int m_readpos;
1027    private ByteArrayOStream m_baos; // TODO: code to guard this from becoming too large
1028    private int m_jobPos;
1029    private long m_currentArchiveTS;
1030    private File m_origArchiveFile, m_tempArchiveFile;
1031    private JarOutputStream m_archiveOut;
1032    private long m_timeStamp;
1033
1034
1035    private static final int BUF_SIZE = 32 * 1024;
1036    private static final int JOB_QUEUE_SIZE = 128; // a reasonable size chosen empirically after testing a few SCSI/IDE machines
1037    private static final boolean CLEANUP_TEMP_ARCHIVE_ON_ERRORS = true;
1038    private static final boolean DO_RAF_EXTENSION = true;
1039
1040    private static final boolean DO_DEPENDS_CHECKING = true;
1041    private static final Class [] EXPECTED_FAILURES; // set in <clinit>
1042
1043    static
1044    {
1045        EXPECTED_FAILURES = new Class []
1046        {
1047            EMMARuntimeException.class,
1048            IllegalArgumentException.class,
1049            IllegalStateException.class,
1050        };
1051    }
1052
1053} // end of class
1054// ----------------------------------------------------------------------------