001package votorola.s.line; // Copyright 2008-2011, 2013, Michael Allan.  Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Votorola Software"), to deal in the Votorola Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicence, and/or sell copies of the Votorola Software, and to permit persons to whom the Votorola Software is furnished to do so, subject to the following conditions: The preceding copyright notice and this permission notice shall be included in all copies or substantial portions of the Votorola Software. THE VOTOROLA SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE VOTOROLA SOFTWARE OR THE USE OR OTHER DEALINGS IN THE VOTOROLA SOFTWARE.
002
003import java.io.*;
004import java.net.*;
005import java.util.*;
006import java.util.logging.*;
007import java.util.regex.*;
008import javax.mail.internet.*;
009import javax.xml.stream.*;
010import votorola.a.*;
011import votorola.a.response.line.*;
012import votorola.a.trust.*;
013import votorola.a.voter.*;
014import votorola.g.*;
015import votorola.g.hold.*;
016import votorola.g.io.*;
017import votorola.g.lang.*;
018import votorola.g.logging.*;
019import votorola.g.net.*;
020import votorola.g.option.*;
021import votorola.g.script.*;
022
023
024/** Main class of the executable <code>votrace</code> - a tool to trace a neigbourhood
025  * trust network and compile a residential register.
026  *
027  *     @see <a href='../../../../../s/manual.xht#line-votrace' target='_top'>votrace</a>
028  */
029public final class VOTrace extends ResultsCompiler
030{
031
032    // cf. a/count/VOCount
033
034
035    /** Runs the tool from the command line.
036      *
037      *     @param argv command line argument array
038      */
039    public static void main( final String[] argv )
040    {
041        logger.info( "votrace is running with arguments " + Arrays.toString( argv ));
042        new VOTrace().run( argv );
043    }
044
045
046
047//// P r i v a t e ///////////////////////////////////////////////////////////////////////
048
049
050    private static final Logger logger = LoggerX.i( VOTrace.class );
051
052
053
054    private static ArrayList<File> nominalReadyDirectories( final File votraceDirectory )
055    {
056        final ArrayList<File> list = new ArrayList<File>();
057        final File[] snapDirectoryArray = FileX.listFilesNoNull( votraceDirectory,
058          OutputStore.SNAP_DIRECTORY_FILTER );
059        for( final File snapDirectory: snapDirectoryArray )
060        {
061            final File[] readyDirectoryArray = FileX.listFilesNoNull( snapDirectory,
062              ReadyDirectory.READY_DIRECTORY_FILTER );
063            for( final File readyDirectory: readyDirectoryArray ) list.add( readyDirectory );
064        }
065     // Collections.sort( list ); // sorts on pathnames, with '/' messing up the order
066     //// no need, it's fine on Linux
067        return list;
068    }
069
070
071
072    private void run( final String[] argv )
073    {
074        final Map<String,Option> optionMap = compileBaseOptions();
075        {
076            final String name = "churn";
077            optionMap.put( name, new Option( name, Option.NO_ARGUMENT ));
078        }
079
080        final int aFirstNonOption = GetoptX.parse( "votrace", argv, optionMap );
081        if( optionMap.get("help").hasOccured() )
082        {
083            System.out.print
084            (
085              "Usage: votrace [--verbose] [--etc[=ready|mount|report|umount]] [--churn]\n" +
086              "   or  votrace snap [--verbose] [--etc[=ready|mount|report|umount]] [--churn]\n" +
087              "       votrace ready [--verbose] [--etc[=mount|report|umount]]\n" +
088              "          ~/votorola/out/votrace/snap-YYYY-MD-S\n" +
089              "       votrace mount [--verbose] [--etc[=report|umount]]\n" +
090              "          [~/votorola/out/votrace/snap-YYYY-MD-S/readyTrace-S]\n" +
091              "       votrace report [--verbose] [--etc]\n" +
092              "          [~/votorola/out/votrace/snap-YYYY-MD-S/readyTrace-S]\n" +
093              "       votrace umount [--verbose]\n" +
094              "          [~/votorola/out/votrace/snap-OLD-YYYY-MD-S/readyTrace-OLD-S]\n" +
095              "   or  votrace ureport [--verbose]\n" +
096              "   or  votrace status\n" +
097              "Trace a neigbourhood trust network and compile a residential register.\n"
098            );
099            return;
100        }
101
102        final int argCount = argv.length - aFirstNonOption;
103        final Action action;
104        final String argument;
105        if( argCount == 0 )
106        {
107            action = Action.snap;
108            argument = null;
109            final Option etc = optionMap.get( "etc" );
110            if( !etc.hasOccured() ) etc.addOccurence( Action.END_ACTION_LAST.name() );
111        }
112        else if( argCount > 2 )
113        {
114            System.err.println( "votrace: too many arguments" );
115            System.err.println( GetoptX.createHelpPrompt( "votrace" ));
116            System.exit( 1 ); return; // redundant return, to prevent compiler warnings
117        }
118        else
119        {
120            final String actionArg = argv[aFirstNonOption];
121            try
122            {
123                action = Action.valueOf( actionArg );
124            }
125            catch( IllegalArgumentException x )
126            {
127                System.err.println( "votrace: unrecognized action argument: " + actionArg );
128                System.err.println( GetoptX.createHelpPrompt( "votrace" ));
129                System.exit( 1 ); return; // redundant return, to prevent compiler warnings
130            }
131
132            if( argCount == 1 ) argument = null;
133            else
134            {
135                assert argCount == 2;
136                argument = argv[aFirstNonOption + 1];
137            }
138        }
139        run( action, argument, optionMap );
140    }
141
142
143
144    private void run( final Action action, String argument, final Map<String,Option> optionMap )
145    {
146        final boolean isEtc = action.isEtc( "votrace", optionMap );
147        final boolean isVerbose = optionMap.get("verbose").hasOccured();
148        try
149        {
150            final VoteServer.Run vsRun = vsRun();
151            final VoteServer vS = vsRun.voteServer();
152            final JavaScriptIncluder configScript = new JavaScriptIncluder( new File(
153              vsRun.trustserver().serviceDirectory(), "votrace.js" ));
154
155          // 1. Snap
156          // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
157            if( action == Action.snap )
158            {
159                if( argument != null )
160                {
161                    System.err.println( "votrace: too many arguments" );
162                    System.err.println( GetoptX.createHelpPrompt( "votrace" ));
163                    System.exit( 1 );
164                }
165
166              // timestamp santity check
167              // ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` `
168                final File votraceDirectory = new File( vS.outDirectory(), "votrace" );
169                final long msStart = System.currentTimeMillis();
170                for( final File d: FileX.listFilesNoNull( votraceDirectory,
171                  OutputStore.SNAP_DIRECTORY_FILTER ))
172                {
173                    if( d.lastModified() > msStart )
174                    {
175                        System.err.println(
176                          "votrace, warning: existing snap directory timestamped in future: " + d );
177                          // Problematic because the default for the 'ready' action
178                          // depends on timestamps.
179                    }
180                }
181
182              // churn
183              // ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` `
184              if( optionMap.get("churn").hasOccured() ) vS.pollwiki().cache().churn();
185
186              // snap
187              // ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` `
188                final File snapDirectory = OutputStore.mkdirsY4MDS( votraceDirectory, "snap" );
189                final File inRegFile = new File( snapDirectory, "_in_registration.xml" );
190
191                final Spool outSpool = new Spool1();
192                try
193                {
194                    final BufferedWriter w = new BufferedWriter(
195                      new OutputStreamWriter( new FileOutputStream( inRegFile ), "UTF-8" ));
196                    outSpool.add( new Hold()
197                    {
198                        public void release() { try{ w.close(); } catch( IOException x ) { throw new RuntimeException( x ); }}
199                    });
200                    w.append( "<?xml version='1.0' encoding='UTF-8'?> <!-- -*-coding: utf-8;-*- -->\n"
201                             + "<registrantInput>\n" );
202                    outSpool.add( new Hold()
203                    {
204                        public void release() { try{ w.append( "\t</registrantInput>\n"); } catch( IOException x ) { throw new RuntimeException( x ); }}
205                    });
206
207                    queryChain: for( String queryContinuation = "";; )
208                    {
209                        final Spool inSpool = new Spool1();
210                        try
211                        {
212                            final HttpURLConnection http;
213                            {
214                                // http://www.mediawiki.org/wiki/API:Query_-_Lists#categorymembers_.2F_cm
215                                final URI s = vS.pollwiki().scriptURI();
216                                final URI queryURI = new URI(
217                                  s.getScheme(), s.getAuthority(), s.getPath() + "/api.php",
218                                  /*query*/"action=query&list=categorymembers&cmlimit=500&cmnamespace=2&cmprop=title&cmtitle=Category:Registrant&format=xml"
219                                    + queryContinuation,
220                                  /*fragment*/null );
221                                logger.fine( "querying streetwiki " + queryURI );
222                                http = (HttpURLConnection)( queryURI.toURL().openConnection() );
223                            }
224                            URLConnectionX.connect( http );
225                            inSpool.add( new Hold()
226                            {
227                                public void release() { http.disconnect(); }
228                            });
229
230                            final InputStream in = http.getInputStream();
231                            inSpool.add( new Hold()
232                            {
233                                public void release() { try{ in.close(); } catch( IOException x ) { throw new RuntimeException( x ); }}
234                            });
235
236                            final XMLStreamReader r = MediaWiki.newXMLStreamReader( in, inSpool );
237                            queryContinuation = null;
238                            while( r.hasNext() )
239                            {
240                                r.next();
241                                if( r.isStartElement() && "cm".equals( r.getLocalName() ))
242                                {
243                                    final Registration reg;
244                                    {
245                                        final String ti = r.getAttributeValue( /*ns*/null, "title" );
246                                        final MatchResult m = MediaWiki.parsePageNameS( ti );
247                                        if( m == null )
248                                        {
249                                            throw new VotorolaRuntimeException(
250                                              "malformed page name '" + ti + "'" );
251                                        }
252
253                                        final String username = m.group( 2 );
254                                        try
255                                        {
256                                            reg = new Registration( IDPair.fromUsername( username ));
257                                        }
258                                        catch( final AddressException x )
259                                        {
260                                            logger.log( LoggerX.CONFIG, /*message*/"malformed username, ignoring registrant: " + username, x );
261                                            continue;
262                                        }
263                                    }
264
265                                    final Registration.SnapshotContext rSC =
266                                      new Registration.SnapshotContext( vS, reg );
267                                    configScript.invokeKnownFunction( "snapRegistration", rSC );
268                                    try
269                                    {
270                                        reg.toXML( new VoterInputTable.XMLColumnAppender( w ));
271                                    }
272                                    catch( VoterInputTable.BadInputException x )
273                                    {
274                                        logger.log( LoggerX.CONFIG, /*message*/"unable to serialize snapshot, ignoring registrant: " + reg.registrant().username(), x );
275                                    }
276                                }
277                                else if( r.isStartElement()
278                                  && "categorymembers".equals( r.getLocalName() ))
279                                {
280                                    final String cmcontinue = r.getAttributeValue(
281                                      /*ns*/null, "cmcontinue" );
282                                    if( cmcontinue != null ) // also serves to gaurd against clobbering.  Up to two elements are expected with this same name, only one of which has the sought for attribute.
283                                    {
284                                        queryContinuation = "&cmcontinue=" + cmcontinue;
285                                    }
286                                }
287                                else if( r.isStartElement() && "error".equals( r.getLocalName() ))
288                                {
289                                    throw new MediaWiki.APIError( r );
290                                }
291                            }
292                            if( queryContinuation == null ) break queryChain;
293                        }
294                        finally{ inSpool.unwind(); }
295                    }
296                }
297                finally{ outSpool.unwind(); }
298
299                if( !isEtc || isVerbose ) System.out.println( snapDirectory );
300            }
301
302          // 2. Ready
303          // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
304            else if( action == Action.ready )
305            {
306                final File votraceDirectory = new File( vS.outDirectory(), "votrace" );
307                if( argument == null )
308                {
309                    File snapDirectory = null; // till found
310                    for( final File d: FileX.listFilesNoNull( votraceDirectory,
311                      OutputStore.SNAP_DIRECTORY_FILTER ))
312                    {
313                        if( snapDirectory == null
314                         || d.lastModified() > snapDirectory.lastModified() ) snapDirectory = d;
315                    }
316
317                    if( snapDirectory == null )
318                    {
319                        System.err.println(
320                            "votrace: nothing to ready, no snap directory exists\n"
321                          + "    try: votrace snap" );
322                        System.exit( 1 );
323                    }
324
325                    argument = snapDirectory.getPath();
326                    if( isVerbose )
327                    {
328                        System.out.println( "readying most recently modified snap directory: "
329                          + argument );
330                    }
331                }
332
333              // timestamp santity check
334              // ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` `
335                final long msStart = System.currentTimeMillis();
336                for( final File r: nominalReadyDirectories( votraceDirectory ))
337                {
338                    if( r.lastModified() > msStart )
339                    {
340                        System.err.println(
341                          "votrace, warning: existing record timestamped in future: " + r );
342                          // Problematic because the default for the 'mount' action
343                          // depends on timestamps.
344                    }
345                }
346
347              // ready
348              // ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` `
349                final File snapDirectory = new File( argument );
350                final ReadyDirectory r = ReadyDirectory.createReadyDirectory( snapDirectory );
351                if( !isEtc || isVerbose ) System.out.println( r );
352            }
353
354          // 3. Mount
355          // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
356            else if( action == Action.mount )
357            {
358                final File votraceDirectory = new File( vS.outDirectory(), "votrace" );
359                final ArrayList<File> readyDirectoryList =
360                  nominalReadyDirectories( votraceDirectory );
361                if( argument == null )
362                {
363                    ReadyDirectory readyDirectory = null; // till found
364                    File nominalReadyDirectory = null;
365                    for( final File d: readyDirectoryList )
366                    {
367                        final ReadyDirectory r = new ReadyDirectory( d ); // as such
368                        if( readyDirectory == null || r.lastModified()
369                          > readyDirectory.lastModified() )
370                        {
371                            readyDirectory = r;
372                            nominalReadyDirectory = d;
373                        }
374                    }
375
376                    if( readyDirectory == null )
377                    {
378                        System.err.println(
379                            "votrace: nothing to mount, no snap/readyTrace record exists\n"
380                          + "    try: votrace ready" );
381                        System.exit( 1 );
382                    }
383
384                    if( readyDirectory.isMounted() )
385                    {
386                        System.err.println(
387                          "votrace: already mounted most recently modified record: "
388                            + nominalReadyDirectory );
389                        System.exit( 1 );
390                    }
391
392                    argument = nominalReadyDirectory.getPath();
393                    if( isVerbose )
394                    {
395                        System.out.println( "mounting most recently modified record: " + argument );
396                    }
397                }
398
399                final String nominalReadyPath = argument;
400                final ReadyDirectory readyDirectory = new ReadyDirectory( nominalReadyPath );
401                if( readyDirectory.isMounted() )
402                {
403                    System.err.println( "votrace: already mounted: " + nominalReadyPath );
404                    System.exit( 1 );
405                }
406
407              // timestamp santity check
408              // ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` `
409                final long msStart = System.currentTimeMillis();
410                for( final File d: readyDirectoryList )
411                {
412                    final ReadyDirectory r = new ReadyDirectory( d ); // as such
413                    if( !r.isMounted() ) continue;
414
415                    if( r.mountedDirectory().lastModified() > msStart )
416                    {
417                        System.err.println(
418                          "votrace, warning: existing mount timestamped in future: "
419                          + r.mountedDirectory() );
420                          // Problematic because the default for the 'report' action
421                          // depends on timestamps.
422                    }
423                }
424
425              // mount
426              // ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` `
427                readyDirectory.mount( vsRun.trustserver(), isVerbose );
428            }
429
430          // 4. Report
431          // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
432            else if( action == Action.report )
433            {
434                final NetworkTrace.VoteServerScope scopeTrace = vS.scopeTrace();
435                final File readyToReportLink = scopeTrace.readyToReportLink();
436                if( argument == null )
437                {
438                    final File votraceDirectory = new File( vS.outDirectory(), "votrace" );
439                    ReadyDirectory readyDirectory = null; // till found
440                    File nominalReadyDirectory = null;
441                    for( final File d: nominalReadyDirectories( votraceDirectory ))
442                    {
443                        final ReadyDirectory r = new ReadyDirectory( d ); // as such
444                        if( !r.isMounted() ) continue;
445
446                        if( readyDirectory == null || r.mountedDirectory().lastModified()
447                          > readyDirectory.mountedDirectory().lastModified() )
448                        {
449                            readyDirectory = r;
450                            nominalReadyDirectory = d;
451                        }
452                    }
453                    if( readyDirectory == null )
454                    {
455                        System.err.println( "votrace: nothing to report, no trace is mounted" );
456                        System.exit( 1 );
457                    }
458
459                    if( readyToReportLink.exists()
460                     && readyDirectory.equals( readyToReportLink.getCanonicalFile() ))
461                    {
462                        System.err.println(
463                          "votrace: already reporting most recently mounted record: "
464                          + nominalReadyDirectory );
465                        System.exit( 1 );
466                    }
467
468                    argument = nominalReadyDirectory.getPath();
469                    if( isVerbose )
470                    {
471                        System.out.println( "reporting most recently mounted record: " + argument );
472                    }
473                }
474                final String nominalReadyPath = argument;
475                final ReadyDirectory readyDirectory = new ReadyDirectory( nominalReadyPath );
476                if( readyToReportLink.exists()
477                 && readyDirectory.equals( readyToReportLink.getCanonicalFile() ))
478                {
479                    System.err.println( "votrace: already reported: " + nominalReadyPath );
480                    System.exit( 1 );
481                }
482
483                if( !readyDirectory.isMounted() )
484                {
485                    System.err.println( "votrace: cannot report, trace is not mounted\n"
486                                      + "    try: votrace mount " + nominalReadyPath );
487                    System.exit( 1 );
488                }
489
490                FileX.symlink( readyDirectory.getPath(), readyToReportLink.getPath() );
491                FileX.symlink( readyDirectory.snapDirectory().getName(),
492                  scopeTrace.snapToReportLink().getPath() );
493            }
494
495          // 5. Umount
496          // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
497            else if( action == Action.umount )
498            {
499                final File readyToReportLink = vS.scopeTrace().readyToReportLink();
500                if( argument == null ) // unmount all
501                {
502                    final File votraceDirectory = new File( vS.outDirectory(), "votrace" );
503                    for( final File nominalReadyDirectory:
504                      nominalReadyDirectories( votraceDirectory ))
505                    {
506                        final ReadyDirectory r = new ReadyDirectory( nominalReadyDirectory ); // as such
507                        if( !r.isMounted() ) continue;
508
509                        argument = nominalReadyDirectory.getPath();
510                        if( r.equals( readyToReportLink.getCanonicalFile() ))
511                        {
512                            if( isVerbose )
513                            {
514                                System.out.println( "skipping unmount of reported trace "
515                                  + argument );
516                            }
517                            continue;
518                        }
519
520                        if( isVerbose ) System.out.println( "unmounting " + argument );
521                        run( action, argument, optionMap );
522                    }
523                    return;
524                }
525                else // unmount one
526                {
527                    final String nominalReadyPath = argument;
528                    final ReadyDirectory readyDirectory = new ReadyDirectory( nominalReadyPath );
529                    if( readyToReportLink.exists()
530                     && readyDirectory.equals( readyToReportLink.getCanonicalFile() ))
531                    {
532                        System.err.println( "votrace: cannot unmount, trace is currently reported\n"
533                                          + "   try: votrace ureport" );
534                        System.exit( 1 );
535                    }
536
537                    if( !readyDirectory.unmount( vsRun ))
538                    {
539                        System.err.println( "votrace: not mounted: " + nominalReadyPath );
540                        System.exit( 1 );
541                    }
542                }
543
544            }
545
546          // *. Ureport
547          // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
548            else if( action == Action.ureport )
549            {
550                if( argument != null )
551                {
552                    System.err.println( "votrace: too many arguments" );
553                    System.err.println( GetoptX.createHelpPrompt( "votrace" ));
554                    System.exit( 1 );
555                }
556
557                final NetworkTrace.VoteServerScope scopeTrace = vS.scopeTrace();
558                final File readyToReportLink = scopeTrace.readyToReportLink();
559                if( !readyToReportLink.exists() )
560                {
561                    System.err.println( "votrace: nothing to unreport" );
562                    System.exit( 1 );
563                }
564
565                if( isVerbose )
566                {
567                    System.out.println( "unreporting " + readyToReportLink.getCanonicalFile() ); // difficult to recover the nominal path in this case, because getCanonicalFile() is the only easy way to de-reference the link
568                }
569                if( !readyToReportLink.delete() )
570                {
571                    System.err.println(
572                      "votrace: unable to delete report link, please delete it manually: "
573                      + readyToReportLink );
574                }
575                final File snapToReportLink = scopeTrace.snapToReportLink();
576                if( !snapToReportLink.delete() )
577                {
578                    System.err.println(
579                      "votrace: unable to delete snap link, please delete it manually: "
580                      + snapToReportLink );
581                }
582            }
583
584          // *. Status
585          // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
586            else if( action == Action.status )
587            {
588                if( argument != null )
589                {
590                    System.err.println( "votrace: too many arguments" );
591                    System.err.println( GetoptX.createHelpPrompt( "votrace" ));
592                    System.exit( 1 );
593                }
594
595                final File votraceDirectory = new File( vS.outDirectory(), "votrace" );
596                final File readyDirectoryToReportOrNull;
597                {
598                    final File readyToReportLink = vS.scopeTrace().readyToReportLink();
599                    readyDirectoryToReportOrNull = readyToReportLink.exists()?
600                        readyToReportLink.getCanonicalFile(): null;
601                }
602                final File[] snapDirectoryArray = FileX.listFilesNoNull( votraceDirectory,
603                  OutputStore.SNAP_DIRECTORY_FILTER );
604                Arrays.sort( snapDirectoryArray );
605                final StringBuilder flagB = new StringBuilder( ReadyDirectory.STATUS_FLAGS_COUNT );
606                for( final File snapDirectory: snapDirectoryArray )
607                {
608                    final File[] readyDirectoryArray = FileX.listFilesNoNull( snapDirectory,
609                      ReadyDirectory.READY_DIRECTORY_FILTER );
610                    if( readyDirectoryArray.length == 0 ) // unreadied snap
611                    {
612                        ReadyDirectory.statusPrintln( StringBuilderX.clear(flagB),
613                          snapDirectory );
614                        continue;
615                    }
616
617                    Arrays.sort( readyDirectoryArray );
618                    for( final File nominalReadyDirectory: readyDirectoryArray )
619                    {
620                        final ReadyDirectory readyDirectory =
621                          new ReadyDirectory( nominalReadyDirectory ); // as such
622                        readyDirectory.statusPrintln( StringBuilderX.clear(flagB),
623                          nominalReadyDirectory, readyDirectoryToReportOrNull );
624                    }
625                }
626            }
627
628          // - - -
629            else assert false;
630            if( isEtc )
631            {
632                run( /*next*/Action.values()[action.ordinal() + 1], /*argument*/null, optionMap );
633            }
634        }
635        catch( RuntimeException x ) { throw x; }
636        catch( Exception x )
637        {
638            System.err.print( "votrace: fatal error" + Votorola.unmessagedDetails(x) + ": " );
639         // System.err.println( x );
640            x.printStackTrace( System.err );
641            System.exit( 1 );
642        }
643    }
644
645
646}