Skip to main content
Ben Nadel
On User Experience (UX) Design, JavaScript, ColdFusion, Node.js, Life, and Love.

Executing Command-Line Processes From A Working Directory Using ProcessBuilder In Lucee CFML 5.2.9.31

By Ben Nadel on
Tags: ColdFusion

In ColdFusion, we've always had the CFExecute tag as a means to execute external command-line processes from within our CFML code. However, one of the big missing features in the CFExecute tag API is the ability to set the "working directory" for the execution. In many cases, this doesn't matter. However, for some processes, the working directory is used to change the behavior of the execution. Inspired by Brad Wood (see Tweet thread), I wanted to take a look at how I might execute external processes from a given working directory using Java's ProcessBuilder class in Lucee CFML 5.2.9.31.

The other day, I was looking at using the zip command to archive images using the STORAGE compression method in Lucee CFML. In that post, I had to -j / "Junk" the file paths within the archive since they were being generated as absolute paths from the server root. According to the documentation, if I wanted to create an archive with "relative paths", I would have had to cd (Change Directory) into the desired "working directory" and then execute the zip command using a relative-path input. And, since CFExecute has no sense of "working directory", this latter approach wasn't an option.

And, that's where Java's ProcessBuilder comes into play. In this post, I want to experiment with using the ProcessBuilder to execute the same zip command; but, do so from within a "working directory" that allows me to generate relative folder paths in the resultant ZIP archive file.

To be clear, I am purposefully saying "experiment" here because I've never used the ProcessBuilder class before (I had never even heard of it before Brad mentioned it). As such, please accept that nothing that I demonstrate here is meant to imply best practices. This is just me trying to figure out how the ProcessBuilder class works; and, how I can leverage it from within my ColdFusion code.

From what I've read on the Googles, the biggest hurdle with the ProcessBuilder class is that you run into a bit of a Catch-22 when you want to read the generated output while also using a timeout. According to a post by Coty Condry, if the child-process Input-Stream fills up, the .waitFor() method can hang indefinitely. Which means that you have to start reading from the Input-Stream prior to calling .waitFor(). But, reading from the Input-Stream is a blocking operation. Which means, you don't get a chance to call the .waitFor() method in order to implement a timeout.

To get around this, it seems we have two options:

  1. Redirect the child process output to a File so that we don't have to explicitly manage it in the code.

  2. Read from the Input-Stream in a separate thread so that it doesn't block our call to the .waitFor() method.

I've tried to play around with both of these approaches. In the first demo, I'm redirecting the process output to a temporary file that I'm creating via ColdFusion's getTempFile() function. The spawned process will then write to that file behind the scenes; and then, when I go to return the results of the process execution, I'll read and delete the temp file in order to report the generated output back to the calling context.

To explore this ProcessBuilder approach with the zip command, I've created a folder full of images:

./path/to/my-cool-images/

When I generate the ZIP archive, I want the root folder within the archive to be the my-cool-images folder. As such, when I execute the zip process, I need to do so from with the working directory, ./path/to/. Essentially, I need to recreate this series of terminal commands:

  • pwd => /app/wwwroot
  • cd ./path/to
  • zip -0 -r /app/wwwroot/images.zip ./my-cool-images/

NOTE: I'm including the pwd (Print Working Directory) command here just to paint a picture of where we are in the server.

With that said, here's what I came up with for the file-based output approach:

