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(); } |