Skip to content

Commit 727d9ab

Browse files
fabiocavmathewc
authored andcommitted
Allowing relative script file references
1 parent 3dbd0cd commit 727d9ab

File tree

14 files changed

+251
-104
lines changed

14 files changed

+251
-104
lines changed

WebJobs.Script.sln

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -425,6 +425,11 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "json", "json", "{3A4AF861-6
425425
EndProject
426426
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "WebJobs.Extensibility.Tests", "test\WebJobs.Extensibility.Tests\WebJobs.Extensibility.Tests.csproj", "{9A8111D1-A7A1-4D60-98D1-705D1C849588}"
427427
EndProject
428+
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "HttpTriggerShared", "HttpTriggerShared", "{EB76E1C8-C823-4707-BD83-20304425EFC4}"
429+
ProjectSection(SolutionItems) = preProject
430+
sample\HttpTriggerShared\function.json = sample\HttpTriggerShared\function.json
431+
EndProjectSection
432+
EndProject
428433
Global
429434
GlobalSection(SharedMSBuildProjectFiles) = preSolution
430435
test\WebJobs.Script.Tests.Shared\WebJobs.Script.Tests.Shared.projitems*{35a2025d-f68a-4b57-83a2-ed4eb9c3894d}*SharedItemsImports = 4
@@ -546,5 +551,6 @@ Global
546551
{35C9CCB7-D8B6-4161-BB0D-BCFA7C6DCFFB} = {FD93A1ED-5AC4-4F9B-9087-E1C24320F36E}
547552
{3A4AF861-66F2-4C34-BB9E-69F0D392BB1A} = {67174433-2838-460C-9880-419476D02994}
548553
{9A8111D1-A7A1-4D60-98D1-705D1C849588} = {FD93A1ED-5AC4-4F9B-9087-E1C24320F36E}
554+
{EB76E1C8-C823-4707-BD83-20304425EFC4} = {FF9C0818-30D3-437A-A62D-7A61CA44F459}
549555
EndGlobalSection
550556
EndGlobal
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
{
2+
"scriptFile": "..\\HttpTrigger\\index.js",
3+
"bindings": [
4+
{
5+
"type": "httpTrigger",
6+
"name": "req",
7+
"direction": "in",
8+
"methods": [ "get" ]
9+
},
10+
{
11+
"type": "http",
12+
"name": "$return",
13+
"direction": "out"
14+
}
15+
]
16+
}

src/WebJobs.Script/Host/ScriptHost.cs

Lines changed: 51 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,8 @@
3030
using Newtonsoft.Json;
3131
using Newtonsoft.Json.Linq;
3232
using System.Collections.Immutable;
33+
using System.Configuration;
34+
using System.IO.Abstractions;
3335

