diff --git a/CHANGELOG.md b/CHANGELOG.md index cf7beaebc581..6fbb743865e6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,15 @@ +# 1.9.1 (2023-07-17) + +### Bugfixes + +#### stdlib + +- *(serialization)* Fix `Serializable` with converter parsing `null` value ([#13656](https://github.com/crystal-lang/crystal/pull/13656), thanks @straight-shoota) + +#### compiler + +- *(codegen)* Fix generated cc command for cross compile ([#13661](https://github.com/crystal-lang/crystal/pull/13661), thanks @fnordfish) + # 1.9.0 (2023-07-11) ### Breaking changes diff --git a/spec/std/json/serializable_spec.cr b/spec/std/json/serializable_spec.cr index 0cc9d662cc93..042e42ff5fd5 100644 --- a/spec/std/json/serializable_spec.cr +++ b/spec/std/json/serializable_spec.cr @@ -212,6 +212,20 @@ class JSONAttrWithTimeEpoch property value : Time end +class JSONAttrNilableWithTimeEpoch + include JSON::Serializable + + @[JSON::Field(converter: Time::EpochConverter)] + property value : Time? +end + +class JSONAttrDefaultWithTimeEpoch + include JSON::Serializable + + @[JSON::Field(converter: Time::EpochConverter)] + property value : Time = Time.unix(0) +end + class JSONAttrWithTimeEpochMillis include JSON::Serializable @@ -791,6 +805,16 @@ describe "JSON mapping" do end end + it "converter with null value (#13655)" do + JSONAttrNilableWithTimeEpoch.from_json(%({"value": null})).value.should be_nil + JSONAttrNilableWithTimeEpoch.from_json(%({"value":1459859781})).value.should eq Time.unix(1459859781) + end + + it "converter with default value" do + JSONAttrDefaultWithTimeEpoch.from_json(%({"value": null})).value.should eq Time.unix(0) + JSONAttrDefaultWithTimeEpoch.from_json(%({"value":1459859781})).value.should eq Time.unix(1459859781) + end + it "uses Time::EpochConverter" do string = %({"value":1459859781}) json = JSONAttrWithTimeEpoch.from_json(string) diff --git a/spec/std/kernel_spec.cr b/spec/std/kernel_spec.cr index efe5e81563e4..149e6385ac97 100644 --- a/spec/std/kernel_spec.cr +++ b/spec/std/kernel_spec.cr @@ -294,64 +294,3 @@ describe "hardware exception" do error.should contain("Stack overflow") end end - -private def compile_and_run_exit_handler(&block : Process -> _) - with_tempfile("source_file") do |source_file| - File.write(source_file, <<-CRYSTAL) - at_exit { print "Exiting" } - print "." - STDOUT.flush - sleep - CRYSTAL - output = nil - compile_file(source_file) do |executable_file| - error = IO::Memory.new - process = Process.new executable_file, output: :pipe, error: error - - spawn do - process.output.read_byte - block.call(process) - output = process.output.gets_to_end - end - - status = process.wait - {status, output, error.to_s} - end - end -end - -describe "default interrupt handlers", tags: %w[slow] do - # TODO: Implementation for sending console control commands on Windows. - # So long this behaviour can only be tested manually. - # - # ``` - # lib LibC - # fun GenerateConsoleCtrlEvent(dwCtrlEvent : DWORD, dwProcessGroupId : DWORD) : BOOL - # end - - # at_exit { print "Exiting"; } - # print "." - # STDOUT.flush - # LibC.GenerateConsoleCtrlEvent(LibC::CTRL_C_EVENT, 0) - # sleep - # ``` - {% unless flag?(:windows) %} - it "handler for SIGINT" do - status, output, _ = compile_and_run_exit_handler(&.signal(Signal::INT)) - output.should eq "Exiting" - status.inspect.should eq "Process::Status[130]" - end - - it "handler for SIGTERM" do - status, output, _ = compile_and_run_exit_handler(&.terminate) - output.should eq "Exiting" - status.inspect.should eq "Process::Status[143]" - end - {% end %} - - it "no handler for SIGKILL" do - status, output, _ = compile_and_run_exit_handler(&.terminate(graceful: false)) - output.should eq "" - status.inspect.should eq {{ flag?(:unix) ? "Process::Status[Signal::KILL]" : "Process::Status[1]" }} - end -end diff --git a/spec/std/yaml/serializable_spec.cr b/spec/std/yaml/serializable_spec.cr index 7a098283e808..a8a7e3558b90 100644 --- a/spec/std/yaml/serializable_spec.cr +++ b/spec/std/yaml/serializable_spec.cr @@ -221,6 +221,20 @@ class YAMLAttrWithTimeEpoch property value : Time end +class YAMLAttrNilableWithTimeEpoch + include YAML::Serializable + + @[YAML::Field(converter: Time::EpochConverter)] + property value : Time? +end + +class YAMLAttrDefaultWithTimeEpoch + include YAML::Serializable + + @[YAML::Field(converter: Time::EpochConverter)] + property value : Time = Time.unix(0) +end + class YAMLAttrWithTimeEpochMillis include YAML::Serializable @@ -871,6 +885,16 @@ describe "YAML::Serializable" do end end + it "converter with null value (#13655)" do + YAMLAttrNilableWithTimeEpoch.from_yaml(%({"value": null})).value.should be_nil + YAMLAttrNilableWithTimeEpoch.from_yaml(%({"value":1459859781})).value.should eq Time.unix(1459859781) + end + + it "converter with default value" do + YAMLAttrDefaultWithTimeEpoch.from_yaml(%({"value": null})).value.should eq Time.unix(0) + YAMLAttrDefaultWithTimeEpoch.from_yaml(%({"value":1459859781})).value.should eq Time.unix(1459859781) + end + it "uses Time::EpochConverter" do string = %({"value":1459859781}) yaml = YAMLAttrWithTimeEpoch.from_yaml(string) diff --git a/src/VERSION b/src/VERSION index f8e233b27332..9ab8337f3962 100644 --- a/src/VERSION +++ b/src/VERSION @@ -1 +1 @@ -1.9.0 +1.9.1 diff --git a/src/compiler/crystal/compiler.cr b/src/compiler/crystal/compiler.cr index f8e02b799859..f32ecd344be1 100644 --- a/src/compiler/crystal/compiler.cr +++ b/src/compiler/crystal/compiler.cr @@ -323,8 +323,9 @@ module Crystal target_machine.emit_obj_to_file llvm_mod, output_filename end - - _, command, args = linker_command(program, [output_filename], output_filename, nil) + object_names = [output_filename] + output_filename = output_filename.rchop(unit.object_extension) + _, command, args = linker_command(program, object_names, output_filename, nil) print_command(command, args) end @@ -684,7 +685,7 @@ module Crystal getter original_name getter llvm_mod getter? reused_previous_compilation = false - @object_extension : String + getter object_extension : String def initialize(@compiler : Compiler, program : Program, @name : String, @llvm_mod : LLVM::Module, @output_dir : String, @bc_flags_changed : Bool) diff --git a/src/crystal/system/process.cr b/src/crystal/system/process.cr index 8a5598fc7b16..e6d1a05898b9 100644 --- a/src/crystal/system/process.cr +++ b/src/crystal/system/process.cr @@ -56,9 +56,6 @@ struct Crystal::System::Process # thread. # def self.start_interrupt_loop - # Trap interrupt to exit normally with `at_exit` handlers being executed. - # def self.setup_default_interrupt_handler - # Whether the process identified by *pid* is still registered in the system. # def self.exists?(pid : Int) : Bool @@ -80,6 +77,15 @@ struct Crystal::System::Process # Changes the root directory for the current process. # def self.chroot(path : String) + + enum Interrupt + # sigint, ctrl-c or ctrl-break events + USER_SIGNALLED + # sighup or closed event + TERMINAL_DISCONNECTED + # sigterm, logoff or shutdown events + SESSION_ENDED + end end module Crystal::System diff --git a/src/crystal/system/unix/process.cr b/src/crystal/system/unix/process.cr index 9212226d9aa4..240c97f54d3b 100644 --- a/src/crystal/system/unix/process.cr +++ b/src/crystal/system/unix/process.cr @@ -58,8 +58,28 @@ struct Crystal::System::Process raise RuntimeError.from_errno("kill") if ret < 0 end - def self.on_interrupt(&handler : ->) : Nil - ::Signal::INT.trap { |_signal| handler.call } + def self.on_interrupt(&handler : Interrupt ->) : Nil + sig_handler = Proc(::Signal, Nil).new do |signal| + int_type = case signal + when .int? + Interrupt::USER_SIGNALLED + when .hup? + Interrupt::TERMINAL_DISCONNECTED + when .term? + Interrupt::SESSION_ENDED + else + Interrupt::USER_SIGNALLED + end + handler.call int_type + + # ignore prevents system defaults and clears registered interrupts + # hence we need to re-register + signal.ignore + Process.on_interrupt &handler + end + ::Signal::INT.trap &sig_handler + ::Signal::HUP.trap &sig_handler + ::Signal::TERM.trap &sig_handler end def self.ignore_interrupts! : Nil @@ -74,12 +94,6 @@ struct Crystal::System::Process # do nothing; `Crystal::System::Signal.start_loop` takes care of this end - def self.setup_default_interrupt_handlers - # Status 128 + signal number indicates process exit was caused by the signal. - ::Signal::INT.trap { ::exit 128 + ::Signal::INT.value } - ::Signal::TERM.trap { ::exit 128 + ::Signal::TERM.value } - end - def self.exists?(pid) ret = LibC.kill(pid, 0) if ret == 0 diff --git a/src/crystal/system/win32/process.cr b/src/crystal/system/win32/process.cr index e39db2f46fe4..5af21a395963 100644 --- a/src/crystal/system/win32/process.cr +++ b/src/crystal/system/win32/process.cr @@ -14,6 +14,7 @@ struct Crystal::System::Process @@interrupt_count = Crystal::AtomicSemaphore.new @@win32_interrupt_handler : LibC::PHANDLER_ROUTINE? @@setup_interrupt_handler = Atomic::Flag.new + @@last_interrupt = Interrupt::USER_SIGNALLED def initialize(process_info) @pid = process_info.dwProcessId @@ -103,7 +104,16 @@ struct Crystal::System::Process def self.on_interrupt(&@@interrupt_handler : ->) : Nil restore_interrupts! @@win32_interrupt_handler = handler = LibC::PHANDLER_ROUTINE.new do |event_type| - next 0 unless event_type.in?(LibC::CTRL_C_EVENT, LibC::CTRL_BREAK_EVENT) + @@last_interrupt = case event_type + when LibC::CTRL_C_EVENT, LibC::CTRL_BREAK_EVENT + Interrupt::USER_SIGNALLED + when LibC::CTRL_CLOSE_EVENT + Interrupt::TERMINAL_DISCONNECTED + when LibC::CTRL_LOGOFF_EVENT, LibC::CTRL_SHUTDOWN_EVENT + Interrupt::SESSION_ENDED + else + next 0 + end @@interrupt_count.signal 1 end @@ -136,8 +146,9 @@ struct Crystal::System::Process if handler = @@interrupt_handler non_nil_handler = handler # if handler is closured it will also have the Nil type + int_type = @@last_interrupt spawn do - non_nil_handler.call + non_nil_handler.call int_type rescue ex ex.inspect_with_backtrace(STDERR) STDERR.puts("FATAL: uncaught exception while processing interrupt handler, exiting") @@ -149,13 +160,6 @@ struct Crystal::System::Process end end - def self.setup_default_interrupt_handlers - on_interrupt do - # Exit code 3 indicates `std::abort` was called which corresponds to the interrupt handler - ::exit 3 - end - end - def self.exists?(pid) handle = LibC.OpenProcess(LibC::PROCESS_QUERY_INFORMATION, 0, pid) return false unless handle diff --git a/src/json/serialization.cr b/src/json/serialization.cr index 386be1191887..610979517a18 100644 --- a/src/json/serialization.cr +++ b/src/json/serialization.cr @@ -219,8 +219,14 @@ module JSON {% for name, value in properties %} when {{value[:key]}} begin - {% if (value[:has_default] && !value[:nilable]) || value[:root] %} - next if pull.read_null? + {% if value[:has_default] || value[:nilable] || value[:root] %} + if pull.read_null? + {% if value[:nilable] %} + %var{name} = nil + %found{name} = true + {% end %} + next + end {% end %} %var{name} = diff --git a/src/kernel.cr b/src/kernel.cr index 5291a4dc57f1..7bca29cef605 100644 --- a/src/kernel.cr +++ b/src/kernel.cr @@ -569,7 +569,6 @@ end {% else %} Crystal::System::Signal.setup_default_handlers {% end %} - Crystal::System::Process.setup_default_interrupt_handlers # load debug info on start up of the program is executed with CRYSTAL_LOAD_DEBUG_INFO=1 # this will make debug info available on print_frame that is used by Crystal's segfault handler diff --git a/src/lib_c/x86_64-windows-msvc/c/consoleapi.cr b/src/lib_c/x86_64-windows-msvc/c/consoleapi.cr index 680e199be2ab..2938b544ddcc 100644 --- a/src/lib_c/x86_64-windows-msvc/c/consoleapi.cr +++ b/src/lib_c/x86_64-windows-msvc/c/consoleapi.cr @@ -14,8 +14,11 @@ lib LibC fun GetConsoleCP : DWORD fun GetConsoleOutputCP : DWORD - CTRL_C_EVENT = 0 - CTRL_BREAK_EVENT = 1 + CTRL_C_EVENT = 0 + CTRL_BREAK_EVENT = 1 + CTRL_CLOSE_EVENT = 2 + CTRL_LOGOFF_EVENT = 5 + CTRL_SHUTDOWN_EVENT = 6 alias PHANDLER_ROUTINE = DWORD -> BOOL diff --git a/src/process.cr b/src/process.cr index 0eea5263b223..159b08b39eaa 100644 --- a/src/process.cr +++ b/src/process.cr @@ -58,10 +58,6 @@ class Process # * On Unix-like systems, this traps `SIGINT`. # * On Windows, this captures Ctrl + C and # Ctrl + Break signals sent to a console application. - # - # The default interrupt handler calls `::exit` to ensure `at_exit` handlers - # execute. It returns a platform-specific status code indicating an interrupt - # (`130` on Unix, `3` on Windows). def self.on_interrupt(&handler : ->) : Nil Crystal::System::Process.on_interrupt(&handler) end @@ -76,14 +72,8 @@ class Process end # Restores default handling of interrupt requests. - # - # The default interrupt handler calls `::exit` to ensure `at_exit` handlers - # execute. It returns a platform-specific status code indicating an interrupt - # (`130` on Unix, `3` on Windows). def self.restore_interrupts! : Nil Crystal::System::Process.restore_interrupts! - - Crystal::System::Process.setup_default_interrupt_handlers end # Returns `true` if the process identified by *pid* is valid for @@ -311,12 +301,10 @@ class Process end end - {% if flag?(:unix) %} - # :nodoc: - def initialize(pid : LibC::PidT) - @process_info = Crystal::System::Process.new(pid) - end - {% end %} + # :nodoc: + def initialize(pid : LibC::PidT) + @process_info = Crystal::System::Process.new(pid) + end # Sends *signal* to this process. # diff --git a/src/yaml/serialization.cr b/src/yaml/serialization.cr index 30e9fa6640d2..d5fae8dfe9c0 100644 --- a/src/yaml/serialization.cr +++ b/src/yaml/serialization.cr @@ -224,8 +224,14 @@ module YAML {% for name, value in properties %} when {{value[:key]}} begin - {% if value[:has_default] && !value[:nilable] %} - next if YAML::Schema::Core.parse_null?(value_node) + {% if value[:has_default] || value[:nilable] %} + if YAML::Schema::Core.parse_null?(value_node) + {% if value[:nilable] %} + %var{name} = nil + %found{name} = true + {% end %} + next + end {% end %} %var{name} =