Compare commits

..

12 Commits

Author SHA1 Message Date
cdo b2f5288c4b Remove sorting from server
The client is doing it itself now anyway.
2025-03-03 01:06:43 +00:00
cdo 8123c5375d Regularly refresh activities and update cards in-place 2025-03-03 01:04:56 +00:00
cdo 3e99cc293a Create Refresher class (and IRefreshable interface) 2025-03-02 18:16:08 +00:00
cdo e275fa01ed Add start() method to Periodic (and construct stopped) 2025-03-02 18:16:04 +00:00
cdo 487427cf0a Add connected property to Client 2025-03-02 17:05:16 +00:00
cdo 5912302043 Add connection indicator to client 2025-03-02 16:53:11 +00:00
cdo 7c26a9278f Transfer ownership of client socket in session_server:init/1 2025-03-02 16:53:11 +00:00
cdo 8a7032309f Replace deprecated CSS functions with standard ones 2025-03-02 16:53:11 +00:00
cdo 708343c37f Add connection_status signal to Client 2025-03-02 16:53:08 +00:00
cdo 10a7fe5c82 Remove refreshing indicator from ActivitiesView
I've decided I'm going to add an indicator for when the client is
disconnected so the indicator would be pretty redundant (and it
requires a bunch of code to implement).
2025-03-02 15:14:43 +00:00
cdo c2d81778a8 Extract Card and CardArea classes 2025-03-02 14:47:09 +00:00
cdo 94df48db7b Extract Periodic class from connection's Worker 2025-03-02 14:03:33 +00:00
14 changed files with 348 additions and 139 deletions
+80 -73
View File
@@ -1,59 +1,31 @@
namespace StudySystemClient { namespace StudySystemClient {
public class ActivitiesView : Gtk.Box { public class ActivitiesView : CardArea<ActivityCard>, IRefreshable {
private const uint REFRESH_PERIOD_MS = 30000;
private Client client; private Client client;
private Gtk.FlowBox card_container; private Refresher refresher;
private RefreshingIndicator refreshing_indicator; private bool pending_sort;
public ActivitiesView(Client client) { public ActivitiesView(Client client) {
Object(orientation: Gtk.Orientation.VERTICAL,
hexpand: true,
vexpand: true,
margin_top: 0,
margin_bottom: 0,
margin_start: 0,
margin_end: 0);
this.client = client; this.client = client;
refresher = new Refresher(this, REFRESH_PERIOD_MS);
card_container = new Gtk.FlowBox(); pending_sort = false;
card_container.homogeneous = true; this.map.connect(() => {
card_container.min_children_per_line = 1; refresher.start();
card_container.max_children_per_line = 1; refresh.begin();
card_container.selection_mode = Gtk.SelectionMode.NONE; });
card_container.valign = Gtk.Align.START;
var scrolled_window = new Gtk.ScrolledWindow();
scrolled_window.hscrollbar_policy = Gtk.PolicyType.NEVER;
scrolled_window.hexpand = true;
scrolled_window.vexpand = true;
scrolled_window.add_css_class("card-container");
scrolled_window.set_child(card_container);
var overlay = new Gtk.Overlay();
overlay.hexpand = overlay.vexpand = true;
overlay.set_child(scrolled_window);
this.append(overlay);
refreshing_indicator = new RefreshingIndicator(overlay);
this.map.connect(refresh);
} }
private async void refresh() { private async void refresh() {
refreshing_indicator.show(); if (!client.connected)
return;
try { try {
var activities = yield client.list_activities(); var activities = yield client.list_activities();
card_container.remove_all(); update_cards(activities);
foreach (var activity in activities) {
var card = new ActivityCard(activity);
card.session_logged.connect(log_session);
card_container.append(card);
}
} catch (ClientError e) { } catch (ClientError e) {
stderr.printf("Error refreshing activities: %s\n", stderr.printf("Error refreshing activities: %s\n",
e.message); e.message);
} }
refreshing_indicator.hide();
} }
private async void log_session(string subject, ActivityType type, private async void log_session(string subject, ActivityType type,
@@ -65,43 +37,78 @@ namespace StudySystemClient {
stderr.printf("Error logging session: %s\n", e.message); stderr.printf("Error logging session: %s\n", e.message);
} }
} }
private void handle_pending_sort() {
if (pending_sort) {
container.sort(compare_cards);
pending_sort = false;
}
} }
private class RefreshingIndicator { private void update_cards(Array<Activity> activities) {
private Gtk.Overlay overlay; update_existing_cards(activities);
private Gtk.Frame frame; for (uint i = 0; i < activities.length; ++i)
create_card(activities.index(i));
public RefreshingIndicator(Gtk.Overlay overlay) { if (log_in_progress())
this.overlay = overlay; pending_sort = true;
else
var label = new Gtk.Label("Refreshing"); container.sort(compare_cards);
label.halign = Gtk.Align.START;
var spinner = new Gtk.Spinner();
spinner.start();
var content = new Gtk.Box(Gtk.Orientation.HORIZONTAL, 6);
content.margin_top = content.margin_bottom
= content.margin_start = content.margin_end = 12;
content.append(label);
content.append(spinner);
frame = new Gtk.Frame(null);
frame.halign = Gtk.Align.CENTER;
frame.valign = Gtk.Align.START;
frame.add_css_class("osd");
frame.add_css_class("popdown");
frame.set_child(content);
overlay.add_overlay(frame);
} }
public void show() { private void update_existing_cards(Array<Activity> activities) {
frame.add_css_class("visible"); var to_remove = new List<ActivityCard>();
foreach (var card in container) {
if (!update_existing_card(card, activities))
to_remove.append(card);
}
foreach (var card in to_remove)
container.remove(card);
} }
public void hide() { private bool update_existing_card(ActivityCard card,
frame.remove_css_class("visible"); Array<Activity> activities) {
var activity_index = find_activity(card, activities);
if (activity_index == null)
return false;
var priority = activities.index(activity_index).priority;
card.update_priority(priority);
activities._remove_index_fast(activity_index);
return true;
}
private uint? find_activity(ActivityCard card,
Array<Activity> activities) {
for (uint i = 0; i < activities.length; ++i) {
if (activities.index(i).subject == card.activity.subject
&& activities.index(i).type == card.activity.type)
return i;
}
return null;
}
private void create_card(Activity activity) {
var card = new ActivityCard(activity);
card.session_logged.connect(log_session);
card.log_closed.connect(handle_pending_sort);
container.append(card);
}
private bool log_in_progress() {
foreach (var card in container) {
if (card.logging)
return true;
}
return false;
}
private static int compare_cards(ActivityCard card1,
ActivityCard card2) {
if (card1.activity.priority < card2.activity.priority)
return -1;
else if (card1.activity.priority > card2.activity.priority)
return 1;
else
return 0;
} }
} }
} }
+33 -6
View File
@@ -1,10 +1,19 @@
namespace StudySystemClient { namespace StudySystemClient {
public class ActivityCard : Gtk.Frame { public class ActivityCard : Card {
public signal void session_logged(string subject, ActivityType type, public signal void session_logged(string subject, ActivityType type,
int minutes); int minutes);
public signal void log_closed();
public Activity activity { get; private set; }
public bool logging { get; private set; }
private Gtk.Label priority_label;
public ActivityCard(Activity activity) { public ActivityCard(Activity activity) {
add_css_class("card"); base();
this.activity = activity;
logging = false;
var subject = new Gtk.Label(activity.subject); var subject = new Gtk.Label(activity.subject);
subject.halign = Gtk.Align.START; subject.halign = Gtk.Align.START;
@@ -14,13 +23,14 @@ namespace StudySystemClient {
type.add_css_class("activity-type"); type.add_css_class("activity-type");
var separator = new Gtk.Label("·"); var separator = new Gtk.Label("·");
separator.add_css_class("activity-priority"); separator.add_css_class("activity-priority");
var priority = new Gtk.Label("%0.2f".printf(activity.priority));
priority.add_css_class("activity-priority"); priority_label = new Gtk.Label(priority_text(activity.priority));
priority_label.add_css_class("activity-priority");
var details = new Gtk.Box(Gtk.Orientation.HORIZONTAL, 6); var details = new Gtk.Box(Gtk.Orientation.HORIZONTAL, 6);
details.append(type); details.append(type);
details.append(separator); details.append(separator);
details.append(priority); details.append(priority_label);
var text = new Gtk.Box(Gtk.Orientation.VERTICAL, 6); var text = new Gtk.Box(Gtk.Orientation.VERTICAL, 6);
text.hexpand = true; text.hexpand = true;
@@ -42,10 +52,27 @@ namespace StudySystemClient {
var log_session_popover = new LogSessionPopover(); var log_session_popover = new LogSessionPopover();
log_session_popover.set_parent(button); log_session_popover.set_parent(button);
button.clicked.connect(() => log_session_popover.popup()); log_session_popover.closed.connect(() => {
logging = false;
log_closed();
});
log_session_popover.session_logged.connect((minutes) => { log_session_popover.session_logged.connect((minutes) => {
session_logged(activity.subject, activity.type, minutes); session_logged(activity.subject, activity.type, minutes);
}); });
button.clicked.connect(() => {
logging = true;
log_session_popover.popup();
});
}
public void update_priority(double new_priority) {
activity = { activity.subject, activity.type, new_priority };
priority_label.set_text(priority_text(new_priority));
}
private static string priority_text(double priority) {
return "%0.2f".printf(priority);
} }
} }
+28
View File
@@ -0,0 +1,28 @@
namespace StudySystemClient {
public class Card : Gtk.Frame {
public Card() {
hexpand = true;
add_css_class("card");
}
}
public class CardArea<T> : Gtk.Box {
protected IterableBox<T> container;
public CardArea() {
hexpand = vexpand = true;
margin_top = margin_bottom = margin_start = margin_end = 0;
container = new IterableBox<T>(Gtk.Orientation.VERTICAL, 6);
container.valign = Gtk.Align.START;
var scrolled_window = new Gtk.ScrolledWindow();
scrolled_window.hscrollbar_policy = Gtk.PolicyType.NEVER;
scrolled_window.hexpand = scrolled_window.vexpand = true;
scrolled_window.add_css_class("card-container");
scrolled_window.set_child(container);
append(scrolled_window);
}
}
}
+7 -1
View File
@@ -5,10 +5,16 @@ namespace StudySystemClient {
} }
public class Client { public class Client {
public signal void connection_status(bool connected);
public bool connected { get { return connection.connected; } }
private Connection connection; private Connection connection;
public Client(string cert_dir) throws Error { public Client(string cert_dir) throws Error {
connection = new Connection(cert_dir); connection = new Connection(cert_dir, (connected) => {
connection_status(connected);
});
} }
public async void ping() throws ClientError { public async void ping() throws ClientError {
+27 -17
View File
@@ -1,17 +1,27 @@
namespace StudySystemClient { namespace StudySystemClient {
public class Connection { public class Connection {
public delegate void StatusCallback(bool connected);
public bool connected { get; private set; }
private StatusCallback status_callback;
private SessionManager session_manager; private SessionManager session_manager;
private TransactionManager transaction_manager; private TransactionManager transaction_manager;
private Worker worker; private Worker worker;
public Connection(string cert_dir) throws Error { public Connection(string cert_dir,
owned StatusCallback status_callback)
throws Error {
var loopback = new InetAddress.loopback(SocketFamily.IPV6); var loopback = new InetAddress.loopback(SocketFamily.IPV6);
var session_factory var session_factory
= new SessionFactory(loopback, 12888, cert_dir); = new SessionFactory(loopback, 12888, cert_dir);
this.status_callback = (owned) status_callback;
session_manager = new SessionManager( session_manager = new SessionManager(
session_factory, (msg) => receive(msg)); session_factory, (msg) => receive(msg),
(connected) => update_status(connected));
transaction_manager = new TransactionManager(); transaction_manager = new TransactionManager();
worker = new Worker(session_manager); worker = new Worker(session_manager);
connected = true;
} }
public async Response.Body? send(Request.Body body) { public async Response.Body? send(Request.Body body) {
@@ -35,6 +45,16 @@ namespace StudySystemClient {
return false; return false;
}, GLib.Priority.DEFAULT_IDLE); }, GLib.Priority.DEFAULT_IDLE);
} }
private void update_status(bool connected) {
Idle.add(() => {
if (connected != this.connected) {
this.connected = connected;
status_callback(connected);
}
return false;
}, GLib.Priority.DEFAULT_IDLE);
}
} }
private class Continuation { private class Continuation {
@@ -84,29 +104,19 @@ namespace StudySystemClient {
} }
} }
private class Worker { private class Worker : Periodic {
private uint TASK_PERIOD_MS = 10; private const uint TASK_PERIOD_MS = 10;
private SessionManager session_manager; private SessionManager session_manager;
private bool exit;
private Thread<void> thread;
public Worker(SessionManager session_manager) { public Worker(SessionManager session_manager) {
base(TASK_PERIOD_MS);
this.session_manager = session_manager; this.session_manager = session_manager;
exit = false; start();
thread = new Thread<void>("connection_worker", body);
} }
~Worker() { protected override void task() {
exit = true;
thread.join();
}
private void body() {
while (!exit) {
session_manager.task(); session_manager.task();
Thread.usleep(1000 * TASK_PERIOD_MS);
}
} }
} }
} }
+52
View File
@@ -0,0 +1,52 @@
namespace StudySystemClient {
public class IterableBox<T> : Gtk.Box {
private List<T> elements;
public IterableBox(Gtk.Orientation orientation, int spacing) {
this.orientation = orientation;
this.spacing = spacing;
elements = new List<T>();
}
public new void append(T element) {
elements.append(element);
base.append(element as Gtk.Widget);
}
public new void remove(T element) {
elements.remove(element);
base.remove(element as Gtk.Widget);
}
public Iterator<Type, T> iterator() {
return new Iterator<Type, T>(elements);
}
public void sort(CompareDataFunc<T> comparison) {
elements.sort_with_data(comparison);
foreach (var element in elements)
reorder_child_after(element as Gtk.Widget, null);
}
public class Iterator<Type, T> {
private unowned List<T> head;
private unowned T value;
public Iterator(List<T> elements) {
head = elements;
}
public bool next() {
if (head.is_empty())
return false;
value = head.data;
head = head.next;
return true;
}
public unowned T get() {
return value;
}
}
}
}
+31 -1
View File
@@ -12,8 +12,38 @@ namespace StudySystemClient {
header_bar.title_widget = title; header_bar.title_widget = title;
set_titlebar(header_bar); set_titlebar(header_bar);
var connection_indicator = new ConnectionIndicator(client);
var activities_view = new ActivitiesView(client); var activities_view = new ActivitiesView(client);
set_child(activities_view);
var content = new Gtk.Box(Gtk.Orientation.VERTICAL, 0);
content.append(connection_indicator);
content.append(activities_view);
set_child(content);
}
}
private class ConnectionIndicator : Gtk.Box {
public ConnectionIndicator(Client client) {
var icon
= new Gtk.Image.from_icon_name("network-offline-symbolic");
var label = new Gtk.Label("Disconnected");
var content = new Gtk.Box(Gtk.Orientation.HORIZONTAL, 8);
content.margin_top = content.margin_bottom
= content.margin_start = content.margin_end = 12;
content.hexpand = true;
content.halign = Gtk.Align.CENTER;
content.append(icon);
content.append(label);
var revealer = new Gtk.Revealer();
revealer.set_child(content);
append(revealer);
client.connection_status.connect((connected) => {
revealer.reveal_child = !connected;
});
} }
} }
} }
+4
View File
@@ -13,10 +13,14 @@ lib = library(
'activities_view.vala', 'activities_view.vala',
'activity.vala', 'activity.vala',
'activity_card.vala', 'activity_card.vala',
'card.vala',
'client.vala', 'client.vala',
'connection.vala', 'connection.vala',
'der.vala', 'der.vala',
'iterable_box.vala',
'main_window.vala', 'main_window.vala',
'periodic.vala',
'refresher.vala',
'request.vala', 'request.vala',
'response.vala', 'response.vala',
'session_manager.vala', 'session_manager.vala',
+35
View File
@@ -0,0 +1,35 @@
namespace StudySystemClient {
public abstract class Periodic {
private uint period_ms;
private bool exit;
private Thread<void> thread;
protected Periodic(uint period_ms) {
this.period_ms = period_ms;
exit = true;
}
~Periodic() {
if (!exit) {
exit = true;
thread.join();
}
}
protected abstract void task();
public void start() {
if (exit) {
exit = false;
thread = new Thread<void>("Periodic task", body);
}
}
private void body() {
while (!exit) {
task();
Thread.usleep(1000 * period_ms);
}
}
}
}
+23
View File
@@ -0,0 +1,23 @@
namespace StudySystemClient {
public interface IRefreshable {
public abstract async void refresh();
}
private class Refresher : Periodic {
private weak IRefreshable target;
public Refresher(IRefreshable target, uint period_ms) {
base(period_ms);
this.target = target;
}
protected override void task() {
if (target != null) {
Idle.add(() => {
target.refresh.begin();
return false;
});
}
}
}
}
+11 -4
View File
@@ -3,19 +3,23 @@ namespace StudySystemClient {
public delegate void ReceiveCallback(owned uint8[] msg); public delegate void ReceiveCallback(owned uint8[] msg);
private const uint INIT_RECONNECT_WAIT_MS = 500; private const uint INIT_RECONNECT_WAIT_MS = 500;
private const uint MAX_RECONNECT_WAIT_MS = 60000; private const uint MAX_RECONNECT_WAIT_MS = 30000;
private const double RECONNECT_BACKOFF = 1.6; private const double RECONNECT_BACKOFF = 1.6;
private SessionFactory session_factory; private SessionFactory session_factory;
private ReceiveCallback receive_callback; private ReceiveCallback receive_callback;
private Connection.StatusCallback status_callback;
private Session? session; private Session? session;
private AsyncQueue<OutgoingMessage> queue; private AsyncQueue<OutgoingMessage> queue;
private uint reconnect_wait_ms; private uint reconnect_wait_ms;
public SessionManager(SessionFactory session_factory, public SessionManager(
owned ReceiveCallback receive_callback) { SessionFactory session_factory,
owned ReceiveCallback receive_callback,
owned Connection.StatusCallback status_callback) {
this.session_factory = session_factory; this.session_factory = session_factory;
this.receive_callback = (owned) receive_callback; this.receive_callback = (owned) receive_callback;
this.status_callback = (owned) status_callback;
this.session = null; this.session = null;
queue = new AsyncQueue<OutgoingMessage>(); queue = new AsyncQueue<OutgoingMessage>();
reconnect_wait_ms = INIT_RECONNECT_WAIT_MS; reconnect_wait_ms = INIT_RECONNECT_WAIT_MS;
@@ -40,6 +44,7 @@ namespace StudySystemClient {
if (msg.should_retry()) if (msg.should_retry())
queue.push(msg); queue.push(msg);
session = null; session = null;
status_callback(false);
} }
private void try_start_session() { private void try_start_session() {
@@ -47,7 +52,9 @@ namespace StudySystemClient {
session = session_factory.start_session(); session = session_factory.start_session();
session.received.connect((msg) => receive_callback(msg)); session.received.connect((msg) => receive_callback(msg));
reconnect_wait_ms = INIT_RECONNECT_WAIT_MS; reconnect_wait_ms = INIT_RECONNECT_WAIT_MS;
status_callback(true);
} catch (Error _) { } catch (Error _) {
status_callback(false);
Thread.usleep(1000 * reconnect_wait_ms); Thread.usleep(1000 * reconnect_wait_ms);
update_reconnect_wait(); update_reconnect_wait();
} }
@@ -65,7 +72,7 @@ namespace StudySystemClient {
public class SessionFactory { public class SessionFactory {
private const string CA_FILENAME = "/ca.pem"; private const string CA_FILENAME = "/ca.pem";
private const string CERT_FILENAME = "/client.pem"; private const string CERT_FILENAME = "/client.pem";
private const uint TIMEOUT_S = 1; private const uint TIMEOUT_S = 2;
private InetSocketAddress host; private InetSocketAddress host;
private TlsCertificate cert; private TlsCertificate cert;
+4 -21
View File
@@ -1,11 +1,11 @@
.card-container { .card-container {
background-color: mix(@theme_base_color, @theme_bg_color, 0.7); background-color: color-mix(in oklab, @theme_base_color,
@theme_bg_color 60%);
padding: 6px; padding: 6px;
} }
.card { .card {
border: 1px solid alpha(@theme_fg_color, 0.2); box-shadow: 0 1px 2px rgba(0, 0, 0, 0.15);
box-shadow: 0 1px 2px alpha(black, 0.15);
background-color: @theme_bg_color; background-color: @theme_bg_color;
padding: 12px 16px; padding: 12px 16px;
} }
@@ -15,7 +15,7 @@
} }
.activity-priority { .activity-priority {
color: alpha(@theme_fg_color, 0.6); color: color-mix(in oklab, @theme_fg_color 60%, @theme_bg_color);
} }
/* /*
@@ -27,20 +27,3 @@
margin-top: -2px; margin-top: -2px;
margin-left: 3px; margin-left: 3px;
} }
/*
* Couldn't find a built-in way to get the common OSD overlay style of
* rounding the bottom corners but not the top ones, so doing this
* myself with this CSS rule.
*/
overlay > frame.osd {
border-radius: 0 0 8px 8px;
}
.popdown {
transition: transform 200ms ease-in-out;
transform: translateY(-100px);
}
.popdown.visible {
transform: translateY(0);
}
+9 -11
View File
@@ -12,6 +12,7 @@ start_link(Socket) ->
gen_server:start_link(?MODULE, Socket, []). gen_server:start_link(?MODULE, Socket, []).
init(Socket) -> init(Socket) ->
ok = ssl:controlling_process(Socket, self()),
ok = ssl:setopts(Socket, [{active, true}]), ok = ssl:setopts(Socket, [{active, true}]),
process_flag(trap_exit, true), process_flag(trap_exit, true),
{ok, #{socket => Socket, transactions => #{}}}. {ok, #{socket => Socket, transactions => #{}}}.
@@ -38,9 +39,7 @@ handle_info({'EXIT', Pid, Reason},
TransactionId = maps:get(Pid, Transactions), TransactionId = maps:get(Pid, Transactions),
Response = case Reason of Response = case Reason of
{response, Value} -> Value; {response, Value} -> Value;
_ -> _ -> {error, serverError}
io:format("Error handling request: ~p~n", [Reason]),
{error, serverError}
end, end,
send(Socket, TransactionId, Response), send(Socket, TransactionId, Response),
{noreply, State#{transactions := maps:remove(Pid, Transactions)}}; {noreply, State#{transactions := maps:remove(Pid, Transactions)}};
@@ -59,25 +58,24 @@ code_change(_OldVsn, State, _Extra) ->
handle_request(Request) -> handle_request(Request) ->
timer:kill_after(500), timer:kill_after(500),
exit(map_request(Request)). exit({response, map_request(Request)}).
map_request({ping, 'NULL'}) -> map_request({ping, 'NULL'}) ->
{response, {ack, 'NULL'}}; {ack, 'NULL'};
map_request({listActivities, 'NULL'}) -> map_request({listActivities, 'NULL'}) ->
{activities, Activities} = subject_router:get_activities(), {activities, Activities} = subject_router:get_activities(),
SortedActivities = lists:reverse(lists:keysort(3, Activities)), {activities,
{response, {activities,
[{'Activity', Subject, Type, round(Priority * 100)} [{'Activity', Subject, Type, round(Priority * 100)}
|| {Subject, Type, Priority} <- SortedActivities]}}; || {Subject, Type, Priority} <- Activities]};
map_request({logSession, {'Session', Subject, Type, Timestamp, Minutes}}) -> map_request({logSession, {'Session', Subject, Type, Timestamp, Minutes}}) ->
Session = {unicode:characters_to_list(Subject), Session = {unicode:characters_to_list(Subject),
Type, Timestamp, Minutes}, Type, Timestamp, Minutes},
case subject_router:log_session(Session) of case subject_router:log_session(Session) of
ok -> {response, {ack, 'NULL'}}; ok -> {ack, 'NULL'};
{error, _Error} -> {response, {error, invalidArguments}} {error, _Error} -> {error, invalidArguments}
end; end;
map_request(_) -> map_request(_) ->
{response, {error, invalidArguments}}. {error, invalidArguments}.
send(Socket, TransactionId, Response) -> send(Socket, TransactionId, Response) ->
{ok, Encoded} = 'StudySystemProtocol':encode( {ok, Encoded} = 'StudySystemProtocol':encode(
+1 -2
View File
@@ -41,8 +41,7 @@ handle_info(accept, State = #state{socket = Socket}) ->
handle_info({handshake, TlsSocket}, State) -> handle_info({handshake, TlsSocket}, State) ->
case ssl:handshake(TlsSocket, 5000) of case ssl:handshake(TlsSocket, 5000) of
{ok, ClientSocket} -> {ok, ClientSocket} ->
{ok, Pid} = session_sup:start_session(ClientSocket), {ok, _Pid} = session_sup:start_session(ClientSocket);
ok = ssl:controlling_process(ClientSocket, Pid);
{error, _Reason} -> {error, _Reason} ->
ok ok
end, end,