diff --git a/.gitignore b/.gitignore
index 78477c1..c3891d7 100644
--- a/.gitignore
+++ b/.gitignore
@@ -4,3 +4,4 @@ obj
*.user
node_modules
*.PublishSettings
+*_*
diff --git a/CloudPad.FunctionApp/CloudPad.FunctionApp.csproj b/CloudPad.FunctionApp/CloudPad.FunctionApp.csproj
index 3adcf6c..daa5f1a 100644
--- a/CloudPad.FunctionApp/CloudPad.FunctionApp.csproj
+++ b/CloudPad.FunctionApp/CloudPad.FunctionApp.csproj
@@ -20,6 +20,7 @@
C:\Program Files (x86)\LINQPad5\LINQPad.exe
+ true
diff --git a/CloudPad.FunctionApp/HttpTrigger.cs b/CloudPad.FunctionApp/HttpTrigger.cs
index df03190..622db5d 100644
--- a/CloudPad.FunctionApp/HttpTrigger.cs
+++ b/CloudPad.FunctionApp/HttpTrigger.cs
@@ -1,6 +1,8 @@
using CloudPad.Internal;
using Microsoft.Azure.WebJobs;
using Microsoft.Azure.WebJobs.Host;
+using System;
+using System.Net;
using System.Net.Http;
using System.Threading.Tasks;
@@ -22,7 +24,16 @@ public static async Task Run(
arguments.AddArgument(typeof(TraceWriter), log);
arguments.AddArgument(typeof(ILogger), new TraceWriterLogger(log));
- var result = await func.InvokeAsync(arguments, log);
+ object result;
+ try {
+ result = await func.InvokeAsync(arguments, log);
+ } catch (ArgumentException ex) {
+ // special sauce for HTTP trigger
+ log.Error(ex.Message, ex);
+ return req.CreateResponse(HttpStatusCode.BadRequest, new { ok = false, message = ex.Message });
+ } catch {
+ throw;
+ }
var taskWithValue = result as Task;
if (taskWithValue != null) {
diff --git a/CloudPad/CommandLine.cs b/CloudPad/CommandLine.cs
index 9b03e37..730451c 100644
--- a/CloudPad/CommandLine.cs
+++ b/CloudPad/CommandLine.cs
@@ -7,6 +7,7 @@ class Options {
public bool compile;
public bool publish;
public bool prepare;
+ public bool install;
public string out_dir;
}
diff --git a/CloudPad/FirstRun.cs b/CloudPad/FirstRun.cs
new file mode 100644
index 0000000..ea97d85
--- /dev/null
+++ b/CloudPad/FirstRun.cs
@@ -0,0 +1,72 @@
+using Microsoft.Win32;
+using System;
+using System.Collections.Generic;
+using System.Diagnostics;
+using System.IO;
+using System.Linq;
+using System.Reflection;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace CloudPad {
+ static class FirstRun {
+ public static string Lockfile => Path.Combine(Env.GetLocalAppDataDirectory(), "first_run");
+
+ public static bool ShouldPrompt() {
+ if (Environment.UserInteractive) {
+ if (!File.Exists(Lockfile)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ public static void Prompt() {
+ var mbType = Type.GetType("System.Windows.Forms.MessageBox, System.Windows.Forms");
+
+ var show = mbType.GetMethod("Show", BindingFlags.Public | BindingFlags.Static, null, new[] {
+ typeof(string),
+ typeof(string),
+ Type.GetType("System.Windows.Forms.MessageBoxButtons, System.Windows.Forms"),
+ Type.GetType("System.Windows.Forms.MessageBoxIcon, System.Windows.Forms"),
+ }, null);
+
+ var result = show.Invoke(null, new object[] {
+ "Looks like this is your first run. Would you like to add a Explorer context menu item to help with deployment of scripts to Azure?",
+ "Welcome to CloudPad!",
+ 4, // YesNo
+ 0x20 // Question
+ });
+
+ if (Convert.ToInt32(result) == 6) { // Yes
+ var startInfo = new ProcessStartInfo();
+
+ startInfo.FileName = @"C:\Program Files (x86)\LINQPad5\LPRun.exe";
+ startInfo.Arguments = $"\"{Util.CurrentQueryPath}\" -install";
+ startInfo.UseShellExecute = true;
+ startInfo.Verb = "runas";
+#if !DEBUG
+ startInfo.WindowStyle = ProcessWindowStyle.Hidden;
+#endif
+
+ using (var p = Process.Start(startInfo)) {
+ p.WaitForExit();
+ }
+ }
+
+ File.WriteAllText(Lockfile, "");
+ }
+
+ public static void Install() {
+ using (var shell = Registry.ClassesRoot.OpenSubKey(@"LINQPad\shell", true)) {
+ using (var publish = shell.CreateSubKey("publish", true)) {
+ publish.SetValue("", "Publish LINQPad script to Azure");
+ publish.SetValue("Icon", @"C:\\Program Files (x86)\\LINQPad5\\LINQPad.EXE,0");
+ using (var command = publish.CreateSubKey("command", true)) {
+ command.SetValue("", "\"C:\\Program Files (x86)\\LINQPad5\\LPRun.EXE\" \"%1\" -publish");
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/CloudPad/InternalsVisibleTo.cs b/CloudPad/InternalsVisibleTo.cs
deleted file mode 100644
index 2658265..0000000
--- a/CloudPad/InternalsVisibleTo.cs
+++ /dev/null
@@ -1,3 +0,0 @@
-using System.Runtime.CompilerServices;
-
-[assembly: InternalsVisibleTo("CloudPad.FunctionApp")]
\ No newline at end of file
diff --git a/CloudPad/Program.cs b/CloudPad/Program.cs
index eb9cd8e..81ff888 100644
--- a/CloudPad/Program.cs
+++ b/CloudPad/Program.cs
@@ -39,6 +39,8 @@ public static async Task MainAsync(object userQuery, string[] args) {
var LPRun = false;
if ("LPRun.exe".Equals(Process.GetCurrentProcess().MainModule.ModuleName, StringComparison.OrdinalIgnoreCase)) {
LPRun = true;
+
+ // pipe trace to console
Trace.Listeners.Add(new ConsoleTraceListener());
}
@@ -65,6 +67,10 @@ public static async Task MainAsync(object userQuery, string[] args) {
if (args.Length == 0) {
// todo: storage emulator?
+ if (FirstRun.ShouldPrompt()) {
+ FirstRun.Prompt();
+ }
+
var workingDirectory = Path.Combine(Env.GetLocalAppDataDirectory(), currentQueryPathInfo.InstanceId);
FunctionApp.Deploy(workingDirectory);
@@ -110,6 +116,9 @@ public static async Task MainAsync(object userQuery, string[] args) {
Compiler.Compile(userQueryInfo, currentQueryInfo, compilationOptions, currentQueryInfo);
Trace.WriteLine($"Done. Output written to '{compilationOptions.OutDir}'");
return 0;
+ } else if(options.install) {
+ FirstRun.Install();
+ return 0;
}
} catch (Exception ex) {
if (Environment.UserInteractive) {
diff --git a/CloudPad/Properties.cs b/CloudPad/Properties.cs
new file mode 100644
index 0000000..9c0f76b
--- /dev/null
+++ b/CloudPad/Properties.cs
@@ -0,0 +1,4 @@
+using System.Reflection;
+using System.Runtime.CompilerServices;
+
+[assembly: InternalsVisibleTo("CloudPad.FunctionApp")]
\ No newline at end of file
diff --git a/CloudPad/internal/AssemblyBindingConfig.cs b/CloudPad/internal/AssemblyBindingConfig.cs
new file mode 100644
index 0000000..c03de54
--- /dev/null
+++ b/CloudPad/internal/AssemblyBindingConfig.cs
@@ -0,0 +1,72 @@
+using System;
+using System.Collections.Generic;
+using System.Reflection;
+
+namespace CloudPad.Internal {
+ class AssemblyBindingRedirect {
+ public Version OldMinVersion { get; set; }
+ public Version OldMaxVersion { get; set; }
+
+ public Version NewVersion { get; set; }
+ }
+
+ class AssemblyBindingConfig {
+ public static AssemblyBindingConfig LoadFrom(string path) {
+ var config = new AssemblyBindingConfig();
+
+ var funcConfig = System.Xml.Linq.XElement.Load(path);
+ var runtime = funcConfig.Element("runtime");
+
+ System.Xml.Linq.XNamespace ns = "urn:schemas-microsoft-com:asm.v1";
+ var assemblyBinding = runtime.Element(ns + "assemblyBinding");
+ var assemblyIdentityName = ns + "assemblyIdentity";
+ var bindingRedirectName = ns + "bindingRedirect";
+ foreach (var dependentAssembly in assemblyBinding.Elements(ns + "dependentAssembly")) {
+ var assemblyIdentity = dependentAssembly.Element(assemblyIdentityName);
+
+ var fullName = (string)assemblyIdentity.Attribute("name");
+
+ if ((string)assemblyIdentity.Attribute("culture") != null) {
+ fullName += ", Culture=" + (string)assemblyIdentity.Attribute("culture");
+ }
+
+ if ((string)assemblyIdentity.Attribute("publicKeyToken") != null) {
+ fullName += ", PublicKeyToken=" + (string)assemblyIdentity.Attribute("publicKeyToken");
+ }
+
+ var assemblyName = new AssemblyName(fullName);
+
+ var bindingRedirect = dependentAssembly.Element(bindingRedirectName);
+
+ var oldVersion = ((string)bindingRedirect.Attribute("oldVersion")).Split('-');
+ var newVerison = (string)bindingRedirect.Attribute("newVersion");
+
+ var binding = new AssemblyBindingRedirect {
+ OldMinVersion = new Version(oldVersion[0]),
+ OldMaxVersion = new Version(oldVersion[1]),
+ NewVersion = new Version(newVerison),
+ };
+
+ config.Add(assemblyName.Name, binding);
+ }
+
+ return config;
+ }
+
+ private Dictionary> config = new Dictionary>();
+
+ public void Add(string name, AssemblyBindingRedirect binding) {
+ if (!config.TryGetValue(name, out var bindings)) {
+ config.Add(name, bindings = new List());
+ }
+ bindings.Add(binding);
+ }
+
+ public AssemblyBindingRedirect Find(AssemblyName name) {
+ if (config.TryGetValue(name.Name, out var bindings)) {
+ return bindings.Find(binding => binding.OldMinVersion <= name.Version && name.Version <= binding.OldMaxVersion);
+ }
+ return null;
+ }
+ }
+}
diff --git a/CloudPad/internal/AssemblyCandidateSet.cs b/CloudPad/internal/AssemblyCandidateSet.cs
new file mode 100644
index 0000000..bc4386c
--- /dev/null
+++ b/CloudPad/internal/AssemblyCandidateSet.cs
@@ -0,0 +1,48 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Reflection;
+
+namespace CloudPad.Internal {
+ class AssemblyCandidate {
+ public string FullName => Name.FullName;
+ public AssemblyName Name { get; set; }
+ public string Location { get; set; }
+ public string Source { get; set; }
+ }
+
+ class AssemblyCandidateSet {
+ private readonly Dictionary> _d = new Dictionary>(StringComparer.OrdinalIgnoreCase);
+
+ public IEnumerable>> Set {
+ get {
+ return _d.OrderBy(x => x.Key); // sort is just to make debugging easier
+ }
+ }
+
+ public void Add(string location, AssemblyName name, string source) {
+ if (!_d.TryGetValue(name.Name, out var list)) {
+ _d.Add(name.Name, list = new List());
+ }
+
+ var candidiate = list.Find(c => c.FullName == name.FullName);
+ if (candidiate == null) {
+ list.Add(new AssemblyCandidate { Location = location, Name = name, Source = source });
+ }
+ }
+
+ public bool Unref(AssemblyName name) {
+ if (_d.TryGetValue(name.Name, out var list)) {
+ var candidiate = list.FindIndex(c => c.FullName == name.FullName);
+ if (candidiate != -1) {
+ list.RemoveAt(candidiate);
+ if (list.Count == 0) {
+ _d.Remove(name.Name);
+ }
+ return true;
+ }
+ }
+ return false;
+ }
+ }
+}
diff --git a/CloudPad/internal/Compiler.cs b/CloudPad/internal/Compiler.cs
index b88991b..6dce489 100644
--- a/CloudPad/internal/Compiler.cs
+++ b/CloudPad/internal/Compiler.cs
@@ -88,105 +88,100 @@ public static void Compile(UserQueryTypeInfo userQuery, QueryInfo currentQuery,
// ====
var lib = Path.Combine(options.OutDir, "scripts", options.QueryName + "_" + userQuery.Id);
-
Directory.CreateDirectory(lib);
-
- foreach (var location in userAssemblies) {
- var destination = Path.Combine(lib, Path.GetFileName(location));
+ foreach (var userAssembly in userAssemblies) {
+ var destination = Path.Combine(lib, Path.GetFileName(userAssembly.Name.Name + ".dll")); // stabilize DLL name (simplifies assembly resolve)
if (File.Exists(destination)) {
continue;
}
- File.Copy(location, destination);
+ File.Copy(userAssembly.Location, destination);
}
// ====
var root = VirtualFileSystemRoot.GetRoot();
-
root.SaveTo(lib);
// ====
- Debug.WriteLine($"==== Compiler pass end (out='{options.OutDir}') ====", nameof(Compiler));
+ Debug.WriteLine($"==== Compiler pass end, OutDir= {options.OutDir} ====", nameof(Compiler));
}
- class CandidateSet {
- public class Candidate {
- public string FullName => Name.FullName;
- public AssemblyName Name { get; set; }
- public Version Version => Name.Version;
- public string Location { get; set; }
- public string Source { get; set; }
- }
-
- public class CandidateList {
- public List Candidates { get; } = new List();
+ private static List LoadAllUserAssemblies2(UserQueryTypeInfo userQuery, QueryInfo currentQuery) {
+ // strategy for finding what assembly version to bundle
+ // whenever there is a version ambiguity, we will remove the version that CloudPad referenced
+ // (multiple versions show up because users bring in different code not same)
- public void Add(Candidate candidate) {
- if (Candidates.Any(c => c.Version == candidate.Version)) {
- return; // has version
- }
- Candidates.Add(candidate);
- }
- }
+ var cs = new AssemblyCandidateSet();
- private readonly Dictionary _d = new Dictionary();
+ // the purpose of this code is to get the typed data context, if used
- public IEnumerable> Set {
- get { return _d.OrderBy(x => x.Key); }
- }
+ cs.Add(userQuery.Assembly.Location, userQuery.Assembly.GetName(), "");
- public void Add(string f, string source) {
- var name = AssemblyName.GetAssemblyName(f);
- if (!_d.TryGetValue(name.Name, out var list)) {
- _d.Add(name.Name, list = new CandidateList());
+ foreach (var r in userQuery.Assembly.GetReferencedAssemblies()) {
+ var b = AppDomain.CurrentDomain.GetAssemblies().FirstOrDefault(a => a.FullName == r.FullName); // if exact
+ if (b != null) {
+ cs.Add(b.Location, r, "");
+ } else {
+ var referencedAssembly = Assembly.Load(r);
+ if (referencedAssembly.FullName == r.FullName) {
+ cs.Add(referencedAssembly.Location, r, "");
+ }
}
- list.Add(new Candidate { Name = name, Location = f, Source = source });
}
- }
- private static List LoadAllUserAssemblies2(UserQueryTypeInfo userQuery, QueryInfo currentQuery) {
-
- // strategy for finding what assembly version to bundle
-
- // whenever there is a version ambiguity, we will remove the version that CloudPad referenced
- // (multiple versions show up because users bring in different code not same)
-
-
- var cs = new CandidateSet();
+ // the rest is solved for us by LINQPad
foreach (var f in currentQuery.GetFileReferences()) {
- cs.Add(f, f);
+ var name = AssemblyName.GetAssemblyName(f);
+ cs.Add(f, name, "");
}
foreach (var nuget in currentQuery.GetNuGetReferences()) {
var packageID = nuget.PackageID;
foreach (var f in nuget.GetAssemblyReferences()) {
- cs.Add(f, packageID);
+ var name = AssemblyName.GetAssemblyName(f);
+ cs.Add(f, name, packageID);
}
}
- Extensions.Dump(cs);
-
// ====
- var list = new List { userQuery.Assembly };
-
- // ====
+ void unrefReferencedAssemblies(Assembly assembly) {
+ foreach (var r in assembly.GetReferencedAssemblies()) {
+ Debug.WriteLine($"Unref '{r.FullName}'", "Unref");
+ if (cs.Unref(r)) {
+ var b = AppDomain.CurrentDomain.GetAssemblies().FirstOrDefault(a => a.FullName == r.FullName); // if exact
+ if (b != null) {
+ unrefReferencedAssemblies(b);
+ } else {
+ Debug.WriteLine($"Load '{r}'", "Unref");
+ var referencedAssembly = Assembly.Load(r);
+ Debug.WriteLine($"Loaded '{referencedAssembly.FullName}'", "Unref");
+ if (referencedAssembly.FullName == r.FullName) { // if exact
+ unrefReferencedAssemblies(referencedAssembly);
+ }
+ }
+ }
+ }
+ }
var cp = typeof(Program).Assembly; // CloudPad assembly
- // ====
+ // Unref everything that CloudPad brings in
+ // we don't need it and it will intermingle
+ // with other versions which we don't want.
- var excludeFullName = new HashSet {
- cp.FullName,
- };
- foreach (var r in cp.GetReferencedAssemblies()) {
- excludeFullName.Add(r.FullName);
+ if (cs.Unref(cp.GetName())) {
+ unrefReferencedAssemblies(cp);
}
// ====
+ var fs = new List();
+
+ // ====
+
var excludeLocations = new List() {
// Ignore stuff from LINQPad installation dir
CanonicalDirectoryName(Path.GetDirectoryName(Assembly.Load("LINQPad").Location)),
@@ -209,172 +204,27 @@ bool shouldExcludeLocation(string location) {
// ====
- void walkReferencedAssemblies(Assembly assembly) {
- foreach (var r in assembly.GetReferencedAssemblies()) {
- if (excludeFullName.Contains(r.FullName)) {
- continue;
- }
- Debug.WriteLine($"ReferencedAssembly '{r}'", nameof(Compiler));
- var referencedAssembly = Assembly.Load(r.FullName);
- Debug.WriteLine($"Assembly loaded from '{referencedAssembly.Location}'", nameof(Compiler));
- if (shouldExcludeLocation(referencedAssembly.Location)) {
- Debug.WriteLine($"Assembly excluded by location", nameof(Compiler));
- continue;
- }
- list.Add(referencedAssembly);
- walkReferencedAssemblies(referencedAssembly);
- }
- }
-
- walkReferencedAssemblies(userQuery.Assembly);
-
- // ====
-
- foreach (var g in list.Select(x => {
- var name = x.GetName();
- return new {
- name.Name,
- name.Version,
- Assembly = x
- };
- }).GroupBy(x => x.Name).OrderBy(x => x.Key)) {
- if (1 < g.Count()) {
- Debug.WriteLine($"Assembly '{g.Key}' has multiple copies", nameof(Compiler));
- foreach (var assembly in g) {
- Debug.WriteLine($" - '{assembly.Version}', '{assembly.Assembly.Location}'", nameof(Compiler));
- }
- }
- }
-
- // ====
-
- return list.Select(x => x.Location).OrderBy(x => x).ToList();
- }
-
-
- private static List LoadAllUserAssemblies(List functions) {
-
- //// This trick will ensure that the dependant assemblies get loaded
- //foreach (var f in functions) {
- // System.Runtime.CompilerServices.RuntimeHelpers.PrepareMethod(f.Method.MethodHandle);
- //}
-
- var linqPadAssembly = Assembly.Load("LINQPad");
-
- var visited = new HashSet {
- linqPadAssembly.FullName,
-
- // Not supported on the desktop (but I don't understand why it's getting pulled in here)
- "System.Runtime.Loader, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a",
- };
-
- foreach (var r in linqPadAssembly.GetReferencedAssemblies().OrderBy(r => r.FullName)) {
- Debug.WriteLine($"Ignore assembly '{r.FullName}' referenced by LINQPad");
- visited.Add(r.FullName);
- }
-
- void WalkReferencedAssemblies(Assembly assembly) {
- if (assembly.IsDynamic) {
- return;
- }
- Debug.WriteLine($"Walk assembly '{assembly.FullName}'");
- foreach (var r in assembly.GetReferencedAssemblies().OrderBy(r => r.FullName)) {
- if (visited.Add(r.FullName)) {
- Debug.WriteLine($"Load assembly '{r.FullName}'");
- Assembly referencedAssembly;
- try {
- referencedAssembly = Assembly.Load(r);
- Debug.WriteLine($"Assembly '{r.FullName}' loaded from location '{referencedAssembly.Location}'");
- } catch {
- // track
- Debug.WriteLine($"Cannot load assembly '{r}' referenced by '{assembly.GetName()}'");
- throw;
- }
- WalkReferencedAssemblies(referencedAssembly);
- }
- }
- };
-
- foreach (var assembly in AppDomain.CurrentDomain.GetAssemblies()) {
- if (visited.Add(assembly.FullName)) {
- WalkReferencedAssemblies(assembly);
- }
- }
-
- // There are alot of assemblies that don't need to be added since they are provded by the hosting environment
- // we filter out these here, it's important that we don't filter out actual dependencies because if we do
- // the code won't run
-
- var exclude = new[]{
- // Ignore stuff from the LINQPad installation dir
- CanonicalDirectoryName(Path.GetDirectoryName(linqPadAssembly.Location)),
-
- // Ignore stuff from the azure-functions-core-tools installation dir
- CanonicalDirectoryName(Env.GetProgramDataDirectory()), // only necessary when running from within LINQPad but it doesn't hurt
-
- // Ignore stuff from the Windows dir
- CanonicalDirectoryName(Environment.GetEnvironmentVariable("WINDIR")),
- };
-
- var excludeAssemblyByFullName = new HashSet {
- typeof(System.Web.Http.IHttpActionResult).Assembly.FullName // ASP.NET MVC 5 stack
- };
-
- // If it's a reference of CloudPad we don't need to pack it
- var cloudPadAssembly = typeof(Program).Assembly;
- excludeAssemblyByFullName.Add(cloudPadAssembly.FullName);
- foreach (var assemblyRef in cloudPadAssembly.GetReferencedAssemblies()) {
- excludeAssemblyByFullName.Add(assemblyRef.FullName);
- }
-
- var list = new List();
-
- foreach (var assembly in AppDomain.CurrentDomain.GetAssemblies().OrderBy(x => x.FullName)) {
- if (assembly.IsDynamic) {
- continue;
- }
-
- var assemblyFullPath = assembly.Location;
-
- // Root out assemblies that are not files on disk
-
- if (string.IsNullOrEmpty(assemblyFullPath)) {
- continue;
- }
-
- if (!File.Exists(assemblyFullPath)) {
- continue;
- }
-
- // ====
-
- // Ignore assemblies that originate in the .NET Framework (or LINQPad)
-
- var skip = false;
- foreach (var prefix in exclude) {
- if (assemblyFullPath.StartsWith(prefix, StringComparison.OrdinalIgnoreCase)) {
- skip = true;
- break;
+ foreach (var item in cs.Set) {
+ var list = item.Value;
+ var c = list[0];
+ if (1 < list.Count) {
+ Trace.WriteLine($"Warning: Multiple versions of assembly '{c.Name.Name}' found.");
+ Trace.WriteLine($"Warning: Assembly '{c.Name.FullName}' from location '{c.Location}' used.");
+ Trace.WriteLine("Warning: The following verion(s) will not be used:");
+ for (int i = 1; i < list.Count; i++) {
+ var d = list[i];
+ Trace.WriteLine($"Warning: Assembly '{d.FullName}' from location '{d.Location}' ignored.");
}
}
- if (skip) {
- Debug.WriteLine($"Excluded '{assembly.FullName}'", nameof(Compiler));
+ if (shouldExcludeLocation(c.Location)) {
continue;
}
+ fs.Add(c);
+ };
- // ====
-
- if (excludeAssemblyByFullName.Contains(assembly.FullName)) {
- Debug.WriteLine($"Excluded '{assembly.FullName}'", nameof(Compiler));
- continue;
- }
-
- list.Add(assemblyFullPath);
-
- Debug.WriteLine($"Included '{assembly.FullName}'\n from '{assemblyFullPath}'", nameof(Compiler));
- }
+ // ====
- return list;
+ return fs;
}
private static string CanonicalDirectoryName(string path) {
diff --git a/CloudPad/internal/FunctionApp.cs b/CloudPad/internal/FunctionApp.cs
index a01cc73..649c955 100644
--- a/CloudPad/internal/FunctionApp.cs
+++ b/CloudPad/internal/FunctionApp.cs
@@ -1,5 +1,4 @@
-using System;
-using System.Diagnostics;
+using System.Diagnostics;
using System.IO;
using System.IO.Compression;
using System.Net;
diff --git a/CloudPad/internal/JobHost.cs b/CloudPad/internal/JobHost.cs
index 678d131..b38b886 100644
--- a/CloudPad/internal/JobHost.cs
+++ b/CloudPad/internal/JobHost.cs
@@ -1,5 +1,4 @@
using System;
-using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.IO.Compression;
@@ -28,48 +27,15 @@ private static string GetAzureFunctionsCoreTools(string version) {
return funcDir;
}
- public static void Prepare() {
+ public static string Prepare() {
// this should be done exactly once before any call to `LaunchAsync`
var azureFunctionsCoreTools = GetAzureFunctionsCoreTools("1.0.19");
- var funcConfig = System.Xml.Linq.XElement.Load(Path.Combine(azureFunctionsCoreTools, "func.exe.config"));
- var runtime = funcConfig.Element("runtime");
-
- var assemblyBindingRedirects = new Dictionary();
-
- System.Xml.Linq.XNamespace ns = "urn:schemas-microsoft-com:asm.v1";
- var assemblyBinding = runtime.Element(ns + "assemblyBinding");
- var assemblyIdentityName = ns + "assemblyIdentity";
- var bindingRedirectName = ns + "bindingRedirect";
- foreach (var dependentAssembly in assemblyBinding.Elements(ns + "dependentAssembly")) {
- var assemblyIdentity = dependentAssembly.Element(assemblyIdentityName);
-
- var fullName = (string)assemblyIdentity.Attribute("name");
-
- if ((string)assemblyIdentity.Attribute("culture") != null) {
- fullName += ", Culture=" + (string)assemblyIdentity.Attribute("culture");
- }
-
- if ((string)assemblyIdentity.Attribute("publicKeyToken") != null) {
- fullName += ", PublicKeyToken=" + new StrongNameKeyPair((string)assemblyIdentity.Attribute("publicKeyToken"));
- }
-
- var assemblyName = new AssemblyName(fullName);
-
- var bindingRedirect = dependentAssembly.Element(bindingRedirectName);
- var oldVersion = ((string)bindingRedirect.Attribute("oldVersion")).Split('-');
- assemblyBindingRedirects[assemblyName.FullName] = new {
- minVersion = new Version(oldVersion[0]),
- maxVersion = new Version(oldVersion[1]),
- newVersion = new Version((string)bindingRedirect.Attribute("newVersion")),
- };
- }
-
- Extensions.Dump(assemblyBindingRedirects);
+ var assemblyBindings = AssemblyBindingConfig.LoadFrom(Path.Combine(azureFunctionsCoreTools, "func.exe.config"));
AppDomain.CurrentDomain.AssemblyResolve += (sender, e) => {
- // note: e.RequestingAssembly is always null (for some reason?)
+ // note: e.RequestingAssembly is always null (we don't need it and shouldn't use it)
Debug.WriteLine($"AssemblyResolve '{e.Name}'", "func.exe");
@@ -82,10 +48,19 @@ public static void Prepare() {
foreach (var probePath in probePaths) {
if (File.Exists(probePath)) {
- var probeAssemblyName = AssemblyName.GetAssemblyName(probePath);
- if (probeAssemblyName.FullName == e.Name) {
- Debug.WriteLine($"ResolvedAssembly '{e.Name}'", "func.exe");
+ var probeName = AssemblyName.GetAssemblyName(probePath);
+
+ // look for redirect
+ var bindingRedirect = assemblyBindings.Find(probeName);
+ if (bindingRedirect != null) {
+ if (bindingRedirect.NewVersion == probeName.Version) {
+ Debug.WriteLine($"ResolvedAssembly '{e.Name}'", "func.exe");
+ return Assembly.LoadFrom(probePath);
+ }
+ }
+ if (probeName.FullName == e.Name) {
+ Debug.WriteLine($"ResolvedAssembly '{e.Name}'", "func.exe");
return Assembly.LoadFrom(probePath);
}
}
@@ -95,125 +70,52 @@ public static void Prepare() {
return null;
};
- //var funcAssembly = Assembly.LoadFrom(Path.Combine(azureFunctionsCoreTools, "func.exe"));
-
- //var pass2 = new List();
-
- //foreach (var r in funcAssembly.GetReferencedAssemblies()) {
- // var probePaths = new[] {
- // Path.Combine(azureFunctionsCoreTools, r.Name + ".dll"), // DLL first
- // Path.Combine(azureFunctionsCoreTools, r.Name + ".exe"),
- // };
-
- // foreach (var probePath in probePaths) {
- // if (File.Exists(probePath)) {
- // Debug.WriteLine($"ResolvedAssembly '{r.Name}' form location '{probePath}'", "func.exe");
- // pass2.Add(Assembly.LoadFrom(probePath));
- // }
- // }
- //}
-
- //foreach (var funcAssembly2 in pass2) {
- // foreach (var r in funcAssembly2.GetReferencedAssemblies()) {
- // var probePaths = new[] {
- // Path.Combine(azureFunctionsCoreTools, r.Name + ".dll"), // DLL first
- // Path.Combine(azureFunctionsCoreTools, r.Name + ".exe"),
- // };
-
- // foreach (var probePath in probePaths) {
- // if (File.Exists(probePath)) {
- // Debug.WriteLine($"ResolvedAssembly '{r.Name}' form location '{probePath}'", "func.exe");
- // Assembly.LoadFrom(probePath);
- // }
- // }
- // }
- //}
+ return azureFunctionsCoreTools;
}
public static async Task LaunchAsync(string functionAppDirectory) {
// StartHostAction.RunAsync
// https://github.com/Azure/azure-functions-core-tools/blob/1.0.19/src/Azure.Functions.Cli/Actions/HostActions/StartHostAction.cs#L102-L143
- // The code does this based on the 1.0.19 release of the azure-functions-core-tools
- // We use reflection so that the CloudPad assembly (and NuGet package) doesn't depend on
- // the job host
-
- // the primary reasons are
- // 1. type ambiguity
- // 2. file size
-
- var azureFunctionsCoreTools = GetAzureFunctionsCoreTools("1.0.19");
-
// ================
- // this implementation is worth revising with respect to the following documentation
- // https://docs.microsoft.com/en-us/dotnet/framework/app-domains/resolve-assembly-loads?view=netframework-4.8
-
- // for some reason, I ran into a failure to resolve "System.Runtime.Loader"
- // the next day after I restarted all applications (not a reboot) the problem wasn't there
- // and I used procmon to note that "System.Runtime.Loader" was loaded from GAC
-
- //AppDomain.CurrentDomain.AssemblyResolve += (sender, e) => {
- // Debug.WriteLine($"AssemblyResolve '{e.Name}'", "func.exe");
- // if (e.RequestingAssembly != null) {
- // Debug.WriteLine($" RequestingAssembly '{e.RequestingAssembly}'", "func.exe");
- // }
-
- // //I don't remember precisly if this is useful or not, couldn't tell that it was, so...
- // //foreach (var assembly in AppDomain.CurrentDomain.GetAssemblies()) {
- // // if (assembly.FullName == e.Name) {
- // // Debug.WriteLine($"ResolvedAssembly '{e.Name}' from loaded set of assemblies", "func.exe");
-
- // // return assembly;
- // // }
- // //}
-
- // var name = new AssemblyName(e.Name);
-
- // var probePaths = new[] {
- // Path.Combine(azureFunctionsCoreTools, name.Name + ".dll"), // DLL first
- // Path.Combine(azureFunctionsCoreTools, name.Name + ".exe"),
- // };
-
- // foreach (var probePath in probePaths) {
- // if (File.Exists(probePath)) {
- // Debug.WriteLine($"ResolvedAssembly '{e.Name}' form location '{probePath}'", "func.exe");
- // return Assembly.LoadFrom(probePath);
- // }
- // }
-
- // Debug.WriteLine($"UnresolvedAssembly '{e.Name}'", "func.exe");
- // return null;
- //};
+ var azureFunctionsCoreTools = GetAzureFunctionsCoreTools("1.0.19");
// ================
var funcAssembly = Assembly.LoadFrom(Path.Combine(azureFunctionsCoreTools, "func.exe"));
+ Debug.WriteLine("new StartHostAction();", "func.exe");
var startHostAction = funcAssembly.GetType("Azure.Functions.Cli.Actions.HostActions.StartHostAction");
var action = Activator.CreateInstance(startHostAction, new object[] { null });
// Utilities.PrintLogo(); // skip this, the log is spammy enough as it is
+ Debug.WriteLine("var scriptPath = ScriptHostHelpers.GetFunctionAppRootDirectory(...);", "func.exe");
var scriptHostHelpers = funcAssembly.GetType("Azure.Functions.Cli.Helpers.ScriptHostHelpers");
var getFunctionAppRootDirectory = scriptHostHelpers.GetMethod("GetFunctionAppRootDirectory", BindingFlags.Public | BindingFlags.Static);
- var getTraceLevel = scriptHostHelpers.GetMethod("GetTraceLevel", BindingFlags.NonPublic | BindingFlags.Static);
var scriptPath = getFunctionAppRootDirectory.Invoke(null, new object[] { functionAppDirectory });
+
+ Debug.WriteLine("var traceLevel = await ScriptHostHelpers.GetTraceLevel(scriptPath);", "func.exe");
+ var getTraceLevel = scriptHostHelpers.GetMethod("GetTraceLevel", BindingFlags.NonPublic | BindingFlags.Static);
var traceLevelTask = (Task)getTraceLevel.Invoke(null, new object[] { scriptPath });
await traceLevelTask;
var traceLevel = GetTaskResult(traceLevelTask);
+ Debug.WriteLine("var settings = SelfHostWebHostSettingsFactory.Create(traceLevel, scriptPath);", "func.exe");
var selfHostWebHostSettingsFactory = funcAssembly.GetType("Azure.Functions.Cli.Common.SelfHostWebHostSettingsFactory");
var create = selfHostWebHostSettingsFactory.GetMethod("Create", BindingFlags.Public | BindingFlags.Static);
var settings = create.Invoke(null, new object[] { traceLevel, scriptPath });
- // Setup(); // skip this, it just does some URLACL validation with an ugly popup
+ // Setup(); // skip
var baseAddress = new Uri("http://localhost:7071/");
- // ReadSecrets(scriptPath, baseAddress); // skip this, it's hardcoded to use Environment.CurrentDirectory
+ // hardcoded to use Environment.CurrentDirectory
+ // ReadSecrets(scriptPath, baseAddress); // skip
var selfHostAssembly = Assembly.LoadFrom(Path.Combine(azureFunctionsCoreTools, "System.Web.Http.SelfHost.dll"));
+ Debug.WriteLine("var config = new HttpSelfHostConfiguration(baseAddress);", "func.exe");
var httpSelfHostConfiguration = selfHostAssembly.GetType("System.Web.Http.SelfHost.HttpSelfHostConfiguration");
var config = Activator.CreateInstance(httpSelfHostConfiguration, new object[] { baseAddress });
httpSelfHostConfiguration.GetProperty("IncludeErrorDetailPolicy").SetValue(config, 2); // Always
diff --git a/CloudPad/triggers/BlobTriggerAttribute.cs b/CloudPad/triggers/BlobTriggerAttribute.cs
index d0e647f..324dd1c 100644
--- a/CloudPad/triggers/BlobTriggerAttribute.cs
+++ b/CloudPad/triggers/BlobTriggerAttribute.cs
@@ -20,7 +20,7 @@ string ITriggerAttribute.GetEntryPoint() {
}
Type[] ITriggerAttribute.GetRequiredParameterTypes() {
- throw new NotImplementedException();
+ return new[] { typeof(Microsoft.WindowsAzure.Storage.Blob.CloudBlockBlob) };
}
}
}
diff --git a/DESIGN.md b/DESIGN.md
index eb2326d..c878063 100644
--- a/DESIGN.md
+++ b/DESIGN.md
@@ -67,5 +67,18 @@ Windows Registry Editor Version 5.00
"Icon"="C:\\Program Files (x86)\\LINQPad5\\LINQPad.EXE,0"
[HKEY_CLASSES_ROOT\LINQPad\shell\publish\command]
-@="\"C:\\Program Files (x86)\\LINQPad5\\LPRun.EXE\" \"%1\" -publish"
+@=""
```
+
+# LINQPad Version
+
+`5.36.03`
+
+# CloudPad 3
+
+CloudPad 3 will target .NET Core 3 and LINQPad 6
+
+# Func 1.0.19
+
+see https://github.com/Azure/azure-functions-core-tools/blob/1.0.19/src/Azure.Functions.Cli/Azure.Functions.Cli.csproj
+
diff --git a/Directory.Build.props b/Directory.Build.props
index 45249d7..b8100da 100644
--- a/Directory.Build.props
+++ b/Directory.Build.props
@@ -4,5 +4,9 @@
John Leidegren
Tessin Nordic AB
CloudPad
+ 2.0.0
+
+ 2.0.0.0
+ 2.0.0.0
\ No newline at end of file
diff --git a/cloud-pad.sln b/cloud-pad.sln
index c5fbbb2..14c96fc 100644
--- a/cloud-pad.sln
+++ b/cloud-pad.sln
@@ -69,12 +69,12 @@ Global
{5B077C09-B76A-4B2C-A46C-772CADED29FF}.Release|x64.Build.0 = Release|Any CPU
{5B077C09-B76A-4B2C-A46C-772CADED29FF}.Release|x86.ActiveCfg = Release|Any CPU
{5B077C09-B76A-4B2C-A46C-772CADED29FF}.Release|x86.Build.0 = Release|Any CPU
- {D72994B8-B379-4244-9F1C-AEE715218C37}.Debug|Any CPU.ActiveCfg = Release|x86
- {D72994B8-B379-4244-9F1C-AEE715218C37}.Debug|x64.ActiveCfg = Release|x86
- {D72994B8-B379-4244-9F1C-AEE715218C37}.Debug|x86.ActiveCfg = Release|x86
- {D72994B8-B379-4244-9F1C-AEE715218C37}.Release|Any CPU.ActiveCfg = Release|x86
- {D72994B8-B379-4244-9F1C-AEE715218C37}.Release|x64.ActiveCfg = Release|x86
- {D72994B8-B379-4244-9F1C-AEE715218C37}.Release|x86.ActiveCfg = Release|x86
+ {D72994B8-B379-4244-9F1C-AEE715218C37}.Debug|Any CPU.ActiveCfg = Release
+ {D72994B8-B379-4244-9F1C-AEE715218C37}.Debug|x64.ActiveCfg = Release
+ {D72994B8-B379-4244-9F1C-AEE715218C37}.Debug|x86.ActiveCfg = Release
+ {D72994B8-B379-4244-9F1C-AEE715218C37}.Release|Any CPU.ActiveCfg = Release
+ {D72994B8-B379-4244-9F1C-AEE715218C37}.Release|x64.ActiveCfg = Release
+ {D72994B8-B379-4244-9F1C-AEE715218C37}.Release|x86.ActiveCfg = Release
{E5627396-C7D4-48FC-AE59-6EC6BFA98287}.Debug|Any CPU.ActiveCfg = Release|x86
{E5627396-C7D4-48FC-AE59-6EC6BFA98287}.Debug|x64.ActiveCfg = Release|x86
{E5627396-C7D4-48FC-AE59-6EC6BFA98287}.Debug|x86.ActiveCfg = Release|x86
diff --git a/build.cmd b/publish.cmd
similarity index 79%
rename from build.cmd
rename to publish.cmd
index 61ea682..31a073d 100644
--- a/build.cmd
+++ b/publish.cmd
@@ -1,6 +1,7 @@
@echo off
dotnet pack -c Release CloudPad.FunctionApp
+if %errorlevel% neq 0 exit /b %errorlevel%
"C:\Program Files (x86)\LINQPad5\LPRun.exe" publish.linq CloudPad.FunctionApp\bin\Release\net461\publish
diff --git a/scripts/netstandard.linq b/scripts/netstandard.linq
new file mode 100644
index 0000000..2437c9f
--- /dev/null
+++ b/scripts/netstandard.linq
@@ -0,0 +1,28 @@
+
+ C:\Users\leidegre\Source\tessin\cloud-pad2\CloudPad\bin\Debug\net461\CloudPad.dll
+ C:\Users\leidegre\Source\tessin\cloud-pad2\CloudPad\bin\Debug\net461\Microsoft.Azure.KeyVault.Core.dll
+ C:\Users\leidegre\Source\tessin\cloud-pad2\CloudPad\bin\Debug\net461\Microsoft.Data.Edm.dll
+ C:\Users\leidegre\Source\tessin\cloud-pad2\CloudPad\bin\Debug\net461\Microsoft.Data.OData.dll
+ C:\Users\leidegre\Source\tessin\cloud-pad2\CloudPad\bin\Debug\net461\Microsoft.Data.Services.Client.dll
+ C:\Users\leidegre\Source\tessin\cloud-pad2\CloudPad\bin\Debug\net461\Microsoft.WindowsAzure.Storage.dll
+ C:\Users\leidegre\Source\tessin\cloud-pad2\CloudPad\bin\Debug\net461\Newtonsoft.Json.dll
+ C:\Users\leidegre\Source\tessin\cloud-pad2\CloudPad\bin\Debug\net461\System.Net.Http.Formatting.dll
+ C:\Users\leidegre\Source\tessin\cloud-pad2\CloudPad\bin\Debug\net461\System.Spatial.dll
+ C:\Users\leidegre\Source\tessin\cloud-pad2\CloudPad\bin\Debug\net461\System.Web.Http.dll
+ Tessin.XamlBitmapClient
+ CloudPad
+ System.Threading.Tasks
+ Tessin
+ System.Net.Http
+
+
+Task Main(string[] args) => Program.MainAsync(this, args);
+//Task Main(string[] args) => Program.MainAsync(this, new[] { "--compile" });
+
+[HttpTrigger(AuthorizationLevel.Anonymous, "get")]
+public void X(HttpRequestMessage req)
+{
+ new Tessin.XamlBitmapClient("");
+}
+
+// Define other methods and classes here
\ No newline at end of file