Skip to content

Commit e279f26

Browse files
fabiocavmathewc
authored andcommitted
Shadow copy precompiled functions and shared dependencies
Shutdown on assembly changes
1 parent 93e1749 commit e279f26

31 files changed

+435
-112
lines changed

src/WebJobs.Script.WebHost/App_Start/WebApiConfig.cs

Lines changed: 1 addition & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ public static void Register(HttpConfiguration config, ScriptSettingsManager sett
3636
}
3737

3838
settingsManager = settingsManager ?? ScriptSettingsManager.Instance;
39-
settings = settings ?? GetDefaultSettings(settingsManager);
39+
settings = settings ?? WebHostSettings.CreateDefault(settingsManager);
4040

4141
var builder = new ContainerBuilder();
4242
builder.RegisterApiControllers(typeof(FunctionsController).Assembly);
@@ -81,33 +81,5 @@ public static void Register(HttpConfiguration config, ScriptSettingsManager sett
8181
config.InitializeReceiveGitHubWebHooks();
8282
config.InitializeReceiveSalesforceWebHooks();
8383
}
84-
85-
private static WebHostSettings GetDefaultSettings(ScriptSettingsManager settingsManager)
86-
{
87-
WebHostSettings settings = new WebHostSettings();
88-
89-
string home = settingsManager.GetSetting(EnvironmentSettingNames.AzureWebsiteHomePath);
90-
bool isLocal = string.IsNullOrEmpty(home);
91-
if (isLocal)
92-
{
93-
settings.ScriptPath = settingsManager.GetSetting(EnvironmentSettingNames.AzureWebJobsScriptRoot);
94-
settings.LogPath = Path.Combine(Path.GetTempPath(), @"Functions");
95-
settings.SecretsPath = HttpContext.Current.Server.MapPath("~/App_Data/Secrets");
96-
}
97-
else
98-
{
99-
// we're running in Azure
100-
settings.ScriptPath = Path.Combine(home, @"site\wwwroot");
101-
settings.LogPath = Path.Combine(home, @"LogFiles\Application\Functions");
102-
settings.SecretsPath = Path.Combine(home, @"data\Functions\secrets");
103-
}
104-
105-
if (string.IsNullOrEmpty(settings.ScriptPath))
106-
{
107-
throw new InvalidOperationException("Unable to determine function script root directory.");
108-
}
109-
110-
return settings;
111-
}
11284
}
11385
}

src/WebJobs.Script.WebHost/App_Start/WebHostSettings.cs

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,10 @@
11
// Copyright (c) .NET Foundation. All rights reserved.
22
// Licensed under the MIT License. See License.txt in the project root for license information.
33

4+
using System;
5+
using System.IO;
46
using Microsoft.Azure.WebJobs.Host;
7+
using Microsoft.Azure.WebJobs.Script.Config;
58

69
namespace Microsoft.Azure.WebJobs.Script.WebHost
710
{
@@ -19,5 +22,33 @@ public class WebHostSettings
1922
public bool IsAuthDisabled { get; set; } = false;
2023

2124
public TraceWriter TraceWriter { get; set; }
25+
26+
internal static WebHostSettings CreateDefault(ScriptSettingsManager settingsManager)
27+
{
28+
WebHostSettings settings = new WebHostSettings();
29+
30+
string home = settingsManager.GetSetting(EnvironmentSettingNames.AzureWebsiteHomePath);
31+
bool isLocal = string.IsNullOrEmpty(home);
32+
if (isLocal)
33+
{
34+
settings.ScriptPath = settingsManager.GetSetting(EnvironmentSettingNames.AzureWebJobsScriptRoot);
35+
settings.LogPath = Path.Combine(Path.GetTempPath(), @"Functions");
36+
settings.SecretsPath = System.Web.HttpContext.Current.Server.MapPath("~/App_Data/Secrets");
37+
}
38+
else
39+
{
40+
// we're running in Azure
41+
settings.ScriptPath = Path.Combine(home, @"site\wwwroot");
42+
settings.LogPath = Path.Combine(home, @"LogFiles\Application\Functions");
43+
settings.SecretsPath = Path.Combine(home, @"data\Functions\secrets");
44+
}
45+
46+
if (string.IsNullOrEmpty(settings.ScriptPath))
47+
{
48+
throw new InvalidOperationException("Unable to determine function script root directory.");
49+
}
50+
51+
return settings;
52+
}
2253
}
2354
}

src/WebJobs.Script.WebHost/Global.asax.cs

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,10 @@
22
// Licensed under the MIT License. See License.txt in the project root for license information.
33