<cfscript>

	zipFilepath = expandPath( "./images.zip" );

	cleanup( zipFilepath );

	// ------------------------------------------------------------------------------- //
	// ------------------------------------------------------------------------------- //

	results = processCommand(
		commandName = "zip",
		commandArguments = [
			// Regulate the speed of compression: 0 means NO compression. This is setting
			// the compression method to STORE, as opposed to DEFLATE (which is the
			// default method). This will apply to all files within the zip. If we wanted
			// to target only a subset of file-types, we could have used "-n" to white-
			// list a subset of the input files (ex, "-n .gif:.jpg:.jpeg:.png").
			"-0",
			// Recurse the input directory.
			"-r",
			// Generate the zip at the following output path.
			zipFilepath,
			// Input path.
			// --
			// NOTE: If the input directory is provided as an absolute path, the
			// resultant zip file will contain absolute file paths (from the server
			// root). In order to make the internal zip directory structure we are going
			// to use a relative path which is relative to the WORKING DIRECTORY.
			"./my-cool-images/",
			// Don't include these files in zip.
			"-x *.DS_Store"
		],
		timeoutInSeconds = 5,
		workingDirectory = expandPath( "./path/to/" )
	);

	dump( results );
	echo( "<pre>" & encodeForHtml( results.output ) & "</pre>" );

	// ------------------------------------------------------------------------------- //
	// ------------------------------------------------------------------------------- //

	/**
	* I execute the given command with the given arguments. The result is a struct that
	* contains three properties:
	* 
	* - code: The exit code of the process (or -1 if no timeout was provided).
	* - output: The output generated by the process.
	* - duration: The time-in-milliseconds for which the command executed.
	* 
	* @commandName I am the executable.
	* @commandArguments I am the collection of arguments to pass to the command.
	* @timeoutInSeconds I am the number of seconds to wait for the command to complete.
	* @terminateOnTimeout I determine if command should be forcibly terminated upon timeout.
	* @workingDirectory I am the working directory in which to execute the command.
	*/
	public struct function processCommand(
		required string commandName,
		required array commandArguments,
		numeric timeoutInSeconds = 0,
		boolean terminateOnTimeout = true,
		string workingDirectory = ""
		) {

		// The ProcessBuilder is supposed to be initialized with a list of String
		// arguments. As such, let's map the input arguments onto their stringified
		// version just to make sure any numeric type (or other types) are cast to the
		// expected format.
		var normalizedArguments = [ commandName ].append(
			commandArguments.map(
				( commandArgument ) => {

					return( toString( commandArgument ) );

				}
			),
			// Merge the mapped values onto the end of the arguments collection.
			true
		);

		var processBuilder = createObject( "java", "java.lang.ProcessBuilder" )
			.init( normalizedArguments )
		;

		// If a working directory has been provided, apply it to the process builder.
		if ( workingDirectory.len() ) {

			processBuilder.directory(
				createObject( "java", "java.io.File" ).init( workingDirectory )
			);

		}

		// If no timeout has been provided, it means that the calling context doesn't
		// want to block and wait for the process to complete. As such, we can spawn the
		// child process and return immediately.
		if ( ! timeoutInSeconds ) {

			var process = processBuilder.start();

			return({
				code: -1,
				output: "",
				duration: 0
			});

		}

		// To make things easier, let's merge the process' error output stream into the
		// standard output stream. This way, we only have one source of data to consume.
		// --
		// NOTE: I BELIEVE we can still use the exitValue to differentiate the expected
		// output from the error output.
		processBuilder.redirectErrorStream( true );

		// CAUTION: The Java ProcessBuilder has a 32Kb buffer which will cause the
		// .waitFor() method to hang if the buffer is full. This presents us with a bit
		// of a chicken-and-egg problem because we want to call .waitFor() first so that
		// we can respect the desired timeout. However, if the buffer gets filled-up, we
		// don't want to hang. On the other hand, if we try to read from the output
		// stream first, the process can end up running longer than the calling context
		// is willing to wait. To find a compromise to this, I am going to redirect the
		// output to a FILE so that I can call the .waitFor() immediately without the
		// possibility of hanging.
		// --
		// READ MORE: http://juxed.blogspot.com/2015/09/java-processbuilder-has-32kb-buffer.html
		var outputFilePath = getTempFile( getTempDirectory(), "process-command-output" );

		try {

			processBuilder.redirectOutput(
				createObject( "java", "java.io.File" ).init( outputFilePath )
			);

			// Start the child process.
			var startedAt = getTickCount();
			var process = processBuilder.start();

			// Block and wait for the spawned process to complete OR for the given
			// timeout to elapse. If the process is still running at the timeout cutoff,
			// then return value will be False.
			var isProcessExited = process.waitFor(
				timeoutInSeconds,
				createObject( "java", "java.util.concurrent.TimeUnit" ).SECONDS
			);

			// If the calling context wants to terminate the process when the timeout
			// elapses, let's try to forcibly kill the process.
			// --
			// CAUTION: This may not take place immediately - the processing may still be
			// alive a brief period after this call.
			if ( ! isProcessExited && terminateOnTimeout ) {

				process.destroyForcibly();

				// Give the runtime a chance to kill the process if it is still alive.
				if ( process.isAlive() ) {

					sleep( 10 );

				}

			}

			// If we tried to forcefully kill the process, but it hasn't been terminated
			// yet, our attempt to read the exit value will throw an error. As such,
			// let's catch that edge-case and explicitly use the exit code that the child
			// process WOULD HAVE LIKELY RETURNED if the termination request had
			// completed in time.
			// --
			// NOTE: I am getting the 137 code from trial-and-error.
			try {

				var exitValue = process.exitValue();

			} catch ( any error ) {

				var exitValue = 137;

			}

			return({
				code: exitValue,
				output: fileRead( outputFilePath ),
				duration: ( getTickCount() - startedAt )
			});

		} finally {

			// TODO: Is it safe to read from the file if we tried to forcibly terminate
			// the process? Could it still be locked? Furthermore, is it safe to try and
			// delete file if the process is still alive?
			fileDelete( outputFilePath );

		}

	}


	/**
	* I delete the given file path if it exists.
	* 
	* @filepath I am the path being deleted.
	*/
	public void function cleanup( required string filepath ) {

		if ( fileExists( filepath ) ) {

			fileDelete( filepath );

		}

	}
	
