001package Torello.Java;
002
003import Torello.Java.Additional.TriAppendable;
004
005import java.io.IOException;
006import java.io.File;
007import java.util.Map;
008import java.util.function.Consumer;
009
010/**
011 * Root Ancestor / Parent Class for all Operating-System Execution Classes - including:
012 * {@link MSDOS}, {@link Shell}, {@link GSUTIL} etc...
013 * 
014 * <EMBED CLASS='external-html' DATA-FILE-ID=OSCOMMANDS>
015 * <EMBED CLASS='external-html' DATA-FILE-ID=OSC_WILD_CARD_01>
016 * <EMBED CLASS='external-html' DATA-FILE-ID=OSC_WORA>
017 */
018public abstract class OSCommands implements Cloneable
019{
020    /**
021     * <EMBED CLASS='external-html' DATA-FILE-ID=OSC_APPENDABLE_FIELD>
022     * <EMBED CLASS='external-html' DATA-FILE-ID=APPENDABLE>
023     */
024    public Appendable outputAppendable = System.out;
025
026    /** <EMBED CLASS='external-html' DATA-FILE-ID=OSC_CSA_FIELD> */
027    public Appendable commandStrAppendable = null;
028
029    /** <EMBED CLASS='external-html' DATA-FILE-ID=OSC_STND_OUT_FIELD> */
030    public Appendable standardOutput = null;
031
032    /** <EMBED CLASS='external-html' DATA-FILE-ID=OSC_ERROR_OUT_FIELD> */
033    public Appendable errorOutput = null;
034
035    /** Allows for redirects and other features */
036    public OSExtras osExtras = null;
037
038
039    // ********************************************************************************************
040    // ********************************************************************************************
041    // Multi-Threaded Helper
042    // ********************************************************************************************
043    // ********************************************************************************************
044
045
046    private class ThreadSafeSnapshot
047    {
048        final Appendable outputAppendable, commandStrAppendable, standardOuput, errorOutput;
049        final OSExtras osExtras;
050
051        ThreadSafeSnapshot()
052        {
053            this.outputAppendable       = OSCommands.this.outputAppendable;
054            this.commandStrAppendable   = OSCommands.this.commandStrAppendable;
055            this.standardOuput          = OSCommands.this.standardOutput;
056            this.errorOutput            = OSCommands.this.errorOutput;
057
058            final OSExtras x = OSCommands.this.osExtras;
059
060            // This part makes sure not to call "clone()" on a null-valued "OSExtras" - to avoid NPE
061            this.osExtras = (x == null)
062                ? null
063                : (OSExtras) x.clone();
064        }
065    }
066
067
068    // ********************************************************************************************
069    // ********************************************************************************************
070    // Constructors
071    // ********************************************************************************************
072    // ********************************************************************************************
073
074
075    /** <EMBED CLASS='external-html' DATA-FILE-ID=OSC_CTOR_1> */
076    public OSCommands() { }
077
078    /**
079     * <EMBED CLASS='external-html' DATA-FILE-ID=OSC_CTOR_2>
080     * @param outputAppendable <EMBED CLASS='external-html' DATA-FILE-ID=OSC_APPENDABLE_PARAM>
081     * @see #outputAppendable
082     */
083    public OSCommands(Appendable outputAppendable)
084    { this.outputAppendable = outputAppendable; }
085
086    /**
087     * <EMBED CLASS='external-html' DATA-FILE-ID=OSC_CTOR_3>
088     * 
089     * @param outputAppendable      <EMBED CLASS='external-html' DATA-FILE-ID=OSC_APPENDABLE_PARAM>
090     * @param commandStrAppendable  <EMBED CLASS='external-html' DATA-FILE-ID=OSC_CSA_PARAM>
091     * @param standardOutput        <EMBED CLASS='external-html' DATA-FILE-ID=OSC_STND_OUT_PARAM>
092     * @param errorOutput           <EMBED CLASS='external-html' DATA-FILE-ID=OSC_ERROR_OUT_PARAM>
093     * 
094     * @see #outputAppendable
095     * @see #commandStrAppendable
096     * @see #standardOutput 
097     * @see #errorOutput 
098     */
099    public OSCommands(
100            Appendable outputAppendable,
101            Appendable commandStrAppendable,
102            Appendable standardOutput,
103            Appendable errorOutput
104        )
105    {
106        this.outputAppendable       = outputAppendable;
107        this.commandStrAppendable   = commandStrAppendable;
108        this.standardOutput         = standardOutput;
109        this.errorOutput            = errorOutput;
110    }
111
112    /**
113     * <EMBED CLASS='external-html' DATA-FILE-ID=OSC_CTOR_4>
114     * @param standardOutput    <EMBED CLASS='external-html' DATA-FILE-ID=OSC_STND_OUT_PARAM>
115     * @param errorOutput       <EMBED CLASS='external-html' DATA-FILE-ID=OSC_ERROR_OUT_PARAM>
116     */
117    public OSCommands(Appendable standardOutput, Appendable errorOutput)
118    {
119        this.outputAppendable   = null;
120        this.standardOutput     = standardOutput;
121        this.errorOutput        = errorOutput;
122    }
123
124    /**
125     * Clone-Constructor.  This is used by sub-classes to allow for a {@code clone()} method.
126     * 
127     * @param other This is an instance that is passed by a {@code 'clone'} method.  It is always
128     * just {@code 'this'}.
129     */
130    protected OSCommands(OSCommands other)
131    {
132        this.outputAppendable       = other.outputAppendable;
133        this.commandStrAppendable   = other.commandStrAppendable;
134        this.standardOutput         = other.standardOutput;
135        this.errorOutput            = other.errorOutput;
136        this.osExtras               = other.osExtras;
137    }
138
139
140    // ********************************************************************************************
141    // ********************************************************************************************
142    // Main Execution Methods
143    // ********************************************************************************************
144    // ********************************************************************************************
145
146
147    /**
148     * Executes a command by spawning an operating-system process.
149     * 
150     * <BR /><BR /><B CLASS=JDDescLabel>Sub-Class Note:</B>
151     * 
152     * <BR />If you are intending to write an extension of this class for executing Operating
153     * System Commands, this method here should be used as the launch-pad for invoking those
154     * commands.
155     * 
156     * <BR /><BR />Please review the classes {@link Shell} or {@link MSDOS} to see how to employ
157     * a Shell-Script Class to extend this class ({@code 'OSCommands'}), and specifically how to
158     * invoke this method to run those scripts.
159     * 
160     * @param command A {@code String[]}-Array Shell / UNIX / MSDOS Command.  This parameter (and
161     * this method) are usually created/invoked by the classes {@link Shell}, {@link GSUTIL}, 
162     * {@link MSDOS} etc...
163     * 
164     * @return <EMBED CLASS='external-html' DATA-FILE-ID=OSRET>
165     */
166    public OSResponse printAndRun(final String[] command) throws IOException
167    {
168        final ThreadSafeSnapshot    config = new ThreadSafeSnapshot();
169        final boolean               hasOSE = (config.osExtras != null);
170
171        // This line is here because this field is intentionally cleared after each and every use
172        // the method "printAndRun".  The actual value/instance that was stored into this field
173        // has already been retrieved and saved into the "ThreadSafeSnapshot" field.
174
175        this.osExtras = null;
176
177
178        // *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** ***
179        // Do as much error / exception checking as is possible... (It isn't much)
180        // *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** ***
181
182        for (int i=0; i < command.length; i++)
183
184            if (command[i] == null) throw new NullPointerException(
185                "The " + i + StringParse.ordinalIndicator(i) + " array element in the 'command' " +
186                "(O/S Command) array-parameter that was passed to this method was 'null'.  " +
187                "Java's method Runtime.getRuntime().exec(command) will not accept a 'command' " +
188                "array that contains any nulls."
189            );
190
191        if (hasOSE && config.osExtras.currentWorkingDirectory != null)
192
193            if (! config.osExtras.currentWorkingDirectory.isDirectory())
194
195                throw new IllegalArgumentException(
196                    "The file-system has stated that the reference passed to parameter " +
197                    "'currentWorkingDirectory' was not a valid directory on the file-system: " +
198                    '[' + config.osExtras.currentWorkingDirectory.getPath() + ']'
199                );
200
201        // This check actually executes if-and-when the user has provided a non-null instance
202        // of "OSExtras" to his "OSCommands" configuration.  The user does this assignment by
203        // simply calling 'varName.osExtras = myOSExtrasConfigurationsInstance'
204        // 
205        // Yes, I used an extremely long variable name above.  Sorry, deal with it.
206        // 
207        // This "Error-Check" is herely solely to prevent the user from accidentally trying
208        // to "Redirect Process Input" from a File-System File (via 'ose.inputRedirect'), and
209        // simultaneously from a "Memory-Pipe" - using the class "OSJavaPipe".
210        // 
211        // The user has the option of:
212        //  1) File-System File Input-Redirect, via his ose-instance' "inputRedirect" Field
213        //  2) Java-Memory (direct) Redirect, via his ose-instance' "javaPipe" field
214        //  1) No Input-Redirect at all (the most common) - by leaving both of these null
215
216        if (hasOSE)
217
218            if ((config.osExtras.javaPipe != null) && (config.osExtras.inputRedirect != null))
219
220                throw new RuntimeException(
221                    "You cannot provide a non-nul value to both OSExtras.inputRedirect and to " +
222                    "OSExtras.javaPipe.  These are optional configurations, and if you wish to " +
223                    "use some variant of Input-Redirection, you must choose between one or the " +
224                    "other."
225                );
226
227
228        // *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** ***
229        // Run the "Command String Appendable" output-logger thingy
230        // *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** ***
231
232        final StringBuilder sb = new StringBuilder();
233        for (int i=0; i < command.length; i++) sb.append(command[i] + " ");
234        sb.append('\n');
235
236        // This is needed again at the end of this method
237        final String commandStr = sb.toString();
238
239        if (config.commandStrAppendable != null) config.commandStrAppendable.append(commandStr);
240
241
242        // *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** ***
243        // Now execute the command!
244        // *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** ***
245
246        // Used to build the String's for this class standardOutput and errorOutput fields!
247        final StringBuilder standardOutputSB    = new StringBuilder();
248        final StringBuilder errorOutputSB       = new StringBuilder();
249
250        try
251        {
252            // *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** ***
253            // First, build a java.lang.Process object-instance
254            // *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** ***
255
256            // This is the java.lang.Process class instance.  It is built with a simple constructor
257            // unless the user has passed all of the OSExtras stuff.  If 'hasOSE' is true, then a
258            // java.lang.ProcessBuilder object is needed to actually build the Process
259            //
260            // The end of this (kind of long) if-then-else statement has a simply assignment
261            // ==> pro = Runtime.getRuntime().exec(command);
262
263            final Process pro;
264
265            if (hasOSE)
266            {
267                // Save some typing, that's all!  Use a 1-character variable named 'x' !!
268                final OSExtras x = config.osExtras;
269
270                // This will create a process with specified modifications
271                ProcessBuilder b = new ProcessBuilder(command);
272
273
274                // NOTE: These "extra" configurations aren't that useful - but they are provided as
275                //       an option in "Java-HTML".  The redirects are set here!  All they do is ask
276                //       the OS Process to do that "Operating System Thing" where input and/or
277                //       output are retrieved/sent to a file
278                //
279                // August 2024 NOTE: I have uncovered, yet another, "Feature".  Of interest is that
280                //                   the Java-Class "ProcessBuilder.Redirect" only works for
281                //                   Redirecting Processes I/O to-and-from File-System Files!
282                // 
283                // The new feature "OSJavaPipe" allows for redirecting input from anything in
284                // Java's Memory without a lot of work.  Note that one issue that must be checked
285                // is that the user CANNOT both use the 'inputRedirect' (from a file) AND the
286                // OSJavaPipe instance, simultaneously, at the same time.
287                // 
288                // If they try it, there is an exception throw inside the Static-Inner Class named
289                // "ThreadSafeSnapshot"
290    
291                if (x.currentWorkingDirectory != null)  b.directory(x.currentWorkingDirectory);
292                if (x.mergeStdErrorAndStdOut)           b.redirectErrorStream(true);
293                if (x.inputRedirect != null)            b.redirectInput(x.inputRedirect);
294                if (x.outputRedirect != null)           b.redirectOutput(x.outputRedirect);
295                if (x.errorRedirect != null)            b.redirectError(x.errorRedirect);
296
297                if (x.environmentVariableUpdater != null)
298                    x.environmentVariableUpdater.accept(b.environment());
299
300                pro = b.start();
301
302                // NOTE: The user cannot set BOTH the 'OSJavaPipe' Field of OSExtras, and the
303                //       'inputRedirect' Field - at the same timee, in the same OSExtras-instance.
304                //       This problem was, however, already checked (way above), in the
305                //       ThreadSafeSnapshot constructor.
306                // 
307                // The following line, theoretically, writes all of the users input data directly
308                // to the OS-Process.  Also note (at least according to "ChatGPT" - and THIS
309                // HAS NOT BEEN VERIFIED) that the process, in the code below, has already been
310                // started when we start doing the actual "write" to the Process!
311                // 
312                // ChatGPT told me, and I quote, "Don't worry about it. I asked the bartender and
313                // he said it was OK."  It's pretty fun using that thing, by the way...  It
314                // mentioned that Operating-System Commands which are expecting Piped User-Input
315                // will "hang" (idle), until that User-Input has been received.
316                //
317                // So... though it seems as if (at least to me) the line at the bottom should come
318                // before the line (above):
319                // 
320                // `pro = b.start`.
321                // 
322                // Alas, there is no way to invoke the command `pro.getOutputStream`, until
323                // `Process pro;` variable has been created!
324
325                if (x.javaPipe != null) x.javaPipe.writeToPipe(pro.getOutputStream());
326            }
327
328            // Otherwise just call this version - if there are no "OSExtras"
329            else pro = Runtime.getRuntime().exec(command);
330
331
332            // *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** ***
333            // Now just collect output with the (Java-HTML internal) "ISPT" Printer-Reader Threads
334            // *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** ***
335            //
336            // The next 3 lines create a "Completion Monitor" instance, and then register the
337            // "Reader Threads" for reading from Standard-Out and Standard-Error from the Process.
338            // These create two "Daemon Threads" that read process-output and error-output using
339            // Java Thread's.  These will not hang **UNLESS** one of the InputStream read method's
340            // hang/lock.
341
342            Completed completed = new Completed();
343
344            // Note that the constructor's below start the printer/reader/monitors - there is no
345            // need to actually keep a reference/pointer to these classes once they are
346            // constructed.  Passing the 'completed' instance to these constructors is enough!
347            //
348            // ALSO: It is completely immaterial whether/if any of the three appendable's passed to
349            //       the TriAppendable-Constructor are actually null.  Note that only the one whose
350            //       name ends with "SB" is guaranteed not to be null.  The other two
351            //       (user-provided) Appendable's can easily be null!
352
353            new ISPT(
354                pro.getInputStream(),
355                new TriAppendable(
356                    config.outputAppendable,    // User-Provided, May be null!
357                    standardOutputSB,           // OSResponse uses this to collect standardOutput
358                    config.standardOuput        // User-Provided too, may be null.
359                ),
360                completed,
361                "Thread for Reading from Standard Output"
362            );
363
364            new ISPT(
365                pro.getErrorStream(),
366                new TriAppendable(
367                    config.outputAppendable,    // User-Provided, might be null
368                    errorOutputSB,              // OSResonse uses this to collect errorOutput 
369                    config.errorOutput          // User-Provided
370                ),
371                completed,
372                "Thread for Reading from Error Output"
373            );
374
375            // NOTE: The process, once it is instantiated, is already running.  There is no
376            // need to "start" the process, the call to Runtime.exec (above does that).  Here
377            // we can just wait for the reader threads to receive the EOF messages, which  is
378            // how they terminate.
379
380            completed.waitForCompletionOfAllThreads();
381
382            // It is unlikely this would cause the current thread to wait, because the previous
383            // line will wait until both Standard-Out and Error-Out have received EOF...
384            // Perhaps the process COULD delay the return response-code here.  The reality is that
385            // method 'waitFor' is the PREFERRED way to retrieve the exit-value from this process...
386            // although method 'Process.exitValue()' would also probably work.
387
388            int response = pro.waitFor();
389
390
391            // *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** ***
392            // Return the 'OSResponse' result (or throw an exception, if the process threw one)
393            // *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** ***
394
395            // This prints a friendly little message at the end stating what the response-code was
396            if (config.outputAppendable != null) config.outputAppendable.append
397                ("Command exit with return value " + response + '\n');
398
399            completed.ifExceptionThrowException();
400
401            // Note that a '0' response-code usually means 'succesfully terminated'
402            return new OSResponse
403                (commandStr, response, standardOutputSB.toString(), errorOutputSB.toString());
404        }
405
406        catch (InterruptedException e)
407        {
408            return new OSResponse(
409                commandStr, OSResponse.INTERRUPTED, standardOutputSB.toString(),
410                errorOutputSB.toString()
411            );
412        }
413    }
414
415
416    // ********************************************************************************************
417    // ********************************************************************************************
418    // toString & Clone Methods
419    // ********************************************************************************************
420    // ********************************************************************************************
421
422
423    private static String toStrApp(Appendable a)
424    { return (a == null) ? "null\n" : (a.getClass().getName() + '\n'); }
425
426    /**
427     * Generates a {@code String} this class.  The returned {@code String} merely encodes the
428     * class-names of the non-null {@code Appendable's}.
429     *     
430     * @return A simple representation of this class, as a {@code java.lang.String}
431     */
432    public String toString()
433    {
434        // private static String toStrApp(Appendable a)
435        // { return (a == null) ? "null\n" : (a.getClass().getName() + '\n'); }
436
437        return
438            "outputAppendable:     " + toStrApp(this.outputAppendable) +
439            "commandStrAppendable: " + toStrApp(this.commandStrAppendable) +
440            "standardOutput:       " + toStrApp(this.standardOutput) +
441            "errorOutput:          " + toStrApp(this.errorOutput);
442    }
443
444    /**
445     * Creates a clone of {@code 'this'} instance.  Though unlikely of much use, this could
446     * conceivably have some function if similar, but non-identical, output mechanisms were being
447     * used.
448     * 
449     * @return An exact copy of {@code 'this'} instance - one in which all output
450     * {@code Appendable's} have had their references copied into the new instance.
451     */
452    public abstract OSCommands clone();
453}