diff --git a/apidocs/index.cfm b/apidocs/index.cfm index 242044038..291877fe4 100644 --- a/apidocs/index.cfm +++ b/apidocs/index.cfm @@ -1,3 +1,4 @@ + diff --git a/apidocs/internal.cfm b/apidocs/internal.cfm index 664b712bc..034daa879 100644 --- a/apidocs/internal.cfm +++ b/apidocs/internal.cfm @@ -1,6 +1,7 @@ + diff --git a/build/build-auto.properties b/build/build-auto.properties index c12b1597a..72fa144c1 100644 --- a/build/build-auto.properties +++ b/build/build-auto.properties @@ -1,5 +1,5 @@ #java build -java.pack200=true +java.pack200=false local.build=false #build locations diff --git a/build/build.properties b/build/build.properties index f9ead1a16..305a04c03 100644 --- a/build/build.properties +++ b/build/build.properties @@ -7,17 +7,16 @@ commandbox.description=CommandBox is a ColdFusion (CFML) CLI, Package Manager, S #java build java.compiler=1.7 java.debug=true -java.pack200=false #dependencies dependencies.dir=${basedir}/lib -cfml.version=4.5.3.020 -cfml.loader.version=1.4.1 +cfml.version=4.5.4.017 +cfml.loader.version=1.4.6 cfml.cli.version=${cfml.loader.version}.${cfml.version} lucee.version=${cfml.version} jre.version=1.8.0_102 launch4j.version=3.4 -runwar.version=3.4.11 +runwar.version=3.5.0 #build locations build.type=localdev diff --git a/build/build.xml b/build/build.xml index 0c172110e..1540df5f4 100644 --- a/build/build.xml +++ b/build/build.xml @@ -15,8 +15,9 @@ External Dependencies: - - + + + @@ -182,14 +183,14 @@ External Dependencies: + dump-cfc="${temp.dir}/engine/cfml/cli/lucee-server/context/library/tag/Dump.cfc"/> @@ -270,8 +271,9 @@ External Dependencies: - + + This is a stable build, let's compress these jars! @@ -485,9 +487,13 @@ External Dependencies: + + + + - + @@ -512,8 +518,15 @@ External Dependencies: + retries="5" + ignoreerrors="true" /> + + + + + + @@ -603,7 +616,7 @@ External Dependencies: - + @@ -792,7 +805,8 @@ External Dependencies: --> - + + diff --git a/src/cfml/Application.cfc b/src/cfml/Application.cfc index 02356f202..75d1e5dab 100644 --- a/src/cfml/Application.cfc +++ b/src/cfml/Application.cfc @@ -15,7 +15,7 @@ component{ // Move everything over to this mapping which is the "root" of our app CFMLRoot = getDirectoryFromPath( getMetadata( this ).path ); this.mappings[ '/commandbox' ] = CFMLRoot; - this.mappings[ '/commandbox-home' ] = expandPath( CFMLRoot & '/../' ); + this.mappings[ '/commandbox-home' ] = createObject( 'java', 'java.lang.System' ).getProperty( 'cfml.cli.home' ); this.mappings[ '/wirebox' ] = CFMLRoot & '/system/wirebox'; function onApplicationStart(){ diff --git a/src/cfml/system/BaseCommand.cfc b/src/cfml/system/BaseCommand.cfc index ecec1c009..b52473fed 100644 --- a/src/cfml/system/BaseCommand.cfc +++ b/src/cfml/system/BaseCommand.cfc @@ -63,15 +63,17 @@ component accessors="true" singleton { function getCWD() { return shell.pwd(); } - + /** - * Ask the user a question and wait for response + * ask the user a question and wait for response * @message.hint message to prompt the user with * @mask.hint When not empty, keyboard input is masked as that character + * + * @return the response from the user **/ - function ask( required message, string mask='' ) { + string function ask( message, string mask='', string defaultResponse='' ) { print.toConsole(); - return shell.ask( arguments.message, arguments.mask ); + return shell.ask( arguments.message, arguments.mask, arguments.defaultResponse ); } /** diff --git a/src/cfml/system/Bootstrap.cfm b/src/cfml/system/Bootstrap.cfm index 9ae061a0c..c0784c7e1 100644 --- a/src/cfml/system/Bootstrap.cfm +++ b/src/cfml/system/Bootstrap.cfm @@ -13,17 +13,18 @@ This file will stay running the entire time the shell is open - - _____ _ ____ - / ____| | | _ \ -| | ___ _ __ ___ _ __ ___ __ _ _ __ __| | |_) | _____ __ -| | / _ \| '_ ` _ \| '_ ` _ \ / _` | '_ \ / _` | _ < / _ \ \/ / -| |___| (_) | | | | | | | | | | | (_| | | | | (_| | |_) | (_) > < - \_____\___/|_| |_| |_|_| |_| |_|\__,_|_| |_|\__,_|____/ \___/_/\_\ v@@version@@ +#chr( 27 )#[32m#chr( 27 )#[40m#chr( 27 )#[1m + _____ _ ____ + / ____| | | _ \ + | | ___ _ __ ___ _ __ ___ __ _ _ __ __| | |_) | _____ __ + | | / _ \| '_ ` _ \| '_ ` _ \ / _` | '_ \ / _` | _ < / _ \ \/ / + | |___| (_) | | | | | | | | | | | (_| | | | | (_| | |_) | (_) > < + \_____\___/|_| |_| |_|_| |_| |_|\__,_|_| |_|\__,_|____/ \___/_/\_\ #chr( 27 )#[0m v@@version@@ -Welcome to CommandBox! -Type "help" for help, or "help [command]" to be more specific. - +#chr( 27 )#[1mWelcome to CommandBox! +Type "help" for help, or "help [command]" to be more specific.#chr( 27 )#[0m + + system = createObject( "java", "java.lang.System" ); args = system.getProperty( "cfml.cli.arguments" ); @@ -105,17 +106,16 @@ Type "help" for help, or "help [command]" to be more specific. if( !silent ) { // Output the welcome banner - systemOutput( replace( interceptData.banner, '@@version@@', shell.getVersion() ) ); + shell.printString( replace( interceptData.banner, '@@version@@', shell.getVersion() ) ); } // Running the "reload" command will enter this while loop once while( shell.run( silent=silent ) ){ clearScreen = shell.getDoClearScreen(); - interceptorService.announceInterception( 'onCLIExit' ); - + interceptorService.announceInterception( 'onCLIExit' ); if( clearScreen ){ - shell.getReader().clearScreen(); + shell.clearScreen(); } // Clear all caches: template, ... @@ -137,7 +137,7 @@ Type "help" for help, or "help [command]" to be more specific. if( clearScreen ){ // Output the welcome banner - systemOutput( replace( interceptData.banner, '@@version@@', shell.getVersion() ) ); + shell.printString( replace( interceptData.banner, '@@version@@', shell.getVersion() ) ); } } diff --git a/src/cfml/system/Shell.cfc b/src/cfml/system/Shell.cfc index 6fca23e61..ef8382f22 100644 --- a/src/cfml/system/Shell.cfc +++ b/src/cfml/system/Shell.cfc @@ -83,6 +83,14 @@ component accessors="true" singleton { required string tempDir, boolean asyncLoad=true ){ + // Possible byte order marks + variables.BOMS = [ + chr( 254 ) & chr( 255 ), + chr( 255 ) & chr( 254 ), + chr( 239 ) & chr( 187 ) & chr( 191 ), + chr( 00 ) & chr( 254 ) & chr( 255 ), + chr( 255 ) & chr( 254 ) & chr( 00 ) + ]; // Version is stored in cli-build.xml. Build number is generated by Ant. // Both are replaced when CommandBox is built. @@ -197,10 +205,11 @@ component accessors="true" singleton { * ask the user a question and wait for response * @message.hint message to prompt the user with * @mask.hint When not empty, keyboard input is masked as that character + * @defaultResponse Text to populate the buffer with by default that will be submitted if the user presses enter without typing anything * * @return the response from the user **/ - string function ask( message, string mask='', string buffer='' ) { + string function ask( message, string mask='', string defaultResponse='' ) { try { // read reponse while masking input @@ -208,10 +217,10 @@ component accessors="true" singleton { // Prompt for the user arguments.message, // Optionally mask their input - len( arguments.mask ) ? javacast( "char", left( arguments.mask, 1 ) ) : javacast( "null", '' )//, + len( arguments.mask ) ? javacast( "char", left( arguments.mask, 1 ) ) : javacast( "null", '' ), // This won't work until we can upgrade to Jline 2.14 // Optionally pre-fill a default response for them - // len( arguments.buffer ) ? javacast( "String", arguments.buffer ) : javacast( "null", '' ) + len( arguments.defaultResponse ) ? javacast( "String", arguments.defaultResponse ) : javacast( "null", '' ) ); } catch( jline.console.UserInterruptException var e ) { throw( message='CANCELLED', type="UserInterruptException"); @@ -262,22 +271,9 @@ component accessors="true" singleton { * @note Almost works on Windows, but doesn't clear text background * **/ - Shell function clearScreen( addLines = true ) { - // This outputs a double prompt due to the redrawLine() call - // reader.clearScreen(); - - // A temporary workaround for windows. Since background colors aren't cleared - // this will force them off the screen with blank lines before clearing. - if( variables.fileSystem.isWindows() && arguments.addLines ) { - var i = 0; - while( ++i <= getTermHeight() + 5 ) { - variables.reader.println(); - } - } - - variables.reader.print( '' ); - variables.reader.print( '' ); - + Shell function clearScreen() { + reader.clearScreen(); + variables.reader.flush(); return this; } @@ -413,12 +409,19 @@ component accessors="true" singleton { } else { line = variables.reader.readLine(); } - + // If the standard input isn't avilable, bail. This happens // when commands are piped in and we've reached the end of the piped stream if( !isDefined( 'line' ) ) { return false; } + + // Clean BOM from start of text in case something was piped from a file + BOMS.each( function( i ){ + if( line.startsWith( i ) ) { + line = replace( line, i, '' ); + } + } ); // If there's input, try to run it. if( len( trim( line ) ) ) { @@ -539,11 +542,13 @@ component accessors="true" singleton { setExitCode( 1 ); - getInterceptorService().announceInterception( 'onException', { exception=err } ); + // If CommandBox blows up while starting, the interceptor service won't be ready yet. + if( getInterceptorService().getConfigured() ) { + getInterceptorService().announceInterception( 'onException', { exception=err } ); + } variables.logger.error( '#arguments.err.message# #arguments.err.detail ?: ''#', arguments.err.stackTrace ?: '' ); - variables.reader.print( variables.print.whiteOnRedLine( 'ERROR (#variables.version#)' ) ); variables.reader.println(); variables.reader.print( variables.print.boldRedText( variables.formatterUtil.HTML2ANSI( arguments.err.message ) ) ); diff --git a/src/cfml/system/config/server-icons/trayicon.png b/src/cfml/system/config/server-icons/trayicon.png new file mode 100644 index 000000000..06e27c927 Binary files /dev/null and b/src/cfml/system/config/server-icons/trayicon.png differ diff --git a/src/cfml/system/config/urlrewrite.xml b/src/cfml/system/config/urlrewrite.xml index c724f6943..42380274c 100644 --- a/src/cfml/system/config/urlrewrite.xml +++ b/src/cfml/system/config/urlrewrite.xml @@ -5,6 +5,9 @@ Generic Front-Controller URLs /(flex2gateway|flashservices/gateway|messagebroker|lucee|rest|cfide|CFIDE|cfformgateway|jrunscripts)/.* + + /.*\.cf(m|ml)/.* diff --git a/src/cfml/system/config/web.xml b/src/cfml/system/config/web.xml new file mode 100644 index 000000000..72eb965a1 --- /dev/null +++ b/src/cfml/system/config/web.xml @@ -0,0 +1,132 @@ + + + + + + + + + + + + + + + Lucee Engine + CFMLServlet + lucee.loader.servlet.CFMLServlet + + + + + + + + + + + + + + 1 + + + + CFMLServlet + *.cfc + *.cfm + *.cfml + /index.cfc/* + /index.cfm/* + /index.cfml/* + + + + + + + + + + + + + + Lucee Servlet for RESTful services + RESTServlet + lucee.loader.servlet.RestServlet + 2 + + + + RESTServlet + /rest/* + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + index.cfm + index.cfml + index.html + index.htm + index.jsp + + + diff --git a/src/cfml/system/endpoints/ForgeBox.cfc b/src/cfml/system/endpoints/ForgeBox.cfc index e6158a6f3..1fa192406 100644 --- a/src/cfml/system/endpoints/ForgeBox.cfc +++ b/src/cfml/system/endpoints/ForgeBox.cfc @@ -41,9 +41,10 @@ component accessors="true" implements="IEndpointInteractive" singleton { public string function resolvePackage( required string package, boolean verbose=false ) { var slug = parseSlug( arguments.package ); var version = parseVersion( arguments.package ); - - // If we have a specific version and it exists in artifacts, use it. Otherwise, to ForgeBox!! - if( semanticVersion.isExactVersion( version ) && artifactService.artifactExists( slug, version ) ) { + var strVersion = semanticVersion.parseVersion( version ); + + // If we have a specific version and it exists in artifacts and this isn't a snapshot build, use it. Otherwise, to ForgeBox!! + if( semanticVersion.isExactVersion( version ) && artifactService.artifactExists( slug, version ) && strVersion.preReleaseID != 'snapshot' ) { consoleLogger.info( "Package found in local artifacts!"); // Install the package var thisArtifactPath = artifactService.getArtifactPath( slug, version ); @@ -368,8 +369,10 @@ component accessors="true" implements="IEndpointInteractive" singleton { // Advice we found it consoleLogger.info( "Verified entry in ForgeBox: '#slug#'" ); - // If the local artifact doesn't exist, download and create it - if( !artifactService.artifactExists( slug, version ) ) { + var strVersion = semanticVersion.parseVersion( version ); + + // If the local artifact doesn't exist or it's a snapshot build, download and create it + if( !artifactService.artifactExists( slug, version ) || strVersion.preReleaseID == 'snapshot' ) { // Test package location to see what endpoint we can refer to. var endpointData = endpointService.resolveEndpoint( downloadURL, 'fakePath' ); @@ -403,8 +406,27 @@ component accessors="true" implements="IEndpointInteractive" singleton { } catch( forgebox var e ) { - // This can include "expected" errors such as "slug not found" - throw( '#e.message##CR##e.detail#', 'endpointException' ); + + consoleLogger.error( "."); + consoleLogger.error( "Aww man, ForgeBox isn't feeling well."); + consoleLogger.debug( "#e.message# #e.detail#"); + consoleLogger.error( "We're going to look in your local artifacts cache and see if one of those versions will work."); + + // See if there's something usable in the artifacts cache. If so, we'll use that version. + var satisfyingVersion = artifactService.findSatisfyingVersion( slug, version ); + + if( len( satisfyingVersion ) ) { + consoleLogger.info( "."); + consoleLogger.info( "Sweet! We found a local version of [#satisfyingVersion#] that we can use in your artifacts."); + consoleLogger.info( "."); + + var thisArtifactPath = artifactService.getArtifactPath( slug, satisfyingVersion ); + // Defer to file endpoint + return fileEndpoint.resolvePackage( thisArtifactPath, arguments.verbose ); + } else { + throw( 'No satisfying version found for [#version#].', 'endpointException', 'Well, we tried as hard as we can. ForgeBox is unreachable and you don''t have a usable version in your local artifacts cache. Please try another version.' ); + } + } } diff --git a/src/cfml/system/endpoints/Git.cfc b/src/cfml/system/endpoints/Git.cfc index 51406cdfe..9d82c1727 100644 --- a/src/cfml/system/endpoints/Git.cfc +++ b/src/cfml/system/endpoints/Git.cfc @@ -123,6 +123,12 @@ component accessors="true" implements="IEndpoint" singleton { } } + // Clean up a bit + var gitFolder = localPath.getPath() & '/.git'; + if( directoryExists( gitFolder ) ) { + directoryDelete( gitFolder, true ); + } + // Defer to file endpoint return folderEndpoint.resolvePackage( localPath.getPath(), arguments.verbose ); diff --git a/src/cfml/system/modules/semver/models/SemanticVersion.cfc b/src/cfml/system/modules/semver/models/SemanticVersion.cfc index 0e03075c4..aaf6778d0 100644 --- a/src/cfml/system/modules/semver/models/SemanticVersion.cfc +++ b/src/cfml/system/modules/semver/models/SemanticVersion.cfc @@ -482,7 +482,7 @@ component singleton{ sComparator.sVersion.minor=val( sComparator.sVersion.minor )+1; sComparator.sVersion.revision = 0; } else { - sComparator.sVersion.revision=val( sComparator.sVersion.revision )+1; + sComparator.sVersion.revision=val( sComparator.sVersion.revision )+1; } sComparator.version = getVersionAsString( sComparator.sVersion ); diff --git a/src/cfml/system/modules_app/coldbox-commands/commands/coldbox/reinit.cfc b/src/cfml/system/modules_app/coldbox-commands/commands/coldbox/reinit.cfc index b1f2100ab..1f9ad00b8 100644 --- a/src/cfml/system/modules_app/coldbox-commands/commands/coldbox/reinit.cfc +++ b/src/cfml/system/modules_app/coldbox-commands/commands/coldbox/reinit.cfc @@ -25,7 +25,7 @@ component aliases="fwreinit" { if( structCount( serverInfo ) eq 0 ){ print.boldRedLine( "No server configurations found for '#getCWD()#', so have no clue what to reinit buddy!" ); } else { - var thisURL = "localhost:#serverInfo.port#/?fwreinit=#arguments.password#"; + var thisURL = "#serverInfo.host#:#serverInfo.port#/?fwreinit=#arguments.password#"; print.greenLine( "Hitting...#thisURL#" ); http result="local.results" url="#thisURL#"; @@ -41,4 +41,4 @@ component aliases="fwreinit" { } } -} \ No newline at end of file +} diff --git a/src/cfml/system/modules_app/package-commands/commands/package/set.cfc b/src/cfml/system/modules_app/package-commands/commands/package/set.cfc index c8acdde45..62ee63dfe 100644 --- a/src/cfml/system/modules_app/package-commands/commands/package/set.cfc +++ b/src/cfml/system/modules_app/package-commands/commands/package/set.cfc @@ -81,6 +81,6 @@ component { function completeProperty() { var directory = fileSystemUtil.resolvePath( '' ); // all=true will cause "package set" to prompt all possible box.json properties - return packageService.completeProperty( directory, true ); + return packageService.completeProperty( directory, true, true ); } } \ No newline at end of file diff --git a/src/cfml/system/modules_app/package-commands/interceptors/packageScripts.cfc b/src/cfml/system/modules_app/package-commands/interceptors/packageScripts.cfc index c0e949277..2440ef2b2 100644 --- a/src/cfml/system/modules_app/package-commands/interceptors/packageScripts.cfc +++ b/src/cfml/system/modules_app/package-commands/interceptors/packageScripts.cfc @@ -21,14 +21,27 @@ component { function postModuleLoad() { processScripts( 'postModuleLoad' ); } function preModuleUnLoad() { processScripts( 'preModuleUnLoad' ); } function postModuleUnload() { processScripts( 'postModuleUnload' ); } + function preServerStart() { processScripts( 'preServerStart' ); } + function onServerInstall() { processScripts( 'onServerInstall', interceptData.serverinfo.webroot ); } function onServerStart() { processScripts( 'onServerStart', interceptData.serverinfo.webroot ); } function onServerStop() { processScripts( 'onServerStop', interceptData.serverinfo.webroot ); } function onException() { processScripts( 'onException' ); } - function preInstall() { processScripts( 'preInstall' ); } - function onInstall() { processScripts( 'onInstall' ); } - function postInstall() { processScripts( 'postInstall' ); } - function preUninstall() { processScripts( 'preUninstall' ); } - function postUninstall() { processScripts( 'postUninstall' ); } + + // preInstall gets package requesting the installation because dep isn't installed yet + function preInstall() { processScripts( 'preInstall', interceptData.packagePathRequestingInstallation ); } + + // onInstall gets package requesting the installation because dep isn't installed yet + function onInstall() { processScripts( 'onInstall', interceptData.packagePathRequestingInstallation ); } + + // postInstall runs in the newly installed package + function postInstall() { processScripts( 'postInstall', interceptData.installDirectory ); } + + // preUninstall runs in the package that's about to be uninstalled + function preUninstall() { processScripts( 'preUninstall', interceptData.uninstallDirectory ); } + + // postUninstall gets package that requested uninstallation because dep isn't there any longer + function postUninstall() { processScripts( 'postUninstall', interceptData.uninstallArgs.packagePathRequestingUninstallation ); } + function preVersion() { processScripts( 'preVersion' ); } function postVersion() { processScripts( 'postVersion' ); } function onRelease() { processScripts( 'onRelease' ); } diff --git a/src/cfml/system/modules_app/server-commands/commands/server/forget.cfc b/src/cfml/system/modules_app/server-commands/commands/server/forget.cfc index 2a4891e49..b54e19711 100644 --- a/src/cfml/system/modules_app/server-commands/commands/server/forget.cfc +++ b/src/cfml/system/modules_app/server-commands/commands/server/forget.cfc @@ -38,16 +38,24 @@ component { } var serverInfo = serverService.resolveServerDetails( arguments ).serverinfo; - if( arguments.all ) { - var servers = serverService.getServers(); - servers.each( function( ID ){ runningServerCheck( servers[ arguments.ID ] ); } ); + var servers = arguments.all ? serverService.getServers() : { "#serverInfo.id#": serverInfo }; + if( arguments.force ) { + var runningServers = getRunningServers( servers ); + if ( ! runningServers.isEmpty() ) { + var stopMessage = arguments.all ? + "Stopping all running servers (#getServerNames( runningServers ).toList()#) first...." : + "Stopping server #serverInfo.name# first...."; + print.line( stopMessage ); + runningServers.each( function( ID ){ serverService.stop( runningServers[ arguments.ID ] ); } ); + } } else { - runningServerCheck( serverInfo ); + servers.each( function( ID ){ runningServerCheck( servers[ arguments.ID ] ); } ); } // Confirm deletion - var askMessage = arguments.all ? "Really forget & delete all servers (#arrayToList( serverService.getServerNames() )#) forever [y/n]?" : - "Really forget & delete server '#serverinfo.name#' forever [y/n]?"; + var askMessage = arguments.all ? + "Really forget & delete all servers (#arrayToList( serverService.getServerNames() )#) forever [y/n]?" : + "Really forget & delete server '#serverinfo.name#' forever [y/n]?"; if( arguments.force || confirm( askMessage ) ){ print.line( serverService.forget( serverInfo, arguments.all ) ); @@ -60,10 +68,22 @@ component { private function runningServerCheck( required struct serverInfo ) { if( serverService.isServerRunning( serverInfo ) ) { print.redBoldLine( 'Server "#serverInfo.name#" (#serverInfo.webroot#) appears to still be running!' ) - .yellowLine( 'Forgetting it now may leave the server in a currupt state. Please stop it first.' ) + .yellowLine( 'Forgetting it now may leave the server in a corrupt state. Please stop it first.' ) .line(); } } + + private function getRunningServers( required struct servers ) { + return servers.filter( function( ID ){ + return serverService.isServerRunning( servers[ arguments.ID ] ); + } ) + } + + private function getServerNames( required struct servers ){ + return servers.keyArray().map( function( ID ){ + return servers[ arguments.ID ].name; + } ); + } function serverNameComplete() { return serverService.getServerNames(); diff --git a/src/cfml/system/modules_app/server-commands/commands/server/list.cfc b/src/cfml/system/modules_app/server-commands/commands/server/list.cfc index a17b38c34..e1209eec6 100644 --- a/src/cfml/system/modules_app/server-commands/commands/server/list.cfc +++ b/src/cfml/system/modules_app/server-commands/commands/server/list.cfc @@ -86,12 +86,19 @@ component { .bold( status, statusColors.keyExists( status ) ? statusColors[ status ] : 'yellow' ) .bold( ')' ) .line(); - + if( arguments.verbose ) { - print.indentedLine( "host: " & thisServerInfo.host ) - .indentedLine( "webroot: " & thisServerInfo.webroot ) - .indentedLine( "HTTPEnable: " & thisServerInfo.HTTPEnable ) + print.indentedLine( "host: " & thisServerInfo.host ); + if( len( thisServerInfo.engineName ) ) { + print.indentedLine( "CF Engine: " & thisServerInfo.engineName & ' ' & serverInfo.engineVersion ); + } + if( len( thisServerInfo.WARPath ) ) { + print.indentedLine( "WARPath: " & thisServerInfo.WARPath ); + } else { + print.indentedLine( "webroot: " & thisServerInfo.webroot ); + } + print.indentedLine( "HTTPEnable: " & thisServerInfo.HTTPEnable ) .indentedLine( "port: " & thisServerInfo.port ) .indentedLine( "SSLEnable: " & thisServerInfo.SSLEnable ) .indentedLine( "SSLport: " & thisServerInfo.SSLport ) @@ -101,8 +108,6 @@ component { .indentedLine( "debug: " & thisServerInfo.debug ) .indentedLine( "ID: " & thisServerInfo.id ); - if( len( thisServerInfo.cfengine ) ) { print.indentedLine( "cfengine: " & thisServerInfo.cfengine ); } - if( len( thisServerInfo.WARPath ) ) { print.indentedLine( "WARPath: " & thisServerInfo.WARPath ); } if( len( thisServerInfo.libDirs ) ) { print.indentedLine( "libDirs: " & thisServerInfo.libDirs ); } if( len( thisServerInfo.webConfigDir ) ) { print.indentedLine( "webConfigDir: " & thisServerInfo.webConfigDir ); } if( len( thisServerInfo.serverConfigDir ) ) { print.indentedLine( "serverConfigDir: " & thisServerInfo.serverConfigDir ); } @@ -118,7 +123,14 @@ component { if( thisServerInfo.SSLEnable ) { print.indentedLine( 'https://' & thisServerInfo.host & ':' & thisServerInfo.SSLport ); } - print.indentedLine( thisServerInfo.webroot ); + if( len( thisServerInfo.engineName ) ) { + print.indentedLine( 'CF Engine: ' & thisServerInfo.engineName & ' ' & thisServerInfo.engineVersion ); + } + if( len( thisServerInfo.warPath ) ) { + print.indentedLine( 'WAR Path: ' & thisServerInfo.warPath ); + } else { + print.indentedLine( 'Webroot: ' & thisServerInfo.webroot ); + } }// end verbose } // End "filter" if diff --git a/src/cfml/system/modules_app/server-commands/commands/server/log.cfc b/src/cfml/system/modules_app/server-commands/commands/server/log.cfc index d111ed637..f11c3e514 100644 --- a/src/cfml/system/modules_app/server-commands/commands/server/log.cfc +++ b/src/cfml/system/modules_app/server-commands/commands/server/log.cfc @@ -17,11 +17,13 @@ component { * @name.optionsUDF serverNameComplete * @directory.hint web root for the server * @serverConfigFile The path to the server's JSON file. + * @follow Tail the log file with the "follow" flag. Press Ctrl-C to quit. **/ function run( string name, string directory, - String serverConfigFile + String serverConfigFile, + Boolean follow=false ){ if( !isNull( arguments.directory ) ) { arguments.directory = fileSystemUtil.resolvePath( arguments.directory ); @@ -39,7 +41,16 @@ component { var logfile = serverInfo.logdir & "/server.out.txt"; if( fileExists( logfile) ){ - return fileRead( logfile ); + + if( follow ) { + command( 'tail' ) + .params( logfile, 50 ) + .flags( 'follow' ) + .run(); + } else { + return fileRead( logfile ); + } + } else { print.boldRedLine( "No log file found for '#serverInfo.webroot#'!" ) .line( "#logFile#" ); diff --git a/src/cfml/system/modules_app/server-commands/commands/server/set.cfc b/src/cfml/system/modules_app/server-commands/commands/server/set.cfc index ff8f32ee9..b133c746b 100644 --- a/src/cfml/system/modules_app/server-commands/commands/server/set.cfc +++ b/src/cfml/system/modules_app/server-commands/commands/server/set.cfc @@ -84,6 +84,6 @@ component { // Dynamic completion for property name based on contents of server.json function completeProperty() { // all=true will cause "server set" to prompt all possible server.json properties - return ServerService.completeProperty( getCWD(), true ); + return ServerService.completeProperty( getCWD(), true, true ); } } \ No newline at end of file diff --git a/src/cfml/system/modules_app/server-commands/commands/server/start.cfc b/src/cfml/system/modules_app/server-commands/commands/server/start.cfc index 6b82fed58..591ba37a1 100644 --- a/src/cfml/system/modules_app/server-commands/commands/server/start.cfc +++ b/src/cfml/system/modules_app/server-commands/commands/server/start.cfc @@ -54,10 +54,10 @@ component aliases="start" { * @directory web root for this server * @stopPort stop socket listener port number * @force force start if status is not stopped - * @debug sets debug log level + * @debug Turns on debug output while starting and streams server output to console. * @webConfigDir custom location for web context configuration * @serverConfigDir custom location for server configuration - * @libDirs comma-separated list of extra lib directories for the server + * @libDirs comma-separated list of extra lib directories for the server to load * @trayIconFile path to .png file for tray icon * @webXML path to web.xml file used to configure the server * @HTTPEnable enable HTTP @@ -77,6 +77,10 @@ component aliases="start" { * @cfengine.optionsUDF cfengineNameComplete * @WARPath sets the path to an existing war to use * @serverConfigFile The path to the server's JSON file. Created if it doesn't exist. + * @startTimeout The amount of time in seconds to wait for the server to start (in the background). + * @console Start this server in the forground console process and wait until Ctrl-C is pressed to stop it. + * @welcomeFiles A comma-delimited list of default files to load when visiting a directory (index.cfm,index.htm,etc) + * @serverHomeDirectory The folder where the CF engine WAR should be extracted **/ function run( @@ -102,13 +106,17 @@ component aliases="start" { Boolean rewritesEnable, String rewritesConfig, Numeric heapSize, - boolean directoryBrowsing, + Boolean directoryBrowsing, String JVMArgs, String runwarArgs, - boolean saveSettings=true, + Boolean saveSettings=true, String cfengine, String WARPath, - String serverConfigFile + String serverConfigFile, + Numeric startTimeout, + Boolean console, + String welcomeFiles, + String serverHomeDirectory ){ // This is a common mis spelling diff --git a/src/cfml/system/modules_app/server-commands/commands/server/status.cfc b/src/cfml/system/modules_app/server-commands/commands/server/status.cfc index a03836c2d..f37f0eea0 100644 --- a/src/cfml/system/modules_app/server-commands/commands/server/status.cfc +++ b/src/cfml/system/modules_app/server-commands/commands/server/status.cfc @@ -45,6 +45,8 @@ component aliases='status,server info' { * @showAll.hint show all server statuses * @verbose.hint Show extra details * @json Output the server data as json + * @property Name of a specific property to output. JSON default to true if this is used. + * @property.optionsUDF propertyComplete **/ function run( string name, @@ -52,11 +54,14 @@ component aliases='status,server info' { String serverConfigFile, boolean showAll=false, boolean verbose=false, - boolean JSON=false - ){ + boolean JSON=false, + string property='' ){ // Get all server definitions var servers = serverService.getServers(); + // If you specify a property, JSON gets enabled. + arguments.JSON = ( arguments.JSON || len( property ) ); + // Display ALL as JSON? if( arguments.showALL && arguments.json ){ print.line( @@ -92,9 +97,36 @@ component aliases='status,server info' { // Are we doing JSON? if( arguments.json ){ - print.line( - formatterUtil.formatJson( serializeJSON( thisServerInfo ) ) - ); + + // Are we outputing a specific propery + if( len( arguments.property ) ) { + + // If the key doesn't exist, give a useful error + if( !isDefined( 'thisServerInfo.#arguments.property#' ) ) { + error( "The propery [#arguments.property#] isn't defined in the JSON.", "Valid keys are: " & chr( 10 ) & " - " & thisServerInfo.keyList().lCase().listChangeDelims( chr( 10 ) & " - " ) ); + } + + // Output a single property + var thisValue = evaluate( 'thisServerInfo.#arguments.property#' ); + // Output simple values directly so they're useful + if( isSimpleValue( thisValue ) ) { + print.line( thisValue ); + // Format Complex values as JSON + } else { + print.line( + formatterUtil.formatJson( serializeJSON( thisValue ) ) + ); + } + + } else { + + // Output the entire object + print.line( + formatterUtil.formatJson( serializeJSON( thisServerInfo ) ) + ); + + } + continue; } @@ -106,8 +138,14 @@ component aliases='status,server info' { .bold( ')' ); print.indentedLine( thisServerInfo.host & ':' & thisServerInfo.port & ' --> ' & thisServerInfo.webroot ); - + if( len( serverInfo.engineName ) ) { + print.indentedLine( 'CF Engine: ' & serverInfo.engineName & ' ' & serverInfo.engineVersion ); + } + if( len( serverInfo.warPath ) ) { + print.indentedLine( 'WAR Path: ' & serverInfo.warPath ); + } print.line(); + print.indentedLine( 'Last status message: ' ); print.indentedLine( thisServerInfo.statusInfo.result ); if( arguments.verbose ) { @@ -134,5 +172,12 @@ component aliases='status,server info' { function serverNameComplete() { return serverService.getServerNames(); } + + /** + * AutoComplete serverInfo properties + */ + function propertyComplete() { + return serverService.newServerInfoStruct().keyArray(); + } } \ No newline at end of file diff --git a/src/cfml/system/modules_app/system-commands/commands/cfml.cfc b/src/cfml/system/modules_app/system-commands/commands/cfml.cfc index abb47f7f7..d5888057e 100644 --- a/src/cfml/system/modules_app/system-commands/commands/cfml.cfc +++ b/src/cfml/system/modules_app/system-commands/commands/cfml.cfc @@ -135,9 +135,17 @@ component{ // double quotes are \" instead of "". Any escaped double quotes must be converted // to the CFML version to work as an object literal. private function convertJSONEscapesToCFML( required string arg ) { - arguments.arg = replaceNoCase( arguments.arg, '\\', '__DOUBLE_ESCAPE__', 'all' ); + arguments.arg = replaceNoCase( arguments.arg, '\\', '__double_backslash_', 'all' ); + arguments.arg = replaceNoCase( arguments.arg, '\/', '/', 'all' ); arguments.arg = replaceNoCase( arguments.arg, '\"', '""', 'all' ); - arguments.arg = replaceNoCase( arguments.arg, '__DOUBLE_ESCAPE__', '\\', 'all' ); + arguments.arg = replaceNoCase( arguments.arg, '\t', ' ', 'all' ); + arguments.arg = replaceNoCase( arguments.arg, '\n', chr(13)&chr(10), 'all' ); + arguments.arg = replaceNoCase( arguments.arg, '\r', chr(13), 'all' ); + arguments.arg = replaceNoCase( arguments.arg, '\f', chr(12), 'all' ); + arguments.arg = replaceNoCase( arguments.arg, '\b', chr(8), 'all' ); + // This doesn't work-- I'd need to do it in a loop and replace each one individually. Meh... + // arguments.arg = reReplaceNoCase( arguments.arg, '\\u([0-9a-f]{4})', chr( inputBaseN( '\1', 16 ) ), 'all' ); + arguments.arg = replaceNoCase( arguments.arg, '__double_backslash_', '\', 'all' ); return arguments.arg; } diff --git a/src/cfml/system/modules_app/system-commands/commands/config/set.cfc b/src/cfml/system/modules_app/system-commands/commands/config/set.cfc index 701a105e7..4b7805e51 100644 --- a/src/cfml/system/modules_app/system-commands/commands/config/set.cfc +++ b/src/cfml/system/modules_app/system-commands/commands/config/set.cfc @@ -72,6 +72,6 @@ component { // Dynamic completion for property name based on contents of box.json function completeProperty() { // all=true will cause "config set" to prompt all possible commandbox.json settings - return ConfigService.completeProperty( true ); + return ConfigService.completeProperty( true, true ); } } \ No newline at end of file diff --git a/src/cfml/system/modules_app/system-commands/commands/cp.cfc b/src/cfml/system/modules_app/system-commands/commands/cp.cfc index f03bb99be..c04c9b5bd 100644 --- a/src/cfml/system/modules_app/system-commands/commands/cp.cfc +++ b/src/cfml/system/modules_app/system-commands/commands/cp.cfc @@ -39,6 +39,7 @@ component aliases="copy" { // It's a file } else if( fileExists( arguments.path ) ){ // Copy file + DirectoryCreate( getDirectoryFromPath( arguments.newPath ), true, true ); fileCopy( arguments.path, arguments.newPath ); print.greenLine( "File copied to #arguments.newPath#" ); } else { diff --git a/src/cfml/system/modules_app/system-commands/commands/run.cfc b/src/cfml/system/modules_app/system-commands/commands/run.cfc index 00c1d1c0c..ef9ae92e6 100644 --- a/src/cfml/system/modules_app/system-commands/commands/run.cfc +++ b/src/cfml/system/modules_app/system-commands/commands/run.cfc @@ -81,7 +81,7 @@ component{ // Output Results if( !isNull( executeResult ) && len( executeResult ) ) { - print.line( executeResult ); + print.Text( executeResult ); } // Output error if( !isNull( executeError ) && len( executeError ) ) { @@ -94,7 +94,7 @@ component{ executeError = replaceNoCase( executeError, 'bash: no job control in this shell', '' ); executeError = trim( executeError ); } - print.redLine( executeError ); + print.redText( executeError ); } } catch (any e) { diff --git a/src/cfml/system/modules_app/system-commands/commands/tail.cfc b/src/cfml/system/modules_app/system-commands/commands/tail.cfc index 802918b0b..9a552e41f 100644 --- a/src/cfml/system/modules_app/system-commands/commands/tail.cfc +++ b/src/cfml/system/modules_app/system-commands/commands/tail.cfc @@ -15,38 +15,56 @@ component { /** - * @path.hint file or directory to tail - * @lines.hint number of lines to display. + * @path file or directory to tail or raw input to process + * @lines number of lines to display. + * @follow Keep outputting new lines to the file until Ctrl-C is pressed **/ - function run( required path, numeric lines = 15 ){ - + function run( required path, numeric lines = 15, boolean follow = false ){ + var rawText = false; + var inputAsArray = listToArray( arguments.path, chr(13) & chr(10) ); + + // If there is a line break in the input, then it's raw text + if( inputAsArray.len() > 1 ) { + var rawText = true; + } var filePath = fileSystemUtil.resolvePath( arguments.path ); if( !fileExists( filePath ) ){ - return error( "The file does not exist: #arguments.path#" ); + var rawText = true; + } + + // If we're piping raw text and not a file + if( rawText ) { + // Only show the last X lines + var startIndex = max( inputAsArray.len() - arguments.lines + 1, 1 ); + while( startIndex <= inputAsArray.len() ) { + print.line( inputAsArray[ startIndex++ ] ); + } + + return; } + variables.file = createObject( "java", "java.io.File" ).init( filePath ); + + var startPos = findStartPos(); + var startingLength = 0; + try { var lineCounter = 0; var buffer = []; - var file = createObject( "java", "java.io.File" ).init( filePath ); - var randomAccessFile = createObject( "java", "java.io.RandomAccessFile" ).init( file, "r" ); - var position = file.length(); + var randomAccessFile = createObject( "java", "java.io.RandomAccessFile" ).init( variables.file, "r" ); + var startingLength = variables.file.length(); + variables.position = startingLength; // move to the end of the file randomAccessFile.seek( position ); // Was the last character a line feed. // Remeber the CRLFs will be coming in reverse order var lastLF = false; - - while( true ){ - - // stop looping if we have met our line limit or if end of file - if ( position < 0 || lineCounter == arguments.lines ) { - break; - } - + + while( true && startingLength ){ + var char = randomAccessFile.read(); // Only increment CRs that were preceeded by a LF @@ -61,14 +79,28 @@ component { lastLF=false; } if ( char != -1 ) buffer.append( chr( char ) ); + + position--; + + // stop looping if we have met our line limit or if end of file + if ( position < startPos || lineCounter == arguments.lines ) { + break; + } // move to the preceding character - randomAccessFile.seek( position-- ); + randomAccessFile.seek( position ); + } // End while + + if( buffer.len() ) { + // Strip any CR or LF from the last (first really) line to eliminate leading line breaks in console output + buffer[ buffer.len() ] = listChangeDelims( buffer[ buffer.len() ], '', chr(13) & chr( 10 ) ); } // print our file to console - print.line( buffer.reverse().toList( "" ) ); + print + .text( buffer.reverse().toList( "" ) ) + .toConsole(); } finally { @@ -76,6 +108,144 @@ component { randomAccessFile.close(); } } + + // If we're not following the file, just bail here. + if( !follow ) { + if( buffer.len() ) { + print.line(); + } + return; + } + + position = startingLength; + // This lets the thread know we're still running + variables.tailRun = true; + + try { + // This thread will keep redrawing the screen while the main thread waits for user input + threadName = 'tail#createUUID()#'; + thread action="run" name=threadName priority="HIGH" { + try{ + // Only keep drawing as long as the main thread is active + while( variables.tailRun ) { + + var randomAccessFile = createObject( "java", "java.io.RandomAccessFile" ).init( file, "r" ); + randomAccessFile.seek( position ); + // As long as there is at least one more character in the file + while( ( var char = randomAccessFile.read() ) > -1 ){ + // output it + print + .text( chr( char ) ) + .toConsole(); + + randomAccessFile.seek( ++position ); + } + // Close the file every time so we don't keep it open and locked + randomAccessFile.close(); + + // Decrease this to speed up the Tail + sleep( 300 ); + } + } catch( any e ) { + logger.error( e.message & ' ' & e.detail, e.stacktrace ); + } finally { + // Clean up after ourselves if anything went wrong + if( isDefined( 'randomAccessFile' ) ) { + randomAccessFile.close(); + } + } + + } // End thread + + while( true ) { + // Wipe out prompt so it doesn't redraw if the user hits enter + shell.getReader().setPrompt( '' ); + + // Detect user pressing Ctrl-C + // Any other characters captured will be ignored + var line = shell.getReader().readLine(); + if( line == 'q' ) { + break; + } else { + print.boldRedLine( 'To exit press Ctrl-C or "q" followed the enter key.' ).toConsole(); + } + } + + + // user wants to exit, they've pressed Ctrl-C + } catch ( jline.console.UserInterruptException e ) { + // make sure the thread exits + variables.tailRun = false; + // Wait until the thread finishes its last draw + thread action="join" name=threadName; + shell.setPrompt(); + // Something horrible went wrong + } catch ( any e ) { + // make sure the thread exits + variables.tailRun = false; + // Wait until the thread finishes its last draw + thread action="join" name=threadName; + shell.setPrompt(); + rethrow; + } + + // We're done with the Tail, clean up. + variables.tailRun = false; + // Wait until the thread finishes its last draw + thread action="join" name=threadName; + shell.setPrompt(); + + } + + // Deal with BOM (Byte order mark) + // TODO: Actually pay attention to the BOM! + function findStartPos() { + var randomAccessFile = createObject( "java", "java.io.RandomAccessFile" ).init( file, "r" ); + randomAccessFile.seek( 0 ); + var length = randomAccessFile.length(); + var startPos = 0 + ; + // Will contain the first few bytes of the file represented by an integer + var peek = ''; + + // If the file has a least 2 bytes + if( length > 1 ) { + // read them + peek &= randomAccessFile.read(); + randomAccessFile.seek( 1 ); + peek &= randomAccessFile.read(); + // If we found one of the 3 char BOMs + if( listFindNoCase( '254255,255254', peek ) ) { + // Start after it + startPos=2; + } + } + + // If the file has a least 3 bytes + if( length > 2 && ! startPos ) { + // read them + randomAccessFile.seek( 2 ); + peek &= randomAccessFile.read(); + // If we found one of the 3 char BOMs + if( listFindNoCase( '239187191', peek ) ) { + // Start after it + startPos=3; + } + } + // If the file has at least 4 bytes and we didn't find a 3 byte BOM + if( length > 3 && ! startPos) { + // Read the fourth byte + randomAccessFile.seek( 3 ); + peek &= randomAccessFile.read(); + // If we found one of the 4 char BOMs + if( listFindNoCase( '00254255,25525400', peek ) ) { + // Start after it + startPos=4; + } + } + + randomAccessFile.close(); + return startPos; } } \ No newline at end of file diff --git a/src/cfml/system/modules_app/testbox-commands/commands/testbox/help.cfc b/src/cfml/system/modules_app/testbox-commands/commands/testbox/help.cfc index b342a3d55..f8ca61870 100644 --- a/src/cfml/system/modules_app/testbox-commands/commands/testbox/help.cfc +++ b/src/cfml/system/modules_app/testbox-commands/commands/testbox/help.cfc @@ -3,7 +3,7 @@ component excludeFromHelp=true { function run() { print.line(); - print.yellow( 'The ' ); print.boldYellow( 'testbox' ); print.yellowLine( ' namespace helps you do anything related to your TestBox isntallation. Use these commands' ); + print.yellow( 'The ' ); print.boldYellow( 'testbox' ); print.yellowLine( ' namespace helps you do anything related to your TestBox installation. Use these commands' ); print.yellowLine( 'to create tests, generate runners, and even run your tests for you from the command line.' ); print.yellowLine( 'Type help before any command name to get additional information on how to call that specific command.' ); @@ -13,4 +13,4 @@ component excludeFromHelp=true { } -} \ No newline at end of file +} diff --git a/src/cfml/system/services/ArtifactService.cfc b/src/cfml/system/services/ArtifactService.cfc index 7689f1872..fbf940d5b 100644 --- a/src/cfml/system/services/ArtifactService.cfc +++ b/src/cfml/system/services/ArtifactService.cfc @@ -21,6 +21,8 @@ component accessors="true" singleton { property name='packageService' inject='PackageService'; property name='logger' inject='logbox:logger:{this}'; property name="semanticVersion" inject="semanticVersion"; + // COMMANDBOX-479 + property name="configService" inject="ConfigService"; /** @@ -29,8 +31,9 @@ component accessors="true" singleton { function onDIComplete() { // Create the artifacts directory if it doesn't exist - if( !directoryExists( variables.artifactDir ) ) { - directoryCreate( variables.artifactDir ); + // COMMANDBOX-479 + if( !directoryExists( getArtifactsDirectory() ) ) { + directoryCreate( getArtifactsDirectory() ); } } @@ -42,7 +45,8 @@ component accessors="true" singleton { */ struct function listArtifacts( packageName='' ) { var result = {}; - var dirList = directoryList( path=variables.artifactDir, recurse=false, listInfo='query', sort='name asc' ); + // COMMANDBOX-479 + var dirList = directoryList( path=getArtifactsDirectory(), recurse=false, listInfo='query', sort='name asc' ); for( var dir in dirList ) { if( dir.type == 'dir' && ( !arguments.packageName.len() || arguments.packageName == dir.name ) ) { @@ -67,7 +71,8 @@ component accessors="true" singleton { * Removes all artifacts from the cache and returns the number of wiped out directories */ numeric function cleanArtifacts() { - var qryDir = directoryList( path=variables.artifactDir, recurse=false, listInfo='query' ); + // COMMANDBOX-479 + var qryDir = directoryList( path=getArtifactsDirectory(), recurse=false, listInfo='query' ); var numRemoved = 0; for( var path in qryDir ) { @@ -110,7 +115,8 @@ component accessors="true" singleton { */ function getPackagePath( required packageName, version="" ){ // This will likely change, so I'm only going to put the code here. - var path = variables.artifactDir & '/' & arguments.packageName; + // COMMANDBOX-479 + var path = getArtifactsDirectory() & '/' & arguments.packageName; // do we have a version? if( arguments.version.len() ){ path &= "/" & arguments.version; @@ -261,5 +267,8 @@ component accessors="true" singleton { } } - + // COMMANDBOX-479 + string function getArtifactsDirectory() { + return configService.getSetting( 'artifactsDirectory', variables.artifactDir ); + } } \ No newline at end of file diff --git a/src/cfml/system/services/CommandService.cfc b/src/cfml/system/services/CommandService.cfc index e10ff28e9..08e82fbcd 100644 --- a/src/cfml/system/services/CommandService.cfc +++ b/src/cfml/system/services/CommandService.cfc @@ -156,10 +156,6 @@ component accessors="true" singleton { **/ function runCommand( required array commandChain, required string line, string piped ){ - if( structKeyExists( arguments, 'piped' ) ) { - var result = arguments.piped; - } - // If nothing is returned, something bad happened (like an error instatiating the CFC) if( !commandChain.len() ){ return 'Command not run.'; @@ -169,6 +165,12 @@ component accessors="true" singleton { // default behavior is to keep trucking var previousCommandSeparator = ';'; var lastCommandErrored = false; + + if( structKeyExists( arguments, 'piped' ) ) { + var result = arguments.piped; + previousCommandSeparator = '|'; + } + // If piping commands, each one will be an item in the chain. // i.e. forgebox show | grep | more // Would result in three separate, chained commands. diff --git a/src/cfml/system/services/ConfigService.cfc b/src/cfml/system/services/ConfigService.cfc index e62e06dc3..812c0b1d0 100644 --- a/src/cfml/system/services/ConfigService.cfc +++ b/src/cfml/system/services/ConfigService.cfc @@ -50,7 +50,9 @@ component accessors="true" singleton { 'endpoints.forgebox.APIURL', // Servers 'server', - 'server.defaults' + 'server.defaults', + // used in Artifactsservice + 'artifactsDirectory' ]); setConfigFilePath( '/commandbox-home/CommandBox.json' ); @@ -154,9 +156,10 @@ component accessors="true" singleton { /** * Dynamic completion for property name based on contents of commandbox.json - * @all.hint Pass false to ONLY suggest existing setting names. True will suggest all possible settings. + * @all Pass false to ONLY suggest existing setting names. True will suggest all possible settings. + * @asSet Pass true to add = to the end of the options */ - function completeProperty( all=false ) { + function completeProperty( all=false, asSet=false ) { // Get all config settings currently set var props = JSONService.addProp( [], '', '', getConfigSettings() ); @@ -165,7 +168,9 @@ component accessors="true" singleton { // ... Then add them in props.append( getPossibleConfigSettings(), true ); } - + if( asSet ) { + props = props.map( function( i ){ return i &= '='; } ); + } return props; } } \ No newline at end of file diff --git a/src/cfml/system/services/InterceptorService.cfc b/src/cfml/system/services/InterceptorService.cfc index 69d811999..baca2c8f6 100644 --- a/src/cfml/system/services/InterceptorService.cfc +++ b/src/cfml/system/services/InterceptorService.cfc @@ -11,6 +11,7 @@ component accessors=true singleton { property name='Shell'; property name='EventPoolManager'; property name='InterceptionPoints'; + property name='configured' type="boolean" default="false"; // DI property name='log' inject='logbox:logger:{this}'; @@ -20,6 +21,8 @@ component accessors=true singleton { */ InterceptorService function init( required shell ) { + setConfigured( false ); + setShell( arguments.shell ); setInterceptionPoints( [ @@ -30,7 +33,7 @@ component accessors=true singleton { // Module lifecycle 'preModuleLoad','postModuleLoad','preModuleUnLoad','postModuleUnload', // Server lifecycle - 'onServerStart','onServerInstall','onServerStop', + 'preServerStart','onServerStart','onServerInstall','onServerStop', // Error handling 'onException', // Package lifecycle @@ -44,6 +47,7 @@ component accessors=true singleton { function configure() { setEventPoolManager( getShell().getWireBox().getEventManager() ); appendInterceptionPoints( getInterceptionPoints().toList() ); + setConfigured( true ); return this; } diff --git a/src/cfml/system/services/JSONService.cfc b/src/cfml/system/services/JSONService.cfc index 673ddb918..a1a4f4c82 100644 --- a/src/cfml/system/services/JSONService.cfc +++ b/src/cfml/system/services/JSONService.cfc @@ -65,7 +65,7 @@ component accessors="true" singleton { // The target property we're trying to append to var targetProperty = evaluate( fullPropertyName ); // The value we want to append - var complexValue = deserializeJSON( arguments.properties[ prop ] ); + var complexValue = deserializeJSON( propertyValue ); // The target property is not simple, and matches the same data type as the incoming data if( !isSimpleValue( targetProperty ) && ( isArray( targetProperty ) == isArray( complexValue ) ) ) { // Make this idempotent so arrays don't get duplicate values @@ -82,17 +82,23 @@ component accessors="true" singleton { } else { targetProperty.append( complexValue, true ); } - results.append( '#arguments.properties[ prop ]# appended to #prop#' ); + results.append( '#propertyValue# appended to #prop#' ); continue; } } // If any of the ifs above fail, we'll fall back through to this - evaluate( '#fullPropertyName# = deserializeJSON( arguments.properties[ prop ] )' ); + + // Double check if value is really JSON due to Lucee bug + if( listFind( '",{,[', left( propertyValue, 1 ) ) ) { + evaluate( '#fullPropertyName# = deserializeJSON( propertyValue )' ); + } else { + evaluate( '#fullPropertyName# = propertyValue' ); + } } else { - evaluate( '#fullPropertyName# = arguments.properties[ prop ]' ); + evaluate( '#fullPropertyName# = propertyValue' ); } - results.append( 'Set #prop# = #arguments.properties[ prop ]#' ); + results.append( 'Set #prop# = #propertyValue#' ); } return results; } diff --git a/src/cfml/system/services/PackageService.cfc b/src/cfml/system/services/PackageService.cfc index 66835e335..f46a89812 100644 --- a/src/cfml/system/services/PackageService.cfc +++ b/src/cfml/system/services/PackageService.cfc @@ -61,8 +61,10 @@ component accessors="true" singleton { * @verbose If set, it will produce much more verbose information about the package installation * @force When set to true, it will force dependencies to be installed whether they already exist or not * @packagePathRequestingInstallation If installing smart dependencies packages (like ColdBox modules) that are capable of being nested, this is our current level + * + * @returns True if no errors encountered, false if things went boom. **/ - function installPackage( + boolean function installPackage( required string ID, string directory, boolean save=false, @@ -74,7 +76,7 @@ component accessors="true" singleton { string packagePathRequestingInstallation = arguments.currentWorkingDirectory ){ - interceptorService.announceInterception( 'preInstall', { installArgs=arguments } ); + interceptorService.announceInterception( 'preInstall', { installArgs=arguments, packagePathRequestingInstallation=packagePathRequestingInstallation } ); // If there is a package to install, install it if( len( arguments.ID ) ) { @@ -91,7 +93,7 @@ component accessors="true" singleton { var endpointData = endpointService.resolveEndpoint( arguments.ID, arguments.currentWorkingDirectory ); } catch( EndpointNotFound var e ) { consoleLogger.error( e.message ); - return; + return false; } consoleLogger.info( '.'); @@ -104,7 +106,7 @@ component accessors="true" singleton { // but I don't want to "blow up" the console with a full error. } catch( endpointException var e ) { consoleLogger.error( e.message & ' ' & e.detail ); - return; + return false; } // Support box.json in the root OR in a subfolder (NPM-style!) @@ -190,7 +192,7 @@ component accessors="true" singleton { // Does the package that we found satisfy what we need? if( semanticVersion.satisfies( candidateBoxJSON.version, version ) ) { consoleLogger.warn( '#packageName# (#version#) is already satisfied by #candidateInstallPath# (#candidateBoxJSON.version#). Skipping installation.' ); - return; + return true; } } } @@ -243,7 +245,8 @@ component accessors="true" singleton { artifactDescriptor = artifactDescriptor, ignorePatterns = ignorePatterns, endpointData = endpointData, - artifactPath = tmpPath + artifactPath = tmpPath, + packagePathRequestingInstallation = packagePathRequestingInstallation }; interceptorService.announceInterception( 'onInstall', interceptData ); // Make sure these get set back into their original variables in case the interceptor changed them. @@ -272,6 +275,10 @@ component accessors="true" singleton { // ContentBox Widget } else if( packageType == 'contentbox-widgets' ) { installDirectory = arguments.packagePathRequestingInstallation & '/modules/contentbox/widgets'; + // widgets just get dumped in + artifactDescriptor.createPackageDirectory = false; + // Don't trash the widgets folder with this + ignorePatterns.append( '/box.json' ); // ContentBox themes/layouts } else if( packageType == 'contentbox-themes' || packageType == 'contentbox-layouts' ) { installDirectory = arguments.packagePathRequestingInstallation & '/modules/contentbox/themes'; @@ -350,7 +357,7 @@ component accessors="true" singleton { directoryDelete( tmpPath, true ); } consoleLogger.warn( "The package #packageName# is already installed at #installDirectory#. Skipping installation. Use --force option to force install." ); - return; + return true; } } @@ -483,8 +490,8 @@ component accessors="true" singleton { consoleLogger.info( "No dependencies found to install, but it's the thought that counts, right?" ); } - interceptorService.announceInterception( 'postInstall', { installArgs=arguments } ); - + interceptorService.announceInterception( 'postInstall', { installArgs=arguments, installDirectory=installDirectory } ); + return true; } // DRY @@ -538,9 +545,7 @@ component accessors="true" singleton { required string currentWorkingDirectory, string packagePathRequestingUninstallation = arguments.currentWorkingDirectory ){ - - interceptorService.announceInterception( 'preUninstall', { uninstallArgs=arguments } ); - + // In case someone types "uninstall coldbox@4.0.0" var packageName = listFirst( arguments.ID, '@' ); @@ -563,6 +568,9 @@ component accessors="true" singleton { uninstallDirectory = fileSystemUtil.resolvePath( installPaths[ packageName ] ); } } + + // Wait to run this until we've decided where the package lives that's being uninstalled. + interceptorService.announceInterception( 'preUninstall', { uninstallArgs=arguments, uninstallDirectory=uninstallDirectory } ); // See if the package exists here if( len( uninstallDirectory ) && directoryExists( uninstallDirectory ) ) { @@ -1025,8 +1033,9 @@ component accessors="true" singleton { * Dynamic completion for property name based on contents of box.json * @directory The package root * @all Pass false to ONLY suggest existing property names. True will suggest all possible box.json properties. + * @asSet Pass true to add = to the end of the options */ - function completeProperty( required directory, all=false ) { + function completeProperty( required directory, all=false, asSet=false ) { var props = []; // Check and see if box.json exists @@ -1038,6 +1047,9 @@ component accessors="true" singleton { } props = JSONService.addProp( props, '', '', boxJSON ); } + if( asSet ) { + props = props.map( function( i ){ return i &= '='; } ); + } return props; } diff --git a/src/cfml/system/services/ServerEngineService.cfc b/src/cfml/system/services/ServerEngineService.cfc index f8d930133..51f5ad876 100644 --- a/src/cfml/system/services/ServerEngineService.cfc +++ b/src/cfml/system/services/ServerEngineService.cfc @@ -26,20 +26,22 @@ component accessors="true" singleton="true" { * * @cfengine CFML Engine name (lucee, adobe, railo) * @baseDirectory base directory for server install + * @serverInfo The struct of server settings + * @serverHomeDirectory Override where the server's home with be **/ - public function install( required cfengine, required baseDirectory ) { + public function install( required cfengine, required baseDirectory, required struct serverInfo, required string serverHomeDirectory ) { var version = listLen( cfengine, "@" )>1 ? listLast( cfengine, "@" ) : ""; - var engineName = listFirst(cfengine,"@"); + var engineName = listFirst( cfengine, "@" ); arguments.baseDirectory = !arguments.baseDirectory.endsWith( "/" ) ? arguments.baseDirectory & "/" : arguments.baseDirectory; if( engineName == "adobe" ) { - return installAdobe( destination=arguments.baseDirectory, version=version ); + return installAdobe( destination=arguments.baseDirectory, version=version, serverInfo=serverInfo, serverHomeDirectory=serverHomeDirectory ); } else if (engineName == "railo") { - return installRailo( destination=arguments.baseDirectory, version=version ); + return installRailo( destination=arguments.baseDirectory, version=version, serverInfo=serverInfo, serverHomeDirectory=serverHomeDirectory ); } else if (engineName == "lucee") { - return installLucee( destination=arguments.baseDirectory, version=version ); + return installLucee( destination=arguments.baseDirectory, version=version, serverInfo=serverInfo, serverHomeDirectory=serverHomeDirectory ); } else { - return installEngineArchive( cfengine, arguments.baseDirectory ); + return installEngineArchive( cfengine, arguments.baseDirectory, serverInfo, serverHomeDirectory ); } } @@ -48,20 +50,19 @@ component accessors="true" singleton="true" { * * @destination target directory * @version Version number or empty to use default + * @serverInfo Struct of server settings + * @serverHomeDirectory Override where the server's home with be **/ - public function installAdobe( required destination, required version ) { - var installDetails = installEngineArchive( 'adobe@#version#', destination ); + public function installAdobe( required destination, required version, required struct serverInfo, required string serverHomeDirectory ) { + var installDetails = installEngineArchive( 'adobe@#version#', destination, serverInfo, serverHomeDirectory ); - // set password to "commandbox" - // TODO: Just make this changes directly in the WAR files - fileWrite( installDetails.installDir & "/WEB-INF/cfusion/lib/password.properties", "rdspassword=#cr#password=commandbox#cr#encrypted=false" ); // set flex log dir to prevent WEB-INF/cfform being created in project dir if (fileExists(installDetails.installDir & "/WEB-INF/cfform/flex-config.xml")) { var flexConfig = fileRead(installDetails.installDir & "/WEB-INF/cfform/flex-config.xml"); - if(!installDetails.internal && installDetails.initialInstall ) { + if( installDetails.initialInstall ) { flexConfig = replace(flexConfig, "/WEB-INF/", installDetails.installDir & "/WEB-INF/","all"); - - fileWrite(installDetails.installDir & "/WEB-INF/cfform/flex-config.xml", flexConfig); + fileWrite(installDetails.installDir & "/WEB-INF/cfform/flex-config.xml", flexConfig); } else { // TODO: Remove this ELSE block in a future revision. // This will fix the flex-config.xml files that have been corrupted because we weren't checking initialInstall, above. @@ -81,12 +82,15 @@ component accessors="true" singleton="true" { * * @destination target directory * @version Version number or empty to use default + * @serverInfo struct of server settings + * @serverHomeDirectory Override where the server's home with be **/ - public function installLucee( required destination, required version ) { - var installDetails = installEngineArchive( 'lucee@#version#', destination ); + public function installLucee( required destination, required version, required struct serverInfo, required string serverHomeDirectory ) { + var installDetails = installEngineArchive( 'lucee@#version#', destination, serverInfo, serverHomeDirectory ); - if( !installDetails.internal && installDetails.initialInstall ) { - configureWebXML( cfengine="lucee", version=installDetails.version, source="#installDetails.installDir#/WEB-INF/web.xml", destination="#installDetails.installDir#/WEB-INF/web.xml" ); } + if( installDetails.initialInstall ) { + configureWebXML( cfengine="lucee", version=installDetails.version, source=serverInfo.webXML, destination=serverInfo.webXML, serverInfo=serverInfo ); + } return installDetails; } @@ -95,11 +99,14 @@ component accessors="true" singleton="true" { * * @destination target directory * @version Version number or empty to use default + * @serverInfo struct of server settings + * @serverHomeDirectory Override where the server's home with be **/ - public function installRailo( required destination, required version ) { - var installDetails = installEngineArchive( 'railo@#version#', destination ); + public function installRailo( required destination, required version, required struct serverInfo, required string serverHomeDirectory ) { + var installDetails = installEngineArchive( 'railo@#version#', destination, serverInfo, serverHomeDirectory ); + if( installDetails.initialInstall ) { - configureWebXML( cfengine="railo", version=installDetails.version, source="#installDetails.installDir#/WEB-INF/web.xml", destination="#installDetails.installDir#/WEB-INF/web.xml" ); + configureWebXML( cfengine="railo", version=installDetails.version, source=serverInfo.webXML, destination=serverInfo.webXML, serverInfo=serverInfo ); } return installDetails; } @@ -113,11 +120,13 @@ component accessors="true" singleton="true" { */ function installEngineArchive( required string ID, - required string destination + required string destination, + required struct serverInfo, + required string serverHomeDirectory ) { var installDetails = { - internal : false, + engineName : '', version : '', installDir : '', initialInstall : false @@ -129,6 +138,7 @@ component accessors="true" singleton="true" { var endpointData = endpointService.resolveEndpoint( ID, shell.pwd() ); var endpoint = endpointData.endpoint; var engineName = endpoint.getDefaultName( arguments.ID ); + installDetails.engineName = engineName; // In order to prevent uneccessary work, we're going to try REALLY hard to figure out exactly what engine will be installed // before it actually happens so we can skip this whole mess if it's already in place. @@ -137,13 +147,15 @@ component accessors="true" singleton="true" { var version = endpoint.parseVersion( arguments.ID ); // If the user gave us an exact version, just use it! - if( semanticVersion.isExactVersion( version ) ) { - var satisfyingVersion = version; + // Require buildID like 5.1.0+34 + if( semanticVersion.isExactVersion( version=version, includeBuildID=true ) ) { + var satisfyingVersion = version; } else { - consoleLogger.info( "Contacting ForgeBox to determine the best version match for [#version#]. Use an exact 'cfengine' version to skip this check."); + consoleLogger.warn( "Contacting ForgeBox to determine the latest & greatest version of [#engineName##( len( version ) ? ' ' : '' )##version#]... Use an exact 'cfengine' version to skip this check."); // If ForgeBox is down, don't rain on people's parade. try { var satisfyingVersion = endpoint.findSatisfyingVersion( endpoint.parseSlug( arguments.ID ), version ).version; + consoleLogger.info( "OK, [#engineName# #satisfyingVersion#] it is!"); } catch( any var e ) { consoleLogger.error( "."); @@ -163,34 +175,103 @@ component accessors="true" singleton="true" { } } } - installDetails.installDir = destination & engineName & "-" & replace( satisfyingVersion, '+', '.', 'all' ); + // Overriding server home which is where the exploded war lives + if( len( arguments.serverHomeDirectory ) ) { + installDetails.installDir = arguments.serverHomeDirectory; + // Default is engine-version folder in base dir + } else { + installDetails.installDir = destination & engineName & "-" & replace( satisfyingVersion, '+', '.', 'all' ); + } installDetails.version = satisfyingVersion; + + var thisEngineTag = installDetails.engineName & '@' & installDetails.version; } else { // For all other endpoints, create a predictable folder based on the endpoint ID. // If the file that the endpoint points to changes, you'll have to forget the server to pick up changes. // The alternative is re-downloading the engine EVERY. SINGLE. TIME. - installDetails.installDir = destination & engineName; + // Overriding server home which is where the exploded war lives + if( len( arguments.serverHomeDirectory ) ) { + installDetails.installDir = arguments.serverHomeDirectory; + // Default is engine-version folder in base dir + } else { + installDetails.installDir = destination & engineName; + } + + var thisEngineTag = arguments.ID; } - // If we're starting a Lucee server whose version matches the CLI engine, then don't download anyting, we're using internal jars. - if( listFirst( arguments.ID, '@' ) == 'lucee' && server.lucee.version == replace( installDetails.version, '+', '.', 'all' ) ) { - installDetails.internal = true; - return installDetails; + // Set default web.xml path now that we have an install dir + if( !len( serverInfo.webXML ) ) { + serverInfo.webXML = "#installDetails.installDir#/WEB-INF/web.xml"; } + // Set up server and web context dirs if Railo or Lucee + if( serverinfo.cfengine contains 'lucee' || serverinfo.cfengine contains 'railo' ) { + // Default web context + if( !len( serverInfo.webConfigDir ) ) { + serverInfo.webConfigDir = "/WEB-INF/#lcase( listFirst( serverinfo.cfengine, "@" ) )#-web"; + } + // Default server context + if( !len( serverInfo.serverConfigDir ) ) { + serverInfo.serverConfigDir = "/WEB-INF"; + } + // Make relative to WEB-INF if possible + serverInfo.webConfigDir = replace( serverInfo.webConfigDir, '\', '/', 'all' ); + serverInfo.serverConfigDir = replace( serverInfo.serverConfigDir, '\', '/', 'all' ); + installDetails.installDir = replace( installDetails.installDir, '\', '/', 'all' ); + + serverInfo.webConfigDir = replace( serverInfo.webConfigDir, installDetails.installDir, '' ); + serverInfo.serverConfigDir = replace( serverInfo.serverConfigDir, installDetails.installDir, '' ); + } + + var engineTagFile = installDetails.installDir & '/.engineInstall'; // Check to see if this WAR has already been exploded - if( fileExists( installDetails.installDir & '/WEB-INF/web.xml' ) ) { + if( fileExists( engineTagFile ) ) { + + // Check and see if another version of this engine has already been started in the server home. + var previousEngineTag = fileRead( engineTagFile ); + if( previousEngineTag != thisEngineTag ) { + consoleLogger.warn( "You've asked for the engine [#thisEngineTag#] to be started," ); + consoleLogger.warn( "but this server home already has [#previousEngineTag#] deployed to it!" ); + consoleLogger.warn( "In orer to get the new version, you need to run 'server forget' on this server and start it again." ); + } + consoleLogger.info( "WAR/zip archive already installed."); + return installDetails; } - + // Install the engine via our standard package service installDetails.initialInstall = true; - packageService.installPackage( ID=arguments.ID, directory=thisTempDir, save=false ); - + + // If we're starting a Lucee server whose version matches the CLI engine, then don't download anything, we're using internal jars. + if( listFirst( arguments.ID, '@' ) == 'lucee' && server.lucee.version == replace( installDetails.version, '+', '.', 'all' ) ) { + + consoleLogger.info( "Building a WAR from local jars."); + + // Spoof a WAR file. + var thisWebinf = installDetails.installDir & '/WEB-INF'; + var thislib = thisWebinf & '/lib'; + var thiServerContext = thisWebinf & '/server-context'; + var thiWebContext = thisWebinf & '/web-context'; + + directoryCreate( installDetails.installDir & '/WEB-INF', true, true ); + directoryCopy( '/commandbox-home/lib', thislib, false, '*.jar' ); + fileCopy( '/commandbox/system/config/web.xml', thisWebinf & '/web.xml'); + + // Mark this WAR as being exploded already + fileWrite( engineTagFile, thisEngineTag ); + + return installDetails; + } + + if( !packageService.installPackage( ID=arguments.ID, directory=thisTempDir, save=false ) ) { + throw( message='Server not installed.', type="commandException"); + } + // Look for a war or zip archive inside the package var theArchive = ''; for( var thisFile in directoryList( thisTempDir ) ) { @@ -206,7 +287,11 @@ component accessors="true" singleton="true" { } consoleLogger.info( "Exploding WAR/zip archive..."); - zip action="unzip" file="#theArchive#" destination="#installDetails.installDir#" overwrite="true"; + directoryCreate( installDetails.installDir, true, true ); + zip action="unzip" file="#theArchive#" destination="#installDetails.installDir#" overwrite="false"; + + // Mark this WAR as being exploded already + fileWrite( engineTagFile, thisEngineTag ); // Catch this to gracefully handle where the OS or another program // has the folder locked. @@ -221,20 +306,27 @@ component accessors="true" singleton="true" { /** * configure web.xml file for Lucee and Railo - * TODO: Just make these changes directly in the WAR files * * @cfengine lucee or railo * @source source web.xml * @destination target web.xml **/ - public function configureWebXML( required cfengine, required version, required source, destination ) { + public function configureWebXML( required cfengine, required version, required source, required destination, required struct serverInfo ) { var webXML = XMLParse( source ); var servlets = xmlSearch(webXML,"//:servlet-class[text()='#lcase( cfengine )#.loader.servlet.CFMLServlet']"); var initParam = xmlElemnew(webXML,"http://java.sun.com/xml/ns/javaee","init-param"); initParam.XmlChildren[1] = xmlElemnew(webXML,"param-name"); initParam.XmlChildren[1].XmlText = "#lcase( cfengine )#-web-directory"; initParam.XmlChildren[2] = xmlElemnew(webXML,"param-value"); - initParam.XmlChildren[2].XmlText = "/WEB-INF/#lcase( cfengine )#/{web-context-label}"; + initParam.XmlChildren[2].XmlText = serverInfo.webConfigDir; + arrayInsertAt(servlets[1].XmlParent.XmlChildren,4,initParam); + + var servlets = xmlSearch(webXML,"//:servlet-class[text()='#lcase( cfengine )#.loader.servlet.CFMLServlet']"); + var initParam = xmlElemnew(webXML,"http://java.sun.com/xml/ns/javaee","init-param"); + initParam.XmlChildren[1] = xmlElemnew(webXML,"param-name"); + initParam.XmlChildren[1].XmlText = "#lcase( cfengine )#-server-directory"; + initParam.XmlChildren[2] = xmlElemnew(webXML,"param-value"); + initParam.XmlChildren[2].XmlText = serverInfo.serverConfigDir; arrayInsertAt(servlets[1].XmlParent.XmlChildren,4,initParam); // Lucee 5+ has a LuceeServlet as well as will create the WEB-INF by default in your web root @@ -244,7 +336,15 @@ component accessors="true" singleton="true" { initParam.XmlChildren[1] = xmlElemnew(webXML,"param-name"); initParam.XmlChildren[1].XmlText = "#lcase( cfengine )#-web-directory"; initParam.XmlChildren[2] = xmlElemnew(webXML,"param-value"); - initParam.XmlChildren[2].XmlText = "/WEB-INF/#lcase( cfengine )#/{web-context-label}"; + initParam.XmlChildren[2].XmlText = serverInfo.webConfigDir; + arrayInsertAt(servlets[1].XmlParent.XmlChildren,4,initParam); + + var servlets = xmlSearch(webXML,"//:servlet-class[text()='#lcase( cfengine )#.loader.servlet.LuceeServlet']"); + var initParam = xmlElemnew(webXML,"http://java.sun.com/xml/ns/javaee","init-param"); + initParam.XmlChildren[1] = xmlElemnew(webXML,"param-name"); + initParam.XmlChildren[1].XmlText = "#lcase( cfengine )#-server-directory"; + initParam.XmlChildren[2] = xmlElemnew(webXML,"param-value"); + initParam.XmlChildren[2].XmlText = serverInfo.serverConfigDir; arrayInsertAt(servlets[1].XmlParent.XmlChildren,4,initParam); } diff --git a/src/cfml/system/services/ServerService.cfc b/src/cfml/system/services/ServerService.cfc index fdb490021..e913f03dc 100644 --- a/src/cfml/system/services/ServerService.cfc +++ b/src/cfml/system/services/ServerService.cfc @@ -19,10 +19,6 @@ component accessors="true" singleton { */ property name="serverConfig"; /** - * Where core and custom servers are stored - */ - property name="serverHomeDirectory"; - /** * Where custom servers are stored */ property name="customServerDirectory"; @@ -44,6 +40,7 @@ component accessors="true" singleton { property name='consoleLogger' inject='logbox:logger:console'; property name='wirebox' inject='wirebox'; property name='CR' inject='CR@constants'; + property name='parser' inject='parser'; /** * Constructor @@ -82,8 +79,6 @@ component accessors="true" singleton { variables.homeDir = arguments.homeDir; // the lib dir location, populated from shell later. variables.libDir = arguments.homeDir & "/lib"; - // Where core server is installed - variables.serverHomeDirectory = arguments.homeDir & "/engine/cfml/server/"; // Where custom server configs are stored variables.serverConfig = arguments.homeDir & "/servers.json"; // Where custom servers are stored @@ -115,53 +110,56 @@ component accessors="true" singleton { var d = ConfigService.getSetting( 'server.defaults', {} ); return { - name : d.name ?: '', - openBrowser : d.openBrowser ?: true, - stopsocket : d.stopsocket ?: 0, - debug : d.debug ?: false, - trayicon : d.trayicon ?: '', + 'name' : d.name ?: '', + 'openBrowser' : d.openBrowser ?: true, + 'startTimeout' : 240, + 'stopsocket' : d.stopsocket ?: 0, + 'debug' : d.debug ?: false, + 'trayicon' : d.trayicon ?: '', // Duplicate so onServerStart interceptors don't actually change config settings via refernce. - trayOptions : duplicate( d.trayOptions ?: [] ), - jvm : { - heapSize : d.jvm.heapSize ?: 512, - args : d.jvm.args ?: '' + 'trayOptions' : duplicate( d.trayOptions ?: [] ), + 'jvm' : { + 'heapSize' : d.jvm.heapSize ?: 512, + 'args' : d.jvm.args ?: '' }, - web : { - host : d.web.host ?: '127.0.0.1', - directoryBrowsing : d.web.directoryBrowsing ?: true, - webroot : d.web.webroot ?: '', + 'web' : { + 'host' : d.web.host ?: '127.0.0.1', + 'directoryBrowsing' : d.web.directoryBrowsing ?: true, + 'webroot' : d.web.webroot ?: '', // Duplicate so onServerStart interceptors don't actually change config settings via refernce. - aliases : duplicate( d.web.aliases ?: {} ), + 'aliases' : duplicate( d.web.aliases ?: {} ), // Duplicate so onServerStart interceptors don't actually change config settings via refernce. - errorPages : duplicate( d.web.errorPages ?: {} ), - http : { - port : d.web.http.port ?: 0, - enable : d.web.http.enable ?: true + 'errorPages' : duplicate( d.web.errorPages ?: {} ), + 'welcomeFiles' : d.web.welcomeFiles ?: '', + 'http' : { + 'port' : d.web.http.port ?: 0, + 'enable' : d.web.http.enable ?: true }, - ssl : { - enable : d.web.ssl.enable ?: false, - port : d.web.ssl.port ?: 1443, - cert : d.web.ssl.cert ?: '', - key : d.web.ssl.key ?: '', - keyPass : d.web.ssl.keyPass ?: '' + 'ssl' : { + 'enable' : d.web.ssl.enable ?: false, + 'port' : d.web.ssl.port ?: 1443, + 'cert' : d.web.ssl.cert ?: '', + 'key' : d.web.ssl.key ?: '', + 'keyPass' : d.web.ssl.keyPass ?: '' }, - rewrites : { - enable : d.web.rewrites.enable ?: false, - config : d.web.rewrites.config ?: variables.rewritesDefaultConfig + 'rewrites' : { + 'enable' : d.web.rewrites.enable ?: false, + 'config' : d.web.rewrites.config ?: variables.rewritesDefaultConfig } }, - app : { - logDir : d.app.logDir ?: '', - libDirs : d.app.libDirs ?: '', - webConfigDir : d.app.webConfigDir ?: '', - serverConfigDir : d.app.serverConfigDir ?: variables.serverHomeDirectory, - webXML : d.app.webXML ?: '', - standalone : d.app.standalone ?: false, - WARPath : d.app.WARPath ?: "", - cfengine : d.app.cfengine ?: "" + 'app' : { + 'logDir' : d.app.logDir ?: '', + 'libDirs' : d.app.libDirs ?: '', + 'webConfigDir' : d.app.webConfigDir ?: '', + 'serverConfigDir' : d.app.serverConfigDir ?: '', + 'webXML' : d.app.webXML ?: '', + 'standalone' : d.app.standalone ?: false, + 'WARPath' : d.app.WARPath ?: "", + 'cfengine' : d.app.cfengine ?: "", + 'serverHomeDirectory' : d.app.serverHomeDirectory ?: "" }, - runwar : { - args : d.runwar.args ?: '' + 'runwar' : { + 'args' : d.runwar.args ?: '' } }; } @@ -185,16 +183,37 @@ component accessors="true" singleton { if( !isNull( serverProps.WARPath ) ) { serverProps.WARPath = fileSystemUtil.resolvePath( serverProps.WARPath ); } + if( !isNull( serverProps.serverHomeDirectory ) ) { + serverProps.serverHomeDirectory = fileSystemUtil.resolvePath( serverProps.serverHomeDirectory ); + } if( !isNull( serverProps.trayIcon ) ) { serverProps.trayIcon = fileSystemUtil.resolvePath( serverProps.trayIcon ); } if( !isNull( serverProps.rewritesConfig ) ) { serverProps.rewritesConfig = fileSystemUtil.resolvePath( serverProps.rewritesConfig ); } + if( !isNull( serverProps.webConfigDir ) ) { + serverProps.webConfigDir = fileSystemUtil.resolvePath( serverProps.webConfigDir ); + } + if( !isNull( serverProps.serverConfigDir ) ) { + serverProps.serverConfigDir = fileSystemUtil.resolvePath( serverProps.serverConfigDir ); + } + if( !isNull( serverProps.webXML ) ) { + serverProps.webXML = fileSystemUtil.resolvePath( serverProps.webXML ); + } + if( !isNull( serverProps.libDirs ) ) { + // Comma-delimited list needs each item resolved + serverProps.libDirs = serverProps.libDirs + .map( function( thisLibDir ){ + return fileSystemUtil.resolvePath( thisLibDir ); + } ); + } // Look up the server that we're starting var serverDetails = resolveServerDetails( arguments.serverProps ); - + + interceptorService.announceInterception( 'preServerStart', { serverDetails=serverDetails, serverProps=serverProps } ); + var defaultName = serverDetails.defaultName; var defaultwebroot = serverDetails.defaultwebroot; var defaultServerConfigFile = serverDetails.defaultServerConfigFile; @@ -225,13 +244,13 @@ component accessors="true" singleton { // Backwards compat for default port in box.json. Remove this eventually... // * // * // Get package descriptor // * - var boxJSON = packageService.readPackageDescriptor( defaultwebroot ); // * + var boxJSON = packageService.readPackageDescriptorRaw( defaultwebroot ); // * // Get defaults // * var defaults = getDefaultServerJSON(); // * // * // Backwards compat with boxJSON default port. Remove in a future version // * // The property in box.json is deprecated. // * - if( boxJSON.defaultPort > 0 ) { // * + if( (boxJSON.defaultPort ?: 0) > 0 ) { // * // * // Remove defaultPort from box.json and pretend it was // * // manually typed which will cause server.json to save it. // * @@ -248,9 +267,10 @@ component accessors="true" singleton { // Save hand-entered properties in our server.json for next time for( var prop in serverProps ) { // Ignore null props or ones that shouldn't be saved - if( isNull( serverProps[ prop ] ) || listFindNoCase( 'saveSettings,serverConfigFile,debug,force', prop ) ) { + if( isNull( serverProps[ prop ] ) || listFindNoCase( 'saveSettings,serverConfigFile,debug,force,console', prop ) ) { continue; } + var configPath = replace( fileSystemUtil.resolvePath( defaultServerConfigFileDirectory ), '\', '/', 'all' ) & '/'; // Only need switch cases for properties that are nested or use different name switch(prop) { case "port": @@ -260,9 +280,8 @@ component accessors="true" singleton { serverJSON[ 'web' ][ 'host' ] = serverProps[ prop ]; break; case "directory": - // Both of these are canonical already. + // This path is canonical already. var thisDirectory = replace( serverProps[ 'directory' ], '\', '/', 'all' ) & '/'; - var configPath = replace( fileSystemUtil.resolvePath( defaultServerConfigFileDirectory ), '\', '/', 'all' ) & '/'; // If the web root is south of the server's JSON, make it relative for better portability. if( thisDirectory contains configPath ) { thisDirectory = replaceNoCase( thisDirectory, configPath, '' ); @@ -270,9 +289,8 @@ component accessors="true" singleton { serverJSON[ 'web' ][ 'webroot' ] = thisDirectory; break; case "trayIcon": - // Both of these are canonical already. + // This path is canonical already. var thisFile = replace( serverProps[ 'trayIcon' ], '\', '/', 'all' ); - var configPath = replace( fileSystemUtil.resolvePath( defaultServerConfigFileDirectory ), '\', '/', 'all' ) & '/'; // If the trayIcon is south of the server's JSON, make it relative for better portability. if( thisFile contains configPath ) { thisFile = replaceNoCase( thisFile, configPath, '' ); @@ -283,30 +301,67 @@ component accessors="true" singleton { serverJSON[ 'stopsocket' ] = serverProps[ prop ]; break; case "webConfigDir": - serverJSON[ 'app' ][ 'webConfigDir' ] = serverProps[ prop ]; - break; + // This path is canonical already. + var thisDirectory = replace( serverProps[ 'webConfigDir' ], '\', '/', 'all' ) & '/'; + // If the webConfigDir is south of the server's JSON, make it relative for better portability. + if( thisDirectory contains configPath ) { + thisDirectory = replaceNoCase( thisDirectory, configPath, '' ); + } + serverJSON[ 'app' ][ 'webConfigDir' ] = thisDirectory; + break; case "serverConfigDir": - serverJSON[ 'app' ][ 'serverConfigDir' ] = serverProps[ prop ]; - break; - case "libDirs": - serverJSON[ 'app' ][ 'libDirs' ] = serverProps[ prop ]; + // This path is canonical already. + var thisDirectory = replace( serverProps[ 'serverConfigDir' ], '\', '/', 'all' ) & '/'; + // If the webConfigDir is south of the server's JSON, make it relative for better portability. + if( thisDirectory contains configPath ) { + thisDirectory = replaceNoCase( thisDirectory, configPath, '' ); + } + serverJSON[ 'app' ][ 'serverConfigDir' ] = thisDirectory; break; case "webXML": - serverJSON[ 'app' ][ 'webXML' ] = serverProps[ prop ]; + // This path is canonical already. + var thisFile = replace( serverProps[ 'webXML' ], '\', '/', 'all' ); + // If the webXML is south of the server's JSON, make it relative for better portability. + if( thisFile contains configPath ) { + thisFile = replaceNoCase( thisFile, configPath, '' ); + } + serverJSON[ 'app' ][ 'webXML' ] = thisFile; + break; + case "libDirs": + serverJSON[ 'app' ][ 'libDirs' ] = serverProps[ 'libDirs' ] + .listMap( function( thisLibDir ) { + // This path is canonical already. + var thisLibDir = replace( thisLibDir, '\', '/', 'all' ); + // If the libDir is south of the server's JSON, make it relative for better portability. + if( thisLibDir contains configPath ) { + return replaceNoCase( thisLibDir, configPath, '' ); + } else { + return thisLibDir; + } + } ); + break; case "cfengine": serverJSON[ 'app' ][ 'cfengine' ] = serverProps[ prop ]; break; case "WARPath": - // Both of these are canonical already. + // This path is canonical already. var thisFile = replace( serverProps[ 'WARPath' ], '\', '/', 'all' ); - var configPath = replace( fileSystemUtil.resolvePath( defaultServerConfigFileDirectory ), '\', '/', 'all' ) & '/'; // If the trayIcon is south of the server's JSON, make it relative for better portability. if( thisFile contains configPath ) { thisFile = replaceNoCase( thisFile, configPath, '' ); } serverJSON[ 'app' ][ 'WARPath' ] = thisFile; break; + case "serverHomeDirectory": + // This path is canonical already. + var thisDirectory = replace( serverProps[ 'serverHomeDirectory' ], '\', '/', 'all' ) & '/'; + // If the webConfigDir is south of the server's JSON, make it relative for better portability. + if( thisDirectory contains configPath ) { + thisDirectory = replaceNoCase( thisDirectory, configPath, '' ); + } + serverJSON[ 'app' ][ 'serverHomeDirectory' ] = thisDirectory; + break; case "HTTPEnable": serverJSON[ 'web' ][ 'HTTP' ][ 'enable' ] = serverProps[ prop ]; break; @@ -325,13 +380,15 @@ component accessors="true" singleton { case "SSLKeyPass": serverJSON[ 'web' ][ 'SSL' ][ 'keyPass' ] = serverProps[ prop ]; break; + case "welcomeFiles": + serverJSON[ 'web' ][ 'welcomeFiles' ] = serverProps[ prop ]; + break; case "rewritesEnable": serverJSON[ 'web' ][ 'rewrites' ][ 'enable' ] = serverProps[ prop ]; break; case "rewritesConfig": - // Both of these are canonical already. + // This path is canonical already. var thisFile = replace( serverProps[ 'rewritesConfig' ], '\', '/', 'all' ); - var configPath = replace( fileSystemUtil.resolvePath( defaultServerConfigFileDirectory ), '\', '/', 'all' ) & '/'; // If the trayIcon is south of the server's JSON, make it relative for better portability. if( thisFile contains configPath ) { thisFile = replaceNoCase( thisFile, configPath, '' ); @@ -348,7 +405,7 @@ component accessors="true" singleton { serverJSON[ 'runwar' ][ 'args' ] = serverProps[ prop ]; break; default: - serverJSON[ prop ] = serverProps[ prop ]; + serverJSON[ prop ] = serverProps[ prop ]; } // end switch } // for loop @@ -376,14 +433,31 @@ component accessors="true" singleton { } serverInfo.stopsocket = serverProps.stopsocket ?: serverJSON.stopsocket ?: getRandomPort( serverInfo.host ); + + // relative trayIcon in server.json is resolved relative to the server.json + if( serverJSON.keyExists( 'app' ) && serverJSON.app.keyExists( 'webConfigDir' ) ) { serverJSON.app.webConfigDir = fileSystemUtil.resolvePath( serverJSON.app.webConfigDir, defaultServerConfigFileDirectory ); } + // relative trayIcon in config setting server defaults is resolved relative to the web root + if( len( defaults.app.webConfigDir ?: '' ) ) { defaults.app.webConfigDir = fileSystemUtil.resolvePath( defaults.app.webConfigDir, defaultwebroot ); } serverInfo.webConfigDir = serverProps.webConfigDir ?: serverJSON.app.webConfigDir ?: defaults.app.webConfigDir; - if( !len( serverInfo.webConfigDir ) ) { serverInfo.webConfigDir = getCustomServerFolder( serverInfo ); } + + // relative trayIcon in server.json is resolved relative to the server.json + if( serverJSON.keyExists( 'app' ) && serverJSON.app.keyExists( 'serverConfigDir' ) ) { serverJSON.app.serverConfigDir = fileSystemUtil.resolvePath( serverJSON.app.serverConfigDir, defaultServerConfigFileDirectory ); } + // relative trayIcon in config setting server defaults is resolved relative to the web root + if( len( defaults.app.serverConfigDir ?: '' ) ) { defaults.app.serverConfigDir = fileSystemUtil.resolvePath( defaults.app.serverConfigDir, defaultwebroot ); } serverInfo.serverConfigDir = serverProps.serverConfigDir ?: serverJSON.app.serverConfigDir ?: defaults.app.serverConfigDir; - serverInfo.libDirs = serverProps.libDirs ?: serverJSON.app.libDirs ?: defaults.app.libDirs; + + // relative trayIcon in server.json is resolved relative to the server.json + if( serverJSON.keyExists( 'app' ) && serverJSON.app.keyExists( 'webXML' ) ) { serverJSON.app.webXML = fileSystemUtil.resolvePath( serverJSON.app.webXML, defaultServerConfigFileDirectory ); } + // relative trayIcon in config setting server defaults is resolved relative to the web root + if( len( defaults.app.webXML ?: '' ) ) { defaults.app.webXML = fileSystemUtil.resolvePath( defaults.app.webXML, defaultwebroot ); } + serverInfo.webXML = serverProps.webXML ?: serverJSON.app.webXML ?: defaults.app.webXML; + + // relative trayIcon in server.json is resolved relative to the server.json if( serverJSON.keyExists( 'trayIcon' ) ) { serverJSON.trayIcon = fileSystemUtil.resolvePath( serverJSON.trayIcon, defaultServerConfigFileDirectory ); } + // relative trayIcon in config setting server defaults is resolved relative to the web root if( defaults.keyExists( 'trayIcon' ) && len( defaults.trayIcon ) ) { defaults.trayIcon = fileSystemUtil.resolvePath( defaults.trayIcon, defaultwebroot ); } serverInfo.trayIcon = serverProps.trayIcon ?: serverJSON.trayIcon ?: defaults.trayIcon; - serverInfo.webXML = serverProps.webXML ?: serverJSON.app.webXML ?: defaults.app.webXML; + serverInfo.SSLEnable = serverProps.SSLEnable ?: serverJSON.web.SSL.enable ?: defaults.web.SSL.enable; serverInfo.HTTPEnable = serverProps.HTTPEnable ?: serverJSON.web.HTTP.enable ?: defaults.web.HTTP.enable; serverInfo.SSLPort = serverProps.SSLPort ?: serverJSON.web.SSL.port ?: defaults.web.SSL.port; @@ -391,9 +465,17 @@ component accessors="true" singleton { serverInfo.SSLKey = serverProps.SSLKey ?: serverJSON.web.SSL.key ?: defaults.web.SSL.key; serverInfo.SSLKeyPass = serverProps.SSLKeyPass ?: serverJSON.web.SSL.keyPass ?: defaults.web.SSL.keyPass; serverInfo.rewritesEnable = serverProps.rewritesEnable ?: serverJSON.web.rewrites.enable ?: defaults.web.rewrites.enable; + serverInfo.welcomeFiles = serverProps.welcomeFiles ?: serverJSON.web.welcomeFiles ?: defaults.web.welcomeFiles; + // Clean up spaces in welcome file list + serverInfo.welcomeFiles = serverInfo.welcomeFiles.listMap( function( i ){ return trim( i ); } ); + + + // relative rewrite config path in server.json is resolved relative to the server.json if( isDefined( 'serverJSON.web.rewrites.config' ) ) { serverJSON.web.rewrites.config = fileSystemUtil.resolvePath( serverJSON.web.rewrites.config, defaultServerConfigFileDirectory ); } + // relative rewrite config path in config setting server defaults is resolved relative to the web root if( isDefined( 'defaults.web.rewrites.config' ) ) { defaults.web.rewrites.config = fileSystemUtil.resolvePath( defaults.web.rewrites.config, defaultwebroot ); } serverInfo.rewritesConfig = serverProps.rewritesConfig ?: serverJSON.web.rewrites.config ?: defaults.web.rewrites.config; + serverInfo.heapSize = serverProps.heapSize ?: serverJSON.JVM.heapSize ?: defaults.JVM.heapSize; serverInfo.directoryBrowsing = serverProps.directoryBrowsing ?: serverJSON.web.directoryBrowsing ?: defaults.web.directoryBrowsing; @@ -418,21 +500,52 @@ component accessors="true" singleton { // Global defauls are always added on top of whatever is specified by the user or server.json serverInfo.runwarArgs = ( serverProps.runwarArgs ?: serverJSON.runwar.args ?: '' ) & ' ' & defaults.runwar.args; + + // Server startup timeout + serverInfo.startTimeout = serverProps.startTimeout ?: serverJSON.startTimeout ?: defaults.startTimeout; + // relative lib dirs in server.json are resolved relative to the server.json + if( serverJSON.keyExists( 'app' ) && serverJSON.app.keyExists( 'libDirs' ) ) { + serverJSON.app.libDirs = serverJSON.app.libDirs.listMap( function( thisLibDir ){ + return fileSystemUtil.resolvePath( thisLibDir, defaultServerConfigFileDirectory ); + }); + } + // relative lib dirs in config setting server defaults are resolved relative to the web root + if( defaults.keyExists( 'app' ) && defaults.app.keyExists( 'libDirs' ) && len( defaults.app.libDirs ) ) { + // For each lib dir in the list, resolve the path, but only keep it if the folder actually exists. + // This allows for "optional" global lib dirs. + // listReduce starts with an initial value of "" and aggregates the new list, onluy appending the items it wants to keep + defaults.app.libDirs = defaults.app.libDirs.listReduce( function( thisLibDirs, thisLibDir ){ + thisLibDir = fileSystemUtil.resolvePath( thisLibDir, defaultwebroot ); + if( directoryExists( thisLibDir ) ) { + thisLibDirs.listAppend( thisLibDir ); + } else if( serverInfo.debug ) { + consoleLogger.info( "Ignoring non-existant global lib dir: " & thisLibDir ); + } + return thisLibDirs; + }, '' ); + } // Global defauls are always added on top of whatever is specified by the user or server.json serverInfo.libDirs = ( serverProps.libDirs ?: serverJSON.app.libDirs ?: '' ).listAppend( defaults.app.libDirs ); serverInfo.cfengine = serverProps.cfengine ?: serverJSON.app.cfengine ?: defaults.app.cfengine; + + // relative rewrite config path in server.json is resolved relative to the server.json + if( isDefined( 'serverJSON.app.WARPath' ) && len( serverJSON.app.WARPath ) ) { serverJSON.app.WARPath = fileSystemUtil.resolvePath( serverJSON.app.WARPath, defaultServerConfigFileDirectory ); } + if( isDefined( 'defaults.app.WARPath' ) && len( defaults.app.WARPath ) ) { defaults.app.WARPath = fileSystemUtil.resolvePath( defaults.app.WARPath, defaultwebroot ); } serverInfo.WARPath = serverProps.WARPath ?: serverJSON.app.WARPath ?: defaults.app.WARPath; + + // relative rewrite config path in server.json is resolved relative to the server.json + if( isDefined( 'serverJSON.app.serverHomeDirectory' ) && len( serverJSON.app.serverHomeDirectory ) ) { serverJSON.app.serverHomeDirectory = fileSystemUtil.resolvePath( serverJSON.app.serverHomeDirectory, defaultServerConfigFileDirectory ); } + if( isDefined( 'defaults.app.serverHomeDirectory' ) && len( defaults.app.serverHomeDirectory ) ) { defaults.app.serverHomeDirectory = fileSystemUtil.resolvePath( defaults.app.serverHomeDirectory, defaultwebroot ); } + serverInfo.serverHomeDirectory = serverProps.serverHomeDirectory ?: serverJSON.app.serverHomeDirectory ?: defaults.app.serverHomeDirectory; // These are already hammered out above, so no need to go through all the defaults. serverInfo.serverConfigFile = defaultServerConfigFile; serverInfo.name = defaultName; serverInfo.webroot = defaultwebroot; - - serverInfo.logdir = serverInfo.webConfigDir & "/logs"; - + if( serverInfo.debug ) { consoleLogger.info( "start server in - " & serverInfo.webroot ); consoleLogger.info( "server name - " & serverInfo.name ); @@ -440,7 +553,8 @@ component accessors="true" singleton { } if( !len( serverInfo.WARPath ) && !len( serverInfo.cfengine ) ) { - serverInfo.cfengine = 'lucee@' & server.lucee.version; + // Turn 1.2.3.4 into 1.2.3+4 + serverInfo.cfengine = 'lucee@' & reReplace( server.lucee.version, '([0-9]*.[0-9]*.[0-9]*)(.)([0-9]*)', '\1+\3' ); } if( serverInfo.cfengine.endsWith( '@' ) ) { @@ -449,66 +563,101 @@ component accessors="true" singleton { var launchUtil = java.LaunchUtil; - // log directory location - if( !directoryExists( serverInfo.logDir ) ){ directoryCreate( serverInfo.logDir ); } - - - // Default java agent for embedded Lucee engine - var javaagent = serverinfo.cfengine contains 'lucee' ? '-javaagent:"#libdir#/lucee-inst.jar"' : ''; - - // Not sure what Runwar does with this, but it wants to know what CFEngine we're starting (if we know) - var CFEngineName = ''; - CFEngineName = serverinfo.cfengine contains 'lucee' ? 'lucee' : CFEngineName; - CFEngineName = serverinfo.cfengine contains 'railo' ? 'railo' : CFEngineName; - CFEngineName = serverinfo.cfengine contains 'adobe' ? 'adobe' : CFEngineName; - - var thisVersion = ''; - var processName = ( serverInfo.name is "" ? "CommandBox" : serverInfo.name ); - - // As long as there's no WAR Path, let's install the engine to use. - if( serverInfo.WARPath == '' ){ - - // This will install the engine war to start, possibly downloading it first - var installDetails = serverEngineService.install( cfengine=serverInfo.cfengine, basedirectory=serverInfo.webConfigDir ); - // This interception point can be used for additional configuration of the engine before it actually starts. - interceptorService.announceInterception( 'onServerInstall', { serverInfo=serverInfo, installDetails=installDetails } ); - thisVersion = ' ' & installDetails.version; - serverInfo.serverHome = installDetails.installDir; - serverInfo.logdir = installDetails.installDir & "/logs"; + // Default java agent for embedded Lucee engine + var javaagent = serverinfo.cfengine contains 'lucee' ? '-javaagent:#libdir#/lucee-inst.jar' : ''; + + // Regardless of a custom server home, this is still used for various temp files and logs + directoryCreate( getCustomServerFolder( serverInfo ), true, true ); + + // Not sure what Runwar does with this, but it wants to know what CFEngine we're starting (if we know) + var CFEngineName = ''; + CFEngineName = serverinfo.cfengine contains 'lucee' ? 'lucee' : CFEngineName; + CFEngineName = serverinfo.cfengine contains 'railo' ? 'railo' : CFEngineName; + CFEngineName = serverinfo.cfengine contains 'adobe' ? 'adobe' : CFEngineName; + CFEngineName = serverinfo.warPath contains 'adobe' ? 'adobe' : CFEngineName; + + var processName = ( serverInfo.name is "" ? "CommandBox" : serverInfo.name ); + + // As long as there's no WAR Path, let's install the engine to use. + if( serverInfo.WARPath == '' ){ + + // This will install the engine war to start, possibly downloading it first + var installDetails = serverEngineService.install( cfengine=serverInfo.cfengine, basedirectory=getCustomServerFolder( serverInfo ), serverInfo=serverInfo, serverHomeDirectory=serverInfo.serverHomeDirectory ); + serverInfo.serverHomeDirectory = installDetails.installDir; + // TODO: As of 3.5 this is for backwards compat. Remove in later version + serverInfo.serverHome = installDetails.installDir; + serverInfo.logdir = serverInfo.serverHomeDirectory & "/logs"; + serverInfo.consolelogPath = serverInfo.logdir & '/server.out.txt'; + serverInfo.engineName = installDetails.engineName; + serverInfo.engineVersion = installDetails.version; - // If external Lucee server, set the java agent - if( !installDetails.internal && serverInfo.cfengine contains "lucee" ) { - javaagent = "-javaagent:""#installDetails.installDir#/WEB-INF/lib/lucee-inst.jar"""; - } - // If external Railo server, set the java agent - if( !installDetails.internal && serverInfo.cfengine contains "railo" ) { - javaagent = "-javaagent:""#installDetails.installDir#/WEB-INF/lib/railo-inst.jar"""; - } - // Using built in server that hasn't been started before - if( installDetails.internal && !directoryExists( serverInfo.webConfigDir & '/WEB-INF' ) ) { - serverInfo.webConfigDir = installDetails.installDir; - serverInfo.logdir = serverInfo.webConfigDir & "/logs"; - } - - // The process native name - var processName = ( serverInfo.name is "" ? "CommandBox" : serverInfo.name ) & ' [' & listFirst( serverinfo.cfengine, '@' ) & thisVersion & ']'; + // This is for one-time setup tasks on first install + if( installDetails.initialInstall ) { + // Make current settings available to package scripts + setServerInfo( serverInfo ); + + // This interception point can be used for additional configuration of the engine before it actually starts. + interceptorService.announceInterception( 'onServerInstall', { serverInfo=serverInfo, installDetails=installDetails } ); + } + + // If Lucee server, set the java agent + if( serverInfo.cfengine contains "lucee" ) { + // Detect Lucee 4.x + if( installDetails.version.listFirst( '.' ) < 5 ) { + javaagent = "-javaagent:#serverInfo.serverHomeDirectory#/WEB-INF/lib/lucee-inst.jar"; + } else { + // Lucee 5+ doesn't need the Java agent + javaagent = ""; + } + } + // If external Railo server, set the java agent + if( serverInfo.cfengine contains "railo" ) { + javaagent = "-javaagent:#serverInfo.serverHomeDirectory#/WEB-INF/lib/railo-inst.jar"; + } + + // The process native name + var processName = ( serverInfo.name is "" ? "CommandBox" : serverInfo.name ) & ' [' & listFirst( serverinfo.cfengine, '@' ) & ' ' & installDetails.version & ']'; + + // This is a WAR + } else { + // If WAR is a file + if( fileExists( serverInfo.WARPath ) ){ + // It will be extracted into a folder named after the file + serverInfo.serverHomeDirectory = reReplaceNoCase( serverInfo.WARPath, '(.*)(\.zip|\.war)', '\1' ); + + // Expand the war if it doesn't exist or we're forcing + if( !directoryExists( serverInfo.serverHomeDirectory ) || serverProps.force ?: false ) { + consoleLogger.info( "Exploding WAR archive..."); + directoryCreate( serverInfo.serverHomeDirectory, true, true ); + zip action="unzip" file="#serverInfo.WARPath#" destination="#serverInfo.serverHomeDirectory#" overwrite="true"; + } + // If WAR is a folder + } else { + // Just use it + serverInfo.serverHomeDirectory = serverInfo.WARPath; + } + // Create a custom server folder to house the logs + serverInfo.logdir = getCustomServerFolder( serverInfo ) & "/logs"; + serverInfo.consolelogPath = serverInfo.logdir & '/server.out.txt'; + } + // Find the correct tray icon for this server if( !len( serverInfo.trayIcon ) ) { var iconSize = fileSystemUtil.isWindows() ? '-32px' : ''; - if( serverInfo.cfengine contains "lucee" ) { + if( CFEngineName contains "lucee" ) { serverInfo.trayIcon = '/commandbox/system/config/server-icons/trayicon-lucee#iconSize#.png'; - } else if( serverInfo.cfengine contains "railo" ) { + } else if( CFEngineName contains "railo" ) { serverInfo.trayIcon = '/commandbox/system/config/server-icons/trayicon-railo#iconSize#.png'; - } else if( serverInfo.cfengine contains "adobe" ) { + } else if( CFEngineName contains "adobe" ) { - if( listFirst( installDetails.version, '.' ) == 9 ) { + if( listFirst( serverInfo.engineVersion, '.' ) == 9 ) { serverInfo.trayIcon = '/commandbox/system/config/server-icons/trayicon-cf09#iconSize#.png'; - } else if( listFirst( installDetails.version, '.' ) == 10 ) { + } else if( listFirst( serverInfo.engineVersion, '.' ) == 10 ) { serverInfo.trayIcon = '/commandbox/system/config/server-icons/trayicon-cf10#iconSize#.png'; - } else if( listFirst( installDetails.version, '.' ) == 11 ) { + } else if( listFirst( serverInfo.engineVersion, '.' ) == 11 ) { serverInfo.trayIcon = '/commandbox/system/config/server-icons/trayicon-cf11#iconSize#.png'; - } else if( listFirst( installDetails.version, '.' ) == 2016 ) { + } else if( listFirst( serverInfo.engineVersion, '.' ) == 2016 ) { serverInfo.trayIcon = '/commandbox/system/config/server-icons/trayicon-cf2016#iconSize#.png'; } else { serverInfo.trayIcon = '/commandbox/system/config/server-icons/trayicon-cf2016#iconSize#.png'; @@ -516,181 +665,271 @@ component accessors="true" singleton { } } - - // This is a WAR - } else { - serverInfo.serverHome = getDirectoryFromPath( serverInfo.WARPath ); - } + + // Default tray icon + serverInfo.trayIcon = ( len( serverInfo.trayIcon ) ? serverInfo.trayIcon : '/commandbox/system/config/server-icons/trayicon.png' ); + serverInfo.trayIcon = expandPath( serverInfo.trayIcon ); - // Default tray icon - serverInfo.trayIcon = ( len( serverInfo.trayIcon ) ? serverInfo.trayIcon : '#variables.libdir#/trayicon.png' ); - serverInfo.trayIcon = expandPath( serverInfo.trayIcon ); - - // Set default options for all servers - // TODO: Don't overwrite existing options with the same label. - - if( serverInfo.cfengine contains "lucee" ) { - serverInfo.trayOptions.prepend( { 'label':'Open Web Admin', 'action':'openbrowser', 'url':'http://${runwar.host}:${runwar.port}/lucee/admin/web.cfm', 'image' : expandPath('/commandbox/system/config/server-icons/web_settings.png' ) } ); - serverInfo.trayOptions.prepend( { 'label':'Open Server Admin', 'action':'openbrowser', 'url':'http://${runwar.host}:${runwar.port}/lucee/admin/server.cfm', 'image' : expandPath('/commandbox/system/config/server-icons/server_settings.png' ) } ); - } else if( serverInfo.cfengine contains "railo" ) { - serverInfo.trayOptions.prepend( { 'label':'Open Web Admin', 'action':'openbrowser', 'url':'http://${runwar.host}:${runwar.port}/railo-context/admin/web.cfm', 'image' : expandPath('/commandbox/system/config/server-icons/web_settings.png' ) } ); - serverInfo.trayOptions.prepend( { 'label':'Open Server Admin', 'action':'openbrowser', 'url':'http://${runwar.host}:${runwar.port}/railo-context/admin/server.cfm', 'image' : expandPath('/commandbox/system/config/server-icons/server_settings.png' ) } ); - } else if( serverInfo.cfengine contains "adobe" ) { - serverInfo.trayOptions.prepend( { 'label':'Open Server Admin', 'action':'openbrowser', 'url':'http://${runwar.host}:${runwar.port}/CFIDE/administrator/enter.cfm', 'image' : expandPath('/commandbox/system/config/server-icons/server_settings.png' ) } ); - } - - serverInfo.trayOptions.prepend( { 'label':'Open Browser', 'action':'openbrowser', 'url':'http://${runwar.host}:${runwar.port}/', 'image' : expandPath('/commandbox/system/config/server-icons/home.png' ) } ); - serverInfo.trayOptions.prepend( { 'label':'Stop Server', 'action':'stopserver', 'image' : expandPath('/commandbox/system/config/server-icons/stop.png' ) } ); - serverInfo.trayOptions.prepend( { 'label': processName, 'disabled':true, 'image' : expandPath('/commandbox/system/config/server-icons/info.png' ) } ); - - // This is due to a bug in RunWar not creating the right directory for the logs - directoryCreate( serverInfo.logDir, true, true ); - - interceptorService.announceInterception( 'onServerStart', { serverInfo=serverInfo } ); - - // Turn struct of aliases into a comma-delimited list, plus resolve relative paths. - // "/foo=C:\path,/bar=C:\another/path" - var CLIAliases = ''; - for( var thisAlias in serverInfo.aliases ) { - CLIAliases = CLIAliases.listAppend( thisAlias & '=' & fileSystemUtil.resolvePath( serverInfo.aliases[ thisAlias ], serverInfo.webroot ) ); - } - - // Turn struct of errorPages into a comma-delimited list. - // --error-pages="404=/path/to/404.html,500=/path/to/500.html,1=/path/to/default.html" - var errorPages = ''; - for( var thisErrorPage in serverInfo.errorPages ) { - // "default" turns into "1" - var tmp = thisErrorPage == 'default' ? 1 : thisErrorPage; - tmp &= '='; - // normalize slashes - var thisPath = replace( serverInfo.errorPages[ thisErrorPage ], '\', '/', 'all' ); - // Add leading slash if it doesn't exist. - tmp &= thisPath.startsWith( '/' ) ? thisPath : '/' & thisPath; - errorPages = errorPages.listAppend( tmp ); - } - // Bug in runwar requires me to completley omit this param unless it's populated - // https://github.com/cfmlprojects/runwar/issues/33 - if( len( errorPages ) ) { - errorPages = '--error-pages="#errorPages#"'; - } - - // Serialize tray options and write to temp file - var trayOptionsPath = serverInfo.serverHome & '/trayOptions.json'; - var trayJSON = { - 'title' : processName, - 'tooltip' : processName, - 'items' : serverInfo.trayOptions - }; - fileWrite( trayOptionsPath, serializeJSON( trayJSON ) ); - - - var startupTimeout = 120; - // Increase our startup allowance for Adobe engines, since a number of files are generated on the first request - if( CFEngineName == 'adobe' ){ - startupTimeout=240; - } + // Set default options for all servers + // TODO: Don't overwrite existing options with the same label. + + if( CFEngineName contains "lucee" ) { + serverInfo.trayOptions.prepend( { 'label':'Open Web Admin', 'action':'openbrowser', 'url':'http://${runwar.host}:${runwar.port}/lucee/admin/web.cfm', 'image' : expandPath('/commandbox/system/config/server-icons/web_settings.png' ) } ); + serverInfo.trayOptions.prepend( { 'label':'Open Server Admin', 'action':'openbrowser', 'url':'http://${runwar.host}:${runwar.port}/lucee/admin/server.cfm', 'image' : expandPath('/commandbox/system/config/server-icons/server_settings.png' ) } ); + } else if( CFEngineName contains "railo" ) { + serverInfo.trayOptions.prepend( { 'label':'Open Web Admin', 'action':'openbrowser', 'url':'http://${runwar.host}:${runwar.port}/railo-context/admin/web.cfm', 'image' : expandPath('/commandbox/system/config/server-icons/web_settings.png' ) } ); + serverInfo.trayOptions.prepend( { 'label':'Open Server Admin', 'action':'openbrowser', 'url':'http://${runwar.host}:${runwar.port}/railo-context/admin/server.cfm', 'image' : expandPath('/commandbox/system/config/server-icons/server_settings.png' ) } ); + } else if( CFEngineName contains "adobe" ) { + serverInfo.trayOptions.prepend( { 'label':'Open Server Admin', 'action':'openbrowser', 'url':'http://${runwar.host}:${runwar.port}/CFIDE/administrator/enter.cfm', 'image' : expandPath('/commandbox/system/config/server-icons/server_settings.png' ) } ); + } + + serverInfo.trayOptions.prepend( { 'label':'Open Browser', 'action':'openbrowser', 'url':'http://${runwar.host}:${runwar.port}/', 'image' : expandPath('/commandbox/system/config/server-icons/home.png' ) } ); + serverInfo.trayOptions.prepend( { 'label':'Stop Server', 'action':'stopserver', 'image' : expandPath('/commandbox/system/config/server-icons/stop.png' ) } ); + serverInfo.trayOptions.prepend( { 'label': processName, 'disabled':true, 'image' : expandPath('/commandbox/system/config/server-icons/info.png' ) } ); + + // This is due to a bug in RunWar not creating the right directory for the logs + directoryCreate( serverInfo.logDir, true, true ); + + // Make current settings available to package scripts + setServerInfo( serverInfo ); + interceptorService.announceInterception( 'onServerStart', { serverInfo=serverInfo } ); - // The java arguments to execute: Shared server, custom web configs - var args = ' #serverInfo.JVMargs# -Xmx#serverInfo.heapSize#m -Xms#serverInfo.heapSize#m' - & ' #javaagent# -jar "#variables.jarPath#"' - & ' --background=true --port #serverInfo.port# --host #serverInfo.host# --debug=#serverInfo.debug#' - & ' --stop-port #serverInfo.stopsocket# --processname "#processName#" --log-dir "#serverInfo.logDir#"' - & ' --open-browser #serverInfo.openbrowser#' - & ' --open-url ' & ( serverInfo.SSLEnable ? 'https://#serverInfo.host#:#serverInfo.SSLPort#' : 'http://#serverInfo.host#:#serverInfo.port#' ) - & ( len( CFEngineName ) ? ' --cfengine-name "#CFEngineName#"' : '' ) - & ' --server-name "#serverInfo.name#" #errorPages#' - & ' --tray-icon "#serverInfo.trayIcon#" --tray-config "#trayOptionsPath#"' - & ' --directoryindex "#serverInfo.directoryBrowsing#" --cfml-web-config "#serverInfo.webConfigDir#"' - & ( len( CLIAliases ) ? ' --dirs "#CLIAliases#"' : '' ) - & ' --cfml-server-config "#serverInfo.serverConfigDir#" #serverInfo.runwarArgs# --timeout #startupTimeout#'; - - // Starting a WAR - if (serverInfo.WARPath != "" ) { - args &= " -war ""#serverInfo.WARPath#"""; - // Stand alone server - } else if( !installDetails.internal ){ - args &= " -war ""#serverInfo.webroot#"" --lib-dirs ""#installDetails.installDir#/WEB-INF/lib"" --web-xml-path ""#installDetails.installDir#/WEB-INF/web.xml"""; - // internal server - } else { - // The internal server borrows the CommandBox lib directory - serverInfo.libDirs = serverInfo.libDirs.listAppend( variables.libDir ); - args &= " -war ""#serverInfo.webroot#"" --lib-dirs ""#serverInfo.libDirs#"""; - } - // Incorporate SSL to command - if( serverInfo.SSLEnable ){ - args &= " --http-enable #serverInfo.HTTPEnable# --ssl-enable #serverInfo.SSLEnable# --ssl-port #serverInfo.SSLPort#"; - } - if( serverInfo.SSLEnable && serverInfo.SSLCert != "") { - args &= " --ssl-cert ""#serverInfo.SSLCert#"" --ssl-key ""#serverInfo.SSLKey#"" --ssl-keypass ""#serverInfo.SSLKeyPass#"""; - } - // Incorporate web-xml to command - if ( Len( Trim( serverInfo.webXml ?: "" ) ) ) { - args &= " --web-xml-path ""#serverInfo.webXml#"""; - } - // Incorporate rewrites to command - args &= " --urlrewrite-enable #serverInfo.rewritesEnable#"; - - if( serverInfo.rewritesEnable ){ - if( !fileExists(serverInfo.rewritesConfig) ){ - consoleLogger.error( '.' ); - consoleLogger.error( 'URL rewrite config not found [#serverInfo.rewritesConfig#]' ); - consoleLogger.error( '.' ); - return; + // Turn struct of aliases into a comma-delimited list, plus resolve relative paths. + // "/foo=C:\path,/bar=C:\another/path" + var CLIAliases = ''; + for( var thisAlias in serverInfo.aliases ) { + CLIAliases = CLIAliases.listAppend( thisAlias & '=' & fileSystemUtil.resolvePath( serverInfo.aliases[ thisAlias ], serverInfo.webroot ) ); + } + + // Turn struct of errorPages into a comma-delimited list. + // --error-pages="404=/path/to/404.html,500=/path/to/500.html,1=/path/to/default.html" + var errorPages = ''; + for( var thisErrorPage in serverInfo.errorPages ) { + // "default" turns into "1" + var tmp = thisErrorPage == 'default' ? 1 : thisErrorPage; + tmp &= '='; + // normalize slashes + var thisPath = replace( serverInfo.errorPages[ thisErrorPage ], '\', '/', 'all' ); + // Add leading slash if it doesn't exist. + tmp &= thisPath.startsWith( '/' ) ? thisPath : '/' & thisPath; + errorPages = errorPages.listAppend( tmp ); + } + // Bug in runwar requires me to completley omit this param unless it's populated + // https://github.com/cfmlprojects/runwar/issues/33 + if( len( errorPages ) ) { + errorPages = '--error-pages="#errorPages#"'; + } + + // Serialize tray options and write to temp file + var trayOptionsPath = getCustomServerFolder( serverInfo ) & '/trayOptions.json'; + var trayJSON = { + 'title' : processName, + 'tooltip' : processName, + 'items' : serverInfo.trayOptions + }; + fileWrite( trayOptionsPath, serializeJSON( trayJSON ) ); + var background = !(serverProps.console ?: false); + // The java arguments to execute: Shared server, custom web configs + + // If background, wrap up JVM args to pass through to background servers + // "real" JVM args must come before Runwar args, so creating two variables, once of which will always be empty. + if( background ) { + var thisJVMArgs = ''; + // "borrow" the CommandBox commandline parser to tokenize the JVM args. Not perfect, but close. Handles quoted values with spaces. + var argTokens = parser.tokenizeInput( serverInfo.JVMargs ) + .map( function( i ){ + // Clean up a couple escapes the parser does that we don't need + return i.replace( '\=', '=', 'all' ).replace( '\\', '\', 'all' ); + }); + // Add in heap size and java agent + argTokens + .append( '-Xmx#serverInfo.heapSize#m' ) + .append( '-Xms#serverInfo.heapSize#m' ); + if( len( trim( javaAgent ) ) ) { argTokens.append( '#javaagent#' ); } + + argString = argTokens.toList( ';' ).replace( '"', '\"', 'all' ); + + var thispassthroughJVMArgs = '--jvm-args="#argString#"'; + // If foreground, just stick them in. + } else { + var thisJVMArgs = ' -Xmx#serverInfo.heapSize#m -Xms#serverInfo.heapSize#m #javaagent# #serverInfo.JVMargs# '; + var thispassthroughJVMArgs = ''; + } + + var args = ' #thisJVMArgs# -jar #variables.jarPath#' + // debug and background need to parse as a single token. Leave the = + & ' --background=#background# --port #serverInfo.port# --host #serverInfo.host# --debug=#serverInfo.debug#' + & ' --stop-port #serverInfo.stopsocket# --processname "#processName#" --log-dir "#serverInfo.logDir#"' + & ' --open-browser #serverInfo.openbrowser#' + & ' --open-url ' & ( serverInfo.SSLEnable ? 'https://#serverInfo.host#:#serverInfo.SSLPort#' : 'http://#serverInfo.host#:#serverInfo.port#' ) + & ( len( CFEngineName ) ? ' --cfengine-name "#CFEngineName#"' : '' ) + & ' --server-name "#serverInfo.name#" #errorPages#' + & ( len( serverInfo.welcomeFiles ) ? ' --welcome-files "#serverInfo.welcomeFiles#" ' : '' ) + & ' --tray-icon "#serverInfo.trayIcon#" --tray-config "#trayOptionsPath#" --servlet-rest-mappings "/rest/*,/api/*"' + & ' --directoryindex "#serverInfo.directoryBrowsing#" ' + & ( len( CLIAliases ) ? ' --dirs "#CLIAliases#"' : '' ) + & ' #serverInfo.runwarArgs# --timeout #serverInfo.startTimeout# #thispassthroughJVMArgs# '; + + // Starting a WAR + if (serverInfo.WARPath != "" ) { + args &= " -war ""#serverInfo.WARPath#"""; + // Stand alone server + } else { + args &= " -war ""#serverInfo.webroot#"""; + } + // Custom web.xml (doesn't work right now) + if ( Len( Trim( serverInfo.webXml ) ) && false ) { + args &= " --web-xml-path ""#serverInfo.webXml#"""; + // Default is in WAR home + } else { + args &= " --web-xml-path ""#serverInfo.serverHomeDirectory#/WEB-INF/web.xml"""; + } + + if( len( serverInfo.libDirs ) ) { + // Have to get rid of empty list elements + args &= " --lib-dirs ""#serverInfo.libDirs.listChangeDelims( ',', ',' )#"""; } - args &= " --urlrewrite-file ""#serverInfo.rewritesConfig#"""; - } - // change status to starting + persist - serverInfo.status = "starting"; - setServerInfo( serverInfo ); - if( serverInfo.debug ) { - var cleanedArgs = cr & ' ' & trim( replaceNoCase( args, ' -', cr & ' -', 'all' ) ); - consoleLogger.debug("Server start command: #javaCommand# #cleanedArgs#"); - } - - // thread the execution + // Incorporate SSL to command + if( serverInfo.SSLEnable ){ + args &= " --http-enable #serverInfo.HTTPEnable# --ssl-enable #serverInfo.SSLEnable# --ssl-port #serverInfo.SSLPort#"; + } + if( serverInfo.SSLEnable && serverInfo.SSLCert != "") { + args &= " --ssl-cert ""#serverInfo.SSLCert#"" --ssl-key ""#serverInfo.SSLKey#"" --ssl-keypass ""#serverInfo.SSLKeyPass#"""; + } + // Incorporate rewrites to command + args &= " --urlrewrite-enable #serverInfo.rewritesEnable#"; + + if( serverInfo.rewritesEnable ){ + if( !fileExists(serverInfo.rewritesConfig) ){ + consoleLogger.error( '.' ); + consoleLogger.error( 'URL rewrite config not found [#serverInfo.rewritesConfig#]' ); + consoleLogger.error( '.' ); + return; + } + args &= " --urlrewrite-file ""#serverInfo.rewritesConfig#"""; + } + // change status to starting + persist + serverInfo.status = "starting"; + setServerInfo( serverInfo ); + + if( serverInfo.debug ) { + var cleanedArgs = cr & ' ' & trim( replaceNoCase( args, ' -', cr & ' -', 'all' ) ); + consoleLogger.debug("Server start command: #javaCommand# #cleanedArgs#"); + } + + // needs to be unique in each run to avoid errors var threadName = 'server#hash( serverInfo.webroot )##createUUID()#'; - thread name="#threadName#" serverInfo=serverInfo args=args { + // Construct a new process object + var processBuilder = createObject( "java", "java.lang.ProcessBuilder" ); + // Pass array of tokens comprised of command plus arguments + var processTokens = [ variables.javaCommand ] + processTokens.append( args.listToArray( ' ' ), true ); + processBuilder.init( processTokens ); + // Conjoin standard error and output for convenience. + processBuilder.redirectErrorStream( true ); + // Kick off actual process + variables.process = processBuilder.start(); + + // She'll be coming 'round the mountain when she comes... + consoleLogger.warn( "The server for #serverInfo.webroot# is starting on #serverInfo.host#:#serverInfo.port#..." ); + + // If the user is running a one-off command to start a server or specified the debug flag, stream the output and wait until it's finished starting. + var interactiveStart = ( shell.getShellType() == 'command' || serverInfo.debug || !background ); + + // Spin up a thread to capture the standard out and error from the server + thread name="#threadName#" interactiveStart=interactiveStart serverInfo=serverInfo args=args startTimeout=serverInfo.startTimeout { try{ - // execute the server command - var executeResult = ''; - var executeError = ''; + // save server info and persist serverInfo.statusInfo = { command:variables.javaCommand, arguments:attributes.args, result:'' }; + serverInfo.status="starting"; setServerInfo( serverInfo ); - // Note this timeout is purposefully longer than the Runwar timeout so if the server takes too long, we get to capture the console info - execute name=variables.javaCommand arguments=attributes.args timeout="150" variable="executeResult" errorVariable="executeError"; - serverInfo.status="running"; - } catch (any e) { - logger.error( "Error starting server: #e.message# #e.detail#", arguments ); - serverInfo.statusInfo.result &= e.message & ' ' & e.detail; + + var startOutput = createObject( 'java', 'java.lang.StringBuilder' ).init(); + var inputStream = process.getInputStream(); + var inputStreamReader = createObject( 'java', 'java.io.InputStreamReader' ).init( inputStream ); + var bufferedReader = createObject( 'java', 'java.io.BufferedReader' ).init( inputStreamReader ); + var print = wirebox.getInstance( "PrintBuffer" ); + + var line = bufferedReader.readLine(); + while( !isNull( line ) ){ + // Clean log4j cruft from line + line = reReplaceNoCase( line, '^.* (INFO|DEBUG|ERROR|WARN) RunwarLogger processoutput: ', '' ); + line = reReplaceNoCase( line, '^.* (INFO|DEBUG|ERROR|WARN) RunwarLogger lib: ', 'Runwar: ' ); + line = reReplaceNoCase( line, '^.* (INFO|DEBUG|ERROR|WARN) RunwarLogger ', 'Runwar: ' ); + + // Build up our output + startOutput.append( line & chr( 13 ) & chr( 10 ) ); + + // output it if we're being interactive + if( attributes.interactiveStart ) { + print + .line( line ) + .toConsole(); + } + line = bufferedReader.readLine(); + } // End of inputStream + + // When we require Java 8 for CommandBox, we can pass a timeout to waitFor(). + var exitCode = process.waitFor(); + + if( exitCode == 0 ) { + serverInfo.status="running"; + } else { + serverInfo.status="unknown"; + } + + } catch( any e ) { + logger.error( e.message & ' ' & e.detail, e.stacktrace ); serverInfo.status="unknown"; } finally { - // make sure these don't come back as nulls - param name='local.executeResult' default=''; - param name='local.executeError' default=''; - serverInfo.statusInfo.result = serverInfo.statusInfo.result & executeResult & ' ' & executeError; - setServerInfo( serverInfo ); + // Make sure we always close the file or the process will never quit! + if( isDefined( 'bufferedReader' ) ) { + bufferedReader.close(); + } + serverInfo.statusInfo.result = startOutput.toString(); + setServerInfo( serverInfo ); } } - - // She'll be coming 'round the mountain when she comes... - consoleLogger.warn( "The server for #serverInfo.webroot# is starting on #serverInfo.host#:#serverInfo.port#... type 'server status' to see result" ); - - // If this is a one off command, wait for the thread to finish, otherwise the JVM will shutdown before - // the server is started and the json files get updated. - if( shell.getShellType() == 'command' || serverInfo.debug ) { - thread action="join" name="#threadName#"; + // Block until the process ends and the streaming output thread above is done. + if( interactiveStart ) { - // Pull latest info that was saved in the thread and output it. Since we made the - // user wait for the thread to finish, we might as well tell them what happened. - wirebox.getinstance( name='CommandDSL', initArguments={ name : 'server status' } ) - .params( name = serverInfo.name ) - .run(); + if( !background ) { + try { + + while( true ) { + // Wipe out prompt so it doesn't redraw if the user hits enter + shell.getReader().setPrompt( '' ); + + // Detect user pressing Ctrl-C + // Any other characters captured will be ignored + var line = shell.getReader().readLine(); + if( line == 'q' ) { + break; + } else { + consoleLogger.error( 'To exit press Ctrl-C or "q" followed the enter key.' ); + } + } + + // user wants to exit, they've pressed Ctrl-C + } catch ( jline.console.UserInterruptException e ) { + // Something bad happened + } catch ( Any e ) { + logger.error( '#e.message# #e.detail#' , e.stackTrace ); + consoleLogger.error( '#e.message##chr(10)##e.detail#' ); + // Either way, this server is done like dinner + } finally { + consoleLogger.error( 'Stopping server...' ); + shell.setPrompt(); + process.destroy(); + } + } + + thread action="join" name="#threadName#"; } - } /** @@ -703,10 +942,11 @@ component accessors="true" singleton { * * @returns a struct containing * - defaultName - * - defaults + * - defaultwebroot * - defaultServerConfigFile * - serverJSON - * - serverInfo + * - serverInfo + * - serverIsNew */ function resolveServerDetails( required struct serverProps @@ -873,34 +1113,32 @@ component accessors="true" singleton { if( !arguments.all ){ var servers = getServers(); var serverdir = getCustomServerFolder( arguments.serverInfo ); - var defaultServerJSONPath = arguments.serverInfo.webroot & '/server.json'; - var serverJSONPath = arguments.serverInfo.webroot & '/server-#arguments.serverInfo.name#.json'; - - // try to delete from config first - structDelete( servers, arguments.serverInfo.id ); - setServers( servers ); - // try to delete server - if( directoryExists( serverDir ) ){ - // Catch this to gracefully handle where the OS or another program - // has the folder locked. - try { + + // Catch this to gracefully handle where the OS or another program + // has the folder locked. + try { + + // try to delete interal server dir server + if( directoryExists( serverDir ) ){ directoryDelete( serverdir, true ); - } catch( any e ) { - consoleLogger.error( '#e.message##chr(10)#Did you leave the server running? ' ); - logger.error( '#e.message# #e.detail#' , e.stackTrace ); } + + // Server home may be custom, so delete it as well + if( len( serverInfo.serverHomeDirectory ) && directoryExists( serverInfo.serverHomeDirectory ) ){ + directoryDelete( serverInfo.serverHomeDirectory, true ); + } + + + } catch( any e ) { + consoleLogger.error( '#e.message##chr(10)#Did you leave the server running? ' ); + logger.error( '#e.message# #e.detail#' , e.stackTrace ); + return ''; } - // Delete server.json if it exists - if( fileExists( serverJSONPath ) ) { - fileDelete( serverJSONPath ); - // return now so we don't wipe out the original server.json file too in the next - // if statement! (because this was a "server-#name#.json" file) - return "Poof! Wiped out server " & serverInfo.name; - } - if( fileExists( defaultServerJSONPath ) ) { - fileDelete( defaultServerJSONPath ); - } + // try to delete from config first + structDelete( servers, arguments.serverInfo.id ); + setServers( servers ); + // return message return "Poof! Wiped out server " & serverInfo.name; } else { @@ -1148,40 +1386,47 @@ component accessors="true" singleton { */ struct function newServerInfoStruct(){ return { - id : "", - port : 0, - host : "127.0.0.1", - stopsocket : 0, - debug : false, - status : "stopped", - statusInfo : { - result : "", - arguments : "", - command : "" + 'id' : "", + 'port' : 0, + 'host' : "127.0.0.1", + 'stopSocket' : 0, + 'debug' : false, + 'status' : "stopped", + 'statusInfo' : { + 'result' : "", + 'arguments' : "", + 'command' : "" }, - name : "", - logDir : "", - trayicon : "", - libDirs : "", - webConfigDir : "", - serverConfigDir : "", - webroot : "", - webXML : "", - HTTPEnable : true, - SSLEnable : false, - SSLPort : 1443, - SSLCert : "", - SSLKey : "", - SSLKeyPass : "", - rewritesEnable : false, - rewritesConfig : "", - heapSize : 512, - directoryBrowsing : true, - JVMargs : "", - runwarArgs : "", - cfengine : "", - WARPath : "", - serverConfigFile : "" + 'name' : "", + 'logDir' : "", + 'consolelogPath' : "", + 'trayicon' : "", + 'libDirs' : "", + 'webConfigDir' : "", + 'serverConfigDir' : "", + 'serverHomeDirectory' : "", + 'webroot' : "", + 'webXML' : "", + 'HTTPEnable' : true, + 'SSLEnable' : false, + 'SSLPort' : 1443, + 'SSLCert' : "", + 'SSLKey' : "", + 'SSLKeyPass' : "", + 'rewritesEnable' : false, + 'rewritesConfig' : "", + 'heapSize' : 512, + 'directoryBrowsing' : true, + 'JVMargs' : "", + 'runwarArgs' : "", + 'cfengine' : "", + 'engineName' : "", + 'engineVersion' : "", + 'WARPath' : "", + 'serverConfigFile' : "", + 'aliases' : {}, + 'errorPages' : {}, + 'trayOptions' : {} }; } @@ -1214,10 +1459,11 @@ component accessors="true" singleton { /** * Dynamic completion for property name based on contents of server.json - * @directory.hint web root - * @all.hint Pass false to ONLY suggest existing setting names. True will suggest all possible settings. + * @directory web root + * @all Pass false to ONLY suggest existing setting names. True will suggest all possible settings. + * @asSet Pass true to add = to the end of the options */ - function completeProperty( required directory, all=false ) { + function completeProperty( required directory, all=false, asSet=false ) { // Get all config settings currently set var props = JSONService.addProp( [], '', '', readServerJSON( arguments.directory & '/server.json' ) ); @@ -1227,15 +1473,18 @@ component accessors="true" singleton { props = JSONService.addProp( props, '', '', getDefaultServerJSON() ); // Suggest a couple optional web error pages props = JSONService.addProp( props, '', '', { - web : { - errorPages : { - 404 : '', - 500 : '', - default : '' + 'web' : { + 'errorPages' : { + '404' : '', + '500' : '', + 'default' : '' } } } ); } + if( asSet ) { + props = props.map( function( i ){ return i &= '='; } ); + } return props; } diff --git a/src/cfml/system/util/Completor.cfc b/src/cfml/system/util/Completor.cfc index 7a7fc3165..b9247cf2b 100644 --- a/src/cfml/system/util/Completor.cfc +++ b/src/cfml/system/util/Completor.cfc @@ -357,23 +357,18 @@ component singleton { * @type.showFiles Whether to hit files as well as directories **/ private function pathCompletion(String startsWith, required candidates, showFiles=true ) { - // This is what we add to relative paths, with the slashes normalized - var relativeRootPath = replace( shell.pwd() & '/', "\", "/", "all" ); - - // Keep track of whether this is a relative path or not. - var isRelative = false; - - // Note, I'm NOT using fileSystemUtil.resolvePath() here because I don't want the - // path canoncalized since that will break my text comparisons. Leave the ../ stuff in - var oPath = createObject( 'java', 'java.io.File' ).init( arguments.startsWith ); - if( !oPath.isAbsolute() ) { - isRelative = true; - // If it's relative, we assume it's relative to the current working directory and make it absolute - arguments.startsWith = relativeRootPath & arguments.startsWith; - } - - // This is out absolute directory as typed by the user + + // keep track of the original here so we can put it back like the user had + var originalStartsWith = replace( arguments.startsWith, "\", "/", "all" ); + // Fully resolve the path. + arguments.startsWith = fileSystemUtil.resolvePath( arguments.startsWith ); startsWith = replace( startsWith, "\", "/", "all" ); + + // make sure dirs are suffixed with a trailing slash or we'll strip it off, thinking it's a partial name + if( ( originalStartsWith == '' || originalStartsWith.endsWith( '/' ) ) && !startsWith.endsWith( '/' ) ) { + startsWith &= '/'; + } + // searchIn strips off partial directories, and has the last complete actual directory for searching. var searchIn = startsWith; // This is the bit at the end that is a partially typed directory or file name @@ -387,7 +382,7 @@ component singleton { partialMatch = replaceNoCase( startsWith, searchIn, '' ); } } - + // Don't even bother if search location doesn't exist if( directoryExists( searchIn ) ) { // Pull a list of paths in there @@ -410,11 +405,9 @@ component singleton { // This is the absolute path that we matched var thisCandidate = searchIn & ( right( searchIn, 1 ) == '/' ? '' : '/' ) & path.name; - // If we started with a relative path... - if( isRelative ) { - // ...strip it back down to what they typed - thisCandidate = replaceNoCase( thisCandidate, relativeRootPath, '' ); - } + // ...strip it back down to what they typed + thisCandidate = replaceNoCase( thisCandidate, startsWith, originalStartsWith ); + // Finally add this candidate into the list candidates.add( thisCandidate & ( path.type == 'dir' ? '/' : '' ) ); } @@ -431,10 +424,12 @@ component singleton { * @candidates.hint tree to populate with completion candidates **/ private function addCandidateIfMatch( required match, required startsWith, required candidates ) { - match = lcase( match ); startsWith = lcase( startsWith ); - if( match.startsWith( startsWith ) || len( startsWith ) == 0 ) { - candidates.add( match & ' ' ); + if( lcase( match ).startsWith( startsWith ) || len( startsWith ) == 0 ) { + if( !match.endsWith( '=' ) ) { + match &= ' '; + } + candidates.add( match ); } } diff --git a/src/cfml/system/util/FileSystem.cfc b/src/cfml/system/util/FileSystem.cfc index 301774a1c..73eed4c00 100644 --- a/src/cfml/system/util/FileSystem.cfc +++ b/src/cfml/system/util/FileSystem.cfc @@ -21,6 +21,7 @@ component accessors="true" singleton { function init() { variables.os = createObject( "java", "java.lang.System" ).getProperty( "os.name" ).toLowerCase(); + variables.userHome = createObject( 'java', 'java.lang.System' ).getProperty( 'user.home' ); return this; } @@ -38,9 +39,30 @@ component accessors="true" singleton { // This tells us if it's a relative path // Note, at this point we don't actually know if it actually even exists yet - if( !oPath.isAbsolute() ) { + + // If we're on windows and the path starts with / or \ + if( isWindows() && reFind( '^[\\\/]', path ) ) { + + // Concat it with the drive root in the base path so "/foo" becomes "C:/foo" (if the basepath is C:/etc) + oPath = createObject( 'java', 'java.io.File' ).init( listFirst( arguments.basePath, '/\' ) & '/' & path ); + + // If path is "~" + // Note, we're supporting this on Windows as well as Linux because it seems useful + } else if( path == '~' ) { + + var userHome = createObject( 'java', 'java.lang.System' ).getProperty( 'user.home' ); + oPath = createObject( 'java', 'java.io.File' ).init( userHome ); + + // If path starts with "~/something" but not "~foo" (a valid folder name) + } else if( reFind( '^~[\\\/]', path ) ) { + + oPath = createObject( 'java', 'java.io.File' ).init( userHome & right( path, len( path ) - 1 ) ); + + } else if( !oPath.isAbsolute() ) { + // If it's relative, we assume it's relative to the current working directory and make it absolute oPath = createObject( 'java', 'java.io.File' ).init( arguments.basePath & '/' & path ); + } // This will standardize the name and calculate stuff like ../../ diff --git a/src/cfml/system/util/Parser.cfc b/src/cfml/system/util/Parser.cfc index 79e5c2792..db7e3fa3b 100644 --- a/src/cfml/system/util/Parser.cfc +++ b/src/cfml/system/util/Parser.cfc @@ -64,6 +64,11 @@ component { // If we're in the middle of a quoted string, just keep appending if( inQuotes ) { + // Auto-escape = in a quoted string so it doesn't screw up named-parmeter detection. + // It will be unescaped later when we parse the params. + if( char == '=' && !isEscaped ) { + token &= '\'; + } token &= char; // We just reached the end of our quoted string if( char == quoteChar && !isEscaped ) { diff --git a/src/cfml/system/util/ReaderFactory.cfc b/src/cfml/system/util/ReaderFactory.cfc index b267736ef..723643595 100644 --- a/src/cfml/system/util/ReaderFactory.cfc +++ b/src/cfml/system/util/ReaderFactory.cfc @@ -38,12 +38,15 @@ component singleton{ reader = createObject( "java", "jline.console.ConsoleReader" ).init( arguments.inStream, arguments.outputStream ); } - // Let JLine handle Cntrl-C, and throw a UserInterruptException (instead of dying) - reader.setHandleUserInterrupt( true ); + // Let JLine handle Cntrl-C, and throw a UserInterruptException (instead of dying) + reader.setHandleUserInterrupt( true ); // This turns off special stuff that JLine2 looks for related to exclamation marks reader.setExpandEvents( false ); + // Turn off option to add space to end of completion that messes up stuff like path completion. + reader.getCompletionHandler().setPrintSpaceAfterFullCompletion( false ); + // Create our completer and set it in the console reader var jCompletor = createDynamicProxy( completor , [ 'jline.console.completer.Completer' ] ); reader.addCompleter( jCompletor ); diff --git a/src/cfml/system/util/SemanticVersion.cfc b/src/cfml/system/util/SemanticVersion.cfc index 0e03075c4..27f275c4a 100644 --- a/src/cfml/system/util/SemanticVersion.cfc +++ b/src/cfml/system/util/SemanticVersion.cfc @@ -578,10 +578,9 @@ component singleton{ * True if a specific version, false if a range that could match multiple versions * version.hint A string that contains a version or a range */ - boolean function isExactVersion( required string version ) { - // Default any missing pieces to "x" so "3" becomes "3.x.x". - arguments.version = getVersionAsString (parseVersion( clean( arguments.version ), 'x' ) ); + boolean function isExactVersion( required string version, boolean includeBuildID=false ) { + // First test for some sort of range if( version contains '*' ) return false; if( version contains 'x.' ) return false; if( version contains '.x' ) return false; @@ -593,7 +592,18 @@ component singleton{ if( version contains '~' ) return false; if( version contains '^' ) return false; if( version contains ' || ' ) return false; - return len( trim( version ) ) > 0; + + // Ok, looks like it might be a simple version format, so let's fire up the parser. + // Default any missing pieces to "x" so "3" becomes "3.x.x". + arguments.version = parseVersion( clean( arguments.version ), 'x' ); + + // If any of these bits are "x" it means they weren't specified. + if( version.major == 'x' ) { return false; } + if( version.minor == 'x' ) { return false; } + if( version.revision == 'x' ) { return false; } + if( includeBuildID && val( version.buildID ) == 0 ) { return false; } + + return true; } } \ No newline at end of file diff --git a/src/java/cliloader/BOMInputStream.java b/src/java/cliloader/BOMInputStream.java new file mode 100644 index 000000000..a6ad17a84 --- /dev/null +++ b/src/java/cliloader/BOMInputStream.java @@ -0,0 +1,341 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package cliloader; + +import java.io.IOException; +import java.io.InputStream; +import java.util.Arrays; +import java.util.List; + + +/** + * This class is used to wrap a stream that includes an encoded + * {@link ByteOrderMark} as its first bytes. + * + * This class detects these bytes and, if required, can automatically skip them + * and return the subsequent byte as the first byte in the stream. + * + * The {@link ByteOrderMark} implementation has the following pre-defined BOMs: + *
    + *
  • UTF-8 - {@link ByteOrderMark#UTF_8}
  • + *
  • UTF-16BE - {@link ByteOrderMark#UTF_16LE}
  • + *
  • UTF-16LE - {@link ByteOrderMark#UTF_16BE}
  • + *
+ * + * + *

Example 1 - Detect and exclude a UTF-8 BOM

+ *
+ *      BOMInputStream bomIn = new BOMInputStream(in);
+ *      if (bomIn.hasBOM()) {
+ *          // has a UTF-8 BOM
+ *      }
+ * 
+ * + *

Example 2 - Detect a UTF-8 BOM (but don't exclude it)

+ *
+ *      boolean include = true;
+ *      BOMInputStream bomIn = new BOMInputStream(in, include);
+ *      if (bomIn.hasBOM()) {
+ *          // has a UTF-8 BOM
+ *      }
+ * 
+ * + *

Example 3 - Detect Multiple BOMs

+ *
+ *      BOMInputStream bomIn = new BOMInputStream(in, ByteOrderMark.UTF_16LE, ByteOrderMark.UTF_16BE);
+ *      if (bomIn.hasBOM() == false) {
+ *          // No BOM found
+ *      } else if (bomIn.hasBOM(ByteOrderMark.UTF_16LE)) {
+ *          // has a UTF-16LE BOM
+ *      } else if (bomIn.hasBOM(ByteOrderMark.UTF_16BE)) {
+ *          // has a UTF-16BE BOM
+ *      }
+ * 
+ * + * @see org.apache.commons.io.ByteOrderMark + * @see Wikipedia - Byte Order Mark + * @version $Revision: 1052095 $ $Date: 2010-12-22 23:03:20 +0000 (Wed, 22 Dec 2010) $ + * @since Commons IO 2.0 + */ +public class BOMInputStream extends ProxyInputStream { + private final boolean include; + private final List boms; + private ByteOrderMark byteOrderMark; + private int[] firstBytes; + private int fbLength; + private int fbIndex; + private int markFbIndex; + private boolean markedAtStart; + + /** + * Constructs a new BOM InputStream that excludes + * a {@link ByteOrderMark#UTF_8} BOM. + * @param delegate the InputStream to delegate to + */ + public BOMInputStream(InputStream delegate) { + this(delegate, false, ByteOrderMark.UTF_8); + } + + /** + * Constructs a new BOM InputStream that detects a + * a {@link ByteOrderMark#UTF_8} and optionally includes it. + * @param delegate the InputStream to delegate to + * @param include true to include the UTF-8 BOM or + * false to exclude it + */ + public BOMInputStream(InputStream delegate, boolean include) { + this(delegate, include, ByteOrderMark.UTF_8); + } + + /** + * Constructs a new BOM InputStream that excludes + * the specified BOMs. + * @param delegate the InputStream to delegate to + * @param boms The BOMs to detect and exclude + */ + public BOMInputStream(InputStream delegate, ByteOrderMark... boms) { + this(delegate, false, boms); + } + + /** + * Constructs a new BOM InputStream that detects the + * specified BOMs and optionally includes them. + * @param delegate the InputStream to delegate to + * @param include true to include the specified BOMs or + * false to exclude them + * @param boms The BOMs to detect and optionally exclude + */ + public BOMInputStream(InputStream delegate, boolean include, ByteOrderMark... boms) { + super(delegate); + if (boms == null || boms.length == 0) { + throw new IllegalArgumentException("No BOMs specified"); + } + this.include = include; + this.boms = Arrays.asList(boms); + } + + /** + * Indicates whether the stream contains one of the specified BOMs. + * + * @return true if the stream has one of the specified BOMs, otherwise false + * if it does not + * @throws IOException if an error reading the first bytes of the stream occurs + */ + public boolean hasBOM() throws IOException { + return (getBOM() != null); + } + + /** + * Indicates whether the stream contains the specified BOM. + * + * @param bom The BOM to check for + * @return true if the stream has the specified BOM, otherwise false + * if it does not + * @throws IllegalArgumentException if the BOM is not one the stream + * is configured to detect + * @throws IOException if an error reading the first bytes of the stream occurs + */ + public boolean hasBOM(ByteOrderMark bom) throws IOException { + if (!boms.contains(bom)) { + throw new IllegalArgumentException("Stream not configure to detect " + bom); + } + return (byteOrderMark != null && getBOM().equals(bom)); + } + + /** + * Return the BOM (Byte Order Mark). + * + * @return The BOM or null if none + * @throws IOException if an error reading the first bytes of the stream occurs + */ + public ByteOrderMark getBOM() throws IOException { + if (firstBytes == null) { + int max = 0; + for (ByteOrderMark bom : boms) { + max = Math.max(max, bom.length()); + } + firstBytes = new int[max]; + for (int i = 0; i < firstBytes.length; i++) { + firstBytes[i] = in.read(); + fbLength++; + if (firstBytes[i] < 0) { + break; + } + + byteOrderMark = find(); + if (byteOrderMark != null) { + if (!include) { + fbLength = 0; + } + break; + } + } + } + return byteOrderMark; + } + + /** + * Return the BOM charset Name - {@link ByteOrderMark#getCharsetName()}. + * + * @return The BOM charset Name or null if no BOM found + * @throws IOException if an error reading the first bytes of the stream occurs + * + */ + public String getBOMCharsetName() throws IOException { + getBOM(); + return (byteOrderMark == null ? null : byteOrderMark.getCharsetName()); + } + + /** + * This method reads and either preserves or skips the first bytes in the + * stream. It behaves like the single-byte read() method, + * either returning a valid byte or -1 to indicate that the initial bytes + * have been processed already. + * @return the byte read (excluding BOM) or -1 if the end of stream + * @throws IOException if an I/O error occurs + */ + private int readFirstBytes() throws IOException { + getBOM(); + return (fbIndex < fbLength) ? firstBytes[fbIndex++] : -1; + } + + /** + * Find a BOM with the specified bytes. + * + * @return The matched BOM or null if none matched + */ + private ByteOrderMark find() { + for (ByteOrderMark bom : boms) { + if (matches(bom)) { + return bom; + } + } + return null; + } + + /** + * Check if the bytes match a BOM. + * + * @param bom The BOM + * @return true if the bytes match the bom, otherwise false + */ + private boolean matches(ByteOrderMark bom) { + if (bom.length() != fbLength) { + return false; + } + for (int i = 0; i < bom.length(); i++) { + if (bom.get(i) != firstBytes[i]) { + return false; + } + } + return true; + } + + //---------------------------------------------------------------------------- + // Implementation of InputStream + //---------------------------------------------------------------------------- + + /** + * Invokes the delegate's read() method, detecting and + * optionally skipping BOM. + * @return the byte read (excluding BOM) or -1 if the end of stream + * @throws IOException if an I/O error occurs + */ + @Override + public int read() throws IOException { + int b = readFirstBytes(); + return (b >= 0) ? b : in.read(); + } + + /** + * Invokes the delegate's read(byte[], int, int) method, detecting + * and optionally skipping BOM. + * @param buf the buffer to read the bytes into + * @param off The start offset + * @param len The number of bytes to read (excluding BOM) + * @return the number of bytes read or -1 if the end of stream + * @throws IOException if an I/O error occurs + */ + @Override + public int read(byte[] buf, int off, int len) throws IOException { + int firstCount = 0; + int b = 0; + while ((len > 0) && (b >= 0)) { + b = readFirstBytes(); + if (b >= 0) { + buf[off++] = (byte) (b & 0xFF); + len--; + firstCount++; + } + } + int secondCount = in.read(buf, off, len); + return (secondCount < 0) ? (firstCount > 0 ? firstCount : -1) : firstCount + secondCount; + } + + /** + * Invokes the delegate's read(byte[]) method, detecting and + * optionally skipping BOM. + * @param buf the buffer to read the bytes into + * @return the number of bytes read (excluding BOM) + * or -1 if the end of stream + * @throws IOException if an I/O error occurs + */ + @Override + public int read(byte[] buf) throws IOException { + return read(buf, 0, buf.length); + } + + /** + * Invokes the delegate's mark(int) method. + * @param readlimit read ahead limit + */ + @Override + public synchronized void mark(int readlimit) { + markFbIndex = fbIndex; + markedAtStart = (firstBytes == null); + in.mark(readlimit); + } + + /** + * Invokes the delegate's reset() method. + * @throws IOException if an I/O error occurs + */ + @Override + public synchronized void reset() throws IOException { + fbIndex = markFbIndex; + if (markedAtStart) { + firstBytes = null; + } + + in.reset(); + } + + /** + * Invokes the delegate's skip(long) method, detecting + * and optionallyskipping BOM. + * @param n the number of bytes to skip + * @return the number of bytes to skipped or -1 if the end of stream + * @throws IOException if an I/O error occurs + */ + @Override + public long skip(long n) throws IOException { + while ((n > 0) && (readFirstBytes() >= 0)) { + n--; + } + return in.skip(n); + } +} diff --git a/src/java/cliloader/ByteOrderMark.java b/src/java/cliloader/ByteOrderMark.java new file mode 100644 index 000000000..5808610fb --- /dev/null +++ b/src/java/cliloader/ByteOrderMark.java @@ -0,0 +1,169 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package cliloader; + +import java.io.Serializable; + +/** + * Byte Order Mark (BOM) representation - + * see {@link org.apache.commons.io.input.BOMInputStream}. + * + * @see org.apache.commons.io.input.BOMInputStream + * @see Wikipedia - Byte Order Mark + * @version $Id: ByteOrderMark.java 1005099 2010-10-06 16:13:01Z niallp $ + * @since Commons IO 2.0 + */ +public class ByteOrderMark implements Serializable { + + private static final long serialVersionUID = 1L; + + /** UTF-8 BOM */ + public static final ByteOrderMark UTF_8 = new ByteOrderMark("UTF-8", 0xEF, 0xBB, 0xBF); + /** UTF-16BE BOM (Big Endian) */ + public static final ByteOrderMark UTF_16BE = new ByteOrderMark("UTF-16BE", 0xFE, 0xFF); + /** UTF-16LE BOM (Little Endian) */ + public static final ByteOrderMark UTF_16LE = new ByteOrderMark("UTF-16LE", 0xFF, 0xFE); + + private final String charsetName; + private final int[] bytes; + + /** + * Construct a new BOM. + * + * @param charsetName The name of the charset the BOM represents + * @param bytes The BOM's bytes + * @throws IllegalArgumentException if the charsetName is null or + * zero length + * @throws IllegalArgumentException if the bytes are null or zero + * length + */ + public ByteOrderMark(String charsetName, int... bytes) { + if (charsetName == null || charsetName.length() == 0) { + throw new IllegalArgumentException("No charsetName specified"); + } + if (bytes == null || bytes.length == 0) { + throw new IllegalArgumentException("No bytes specified"); + } + this.charsetName = charsetName; + this.bytes = new int[bytes.length]; + System.arraycopy(bytes, 0, this.bytes, 0, bytes.length); + } + + /** + * Return the name of the {@link java.nio.charset.Charset} the BOM represents. + * + * @return the character set name + */ + public String getCharsetName() { + return charsetName; + } + + /** + * Return the length of the BOM's bytes. + * + * @return the length of the BOM's bytes + */ + public int length() { + return bytes.length; + } + + /** + * The byte at the specified position. + * + * @param pos The position + * @return The specified byte + */ + public int get(int pos) { + return bytes[pos]; + } + + /** + * Return a copy of the BOM's bytes. + * + * @return a copy of the BOM's bytes + */ + public byte[] getBytes() { + byte[] copy = new byte[bytes.length]; + for (int i = 0; i < bytes.length; i++) { + copy[i] = (byte)bytes[i]; + } + return copy; + } + + /** + * Indicates if this BOM's bytes equals another. + * + * @param obj The object to compare to + * @return true if the bom's bytes are equal, otherwise + * false + */ + @Override + public boolean equals(Object obj) { + if (!(obj instanceof ByteOrderMark)) { + return false; + } + ByteOrderMark bom = (ByteOrderMark)obj; + if (bytes.length != bom.length()) { + return false; + } + for (int i = 0; i < bytes.length; i++) { + if (bytes[i] != bom.get(i)) { + return false; + } + } + return true; + } + + /** + * Return the hashcode for this BOM. + * + * @return the hashcode for this BOM. + * @see java.lang.Object#hashCode() + */ + @Override + public int hashCode() { + int hashCode = getClass().hashCode(); + for (int b : bytes) { + hashCode += b; + } + return hashCode; + } + + /** + * Provide a String representation of the BOM. + * + * @return the length of the BOM's bytes + */ + @Override + public String toString() { + StringBuilder builder = new StringBuilder(); + builder.append(getClass().getSimpleName()); + builder.append('['); + builder.append(charsetName); + builder.append(": "); + for (int i = 0; i < bytes.length; i++) { + if (i > 0) { + builder.append(","); + } + builder.append("0x"); + builder.append(Integer.toHexString(0xFF & bytes[i]).toUpperCase()); + } + builder.append(']'); + return builder.toString(); + } + +} diff --git a/src/java/cliloader/LoaderCLIMain.java b/src/java/cliloader/LoaderCLIMain.java index a8971b001..17569f49d 100644 --- a/src/java/cliloader/LoaderCLIMain.java +++ b/src/java/cliloader/LoaderCLIMain.java @@ -295,11 +295,11 @@ private static File getCLI_HOME( ArrayList< String > cliArguments, } if( cliPropFile.isFile() ) { Properties userProps = new Properties(); - FileInputStream fi; + InputStream fi; try { log.debug( "checking for home in properties from " + cliPropFile.getCanonicalPath() ); - fi = new FileInputStream( cliPropFile ); + fi = new BOMInputStream( new FileInputStream( cliPropFile ), false ); userProps.load( fi ); fi.close(); if( mapGetNoCase( userProps, "cli.home" ) != null ) { @@ -476,7 +476,7 @@ public static void main( String[] arguments ) throws Throwable{ if( cliPropFile.isFile() ) { log.debug( "merging properties from " + cliPropFile.getCanonicalPath() ); - FileInputStream fi = new FileInputStream( cliPropFile ); + InputStream fi = new BOMInputStream( new FileInputStream( cliPropFile ), false ); userProps.load( fi ); fi.close(); props = mergeProperties( props, userProps ); @@ -497,8 +497,7 @@ public static void main( String[] arguments ) throws Throwable{ } if( new File( cli_home, "cli.properties" ).isFile() ) { - FileInputStream fi = new FileInputStream( new File( cli_home, - "cli.properties" ) ); + InputStream fi = new BOMInputStream( new FileInputStream( new File( cli_home, "cli.properties" ) ), false ); userProps.load( fi ); fi.close(); props = mergeProperties( props, userProps ); diff --git a/src/java/cliloader/ProxyInputStream.java b/src/java/cliloader/ProxyInputStream.java new file mode 100644 index 000000000..a2a9d7737 --- /dev/null +++ b/src/java/cliloader/ProxyInputStream.java @@ -0,0 +1,237 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package cliloader; + +import java.io.FilterInputStream; +import java.io.IOException; +import java.io.InputStream; + +/** + * A Proxy stream which acts as expected, that is it passes the method + * calls on to the proxied stream and doesn't change which methods are + * being called. + *

+ * It is an alternative base class to FilterInputStream + * to increase reusability, because FilterInputStream changes the + * methods being called, such as read(byte[]) to read(byte[], int, int). + *

+ * See the protected methods for ways in which a subclass can easily decorate + * a stream with custom pre-, post- or error processing functionality. + * + * @author Stephen Colebourne + * @version $Id: ProxyInputStream.java 934041 2010-04-14 17:37:24Z jukka $ + */ +public abstract class ProxyInputStream extends FilterInputStream { + + /** + * Constructs a new ProxyInputStream. + * + * @param proxy the InputStream to delegate to + */ + public ProxyInputStream(InputStream proxy) { + super(proxy); + // the proxy is stored in a protected superclass variable named 'in' + } + + /** + * Invokes the delegate's read() method. + * @return the byte read or -1 if the end of stream + * @throws IOException if an I/O error occurs + */ + @Override + public int read() throws IOException { + try { + beforeRead(1); + int b = in.read(); + afterRead(b != -1 ? 1 : -1); + return b; + } catch (IOException e) { + handleIOException(e); + return -1; + } + } + + /** + * Invokes the delegate's read(byte[]) method. + * @param bts the buffer to read the bytes into + * @return the number of bytes read or -1 if the end of stream + * @throws IOException if an I/O error occurs + */ + @Override + public int read(byte[] bts) throws IOException { + try { + beforeRead(bts != null ? bts.length : 0); + int n = in.read(bts); + afterRead(n); + return n; + } catch (IOException e) { + handleIOException(e); + return -1; + } + } + + /** + * Invokes the delegate's read(byte[], int, int) method. + * @param bts the buffer to read the bytes into + * @param off The start offset + * @param len The number of bytes to read + * @return the number of bytes read or -1 if the end of stream + * @throws IOException if an I/O error occurs + */ + @Override + public int read(byte[] bts, int off, int len) throws IOException { + try { + beforeRead(len); + int n = in.read(bts, off, len); + afterRead(n); + return n; + } catch (IOException e) { + handleIOException(e); + return -1; + } + } + + /** + * Invokes the delegate's skip(long) method. + * @param ln the number of bytes to skip + * @return the actual number of bytes skipped + * @throws IOException if an I/O error occurs + */ + @Override + public long skip(long ln) throws IOException { + try { + return in.skip(ln); + } catch (IOException e) { + handleIOException(e); + return 0; + } + } + + /** + * Invokes the delegate's available() method. + * @return the number of available bytes + * @throws IOException if an I/O error occurs + */ + @Override + public int available() throws IOException { + try { + return super.available(); + } catch (IOException e) { + handleIOException(e); + return 0; + } + } + + /** + * Invokes the delegate's close() method. + * @throws IOException if an I/O error occurs + */ + @Override + public void close() throws IOException { + try { + in.close(); + } catch (IOException e) { + handleIOException(e); + } + } + + /** + * Invokes the delegate's mark(int) method. + * @param readlimit read ahead limit + */ + @Override + public synchronized void mark(int readlimit) { + in.mark(readlimit); + } + + /** + * Invokes the delegate's reset() method. + * @throws IOException if an I/O error occurs + */ + @Override + public synchronized void reset() throws IOException { + try { + in.reset(); + } catch (IOException e) { + handleIOException(e); + } + } + + /** + * Invokes the delegate's markSupported() method. + * @return true if mark is supported, otherwise false + */ + @Override + public boolean markSupported() { + return in.markSupported(); + } + + /** + * Invoked by the read methods before the call is proxied. The number + * of bytes that the caller wanted to read (1 for the {@link #read()} + * method, buffer length for {@link #read(byte[])}, etc.) is given as + * an argument. + *

+ * Subclasses can override this method to add common pre-processing + * functionality without having to override all the read methods. + * The default implementation does nothing. + *

+ * Note this method is not called from {@link #skip(long)} or + * {@link #reset()}. You need to explicitly override those methods if + * you want to add pre-processing steps also to them. + * + * @since Commons IO 2.0 + * @param n number of bytes that the caller asked to be read + * @throws IOException if the pre-processing fails + */ + protected void beforeRead(int n) throws IOException { + } + + /** + * Invoked by the read methods after the proxied call has returned + * successfully. The number of bytes returned to the caller (or -1 if + * the end of stream was reached) is given as an argument. + *

+ * Subclasses can override this method to add common post-processing + * functionality without having to override all the read methods. + * The default implementation does nothing. + *

+ * Note this method is not called from {@link #skip(long)} or + * {@link #reset()}. You need to explicitly override those methods if + * you want to add post-processing steps also to them. + * + * @since Commons IO 2.0 + * @param n number of bytes read, or -1 if the end of stream was reached + * @throws IOException if the post-processing fails + */ + protected void afterRead(int n) throws IOException { + } + + /** + * Handle any IOExceptions thrown. + *

+ * This method provides a point to implement custom exception + * handling. The default behaviour is to re-throw the exception. + * @param e The IOException thrown + * @throws IOException if an I/O error occurs + * @since Commons IO 2.0 + */ + protected void handleIOException(IOException e) throws IOException { + throw e; + } + +} diff --git a/tests/cfml/system/util/TestSemanticVersion.cfc b/tests/cfml/system/util/TestSemanticVersion.cfc index f7f5fadbf..5591fc34d 100644 --- a/tests/cfml/system/util/TestSemanticVersion.cfc +++ b/tests/cfml/system/util/TestSemanticVersion.cfc @@ -54,6 +54,8 @@ component name="TestPrint" extends="mxunit.framework.TestCase" { assertFalse( semver.isExactVersion( '>1.0.0-alpha' ) ); assertFalse( semver.isExactVersion( '>=1.0.0-rc.0 <1.0.1' ) ); assertFalse( semver.isExactVersion( '^2 <2.2 || > 2.3' ) ); + assertTrue( semver.isExactVersion( '1.2.3+123', true ) ); + assertFalse( semver.isExactVersion( '1.2.3', true ) ); } public void function testIsNew() {