3436
namespace Microsoft.Azure.WebJobs.Script
3537
{
@@ -42,7 +44,7 @@ public class ScriptHost : JobHost
4244
private Action<FileSystemEventArgs> _restart;
4345
private AutoRecoveringFileSystemWatcher _scriptFileWatcher;
4446
private AutoRecoveringFileSystemWatcher _debugModeFileWatcher;
45-
private ImmutableArray<string> _directorySnapshot;
47+
private ImmutableArray<string> _directorySnapshot;
4648
private BlobLeaseManager _blobLeaseManager;
4749
private static readonly TimeSpan MinTimeout = TimeSpan.FromSeconds(1);
4850
private static readonly TimeSpan MaxTimeout = TimeSpan.FromMinutes(5);
@@ -270,7 +272,7 @@ protected virtual void Initialize()
270272

271273
_debugModeFileWatcher = new AutoRecoveringFileSystemWatcher(hostLogPath, ScriptConstants.DebugSentinelFileName,
272274
includeSubdirectories: false, changeTypes: WatcherChangeTypes.Created | WatcherChangeTypes.Changed);
273-
275+
274276
_debugModeFileWatcher.Changed += OnDebugModeFileChanged;
275277

276278
var storageString = AmbientConnectionStringProvider.Instance.GetConnectionString(ConnectionStringNames.Storage);
@@ -624,11 +626,10 @@ public static Collection<FunctionMetadata> ReadFunctionMetadata(ScriptHostConfig
624626
string json = File.ReadAllText(functionConfigPath);
625627
JObject functionConfig = JObject.Parse(json);
626628

627-
Lazy<string[]> functionFiles = new Lazy<string[]>(() => Directory.EnumerateFiles(scriptDir).Where(p => Path.GetFileName(p).ToLowerInvariant() != ScriptConstants.FunctionMetadataFileName).ToArray());
628629
string functionError = null;
629630
FunctionMetadata functionMetadata = null;
630631
var mappedHttpFunctions = new Dictionary<string, HttpTriggerBindingMetadata>();
631-
if (!TryParseFunctionMetadata(functionName, functionConfig, mappedHttpFunctions, traceWriter, functionFiles, out functionMetadata, out functionError))
632+
if (!TryParseFunctionMetadata(functionName, functionConfig, mappedHttpFunctions, traceWriter, scriptDir, out functionMetadata, out functionError))
632633
{
633634
// for functions in error, log the error and don't
634635
// add to the functions collection
@@ -650,8 +651,11 @@ public static Collection<FunctionMetadata> ReadFunctionMetadata(ScriptHostConfig
650651
return functions;
651652
}
652653

653-
internal static bool TryParseFunctionMetadata(string functionName, JObject functionConfig, Dictionary<string, HttpTriggerBindingMetadata> mappedHttpFunctions, TraceWriter traceWriter, Lazy<string[]> functionFilesProvider, out FunctionMetadata functionMetadata, out string error)
654+
internal static bool TryParseFunctionMetadata(string functionName, JObject functionConfig, Dictionary<string, HttpTriggerBindingMetadata> mappedHttpFunctions,
655+
TraceWriter traceWriter, string scriptDirectory, out FunctionMetadata functionMetadata, out string error, IFileSystem fileSystem = null)
654656
{
657+
fileSystem = fileSystem ?? new FileSystem();
658+
655659
error = null;
656660
functionMetadata = ParseFunctionMetadata(functionName, functionConfig);
657661

@@ -667,22 +671,15 @@ internal static bool TryParseFunctionMetadata(string functionName, JObject funct
667671
traceWriter.Info(string.Format("Function '{0}' is disabled", functionName));
668672
}
669673

670-
// determine the primary script
671-
string[] functionFiles = functionFilesProvider.Value;
672-
if (functionFiles.Length == 0)
674+
try
673675
{
674-
error = "No function script files present.";
675-
return false;
676+
functionMetadata.ScriptFile = DeterminePrimaryScriptFile(functionConfig, scriptDirectory, fileSystem);
676677
}
677-
string scriptFile = DeterminePrimaryScriptFile(functionConfig, functionFiles);
678-
if (string.IsNullOrEmpty(scriptFile))
678+
catch (ConfigurationErrorsException exc)
679679
{
680-
error =
681-
"Unable to determine the primary function script. Try renaming your entry point script to 'run' (or 'index' in the case of Node), " +
682-
"or alternatively you can specify the name of the entry point script explicitly by adding a 'scriptFile' property to your function metadata.";
680+
error = exc.Message;
683681
return false;
684682
}
685-
functionMetadata.ScriptFile = scriptFile;
686683

687684
// determine the script type based on the primary script file extension
688685
functionMetadata.ScriptType = ParseScriptType(functionMetadata.ScriptFile);
@@ -754,35 +751,59 @@ internal static void ValidateFunctionName(string functionName)
754751
/// <summary>
755752
/// Determines which script should be considered the "primary" entry point script.
756753
/// </summary>
757-
internal static string DeterminePrimaryScriptFile(JObject functionConfig, string[] functionFiles)
754+
/// <exception cref="ConfigurationErrorsException">Thrown if the function metadata points to an invalid script file, or no script files are present.</exception>
755+
internal static string DeterminePrimaryScriptFile(JObject functionConfig, string scriptDirectory, IFileSystem fileSystem = null)
758756
{
759-
if (functionFiles.Length == 1)
757+
fileSystem = fileSystem ?? new FileSystem();
758+
759+
// First see if there is an explicit primary file indicated
760+
// in config. If so use that.
761+
string functionPrimary = null;
762+
string scriptFile = (string)functionConfig["scriptFile"];
763+
764+
if (!string.IsNullOrEmpty(scriptFile))
760765
{
761-
// if there is only a single file, that file is primary
762-
return functionFiles[0];
766+
string scriptPath = fileSystem.Path.Combine(scriptDirectory, scriptFile);
767+
if (!fileSystem.File.Exists(scriptPath))
768+
{
769+
throw new ConfigurationErrorsException("Invalid script file name configuration. The 'scriptFile' property is set to a file that does not exist.");
770+
}
771+
772+
functionPrimary = scriptPath;
763773
}
764774
else
765775
{
766-
// First see if there is an explicit primary file indicated
767-
// in config. If so use that.
768-
string functionPrimary = null;
769-
string scriptFileName = (string)functionConfig["scriptFile"];
770-
if (!string.IsNullOrEmpty(scriptFileName))
776+
string[] functionFiles = fileSystem.Directory.EnumerateFiles(scriptDirectory)
777+
.Where(p => fileSystem.Path.GetFileName(p).ToLowerInvariant() != ScriptConstants.FunctionMetadataFileName)
778+
.ToArray();
779+
780+
if (functionFiles.Length == 0)
771781
{
772-
functionPrimary = functionFiles.FirstOrDefault(p =>
773-
string.Compare(Path.GetFileName(p), scriptFileName, StringComparison.OrdinalIgnoreCase) == 0);
782+
throw new ConfigurationErrorsException("No function script files present.");
783+
}
784+
785+
if (functionFiles.Length == 1)
786+
{
787+
// if there is only a single file, that file is primary
788+
functionPrimary = functionFiles[0];
774789
}
775790
else
776791
{
777792
// if there is a "run" file, that file is primary,
778793
// for Node, any index.js file is primary
779794
functionPrimary = functionFiles.FirstOrDefault(p =>
780-
Path.GetFileNameWithoutExtension(p).ToLowerInvariant() == "run" ||
781-
Path.GetFileName(p).ToLowerInvariant() == "index.js");
795+
fileSystem.Path.GetFileNameWithoutExtension(p).ToLowerInvariant() == "run" ||
796+
fileSystem.Path.GetFileName(p).ToLowerInvariant() == "index.js");
782797
}
798+
}
783799

784-
return functionPrimary;
800+
if (string.IsNullOrEmpty(functionPrimary))
801+
{
802+
throw new ConfigurationErrorsException("Unable to determine the primary function script. Try renaming your entry point script to 'run' (or 'index' in the case of Node), " +
803+
"or alternatively you can specify the name of the entry point script explicitly by adding a 'scriptFile' property to your function metadata.");
785804
}
805+
806+
return Path.GetFullPath(functionPrimary);
786807
}
787808

788809
private static ScriptType ParseScriptType(string scriptFilePath)

src/WebJobs.Script/WebJobs.Script.csproj

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -289,6 +289,10 @@
289289
<HintPath>..\..\packages\System.Diagnostics.StackTrace.4.0.1\lib\net46\System.Diagnostics.StackTrace.dll</HintPath>
290290
<Private>True</Private>
291291
</Reference>
292+
<Reference Include="System.IO.Abstractions, Version=2.0.0.140, Culture=neutral, processorArchitecture=MSIL">
293+
<HintPath>..\..\packages\System.IO.Abstractions.2.0.0.140\lib\net40\System.IO.Abstractions.dll</HintPath>
294+
<Private>True</Private>
295+
</Reference>
292296
<Reference Include="System.IO.FileSystem, Version=4.0.1.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a, processorArchitecture=MSIL">
293297
<HintPath>..\..\packages\System.IO.FileSystem.4.0.1\lib\net46\System.IO.FileSystem.dll</HintPath>
294298
<Private>True</Private>

src/WebJobs.Script/packages.config

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@
5959
<package id="System.Dynamic.Runtime" version="4.0.11" targetFramework="net46" />
6060
<package id="System.Globalization" version="4.0.11" targetFramework="net46" />
6161
<package id="System.IO" version="4.1.0" targetFramework="net46" />
62+
<package id="System.IO.Abstractions" version="2.0.0.140" targetFramework="net46" />
6263
<package id="System.IO.FileSystem" version="4.0.1" targetFramework="net46" />
6364
<package id="System.IO.FileSystem.Primitives" version="4.0.1" targetFramework="net46" />
6465
<package id="System.Linq" version="4.1.0" targetFramework="net46" />

test/WebJobs.Script.Tests.Integration/NodeEndToEndTests.cs

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -334,12 +334,14 @@ public async Task SingleNamedExport()
334334
Assert.True(logs[1].Contains("Exports: IsObject=true, Count=1"));
335335
}
336336

337-
[Fact]
338-
public async Task HttpTrigger_Get()
337+
[Theory]
338+
[InlineData("httptrigger")]
339+
[InlineData("httptriggershared")]
340+
public async Task HttpTrigger_Get(string functionName)
339341
{
340342
HttpRequestMessage request = new HttpRequestMessage
341343
{
342-
RequestUri = new Uri(string.Format("http://localhost/api/httptrigger?name=Mathew%20Charles&location=Seattle")),
344+
RequestUri = new Uri($"http://localhost/api/{functionName}?name=Mathew%20Charles&location=Seattle"),
343345
Method = HttpMethod.Get,
344346
};
345347
request.SetConfiguration(Fixture.RequestConfiguration);

test/WebJobs.Script.Tests.Integration/Properties/Resources.Designer.cs

Lines changed: 7 additions & 7 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

test/WebJobs.Script.Tests.Integration/Properties/Resources.resx

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -149,23 +149,23 @@
149149
}</value>
150150
</data>
151151
<data name="DotNetFunctionJson" xml:space="preserve">
152-
<value>{
153-
"scriptFile":"DotNetFunctionAssembly.dll",
152+
<value>{{
153+
"scriptFile":"{0}",
154154
"entryPoint": "TestFunction.Function.Run",
155155
"bindings": [
156-
{
156+
{{
157157
"type": "httpTrigger",
158158
"name": "req",
159159
"direction": "in",
160160
"methods": [ "get" ]
161-
},
162-
{
161+
}},
162+
{{
163163
"type": "http",
164164
"name": "$return",
165165
"direction": "out"
166-
}
166+
}}
167167
]
168-
}</value>
168+
}}</value>
169169
</data>
170170
<data name="DotNetFunctionSource" xml:space="preserve">
171171
<value>using System.Net;

test/WebJobs.Script.Tests.Integration/RawAssemblyEndToEndTests.cs

Lines changed: 33 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
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;
45
using System.Collections.Generic;
56
using System.IO;
67
using System.Linq;
@@ -23,14 +24,25 @@ public RawAssemblyEndToEndTests(TestFixture fixture) : base(fixture)
2324

2425
[Fact]
2526
public async Task Invoking_DotNetFunction()
27+
{
28+
await InvokeDotNetFunction("DotNetFunction");
29+
}
30+
31+
[Fact]
32+
public async Task Invoking_DotNetFunctionShared()
33+
{
34+
await InvokeDotNetFunction("DotNetFunctionShared");
35+
}
36+
37+
public async Task InvokeDotNetFunction(string functionName)
2638
{
2739
var request = new HttpRequestMessage(HttpMethod.Get, "http://functions/myfunc");
2840
Dictionary<string, object> arguments = new Dictionary<string, object>()
2941
{
3042
{ "req", request }
3143
};
3244

33-
await Fixture.Host.CallAsync("DotNetFunction", arguments);
45+
await Fixture.Host.CallAsync(functionName, arguments);
3446

3547
HttpResponseMessage response = (HttpResponseMessage)request.Properties[ScriptConstants.AzureFunctionsHttpResponseKey];
3648

@@ -41,10 +53,14 @@ public class TestFixture : EndToEndTestFixture
4153
{
4254
private const string ScriptRoot = @"TestScripts\DotNet";
4355
private static readonly string FunctionPath;
56+
private static readonly string FunctionSharedPath;
57+
private static readonly string FunctionSharedBinPath;
4458

4559
static TestFixture()
4660
{
4761
FunctionPath = Path.Combine(ScriptRoot, "DotNetFunction");
62+
FunctionSharedPath = Path.Combine(ScriptRoot, "DotNetFunctionShared");
63+
FunctionSharedBinPath = Path.Combine(ScriptRoot, "DotNetFunctionSharedBin");
4864
CreateFunctionAssembly();
4965
}
5066

@@ -56,15 +72,17 @@ public override void Dispose()
5672
{
5773
base.Dispose();
5874

59-
if (Directory.Exists(FunctionPath))
60-
{
61-
Directory.Delete(FunctionPath, true);
62-
}
75+
Task.WaitAll(
76+
FileUtility.DeleteIfExistsAsync(FunctionPath),
77+
FileUtility.DeleteIfExistsAsync(FunctionSharedPath),
78+
FileUtility.DeleteIfExistsAsync(FunctionSharedBinPath));
6379
}
6480

6581
private static void CreateFunctionAssembly()
6682
{
6783
Directory.CreateDirectory(FunctionPath);
84+
Directory.CreateDirectory(FunctionSharedBinPath);
85+
Directory.CreateDirectory(FunctionSharedPath);
6886

6987
var syntaxTree = CSharpSyntaxTree.ParseText(Resources.DotNetFunctionSource);
7088
Compilation compilation = CSharpCompilation.Create("DotNetFunctionAssembly", new[] { syntaxTree })
@@ -75,10 +93,18 @@ private static void CreateFunctionAssembly()
7593
MetadataReference.CreateFromFile(typeof(Enumerable).Assembly.Location),
7694
MetadataReference.CreateFromFile(typeof(object).Assembly.Location));
7795

78-
var result = compilation.Emit(Path.Combine(FunctionPath, "DotNetFunctionAssembly.dll"));
96+
compilation.Emit(Path.Combine(FunctionPath, "DotNetFunctionAssembly.dll"));
97+
compilation.Emit(Path.Combine(FunctionSharedBinPath, "DotNetFunctionSharedAssembly.dll"));
7998

99+
CreateFunctionMetadata(FunctionPath, "DotNetFunctionAssembly.dll");
100+
CreateFunctionMetadata(FunctionSharedPath, $@"..\\{Path.GetFileName(FunctionSharedBinPath)}\\DotNetFunctionSharedAssembly.dll");
80101
// Create function metadata
81-
File.WriteAllText(Path.Combine(FunctionPath, "function.json"), Resources.DotNetFunctionJson);
102+
}
103+
104+
private static void CreateFunctionMetadata(string path, string scriptFilePath)
105+
{
106+
File.WriteAllText(Path.Combine(path, "function.json"),
107+
string.Format(Resources.DotNetFunctionJson, scriptFilePath));
82108
}
83109
}
84110
}
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
{
2+
"scriptFile": "../HttpTrigger/index.js",
3+
"bindings": [
4+
{
5+
"type": "httpTrigger",
6+
"name": "request",
7+
"direction": "in",
8+
"methods": [ "get", "post" ]
9+
},
10+
{
11+
"type": "http",
12+
"name": "response",
13+
"direction": "out"
14+
}
15+
]
16+
}

0 commit comments

Comments
 (0)