Cleanup event calls and continue implementing

This commit is contained in:
Joel Wetzell
2020-06-13 17:30:19 -05:00
parent 2d1fe11435
commit 0fd5e46495
3 changed files with 246 additions and 79 deletions
+66 -38
View File
@@ -4,6 +4,9 @@ using System.Linq;
using Newtonsoft.Json; using Newtonsoft.Json;
using Newtonsoft.Json.Linq; using Newtonsoft.Json.Linq;
using SharpOSC; using SharpOSC;
using Serilog;
using System.Collections.Concurrent;
using System.ComponentModel;
namespace QSharp namespace QSharp
{ {
@@ -32,7 +35,7 @@ namespace QSharp
public QClient(string host, int port) public QClient(string host, int port)
{ {
tcpClient = new TCPClient(host, port); tcpClient = new TCPClient(host, port);
Console.WriteLine($"[QClient] setup connection to: <{host}:{port}>"); Log.Debug($"[client] setup connection to: <{host}:{port}>");
tcpClient.MessageReceived += ProcessMessage; tcpClient.MessageReceived += ProcessMessage;
} }
@@ -41,7 +44,10 @@ namespace QSharp
public bool connect() public bool connect()
{ {
return tcpClient.Connect(); if (tcpClient == null)
return false;
else
return tcpClient.Connect();
} }
public void disconnect() public void disconnect()
@@ -53,7 +59,7 @@ namespace QSharp
public void sendMessage(string address, params object[] args) public void sendMessage(string address, params object[] args)
{ {
tcpClient.Send(new OscMessage(address, args)); tcpClient.Send(new OscMessage(address, args));
// Console.WriteLine($"QClient send message {address}"); Log.Debug($"[client] send message {address} : {args}");
} }
private void ProcessMessage(object source, MessageEventArgs args) private void ProcessMessage(object source, MessageEventArgs args)
@@ -64,10 +70,30 @@ namespace QSharp
if (message.IsReply) if (message.IsReply)
{ {
JToken data = message.response; JToken data = message.response;
//special case, want to update cue properties //special case, want to update cue properties
if (message.IsReplyFromCue) if (message.IsReplyFromCue)
{ {
//todo check data type? if(data.Type == JTokenType.Object)
{
OnCueUpdated(message.cueID, data);
}
else if (data.Type == JTokenType.String || data.Type == JTokenType.Integer || data.Type == JTokenType.Float)
{
string property = message.AddressParts.Last();
if (property == null)
return;
//create object manually since single value replies don't have dictionaries
JObject properties = new JObject();
properties.Add(property, data);
OnCueUpdated(message.cueID, properties);
}
else
{
Log.Debug($"[client] unhandled reply from cue: Type: {data.Type} value: {message.response}");
}
} }
else if (message.IsReplyFromCueLists) else if (message.IsReplyFromCueLists)
{ {
@@ -86,7 +112,7 @@ namespace QSharp
} }
else else
{ {
Console.WriteLine($"[client] unhandled reply message: {message.address}"); Log.Debug($"[client] unhandled reply message: {message.address}");
} }
} }
else if(message.IsUpdate) { else if(message.IsUpdate) {
@@ -96,7 +122,8 @@ namespace QSharp
} }
else if (message.IsPlaybackPositionUpdate) else if (message.IsPlaybackPositionUpdate)
{ {
OnCueListChangedPlaybackPosition(message.cueID); string cueListID = message.AddressParts[4];
OnCueListChangedPlaybackPosition(cueListID, message.cueID);
} }
else if (message.IsWorkspaceUpdate) else if (message.IsWorkspaceUpdate)
{ {
@@ -125,86 +152,81 @@ namespace QSharp
} }
else if (message.IsDisconnect) else if (message.IsDisconnect)
{ {
Console.WriteLine($"[client] disconnect message received: {message.address}"); Log.Debug($"[client] disconnect message received: {message.address}");
OnWorkspaceDisconnected(); OnWorkspaceDisconnected();
} }
else else
{ {
Console.WriteLine($"[client] unhandled update message: {message.address}"); Log.Debug($"[client] unhandled update message: {message.address}");
} }
} }
else else
{ {
Console.WriteLine($"[client] unhandled message: {message.address}"); Log.Debug($"[client] unhandled message: {message.address}");
} }
} }
protected virtual void OnCueUpdated(string cueID, JToken properties)
{
Log.Debug($"[client] cue updated: {cueID}");
CueUpdated?.Invoke(this, new QCueUpdatedArgs { cueID = cueID, data = properties });
}
protected virtual void OnCueNeedsUpdated(string cueID) protected virtual void OnCueNeedsUpdated(string cueID)
{ {
Console.WriteLine($"[client] cue needs updated: {cueID}"); Log.Debug($"[client] cue needs updated: {cueID}");
if (CueNeedsUpdated != null) CueNeedsUpdated?.Invoke(this, new QCueNeedsUpdatedArgs { cueID = cueID });
CueNeedsUpdated(this, new QCueNeedsUpdatedArgs { cueID = cueID });
} }
protected virtual void OnCueListsUpdated(JToken response) protected virtual void OnCueListsUpdated(JToken response)
{ {
Console.WriteLine($"[client] Cue Lists Updated"); Log.Debug($"[client] Cue Lists Updated");
if (CueListsUpdated != null) CueListsUpdated?.Invoke(this, new QCueListsUpdatedArgs { data = response });
CueListsUpdated(this, new QCueListsUpdatedArgs { data = response });
} }
protected virtual void OnCueListChangedPlaybackPosition(string cueID) protected virtual void OnCueListChangedPlaybackPosition(string cueListID, string cueID)
{ {
Console.WriteLine($"[client] Playback Position Changed: {cueID}"); Log.Debug($"[client] CueList <{cueListID}> Playback Position Changed to <{cueID}>");
if (CueListChangedPlaybackPosition != null) CueListChangedPlaybackPosition?.Invoke(this, new QCueListChangedPlaybackPositionArgs { cueListID = cueListID, cueID = cueID });
CueListChangedPlaybackPosition(this, new QCueListChangedPlaybackPositionArgs { cueID = cueID });
} }
protected virtual void OnWorkspaceUpdated() protected virtual void OnWorkspaceUpdated()
{ {
Console.WriteLine($"[client] Workspace Updated"); Log.Debug($"[client] Workspace Updated");
if (WorkspaceUpdated != null) WorkspaceUpdated?.Invoke(this, new QWorkspaceUpdatedArgs());
WorkspaceUpdated(this, new QWorkspaceUpdatedArgs());
} }
protected virtual void OnWorkspaceSettingsUpdated(string settingsType) protected virtual void OnWorkspaceSettingsUpdated(string settingsType)
{ {
Console.WriteLine($"[client] Workspace Settings Updated"); Log.Debug($"[client] Workspace Settings Updated");
if (WorkspaceSettingsUpdated != null) WorkspaceSettingsUpdated?.Invoke(this, new QWorkspaceSettingsUpdatedArgs { settingsType = settingsType });
WorkspaceSettingsUpdated(this, new QWorkspaceSettingsUpdatedArgs { settingsType = settingsType});
} }
protected virtual void OnWorkspaceLightDashboardUpdated() protected virtual void OnWorkspaceLightDashboardUpdated()
{ {
Console.WriteLine($"[client] Workspace Light Dashboard Updated"); Log.Debug($"[client] Workspace Light Dashboard Updated");
if (WorkspaceLightDashboardUpdated != null) WorkspaceLightDashboardUpdated?.Invoke(this, new QWorkspaceLightDashboardUpdatedArgs());
WorkspaceLightDashboardUpdated(this, new QWorkspaceLightDashboardUpdatedArgs());
} }
protected virtual void OnQLabPreferencesUpdated(string key) protected virtual void OnQLabPreferencesUpdated(string key)
{ {
Console.WriteLine($"[client] QLab Preferences Updated"); Log.Debug($"[client] QLab Preferences Updated");
if (QLabPreferencesUpdated != null) QLabPreferencesUpdated?.Invoke(this, new QQLabPreferencesUpdatedArgs { key = key });
QLabPreferencesUpdated(this, new QQLabPreferencesUpdatedArgs { key = key });
} }
protected virtual void OnWorkspaceDisconnected() protected virtual void OnWorkspaceDisconnected()
{ {
Console.WriteLine($"[client] Workspace Disconnected"); Log.Debug($"[client] Workspace Disconnected");
if (WorkspaceDisconnected != null) WorkspaceDisconnected?.Invoke(this, new QWorkspaceDisconnectedArgs());
WorkspaceDisconnected(this, new QWorkspaceDisconnectedArgs());
} }
protected virtual void OnWorkspaceConnected() protected virtual void OnWorkspaceConnected()
{ {
if (WorkspaceConnected != null) WorkspaceConnected?.Invoke(this, new QWorkspaceConnectedArgs());
WorkspaceConnected(this, new QWorkspaceConnectedArgs());
} }
protected virtual void OnWorkspaceConnectionError(string status) protected virtual void OnWorkspaceConnectionError(string status)
{ {
if (WorkspaceConnectionError != null) WorkspaceConnectionError?.Invoke(this, new QWorkspaceConnectionErrorArgs { status = status });
WorkspaceConnectionError(this, new QWorkspaceConnectionErrorArgs { status = status});
} }
protected virtual void OnWorkspacesUpdated(QMessage message) protected virtual void OnWorkspacesUpdated(QMessage message)
@@ -222,5 +244,11 @@ namespace QSharp
} }
public void Close()
{
this.tcpClient.Close();
}
} }
} }
+27 -9
View File
@@ -1,4 +1,5 @@
//General information about a QLab Workspace //General information about a QLab Workspace
using Serilog;
using System.Collections.Concurrent; using System.Collections.Concurrent;
using System.Collections.Generic; using System.Collections.Generic;
using Zeroconf; using Zeroconf;
@@ -7,13 +8,15 @@ namespace QSharp
{ {
public class QServer public class QServer
{ {
public event QServerUpdatedHandler ServerUpdated;
private QClient client; private QClient client;
public string host; public string host;
public int port; public int port;
public string name; public string name;
public IZeroconfHost zeroconfHost; public IZeroconfHost zeroconfHost;
List<QWorkspace> workspaces = new List<QWorkspace>(); public List<QWorkspace> workspaces = new List<QWorkspace>();
public QServer(string host, int port) public QServer(string host, int port)
{ {
@@ -21,7 +24,12 @@ namespace QSharp
this.port = port; this.port = port;
client = new QClient(host, port); client = new QClient(host, port);
client.WorkspacesUpdated += OnWorkspacesUpdated; client.WorkspacesUpdated += OnServerWorkspacesUpdated;
if (!client.connect())
{
Log.Error($"[server] unable to connect to QLab Server: {host}:{port}");
}
} }
public string description { get { return $"{name} - {host} - {port}"; } } public string description { get { return $"{name} - {host} - {port}"; } }
@@ -29,11 +37,6 @@ namespace QSharp
public void refreshWorkspaces() public void refreshWorkspaces()
{ {
if (!client.connect())
{
System.Console.WriteLine($"Error: QServer unable to connect to QLab Server: {host}:{port}");
return;
}
client.sendMessage("/workspaces"); client.sendMessage("/workspaces");
} }
@@ -48,7 +51,7 @@ namespace QSharp
} }
#region EventHandlers #region EventHandlers
private void OnWorkspacesUpdated(object source, QWorkspacesUpdatedArgs args) private void OnServerWorkspacesUpdated(object source, QWorkspacesUpdatedArgs args)
{ {
foreach (var workspace in args.Workspaces) foreach (var workspace in args.Workspaces)
{ {
@@ -57,11 +60,26 @@ namespace QSharp
{ {
QWorkspace workspaceToAdd = new QWorkspace(workspace, this); QWorkspace workspaceToAdd = new QWorkspace(workspace, this);
workspaces.Add(workspaceToAdd); workspaces.Add(workspaceToAdd);
workspaceToAdd.connectWithPasscode("1234");
} }
} }
OnServerUpdated(this);
}
protected virtual void OnServerUpdated(QServer server)
{
ServerUpdated?.Invoke(this, new QServerUpdatedArgs { server = server });
} }
#endregion #endregion
public void Close()
{
foreach (var workspace in workspaces)
{
if(workspace.connected)
workspace.Close();
}
client.Close();
}
} }
} }
+153 -32
View File
@@ -1,8 +1,9 @@
//TODO: connect methods, cue property fetch methods, everything else //TODO: connect methods, cue property fetch methods, everything else
using Newtonsoft.Json;
using Serilog;
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Security.Cryptography;
using System.Timers; using System.Timers;
namespace QSharp namespace QSharp
@@ -16,7 +17,7 @@ namespace QSharp
public string name; public string name;
public string uniqueID; public string uniqueID;
public bool connected; public bool connected;
private bool hasPasscode; public bool hasPasscode;
private string passcode; private string passcode;
private QCue root; private QCue root;
@@ -28,6 +29,9 @@ namespace QSharp
public string version; public string version;
public event QWorkspaceUpdatedHandler WorkspaceUpdated;
public event QCueListChangedPlaybackPositionHandler CueListChangedPlaybackPosition;
private void Init() private void Init()
{ {
name = ""; name = "";
@@ -63,13 +67,16 @@ namespace QSharp
client = new QClient(server.host, server.port); client = new QClient(server.host, server.port);
client.WorkspaceConnected += OnWorkspaceConnection; client.WorkspaceConnected += OnWorkspaceConnected;
client.WorkspaceConnectionError += OnWorkspaceConnectionError; client.WorkspaceConnectionError += OnWorkspaceConnectionError;
client.WorkspaceDisconnected += OnWorkspaceDisconnected; client.WorkspaceDisconnected += OnWorkspaceDisconnected;
client.CueListsUpdated += OnCueListsUpdated; client.CueListsUpdated += OnCueListsUpdated;
client.CueListChangedPlaybackPosition += OnCueListChangedPlaybackPosition;
client.CueNeedsUpdated += OnCueNeedsUpdated;
client.CueUpdated += OnCueUpdated;
this.server = server; this.server = server;
Console.WriteLine($"[workspace] <{name}> initizalied for server: {server.name} "); Log.Debug($"[workspace] <{name}> initizalied for server: {server.name} ");
} }
//updateWithDictionary //updateWithDictionary
@@ -90,7 +97,36 @@ namespace QSharp
public string description { get { return $"{name} : {uniqueID}"; } } public string description { get { return $"{name} : {uniqueID}"; } }
public string nameWithoutPathExtension { get { return name; } } //TODO public bool isOlderThanVersion(string version)
{
var thisVersion = new Version(this.version);
var otherVersion = new Version(version);
return thisVersion.CompareTo(otherVersion) < 0;
}
public bool isEqualToVersion(string version)
{
var thisVersion = new Version(this.version);
var otherVersion = new Version(version);
return thisVersion.CompareTo(otherVersion) == 0;
}
public bool isNewerThanVersion(string version)
{
var thisVersion = new Version(this.version);
var otherVersion = new Version(version);
return thisVersion.CompareTo(otherVersion) > 0;
}
public string nameWithoutPathExtension {
get {
if (name.EndsWith(".cues"))
return name.Substring(0, name.Length - 5);
else if (name.EndsWith(".qlab4"))
return name.Substring(0, name.Length - 6);
return name;
}
} //TODO
public string serverName { get { return server.name; } } public string serverName { get { return server.name; } }
@@ -98,6 +134,8 @@ namespace QSharp
public QCue firstCue { get { return firstCueList.firstCue; } } public QCue firstCue { get { return firstCueList.firstCue; } }
public QCue firstCueList { get { return root.firstCue; } } public QCue firstCueList { get { return root.firstCue; } }
public List<QCue> cueLists { get { return root.cues; } }
public string fullNameWithCueList(QCue cueList) { return ""; } public string fullNameWithCueList(QCue cueList) { return ""; }
//TODO other convenience methods //TODO other convenience methods
public string[] versionParts { get { return version.Split('.'); } } public string[] versionParts { get { return version.Split('.'); } }
@@ -110,20 +148,20 @@ namespace QSharp
//TODO //TODO
if (!client.connect()) if (!client.connect())
{ {
System.Console.WriteLine($"[workspace] *** Error: couldn't connect to server client is not connected"); Log.Error($"[workspace] *** Error: couldn't connect to server client is not connected");
return; return;
} }
//save password for reuse //save password for reuse
this.passcode = passcode; this.passcode = passcode;
System.Console.WriteLine("[workspace] connecting..."); Log.Information("[workspace] connecting...");
client.sendMessage($"{workspacePrefix}/connect",passcode); client.sendMessage($"{workspacePrefix}/connect",passcode);
} }
private void finishConnection() private void finishConnection()
{ {
//TODO //TODO
System.Console.WriteLine($"[workspace] connected to <{name}> running on QLab version <{version}>"); Log.Information($"[workspace] connected to <{name}> running on QLab version <{version}>");
connected = true; connected = true;
startReceivingUpdates(); startReceivingUpdates();
//fetchQLabVersion(); //fetchQLabVersion();
@@ -143,7 +181,7 @@ namespace QSharp
public void disconnect() public void disconnect()
{ {
//TODO //TODO
Console.WriteLine($"[workspace] disconnect: {name}"); Log.Information($"[workspace] disconnect: {name}");
stopHeartbeat(); stopHeartbeat();
stopReceivingUpdates(); stopReceivingUpdates();
disconnectFromWorkspace(); disconnectFromWorkspace();
@@ -163,7 +201,7 @@ namespace QSharp
#endregion #endregion
#region Cues #region Cues
public QCue cueWithId(string uid) public QCue cueWithID(string uid)
{ {
return root.cueWithID(uid); return root.cueWithID(uid);
} }
@@ -250,13 +288,18 @@ namespace QSharp
public void valueForKey(QCue cue, string key) { client.sendMessage(addressForCue(cue, key)); } public void valueForKey(QCue cue, string key) { client.sendMessage(addressForCue(cue, key)); }
//TODO public void valuesForKeys(QCue cue, string[] keys)
public void valuesForKeys(QCue cue, string[] keys) { } {
//TODO string keyString = JsonConvert.SerializeObject(keys);
public void updatePropertySend(QCue cue, object value, string key) { } client.sendMessage(addressForCue(cue, "valuesForKeys"), keyString);
//TODO }
public void updatePropertiesSend(QCue cue, object[] values, string key) { }
//TODO public void updatePropertySend(QCue cue, object value, string key) {
client.sendMessage(addressForCue(cue, key), value);
}
public void updatePropertiesSend(QCue cue, object[] values, string key) { client.sendMessage(addressForCue(cue, key), values); }
public void updateAllCueProperties() public void updateAllCueProperties()
{ {
root.sendAllPropertiesToQLab(); root.sendAllPropertiesToQLab();
@@ -270,9 +313,34 @@ namespace QSharp
#region Property Fetching #region Property Fetching
//TODO //TODO
public void fetchPropertiesForCue(QCue cue, string[] keys)
{
public void fetchDefaultPropertiesForCue(QCue cue)
{
string[] keys = new string[] { QOSCKey.UID, QOSCKey.Number, QOSCKey.Name,
QOSCKey.ListName, QOSCKey.Type, QOSCKey.ColorName,
QOSCKey.Flagged, QOSCKey.Armed, QOSCKey.Notes };
fetchPropertiesForCue(cue, keys, false);
}
public void fetchBasicPropertiesForCue(QCue cue)
{
string[] keys = new string[] { QOSCKey.Name, QOSCKey.Number, QOSCKey.FileTarget, QOSCKey.CueTargetNumber,
QOSCKey.HasFileTargets, QOSCKey.HasCueTargets, QOSCKey.Armed, QOSCKey.ColorName,
QOSCKey.ContinueMode, QOSCKey.Flagged, QOSCKey.PreWait, QOSCKey.PostWait,
QOSCKey.Duration, QOSCKey.AllowsEditingDuration };
fetchPropertiesForCue(cue, keys, false);
}
public void fetchPropertiesForCue(QCue cue, string[] keys, bool includeChildren)
{
valuesForKeys(cue, keys);
if (!includeChildren)
return;
foreach (var childCue in cue.cues)
{
fetchPropertiesForCue(cue, keys, includeChildren);
}
} }
#endregion #endregion
@@ -280,12 +348,14 @@ namespace QSharp
public void sendMessage(string address, params object[] args) public void sendMessage(string address, params object[] args)
{ {
//check for workspace prefix? //check for workspace prefix?
if (!address.StartsWith(workspacePrefix))
address = workspacePrefix + address;
client.sendMessage(address, args); client.sendMessage(address, args);
} }
private string addressForCue(QCue cue, string action) private string addressForCue(QCue cue, string action)
{ {
return $"{workspacePrefix}/cue_id/{cue.propertyForKey("uniqueId")}/{action}"; return $"{workspacePrefix}/cue_id/{cue.propertyForKey(QOSCKey.UID)}/{action}";
} }
private string addressForWildcardNumber(string number, string action) { private string addressForWildcardNumber(string number, string action) {
@@ -314,23 +384,23 @@ namespace QSharp
{ {
//clear passcode if there was one set in the connect() method //clear passcode if there was one set in the connect() method
this.passcode = ""; this.passcode = "";
Console.WriteLine($"[workspace] *** Error: Password for workspace {name} was incorrect!"); Log.Error($"[workspace] *** Password for workspace {name} was incorrect!");
} }
else else
Console.WriteLine($"[workspace] *** Error: Unable to connect to workspace: {name} on server: {server.name}"); Log.Error($"[workspace] *** Unable to connect to workspace: {name} on server: {server.name}");
} }
private void OnWorkspaceConnection(object source, QWorkspaceConnectedArgs args) private void OnWorkspaceConnected(object source, QWorkspaceConnectedArgs args)
{ {
Log.Debug($"[workspace] Connection being finished.");
finishConnection(); finishConnection();
Console.WriteLine($"[workspace] Connection finished this is the first cue in this workspace {root.firstCue.displayName}");
} }
private void OnWorkspaceDisconnected(object source, QWorkspaceDisconnectedArgs args) private void OnWorkspaceDisconnected(object source, QWorkspaceDisconnectedArgs args)
{ {
//this might not be called with TCP? //this might not be called with TCP?
Console.WriteLine($"[workspace] *** Workspace has indicated it is disconnecting"); Log.Warning($"[workspace] *** Workspace has indicated it is disconnecting");
} }
private void OnCueListsUpdated(object source, QCueListsUpdatedArgs args) private void OnCueListsUpdated(object source, QCueListsUpdatedArgs args)
@@ -399,31 +469,82 @@ namespace QSharp
//add Event handled? use CueUpdated one? //add Event handled? use CueUpdated one?
} }
Console.WriteLine($"[workspace] cueLists finished processing. root updated? {rootCueUpdated}"); Log.Debug($"[workspace] cueLists finished processing. root updated? {rootCueUpdated}");
Print();
OnWorkspaceUpdated();
} }
private void OnCueListChangedPlaybackPosition(object source, QCueListChangedPlaybackPositionArgs args)
{
QCue cueList = cueWithID(args.cueListID);
if (cueList == null)
return;
if (cueList.ignoreUpdates)
return;
cueList.setProperty(args.cueID, QOSCKey.PlaybackPositionId, false);
QCue selectedCue = cueWithID(args.cueID);
if (selectedCue != null)
{
Log.Information($"[workspace] cue list <{cueList.displayName}> playback position changed to <{selectedCue.displayName}>");
CueListChangedPlaybackPosition?.Invoke(this, new QCueListChangedPlaybackPositionArgs { cueListID = cueList.uid, cueID = selectedCue.uid });
}
}
//OnWorkspaceUpdated //OnWorkspaceUpdated
//OnWorkspaceSettingsUpdated //OnWorkspaceSettingsUpdated
//LightDashboardUpdated //LightDashboardUpdated
//PreferencesUpdated //PreferencesUpdated
//CueNeedsUpdated
//CueUpdated private void OnCueNeedsUpdated(object source, QCueNeedsUpdatedArgs args)
//ClientShouldDisconnect {
QCue cueToUpdate = cueWithID(args.cueID);
if (cueToUpdate == null)
return;
fetchBasicPropertiesForCue(cueToUpdate);
}
private void OnCueUpdated(object source, QCueUpdatedArgs args)
{
QCue cue = cueWithID(args.cueID);
if (cue == null)
return;
if (cue.ignoreUpdates)
return;
cue.updatePropertiesWithDictionary(args.data);
}
private void OnWorkspaceUpdated()
{
WorkspaceUpdated?.Invoke(this, new QWorkspaceUpdatedArgs());
}
#endregion #endregion
public void Close()
{
client.Close();
}
#region Printing #region Printing
public void Print() public void Print()
{ {
Console.WriteLine($"[workspace] {name}"); Log.Information($"[workspace] {name}");
foreach (var cueList in root.cues) foreach (var cueList in root.cues)
{ {
cueList.Print(); cueList.Print();
} }
Console.WriteLine("[workspace] End of Workspace"); Log.Information("[workspace] End of Workspace");
} }
#endregion #endregion
} }