summaryrefslogtreecommitdiff
path: root/modules/mono/editor/GodotTools/GodotTools.IdeConnection/GodotIdeConnection.cs
blob: 6441be8d6e53dc60ffe6522ca251dc08cd72ddf6 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
using System;
using System.Diagnostics;
using System.IO;
using System.Net.Sockets;
using System.Text;

namespace GodotTools.IdeConnection
{
    public abstract class GodotIdeConnection : IDisposable
    {
        protected const string Version = "1.0";

        protected static readonly string ClientHandshake = $"Godot Ide Client Version {Version}";
        protected static readonly string ServerHandshake = $"Godot Ide Server Version {Version}";

        private const int ClientWriteTimeout = 8000;
        private readonly TcpClient tcpClient;

        private TextReader clientReader;
        private TextWriter clientWriter;

        private readonly object writeLock = new object();

        private readonly Func<Message, bool> messageHandler;

        public event Action Connected;

        private ILogger logger;

        public ILogger Logger
        {
            get => logger ?? (logger = new ConsoleLogger());
            set => logger = value;
        }

        public bool IsDisposed { get; private set; } = false;

        public bool IsConnected => tcpClient.Client != null && tcpClient.Client.Connected;

        protected GodotIdeConnection(TcpClient tcpClient, Func<Message, bool> messageHandler)
        {
            this.tcpClient = tcpClient;
            this.messageHandler = messageHandler;
        }

        public void Start()
        {
            try
            {
                if (!StartConnection())
                    return;

                string messageLine;
                while ((messageLine = ReadLine()) != null)
                {
                    if (!MessageParser.TryParse(messageLine, out Message msg))
                    {
                        Logger.LogError($"Received message with invalid format: {messageLine}");
                        continue;
                    }

                    Logger.LogDebug($"Received message: {msg}");

                    if (msg.Id == "close")
                    {
                        Logger.LogInfo("Closing connection");
                        return;
                    }

                    try
                    {
                        try
                        {
                            Debug.Assert(messageHandler != null);

                            if (!messageHandler(msg))
                                Logger.LogError($"Received unknown message: {msg}");
                        }
                        catch (Exception e)
                        {
                            Logger.LogError($"Message handler for '{msg}' failed with exception", e);
                        }
                    }
                    catch (Exception e)
                    {
                        Logger.LogError($"Exception thrown from message handler. Message: {msg}", e);
                    }
                }
            }
            catch (Exception e)
            {
                Logger.LogError($"Unhandled exception in the Godot Ide Connection thread", e);
            }
            finally
            {
                Dispose();
            }
        }

        private bool StartConnection()
        {
            NetworkStream clientStream = tcpClient.GetStream();

            clientReader = new StreamReader(clientStream, Encoding.UTF8);

            lock (writeLock)
                clientWriter = new StreamWriter(clientStream, Encoding.UTF8);

            clientStream.WriteTimeout = ClientWriteTimeout;

            if (!WriteHandshake())
            {
                Logger.LogError("Could not write handshake");
                return false;
            }

            if (!IsValidResponseHandshake(ReadLine()))
            {
                Logger.LogError("Received invalid handshake");
                return false;
            }

            Connected?.Invoke();

            Logger.LogInfo("Godot Ide connection started");

            return true;
        }

        private string ReadLine()
        {
            try
            {
                return clientReader?.ReadLine();
            }
            catch (Exception e)
            {
                if (IsDisposed)
                {
                    var se = e as SocketException ?? e.InnerException as SocketException;
                    if (se != null && se.SocketErrorCode == SocketError.Interrupted)
                        return null;
                }

                throw;
            }
        }

        public bool WriteMessage(Message message)
        {
            Logger.LogDebug($"Sending message {message}");

            var messageComposer = new MessageComposer();

            messageComposer.AddArgument(message.Id);
            foreach (string argument in message.Arguments)
                messageComposer.AddArgument(argument);

            return WriteLine(messageComposer.ToString());
        }

        protected bool WriteLine(string text)
        {
            if (clientWriter == null || IsDisposed || !IsConnected)
                return false;

            lock (writeLock)
            {
                try
                {
                    clientWriter.WriteLine(text);
                    clientWriter.Flush();
                }
                catch (Exception e)
                {
                    if (!IsDisposed)
                    {
                        var se = e as SocketException ?? e.InnerException as SocketException;
                        if (se != null && se.SocketErrorCode == SocketError.Shutdown)
                            Logger.LogInfo("Client disconnected ungracefully");
                        else
                            Logger.LogError("Exception thrown when trying to write to client", e);

                        Dispose();
                    }
                }
            }

            return true;
        }

        protected abstract bool WriteHandshake();
        protected abstract bool IsValidResponseHandshake(string handshakeLine);

        public void Dispose()
        {
            if (IsDisposed)
                return;

            IsDisposed = true;

            clientReader?.Dispose();
            clientWriter?.Dispose();
            ((IDisposable)tcpClient)?.Dispose();
        }
    }
}