44
using System;
5+
using System.Collections.Generic;
6+
using System.Linq;
57
using System.Web.Http;
8+
using Microsoft.Azure.WebJobs.Script.Config;
69
using Microsoft.Azure.WebJobs.Script.Diagnostics;
710
using Microsoft.Azure.WebJobs.Script.WebHost.Diagnostics;
811

@@ -12,13 +15,36 @@ public class WebApiApplication : System.Web.HttpApplication
1215
{
1316
protected void Application_Start()
1417
{
18+
var settingsManager = ScriptSettingsManager.Instance;
19+
var webHostSettings = WebHostSettings.CreateDefault(settingsManager);
20+
21+
VerifyAndEnableShadowCopy(webHostSettings);
22+
1523
using (var metricsLogger = new WebHostMetricsLogger())
1624
using (metricsLogger.LatencyEvent(MetricEventNames.ApplicationStartLatency))
1725
{
18-
GlobalConfiguration.Configure(c => WebApiConfig.Initialize(c));
26+
GlobalConfiguration.Configure(c => WebApiConfig.Initialize(c, settingsManager, webHostSettings));
1927
}
2028
}
2129

30+
private static void VerifyAndEnableShadowCopy(WebHostSettings webHostSettings)
31+
{
32+
if (!FeatureFlags.IsEnabled(ScriptConstants.FeatureFlagDisableShadowCopy))
33+
{
34+
string currentShadowCopyDirectories = AppDomain.CurrentDomain.SetupInformation.ShadowCopyDirectories;
35+
string shadowCopyPath = GetShadowCopyPath(currentShadowCopyDirectories, webHostSettings.ScriptPath);
36+
37+
#pragma warning disable CS0618
38+
AppDomain.CurrentDomain.SetShadowCopyPath(shadowCopyPath);
39+
#pragma warning restore CS0618
40+
}
41+
}
42+
43+
internal static string GetShadowCopyPath(string currentShadowCopyDirectories, string scriptPath)
44+
{
45+
return string.Join(";", new[] { currentShadowCopyDirectories, scriptPath }.Where(s => !string.IsNullOrEmpty(s)));
46+
}
47+
2248
protected void Application_Error(object sender, EventArgs e)
2349
{
2450
// TODO: Log any unhandled exceptions

src/WebJobs.Script.WebHost/WebScriptHostManager.cs

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@
33

44
using System;
55
using System.Collections.Generic;
6-
using System.Collections.ObjectModel;
76
using System.Diagnostics;
87
using System.IO;
98
using System.Linq;
@@ -225,7 +224,7 @@ public static void WarmUp(WebHostSettings settings)
225224
config.HostConfig.StorageConnectionString = null;
226225
config.HostConfig.DashboardConnectionString = null;
227226

228-
host = ScriptHost.Create(config, ScriptSettingsManager.Instance);
227+
host = ScriptHost.Create(new NullScriptHostEnvironment(), config, ScriptSettingsManager.Instance);
229228
traceWriter.Info(string.Format("Starting Host (Id={0})", host.ScriptConfig.HostConfig.HostId));
230229

231230
host.Start();
@@ -433,5 +432,13 @@ internal void InitializeHttpFunctions(IEnumerable<FunctionDescriptor> functions)
433432
}
434433
}
435434
}
435+
436+
public override void Shutdown()
437+
{
438+
Instance?.TraceWriter.Info("Environment shutdown has been triggered. Stopping host and signaling shutdown.");
439+
440+
Stop();
441+
HostingEnvironment.InitiateShutdown();
442+
}
436443
}
437444
}

src/WebJobs.Script/Description/DotNet/DotNetFunctionInvoker.cs

Lines changed: 32 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -36,22 +36,22 @@ public sealed class DotNetFunctionInvoker : FunctionInvokerBase
3636
private FunctionSignature _functionSignature;
3737
private IFunctionMetadataResolver _metadataResolver;
3838
private Action _reloadScript;
39+
private Action _shutdown;
3940
private Action _restorePackages;
4041
private Action<MethodInfo, object[], object[], object> _resultProcessor;
4142
private string[] _watchedFileTypes;
4243

