gdharness 0.5.8 → 0.5.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/build/cli.js +240 -82
- package/build/godot/addons/gdharness_editor/bridge_client.gd +136 -1
- package/build/index.js +217 -74
- package/package.json +1 -1
|
@@ -11,11 +11,34 @@ signal tool_requested(request_id: String, tool_name: String, args: Dictionary)
|
|
|
11
11
|
const DEFAULT_URL: String = "ws://127.0.0.1:6505/godot"
|
|
12
12
|
## Written beside the addon by the install, so it names the version this copy came from.
|
|
13
13
|
const VERSION_MARKER: String = "res://addons/gdharness_editor/.gdharness-version"
|
|
14
|
+
## Written by the server that serves this project, saying where its bridge actually is. Kept in
|
|
15
|
+
## step with `announcementPath` in src/bridge-announce.ts.
|
|
16
|
+
const ANNOUNCEMENT: String = "res://.godot/gdharness-bridge.json"
|
|
17
|
+
const ANNOUNCE_PROTOCOL: int = 1
|
|
14
18
|
const RECONNECT_DELAY: float = 3.0
|
|
15
19
|
const MAX_RECONNECT_DELAY: float = 30.0
|
|
16
20
|
|
|
21
|
+
## How long one attempt is given before the address is called a bad one.
|
|
22
|
+
##
|
|
23
|
+
## A socket pointed at a port nothing holds gives up by itself, in thirty seconds on Windows. One
|
|
24
|
+
## pointed at something that accepts and then never speaks WebSocket does not give up at all:
|
|
25
|
+
## measured here, still connecting after forty-five seconds, because the peer has no handshake
|
|
26
|
+
## timeout of its own in 4.7. A leftover announcement can be either, and a port that has been
|
|
27
|
+
## reused by some other program is the second.
|
|
28
|
+
const CONNECT_TIMEOUT: float = 10.0
|
|
29
|
+
|
|
30
|
+
## How often the announcement is read again while connected, so a newer server is moved to
|
|
31
|
+
## rather than waited for. A harness reconnect leaves the server it replaced running and holding
|
|
32
|
+
## the old port, and an editor with no reason to look elsewhere stayed on it for the session.
|
|
33
|
+
const FOLLOW_INTERVAL: float = 5.0
|
|
34
|
+
|
|
17
35
|
var socket: WebSocketPeer = WebSocketPeer.new()
|
|
18
36
|
var server_url: String = DEFAULT_URL
|
|
37
|
+
|
|
38
|
+
## What this copy was when it loaded, which is what the editor is running until it is restarted.
|
|
39
|
+
## See [method _loaded_version] for why it is held rather than read when it is wanted.
|
|
40
|
+
var version_at_load: String = ""
|
|
41
|
+
|
|
19
42
|
var _is_connected: bool = false
|
|
20
43
|
var _reconnect_timer: Timer
|
|
21
44
|
var _current_reconnect_delay: float = RECONNECT_DELAY
|
|
@@ -24,9 +47,21 @@ var _should_reconnect: bool = false
|
|
|
24
47
|
var _project_path: String
|
|
25
48
|
var _initialized: bool = false
|
|
26
49
|
|
|
50
|
+
## Whether the address came from a caller rather than from the project, in which case it is not
|
|
51
|
+
## this node's to change.
|
|
52
|
+
var _named_by_caller: bool = false
|
|
53
|
+
var _since_looked: float = 0.0
|
|
54
|
+
|
|
55
|
+
## The announced address that did not answer, so the fallback gets a turn. Cleared the moment
|
|
56
|
+
## anything connects or the project names a different one.
|
|
57
|
+
var _refused_url: String = ""
|
|
58
|
+
var _tried_announced: bool = false
|
|
59
|
+
var _connecting_for: float = 0.0
|
|
60
|
+
|
|
27
61
|
|
|
28
62
|
func _ready() -> void:
|
|
29
63
|
_project_path = ProjectSettings.globalize_path("res://")
|
|
64
|
+
version_at_load = _loaded_version()
|
|
30
65
|
|
|
31
66
|
_reconnect_timer = Timer.new()
|
|
32
67
|
_reconnect_timer.one_shot = true
|
|
@@ -48,6 +83,8 @@ func _process(_delta: float) -> void:
|
|
|
48
83
|
# A connection refused never opened, so it reaches CLOSED without passing through
|
|
49
84
|
# _handle_disconnect and nothing would ask again. An editor opened before the server
|
|
50
85
|
# is the ordinary way that happens, and it then sat there for the rest of the day.
|
|
86
|
+
if _tried_announced:
|
|
87
|
+
_refused_url = server_url
|
|
51
88
|
_schedule_reconnect()
|
|
52
89
|
return
|
|
53
90
|
|
|
@@ -57,11 +94,17 @@ func _process(_delta: float) -> void:
|
|
|
57
94
|
WebSocketPeer.STATE_OPEN:
|
|
58
95
|
if not _is_connected:
|
|
59
96
|
_handle_connect()
|
|
97
|
+
_follow_whoever_is_newest(_delta)
|
|
60
98
|
|
|
61
99
|
while socket.get_available_packet_count() > 0:
|
|
62
100
|
var packet: PackedByteArray = socket.get_packet()
|
|
63
101
|
_handle_message(packet.get_string_from_utf8())
|
|
64
102
|
|
|
103
|
+
WebSocketPeer.STATE_CONNECTING:
|
|
104
|
+
_connecting_for += _delta
|
|
105
|
+
if _connecting_for >= CONNECT_TIMEOUT:
|
|
106
|
+
_give_up_on_this_address()
|
|
107
|
+
|
|
65
108
|
WebSocketPeer.STATE_CLOSING:
|
|
66
109
|
pass
|
|
67
110
|
|
|
@@ -70,7 +113,38 @@ func _process(_delta: float) -> void:
|
|
|
70
113
|
_handle_disconnect()
|
|
71
114
|
|
|
72
115
|
|
|
116
|
+
## Moves to the server the project now names, when that is not the one this is talking to.
|
|
117
|
+
##
|
|
118
|
+
## A harness reconnect leaves the server it replaced running, still holding the port it bound and
|
|
119
|
+
## still answering: the editor has no reason to notice, and stayed on a server nothing was
|
|
120
|
+
## speaking to for the rest of the session. The replacement announces where it landed, so the
|
|
121
|
+
## editor can go to it rather than anybody ending a process.
|
|
122
|
+
##
|
|
123
|
+
## Only while connected by a URL this worked out for itself. A caller that named one is holding
|
|
124
|
+
## this to that address, which is what every fixture does.
|
|
125
|
+
func _follow_whoever_is_newest(delta: float) -> void:
|
|
126
|
+
if _named_by_caller:
|
|
127
|
+
return
|
|
128
|
+
_since_looked += delta
|
|
129
|
+
if _since_looked < FOLLOW_INTERVAL:
|
|
130
|
+
return
|
|
131
|
+
_since_looked = 0.0
|
|
132
|
+
|
|
133
|
+
var announced: String = announced_url()
|
|
134
|
+
if announced == "" or announced == server_url or announced == _refused_url:
|
|
135
|
+
return
|
|
136
|
+
server_url = announced
|
|
137
|
+
# Put down before it is picked up again, because the arrival is what tells a server who this
|
|
138
|
+
# editor is: left standing, the open socket on the new address would never be greeted and the
|
|
139
|
+
# server would report no editor while holding one.
|
|
140
|
+
_is_connected = false
|
|
141
|
+
disconnected.emit()
|
|
142
|
+
_current_reconnect_delay = RECONNECT_DELAY
|
|
143
|
+
_attempt_connection()
|
|
144
|
+
|
|
145
|
+
|
|
73
146
|
func connect_to_server(url: String = "") -> void:
|
|
147
|
+
_named_by_caller = url != ""
|
|
74
148
|
server_url = _resolve_server_url(url)
|
|
75
149
|
_should_reconnect = true
|
|
76
150
|
_current_reconnect_delay = RECONNECT_DELAY
|
|
@@ -81,6 +155,12 @@ func _resolve_server_url(explicit_url: String) -> String:
|
|
|
81
155
|
if explicit_url != "":
|
|
82
156
|
return explicit_url
|
|
83
157
|
|
|
158
|
+
var announced: String = announced_url()
|
|
159
|
+
if announced != "" and announced != _refused_url:
|
|
160
|
+
_tried_announced = true
|
|
161
|
+
return announced
|
|
162
|
+
_tried_announced = false
|
|
163
|
+
|
|
84
164
|
# The same variable the server reads, so the two agree on the port by construction.
|
|
85
165
|
var raw: String = OS.get_environment("GDHARNESS_BRIDGE_PORT")
|
|
86
166
|
if raw != "":
|
|
@@ -91,6 +171,48 @@ func _resolve_server_url(explicit_url: String) -> String:
|
|
|
91
171
|
return DEFAULT_URL
|
|
92
172
|
|
|
93
173
|
|
|
174
|
+
## Stops waiting on an address that is not answering, and asks again elsewhere.
|
|
175
|
+
func _give_up_on_this_address() -> void:
|
|
176
|
+
if _tried_announced:
|
|
177
|
+
_refused_url = server_url
|
|
178
|
+
_connecting_for = 0.0
|
|
179
|
+
socket.close()
|
|
180
|
+
_schedule_reconnect()
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
## Where the server says its bridge is, or "" when nothing has said.
|
|
184
|
+
##
|
|
185
|
+
## Written by a server that knows which project it serves, inside that project, so the two sides
|
|
186
|
+
## agree by construction rather than by deriving a temporary directory the same way: they do not
|
|
187
|
+
## share an environment, and the runtime's own announcement cost a session learning that.
|
|
188
|
+
##
|
|
189
|
+
## A leftover is found out by trying it rather than by asking whether its process is still there.
|
|
190
|
+
## `OS.is_process_running` answers that only for a child of the caller on Unix, where it prints
|
|
191
|
+
## "does not exist or is not a child of the calling process" and says no about every server there
|
|
192
|
+
## is: it works on Windows and quietly disables the whole thing everywhere else. So an
|
|
193
|
+
## announcement that does not answer is set aside, the fallback gets the next turn, and a
|
|
194
|
+
## different announcement puts it back in play.
|
|
195
|
+
func announced_url() -> String:
|
|
196
|
+
if not FileAccess.file_exists(ANNOUNCEMENT):
|
|
197
|
+
return ""
|
|
198
|
+
var file: FileAccess = FileAccess.open(ANNOUNCEMENT, FileAccess.READ)
|
|
199
|
+
if file == null:
|
|
200
|
+
return ""
|
|
201
|
+
var said: Variant = JSON.parse_string(file.get_as_text())
|
|
202
|
+
file.close()
|
|
203
|
+
if not said is Dictionary:
|
|
204
|
+
return ""
|
|
205
|
+
|
|
206
|
+
var announcement: Dictionary = said
|
|
207
|
+
if int(announcement.get("protocol", 0)) != ANNOUNCE_PROTOCOL:
|
|
208
|
+
return ""
|
|
209
|
+
var port: int = int(announcement.get("port", 0))
|
|
210
|
+
if port < 1 or port > 65535:
|
|
211
|
+
return ""
|
|
212
|
+
var host: String = str(announcement.get("host", "127.0.0.1"))
|
|
213
|
+
return "ws://%s:%d/godot" % [host, port]
|
|
214
|
+
|
|
215
|
+
|
|
94
216
|
func disconnect_from_server() -> void:
|
|
95
217
|
_should_reconnect = false
|
|
96
218
|
if _reconnect_timer:
|
|
@@ -104,6 +226,7 @@ func _attempt_connection() -> void:
|
|
|
104
226
|
if socket.get_ready_state() != WebSocketPeer.STATE_CLOSED:
|
|
105
227
|
socket.close()
|
|
106
228
|
|
|
229
|
+
_connecting_for = 0.0
|
|
107
230
|
var err: Error = socket.connect_to_url(server_url)
|
|
108
231
|
if err != OK:
|
|
109
232
|
push_error("[gdharness] Failed to connect to %s: %s" % [server_url, error_string(err)])
|
|
@@ -113,6 +236,7 @@ func _attempt_connection() -> void:
|
|
|
113
236
|
func _handle_connect() -> void:
|
|
114
237
|
_is_connected = true
|
|
115
238
|
_current_reconnect_delay = RECONNECT_DELAY
|
|
239
|
+
_refused_url = ""
|
|
116
240
|
|
|
117
241
|
# The version reported is the one this editor loaded at startup, not the one on disk: an
|
|
118
242
|
# upgrade replaces the files under a running editor, which goes on serving the old code until
|
|
@@ -123,7 +247,7 @@ func _handle_connect() -> void:
|
|
|
123
247
|
{
|
|
124
248
|
"type": "godot_ready",
|
|
125
249
|
"project_path": _project_path,
|
|
126
|
-
"addon_version":
|
|
250
|
+
"addon_version": version_at_load,
|
|
127
251
|
"editor_pid": OS.get_process_id()
|
|
128
252
|
}
|
|
129
253
|
)
|
|
@@ -132,6 +256,13 @@ func _handle_connect() -> void:
|
|
|
132
256
|
|
|
133
257
|
|
|
134
258
|
## The version marker beside this addon, or "" when the copy was not installed by gdharness.
|
|
259
|
+
##
|
|
260
|
+
## Read once, when this copy loads, and never again: an upgrade replaces the files under a
|
|
261
|
+
## running editor and rewrites the marker with them, so reading it at connect time answers with
|
|
262
|
+
## the version on disk rather than the one in memory. That is the wrong answer at the one moment
|
|
263
|
+
## it matters. An upgrade ends with the harness reconnecting, the editor reconnecting behind it,
|
|
264
|
+
## and `addonIsStale` reporting false over an editor still running the old code, which is exactly
|
|
265
|
+
## what it exists to catch.
|
|
135
266
|
func _loaded_version() -> String:
|
|
136
267
|
if not FileAccess.file_exists(VERSION_MARKER):
|
|
137
268
|
return ""
|
|
@@ -159,6 +290,10 @@ func _schedule_reconnect() -> void:
|
|
|
159
290
|
|
|
160
291
|
|
|
161
292
|
func _on_reconnect_timer() -> void:
|
|
293
|
+
# Asked again rather than remembered: the reason a connection dropped is often that its server
|
|
294
|
+
# did, and the one that replaced it has said where it is since.
|
|
295
|
+
if not _named_by_caller:
|
|
296
|
+
server_url = _resolve_server_url("")
|
|
162
297
|
_attempt_connection()
|
|
163
298
|
|
|
164
299
|
|