From a556fbfa9b88e4923ac79ca9f26ae35d07d1fd8a Mon Sep 17 00:00:00 2001 From: John Leidegren Date: Fri, 24 May 2019 16:02:43 +0200 Subject: [PATCH] WIP (fixes some of the issues with multiple versions of same assembly) --- .gitignore | 1 + .../CloudPad.FunctionApp.csproj | 1 + CloudPad.FunctionApp/HttpTrigger.cs | 13 +- CloudPad/CommandLine.cs | 1 + CloudPad/FirstRun.cs | 72 +++++ CloudPad/InternalsVisibleTo.cs | 3 - CloudPad/Program.cs | 9 + CloudPad/Properties.cs | 4 + CloudPad/internal/AssemblyBindingConfig.cs | 72 +++++ CloudPad/internal/AssemblyCandidateSet.cs | 48 +++ CloudPad/internal/Compiler.cs | 284 +++++------------- CloudPad/internal/FunctionApp.cs | 3 +- CloudPad/internal/JobHost.cs | 152 ++-------- CloudPad/triggers/BlobTriggerAttribute.cs | 2 +- DESIGN.md | 15 +- Directory.Build.props | 4 + cloud-pad.sln | 12 +- build.cmd => publish.cmd | 1 + scripts/netstandard.linq | 28 ++ 19 files changed, 369 insertions(+), 356 deletions(-) create mode 100644 CloudPad/FirstRun.cs delete mode 100644 CloudPad/InternalsVisibleTo.cs create mode 100644 CloudPad/Properties.cs create mode 100644 CloudPad/internal/AssemblyBindingConfig.cs create mode 100644 CloudPad/internal/AssemblyCandidateSet.cs rename build.cmd => publish.cmd (79%) create mode 100644 scripts/netstandard.linq 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