43-
private static readonly string[] AssemblyFileTypes = { ".dll", ".exe" };
44-
4544
internal DotNetFunctionInvoker(ScriptHost host, FunctionMetadata functionMetadata,
4645
Collection<FunctionBinding> inputBindings, Collection<FunctionBinding> outputBindings,
4746
IFunctionEntryPointResolver functionEntryPointResolver, FunctionAssemblyLoader assemblyLoader,
48-
ICompilationServiceFactory compilationServiceFactory, ITraceWriterFactory traceWriterFactory = null)
47+
ICompilationServiceFactory compilationServiceFactory, ITraceWriterFactory traceWriterFactory = null,
48+
IFunctionMetadataResolver metadataResolver = null)
4949
: base(host, functionMetadata, traceWriterFactory)
5050
{
5151
_metricsLogger = Host.ScriptConfig.HostConfig.GetService<IMetricsLogger>();
5252
_functionEntryPointResolver = functionEntryPointResolver;
5353
_assemblyLoader = assemblyLoader;
54-
_metadataResolver = new FunctionMetadataResolver(functionMetadata, host.ScriptConfig.BindingProviders, TraceWriter);
54+
_metadataResolver = metadataResolver ?? new FunctionMetadataResolver(functionMetadata, host.ScriptConfig.BindingProviders, TraceWriter);
5555
_compilationService = compilationServiceFactory.CreateService(functionMetadata.ScriptType, _metadataResolver);
5656
_inputBindings = inputBindings;
5757
_outputBindings = outputBindings;
@@ -66,6 +66,9 @@ internal DotNetFunctionInvoker(ScriptHost host, FunctionMetadata functionMetadat
6666
_reloadScript = ReloadScript;
6767
_reloadScript = _reloadScript.Debounce();
6868

69+
_shutdown = () => Host.Shutdown();
70+
_shutdown = _shutdown.Debounce();
71+
6972
_restorePackages = RestorePackages;
7073
_restorePackages = _restorePackages.Debounce();
7174
}
@@ -74,7 +77,7 @@ private void InitializeFileWatcher()
7477
{
7578
if (InitializeFileWatcherIfEnabled())
7679
{
77-
_watchedFileTypes = AssemblyFileTypes
80+
_watchedFileTypes = ScriptConstants.AssemblyFileTypes
7881
.Concat(_compilationService.SupportedFileTypes)
7982
.ToArray();
8083
}
@@ -84,7 +87,14 @@ protected override void OnScriptFileChanged(object sender, FileSystemEventArgs e
8487
{
8588
// The ScriptHost is already monitoring for changes to function.json, so we skip those
8689
string fileExtension = Path.GetExtension(e.Name);
87-
if (_watchedFileTypes.Contains(fileExtension))
90+
if (ScriptConstants.AssemblyFileTypes.Contains(fileExtension, StringComparer.OrdinalIgnoreCase))
91+
{
92+
TraceWriter.Info("Assembly changes detected. Restarting host...");
93+
94+
// As a result of an assembly change, we initiate a full host shutdown
95+
_shutdown();
96+
}
97+
else if (_watchedFileTypes.Contains(fileExtension))
8898
{
8999
_reloadScript();
90100
}
@@ -146,7 +156,7 @@ private void ReloadScript()
146156
(_functionSignature == null ||
147157
(_functionSignature.HasLocalTypeReference || !_functionSignature.Equals(signature))))
148158
{
149-
Host.RestartEvent.Set();
159+
Host.Restart();
150160
}
151161
}
152162

@@ -172,19 +182,31 @@ private void RestorePackages()
172182
.ContinueWith(t => t.Exception.Handle(e => true), TaskContinuationOptions.OnlyOnFaulted | TaskContinuationOptions.ExecuteSynchronously);
173183
}
174184

175-
private async Task RestorePackagesAsync(bool reloadScriptOnSuccess = true)
185+
internal async Task RestorePackagesAsync(bool reloadScriptOnSuccess = true)
176186
{
177187
TraceOnPrimaryHost("Restoring packages.", TraceLevel.Info);
178188

179189
try
180190
{
181-
await _metadataResolver.RestorePackagesAsync();
191+
PackageRestoreResult result = await _metadataResolver.RestorePackagesAsync();
182192

183193
TraceOnPrimaryHost("Packages restored.", TraceLevel.Info);
184194

185195
if (reloadScriptOnSuccess)
186196
{
187-
_reloadScript();
197+
if (!result.IsInitialInstall && result.ReferencesChanged)
198+
{
199+
TraceOnPrimaryHost("Package references have changed. Restarting host...", TraceLevel.Info);
200+
201+
// If this is not the initial package install and references changed,
202+
// shutdown the host, which will cause it to have a clean start and load the new
203+
// assembly references
204+
_shutdown();
205+
}
206+
else
207+
{
208+
_reloadScript();
209+
}
188210
}
189211
}
190212
catch (Exception exc)

src/WebJobs.Script/Description/DotNet/FunctionMetadataResolver.cs

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -255,13 +255,15 @@ private IEnumerable<string> GetProbingFilePaths(string name)
255255
return _assemblyExtensions.Select(ext => Path.Combine(_privateAssembliesPath, assemblyName.Name + ext));
256256
}
257257

258-
public async Task RestorePackagesAsync()
258+
public async Task<PackageRestoreResult> RestorePackagesAsync()
259259
{
260260
var packageManager = new PackageManager(_functionMetadata, _traceWriter);
261-
await packageManager.RestorePackagesAsync();
261+
PackageRestoreResult result = await packageManager.RestorePackagesAsync();
262262

263263
// Reload the resolver
264264
_packageAssemblyResolver = new PackageAssemblyResolver(_functionMetadata);
265+
266+
return result;
265267
}
266268

267269
public bool RequiresPackageRestore(FunctionMetadata metadata)

src/WebJobs.Script/Description/DotNet/IFunctionMetadataResolver.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ public interface IFunctionMetadataResolver
2222

2323
ImmutableArray<PortableExecutableReference> ResolveReference(string reference, string baseFilePath, MetadataReferenceProperties properties);
2424

25-
Task RestorePackagesAsync();
25+
Task<PackageRestoreResult> RestorePackagesAsync();
2626

2727
bool RequiresPackageRestore(FunctionMetadata metadata);
2828

src/WebJobs.Script/Description/DotNet/PackageManager.cs

Lines changed: 39 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@
88
using System.Globalization;
99
using System.IO;
1010
using System.Linq;
11+
using System.Security.Cryptography;
12+
using System.Text;
1113
using System.Threading.Tasks;
1214
using Microsoft.Azure.WebJobs.Host;
1315
using Microsoft.Azure.WebJobs.Script.Config;
@@ -35,21 +37,22 @@ public PackageManager(FunctionMetadata metadata, TraceWriter traceWriter)
3537
_traceWriter = traceWriter;
3638
}
3739

