From 1b276f349ae1c1df4c1d83a94f115e1ac0813045 Mon Sep 17 00:00:00 2001 From: Akihiko Odaki Date: Wed, 12 Jul 2023 15:02:34 +0900 Subject: [PATCH] Introduce AutoHCK::ResourceScope Before this change, the management of resources that require explicit deallocations were done ad-hoc and inconsistent. This new class provides a unified mechanism for this purpose and ensures the following: - Resource deallocation happens in reverse-allocation order. This is important because an object often depend on another object allocated earlier and cannot outlive the earlier object. - Interrupts during deallocation will be postponed. (Ensured with Thread.handle_interrupt(Object => :never)) - An interrupt won't be handled while an allocated object is assigned to a AutoHCK::ResourceScope. (Ensured with Thread.handle_interrupt(Object => :on_blocking)) Moreover, this class facilitates error handling during resource allocation. A resource allocation procedure can be wrapped with AutoHCK::ResourceScope.open, and end with AutoHCK::ResourceScope#move_to call with the outer scope as an argument; if an error happens during the allocation, resources already allocated will be safely deallocated. If no error occurs, the allocated resources will escape the inner scope with AutoHCK::ResourceScope#move_to call. Signed-off-by: Akihiko Odaki --- bin/auto_hck | 9 +- lib/auxiliary/cmd_run.rb | 21 +- lib/auxiliary/id_gen.rb | 7 +- lib/auxiliary/resource_scope.rb | 41 +++ lib/engines/config_manager/config_manager.rb | 13 +- lib/engines/hckinstall/hckinstall.rb | 45 ++- lib/engines/hcktest/hcktest.rb | 56 ++-- lib/exceptions.rb | 6 + lib/project.rb | 11 +- lib/resultuploaders/result_uploader.rb | 8 +- lib/setupmanagers/hckclient.rb | 36 +-- lib/setupmanagers/hckstudio.rb | 45 +-- lib/setupmanagers/physhck/physhck.rb | 68 ++--- lib/setupmanagers/qemuhck/qemu_machine.rb | 274 ++++++++++--------- lib/setupmanagers/qemuhck/qemuhck.rb | 53 +--- lib/setupmanagers/qemuhck/qmp.rb | 9 +- lib/trap.rb | 4 +- 17 files changed, 334 insertions(+), 372 deletions(-) create mode 100644 lib/auxiliary/resource_scope.rb diff --git a/bin/auto_hck b/bin/auto_hck index bbd17201..21c87636 100755 --- a/bin/auto_hck +++ b/bin/auto_hck @@ -7,6 +7,7 @@ require './lib/config/sentry' begin require 'filelock' + require './lib/auxiliary/resource_scope' require './lib/cli' require './lib/project' require './lib/trap' @@ -25,8 +26,8 @@ begin Thread.abort_on_exception = true Thread.report_on_exception = false - @project = Project.new(cli) - begin + ResourceScope.open do |scope| + @project = Project.new(scope, cli) Trap.project = @project @project.run if @project.prepare rescue StandardError => e @@ -37,9 +38,9 @@ begin @project.log_exception(e, 'fatal') @project.handle_error exit(1) - ensure - @project.close end + rescue AutoHCKInterrupt + exit false end rescue StandardError => e Sentry.capture_exception(e) diff --git a/lib/auxiliary/cmd_run.rb b/lib/auxiliary/cmd_run.rb index 2efaa821..0b09a35c 100644 --- a/lib/auxiliary/cmd_run.rb +++ b/lib/auxiliary/cmd_run.rb @@ -1,6 +1,7 @@ # frozen_string_literal: true require 'tempfile' +require_relative 'resource_scope' # AutoHCK module module AutoHCK @@ -42,22 +43,18 @@ def wait_for_status(flags) _, status = Process.wait2(@pid, flags) return if status.nil? - begin - begin - @stdout.rewind - stdout = @stdout.read - ensure - @stdout.close - end + ResourceScope.open do |scope| + scope << @stdout + scope << @stderr + @stdout.rewind + stdout = @stdout.read @stderr.rewind stderr = @stderr.read - ensure - @stderr.close - end - @logger.info("Info dump:#{prep_log_stream(stdout)}") unless stdout.empty? - @logger.warn("Error dump:#{prep_log_stream(stderr)}") unless stderr.empty? + @logger.info("Info dump:#{prep_log_stream(stdout)}") unless stdout.empty? + @logger.warn("Error dump:#{prep_log_stream(stderr)}") unless stderr.empty? + end yield status diff --git a/lib/auxiliary/id_gen.rb b/lib/auxiliary/id_gen.rb index a339149b..1a90781c 100644 --- a/lib/auxiliary/id_gen.rb +++ b/lib/auxiliary/id_gen.rb @@ -7,11 +7,12 @@ module AutoHCK # Id Generator class class Idgen - def initialize(range, timeout) + def initialize(scope, range, timeout) @db = './id_gen.db' @range = range - @conn = SQLite3::Database.new @db.to_s @threshold = timeout * 24 * 60 * 60 + @conn = SQLite3::Database.new @db.to_s + scope << @conn end def load_data @@ -55,8 +56,6 @@ def release(id) 1 rescue SQLite3::Exception -1 - ensure - @conn&.close end private diff --git a/lib/auxiliary/resource_scope.rb b/lib/auxiliary/resource_scope.rb new file mode 100644 index 00000000..ee1c1765 --- /dev/null +++ b/lib/auxiliary/resource_scope.rb @@ -0,0 +1,41 @@ +# frozen_string_literal: true + +require './lib/exceptions' + +module AutoHCK + # ResourceScope is a class that manages objects with close methods. + class ResourceScope + def initialize + @resources = [] + end + + def <<(resource) + @resources << resource + end + + def close + @resources.reverse_each(&:close) + @resources.clear + end + + def concat(...) + @resources.concat(...) + end + + def move_to(another) + another.concat @resources + @resources.clear + end + + def self.open + scope = new + Thread.handle_interrupt(AutoHCKInterrupt => :never) do + Thread.handle_interrupt(AutoHCKInterrupt => :on_blocking) do + yield scope + end + ensure + scope.close + end + end + end +end diff --git a/lib/engines/config_manager/config_manager.rb b/lib/engines/config_manager/config_manager.rb index d75951f9..d8a28c73 100644 --- a/lib/engines/config_manager/config_manager.rb +++ b/lib/engines/config_manager/config_manager.rb @@ -1,6 +1,7 @@ # frozen_string_literal: true require 'fileutils' +require './lib/auxiliary/resource_scope' require './lib/resultuploaders/result_uploader' # AutoHCK module @@ -34,9 +35,11 @@ def tag end def run - @project.logger.info('Stating result uploader token initialization') - @result_uploader = ResultUploader.new(@project) - @result_uploader.ask_token + ResourceScope.open do |scope| + @project.logger.info('Stating result uploader token initialization') + @result_uploader = ResultUploader.new(scope, @project) + @result_uploader.ask_token + end end def drivers @@ -50,9 +53,5 @@ def platform def result_uploader_needed? false end - - def close - @project.logger.info('Closing helpers engine') - end end end diff --git a/lib/engines/hckinstall/hckinstall.rb b/lib/engines/hckinstall/hckinstall.rb index 43967321..e54e6886 100644 --- a/lib/engines/hckinstall/hckinstall.rb +++ b/lib/engines/hckinstall/hckinstall.rb @@ -7,6 +7,7 @@ require './lib/auxiliary/json_helper' require './lib/auxiliary/host_helper' require './lib/auxiliary/iso_helper' +require './lib/auxiliary/resource_scope' require './lib/engines/hckinstall/setup_scripts_helper' # AutoHCK module @@ -188,17 +189,17 @@ def result_uploader_needed? true end - def run_studio(iso_list = [], keep_alive:, snapshot: true) + def run_studio(scope, iso_list = [], keep_alive:, snapshot: true) st_opts = { keep_alive: keep_alive, create_snapshot: snapshot, attach_iso_list: iso_list } - @project.setup_manager.run_hck_studio(st_opts) + @project.setup_manager.run_hck_studio(scope, st_opts) end - def run_client(name, snapshot: true) + def run_client(scope, studio, name, snapshot: true) cl_opts = { create_snapshot: snapshot, attach_iso_list: [ @@ -207,48 +208,40 @@ def run_client(name, snapshot: true) ] } - @project.setup_manager.run_hck_client(name, cl_opts) + @project.setup_manager.run_hck_client(scope, studio, name, cl_opts) end def run_studio_installer @project.setup_manager.create_studio_image - st = run_studio([ - @setup_studio_iso, - @studio_iso_info['path'] - ], keep_alive: false, snapshot: false) - begin + ResourceScope.open do |scope| + st = run_studio(scope, [ + @setup_studio_iso, + @studio_iso_info['path'] + ], keep_alive: false, snapshot: false) Timeout.timeout(@studio_install_timeout) do @logger.info('Waiting for studio installation finished') sleep 5 while st.alive? end - ensure - st.clean_last_run end end - def run_client_installer(name) + def run_client_installer(scope, studio, name) @project.setup_manager.create_client_image(name) - run_client(name, snapshot: false) + run_client(scope, studio, name, snapshot: false) end def run_clients_installer - st = run_studio([], keep_alive: true) - begin - cl = @clients_name.map { |c| run_client_installer(c) } - begin - Timeout.timeout(@client_install_timeout) do - cl.each do |client| - @logger.info("Waiting for #{client.name} installation finished") - sleep 5 while client.alive? - end + ResourceScope.open do |scope| + st = run_studio(scope, [], keep_alive: true) + cl = @clients_name.map { |c| run_client_installer(scope, st, c) } + Timeout.timeout(@client_install_timeout) do + cl.each do |client| + @logger.info("Waiting for #{client.name} installation finished") + sleep 5 while client.alive? end - ensure - cl.each(&:clean_last_run) end - ensure - st.clean_last_run end end diff --git a/lib/engines/hcktest/hcktest.rb b/lib/engines/hcktest/hcktest.rb index 26b2dec2..817a2a25 100644 --- a/lib/engines/hcktest/hcktest.rb +++ b/lib/engines/hcktest/hcktest.rb @@ -4,6 +4,7 @@ require './lib/setupmanagers/hckclient' require './lib/auxiliary/diff_checker' require './lib/auxiliary/json_helper' +require './lib/auxiliary/resource_scope' require './lib/auxiliary/zip_helper' # AutoHCK module @@ -144,14 +145,14 @@ def read_platform Json.read_json(platform_json, @logger) end - def run_studio(run_opts = {}) - @studio = @project.setup_manager.run_hck_studio(run_opts) + def run_studio(scope, run_opts = {}) + @studio = @project.setup_manager.run_hck_studio(scope, run_opts) end - def run_clients(run_opts = {}) + def run_clients(scope, run_opts = {}) @clients = {} @platform['clients'].each do |_name, client| - @clients[client['name']] = @project.setup_manager.run_hck_client(client['name'], run_opts) + @clients[client['name']] = @project.setup_manager.run_hck_client(scope, @studio, client['name'], run_opts) break if @project.options.test.svvp break unless @drivers.any? { |d| d['support'] } @@ -162,12 +163,6 @@ def run_clients(run_opts = {}) this platform is incorrect' end - def synchronize_clients(exit: false) - @clients.each_value do |client| - client.synchronize(exit: exit) - end - end - def configure_clients run_only = @project.options.test.manual && @project.options.test.driver_path.nil? @@ -179,35 +174,29 @@ def configure_clients def configure_setup_and_synchronize @studio.configure(@platform['clients']) configure_clients - synchronize_clients + @clients.each_value(&:synchronize) @client1 = @clients.values[0] @client2 = @clients.values[1] @client1.support = @client2 + @studio.keep_snapshot + @clients.each_value(&:keep_snapshot) end - def clean_last_run_clients - @clients.values.map(&:clean_last_run) - end - - def clean_last_run_machines - @studio.clean_last_run - clean_last_run_clients - end - - def run_and_configure_setup + def run_and_configure_setup(scope) retries ||= 0 - run_studio - sleep 5 until @studio.up? - run_clients keep_alive: true + ResourceScope.open do |tmp_scope| + run_studio tmp_scope + sleep 5 until @studio.up? + run_clients tmp_scope, keep_alive: true - configure_setup_and_synchronize + configure_setup_and_synchronize + tmp_scope.move_to scope + end rescue AutoHCKError => e - synchronize_clients(exit: true) @project.logger.warn("Running and configuring setup failed: (#{e.class}) #{e.message}") raise e unless (retries += 1) < AUTOHCK_RETRIES - clean_last_run_machines @project.logger.info('Trying again to run and configure setup') retry end @@ -230,19 +219,18 @@ def tag end def manual_run - run_studio({ dump_only: true }) - run_clients({ dump_only: true }) + ResourceScope.open do |scope| + run_studio(scope, { dump_only: true }) + run_clients(scope, { dump_only: true }) + end end def auto_run - run_and_configure_setup - begin + ResourceScope.open do |scope| + run_and_configure_setup scope client = @client1 client.run_tests client.create_package - ensure - @clients&.values&.map(&:abort) - @studio&.abort end end diff --git a/lib/exceptions.rb b/lib/exceptions.rb index c8888586..1b3838a0 100644 --- a/lib/exceptions.rb +++ b/lib/exceptions.rb @@ -5,6 +5,12 @@ module AutoHCK # A custom AutoHCK error exception class AutoHCKError < StandardError; end + # A custom AutoHCK interrupt exception that can be safely blocked with + # Thread.handle_interrupt without blocking the other exceptions. + # rubocop:disable Lint/InheritException + class AutoHCKInterrupt < Exception; end + # rubocop:enable Lint/InheritException + # A custom GithubCommitInvalid error exception class GithubCommitInvalid < AutoHCKError; end diff --git a/lib/project.rb b/lib/project.rb index 7e67085d..af6fad0d 100644 --- a/lib/project.rb +++ b/lib/project.rb @@ -24,7 +24,8 @@ class Project CONFIG_JSON = 'config.json' - def initialize(options) + def initialize(scope, options) + @scope = scope @options = options Json.update_json_override(options.common.config) unless options.common.config.nil? @@ -32,6 +33,7 @@ def initialize(options) init_class_variables init_workspace @id = assign_id + scope << self end def diff_checker(drivers, diff) @@ -115,7 +117,7 @@ def init_class_variables end def assign_id - @id_gen = Idgen.new(@config['id_range'], @config['time_out']) + @id_gen = Idgen.new(@scope, @config['id_range'], @config['time_out']) id = @id_gen.allocate while id.negative? @logger.info('No available ID') @@ -133,7 +135,7 @@ def release_id def configure_result_uploader @logger.info('Initializing result uploaders') - @result_uploader = ResultUploader.new(self) + @result_uploader = ResultUploader.new(@scope, self) @result_uploader.connect @result_uploader.create_project_folder end @@ -184,10 +186,7 @@ def handle_error def close @logger.debug('Closing AutoHCK project') - @result_uploader&.upload_file(@logfile_path, 'AutoHCK.log') - - @setup_manager&.close release_id end end diff --git a/lib/resultuploaders/result_uploader.rb b/lib/resultuploaders/result_uploader.rb index 1878c82d..4866d009 100644 --- a/lib/resultuploaders/result_uploader.rb +++ b/lib/resultuploaders/result_uploader.rb @@ -34,7 +34,8 @@ def self.can_create?(type) end end - def initialize(project) + def initialize(scope, project) + @scope = scope @project = project @connected_uploaders = {} @uploaders = {} @@ -55,6 +56,7 @@ def connect @uploaders.each_pair do |type, uploader| if uploader.connect @connected_uploaders[type] = uploader + @scope << uploader else @project.logger.info("#{type} connection failed, (ignoring)") end @@ -90,9 +92,5 @@ def url def html_url @connected_uploaders.values.filter_map(&:html_url).first end - - def close - @connected_uploaders.each_value(&:close) - end end end diff --git a/lib/setupmanagers/hckclient.rb b/lib/setupmanagers/hckclient.rb index a6a1ca02..0b7a13b4 100644 --- a/lib/setupmanagers/hckclient.rb +++ b/lib/setupmanagers/hckclient.rb @@ -17,23 +17,19 @@ class HCKClient attr_reader :name, :kit attr_writer :support - def initialize(project, setup_manager, studio, name, run_opts) - @project = project - @logger = project.logger + def initialize(setup_manager, scope, studio, name, run_opts) + @project = setup_manager.project + @logger = @project.logger @studio = studio @name = name @kit = setup_manager.kit - @setup_manager = setup_manager @pool = 'Default Pool' - @setup_manager.run_client(@name, run_opts) + @runner = setup_manager.run_client(scope, @name, run_opts) + scope << self end - def alive? - @setup_manager.client_alive?(@name) - end - - def clean_last_run - @setup_manager.clean_last_client_run(@name) + def keep_snapshot + @runner.keep_snapshot end def add_target_to_project @@ -171,15 +167,11 @@ def configure(run_only: false) end end - def synchronize(exit: false) - if exit - @cooldown_thread&.exit - else - return unless @cooldown_thread&.join(CLIENT_COOLDOWN_TIMEOUT).nil? + def synchronize + return unless @cooldown_thread&.join(CLIENT_COOLDOWN_TIMEOUT).nil? - e_message = "Timeout expired for the cooldown thread of client #{@name}" - raise ClientRunError, e_message - end + e_message = "Timeout expired for the cooldown thread of client #{@name}" + raise ClientRunError, e_message end def not_ready? @@ -195,11 +187,9 @@ def reset_to_ready_state set_machine_ready end - def abort + def close @logger.info("Aborting HLKClient #{@name}") - - @setup_manager.abort_client(@name) - synchronize(exit: true) + @cooldown_thread&.exit end end end diff --git a/lib/setupmanagers/hckstudio.rb b/lib/setupmanagers/hckstudio.rb index b0be5da5..0d348b53 100644 --- a/lib/setupmanagers/hckstudio.rb +++ b/lib/setupmanagers/hckstudio.rb @@ -11,13 +11,13 @@ class HCKStudio CONNECT_RETRIES = 5 CONNECT_RETRY_SLEEP = 10 - def initialize(project, setup_manager, run_opts, &ip_getter) - @project = project - @tag = project.engine.tag - @setup_manager = setup_manager + def initialize(setup_manager, scope, run_opts, &ip_getter) + @project = setup_manager.project + @tag = @project.engine.tag @ip_getter = ip_getter - @logger = project.logger - @setup_manager.run_studio(run_opts) + @logger = @project.logger + @scope = scope + @runner = setup_manager.run_studio(scope, run_opts) end def up? @@ -29,21 +29,11 @@ def create_pool @tools.create_pool(@tag) end - def delete_pool - @logger.info('Deleting pool') - @tools.delete_pool(@tag) - end - def create_project @logger.info('Creating project') @tools.create_project(@tag) end - def delete_project - @logger.info('Deleting project') - @tools.delete_project(@tag) - end - def list_pools @tools.list_pools end @@ -62,6 +52,7 @@ def connect begin @logger.info('Initiating connection to studio') @tools = Tools.new(@project, @ip_getter.call, @clients) + @scope << @tools rescue Errno::ECONNREFUSED, Errno::EHOSTUNREACH, RToolsHCKConnectionError raise StudioConnectError, 'Initiating connection to studio failed' end @@ -80,20 +71,8 @@ def verify_tools raise StudioConnectError, 'Tools did not pass the connection check' end - def alive? - @setup_manager.studio_alive? - end - - def clean_tools - delete_project - delete_pool - @tools&.close - @tools = nil - end - - def clean_last_run - clean_tools unless @tools.nil? - @setup_manager.clean_last_studio_run + def keep_snapshot + @runner.keep_snapshot end def configure(clients) @@ -107,12 +86,6 @@ def configure(clients) create_project end - def abort - @logger.info('Aborting HLK Studio') - @tools&.close - @setup_manager.abort_studio - end - def shutdown @logger.info('Shutting down studio') @tools.shutdown diff --git a/lib/setupmanagers/physhck/physhck.rb b/lib/setupmanagers/physhck/physhck.rb index 06098b94..968b5623 100644 --- a/lib/setupmanagers/physhck/physhck.rb +++ b/lib/setupmanagers/physhck/physhck.rb @@ -7,9 +7,28 @@ module AutoHCK # PhysHCK class PhysHCK + # Runner is a class that represents a run. + class Runner + def initialize(logger) + @logger = logger + end + + def alive? + @logger.info('Physical machine is always alive') + end + + def keep_snapshot + @logger.info('Keeping snapshot is currently not supported for physical machines') + end + + def close + @logger.info('Abort is currently not supported for physical machines') + end + end + include Helper - attr_reader :kit + attr_reader :kit, :project PHYSHCK_CONFIG_JSON = 'lib/setupmanagers/physhck/physhck.json' @@ -45,52 +64,21 @@ def create_client_image(_name) @logger.info('Image creating is currently not supported for physical machines') end - def run_studio(*); end - def run_client(*); end - - def studio_alive? - @logger.info('Physical machine is always alive') - end - - def client_alive?(_name) - @logger.info('Physical machine is always alive') - end - - def keep_studio_alive - @logger.info('Physical machine is always alive') + def run_studio(*) + Runner.new(@logger) end - def keep_client_alive(_name) - @logger.info('Physical machine is always alive') + def run_client(*) + Runner.new(@logger) end - def clean_last_studio_run - @logger.info('Clean last run is currently not supported for physical machines') - end - - def clean_last_client_run(_name) - @logger.info('Clean last run is currently not supported for physical machines') - end - - def run_hck_studio(run_opts) + def run_hck_studio(scope, run_opts) studio_ip = @setup['st_ip'] - @studio = HCKStudio.new(@project, self, run_opts) { studio_ip } - end - - def run_hck_client(name, run_opts) - HCKClient.new(@project, self, @studio, name, run_opts) - end - - def abort_studio - @logger.info('Abort is currently not supported for physical machines') - end - - def abort_client(_name) - @logger.info('Abort is currently not supported for physical machines') + HCKStudio.new(self, scope, run_opts) { studio_ip } end - def close - @logger.info('Closing setup manager') + def run_hck_client(scope, studio, name, run_opts) + HCKClient.new(self, scope, studio, name, run_opts) end end end diff --git a/lib/setupmanagers/qemuhck/qemu_machine.rb b/lib/setupmanagers/qemuhck/qemu_machine.rb index 17f4ecc3..743675c7 100644 --- a/lib/setupmanagers/qemuhck/qemu_machine.rb +++ b/lib/setupmanagers/qemuhck/qemu_machine.rb @@ -8,12 +8,154 @@ require_relative '../../auxiliary/json_helper' require_relative '../../auxiliary/host_helper' +require_relative '../../auxiliary/resource_scope' require_relative '../../auxiliary/string_helper' # AutoHCK module module AutoHCK # QemuMachine class class QemuMachine + # Hostfwd is a class that holds ports forwarded for a run. + class Hostfwd + def initialize(slirp, ports) + @slirp = slirp + @ids = [] + begin + ports.each do |port| + @ids << slirp.run({ + 'execute' => 'add_hostfwd', + 'arguments' => { + 'proto' => 'tcp', + 'host_port' => port, + 'guest_port' => port + } + }) + end + rescue StandardError + close + end + end + + def close + @ids.each do |id| + @slirp.run({ + 'execute' => 'remove_hostfwd', + 'arguments' => id + }) + end + + @ids.clear + end + end + + # Runner is a class that represents a run. + class Runner + include Helper + + # machine soft abort trials before force abort + SOFT_ABORT_RETRIES = 3 + # machine abort sleep for each trial + ABORT_SLEEP = 30 + + def initialize(scope, logger, machine, run_name, run_opts) + @logger = logger + @machine = machine + @run_name = run_name + @run_opts = run_opts + @keep_alive = run_opts[:keep_alive] + @delete_snapshot = run_opts[:create_snapshot] + @qmp = QMP.new(scope, @run_name, @logger) + @machine.run_config_commands + run_vm + scope << self + end + + def keep_snapshot + @delete_snapshot = false + end + + def run_qemu + @logger.info("Starting #{@run_name}") + cmd = replace_string_recursive(@machine.dirty_command.join(' '), @machine.full_replacement_list) + cmd += " -chardev socket,id=qmp,fd=#{@qmp.socket.fileno},server=off -mon chardev=qmp,mode=control" + qemu = CmdRun.new(@logger, cmd, { @qmp.socket.fileno => @qmp.socket.fileno }) + @logger.info("#{@run_name} started with PID #{qemu.pid}") + qemu + end + + def run_vm + @machine.dump_config + @machine.run_pre_start_commands + + @qemu_thread = Thread.new do + loop do + qemu = nil + Thread.handle_interrupt(Object => :on_blocking) do + qemu = run_qemu + qemu.wait_no_fail + qemu = nil + ensure + unless qemu.nil? + Process.kill 'KILL', qemu.pid + qemu.wait_no_fail + end + end + + break unless @keep_alive + end + end + end + + def alive? + @qemu_thread.alive? + end + + def soft_abort + SOFT_ABORT_RETRIES.times do + @qmp.powerdown + + return true unless @qemu_thread.join(ABORT_SLEEP).nil? + + @logger.debug("Powerdown was sent, but #{@run_name} is still alive :(") + end + false + end + + def hard_abort + @qmp.quit + + return true unless @qemu_thread.join(ABORT_SLEEP).nil? + + @logger.debug("Quit was sent, but #{@run_name} is still alive :(") + false + end + + def vm_abort + @keep_alive = false + + return unless alive? + + return if soft_abort + + @logger.info("#{@run_name} soft abort failed, hard aborting...") + + return if hard_abort + + @logger.info("#{@run_name} hard abort failed, force aborting...") + + @qemu_thread.kill + @qemu_thread.join + end + + def close + return if @run_opts[:dump_only] + + vm_abort + @machine.run_post_stop_commands + @machine.delete_snapshot if @delete_snapshot + end + end + include Helper MONITOR_BASE_PORT = 10_000 @@ -35,11 +177,6 @@ class QemuMachine CONFIG_JSON = 'lib/setupmanagers/qemuhck/qemu_machine.json' STATES_JSON = 'lib/setupmanagers/qemuhck/states.json' - # machine soft abort trials before force abort - SOFT_ABORT_RETRIES = 3 - # machine abort sleep for each trial - ABORT_SLEEP = 30 - def initialize(options) define_local_variables @@ -66,7 +203,6 @@ def define_local_variables @drive_cache_options = [] @define_variables = {} @run_opts = {} - @hostfwds = [] end def load_options(options) @@ -394,62 +530,6 @@ def save_run_script(file_name, file_content) create_run_script(file_path, file_content) end - def run_qemu - @logger.info("Starting #{@run_name}") - cmd = replace_string_recursive(dirty_command.join(' '), full_replacement_list) - cmd += " -chardev socket,id=qmp,fd=#{@qmp.socket.fileno},server=off -mon chardev=qmp,mode=control" - qemu = CmdRun.new(@logger, cmd, { @qmp.socket.fileno => @qmp.socket.fileno }) - @logger.info("#{@run_name} started with PID #{qemu.pid}") - qemu - end - - def run_vm - dump_config - run_pre_start_commands - - @qemu_thread = Thread.new do - loop do - qemu = nil - Thread.handle_interrupt(Object => :on_blocking) do - qemu = run_qemu - qemu.wait_no_fail - qemu = nil - ensure - unless qemu.nil? - Process.kill 'KILL', qemu.pid - qemu.wait_no_fail - end - end - - break unless @keep_alive - end - end - end - - def add_hostfwd - [@monitor_port, @vnc_port].each do |port| - @hostfwds << @options['slirp'].run({ - 'execute' => 'add_hostfwd', - 'arguments' => { - 'proto' => 'tcp', - 'host_port' => port, - 'guest_port' => port - } - }) - end - end - - def remove_hostfwd - @hostfwds.each do |fwd_id| - @options['slirp'].run({ - 'execute' => 'remove_hostfwd', - 'arguments' => fwd_id - }) - end - - @hostfwds.clear - end - def run_config_commands @config_commands.each do |dirty_cmd| cmd = replace_string_recursive(dirty_cmd, full_replacement_list) @@ -549,10 +629,8 @@ def dump_commands create_run_script(file_name, content.join) end - def run(run_opts = nil) - @qmp = QMP.new(@run_name, @logger) + def run(scope, run_opts = nil) @run_opts = validate_run_opts(run_opts.to_h) - @keep_alive = run_opts[:keep_alive] create_snapshot if @run_opts[:create_snapshot] @devices_list.flatten! @@ -564,66 +642,14 @@ def run(run_opts = nil) if @run_opts[:dump_only] dump_commands else - add_hostfwd - run_config_commands - run_vm - end - end - - def alive? - @qemu_thread.alive? - end - - def soft_abort - SOFT_ABORT_RETRIES.times do - @qmp.powerdown - - return true unless @qemu_thread.join(ABORT_SLEEP).nil? - - @logger.debug("Powerdown was sent, but #{@run_name} is still alive :(") + ResourceScope.open do |tmp_scope| + hostfwd = Hostfwd.new(@options['slirp'], [@monitor_port, @vnc_port]) + tmp_scope << hostfwd + runner = Runner.new(tmp_scope, @logger, self, @run_name, @run_opts) + tmp_scope.move_to scope + runner + end end - false - end - - def hard_abort - @qmp.quit - - return true unless @qemu_thread.join(ABORT_SLEEP).nil? - - @logger.debug("Quit was sent, but #{@run_name} is still alive :(") - false - end - - def vm_abort - @keep_alive = false - - return unless alive? - - return if soft_abort - - @logger.info("#{@run_name} soft abort failed, hard aborting...") - - return if hard_abort - - @logger.info("#{@run_name} hard abort failed, force aborting...") - - @qemu_thread.kill - @qmp.close - @qemu_thread.join - end - - def clean_last_run - @logger.info("Cleaning last #{@run_name} run") - close - delete_snapshot - end - - def close - return if @run_opts[:dump_only] - - remove_hostfwd - vm_abort - run_post_stop_commands end end end diff --git a/lib/setupmanagers/qemuhck/qemuhck.rb b/lib/setupmanagers/qemuhck/qemuhck.rb index df93a2b2..a8b40c15 100644 --- a/lib/setupmanagers/qemuhck/qemuhck.rb +++ b/lib/setupmanagers/qemuhck/qemuhck.rb @@ -12,7 +12,7 @@ module AutoHCK class QemuHCK include Helper - attr_reader :kit + attr_reader :kit, :project OPT_NAMES = %w[viommu_state enlightenments_state vhost_state machine_type fw_type cpu].freeze @@ -128,55 +128,20 @@ def client_option_config(name, option) @clients_vm[name].option_config(option) end - def run_studio(run_opts = nil) - @studio_vm.run(run_opts) + def run_studio(scope, run_opts = nil) + @studio_vm.run(scope, run_opts) end - def run_client(name, run_opts = nil) - @clients_vm[name].run(run_opts) + def run_client(scope, name, run_opts = nil) + @clients_vm[name].run(scope, run_opts) end - def studio_alive? - @studio_vm.alive? + def run_hck_studio(scope, run_opts) + HCKStudio.new(self, scope, run_opts) { @studio_vm.find_world_ip } end - def client_alive?(name) - @clients_vm[name].alive? - end - - def clean_last_studio_run - @studio_vm.clean_last_run - end - - def clean_last_client_run(name) - @clients_vm[name].clean_last_run - end - - def abort_studio - @logger.info('Aborting studio VM') - @studio_vm&.close - end - - def abort_client(name) - @logger.info("Aborting client #{name} VM") - @clients_vm[name]&.close - end - - def close - @clients_vm.each { |_k, vm| vm.close } - if @studio.nil? - @studio_vm&.close - else - @studio.abort - end - end - - def run_hck_studio(run_opts) - @studio = HCKStudio.new(@project, self, run_opts) { @studio_vm.find_world_ip } - end - - def run_hck_client(name, run_opts) - HCKClient.new(@project, self, @studio, name, run_opts) + def run_hck_client(scope, studio, name, run_opts) + HCKClient.new(self, scope, studio, name, run_opts) end end end diff --git a/lib/setupmanagers/qemuhck/qmp.rb b/lib/setupmanagers/qemuhck/qmp.rb index ede4e2cc..bea0a055 100644 --- a/lib/setupmanagers/qemuhck/qmp.rb +++ b/lib/setupmanagers/qemuhck/qmp.rb @@ -11,17 +11,14 @@ class QemuMachine class QMP attr_reader :socket - def initialize(name, logger) + def initialize(scope, name, logger) @name = name @logger = logger @negotiated = false @logger.info("Initiating QMP session for #{name}") @socket, @socket_internal = UNIXSocket.pair - end - - def close - @socket.close - @socket_internal.close + scope << @socket + scope << @socket_internal end def quit diff --git a/lib/trap.rb b/lib/trap.rb index b41e89cc..16fdb46c 100644 --- a/lib/trap.rb +++ b/lib/trap.rb @@ -1,5 +1,7 @@ # frozen_string_literal: true +require_relative 'exceptions' + # AutoHCK module module AutoHCK # Trap class @@ -21,7 +23,7 @@ def self.perform_trap(signal) write_log("SIG#{signal}(*) received, ignoring...") end @project&.handle_cancel - exit + raise AutoHCKInterrupt end @sig_timestamps = {}