void Run(List <string> args)
        {
            // prepare
            Console.OutputEncoding = System.Text.Encoding.UTF8;
            var time      = DateTime.Now;
            var stopwatch = Stopwatch.StartNew();

            var apiCall           = args?.FirstOrDefault(arg => arg.IsStartsWith("/agc:"));
            var isUserInteractive = Environment.UserInteractive && apiCall == null;
            var powered           = $"VIEApps NGX API Gateway - Service Hosting {RuntimeInformation.ProcessArchitecture.ToString().ToLower()} {Assembly.GetCallingAssembly().GetVersion()}";

            // prepare type name
            this.ServiceTypeName = args?.FirstOrDefault(arg => arg.IsStartsWith("/svc:"))?.Replace(StringComparison.OrdinalIgnoreCase, "/svc:", "");
            if (string.IsNullOrWhiteSpace(this.ServiceTypeName) && args?.FirstOrDefault(arg => arg.IsStartsWith("/svn:")) != null)
            {
                var configFilePath = Path.Combine($"{UtilityService.GetAppSetting("Path:APIGateway:Controller")}", $"VIEApps.Services.APIGateway.{(RuntimeInformation.FrameworkDescription.IsContains(".NET Framework") ? "exe" : "dll")}.config");
                if (File.Exists(configFilePath))
                {
                    try
                    {
                        var xml = new System.Xml.XmlDocument();
                        xml.LoadXml(UtilityService.ReadTextFile(configFilePath));
                        this.ServiceTypeName = args.First(arg => arg.IsStartsWith("/svn:")).Replace(StringComparison.OrdinalIgnoreCase, "/svn:", "").Trim();
                        var typeNode = xml.SelectSingleNode($"/configuration/{UtilityService.GetAppSetting("Section:Services", "net.vieapps.services")}")?.ChildNodes?.ToList()?.FirstOrDefault(node => this.ServiceTypeName.IsEquals(node.Attributes["name"]?.Value));
                        this.ServiceTypeName = typeNode?.Attributes["type"]?.Value;
                    }
                    catch
                    {
                        this.ServiceTypeName = null;
                    }
                }
            }

            // stop if has no type name of a service component
            if (string.IsNullOrWhiteSpace(this.ServiceTypeName))
            {
                Console.Error.WriteLine(powered);
                Console.Error.WriteLine("");
                Console.Error.WriteLine("Error: The service component is invalid (no type name)");
                Console.Error.WriteLine("");
                Console.Error.WriteLine("Syntax: VIEApps.Services.APIGateway /svc:<service-component-namespace,service-assembly>");
                Console.Error.WriteLine("");
                Console.Error.WriteLine("Ex.: VIEApps.Services.APIGateway /svc:net.vieapps.Services.Users.ServiceComponent,VIEApps.Services.Users");
                Console.Error.WriteLine("");
                if (isUserInteractive)
                {
                    Console.ReadLine();
                }
                return;
            }

            // prepare type name & assembly name of the service component
            var serviceTypeInfo = this.ServiceTypeName.ToArray();

            this.ServiceTypeName     = serviceTypeInfo[0];
            this.ServiceAssemblyName = serviceTypeInfo.Length > 1 ? serviceTypeInfo[1] : "Unknown";

            // prepare the type of the service component
            try
            {
                this.PrepareServiceType();
                if (this.ServiceType == null)
                {
                    Console.Error.WriteLine(powered);
                    Console.Error.WriteLine("");
                    Console.Error.WriteLine($"Error: The service component is invalid [{this.ServiceTypeName},{this.ServiceAssemblyName}]");
                    if (isUserInteractive)
                    {
                        Console.ReadLine();
                    }
                    return;
                }
            }
            catch (Exception ex)
            {
                Console.Error.WriteLine(powered);
                Console.Error.WriteLine("");
                if (ex is ReflectionTypeLoadException)
                {
                    Console.Error.WriteLine($"Error: The service component [{this.ServiceTypeName},{this.ServiceAssemblyName}] got an unexpected error while preparing");
                    (ex as ReflectionTypeLoadException).LoaderExceptions.ForEach(exception =>
                    {
                        Console.Error.WriteLine($"{exception.Message} [{exception.GetType()}]\r\nStack: {exception.StackTrace}");
                        var inner = exception.InnerException;
                        while (inner != null)
                        {
                            Console.Error.WriteLine($"-------------------------\r\n{inner.Message} [{inner.GetType()}]\r\nStack: {inner.StackTrace}");
                            inner = inner.InnerException;
                        }
                    });
                }
                else
                {
                    Console.Error.WriteLine($"Error: The service component [{this.ServiceTypeName},{this.ServiceAssemblyName}] got an unexpected error while preparing => {ex.Message} [{ex.GetType()}]\r\nStack: {ex.StackTrace}");
                    var inner = ex.InnerException;
                    while (inner != null)
                    {
                        Console.Error.WriteLine($"-------------------------\r\n{inner.Message} [{inner.GetType()}]\r\nStack: {inner.StackTrace}");
                        inner = inner.InnerException;
                    }
                }
                if (isUserInteractive)
                {
                    Console.ReadLine();
                }
                return;
            }

            // check the type of the service component
            if (!typeof(ServiceBase).IsAssignableFrom(this.ServiceType))
            {
                Console.Error.WriteLine(powered);
                Console.Error.WriteLine("");
                Console.Error.WriteLine($"Error: The service component is invalid [{this.ServiceTypeName},{this.ServiceAssemblyName}]");
                if (isUserInteractive)
                {
                    Console.ReadLine();
                }
                return;
            }

            // initialize the instance of the service
            var service = this.ServiceType.CreateInstance() as ServiceBase;

            // prepare the signal to start/stop when the service was called from API Gateway
            EventWaitHandle eventWaitHandle    = null;
            var             useEventWaitHandle = !isUserInteractive && RuntimeInformation.IsOSPlatform(OSPlatform.Windows);

            if (useEventWaitHandle)
            {
                // get the flag of the existing instance
                var runtimeArguments = Extensions.GetRuntimeArguments();
                var name             = $"{service.ServiceURI}#{$"/interactive:{isUserInteractive} /user:{runtimeArguments.Item1} /host:{runtimeArguments.Item2} /platform:{runtimeArguments.Item3} /os:{runtimeArguments.Item4}".GenerateUUID()}";
                eventWaitHandle = new EventWaitHandle(false, EventResetMode.AutoReset, name, out var createdNew);

                // process the call to stop
                if ("/agc:s".IsEquals(apiCall))
                {
                    // raise an event to stop current existing instance
                    if (!createdNew)
                    {
                        eventWaitHandle.Set();
                    }

                    // then exit
                    eventWaitHandle.Dispose();
                    service.Dispose();
                    return;
                }
            }

            // prepare environment
            JsonConvert.DefaultSettings = () => new JsonSerializerSettings
            {
                Formatting            = Formatting.None,
                ReferenceLoopHandling = ReferenceLoopHandling.Ignore,
                DateTimeZoneHandling  = DateTimeZoneHandling.Local
            };

            // prepare logging
            var loglevel = args?.FirstOrDefault(arg => arg.IsStartsWith("/loglevel:"))?.Replace(StringComparison.OrdinalIgnoreCase, "/loglevel:", "");

            if (string.IsNullOrWhiteSpace(loglevel))
#if DEBUG
            { loglevel = UtilityService.GetAppSetting("Logs:Level", "Debug"); }
#else
            { loglevel = UtilityService.GetAppSetting("Logs:Level", "Information"); }
#endif
            if (!loglevel.TryToEnum(out LogLevel logLevel))
#if DEBUG
            { logLevel = LogLevel.Debug; }
#else
            { logLevel = LogLevel.Information; }
#endif

            Logger.AssignLoggerFactory(new ServiceCollection().AddLogging(builder =>
            {
                builder.SetMinimumLevel(logLevel);
                if (isUserInteractive)
                {
                    builder.AddConsole();
                }
            }).BuildServiceProvider().GetService <ILoggerFactory>());
            Components.Caching.Cache.AssignLoggerFactory(Logger.GetLoggerFactory());

            var logPath = UtilityService.GetAppSetting("Path:Logs");
            if (!string.IsNullOrWhiteSpace(logPath) && Directory.Exists(logPath))
            {
                logPath = Path.Combine(logPath, "{Hour}_" + $"{service.ServiceName.ToLower()}.all.txt");
                Logger.GetLoggerFactory().AddFile(logPath, logLevel);
            }
            else
            {
                logPath = null;
            }

            var logger = (service as IServiceComponent).Logger = Logger.CreateLogger(this.ServiceType);

            // prepare outgoing proxy
            var proxy = UtilityService.GetAppSetting("Proxy:Host");
            if (!string.IsNullOrWhiteSpace(proxy))
            {
                try
                {
                    UtilityService.AssignWebProxy(proxy, UtilityService.GetAppSetting("Proxy:Port").CastAs <int>(), UtilityService.GetAppSetting("Proxy:User"), UtilityService.GetAppSetting("Proxy:UserPassword"), UtilityService.GetAppSetting("Proxy:Bypass")?.ToArray(";"));
                }
                catch (Exception ex)
                {
                    logger.LogError($"Error occurred while assigning web-proxy => {ex.Message}", ex);
                }
            }

            // setup hooks
            void terminate(string message, bool available = true, bool disconnect = true)
            => (service.Disposed ? Task.CompletedTask : service.DisposeAsync(args?.ToArray(), available, disconnect, _ => logger.LogInformation(message)).AsTask()).ContinueWith(async _ => await Task.Delay(123).ConfigureAwait(false), TaskContinuationOptions.OnlyOnRanToCompletion).Wait();

            AppDomain.CurrentDomain.ProcessExit += (sender, arguments) => terminate($"The service was terminated (by \"process exit\" signal) - Served times: {time.GetElapsedTimes()}", false);

            Console.CancelKeyPress += (sender, arguments) =>
            {
                terminate($"The service was terminated (by \"cancel key press\" signal) - Served times: {time.GetElapsedTimes()}", false);
                Environment.Exit(0);
            };

            // start the service
            logger.LogInformation($"The service is starting");
            logger.LogInformation($"Service info: {service.ServiceName} - v{this.ServiceType.Assembly.GetVersion()}");
            logger.LogInformation($"Working mode: {(isUserInteractive ? "Interactive app" : "Background service")}");
            logger.LogInformation($"Starting arguments: {(args != null && args.Count > 0 ? args.Join(" ") : "None")}");

            ServiceBase.ServiceComponent = service;
            service.Start(
                args?.ToArray(),
                !"false".IsEquals(args?.FirstOrDefault(a => a.IsStartsWith("/repository:"))?.Replace(StringComparison.OrdinalIgnoreCase, "/repository:", "")),
                _ =>
            {
                logger.LogInformation($"API Gateway Router: {new Uri(Router.GetRouterStrInfo()).GetResolvedURI()}");
                logger.LogInformation($"API Gateway HTTP service: {UtilityService.GetAppSetting("HttpUri:APIs", "None")}");
                logger.LogInformation($"Files HTTP service: {UtilityService.GetAppSetting("HttpUri:Files", "None")}");
                logger.LogInformation($"Portals HTTP service: {UtilityService.GetAppSetting("HttpUri:Portals", "None")}");
                logger.LogInformation($"Passport HTTP service: {UtilityService.GetAppSetting("HttpUri:Passports", "None")}");
                logger.LogInformation($"Root (base) directory: {AppDomain.CurrentDomain.BaseDirectory}");
                logger.LogInformation($"Temporary directory: {UtilityService.GetAppSetting("Path:Temp", "None")}");
                logger.LogInformation($"Static files directory: {UtilityService.GetAppSetting("Path:StaticFiles", "None")}");
                logger.LogInformation($"Status files directory: {UtilityService.GetAppSetting("Path:Status", "None")}");
                logger.LogInformation($"Logging level: {logLevel} - Local rolling log files is {(string.IsNullOrWhiteSpace(logPath) ? "disabled" : $"enabled => {logPath}")}");
                logger.LogInformation($"Show debugs: {service.IsDebugLogEnabled} - Show results: {service.IsDebugResultsEnabled} - Show stacks: {service.IsDebugStacksEnabled}");
                logger.LogInformation($"Service URIs:\r\n\t- Round robin: {service.ServiceURI}\r\n\t- Single (unique): {service.ServiceUniqueURI}");
                logger.LogInformation($"Environment:\r\n\t{Extensions.GetRuntimeEnvironment()}\r\n\t- Node ID: {service.NodeID}\r\n\t- Powered: {powered}");

                stopwatch.Stop();
                logger.LogInformation($"The service was started - PID: {Process.GetCurrentProcess().Id} - Execution times: {stopwatch.GetElapsedTimes()}");

                if (isUserInteractive)
                {
                    logger.LogWarning($"=====> Enter \"exit\" to terminate ...............");
                }
            }
        static void Main(string[] args)
        {
            // setup environment
            Directory.SetCurrentDirectory(AppDomain.CurrentDomain.BaseDirectory);
            Program.Arguments = args;

            // prepare logging
            var loglevel = args?.FirstOrDefault(a => a.IsStartsWith("/loglevel:"))?.Replace(StringComparison.OrdinalIgnoreCase, "/loglevel:", "");

            if (string.IsNullOrWhiteSpace(loglevel))
#if DEBUG
            { loglevel = UtilityService.GetAppSetting("Logs:Level", "Debug"); }
#else
            { loglevel = UtilityService.GetAppSetting("Logs:Level", "Information"); }
#endif
            if (!loglevel.TryToEnum(out LogLevel logLevel))
#if DEBUG
            { logLevel = LogLevel.Debug; }
#else
            { logLevel = LogLevel.Information; }
#endif

            Components.Utility.Logger.AssignLoggerFactory(new ServiceCollection().AddLogging(builder => builder.SetMinimumLevel(logLevel)).BuildServiceProvider().GetService <ILoggerFactory>());

            var logPath = UtilityService.GetAppSetting("Path:Logs");
            if (logPath != null && Directory.Exists(logPath))
            {
                logPath = Path.Combine(logPath, "{Hour}_apigateway.controller.txt");
                Components.Utility.Logger.GetLoggerFactory().AddFile(logPath, logLevel);
            }
            else
            {
                logPath = null;
            }

            Program.Logger = Components.Utility.Logger.CreateLogger <Controller>();

            JsonConvert.DefaultSettings = () => new JsonSerializerSettings
            {
                Formatting            = Formatting.None,
                ReferenceLoopHandling = ReferenceLoopHandling.Ignore,
                DateTimeZoneHandling  = DateTimeZoneHandling.Local
            };

            // prepare outgoing proxy
            var proxy = UtilityService.GetAppSetting("Proxy:Host");
            if (!string.IsNullOrWhiteSpace(proxy))
            {
                try
                {
                    UtilityService.AssignWebProxy(proxy, UtilityService.GetAppSetting("Proxy:Port").CastAs <int>(), UtilityService.GetAppSetting("Proxy:User"), UtilityService.GetAppSetting("Proxy:UserPassword"), UtilityService.GetAppSetting("Proxy:Bypass")?.ToArray(";"));
                }
                catch (Exception ex)
                {
                    Program.Logger.LogError($"Error occurred while assigning web-proxy => {ex.Message}", ex);
                }
            }

            // setup event handlers
            Program.SetupEventHandlers();

            // run as a Windows desktop app
            if (Environment.UserInteractive)
            {
                Application.EnableVisualStyles();
                Application.SetCompatibleTextRenderingDefault(false);
                Program.MainForm = new MainForm();
                Application.Run(Program.MainForm);
            }

            // run as a Windows service
            else
            {
                System.ServiceProcess.ServiceBase.Run(new ServiceRunner());
            }
        }
Exemple #3
0
        static void Main(string[] args)
        {
            // prepare environment
            Program.IsUserInteractive = Environment.UserInteractive && args?.FirstOrDefault(a => a.IsStartsWith("/daemon")) == null;
            Directory.SetCurrentDirectory(AppDomain.CurrentDomain.BaseDirectory);
            Console.OutputEncoding = System.Text.Encoding.UTF8;

            // prepare logging
            var loglevel = args?.FirstOrDefault(a => a.IsStartsWith("/loglevel:"))?.Replace(StringComparison.OrdinalIgnoreCase, "/loglevel:", "");

            if (string.IsNullOrWhiteSpace(loglevel))
#if DEBUG
            { loglevel = UtilityService.GetAppSetting("Logs:Level", "Debug"); }
#else
            { loglevel = UtilityService.GetAppSetting("Logs:Level", "Information"); }
#endif
            if (!loglevel.TryToEnum(out LogLevel logLevel))
#if DEBUG
            { logLevel = LogLevel.Debug; }
#else
            { logLevel = LogLevel.Information; }
#endif

            Components.Utility.Logger.AssignLoggerFactory(new ServiceCollection().AddLogging(builder =>
            {
                builder.SetMinimumLevel(logLevel);
                if (Program.IsUserInteractive)
                {
                    builder.AddConsole();
                }
            }).BuildServiceProvider().GetService <ILoggerFactory>());

            var logPath = UtilityService.GetAppSetting("Path:Logs");
            if (logPath != null && Directory.Exists(logPath))
            {
                logPath = Path.Combine(logPath, "{Hour}_apigateway.controller.txt");
                Components.Utility.Logger.GetLoggerFactory().AddFile(logPath, logLevel);
            }
            else
            {
                logPath = null;
            }

            Program.Logger = Components.Utility.Logger.CreateLogger <Controller>();

            JsonConvert.DefaultSettings = () => new JsonSerializerSettings
            {
                Formatting            = Formatting.None,
                ReferenceLoopHandling = ReferenceLoopHandling.Ignore,
                DateTimeZoneHandling  = DateTimeZoneHandling.Local
            };

            // prepare outgoing proxy
            var proxy = UtilityService.GetAppSetting("Proxy:Host");
            if (!string.IsNullOrWhiteSpace(proxy))
            {
                try
                {
                    UtilityService.AssignWebProxy(proxy, UtilityService.GetAppSetting("Proxy:Port").CastAs <int>(), UtilityService.GetAppSetting("Proxy:User"), UtilityService.GetAppSetting("Proxy:UserPassword"), UtilityService.GetAppSetting("Proxy:Bypass")?.ToArray(";"));
                }
                catch (Exception ex)
                {
                    Program.Logger.LogError($"Error occurred while assigning web-proxy => {ex.Message}", ex);
                }
            }

            // prepare event handlers
            Program.SetupEventHandlers();

            // prepare hooks
            AppDomain.CurrentDomain.ProcessExit += (sender, arguments) =>
            {
                if (!Program.IsStopped)
                {
                    Program.Logger.LogWarning(">>> Terminated by signal of process exit");
                    Program.Stop();
                }
            };

            Console.CancelKeyPress += (sender, arguments) =>
            {
                Program.Logger.LogWarning(">>> Terminated by signal of cancel key press");
                Program.Stop();
                Environment.Exit(0);
            };

            // start
            Program.Start(args);

            // processing commands util got an exit signal (not available when running in Docker)
            if (Program.IsUserInteractive && args?.FirstOrDefault(a => a.IsStartsWith("/docker")) == null)
            {
                var command = Console.ReadLine();
                while (command != null)
                {
                    var commands = command.ToArray(' ');

                    if (commands[0].IsEquals("info"))
                    {
                        var controllerID = commands.Length > 1 ? commands[1].ToLower() : "local";
                        if (controllerID.IsEquals("global"))
                        {
                            var controllers = Program.Manager.AvailableControllers;
                            var info        = $"Controllers - Total instance(s): {Program.Manager.AvailableControllers.Count:#,##0} - Available instance(s): {Program.Manager.AvailableControllers.Where(kvp => kvp.Value.Available).Count():#,##0}";
                            Program.Manager.AvailableControllers.ForEach(controller => info += "\r\n\t" + $"- ID: {controller.ID} - Status: {(controller.Available ? "Available" : "Unavailable")} - Working mode: {controller.Mode} - Platform: {controller.Platform}");
                            info += "\r\n" + $"Services - Total: {Program.Manager.AvailableServices.Count:#,##0} - Available: {Program.Manager.AvailableServices.Where(kvp => kvp.Value.FirstOrDefault(svc => svc.Available) != null).Count():#,##0} - Running: {Program.Manager.AvailableServices.Where(kvp => kvp.Value.FirstOrDefault(svc => svc.Running) != null).Count():#,##0}";
                            Program.Manager.AvailableServices.OrderBy(kvp => kvp.Key).ForEach(kvp => info += "\r\n\t" + $"- URI: services.{kvp.Key} - Available instance(s): {kvp.Value.Where(svc => svc.Available).Count():#,##0} - Running instance(s): {kvp.Value.Where(svc => svc.Running).Count():#,##0}");
                            Program.Logger.LogInformation(info);
                        }
                        else if (controllerID.IsEquals("local") || controllerID.IsEquals(Program.Controller.Info.ID))
                        {
                            var info =
                                $"Controller:" + "\r\n\t" +
                                $"- Version: {Assembly.GetExecutingAssembly().GetVersion()}" + "\r\n\t" +
                                $"- Working mode: {(Environment.UserInteractive ? "Interactive app" : "Background service")}" + "\r\n\t" +
                                $"- Environment:\r\n\t\t{Extensions.GetRuntimeEnvironment("\r\n\t\t")}" + "\r\n\t" +
                                $"- API Gateway Router: {new Uri(Router.GetRouterStrInfo()).GetResolvedURI()}" + "\r\n\t" +
                                $"- Incoming channel session identity: {Router.IncomingChannelSessionID}" + "\r\n\t" +
                                $"- Outgoing channel session identity: {Router.OutgoingChannelSessionID}" + "\r\n\t" +
                                $"- Number of helper services: {Program.Controller.NumberOfHelperServices:#,##0}" + "\r\n\t" +
                                $"- Number of scheduling timers: {Program.Controller.NumberOfTimers:#,##0}" + "\r\n\t" +
                                $"- Number of scheduling tasks: {Program.Controller.NumberOfTasks:#,##0}";
                            var services = Program.Controller.AvailableBusinessServices.OrderBy(kvp => kvp.Key).ToDictionary(kvp => kvp.Key, kvp => kvp.Value.Instance);
                            info += "\r\n" + $"Services - Available: {services.Count:#,##0} - Running: {services.Where(kvp => kvp.Value != null).Count():#,##0}";
                            services.ForEach(kvp =>
                            {
                                info += "\r\n\t" + $"- URI: services.{kvp.Key}";
                                if (kvp.Value != null)
                                {
                                    var svcArgs = kvp.Value.Arguments?.ToArray(' ') ?? new string[] { };
                                    info       += $" (services.{Extensions.GetUniqueName(kvp.Key.ToArray('.').Last(), svcArgs)}) - Status: Running";
                                    if (kvp.Value.ID != null)
                                    {
                                        info += $" - Process ID: {kvp.Value.ID.Value}";
                                    }
                                    if (kvp.Value.StartTime != null)
                                    {
                                        info += $" - Serving times: {kvp.Value.StartTime.Value.GetElapsedTimes()}";
                                    }
                                    var user = svcArgs.FirstOrDefault(a => a.IsStartsWith("/call-user:"******" - Invoked by: {user.Replace(StringComparison.OrdinalIgnoreCase, "/call-user:"******"").UrlDecode()}";
                                        var host     = svcArgs.FirstOrDefault(a => a.IsStartsWith("/call-host:"));
                                        var platform = svcArgs.FirstOrDefault(a => a.IsStartsWith("/call-platform:"));
                                        var os       = svcArgs.FirstOrDefault(a => a.IsStartsWith("/call-os:"));
                                        if (!string.IsNullOrWhiteSpace(host) && !string.IsNullOrWhiteSpace(platform) && !string.IsNullOrWhiteSpace(os))
                                        {
                                            info += $" [Host: {host.Replace(StringComparison.OrdinalIgnoreCase, "/call-host:", "").UrlDecode()} - Platform: {platform.Replace(StringComparison.OrdinalIgnoreCase, "/call-platform:", "").UrlDecode()} @ {os.Replace(StringComparison.OrdinalIgnoreCase, "/call-os:", "").UrlDecode()}]";
                                        }
                                    }
                                }
                                else
                                {
                                    info += " - Status: Stopped";
                                }
                            });
                            Program.Logger.LogInformation(info);
                        }
                        else if (Program.Manager.AvailableControllers.ContainsKey(controllerID))
                        {
                            var controller = Program.Manager.AvailableControllers[controllerID];
                            var info       =
                                $"Controller:" + "\r\n\t" +
                                $"- ID: {controller.ID}" + "\r\n\t" +
                                $"- Platform: {controller.Platform})" + "\r\n\t" +
                                $"- Working mode: {controller.Mode}" + "\r\n\t" +
                                $"- Host: {controller.Host}" + "\r\n\t" +
                                $"- User: {controller.User}" + "\r\n\t" +
                                $"- Status: {(controller.Available ? "Available" : "Unvailable")}" + "\r\n\t";
                            info += controller.Available
                                                                ? $"- Starting time: {controller.Timestamp.ToDTString()} [Served times: {controller.Timestamp.GetElapsedTimes()}]"
                                                                : $"- Last working time: {controller.Timestamp.ToDTString()}";
                            var services = Program.Manager.AvailableServices.Values.Select(svc => svc.FirstOrDefault(svcInfo => svcInfo.ControllerID.Equals(controller.ID))).Where(svcInfo => svcInfo != null).OrderBy(svcInfo => svcInfo.Name).ToList();
                            info += "\r\n" + $"Services - Available: {services.Where(svc => svc.Available).Count():#,##0} - Running: {services.Where(svc => svc.Running).Count():#,##0}";
                            services.ForEach(svc =>
                            {
                                info += "\r\n\t" + $"- URI: services.{svc.Name} ({svc.UniqueName}) - Status: {(svc.Running ? "Running" : "Stopped")}";
                                info += svc.Running
                                                                        ? $" - Starting time: {svc.Timestamp.ToDTString()} [Served times: {svc.Timestamp.GetElapsedTimes()}] - Invoked by: {svc.InvokeInfo}"
                                                                        : $" - Last working time: {svc.Timestamp.ToDTString()}";
                            });
                            Program.Logger.LogInformation(info);
                        }
                        else
                        {
                            Program.Logger.LogWarning($"Controller with identity \"{controllerID}\" is not found");
                        }
                    }

                    else if (commands[0].IsEquals("start"))
                    {
                        if (commands.Length > 1)
                        {
                            var controllerID = commands.Length > 2
                                                                ? commands[2].ToLower()
                                                                : Program.Controller.Info.ID;
                            if (!Program.Manager.AvailableControllers.ContainsKey(controllerID))
                            {
                                Program.Logger.LogWarning($"Controller with identity \"{controllerID}\" is not found");
                            }
                            else
                            {
                                Program.Manager.StartBusinessService(controllerID, commands[1].ToLower(), Program.Controller.GetServiceArguments().Replace("/", "/call-"));
                            }
                        }
                        else
                        {
                            Program.Logger.LogInformation($"Invalid {command} command");
                        }
                    }

                    else if (commands[0].IsEquals("stop"))
                    {
                        if (commands.Length > 1)
                        {
                            var controllerID = commands.Length > 2
                                                                ? commands[2].ToLower()
                                                                : Program.Controller.Info.ID;
                            if (!Program.Manager.AvailableControllers.ContainsKey(controllerID))
                            {
                                Program.Logger.LogWarning($"Controller with identity \"{controllerID}\" is not found");
                            }
                            else
                            {
                                Program.Manager.StopBusinessService(controllerID, commands[1].ToLower());
                            }
                        }
                        else
                        {
                            Program.Logger.LogInformation($"Invalid {command} command");
                        }
                    }

                    else if (commands[0].IsEquals("refresh"))
                    {
                        Task.Run(async() =>
                        {
                            await Program.Manager.SendInterCommunicateMessageAsync("Controller#RequestInfo").ConfigureAwait(false);
                            await Program.Manager.SendInterCommunicateMessageAsync("Service#RequestInfo").ConfigureAwait(false);
                        }).ConfigureAwait(false);
                    }

                    else if (!commands[0].IsEquals("exit"))
                    {
                        Program.Logger.LogInformation(
                            "Commands:" + "\r\n\t" +
                            "info [global | controller-id]: show the information of controllers & services" + "\r\n\t" +
                            "start <name> [controller-id]: start a business service" + "\r\n\t" +
                            "stop <name> [controller-id]: stop a business service" + "\r\n\t" +
                            "refresh: refresh the information of controllers & services" + "\r\n\t" +
                            "help: show available commands" + "\r\n\t" +
                            "exit: shutdown & terminate"
                            );
                    }

                    command = commands[0].IsEquals("exit")
                                                ? null
                                                : Console.ReadLine();
                }
            }

            // wait until be killed
            else
            {
                while (true)
                {
                    Task.Delay(54321).GetAwaiter().GetResult();
                }
            }

            // stop and do clean up
            Program.Stop();
        }