</cfscript>

As you can see, my processCommand() User-Defined Function (UDF) takes a workingDirectory argument. This is used to set the .directory() call on the ProcessBuilder instance. This is the working directory from which the commandName process (zip in our case) will be executed.

Now, when we run this ColdFusion code, we can see from the output exactly how the paths were generated within the ZIP archive:

Running the zip command with ProcessBuilder to zip up images in Lucee CFML.

Woot! As you can see, because the zip command was executed with a relative path for the input directory - ./my-cool-images - the generated archive uses my-cool-images/ as the root directory.

This seems to work, but using a File object as the output destination makes me a little uneasy. Not only is file I/O slower than in-memory processing, but I've run into loads of issues in the past where mysterious "file locks" prevent ColdFusion from deleting a file that's still "in use" by another process. As such, I wanted to see if I could get the same outcome without hitting the file system.

The other approach that I came up with leverages the parallel iteration features of Lucee CFML: given an array of values, we can have Lucee map those values onto another array of values using asynchronous threads. This gives us an opportunity to create an array that contains "units of work", which we can then have Lucee execute in parallel.

In this case, the two "units of work" will be:

  1. Implement the .waitFor() method with the given timeout.
  2. Read from the child process' Input-Stream.

The hope here is that these two tasks won't block each other thanks to Lucee's parallel iteration; but, the second task - consuming the Input-Stream - should prevent the .waitFor() from hanging.

