From 53e99a12e5b4afbeea96ce21b59421ce244d449e Mon Sep 17 00:00:00 2001 From: Lookey Date: Fri, 27 Sep 2024 22:27:32 +0200 Subject: [PATCH] Stabilize and enhance some minor bits This adds some improvements and changes to make the bot more stable. Some were mistakes I stumbled upon when doing some random testing. - Widgets now can be in an "unloaded" state. WidgetUserModule holds references to all IWidget instances that are assigned to given user, while IWidget provides two new abstracts that derivatives have to implement: OnLoad() and OnUnload(). This allows to allocate a widget and attempt a load, and when it fails keep the widget unloaded. Failures can happen for many different reasons, ex. widget can be created when module that produces its events is not yet enabled. - Widgets can be manually reloaded by using `widget reload` command. No ID added will reload all widgets assigned to current user. - EventSystem now throws dedicated Event/Dispatcher not found exceptions. This is for more clarity, as it used to return "key not present in dictionary" generic error. - Some parts of LukeBot now rely on https_domain prop instead of server_ip. This is to ensure IP is not visible in ex. OAuth callbacks (although it does not matter much, since anyone can DNS lookup the domain...) --- LukeBot.API/SpotifyToken.cs | 2 +- LukeBot.API/TwitchToken.cs | 2 +- LukeBot.Common/Constants.cs | 5 +- LukeBot.Common/Utils.cs | 1 + LukeBot.Communication/EventSystem.cs | 7 ++ .../Exception/DispatcherNotFoundException.cs | 12 ++ .../Exception/EventNotFoundException.cs | 12 ++ LukeBot.Endpoint/Endpoint.cs | 17 ++- LukeBot.Module/IUserModule.cs | 4 +- LukeBot.Twitch/IRCChannel.cs | 7 +- LukeBot.Twitch/TwitchIRC.cs | 7 +- LukeBot.Widget/Alerts.cs | 22 +++- LukeBot.Widget/Chat.cs | 15 ++- LukeBot.Widget/Echo.cs | 8 ++ LukeBot.Widget/IWidget.cs | 50 +++++--- LukeBot.Widget/NowPlaying.cs | 15 ++- LukeBot.Widget/WidgetMainModule.cs | 10 ++ LukeBot.Widget/WidgetUserModule.cs | 114 +++++++++++++++--- LukeBot/ServerCLI.cs | 26 +++- LukeBot/TwitchCLIProcessor.cs | 2 +- LukeBot/UserInterface.cs | 2 +- LukeBot/WidgetCLIProcessor.cs | 90 ++++++++++---- LukeBotClient/Constants.cs | 2 - LukeBotClient/LukeBotClient.cs | 7 ++ LukeBotClient/ProgramOptions.cs | 17 +-- Tools/propmgr/StoreTemplates.cs | 2 +- 26 files changed, 350 insertions(+), 108 deletions(-) create mode 100644 LukeBot.Communication/Exception/DispatcherNotFoundException.cs create mode 100644 LukeBot.Communication/Exception/EventNotFoundException.cs diff --git a/LukeBot.API/SpotifyToken.cs b/LukeBot.API/SpotifyToken.cs index f1e98a8..c11364f 100644 --- a/LukeBot.API/SpotifyToken.cs +++ b/LukeBot.API/SpotifyToken.cs @@ -13,7 +13,7 @@ public SpotifyToken(AuthFlow flow, string lbUser) "https://accounts.spotify.com/authorize", "https://accounts.spotify.com/api/token", "https://accounts.spotify.com/api/revoke", - "http://" + Conf.Get(Common.Constants.PROP_STORE_SERVER_IP_PROP) + "/callback/spotify" + "http://" + Conf.Get(Common.Constants.PROP_STORE_HTTPS_DOMAIN_PROP) + "/callback/spotify" ) { } diff --git a/LukeBot.API/TwitchToken.cs b/LukeBot.API/TwitchToken.cs index 47e059c..9d9ac14 100644 --- a/LukeBot.API/TwitchToken.cs +++ b/LukeBot.API/TwitchToken.cs @@ -13,7 +13,7 @@ public TwitchToken(AuthFlow flow, string lbUser) "https://id.twitch.tv/oauth2/authorize", "https://id.twitch.tv/oauth2/token", "https://id.twitch.tv/oauth2/revoke", - "https://" + Conf.Get(Common.Constants.PROP_STORE_SERVER_IP_PROP) + "/callback/twitch" + "https://" + Conf.Get(Common.Constants.PROP_STORE_HTTPS_DOMAIN_PROP) + "/callback/twitch" ) { } diff --git a/LukeBot.Common/Constants.cs b/LukeBot.Common/Constants.cs index d71dd8e..bbbf5cc 100644 --- a/LukeBot.Common/Constants.cs +++ b/LukeBot.Common/Constants.cs @@ -21,12 +21,15 @@ public class Constants public static readonly Path PROP_STORE_USERS_PROP = Path.Form(LUKEBOT_USER_ID, PROP_STORE_USERS_PROP_NAME); public static readonly Path PROP_STORE_RECONNECT_COUNT_PROP = Path.Form(LUKEBOT_USER_ID, PROP_STORE_RECONNECT_COUNT_PROP_NAME); - public const string DEFAULT_SERVER_IP = "localhost:5000"; + public const string DEFAULT_SERVER_IP = "127.0.0.1"; + public const string DEFAULT_SERVER_HTTPS_DOMAIN = "localhost"; public const string PROPERTY_STORE_FILE = "Data/props.lukebot"; public const string DEFAULT_LOGIN_NAME = "SET_BOT_LOGIN_HERE"; public const string DEFAULT_CLIENT_ID_NAME = "SET_YOUR_CLIENT_ID_HERE"; public const string DEFAULT_CLIENT_SECRET_NAME = "SET_YOUR_CLIENT_SECRET_HERE"; + public const int SERVERCLI_DEFAULT_PORT = 55268; // in T9: LKBOT + public const string SPOTIFY_MODULE_NAME = "spotify"; public const string TWITCH_MODULE_NAME = "twitch"; public const string WIDGET_MODULE_NAME = "widget"; diff --git a/LukeBot.Common/Utils.cs b/LukeBot.Common/Utils.cs index 0f7ed30..09d4d8a 100644 --- a/LukeBot.Common/Utils.cs +++ b/LukeBot.Common/Utils.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.Diagnostics; using System.Net; +using System.Net.Sockets; using System.Runtime.InteropServices; diff --git a/LukeBot.Communication/EventSystem.cs b/LukeBot.Communication/EventSystem.cs index ba2fdb9..fdee4c0 100644 --- a/LukeBot.Communication/EventSystem.cs +++ b/LukeBot.Communication/EventSystem.cs @@ -71,6 +71,9 @@ internal EventCollection(string lbUser) */ public Event Event(string name) { + if (!mEvents.ContainsKey(name)) + throw new EventNotFoundException(name); + return mEvents[name]; } @@ -139,6 +142,8 @@ public List RegisterPublisher(IEventPublisher p) { string pubName = p.GetName(); + Logger.Log().Debug("Registering publisher {0}", pubName); + if (mPublishers.ContainsKey(pubName)) throw new PublisherAlreadyRegisteredException(pubName); @@ -170,6 +175,8 @@ public void UnregisterPublisher(IEventPublisher p) { string pubName = p.GetName(); + Logger.Log().Debug("Unregistering publisher {0}", pubName); + if (!mPublishers.ContainsKey(pubName)) return; diff --git a/LukeBot.Communication/Exception/DispatcherNotFoundException.cs b/LukeBot.Communication/Exception/DispatcherNotFoundException.cs new file mode 100644 index 0000000..ba872c5 --- /dev/null +++ b/LukeBot.Communication/Exception/DispatcherNotFoundException.cs @@ -0,0 +1,12 @@ +using LukeBot.Communication.Common; + + +namespace LukeBot.Communication +{ + public class DispatcherNotFoundException: LukeBot.Common.Exception + { + public DispatcherNotFoundException(string dispatcherName) + : base(string.Format("Not found dispatcher: {0}", dispatcherName)) + {} + } +} diff --git a/LukeBot.Communication/Exception/EventNotFoundException.cs b/LukeBot.Communication/Exception/EventNotFoundException.cs new file mode 100644 index 0000000..687046d --- /dev/null +++ b/LukeBot.Communication/Exception/EventNotFoundException.cs @@ -0,0 +1,12 @@ +using LukeBot.Communication.Common; + + +namespace LukeBot.Communication +{ + public class EventNotFoundException: LukeBot.Common.Exception + { + public EventNotFoundException(string eventName) + : base(string.Format("Not found event: {0}", eventName)) + {} + } +} diff --git a/LukeBot.Endpoint/Endpoint.cs b/LukeBot.Endpoint/Endpoint.cs index 03e9cc5..e38d3ed 100644 --- a/LukeBot.Endpoint/Endpoint.cs +++ b/LukeBot.Endpoint/Endpoint.cs @@ -61,16 +61,21 @@ public IWebHostBuilder CreateHostBuilder() logging.AddProvider(new LBLoggingProvider()); }); - string IP = Conf.Get(Common.Constants.PROP_STORE_SERVER_IP_PROP); + string domain; string[] URLs; - if (IP.Contains("localhost")) + if (!Conf.TryGet(Common.Constants.PROP_STORE_HTTPS_DOMAIN_PROP, out domain)) + { + domain = "localhost"; + } + + if (domain.Contains("localhost")) { // manually set only localhost // we do this path just in case someone prefers to use different-than-default port 5000 URLs = new string[] { - "https://" + IP + "/", + "https://" + domain + "/", }; } else @@ -78,15 +83,15 @@ public IWebHostBuilder CreateHostBuilder() // add defined address + localhost:5000 URLs = new string[] { - "https://" + IP + "/", + "https://" + domain + "/", "https://localhost:5000/" }; } - Logger.Log().Debug("Endpoint using host addresses:"); + Logger.Log().Info("Endpoint using host addresses:"); foreach (string addr in URLs) { - Logger.Log().Debug(" - https://" + IP + "/"); + Logger.Log().Info(" - https://" + addr + "/"); } builder.UseUrls(URLs); diff --git a/LukeBot.Module/IUserModule.cs b/LukeBot.Module/IUserModule.cs index ed965f9..9ee268a 100644 --- a/LukeBot.Module/IUserModule.cs +++ b/LukeBot.Module/IUserModule.cs @@ -3,8 +3,8 @@ public interface IUserModule { public void Run(); - public void RequestShutdown(); - public void WaitForShutdown(); + public void RequestShutdown(); // TODO replace with single call Shutdown() + public void WaitForShutdown(); // TODO ^ public ModuleType GetModuleType(); } } diff --git a/LukeBot.Twitch/IRCChannel.cs b/LukeBot.Twitch/IRCChannel.cs index 4abfc86..ae919a7 100644 --- a/LukeBot.Twitch/IRCChannel.cs +++ b/LukeBot.Twitch/IRCChannel.cs @@ -11,7 +11,7 @@ namespace LukeBot.Twitch { - class IRCChannel: IEventPublisher + class IRCChannel: IEventPublisher, IDisposable { private string mLBUser; private string mChannelName; @@ -109,6 +109,11 @@ public List GetEvents() return events; } + public void Dispose() + { + Comms.Event.User(mLBUser).UnregisterPublisher(this); + } + public IRCChannel(string lbUser, API.Twitch.GetUserData userData, Token userToken, BadgeCollection globalBadges) { mLBUser = lbUser; diff --git a/LukeBot.Twitch/TwitchIRC.cs b/LukeBot.Twitch/TwitchIRC.cs index a143d9f..f2228e5 100644 --- a/LukeBot.Twitch/TwitchIRC.cs +++ b/LukeBot.Twitch/TwitchIRC.cs @@ -355,12 +355,6 @@ public TwitchIRC(string username, Token token) Logger.Log().Info("Twitch IRC module initialized"); } - ~TwitchIRC() - { - Disconnect(); - WaitForShutdown(); - } - public void JoinChannel(string lbUser, API.Twitch.GetUserData user, Token token) { mChannelsMutex.WaitOne(); @@ -390,6 +384,7 @@ public void PartChannel(API.Twitch.GetUserData user) mIRCClient.Send(IRCMessage.PART(user.login)); + mChannels[user.login].Dispose(); mChannels.Remove(user.login); mChannelsMutex.ReleaseMutex(); diff --git a/LukeBot.Widget/Alerts.cs b/LukeBot.Widget/Alerts.cs index 0992f4d..69e478c 100644 --- a/LukeBot.Widget/Alerts.cs +++ b/LukeBot.Widget/Alerts.cs @@ -148,8 +148,7 @@ protected override void OnConfigurationUpdate() AwaitEventCompletion(); } - public Alerts(string lbUser, string id, string name) - : base(lbUser, "LukeBot.Widget/Widgets/Alerts.html", id, name, new AlertWidgetConfig()) + protected override void OnLoad() { EventCollection collection = Comms.Event.User(mLBUser); @@ -163,6 +162,25 @@ public Alerts(string lbUser, string id, string name) collection.Event(Events.TWITCH_SUBSCRIPTION).InterruptEndpoint += OnEventInterrupt; } + protected override void OnUnload() + { + EventCollection collection = Comms.Event.User(mLBUser); + + collection.Event(Events.TWITCH_CHANNEL_POINTS_REDEMPTION).Endpoint -= OnSimpleEvent; + collection.Event(Events.TWITCH_CHANNEL_POINTS_REDEMPTION).InterruptEndpoint -= OnEventInterrupt; + + collection.Event(Events.TWITCH_CHEER).Endpoint -= OnSimpleEvent; + collection.Event(Events.TWITCH_CHEER).InterruptEndpoint -= OnEventInterrupt; + + collection.Event(Events.TWITCH_SUBSCRIPTION).Endpoint -= OnSubscriptionEvent; + collection.Event(Events.TWITCH_SUBSCRIPTION).InterruptEndpoint -= OnEventInterrupt; + } + + public Alerts(string lbUser, string id, string name) + : base(lbUser, "LukeBot.Widget/Widgets/Alerts.html", id, name, new AlertWidgetConfig()) + { + } + public override WidgetType GetWidgetType() { return WidgetType.alerts; diff --git a/LukeBot.Widget/Chat.cs b/LukeBot.Widget/Chat.cs index 77259a8..f768f39 100644 --- a/LukeBot.Widget/Chat.cs +++ b/LukeBot.Widget/Chat.cs @@ -36,14 +36,25 @@ protected override void OnConnected() // noop } - public Chat(string lbUser, string id, string name) - : base(lbUser, "LukeBot.Widget/Widgets/Chat.html", id, name) + protected override void OnLoad() { Comms.Event.User(mLBUser).Event(Events.TWITCH_CHAT_MESSAGE).Endpoint += OnMessage; Comms.Event.User(mLBUser).Event(Events.TWITCH_CHAT_CLEAR_USER).Endpoint += OnClearChat; Comms.Event.User(mLBUser).Event(Events.TWITCH_CHAT_CLEAR_MESSAGE).Endpoint += OnClearMsg; } + protected override void OnUnload() + { + Comms.Event.User(mLBUser).Event(Events.TWITCH_CHAT_MESSAGE).Endpoint -= OnMessage; + Comms.Event.User(mLBUser).Event(Events.TWITCH_CHAT_CLEAR_USER).Endpoint -= OnClearChat; + Comms.Event.User(mLBUser).Event(Events.TWITCH_CHAT_CLEAR_MESSAGE).Endpoint -= OnClearMsg; + } + + public Chat(string lbUser, string id, string name) + : base(lbUser, "LukeBot.Widget/Widgets/Chat.html", id, name) + { + } + public override WidgetType GetWidgetType() { return WidgetType.chat; diff --git a/LukeBot.Widget/Echo.cs b/LukeBot.Widget/Echo.cs index e930f68..18cd212 100644 --- a/LukeBot.Widget/Echo.cs +++ b/LukeBot.Widget/Echo.cs @@ -66,6 +66,14 @@ protected override void OnConnected() } } + protected override void OnLoad() + { + } + + protected override void OnUnload() + { + } + public Echo(string lbUser, string id, string name) : base(lbUser, "LukeBot.Widget/Widgets/Echo.html", id, name) { diff --git a/LukeBot.Widget/IWidget.cs b/LukeBot.Widget/IWidget.cs index cfdf813..55acd60 100644 --- a/LukeBot.Widget/IWidget.cs +++ b/LukeBot.Widget/IWidget.cs @@ -36,6 +36,7 @@ public WebSocketRecv(WebSocketReceiveResult result, string data) public string ID { get; private set; } public string Name { get; private set; } + public bool Loaded { get; private set; } public string mWidgetFilePath; protected string mLBUser; private List mHead; @@ -50,6 +51,8 @@ public WebSocketRecv(WebSocketReceiveResult result, string data) private Config.Path mConfigurationPath; protected bool Connected { get { return mWS != null && mWS.State == WebSocketState.Open; } } + protected abstract void OnLoad(); // called when widget is loaded. Can throw, which will leave widget in unloaded state. + protected abstract void OnUnload(); // called when widget is loaded. Can throw, which will leave widget in unloaded state. protected abstract void OnConnected(); protected virtual void OnConfigurationUpdate() {} @@ -67,13 +70,13 @@ private string GetWidgetCode() internal string GetWidgetAddress() { - string serverAddress = Conf.Get(LukeBot.Common.Constants.PROP_STORE_SERVER_IP_PROP); + string serverAddress = Conf.Get(LukeBot.Common.Constants.PROP_STORE_HTTPS_DOMAIN_PROP); return "https://" + serverAddress + "/widget/" + ID; } private string GetWidgetWSAddress() { - string serverAddress = Conf.Get(LukeBot.Common.Constants.PROP_STORE_SERVER_IP_PROP); + string serverAddress = Conf.Get(LukeBot.Common.Constants.PROP_STORE_HTTPS_DOMAIN_PROP); return "wss://" + serverAddress + "/widgetws/" + ID; } @@ -237,6 +240,7 @@ public IWidget(string lbUser, string widgetFilePath, string id, string name, Wid ID = id; Name = name; + Loaded = false; mLBUser = lbUser; mHead = new List(); mWS = null; @@ -251,7 +255,6 @@ public IWidget(string lbUser, string widgetFilePath, string id, string name, Wid .Push(ID) .Push(Constants.PROP_CONFIG); mConfiguration = config; - LoadConfiguration(); } public IWidget(string lbUser, string widgetFilePath, string id, string name) @@ -259,6 +262,33 @@ public IWidget(string lbUser, string widgetFilePath, string id, string name) { } + public void Load() + { + if (Loaded) + return; + + LoadConfiguration(); + OnLoad(); + Loaded = true; + } + + public void Unload() + { + if (!Loaded) + return; + + mWSThreadDone = true; + CloseWS(WebSocketCloseStatus.NormalClosure); + + if (mWSMessagingThread != null) + mWSMessagingThread.Join(); + + SaveConfiguration(); + + OnUnload(); + Loaded = false; + } + public string GetPage() { string page = ""; @@ -314,19 +344,5 @@ public WidgetDesc GetDesc() } public abstract WidgetType GetWidgetType(); - - public virtual void RequestShutdown() - { - mWSThreadDone = true; - CloseWS(WebSocketCloseStatus.NormalClosure); - } - - public virtual void WaitForShutdown() - { - if (mWSMessagingThread != null) - mWSMessagingThread.Join(); - - SaveConfiguration(); - } } } diff --git a/LukeBot.Widget/NowPlaying.cs b/LukeBot.Widget/NowPlaying.cs index 190f177..9db504c 100644 --- a/LukeBot.Widget/NowPlaying.cs +++ b/LukeBot.Widget/NowPlaying.cs @@ -42,14 +42,23 @@ protected override void OnConnected() } } + protected override void OnLoad() + { + Comms.Event.User(mLBUser).Event(Events.SPOTIFY_STATE_UPDATE).Endpoint += OnStateUpdate; + Comms.Event.User(mLBUser).Event(Events.SPOTIFY_TRACK_CHANGED).Endpoint += OnTrackChanged; + } + + protected override void OnUnload() + { + Comms.Event.User(mLBUser).Event(Events.SPOTIFY_STATE_UPDATE).Endpoint -= OnStateUpdate; + Comms.Event.User(mLBUser).Event(Events.SPOTIFY_TRACK_CHANGED).Endpoint -= OnTrackChanged; + } + public NowPlaying(string lbUser, string id, string name) : base(lbUser, "LukeBot.Widget/Widgets/NowPlaying.html", id, name) { mState = null; mCurrentTrack = null; - - Comms.Event.User(mLBUser).Event(Events.SPOTIFY_STATE_UPDATE).Endpoint += OnStateUpdate; - Comms.Event.User(mLBUser).Event(Events.SPOTIFY_TRACK_CHANGED).Endpoint += OnTrackChanged; } public override WidgetType GetWidgetType() diff --git a/LukeBot.Widget/WidgetMainModule.cs b/LukeBot.Widget/WidgetMainModule.cs index 902bb72..8fa690f 100644 --- a/LukeBot.Widget/WidgetMainModule.cs +++ b/LukeBot.Widget/WidgetMainModule.cs @@ -142,11 +142,21 @@ public WidgetDesc GetWidgetInfo(string lbUser, string id) return mUsers[lbUser].GetWidgetInfo(id); } + public bool IsWidgetLoaded(string lbUser, string id) + { + return mUsers[lbUser].IsWidgetLoaded(id); + } + public void DeleteWidget(string lbUser, string id) { mUsers[lbUser].DeleteWidget(id); } + public void ReloadWidget(string lbUser, string id) + { + mUsers[lbUser].ReloadWidget(id); + } + public void UpdateWidgetConfiguration(string lbUser, string id, IEnumerable<(string, string)> changes) { mUsers[lbUser].UpdateWidgetConfiguration(id, changes); diff --git a/LukeBot.Widget/WidgetUserModule.cs b/LukeBot.Widget/WidgetUserModule.cs index 42e8a0b..8eeb8bd 100644 --- a/LukeBot.Widget/WidgetUserModule.cs +++ b/LukeBot.Widget/WidgetUserModule.cs @@ -3,6 +3,7 @@ using System.Net.WebSockets; using System.Threading.Tasks; using LukeBot.Config; +using LukeBot.Logging; using LukeBot.Module; using LukeBot.Widget.Common; @@ -35,10 +36,27 @@ private void LoadWidgetsFromConfig() foreach (WidgetDesc wd in widgets) { - LoadWidget(wd); + AddWidgetFromDesc(wd); } } + private WidgetDesc GetWidgetDescFromConfig(string id) + { + Path widgetCollectionProp = GetWidgetCollectionPropertyName(); + + WidgetDesc[] widgets; + if (!Conf.TryGet(widgetCollectionProp, out widgets)) + throw new WidgetNotFoundException(id); + + foreach (WidgetDesc wd in widgets) + { + if (wd.Id == id) + return wd; + } + + throw new WidgetNotFoundException(id); + } + private void SaveWidgetToConfig(IWidget w) { WidgetDesc wd = w.GetDesc(); @@ -53,6 +71,21 @@ private void RemoveWidgetFromConfig(string id) ConfUtil.ArrayRemove(widgetCollectionProp, (WidgetDesc d) => d.Id != id); } + + + private IWidget AddWidgetFromDesc(WidgetDesc wd) + { + IWidget w = AllocateWidget(wd.Type, wd.Id, wd.Name); + mWidgets.Add(wd.Id, w); + + if (wd.Name != null && wd.Name.Length > 0) + mNameToId.Add(wd.Name, wd.Id); + + LoadWidget(wd.Id); + + return w; + } + private IWidget AllocateWidget(WidgetType type, string id, string name) { switch (type) @@ -66,16 +99,53 @@ private IWidget AllocateWidget(WidgetType type, string id, string name) } } + private void RemoveWidget(string id) + { + string name = mWidgets[id].Name; + + UnloadWidget(id); + mWidgets.Remove(id); + + if (mNameToId.ContainsKey(name)) + mNameToId.Remove(name); + + RemoveWidgetFromConfig(id); + } - internal IWidget LoadWidget(WidgetDesc wd) + private void LoadWidget(string id) { - IWidget w = AllocateWidget(wd.Type, wd.Id, wd.Name); - mWidgets.Add(wd.Id, w); + IWidget w = mWidgets[id]; - if (wd.Name != null && wd.Name.Length > 0) - mNameToId.Add(wd.Name, wd.Id); + try + { + // Load() can fail, which will leave the Widget in unloaded state. + w.Load(); + } + catch (Exception e) + { + if (w.Name.Length > 0) + Logger.Log().Error("Falied to load Widget {0} ({1}): {2}", w.Name, w.ID, e.Message); + else + Logger.Log().Error("Falied to load Widget {0}: {1}", w.ID, e.Message); + } + } - return w; + private void UnloadWidget(string id) + { + IWidget w = mWidgets[id]; + + try + { + // Unload() can fail, but we should ignore it and move on + w.Unload(); + } + catch (Exception e) + { + if (w.Name.Length > 0) + Logger.Log().Error("Falied to unload Widget {0} ({1}): {2}", w.Name, w.ID, e.Message); + else + Logger.Log().Error("Falied to unload Widget {0}: {1}", w.ID, e.Message); + } } internal string GetWidgetPage(string widgetID) @@ -149,6 +219,8 @@ public IWidget AddWidget(WidgetType type, string name) SaveWidgetToConfig(w); + LoadWidget(id); + return w; } @@ -169,19 +241,24 @@ public WidgetDesc GetWidgetInfo(string id) return mWidgets[GetActualWidgetId(id)].GetDesc(); } + public bool IsWidgetLoaded(string id) + { + return mWidgets[GetActualWidgetId(id)].Loaded; + } + public void DeleteWidget(string id) { string actualId = GetActualWidgetId(id); - IWidget w = mWidgets[actualId]; - w.RequestShutdown(); - w.WaitForShutdown(); + RemoveWidget(actualId); + } - mWidgets.Remove(actualId); - if (mNameToId.ContainsKey(id)) - mNameToId.Remove(id); + public void ReloadWidget(string id) + { + string actualId = GetActualWidgetId(id); - RemoveWidgetFromConfig(actualId); + UnloadWidget(actualId); + LoadWidget(actualId); } public WidgetConfiguration GetWidgetConfiguration(string id) @@ -198,18 +275,15 @@ public void UpdateWidgetConfiguration(string id, IEnumerable<(string, string)> c public void RequestShutdown() { - foreach (var w in mWidgets) + foreach (IWidget w in mWidgets.Values) { - w.Value.RequestShutdown(); + string id = w.ID; + UnloadWidget(id); } } public void WaitForShutdown() { - foreach (var w in mWidgets) - { - w.Value.WaitForShutdown(); - } } public ModuleType GetModuleType() diff --git a/LukeBot/ServerCLI.cs b/LukeBot/ServerCLI.cs index 28c5c3f..bd6afec 100644 --- a/LukeBot/ServerCLI.cs +++ b/LukeBot/ServerCLI.cs @@ -155,7 +155,7 @@ private void MainLoop() while (!mRecvThreadDone) { ServerMessage msg = ReceiveObject(); - LogClientContext(LogLevel.Info, "Mesage: {0}", msg.Type.ToString()); + LogClientContext(LogLevel.Info, "Mesage: {0}", msg.ToString()); if (!ValidateMessage(msg)) { // cut the connection, something was not correct @@ -441,7 +441,7 @@ private enum InterruptReason private IUserManager mUserManager = null; private TcpListener mServer; private X509Certificate2 mSSLCert; - private string mAddress; + private IPAddress mAddress; private int mPort; public void OnClientRecvThreadDone(string cookie) @@ -529,16 +529,30 @@ private void ClearClients() mClients.Clear(); } - public ServerCLI(string address, int port, IUserManager userManager) + public ServerCLI(IUserManager userManager) { if (userManager == null) throw new ArgumentException("User manager is required for Server CLI to work."); - mAddress = address; - mPort = port; + mAddress = IPAddress.Parse("127.0.0.1"); + mPort = Common.Constants.SERVERCLI_DEFAULT_PORT; mUserManager = userManager; - mServer = new TcpListener(IPAddress.Parse(address), port); + string address; + if (!Conf.TryGet(Common.Constants.PROP_STORE_SERVER_IP_PROP, out address)) + throw new ServerCLIException("server_ip property not set, IP address for Server's TCP listener unknown"); + + try + { + mAddress = IPAddress.Parse(address); + } + catch (FormatException) + { + throw new ServerCLIException("server_ip property is invalid"); + } + + Logger.Log().Info("ServerCLI: Will listen on {0}", mAddress); + mServer = new TcpListener(mAddress, mPort); string httpsDomain; if (!Conf.TryGet(Common.Constants.PROP_STORE_HTTPS_DOMAIN_PROP, out httpsDomain)) diff --git a/LukeBot/TwitchCLIProcessor.cs b/LukeBot/TwitchCLIProcessor.cs index b3e52a1..1bb0769 100644 --- a/LukeBot/TwitchCLIProcessor.cs +++ b/LukeBot/TwitchCLIProcessor.cs @@ -51,7 +51,7 @@ private void CheckForLogin(CLIMessageProxy CLI) if (!Conf.TryGet(path, out string login)) { - login = CLI.Query(false, "Spotify login for user " + CLI.GetCurrentUser()); + login = CLI.Query(false, "Twitch login for user " + CLI.GetCurrentUser()); if (login.Length == 0) { throw new ArgumentException("No login provided"); diff --git a/LukeBot/UserInterface.cs b/LukeBot/UserInterface.cs index d279a69..467d2d9 100644 --- a/LukeBot/UserInterface.cs +++ b/LukeBot/UserInterface.cs @@ -53,7 +53,7 @@ public static void Initialize(InterfaceType type, IUserManager userManager) mInterface = new BasicCLI(userManager); break; case InterfaceType.server: - mInterface = new ServerCLI("127.0.0.1", 55268, userManager); + mInterface = new ServerCLI(userManager); break; default: throw new UnrecognizedInterfaceTypeException(mType); diff --git a/LukeBot/WidgetCLIProcessor.cs b/LukeBot/WidgetCLIProcessor.cs index f8fe62b..59b59d5 100644 --- a/LukeBot/WidgetCLIProcessor.cs +++ b/LukeBot/WidgetCLIProcessor.cs @@ -64,6 +64,17 @@ public WidgetDeleteCommand() } } + [Verb("reload", HelpText = "Reload all widgets or selected widget.")] + public class WidgetReloadCommand + { + [Value(0, MetaName = "id", Required = false, HelpText = "Widget's ID, can be either UUID or its name. Omit to reload all.")] + public string Id { get; set; } + + public WidgetReloadCommand() + { + } + } + [Verb("update", HelpText = "Updates Widget's configuration. Each Widget might have different configuration fields depending on type.")] public class WidgetUpdateCommand: WidgetBaseCommand { @@ -98,14 +109,13 @@ public void HandleAddCommand(WidgetAddCommand cmd, CLIMessageProxy CLI, out stri { string lbUser = CLI.GetCurrentUser(); addr = GlobalModules.Widget.AddWidget(lbUser, cmd.Type, cmd.Name); + + msg = "Added new widget at address: " + addr; } catch (System.Exception e) { msg = "Failed to add widget: " + e.Message; - return; } - - msg = "Added new widget at address: " + addr; } public void HandleAddressCommand(WidgetAddressCommand cmd, CLIMessageProxy CLI, out string msg) @@ -116,14 +126,13 @@ public void HandleAddressCommand(WidgetAddressCommand cmd, CLIMessageProxy CLI, { string lbUser = CLI.GetCurrentUser(); wd = GlobalModules.Widget.GetWidgetInfo(lbUser, cmd.Id); + + msg = wd.Address; } catch (System.Exception e) { msg = "Failed to get widget's address: " + e.Message; - return; } - - msg = wd.Address; } public void HandleListCommand(WidgetListCommand cmd, CLIMessageProxy CLI, out string msg) @@ -134,20 +143,25 @@ public void HandleListCommand(WidgetListCommand cmd, CLIMessageProxy CLI, out st { string lbUser = CLI.GetCurrentUser(); widgets = GlobalModules.Widget.ListUserWidgets(lbUser); + + msg = "Available widgets:"; + foreach (WidgetDesc w in widgets) + { + msg += "\n " + w.Id + " ("; + + if (w.Name.Length > 0) + msg += w.Name + ", "; + msg += w.Type.ToString(); + + if (!GlobalModules.Widget.IsWidgetLoaded(lbUser, w.Id)) + msg += ", unloaded)"; + else + msg += ")"; + } } catch (System.Exception e) { msg = "Failed to list widgets: " + e.Message; - return; - } - - msg = "Available widgets:"; - foreach (WidgetDesc w in widgets) - { - msg += "\n " + w.Id + " ("; - if (w.Name.Length > 0) - msg += w.Name + ", "; - msg += w.Type.ToString() + ")"; } } @@ -161,15 +175,14 @@ public void HandleInfoCommand(WidgetInfoCommand cmd, CLIMessageProxy CLI, out st string lbUser = CLI.GetCurrentUser(); wd = GlobalModules.Widget.GetWidgetInfo(lbUser, cmd.Id); conf = GlobalModules.Widget.GetWidgetConfiguration(lbUser, cmd.Id); + + msg = "Widget " + cmd.Id + " info:\n" + wd.ToFormattedString(); + msg += "\nConfiguration:\n" + conf.ToFormattedString(); } catch (System.Exception e) { msg = "Failed to get widget info: " + e.Message; - return; } - - msg = "Widget " + cmd.Id + " info:\n" + wd.ToFormattedString(); - msg += "\nConfiguration:\n" + conf.ToFormattedString(); } public void HandleDeleteCommand(WidgetDeleteCommand cmd, CLIMessageProxy CLI, out string msg) @@ -178,14 +191,42 @@ public void HandleDeleteCommand(WidgetDeleteCommand cmd, CLIMessageProxy CLI, ou { string lbUser = CLI.GetCurrentUser(); GlobalModules.Widget.DeleteWidget(lbUser, cmd.Id); + + msg = "Widget " + cmd.Id + " deleted."; } catch (System.Exception e) { msg = "Failed to delete widget: " + e.Message; - return; } + } - msg = "Widget " + cmd.Id + " deleted."; + public void HandleReloadCommand(WidgetReloadCommand cmd, CLIMessageProxy CLI, out string msg) + { + try + { + string lbUser = CLI.GetCurrentUser(); + + if (cmd.Id != null && cmd.Id.Length > 0) + { + GlobalModules.Widget.ReloadWidget(lbUser, cmd.Id); + msg = "Widget " + cmd.Id + " reloaded."; + } + else + { + List widgets = GlobalModules.Widget.ListUserWidgets(lbUser); + + foreach (WidgetDesc wd in widgets) + { + GlobalModules.Widget.ReloadWidget(lbUser, wd.Id); + } + + msg = "Widgets reloaded."; + } + } + catch (System.Exception e) + { + msg = "Failed to reload widget: " + e.Message; + } } public void HandleUpdateCommand(WidgetUpdateCommand arg, CLIMessageProxy CLI, out string msg) @@ -245,13 +286,14 @@ public void AddCLICommands(LukeBot lb) { string result = ""; Parser p = new Parser(with => with.HelpWriter = new CLIUtils.CLIMessageProxyTextWriter(cliProxy)); - p.ParseArguments(args) + p.ParseArguments(args) .WithParsed((WidgetAddCommand arg) => HandleAddCommand(arg, cliProxy, out result)) .WithParsed((WidgetAddressCommand arg) => HandleAddressCommand(arg, cliProxy, out result)) .WithParsed((WidgetListCommand arg) => HandleListCommand(arg, cliProxy, out result)) .WithParsed((WidgetInfoCommand arg) => HandleInfoCommand(arg, cliProxy, out result)) .WithParsed((WidgetDeleteCommand arg) => HandleDeleteCommand(arg, cliProxy, out result)) + .WithParsed((WidgetReloadCommand arg) => HandleReloadCommand(arg, cliProxy, out result)) .WithParsed((WidgetUpdateCommand arg) => HandleUpdateCommand(arg, cliProxy, out result)) .WithParsed((WidgetEnableCommand arg) => HandleEnableCommand(arg, cliProxy, out result)) .WithParsed((WidgetDisableCommand arg) => HandleDisableCommand(arg, cliProxy, out result)) diff --git a/LukeBotClient/Constants.cs b/LukeBotClient/Constants.cs index ec77b67..e863540 100644 --- a/LukeBotClient/Constants.cs +++ b/LukeBotClient/Constants.cs @@ -2,8 +2,6 @@ namespace LukeBotClient { internal class Constants { - public const string SERVER_DEFAULT_ADDRESS = "localhost"; - public const int SERVER_DEFAULT_PORT = 55268; // in T9: LKBOT public const int CLIENT_BUFFER_SIZE = 4096; } } \ No newline at end of file diff --git a/LukeBotClient/LukeBotClient.cs b/LukeBotClient/LukeBotClient.cs index cfd2267..637aa33 100644 --- a/LukeBotClient/LukeBotClient.cs +++ b/LukeBotClient/LukeBotClient.cs @@ -151,6 +151,8 @@ public async void ReceiveThreadMain() { PrintLine("Receive thread exiting - received NULL message, probably connection is broken."); mRecvThreadDone = true; + mState = State.Done; + LukeBot.Common.Utils.CancelConsoleIO(); continue; } @@ -257,6 +259,8 @@ public async Task Login() int tries = 0; while (!loggedIn) { + Console.WriteLine("Attempting login to " + mOpts.Address); + Console.Write("Username: "); string user = Console.ReadLine(); @@ -319,6 +323,7 @@ public async Task Run() // should be a simple "send command and wait for response" here mState = State.InCLI; + mAwaitResponseEvent.Reset(); while (mState != State.Done) { string msg = ""; @@ -326,6 +331,7 @@ public async Task Run() switch (mState) { case State.InCLI: + Print(mCurrentPrompt); msg = Console.ReadLine(); if (msg == "quit") @@ -339,6 +345,7 @@ public async Task Run() mState = State.AwaitingResponse; CommandServerMessage cmdMessage = new(mSessionData, msg); await SendObject(cmdMessage); + PrintLine("state = " + mState); break; case State.AwaitingResponse: mAwaitResponseEvent.WaitOne(); diff --git a/LukeBotClient/ProgramOptions.cs b/LukeBotClient/ProgramOptions.cs index ebcbc34..2a75195 100644 --- a/LukeBotClient/ProgramOptions.cs +++ b/LukeBotClient/ProgramOptions.cs @@ -6,26 +6,21 @@ namespace LukeBotClient { internal class ProgramOptions { - [Option('a', "address", + [Value(0, HelpText = "Provide IP address to connect to", - Default = Constants.SERVER_DEFAULT_ADDRESS)] + Required = false, + Default = LukeBot.Common.Constants.DEFAULT_SERVER_HTTPS_DOMAIN)] public string Address { get; set; } [Option('p', "port", HelpText = "Provide a custom port to connect to", - Default = Constants.SERVER_DEFAULT_PORT)] + Default = LukeBot.Common.Constants.SERVERCLI_DEFAULT_PORT)] public int Port { get; set; } - [Option("disable-ssl", - HelpText = "Disable the use of SSL for connection. Production server will refuse a connection like that, use only for development/debug.", - Default = false)] - public bool DisableSSL { get; set; } - public ProgramOptions() { - Address = Constants.SERVER_DEFAULT_ADDRESS; - Port = Constants.SERVER_DEFAULT_PORT; - DisableSSL = false; + Address = LukeBot.Common.Constants.DEFAULT_SERVER_HTTPS_DOMAIN; + Port = LukeBot.Common.Constants.SERVERCLI_DEFAULT_PORT; } } } \ No newline at end of file diff --git a/Tools/propmgr/StoreTemplates.cs b/Tools/propmgr/StoreTemplates.cs index 3e1bf12..e5e3b16 100644 --- a/Tools/propmgr/StoreTemplates.cs +++ b/Tools/propmgr/StoreTemplates.cs @@ -20,7 +20,7 @@ public class DefaultStoreTemplate: StoreTemplate public override void Fill(PropertyStore store) { store.Add(LukeBot.Common.Constants.PROP_STORE_SERVER_IP_PROP, Property.Create(LukeBot.Common.Constants.DEFAULT_SERVER_IP)); - store.Add(LukeBot.Common.Constants.PROP_STORE_HTTPS_DOMAIN_PROP, Property.Create("localhost")); + store.Add(LukeBot.Common.Constants.PROP_STORE_HTTPS_DOMAIN_PROP, Property.Create(LukeBot.Common.Constants.DEFAULT_SERVER_HTTPS_DOMAIN)); store.Add(LukeBot.Common.Constants.PROP_STORE_HTTPS_EMAIL_PROP, Property.Create("my@email.com")); store.Add(LukeBot.Common.Constants.PROP_STORE_USERS_PROP, Property.Create(new string[] {})); store.Add(LukeBot.Common.Constants.PROP_STORE_RECONNECT_COUNT_PROP, Property.Create(10));