diff --git a/.gitattributes b/.gitattributes index bfc0e45b3..ff2f82fbc 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1,4 +1,5 @@ # Auto detect text files and perform LF normalization +.sh eol=lf * text=auto # Custom for Visual Studio diff --git a/.github/FUNDING.YML b/.github/FUNDING.YML new file mode 100644 index 000000000..7e59d1336 --- /dev/null +++ b/.github/FUNDING.YML @@ -0,0 +1 @@ +patreon: ortussolutions diff --git a/build/build.properties b/build/build.properties index 59d70a7d7..299b4719a 100644 --- a/build/build.properties +++ b/build/build.properties @@ -10,19 +10,19 @@ java.debug=true #dependencies dependencies.dir=${basedir}/lib -cfml.version=5.3.8.206 +cfml.version=5.3.9.133-SNAPSHOT cfml.extensions=8D7FB0DF-08BB-1589-FE3975678F07DB17 -cfml.loader.version=2.6.5 +cfml.loader.version=2.6.16 cfml.cli.version=${cfml.loader.version}.${cfml.version} lucee.version=${cfml.version} # Don't bump this version. Need to remove this dependency from cfmlprojects.org lucee.config.version=5.2.4.37 -jre.version=jdk-11.0.12+7 +jre.version=jdk-11.0.15+10 launch4j.version=3.14 -runwar.version=4.5.2 -jline.version=3.19.0 +runwar.version=4.7.4 +jline.version=3.21.0 jansi.version=2.3.2 -jgit.version=5.11.0.202103091610-r +jgit.version=5.13.0.202109080827-r #build locations build.type=localdev diff --git a/build/build.xml b/build/build.xml index bb52e37a7..bdc0d7cd9 100644 --- a/build/build.xml +++ b/build/build.xml @@ -16,8 +16,8 @@ External Dependencies: - - + + @@ -382,9 +382,9 @@ External Dependencies: + - @@ -394,6 +394,7 @@ External Dependencies: + - + @@ -1077,7 +1078,7 @@ External Dependencies: - + @@ -1086,6 +1087,8 @@ External Dependencies: + + diff --git a/src/cfml/system/BaseCommand.cfc b/src/cfml/system/BaseCommand.cfc index 898e7f6a9..2fc576e0b 100644 --- a/src/cfml/system/BaseCommand.cfc +++ b/src/cfml/system/BaseCommand.cfc @@ -221,15 +221,15 @@ component accessors="true" singleton { function error( required message, detail='', clearPrintBuffer=false, exitCode=1 ) { wirebox.getInstance( "ConsolePainter" ).stop( message ); - print.line().toConsole(); + + if( !getSystemSetting( 'box_currentCommandPiped', false ) ) { + print.line().toConsole(); + } setExitCode( arguments.exitCode ); if( arguments.clearPrintBuffer ) { // Wipe print.clear(); - } else { - // Distance ourselves from whatever other output the command may have given so far. - print.line(); } throw( message=arguments.message, detail=arguments.detail, type="commandException", errorcode=arguments.exitCode ); } diff --git a/src/cfml/system/Shell.cfc b/src/cfml/system/Shell.cfc index f7033e8eb..0fcff1032 100644 --- a/src/cfml/system/Shell.cfc +++ b/src/cfml/system/Shell.cfc @@ -142,9 +142,9 @@ component accessors="true" singleton { setTempDir( variables.tempdir ); getInterceptorService().configure(); - - getInterceptorService().registerInterceptor( - interceptor = endpointService, + + getInterceptorService().registerInterceptor( + interceptor = endpointService, interceptorObject = endpointService, interceptorName = "endpoint-service" ); @@ -163,13 +163,13 @@ component accessors="true" singleton { } else { variables.commandService.configure(); } - + // Ensure we have a system box.json var systemBoxJSON = expandPath( '/commandbox/box.json' ); if( !fileExists( systemBoxJSON ) ) { fileWrite( systemBoxJSON, '{ "name":"CommandBox System" }' ); } - + } @@ -439,7 +439,7 @@ component accessors="true" singleton { * Get's terminal width **/ function getTermWidth() { - return variables.reader.getTerminal().getWidth(); + return configService.getSetting( "terminalWidth", variables.reader.getTerminal().getWidth() ); } /** @@ -485,6 +485,10 @@ component accessors="true" singleton { * @directory.hint directory to CD to. Please verify it exists before calling. **/ String function cd( directory="" ){ + // Ensure we have a trailing slash for our directory. + if( !(arguments.directory.endsWith( '/' ) || arguments.directory.endsWith( '\' ) ) ) { + arguments.directory &= '/'; + } variables.pwd = arguments.directory; request.lastCWD = arguments.directory; // Update prompt to reflect directory change @@ -542,14 +546,22 @@ component accessors="true" singleton { var terminal = getReader().getTerminal(); if( terminal.paused() ) { - terminal.resume(); + terminal.resume(); } - // Shell stops on this line while waiting for user input - if( arguments.silent ) { - line = variables.reader.readLine( interceptData.prompt, javacast( "char", ' ' ) ); + param request.developerModeReloading = false; + param request.developerModeCommand=''; + if( len( request.developerModeCommand) ) { + line = request.developerModeCommand; + request.developerModeReloading=true; + request.developerModeCommand = ''; } else { - line = variables.reader.readLine( interceptData.prompt ); + // Shell stops on this line while waiting for user input + if( arguments.silent ) { + line = variables.reader.readLine( interceptData.prompt, javacast( "char", ' ' ) ); + } else { + line = variables.reader.readLine( interceptData.prompt ); + } } // User hits Ctrl-C. Don't let them exit the shell. @@ -598,6 +610,21 @@ component accessors="true" singleton { // If there's input, try to run it. if( len( trim( line ) ) ) { + + param request.developerModeReloaded = false; + if( configService.getSetting( 'developerMode', false ) && !request.developerModeReloading ){ + // If we've never reloaded, the CLI just started, so just clear the cache + if( !request.developerModeReloaded ){ + wirebox.getCacheBox().getCache( 'metadataCache' ).clearAll(); + request.developerModeReloaded = true; + } else { + request.developerModeCommand = line; + reload( clear=false ); + return true; + } + } + request.developerModeReloading=false; + var interceptData = { line : line } @@ -749,17 +776,19 @@ component accessors="true" singleton { /** * Call a command - * @command.hint Either a string containing a text command, or an array of tokens representing the command and parameters. - * @returnOutput.hint True will return the output of the command as a string, false will send the output to the console. If command outputs nothing, an empty string will come back. - * @piped.hint Any text being piped into the command. This will overwrite the first parameter (pushing any positional params back) - * @initialCommand.hint Since commands can recursively call new commands via this method, this flags the first in the chain so exceptions can bubble all the way back to the beginning. + * @command Either a string containing a text command, or an array of tokens representing the command and parameters. + * @returnOutput True will return the output of the command as a string, false will send the output to the console. If command outputs nothing, an empty string will come back. + * @piped Any text being piped into the command. This will overwrite the first parameter (pushing any positional params back) + * @initialCommand Since commands can recursively call new commands via this method, this flags the first in the chain so exceptions can bubble all the way back to the beginning. + * @line If passing an array of tokens, this is the original, unparsed line typed by the user * In other words, if "foo" calls "bar", which calls "baz" and baz errors, all three commands are scrapped and do not finish execution. **/ function callCommand( required any command, returnOutput=false, string piped, - boolean initialCommand=false ) { + boolean initialCommand=false, + string line ) { var job = wirebox.getInstance( 'interactiveJob' ); var ConsolePainter = wirebox.getInstance( 'ConsolePainter' ); @@ -778,10 +807,13 @@ component accessors="true" singleton { try{ if( isArray( command ) ) { + if( isNull( arguments.line ) ) { + arguments.line = command.toList( ' ' ); + } if( structKeyExists( arguments, 'piped' ) ) { - var result = variables.commandService.runCommandTokens( arguments.command, piped, returnOutput ); + var result = variables.commandService.runCommandTokens( arguments.command, piped, returnOutput, line ); } else { - var result = variables.commandService.runCommandTokens( tokens=arguments.command, captureOutput=returnOutput ); + var result = variables.commandService.runCommandTokens( tokens=arguments.command, captureOutput=returnOutput, line=line ); } } else { var result = variables.commandService.runCommandLine( arguments.command, returnOutput ); @@ -796,7 +828,7 @@ component accessors="true" singleton { ConsolePainter.forceStop(); - printError( { message : e.message, detail: e.detail } ); + printError( { message : e.message, detail: e.detail, extendedInfo : e.extendedInfo ?: '' } ); } // This type of error means the user hit Ctrl-C, during a readLine() call. Duck out and move along. } catch (any e) { @@ -817,7 +849,7 @@ component accessors="true" singleton { job.reset(); variables.reader.getTerminal().writer().flush(); variables.reader.getTerminal().writer().println(); - variables.reader.getTerminal().writer().print( variables.print.boldRedLine( 'CANCELLED' ) ); + variables.reader.getTerminal().writer().print( variables.print.boldRedLine( 'CANCELLED' ) ); } // Anything else is completely unexpected and means boom booms happened-- full stack please. } else { @@ -891,6 +923,13 @@ component accessors="true" singleton { setExitCode( 1 ); } + if( !isNull( err.extendedInfo ) && isJSON( err.extendedInfo ) ){ + var info = deserializeJSON( err.extendedInfo ); + if( info.keyExists( 'commandOutput' ) ) { + variables.reader.getTerminal().writer().print( info.commandOutput ); + } + } + var verboseErrors = true; try{ verboseErrors = configService.getSetting( 'verboseErrors', false ); @@ -901,11 +940,17 @@ component accessors="true" singleton { getInterceptorService().announceInterception( 'onException', { exception=err } ); } - variables.logger.error( '#arguments.err.message# #arguments.err.detail ?: ''#', arguments.err.stackTrace ?: '' ); + variables.logger.error( '#arguments.err.message ?: ''# #arguments.err.detail ?: ''#', arguments.err.stackTrace ?: '' ); + variables.reader.getTerminal().writer().println(); + variables.reader.getTerminal().writer().println(); variables.reader.getTerminal().writer().print( variables.print.whiteOnRedLine( 'ERROR (#variables.version#)' ) ); variables.reader.getTerminal().writer().println(); - variables.reader.getTerminal().writer().println( variables.print.boldRedText( variables.formatterUtil.HTML2ANSI( arguments.err.message, 'boldRed' ) ) ); + if( isNull( arguments.err.message ) ) { + variables.reader.getTerminal().writer().println( variables.print.boldRedText( variables.formatterUtil.HTML2ANSI( arguments.err.type, 'boldRed' ) ) ); + } else { + variables.reader.getTerminal().writer().println( variables.print.boldRedText( variables.formatterUtil.HTML2ANSI( arguments.err.message, 'boldRed' ) ) ); + } try{ @@ -919,7 +964,7 @@ component accessors="true" singleton { while( !isNull( cause ) ) { // If the nested exception has the same type as the outer exception and no message, there's no value in it here. (Lucee's nesting of IOExceptions can do this) // Or if there are two levels of causes with the same type and Message. (RabbitMQ's Java client does this) - if( (cause.getClass().getName() == arguments.err.message && isNull( cause.getMessage() ) ) + if( (cause.getClass().getName() == ( arguments.err.message ?: '' ) && isNull( cause.getMessage() ) ) || ( cause.getClass().getName() == previousType && previousMessage == cause.getMessage() ?: '' ) ) { // move the pointer and move on cause = cause.getCause(); diff --git a/src/cfml/system/endpoints/CFLib.cfc b/src/cfml/system/endpoints/CFLib.cfc index ac423f5f0..cc9971a9e 100644 --- a/src/cfml/system/endpoints/CFLib.cfc +++ b/src/cfml/system/endpoints/CFLib.cfc @@ -17,6 +17,7 @@ component accessors="true" implements="IEndpoint" singleton { property name="progressBar" inject="ProgressBar"; property name="CR" inject="CR@constants"; property name='wirebox' inject='wirebox'; + property name='configService' inject='configService'; // Properties property name="namePrefixes" type="string"; @@ -27,6 +28,11 @@ component accessors="true" implements="IEndpoint" singleton { } public string function resolvePackage( required string package, boolean verbose=false ) { + + if( configService.getSetting( 'offlineMode', false ) ) { + throw( 'Can''t download [#getNamePrefixes()#:#package#], CommandBox is in offline mode. Go online with [config set offlineMode=false].', 'endpointException' ); + } + var job = wirebox.getInstance( 'interactiveJob' ); var folderName = tempDir & '/' & 'temp#createUUID()#'; var fullPath = folderName & '/' & package & '.cfm'; diff --git a/src/cfml/system/endpoints/ForgeBox.cfc b/src/cfml/system/endpoints/ForgeBox.cfc index 18eaf523f..acb6ed8d4 100644 --- a/src/cfml/system/endpoints/ForgeBox.cfc +++ b/src/cfml/system/endpoints/ForgeBox.cfc @@ -22,8 +22,8 @@ component accessors="true" implements="IEndpointInteractive" { property name="fileSystemUtil" inject="FileSystem"; property name="fileEndpoint" inject="commandbox.system.endpoints.File"; property name="lexEndpoint" inject="commandbox.system.endpoints.Lex"; - property name='pathPatternMatcher' inject='provider:pathPatternMatcher@globber'; property name='wirebox' inject='wirebox'; + property name='logger' inject='logbox:logger:{this}'; // Properties property name="namePrefixes" type="string"; @@ -52,6 +52,9 @@ component accessors="true" implements="IEndpointInteractive" { job.addLog( "Package found in local artifacts!"); // Install the package var thisArtifactPath = artifactService.getArtifactPath( slug, version ); + + recordInstall( slug, version ); + // Defer to file endpoint return fileEndpoint.resolvePackage( thisArtifactPath, arguments.verbose ); } else { @@ -504,12 +507,7 @@ component accessors="true" implements="IEndpointInteractive" { job.addLog( "Installing version [#arguments.version#]." ); - try { - forgeBox.recordInstall( arguments.slug, arguments.version, APIToken ); - } catch( forgebox var e ) { - job.addLog( e.message ); - job.addLog( e.detail ); - } + recordInstall( arguments.slug, arguments.version ); var packageType = entryData.typeSlug; @@ -578,7 +576,7 @@ component accessors="true" implements="IEndpointInteractive" { throw( e.message, 'endpointException', e.detail ); } - job.addErrorLog( "Aww man, #getNamePrefixes()# ran into an issue."); + job.addErrorLog( "Aww man, #getNamePrefixes()# ran into an issue."); job.addLog( "#e.message# #e.detail#" ); job.addErrorLog( "We're going to look in your local artifacts cache and see if one of those versions will work."); @@ -590,6 +588,8 @@ component accessors="true" implements="IEndpointInteractive" { job.addLog( "Sweet! We found a local version of [#satisfyingVersion#] that we can use in your artifacts."); job.addLog( "" ); + recordInstall( arguments.slug, satisfyingVersion ); + var thisArtifactPath = artifactService.getArtifactPath( slug, satisfyingVersion ); // Defer to file endpoint return fileEndpoint.resolvePackage( thisArtifactPath, arguments.verbose ); @@ -616,17 +616,12 @@ component accessors="true" implements="IEndpointInteractive" { directoryDelete( tmpPath, true ); } directoryCreate( tmpPath ); - directoryCopy( arguments.path, tmpPath, true, function( directoryPath ){ - // This will normalize the slashes to match - directoryPath = fileSystemUtil.resolvePath( directoryPath ); - - // cleanup path so we just get from the archive down - var thisPath = replacenocase( directoryPath, path, "" ); - // Ignore paths that match one of our ignore patterns - var ignored = pathPatternMatcher.matchPatterns( ignorePatterns, thisPath ); - // What do we do with this file/directory - return ! ignored; - }); + wirebox.getInstance( 'globber' ) + .inDirectory( arguments.path ) + .setExcludePattern( ignorePatterns ) + .loose() + .copyTo( tmpPath ); + var zipFileName = tmpPath & ".zip"; cfzip( action = "zip", @@ -712,5 +707,15 @@ component accessors="true" implements="IEndpointInteractive" { } } + function recordInstall( slug, version ) { + thread name="#createUUID()#" slug="#arguments.slug#", version="#arguments.version#" { + try { + var foo = forgeBox.recordInstall( attributes.slug, attributes.version, getAPIToken() ); + } catch( any e ) { + logger.error( 'Error recording install', e ) + } + } + } + } diff --git a/src/cfml/system/endpoints/Git.cfc b/src/cfml/system/endpoints/Git.cfc index 613b83996..5fb94eaf3 100644 --- a/src/cfml/system/endpoints/Git.cfc +++ b/src/cfml/system/endpoints/Git.cfc @@ -34,6 +34,7 @@ component accessors="true" implements="IEndpoint" singleton { property name='wirebox' inject='wirebox'; property name="semanticVersion" inject="provider:semanticVersion@semver"; property name='semverRegex' inject='semverRegex@constants'; + property name='configService' inject='configService'; // Properties property name="namePrefixes" type="string"; @@ -44,6 +45,11 @@ component accessors="true" implements="IEndpoint" singleton { } public string function resolvePackage( required string package, boolean verbose=false ) { + + if( configService.getSetting( 'offlineMode', false ) ) { + throw( 'Can''t clone [#getNamePrefixes()#:#package#], CommandBox is in offline mode. Go online with [config set offlineMode=false].', 'endpointException' ); + } + var job = wirebox.getInstance( 'interactiveJob' ); var GitURL = replace( arguments.package, '//', '' ); GitURL = getProtocol() & GitURL; diff --git a/src/cfml/system/endpoints/HTTP.cfc b/src/cfml/system/endpoints/HTTP.cfc index 85e4297d4..f155d9d82 100644 --- a/src/cfml/system/endpoints/HTTP.cfc +++ b/src/cfml/system/endpoints/HTTP.cfc @@ -20,6 +20,7 @@ component accessors=true implements="IEndpoint" singleton { property name='wirebox' inject='wirebox'; property name="semanticVersion" inject="provider:semanticVersion@semver"; property name='semverRegex' inject='semverRegex@constants'; + property name='configService' inject='configService'; // Properties property name="namePrefixes" type="string"; @@ -39,6 +40,11 @@ component accessors=true implements="IEndpoint" singleton { } public string function resolvePackageZip( required string package, boolean verbose=false ) { + + if( configService.getSetting( 'offlineMode', false ) ) { + throw( 'Can''t download [#getNamePrefixes()#:#package#], CommandBox is in offline mode. Go online with [config set offlineMode=false].', 'endpointException' ); + } + var job = wirebox.getInstance( 'interactiveJob' ); var fileName = 'temp#createUUID()#.zip'; diff --git a/src/cfml/system/endpoints/Jar.cfc b/src/cfml/system/endpoints/Jar.cfc index bd2ba96b9..2fd258931 100644 --- a/src/cfml/system/endpoints/Jar.cfc +++ b/src/cfml/system/endpoints/Jar.cfc @@ -19,6 +19,7 @@ component accessors=true implements="IEndpoint" singleton { property name='JSONService' inject='JSONService'; property name='wirebox' inject='wirebox'; property name='S3Service' inject='S3Service'; + property name='configService' inject='configService'; // Properties property name="namePrefixes" type="string"; @@ -30,6 +31,11 @@ component accessors=true implements="IEndpoint" singleton { } public string function resolvePackage( required string package, boolean verbose=false ) { + + if( configService.getSetting( 'offlineMode', false ) ) { + throw( 'Can''t download [#getNamePrefixes()#:#package#], CommandBox is in offline mode. Go online with [config set offlineMode=false].', 'endpointException' ); + } + var job = wirebox.getInstance( 'interactiveJob' ); var folderName = tempDir & '/' & 'temp#createUUID()#'; var fullJarPath = folderName & '/' & getDefaultName( package ) & '.jar'; diff --git a/src/cfml/system/endpoints/Java.cfc b/src/cfml/system/endpoints/Java.cfc index 4879d925e..b2a8e793d 100644 --- a/src/cfml/system/endpoints/Java.cfc +++ b/src/cfml/system/endpoints/Java.cfc @@ -36,6 +36,7 @@ component accessors=true implements="IEndpoint" singleton { property name='filesystemUtil' inject='fileSystem'; property name="folderEndpoint" inject="commandbox.system.endpoints.Folder"; property name="PackageService" inject="packageService"; + property name='configService' inject='configService'; // Properties property name="namePrefixes" type="string"; @@ -46,6 +47,11 @@ component accessors=true implements="IEndpoint" singleton { } public string function resolvePackage( required string package, boolean verbose=false ) { + + if( configService.getSetting( 'offlineMode', false ) ) { + throw( 'Can''t download [#getNamePrefixes()#:#package#], CommandBox is in offline mode. Go online with [config set offlineMode=false].', 'endpointException' ); + } + var lockVersion = false; if( package.right( 12 ) == ':lockVersion' ) { var lockVersion = true; @@ -61,12 +67,12 @@ component accessors=true implements="IEndpoint" singleton { // Turn it into the maven-style semver range [11,12) which is the equiv of >= 11 && < 12 thus getting 11.x var thisVersionNum = replaceNoCase( javaDetails.version, 'openjdk', '' ); var thisVersion = '[#thisVersionNum#,#thisVersionNum+1#)'; - var APIURLInfo = 'https://api.adoptopenjdk.net/v3/assets/version/#encodeForURL( thisVersion )#?page_size=1000&release_type=ga&vendor=adoptopenjdk&project=jdk&heap_size=normal&jvm_impl=#encodeForURL( javaDetails['jvm-implementation'] )#&os=#encodeForURL( javaDetails.os )#&architecture=#encodeForURL( javaDetails.arch )#&image_type=#encodeForURL( javaDetails.type )#'; + var APIURLInfo = 'https://api.adoptium.net/v3/assets/version/#encodeForURL( thisVersion )#?page_size=1000&release_type=ga&vendor=eclipse&project=jdk&heap_size=normal&jvm_impl=#encodeForURL( javaDetails['jvm-implementation'] )#&os=#encodeForURL( javaDetails.os )#&architecture=#encodeForURL( javaDetails.arch )#&image_type=#encodeForURL( javaDetails.type )#'; if( javaDetails.release.len() && javaDetails.release != 'latest' ) { - var APIURL = 'https://api.adoptopenjdk.net/v3/binary/version/#encodeForURL( javaDetails.release )#/#encodeForURL( javaDetails.os )#/#encodeForURL( javaDetails.arch )#/#encodeForURL( javaDetails.type )#/#encodeForURL( javaDetails['jvm-implementation'] )#/normal/adoptopenjdk'; + var APIURL = 'https://api.adoptium.net/v3/binary/version/#encodeForURL( javaDetails.release )#/#encodeForURL( javaDetails.os )#/#encodeForURL( javaDetails.arch )#/#encodeForURL( javaDetails.type )#/#encodeForURL( javaDetails['jvm-implementation'] )#/normal/eclipse'; } else { - var APIURL = 'https://api.adoptopenjdk.net/v3/binary/latest/#thisVersionNum#/ga/#encodeForURL( javaDetails.os )#/#encodeForURL( javaDetails.arch )#/#encodeForURL( javaDetails.type )#/#encodeForURL( javaDetails['jvm-implementation'] )#/normal/adoptopenjdk'; + var APIURL = 'https://api.adoptium.net/v3/binary/latest/#thisVersionNum#/ga/#encodeForURL( javaDetails.os )#/#encodeForURL( javaDetails.arch )#/#encodeForURL( javaDetails.type )#/#encodeForURL( javaDetails['jvm-implementation'] )#/normal/eclipse'; } job.addLog( "Installing [#package#]" ); @@ -82,7 +88,7 @@ component accessors=true implements="IEndpoint" singleton { return serveFromArtifacts( package, packageFullName, lockVersion ); } - job.addLog( 'Hitting the AdoptOpenJDK API to find your download.' ); + job.addLog( 'Hitting the Adoptium API to find your download.' ); job.addLog( APIURLInfo ); @@ -124,7 +130,7 @@ component accessors=true implements="IEndpoint" singleton { // which is the equiv of >= 11 && < 12 thus getting 11.x var thisVersion = '[#thisVersionNum#,#thisVersionNum+1#)'; - var APIURLCheck = 'https://api.adoptopenjdk.net/v3/assets/version/#encodeForURL(thisVersion )#?release_type=ga&vendor=adoptopenjdk&project=jdk&heap_size=normal&jvm_impl=#encodeForURL( javaDetails['jvm-implementation'] )#&os=#encodeForURL( javaDetails.os )#&architecture=#encodeForURL( javaDetails.arch )#&image_type=#encodeForURL( javaDetails.type )#'; + var APIURLCheck = 'https://api.adoptium.net/v3/assets/version/#encodeForURL(thisVersion )#?release_type=ga&vendor=eclipse&project=jdk&heap_size=normal&jvm_impl=#encodeForURL( javaDetails['jvm-implementation'] )#&os=#encodeForURL( javaDetails.os )#&architecture=#encodeForURL( javaDetails.arch )#&image_type=#encodeForURL( javaDetails.type )#'; http url="#APIURLCheck#" @@ -155,7 +161,7 @@ component accessors=true implements="IEndpoint" singleton { job.addErrorLog( message ); // Before we give up, check artifacts for a downloaded version that might work - // Ideally I'd only do this for catastrophic errors, but the AdoptOpenJDK API doesn't really allow me to + // Ideally I'd only do this for catastrophic errors, but the Adoptium API doesn't really allow me to // tell the difference since it pretty much just pukes non-JSON if it can't find what I was looking for var artifactJDKs = artifactService.listArtifacts( 'OpenJDK' ); if( artifactJDKs.keyExists( 'OpenJDK' ) ) { @@ -250,8 +256,8 @@ component accessors=true implements="IEndpoint" singleton { 'type' : 'projects', 'java' : artifactJSON.binaries[ 1 ], 'author' : 'AdoptOpenJDK', - 'projectURL' : 'https://adoptopenjdk.net/', - 'homepage' : 'https://adoptopenjdk.net/' + 'projectURL' : 'https://adoptium.net/', + 'homepage' : 'https://adoptium.net/' }; JSONService.writeJSONFile( fullBoxJSONPath, boxJSON ); diff --git a/src/cfml/system/endpoints/Lex.cfc b/src/cfml/system/endpoints/Lex.cfc index aae583550..8eae652f2 100644 --- a/src/cfml/system/endpoints/Lex.cfc +++ b/src/cfml/system/endpoints/Lex.cfc @@ -18,6 +18,7 @@ component accessors=true implements="IEndpoint" singleton { property name='JSONService' inject='JSONService'; property name='wirebox' inject='wirebox'; property name='S3Service' inject='S3Service'; + property name='configService' inject='configService'; // Properties property name="namePrefixes" type="string"; @@ -28,6 +29,11 @@ component accessors=true implements="IEndpoint" singleton { } public string function resolvePackage( required string package, boolean verbose=false ) { + + if( configService.getSetting( 'offlineMode', false ) ) { + throw( 'Can''t download [#getNamePrefixes()#:#package#], CommandBox is in offline mode. Go online with [config set offlineMode=false].', 'endpointException' ); + } + var job = wirebox.getInstance( 'interactiveJob' ); var folderName = tempDir & '/' & 'temp#createUUID()#'; var fullLexPath = folderName & '/' & getDefaultName( package ) & '.lex'; diff --git a/src/cfml/system/endpoints/S3.cfc b/src/cfml/system/endpoints/S3.cfc index a50c566d1..f5ddf4028 100644 --- a/src/cfml/system/endpoints/S3.cfc +++ b/src/cfml/system/endpoints/S3.cfc @@ -20,6 +20,7 @@ component accessors="true" implements="IEndpoint" singleton { property name='wirebox' inject='wirebox'; property name="semanticVersion" inject="provider:semanticVersion@semver"; property name='semverRegex' inject='semverRegex@constants'; + property name='configService' inject='configService'; // Properties property name="namePrefixes" type="string"; @@ -30,6 +31,11 @@ component accessors="true" implements="IEndpoint" singleton { } public string function resolvePackage(required string package, boolean verbose=false) { + + if( configService.getSetting( 'offlineMode', false ) ) { + throw( 'Can''t download [#getNamePrefixes()#:#package#], CommandBox is in offline mode. Go online with [config set offlineMode=false].', 'endpointException' ); + } + var job = wirebox.getInstance('interactiveJob'); var fileName = 'temp#createUUID()#.zip'; diff --git a/src/cfml/system/modules/globber/box.json b/src/cfml/system/modules/globber/box.json index 38724feee..412c14137 100644 --- a/src/cfml/system/modules/globber/box.json +++ b/src/cfml/system/modules/globber/box.json @@ -1,8 +1,7 @@ { "name":"Globber", - "version":"3.0.6", + "version":"3.1.1", "author":"Brad Wood", - "location":"Ortus-Solutions/globber#v3.0.6", "homepage":"https://github.com/Ortus-Solutions/globber/", "documentation":"https://github.com/Ortus-Solutions/globber/", "repository":{ @@ -15,15 +14,14 @@ "type":"modules", "dependencies":{}, "devDependencies":{ - "coldbox":"^4.3.0+188", - "testbox":"^2.4.0+80" + "testbox":"^4.4.0-snapshot", + "coldbox":"^4.3.0+188" }, "installPaths":{ - "testbox":"testbox", - "coldbox":"tests\\resources\\app\\coldbox" + "coldbox":"tests/resources/app/coldbox/", + "testbox":"testbox/" }, "scripts":{ - "postVersion":"package set location='Ortus-Solutions/globber#v`package version`'", "onRelease":"forgebox use ortus && publish", "postPublish":"!git push --follow-tags" }, diff --git a/src/cfml/system/modules/globber/models/Globber.cfc b/src/cfml/system/modules/globber/models/Globber.cfc index e3567dd61..817306df4 100644 --- a/src/cfml/system/modules/globber/models/Globber.cfc +++ b/src/cfml/system/modules/globber/models/Globber.cfc @@ -19,10 +19,14 @@ component accessors="true" { property name='pattern'; /** The file globbing pattern NOT to match. */ property name='excludePattern'; + property name='notExcludePattern'; /** query of real file system resources that match the pattern */ property name='matchQuery'; + property name='matchQueryArray'; /** Return matches as a query instead of an array */ property name='format' default='array'; + /** Uses Gitignore rules that match pattern anywhere inside path, not requiring explicit * at the start or end of the pattern */ + property name='loose' default='false'; /** Sort to use */ property name='sort' default='type, name'; /** Directory the list was pulled from */ @@ -33,6 +37,26 @@ component accessors="true" { variables.format = 'array'; variables.pattern = []; variables.excludePattern = []; + variables.notExcludePattern = []; + variables.loose = false; + variables.matchQueryArray=[]; + return this; + } + + /** + * Enable loose matching + */ + function inDirectory( required string baseDirectory ) { + baseDirectory = pathPatternMatcher.normalizeSlashes( baseDirectory ); + setBaseDir( baseDirectory & ( baseDirectory.endsWith( '/' ) ? '' : '/' ) ); + return this; + } + + /** + * Enable loose matching + */ + function loose( boolean loose=true ) { + setLoose( loose ); return this; } @@ -65,16 +89,14 @@ component accessors="true" { * Can be list of patterns or array of patterns. * Empty patterns will be ignored */ - function setPattern( required any pattern ) { - if( isSimpleValue( arguments.pattern ) ) { - arguments.pattern = listToArray( arguments.pattern ); + function setPattern( required any thisPattern ) { + variables.pattern = []; + if( isSimpleValue( arguments.thisPattern ) ) { + arguments.thisPattern = listToArray( arguments.thisPattern ); } - arguments.pattern = arguments.pattern.map( function( p ) { - return pathPatternMatcher.normalizeSlashes( arguments.p ); - }).filter( function( p ){ - return len( arguments.p ); - } ); - variables.pattern = arguments.pattern; + arguments.thisPattern.each( function( p ) { + addPattern( arguments.p ); + }); return this; } @@ -83,6 +105,7 @@ component accessors="true" { */ function addPattern( required string pattern ) { if( len( arguments.pattern ) ) { + arguments.pattern = pathPatternMatcher.normalizeSlashes( arguments.pattern ); variables.pattern.append( arguments.pattern ); } return this; @@ -107,16 +130,14 @@ component accessors="true" { * Can be list of excludePatterns or array of excludePatterns. * Empty excludePatterns will be ignored */ - function setExcludePattern( required any excludePattern ) { - if( isSimpleValue( arguments.excludePattern ) ) { - arguments.excludePattern = listToArray( arguments.excludePattern ); + function setExcludePattern( required any thisExcludePattern ) { + variables.excludePattern = []; + if( isSimpleValue( arguments.thisExcludePattern ) ) { + arguments.thisExcludePattern = listToArray( arguments.thisExcludePattern ); } - arguments.excludePattern = arguments.excludePattern.map( function( p ) { - return pathPatternMatcher.normalizeSlashes( arguments.p ); - }).filter( function( p ){ - return len( arguments.p ); - } ); - variables.excludePattern = arguments.excludePattern; + arguments.thisExcludePattern.each( function( p ) { + addExcludePattern( p ); + }); return this; } @@ -125,11 +146,27 @@ component accessors="true" { */ function addExcludePattern( required string excludePattern ) { if( len( arguments.excludePattern ) ) { - variables.excludePattern.append( arguments.excludePattern ); + if ( arguments.excludePattern.startsWith( '!' ) ) { + addNotExcludePattern( mid( arguments.excludePattern, 2, len( arguments.excludePattern ) - 1 ) ); + } else { + arguments.excludePattern = pathPatternMatcher.normalizeSlashes( arguments.excludePattern ); + variables.excludePattern.append( arguments.excludePattern ); + } } return this; } + /** + * Add not excludePattern to process + */ + function addNotExcludePattern( required string notExcludePattern ) { + if( len( arguments.notExcludePattern ) ) { + arguments.notExcludePattern = pathPatternMatcher.normalizeSlashes( arguments.notExcludePattern ); + variables.notExcludePattern.append( arguments.notExcludePattern ); + } + return this; + } + /** * Always returns a string which is a list of excludePatterns */ @@ -148,11 +185,55 @@ component accessors="true" { * Pass a closure to this function to have it * applied to each paths matched by the pattern. */ - function apply( udf ) { + function apply( required udf ) { matches().each( udf ); return this; } + /** + * Copy all matched paths to a new folder + */ + function copyTo( required string targetPath ) { + targetPath = pathPatternMatcher.normalizeSlashes( targetPath ); + if( !targetPath.endsWith( '/' ) ) { + targetPath &= '/'; + } + ensureMatches(); + var paths = getMatchQuery(); + if( !directoryExists( targetPath ) ) { + directoryCreate( targetPath, true, true ); + } + // Create all folders first + paths + .filter( (p)=>p.type=='dir' ) + .sort( (a,b)=>len(a.directory&a.name )-len(b.directory&b.name ) ) + .each( (p)=>{ + var newDir = p.directory.listAppend( p.name, '/', false ); + newDir = pathPatternMatcher.normalizeSlashes( newDir ); + newDir = newDir.replace( getBaseDir(), '' ) + directoryCreate( targetPath & newDir, true, true ) + } ); + // Copy files asynch + paths + .filter( (p)=>p.type=='file' ) + .each( (p)=>{ + var oldDir = pathPatternMatcher.normalizeSlashes( p.directory ); + if ( !oldDir.endsWith( '/' ) ) { + oldDir &= "/"; + } + var oldFile = oldDir & p.name; + var newFile = targetPath & oldFile.replace( getBaseDir(), '' ); + // Just in case + newDirectory = getDirectoryFromPath( newFile ); + if( !directoryExists( newDirectory ) ) { + directoryCreate( newDirectory, true, true ); + } + fileCopy( oldFile, newFile ) + }, true ); + + return this; + } + /** * Get array of matched file system paths */ @@ -163,7 +244,11 @@ component accessors="true" { } else { return getMatchQuery().reduce( function( arr, row ) { // Turn all the slashes the right way for this OS - return arr.append( row.directory & '/' & row.name & ( row.type == 'Dir' ? '/' : '' ) ); + if( row.directory == '/' ) { + return arr.append( row.directory & row.name & ( row.type == 'Dir' ? '/' : '' ) ); + } else { + return arr.append( row.directory & '/' & row.name & ( row.type == 'Dir' ? '/' : '' ) ); + } }, [] ); } } @@ -190,32 +275,25 @@ component accessors="true" { private function process() { var patterns = getPatternArray(); - if( !patterns.len() ) { - throw( 'Cannot glob empty pattern.' ); + if( !patterns.len() && !getBaseDir().len() ) { + throw( 'Cannot glob empty pattern with no base directory.' ); + } else if( !patterns.len() ) { + patterns = [ '**' ]; } + if( getLoose() && !len( getBaseDir() ) ) { + throw( 'You must use [inDirectory()] to set a base dir when using loose matching.' ); + } + for( var thisPattern in patterns ) { processPattern( thisPattern ); } var matchQuery = getMatchQuery(); + + combineMatchQueries(); - if( isNull( matchQuery ) ) { - setMatchQuery( queryNew( 'name,size,type,dateLastModified,attributes,mode,directory' ) ); - } else { - // UNION isn't removing dupes on Lucee so doing second select here for that purpose. - cfquery( dbtype="query" ,name="local.newMatchQuery" ) { - writeOutput( 'SELECT DISTINCT * FROM matchQuery ' ); - if( len( getSort() ) ) { - writeOutput( 'ORDER BY #getCleanSort()#' ); - } - } - - setMatchQuery( local.newMatchQuery ); - } - - - if( patterns.len() > 1 ) { + if( patterns.len() > 1 && !getLoose() ) { var dirs = queryColumnData( getMatchQuery(), 'directory' ); var lookups = {}; dirs.each( function( dir ) { @@ -241,109 +319,129 @@ component accessors="true" { } function appendMatchQuery( matchQuery ) { - // First one in just gets set - if( isNull( getMatchQuery() ) ) { - setMatchQuery( matchQuery ); - // merge remaining patterns - } else { - var previousMatch = getMatchQuery(); - - cfquery( dbtype="query" ,name="local.newMatchQuery" ) { - writeOutput( 'SELECT * FROM matchQuery UNION SELECT * FROM previousMatch ' ); - } - - setMatchQuery( local.newMatchQuery ); - } + matchQueryArray.append( matchQuery ); + return; } - private function processPattern( string pattern ) { - + private function processPattern( string pattern, baseDir, skipExcludes=false ) { local.thisPattern = pathPatternMatcher.normalizeSlashes( arguments.pattern ); - - // To optimize this as much as possible, we want to get a directory listing as deep as possible so we process a few files as we can. - // Find the deepest folder that doesn't have a wildcard in it. - var baseDir = ''; - var i = 0; - // Skip last token - while( ++i < thisPattern.listLen( '/' ) ) { - var token = thisPattern.listGetAt( i, '/' ); - if( token contains '*' || token contains '?' ) { - break; - } - baseDir = baseDir.listAppend( token, '/' ); - } - // Unix paths need the leading slash put back - if( thisPattern.startsWith( '/' ) ) { - baseDir = '/' & baseDir; - } - - // Windows drive letters need trailing slash. - if( baseDir.listLen( '/' ) == 1 && baseDir contains ':' ) { - baseDir = baseDir & '/'; - } - - if( !baseDir.len() ) { - baseDir = '/'; - } - - var everythingAfterBaseDir = thisPattern.replace( baseDir, '' ); - - // If we have a partial directory next such as modules* optimize here - if( everythingAfterBaseDir.listLen( '/' ) > 1 && everythingAfterBaseDir.listFirst( '/' ).reFind( '[^\*^\?]' ) && !everythingAfterBaseDir.listFirst( '/' ).startsWith( '**' ) ) { - - thisPattern = baseDir & '/' & everythingAfterBaseDir.listFirst( '/' ) & '/'; - // Manually loop over the dirs at this level that match to narrow what we're looking at - directoryList ( - listInfo='query', - recurse=false, - path=baseDir, - type='dir', - sort=getSort(), - filter=( path )=>{ - var thisPath = path & '/'; - if( pathPatternMatcher.matchPattern( thisPattern, thisPath, true ) ) { - return true; + var exactPattern = thisPattern.startsWith('/'); + var fileFilter = ''; + var fullPatternPath = ( getLoose() ? getBaseDir().listAppend( thisPattern, '/', false ) : thisPattern ); + + // Optimization for exact file path + if( ( !getLoose() || exactPattern ) + && ( thisPattern does not contain '*' && thisPattern does not contain '?' && fileExists( fullPatternPath ) ) + ) { + arguments.baseDir = getDirectoryFromPath( fullPatternPath ); + fileFilter = '*' & listLast( fullPatternPath, '/' ) + } else { + if( isNull( arguments.baseDir ) ) { + // To optimize this as much as possible, we want to get a directory listing as deep as possible so we process a few files as we can. + // Find the deepest folder that doesn't have a wildcard in it. + if( getLoose() ) { + if( exactPattern ) { + arguments.baseDir = calculateBaseDir( getBaseDir() & thisPattern.right( -1 ) ) + } else { + arguments.baseDir = calculateBaseDir( getBaseDir() ) } - return false; + } else { + arguments.baseDir = calculateBaseDir( thisPattern ); } - ).each( ( folder )=>processPattern( baseDir & '/' & folder.name & '/' & everythingAfterBaseDir.listRest( '/' ) ) ) - - return; - } - - var recurse = false; - if( thisPattern contains '**' ) { - recurse = true; - } - - var optimizeFilter = ''; - if( reFind( '\.[a-zA-Z0-9\?\*]{2,4}$', thisPattern ) ) { - optimizeFilter = '*.' & thisPattern.listLast( '.' ).replace( '?', '*', 'all' ); + } } + // Strip off the "not found" part + var remainingPattern = findUnmatchedPattern( thisPattern, baseDir ) var dl = directoryList ( listInfo='query', - recurse=local.recurse, + recurse=false, path=baseDir, - sort=getSort(), - filter=optimizeFilter + filter=fileFilter ).filter( ( path )=>{ - if( path.directory.endsWith( '/' ) || path.directory.endsWith( '\' ) ) { - var thisPath = path.directory & path.name & ( path.type == 'dir' ? '/' : '' ); + // All of this nonsense is to build the full normalized path of this item WITH a trailing slash if it's a directory + if( arguments.path.directory.endsWith( '/' ) || arguments.path.directory.endsWith( '\' ) ) { + var thisPath = arguments.path.directory & arguments.path.name & ( arguments.path.type == 'dir' ? '/' : '' ); } else { - var thisPath = path.directory & '/' & path.name & ( path.type == 'dir' ? '/' : '' ); + var thisPath = arguments.path.directory & '/' & arguments.path.name & ( arguments.path.type == 'dir' ? '/' : '' ); + } + local.thisPath = pathPatternMatcher.normalizeSlashes( thisPath ); + + var pathToMatch = local.thisPath; + if( getLoose() ) { + pathToMatch = local.thisPath.replaceNoCase( getBaseDir(), '' ); + } + + // If we've hit an exclude pattern, we can bail now-- skipping all recursion and processing of files at this level. + var thisExcludePattern = this.getExcludePatternArray(); + if( !skipExcludes && thisExcludePattern.len() && pathPatternMatcher.matchPatterns( thisExcludePattern, pathToMatch, !getLoose() ) ) { + // UNLESS we have a negated ignore! + var possiblePatterns = pathMayNotBeExcluded( pathToMatch, path.type, baseDir ); + if( possiblePatterns.len() ) { + // If we're looking at a file, just check it. No need to recurse. + if( path.type == 'file' ) { + if( pathPatternMatcher.matchPatterns( possiblePatterns, pathToMatch, !getLoose() ) ) { + return true; + } + } else { + // For each of our possible patterns, let's recurse and check each of them. + // TODO: optimize this by recursing once and checking all patterns at a time, + // but that will require a major refactor of processPatterns() to accept more than one pattern. + for( var possiblePattern in possiblePatterns) { + if( getLoose() ) { + if( possiblePattern.startsWith( '/' ) ) { + // Exact patterns in loose mode like /foo/bar/baz.txt we want to zoom staright down to the + // deepest folder possible to reduce unnessary recursion. + possiblePattern = getBaseDir().listAppend( possiblePattern, '/', false ); + var thisBaseDir = calculateBaseDir( possiblePattern ); + possiblePattern = possiblePattern.replace( thisBaseDir, '' ); + processPattern( possiblePattern, thisBaseDir, true ); + } else { + // non-exact patters which can be in any sub directory such as foo.txt just recurse down from the current folder we're looking at + processPattern( possiblePattern, thisPath, true ) + } + } else { + // For non-loose mode just grab the deepest folder we can and start there. + var thisBaseDir = calculateBaseDir( possiblePattern ); + processPattern( possiblePattern, thisBaseDir, true ) + } + } + } + } + return false; } - if( pathPatternMatcher.matchPattern( thisPattern, thisPath, true ) ) { - if( getExcludePatternArray().len() && pathPatternMatcher.matchPatterns( getExcludePatternArray(), thisPath, true ) ) { - return false; + + // If we're inside a **, then we just blindly recurse forever + if( arguments.path.type == 'dir' && remainingPattern.startsWith( '**' ) ) { + processPattern( thisPattern, local.thisPath, skipExcludes ) + // If we're in loose mode, see if the next folder is a positive match + } else if( arguments.path.type == 'dir' && getLoose() ) { + if( exactPattern ) { + if( pathPatternMatcher.matchPattern( '/' & remainingPattern.listFirst( '/' ), pathToMatch, !getLoose() ) ) { + processPattern( '/' & remainingPattern.listRest( '/' ), local.thisPath, skipExcludes ); + } + } else { + processPattern( thisPattern, local.thisPath, skipExcludes ); } + // For all other remaining patterns, only recurse if we've found a folder that matches the next part of the pattern + } if( arguments.path.type == 'dir' && remainingPattern.listLen( '/' ) > 1 ) { + if( pathPatternMatcher.matchPattern( baseDir & remainingPattern.listFirst( '/' ) & '/**', pathToMatch, !getLoose() ) ) { + processPattern( local.thisPath & remainingPattern.listRest( '/' ), local.thisPath, skipExcludes ); + } + } + + // This check applies to files/folders that are immediate children of the current base dir. + // We've already recursed into all worthy subfolders above + if( pathPatternMatcher.matchPattern( thisPattern, local.pathToMatch, !getLoose() ) ) { return true; } return false; } ); appendMatchQuery( dl ); - setBaseDir( baseDir & ( baseDir.endsWith( '/' ) ? '' : '/' ) ); + if( !getLoose() ) { + setBaseDir( baseDir & ( baseDir.endsWith( '/' ) ? '' : '/' ) ); + } } @@ -372,4 +470,141 @@ component accessors="true" { return getSort(); } + private function calculateBaseDir( required string pattern ) { + var baseDir = ''; + var i = 0; + // Skip last token + while( ++i <= pattern.listLen( '/' ) ) { + var token = pattern.listGetAt( i, '/' ); + if( token contains '*' || token contains '?' ) { + break; + } + // If we have a partial name like /foo/bar we may still match /foo/barstool. + // Only if it's /foo/bar/ do we know we can trust that in the base path + if( i == pattern.listLen( '/' ) && !pattern.endsWith( '/' ) ) { + break; + } + baseDir = baseDir.listAppend( token, '/', false ); + } + + // Unix paths need the leading slash put back + if( pattern.startsWith( '/' ) ) { + baseDir = '/' & baseDir; + } + + // Windows drive letters need trailing slash. + if( baseDir.listLen( '/' ) == 1 && baseDir contains ':' ) { + baseDir = baseDir & '/'; + } + + if( !baseDir.endsWith( '/' ) ) { + baseDir &= '/'; + } + return baseDir; + } + + private function combineMatchQueries() { + if( !matchQueryArray.len() ) { + setMatchQuery( queryNew( 'name,size,type,dateLastModified,attributes,mode,directory' ) ); + } else { + var SQL = '' + var i = 0; + for( var thisQ in matchQueryArray ) { + i++; + local[ 'thisMatchQuery#i#' ] = thisQ; + SQL &= ' SELECT * FROM thisMatchQuery#i# '; + if( i < matchQueryArray.len() ) { + SQL &= ' UNION ALL' + } + } + + var newMatchQuery = queryExecute( + SQL, + [], + { dbtype="query" } + ); + + var newMatchQuery = queryExecute( + 'SELECT * FROM newMatchQuery + GROUP BY directory, name ' + & ( len( getSort() ) ? ' ORDER BY #getCleanSort()#' : '' ), + [], + { dbtype="query" } + ); + + setMatchQuery( local.newMatchQuery ); + } + } + + function pathMayNotBeExcluded( pathToMatch, type, currentBaseDir ) { + if( !getNotExcludePattern().len() ) { + return []; + } + var possiblePatterns=[]; + + for( notExclude in getNotExcludePattern() ) { + var exactPattern = notExclude.startsWith('/'); + var remainingPattern = findUnmatchedPattern( notExclude, currentBaseDir ) + + // Well, crumbs-- all bets are off! + // /temp/** + // !foo.txt + if( type == 'dir' && getLoose() && !exactPattern ) { + possiblePatterns.append( notExclude ); + continue; + } + + // If it's a file, just check it + if( type == 'file' ) { + if( pathPatternMatcher.matchPattern( notExclude, pathToMatch, false ) ) { + possiblePatterns.append( notExclude ); + } + // Even if we didn't find a match, all the checks below only apply to directories + continue; + } + + // These all apply to directories. The question is whether or not we MAY need to recurse into the dir + // based on whether there is a notexclude pattern that could possibly be inside the dir + + // If we're inside a **, then we just blindly recurse forever + if( remainingPattern.startsWith( '**' ) ) { + possiblePatterns.append( notExclude ); + continue; + // If we're in loose mode with an exact pattern, see if the next folder is a positive match + } else if( getLoose() && exactPattern && pathPatternMatcher.matchPattern( '/' & remainingPattern.listFirst( '/' ), pathToMatch, !getLoose() ) ) { + possiblePatterns.append( notExclude ); + continue; + // For all other remaining patterns, only recurse if we've found a folder that matches the next part of the pattern + } if( remainingPattern.listLen( '/' ) > 1 && pathPatternMatcher.matchPattern( currentBaseDir & remainingPattern.listFirst( '/' ) & '/**', pathToMatch, !getLoose() ) ) { + possiblePatterns.append( notExclude ); + continue; + } + + } + return possiblePatterns; + } + + function findUnmatchedPattern( thisPattern, currentBaseDir ) { + var exactPattern = thisPattern.startsWith('/'); + if( getLoose() ) { + if( exactPattern ) { + if( currentBaseDir == getBaseDir() ) { + var remainingPattern = thisPattern; + } else { + var remainingPattern = thisPattern.replaceNoCase( currentBaseDir.replaceNoCase( getBaseDir(), '' ), '' ); + } + } else { + // A loose pattern without a leading slash can be any levels deep + remainingPattern = '**'; + } + } else { + var remainingPattern = thisPattern.replaceNoCase( currentBaseDir, '' ); + // if our base path isn't contained inside the pattern, we have entered a ** portion and we can't short circuit anything now + if( remainingPattern == thisPattern ) { + remainingPattern = '**'; + } + } + return remainingPattern; + } + } diff --git a/src/cfml/system/modules/globber/models/PathPatternMatcher.cfc b/src/cfml/system/modules/globber/models/PathPatternMatcher.cfc index 17e168d99..c1c9ba8a6 100644 --- a/src/cfml/system/modules/globber/models/PathPatternMatcher.cfc +++ b/src/cfml/system/modules/globber/models/PathPatternMatcher.cfc @@ -74,15 +74,23 @@ component accessors="true" singleton { regex = replace( regex, '/**/', '__zeroOrMoreDirs_', 'all' ); // Double ** matches anything regex = replace( regex, '**', '__anything_', 'all' ); - // Single * matches anything BUT slash - regex = replace( regex, '*', '__anythingButSlash__', 'all' ); + // Match a single dir + regex = replace( regex, '/*/', '/__anythingButSlashOneOrMore__/', 'all' ); + + // Single * matches anything BUT slash one or more chars + if( regex.endsWith( '/*' ) ) { + regex = regex.left( -1 ) & '__anythingButSlashOneOrMore__'; + } + // Single * matches anything BUT slash zero or more chars + regex = replace( regex, '*', '__anythingButSlashZeroOrMore__', 'all' ); // ? matches any single non-slash character regex = replace( regex, '?', '__singleNonSlash__', 'all' ); // Switch placeholders for actual regex regex = replace( regex, '__zeroOrMoreDirs_', '(/.*/|/)', 'all' ); regex = replace( regex, '__anything_', '.*', 'all' ); - regex = replace( regex, '__anythingButSlash__', '[^/]*', 'all' ); + regex = replace( regex, '__anythingButSlashOneOrMore__', '[^/]+', 'all' ); + regex = replace( regex, '__anythingButSlashZeroOrMore__', '[^/]*', 'all' ); regex = replace( regex, '__singleNonSlash__', '[^/]', 'all' ); // If the pattern doesn't come with an explicit ending slash, add an optional one diff --git a/src/cfml/system/modules/jmespath/.gitignore b/src/cfml/system/modules/jmespath/.gitignore deleted file mode 100644 index 279c45579..000000000 --- a/src/cfml/system/modules/jmespath/.gitignore +++ /dev/null @@ -1 +0,0 @@ -/testbox/ \ No newline at end of file diff --git a/src/cfml/system/modules/propertyFile/box.json b/src/cfml/system/modules/propertyFile/box.json index 7c2184fa7..2afa2d608 100644 --- a/src/cfml/system/modules/propertyFile/box.json +++ b/src/cfml/system/modules/propertyFile/box.json @@ -1,8 +1,8 @@ { "name":"PropertyFile Util", - "version":"1.2.0", + "version":"1.3.2", "author":"Brad Wood", - "location":"bdw429s/PropertyFile#v1.2.0", + "location":"forgeboxStorage", "homepage":"https://github.com/bdw429s/PropertyFile/", "documentation":"https://github.com/bdw429s/PropertyFile/blob/master/readme.md", "repository":{ @@ -16,7 +16,7 @@ "keywords":"java,property,files", "projectURL":"https://github.com/bdw429s/PropertyFile/", "scripts":{ - "postVersion":"package set location='bdw429s/PropertyFile#v`package version`'", + "postVersion":"publish", "postPublish":"!git push --follow-tags" }, "ignore":[ diff --git a/src/cfml/system/modules/propertyFile/models/PropertyFile.cfc b/src/cfml/system/modules/propertyFile/models/PropertyFile.cfc index 30c8fb972..3ef11c404 100644 --- a/src/cfml/system/modules/propertyFile/models/PropertyFile.cfc +++ b/src/cfml/system/modules/propertyFile/models/PropertyFile.cfc @@ -2,13 +2,13 @@ * I am a new Model Object */ component accessors="true"{ - + // Properties property name='javaPropertyFile'; // A fully qualified path to a property file property name='path'; property name='syncedNames'; - + /** * Constructor @@ -18,7 +18,7 @@ component accessors="true"{ setJavaPropertyFile( createObject( 'java', 'java.util.Properties' ).init() ); return this; } - + /** * @load A fully qualified path to a property file */ @@ -29,17 +29,17 @@ component accessors="true"{ var propertyFile = getJavaPropertyFile(); propertyFile.load( BOMfis ); BOMfis.close(); - - + + var props = propertyFile.propertyNames(); var syncedNames = getSyncedNames(); while( props.hasMoreElements() ) { var prop = props.nextElement(); this[ prop ] = get( prop ); - syncedNames.append( prop ); + syncedNames.append( prop ); } setSyncedNames( syncedNames ); - + return this; } @@ -48,16 +48,16 @@ component accessors="true"{ */ function store( string path=variables.path ){ syncProperties(); - + if( !fileExists( arguments.path ) ) { directoryCreate( getDirectoryFromPath( arguments.path ), true, true ); fileWrite( arguments.path, '' ); } - + var fos = CreateObject( 'java', 'java.io.FileOutputStream' ).init( arguments.path ); getJavaPropertyFile().store( fos, '' ); fos.close(); - + return this; } @@ -66,7 +66,7 @@ component accessors="true"{ */ function get( required string name, string defaultValue ){ if( structKeyExists( arguments, 'defaultValue' ) ) { - return getJavaPropertyFile().getProperty( name, defaultValue ); + return getJavaPropertyFile().getProperty( name, defaultValue ); } else if( exists( name ) ) { return getJavaPropertyFile().getProperty( name ); } else { @@ -79,14 +79,14 @@ component accessors="true"{ */ function set( required string name, required string value ){ getJavaPropertyFile().setProperty( name, value ); - + var syncedNames = getSyncedNames(); this[ name ] = value; if( !arrayContains( syncedNames, name ) ){ syncedNames.append( name ); } setSyncedNames( syncedNames ); - + return this; } @@ -96,7 +96,7 @@ component accessors="true"{ function remove( required string name ){ if( exists( name ) ) { getJavaPropertyFile().remove( name ); - + var syncedNames = getSyncedNames(); if( arrayFind( syncedNames, name ) ){ syncedNames.deleteAt( arrayFind( syncedNames, name ) ); @@ -124,14 +124,23 @@ component accessors="true"{ return result; } + /** + * mergeStruct + */ + function mergeStruct( struct incomingStruct ){ + structAppend( this, incomingStruct ); + syncProperties(); + return this; + } + /** * Keeps public properties in sync with Java object */ private function syncProperties() { var syncedNames = getSyncedNames(); - var ignore = listToArray( 'init,load,store,get,set,exists,remove,exists,getAsStruct,$mixed' ); + var ignore = listToArray( 'init,load,store,get,set,exists,remove,exists,getAsStruct,$mixed,mergeStruct' ); var propertyFile = getJavaPropertyFile(); - + // This CFC's public properties for( var prop in this ) { // Set any new/updated properties in, excluding actual methods and non-simple values @@ -139,7 +148,7 @@ component accessors="true"{ set( prop, this[ prop ] ); } } - + // All the properties in the Java object var props = propertyFile.propertyNames(); while( props.hasMoreElements() ) { @@ -149,7 +158,7 @@ component accessors="true"{ remove( prop ); } } - + } } \ No newline at end of file diff --git a/src/cfml/system/modules/semver/models/SemanticVersion.cfc b/src/cfml/system/modules/semver/models/SemanticVersion.cfc index 2480903e1..f3fe7fae6 100644 --- a/src/cfml/system/modules/semver/models/SemanticVersion.cfc +++ b/src/cfml/system/modules/semver/models/SemanticVersion.cfc @@ -116,6 +116,7 @@ component singleton{ */ boolean function satisfies( required string version, required string range ){ + arguments.version = clean( arguments.version ); if( range == 'be' ) { @@ -136,6 +137,7 @@ component singleton{ for( var comparatorSet in semverRange ) { // If the version we're inspecting is a pre-release, don't consider it unless at least one comparator in this // set specifically mentions a pre release matching this major.minor.revision. + if( isPreRelease( arguments.version ) && !interestedInPreReleasesOfThisVersion( comparatorSet, arguments.version ) ) { continue; } @@ -178,9 +180,12 @@ component singleton{ for( var comparator in arguments.comparatorSet ) { // And see if there is a pre release version that matches major.minor.revision if( isPreRelease( comparator.version ) + // major must match && comparator.sVersion.major == sVersion.major - && comparator.sVersion.minor == sVersion.minor - && comparator.sVersion.revision == sVersion.revision) { + // minor needs to match OR have been x or blank in the range + && ( comparator.xVersion.minor == 'x' || comparator.sVersion.minor == sVersion.minor ) + // revision needs to match OR have been x or blank in the range + && ( comparator.xVersion.revision == 'x' || comparator.sVersion.revision == sVersion.revision ) ) { return true; } } @@ -218,7 +223,7 @@ component singleton{ lowerBound = replaceNoCase( lowerBound, '*', 'x', 'all' ); upperBound = replaceNoCase( upperBound, '*', 'x', 'all' ); - sVersion = parseVersion( lowerBound, 'x' ); + var sVersion = parseVersion( lowerBound, 'x' ); comparatorSet.append( expandXRanges( { @@ -301,6 +306,7 @@ component singleton{ private function expandXRanges( required struct sComparator ) { var comparatorSet = []; + sComparator.xVersion = duplicate( sComparator.sVersion ); switch( sComparator.operator ) { case "<": diff --git a/src/cfml/system/modules_app/coldbox-commands/commands/coldbox/create/app-wizard.cfc b/src/cfml/system/modules_app/coldbox-commands/commands/coldbox/create/app-wizard.cfc index 1ebc5760f..eb26b6aa4 100644 --- a/src/cfml/system/modules_app/coldbox-commands/commands/coldbox/create/app-wizard.cfc +++ b/src/cfml/system/modules_app/coldbox-commands/commands/coldbox/create/app-wizard.cfc @@ -6,19 +6,78 @@ component extends="app" aliases="" { /** * @name The name of the app you want to create * @skeleton The application skeleton you want to use (AdvancedScript, rest, rest-hmvc, Simple, SuperSimple) - * @skeleton.optionsUDF skeletonComplete - * @init Init this as a CommandBox Package + * @init Init this as a package **/ function run( required name, - required skeleton, - required boolean init + skeleton ){ + + arguments.directory = getCWD(); + if( !confirm( 'Are you currently inside the "/#name#" folder (if "No" we will create it)? [y/n]' ) ) { + arguments.directory = getCWD() & name & '/'; + if ( !directoryExists( arguments.directory ) ) { + directoryCreate( arguments.directory ); + } + shell.cd(arguments.directory); + } + + print.boldgreenline( '------------------------------------------------------------------------------------------' ); + print.boldgreenline("Files will be installed in the " & arguments.directory & " directory" ); + print.boldgreenline( '------------------------------------------------------------------------------------------' ); + + if( confirm( 'Are you creating an API? [y/n]' ) ) { + print.boldgreenline( '------------------------------------------------------------------------------------------' ); + print.boldgreenline( 'We have 2 different API template options' ); + print.boldgreenline( 'Both include the modules: cbsecurity, cbvalidation, mementifier, relax, & route-visualizer' ); + print.boldgreenline( '------------------------------------------------------------------------------------------'); + + arguments.skeleton = multiselect( 'Which template would you like to use?' ) + .options( [ + {accessKey=1, display='Modular (API/REST) Template - provide an "api" module with a "v1" sub-module within it', value='cbtemplate-rest-hmvc', selected=true }, + {accessKey=2, display='Simple (API/REST) Template - proivdes api endpoints via the handlers/ folder', value='cbtemplate-rest' }, + ] ) + .required() + .ask(); + + } else { + print.boldgreenline( '------------------------------------------------------------------------------------------',true); + print.greenline( 'We have a few different Non-API template options' ); + print.greenline( 'No default modules are installed for these templates' ); + print.boldgreenline( '------------------------------------------------------------------------------------------'); + + arguments.skeleton = multiselect( 'Which template would you like to use?') + .options( [ + {accessKey=1, value="cbtemplate-simple", display="Simple Script - Script based Coldbox App WITHOUT cfconfig & .env settings"}, + {accessKey=2, value="cbtemplate-advanced-script", display="Advanced Script - Script based Coldbox App which uses cfconfig & .env settings", selected=true}, + {accessKey=3, value="cbtemplate-elixir", display="Elixir Template - Advanced Script + ColdBox Elixir: Enable Webpack tasks for your ColdBox applications"}, + {accessKey=4, value="cbtemplate-elixir-vuejs", display="Elixir + Vuejs Template - Elixir Template + pre-installed & configured VueJS"}, + ] ) + .required() + .ask(); + + if(arguments.skeleton != 'cbtemplate-simple'){ + print.boldgreenline( ''); + print.boldgreenline( 'This Coldbox Template uses cfconfig & .env "dotenv" ' ); + print.boldgreenline( '----------------------------------------------------------------------------------------'); + print.boldgreenline( 'CFConfig is a module that creates a local settings file' ); + print.greenline( 'in your project directory of all of the ColdFusion Admin Settings' ); + print.greenline( 'Check out more details in the docs: https://cfconfig.ortusbooks.com/' ); + print.boldgreenline( '----------------------------------------------------------------------------------------'); + print.boldgreenline( '.env is a module that creates a local variables that can be' ); + print.greenline( 'used in many places such as .cfconfig.json, box.json, Coldbox.cfc, etc.' ); + print.greenline( 'You will see these used in the template in some of the files above' ); + print.greenline( 'ex. "${DB_DATABASE}" or getSystemSetting( "APPNAME", "Your app name here" )' ); + print.greenline( 'More info at https://github.com/commandbox-modules/commandbox-dotenv' ); + print.boldgreenline( '----------------------------------------------------------------------------------------'); + } + } + print.line('Creating your site...').toConsole(); + var skeletons = skeletonComplete(); // turn off wizard arguments.wizard = false; arguments.initWizard = true; - arguments.directory = getCWD(); if ( !arguments.skeleton.len() ) { // Remove if empty so it can default correctly diff --git a/src/cfml/system/modules_app/coldbox-commands/commands/coldbox/create/app.cfc b/src/cfml/system/modules_app/coldbox-commands/commands/coldbox/create/app.cfc index 1b4cb28a8..da9ef4640 100644 --- a/src/cfml/system/modules_app/coldbox-commands/commands/coldbox/create/app.cfc +++ b/src/cfml/system/modules_app/coldbox-commands/commands/coldbox/create/app.cfc @@ -48,6 +48,8 @@ component { "Simple" : "cbtemplate-simple", "SuperSimple" : "cbtemplate-supersimple" }; + + variables.defaultAppName = "My ColdBox App"; return this; } @@ -62,80 +64,106 @@ component { * @initWizard Run the init creation package wizard **/ function run( - name = "My ColdBox App", + name = defaultAppName, skeleton = "AdvancedScript", directory = getCWD(), boolean init = true, boolean wizard = false, - boolean initWizard = false + boolean initWizard = false, + boolean verbose = false ){ // Check for wizard argument if ( arguments.wizard ) { - runCommand( "coldbox create app-wizard" ); + command( "coldbox create app-wizard" ) + .params( verbose=arguments.verbose ) + .run(); return; } - - // This will make the directory canonical and absolute - arguments.directory = resolvePath( arguments.directory ); - - // Validate directory, if it doesn't exist, create it. - if ( !directoryExists( arguments.directory ) ) { - directoryCreate( arguments.directory ); - } - - // If the skeleton is one of our "shortcut" names - if ( variables.templateMap.keyExists( arguments.skeleton ) ) { - // Replace it with the actual ForgeBox slug name. - arguments.skeleton = variables.templateMap[ arguments.skeleton ]; - } - - // Install the skeleton - packageService.installPackage( - ID = arguments.skeleton, - directory = arguments.directory, - save = false, - saveDev = false, - production = false, - currentWorkingDirectory = arguments.directory - ); - - // Check for the @appname@ in .project files - if ( fileExists( "#arguments.directory#/.project" ) ) { - var sProject = fileRead( "#arguments.directory#/.project" ); - sProject = replaceNoCase( - sProject, - "@appName@", - arguments.name, - "all" + + job.start( "Creating App [#arguments.name#]" ); + + if( verbose ) { + job.setDumpLog( verbose ); + } + + // This will make the directory canonical and absolute + arguments.directory = resolvePath( arguments.directory ); + + // Validate directory, if it doesn't exist, create it. + if ( !directoryExists( arguments.directory ) ) { + directoryCreate( arguments.directory ); + } + + // If the skeleton is one of our "shortcut" names + if ( variables.templateMap.keyExists( arguments.skeleton ) ) { + // Replace it with the actual ForgeBox slug name. + arguments.skeleton = variables.templateMap[ arguments.skeleton ]; + } + + // Install the skeleton + packageService.installPackage( + ID = arguments.skeleton, + directory = arguments.directory, + save = false, + saveDev = false, + production = false, + currentWorkingDirectory = arguments.directory ); - file action="write" file="#arguments.directory#/.project" mode="755" output="#sProject#"; - } + + // Check for the @appname@ in .project files + if ( fileExists( "#arguments.directory#/.project" ) ) { + var sProject = fileRead( "#arguments.directory#/.project" ); + sProject = replaceNoCase( + sProject, + "@appName@", + arguments.name, + "all" + ); + file action="write" file="#arguments.directory#/.project" mode="755" output="#sProject#"; + } + + job.start( "Preparing box.json" ); + + // Init, if not a package as a Box Package + if ( arguments.init && !packageService.isPackage( arguments.directory ) ) { + var originalPath = getCWD(); + // init must be run from CWD + shell.cd( arguments.directory ); + command( "init" ) + .params( + name = arguments.name, + slug = replace( arguments.name, " ", "", "all" ), + wizard = arguments.initWizard + ) + .run(); + shell.cd( originalPath ); + } - // Init, if not a package as a Box Package - if ( arguments.init && !packageService.isPackage( arguments.directory ) ) { - var originalPath = getCWD(); - // init must be run from CWD - shell.cd( arguments.directory ); - command( "init" ) + // Prepare defaults on box.json so we remove template based ones + command( "package set" ) .params( - name = arguments.name, - slug = replace( arguments.name, " ", "", "all" ), - wizard = arguments.initWizard + name = arguments.name, + slug = variables.formatterUtil.slugify( arguments.name ), + version = "1.0.0", + location = "", + scripts = "{}" ) .run(); - shell.cd( originalPath ); - } + + job.complete(); - // Prepare defaults on box.json so we remove template based ones - command( "package set" ) - .params( - name = arguments.name, - slug = variables.formatterUtil.slugify( arguments.name ), - version = "1.0.0", - location = "", - scripts = "{}" - ) - .run(); + //set the server name if the user provided one + if( arguments.name != defaultAppName ) { + + job.start( "Preparing server.json" ); + command( "server set" ) + .params( name=arguments.name ) + .run(); + job.complete(); + } + + job.complete(); + } /** diff --git a/src/cfml/system/modules_app/package-commands/ModuleConfig.cfc b/src/cfml/system/modules_app/package-commands/ModuleConfig.cfc index b56ae79d8..e2ca007be 100644 --- a/src/cfml/system/modules_app/package-commands/ModuleConfig.cfc +++ b/src/cfml/system/modules_app/package-commands/ModuleConfig.cfc @@ -8,7 +8,7 @@ component { function configure() { interceptors = [ - { class="#moduleMapping#.interceptors.packageScripts" }, + { class="#moduleMapping#.interceptors.PackageScripts" }, { class="#moduleMapping#.interceptors.PackageSystemSettingExpansions" } ]; } diff --git a/src/cfml/system/modules_app/package-commands/commands/package/init.cfc b/src/cfml/system/modules_app/package-commands/commands/package/init.cfc index 7ed407020..d9b4f8811 100644 --- a/src/cfml/system/modules_app/package-commands/commands/package/init.cfc +++ b/src/cfml/system/modules_app/package-commands/commands/package/init.cfc @@ -56,13 +56,12 @@ component aliases="init" { return; } + var endpointName = arguments.endpointName ?: configService.getSetting( 'endpoints.defaultForgeBoxEndpoint', 'forgebox' ); + // Clean this up so it doesn't get written as a property structDelete( arguments, "wizard" ); - var endpointName = arguments.endpointName; structDelete( arguments, "endpointName" ); - - endpointName = endpointName ?: configService.getSetting( 'endpoints.defaultForgeBoxEndpoint', 'forgebox' ); - + try { var oEndpoint = endpointService.getEndpoint( endpointName ); } catch( EndpointNotFound var e ) { diff --git a/src/cfml/system/modules_app/package-commands/commands/package/link.cfc b/src/cfml/system/modules_app/package-commands/commands/package/link.cfc index 096cc122e..2e2cc3c61 100644 --- a/src/cfml/system/modules_app/package-commands/commands/package/link.cfc +++ b/src/cfml/system/modules_app/package-commands/commands/package/link.cfc @@ -33,6 +33,7 @@ **/ component aliases='link' { property name="packageService" inject="PackageService"; + property name="moduleService" inject="moduleService"; /** * @moduleDirectory Path to an app's modules directory @@ -100,12 +101,30 @@ component aliases='link' { } if( commandBoxCoreLinked ) { - print.greenLine( 'Package [#boxJSON.slug#] linked to CommandBox core.' ); - command( 'reload' ) - .params( clearScreen=false ) - .run(); + print.greenLine( 'Package [#boxJSON.slug#] linked to CommandBox core. Activating!' ); + loadModule( linkTarget ); } else { print.greenLine( 'Package [#boxJSON.slug#] linked to [#moduleDirectory#]' ); } } + + + function loadModule( required string moduleDirectory ) { + moduleDirectory = resolvePath( moduleDirectory ); + + // Generate a CF mapping that points to the module's folder + var relativeModulePath = fileSystemUtil.makePathRelative( moduleDirectory ); + + // A dot delimited path that points to the folder containing the module + var invocationPath = relativeModulePath + .listChangeDelims( '.', '/\' ) + .listDeleteAt( relativeModulePath.listLen( '/\' ), '.' ); + + // The name of the module + var moduleName = relativeModulePath.listLast( '/\' ); + + // Load it up!! + moduleService.registerAndActivateModule( moduleName, invocationPath ); + } + } diff --git a/src/cfml/system/modules_app/package-commands/commands/package/outdated.cfc b/src/cfml/system/modules_app/package-commands/commands/package/outdated.cfc index 735e4ed74..baeac8e20 100644 --- a/src/cfml/system/modules_app/package-commands/commands/package/outdated.cfc +++ b/src/cfml/system/modules_app/package-commands/commands/package/outdated.cfc @@ -34,7 +34,9 @@ component aliases="outdated" { function run( boolean verbose=false, boolean JSON=false, - boolean system=false ) { + boolean system=false, + boolean hideUpToDate=false + ) { if( arguments.JSON ) { arguments.verbose = false; @@ -53,16 +55,20 @@ component aliases="outdated" { // echo output if( !arguments.JSON ) { - print.yellowLine( "Resolving Dependencies, please wait..." ).toConsole(); + print.yellowLine( "Checking for outdated #( system ? 'system ' : '' )#dependencies, please wait..." ).toConsole(); } // build dependency tree var aAllDependencies = packageService.getOutdatedDependencies( directory=directory, print=print, verbose=arguments.verbose ); var aOutdatedDependencies = aAllDependencies.filter( (d)=>d.isOutdated ); + if( hideUpToDate ) { + aAllDependencies = aAllDependencies.filter( (d)=>d.isOutdated || ( !d.isLatest && d.depth == 1 ) ); + } + // JSON output if( arguments.JSON ) { - print.line( aAllDependencies.filter( (d)=>d.isOutdated ) ); + print.line( aAllDependencies ); return; } @@ -89,13 +95,13 @@ component aliases="outdated" { print.line() .green( 'Found ' ) .boldGreen( '(#aOutdatedDependencies.len()#)' ) - .green( ' Outdated Dependenc#( aOutdatedDependencies.len() == 1 ? 'y' : 'ies' )# ' ) + .green( ' Outdated #( system ? ' system' : '' )#Dependenc#( aOutdatedDependencies.len() == 1 ? 'y' : 'ies' )# ' ) .line(); printDependencies( data=aOutdatedDependencies, verbose=arguments.verbose ); print .line() - .cyanLine( "Run the 'update' command to update all the outdated dependencies to their latest version." ) - .cyanLine( "Or use 'update {slug}' to update a specific dependency" ); + .cyanLine( "Run the 'update#( system ? ' --system' : '' )#' command to update all the outdated dependencies to their latest version." ) + .cyanLine( "Or use 'update {slug}#( system ? ' --system' : '' )#' to update a specific dependency" ); } else { print.blueLine( 'There are no outdated dependencies!' ); } diff --git a/src/cfml/system/modules_app/package-commands/commands/package/unlink.cfc b/src/cfml/system/modules_app/package-commands/commands/package/unlink.cfc index d45596b42..2f21f5cde 100644 --- a/src/cfml/system/modules_app/package-commands/commands/package/unlink.cfc +++ b/src/cfml/system/modules_app/package-commands/commands/package/unlink.cfc @@ -21,6 +21,7 @@ **/ component aliases='unlink' { property name="packageService" inject="PackageService"; + property name="moduleService" inject="moduleService"; /** * @moduleDirectory Path to an app's modules directory @@ -51,14 +52,13 @@ component aliases='unlink' { var linkTarget = moduleDirectory & '/' & boxJSON.slug; if( directoryExists( linkTarget ) ) { - directoryDelete( linkTarget ); if( commandBoxCoreLinked ) { - print.greenLine( 'Package [#boxJSON.slug#] unlinked from CommandBox core.' ); - command( 'reload' ) - .params( clearScreen=false ) - .run(); + print.greenLine( 'Package [#boxJSON.slug#] unlinked from CommandBox core. Deactivating!' ); + moduleService.unloadAndUnregisterModule( boxJSON.slug ); + directoryDelete( linkTarget ); } else { + directoryDelete( linkTarget ); print.greenLine( 'Package [#boxJSON.slug#] unlinked from [#moduleDirectory#]' ); } diff --git a/src/cfml/system/modules_app/package-commands/interceptors/packageScripts.cfc b/src/cfml/system/modules_app/package-commands/interceptors/PackageScripts.cfc similarity index 100% rename from src/cfml/system/modules_app/package-commands/interceptors/packageScripts.cfc rename to src/cfml/system/modules_app/package-commands/interceptors/PackageScripts.cfc diff --git a/src/cfml/system/modules_app/server-commands/ModuleConfig.cfc b/src/cfml/system/modules_app/server-commands/ModuleConfig.cfc index f212d78bc..626b709d5 100644 --- a/src/cfml/system/modules_app/server-commands/ModuleConfig.cfc +++ b/src/cfml/system/modules_app/server-commands/ModuleConfig.cfc @@ -8,6 +8,7 @@ component { function configure() { interceptors = [ + { class="#moduleMapping#.interceptors.ServerScripts" }, { class="#moduleMapping#.interceptors.ServerCommandLine" }, { class="#moduleMapping#.interceptors.ServerSystemSettingExpansions" } ]; diff --git a/src/cfml/system/modules_app/server-commands/commands/server/cfpm.cfc b/src/cfml/system/modules_app/server-commands/commands/server/cfpm.cfc index d8d77d0d6..a4a67bc7f 100644 --- a/src/cfml/system/modules_app/server-commands/commands/server/cfpm.cfc +++ b/src/cfml/system/modules_app/server-commands/commands/server/cfpm.cfc @@ -1,5 +1,7 @@ /** - * Run cfpm for an Adobe ColdFuson 2021+ server + * Run cfpm for an Adobe ColdFuson 2021+ server. If there is more than one server started in the current working + * directory, this command will search for the first Adobe 2021+ server and use that. + * If this command is run as part of a server package script, it will applly to the server being started. * . * Open the cfpm shell * . @@ -12,22 +14,73 @@ * {code:bash} * cfpm install feed * {code} - **/ + * . + * If there is more than one Adobe 2021+ server started in a given directory, you can specific the server you want + * by setting the server name into the CFPM_SERVER environment variable. Note this works from any directory. + * Make sure to clear the env var afterwards so it doesn't surprise you on later usage of this command in the same shell. + * . + * {code:bash} + * set CFPM_SERVER=myTestServer + * cfpm install feed + * env clear CFPM_SERVER + * {code} +**/ component aliases='cfpm' { property name='serverService' inject='ServerService'; function run(){ - // Since any args passed in are sent on to cfpm, we can't allow the user to send us details of what server they want. - // Therefore, this command only works in the web root of the server and if the Adobe server is the default one. - var serverDetails = serverService.resolveServerDetails( {} ); - - if( serverDetails.serverIsNew ) { - error( 'No Server found in [#getCWD()#]' ); + var serverInfo = {}; + var cfpm_server = systemSettings.getSystemSetting( 'CFPM_SERVER', '' ); + var interceptData_serverInfo_name = systemSettings.getSystemSetting( 'interceptData.SERVERINFO.name', '' ); + + if( configService.getSetting( 'server.singleServerMode', false ) && serverService.getServers().count() ){ + serverInfo = serverService.getFirstServer().serverInfo; + // If we're running inside of a server-related package script, use that server + } else if( interceptData_serverInfo_name != '' ) { + print.yellowLine( 'Using interceptData to load server [#interceptData_serverInfo_name#]' ); + serverInfo = serverService.resolveServerDetails( { name=interceptData_serverInfo_name } ).serverInfo; + if( !(serverInfo.engineName contains 'adobe' && val( listFirst( serverInfo.engineVersion, '.' ) ) >= 2021 ) ){ + print.redLine( 'Server [#interceptData_serverInfo_name#] is of type [#serverInfo.cfengine#] and not an Adobe 2021+ server. Ignoring.' ); + return; + } + // Allow an env var hint to tell us what server to use + // CFPM_SERVER=servername + } else if( cfpm_server != '' ) { + print.yellowLine( 'Using CFPM_SERVER environment variable to load server [#cfpm_server#]' ); + var serverDetails = serverService.resolveServerDetails( { name=cfpm_server } ); + if( serverDetails.serverIsNew ) { + error( 'Server [#cfpm_server#] specified in CFPM_SERVER environment variable does not exist.' ); + return; + } + serverInfo = serverDetails.serverInfo; + if( !(serverInfo.engineName contains 'adobe' && val( listFirst( serverInfo.engineVersion, '.' ) ) >= 2021 ) ){ + print.redLine( 'Server [#cfpm_server#] is of type [#serverInfo.cfengine#] and not an Adobe 2021+ server. Ignoring.' ); + return; + } + } else { + // Fallback is to look for the first Adobe 2021+ server using the current working directory as its web root + var webroot = fileSystemUtil.resolvePath( getCWD() ); + var servers = serverService.getServers(); + for( var serverID in servers ){ + var thisServerInfo = servers[ serverID ]; + if( fileSystemUtil.resolvePath( path=thisServerInfo.webroot, forceDirectory=true ) == webroot + && thisServerInfo.engineName contains 'adobe' + && val( listFirst( thisServerInfo.engineVersion, '.' ) ) >= 2021 ){ + serverInfo = thisServerInfo; + print.yellowLine( 'Found server [#serverInfo.name#] in current directory.' ); + break; + } + } + if( !serverInfo.count() ) { + print.redLine( 'No Adobe 2021+ server found in [#getCWD()#]', 'Specify the server you want by setting the name of your server into the CFPM_SERVER environment variable.' ); + return; + } } - - var serverInfo = serverDetails.serverInfo; + + // ASSERT: At this point, we've found a specific Adobe 2021 server via env var, intercept data, or web root convention. + var cfpmPath = resolvePath( serverInfo.serverHomeDirectory ) & 'WEB-INF/cfusion/bin/cfpm'; if( !fileExists( cfpmPath & '.bat' ) ) { @@ -43,10 +96,20 @@ component aliases='cfpm' { while( !isNull( arguments[++i] ) ) { cmd &= ' #arguments[i]#'; } - - command( 'run' ) + + // The user's OS may not have a JAVA_HOME set up + if( systemSettings.getSystemSetting( 'JAVA_HOME', '' ) == '' ) { + systemSettings.setSystemSetting( 'JAVA_HOME', fileSystemUtil.getJREExecutable().reReplaceNoCase( '(/|\\)bin(/|\\)java(.exe)?', '' ) ); + } + print.toConsole(); + var output = command( 'run' ) .params( cmd ) - .run( echo=true ); + // Try to contain the output if we're in an interactive job and there are arguments (no args opens the cfpm shell) + .run( echo=true, returnOutput=( job.isActive() && arguments.count() ) ); + + if( job.isActive() && arguments.count() ) { + print.line( output ); + } } diff --git a/src/cfml/system/modules_app/server-commands/commands/server/java/search.cfc b/src/cfml/system/modules_app/server-commands/commands/server/java/search.cfc index 696215a15..90ee77d81 100644 --- a/src/cfml/system/modules_app/server-commands/commands/server/java/search.cfc +++ b/src/cfml/system/modules_app/server-commands/commands/server/java/search.cfc @@ -21,6 +21,13 @@ * server java search jvm= arch= type= os= * {code} * + * Or get the raw JSON as it was returned from the API + * {code:bash} + * server java search --JSON + * {code} + * + * If a failing HTTP status code is received from the API, this command will return an exit code of 1 + * **/ component aliases='java search' { @@ -42,6 +49,7 @@ component aliases='java search' { * @type.options jdk,jre * @release A specific release name or the word "latest" * @release.options latest + * @release.JSON Output the RAW JSON received from the remote API */ function run( version, @@ -49,7 +57,8 @@ component aliases='java search' { os, arch = server.java.archModel contains 32 ? 'x32' : 'x64', type = 'jre', - release = 'latest' + release = 'latest', + boolean JSON = false ){ // If there is no version passed but we have a release, default the version based on the release. @@ -63,8 +72,11 @@ component aliases='java search' { } // If there is no version and no release, hit the API to get the latest LTS version } else if( isNull( version ) ) { + // Until Adobe and Lucee support Java 17, we'll keep this defaulting to Java 11-- the current LTS release supported by CF engines. + version = 11; + /* http - url="https://api.adoptopenjdk.net/v3/info/available_releases" + url="https://api.adoptium.net/v3/info/available_releases" throwOnError=false timeout=5 proxyServer="#ConfigService.getSetting( 'proxy.server', '' )#" @@ -81,6 +93,7 @@ component aliases='java search' { } else { version = 11; } + */ } // Backwards compat so 8 so the same as openjdk8 @@ -101,7 +114,7 @@ component aliases='java search' { } } - var APIURLCheck = 'https://api.adoptopenjdk.net/v3/assets/version/#encodeForURL(version)#?page_size=100&release_type=ga&vendor=adoptopenjdk&project=jdk&heap_size=normal'; + var APIURLCheck = 'https://api.adoptium.net/v3/assets/version/#encodeForURL(version)#?page_size=100&release_type=ga&vendor=eclipse&project=jdk&heap_size=normal'; if( jvm.len() ) { APIURLCheck &= '&jvm_impl=#encodeForURL( jvm )#'; @@ -116,13 +129,6 @@ component aliases='java search' { APIURLCheck &= '&image_type=#encodeForURL( type )#'; } - print - .line() - .line( 'Hitting API URL:' ) - .indentedline( APIURLCheck ) - .line() - .line(); - http url="#APIURLCheck#" timeout=20 @@ -134,51 +140,69 @@ component aliases='java search' { result="local.artifactResult"; var fileContent = toString( local.artifactResult.fileContent ); - if( local.artifactResult.status_code == 200 && isJSON( fileContent ) ) { + if( local.artifactResult.status_code == 404 ) { + var artifactJSON = []; + } else if( local.artifactResult.status_code == 200 && isJSON( fileContent ) ) { var artifactJSON = deserializeJSON( fileContent ); - // If we have a release, we need to filter it now if( release.len() && release != 'latest' ) { artifactJSON = artifactJSON.filter( (thisRelease)=>thisRelease.release_name==release ); - } + } + } else { + print.redLine( fileContent.left( 100 ) ); + error( 'There was an error hitting the API. [#local.artifactResult.status_code#]' ); + } + + // Sometimes the API gives me back a struct, sometimes I get an array of structs. ¯\_(ツ)_/¯ + if( isStruct( artifactJSON ) ) { + artifactJSON = [ artifactJSON ]; + } - // Sometimes the API gives me back a struct, sometimes I get an array of structs. ¯\_(ツ)_/¯ - if( isStruct( artifactJSON ) ) { - artifactJSON = [ artifactJSON ]; - } + if( JSON ) { + print.line( artifactJSON ); + return; + } else { + print + .line() + .line( 'Hitting API URL:' ) + .indentedline( APIURLCheck ) + .line() + .line(); + } + + if( !artifactJSON.len() ) { + print.redLine( 'No matching Java versions found for your search criteria' ); + return; + } - for( var javaVer in artifactJSON ) { - var headerWidth = ('Release Name: ' & javaVer.release_name & ' Release Date: ' & dateFormat( javaVer.timestamp )).len()+4; - var colWidth = int( ( headerWidth/4 )-1 ); - var lastColWidth = headerWidth - ( (colWidth*4)+5 ) + colWidth; + for( var javaVer in artifactJSON ) { + var headerWidth = ('Release Name: ' & javaVer.release_name & ' Release Date: ' & dateFormat( javaVer.timestamp )).len()+4; + var colWidth = int( ( headerWidth/4 )-1 ); + var lastColWidth = headerWidth - ( (colWidth*4)+5 ) + colWidth; + print + .boldLine( repeatString( '-', headerWidth ) ) + .boldText( '| Release Name: ' ).boldCyanText( javaVer.release_name ).boldtext( ' Release Date: ' ).boldCyanText( dateFormat( javaVer.timestamp ) ).boldLine( ' |' ) + .boldLine( repeatString( '-', headerWidth ) ) + .bold( '|' ).boldCyan( printColumnValue( 'JVM', colWidth ) ).bold( '|' ).boldCyan( printColumnValue( 'OS', colWidth ) ).bold( '|' ).boldCyan( printColumnValue( 'Arch', colWidth ) ).bold( '|' ).boldCyan( printColumnValue( 'Type', lastColWidth ) ).boldLine( '|' ) + .boldLine( repeatString( '-', headerWidth ) ); + + javaVer.binaries = javaVer.binaries.sort( function( a, b ) { + return compareNoCase( a.jvm_impl & a.os & a.architecture & a.image_type, b.jvm_impl & b.os & b.architecture & b.image_type ) + } ); + for( var binary in javaVer.binaries ) { print - .boldLine( repeatString( '-', headerWidth ) ) - .boldText( '| Release Name: ' ).boldCyanText( javaVer.release_name ).boldtext( ' Release Date: ' ).boldCyanText( dateFormat( javaVer.timestamp ) ).boldLine( ' |' ) - .boldLine( repeatString( '-', headerWidth ) ) - .bold( '|' ).boldCyan( printColumnValue( 'JVM', colWidth ) ).bold( '|' ).boldCyan( printColumnValue( 'OS', colWidth ) ).bold( '|' ).boldCyan( printColumnValue( 'Arch', colWidth ) ).bold( '|' ).boldCyan( printColumnValue( 'Type', lastColWidth ) ).boldLine( '|' ) - .boldLine( repeatString( '-', headerWidth ) ); - - javaVer.binaries = javaVer.binaries.sort( function( a, b ) { - return compareNoCase( a.jvm_impl & a.os & a.architecture & a.image_type, b.jvm_impl & b.os & b.architecture & b.image_type ) - } ); - for( var binary in javaVer.binaries ) { - print - .line( '|' & printColumnValue( binary.jvm_impl, colWidth ) - & '|' & printColumnValue( binary.os, colWidth ) - & '|' & printColumnValue( binary.architecture, colWidth ) - & '|' & printColumnValue( binary.image_type, lastColWidth ) & '|' ) - .text( '|' ).yellowText( printColumnValue( 'ID: ' & java.getDefaultNameFromStruct( { version : 'openjdk'&javaVer.version_data.major, type : binary.image_type, arch : binary.architecture, os : binary.os, 'jvm-implementation' : binary.jvm_impl, release : javaVer.release_name } ), headerWidth-2 ) ).line( '|' ) - .line( repeatString( '-', headerWidth ) ); - } - print.line(); - if( release == 'latest' ) { - break; - } + .line( '|' & printColumnValue( binary.jvm_impl, colWidth ) + & '|' & printColumnValue( binary.os, colWidth ) + & '|' & printColumnValue( binary.architecture, colWidth ) + & '|' & printColumnValue( binary.image_type, lastColWidth ) & '|' ) + .text( '|' ).yellowText( printColumnValue( 'ID: ' & java.getDefaultNameFromStruct( { version : 'openjdk'&javaVer.version_data.major, type : binary.image_type, arch : binary.architecture, os : binary.os, 'jvm-implementation' : binary.jvm_impl, release : javaVer.release_name } ), headerWidth-2 ) ).line( '|' ) + .line( repeatString( '-', headerWidth ) ); + } + print.line(); + if( release == 'latest' ) { + break; } - } else { - print.boldRedLine( 'There was an error hitting the API. [#local.artifactResult.status_code#]' ); - print.redLine( fileContent.left( 100 ) ); } } @@ -197,7 +221,7 @@ component aliases='java search' { function versionComplete() { http - url="https://api.adoptopenjdk.net/v3/info/available_releases" + url="https://api.adoptium.net/v3/info/available_releases" throwOnError=false timeout=5 proxyServer="#ConfigService.getSetting( 'proxy.server', '' )#" @@ -214,4 +238,4 @@ component aliases='java search' { return []; } -} \ No newline at end of file +} diff --git a/src/cfml/system/modules_app/server-commands/commands/server/open.cfc b/src/cfml/system/modules_app/server-commands/commands/server/open.cfc index 6ec0df3cc..a47ca3134 100644 --- a/src/cfml/system/modules_app/server-commands/commands/server/open.cfc +++ b/src/cfml/system/modules_app/server-commands/commands/server/open.cfc @@ -48,7 +48,7 @@ component { var serverInfo = serverDetails.serverInfo; if( serverDetails.serverIsNew ){ - print.boldRedLine( "No server configurations found so have no clue what to open buddy!" ); + print.boldRedLine( "No servers found." ); } else { // myPath/file.cfm is normalized to /myMapth/file.cfm if( !arguments.URI.startsWith( '/' ) ) { diff --git a/src/cfml/system/modules_app/server-commands/commands/server/run-script.cfc b/src/cfml/system/modules_app/server-commands/commands/server/run-script.cfc new file mode 100644 index 000000000..50c0bc550 --- /dev/null +++ b/src/cfml/system/modules_app/server-commands/commands/server/run-script.cfc @@ -0,0 +1,92 @@ +/** + * Runs a server script, by name. Scripts are stored in server.json. + * . + * {code:bash} + * server run-script myScript + * {code} + * . + * Positional parameters can be passed and will be available as environment variables inside the script as ${1}, ${2}, etc + * . + * {code:bash} + * server run-script myScript param1 param2 + * {code} + * . + * Named parameters can be passed and will be available as environment variables inside the script as ${name1}, ${name2}, etc + * Note in this case, ALL parameters much be named including the scriptName param to the command. + * . + * {code:bash} + * server run-script scriptName=myScript name1=value1 name2=value2 + * {code} + **/ +component { + + property name="serverService" inject="ServerService"; + + /** + * @scriptName Name of the script to run + * @scriptName.optionsUDF scriptNameComplete + * @name.hint the short name of the server + * @name.optionsUDF serverNameComplete + * @directory.hint web root for the server + * @serverConfigFile The path to the server's JSON file. + **/ + function run( + required string scriptname, + string name, + string directory, + string serverConfigFile ){ + + if( !isNull( arguments.directory ) ) { + arguments.directory = resolvePath( arguments.directory ); + } + if( !isNull( arguments.serverConfigFile ) ) { + arguments.serverConfigFile = resolvePath( arguments.serverConfigFile ); + } + var serverDetails = serverService.resolveServerDetails( arguments ); + + // package check + if( serverDetails.serverIsNew ) { + error( "No servers found." ); + } + + // Add any additional arguments as env vars for the script to access + arguments + .filter( ( k, v ) => !'scriptName,name,directory,serverConfigFile'.listFindNoCase( k ) ) + .each( ( k, v ) => { + // Decrement positional params so they start at 1 + if( isNumeric( k ) && k > 4 ) { + k -= 4; + } + systemSettings.setSystemSetting( k, v ); + } ); + + serverService.runScript( scriptName=arguments.scriptName, ignoreMissing=false, interceptData={ serverJSON : serverDetails.serverJSON } ); + + } + + function scriptNameComplete( string paramSoFar, struct passedNamedParameters ) { + + if( !isNull( passedNamedParameters.directory ) ) { + passedNamedParameters.directory = resolvePath( passedNamedParameters.directory ); + } + if( !isNull( passedNamedParameters.serverConfigFile ) ) { + passedNamedParameters.serverConfigFile = resolvePath( passedNamedParameters.serverConfigFile ); + } + + var serverDetails = serverService.resolveServerDetails( passedNamedParameters ); + var results = []; + // package check + if( !serverDetails.serverIsNew ) { + results = ( serverDetails.serverJSON.scripts ?: {} ).keyArray(); + } + return ( serverService.getDefaultServerJSON().scripts ?: {} ).keyArray().append( results, true ); + } + + /** + * Complete server names + */ + function serverNameComplete() { + return serverService.serverNameComplete(); + } + +} 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 6b2436cc9..ac984c1a6 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 @@ -157,40 +157,26 @@ component aliases='status,server info' { print.indentedLine( 'ID: ' & thisServerInfo.id ); print.line().indentedLine( 'Server Home: ' & thisServerInfo.serverHome ); - - var portToCheck = 'stop socket'; - var portToCheckValue = thisServerInfo.stopSocket; - if( thisServerInfo.HTTPEnable ) { - portToCheck = 'HTTP port'; - portToCheckValue = thisServerInfo.port; - } else if( thisServerInfo.SSLEnable ) { - portToCheck = 'HTTPS port'; - portToCheckValue = thisServerInfo.SSLPort; - } else if( thisServerInfo.AJPEnable ) { - portToCheck = 'AJP port'; - portToCheckValue = thisServerInfo.AJPPort; - } - - print.line().indentedLine( 'Host/Port used for "running" check: #portToCheck# (#thisServerInfo.host#:#portToCheckValue#)'); - - var bindException = ''; - try { - var serverSocket = createObject( "java", "java.net.ServerSocket" ) - .init( - javaCast( "int", portToCheckValue ), - javaCast( "int", 1 ), - createObject( "java", "java.net.InetAddress" ).getByName( thisServerInfo.host ) ); - serverSocket.close(); - } catch( any var e ) { - bindException = e; - } - - if( !isSimpleValue( bindException ) ) { - print.indentedLine( 'Port bind result for "running" check: #bindException.type# #bindException.message# #bindException.detail#'); + + print.line().indentedLine( 'PID file used for "running" check: ' ) + .indentedIndentedLine( serverInfo.pidFile ); + + if( fileExists( serverInfo.pidFile ) ){ + print.indentedIndentedLine( 'PID file exists.' ); + try { + var serverPID = fileRead(serverInfo.pidFile); + if( serverService.isProcessAlive( serverPID, true ) ) { + print.indentedIndentedLine( 'PID [#serverPID#] is running' ); + } else { + print.indentedIndentedLine( 'PID [#serverPID#] is NOT running' ); + } + } catch( any var e ) { + print.indentedIndentedText( 'Error checking if server PID was running: [' ).redText( e.message & ' ' & e.detail ).line( '] Server is assumed running.' ); + } } else { - print.indentedLine( 'Port bind result for "running" check: successful bound, port not in use.'); + print.indentedIndentedLine( 'PID file does not exist. Server is assumed stopped.' ); } - + print.line().indentedLine( 'Last Command: ' ); // Put each --arg or -arg on a new line diff --git a/src/cfml/system/modules_app/server-commands/commands/server/stop.cfc b/src/cfml/system/modules_app/server-commands/commands/server/stop.cfc index 2064898e6..ce2d0f882 100644 --- a/src/cfml/system/modules_app/server-commands/commands/server/stop.cfc +++ b/src/cfml/system/modules_app/server-commands/commands/server/stop.cfc @@ -21,6 +21,7 @@ component aliases="stop" { * @forget Remove the directory information from disk * @all Stop ALL running servers * @verbose Show raw output of stop command + * @local Stop servers with webroot matching the current directory **/ function run( string name, @@ -28,11 +29,15 @@ component aliases="stop" { String serverConfigFile, boolean forget=false, boolean all=false, - boolean verbose=false ){ - - + boolean verbose=false, + boolean local=false + ){ if( arguments.all ) { var servers = serverService.getServers(); + } else if (arguments.local) { + var servers = serverService.getServers().filter( ( serverName, thisServerInfo ) => { + return getCanonicalPath( getCWD() ) == getCanonicalPath( thisServerInfo.webroot ); + }, true ); } else { if( !isNull( arguments.directory ) ) { diff --git a/src/cfml/system/modules_app/server-commands/interceptors/serverCommandLine.cfc b/src/cfml/system/modules_app/server-commands/interceptors/ServerCommandLine.cfc similarity index 100% rename from src/cfml/system/modules_app/server-commands/interceptors/serverCommandLine.cfc rename to src/cfml/system/modules_app/server-commands/interceptors/ServerCommandLine.cfc diff --git a/src/cfml/system/modules_app/server-commands/interceptors/ServerScripts.cfc b/src/cfml/system/modules_app/server-commands/interceptors/ServerScripts.cfc new file mode 100644 index 000000000..09cb818e9 --- /dev/null +++ b/src/cfml/system/modules_app/server-commands/interceptors/ServerScripts.cfc @@ -0,0 +1,35 @@ +/** +********************************************************************************* +* Copyright Since 2014 CommandBox by Ortus Solutions, Corp +* www.coldbox.org | www.ortussolutions.com +******************************************************************************** +* @author Brad Wood +* +* I am an interceptor that listens to all the server interception points and runs server scripts for them if they exist. +* +*/ +component { + property name="serverService" inject="ServerService"; + property name="shell" inject="shell"; + + function init() { + variables.inScript=false; + } + + function preServerStart() { processScripts( 'preServerStart', shell.pwd(), interceptData ); } + function onServerInstall() { processScripts( 'onServerInstall', interceptData.serverinfo.webroot, interceptData ); } + function onServerStart() { processScripts( 'onServerStart', interceptData.serverinfo.webroot, interceptData ); } + function onServerStop() { processScripts( 'onServerStop', interceptData.serverinfo.webroot, interceptData ); } + function preServerForget() { processScripts( 'preServerForget', interceptData.serverinfo.webroot, interceptData ); } + function postServerForget() { processScripts( 'postServerForget', interceptData.serverinfo.webroot, interceptData ); } + + function processScripts( required string interceptionPoint, string directory=shell.pwd(), interceptData={} ) { + inScript=true; + try { + serverService.runScript( arguments.interceptionPoint, arguments.directory, true, interceptData ); + } finally { + inScript=false; + } + } + +} diff --git a/src/cfml/system/modules_app/system-commands/commands/assertEqual.cfc b/src/cfml/system/modules_app/system-commands/commands/assertEqual.cfc index 2e764ddcb..aba922bd7 100644 --- a/src/cfml/system/modules_app/system-commands/commands/assertEqual.cfc +++ b/src/cfml/system/modules_app/system-commands/commands/assertEqual.cfc @@ -1,5 +1,5 @@ /** - * Returns a passing (0) or failing (1) exit code whether both parameters match. Command outputs nothing. + * Returns a passing (0) or failing (1) exit code if both parameters match. Command outputs nothing. * Comparison is case insensitive. * . * {code:bash} diff --git a/src/cfml/system/modules_app/system-commands/commands/assertFalse.cfc b/src/cfml/system/modules_app/system-commands/commands/assertFalse.cfc new file mode 100644 index 000000000..7809390a8 --- /dev/null +++ b/src/cfml/system/modules_app/system-commands/commands/assertFalse.cfc @@ -0,0 +1,24 @@ +/** + * Returns a passing (0) or failing (1) exit code if a falsey parameter passed. Command outputs nothing. + * Falsey values are arenything OTHER than "yes", "true" and positive integers. + * . + * {code:bash} + * assertFalse `package show private` && run-script foo + * assertFalse ${GOOD_THINGS} && run-doom + * assertFalse `#fileExists foo.txt` && echo "it's not there!" + * {code} +**/ +component { + + /** + * @predicate A value that is truthy or falsy. + **/ + function run( required string predicate ) { + + if( isBoolean( predicate ) && predicate ) { + setExitCode( 1 ); + } + + } + +} diff --git a/src/cfml/system/modules_app/system-commands/commands/assertNotEqual.cfc b/src/cfml/system/modules_app/system-commands/commands/assertNotEqual.cfc new file mode 100644 index 000000000..5ef9653b2 --- /dev/null +++ b/src/cfml/system/modules_app/system-commands/commands/assertNotEqual.cfc @@ -0,0 +1,25 @@ +/** + * Returns a passing (0) or failing (1) exit code if parameters do not match. Command outputs nothing. + * Comparison is case insensitive. + * . + * {code:bash} + * assertNotEqual `package show name` "My Package" && package set name="My Package" + * assertNotEqual ${ENVIRONMENT} development && install --production + * {code} + * +**/ +component { + + /** + * @value1 A value to be compared to value2 + * @value2 A value to be compared to value1 + **/ + function run( required string value1, required string value2 ) { + + if( value1 == value2 ) { + setExitCode( 1 ); + } + + } + +} diff --git a/src/cfml/system/modules_app/system-commands/commands/assertTrue.cfc b/src/cfml/system/modules_app/system-commands/commands/assertTrue.cfc index 446fdcb3e..c3a0d4db7 100644 --- a/src/cfml/system/modules_app/system-commands/commands/assertTrue.cfc +++ b/src/cfml/system/modules_app/system-commands/commands/assertTrue.cfc @@ -1,7 +1,7 @@ /** - * Returns a passing (0) or failing (1) exit code whether truthy parameter passed. Command outputs nothing. + * Returns a passing (0) or failing (1) exit code if truthy parameter passed. Command outputs nothing. * Truthy values are "yes", "true" and positive integers. - * All other values are considered falsy + * All other values are considered falsey * . * {code:bash} * assertTrue `package show private` && run-script foo diff --git a/src/cfml/system/modules_app/system-commands/commands/dir.cfc b/src/cfml/system/modules_app/system-commands/commands/dir.cfc index fbe023db8..c0d4220d4 100644 --- a/src/cfml/system/modules_app/system-commands/commands/dir.cfc +++ b/src/cfml/system/modules_app/system-commands/commands/dir.cfc @@ -53,7 +53,10 @@ component aliases="ls,ll,directory" { paths.setPattern( paths.getPatternArray().map( (p) => { if( directoryExists( p ) ){ - return p & '*' & ( recurse ? '*' : '' ); + if( !p.endsWith( '/' ) && !p.endsWith( '\' ) ) { + p &= '/'; + } + return p &= '*' & ( recurse ? '*' : '' ); } return p; } ) @@ -62,7 +65,10 @@ component aliases="ls,ll,directory" { excludePaths = excludePaths.listMap( (p) => { p = fileSystemUtil.resolvePath( p ) if( directoryExists( p ) ){ - return p & '*' & ( recurse ? '*' : '' ); + if( !p.endsWith( '/' ) && !p.endsWith( '\' ) ) { + p &= '/'; + } + return p &= '*' & ( recurse ? '*' : '' ); } return p; } ); diff --git a/src/cfml/system/modules_app/system-commands/commands/edit.cfc b/src/cfml/system/modules_app/system-commands/commands/edit.cfc index 165c00836..527a2c25d 100644 --- a/src/cfml/system/modules_app/system-commands/commands/edit.cfc +++ b/src/cfml/system/modules_app/system-commands/commands/edit.cfc @@ -14,14 +14,10 @@ component aliases="open" { /** * @path.hint Path to open natively. **/ - function run( Globber path=globber( getCWD() ) ) { + function run( Globber path=globber( getCWD().left(-1) ) ) { path.apply( function( thisPath ) { - if( !fileExists( thisPath ) AND !directoryExists( thisPath ) ){ - return error( "Path: #thisPath# does not exist, cannot open it!" ); - } - if( fileSystemUtil.openNatively( thisPath ) ){ print.line( "Resource Opened!" ); } else { diff --git a/src/cfml/system/modules_app/system-commands/commands/head.cfc b/src/cfml/system/modules_app/system-commands/commands/head.cfc new file mode 100644 index 000000000..f694d2a4e --- /dev/null +++ b/src/cfml/system/modules_app/system-commands/commands/head.cfc @@ -0,0 +1,66 @@ +/** + * Show the first x lines of a file. Path may be absolute or relative to the current working directory. + * . + * {code:bash} + * head file.txt + * {code} + * Displays the contents of a file to standard CommandBox output according to the number of lines argument. + * . + * Use the "lines" param to specify the number of lines to display, or it defaults to 15 lines. + * . + * {code:bash} + * head file.txt 100 + * {code} + **/ + component { + property name="printUtil" inject="print"; + property name='ansiFormatter' inject='AnsiFormatter'; + + /** + * @path file or directory to tail or raw input to process + * @lines number of lines to display. + **/ + function run( required path, numeric lines = 15 ){ + 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 = resolvePath( arguments.path ); + + if( !fileExists( filePath ) ){ + var rawText = true; + } + + // If we're piping raw text and not a file + if( rawText ) { + // Only show the first X lines + var i = 1; + while( i <= inputAsArray.len() && i <= lines ) { + print.line( inputAsArray[ i++ ] ); + } + + return; + } + + try { + var fileObject = fileObject = fileOpen( filePath ); + // Only show the first X lines + var i = 1; + while( i++ <= lines ) { + print.line( fileReadLine( fileObject ) ); + if( fileIsEOF( fileObject ) ) { + return; + } + } + } finally { + if( !isNull( fileObject ) ) { + fileClose( fileObject ); + } + } + + + } +} diff --git a/src/cfml/system/modules_app/system-commands/commands/printTable.cfc b/src/cfml/system/modules_app/system-commands/commands/printTable.cfc index 1509d87e4..041f5e41e 100644 --- a/src/cfml/system/modules_app/system-commands/commands/printTable.cfc +++ b/src/cfml/system/modules_app/system-commands/commands/printTable.cfc @@ -109,13 +109,15 @@ component { * @includedHeaders A list of headers to include. * @headerNames An list/array of column headers to use instead of the default specifically for array of arrays * @debug Only print out the names of the columns and the first row values + * @width Override the terminal width */ public string function run( required any data=[], any includedHeaders="", any headerNames="", - boolean debug=false + boolean debug=false, + width=-1 ) { // Treat input as a potential file path diff --git a/src/cfml/system/modules_app/system-commands/commands/recipe.cfc b/src/cfml/system/modules_app/system-commands/commands/recipe.cfc index 2e6be92c5..d789a197f 100644 --- a/src/cfml/system/modules_app/system-commands/commands/recipe.cfc +++ b/src/cfml/system/modules_app/system-commands/commands/recipe.cfc @@ -66,6 +66,10 @@ component { // Validate the file if( !fileExists( tmpRecipeFile ) ){ + // If the input is a single line ending in .boxr, it was supposed to be a file! + if( lcase( arguments.recipeFile ).endsWith( '.boxr' ) && listToArray( recipe, chr( 10 ) & chr( 13 ) ).len() == 1 ) { + error( 'Recipe file [#tmpRecipeFile#] not found.' ) + } // If the file doesn't exist, accept the input as commands var recipe = arguments.recipeFile; } else { diff --git a/src/cfml/system/modules_app/system-commands/commands/upgrade.cfc b/src/cfml/system/modules_app/system-commands/commands/upgrade.cfc index 7d476cc76..60ad5a0f9 100644 --- a/src/cfml/system/modules_app/system-commands/commands/upgrade.cfc +++ b/src/cfml/system/modules_app/system-commands/commands/upgrade.cfc @@ -34,7 +34,11 @@ component { * @force.hint Force the update even if the version on the server is the same as locally **/ function run( boolean latest, boolean force=false ) { - + + if( configService.getSetting( 'offlineMode', false ) ) { + error( 'Can''t check for updates, CommandBox is in offline mode. Go online with [config set offlineMode=false].' ); + } + if( isNull( arguments.latest ) ) { if( semanticVersion.isPreRelease( shell.getVersion() ) ) { print diff --git a/src/cfml/system/modules_app/testbox-commands/commands/testbox/watch.cfc b/src/cfml/system/modules_app/testbox-commands/commands/testbox/watch.cfc index 090028e11..53fcb9cd4 100644 --- a/src/cfml/system/modules_app/testbox-commands/commands/testbox/watch.cfc +++ b/src/cfml/system/modules_app/testbox-commands/commands/testbox/watch.cfc @@ -43,6 +43,12 @@ component { * @bundles The path or list of paths of the spec bundle CFCs to run and test * @labels The list of labels that a suite or spec must have in order to execute. * @verbose Display extra details including passing and skipped tests. + * @recurse Recurse the directory mapping or not, by default it does + * @reporter The type of reporter to use for the results, by default is uses our 'simple' report. You can pass in a core reporter string type or a class path to the reporter to use. + * @options Add adhoc URL options to the runner as options:name=value options:name2=value2 + * @testBundles A list or array of bundle names that are the ones that will be executed ONLY! + * @testSuites A list of suite names that are the ones that will be executed ONLY! + * @testSpecs A list of test names that are the ones that will be executed ONLY! **/ function run( string paths, @@ -50,10 +56,16 @@ component { directory, bundles, labels, - boolean verbose + boolean verbose, + boolean recurse, + string reporter, + struct options={}, + string testBundles, + string testSuites, + string testSpecs ){ // Get testbox options from package descriptor - var boxOptions = packageService.readPackageDescriptor( getCWD() ).testbox; + var boxOptions = variables.packageService.readPackageDescriptor( getCWD() ).testbox; var getOptionsWatchers = function(){ // Return to List @@ -72,23 +84,8 @@ component { // Tabula rasa command( "cls" ).run(); - // Prepare test args - var testArgs = {}; - - if ( !isNull( arguments.verbose ) ) { - testArgs.verbose = arguments.verbose; - } - if ( !isNull( arguments.directory ) ) { - testArgs.directory = arguments.directory; - } - if ( !isNull( arguments.bundles ) ) { - testArgs.bundles = arguments.bundles; - } - if ( !isNull( arguments.labels ) ) { - testArgs.labels = arguments.labels; - } - - // Start watcher + // Start watcher with passed args only. + var testArgs = arguments.filter( (k,v) => !isNull( v ) ); watch() .paths( globbingPaths.listToArray() ) .inDirectory( getCWD() ) diff --git a/src/cfml/system/services/CommandService.cfc b/src/cfml/system/services/CommandService.cfc index c328c37c2..f8e727cf9 100644 --- a/src/cfml/system/services/CommandService.cfc +++ b/src/cfml/system/services/CommandService.cfc @@ -76,9 +76,9 @@ component accessors="true" singleton { /** * Initialize the commands. This will recursively call itself for subdirectories. - * @baseCommandDirectory.hint The starting directory - * @commandDirectory.hint The current directory we've recursed into - * @commandPath.hint The dot-delimited path so far-- only used when recursing + * @baseCommandDirectory The starting directory + * @commandDirectory The current directory we've recursed into + * @commandPath The dot-delimited path so far-- only used when recursing **/ CommandService function initCommands( required string baseCommandDirectory, @@ -105,7 +105,39 @@ component accessors="true" singleton { return this; } + /** + * Remove commands from the CommandService. This will recursively call itself for subdirectories. + * @baseCommandDirectory The starting directory + * @commandDirectory The current directory we've recursed into + * @commandPath The dot-delimited path so far-- only used when recursing + **/ + CommandService function removeCommands( + required string baseCommandDirectory, + string commandDirectory=baseCommandDirectory, + string commandPath='' + ){ + var varDirs = DirectoryList( + path = expandPath( arguments.commandDirectory ), + recurse = false, + listInfo = 'query', + sort = 'type desc, name asc' + ); + + for( var dir in varDirs ){ + // For CFC files, process them as a command + if( dir.type == 'File' && listLast( dir.name, '.' ) == 'cfc' ){ + removeCommand( baseCommandDirectory, dir.name, commandPath ); + // For folders, search them for commands + } else { + removeCommands( baseCommandDirectory, dir.directory & '\' & dir.name, listAppend( commandPath, dir.name, '.' ) ); + } + } + + return this; + } + function addToDictionary( required command, required commandPath ){ + // Build bracketed string of command path to allow special characters var commandPathBracket = ''; var commandName = ''; @@ -121,9 +153,62 @@ component accessors="true" singleton { instance.flattenedCommands[ trim(commandName) ] = command; } + function removeFromDictionary( required command, required commandPath ){ + + // Build bracketed string of command path to allow special characters + var commandPathBracket = ''; + var commandName = ''; + + commandPathArray = listToArray( commandPath, '.' ); + + for( var item in commandPathArray ){ + commandPathBracket &= '[ "#item#" ]'; + commandName &= "#item# "; + } + + // This happens if a command is registered twice and we've already removed it + if( !isDefined( "instance.commands#commandPathBracket#" ) ) { + return; + } + + // Here in this flat collection for help usage + instance.flattenedCommands.delete( trim(commandName) ); + + if( isDefined( "instance.commands#commandPathBracket#[ '$' ]" ) ) { + evaluate( "structDelete( instance.commands#commandPathBracket#, '$' )" ); + } + + + // Remove from the leaf nodes up, removing any empty namespaces as we go. + // We can't just kill the namespace, because a command that contributes a "server foobar" command would not want to remove the 'server' namespace. + while( commandPathArray.len() ) { + + // If there are other commands in this namespace, we're done + if( evaluate( "structCount( instance.commands#commandPathBracket# )" ) ) { + break; + } + + // If this namespace is empty, pop last item off the array and remove it + var lastItem = commandPathArray.pop(); + + commandPathBracket = ''; + for( var item in commandPathArray ){ + commandPathBracket &= '[ "#item#" ]'; + } + + evaluate( "structDelete( instance.commands#commandPathBracket#, lastItem )" ); + + // We'll keep climbing "up" in our while() until we run out of namespaces, or we reach a namespace that still populated + + } + + + } + + /** * run a command line - * @line.hint line to run + * @line line to run * @captureOutput Temp workaround to allow capture of run command **/ function runCommandline( required string line, boolean captureOutput=false ){ @@ -141,27 +226,28 @@ component accessors="true" singleton { /** * run a command tokens - * @tokens.hint tokens to run - * @piped.hint Data to pipe in to the first command + * @tokens tokens to run + * @piped Data to pipe in to the first command * @captureOutput Temp workaround to allow capture of run command + * @line This is the original, unparsed line typed by the user **/ - function runCommandTokens( required array tokens, string piped, boolean captureOutput=false ){ + function runCommandTokens( required array tokens, string piped, boolean captureOutput=false, required string line ){ // Resolve the command they are wanting to run - var commandChain = resolveCommandTokens( tokens ); + var commandChain = resolveCommandTokens( tokens, line ); // If there was piped input if( structKeyExists( arguments, 'piped' ) ) { - return runCommand( commandChain, tokens.toList( ' ' ), arguments.piped, captureOutput ); + return runCommand( commandChain, line, arguments.piped, captureOutput ); } - return runCommand( commandChain=commandChain, line=tokens.toList( ' ' ), captureOutput=captureOutput ); + return runCommand( commandChain=commandChain, line=line, captureOutput=captureOutput ); } /** * run a command - * @commandChain.hint the chain of commands to run + * @commandChain the chain of commands to run * @captureOutput Temp workaround to allow capture of run command **/ function runCommand( required array commandChain, required string line, string piped, boolean captureOutput=false ){ @@ -194,14 +280,15 @@ component accessors="true" singleton { } if( previousCommandSeparator == '&&' && lastCommandErrored ) { + continue; return result ?: ''; } if( previousCommandSeparator == '||' && !lastCommandErrored ) { + continue; return result ?: ''; } - // If nothing was found, bail out here. if( !commandInfo.found ){ var detail = generateListOfSimilarCommands( commandInfo ); @@ -263,7 +350,11 @@ component accessors="true" singleton { // If we're not piping, output what we've got } else { if( structKeyExists( local, 'result' ) && len( result ) ){ - shell.printString( result & cr ); + if( job.getActive() ) { + job.addLog( result ); + } else { + shell.printString( result & cr ); + } } structDelete( local, 'result', false ); } @@ -282,7 +373,13 @@ component accessors="true" singleton { // This will prevent the output from showing up out of order if one command nests a call to another. if( instance.callStack.len() && !captureOutput ){ // Print anything in the buffer - shell.printString( instance.callStack[1].commandInfo.commandReference.CFC.getResult() ); + var job = wirebox.getInstance( 'interactiveJob' ); + // If there is an active job, print our output through it + if( job.getActive() ) { + job.addLog( instance.callStack[1].commandInfo.commandReference.CFC.getResult() ); + } else { + shell.printString( instance.callStack[1].commandInfo.commandReference.CFC.getResult() ); + } // And reset it now that it's been printed. // This command can add more to the buffer once it's executing again. instance.callStack[1].commandInfo.commandReference.CFC.reset(); @@ -346,6 +443,7 @@ component accessors="true" singleton { lastCommandErrored = commandInfo.commandReference.CFC.hasError(); } catch( any e ){ + FRTransService.errorTransaction( FRTrans, e.getPageException() ); lastCommandErrored = true; // If this command didn't already set a failing exit code... @@ -360,26 +458,46 @@ component accessors="true" singleton { } - // Dump out anything the command had printed so far var result = commandInfo.commandReference.CFC.getResult(); - if( len( result ) ){ - shell.printString( result & cr ); + // Add the command output thus far into the exception + var originalInfo = ''; + if( len( result ) ) { + originalInfo = e.extendedInfo + e.extendedInfo=serializeJSON( { + 'extendedInfo'=originalInfo, + 'commandOutput'=result + } ); } + // This is a little hacky, but basically if there are more commands in the chain that need to run, // just print an exception and move on. Otherwise, throw so we can unwrap the call stack all the way // back up. That is necessary for command expressions that fail like "echo `cat noExists.txt`" // since in that case I don't want to execute the "echo" command since the "cat" failed. if( arrayLen( commandChain ) > i && listFindNoCase( '||,;', commandChain[ i+1 ].originalLine ) ) { + + // Dump out anything the command had printed so far + if( len( result ) ){ + shell.printString( result & cr ); + } + // These are "expected" exceptions like validation errors that can be "pretty" if( e.type.toString() == 'commandException' ) { - shell.printError( { message : e.message, detail: e.detail } ); + shell.printError( { message : e.message, detail: e.detail, extendedInfo : e.extendedInfo ?: '' } ); // These are catastrophic errors that warrant a full stack trace. } else { shell.printError( e ); } // Unwind the stack to the closest catch } else { - rethrow; + + if( !captureOutput && len( result ) ) { + // Dump out anything the command had printed so far + shell.printString( result & cr ); + // If we're printing it now, remove it from the exception so the shell doesn't double-print it. + e.extendedInfo=originalInfo; + + } + throw e; } } finally { // Remove it from the stack @@ -547,8 +665,8 @@ component accessors="true" singleton { /** * Take an array of parameters and parse them out as named or positional - * @parameters.hint The array of params to parse. - * @commandParameters.hint valid params defined by the command + * @parameters The array of params to parse. + * @commandParameters valid params defined by the command **/ function parseParameters( parameters, commandParameters ){ return parser.parseParameters( parameters, commandParameters ); @@ -556,7 +674,7 @@ component accessors="true" singleton { /** * Figure out what command to run based on the user input string - * @line.hint A string containing the command and parameters that the user entered + * @line A string containing the command and parameters that the user entered **/ function resolveCommand( required string line, boolean forCompletion=false ){ // Turn the users input into an array of tokens @@ -567,7 +685,7 @@ component accessors="true" singleton { /** * Figure out what command to run based on the tokenized user input - * @tokens.hint An array containing the command and parameters that the user entered + * @tokens An array containing the command and parameters that the user entered **/ function resolveCommandTokens( required array tokens, string rawLine=tokens.toList( ' ' ), boolean forCompletion=false ){ @@ -626,7 +744,6 @@ component accessors="true" singleton { * run "cmd /c dir" */ if( tokens.len() > 1 && tokens.first() == 'run' ) { - var tokens2 = tokens[ 2 ]; // Escape any regex metacharacters in the pattern tokens2 = replace( tokens2, '\', '\\', 'all' ); @@ -671,7 +788,7 @@ component accessors="true" singleton { tokens[1] = right( tokens[1], len( tokens[1] ) - 1 ); // If it looks like we have named params, convert the "name" to be named - if( tokens.len() > 1 && tokens[2] contains '=' ) { + if( tokens.len() > 1 && parser.parseParameters( [ tokens[2] ], [] ).namedParameters.count() ) { tokens[1] = 'name=' & tokens[1]; } @@ -868,7 +985,7 @@ component accessors="true" singleton { /** * Takes a struct of command data and lazy loads the actual CFC instance if necessary - * @commandData.hint Struct created by registerCommand() + * @commandData Struct created by registerCommand() **/ private function lazyLoadCommandCFC( commandData ){ @@ -902,7 +1019,7 @@ component accessors="true" singleton { /** * Looks at the call stack to determine if we're currently "inside" a command. * Useful to prevent endless recursion. - * @command.hint Name of the command to look for as typed from the shell. If empty, returns true for any command + * @command Name of the command to look for as typed from the shell. If empty, returns true for any command **/ function inCommand( command='' ){ @@ -948,9 +1065,9 @@ component accessors="true" singleton { /** * load command CFC - * @baseCommandDirectory.hint The base directory for this command - * @cfc.hint CFC name that represents the command - * @commandPath.hint The relative dot-delimited path to the CFC starting in the commands dir + * @baseCommandDirectory The base directory for this command + * @cfc CFC name that represents the command + * @commandPath The relative dot-delimited path to the CFC starting in the commands dir **/ private function registerCommand( baseCommandDirectory, CFC, commandPath ){ @@ -988,6 +1105,50 @@ component accessors="true" singleton { } } + /** + * Remove a command from memory + * @baseCommandDirectory The base directory for this command + * @cfc CFC name that represents the command + * @commandPath The relative dot-delimited path to the CFC starting in the commands dir + **/ + private function removeCommand( baseCommandDirectory, CFC, commandPath ){ + + // Strip cfc extension from filename + var CFCName = mid( CFC, 1, len( CFC ) - 4 ); + var commandName = iif( len( commandPath ), de( commandPath & '.' ), '' ) & CFCName; + // Build CFC's path + var fullCFCPath = baseCommandDirectory & '.' & commandName; + + + try { + // Create a nice struct of command metadata + var commandData = createCommandData( fullCFCPath, commandName ); + // This will catch nasty parse errors so the shell can keep loading + } catch( any e ){ + shell.printString( 'Error loading command data [#fullCFCPath#]#cr#' ); + logger.error( 'Error loading command data [#fullCFCPath#]. #e.message# #e.detail ?: ''#', e.stackTrace ); + // pretty print the exception + shell.printError( e ); + return; + } + + // must be CommandBox CFC, can't be Application.cfc + if( CFCName == 'Application' || !isCommandCFC( commandData ) ){ + return; + } + + // Add it to the command dictionary + removeFromDictionary( commandData, commandPath & '.' & CFCName ); + + // Register the aliases + for( var alias in commandData.aliases ){ + // Alias is allowed to be anything. This means it may even overwrite another command already loaded. + removeFromDictionary( commandData, listChangeDelims( trim( alias ), '.', ' ' ) ); + } + var mappingName = "command-" & fullCFCPath; + wirebox.getScope( 'singleton' ).getSingletons().delete( mappingName ); + } + /** * Create command metadata * @fullCFCPath the full CFC path @@ -1134,7 +1295,19 @@ component accessors="true" singleton { // Overwrite it with an actual Globber instance seeded with the original canonical path as the pattern. var originalPath = parameterInfo.namedParameters[ paramName ]; - var newPath = originalPath.listMap( (p) => fileSystemUtil.resolvePath( p ) ); + var newPath = originalPath.listMap( (p) => { + p = fileSystemUtil.resolvePath( p ); + // The globber won't match a dir if you include the trailing slash, so pull them off + // This allows + // > rm tests/ + // or + // > touch tests + // to affect the "tests" folder itself + if( p.endsWith( '/' ) || p.endsWith( '\' ) ) { + p = p.left(-1); + } + return p; + } ); parameterInfo.namedParameters[ paramName ] = wirebox.getInstance( 'Globber' ) .setPattern( newPath ); @@ -1156,7 +1329,7 @@ component accessors="true" singleton { && !isValid( param.type, userNamedParams[ param.name ] ) ){ shell.setExitCode( 1 ); - throw( message='Parameter [#param.name#] has a value of [#userNamedParams[ param.name ]#] which is not of type [#param.type#].', type="commandException"); + throw( message='Parameter [#param.name#] has a value of [#serialize( userNamedParams[ param.name ] )#] which is not of type [#param.type#].', type="commandException"); } } // end for loop diff --git a/src/cfml/system/services/ConfigService.cfc b/src/cfml/system/services/ConfigService.cfc index 80b7f18e0..d8e9c2af1 100644 --- a/src/cfml/system/services/ConfigService.cfc +++ b/src/cfml/system/services/ConfigService.cfc @@ -74,6 +74,7 @@ component accessors="true" singleton { 'nonInteractiveShell', 'tabCompleteInline', 'colorInDumbTerminal', + 'terminalWidth', // JSON 'JSON.indent', 'JSON.lineEnding', @@ -86,6 +87,8 @@ component accessors="true" singleton { // General 'verboseErrors', 'debugNativeExecution', + 'developerMode', + 'offlineMode', // Task Runners 'taskCaching' ]); @@ -196,7 +199,7 @@ component accessors="true" singleton { return variables.configSettings; } - // env var/system property overrides which we want to keep seperate so we don't write them back to the JSON file. + // env var/system property overrides which we want to keep separate so we don't write them back to the JSON file. return JSONService.mergeData( duplicate( variables.configSettings ), getConfigSettingOverrides() ); } diff --git a/src/cfml/system/services/InterceptorService.cfc b/src/cfml/system/services/InterceptorService.cfc index 9ac1eb6b2..b1e539ab1 100644 --- a/src/cfml/system/services/InterceptorService.cfc +++ b/src/cfml/system/services/InterceptorService.cfc @@ -53,12 +53,16 @@ component accessors=true singleton { return this; } - function announceInterception( required string state, struct interceptData={} ) { + function announce( required string state, struct interceptData={} ) { getEventPoolManager().announce( state, interceptData ); } + function announceInterception( required string state, struct interceptData={} ) { + announce( state, interceptData ); + } + function processState( required string state, struct interceptData={} ) { - announceInterception( state, interceptData ); + announce( state, interceptData ); } /** diff --git a/src/cfml/system/services/JSONService.cfc b/src/cfml/system/services/JSONService.cfc index 5a094f78c..b6fe8e3d2 100644 --- a/src/cfml/system/services/JSONService.cfc +++ b/src/cfml/system/services/JSONService.cfc @@ -120,7 +120,7 @@ component accessors="true" singleton { } // structs } else { - targetProperty.append( complexValue, true ); + mergeData( targetProperty, complexValue ); } results.append( '#propertyValue# appended to #prop#' ); continue; diff --git a/src/cfml/system/services/ModuleService.cfc b/src/cfml/system/services/ModuleService.cfc index 9a3f9c961..ca2ff7fb7 100644 --- a/src/cfml/system/services/ModuleService.cfc +++ b/src/cfml/system/services/ModuleService.cfc @@ -142,7 +142,7 @@ if( registerModule( arguments.moduleName, arguments.invocationPath ) ) { - activateModule( arguments.moduleName ); + activateModule( arguments.moduleName ); } @@ -345,6 +345,22 @@ + + + + // Remove from module registry + structDelete( instance.moduleRegistry, arguments.moduleName ); + + + + + + + unload( arguments.moduleName ); + unregisterModule( arguments.moduleName ); + + + @@ -491,11 +507,6 @@ } } - // Register module routing entry point pre-pended to routes - /*if( shell.settingExists( 'sesBaseURL' ) AND len( mConfig.entryPoint ) AND NOT find( ":", mConfig.entryPoint ) ){ - interceptorService.getInterceptor( "SES", true ).addModuleRoutes( pattern=mConfig.entryPoint, module=arguments.moduleName, append=false ); - }*/ - // Call on module configuration object onLoad() if found if( structKeyExists( instance.mConfigCache[ arguments.moduleName ], "onLoad" ) ){ instance.mConfigCache[ arguments.moduleName ].onLoad(); @@ -580,6 +591,8 @@ // Check if module is loaded? if( NOT structKeyExists(getModuleData(),arguments.moduleName) ){ return false; } + var modules = getModuleData(); + var mConfig = modules[ arguments.moduleName ]; @@ -608,11 +621,6 @@ // Unregister Config object interceptorService.unregister( "ModuleConfig:#arguments.moduleName#" ); - // Remove SES if enabled. - /*if( shell.settingExists( "sesBaseURL" ) ){ - interceptorService.getInterceptor( "SES", true ).removeModuleRoutes( arguments.moduleName ); - }*/ - // Remove the possible config names with the ConfigService for auto-completion var possibleConfigSettings = []; for( var settingName in ConfigService.getPossibleConfigSettings() ) { @@ -622,6 +630,12 @@ } ConfigService.setPossibleConfigSettings( possibleConfigSettings ); + // Register commands if they exist + if( directoryExists( mconfig.commandsPhysicalPath ) ){ + var commandPath = '/' & replace( mconfig.commandsInvocationPath, '.', '/', 'all' ); + CommandService.removeCommands( commandPath ); + } + // Remove configuration structDelete( getModuleData(), arguments.moduleName ); @@ -682,9 +696,9 @@ oConfig.injectPropertyMixin( "log", shell.getLogBox().getLogger( oConfig) ); oConfig.injectPropertyMixin( "wirebox", shell.getWireBox() ); oConfig.injectPropertyMixin( "binder", shell.getWireBox().getBinder() ); - oConfig.injectPropertyMixin( "getSystemSetting", systemSettings.getSystemSetting ); - oConfig.injectPropertyMixin( "getSystemProperty", systemSettings.getSystemProperty ); - oConfig.injectPropertyMixin( "getEnv", systemSettings.getEnv ); + oConfig.injectPropertyMixin( "getSystemSetting", ()=>systemSettings.getSystemSetting(argumentCollection=arguments) ); + oConfig.injectPropertyMixin( "getSystemProperty", ()=>systemSettings.getSystemProperty(argumentCollection=arguments) ); + oConfig.injectPropertyMixin( "getEnv", ()=>systemSettings.getEnv(argumentCollection=arguments) ); // Configure the module oConfig.configure(); diff --git a/src/cfml/system/services/PackageService.cfc b/src/cfml/system/services/PackageService.cfc index f0d15266b..d333071e7 100644 --- a/src/cfml/system/services/PackageService.cfc +++ b/src/cfml/system/services/PackageService.cfc @@ -30,7 +30,7 @@ component accessors="true" singleton { property name='tempDir' inject='tempDir@constants'; property name='serverService' inject='serverService'; property name='moduleService' inject='moduleService'; - + /** * Constructor @@ -99,7 +99,7 @@ component accessors="true" singleton { arguments.production = arguments.production ?: true; var endpointData = endpointService.resolveEndpoint( arguments.ID, arguments.currentWorkingDirectory ); - + job.start( 'Installing package [#endpointData.ID#]', ( shell.getTermHeight() < 20 ? 1 : 5 ) ); if( verbose ) { @@ -385,7 +385,7 @@ component accessors="true" singleton { if( !serverDetails.serverIsNew && !(serverInfo.engineName contains 'lucee') ) { job.addWarnLog( "We did find a server, but the engine is [#serverInfo.engineName#] instead of 'lucee'" ); } - + } } @@ -622,15 +622,12 @@ component accessors="true" singleton { if( shellWillReload && artifactDescriptor.createPackageDirectory && fileExists( installDirectory & '/ModuleConfig.cfc' ) ) { consoleLogger.warn( 'Activating your new module for instant use...' ); - moduleService.registerAndActivateModule( installDirectory.listLast( '/\' ), fileSystemUtil.makePathRelative( installDirectory ) ); - //shell.reload( clear=false ); - //consoleLogger.warn( '.' ); - //consoleLogger.warn( 'Please sit tight while your shell reloads...' ); + moduleService.registerAndActivateModule( installDirectory.listLast( '/\' ), fileSystemUtil.makePathRelative( installDirectory ).listToArray( '/\' ).slice( 1, -1 ).toList( '.' ) ); } - + interceptorService.announceInterception( 'postInstall', { installArgs=arguments, installDirectory=installDirectory } ); job.complete( verbose ); - + return true; } @@ -766,6 +763,21 @@ component accessors="true" singleton { // uninstall the package if( len( uninstallDirectory ) && directoryExists( uninstallDirectory ) ) { + + // If this package is being uninstalled anywhere south of the CommandBox system folder, unload the module first + if( fileSystemUtil.normalizeSlashes( uninstallDirectory ).startsWith( fileSystemUtil.normalizeSlashes( expandPath( '/commandbox' ) ) ) && fileExists( uninstallDirectory & '/ModuleConfig.cfc' ) ) { + consoleLogger.warn( 'Unloading module...' ); + try { + moduleService.unloadAndUnregisterModule( uninstallDirectory.listLast( '/\' ) ); + // Heavy-handed workaround for the fact that the module service does not unload + // WireBox mappings for this module so they stay in memory + wirebox.getCacheBox().getCache( 'metadataCache' ).clearAll(); + } catch( any e ) { + job.addErrorLog( 'Error Unloading module: ' & e.message & ' ' & e.detail ); + logger.error( '#e.message# #e.detail#' , e.stackTrace ); + } + } + // Catch this to gracefully handle where the OS or another program // has the folder locked. try { @@ -825,9 +837,16 @@ component accessors="true" singleton { var boxJSON = readPackageDescriptor( arguments.currentWorkingDirectory ); // Get reference to appropriate dependency struct - if( arguments.dev ) { + // Save as dev if we have that flag OR if this is already saved as a dev dep + if( arguments.dev || !isNull( boxJSONRaw.devDependencies[ arguments.packageName ] ) ) { boxJSONRaw[ 'devDependencies' ] = boxJSONRaw.devDependencies ?: {}; boxJSON[ 'devDependencies' ] = boxJSON.devDependencies ?: {}; + + // If this package is also saved as a normal dev, remove it from "dependencies" + if( !isNull( boxJSONRaw.dependencies[ arguments.packageName ] ) ) { + boxJSONRaw.dependencies.delete( arguments.packageName ); + } + var dependenciesRaw = boxJSONRaw.devDependencies; var dependencies = boxJSON.devDependencies; } else { @@ -889,7 +908,7 @@ component accessors="true" singleton { arguments.installDirectory = right( arguments.installDirectory, len( arguments.installDirectory ) - 1 ); } } - + var existingInstallPath = ''; if( installPaths.keyExists( arguments.packageName ) ) { existingInstallPath = fileSystemUtil.normalizeSlashes( fileSystemUtil.resolvePath( installPaths[ arguments.packageName ], arguments.currentWorkingDirectory ) ); @@ -901,7 +920,7 @@ component accessors="true" singleton { if( len( existingInstallPath ) && existingInstallPath.left( 1 ) == '/' ) { existingInstallPath = right( existingInstallPath, len( existingInstallPath ) - 1 ); } - } + } } // Just in case-- an empty install dir would be useless. @@ -1142,7 +1161,8 @@ component accessors="true" singleton { 'isOutdated' : updateData.isOutdated, 'isLatest' : !latestData.isOutdated, 'location' : replace( value.directory, directory, "" ) & "/" & slug, - 'endpointName' : endpointData.endpointName + 'endpointName' : endpointData.endpointName, + 'depth' : value.depth }; aAllDependencies.append( dependencyInfo ); @@ -1157,7 +1177,7 @@ component accessors="true" singleton { // Verify outdated dependency graph in parallel structEach( tree.dependencies, fOutdatedCheck, true ); - + return aAllDependencies; } @@ -1176,7 +1196,8 @@ component accessors="true" singleton { 'version': boxJSON.version, 'packageVersion': boxJSON.version, 'isInstalled': true, - 'directory': arguments.directory + 'directory': arguments.directory, + 'depth': 0 }; buildChildren( boxJSON, tree, arguments.directory, depth, 1 ); return tree; @@ -1203,7 +1224,8 @@ component accessors="true" singleton { 'shortDescription' : '', 'packageVersion' : '', 'isInstalled': false, - 'directory': '' + 'directory': '', + 'depth': currentlevel }; if( structKeyExists( arguments.installPaths, dependency ) ) { diff --git a/src/cfml/system/services/ServerEngineService.cfc b/src/cfml/system/services/ServerEngineService.cfc index 84676482f..a80f4327f 100644 --- a/src/cfml/system/services/ServerEngineService.cfc +++ b/src/cfml/system/services/ServerEngineService.cfc @@ -36,6 +36,13 @@ component accessors="true" singleton="true" { var installDetails = installEngineArchive( cfengine, arguments.baseDirectory, serverInfo, serverHomeDirectory ); + if( len( serverInfo.webXMLOverride ) ){ + serverInfo.webXMLOverrideActual = serverInfo.webXML.replace( 'web.xml', 'web-override.xml' ); + fileCopy( serverInfo.webXMLOverride, serverInfo.webXMLOverrideActual ); + } else { + serverInfo.webXMLOverrideActual = ''; + } + if( installDetails.engineName contains "adobe" ) { return installAdobe( installDetails, serverInfo ); } else if ( installDetails.engineName contains "railo" ) { @@ -79,7 +86,7 @@ component accessors="true" singleton="true" { } } - // Users won't be able to create datasources if this file isn't created. + // Users won't be able to create datasources if this file isn't created. // It needs to be different for every install, which is why we're creating it on the fly. var seedPropertiesPath = installDetails.installDir & "/WEB-INF/cfusion/lib/seed.properties"; ensureSeedProperties( seedPropertiesPath ); @@ -93,9 +100,8 @@ component accessors="true" singleton="true" { **/ public function installLucee( installDetails, serverInfo ) { configureWebXML( cfengine="lucee", version=installDetails.version, source=serverInfo.webXML, destination=serverInfo.webXML, serverInfo=serverInfo, installDetails=installDetails ); - if( len( serverInfo.webXMLOverride ) ){ - serverInfo.webXMLOverrideActual = serverInfo.webXML.replace( 'web.xml', 'web-override.xml' ); - configureWebXML( cfengine="lucee", version=installDetails.version, source=serverInfo.webXMLOverride, destination=serverInfo.webXMLOverrideActual, serverInfo=serverInfo, installDetails=installDetails, forceUpdate=true ); + if( len( serverInfo.webXMLOverrideActual ) ){ + configureWebXML( cfengine="lucee", version=installDetails.version, source=serverInfo.webXMLOverrideActual, destination=serverInfo.webXMLOverrideActual, serverInfo=serverInfo, installDetails=installDetails, forceUpdate=true ); } return installDetails; } @@ -106,9 +112,8 @@ component accessors="true" singleton="true" { **/ public function installRailo( installDetails, serverInfo ) { configureWebXML( cfengine="railo", version=installDetails.version, source=serverInfo.webXML, destination=serverInfo.webXML, serverInfo=serverInfo, installDetails=installDetails ); - if( len( serverInfo.webXMLOverride ) ){ - serverInfo.webXMLOverrideActual = serverInfo.webXML.replace( 'web.xml', 'web-override.xml' ); - configureWebXML( cfengine="lucee", version=installDetails.version, source=serverInfo.webXMLOverride, destination=serverInfo.webXMLOverrideActual, serverInfo=serverInfo, installDetails=installDetails, forceUpdate=true ); + if( len( serverInfo.webXMLOverrideActual ) ){ + configureWebXML( cfengine="railo", version=installDetails.version, source=serverInfo.webXMLOverrideActual, destination=serverInfo.webXMLOverrideActual, serverInfo=serverInfo, installDetails=installDetails, forceUpdate=true ); } return installDetails; } @@ -206,7 +211,7 @@ component accessors="true" singleton="true" { if( serverInfo.singleServerHome ) { installDetails.installDir = destination & engineName; } else { - installDetails.installDir = destination & engineName & "-" & replace( satisfyingVersion, '+', '.', 'all' ); + installDetails.installDir = destination & engineName & "-" & replace( satisfyingVersion, '+', '.', 'all' ); } } installDetails.version = satisfyingVersion; @@ -277,7 +282,7 @@ component accessors="true" singleton="true" { unpackLuceeJar( thislib, installDetails.version ); if( !fileExists( thisWebinf & '/web.xml' ) ) { - fileCopy( expandPath( '/commandbox/system/config/web.xml' ), thisWebinf & '/web.xml'); + fileCopy( expandPath( '/commandbox/system/config/web.xml' ), thisWebinf & '/web.xml'); } // Mark this WAR as being exploded already @@ -397,21 +402,27 @@ component accessors="true" singleton="true" { var webXML = XMLParse( source ); var updateMade = false; var package = lcase( cfengine ); - + + // if we're in multi-context mode, we need to deal with more than one web context, so the path MUST be dynamic + // If the user wants the default Lucee behavior, they can set this to "{web-root-directory}/WEB-INF/lucee/" + // If the path doesn't look to already be dynamic, we'll make it so + if( serverInfo.multiContext && not fullWebConfigDir contains '{web-root-directory}' && not fullWebConfigDir contains '{web-context-hash}' ) { + fullWebConfigDir &= '-{web-context-hash}' + } updateMade = ensurePropertServletInitParam( webXML, '#package#.loader.servlet.CFMLServlet', "#package#-web-directory", fullWebConfigDir ); updateMade = ensurePropertServletInitParam( webXML, '#package#.loader.servlet.CFMLServlet', "#package#-server-directory", fullServerConfigDir ) || updateMade; - + // Lucee 5+ has a LuceeServlet as well as will create the WEB-INF by default in your web root - if( arguments.cfengine == 'lucee' && val( listFirst( arguments.version, '.' )) >= 5 ) { + if( arguments.cfengine == 'lucee' && val( listFirst( arguments.version, '.' )) >= 5 ) { updateMade = ensurePropertServletInitParam( webXML, '#package#.loader.servlet.LuceeServlet', "#package#-web-directory", fullWebConfigDir ) || updateMade; updateMade = ensurePropertServletInitParam( webXML, '#package#.loader.servlet.LuceeServlet', "#package#-server-directory", fullServerConfigDir ) || updateMade; } if( updateMade || !fileExists( destination ) || forceUpdate ) { - writeXMLFile( webXML, destination ); + writeXMLFile( webXML, destination ); } return true; } - + /** * Ensure a given servlet has a specific init param value @@ -420,7 +431,7 @@ component accessors="true" singleton="true" { * @servletClass Name of servlet to check * @initParamName Name of init param to ensure exists * @initParamValue Value init param needs to have - * + * * @returns true if changes were made, false if nothing was updated. **/ function ensurePropertServletInitParam( webXML, string servletClass, string initParamName, string initParamValue ) { @@ -431,7 +442,7 @@ component accessors="true" singleton="true" { if( !servlets.len() ) { return false; } - + // If this servlet already has an init-param of this name, ensure the value is correct for( var initParam in servlets[1].XMLParent.XMLChildren.filter( (x)=>x.XMLName=='init-param' ) ) { if( !isNull( initParam[ 'param-name' ].XMLText ) && initParam[ 'param-name' ].XMLText == initParamName ) { @@ -443,7 +454,7 @@ component accessors="true" singleton="true" { } } } - + // if we didn't find a matching init-param above then add it now var initParam = xmlElemnew(webXML,"http://java.sun.com/xml/ns/javaee","init-param"); initParam.XmlChildren[1] = xmlElemnew(webXML,"param-name"); diff --git a/src/cfml/system/services/ServerService.cfc b/src/cfml/system/services/ServerService.cfc index 6a9bd8114..3bfad0d56 100644 --- a/src/cfml/system/services/ServerService.cfc +++ b/src/cfml/system/services/ServerService.cfc @@ -3,7 +3,7 @@ * Copyright Since 2014 CommandBox by Ortus Solutions, Corp * www.coldbox.org | www.ortussolutions.com ******************************************************************************** -* @author Brad Wood, Luis Majano, Denny Valliant +* @author Brad Wood, Luis Majano, Denny Valliant, Scott Steinbeck * * I manage servers * @@ -38,6 +38,7 @@ component accessors="true" singleton { property name='packageService' inject='packageService'; property name='serverEngineService' inject='serverEngineService'; property name='consoleLogger' inject='logbox:logger:console'; + property name='rootLogger' inject='logbox:root'; property name='wirebox' inject='wirebox'; property name='CR' inject='CR@constants'; property name='parser' inject='parser'; @@ -128,7 +129,9 @@ component accessors="true" singleton { 'trayicon' : d.trayicon ?: '', // Duplicate so onServerStart interceptors don't actually change config settings via reference. 'trayOptions' : duplicate( d.trayOptions ?: [] ), - 'trayEnable' : d.trayEnable ?: true, + // Only default this on for Windows-- off for Linux and Mac due to crap unfixed bugs in the + // upstream Java library. https://github.com/dorkbox/SystemTray/issues/119 + 'trayEnable' : d.trayEnable ?: fileSystemUtil.isWindows(), 'dockEnable' : d.dockEnable ?: true, 'profile' : d.profile ?: '', 'jvm' : { @@ -136,7 +139,8 @@ component accessors="true" singleton { 'minHeapSize' : d.jvm.minHeapSize ?: '', 'args' : d.jvm.args ?: '', 'javaHome' : d.jvm.javaHome ?: '', - 'javaVersion' : d.jvm.javaVersion ?: '' + 'javaVersion' : d.jvm.javaVersion ?: '', + 'properties' : d.jvm.properties ?: {} }, 'web' : { 'host' : d.web.host ?: '127.0.0.1', @@ -187,12 +191,18 @@ component accessors="true" singleton { 'enable' : d.web.basicAuth.enable ?: true, 'users' : d.web.basicAuth.users ?: {} }, + 'fileCache' : { + 'enable' : d.web.fileCache.enable ?: '', + 'totalSizeMB' : d.web.fileCache.totalSizeMB ?: 50, + 'maxFileSizeKB' : d.web.fileCache.maxFileSizeKB ?: 50 + }, 'rules' : duplicate( d.web.rules ?: [] ), 'rulesFile' : duplicate( d.web.rulesFile ?: [] ), 'blockCFAdmin' : d.web.blockCFAdmin ?: '', 'blockSensitivePaths' : d.web.blockSensitivePaths ?: '', 'blockFlashRemoting' : d.web.blockFlashRemoting ?: '', - 'allowedExt' : d.web.allowedExt ?: '' + 'allowedExt' : d.web.allowedExt ?: '', + 'useProxyForwardedIP' : d.web.useProxyForwardedIP ?: false }, 'app' : { 'logDir' : d.app.logDir ?: '', @@ -217,7 +227,15 @@ component accessors="true" singleton { 'XNIOOptions' : duplicate( d.runwar.XNIOOptions ?: {} ), // Duplicate so onServerStart interceptors don't actually change config settings via reference. 'undertowOptions' : duplicate( d.runwar.undertowOptions ?: {} ) - } + }, + 'ModCFML' : { + 'enable' : d.ModCFML.enable ?: false, + 'maxContexts' : d.ModCFML.maxContexts ?: 200, + 'sharedKey' : d.ModCFML.sharedKey ?: '', + 'requireSharedKey' : d.ModCFML.requireSharedKey ?: true, + 'createVirtualDirectories' : d.ModCFML.createVirtualDirectories ?: true + }, + 'scripts' : d.scripts ?: {} }; } @@ -280,22 +298,32 @@ component accessors="true" singleton { // Look up the server that we're starting var serverDetails = resolveServerDetails( arguments.serverProps ); - // This will allow settings in the "env" object to also refernce env vars which are already set - systemSettings.expandDeepSystemSettings( serverDetails.serverJSON.env ?: {} ); - // Load up our fully-realized server.json-specific env vars into CommandBox's environment - systemSettings.setDeepSystemSettings( serverDetails.serverJSON.env ?: {}, '' ); // Get defaults var defaults = getDefaultServerJSON(); - - interceptorService.announceInterception( 'preServerStart', { serverDetails=serverDetails, serverProps=serverProps, serverInfo=serverDetails.serverInfo, serverJSON=serverDetails.serverJSON, defaults=defaults } ); - var defaultName = serverDetails.defaultName; var defaultwebroot = serverDetails.defaultwebroot; var defaultServerConfigFile = serverDetails.defaultServerConfigFile; var defaultServerConfigFileDirectory = getDirectoryFromPath( defaultServerConfigFile ); var serverJSON = serverDetails.serverJSON; + var serverJSONToSave = duplicate( serverJSON ); var serverInfo = serverDetails.serverinfo; + systemSettings.expandDeepSystemSettings( serverJSON ); + systemSettings.expandDeepSystemSettings( defaults ); + // Mix in environment variable overrides like BOX_SERVER_PROFILE + loadOverrides( serverJSON, serverInfo, serverProps.verbose ?: serverJSON.verbose ?: defaults.verbose ?: false ); + + // Load up our fully-realized server.json-specific env vars into CommandBox's environment + systemSettings.setDeepSystemSettings( serverDetails.serverJSON.env ?: {}, '', '_' ); + + interceptorService.announceInterception( 'preServerStart', { serverDetails=serverDetails, serverProps=serverProps, serverInfo=serverDetails.serverInfo, serverJSON=serverDetails.serverJSON, defaults=defaults } ); + + // In case the interceptor changed them + defaultName = serverDetails.defaultName; + defaultwebroot = serverDetails.defaultwebroot; + defaultServerConfigFile = serverDetails.defaultServerConfigFile; + defaultServerConfigFileDirectory = getDirectoryFromPath( defaultServerConfigFile ); + // If the server is already running, make sure the user really wants to do this. if( isServerRunning( serverInfo ) && !(serverProps.force ?: false ) && !(serverProps.dryRun ?: false ) ) { job.addErrorLog( 'Server "#serverInfo.name#" (#serverInfo.webroot#) is already running @ #serverInfo.openbrowserURL#!' ); @@ -359,10 +387,10 @@ component accessors="true" singleton { // Only need switch cases for properties that are nested or use different name switch(prop) { case "port": - serverJSON[ 'web' ][ 'http' ][ 'port' ] = serverProps[ prop ]; + serverJSONToSave[ 'web' ][ 'http' ][ 'port' ] = serverProps[ prop ]; break; case "host": - serverJSON[ 'web' ][ 'host' ] = serverProps[ prop ]; + serverJSONToSave[ 'web' ][ 'host' ] = serverProps[ prop ]; break; case "directory": // This path is canonical already. @@ -371,10 +399,10 @@ component accessors="true" singleton { if( thisDirectory contains configPath ) { thisDirectory = replaceNoCase( thisDirectory, configPath, '' ); } - serverJSON[ 'web' ][ 'webroot' ] = thisDirectory; + serverJSONToSave[ 'web' ][ 'webroot' ] = thisDirectory; break; case "trayEnable": - serverJSON[ 'trayEnable' ] = serverProps[ prop ]; + serverJSONToSave[ 'trayEnable' ] = serverProps[ prop ]; break; case "trayIcon": // This path is canonical already. @@ -383,10 +411,10 @@ component accessors="true" singleton { if( thisFile contains configPath ) { thisFile = replaceNoCase( thisFile, configPath, '' ); } - serverJSON[ 'trayIcon' ] = thisFile; + serverJSONToSave[ 'trayIcon' ] = thisFile; break; case "stopPort": - serverJSON[ 'stopsocket' ] = serverProps[ prop ]; + serverJSONToSave[ 'stopsocket' ] = serverProps[ prop ]; break; case "webConfigDir": // This path is canonical already. @@ -395,7 +423,7 @@ component accessors="true" singleton { if( thisDirectory contains configPath ) { thisDirectory = replaceNoCase( thisDirectory, configPath, '' ); } - serverJSON[ 'app' ][ 'webConfigDir' ] = thisDirectory; + serverJSONToSave[ 'app' ][ 'webConfigDir' ] = thisDirectory; break; case "serverConfigDir": // This path is canonical already. @@ -404,10 +432,10 @@ component accessors="true" singleton { if( thisDirectory contains configPath ) { thisDirectory = replaceNoCase( thisDirectory, configPath, '' ); } - serverJSON[ 'app' ][ 'serverConfigDir' ] = thisDirectory; + serverJSONToSave[ 'app' ][ 'serverConfigDir' ] = thisDirectory; break; case "libDirs": - serverJSON[ 'app' ][ 'libDirs' ] = serverProps[ 'libDirs' ] + serverJSONToSave[ 'app' ][ 'libDirs' ] = serverProps[ 'libDirs' ] .listMap( function( thisLibDir ) { // This path is canonical already. var thisLibDir = replace( thisLibDir, '\', '/', 'all' ); @@ -421,10 +449,10 @@ component accessors="true" singleton { break; case "cfengine": - serverJSON[ 'app' ][ 'cfengine' ] = serverProps[ prop ]; + serverJSONToSave[ 'app' ][ 'cfengine' ] = serverProps[ prop ]; break; case "restMappings": - serverJSON[ 'app' ][ 'restMappings' ] = serverProps[ prop ]; + serverJSONToSave[ 'app' ][ 'restMappings' ] = serverProps[ prop ]; break; case "WARPath": // This path is canonical already. @@ -433,7 +461,7 @@ component accessors="true" singleton { if( thisFile contains configPath ) { thisFile = replaceNoCase( thisFile, configPath, '' ); } - serverJSON[ 'app' ][ 'WARPath' ] = thisFile; + serverJSONToSave[ 'app' ][ 'WARPath' ] = thisFile; break; case "serverHomeDirectory": // This path is canonical already. @@ -442,37 +470,37 @@ component accessors="true" singleton { if( thisDirectory contains configPath ) { thisDirectory = replaceNoCase( thisDirectory, configPath, '' ); } - serverJSON[ 'app' ][ 'serverHomeDirectory' ] = thisDirectory; + serverJSONToSave[ 'app' ][ 'serverHomeDirectory' ] = thisDirectory; break; case "HTTPEnable": - serverJSON[ 'web' ][ 'HTTP' ][ 'enable' ] = serverProps[ prop ]; + serverJSONToSave[ 'web' ][ 'HTTP' ][ 'enable' ] = serverProps[ prop ]; break; case "SSLEnable": - serverJSON[ 'web' ][ 'SSL' ][ 'enable' ] = serverProps[ prop ]; + serverJSONToSave[ 'web' ][ 'SSL' ][ 'enable' ] = serverProps[ prop ]; break; case "SSLPort": - serverJSON[ 'web' ][ 'SSL' ][ 'port' ] = serverProps[ prop ]; + serverJSONToSave[ 'web' ][ 'SSL' ][ 'port' ] = serverProps[ prop ]; break; case "AJPEnable": - serverJSON[ 'web' ][ 'AJP' ][ 'enable' ] = serverProps[ prop ]; + serverJSONToSave[ 'web' ][ 'AJP' ][ 'enable' ] = serverProps[ prop ]; break; case "AJPPort": - serverJSON[ 'web' ][ 'AJP' ][ 'port' ] = serverProps[ prop ]; + serverJSONToSave[ 'web' ][ 'AJP' ][ 'port' ] = serverProps[ prop ]; break; case "SSLCertFile": - serverJSON[ 'web' ][ 'SSL' ][ 'certFile' ] = serverProps[ prop ]; + serverJSONToSave[ 'web' ][ 'SSL' ][ 'certFile' ] = serverProps[ prop ]; break; case "SSLKeyFile": - serverJSON[ 'web' ][ 'SSL' ][ 'keyFile' ] = serverProps[ prop ]; + serverJSONToSave[ 'web' ][ 'SSL' ][ 'keyFile' ] = serverProps[ prop ]; break; case "SSLKeyPass": - serverJSON[ 'web' ][ 'SSL' ][ 'keyPass' ] = serverProps[ prop ]; + serverJSONToSave[ 'web' ][ 'SSL' ][ 'keyPass' ] = serverProps[ prop ]; break; case "welcomeFiles": - serverJSON[ 'web' ][ 'welcomeFiles' ] = serverProps[ prop ]; + serverJSONToSave[ 'web' ][ 'welcomeFiles' ] = serverProps[ prop ]; break; case "rewritesEnable": - serverJSON[ 'web' ][ 'rewrites' ][ 'enable' ] = serverProps[ prop ]; + serverJSONToSave[ 'web' ][ 'rewrites' ][ 'enable' ] = serverProps[ prop ]; break; case "rewritesConfig": // This path is canonical already. @@ -481,46 +509,41 @@ component accessors="true" singleton { if( thisFile contains configPath ) { thisFile = replaceNoCase( thisFile, configPath, '' ); } - serverJSON[ 'web' ][ 'rewrites' ][ 'config' ] = thisFile; + serverJSONToSave[ 'web' ][ 'rewrites' ][ 'config' ] = thisFile; break; case "blockCFAdmin": - serverJSON[ 'web' ][ 'blockCFAdmin' ] = serverProps[ prop ]; + serverJSONToSave[ 'web' ][ 'blockCFAdmin' ] = serverProps[ prop ]; break; case "heapSize": - serverJSON[ 'JVM' ][ 'heapSize' ] = serverProps[ prop ]; + serverJSONToSave[ 'JVM' ][ 'heapSize' ] = serverProps[ prop ]; break; case "minHeapSize": - serverJSON[ 'JVM' ][ 'minHeapSize' ] = serverProps[ prop ]; + serverJSONToSave[ 'JVM' ][ 'minHeapSize' ] = serverProps[ prop ]; break; case "JVMArgs": - serverJSON[ 'JVM' ][ 'args' ] = serverProps[ prop ]; + serverJSONToSave[ 'JVM' ][ 'args' ] = serverProps[ prop ]; break; case "javaHomeDirectory": - serverJSON[ 'JVM' ][ 'javaHome' ] = serverProps[ prop ]; + serverJSONToSave[ 'JVM' ][ 'javaHome' ] = serverProps[ prop ]; break; case "javaVersion": - serverJSON[ 'JVM' ][ 'javaVersion' ] = serverProps[ prop ]; + serverJSONToSave[ 'JVM' ][ 'javaVersion' ] = serverProps[ prop ]; break; case "runwarJarPath": - serverJSON[ 'runwar' ][ 'jarPath' ] = serverProps[ prop ]; + serverJSONToSave[ 'runwar' ][ 'jarPath' ] = serverProps[ prop ]; break; case "runwarArgs": - serverJSON[ 'runwar' ][ 'args' ] = serverProps[ prop ]; + serverJSONToSave[ 'runwar' ][ 'args' ] = serverProps[ prop ]; break; default: - serverJSON[ prop ] = serverProps[ prop ]; + serverJSONToSave[ prop ] = serverProps[ prop ]; } // end switch } // for loop - if( !serverJSON.isEmpty() && serverProps.saveSettings ) { - saveServerJSON( defaultServerConfigFile, serverJSON ); + if( !serverJSONToSave.isEmpty() && serverProps.saveSettings ) { + saveServerJSON( defaultServerConfigFile, serverJSONToSave ); } - systemSettings.expandDeepSystemSettings( serverJSON ); - systemSettings.expandDeepSystemSettings( defaults ); - // Mix in environment variable overrides like BOX_SERVER_PROFILE - loadOverrides( serverJSON, serverInfo ); - // These are already hammered out above, so no need to go through all the defaults. serverInfo.serverConfigFile = defaultServerConfigFile; serverInfo.name = defaultName; @@ -585,9 +608,9 @@ component accessors="true" singleton { if( !isNull( serverJSON.profile ) ) { if( serverInfo.envVarHasProfile ?: false ) { - profileReason = 'profile property in in "box_server_profile" env var'; + profileReason = 'profile property in "box_server_profile" env var'; } else { - profileReason = 'profile property in server.json'; + profileReason = 'profile property in server.json'; } } if( !isNull( serverProps.profile ) ) { @@ -619,10 +642,12 @@ component accessors="true" singleton { } } - serverInfo.blockCFAdmin = serverProps.blockCFAdmin ?: serverJSON.web.blockCFAdmin ?: defaults.web.blockCFAdmin; + serverInfo.blockCFAdmin = serverProps.blockCFAdmin ?: serverJSON.web.blockCFAdmin ?: defaults.web.blockCFAdmin; serverInfo.blockSensitivePaths = serverJSON.web.blockSensitivePaths ?: defaults.web.blockSensitivePaths; serverInfo.blockFlashRemoting = serverJSON.web.blockFlashRemoting ?: defaults.web.blockFlashRemoting; - serverInfo.allowedExt = serverJSON.web.allowedExt ?: defaults.web.allowedExt; + serverInfo.allowedExt = serverJSON.web.allowedExt ?: defaults.web.allowedExt; + serverInfo.useProxyForwardedIP = serverJSON.web.useProxyForwardedIP ?: defaults.web.useProxyForwardedIP; + // If there isn't a default for this already if( !isBoolean( defaults.web.directoryBrowsing ) ) { @@ -636,6 +661,19 @@ component accessors="true" singleton { } serverInfo.directoryBrowsing = serverProps.directoryBrowsing ?: serverJSON.web.directoryBrowsing ?: defaults.web.directoryBrowsing; + // If there isn't a default for this already + if( !isBoolean( defaults.web.fileCache.enable ) ) { + if( serverInfo.profile == 'production' ) { + defaults.web.fileCache.enable = true; + } else { + defaults.web.fileCache.enable = false; + } + } + + serverInfo.fileCacheEnable = serverJSON.web.fileCache.enable ?: defaults.web.fileCache.enable; + serverInfo.fileCacheTotalSizeMB = serverJSON.web.fileCache.totalSizeMB ?: defaults.web.fileCache.totalSizeMB; + serverInfo.fileCacheMaxFileSizeKB = serverJSON.web.fileCache.maxFileSizeKB ?: defaults.web.fileCache.maxFileSizeKB; + job.start( 'Setting Server Profile to [#serverInfo.profile#]' ); job.addLog( 'Profile set from #profileReason#' ); if( serverInfo.blockCFAdmin == 'external' ) { @@ -651,6 +689,7 @@ component accessors="true" singleton { job.addLog( 'Allowed Extensions: [#serverInfo.allowedExt#]' ); } job[ 'add#( !serverInfo.directoryBrowsing ? 'Success' : 'Error' )#Log' ]( 'Directory Browsing #( serverInfo.directoryBrowsing ? 'en' : 'dis' )#abled' ); + job[ 'add#( serverInfo.fileCacheEnable ? 'Success' : '' )#Log' ]( 'File Caching #( serverInfo.fileCacheEnable ? 'en' : 'dis' )#abled' ); job.complete( serverInfo.verbose ); // Double check that the port in the user params or server.json isn't in use @@ -797,7 +836,7 @@ component accessors="true" singleton { if( directoryExists( possiblePath ) ) { return possiblePath; } - return fileSystemUtil.resolvePath( p, defaultServerConfigFileDirectory ); + return fileSystemUtil.resolvePath( p, defaultServerConfigFileDirectory ); } ) ); // Global errorPages are always added on top of server.json (but don't overwrite the full struct) @@ -812,13 +851,37 @@ component accessors="true" singleton { serverInfo.rewriteslogEnable = serverJSON.web.rewrites.logEnable ?: defaults.web.rewrites.logEnable; // Global defaults are always added on top of whatever is specified by the user or server.json - serverInfo.JVMargs = ( serverProps.JVMargs ?: serverJSON.JVM.args ?: '' ) & ' ' & defaults.JVM.args; + serverInfo.JVMargsArray = []; + serverInfo.JVMargs = serverProps.JVMargs ?: ''; + if( !isNull( serverJSON.JVM.args ) && isArray( serverJSON.JVM.args ) ) { + serverInfo.JVMargsArray.append( serverJSON.JVM.args, true ); + } else if( !isNull( serverJSON.JVM.args ) && isSimpleValue( serverJSON.JVM.args ) && len( serverJSON.JVM.args ) ) { + serverInfo.JVMargs &= ' ' & serverJSON.JVM.args; + } + if( !isNull( defaults.JVM.args ) && isArray( defaults.JVM.args ) ) { + serverInfo.JVMargsArray.append( defaults.JVM.args, true ); + } else if( !isNull( defaults.JVM.args ) && isSimpleValue( defaults.JVM.args ) && len( defaults.JVM.args ) ) { + serverInfo.JVMargs &= ' ' & defaults.JVM.args; + } + // Global defaults are always added on top of whatever is specified by the user or server.json serverInfo.runwarJarPath = serverProps.runwarJarPath ?: serverJSON.runwar.jarPath ?: defaults.runwar.jarPath; // Global defaults 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; + serverInfo.runwarArgsArray = []; + serverInfo.runwarArgs = serverProps.runwarArgs ?: ''; + if( !isNull( serverJSON.runwar.args ) && isArray( serverJSON.runwar.args ) ) { + serverInfo.runwarArgsArray.append( serverJSON.runwar.args, true ); + } else if( !isNull( serverJSON.runwar.args ) && isSimpleValue( serverJSON.runwar.args ) && len( serverJSON.runwar.args ) ) { + serverInfo.runwarArgs &= ' ' & serverJSON.runwar.args; + } + if( !isNull( defaults.runwar.args ) && isArray( defaults.runwar.args ) ) { + serverInfo.runwarArgsArray.append( defaults.runwar.args, true ); + } else if( !isNull( defaults.runwar.args ) && isSimpleValue( defaults.runwar.args ) && len( defaults.runwar.args ) ) { + serverInfo.runwarArgs &= ' ' & defaults.runwar.args; + } + // Global defaults are always added on top of whatever is specified by the user or server.json serverInfo.runwarXNIOOptions = ( serverJSON.runwar.XNIOOptions ?: {} ).append( defaults.runwar.XNIOOptions, true ); @@ -829,6 +892,9 @@ component accessors="true" singleton { // Server startup timeout serverInfo.startTimeout = serverProps.startTimeout ?: serverJSON.startTimeout ?: defaults.startTimeout; + serverInfo.JVMProperties = serverJSON.JVM.properties ?: {}; + serverInfo.JVMProperties.append( defaults.jvm.properties, false ); + // 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 ){ @@ -879,7 +945,7 @@ component accessors="true" singleton { if( serverJSON.keyExists( 'web' ) && serverJSON.web.keyExists( 'rules' ) ) { if( !isArray( serverJSON.web.rules ) ) { - throw( message="'rules' key in your box.json must be an array of strings.", type="commandException" ); + throw( message="'rules' key in your server.json must be an array of strings.", type="commandException" ); } serverInfo.webRules.append( serverJSON.web.rules, true); } @@ -925,9 +991,9 @@ component accessors="true" singleton { // track and trace verbs can leak data in XSS attacks "disallowed-methods( methods={trace,track} )", // Common config files and sensitive paths that should never be accessed, even on development - "regex( pattern='.*/(box.json|server.json|web.config|urlrewrite.xml|package.json|package-lock.json|Gulpfile.js)', case-sensitive=false ) -> { set-error(404); done }", - // Any file or folder starting with a period - "regex('/\.') -> { set-error( 404 ); done }", + "regex( pattern='.*/(box\.json|server\.json|web\.config|urlrewrite\.xml|package\.json|package-lock\.json|Gulpfile\.js)', case-sensitive=false ) -> { set-error(404); done }", + // Any file or folder starting with a period, unless it's called + "regex('/\.') and not path-prefix(.well-known) -> { set-error( 404 ); done }", // Additional serlvlet mappings in Adobe CF's web.xml "path-prefix( { '/JSDebugServlet','/securityanalyzer','/WSRPProducer' } ) -> { set-error( 404 ); done }", // java web service (Axis) files @@ -937,7 +1003,7 @@ component accessors="true" singleton { if( serverInfo.profile == 'production' ) { serverInfo.webRules.append( [ // Common config files and sensitive paths in ACF and TestBox that may be ok for dev, but not for production - "regex( pattern='.*/(CFIDE/multiservermonitor-access-policy.xml|CFIDE/probe.cfm|CFIDE/main/ide.cfm|tests/runner.cfm|testbox/system/runners/HTMLRunner.cfm)', case-sensitive=false ) -> { set-error(404); done }", + "regex( pattern='.*/(CFIDE/multiservermonitor-access-policy\.xml|CFIDE/probe\.cfm|CFIDE/main/ide\.cfm|tests/runner\.cfm|testbox/system/runners/HTMLRunner\.cfm)', case-sensitive=false ) -> { set-error(404); done }", ], true ); } @@ -975,7 +1041,7 @@ component accessors="true" singleton { 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; - serverInfo.singleServerHome = serverJSON.app.singleServerHome ?: defaults.app.singleServerHome; + serverInfo.singleServerHome = serverJSON.app.singleServerHome ?: defaults.app.singleServerHome; if( len( serverJSON.app.webXMLOverride ?: '' ) ){ serverJSON.app.webXMLOverride = fileSystemUtil.resolvePath( serverJSON.app.webXMLOverride, defaultServerConfigFileDirectory ); } if( len( defaults.app.webXMLOverride ?: '' ) ){ defaults.app.webXMLOverride = fileSystemUtil.resolvePath( defaults.app.webXMLOverride, defaultwebroot ); } @@ -986,9 +1052,18 @@ component accessors="true" singleton { } serverInfo.webXMLOverrideForce = serverJSON.app.webXMLOverrideForce ?: defaults.app.webXMLOverrideForce; - + serverInfo.sessionCookieSecure = serverJSON.app.sessionCookieSecure ?: defaults.app.sessionCookieSecure; - serverInfo.sessionCookieHTTPOnly = serverJSON.app.sessionCookieHTTPOnly ?: defaults.app.sessionCookieHTTPOnly; + serverInfo.sessionCookieHTTPOnly = serverJSON.app.sessionCookieHTTPOnly ?: defaults.app.sessionCookieHTTPOnly; + + serverInfo.ModCFMLenable = serverJSON.ModCFML.enable ?: defaults.ModCFML.enable; + serverInfo.ModCFMLMaxContexts = serverJSON.ModCFML.maxContexts ?: defaults.ModCFML.maxContexts; + serverInfo.ModCFMLSharedKey = serverJSON.ModCFML.sharedKey ?: defaults.ModCFML.sharedKey; + serverInfo.ModCFMLRequireSharedKey = serverJSON.ModCFML.requireSharedKey ?: defaults.ModCFML.requireSharedKey; + serverInfo.ModCFMLcreateVDirs = serverJSON.ModCFML.createVirtualDirectories ?: defaults.ModCFML.createVirtualDirectories; + + // When we add native support for multiple contexts in the server.json, that will also set this to true + serverInfo.multiContext = serverInfo.ModCFMLenable; if( serverInfo.verbose ) { job.addLog( "start server in - " & serverInfo.webroot ); @@ -1125,7 +1200,7 @@ component accessors="true" singleton { serverInfo.consolelogPath = serverInfo.logdir & '/server.out.txt'; serverInfo.accessLogPath = serverInfo.logDir & '/access.txt'; serverInfo.rewritesLogPath = serverInfo.logDir & '/rewrites.txt'; - + // Find the correct tray icon for this server if( !len( serverInfo.trayIcon ) ) { @@ -1277,6 +1352,7 @@ component accessors="true" singleton { return parser.replaceEscapedChars( parser.removeEscapedChars( parser.unwrapQuotes( i ) ) ); }); + argTokens.append( serverInfo.JVMargsArray, true ); // Add in max heap size if( len( serverInfo.heapSize ) ) { @@ -1292,9 +1368,18 @@ component accessors="true" singleton { argTokens.append( '-Xms#serverInfo.minHeapSize#' ); } + serverInfo.JVMProperties.each( (k,v)=>argTokens.append( '-D#k#=#v#' ) ); + // Add java agent if( len( trim( javaAgent ) ) ) { argTokens.append( javaagent ); } + // TODOL Temp stopgap for Java regression that prevents Undertow from starting. + // https://issues.redhat.com/browse/UNDERTOW-2073 + // https://bugs.openjdk.java.net/browse/JDK-8285445 + if( !argTokens.filter( (a)=>a contains 'jdk.io.File.enableADS' ).len() ) { + argTokens.append( '-Djdk.io.File.enableADS=true' ); + } + args .append( '-jar' ).append( serverInfo.runwarJarPath ) .append( '--background=#background#' ) @@ -1307,7 +1392,7 @@ component accessors="true" singleton { .append( '--dock-enable' ).append( serverInfo.dockEnable ) .append( '--directoryindex' ).append( serverInfo.directoryBrowsing ) .append( '--timeout' ).append( serverInfo.startTimeout ) - .append( '--proxy-peeraddress' ).append( 'true' ) + .append( '--proxy-peeraddress' ).append( serverInfo.useProxyForwardedIP ) .append( '--cookie-secure' ).append( serverInfo.sessionCookieSecure ) .append( '--cookie-httponly' ).append( serverInfo.sessionCookieHTTPOnly ) .append( '--pid-file').append( serverInfo.pidfile ); @@ -1316,12 +1401,18 @@ component accessors="true" singleton { args.append( '--preferred-browser' ).append( ConfigService.getSetting( 'preferredBrowser' ) ); } - args.append( serverInfo.runwarArgs.listToArray( ' ' ), true ); + args.append( parser.tokenizeInput( serverInfo.runwarArgs.replace( ';', '\;', 'all' ) ) + .map( function( i ){ + // unwrap quotes, and unescape any special chars like \" inside the string + return parser.replaceEscapedChars( parser.removeEscapedChars( parser.unwrapQuotes( i ) ) ); + }), true ); + + args.append( serverInfo.runwarArgsArray, true ) + // Despite the name, the MacOS Dock also uses this setting. + .append( '--tray-icon' ).append( serverInfo.trayIcon ); if( serverInfo.trayEnable ) { - args - .append( '--tray-icon' ).append( serverInfo.trayIcon ) - .append( '--tray-config' ).append( serverInfo.trayOptionsFile ) + args.append( '--tray-config' ).append( serverInfo.trayOptionsFile ); } if( serverInfo.runwarXNIOOptions.count() ) { @@ -1390,7 +1481,11 @@ component accessors="true" singleton { if( len( CLIAliases ) ) { args.append( '--dirs' ).append( CLIAliases ); } - + if( serverInfo.fileCacheEnable ) { + args.append( '--cache-servlet-paths' ).append( true ); + args.append( '--file-cache-total-size-mb' ).append( val( serverInfo.fileCacheTotalSizeMB ) ); + args.append( '--file-cache-max-file-size-kb' ).append( val( serverInfo.fileCacheMaxFileSizeKB ) ); + } // If background, wrap up JVM args to pass through to background servers. "Real" JVM args must come before Runwar args if( background ) { @@ -1404,7 +1499,7 @@ component accessors="true" singleton { } // If foreground, just stick them in. } else { - argTokens.each( function(i) { args.prepend( i ); } ); + argTokens.reverse().each( function(i) { args.prepend( i ); } ); } // Webroot for normal server, and war home for a standard war @@ -1502,6 +1597,22 @@ component accessors="true" singleton { args.append( '--predicate-file' ).append( serverInfo.predicateFile ); } + if( serverInfo.ModCFMLenable ){ + args.append( '--auto-create-contexts' ).append( serverInfo.ModCFMLenable ); + if( len( serverInfo.ModCFMLMaxContexts ) && isNumeric( serverInfo.ModCFMLMaxContexts ) && serverInfo.ModCFMLMaxContexts > 0 ) { + args.append( '--auto-create-contexts-max' ).append( serverInfo.ModCFMLMaxContexts ); + } + if( !len( serverInfo.ModCFMLSharedKey ) && serverInfo.ModCFMLRequireSharedKey ) { + throw( message='Since ModeCFML support is enabled, [ModCFML.sharedKey] is required for security.', detail='Disable IN DEVELOPMENT ONLY with [ModCFML.RequireSharedKey=false].', type="commandException" ); + } + if( len( serverInfo.ModCFMLSharedKey ) ) { + args.append( '--auto-create-contexts-secret' ).append( serverInfo.ModCFMLSharedKey ); + } + if( serverInfo.ModCFMLcreateVDirs ) { + args.append( '--auto-create-contexts-vdirs' ).append( serverInfo.ModCFMLcreateVDirs ); + } + } + // change status to starting + persist serverInfo.dateLastStarted = now(); serverInfo.status = "starting"; @@ -1568,6 +1679,17 @@ component accessors="true" singleton { } } + // Add COMMANDBOX_HOME env var to the server if not already there + if ( !currentEnv.containsKey( 'COMMANDBOX_HOME' ) ) { + currentEnv.put( 'COMMANDBOX_HOME', expandPath( '/commandbox-home' ) ); + } + + // Add COMMANDBOX_VERSION env var to the server if not already there + if ( !currentEnv.containsKey( 'COMMANDBOX_VERSION' ) ) { + currentEnv.put( 'COMMANDBOX_VERSION', shell.getVersion() ); + } + + // Conjoin standard error and output for convenience. processBuilder.redirectErrorStream( true ); // Kick off actual process @@ -2071,20 +2193,20 @@ component accessors="true" singleton { try{ // Try to stop and set status back - + var processBuilder = createObject( "java", "java.lang.ProcessBuilder" ); processBuilder.init( args ); processBuilder.redirectErrorStream( true ); var process = processBuilder.start(); var inputStream = process.getInputStream(); var exitCode = process.waitFor(); - + var processOutput = toString( inputStream ); - + if( exitCode > 0 ) { throw( message='Error stopping server', detail=processOutput ); } - + //execute name=variables.javaCommand arguments=args timeout="50" variable="results.messages" errorVariable="errorVar"; serverInfo.status = "stopped"; serverInfo.statusInfo = { @@ -2108,7 +2230,7 @@ component accessors="true" singleton { if( !isNull( process ) ) { process.destroy(); } - } + } } /** @@ -2227,7 +2349,7 @@ component accessors="true" singleton { return java.InetAddress.getByName( arguments.host ); } catch( java.net.UnknownHostException var e ) { // It's possible to have "fake" hosts such as mytest.localhost which aren't in DNS - // or your hosts file. Browsers will resolve them to localhost, but the call above + // or your hosts file. Browsers will resolve them to localhost, but the call above // will fail with a UnknownHostException since they aren't real if( host.listLast( '.' ) == 'localhost' ) { return java.InetAddress.getByName( '127.0.0.1' ); @@ -2236,24 +2358,42 @@ component accessors="true" singleton { } } + /** + * Find out if a given Process ID (PID) is a running java service + * @pidStr.hint PID to test on + **/ + function isProcessAlive( required pidStr, throwOnError=false ) { + var result = ""; + var timeStart = millisecond(now()); + try{ + if (fileSystemUtil.isWindows() ) { + cfexecute(name='cmd', arguments='/c tasklist /FI "PID eq #pidStr#"', variable="result" timeout="10"); + } else if (fileSystemUtil.isMac() || fileSystemUtil.isLinux() ) { + cfexecute(name='ps', arguments='-p #pidStr#', variable="result" , timeout="10"); + } + if (findNoCase("java", result) > 0 && findNoCase(pidStr, result) > 0) return true; + } catch ( any e ){ + if( throwOnError ) { + rethrow; + } + rootLogger.error( 'Error checking if server PID was running: ' & e.message & ' ' & e.detail ); + } + return false; + } /** * Logic to tell if a server is running * @serverInfo.hint Struct of server information **/ function isServerRunning( required struct serverInfo ){ - var portToCheck = serverInfo.stopSocket; - if( serverInfo.HTTPEnable ) { - portToCheck = serverInfo.port; - } else if( serverInfo.SSLEnable ) { - portToCheck = serverInfo.SSLPort; - } else if( serverInfo.AJPEnable ) { - portToCheck = serverInfo.AJPPort; - } - - lock name="server-status-check-#portToCheck#" type="exclusive"{ - return !isPortAvailable( serverInfo.host, portToCheck ); + if(fileExists(serverInfo.pidFile)){ + var serverPID = fileRead(serverInfo.pidFile); + thread action="run" name="check_#serverPID##getTickCount()#" serverPID=serverPID pidFile=serverInfo.pidFile { + if(!isProcessAlive(attributes.serverPID,true)) fileDelete(attributes.pidFile) + } + return true; } + return false; } /** @@ -2444,7 +2584,7 @@ component accessors="true" singleton { return getServers() .valueArray() .map( (s)=>s.name ) - .sort( 'textNoCase' ); + .sort( 'textNoCase' ); } /** @@ -2561,7 +2701,9 @@ component accessors="true" singleton { 'javaVersion' : '', 'directoryBrowsing' : false, 'JVMargs' : "", + 'JVMargsArray' : [], 'runwarArgs' : "", + 'runwarArgsArray' : [], 'runwarXNIOOptions' : {}, 'runwarUndertowOptions' : {}, 'cfengine' : "", @@ -2654,6 +2796,17 @@ component accessors="true" singleton { } } } ); + // Suggest server scripts + props = JSONService.addProp( props, '', '', { + 'scripts' : { + 'preServerStart' : '', + 'onServerInstall' : '', + 'onServerStart' : '', + 'onServerStop' : '', + 'preServerForget' : '', + 'postServerForget' : '' + } + } ); } if( asSet ) { props = props.map( function( i ){ return i &= '='; } ); @@ -2666,7 +2819,7 @@ component accessors="true" singleton { * Dynamic completion for server names, sorted by last started */ function serverNameComplete() { - + return getservers() .valueArray() .sort( (a,b)=>{ @@ -2678,49 +2831,138 @@ component accessors="true" singleton { } ) .map( (s,i)=>return { name : s.name, group : 'Server Names', sort : i } ); } - - + + /** * Loads config settings from env vars or Java system properties */ - function loadOverrides( serverJSON, serverInfo ){ + function loadOverrides( serverJSON, serverInfo, boolean verbose=false ){ + var debugMessages = []; + var job = wirebox.getInstance( 'interactiveJob' ); var overrides={}; - - // Look for individual BOX settings to import. - var processVarsUDF = function( envVar, value ) { + + // Look for individual BOX settings to import. + var processVarsUDF = function( envVar, value, string source ) { // Loop over any that look like box_server_xxx if( envVar.len() > 11 && reFindNoCase( 'box[_\.]server[_\.]', left( envVar, 11 ) ) ) { // proxy_host gets turned into proxy.host // Note, the asssumption is made that no config setting will ever have a legitimate underscore in the name var name = right( envVar, len( envVar ) - 11 ).replace( '_', '.', 'all' ); + debugMessages.append( 'Overridding [#name#] with #source# [#envVar#]' ); JSONService.set( JSON=overrides, properties={ '#name#' : value }, thisAppend=true ); } }; - + // Get all OS env vars var envVars = system.getenv(); for( var envVar in envVars ) { - processVarsUDF( envVar, envVars[ envVar ] ); + processVarsUDF( envVar, envVars[ envVar ], 'OS environment variable' ); } - + // Get all System Properties var props = system.getProperties(); for( var prop in props ) { - processVarsUDF( prop, props[ prop ] ); + processVarsUDF( prop, props[ prop ], 'system property' ); } // Get all box environemnt variable var envVars = systemSettings.getAllEnvironmentsFlattened(); for( var envVar in envVars ) { - processVarsUDF( envVar, envVars[ envVar ] ); + processVarsUDF( envVar, envVars[ envVar ], 'box environment variable' ); } - + if( overrides.keyExists( 'profile' ) ) { serverInfo.envVarHasProfile=true } - + + if( verbose && debugMessages.len() ) { + job.start( 'Overriding server.json values from env vars' ); + debugMessages.each( (l)=>job.addLog( l ) ); + job.complete( verbose ); + } + JSONService.mergeData( serverJSON, overrides ); } + + + /** + * Nice wrapper to run a server script + * + * @scriptName Name of the server script to run + * @directory The web root + * @ignoreMissing Set true to ignore missing server scripts, false to throw an exception + * @interceptData An optional struct of data if this server script is being fired as part of an interceptor announcement. Will be loaded into env vars + */ + function runScript( required string scriptName, string directory=shell.pwd(), boolean ignoreMissing=true, interceptData={} ) { + if( !isNull( interceptData.serverJSON ) ){ + var serverJSON = interceptData.serverJSON; + } else if( !isNull( interceptData.serverInfo.name ) && len( interceptData.serverInfo.name ) ){ + var serverDetails = resolveServerDetails( { name=interceptData.serverInfo.name } ); + if( serverDetails.serverIsNew ) { + return; + } + var serverJSON = serverDetails.serverJSON; + systemSettings.expandDeepSystemSettings( serverJSON ); + loadOverrides( serverJSON, serverDetails.serverInfo, serverDetails.serverInfo.verbose ?: false ); + } else { + consoleLogger.warn( 'Could not find server for script [#arguments.scriptName#].' ); + return; + } + var serverJSONScripts = duplicate( serverJSON.scripts ?: {} ); + getDefaultServerJSON().scripts.each( (k,v)=>{ + // Append existing scripts + if( serverJSONScripts.keyExists( k ) ) { + serverJSONScripts[ k ] &= '; ' & v + // Merge missing ones + } else { + serverJSONScripts[ k ] = v; + } + } ); + // If there is a scripts object with a matching key for this interceptor.... + if( serverJSONScripts.keyExists( arguments.scriptName ) ) { + + // Skip this if we're not in a command so we don't litter the default env var namespace + if( systemSettings.getAllEnvironments().len() > 1 ) { + systemSettings.setDeepSystemSettings( interceptData ); + } + + // Run preXXX package script + runScript( 'pre#arguments.scriptName#', arguments.directory, true, interceptData ); + + var thisScript = serverJSONScripts[ arguments.scriptName ]; + consoleLogger.debug( '.' ); + consoleLogger.warn( 'Running server script [#arguments.scriptName#].' ); + consoleLogger.debug( '> ' & thisScript ); + + // Normally the shell retains the previous exit code, but in this case + // it's important for us to know if the scripts return a failing exit code without throwing an exception + shell.setExitCode( 0 ); + + // ... then run the script! (in the context of the package's working directory) + var previousCWD = shell.pwd(); + shell.cd( arguments.directory ); + shell.callCommand( thisScript ); + shell.cd( previousCWD ); + + // If the script ran "exit" + if( !shell.getKeepRunning() ) { + // Just kidding, the shell can stay.... + shell.setKeepRunning( true ); + } + + if( shell.getExitCode() != 0 ) { + throw( message='Server script returned failing exit code (#shell.getExitCode()#)', detail='Failing script: #arguments.scriptName#', type="commandException", errorCode=shell.getExitCode() ); + } + + // Run postXXX package script + runScript( 'post#arguments.scriptName#', arguments.directory, true, interceptData ); + + } else if( !arguments.ignoreMissing ) { + consoleLogger.error( 'The script [#arguments.scriptName#] does not exist in this server.' ); + } + } + + } diff --git a/src/cfml/system/util/AnsiFormatter.cfc b/src/cfml/system/util/AnsiFormatter.cfc index ed709832c..ce87f1567 100644 --- a/src/cfml/system/util/AnsiFormatter.cfc +++ b/src/cfml/system/util/AnsiFormatter.cfc @@ -33,6 +33,14 @@ component accessors=true { // [TRACE] io.undertow.predicate: Path(s) [/CFIDE/main/ide.cfm] MATCH input [/CFIDE/main/ide.cfm] for HttpServerExchange{ GET /CFIDE/main/ide.cfm}. line = reReplaceNoCase( line, '^(\[[^]]*])( io\.undertow\.request\.dump: )(.*)', 'Request Dump: \3' ); + // Log messages from Tuckey Rewrite engine "Rewrite UrlRewriter:" + // Ex: + // [DEBUG] org.tuckey.web.filters.urlrewrite.UrlRewriter: processing request for /services/training + // [DEBUG] org.tuckey.web.filters.urlrewrite.RuleExecutionOutput: needs to be forwarded to /index.cfm/services/training + line = reReplaceNoCase( line, '^(\[[^]]*])( org\.tuckey\.web\.filters\.urlrewrite\.UrlRewriter: )(.*)', '\1 Rewrite: \3' ); + line = reReplaceNoCase( line, '^(\[[^]]*])( org\.tuckey\.web\.filters\.urlrewrite\.RuleExecutionOutput: )(.*)', '\1 Rewrite Output: \3' ); + line = reReplaceNoCase( line, '^(\[[^]]*])( org\.tuckey\.web\.filters\.urlrewrite\.+)([^:]*: )(.*)', '\1 Rewrite \3\4' ); + // Strip off redundant severities that come from wrapping LogBox appenders in Log4j appenders // [INFO ] DEBUG my.logger.name This rain in spain stays mainly in the plains line = reReplaceNoCase( line, '^(\[(INFO |ERROR|DEBUG|WARN )] )(INFO|ERROR|DEBUG|WARN)( .*)', '[\3]\4' ); diff --git a/src/cfml/system/util/CommandDSL.cfc b/src/cfml/system/util/CommandDSL.cfc index 79fb05cb5..d531ea6b2 100644 --- a/src/cfml/system/util/CommandDSL.cfc +++ b/src/cfml/system/util/CommandDSL.cfc @@ -222,7 +222,23 @@ component accessors=true { * Turn this CFC into a string representation **/ string function getCommandString() { - return getTokens().toList( ' ' ); + var tokens = getCommand(); + tokens &= ' ' & processParams().toList( ' ' ); + tokens &= ' '& getFlags().toList( ' ' ); + + if( len( getOverwrite() ) ) { + tokens &= ' > ' & getOverwrite(); + } + + if( len( getAppend() ) ) { + tokens &= ' >> ' & getAppend(); + } + + for( var piperton in getPiped() ) { + tokens &= ' | ' & piperton.getCommandString(); + } + + return tokens; } /** @@ -246,9 +262,9 @@ component accessors=true { try { if( !isNull( getPipedInput() ) ) { - var result = shell.callCommand( getTokens(), getReturnOutput(), getPipedInput() ); + var result = shell.callCommand( getTokens(), getReturnOutput(), getPipedInput(), getCommandString() ); } else { - var result = shell.callCommand( getTokens(), getReturnOutput() ); + var result = shell.callCommand( command=getTokens(), returnOutput=getReturnOutput(), line=getCommandString() ); } // If the previous command chain failed diff --git a/src/cfml/system/util/FileSystem.cfc b/src/cfml/system/util/FileSystem.cfc index 0be719413..935fbfc57 100644 --- a/src/cfml/system/util/FileSystem.cfc +++ b/src/cfml/system/util/FileSystem.cfc @@ -370,18 +370,9 @@ component accessors="true" singleton { } } - // *nix needs to include first folder due to Lucee bug. - // So /usr/brad/foo.cfc becomes /usr + // On Unix, / is both the drive root and the lucee "webroot" so nothing needs done if( !isWindows() ) { - if( listLen( arguments.absolutePath, '/' ) > 1 ) { - var firstFolder = listFirst( arguments.absolutePath, '/' ); - var path = listRest( arguments.absolutePath, '/' ); - } else { - var firstFolder = ''; - var path = listChangeDelims( arguments.absolutePath, '/', '/' ); - } - var mapping = locateUnixDriveMapping( firstFolder ); - return mapping & '/' & path; + return arguments.absolutePath; } // UNC network path. @@ -420,17 +411,6 @@ component accessors="true" singleton { return mappingName; } - /** - * Accepts a Unix root folder and returns a CF Mapping - * Creates the mapping if it doesn't exist - */ - string function locateUnixDriveMapping( required string rootFolder ) { - var mappingName = '/' & arguments.rootFolder & '_root'; - var mappingPath = '/' & arguments.rootFolder & ( len( arguments.rootFolder ) ? '/' : '' ); - createMapping( mappingName, mappingPath ); - return mappingName; - } - /** * Accepts a Windows UNC network share and returns a CF Mapping * Creates the mapping if it doesn't exist diff --git a/src/cfml/system/util/ForgeBox.cfc b/src/cfml/system/util/ForgeBox.cfc index a88066376..7c998cf5c 100644 --- a/src/cfml/system/util/ForgeBox.cfc +++ b/src/cfml/system/util/ForgeBox.cfc @@ -355,35 +355,6 @@ or just add DEBUG to the root logger return results.response.data; } - /** - * Tracks a download - */ - function recordDownload( - required string slug, - string version, - string APIToken='' ) { - - var thisResource = "install/#arguments.slug#"; - if( len( arguments.version ) ) { - thisResource &= "/#arguments.version#"; - } - - var results = makeRequest( - resource=thisResource, - method='post', - headers = { - 'x-api-token' : arguments.APIToken - } ); - - // error - if( results.response.error ){ - throw( "Something went wrong tracking this download from #getEndpointName()#.", 'forgebox', arrayToList( results.response.messages ) ); - } - - return results.response.data; - } - - /** * Autocomplete for slugs */ @@ -457,6 +428,10 @@ or just add DEBUG to the root logger + if( configService.getSetting( 'offlineMode', false ) ) { + throw( 'Can''t access #getEndpointName()# resource [#resource#], CommandBox is in offline mode. Go online with [config set offlineMode=false].', 'forgebox' ); + } + var results = {error=false,response={},message="",responseheader={},rawResponse=""}; var HTTPResults = ""; var param = ""; diff --git a/src/cfml/system/util/Formatter.cfc b/src/cfml/system/util/Formatter.cfc index 08992b964..e797f07f8 100644 --- a/src/cfml/system/util/Formatter.cfc +++ b/src/cfml/system/util/Formatter.cfc @@ -269,4 +269,24 @@ component singleton { // This is an external lib now. Leaving here for backwards compat. return JSONPrettyPrint.formatJSON( argumentCollection = arguments ); } + + /** + * Pretty print XML + * @XMLDoc A string containing XML or a parsed XML document + */ + function formatXML( XMLDoc ) { + var xlt = ' + + + + '; + var XMLDeclaration = ''; + + try { + return toString( XmlTransform( XMLDoc, xlt) ).replace( XMLDeclaration, '', 'once' ); + } catch( any e ) { + return toString( XMLDoc ).replace( XMLDeclaration, '', 'once' ); + } + } + } diff --git a/src/cfml/system/util/MultiSelect.cfc b/src/cfml/system/util/MultiSelect.cfc index e72d9b549..87aacd185 100644 --- a/src/cfml/system/util/MultiSelect.cfc +++ b/src/cfml/system/util/MultiSelect.cfc @@ -191,10 +191,14 @@ component accessors=true { if( job.getActive() ) { job.addLog( getQuestion() & ': ' & response ); } else { - printBuffer - .line() - .text( getQuestion() ) - .line( response ) + var pb = printBuffer.line(); + + if(len(getQuestion() & response) > 80) { + pb.line( getQuestion() ); + } else { + pb.text( getQuestion() ); + } + pb.line( response ) .toConsole(); } diff --git a/src/cfml/system/util/Print.cfc b/src/cfml/system/util/Print.cfc index 777c8b0d5..03f8f2c75 100644 --- a/src/cfml/system/util/Print.cfc +++ b/src/cfml/system/util/Print.cfc @@ -92,7 +92,9 @@ component { // Text needing formatting var text = arrayLen(missingMethodArguments) ? missingMethodArguments[ 1 ] : ''; // Convert complex values to a string representation - if( !isSimpleValue( text ) ) { + if( isXML( text ) ) { + text = formatterUtil.formatXML( text ); + } else if( !isSimpleValue( text ) ) { // Serializable types if( isBinary( text ) ) { @@ -101,6 +103,7 @@ component { if( isJSON( toString( text ) ) ) { text = formatterUtil.formatJson( json=toString( text ), ANSIColors=JSONService.getANSIColors() ); } + } else if( isArray( text ) || isStruct( text ) || isQuery( text ) ) { text = serializeJSON( text, 'struct' ); text = formatterUtil.formatJson( json=text, ANSIColors=JSONService.getANSIColors() ); @@ -289,12 +292,14 @@ component { * @includedHeaders A list of headers to include. Used for query inputs * @headerNames An list/array of column headers to use instead of the default * @debug Only print out the names of the columns and the first row values + * @width Override the terminal width */ function table( required any data=[], any includedHeaders="", any headerNames="", - boolean debug=false + boolean debug=false, + width=-1 ){ return tablePrinter.print( argumentCollection=arguments ); } diff --git a/src/cfml/system/util/PrintBuffer.cfc b/src/cfml/system/util/PrintBuffer.cfc index 7b709db20..399dbee0f 100644 --- a/src/cfml/system/util/PrintBuffer.cfc +++ b/src/cfml/system/util/PrintBuffer.cfc @@ -13,6 +13,7 @@ component accessors="true" extends="Print"{ // DI property name="shell" inject="shell"; + property name="job" inject="interactiveJob"; property name="objectID"; @@ -36,8 +37,13 @@ component accessors="true" extends="Print"{ clear(); } - // Once we get the text to print above, we can release the lock while we actually print it. - variables.shell.printString( thingToPrint ); + // Once we get the text to print above, we can release the lock while we actually print it. + // If there is an active job, print our output through it + if( job.getActive() ) { + job.addLog( thingToPrint ); + } else { + variables.shell.printString( thingToPrint ); + } } // Reset the result diff --git a/src/cfml/system/util/ProgressableDownloader.cfc b/src/cfml/system/util/ProgressableDownloader.cfc index 8cba4ed59..6bee7e8dd 100644 --- a/src/cfml/system/util/ProgressableDownloader.cfc +++ b/src/cfml/system/util/ProgressableDownloader.cfc @@ -25,6 +25,10 @@ component singleton { required string destinationFile, any statusUDF, any redirectUDF='' ) { + + if( configService.getSetting( 'offlineMode', false ) ) { + throw( 'Can''t download [#downloadURL#], CommandBox is in offline mode. Go online with [config set offlineMode=false].' ); + } var data = getByteArray( 1024 ); var total = 0; diff --git a/src/cfml/system/util/REPLParser.cfc b/src/cfml/system/util/REPLParser.cfc index 0608c37b4..abeebd50a 100644 --- a/src/cfml/system/util/REPLParser.cfc +++ b/src/cfml/system/util/REPLParser.cfc @@ -117,7 +117,7 @@ component accessors="true" singleton { return '[EMPTY STRING]'; // XML doc OR XML String } else if( isXML( result ) ) { - return formatXML( XMLParse( result ) ); + return formatterUtil.formatXML( result ); // string } else if( isSimpleValue( result ) ) { @@ -151,15 +151,4 @@ component accessors="true" singleton { return reReplaceNoCase( arguments.command, "//[^""']*$|/\*.*\*/", "", "all" ); } - private function formatXML( XMLDoc ) { - var xlt = ' - - - - '; - - return toString( XmlTransform( XMLDoc, xlt) ); - } - - } diff --git a/src/cfml/system/util/SystemSettings.cfc b/src/cfml/system/util/SystemSettings.cfc index 5060222da..78fecf06b 100644 --- a/src/cfml/system/util/SystemSettings.cfc +++ b/src/cfml/system/util/SystemSettings.cfc @@ -50,13 +50,13 @@ component singleton { } // Now check Java system props - var value = system.getProperty( arguments.key ); + var value = getSystemProperty( key=arguments.key, throwWhenNotFound=false ); if ( ! isNull( value ) ) { return value; } // Finally check OS env vars. - value = system.getEnv( arguments.key ); + value = getEnv( key=arguments.key, throwWhenNotFound=false ); if ( ! isNull( value ) ) { return value; } @@ -79,21 +79,30 @@ component singleton { * @key The name of the setting to look up. * @defaultValue The default value to use if the key does not exist in the system properties */ - function getSystemProperty( required string key, defaultValue ) { + function getSystemProperty( required string key, defaultValue, throwWhenNotFound=true ) { var value = system.getProperty( arguments.key ); if ( ! isNull( value ) ) { return value; } + // Second case-insensitive attempt + for( var propKey in system.getProperties() ) { + if( arguments.key == propKey ) { + return system.getProperty( propKey ); + } + } + if ( ! isNull( arguments.defaultValue ) ) { return arguments.defaultValue; } - throw( - type = "SystemSettingNotFound", - message = "Could not find a Java System property with key [#arguments.key#]." - ); + if( throwWhenNotFound ) { + throw( + type = "SystemSettingNotFound", + message = "Could not find a Java System property with key [#arguments.key#]." + ); + } } /** @@ -124,21 +133,30 @@ component singleton { * @key The name of the setting to look up. * @defaultValue The default value to use if the key does not exist in the env */ - function getEnv( required string key, defaultValue ) { + function getEnv( required string key, defaultValue, throwWhenNotFound=true ) { var value = system.getEnv( arguments.key ); if ( ! isNull( value ) ) { return value; } + // Second case-insensitive attempt + for( var envKey in system.getEnv() ) { + if( arguments.key == envKey ) { + return system.getEnv( envKey ); + } + } + if ( ! isNull( arguments.defaultValue ) ) { return arguments.defaultValue; } - throw( - type = "SystemSettingNotFound", - message = "Could not find a env property with key [#arguments.key#]." - ); + if( throwWhenNotFound ) { + throw( + type = "SystemSettingNotFound", + message = "Could not find a env property with key [#arguments.key#]." + ); + } } @@ -223,14 +241,14 @@ component singleton { * * @dataStructure A string, struct, or array. Initial value should be a struct. */ - function setDeepSystemSettings( any dataStructure, string prefix='interceptData' ) { + function setDeepSystemSettings( any dataStructure, string prefix='interceptData', delim='.' ) { // If it's a struct... if( isStruct( dataStructure ) && !isObject( dataStructure ) ) { // Loop over and process each key for( var key in dataStructure ) { if( key != 'COMMANDREFERENCE' ) { - setDeepSystemSettings( dataStructure[ key ] ?: '', prefix.listAppend( key, '.' ) ); + setDeepSystemSettings( dataStructure[ key ] ?: '', prefix.listAppend( key, delim ), delim ); } } // If it's an array... @@ -239,7 +257,7 @@ component singleton { // Loop over and process each index for( var item in dataStructure ) { i++; - setDeepSystemSettings( item ?: '', prefix & '[#i#]' ); + setDeepSystemSettings( item ?: '', prefix & '[#i#]', delim ); } // If it's a string... } else if ( isSimpleValue( dataStructure ) ) { diff --git a/src/cfml/system/util/TablePrinter.cfc b/src/cfml/system/util/TablePrinter.cfc index 7476b0a8b..a7edf8ca4 100644 --- a/src/cfml/system/util/TablePrinter.cfc +++ b/src/cfml/system/util/TablePrinter.cfc @@ -43,18 +43,20 @@ component { * @includedHeaders A list of headers to include. Used for query inputs * @headerNames An list/array of column headers to use instead of the default * @debug Only print out the names of the columns and the first row values + * @width Override the terminal width */ public string function print( required any data=[], any includedHeaders="", any headerNames="", - boolean debug=false + boolean debug=false, + width=-1 ) { arguments.headerNames = isArray( arguments.headerNames ) ? arrayToList( arguments.headerNames ) : arguments.headerNames; var dataQuery = isQuery( arguments.data ) ? arguments.data : convert.toQuery( arguments.data, arguments.headerNames ); - + // Check for // printTable [] // printTable [{}] @@ -68,13 +70,13 @@ component { columns.listEach( (c)=> { c = trim( c ); // This expression will either evaluate to true or throw an exception - listFindNoCase( dataQuery.columnList, c ) + listFindNoCase( dataQuery.columnList, c ) || c == '*' || throw( message='Header name [#c#] not found.', detail='Valid header names are [#dataQuery.columnList#]', type='commandException' ); } ); dataQuery = queryExecute('SELECT #columns# FROM dataQuery',[],{ dbType : 'query' }); - + // Extract data in array of structs var dataRows = convert.queryToArrayOfOrderedStructs( dataQuery ); @@ -93,7 +95,7 @@ component { } dataRows = autoFormatData( dataHeaders, dataRows ); - dataHeaders = processHeaders( dataHeaders, dataRows, headerNames.listToArray() ) + dataHeaders = processHeaders( dataHeaders, dataRows, headerNames.listToArray(), width ) printHeader( dataHeaders ); printData( dataRows, dataHeaders ); @@ -111,10 +113,14 @@ component { public array function processHeaders( required array headers, required array data, - headerNames=[] + headerNames=[], + width=-1 ) { var headerData = arguments.headers.map( ( header, index ) => calculateColumnData( index, header, data, headerNames ), true ); - var termWidth = shell.getTermWidth()-1; + var termWidth = arguments.width; + if( termWidth <= 0 ) { + termWidth = shell.getTermWidth()-1; + } if( termWidth <= 0 ) { termWidth = 100; } @@ -163,7 +169,7 @@ component { // This happens if the first column is still so big it won't fit and the while loop above never entered if( totalWidth == 1 ) { // Cut down that one column so it fits - headerData[1].maxWidth=termWidth-11 + headerData[1].maxWidth=max( termWidth-11, 3) } } @@ -406,7 +412,7 @@ component { return '[#data.getClass().getName()#]'; } } - + function cellHasFormattingEmbedded( data ) { return isStruct( data ) && data.count() == 2 && data.keyExists( 'options' ) && data.keyExists( 'value' ) && isSimpleValue( data.options ); } diff --git a/src/cfml/system/util/Watcher.cfc b/src/cfml/system/util/Watcher.cfc index ed2b5bed6..92248933f 100644 --- a/src/cfml/system/util/Watcher.cfc +++ b/src/cfml/system/util/Watcher.cfc @@ -139,7 +139,7 @@ component accessors=true { } // Handle "expected" exceptions from commands } catch( commandException e ) { - shell.printError( { message : e.message, detail: e.detail } ); + shell.printError( { message : e.message, detail: e.detail, extendedInfo : e.extendedInfo ?: '' } ); print .line() diff --git a/src/cfml/system/util/jline/CommandCompletor.cfc b/src/cfml/system/util/jline/CommandCompletor.cfc index e846f9846..8034d090e 100644 --- a/src/cfml/system/util/jline/CommandCompletor.cfc +++ b/src/cfml/system/util/jline/CommandCompletor.cfc @@ -536,8 +536,8 @@ component singleton { description.len() ? description : nullValue(), // descr nullValue(), // suffix nullValue(), // key - complete//, // complete - //val( sort ) // sort + complete, // complete + val( sort ) // sort ) ); diff --git a/src/java/cliloader/LoaderCLIMain.java b/src/java/cliloader/LoaderCLIMain.java index e27e31c19..3e5956785 100644 --- a/src/java/cliloader/LoaderCLIMain.java +++ b/src/java/cliloader/LoaderCLIMain.java @@ -286,18 +286,19 @@ && new File( cliArguments.get( 0 ) ).isFile() ) { Security.insertProviderAt( p, 1 ); } ); + // The "webroot" is the drive root where CommandBox's home lives String webroot = Paths.get( uri ).toAbsolutePath().getRoot().toString(); - // On a *nix machine - if( webroot.equals( "/" ) ) { - // Include first folder like /usr/ - webroot += Paths.get( uri ).toAbsolutePath().subpath( 0, 1 ).toString() + "/"; - } // Escape backslash in webroot since replace uses a regular expression + // The bootstrap is the first .cfm file we will cfinclude from the "webroot" String bootstrap = "/" + Paths.get( uri ).toAbsolutePath().toString().replaceFirst( webroot.replace( "\\", "\\\\" ), "" ); - + + // contextroot sets lucee's "webroot" inside the scripting engine to be our drive root + System.setProperty( "lucee.cli.contextRoot", webroot ); + // These next two are the Lucee web context and server context homes System.setProperty( "lucee.web.dir", getLuceeCLIConfigWebDir().getAbsolutePath() ); System.setProperty( "lucee.base.dir", getLuceeCLIConfigServerDir().getAbsolutePath() ); + // A couple tweaks to make Felix faster System.setProperty( "felix.cache.locking", "false" ); System.setProperty( "felix.storage.clean", "none" ); @@ -313,10 +314,7 @@ && new File( cliArguments.get( 0 ) ).isFile() ) { String CFML = "loader = createObject( 'java', 'cliloader.LoaderCLIMain' ); \n" + "if( !isNull( loader.FRTrans ) ) { loader.FRTrans.close(); } \n" + "\n" - + "mappings = getApplicationSettings().mappings; \n" - + " mappings[ '/__commandbox_root/' ] = '" + webroot + "'; \n" - + " application mappings='#mappings#' action='update'; \n" - + " include '/__commandbox_root" + bootstrap.replace( "'", "''" ) + "'; \n"; + + " include '" + bootstrap.replace( "'", "''" ) + "'; \n"; if( debug ) { printStream.println( "" ); @@ -528,7 +526,8 @@ public static boolean listContains( ArrayList< String > argList, String text ){ public static int listIndexOf( ArrayList< String > argList, String text ){ int index = 0; for( String item : argList) { - if( item.startsWith( text ) || item.startsWith( "-" + text ) ) { + if( item.toLowerCase().startsWith( text.toLowerCase() ) + || item.toLowerCase().startsWith( "-" + text.toLowerCase() ) ) { return index; } index++; @@ -577,7 +576,6 @@ public void run() { })); disableAccessWarnings(); - System.setProperty("log4j.configuration", "resource/log4j.xml"); Util.ensureJavaVersion(); execute( initialize( arguments ) ); mainDone = true; @@ -713,6 +711,8 @@ public static ArrayList< String > initialize( String[] arguments ) throws IOExce props.setProperty( "cfml.cli.pwd", cliworkingdirFinal ); File libDir = getLibDir(); + // Default Log4j2 config is in the libn folder + System.setProperty("log4j2.configurationFile", new File( libDir, "log4j2.xml" ).toURI().toString() ); props.setProperty( "cfml.cli.lib", libDir.getAbsolutePath() ); File cfmlDir = new File( cli_home.getPath() + "/cfml" ); File cfmlSystemDir = new File( cli_home.getPath() + "/cfml/system" ); diff --git a/src/resources/log4j2.xml b/src/resources/log4j2.xml new file mode 100644 index 000000000..72f9799ca --- /dev/null +++ b/src/resources/log4j2.xml @@ -0,0 +1,2 @@ + + \ No newline at end of file