<cfscript>

	zipFilepath = expandPath( "./images.zip" );

	cleanup( zipFilepath );

	// ------------------------------------------------------------------------------- //
	// ------------------------------------------------------------------------------- //

	results = processCommand(
		commandName = "zip",
		commandArguments = [
			// Regulate the speed of compression: 0 means NO compression. This is setting
			// the compression method to STORE, as opposed to DEFLATE (which is the
			// default method). This will apply to all files within the zip. If we wanted
			// to target only a subset of file-types, we could have used "-n" to white-
			// list a subset of the input files (ex, "-n .gif:.jpg:.jpeg:.png").
			"-0",
			// Recurse the input directory.
			"-r",
			// Generate the zip at the following output path.
			zipFilepath,
			// Input path.
			// --
			// NOTE: If the input directory is provided as an absolute path, the
			// resultant zip file will contain absolute file paths (from the server
			// root). In order to make the internal zip directory structure we are going
			// to use a relative path which is relative to the WORKING DIRECTORY.
			"./my-cool-images/",
			// Don't include these files in zip.
			"-x *.DS_Store"
		],
		timeoutInSeconds = 5,
		workingDirectory = expandPath( "./path/to/" )
	);

	dump( results );
	echo( "<pre>" & encodeForHtml( results.output ) & "</pre>" );

	// ------------------------------------------------------------------------------- //
	// ------------------------------------------------------------------------------- //

	/**
	* I execute the given command with the given arguments. The result is a struct that
	* contains three properties:
	* 
	* - code: The exit code of the process (or -1 if no timeout was provided).
	* - output: The output generated by the process.
	* - duration: The time-in-milliseconds for which the command executed.
	* 
	* @commandName I am the executable.
	* @commandArguments I am the collection of arguments to pass to the command.
	* @timeoutInSeconds I am the number of seconds to wait for the command to complete.
	* @terminateOnTimeout I determine if command should be forcibly terminated upon timeout.
	* @workingDirectory I am the working directory in which to execute the command.
	*/
	public struct function processCommand(
		required string commandName,
		required array commandArguments,
		numeric timeoutInSeconds = 0,
		boolean terminateOnTimeout = true,
		string workingDirectory = ""
		) {

		// The ProcessBuilder is supposed to be initialized with a list of String
		// arguments. As such, let's map the input arguments onto their stringified
		// version just to make sure any numeric type (or other types) are cast to the
		// expected format.
		var normalizedArguments = [ commandName ].append(
			commandArguments.map(
				( commandArgument ) => {

					return( toString( commandArgument ) );

				}
			),
			// Merge the mapped values onto the end of the arguments collection.
			true
		);

		var processBuilder = createObject( "java", "java.lang.ProcessBuilder" )
			.init( normalizedArguments )
		;

		// If a working directory has been provided, apply it to the process builder.
		if ( workingDirectory.len() ) {

			processBuilder.directory(
				createObject( "java", "java.io.File" ).init( workingDirectory )
			);

		}

		// If no timeout has been provided, it means that the calling context doesn't
		// want to block and wait for the process to complete. As such, we can spawn the
		// child process and return immediately.
		if ( ! timeoutInSeconds ) {

			var process = processBuilder.start();

			return({
				code: -1,
				output: "",
				duration: 0
			});

		}

		// To make things easier, let's merge the process' error output stream into the
		// standard output stream. This way, we only have one source of data to consume.
		// --
		// NOTE: I BELIEVE we can still use the exitValue to differentiate the expected
		// output from the error output.
		processBuilder.redirectErrorStream( true );

		// Start the child process.
		var startedAt = getTickCount();
		var process = processBuilder.start();

		// CAUTION: The Java ProcessBuilder has a 32Kb buffer which will cause the
		// .waitFor() method to hang if the buffer is full. This presents us with a bit
		// of a chicken-and-egg problem because we want to call .waitFor() first so that
		// we can respect the desired timeout. However, if the buffer gets filled-up, we
		// don't want to hang. On the other hand, if we try to read from the output
		// stream first, the process can end up running longer than the calling context
		// is willing to wait (since reading from the input stream is also a blocking
		// operation). To try and get the best of both worlds, I'm going to run the two
		// operations in parallel using parallel array iteration.
		// --
		// READ MORE: http://juxed.blogspot.com/2015/09/java-processbuilder-has-32kb-buffer.html
		var unitsOfWork = [
			// THE FIRST UNIT OF WORK will be to set the timeout for the child process
			// and to try and forcibly terminate it if it runs for too long.
			() => {

				// Block and wait for the spawned process to complete OR for the given
				// timeout to elapse. If the process is still running at the timeout
				// cutoff, then return value is False.
				var isProcessExited = process.waitFor(
					timeoutInSeconds,
					createObject( "java", "java.util.concurrent.TimeUnit" ).SECONDS
				);

				// If the calling context want to terminate the process when the timeout
				// elapses, let's try to forcefully kill the process.
				// --
				// CAUTION: This may not take place immediately - the processing may
				// still be alive for some brief period after this call.
				if ( ! isProcessExited && terminateOnTimeout ) {

					process.destroyForcibly();

					// Give the runtime a chance to kill the process if it is still alive.
					if ( process.isAlive() ) {

						sleep( 10 );

					}

				}

				// If we tried to forcefully kill the process, but it hasn't been
				// terminated yet, our attempt to read the exit value will throw an
				// error. As such, let's catch that edge-case and explicitly use the exit
				// code that the child process WOULD HAVE LIKELY RETURNED if the
				// termination request had completed in time.
				// --
				// NOTE: I am getting the 137 code from trial-and-error.
				try {

					return( process.exitValue() );

				} catch ( any error ) {

					return( 137 );

				}

			},
			// THE SECOND UNIT OF WORK will be to read from the child process' input
			// stream and aggregate the process output.
			() => {

				var inputStreamReader = createObject( "java", "java.io.InputStreamReader" )
					.init( process.getInputStream() )
				;
				var bufferedInputStreamReader = createObject( "java", "java.io.BufferedReader" )
					.init( inputStreamReader )
				;
				var outputLines = [];

				// Read from standard / error MERGED output streams.
				while ( true ) {

					// If the process output stream has already been closed by the time
					// we go to read a line, the .readLine() call will fail.
					try {
					
						var line = bufferedInputStreamReader.readLine();

					} catch ( any error ) {

						break;

					}

					// If the line data is null, the stream has been fully consumed.
					if ( isNull( line ) ) {

						break;

					}

					outputLines.append( line );

				};

				return( outputLines.toList( chr( 10 ) ) );

			}
		];

		// Execute the two UNITS OF WORK in parallel.
		var workResults = unitsOfWork.map(
			( unitOfWork ) => {

				return( unitOfWork() );

			},
			// Run in parallel.
			true
		);

		return({
			code: workResults[ 1 ],
			output: workResults[ 2 ],
			duration: ( getTickCount() - startedAt )
		});

	}


	/**
	* I delete the given file path if it exists.
	* 
	* @filepath I am the path being deleted.
	*/
	public void function cleanup( required string filepath ) {

		if ( fileExists( filepath ) ) {

			fileDelete( filepath );

		}

	}
	
