diff --git a/.appveyor.yml b/.appveyor.yml index aac253a..b68237e 100644 --- a/.appveyor.yml +++ b/.appveyor.yml @@ -25,6 +25,7 @@ after_build: #- cmd: xcopy /s /y WelsonJS.Toolkit\WelsonJS.Toolkit\bin\x86\%CONFIGURATION%\* artifacts\ - cmd: xcopy /s /y WelsonJS.Toolkit\WelsonJS.Service\bin\x86\%CONFIGURATION%\* artifacts\ - cmd: xcopy /s /y WelsonJS.Toolkit\WelsonJS.Launcher\bin\x86\%CONFIGURATION%\* artifacts\ + - cmd: xcopy /s /y WelsonJS.Toolkit\Catswords.Phantomizer\bin\x86\%CONFIGURATION%\* artifacts\ - cmd: nuget pack WelsonJS.Toolkit\WelsonJS.Toolkit\ -properties Configuration=%CONFIGURATION% -properties Platform=x86 -OutputDirectory artifacts\ - ps: Start-BitsTransfer -Source "https://catswords.blob.core.windows.net/welsonjs/welsonjs_setup_unsigned.exe" -Destination "artifacts\welsonjs_setup.exe" - ps: Start-BitsTransfer -Source "https://catswords.blob.core.windows.net/welsonjs/chakracore-build/x86_release/ChakraCore.dll" -Destination "artifacts\ChakraCore.dll" diff --git a/WelsonJS.Toolkit/WelsonJS.Launcher/AssemblyLoader.cs b/WelsonJS.Toolkit/Catswords.Phantomizer/AssemblyLoader.cs similarity index 84% rename from WelsonJS.Toolkit/WelsonJS.Launcher/AssemblyLoader.cs rename to WelsonJS.Toolkit/Catswords.Phantomizer/AssemblyLoader.cs index 28575be..abe4c2a 100644 --- a/WelsonJS.Toolkit/WelsonJS.Launcher/AssemblyLoader.cs +++ b/WelsonJS.Toolkit/Catswords.Phantomizer/AssemblyLoader.cs @@ -1,10 +1,11 @@ -๏ปฟ// AssemblyLoader.cs -// SPDX-License-Identifier: GPL-3.0-or-later +๏ปฟ// AssemblyLoader.cs (Catswords.Phantomizer) +// SPDX-License-Identifier: MIT // SPDX-FileCopyrightText: Namhyeon Go , 2025 Catswords OSS and WelsonJS Contributors // https://github.com/gnh1201/welsonjs // using System; using System.Collections.Generic; +using System.Diagnostics; using System.IO; using System.IO.Compression; using System.Net; @@ -12,14 +13,14 @@ using System.Net.Http; using System.Reflection; using System.Runtime.InteropServices; -namespace WelsonJS.Launcher +namespace Catswords.Phantomizer { /// /// Network-aware loader for managed (.NET) and native (C/C++) binaries. /// - Managed assemblies resolve via AssemblyResolve /// - Native modules explicitly loaded via LoadNativeModules(...) /// - All DLLs must have valid Authenticode signatures - /// - Cached at: %APPDATA%\WelsonJS\assembly\{Name}\{Version}\ + /// - Cached at: %APPDATA%\Catswords\assembly\{Name}\{Version}\ /// - BaseUrl must be set by Main() before calling Register() /// public static class AssemblyLoader @@ -30,12 +31,12 @@ namespace WelsonJS.Launcher /// Must be set before Register() or LoadNativeModules(). /// public static string BaseUrl { get; set; } = null; - public static ICompatibleLogger Logger { get; set; } = null; + public static string LoaderNamespace { get; set; } = typeof(AssemblyLoader).Namespace; + public static string AppName { get; set; } = "Catswords"; private static readonly object SyncRoot = new object(); private static bool _registered; - private static readonly string LoaderNamespace = typeof(AssemblyLoader).Namespace ?? "WelsonJS.Launcher"; private static readonly HttpClientHandler LegacyHttpHandler = new HttpClientHandler { AutomaticDecompression = DecompressionMethods.None @@ -168,20 +169,20 @@ namespace WelsonJS.Launcher if (string.IsNullOrWhiteSpace(BaseUrl)) { - Logger?.Error("AssemblyLoader.Register() called but BaseUrl is not set."); + Trace.TraceError("AssemblyLoader.Register() called but BaseUrl is not set."); throw new InvalidOperationException("AssemblyLoader.BaseUrl must be configured before Register()."); } if (!BaseUrl.StartsWith("https://", StringComparison.OrdinalIgnoreCase)) { - Logger?.Error("AssemblyLoader.BaseUrl must use HTTPS for security."); + Trace.TraceError("AssemblyLoader.BaseUrl must use HTTPS for security."); throw new InvalidOperationException("AssemblyLoader.BaseUrl must use HTTPS."); } AppDomain.CurrentDomain.AssemblyResolve += OnAssemblyResolve; _registered = true; - Logger?.Info("AssemblyLoader: AssemblyResolve handler registered."); + Trace.TraceInformation("AssemblyLoader: AssemblyResolve handler registered."); } } @@ -203,17 +204,17 @@ namespace WelsonJS.Launcher lock (SyncRoot) { string appData = Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData); - string cacheDir = Path.Combine(appData, "WelsonJS", "assembly", ownerAssemblyName, versionString); + string cacheDir = Path.Combine(appData, AppName, "assembly", ownerAssemblyName, versionString); Directory.CreateDirectory(cacheDir); try { if (!SetDllDirectory(cacheDir)) - Logger?.Warn("SetDllDirectory failed for: {0}", cacheDir); + Trace.TraceWarning("SetDllDirectory failed for: {0}", cacheDir); } catch (Exception ex) { - Logger?.Warn("SetDllDirectory threw exception: {0}", ex.Message); + Trace.TraceWarning("SetDllDirectory threw exception: {0}", ex.Message); } foreach (string raw in fileNames) @@ -228,11 +229,11 @@ namespace WelsonJS.Launcher { string url = $"{BaseUrl.TrimEnd('/')}/native/{ownerAssemblyName}/{versionString}/{fileName}"; DownloadFile(url, localPath); - Logger?.Info("Downloaded native module: {0}", fileName); + Trace.TraceInformation("Downloaded native module: {0}", fileName); } else { - Logger?.Info("Using cached native module: {0}", localPath); + Trace.TraceInformation("Using cached native module: {0}", localPath); } EnsureSignedFileOrThrow(localPath, fileName); @@ -241,12 +242,12 @@ namespace WelsonJS.Launcher if (h == IntPtr.Zero) { int errorCode = Marshal.GetLastWin32Error(); - Logger?.Error("LoadLibrary failed for {0} with error code {1}", localPath, errorCode); + Trace.TraceError("LoadLibrary failed for {0} with error code {1}", localPath, errorCode); throw new InvalidOperationException($"Failed to load native module: {fileName} (error: {errorCode})"); } else { - Logger?.Info("Loaded native module: {0}", fileName); + Trace.TraceInformation("Loaded native module: {0}", fileName); } } } @@ -277,7 +278,7 @@ namespace WelsonJS.Launcher private static Assembly OnAssemblyResolve(object sender, ResolveEventArgs args) { - Logger?.Info("AssemblyResolve: {0}", args.Name); + Trace.TraceInformation("AssemblyResolve: {0}", args.Name); AssemblyName req = new AssemblyName(args.Name); string simpleName = req.Name; @@ -290,7 +291,7 @@ namespace WelsonJS.Launcher var entryName = entry.GetName().Name; if (string.Equals(simpleName, entryName, StringComparison.OrdinalIgnoreCase)) { - Logger?.Info("AssemblyResolve: skipping entry assembly {0}", simpleName); + Trace.TraceInformation("AssemblyResolve: skipping entry assembly {0}", simpleName); return null; } } @@ -301,7 +302,7 @@ namespace WelsonJS.Launcher lock (SyncRoot) { string appData = Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData); - string cacheDir = Path.Combine(appData, "WelsonJS", "assembly", simpleName, versionStr); + string cacheDir = Path.Combine(appData, AppName, "assembly", simpleName, versionStr); string dllPath = Path.Combine(cacheDir, simpleName + ".dll"); Directory.CreateDirectory(cacheDir); @@ -310,16 +311,16 @@ namespace WelsonJS.Launcher { string url = $"{BaseUrl.TrimEnd('/')}/managed/{simpleName}/{versionStr}/{simpleName}.dll"; DownloadFile(url, dllPath); - Logger?.Info("Downloaded managed assembly: {0}", simpleName); + Trace.TraceInformation("Downloaded managed assembly: {0}", simpleName); } else { - Logger?.Info("Using cached managed assembly: {0}", dllPath); + Trace.TraceInformation("Using cached managed assembly: {0}", dllPath); } if (!File.Exists(dllPath)) { - Logger?.Warn("AssemblyResolve: managed assembly not found after download attempt: {0}", simpleName); + Trace.TraceWarning("AssemblyResolve: managed assembly not found after download attempt: {0}", simpleName); return null; } @@ -345,13 +346,13 @@ namespace WelsonJS.Launcher if (isDll && TryDownloadCompressedFile(gzUrl, dest)) { - Logger?.Info("Downloaded and decompressed file to: {0}", dest); + Trace.TraceInformation("Downloaded and decompressed file to: {0}", dest); downloaded = true; } if (!downloaded) { - Logger?.Info("Downloading file from: {0}", url); + Trace.TraceInformation("Downloading file from: {0}", url); res = Http.GetAsync(url).GetAwaiter().GetResult(); res.EnsureSuccessStatusCode(); @@ -361,7 +362,7 @@ namespace WelsonJS.Launcher s.CopyTo(fs); } - Logger?.Info("Downloaded file to: {0}", dest); + Trace.TraceInformation("Downloaded file to: {0}", dest); } if (!File.Exists(dest)) @@ -371,12 +372,12 @@ namespace WelsonJS.Launcher } catch (HttpRequestException ex) { - Logger?.Error("Network or I/O error downloading {0}: {1}", url, ex.Message); + Trace.TraceError("Network or I/O error downloading {0}: {1}", url, ex.Message); throw; } catch (Exception ex) { - Logger?.Error("Unexpected error downloading {0}: {1}", url, ex.Message); + Trace.TraceError("Unexpected error downloading {0}: {1}", url, ex.Message); throw; } finally @@ -396,7 +397,7 @@ namespace WelsonJS.Launcher { if (res.StatusCode == HttpStatusCode.NotFound) { - Logger?.Info("No gzipped variant at {0}; falling back to uncompressed URL.", gzUrl); + Trace.TraceInformation("No gzipped variant at {0}; falling back to uncompressed URL.", gzUrl); return false; } @@ -419,12 +420,12 @@ namespace WelsonJS.Launcher } catch (HttpRequestException ex) { - Logger?.Warn("Network or I/O error downloading compressed file from {0}: {1}", gzUrl, ex.Message); + Trace.TraceWarning("Network or I/O error downloading compressed file from {0}: {1}", gzUrl, ex.Message); throw; } catch (Exception ex) { - Logger?.Error("Unexpected error downloading compressed file from {0}: {1}", gzUrl, ex.Message); + Trace.TraceError("Unexpected error downloading compressed file from {0}: {1}", gzUrl, ex.Message); throw; } finally @@ -437,7 +438,7 @@ namespace WelsonJS.Launcher } catch (Exception ex) { - Logger?.Info("Failed to delete temporary file {0}: {1}", tempFile, ex.Message); + Trace.TraceInformation("Failed to delete temporary file {0}: {1}", tempFile, ex.Message); } } } @@ -461,7 +462,7 @@ namespace WelsonJS.Launcher { if (!File.Exists(path)) { - Logger?.Error("File does not exist for signature verification: {0}", logicalName); + Trace.TraceError("File does not exist for signature verification: {0}", logicalName); throw new FileNotFoundException("File not found for signature verification: " + logicalName, path); } @@ -469,17 +470,17 @@ namespace WelsonJS.Launcher if (status == FileSignatureStatus.Valid) { - Logger?.Info("Signature OK: {0}", logicalName); + Trace.TraceInformation("Signature OK: {0}", logicalName); return; } if (status == FileSignatureStatus.NoSignature) { - Logger?.Error("BLOCKED unsigned binary: {0}", logicalName); + Trace.TraceError("BLOCKED unsigned binary: {0}", logicalName); throw new InvalidOperationException("Unsigned binary blocked: " + logicalName); } - Logger?.Error("BLOCKED invalid signature: {0}", logicalName); + Trace.TraceError("BLOCKED invalid signature: {0}", logicalName); throw new InvalidOperationException("Invalid signature: " + logicalName); } diff --git a/WelsonJS.Toolkit/Catswords.Phantomizer/Catswords.Phantomizer.csproj b/WelsonJS.Toolkit/Catswords.Phantomizer/Catswords.Phantomizer.csproj new file mode 100644 index 0000000..dbdcea4 --- /dev/null +++ b/WelsonJS.Toolkit/Catswords.Phantomizer/Catswords.Phantomizer.csproj @@ -0,0 +1,7 @@ +๏ปฟ + + + netstandard2.0 + + + diff --git a/WelsonJS.Toolkit/Catswords.Phantomizer/LICENSE b/WelsonJS.Toolkit/Catswords.Phantomizer/LICENSE new file mode 100644 index 0000000..f144743 --- /dev/null +++ b/WelsonJS.Toolkit/Catswords.Phantomizer/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) Namhyeon Go , 2025 Catswords OSS and WelsonJS Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. \ No newline at end of file diff --git a/WelsonJS.Toolkit/Catswords.Phantomizer/README.md b/WelsonJS.Toolkit/Catswords.Phantomizer/README.md new file mode 100644 index 0000000..60451be --- /dev/null +++ b/WelsonJS.Toolkit/Catswords.Phantomizer/README.md @@ -0,0 +1,93 @@ +# Catswords.Phantomizer + +**Catswords.Phantomizer** is an HTTP-based dynamic-link library (DLL) loader designed for .NET applications. +It allows your application to fetch and load assemblies directly from your CDN (Azure Blob, S3, Cloudflare R2, etc.) at runtime, with optional GZip compression support. + +--- + +## ๐Ÿš€ Features + +* Load managed (`*.dll`) and native (`*.dll`) assemblies over HTTP +* Optional `.dll.gz` decompression for faster network delivery +* CDN-friendly URL structure +* Easy bootstrap through a small embedded loader + +--- + +## ๐Ÿ“ฆ How to Use + +### 1. Embed Phantomizer into your project + +Add `Catswords.Phantomizer.dll.gz` to your `Resources.resx` file. + +--- + +### 2. Initialize Phantomizer at application startup + +Place the following code inside your `Main` method, static constructor, or any early entry point: + +```csharp +static Program() { + InitializeAssemblyLoader(); +} + +private static void InitializeAssemblyLoader() +{ + byte[] gzBytes = Properties.Resources.Phantomizer; + + byte[] dllBytes; + using (var input = new MemoryStream(gzBytes)) + using (var gz = new GZipStream(input, CompressionMode.Decompress)) + using (var output = new MemoryStream()) + { + gz.CopyTo(output); + dllBytes = output.ToArray(); + } + + Assembly phantomAsm = Assembly.Load(dllBytes); + Type loaderType = phantomAsm.GetType("Catswords.Phantomizer.AssemblyLoader", true); + + loaderType.GetProperty("BaseUrl")?.SetValue(null, GetAppConfig("AssemblyBaseUrl")); // Set your CDN base URL + loaderType.GetProperty("AppName")?.SetValue(null, "WelsonJS"); // Set your application name + loaderType.GetMethod("Register")?.Invoke(null, null); + + var loadNativeModulesMethod = loaderType.GetMethod( + "LoadNativeModules", + BindingFlags.Public | BindingFlags.Static, + binder: null, + types: new[] { typeof(string), typeof(Version), typeof(string[]) }, + modifiers: null + ); + + if (loadNativeModulesMethod == null) + throw new InvalidOperationException("LoadNativeModules(string, Version, string[]) method not found."); + + loadNativeModulesMethod.Invoke(null, new object[] + { + "ChakraCore", + new Version(1, 13, 0, 0), + new[] { "ChakraCore.dll" } + }); +} +``` + +--- + +### 3. Upload your DLL files to a CDN + +Upload your managed and native assemblies to your CDN following the URL pattern below. + +#### ๐Ÿ“ URL Rules + +| Type | Example URL | Description | +| ------------------ | ----------------------------------------------------------------------------------- | ------------------------------- | +| Managed DLL | `https://example.cdn.tld/packages/managed/MyManagedLib/1.0.0.0/MyManagedLib.dll` | Normal .NET assembly | +| Managed DLL (GZip) | `https://example.cdn.tld/packages/managed/MyManagedLib/1.0.0.0/MyManagedLib.dll.gz` | GZip-compressed .NET assembly | +| Native DLL | `https://example.cdn.tld/packages/native/MyNativeLib/1.0.0.0/MyNativeLib.dll` | Native assembly | +| Native DLL (GZip) | `https://example.cdn.tld/packages/native/MyNativeLib/1.0.0.0/MyNativeLib.dll.gz` | GZip-compressed native assembly | + +--- + +### 4. ๐ŸŽ‰ Start loading assemblies over HTTP + +Once Phantomizer is initialized, your application will automatically fetch missing assemblies from your CDN. diff --git a/WelsonJS.Toolkit/WelsonJS.Launcher/Program.cs b/WelsonJS.Toolkit/WelsonJS.Launcher/Program.cs index bfac5b5..e42eae2 100644 --- a/WelsonJS.Toolkit/WelsonJS.Launcher/Program.cs +++ b/WelsonJS.Toolkit/WelsonJS.Launcher/Program.cs @@ -8,7 +8,9 @@ using System.Collections.Generic; using System.Configuration; using System.Diagnostics; using System.IO; +using System.IO.Compression; using System.Linq; +using System.Reflection; using System.Threading; using System.Windows.Forms; using WelsonJS.Launcher.Telemetry; @@ -33,10 +35,7 @@ namespace WelsonJS.Launcher _logger = new TraceLogger(); // load external assemblies - AssemblyLoader.BaseUrl = GetAppConfig("AssemblyBaseUrl"); - AssemblyLoader.Logger = _logger; - AssemblyLoader.Register(); - AssemblyLoader.LoadNativeModules("ChakraCore", new Version(1, 13, 0, 0), new[] { "ChakraCore.dll" }); + InitializeAssemblyLoader(); // telemetry try @@ -114,6 +113,48 @@ namespace WelsonJS.Launcher _mutex.Dispose(); } + private static void InitializeAssemblyLoader() + { + byte[] gzBytes = Properties.Resources.Phantomizer; + + byte[] dllBytes; + using (var input = new MemoryStream(gzBytes)) + using (var gz = new GZipStream(input, CompressionMode.Decompress)) + using (var output = new MemoryStream()) + { + gz.CopyTo(output); + dllBytes = output.ToArray(); + } + + Assembly phantomAsm = Assembly.Load(dllBytes); + Type loaderType = phantomAsm.GetType("Catswords.Phantomizer.AssemblyLoader", true); + + loaderType.GetProperty("BaseUrl")?.SetValue(null, GetAppConfig("AssemblyBaseUrl")); + loaderType.GetProperty("AppName")?.SetValue(null, "WelsonJS"); + loaderType.GetMethod("Register")?.Invoke(null, null); + + var loadNativeModulesMethod = loaderType.GetMethod( + "LoadNativeModules", + BindingFlags.Public | BindingFlags.Static, + binder: null, + types: new[] { typeof(string), typeof(Version), typeof(string[]) }, + modifiers: null + ); + + if (loadNativeModulesMethod == null) + { + throw new InvalidOperationException("LoadNativeModules(string, Version, string[]) method not found."); + } + + loadNativeModulesMethod.Invoke(null, new object[] + { + "ChakraCore", + new Version(1, 13, 0, 0), + new[] { "ChakraCore.dll" } + }); + } + + public static void RecordFirstDeployTime(string directory, string instanceId) { // get current time diff --git a/WelsonJS.Toolkit/WelsonJS.Launcher/Properties/Resources.Designer.cs b/WelsonJS.Toolkit/WelsonJS.Launcher/Properties/Resources.Designer.cs index b5f61ae..095137a 100644 --- a/WelsonJS.Toolkit/WelsonJS.Launcher/Properties/Resources.Designer.cs +++ b/WelsonJS.Toolkit/WelsonJS.Launcher/Properties/Resources.Designer.cs @@ -342,6 +342,16 @@ namespace WelsonJS.Launcher.Properties { } } + /// + /// System.Byte[] ํ˜•์‹์˜ ์ง€์—ญํ™”๋œ ๋ฆฌ์†Œ์Šค๋ฅผ ์ฐพ์Šต๋‹ˆ๋‹ค. + /// + internal static byte[] Phantomizer { + get { + object obj = ResourceManager.GetObject("Phantomizer", resourceCulture); + return ((byte[])(obj)); + } + } + /// /// https://github.com/gnh1201/welsonjs๊ณผ(์™€) ์œ ์‚ฌํ•œ ์ง€์—ญํ™”๋œ ๋ฌธ์ž์—ด์„ ์ฐพ์Šต๋‹ˆ๋‹ค. /// diff --git a/WelsonJS.Toolkit/WelsonJS.Launcher/Properties/Resources.resx b/WelsonJS.Toolkit/WelsonJS.Launcher/Properties/Resources.resx index d5d208a..5bc52ea 100644 --- a/WelsonJS.Toolkit/WelsonJS.Launcher/Properties/Resources.resx +++ b/WelsonJS.Toolkit/WelsonJS.Launcher/Properties/Resources.resx @@ -241,4 +241,7 @@ https://catswords.blob.core.windows.net/welsonjs/packages + + ..\Resources\Catswords.Phantomizer.dll.gz;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + \ No newline at end of file diff --git a/WelsonJS.Toolkit/WelsonJS.Launcher/Resources/Catswords.Phantomizer.dll.gz b/WelsonJS.Toolkit/WelsonJS.Launcher/Resources/Catswords.Phantomizer.dll.gz new file mode 100644 index 0000000..3032827 Binary files /dev/null and b/WelsonJS.Toolkit/WelsonJS.Launcher/Resources/Catswords.Phantomizer.dll.gz differ diff --git a/WelsonJS.Toolkit/WelsonJS.Launcher/WelsonJS.Launcher.csproj b/WelsonJS.Toolkit/WelsonJS.Launcher/WelsonJS.Launcher.csproj index a1a876a..b2ae16d 100644 --- a/WelsonJS.Toolkit/WelsonJS.Launcher/WelsonJS.Launcher.csproj +++ b/WelsonJS.Toolkit/WelsonJS.Launcher/WelsonJS.Launcher.csproj @@ -88,7 +88,6 @@ - @@ -171,6 +170,7 @@ + diff --git a/WelsonJS.Toolkit/WelsonJS.Toolkit.sln b/WelsonJS.Toolkit/WelsonJS.Toolkit.sln index 38c0f9a..74d1463 100644 --- a/WelsonJS.Toolkit/WelsonJS.Toolkit.sln +++ b/WelsonJS.Toolkit/WelsonJS.Toolkit.sln @@ -17,6 +17,8 @@ Project("{778DAE3C-4631-46EA-AA77-85C1314464D9}") = "WelsonJS.Cryptography", "We EndProject Project("{778DAE3C-4631-46EA-AA77-85C1314464D9}") = "WelsonJS.Cryptography.Test", "WelsonJS.Cryptography.Test\WelsonJS.Cryptography.Test.vbproj", "{C65EC34B-71C7-47CF-912E-D304283EB412}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Catswords.Phantomizer", "Catswords.Phantomizer\Catswords.Phantomizer.csproj", "{59C67003-C14E-4703-927F-318BFF15F903}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -81,6 +83,14 @@ Global {C65EC34B-71C7-47CF-912E-D304283EB412}.Release|Any CPU.Build.0 = Release|Any CPU {C65EC34B-71C7-47CF-912E-D304283EB412}.Release|x86.ActiveCfg = Release|x86 {C65EC34B-71C7-47CF-912E-D304283EB412}.Release|x86.Build.0 = Release|x86 + {59C67003-C14E-4703-927F-318BFF15F903}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {59C67003-C14E-4703-927F-318BFF15F903}.Debug|Any CPU.Build.0 = Debug|Any CPU + {59C67003-C14E-4703-927F-318BFF15F903}.Debug|x86.ActiveCfg = Debug|Any CPU + {59C67003-C14E-4703-927F-318BFF15F903}.Debug|x86.Build.0 = Debug|Any CPU + {59C67003-C14E-4703-927F-318BFF15F903}.Release|Any CPU.ActiveCfg = Release|Any CPU + {59C67003-C14E-4703-927F-318BFF15F903}.Release|Any CPU.Build.0 = Release|Any CPU + {59C67003-C14E-4703-927F-318BFF15F903}.Release|x86.ActiveCfg = Release|Any CPU + {59C67003-C14E-4703-927F-318BFF15F903}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE