1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
package Torello.Java;

import Torello.Java.Additional.TriAppendable;

import java.io.IOException;
import java.io.File;
import java.util.Map;
import java.util.function.Consumer;

/**
 * Root Ancestor / Parent Class for all Operating-System Execution Classes - including:
 * {@link MSDOS}, {@link Shell}, {@link GSUTIL} etc...
 * 
 * <EMBED CLASS='external-html' DATA-FILE-ID=OSCOMMANDS>
 * <EMBED CLASS='external-html' DATA-FILE-ID=OSC_WILD_CARD_01>
 * <EMBED CLASS='external-html' DATA-FILE-ID=OSC_WORA>
 */
public abstract class OSCommands implements Cloneable
{
    /**
     * <EMBED CLASS='external-html' DATA-FILE-ID=OSC_APPENDABLE_FIELD>
     * <EMBED CLASS='external-html' DATA-FILE-ID=APPENDABLE>
     */
    public Appendable outputAppendable = System.out;

    /** <EMBED CLASS='external-html' DATA-FILE-ID=OSC_CSA_FIELD> */
    public Appendable commandStrAppendable = null;

    /** <EMBED CLASS='external-html' DATA-FILE-ID=OSC_STND_OUT_FIELD> */
    public Appendable standardOutput = null;

    /** <EMBED CLASS='external-html' DATA-FILE-ID=OSC_ERROR_OUT_FIELD> */
    public Appendable errorOutput = null;

    /** Allows for redirects and other features */
    public OSExtras osExtras = null;


    // ********************************************************************************************
    // ********************************************************************************************
    // Multi-Threaded Helper
    // ********************************************************************************************
    // ********************************************************************************************


    private class ThreadSafeSnapshot
    {
        final Appendable outputAppendable, commandStrAppendable, standardOuput, errorOutput;
        final OSExtras osExtras;

        ThreadSafeSnapshot()
        {
            this.outputAppendable       = OSCommands.this.outputAppendable;
            this.commandStrAppendable   = OSCommands.this.commandStrAppendable;
            this.standardOuput          = OSCommands.this.standardOutput;
            this.errorOutput            = OSCommands.this.errorOutput;

            final OSExtras x = OSCommands.this.osExtras;

            // This part makes sure not to call "clone()" on a null-valued "OSExtras" - to avoid NPE
            this.osExtras = (x == null)
                ? null
                : (OSExtras) x.clone();
        }
    }


    // ********************************************************************************************
    // ********************************************************************************************
    // Constructors
    // ********************************************************************************************
    // ********************************************************************************************


    /** <EMBED CLASS='external-html' DATA-FILE-ID=OSC_CTOR_1> */
    public OSCommands() { }

    /**
     * <EMBED CLASS='external-html' DATA-FILE-ID=OSC_CTOR_2>
     * @param outputAppendable <EMBED CLASS='external-html' DATA-FILE-ID=OSC_APPENDABLE_PARAM>
     * @see #outputAppendable
     */
    public OSCommands(Appendable outputAppendable)
    { this.outputAppendable = outputAppendable; }

    /**
     * <EMBED CLASS='external-html' DATA-FILE-ID=OSC_CTOR_3>
     * 
     * @param outputAppendable      <EMBED CLASS='external-html' DATA-FILE-ID=OSC_APPENDABLE_PARAM>
     * @param commandStrAppendable  <EMBED CLASS='external-html' DATA-FILE-ID=OSC_CSA_PARAM>
     * @param standardOutput        <EMBED CLASS='external-html' DATA-FILE-ID=OSC_STND_OUT_PARAM>
     * @param errorOutput           <EMBED CLASS='external-html' DATA-FILE-ID=OSC_ERROR_OUT_PARAM>
     * 
     * @see #outputAppendable
     * @see #commandStrAppendable
     * @see #standardOutput 
     * @see #errorOutput 
     */
    public OSCommands(
            Appendable outputAppendable,
            Appendable commandStrAppendable,
            Appendable standardOutput,
            Appendable errorOutput
        )
    {
        this.outputAppendable       = outputAppendable;
        this.commandStrAppendable   = commandStrAppendable;
        this.standardOutput         = standardOutput;
        this.errorOutput            = errorOutput;
    }