38-
public Task RestorePackagesAsync()
40+
public Task<PackageRestoreResult> RestorePackagesAsync()
3941
{
40-
var tcs = new TaskCompletionSource<bool>();
42+
var tcs = new TaskCompletionSource<PackageRestoreResult>();
4143

4244
string functionDirectory = null;
4345
string projectPath = null;
4446
string nugetHome = null;
4547
string nugetFilePath = null;
46-
48+
string currentLockFileHash = null;
4749
try
4850
{
4951
functionDirectory = Path.GetDirectoryName(_functionMetadata.ScriptFile);
5052
projectPath = Path.Combine(functionDirectory, DotNetConstants.ProjectFileName);
5153
nugetHome = GetNugetPackagesPath();
5254
nugetFilePath = ResolveNuGetPath();
55+
currentLockFileHash = GetCurrentLockFileHash(functionDirectory);
5356

5457
var startInfo = new ProcessStartInfo
5558
{
@@ -70,7 +73,14 @@ public Task RestorePackagesAsync()
7073

7174
process.Exited += (s, e) =>
7275
{
73-
tcs.SetResult(process.ExitCode == 0);
76+
string newLockFileHash = GetCurrentLockFileHash(functionDirectory);
77+
var result = new PackageRestoreResult
78+
{
79+
IsInitialInstall = string.IsNullOrEmpty(currentLockFileHash),
80+
ReferencesChanged = string.Equals(currentLockFileHash, newLockFileHash),
81+
};
82+
83+
tcs.SetResult(result);
7484
process.Close();
7585
};
7686

@@ -86,15 +96,38 @@ public Task RestorePackagesAsync()
8696
_traceWriter.Error($@"NuGet restore failed with message: '{exc.Message}'
8797
Function directory: {functionDirectory}
8898
Project path: {projectPath}
89-
Packages path: {nugetHome},
90-
Nuget client path: {nugetFilePath}");
99+
Packages path: {nugetHome}
100+
Nuget client path: {nugetFilePath}
101+
Lock file hash: {currentLockFileHash}");
91102

92103
tcs.SetException(exc);
93104
}
94105

95106
return tcs.Task;
96107
}
97108

109+
internal static string GetCurrentLockFileHash(string functionDirectory)
110+
{
111+
string lockFilePath = Path.Combine(functionDirectory, DotNetConstants.ProjectLockFileName);
112+
113+
if (!File.Exists(lockFilePath))
114+
{
115+
return string.Empty;
116+
}
117+
118+
using (var md5 = MD5.Create())
119+
{
120+
using (var stream = File.OpenRead(lockFilePath))
121+
{
122+
byte[] hash = md5.ComputeHash(stream);
123+
124+
return hash
125+
.Aggregate(new StringBuilder(), (a, b) => a.Append(b.ToString("x2")))
126+
.ToString();
127+
}
128+
}
129+
}
130+
98131
public static string ResolveNuGetPath(string baseKuduPath = null)
99132
{
100133
// Check if we have the path in the well known environment variable

0 commit comments

Comments
 (0)