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}