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