Sometimes you have a problem that you should absolutely have seen coming. The annual "the company's Sitecore license has expired" fun is very much one of those things. But I'd not thought about this in advance, and the license expired while I was on holiday this year. It caused my team a load of hassle... But I have a plan to avoid this pain in the future:
<Source>EventLog</Source><Time>2021-09-03T19:33:25.000Z</Time><LogEntry><Channel>System</Channel><Level>Error</Level><EventId>701</EventId><Message>Task Scheduler service failed to start Task Compatibility module. Tasks may not be able to register on previous Window versions. Additional Data: Error Value: 2147942450.</Message></LogEntry> Unhandled Exception: Sitecore.Framework.Runtime.Licensing.Exceptions.SitecoreLicenseInvalidOperationException: Invalid or expired license. [Raw] [2021-09-03T19:33:31.000Z][LOGMONITOR] INFO: Entrypoint processs exit code: -532462766 at Sitecore.Framework.Runtime.Licensing.LicenseValidator.LoadLicense(License license) at Sitecore.Framework.Runtime.Commands.SitecoreHostCommand.OnExecuteAsync(CommandLineApplication app) at McMaster.Extensions.CommandLineUtils.Conventions.ExecuteMethodConvention.InvokeAsync(MethodInfo method, Object instance, Object[] arguments) at McMaster.Extensions.CommandLineUtils.Conventions.ExecuteMethodConvention.OnExecute(ConventionContext context, CancellationToken cancellationToken) at McMaster.Extensions.CommandLineUtils.Conventions.ExecuteMethodConvention.<>c__DisplayClass0_0.<<Apply>b__0>d.MoveNext() --- End of stack trace from previous location where exception was thrown --- at McMaster.Extensions.CommandLineUtils.CommandLineApplication.ExecuteAsync(String[] args, CancellationToken cancellationToken) at McMaster.Extensions.CommandLineUtils.CommandLineApplication.ExecuteAsync[TApp](CommandLineContext context, CancellationToken cancellationToken) at Sitecore.Program.<Main>(String[] args)
But the CM container will give a less obvious error:
2021-09-03 19:34:33 ::1 GET /healthz/ready - 80 - ::1 Mozilla/5.0+(Windows+NT;+Windows+NT+10.0;+en-US)+WindowsPowerShell/5.1.17763.1971 - 500 0 0 30804 2076 20:35:07 INFO HttpModule is being initialized 2260 20:35:17 INFO ************************************************** 2260 20:35:17 WARN Sitecore shutting down 2260 20:35:17 WARN Shutdown message: Initialization Error HostingEnvironment initiated shutdown 2021-09-03 19:35:07 ::1 GET /healthz/ready - 80 - ::1 Mozilla/5.0+(Windows+NT;+Windows+NT+10.0;+en-US)+WindowsPowerShell/5.1.17763.1971 - 500 0 0 4130
And if you're running XP, that will get errors for xConnect too:
2021-09-03 20:34:08.802 +01:00 [Error] XConnect Web Application Error: "System.ApplicationException: Exception trying to initialize Service Collection and Provider for WebAPI Dependency Resolver, Inner Exception: Required license is missing: Sitecore.xDB.Base ---> Sitecore.Nexus.Licensing.LicenseException: Required license is missing: Sitecore.xDB.Base at ?????????????????????????????????????????.(????????????????????????????????????????? , String ) at Sitecore.XConnect.Configuration.Extensions.InitializeLicenseCheck(IServiceCollection collection, String licenseFileOrXml) at Sitecore.XConnect.Configuration.Extensions.UseXConnectServiceInitializationConfiguration(IServiceCollection collection, IConfiguration configuration, String[] configurationSectionNames, String initializationSectionName, Boolean validateConfiguration) at Sitecore.XConnect.Web.Host.WebApiConfig.ConfigureServices(HttpConfiguration config) --- End of inner exception stack trace --- at Sitecore.XConnect.Web.Host.WebApiConfig.ConfigureServices(HttpConfiguration config) at System.Web.Http.GlobalConfiguration.Configure(Action`1 configurationCallback) at Sitecore.XConnect.Web.Global.Application_Start(Object sender, EventArgs e)"
And because the health service ends up receiving errors, the Traefik container will fail, and the
docker-compose up
will return errors in the console too:
For Google's benefit, that set of messages is:
Creating network "sitecore-xp0_default" with the default driver Creating sitecore-xp0_mssql_1 ... done Creating sitecore-xp0_solr_1 ... done Creating sitecore-xp0_id_1 ... done Creating sitecore-xp0_solr-init_1 ... done Creating sitecore-xp0_xconnect_1 ... done Creating sitecore-xp0_cm_1 ... done ERROR: for cortexprocessingworker Container "5f077e60de91" is unhealthy. ERROR: for xdbautomationworker Container "5f077e60de91" is unhealthy. ERROR: for xdbsearchworker Container "5f077e60de91" is unhealthy. ERROR: for traefik Container "19caaac9dcea" is unhealthy. ERROR: Encountered errors while bringing up the project.
Now while someone used to reading logs will fairly quickly spot the underlying cause of the console errors there, one of the benefits of containers is supposed to be that it works easily for everyone in your team. Testers, managers or front-end devs are less likely to understand the business of running containers, and given the "sometimes Docker just doesn't work" issues that we've all encountered (looking at you AppCmd errors on v10.0) it's not always obvious to the less hard-core technical amongst us when the issue is "license" and when it's "you didn't apply the right Windows Update and now Docker is UNHAPPY".
This isn't too hard to do, it turns out. We know that the license you're using is encoded into the
.env
file for your project. It's stored as a Base64-encoded GZip stream. (You can
look at how this is done by examining the source for the
SitecoreDockerTools
module
that the
init.ps1
script installs) So to test this, we need some code which can decode the license XML from the environment file field, find the
<expiration/>
element and check that date against the current date.
A bit of Google and some quick hacking lead me to this function:
function Validate-LicenseData { Param ( $EnvironmentFile = ".env", $EnvironmentKey = "SITECORE_LICENSE" ) $file = Get-Content $EnvironmentFile -Encoding UTF8 $key = $file | ForEach-Object { if($_ -imatch "^$EnvironmentKey=.*") { return $_.SubString($EnvironmentKey.Length + 1) } } $data = [System.Convert]::FromBase64String($key) $memory = [System.IO.MemoryStream]::new() $memory.Write($data, 0, $data.Length) $memory.Flush() $memory.Seek(0, [System.IO.SeekOrigin]::Begin) | Out-Null $gzip = [System.IO.Compression.GZipStream]::new($memory, [System.IO.Compression.CompressionMode]::Decompress) $s = [System.IO.StreamReader]::new($gzip); $xml = $s.ReadToEnd() $s.Dispose(); $gzip.Dispose() $memory.Dispose(); $xml -match '<expiration>(.*?)</expiration>' | Out-Null $textExpiry = $Matches[1] $expiry = [System.DateTime]::ParseExact($textExpiry, "yyyyMMddThhmmss", [System.Globalization.CultureInfo]::InvariantCulture) if($expiry -lt [System.DateTime]::Now) { throw "Your Sitecore license has expired." } else { $daysLeft = [int]($expiry - [System.DateTime]::Now).TotalDays Write-Host "You have $daysLeft days left on your license." -ForegroundColor Green } }
(I'm sure I can make this code better with some more thought – but it's a start)
It will decode the license, extract the expiry date and then check it. If the license is expired it will throw (which should stop a script). I've wired that up in the
up.ps1
script in the project, so it checks the license before it kicks off the process of starting containers. Missing out the function above, it's something like:
Param( [switch]$build = $false, [switch]$attach = $false ) try { pushd ".\docker" Validate-LicenseData $buildFlag = "" if($build) { $buildFlag = "--build" } Write-Host "Starting: XP=$xp, Build=$build, Attach=$attach" $detachFlag = "--detach" if($attach) { $detachFlag = "" } docker-compose up $buildFlag $detachFlag } finally { popd }
(The full code is available as a gist if you want to make something of your own from it)
So the result of that for an expired license is an error:
And if the license is valid it will report how many days left you have, before carrying on with the normal startup:
Hopefully that means in the future my developers will know right away if they've hit a license expiry situation, and they can update their
.env
file...