</cfscript>

The overall concept is exactly the same as the first demo. Only this time, instead of writing the generated output to a temp file, we're using in-memory stream consumption inside a parallel thread (thanks to Lucee's parallel iteration). Then, we're reading the generated content from the results of the parallel iteration.

And, when we run this ColdFusion code, we get the same type of output:

Running the zip command with ProcessBuilder to zip up images in Lucee CFML.

As you can see, we end up generating the same exact ZIP archive file; only, this time, we didn't have to mess around with an temp file; which means, we don't have to worry about file locks and what happens if we go to delete the file before the child process has been terminated. This approach is a bit more verbose; but, it makes me feel more comfortable.

Again, this is the first time that I've ever looked at the ProcessBuilder class; so, I am sure there are funky little edge-cases that I'm not even considering. Though, based on a lot of demos that I saw around the web, it doesn't appear to be all that much more complicated. But, take this all with a grain of salt. That said, it's awesome to be able to execute external commands using a working directory in Lucee CFML 5.2.9.31 - something that is sorely lacking in CFExecute.



Reader Comments

TBH, it's going to take me a couple more times through this to entirely grep what's going on, but pretty cool (advance-level) stuff here! Running the unitsOfWork in parallel is pretty clever! 🤯

Reply to this Comment

Wow, cool idea to hand closures to the parallel each operation as a trick for running them in a thread. I've always just ran the waitFor() in the main thread and then used cfthread to read from the input stream (which is really the output!).

Reply to this Comment

@Brad,

Thanks! I'm so eager to use Lucee's parallel processing in more ways :D Though, it definitely locks the code down to a Lucee-only solution. Using CFThread would keep it more general.

I did a lot of Googling over the weekend for ProcessBuilder, and I didn't really find a lot of good information. Maybe I just wasn't using the right keywords. A number of examples all used something called StreamGobbler, which I guess is some sort of async Input-Stream consumer. I don't know if that's a standard library or just some pattern people are using?

I actually found a reference to StreamGobbler right in the Lucee code; but, it looks like that's just some private implementation.

Yo, the most confusing part about all of this is the terminology of Input and Output. I have very little experience with the command-line, so thinking about streams is challenging. And the fact that I'm getting the "input" stream to see what the Process is outputting hurts my brain :D

Reply to this Comment

@Chris,

To be fair, I was completely learning as I go here - so, I barely understand what is going on, ha ha.

Reply to this Comment

I did a lot of Googling over the weekend for ProcessBuilder, and I didn't really find a lot of good information.

It's funny how good examples of java docs can be a little hit and miss. And processbuilder has been part of the core JDK since at least Java 7! I've found that the API for process builder is pretty dang straight forward and I found most everything I needed just reading here and mixing copious experimentation:

This is the "builder" class that creates the process
https://docs.oracle.com/javase/8/docs/api/java/lang/ProcessBuilder.html

This is what represents the running process once you've started it
https://docs.oracle.com/javase/8/docs/api/java/lang/Process.html

A number of examples all used something called StreamGobbler

Yeah, I've seen all those too, as well as the refernce in the Lucee source code. So far as I can tell, the "stream gobbler" is just a sort of design pattern, or perhaps better put, a really common copy paste example from Stackoverflow for consuming a stream (of chars or binary) async. And since Java doesn't have the handy cfthread tag, you have to write a runnable class and hand it off to a thread, etc.

the most confusing part about all of this is the terminology of Input and Output.

