From b90e84f75025f5c1490ee8ca947084d95155791b Mon Sep 17 00:00:00 2001 From: nbilal Date: Mon, 3 Oct 2016 09:08:56 -0700 Subject: [PATCH 1/4] basic IPC working --- Etg.Yams.sln | 7 +++ src/Etg.Yams.Core/Process/Process.cs | 33 ++++++++-- .../Etg.Yams.Core.Test.csproj | 3 + .../Etg.Yams.Core.Test/Process/ProcessTest.cs | 19 +++++- test/Etg.Yams.Core.Test/Utils/TestUtils.cs | 8 ++- test/IPCProcess/App.config | 6 ++ test/IPCProcess/IPCProcess.csproj | 60 +++++++++++++++++++ test/IPCProcess/Program.cs | 41 +++++++++++++ test/IPCProcess/Properties/AssemblyInfo.cs | 36 +++++++++++ 9 files changed, 205 insertions(+), 8 deletions(-) create mode 100644 test/IPCProcess/App.config create mode 100644 test/IPCProcess/IPCProcess.csproj create mode 100644 test/IPCProcess/Program.cs create mode 100644 test/IPCProcess/Properties/AssemblyInfo.cs diff --git a/Etg.Yams.sln b/Etg.Yams.sln index 6cc6192..3b8e4a0 100644 --- a/Etg.Yams.sln +++ b/Etg.Yams.sln @@ -37,6 +37,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AzureUtilsTest", "test\Azur EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Etg.Yams.Core", "src\Etg.Yams.Core\Etg.Yams.Core.csproj", "{7145E485-34FA-4632-89B0-BD27C96AF69C}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "IPCProcess", "test\IPCProcess\IPCProcess.csproj", "{4F7B6739-5452-4F8C-8904-72112FAB3C1E}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -103,6 +105,10 @@ Global {7145E485-34FA-4632-89B0-BD27C96AF69C}.Debug|Any CPU.Build.0 = Debug|Any CPU {7145E485-34FA-4632-89B0-BD27C96AF69C}.Release|Any CPU.ActiveCfg = Release|Any CPU {7145E485-34FA-4632-89B0-BD27C96AF69C}.Release|Any CPU.Build.0 = Release|Any CPU + {4F7B6739-5452-4F8C-8904-72112FAB3C1E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {4F7B6739-5452-4F8C-8904-72112FAB3C1E}.Debug|Any CPU.Build.0 = Debug|Any CPU + {4F7B6739-5452-4F8C-8904-72112FAB3C1E}.Release|Any CPU.ActiveCfg = Release|Any CPU + {4F7B6739-5452-4F8C-8904-72112FAB3C1E}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -123,6 +129,7 @@ Global {E66F281C-BD50-45B0-AAEF-FB87E4C9D0CC} = {2A52BDC8-4B16-43FE-9E9D-A7D0A72C17C8} {2FE963BD-D826-4CC5-8A63-864CDA212233} = {2A52BDC8-4B16-43FE-9E9D-A7D0A72C17C8} {7145E485-34FA-4632-89B0-BD27C96AF69C} = {5925E681-1BEA-456D-B9E0-CA175ABBFA9D} + {4F7B6739-5452-4F8C-8904-72112FAB3C1E} = {2A52BDC8-4B16-43FE-9E9D-A7D0A72C17C8} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution EnterpriseLibraryConfigurationToolBinariesPathV6 = packages\EnterpriseLibrary.TransientFaultHandling.6.0.1304.0\lib\portable-net45+win+wp8 diff --git a/src/Etg.Yams.Core/Process/Process.cs b/src/Etg.Yams.Core/Process/Process.cs index 060a83f..f291f98 100644 --- a/src/Etg.Yams.Core/Process/Process.cs +++ b/src/Etg.Yams.Core/Process/Process.cs @@ -1,6 +1,7 @@ using System; using System.Diagnostics; using System.IO; +using System.IO.Pipes; using System.Threading.Tasks; namespace Etg.Yams.Process @@ -65,12 +66,36 @@ await Task.Run(async () => _process.StartInfo.UseShellExecute = false; _process.StartInfo.CreateNoWindow = true; } - if (!_process.Start()) + + using (AnonymousPipeServerStream pipeServer = + new AnonymousPipeServerStream(PipeDirection.In, + HandleInheritability.Inheritable)) { - await ReleaseResources(); - throw new Exception($"The OS failed to start the process at {_exePath} with arguments {_exeArgs}"); + _process.StartInfo.Arguments += " " + pipeServer.GetClientHandleAsString(); + if (!_process.Start()) + { + await ReleaseResources(); + throw new Exception( + $"The OS failed to start the process at {_exePath} with arguments {_exeArgs}"); + } + _isRunning = true; + pipeServer.DisposeLocalCopyOfClientHandle(); + + using (StreamReader sr = new StreamReader(pipeServer)) + { + string msg; + + // Wait for 'sync message' from the server. + do + { + Debug.WriteLine("Yams is waiting for the app to finish initializing"); + msg = sr.ReadLine(); + } + while (!msg.StartsWith("[STARTED]")); + + Console.WriteLine("App Initialized and ready to receive requests"); + } } - _isRunning = true; }); } diff --git a/test/Etg.Yams.Core.Test/Etg.Yams.Core.Test.csproj b/test/Etg.Yams.Core.Test/Etg.Yams.Core.Test.csproj index dec3656..2fab722 100644 --- a/test/Etg.Yams.Core.Test/Etg.Yams.Core.Test.csproj +++ b/test/Etg.Yams.Core.Test/Etg.Yams.Core.Test.csproj @@ -173,6 +173,9 @@ PreserveNewest + + PreserveNewest + PreserveNewest diff --git a/test/Etg.Yams.Core.Test/Process/ProcessTest.cs b/test/Etg.Yams.Core.Test/Process/ProcessTest.cs index 0c9201d..b933501 100644 --- a/test/Etg.Yams.Core.Test/Process/ProcessTest.cs +++ b/test/Etg.Yams.Core.Test/Process/ProcessTest.cs @@ -13,19 +13,22 @@ public class ProcessTestFixture private readonly string _testDirPath; public readonly string SuicidalExePath; public readonly string HangingExePath; + public readonly string IPCExePath; const string SuicidalProcessExeName = "SuicidalProcess.exe"; const string HangingProcessExeName = "HangingProcess.exe"; + const string IPCProcess = "IPCProcess.exe"; public ProcessTestFixture() { _testDirPath = Path.Combine(Directory.GetCurrentDirectory(), "ProcessTest"); SuicidalExePath = Path.Combine(_testDirPath, SuicidalProcessExeName); HangingExePath = Path.Combine(_testDirPath, HangingProcessExeName); + IPCExePath = Path.Combine(_testDirPath, IPCProcess); Directory.CreateDirectory(_testDirPath); - string[] exes = { SuicidalProcessExeName, HangingProcessExeName }; + string[] exes = { SuicidalProcessExeName, HangingProcessExeName, IPCProcess }; foreach (string exeName in exes) { TestUtils.CopyExe(exeName, _testDirPath); @@ -35,13 +38,15 @@ public ProcessTestFixture() public class ProcessTest : IClassFixture { - private string _hangingExePath; - private string _suicidalExePath; + private readonly string _hangingExePath; + private readonly string _suicidalExePath; + private readonly string _ipcExePath; public ProcessTest(ProcessTestFixture fixture) { _hangingExePath = fixture.HangingExePath; _suicidalExePath = fixture.SuicidalExePath; + _ipcExePath = fixture.IPCExePath; } [Fact] @@ -105,5 +110,13 @@ public void TestProperties() Assert.Equal(_suicidalExePath, suicidalProcess.ExePath); Assert.Equal(exeArgs, suicidalProcess.ExeArgs); } + + [Fact] + public async Task TestThatStartedMessageIsReceivedFromProcessWithIPC() + { + IProcess ipcProcess = new Yams.Process.Process(_ipcExePath, "Foo", true); + await ipcProcess.Start(); + await ipcProcess.Kill(); + } } } diff --git a/test/Etg.Yams.Core.Test/Utils/TestUtils.cs b/test/Etg.Yams.Core.Test/Utils/TestUtils.cs index b307afb..3fab156 100644 --- a/test/Etg.Yams.Core.Test/Utils/TestUtils.cs +++ b/test/Etg.Yams.Core.Test/Utils/TestUtils.cs @@ -34,7 +34,13 @@ public static string GetTestExesDirPath() public static void CopyExe(string exeName, string destPath) { - File.Copy(Path.Combine(GetTestExesDirPath(), exeName), Path.Combine(destPath, exeName), overwrite: true); + try + { + File.Copy(Path.Combine(GetTestExesDirPath(), exeName), Path.Combine(destPath, exeName), overwrite: true); + } + catch (IOException) + { + } } } } diff --git a/test/IPCProcess/App.config b/test/IPCProcess/App.config new file mode 100644 index 0000000..88fa402 --- /dev/null +++ b/test/IPCProcess/App.config @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/test/IPCProcess/IPCProcess.csproj b/test/IPCProcess/IPCProcess.csproj new file mode 100644 index 0000000..c134ecb --- /dev/null +++ b/test/IPCProcess/IPCProcess.csproj @@ -0,0 +1,60 @@ + + + + + Debug + AnyCPU + {4F7B6739-5452-4F8C-8904-72112FAB3C1E} + Exe + Properties + IPCProcess + IPCProcess + v4.5.2 + 512 + true + + + AnyCPU + true + full + false + ..\Etg.Yams.Core.Test\Data\Exes\ + DEBUG;TRACE + prompt + 4 + + + AnyCPU + pdbonly + true + ..\Etg.Yams.Core.Test\Data\Exes\ + TRACE + prompt + 4 + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/test/IPCProcess/Program.cs b/test/IPCProcess/Program.cs new file mode 100644 index 0000000..4e3c0d5 --- /dev/null +++ b/test/IPCProcess/Program.cs @@ -0,0 +1,41 @@ +using System; +using System.IO; +using System.IO.Pipes; +using System.Linq; +using System.Threading; + +namespace IPCProcess +{ + class Program + { + static void Main(string[] args) + { + Thread.Sleep(TimeSpan.FromSeconds(30)); + if (args.Length > 0) + { + using (PipeStream pipeClient = + new AnonymousPipeClientStream(PipeDirection.Out, args.Last())) + { + try + { + // Read user input and send that to the client process. + using (StreamWriter sw = new StreamWriter(pipeClient)) + { + sw.AutoFlush = true; + // Send a 'sync message' and wait for client to receive it. + sw.WriteLine("[STARTED]"); + pipeClient.WaitForPipeDrain(); + } + } + // Catch the IOException that is raised if the pipe is broken + // or disconnected. + catch (IOException e) + { + Console.WriteLine("App Error: {0}", e.Message); + } + } + } + Console.ReadLine(); + } + } +} diff --git a/test/IPCProcess/Properties/AssemblyInfo.cs b/test/IPCProcess/Properties/AssemblyInfo.cs new file mode 100644 index 0000000..481e5cf --- /dev/null +++ b/test/IPCProcess/Properties/AssemblyInfo.cs @@ -0,0 +1,36 @@ +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +// General Information about an assembly is controlled through the following +// set of attributes. Change these attribute values to modify the information +// associated with an assembly. +[assembly: AssemblyTitle("IPCProcess")] +[assembly: AssemblyDescription("")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("")] +[assembly: AssemblyProduct("IPCProcess")] +[assembly: AssemblyCopyright("Copyright © 2016")] +[assembly: AssemblyTrademark("")] +[assembly: AssemblyCulture("")] + +// Setting ComVisible to false makes the types in this assembly not visible +// to COM components. If you need to access a type in this assembly from +// COM, set the ComVisible attribute to true on that type. +[assembly: ComVisible(false)] + +// The following GUID is for the ID of the typelib if this project is exposed to COM +[assembly: Guid("4f7b6739-5452-4f8c-8904-72112fab3c1e")] + +// Version information for an assembly consists of the following four values: +// +// Major Version +// Minor Version +// Build Number +// Revision +// +// You can specify all the values or you can default the Build and Revision Numbers +// by using the '*' as shown below: +// [assembly: AssemblyVersion("1.0.*")] +[assembly: AssemblyVersion("1.0.0.0")] +[assembly: AssemblyFileVersion("1.0.0.0")] From 6c229fa6085725cf9dd7cfbf22260cd40708be45 Mon Sep 17 00:00:00 2001 From: nbilal Date: Sat, 10 Dec 2016 05:21:12 -0800 Subject: [PATCH 2/4] IPC and corresponding E2E tests implemented + Documentation --- .gitignore | 3 + Docs/Deploy&Host_an_App_in_YAMS.md | 107 +++++++++++ Docs/Overview.md | 14 ++ Etg.Yams.sln | 40 +++- README.md | 1 + src/Etg.Yams.Core/Application/Application.cs | 8 +- .../Application/ApplicationConfig.cs | 20 +- .../Application/ApplicationConfigParser.cs | 15 +- .../Application/ApplicationPool.cs | 21 ++- .../Application/ConfigurableApplication.cs | 9 +- src/Etg.Yams.Core/Client/IYamsClient.cs | 13 ++ .../Client/IYamsClientFactory.cs | 7 + src/Etg.Yams.Core/Client/YamsClient.cs | 102 ++++++++++ src/Etg.Yams.Core/Client/YamsClientConfig.cs | 21 +++ .../Client/YamsClientConfigBuilder.cs | 41 ++++ src/Etg.Yams.Core/Client/YamsClientFactory.cs | 33 ++++ src/Etg.Yams.Core/Client/YamsClientOptions.cs | 14 ++ src/Etg.Yams.Core/Etg.Yams.Core.csproj | 31 +++- .../Install/ApplicationInstaller.cs | 17 +- .../Process/AbstractProcessDecorator.cs | 56 ++++++ .../GracefullShutdownProcessDecorator.cs | 61 ++++++ .../Process/HealthProcessDecorator.cs | 97 ++++++++++ src/Etg.Yams.Core/Process/IProcess.cs | 2 +- src/Etg.Yams.Core/Process/IProcessFactory.cs | 6 +- .../Process/Ipc/IIpcConnection.cs | 14 ++ src/Etg.Yams.Core/Process/Ipc/INamedPipe.cs | 14 ++ .../Process/Ipc/INamedPipeFactory.cs | 8 + .../Process/Ipc/IpcConnection.cs | 70 +++++++ .../Process/Ipc/NamedPipeClientAdapter.cs | 42 +++++ .../Process/Ipc/NamedPipeFactory.cs | 15 ++ .../Process/Ipc/NamedPipeServerAdapter.cs | 36 ++++ .../Process/MonitorInitProcessDecorator.cs | 60 ++++++ src/Etg.Yams.Core/Process/Process.cs | 51 ++--- src/Etg.Yams.Core/Process/ProcessFactory.cs | 41 ++++ src/Etg.Yams.Core/Process/ProcessStopper.cs | 30 +-- .../Process/SelfRestartingProcess.cs | 7 +- .../Process/SelfRestartingProcessFactory.cs | 20 -- src/Etg.Yams.Core/Utils/TaskExtensions.cs | 39 ++++ src/Etg.Yams.Core/YamsConfig.cs | 57 +++++- src/Etg.Yams.Core/YamsConfigBuilder.cs | 106 ++++++++--- src/Etg.Yams.Core/YamsDiModule.cs | 2 +- src/Etg.Yams.Core/packages.config | 1 + src/Etg.Yams/Etg.Yams.nuspec | 2 +- .../AzureBlobStorageUpdateSessionTest.csproj | 10 +- .../SimpleStubs.json | 9 +- .../packages.config | 2 +- .../Application/ApplicationPoolTest.cs | 10 +- .../Application/ApplicationTest.cs | 28 +-- .../DeploymentConfigFullIpcApp.json | 9 + .../DeploymentConfigGracefulShutdownApp.json | 9 + .../DeploymentConfigHeartBeatApp.json | 9 + .../DeploymentConfigMonitorInitApp.json | 9 + .../EndToEndTest/DeploymentConfigNoApps.json | 4 + .../FullIpcApp/1.0.0/AppConfig.json | 7 + .../GracefulShutdownApp/1.0.0/AppConfig.json | 5 + .../HeartBeatApp/1.0.0/AppConfig.json | 5 + .../MonitorInitApp/1.0.0/AppConfig.json | 5 + test/Etg.Yams.Core.Test/EndToEndTest.cs | 175 ++++++++++++++++-- .../Etg.Yams.Core.Test.csproj | 42 ++++- .../Process/ProcessStopperTest.cs | 33 +--- .../Etg.Yams.Core.Test/Process/ProcessTest.cs | 61 ++---- .../Process/SelfRestartingProcessTest.cs | 12 +- test/Etg.Yams.Core.Test/Utils/TestUtils.cs | 19 +- test/Etg.Yams.Core.Test/packages.config | 2 +- test/Etg.Yams.Core.Test/stubs/ProcessStub.cs | 8 +- .../{IPCProcess => FullIpcProcess}/App.config | 0 test/FullIpcProcess/FullIpcProcess.csproj | 146 +++++++++++++++ test/FullIpcProcess/Program.cs | 65 +++++++ .../FullIpcProcess/Properties/AssemblyInfo.cs | 36 ++++ test/FullIpcProcess/packages.config | 24 +++ .../GracefullShutdownProcess.csproj | 145 +++++++++++++++ test/GracefullShutdownProcess/Program.cs | 47 +++++ .../Properties/AssemblyInfo.cs | 40 ++++ test/GracefullShutdownProcess/app.config | 3 + test/GracefullShutdownProcess/packages.config | 24 +++ test/HeartBeatProcess/App.config | 6 + test/HeartBeatProcess/HeartBeatProcess.csproj | 146 +++++++++++++++ test/HeartBeatProcess/Program.cs | 40 ++++ .../Properties/AssemblyInfo.cs | 36 ++++ test/HeartBeatProcess/packages.config | 24 +++ test/IPCProcess/IPCProcess.csproj | 60 ------ test/IPCProcess/Program.cs | 41 ---- .../MonitorInitProcess.csproj | 145 +++++++++++++++ test/MonitorInitProcess/Program.cs | 41 ++++ .../Properties/AssemblyInfo.cs | 40 ++++ test/MonitorInitProcess/app.config | 3 + test/MonitorInitProcess/packages.config | 24 +++ .../Properties/AssemblyInfo.cs | 6 +- .../SimpleStubs.json | 0 test/Stubs/Stubs.csproj | 117 ++++++++++++ test/Stubs/packages.config | 13 ++ test/TestProcess/Program.cs | 2 +- 92 files changed, 2699 insertions(+), 402 deletions(-) create mode 100644 src/Etg.Yams.Core/Client/IYamsClient.cs create mode 100644 src/Etg.Yams.Core/Client/IYamsClientFactory.cs create mode 100644 src/Etg.Yams.Core/Client/YamsClient.cs create mode 100644 src/Etg.Yams.Core/Client/YamsClientConfig.cs create mode 100644 src/Etg.Yams.Core/Client/YamsClientConfigBuilder.cs create mode 100644 src/Etg.Yams.Core/Client/YamsClientFactory.cs create mode 100644 src/Etg.Yams.Core/Client/YamsClientOptions.cs create mode 100644 src/Etg.Yams.Core/Process/AbstractProcessDecorator.cs create mode 100644 src/Etg.Yams.Core/Process/GracefullShutdownProcessDecorator.cs create mode 100644 src/Etg.Yams.Core/Process/HealthProcessDecorator.cs create mode 100644 src/Etg.Yams.Core/Process/Ipc/IIpcConnection.cs create mode 100644 src/Etg.Yams.Core/Process/Ipc/INamedPipe.cs create mode 100644 src/Etg.Yams.Core/Process/Ipc/INamedPipeFactory.cs create mode 100644 src/Etg.Yams.Core/Process/Ipc/IpcConnection.cs create mode 100644 src/Etg.Yams.Core/Process/Ipc/NamedPipeClientAdapter.cs create mode 100644 src/Etg.Yams.Core/Process/Ipc/NamedPipeFactory.cs create mode 100644 src/Etg.Yams.Core/Process/Ipc/NamedPipeServerAdapter.cs create mode 100644 src/Etg.Yams.Core/Process/MonitorInitProcessDecorator.cs create mode 100644 src/Etg.Yams.Core/Process/ProcessFactory.cs delete mode 100644 src/Etg.Yams.Core/Process/SelfRestartingProcessFactory.cs create mode 100644 src/Etg.Yams.Core/Utils/TaskExtensions.cs create mode 100644 test/Etg.Yams.Core.Test/Data/EndToEndTest/DeploymentConfigFullIpcApp.json create mode 100644 test/Etg.Yams.Core.Test/Data/EndToEndTest/DeploymentConfigGracefulShutdownApp.json create mode 100644 test/Etg.Yams.Core.Test/Data/EndToEndTest/DeploymentConfigHeartBeatApp.json create mode 100644 test/Etg.Yams.Core.Test/Data/EndToEndTest/DeploymentConfigMonitorInitApp.json create mode 100644 test/Etg.Yams.Core.Test/Data/EndToEndTest/DeploymentConfigNoApps.json create mode 100644 test/Etg.Yams.Core.Test/Data/EndToEndTest/FullIpcApp/1.0.0/AppConfig.json create mode 100644 test/Etg.Yams.Core.Test/Data/EndToEndTest/GracefulShutdownApp/1.0.0/AppConfig.json create mode 100644 test/Etg.Yams.Core.Test/Data/EndToEndTest/HeartBeatApp/1.0.0/AppConfig.json create mode 100644 test/Etg.Yams.Core.Test/Data/EndToEndTest/MonitorInitApp/1.0.0/AppConfig.json rename test/{IPCProcess => FullIpcProcess}/App.config (100%) create mode 100644 test/FullIpcProcess/FullIpcProcess.csproj create mode 100644 test/FullIpcProcess/Program.cs create mode 100644 test/FullIpcProcess/Properties/AssemblyInfo.cs create mode 100644 test/FullIpcProcess/packages.config create mode 100644 test/GracefullShutdownProcess/GracefullShutdownProcess.csproj create mode 100644 test/GracefullShutdownProcess/Program.cs create mode 100644 test/GracefullShutdownProcess/Properties/AssemblyInfo.cs create mode 100644 test/GracefullShutdownProcess/app.config create mode 100644 test/GracefullShutdownProcess/packages.config create mode 100644 test/HeartBeatProcess/App.config create mode 100644 test/HeartBeatProcess/HeartBeatProcess.csproj create mode 100644 test/HeartBeatProcess/Program.cs create mode 100644 test/HeartBeatProcess/Properties/AssemblyInfo.cs create mode 100644 test/HeartBeatProcess/packages.config delete mode 100644 test/IPCProcess/IPCProcess.csproj delete mode 100644 test/IPCProcess/Program.cs create mode 100644 test/MonitorInitProcess/MonitorInitProcess.csproj create mode 100644 test/MonitorInitProcess/Program.cs create mode 100644 test/MonitorInitProcess/Properties/AssemblyInfo.cs create mode 100644 test/MonitorInitProcess/app.config create mode 100644 test/MonitorInitProcess/packages.config rename test/{IPCProcess => Stubs}/Properties/AssemblyInfo.cs (90%) rename test/{Etg.Yams.Core.Test => Stubs}/SimpleStubs.json (100%) create mode 100644 test/Stubs/Stubs.csproj create mode 100644 test/Stubs/packages.config diff --git a/.gitignore b/.gitignore index 0934e23..04fd5d8 100644 --- a/.gitignore +++ b/.gitignore @@ -30,6 +30,9 @@ bld/ # Visual Studo 2015 cache/options directory .vs/ +# Rider ide directory +.idea/ + # MSTest test Results [Tt]est[Rr]esult*/ [Bb]uild[Ll]og.* diff --git a/Docs/Deploy&Host_an_App_in_YAMS.md b/Docs/Deploy&Host_an_App_in_YAMS.md index e2f5780..61dd20d 100644 --- a/Docs/Deploy&Host_an_App_in_YAMS.md +++ b/Docs/Deploy&Host_an_App_in_YAMS.md @@ -250,5 +250,112 @@ That's it! The two versions should be now happily running side-by-side. # Removing or reverting a deployment To remove an app from YAMS, simply remove the corresponding entry from the `DeploymentConfig.json` file. YAMS will terminate the corresponding process and remove the app. You can also delete the associated files from the blob storage if you are never going to use this app again or keep it there to preserve history of deployments and to allow reverts. In fact, to revert a deployment in YAMS, simply edit the `DeploymentConfig.json` file and replace the current version of the app (the version to be reverted) with the old version (the version to revert to). +# Advanced features + +Yams has support for health monitoring and graceful shutdown of apps as described [here](../Docs/Overview.md#health-monitoring-and-graceful-shutdown). Note that you can choose to enable one or multiple features and apps within the same cluster can use different features. + +## Monitored initialization +By default, Yams does not monitor the initialization of apps. In other words, when an app is deployed, the associated process is launched and then Yams assumes that the app is running and ready to receive requests. With the monitored initialization feature enabled, Yams would wait for the app to finish initialization before moving on to the next app (the app would notify Yams that it's done initializing through an IPC message). + +To enable *monitored initialization* for a given app, the corresponding flag must be added to the `AppConfig.json` file as shown below: +```json +{ + "ExeName": "MyProcess.exe", + "ExeArgs": "Foo Bar", + "MonitorInitialization": true +} +``` + +The app source code will also need to be updated so that the app can communicate with Yams (using IPC). Install the `Etg.Yams` NuGet package and modify the app source code so that Yams is notified when initialization is done, as shown in the code below: + +```csharp + public static void Main(string[] args) + { + MainAsync(args).Wait(); + } + + private static async Task MainAsync(string[] args) + { + var yamsClientConfig = new YamsClientConfigBuilder(args).Build(); + var yamsClientFactory = new YamsClientFactory(); + IYamsClient yamsClient = yamsClientFactory.CreateYamsClient(yamsClientConfig); + + await Task.WhenAll(yamsClient.Connect(), Initialize()); + + await yamsClient.SendInitializationDoneMessage(); + + // ... +``` + +## Heat beats +With this feature enabled, the app is expected to send heart beat messages to Yams at steady intervals. If heart beats are not received in time, errors will be logged (more complex handling will be added in the future). To enable this feature, update the `AppConfig.json` and your app source code as shown below: + +```json +{ + "ExeName": "MyProcess.exe", + "ExeArgs": "Foo Bar", + "MonitorHealth": true +} +``` + +```csharp + public static void Main(string[] args) + { + MainAsync(args).Wait(); + } + + private static async Task MainAsync(string[] args) + { + var yamsClientConfig = new YamsClientConfigBuilder(args).Build(); + var yamsClientFactory = new YamsClientFactory(); + IYamsClient yamsClient = yamsClientFactory.CreateYamsClient(yamsClientConfig); + + await Task.WhenAll(yamsClient.Connect(), Initialize()); + + while (true) + { + await Task.Delay(heartBeatPeriod); + await yamsClient.SendHeartBeat(); + } +``` + +## Graceful Shutdown + +When graceful shutdown is enabled for a given app, Yams will deliver an event to the app and allow it a configurable amount of time to exit gracefully before closing/killing it. The graceful shutdown event will be delivered through the `YamsClient` as a normal C# event. To enable this feature, update the `AppConfig.json` and your app source code as shown below: + +```json +{ + "ExeName": "MyProcess.exe", + "ExeArgs": "Foo Bar", + "GracefulShutdown": true +} +``` + +```csharp + public static void Main(string[] args) + { + MainAsync(args).Wait(); + } + + private static async Task MainAsync(string[] args) + { + var yamsClientConfig = new YamsClientConfigBuilder(args).Build(); + var yamsClientFactory = new YamsClientFactory(); + IYamsClient yamsClient = yamsClientFactory.CreateYamsClient(yamsClientConfig); + + await Task.WhenAll(yamsClient.Connect(), Initialize()); + + bool exitMessageReceived = false; + yamsClient.ExitMessageReceived += (sender, eventArgs) => + { + exitMessageReceived = true; + }; + + while (!exitMessageReceived) + { + await DoWork(); + } +``` + # Source code The source code associated with this tutorial can be found in the [Samples/WebApp](../Samples/WebApp) directory. diff --git a/Docs/Overview.md b/Docs/Overview.md index dde1868..05fc679 100644 --- a/Docs/Overview.md +++ b/Docs/Overview.md @@ -15,6 +15,7 @@ To deploy such a Microservices-based application to Azure using the Azure cloud * **Versioning** of microservices, quick **updates**, **reverts**, etc. * Support for **Upgrade Domains** to minimize (and potentially eliminate) application downtime during updates, including first-class support for **Azure Upgrade Domains**. * Microservices can be developed in **any programming language** and deployed with YAMS (as long as your service can be started with an exe). +* **Health monitoring** and **graceful shutdown** of microservices. YAMS has first-class support for deploying applications from Azure **blob storage**, but with its pluggable storage architecture, other providers such as SQL Server or file storage can be created and plugged in as well. @@ -101,6 +102,19 @@ Note that if an update fails, Yams will not try to revert back to the old versio To revert a deployment, simply edit the `DeploymentConfig.json` file and replace the current version of the app (the version to be reverted) with the old version (the version to revert to). +## Health monitoring and graceful shutdown + +Apps deployed with Yams can optionally enable health monitoring and/or graceful shutdown. Yams uses inter-process-communication (currently [named pipes](https://msdn.microsoft.com/en-us/library/bb546085(v=vs.110).aspx)) to communicate with apps. + +There are three features available: +* *Monitored initialization*: Yams waits for the app to finish initialization before considering it ready to receive requests. If an app takes longer than expected to finish initialization (the timeout is configurable), it's considered unhealthy and is killed. +* *Monitored heart beats*: With this feature enabled, Yams expects to receive heart beats from apps at steady intervals. If a heart beat is not received in time, an error is logged (more complex handling will be added in the future). +* *Graceful shutdown*: A event is sent to the app to signal shutdown. If the app does not exit gracefully in time (the timeout is configurable), the app will be closed or killed. + +Note that each of these features can be enabled/disabled separately. In addition, apps running within the same cluster can choose to enable/disable different features. + +The [Yams Client API](../src/Etg.Yams.Core/Client/IYamsClient.cs) can be used by apps to communicate with Yams. See [Deploy and host applications in YAMS tutorial](../Docs/Deploy&Host_an_App_in_YAMS.md) to learn how you can enable these features (one ore more features can be enabled at a time). + ## Sharing infrastructure One of the main goals of Yams is sharing infrastructure to reduce cost. In fact, some microservices consume little resources and can be deployed alongside other microservices. In addition, sharing infrastructure reduces the cost of over-provisioning resources. To illustrate this, consider an application composed of two microservices. Each microservice requires 2 VMs at normal operation load and 4 VMs at peak time. If each microservice is deployed separately, 8 VMs are needed in total (4 VMs per microservice). However, in practice, the peak time resources are over estimated and the peak time of one microservice does not necessarily overlap with the peak time of another microservice. If the same VMs are shared by both microservices and peak times are not likely to overlap, 6 VMs can be sufficient for both microservices (which saves us 2 VMs). In fact, this strategy works better for a large number of microservices where the probability of all microservices peaking at the same time decreases with the number of microservices and as a result, sharing infrastructure can result in large savings. diff --git a/Etg.Yams.sln b/Etg.Yams.sln index 3b8e4a0..bcaf84e 100644 --- a/Etg.Yams.sln +++ b/Etg.Yams.sln @@ -37,7 +37,15 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AzureUtilsTest", "test\Azur EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Etg.Yams.Core", "src\Etg.Yams.Core\Etg.Yams.Core.csproj", "{7145E485-34FA-4632-89B0-BD27C96AF69C}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "IPCProcess", "test\IPCProcess\IPCProcess.csproj", "{4F7B6739-5452-4F8C-8904-72112FAB3C1E}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Stubs", "test\Stubs\Stubs.csproj", "{68CD41F8-A6C3-4D43-93CA-92E898254CBD}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MonitorInitProcess", "test\MonitorInitProcess\MonitorInitProcess.csproj", "{BB84CC4C-EB11-4A61-8ED4-791EADAA46E1}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GracefullShutdownProcess", "test\GracefullShutdownProcess\GracefullShutdownProcess.csproj", "{706E37D7-B148-4734-9695-0D1AE6D4B3D5}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "HeartBeatProcess", "test\HeartBeatProcess\HeartBeatProcess.csproj", "{257044FB-F7A8-499D-8EF2-B5CAD76D617A}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "FullIpcProcess", "test\FullIpcProcess\FullIpcProcess.csproj", "{99F3A36C-7930-4670-A8B3-7137D891671D}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -105,10 +113,26 @@ Global {7145E485-34FA-4632-89B0-BD27C96AF69C}.Debug|Any CPU.Build.0 = Debug|Any CPU {7145E485-34FA-4632-89B0-BD27C96AF69C}.Release|Any CPU.ActiveCfg = Release|Any CPU {7145E485-34FA-4632-89B0-BD27C96AF69C}.Release|Any CPU.Build.0 = Release|Any CPU - {4F7B6739-5452-4F8C-8904-72112FAB3C1E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {4F7B6739-5452-4F8C-8904-72112FAB3C1E}.Debug|Any CPU.Build.0 = Debug|Any CPU - {4F7B6739-5452-4F8C-8904-72112FAB3C1E}.Release|Any CPU.ActiveCfg = Release|Any CPU - {4F7B6739-5452-4F8C-8904-72112FAB3C1E}.Release|Any CPU.Build.0 = Release|Any CPU + {68CD41F8-A6C3-4D43-93CA-92E898254CBD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {68CD41F8-A6C3-4D43-93CA-92E898254CBD}.Debug|Any CPU.Build.0 = Debug|Any CPU + {68CD41F8-A6C3-4D43-93CA-92E898254CBD}.Release|Any CPU.ActiveCfg = Release|Any CPU + {68CD41F8-A6C3-4D43-93CA-92E898254CBD}.Release|Any CPU.Build.0 = Release|Any CPU + {BB84CC4C-EB11-4A61-8ED4-791EADAA46E1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {BB84CC4C-EB11-4A61-8ED4-791EADAA46E1}.Debug|Any CPU.Build.0 = Debug|Any CPU + {BB84CC4C-EB11-4A61-8ED4-791EADAA46E1}.Release|Any CPU.ActiveCfg = Release|Any CPU + {BB84CC4C-EB11-4A61-8ED4-791EADAA46E1}.Release|Any CPU.Build.0 = Release|Any CPU + {706E37D7-B148-4734-9695-0D1AE6D4B3D5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {706E37D7-B148-4734-9695-0D1AE6D4B3D5}.Debug|Any CPU.Build.0 = Debug|Any CPU + {706E37D7-B148-4734-9695-0D1AE6D4B3D5}.Release|Any CPU.ActiveCfg = Release|Any CPU + {706E37D7-B148-4734-9695-0D1AE6D4B3D5}.Release|Any CPU.Build.0 = Release|Any CPU + {257044FB-F7A8-499D-8EF2-B5CAD76D617A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {257044FB-F7A8-499D-8EF2-B5CAD76D617A}.Debug|Any CPU.Build.0 = Debug|Any CPU + {257044FB-F7A8-499D-8EF2-B5CAD76D617A}.Release|Any CPU.ActiveCfg = Release|Any CPU + {257044FB-F7A8-499D-8EF2-B5CAD76D617A}.Release|Any CPU.Build.0 = Release|Any CPU + {99F3A36C-7930-4670-A8B3-7137D891671D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {99F3A36C-7930-4670-A8B3-7137D891671D}.Debug|Any CPU.Build.0 = Debug|Any CPU + {99F3A36C-7930-4670-A8B3-7137D891671D}.Release|Any CPU.ActiveCfg = Release|Any CPU + {99F3A36C-7930-4670-A8B3-7137D891671D}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -129,7 +153,11 @@ Global {E66F281C-BD50-45B0-AAEF-FB87E4C9D0CC} = {2A52BDC8-4B16-43FE-9E9D-A7D0A72C17C8} {2FE963BD-D826-4CC5-8A63-864CDA212233} = {2A52BDC8-4B16-43FE-9E9D-A7D0A72C17C8} {7145E485-34FA-4632-89B0-BD27C96AF69C} = {5925E681-1BEA-456D-B9E0-CA175ABBFA9D} - {4F7B6739-5452-4F8C-8904-72112FAB3C1E} = {2A52BDC8-4B16-43FE-9E9D-A7D0A72C17C8} + {68CD41F8-A6C3-4D43-93CA-92E898254CBD} = {2A52BDC8-4B16-43FE-9E9D-A7D0A72C17C8} + {BB84CC4C-EB11-4A61-8ED4-791EADAA46E1} = {2A52BDC8-4B16-43FE-9E9D-A7D0A72C17C8} + {706E37D7-B148-4734-9695-0D1AE6D4B3D5} = {2A52BDC8-4B16-43FE-9E9D-A7D0A72C17C8} + {257044FB-F7A8-499D-8EF2-B5CAD76D617A} = {2A52BDC8-4B16-43FE-9E9D-A7D0A72C17C8} + {99F3A36C-7930-4670-A8B3-7137D891671D} = {2A52BDC8-4B16-43FE-9E9D-A7D0A72C17C8} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution EnterpriseLibraryConfigurationToolBinariesPathV6 = packages\EnterpriseLibrary.TransientFaultHandling.6.0.1304.0\lib\portable-net45+win+wp8 diff --git a/README.md b/README.md index 7d76fae..a474fec 100644 --- a/README.md +++ b/README.md @@ -10,6 +10,7 @@ YAMS * **Versioning** of microservices, quick **updates**, **reverts**, etc. * Support for **Upgrade Domains** to minimize (and potentially eliminate) application downtime during updates, including first-class support for **Azure Upgrade Domains**. * Microservices can be developed in **any programming language** and deployed with YAMS (as long as your service can be started with an exe). +* **Health monitoring** and **graceful shutdown** of microservices. YAMS has first-class support for deploying applications from Azure **blob storage**, but with its pluggable storage architecture, other providers such as SQL Server or file storage can be created and plugged in as well. diff --git a/src/Etg.Yams.Core/Application/Application.cs b/src/Etg.Yams.Core/Application/Application.cs index 3a874ec..deb9e42 100644 --- a/src/Etg.Yams.Core/Application/Application.cs +++ b/src/Etg.Yams.Core/Application/Application.cs @@ -13,17 +13,17 @@ protected Application(AppIdentity identity, string path) Path = path; } - protected async Task StartProcess(IProcess process) + protected async Task StartProcess(IProcess process, string args) { try { - await process.Start(); + await process.Start(args); process.Exited += OnProcessExited; return true; } - catch (Exception) + catch (Exception e) { - Trace.TraceInformation("Could not start the host process for application {0}", Identity); + Trace.TraceError($"Could not start the host process for application {Identity}, Inner Exception: {e}"); return false; } } diff --git a/src/Etg.Yams.Core/Application/ApplicationConfig.cs b/src/Etg.Yams.Core/Application/ApplicationConfig.cs index c0c6978..7e3eb47 100644 --- a/src/Etg.Yams.Core/Application/ApplicationConfig.cs +++ b/src/Etg.Yams.Core/Application/ApplicationConfig.cs @@ -2,14 +2,26 @@ { public class ApplicationConfig { - public AppIdentity Identity { get; private set; } - public string ExeName { get; private set; } - public string ExeArgs { get; private set; } + public AppIdentity Identity { get; } + public string ExeName { get; } + public string ExeArgs { get; } + public bool MonitorInitialization { get; } + public bool MonitorHealth { get; } + public bool GracefulShutdown { get; } - public ApplicationConfig(AppIdentity identity, string exeName, string exeArgs) + public ApplicationConfig( + AppIdentity identity, + string exeName, + string exeArgs, + bool monitorInitialization = false, + bool monitorHealth = false, + bool gracefulShutdown = false) { Identity = identity; ExeArgs = exeArgs; + MonitorInitialization = monitorInitialization; + MonitorHealth = monitorHealth; + GracefulShutdown = gracefulShutdown; ExeName = exeName; } } diff --git a/src/Etg.Yams.Core/Application/ApplicationConfigParser.cs b/src/Etg.Yams.Core/Application/ApplicationConfigParser.cs index 92c4b54..03c74a9 100644 --- a/src/Etg.Yams.Core/Application/ApplicationConfigParser.cs +++ b/src/Etg.Yams.Core/Application/ApplicationConfigParser.cs @@ -1,11 +1,9 @@ -using System; -using System.Collections.Generic; +using System.Collections.Generic; using System.IO; using System.Text.RegularExpressions; using System.Threading.Tasks; using Etg.Yams.Install; using Etg.Yams.Json; -using Etg.Yams.Storage.Config; namespace Etg.Yams.Application { @@ -20,6 +18,9 @@ private class ApplicationConfigData #pragma warning disable 649 public string ExeName; public string ExeArgs; + public bool MonitorInitialization; + public bool MonitorHealth; + public bool GracefulShutdown; #pragma warning restore 649 } @@ -33,14 +34,16 @@ public async Task ParseFile(string path, AppInstallConfig app { using (StreamReader r = new StreamReader(path)) { - return await Parse(await _jsonSerializer.DeserializeAsync(await r.ReadToEndAsync()), appInstallConfig); + return await Parse(await _jsonSerializer.DeserializeAsync( + await r.ReadToEndAsync()), appInstallConfig); } } private async Task Parse(ApplicationConfigData appConfigData, AppInstallConfig appInstallConfig) { string args = await SubstituteSymbols(appConfigData.ExeArgs, appInstallConfig); - return new ApplicationConfig(appInstallConfig.AppIdentity, appConfigData.ExeName, args); + return new ApplicationConfig(appInstallConfig.AppIdentity, appConfigData.ExeName, args, + appConfigData.MonitorInitialization, appConfigData.MonitorHealth, appConfigData.GracefulShutdown); } private async Task SubstituteSymbols(string str, AppInstallConfig appInstallConfig) @@ -62,7 +65,7 @@ private async Task SubstituteSymbols(string str, AppInstallConfig appIns private async Task SubstitueSymbol(string str, string symbol, AppInstallConfig appInstallConfig) { string symbolValue = await _symbolResolver.ResolveSymbol(appInstallConfig, symbol); - return str.Replace(string.Format("${{{0}}}", symbol), symbolValue); + return str.Replace($"${{{symbol}}}", symbolValue); } } } diff --git a/src/Etg.Yams.Core/Application/ApplicationPool.cs b/src/Etg.Yams.Core/Application/ApplicationPool.cs index 715e7cf..ef5168a 100644 --- a/src/Etg.Yams.Core/Application/ApplicationPool.cs +++ b/src/Etg.Yams.Core/Application/ApplicationPool.cs @@ -19,17 +19,19 @@ public async Task AddApplication(IApplication application) { if (HasApplication(application.Identity)) { - throw new ArgumentException(string.Format("Cannot add application {0} to the application pool because it is already there", application.Identity)); + throw new ArgumentException( + $"Cannot add application {application.Identity} to the application pool because it is already there"); } if (! await StartApplication(application)) { - throw new Exception(string.Format("Failed to start application {0}", application.Identity)); + throw new Exception($"Failed to start application {application.Identity}"); } if (!_applications.TryAdd(application.Identity, application)) { - throw new Exception(string.Format("Could not add the application {0} to the concurent dictionary. This is likely a bug", application.Identity)); + throw new Exception( + $"Could not add the application {application.Identity} to the concurent dictionary. This is likely a bug"); } application.Exited += OnApplicationExited; } @@ -48,19 +50,19 @@ public IApplication GetApplication(AppIdentity appIdentity) return _applications[appIdentity]; } - public IEnumerable Applications { - get { return _applications.Values; } - } + public IEnumerable Applications => _applications.Values; public async Task RemoveApplication(AppIdentity appIdentity) { IApplication application; if (!_applications.TryRemove(appIdentity, out application)) { - throw new ArgumentException(string.Format("Cannot remove application {0} because it doesn't exist in the pool", appIdentity)); + throw new ArgumentException( + $"Cannot remove application {appIdentity} because it doesn't exist in the pool"); } application.Exited -= OnApplicationExited; await application.Stop(); + application.Dispose(); await Task.Delay(5000); } @@ -78,7 +80,7 @@ private static async Task StartApplication(IApplication application) private void OnApplicationExited(object sender, ApplicationExitedArgs args) { AppIdentity appIdentity = args.AppIdentity; - Trace.TraceError("Application {0} exited unexpectedly with message {1}", appIdentity, args.Message); + Trace.TraceError($"Application {appIdentity} exited unexpectedly with message {args.Message}"); if (_applications.ContainsKey(appIdentity)) { RemoveApplication(appIdentity).Wait(); @@ -96,10 +98,13 @@ public async Task Shutdown() public void Dispose() { + var tasks = new List(); foreach (IApplication application in Applications) { + tasks.Add(RemoveApplication(application.Identity)); application.Dispose(); } + Task.WhenAll(tasks).Wait(); } } } diff --git a/src/Etg.Yams.Core/Application/ConfigurableApplication.cs b/src/Etg.Yams.Core/Application/ConfigurableApplication.cs index a46334e..d7f7cc6 100644 --- a/src/Etg.Yams.Core/Application/ConfigurableApplication.cs +++ b/src/Etg.Yams.Core/Application/ConfigurableApplication.cs @@ -17,8 +17,8 @@ public class ConfigurableApplication : Application /// exe name, args, etc. /// A factory to create a process to run the exe /// Used to stop a process - public ConfigurableApplication(string path, ApplicationConfig appConfig, IProcessFactory processFactory, IProcessStopper processStopper) - : base(appConfig.Identity, path) + public ConfigurableApplication(string path, ApplicationConfig appConfig, IProcessFactory processFactory, + IProcessStopper processStopper) : base(appConfig.Identity, path) { _appConfig = appConfig; _processFactory = processFactory; @@ -27,8 +27,9 @@ public ConfigurableApplication(string path, ApplicationConfig appConfig, IProces public override Task Start() { - _process = _processFactory.CreateProcess(System.IO.Path.Combine(Path, _appConfig.ExeName), _appConfig.ExeArgs); - return StartProcess(_process); + _process = _processFactory.CreateProcess(System.IO.Path.Combine(Path, _appConfig.ExeName), + _appConfig.MonitorInitialization, _appConfig.MonitorHealth, _appConfig.GracefulShutdown); + return StartProcess(_process, _appConfig.ExeArgs); } public override Task Stop() diff --git a/src/Etg.Yams.Core/Client/IYamsClient.cs b/src/Etg.Yams.Core/Client/IYamsClient.cs new file mode 100644 index 0000000..31e32f5 --- /dev/null +++ b/src/Etg.Yams.Core/Client/IYamsClient.cs @@ -0,0 +1,13 @@ +using System; +using System.Threading.Tasks; + +namespace Etg.Yams.Client +{ + public interface IYamsClient : IDisposable + { + Task Connect(); + Task SendHeartBeat(); + Task SendInitializationDoneMessage(); + event EventHandler ExitMessageReceived; + } +} \ No newline at end of file diff --git a/src/Etg.Yams.Core/Client/IYamsClientFactory.cs b/src/Etg.Yams.Core/Client/IYamsClientFactory.cs new file mode 100644 index 0000000..5bcd7db --- /dev/null +++ b/src/Etg.Yams.Core/Client/IYamsClientFactory.cs @@ -0,0 +1,7 @@ +namespace Etg.Yams.Client +{ + public interface IYamsClientFactory + { + IYamsClient CreateYamsClient(YamsClientConfig config); + } +} \ No newline at end of file diff --git a/src/Etg.Yams.Core/Client/YamsClient.cs b/src/Etg.Yams.Core/Client/YamsClient.cs new file mode 100644 index 0000000..57a760e --- /dev/null +++ b/src/Etg.Yams.Core/Client/YamsClient.cs @@ -0,0 +1,102 @@ +using Etg.Yams.Process.Ipc; +using Etg.Yams.Utils; +using System; +using System.Diagnostics; +using System.Threading.Tasks; + +namespace Etg.Yams.Client +{ + public class YamsClient : IYamsClient + { + private readonly YamsClientConfig _config; + private readonly IIpcConnection _initConnection; + private readonly IIpcConnection _exitConnection; + private readonly IIpcConnection _healthConnection; + private Task _waitForExit; + + public event EventHandler ExitMessageReceived; + + public YamsClient(YamsClientConfig config, IIpcConnection initConnection, IIpcConnection exitConnection, + IIpcConnection healthConnection) + { + _config = config; + _initConnection = initConnection; + _exitConnection = exitConnection; + _healthConnection = healthConnection; + } + + public async Task Connect() + { + Trace.TraceInformation($"Connecting IPC connections.."); + if (_initConnection != null) + { + await _initConnection.Connect().Timeout(_config.ConnectTimeout, "IPC connection failed to connect"); + } + if(_exitConnection != null) + { + await _exitConnection.Connect().Timeout(_config.ConnectTimeout, "IPC connection failed to connect"); + } + if (_healthConnection != null) + { + await _healthConnection.Connect().Timeout(_config.ConnectTimeout, "IPC connection failed to connect"); + } + Trace.TraceInformation("IPC connections connected!"); + _waitForExit = WaitForExit(); + } + + public async Task SendInitializationDoneMessage() + { + if (_initConnection == null) + { + Trace.TraceError( + "Initialization monitoring is not supported for this app. Check your AppConfig.json file"); + return; + } + Trace.TraceInformation("Sending Initialization message to Yams.."); + await _initConnection.SendMessage("[INITIALIZE_DONE]") + .Timeout(_config.InitDoneMessageTimeout, "Sending initialization message to Yams has timed out"); + Trace.TraceInformation("Initialization message has been sent to Yams successfully!"); + } + + public async Task SendHeartBeat() + { + if (_healthConnection == null) + { + Trace.TraceError( + "Health monitoring is not supported for this app. Check your AppConfig.json file"); + return; + } + Trace.TraceInformation("Sending hear beat message to Yams.."); + await _healthConnection.SendMessage("[HEALTH_OK]") + .Timeout(_config.HeartBeatMessageTimeout, "Send heart beat message to Yams has timed out"); + Trace.TraceInformation("Heart beat message has been sent to Yams successfully!"); + } + + public void Dispose() + { + _initConnection?.Dispose(); + _exitConnection?.Dispose(); + _healthConnection?.Dispose(); + } + + private async Task WaitForExit() + { + if(_exitConnection == null) + { + return; + } + while (true) + { + Trace.TraceInformation("Waiting for an exit message from Yams.."); + string msg = await _exitConnection.ReadMessage(); + if (msg == "[EXIT]") + { + Trace.TraceInformation("Exit request received from Yams"); + ExitMessageReceived?.Invoke(this, EventArgs.Empty); + break; + } + Trace.TraceError($"Unexpected message received from app: {msg}, Expected [EXIT]"); + } + } + } +} diff --git a/src/Etg.Yams.Core/Client/YamsClientConfig.cs b/src/Etg.Yams.Core/Client/YamsClientConfig.cs new file mode 100644 index 0000000..b23aaa0 --- /dev/null +++ b/src/Etg.Yams.Core/Client/YamsClientConfig.cs @@ -0,0 +1,21 @@ +using System; + +namespace Etg.Yams.Client +{ + public class YamsClientConfig + { + public YamsClientConfig(TimeSpan connectTimeout, TimeSpan initDoneMessageTimeout, + TimeSpan heartBeatMessageTimeout, string[] processArgs) + { + ConnectTimeout = connectTimeout; + InitDoneMessageTimeout = initDoneMessageTimeout; + HeartBeatMessageTimeout = heartBeatMessageTimeout; + ProcessArgs = processArgs; + } + + public TimeSpan ConnectTimeout { get; } + public TimeSpan InitDoneMessageTimeout { get; } + public TimeSpan HeartBeatMessageTimeout { get; } + public string[] ProcessArgs { get; set; } + } +} \ No newline at end of file diff --git a/src/Etg.Yams.Core/Client/YamsClientConfigBuilder.cs b/src/Etg.Yams.Core/Client/YamsClientConfigBuilder.cs new file mode 100644 index 0000000..09b711c --- /dev/null +++ b/src/Etg.Yams.Core/Client/YamsClientConfigBuilder.cs @@ -0,0 +1,41 @@ +using System; + +namespace Etg.Yams.Client +{ + public class YamsClientConfigBuilder + { + private readonly string[] _processArgs; + private TimeSpan _connectTimeout = TimeSpan.FromSeconds(30); + private TimeSpan _initDoneMessageTimeout = TimeSpan.FromSeconds(30); + private TimeSpan _heartBeatMessageTimeout = TimeSpan.FromSeconds(30); + + public YamsClientConfigBuilder(string[] processArgs) + { + _processArgs = processArgs; + } + + public YamsClientConfigBuilder SetConnectTimeout(TimeSpan timeout) + { + _connectTimeout = timeout; + return this; + } + + public YamsClientConfigBuilder SetInitDoneMessageTimeout(TimeSpan timeout) + { + _initDoneMessageTimeout = timeout; + return this; + } + + public YamsClientConfigBuilder SetHeartBeatMessageTimeout(TimeSpan timeout) + { + _heartBeatMessageTimeout = timeout; + return this; + } + + public YamsClientConfig Build() + { + return new YamsClientConfig(_connectTimeout, _initDoneMessageTimeout, + _heartBeatMessageTimeout, _processArgs); + } + } +} \ No newline at end of file diff --git a/src/Etg.Yams.Core/Client/YamsClientFactory.cs b/src/Etg.Yams.Core/Client/YamsClientFactory.cs new file mode 100644 index 0000000..517b482 --- /dev/null +++ b/src/Etg.Yams.Core/Client/YamsClientFactory.cs @@ -0,0 +1,33 @@ +using System; +using CommandLine; +using Etg.Yams.Process.Ipc; + +namespace Etg.Yams.Client +{ + public class YamsClientFactory : IYamsClientFactory + { + public IYamsClient CreateYamsClient(YamsClientConfig config) + { + var options = new YamsClientOptions(); + bool isValid = Parser.Default.ParseArgumentsStrict(config.ProcessArgs, options); + + IpcConnection initConnection = null; + IpcConnection exitConnection = null; + IpcConnection healthConnection = null; + + if (!string.IsNullOrEmpty(options.InitializationPipeName)) + { + initConnection = new IpcConnection(new NamedPipeClientAdapter(options.InitializationPipeName)); + } + if (!string.IsNullOrEmpty(options.ExitPipeName)) + { + exitConnection = new IpcConnection(new NamedPipeClientAdapter(options.ExitPipeName)); + } + if (!string.IsNullOrEmpty(options.HealthPipeName)) + { + healthConnection = new IpcConnection(new NamedPipeClientAdapter(options.HealthPipeName)); + } + return new YamsClient(config, initConnection, exitConnection, healthConnection); + } + } +} \ No newline at end of file diff --git a/src/Etg.Yams.Core/Client/YamsClientOptions.cs b/src/Etg.Yams.Core/Client/YamsClientOptions.cs new file mode 100644 index 0000000..1b2a396 --- /dev/null +++ b/src/Etg.Yams.Core/Client/YamsClientOptions.cs @@ -0,0 +1,14 @@ +using CommandLine; + +namespace Etg.Yams.Client +{ + public class YamsClientOptions + { + [Option()] + public string InitializationPipeName { get; set; } + [Option()] + public string ExitPipeName { get; set; } + [Option()] + public string HealthPipeName { get; set; } + } +} \ No newline at end of file diff --git a/src/Etg.Yams.Core/Etg.Yams.Core.csproj b/src/Etg.Yams.Core/Etg.Yams.Core.csproj index c24e7f3..7a79927 100644 --- a/src/Etg.Yams.Core/Etg.Yams.Core.csproj +++ b/src/Etg.Yams.Core/Etg.Yams.Core.csproj @@ -35,6 +35,10 @@ ..\..\packages\Autofac.3.5.2\lib\net40\Autofac.dll True + + ..\..\packages\CommandLineParser.1.9.71\lib\net45\CommandLine.dll + True + ..\..\packages\Newtonsoft.Json.6.0.8\lib\net45\Newtonsoft.Json.dll True @@ -67,6 +71,13 @@ + + + + + + + @@ -75,10 +86,23 @@ + + + + + + + + + + + + + @@ -86,12 +110,11 @@ - - + @@ -118,6 +141,10 @@ Common + + + + + \ No newline at end of file diff --git a/test/FullIpcProcess/Program.cs b/test/FullIpcProcess/Program.cs new file mode 100644 index 0000000..aa26f87 --- /dev/null +++ b/test/FullIpcProcess/Program.cs @@ -0,0 +1,65 @@ +using System; +using System.IO; +using System.Threading.Tasks; +using Etg.Yams.Client; + +namespace FullIpcProcess +{ + internal class Program + { + public static void Main(string[] args) + { + Run(args).Wait(); + } + + private static async Task Run(string[] args) + { + Console.WriteLine("args " + string.Join(" ", args)); + var yamsClientConfig = new YamsClientConfigBuilder(args).Build(); + var yamsClientFactory = new YamsClientFactory(); + + IYamsClient yamsClient = yamsClientFactory.CreateYamsClient(yamsClientConfig); + + await Task.WhenAll(yamsClient.Connect(), Initialize()); + + bool exitMessageReceived = false; + yamsClient.ExitMessageReceived += (sender, eventArgs) => + { + Console.WriteLine("Exit message received!"); + exitMessageReceived = true; + }; + + File.WriteAllText($"FullIpcApp.exe.out", $"FullIpcApp.exe {args[0]} {args[1]}"); + + Console.WriteLine("Send initialization done message..."); + await yamsClient.SendInitializationDoneMessage(); + Console.WriteLine("Initialization done message sent!"); + + while (!exitMessageReceived) + { + await DoWork(); + Console.WriteLine("Sending heart beat.."); + await yamsClient.SendHeartBeat(); + Console.WriteLine("Heart beat sent!"); + } + await Shutdown(); + Console.WriteLine("Exiting.."); + } + + private static async Task Shutdown() + { + await Task.Delay(1000); + } + + private static async Task DoWork() + { + Console.WriteLine("Doing work"); + await Task.Delay(1000); + } + + private static Task Initialize() + { + return Task.Delay(1000); + } + } +} \ No newline at end of file diff --git a/test/FullIpcProcess/Properties/AssemblyInfo.cs b/test/FullIpcProcess/Properties/AssemblyInfo.cs new file mode 100644 index 0000000..6aef49b --- /dev/null +++ b/test/FullIpcProcess/Properties/AssemblyInfo.cs @@ -0,0 +1,36 @@ +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +// General Information about an assembly is controlled through the following +// set of attributes. Change these attribute values to modify the information +// associated with an assembly. +[assembly: AssemblyTitle("FullIpcProcess")] +[assembly: AssemblyDescription("")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("")] +[assembly: AssemblyProduct("FullIpcProcess")] +[assembly: AssemblyCopyright("Copyright © 2016")] +[assembly: AssemblyTrademark("")] +[assembly: AssemblyCulture("")] + +// Setting ComVisible to false makes the types in this assembly not visible +// to COM components. If you need to access a type in this assembly from +// COM, set the ComVisible attribute to true on that type. +[assembly: ComVisible(false)] + +// The following GUID is for the ID of the typelib if this project is exposed to COM +[assembly: Guid("99f3a36c-7930-4670-a8b3-7137d891671d")] + +// Version information for an assembly consists of the following four values: +// +// Major Version +// Minor Version +// Build Number +// Revision +// +// You can specify all the values or you can default the Build and Revision Numbers +// by using the '*' as shown below: +// [assembly: AssemblyVersion("1.0.*")] +[assembly: AssemblyVersion("1.0.0.0")] +[assembly: AssemblyFileVersion("1.0.0.0")] diff --git a/test/FullIpcProcess/packages.config b/test/FullIpcProcess/packages.config new file mode 100644 index 0000000..805b460 --- /dev/null +++ b/test/FullIpcProcess/packages.config @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/test/GracefullShutdownProcess/GracefullShutdownProcess.csproj b/test/GracefullShutdownProcess/GracefullShutdownProcess.csproj new file mode 100644 index 0000000..b15c0dc --- /dev/null +++ b/test/GracefullShutdownProcess/GracefullShutdownProcess.csproj @@ -0,0 +1,145 @@ + + + + + Debug + AnyCPU + {706E37D7-B148-4734-9695-0D1AE6D4B3D5} + {FAE04EC0-301F-11D3-BF4B-00C04F79EFBC} + Exe + Properties + GracefullShutdownProcess + GracefullShutdownProcess + v4.5.2 + 512 + + + + AnyCPU + true + full + false + ..\Etg.Yams.Core.Test\bin\Debug\Data\GracefullShutdownProcess\ + DEBUG;TRACE + prompt + 4 + + + AnyCPU + pdbonly + true + ..\Etg.Yams.Core.Test\bin\Release\Data\GracefullShutdownProcess\ + TRACE + prompt + 4 + + + + ..\..\packages\Autofac.3.5.2\lib\net40\Autofac.dll + True + + + ..\..\packages\CommandLineParser.1.9.71\lib\net45\CommandLine.dll + True + + + ..\..\packages\Etg.Yams.1.2.0-rc\lib\net451\Etg.Yams.dll + True + + + ..\..\packages\Etg.Yams.1.2.0-rc\lib\net451\Etg.Yams.AzureBlobStorageDeploymentRepository.dll + True + + + ..\..\packages\Etg.Yams.1.2.0-rc\lib\net451\Etg.Yams.AzureBlobStorageUpdateSession.dll + True + + + ..\..\packages\Etg.Yams.1.2.0-rc\lib\net451\Etg.Yams.AzureUtils.dll + True + + + ..\..\packages\Etg.Yams.1.2.0-rc\lib\net451\Etg.Yams.Common.dll + True + + + ..\..\packages\Etg.Yams.1.2.0-rc\lib\net451\Etg.Yams.Core.dll + True + + + ..\..\packages\Microsoft.Azure.KeyVault.Core.1.0.0\lib\net40\Microsoft.Azure.KeyVault.Core.dll + True + + + ..\..\packages\Microsoft.Data.Edm.5.6.4\lib\net40\Microsoft.Data.Edm.dll + True + + + ..\..\packages\Microsoft.Data.OData.5.6.4\lib\net40\Microsoft.Data.OData.dll + True + + + ..\..\packages\Microsoft.Data.Services.Client.5.6.4\lib\net40\Microsoft.Data.Services.Client.dll + True + + + ..\..\packages\EnterpriseLibrary.TransientFaultHandling.6.0.1304.0\lib\portable-net45+win+wp8\Microsoft.Practices.EnterpriseLibrary.TransientFaultHandling.dll + True + + + ..\..\packages\WindowsAzure.Storage.6.2.0\lib\net40\Microsoft.WindowsAzure.Storage.dll + True + + + ..\..\packages\Newtonsoft.Json.6.0.8\lib\net45\Newtonsoft.Json.dll + True + + + ..\..\packages\Semver.2.0.4\lib\net452\Semver.dll + True + + + + + + ..\..\packages\Rx-Core.2.2.5\lib\net45\System.Reactive.Core.dll + True + + + ..\..\packages\Rx-Interfaces.2.2.5\lib\net45\System.Reactive.Interfaces.dll + True + + + ..\..\packages\Rx-Linq.2.2.5\lib\net45\System.Reactive.Linq.dll + True + + + ..\..\packages\Rx-PlatformServices.2.2.5\lib\net45\System.Reactive.PlatformServices.dll + True + + + ..\..\packages\System.Spatial.5.6.4\lib\net40\System.Spatial.dll + True + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/test/GracefullShutdownProcess/Program.cs b/test/GracefullShutdownProcess/Program.cs new file mode 100644 index 0000000..49600fa --- /dev/null +++ b/test/GracefullShutdownProcess/Program.cs @@ -0,0 +1,47 @@ +using System; +using System.Threading.Tasks; +using Etg.Yams.Client; +using System.IO; + +namespace GracefullShutdownProcess +{ + internal class Program + { + public static void Main(string[] args) + { + Run(args).Wait(); + } + + private static async Task Run(string[] args) + { + Console.WriteLine("args " + string.Join(" ", args)); + int shutdownDelay = 1000 * Convert.ToInt32(args[2]); + + var yamsClientConfig = new YamsClientConfigBuilder(args).Build(); + var yamsClientFactory = new YamsClientFactory(); + + Console.WriteLine("Initializing..."); + IYamsClient yamsClient = yamsClientFactory.CreateYamsClient(yamsClientConfig); + + await yamsClient.Connect(); + Console.WriteLine("Initialization done!"); + + bool exitMessageReceived = false; + yamsClient.ExitMessageReceived += (sender, eventArgs) => + { + Console.WriteLine("Exit message received!"); + exitMessageReceived = true; + }; + + File.WriteAllText($"GracefulShutdownApp.exe.out", $"GracefulShutdownApp.exe {args[0]} {args[1]}"); + + while (!exitMessageReceived) + { + await Task.Delay(100); + Console.WriteLine("Doing work"); + } + await Task.Delay(shutdownDelay); + Console.WriteLine("Exiting.."); + } + } +} \ No newline at end of file diff --git a/test/GracefullShutdownProcess/Properties/AssemblyInfo.cs b/test/GracefullShutdownProcess/Properties/AssemblyInfo.cs new file mode 100644 index 0000000..c6d6fe5 --- /dev/null +++ b/test/GracefullShutdownProcess/Properties/AssemblyInfo.cs @@ -0,0 +1,40 @@ +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +// General Information about an assembly is controlled through the following +// set of attributes. Change these attribute values to modify the information +// associated with an assembly. + +[assembly: AssemblyTitle("GracefullShutdownProcess")] +[assembly: AssemblyDescription("")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("")] +[assembly: AssemblyProduct("GracefullShutdownProcess")] +[assembly: AssemblyCopyright("Copyright © 2016")] +[assembly: AssemblyTrademark("")] +[assembly: AssemblyCulture("")] + +// Setting ComVisible to false makes the types in this assembly not visible +// to COM components. If you need to access a type in this assembly from +// COM, set the ComVisible attribute to true on that type. + +[assembly: ComVisible(false)] + +// The following GUID is for the ID of the typelib if this project is exposed to COM + +[assembly: Guid("706E37D7-B148-4734-9695-0D1AE6D4B3D5")] + +// Version information for an assembly consists of the following four values: +// +// Major Version +// Minor Version +// Build Number +// Revision +// +// You can specify all the values or you can default the Build and Revision Numbers +// by using the '*' as shown below: +// [assembly: AssemblyVersion("1.0.*")] + +[assembly: AssemblyVersion("1.0.0.0")] +[assembly: AssemblyFileVersion("1.0.0.0")] \ No newline at end of file diff --git a/test/GracefullShutdownProcess/app.config b/test/GracefullShutdownProcess/app.config new file mode 100644 index 0000000..ff99501 --- /dev/null +++ b/test/GracefullShutdownProcess/app.config @@ -0,0 +1,3 @@ + + + diff --git a/test/GracefullShutdownProcess/packages.config b/test/GracefullShutdownProcess/packages.config new file mode 100644 index 0000000..805b460 --- /dev/null +++ b/test/GracefullShutdownProcess/packages.config @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/test/HeartBeatProcess/App.config b/test/HeartBeatProcess/App.config new file mode 100644 index 0000000..88fa402 --- /dev/null +++ b/test/HeartBeatProcess/App.config @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/test/HeartBeatProcess/HeartBeatProcess.csproj b/test/HeartBeatProcess/HeartBeatProcess.csproj new file mode 100644 index 0000000..f560355 --- /dev/null +++ b/test/HeartBeatProcess/HeartBeatProcess.csproj @@ -0,0 +1,146 @@ + + + + + Debug + AnyCPU + {257044FB-F7A8-499D-8EF2-B5CAD76D617A} + Exe + Properties + HeartBeatProcess + HeartBeatProcess + v4.5.2 + 512 + true + + + AnyCPU + true + full + false + ..\Etg.Yams.Core.Test\bin\Debug\Data\HeartBeatProcess\ + DEBUG;TRACE + prompt + 4 + + + AnyCPU + pdbonly + true + ..\Etg.Yams.Core.Test\bin\Release\Data\HeartBeatProcess\ + TRACE + prompt + 4 + + + + ..\..\packages\Autofac.3.5.2\lib\net40\Autofac.dll + True + + + ..\..\packages\CommandLineParser.1.9.71\lib\net45\CommandLine.dll + True + + + ..\..\packages\Etg.Yams.1.2.0-rc\lib\net451\Etg.Yams.dll + True + + + ..\..\packages\Etg.Yams.1.2.0-rc\lib\net451\Etg.Yams.AzureBlobStorageDeploymentRepository.dll + True + + + ..\..\packages\Etg.Yams.1.2.0-rc\lib\net451\Etg.Yams.AzureBlobStorageUpdateSession.dll + True + + + ..\..\packages\Etg.Yams.1.2.0-rc\lib\net451\Etg.Yams.AzureUtils.dll + True + + + ..\..\packages\Etg.Yams.1.2.0-rc\lib\net451\Etg.Yams.Common.dll + True + + + ..\..\packages\Etg.Yams.1.2.0-rc\lib\net451\Etg.Yams.Core.dll + True + + + ..\..\packages\Microsoft.Azure.KeyVault.Core.1.0.0\lib\net40\Microsoft.Azure.KeyVault.Core.dll + True + + + ..\..\packages\Microsoft.Data.Edm.5.6.4\lib\net40\Microsoft.Data.Edm.dll + True + + + ..\..\packages\Microsoft.Data.OData.5.6.4\lib\net40\Microsoft.Data.OData.dll + True + + + ..\..\packages\Microsoft.Data.Services.Client.5.6.4\lib\net40\Microsoft.Data.Services.Client.dll + True + + + ..\..\packages\EnterpriseLibrary.TransientFaultHandling.6.0.1304.0\lib\portable-net45+win+wp8\Microsoft.Practices.EnterpriseLibrary.TransientFaultHandling.dll + True + + + ..\..\packages\WindowsAzure.Storage.6.2.0\lib\net40\Microsoft.WindowsAzure.Storage.dll + True + + + ..\..\packages\Newtonsoft.Json.6.0.8\lib\net45\Newtonsoft.Json.dll + True + + + ..\..\packages\Semver.2.0.4\lib\net452\Semver.dll + True + + + + + + ..\..\packages\Rx-Core.2.2.5\lib\net45\System.Reactive.Core.dll + True + + + ..\..\packages\Rx-Interfaces.2.2.5\lib\net45\System.Reactive.Interfaces.dll + True + + + ..\..\packages\Rx-Linq.2.2.5\lib\net45\System.Reactive.Linq.dll + True + + + ..\..\packages\Rx-PlatformServices.2.2.5\lib\net45\System.Reactive.PlatformServices.dll + True + + + ..\..\packages\System.Spatial.5.6.4\lib\net40\System.Spatial.dll + True + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/test/HeartBeatProcess/Program.cs b/test/HeartBeatProcess/Program.cs new file mode 100644 index 0000000..196d23b --- /dev/null +++ b/test/HeartBeatProcess/Program.cs @@ -0,0 +1,40 @@ +using Etg.Yams.Client; +using System; +using System.IO; +using System.Reflection; +using System.Threading.Tasks; + +namespace HeartBeatProcess +{ + internal class Program + { + public static void Main(string[] args) + { + Run(args).Wait(); + } + + private static async Task Run(string[] args) + { + Console.WriteLine("args " + string.Join(" ", args)); + int heartBeatPeriod = 1000 * Convert.ToInt32(args[2]); + + var yamsClientConfig = new YamsClientConfigBuilder(args).Build(); + var yamsClientFactory = new YamsClientFactory(); + + Console.WriteLine("Initializing..."); + IYamsClient yamsClient = yamsClientFactory.CreateYamsClient(yamsClientConfig); + + await yamsClient.Connect(); + + File.WriteAllText($"HeartBeatApp.exe.out", $"HeartBeatApp.exe {args[0]} {args[1]}"); + + while (true) + { + await Task.Delay(heartBeatPeriod); + Console.WriteLine("Sending heart beat.."); + await yamsClient.SendHeartBeat(); + Console.WriteLine("Heart beat sent!"); + } + } + } +} \ No newline at end of file diff --git a/test/HeartBeatProcess/Properties/AssemblyInfo.cs b/test/HeartBeatProcess/Properties/AssemblyInfo.cs new file mode 100644 index 0000000..be76f90 --- /dev/null +++ b/test/HeartBeatProcess/Properties/AssemblyInfo.cs @@ -0,0 +1,36 @@ +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +// General Information about an assembly is controlled through the following +// set of attributes. Change these attribute values to modify the information +// associated with an assembly. +[assembly: AssemblyTitle("HeartBeatProcess")] +[assembly: AssemblyDescription("")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("")] +[assembly: AssemblyProduct("HeartBeatProcess")] +[assembly: AssemblyCopyright("Copyright © 2016")] +[assembly: AssemblyTrademark("")] +[assembly: AssemblyCulture("")] + +// Setting ComVisible to false makes the types in this assembly not visible +// to COM components. If you need to access a type in this assembly from +// COM, set the ComVisible attribute to true on that type. +[assembly: ComVisible(false)] + +// The following GUID is for the ID of the typelib if this project is exposed to COM +[assembly: Guid("257044fb-f7a8-499d-8ef2-b5cad76d617a")] + +// Version information for an assembly consists of the following four values: +// +// Major Version +// Minor Version +// Build Number +// Revision +// +// You can specify all the values or you can default the Build and Revision Numbers +// by using the '*' as shown below: +// [assembly: AssemblyVersion("1.0.*")] +[assembly: AssemblyVersion("1.0.0.0")] +[assembly: AssemblyFileVersion("1.0.0.0")] diff --git a/test/HeartBeatProcess/packages.config b/test/HeartBeatProcess/packages.config new file mode 100644 index 0000000..805b460 --- /dev/null +++ b/test/HeartBeatProcess/packages.config @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/test/IPCProcess/IPCProcess.csproj b/test/IPCProcess/IPCProcess.csproj deleted file mode 100644 index c134ecb..0000000 --- a/test/IPCProcess/IPCProcess.csproj +++ /dev/null @@ -1,60 +0,0 @@ - - - - - Debug - AnyCPU - {4F7B6739-5452-4F8C-8904-72112FAB3C1E} - Exe - Properties - IPCProcess - IPCProcess - v4.5.2 - 512 - true - - - AnyCPU - true - full - false - ..\Etg.Yams.Core.Test\Data\Exes\ - DEBUG;TRACE - prompt - 4 - - - AnyCPU - pdbonly - true - ..\Etg.Yams.Core.Test\Data\Exes\ - TRACE - prompt - 4 - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/test/IPCProcess/Program.cs b/test/IPCProcess/Program.cs deleted file mode 100644 index 4e3c0d5..0000000 --- a/test/IPCProcess/Program.cs +++ /dev/null @@ -1,41 +0,0 @@ -using System; -using System.IO; -using System.IO.Pipes; -using System.Linq; -using System.Threading; - -namespace IPCProcess -{ - class Program - { - static void Main(string[] args) - { - Thread.Sleep(TimeSpan.FromSeconds(30)); - if (args.Length > 0) - { - using (PipeStream pipeClient = - new AnonymousPipeClientStream(PipeDirection.Out, args.Last())) - { - try - { - // Read user input and send that to the client process. - using (StreamWriter sw = new StreamWriter(pipeClient)) - { - sw.AutoFlush = true; - // Send a 'sync message' and wait for client to receive it. - sw.WriteLine("[STARTED]"); - pipeClient.WaitForPipeDrain(); - } - } - // Catch the IOException that is raised if the pipe is broken - // or disconnected. - catch (IOException e) - { - Console.WriteLine("App Error: {0}", e.Message); - } - } - } - Console.ReadLine(); - } - } -} diff --git a/test/MonitorInitProcess/MonitorInitProcess.csproj b/test/MonitorInitProcess/MonitorInitProcess.csproj new file mode 100644 index 0000000..9b5c5b7 --- /dev/null +++ b/test/MonitorInitProcess/MonitorInitProcess.csproj @@ -0,0 +1,145 @@ + + + + + Debug + AnyCPU + {BB84CC4C-EB11-4A61-8ED4-791EADAA46E1} + {FAE04EC0-301F-11D3-BF4B-00C04F79EFBC} + Exe + Properties + MonitorInitProcess + MonitorInitProcess + v4.5.2 + 512 + + + + AnyCPU + true + full + false + ..\Etg.Yams.Core.Test\bin\Debug\Data\MonitorInitProcess\ + DEBUG;TRACE + prompt + 4 + + + AnyCPU + pdbonly + true + ..\Etg.Yams.Core.Test\bin\Release\Data\MonitorInitProcess\ + TRACE + prompt + 4 + + + + ..\..\packages\Autofac.3.5.2\lib\net40\Autofac.dll + True + + + ..\..\packages\CommandLineParser.1.9.71\lib\net45\CommandLine.dll + True + + + ..\..\packages\Etg.Yams.1.2.0-rc\lib\net451\Etg.Yams.dll + True + + + ..\..\packages\Etg.Yams.1.2.0-rc\lib\net451\Etg.Yams.AzureBlobStorageDeploymentRepository.dll + True + + + ..\..\packages\Etg.Yams.1.2.0-rc\lib\net451\Etg.Yams.AzureBlobStorageUpdateSession.dll + True + + + ..\..\packages\Etg.Yams.1.2.0-rc\lib\net451\Etg.Yams.AzureUtils.dll + True + + + ..\..\packages\Etg.Yams.1.2.0-rc\lib\net451\Etg.Yams.Common.dll + True + + + ..\..\packages\Etg.Yams.1.2.0-rc\lib\net451\Etg.Yams.Core.dll + True + + + ..\..\packages\Microsoft.Azure.KeyVault.Core.1.0.0\lib\net40\Microsoft.Azure.KeyVault.Core.dll + True + + + ..\..\packages\Microsoft.Data.Edm.5.6.4\lib\net40\Microsoft.Data.Edm.dll + True + + + ..\..\packages\Microsoft.Data.OData.5.6.4\lib\net40\Microsoft.Data.OData.dll + True + + + ..\..\packages\Microsoft.Data.Services.Client.5.6.4\lib\net40\Microsoft.Data.Services.Client.dll + True + + + ..\..\packages\EnterpriseLibrary.TransientFaultHandling.6.0.1304.0\lib\portable-net45+win+wp8\Microsoft.Practices.EnterpriseLibrary.TransientFaultHandling.dll + True + + + ..\..\packages\WindowsAzure.Storage.6.2.0\lib\net40\Microsoft.WindowsAzure.Storage.dll + True + + + ..\..\packages\Newtonsoft.Json.6.0.8\lib\net45\Newtonsoft.Json.dll + True + + + ..\..\packages\Semver.2.0.4\lib\net452\Semver.dll + True + + + + + + ..\..\packages\Rx-Core.2.2.5\lib\net45\System.Reactive.Core.dll + True + + + ..\..\packages\Rx-Interfaces.2.2.5\lib\net45\System.Reactive.Interfaces.dll + True + + + ..\..\packages\Rx-Linq.2.2.5\lib\net45\System.Reactive.Linq.dll + True + + + ..\..\packages\Rx-PlatformServices.2.2.5\lib\net45\System.Reactive.PlatformServices.dll + True + + + ..\..\packages\System.Spatial.5.6.4\lib\net40\System.Spatial.dll + True + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/test/MonitorInitProcess/Program.cs b/test/MonitorInitProcess/Program.cs new file mode 100644 index 0000000..2fc4a24 --- /dev/null +++ b/test/MonitorInitProcess/Program.cs @@ -0,0 +1,41 @@ +using Etg.Yams.Client; +using System; +using System.IO; +using System.Threading.Tasks; + +namespace MonitorInitProcess +{ + internal class Program + { + public static void Main(string[] args) + { + Run(args).Wait(); + } + + private static async Task Run(string[] args) + { + Console.WriteLine("args " + string.Join(" ", args)); + var yamsClientConfig = new YamsClientConfigBuilder(args).Build(); + var yamsClientFactory = new YamsClientFactory(); + + Console.WriteLine("Initializing..."); + IYamsClient yamsClient = yamsClientFactory.CreateYamsClient(yamsClientConfig); + + Task initTimeMax = Task.Delay(1000*Convert.ToInt32(args[2])); + + File.WriteAllText($"MonitorInitApp.exe.out", $"MonitorInitApp.exe {args[0]} {args[1]}"); + + await Task.WhenAll(yamsClient.Connect(), Task.Delay(TimeSpan.FromSeconds(5)), initTimeMax); + + Console.WriteLine("Send initialization done message..."); + await yamsClient.SendInitializationDoneMessage(); + Console.WriteLine("Initialization done message sent!"); + + while (true) + { + await Task.Delay(1000); + Console.WriteLine("Doing work"); + } + } + } +} \ No newline at end of file diff --git a/test/MonitorInitProcess/Properties/AssemblyInfo.cs b/test/MonitorInitProcess/Properties/AssemblyInfo.cs new file mode 100644 index 0000000..91131ee --- /dev/null +++ b/test/MonitorInitProcess/Properties/AssemblyInfo.cs @@ -0,0 +1,40 @@ +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +// General Information about an assembly is controlled through the following +// set of attributes. Change these attribute values to modify the information +// associated with an assembly. + +[assembly: AssemblyTitle("MonitorInitProcess")] +[assembly: AssemblyDescription("")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("")] +[assembly: AssemblyProduct("MonitorInitProcess")] +[assembly: AssemblyCopyright("Copyright © 2016")] +[assembly: AssemblyTrademark("")] +[assembly: AssemblyCulture("")] + +// Setting ComVisible to false makes the types in this assembly not visible +// to COM components. If you need to access a type in this assembly from +// COM, set the ComVisible attribute to true on that type. + +[assembly: ComVisible(false)] + +// The following GUID is for the ID of the typelib if this project is exposed to COM + +[assembly: Guid("BB84CC4C-EB11-4A61-8ED4-791EADAA46E1")] + +// Version information for an assembly consists of the following four values: +// +// Major Version +// Minor Version +// Build Number +// Revision +// +// You can specify all the values or you can default the Build and Revision Numbers +// by using the '*' as shown below: +// [assembly: AssemblyVersion("1.0.*")] + +[assembly: AssemblyVersion("1.0.0.0")] +[assembly: AssemblyFileVersion("1.0.0.0")] \ No newline at end of file diff --git a/test/MonitorInitProcess/app.config b/test/MonitorInitProcess/app.config new file mode 100644 index 0000000..ff99501 --- /dev/null +++ b/test/MonitorInitProcess/app.config @@ -0,0 +1,3 @@ + + + diff --git a/test/MonitorInitProcess/packages.config b/test/MonitorInitProcess/packages.config new file mode 100644 index 0000000..805b460 --- /dev/null +++ b/test/MonitorInitProcess/packages.config @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/test/IPCProcess/Properties/AssemblyInfo.cs b/test/Stubs/Properties/AssemblyInfo.cs similarity index 90% rename from test/IPCProcess/Properties/AssemblyInfo.cs rename to test/Stubs/Properties/AssemblyInfo.cs index 481e5cf..6832e87 100644 --- a/test/IPCProcess/Properties/AssemblyInfo.cs +++ b/test/Stubs/Properties/AssemblyInfo.cs @@ -5,11 +5,11 @@ // General Information about an assembly is controlled through the following // set of attributes. Change these attribute values to modify the information // associated with an assembly. -[assembly: AssemblyTitle("IPCProcess")] +[assembly: AssemblyTitle("Stubs")] [assembly: AssemblyDescription("")] [assembly: AssemblyConfiguration("")] [assembly: AssemblyCompany("")] -[assembly: AssemblyProduct("IPCProcess")] +[assembly: AssemblyProduct("Stubs")] [assembly: AssemblyCopyright("Copyright © 2016")] [assembly: AssemblyTrademark("")] [assembly: AssemblyCulture("")] @@ -20,7 +20,7 @@ [assembly: ComVisible(false)] // The following GUID is for the ID of the typelib if this project is exposed to COM -[assembly: Guid("4f7b6739-5452-4f8c-8904-72112fab3c1e")] +[assembly: Guid("68cd41f8-a6c3-4d43-93ca-92e898254cbd")] // Version information for an assembly consists of the following four values: // diff --git a/test/Etg.Yams.Core.Test/SimpleStubs.json b/test/Stubs/SimpleStubs.json similarity index 100% rename from test/Etg.Yams.Core.Test/SimpleStubs.json rename to test/Stubs/SimpleStubs.json diff --git a/test/Stubs/Stubs.csproj b/test/Stubs/Stubs.csproj new file mode 100644 index 0000000..4dbac49 --- /dev/null +++ b/test/Stubs/Stubs.csproj @@ -0,0 +1,117 @@ + + + + + Debug + AnyCPU + {68CD41F8-A6C3-4D43-93CA-92E898254CBD} + Library + Properties + Stubs + Stubs + v4.5.2 + 512 + + + + + true + full + false + bin\Debug\ + DEBUG;TRACE + prompt + 4 + + + pdbonly + true + bin\Release\ + TRACE + prompt + 4 + + + + ..\..\packages\CommandLineParser.1.9.71\lib\net45\CommandLine.dll + True + + + ..\..\packages\Etg.SimpleStubs.2.3.1\lib\dotnet50\Etg.SimpleStubs.dll + True + + + ..\..\packages\Microsoft.Azure.KeyVault.Core.1.0.0\lib\net40\Microsoft.Azure.KeyVault.Core.dll + True + + + ..\..\packages\Microsoft.Data.Edm.5.6.4\lib\net40\Microsoft.Data.Edm.dll + True + + + ..\..\packages\Microsoft.Data.OData.5.6.4\lib\net40\Microsoft.Data.OData.dll + True + + + ..\..\packages\Microsoft.Data.Services.Client.5.6.4\lib\net40\Microsoft.Data.Services.Client.dll + True + + + ..\..\packages\WindowsAzure.Storage.6.2.0\lib\net40\Microsoft.WindowsAzure.Storage.dll + True + + + ..\..\packages\Newtonsoft.Json.6.0.8\lib\net45\Newtonsoft.Json.dll + True + + + ..\..\packages\Semver.2.0.4\lib\net452\Semver.dll + True + + + + + ..\..\packages\System.Spatial.5.6.4\lib\net40\System.Spatial.dll + True + + + + + + + + + + + + + + + + + + + {894b7986-5aa4-42a7-8eb0-6dfd1f604505} + AzureBlobStorageUpdateSession + + + {7145e485-34fa-4632-89b0-bd27c96af69c} + Etg.Yams.Core + + + + + + + This project references NuGet package(s) that are missing on this computer. Use NuGet Package Restore to download them. For more information, see http://go.microsoft.com/fwlink/?LinkID=322105. The missing file is {0}. + + + + + \ No newline at end of file diff --git a/test/Stubs/packages.config b/test/Stubs/packages.config new file mode 100644 index 0000000..6ba0812 --- /dev/null +++ b/test/Stubs/packages.config @@ -0,0 +1,13 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/test/TestProcess/Program.cs b/test/TestProcess/Program.cs index 0495a5a..f75ec60 100644 --- a/test/TestProcess/Program.cs +++ b/test/TestProcess/Program.cs @@ -16,7 +16,7 @@ private void Run(string[] args) string codeBase = Assembly.GetExecutingAssembly().CodeBase; string exeName = Path.GetFileName(codeBase); - File.WriteAllText(string.Format("{0}.out", exeName), string.Format("{0} ", exeName) + string.Join(" ", args)); + File.WriteAllText($"{exeName}.out", $"{exeName} " + string.Join(" ", args)); Console.ReadLine(); } From f7cce358f730e0e5140d0d31fb64a3f4fc95dc98 Mon Sep 17 00:00:00 2001 From: nbilal Date: Thu, 22 Dec 2016 04:58:13 -0800 Subject: [PATCH 3/4] Reduce duration of end-to-end tests --- .../Install/IApplicationInstaller.cs | 2 - test/Etg.Yams.Core.Test/EndToEndTest.cs | 74 ++++++++++--------- 2 files changed, 40 insertions(+), 36 deletions(-) diff --git a/src/Etg.Yams.Core/Install/IApplicationInstaller.cs b/src/Etg.Yams.Core/Install/IApplicationInstaller.cs index 7b25682..184c3f1 100644 --- a/src/Etg.Yams.Core/Install/IApplicationInstaller.cs +++ b/src/Etg.Yams.Core/Install/IApplicationInstaller.cs @@ -2,8 +2,6 @@ using System.Collections.Generic; using System.Threading.Tasks; using Etg.Yams.Application; -using Etg.Yams.Storage.Config; -using Semver; namespace Etg.Yams.Install { diff --git a/test/Etg.Yams.Core.Test/EndToEndTest.cs b/test/Etg.Yams.Core.Test/EndToEndTest.cs index c1c7ee6..dcc0ff0 100644 --- a/test/Etg.Yams.Core.Test/EndToEndTest.cs +++ b/test/Etg.Yams.Core.Test/EndToEndTest.cs @@ -13,6 +13,8 @@ using Etg.Yams.Storage.Config; using Newtonsoft.Json.Serialization; using Semver; +using Etg.Yams.Storage; +using Etg.Yams.Install; namespace Etg.Yams.Test { @@ -38,15 +40,27 @@ public EndToEndTest() } private void InitializeYamsService(YamsConfig yamsConfig) + { + ContainerBuilder builder = InitializeContainerBuilder(yamsConfig); + InitializeYamsService(builder.Build()); + } + + private void InitializeYamsService(IContainer container) + { + _yamsDiModule = new YamsDiModule(container); + _yamsService = _yamsDiModule.YamsService; + } + + private ContainerBuilder InitializeContainerBuilder(YamsConfig yamsConfig) { IUpdateSessionManager updateSessionManager = new StubIUpdateSessionManager() .TryStartUpdateSession(applicationId => Task.FromResult(true)) .EndUpdateSession(applicationId => Task.FromResult(true)); - _yamsDiModule = new YamsDiModule(yamsConfig, new LocalDeploymentRepository( - _deploymentDirPath, new JsonDeploymentConfigSerializer( - new JsonSerializer(new DiagnosticsTraceWriter()))), updateSessionManager); - _yamsService = _yamsDiModule.YamsService; + IDeploymentRepository deploymentRepository = new LocalDeploymentRepository(_deploymentDirPath, + new JsonDeploymentConfigSerializer(new JsonSerializer(new DiagnosticsTraceWriter()))); + + return YamsDiModule.RegisterTypes(yamsConfig, deploymentRepository, updateSessionManager); } private void CopyTestProcessExeToTestApps() @@ -77,27 +91,6 @@ public void Dispose() DeleteDirectory(_testDirPath); } - [Fact] - public async Task TestThatApplicationsAreLoadedAtStartup() - { - var yamsConfig = new YamsConfigBuilder("clusterId1", "1", "instanceId", - _applicationsInstallPath).SetShowApplicationProcessWindow(false).Build(); - - InitializeYamsService(yamsConfig); - - IApplicationUpdateManager applicationUpdateManager = _yamsDiModule.Container.Resolve(); - await applicationUpdateManager.CheckForUpdates(); - - AssertThatApplicationIsRunning(new AppIdentity("test.app1", new SemVersion(1, 0, 0)), "TestProcess"); - AssertThatApplicationIsRunning(new AppIdentity("test.app2", new SemVersion(1, 1, 0)), "TestProcess"); - AssertThatApplicationIsRunning(new AppIdentity("test.app2", new SemVersion(2, 0, 0,"beta")), "TestProcess"); - AssertThatApplicationIsRunning(new AppIdentity("test.app3", new SemVersion(1, 1, 0)), "TestProcess"); - - AssertThatApplicationIsNotRunning(new AppIdentity("test.app4", new SemVersion(1, 0, 0))); - - AssertThatNumberOfApplicationsRunningIs(4); - } - /// /// This test replaces the content DeploymentConfig.json file with DeploymentConfigUpdate.json file. /// Several updates are involved: @@ -120,6 +113,11 @@ public async Task TestMultipleUpdates() IApplicationUpdateManager applicationUpdateManager = _yamsDiModule.Container.Resolve(); await applicationUpdateManager.CheckForUpdates(); + AssertThatApplicationIsRunning(new AppIdentity("test.app1", new SemVersion(1, 0, 0)), "TestProcess"); + AssertThatApplicationIsRunning(new AppIdentity("test.app2", new SemVersion(1, 1, 0)), "TestProcess"); + AssertThatApplicationIsRunning(new AppIdentity("test.app2", new SemVersion(2, 0, 0, "beta")), "TestProcess"); + AssertThatApplicationIsRunning(new AppIdentity("test.app3", new SemVersion(1, 1, 0)), "TestProcess"); + UploadDeploymentConfig("DeploymentConfigUpdate.json"); await applicationUpdateManager.CheckForUpdates(); @@ -151,19 +149,27 @@ public async Task TestThatClusterPropertiesAreUsedToMatchDeployments() .AddClusterProperty("NodeType", "Test") .AddClusterProperty("Region", "East").Build(); - InitializeYamsService(yamsConfig); + AppInstallConfig appInstallConfig = null; + var applicationInstallerStub = new StubIApplicationInstaller().Install( + (config) => + { + Assert.Null(appInstallConfig); + appInstallConfig = config; + return Task.CompletedTask; + }); + + ContainerBuilder builder = InitializeContainerBuilder(yamsConfig); + builder.RegisterInstance(applicationInstallerStub); + InitializeYamsService(builder.Build()); IApplicationUpdateManager applicationUpdateManager = _yamsDiModule.Container.Resolve(); await applicationUpdateManager.CheckForUpdates(); - AssertThatApplicationIsRunning(new AppIdentity("test.app1", new SemVersion(1, 0, 0)), "TestProcess"); - - AssertThatApplicationIsNotRunning(new AppIdentity("test.app2", new SemVersion(1, 1, 0))); - AssertThatApplicationIsNotRunning(new AppIdentity("test.app2", new SemVersion(2, 0, 0, "beta"))); - AssertThatApplicationIsNotRunning(new AppIdentity("test.app3", new SemVersion(1, 1, 0))); - AssertThatApplicationIsNotRunning(new AppIdentity("test.app4", new SemVersion(1, 0, 0))); - - AssertThatNumberOfApplicationsRunningIs(1); + Assert.Equal(new AppIdentity("test.app1", new SemVersion(1, 0, 0)), appInstallConfig.AppIdentity); + Assert.True(appInstallConfig.Properties.ContainsKey("NodeType")); + Assert.Equal("Test", appInstallConfig.Properties["NodeType"]); + Assert.True(appInstallConfig.Properties.ContainsKey("Region")); + Assert.Equal("East", appInstallConfig.Properties["Region"]); } [Fact] From 811749b11a024a8e0e5cfd029a2835dfa3abcee8 Mon Sep 17 00:00:00 2001 From: nbilal Date: Wed, 4 Jan 2017 10:41:32 -0800 Subject: [PATCH 4/4] Fix typo --- Docs/Deploy&Host_an_App_in_YAMS.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Docs/Deploy&Host_an_App_in_YAMS.md b/Docs/Deploy&Host_an_App_in_YAMS.md index 61dd20d..a0b0aa4 100644 --- a/Docs/Deploy&Host_an_App_in_YAMS.md +++ b/Docs/Deploy&Host_an_App_in_YAMS.md @@ -287,7 +287,7 @@ The app source code will also need to be updated so that the app can communicate // ... ``` -## Heat beats +## Heart beats With this feature enabled, the app is expected to send heart beat messages to Yams at steady intervals. If heart beats are not received in time, errors will be logged (more complex handling will be added in the future). To enable this feature, update the `AppConfig.json` and your app source code as shown below: ```json