    /**
     * <EMBED CLASS='external-html' DATA-FILE-ID=OSC_CTOR_4>
     * @param standardOutput    <EMBED CLASS='external-html' DATA-FILE-ID=OSC_STND_OUT_PARAM>
     * @param errorOutput       <EMBED CLASS='external-html' DATA-FILE-ID=OSC_ERROR_OUT_PARAM>
     */
    public OSCommands(Appendable standardOutput, Appendable errorOutput)
    {
        this.outputAppendable   = null;
        this.standardOutput     = standardOutput;
        this.errorOutput        = errorOutput;
    }

    /**
     * Clone-Constructor.  This is used by sub-classes to allow for a {@code clone()} method.
     * 
     * @param other This is an instance that is passed by a {@code 'clone'} method.  It is always
     * just {@code 'this'}.
     */
    protected OSCommands(OSCommands other)
    {
        this.outputAppendable       = other.outputAppendable;
        this.commandStrAppendable   = other.commandStrAppendable;
        this.standardOutput         = other.standardOutput;
        this.errorOutput            = other.errorOutput;
        this.osExtras               = other.osExtras;
    }


    // ********************************************************************************************
    // ********************************************************************************************
    // Main Execution Methods
    // ********************************************************************************************
    // ********************************************************************************************


    /**
     * Executes a command by spawning an operating-system process.
     * 
     * <BR /><BR /><B CLASS=JDDescLabel>Sub-Class Note:</B>
     * 
     * <BR />If you are intending to write an extension of this class for executing Operating
     * System Commands, this method here should be used as the launch-pad for invoking those
     * commands.
     * 
     * <BR /><BR />Please review the classes {@link Shell} or {@link MSDOS} to see how to employ
     * a Shell-Script Class to extend this class ({@code 'OSCommands'}), and specifically how to
     * invoke this method to run those scripts.
     * 
     * @param command A {@code String[]}-Array Shell / UNIX / MSDOS Command.  This parameter (and
     * this method) are usually created/invoked by the classes {@link Shell}, {@link GSUTIL}, 
     * {@link MSDOS} etc...
     * 
     * @return <EMBED CLASS='external-html' DATA-FILE-ID=OSRET>
     */
    public OSResponse printAndRun(final String[] command) throws IOException
    {
        final ThreadSafeSnapshot    config = new ThreadSafeSnapshot();
        final boolean               hasOSE = (config.osExtras != null);

        // This line is here because this field is intentionally cleared after each and every use
        // the method "printAndRun".  The actual value/instance that was stored into this field
        // has already been retrieved and saved into the "ThreadSafeSnapshot" field.

        this.osExtras = null;


        // *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** ***
        // Do as much error / exception checking as is possible... (It isn't much)
        // *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** ***

        for (int i=0; i < command.length; i++)

            if (command[i] == null) throw new NullPointerException(
                "The " + i + StringParse.ordinalIndicator(i) + " array element in the 'command' " +
                "(O/S Command) array-parameter that was passed to this method was 'null'.  " +
                "Java's method Runtime.getRuntime().exec(command) will not accept a 'command' " +
                "array that contains any nulls."
            );

        if (hasOSE && config.osExtras.currentWorkingDirectory != null)

            if (! config.osExtras.currentWorkingDirectory.isDirectory())

                throw new IllegalArgumentException(
                    "The file-system has stated that the reference passed to parameter " +
                    "'currentWorkingDirectory' was not a valid directory on the file-system: " +
                    '[' + config.osExtras.currentWorkingDirectory.getPath() + ']'
                );

        // This check actually executes if-and-when the user has provided a non-null instance
        // of "OSExtras" to his "OSCommands" configuration.  The user does this assignment by
        // simply calling 'varName.osExtras = myOSExtrasConfigurationsInstance'
        // 
        // Yes, I used an extremely long variable name above.  Sorry, deal with it.
        // 
        // This "Error-Check" is herely solely to prevent the user from accidentally trying
        // to "Redirect Process Input" from a File-System File (via 'ose.inputRedirect'), and
        // simultaneously from a "Memory-Pipe" - using the class "OSJavaPipe".
        // 
        // The user has the option of:
        //  1) File-System File Input-Redirect, via his ose-instance' "inputRedirect" Field
        //  2) Java-Memory (direct) Redirect, via his ose-instance' "javaPipe" field
        //  1) No Input-Redirect at all (the most common) - by leaving both of these null

        if (hasOSE)

            if ((config.osExtras.javaPipe != null) && (config.osExtras.inputRedirect != null))

                throw new RuntimeException(
                    "You cannot provide a non-nul value to both OSExtras.inputRedirect and to " +
                    "OSExtras.javaPipe.  These are optional configurations, and if you wish to " +
                    "use some variant of Input-Redirection, you must choose between one or the " +
                    "other."
                );


        // *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** ***
        // Run the "Command String Appendable" output-logger thingy
        // *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** ***

        final StringBuilder sb = new StringBuilder();
        for (int i=0; i < command.length; i++) sb.append(command[i] + " ");
        sb.append('\n');

        // This is needed again at the end of this method
        final String commandStr = sb.toString();

        if (config.commandStrAppendable != null) config.commandStrAppendable.append(commandStr);


        // *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** ***
        // Now execute the command!
        // *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** ***

        // Used to build the String's for this class standardOutput and errorOutput fields!
        final StringBuilder standardOutputSB    = new StringBuilder();
        final StringBuilder errorOutputSB       = new StringBuilder();

        try
        {
            // *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** ***
            // First, build a java.lang.Process object-instance
            // *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** ***

            // This is the java.lang.Process class instance.  It is built with a simple constructor
            // unless the user has passed all of the OSExtras stuff.  If 'hasOSE' is true, then a
            // java.lang.ProcessBuilder object is needed to actually build the Process
            //
            // The end of this (kind of long) if-then-else statement has a simply assignment
            // ==> pro = Runtime.getRuntime().exec(command);

            final Process pro;

            if (hasOSE)
            {
                // Save some typing, that's all!  Use a 1-character variable named 'x' !!
                final OSExtras x = config.osExtras;

                // This will create a process with specified modifications
                ProcessBuilder b = new ProcessBuilder(command);


                // NOTE: These "extra" configurations aren't that useful - but they are provided as
                //       an option in "Java-HTML".  The redirects are set here!  All they do is ask
                //       the OS Process to do that "Operating System Thing" where input and/or
                //       output are retrieved/sent to a file
                //
                // August 2024 NOTE: I have uncovered, yet another, "Feature".  Of interest is that
                //                   the Java-Class "ProcessBuilder.Redirect" only works for
                //                   Redirecting Processes I/O to-and-from File-System Files!
                // 
                // The new feature "OSJavaPipe" allows for redirecting input from anything in
                // Java's Memory without a lot of work.  Note that one issue that must be checked
                // is that the user CANNOT both use the 'inputRedirect' (from a file) AND the
                // OSJavaPipe instance, simultaneously, at the same time.
                // 
                // If they try it, there is an exception throw inside the Static-Inner Class named
                // "ThreadSafeSnapshot"
    
                if (x.currentWorkingDirectory != null)  b.directory(x.currentWorkingDirectory);
                if (x.mergeStdErrorAndStdOut)           b.redirectErrorStream(true);
                if (x.inputRedirect != null)            b.redirectInput(x.inputRedirect);
                if (x.outputRedirect != null)           b.redirectOutput(x.outputRedirect);
                if (x.errorRedirect != null)            b.redirectError(x.errorRedirect);

                if (x.environmentVariableUpdater != null)
                    x.environmentVariableUpdater.accept(b.environment());

                pro = b.start();

                // NOTE: The user cannot set BOTH the 'OSJavaPipe' Field of OSExtras, and the
                //       'inputRedirect' Field - at the same timee, in the same OSExtras-instance.
                //       This problem was, however, already checked (way above), in the
                //       ThreadSafeSnapshot constructor.
                // 
                // The following line, theoretically, writes all of the users input data directly
                // to the OS-Process.  Also note (at least according to "ChatGPT" - and THIS
                // HAS NOT BEEN VERIFIED) that the process, in the code below, has already been
                // started when we start doing the actual "write" to the Process!
                // 
                // ChatGPT told me, and I quote, "Don't worry about it. I asked the bartender and
                // he said it was OK."  It's pretty fun using that thing, by the way...  It
                // mentioned that Operating-System Commands which are expecting Piped User-Input
                // will "hang" (idle), until that User-Input has been received.
                //
                // So... though it seems as if (at least to me) the line at the bottom should come
                // before the line (above):
                // 
                // `pro = b.start`.
                // 
                // Alas, there is no way to invoke the command `pro.getOutputStream`, until
                // `Process pro;` variable has been created!

                if (x.javaPipe != null) x.javaPipe.writeToPipe(pro.getOutputStream());
            }

            // Otherwise just call this version - if there are no "OSExtras"
            else pro = Runtime.getRuntime().exec(command);


            // *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** ***
            // Now just collect output with the (Java-HTML internal) "ISPT" Printer-Reader Threads
            // *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** ***
            //
            // The next 3 lines create a "Completion Monitor" instance, and then register the
            // "Reader Threads" for reading from Standard-Out and Standard-Error from the Process.
            // These create two "Daemon Threads" that read process-output and error-output using
            // Java Thread's.  These will not hang **UNLESS** one of the InputStream read method's
            // hang/lock.

            Completed completed = new Completed();

            // Note that the constructor's below start the printer/reader/monitors - there is no
            // need to actually keep a reference/pointer to these classes once they are
            // constructed.  Passing the 'completed' instance to these constructors is enough!
            //
            // ALSO: It is completely immaterial whether/if any of the three appendable's passed to
            //       the TriAppendable-Constructor are actually null.  Note that only the one whose
            //       name ends with "SB" is guaranteed not to be null.  The other two
            //       (user-provided) Appendable's can easily be null!

            new ISPT(
                pro.getInputStream(),
                new TriAppendable(
                    config.outputAppendable,    // User-Provided, May be null!
                    standardOutputSB,           // OSResponse uses this to collect standardOutput
                    config.standardOuput        // User-Provided too, may be null.
                ),
                completed,
                "Thread for Reading from Standard Output"
            );

            new ISPT(
                pro.getErrorStream(),
                new TriAppendable(
                    config.outputAppendable,    // User-Provided, might be null
                    errorOutputSB,              // OSResonse uses this to collect errorOutput 
                    config.errorOutput          // User-Provided
                ),
                completed,
                "Thread for Reading from Error Output"
            );

            // NOTE: The process, once it is instantiated, is already running.  There is no
            // need to "start" the process, the call to Runtime.exec (above does that).  Here
            // we can just wait for the reader threads to receive the EOF messages, which  is
            // how they terminate.

            completed.waitForCompletionOfAllThreads();

            // It is unlikely this would cause the current thread to wait, because the previous
            // line will wait until both Standard-Out and Error-Out have received EOF...
            // Perhaps the process COULD delay the return response-code here.  The reality is that
            // method 'waitFor' is the PREFERRED way to retrieve the exit-value from this process...
            // although method 'Process.exitValue()' would also probably work.

            int response = pro.waitFor();


            // *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** ***
            // Return the 'OSResponse' result (or throw an exception, if the process threw one)
            // *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** ***

            // This prints a friendly little message at the end stating what the response-code was
            if (config.outputAppendable != null) config.outputAppendable.append
                ("Command exit with return value " + response + '\n');

            completed.ifExceptionThrowException();

            // Note that a '0' response-code usually means 'succesfully terminated'
            return new OSResponse
                (commandStr, response, standardOutputSB.toString(), errorOutputSB.toString());
        }

        catch (InterruptedException e)
        {
            return new OSResponse(
                commandStr, OSResponse.INTERRUPTED, standardOutputSB.toString(),
                errorOutputSB.toString()
            );
        }
    }


    // ********************************************************************************************
    // ********************************************************************************************
    // toString & Clone Methods
    // ********************************************************************************************
    // ********************************************************************************************


    private static String toStrApp(Appendable a)
    { return (a == null) ? "null\n" : (a.getClass().getName() + '\n'); }

    /**
     * Generates a {@code String} this class.  The returned {@code String} merely encodes the
     * class-names of the non-null {@code Appendable's}.
     *     
     * @return A simple representation of this class, as a {@code java.lang.String}
     */
    public String toString()
    {
        // private static String toStrApp(Appendable a)
        // { return (a == null) ? "null\n" : (a.getClass().getName() + '\n'); }

        return
            "outputAppendable:     " + toStrApp(this.outputAppendable) +
            "commandStrAppendable: " + toStrApp(this.commandStrAppendable) +
            "standardOutput:       " + toStrApp(this.standardOutput) +
            "errorOutput:          " + toStrApp(this.errorOutput);
    }

    /**
     * Creates a clone of {@code 'this'} instance.  Though unlikely of much use, this could
     * conceivably have some function if similar, but non-identical, output mechanisms were being
     * used.
     * 
     * @return An exact copy of {@code 'this'} instance - one in which all output
     * {@code Appendable's} have had their references copied into the new instance.
     */
    public abstract OSCommands clone();
}