This is something that defo threw me at first. Any seasoned Java dev will instantly recognize the nomenclature of "input" and "output" streams, but basically they are reversed! What you have to do is think of them from YOUR perspective. Input streams are YOUR input that your program is reading in from somewhere else (in this case from the process). Output streams are YOUR output that you are sending out to somewhere else. For example, you can write to the process's output stream to send it the equivalent of keyboard characters. Once you think of in/out streams as YOUR programs input and output, it makes more sense. In/out streams are actually abstract classes in java that have a ton of concrete implementations such as a FileInputStream or ByteArrayInputStream, etc.

https://docs.oracle.com/javase/8/docs/api/java/io/InputStream.html
https://docs.oracle.com/javase/8/docs/api/java/io/OutputStream.html

The other big thing I wished I had realized earler was the difference between a *Stream and a *StreamReader as this is very important. A stream is always binary data. You may be tempted to read a stream of text as binary and just pass it through the chr() function in CFML where a byte of 65 is an "A" char, etc. But don't do this a it only works for single byte character encodings! As soon as you hit a double byte char like something UTF-16 encoded, you need to read two bytes of the stream at a time, combine them, and then match the actual readable character. This is what InputStreamReaders do. Streams can be just raw binary data like an image, but any stream containing text should be wrapped in an InputStreamReader and you let THAT class read the bytes and assemble them to readable text for you.

https://docs.oracle.com/javase/7/docs/api/java/io/InputStreamReader.html

It's really amazing the depth of different classes the JDK has inside of it. CFML makes it so easy by dealing with such high level concepts. We're just used to everything being a string in CFML, but we pay for that with lack of asynchronicity everywhere. We always just get used to waiting for a tag to complete, then we reality streams are always being spooled up behind the scenes for us by the CF engine.

And finally (sorry for all the exposition, there's just a lot of stuff I wished I had known when I first dug into these things) I think I sent this to you, but here is one of the places CommandBox uses ProcessBuilders. This powers the run command which is what it used when you type something like

!ping google.com

from the CommandBox CLI and I defer the native OS to run the command for you.

https://github.com/Ortus-Solutions/commandbox/blob/development/src/cfml/system/modules_app/system-commands/commands/run.cfc#L92-L178

My first version of this had a "stream gobber" sort of cfthread to read the command's output, but I later changed to doing it all inline. I found that the input stream auto closes when the process exits so I just do this

  • call processbuilder.start()
  • read input stream until it closes
  • call waitfor() just to make sure it's dead

And that lets me do simple operations in a single thread. The main downside of my approach here is you can't interrupt the code reading from the stream if you want to give up on the process early. I worked around that as the JLine implementation of the console that CommandBox uses captures Ctrl-C keystrokes from the user and actually interrupts the main thread at the JVM level so I have a mechanism in place to kill running processes anyway! (this wouldn't apply to a web app, of course) You'll also notice I use an InputStreamReader in that code to get proper character encoding.

If you want to see what a "stream gobbler" would look like in CFML, here is a place where CommandBox uses it to pass through the output of a server that's been started so it prints to the console while the server starts. I apologize for all the moving parts here, it makes it look far more complicated than it really is with me applying formatting to the output and such.

https://github.com/Ortus-Solutions/commandbox/blob/development/src/cfml/system/services/ServerService.cfc#L1343-L1421

I use a BufferedReader, which wraps the InputStreamReader, which wraps the InputStream. This buffers characters in memory in case my gobber can't gobble fast enough to keep up with the process!

Reply to this Comment

@Brad,

Thanks for the brain-dump - trying to let that wash over me. One thing that I am seeing in your CommandBox implementation is that you're explicitly closing the buffered reader:

bufferedReader.close();

This was something that I wasn't sure about. In the Googles, I was finding some examples in which the stream was explicitly closed as well as plenty of examples where it wasn't closed. I guess I should probably add an explicit .close() call once the .readLine() starts returning null.

The other thing that really confused me was the whole inheritIO() call that some examples were making. At first, I had that; but, it seemed to just hang the process. Or maybe it was exiting immediately. Either way, it was not doing the "right thing". And, once I removed the .inheritIO() call, things just started working properly.

I guess that's for when you need to the child-process to interact with the parent-process; which, in my case, is not necessary since the child-process is completely self-contained. But, it was just another "stream" concept that I didn't get :D

