Skip to content

Commit

Permalink
Merge pull request #653 from godot-rust/bugfix/hot-reload-panic
Browse files Browse the repository at this point in the history
Fix hot-reload panic on Linux
  • Loading branch information
Bromeon authored Mar 30, 2024
2 parents 1ff7f36 + 68b5115 commit 55b60fb
Show file tree
Hide file tree
Showing 24 changed files with 706 additions and 209 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ inputs:

with-llvm:
required: false
default: ''
default: 'false'
description: "Set to 'true' if LLVM should be installed"


Expand Down
4 changes: 2 additions & 2 deletions .github/other/check-example.sh
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ set -euo pipefail
# Opening in editor can take a while (import reosurces, load extensions for the first time, ...).
# Unlike EXAMPLE_TIMEOUT, this is an upper bound, after which CI job fails, so not the entire time is necessarily spent.
EXAMPLE_TIMEOUT=5
EDITOR_TIMEOUT=20
EDITOR_TIMEOUT=30 # already encountered 20s on macOS CI.

example="$1"
if [ -z "$example" ]; then
Expand All @@ -38,7 +38,7 @@ timeout "$EDITOR_TIMEOUT"s "$GODOT4_BIN" -e --headless --path "$dir" --quit || {

# Could also use `timeout -s9`, but there were some issues and this gives more control.
echo "$PRE Run example..."
$GODOT4_BIN --headless --verbose --path "$dir" 2> "$logfile" &
$GODOT4_BIN --headless --path "$dir" 2> "$logfile" &
pid=$!

# Keep open for some time (even just main menu or so).
Expand Down
9 changes: 9 additions & 0 deletions .github/workflows/full-ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -216,6 +216,7 @@ jobs:
artifact-name: macos-nightly
godot-binary: godot.macos.editor.dev.x86_64
rust-extra-args: --features godot/custom-godot
with-hot-reload: true

- name: macos-double
os: macos-12
Expand All @@ -242,6 +243,7 @@ jobs:
artifact-name: windows-nightly
godot-binary: godot.windows.editor.dev.x86_64.exe
rust-extra-args: --features godot/custom-godot
with-hot-reload: true

- name: windows-double
os: windows-latest
Expand Down Expand Up @@ -273,6 +275,7 @@ jobs:
artifact-name: linux-nightly
godot-binary: godot.linuxbsd.editor.dev.x86_64
rust-extra-args: --features codegen-full-experimental
with-hot-reload: true

# Combines now a lot of features, but should be OK. lazy-function-tables doesn't work with experimental-threads.
- name: linux-double-lazy
Expand Down Expand Up @@ -369,6 +372,12 @@ jobs:
rust-cache-key: ${{ matrix.rust-cache-key }}
with-llvm: ${{ contains(matrix.name, 'macos') && contains(matrix.rust-extra-args, 'custom-godot') }}
godot-check-header: ${{ matrix.godot-check-header }}

- name: "Build and test hot-reload"
if: ${{ matrix.with-hot-reload }}
working-directory: examples/hot-reload/godot/test
run: ./run-test.sh
shell: bash


run-examples:
Expand Down
6 changes: 6 additions & 0 deletions .github/workflows/minimal-ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,7 @@ jobs:
artifact-name: linux-nightly
godot-binary: godot.linuxbsd.editor.dev.x86_64
rust-extra-args: --features codegen-full-experimental
with-hot-reload: true

- name: linux-features
os: ubuntu-20.04
Expand Down Expand Up @@ -210,6 +211,11 @@ jobs:
with-llvm: ${{ contains(matrix.name, 'macos') && contains(matrix.rust-extra-args, 'custom-godot') }}
godot-check-header: ${{ matrix.godot-check-header }}

- name: "Build and test hot-reload"
if: ${{ matrix.with-hot-reload }}
working-directory: examples/hot-reload/godot/test
run: ./run-test.sh
shell: bash

run-examples:
runs-on: ubuntu-20.04
Expand Down
1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ members = [
# Godot integration
"itest/rust",
"examples/dodge-the-creeps/rust",
"examples/hot-reload/rust",

# utils
"godot-fmt",
Expand Down
1 change: 1 addition & 0 deletions examples/hot-reload/godot/.godot/extension_list.cfg
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
res://rust.gdextension
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
list=Array[Dictionary]([])
3 changes: 3 additions & 0 deletions examples/hot-reload/godot/MainScene.tscn
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
[gd_scene format=3 uid="uid://da7eiv1notj7j"]

[node name="Reloadable" type="Reloadable"]
14 changes: 14 additions & 0 deletions examples/hot-reload/godot/project.godot
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
; Engine configuration file.
; It's best edited using the editor UI and not directly,
; since the parameters that go here are not all obvious.
;
; Format:
; [section] ; section goes between []
; param=value ; assign values to parameters

config_version=5

[application]

config/name="hot-reload"
config/features=PackedStringArray("4.3", "Forward Plus")
16 changes: 16 additions & 0 deletions examples/hot-reload/godot/rust.gdextension
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
[configuration]
entry_symbol = "gdext_rust_init"
compatibility_minimum = 4.1
reloadable = true

[libraries]
linux.debug.x86_64 = "res://../../../target/debug/libhot_reload.so"
linux.release.x86_64 = "res://../../../target/release/libhot_reload.so"
windows.debug.x86_64 = "res://../../../target/debug/hot_reload.dll"
windows.release.x86_64 = "res://../../../target/release/hot_reload.dll"
macos.debug = "res://../../../target/debug/libhot_reload.dylib"
macos.release = "res://../../../target/release/libhot_reload.dylib"
macos.debug.arm64 = "res://../../../target/debug/libhot_reload.dylib"
macos.release.arm64 = "res://../../../target/release/libhot_reload.dylib"
web.debug.wasm32 = "res://../../../target/wasm32-unknown-emscripten/debug/hot_reload.wasm"
web.release.wasm32 = "res://../../../target/wasm32-unknown-emscripten/release/hot_reload.wasm"
6 changes: 6 additions & 0 deletions examples/hot-reload/godot/test/MainScene.tscn
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
[gd_scene load_steps=2 format=3 uid="uid://da7eiv1notj7j"]

[ext_resource type="Script" path="res://test/ReloadTest.gd" id="1_6hpr0"]

[node name="MainScene" type="Node"]
script = ExtResource("1_6hpr0")
98 changes: 98 additions & 0 deletions examples/hot-reload/godot/test/ReloadTest.gd
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
# Copyright (c) godot-rust; Bromeon and contributors.
# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at https://mozilla.org/MPL/2.0/.

# Note: This file is not part of the example, but a script used for gdext integration tests.
# You can safely ignore it.

@tool
extends Node

var udp := PacketPeerUDP.new()
var thread := Thread.new()
var extension_name: String


func _ready() -> void:
print("[GDScript] Start...")

var r = Reloadable.new()
var num = r.get_number()
r.free()

print("[GDScript] Sanity check: initial number is ", num)

var extensions = GDExtensionManager.get_loaded_extensions()
if extensions.size() == 1:
extension_name = extensions[0]
else:
fail(str("Must have 1 extension, has: ", extensions))
return

udp.bind(1337)
print("[GDScript] ReloadTest ready to receive...")

send_udp()


func send_udp():
# Attempt to bind the UDP socket to any available port for sending.
# You can specify a port number instead of 0 if you need to bind to a specific port.
var out_udp = PacketPeerUDP.new()

# Set the destination address and port for the message
if out_udp.set_dest_address("127.0.0.1", 1338) != OK:
fail("Failed to set destination address")
return

if out_udp.put_packet("ready".to_utf8_buffer()) != OK:
fail("Failed to send packet")
return

print("[GDScript] Packet sent successfully")
out_udp.close()


func _exit_tree() -> void:
print("[GDScript] ReloadTest exit.")
udp.close()


func _process(delta: float) -> void:
if udp.get_available_packet_count() == 0:
return

var packet = udp.get_packet().get_string_from_ascii()
print("[GDScript] Received UDP packet [", packet.length(), "]: ", packet)

if not _hot_reload():
return

var r = Reloadable.new()
var num = r.get_number()
r.free()

if num == 777:
print("[GDScript] Successful hot-reload! Exit...")
get_tree().quit(0)
else:
fail(str("Number was not updated correctly (is ", num, ")"))
return


func _hot_reload():
# TODO sometimes fails because .so is not found
var status = GDExtensionManager.reload_extension(extension_name)
if status != OK:
fail(str("Failed to reload extension: ", status))
return false

return true


func fail(s: String) -> void:
print("::error::[GDScript] ", s) # GitHub Action syntax
get_tree().quit(1)


47 changes: 47 additions & 0 deletions examples/hot-reload/godot/test/editor_layout.cfg
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
[docks]

dock_3_selected_tab_idx=0
dock_4_selected_tab_idx=0
dock_5_selected_tab_idx=0
dock_floating={}
dock_bottom=[]
dock_split_2=0
dock_split_3=0
dock_hsplit_1=0
dock_hsplit_2=270
dock_hsplit_3=-270
dock_hsplit_4=0
dock_filesystem_h_split_offset=240
dock_filesystem_v_split_offset=0
dock_filesystem_display_mode=0
dock_filesystem_file_sort=0
dock_filesystem_file_list_display_mode=1
dock_filesystem_selected_paths=PackedStringArray("res://MainScene.tscn")
dock_filesystem_uncollapsed_paths=PackedStringArray("Favorites", "res://")
dock_3="Scene,Import"
dock_4="FileSystem"
dock_5="Inspector,Node,History"

[EditorNode]

open_scenes=PackedStringArray("res://MainScene.tscn")
current_scene="res://MainScene.tscn"
center_split_offset=0
selected_default_debugger_tab_idx=0
selected_main_editor_idx=1

[ScriptEditor]

open_scripts=["res://test/ReloadTest.gd"]
selected_script="res://test/ReloadTest.gd"
open_help=[]
script_split_offset=70
list_split_offset=0
zoom_factor=1.0

[ShaderEditor]

open_shaders=[]
split_offset=0
selected_shader=""
text_shader_zoom_factor=1.0
84 changes: 84 additions & 0 deletions examples/hot-reload/godot/test/orchestrate.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
# Copyright (c) godot-rust; Bromeon and contributors.
# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at https://mozilla.org/MPL/2.0/.

# Python code because it runs outside the engine, and portably sending UDP in bash is cumbersome.

import select
import socket
import sys


def replace_line(file_path: str):
with open(file_path, 'r') as file:
lines = file.readlines()

replaced = 0
with open(file_path, 'w') as file:
for line in lines:
if line.strip() == 'fn get_number(&self) -> i64 { 100 }':
file.write(line.replace('100', '777'))
replaced += 1
else:
file.write(line)

if replaced == 0:
print("[Python] ERROR: Line not found in file.")
return False
else:
return True


def send_udp():
msg = bytes("reload", "utf-8")
udp = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
udp.sendto(msg, ("localhost", 1337))
return True


def receive_udp() -> bool:
timeout = 20
udp = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
udp.bind(("localhost", 1338))

ready = select.select([udp], [], [], 20)
if ready[0]:
# If data is ready, receive it (max 1024 bytes)
data, addr = udp.recvfrom(1024)
print(f"[Python] Await ready; received from {addr}: {data}")
return True
else:
# If no data arrives within the timeout, exit with code 1
print(f"[Python] ERROR: Await timeout: no packet within {timeout} seconds.")
return False


# -----------------------------------------------------------------------------------------------------------------------------------------------
# Main

if len(sys.argv) != 2:
print(f"Usage: {sys.argv[0]} [replace|notify]")
sys.exit(2)

# Get the command from the command line
command = sys.argv[1]

# Dispatch based on the command
if command == 'await':
print("[Python] Await Godot to be ready...")
ok = receive_udp()
elif command == 'replace':
print("[Python] Replace source code...")
ok = replace_line("../../rust/src/lib.rs")
elif command == 'notify':
print("[Python] Notify Godot about change...")
ok = send_udp()
else:
print("[Python] ERROR: Invalid command.")
sys.exit(2)

if not ok:
sys.exit(1)

print("[Python] Done.")
Loading

0 comments on commit 55b60fb

Please sign in to comment.