With all that said, 100% agree that it is so nice that ColdFusion just handles so many high-level abstractions for us. Barney Boisvert used to use a term that I think summed it up perfectly: ceremony. Other languages have so much ceremony around the job of getting things done. But, not ColdFusion - it gets rid of so much of that ceremony and just allows us to focus on the "value add". Not to say that is without trade-offs. But, it's a good trade-off to make (especially when we have "escape valves" built into the language that allows us to go deeper when / if needed).

I heard someone say that future iterations of Lucee may even include ways for us to use Java directly in our CFML (like writing a class that extends a Java class). That would really be something amazing in those cases where we need to get low-level.

Reply to this Comment

I guess I should probably add an explicit .close() call once the .readLine() starts returning null.

You know to be honest, I don't always know when you need to explicitly close streams, but I always do it just for good measure. The trick is, you always want to close them in a finally block. That means even if the code above errors, the stream won't be left open. Once you lose the reference to that object, there's no way to close it again. (note a try block can have finally with no catch block)

The other thing that really confused me was the whole inheritIO() call that some examples were making.

Ahh yes, you don't need to ever use that method in your use cases. What that does is it directly pipes the input of your process to the actual standard input of your JVM (which in this case is probably nothing) and it directly pipes the standard out and error streams of your process to the matching "out" streams of your server's JVM process (which would point to the console or a servlet log file). This makes zero sense to do in a web app really. At least not for the input. I use it in CommandBox since I'm running the JVM as a console application where the input stream CommandBox is connected to your keyboard and the out/error streams are connected to the terminal output on your screen. That's what allows you to run something like

!pause

on Windows inside of CommandBox and your keystroke from the keyboard is piped through to the process. This also means you can do crazy stuff like run

!cmd

from inside of CommandBox and you can run a Windows cmd shell right inside of CommandBox with input/output proxied right through from your keyboard and screen through the processbuilder. Type exit and you're back at commandbox again. This is awesome for the CLI (like running !git commit and then being able to interact with the vi or nano window that captures your commit message) but in your case where you're running your process from insider of a JVM that's running in a "disconnected" web server process, there's no point in it.

ceremony.

I like that -- it's a good way to put it :)

Reply to this Comment

@All,

As a quick follow-up to this post, I wanted to see if I could go back to the CFExecute tag, but allow it to operate from a given working directory:

www.bennadel.com/blog/3812-running-cfexecute-from-a-given-working-directory-in-lucee-cfml-5-2-9-31.htm

Of course, the CFExecute tag doesn't allow for a working directory to be defined natively; so, I attempted to write my first ever bash script that will proxy arbitrary commands from a given directory. The CFExecute tag is then used to execute the proxy rather than the command directly.

I got it working after much trial and error!

Reply to this Comment

@Brad,

Yo, I had no idea you could use ! syntax inside CommandBox. Ugg, I really gotta get past the start and stop command :D Seems like there's a whole host of stuff I'm not taking advantage of. I think there's a CommandBox book or something on the Ortus site. I guess it's time that I just "read the manual".

Good to know that inheritIO() will never apply to me -- that should simplify things.

Reply to this Comment

@All,

So, after posting this, and the re-reading the code a few days later, I noticed that the code actually contains a bug / not-a-bug:

var normalizedArguments = [ commandName ].append(
	commandArguments.map(
		( commandArgument ) => {
			return( toString( commandArgument ) );
		}
	),
	// Merge the mapped values onto the end of the arguments collection.
	true
);

In this code, notice that I am treating the Array.append() member-method call as returning an Array. And, in reality, this does return an array. However, according to the Lucee CFML documentation, this is supposed to return a Boolean.

As it turns out, the built-in functions and the member-methods return different values most of the time:

www.bennadel.com/blog/3813-built-in-functions-and-member-methods-return-different-data-types-in-lucee-cfml-5-3-5-92.htm

To be clear, I think this is a bug in the documentation, not the code. I've filed a ticket with Lucee.

Reply to this Comment

Post A Comment

You — Get Out Of My Dreams, Get Into My Blog
Live in the Now
Oops!
NEW: Some basic markdown formatting is now supported: bold, italic, blockquotes, lists, fenced code-blocks. Read more about markdown syntax »
Comment Etiquette: Please do not post spam. Please keep the comments on-topic. Please do not post unrelated questions or large chunks of code. And, above all, please be nice to each other - we're trying to have a good conversation here.