2026-06-07 11:05:26 +08:00
|
|
|
# <<*>> -^v- SNOWARE APPLICATION
|
|
|
|
|
# usage: Automatic operator implementation.
|
|
|
|
|
# author: S.A.
|
|
|
|
|
# time: 2026-06-07
|
2026-06-07 19:35:17 +08:00
|
|
|
# Copyright (c) [2026] [S.A. SNOWARE-SCU]
|
|
|
|
|
# [MineSentinel] is licensed under Mulan PubL v2.
|
|
|
|
|
# You can use this software according to the terms and conditions of the Mulan PubL v2.
|
|
|
|
|
# You may obtain a copy of Mulan PubL v2 at:
|
|
|
|
|
# http://license.coscl.org.cn/MulanPubL-2.0
|
|
|
|
|
# THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND,
|
|
|
|
|
# EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT,
|
|
|
|
|
# MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
|
|
|
|
|
# See the Mulan PubL v2 for more details.
|
2026-06-07 11:05:26 +08:00
|
|
|
|
|
|
|
|
# frozen_string_literal: true
|
|
|
|
|
|
|
|
|
|
PERSISTENCE_PERIOD = 30 # Wait for systemctl to take the order.
|
|
|
|
|
|
|
|
|
|
require 'logger'
|
|
|
|
|
require_relative 'server_status_detector'
|
2026-06-08 11:47:00 +08:00
|
|
|
|
|
|
|
|
# rubocop:disable Style/ClassVars
|
2026-06-07 11:05:26 +08:00
|
|
|
class MethodExecutor
|
|
|
|
|
RST_COUNTS_LIMIT = 10
|
|
|
|
|
@@rst_counter = 0
|
|
|
|
|
# Initialize the standard log system.
|
2026-06-08 11:47:00 +08:00
|
|
|
@@logger = Logger.new($stdout) # This script will be launched as a systemd server.
|
|
|
|
|
$stdout.sync = true
|
|
|
|
|
$stderr.sync = true
|
2026-06-07 11:05:26 +08:00
|
|
|
# Fetch the environmental variable and convert it into log level.
|
2026-06-08 11:47:00 +08:00
|
|
|
@@logger.level = case ENV.fetch('LOG_LEVEL', nil) # @为单个实例(半生不熟的概念,貌似是在不同的空间独立创建),@@为共享同一个
|
|
|
|
|
when nil, 'debug' # Default config.
|
|
|
|
|
Logger::Severity::DEBUG
|
|
|
|
|
when 'info'
|
|
|
|
|
Logger::Severity::INFO
|
|
|
|
|
when 'warn'
|
|
|
|
|
Logger::Severity::WARN
|
|
|
|
|
when 'error'
|
|
|
|
|
Logger::Severity::ERROR
|
|
|
|
|
else
|
|
|
|
|
raise ArgumentError
|
2026-06-07 11:05:26 +08:00
|
|
|
end
|
2026-06-08 11:47:00 +08:00
|
|
|
@@logger.progname = 'MineSentinel'
|
|
|
|
|
|
|
|
|
|
# rubocop:disable Metrics/MethodLength
|
2026-06-07 11:05:26 +08:00
|
|
|
|
2026-06-08 11:35:51 +08:00
|
|
|
# @param level [String] "info" | "warn" | "error" | "debug"
|
|
|
|
|
# @param module_name [String]
|
|
|
|
|
# @param message [String]
|
|
|
|
|
# @return [void]
|
2026-06-07 11:05:26 +08:00
|
|
|
def self.record(level, module_name, message)
|
|
|
|
|
case level
|
2026-06-08 11:47:00 +08:00
|
|
|
when 'info'
|
2026-06-07 11:05:26 +08:00
|
|
|
@@logger.info("#{module_name}: #{message}")
|
2026-06-08 11:47:00 +08:00
|
|
|
when 'warn'
|
2026-06-07 11:05:26 +08:00
|
|
|
@@logger.warn("#{module_name}: #{message}")
|
2026-06-08 11:47:00 +08:00
|
|
|
when 'error'
|
2026-06-07 11:05:26 +08:00
|
|
|
@@logger.error("#{module_name}: #{message}")
|
2026-06-08 11:47:00 +08:00
|
|
|
when 'debug'
|
2026-06-07 11:05:26 +08:00
|
|
|
@@logger.debug("#{module_name}: #{message}")
|
|
|
|
|
else
|
|
|
|
|
raise ArgumentError
|
|
|
|
|
end
|
|
|
|
|
end
|
|
|
|
|
|
2026-06-08 11:47:00 +08:00
|
|
|
# rubocop:enable Metrics/MethodLength
|
|
|
|
|
# rubocop:disable Metrics/MethodLength, Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
|
|
|
|
|
|
2026-06-08 11:35:51 +08:00
|
|
|
# @return [void]
|
2026-06-07 11:05:26 +08:00
|
|
|
def self.reset_minecraft_server
|
2026-06-08 11:47:00 +08:00
|
|
|
record('info', 'reset', "Attempt ##{@@rst_counter + 1} to reset Minecraft server.")
|
2026-06-07 11:05:26 +08:00
|
|
|
|
|
|
|
|
if @@rst_counter >= RST_COUNTS_LIMIT
|
2026-06-08 11:47:00 +08:00
|
|
|
record('error', 'reset',
|
|
|
|
|
"Tried #{RST_COUNTS_LIMIT} times and failed, now I'll idle until server is manually restarted.")
|
|
|
|
|
loop do
|
2026-06-07 11:05:26 +08:00
|
|
|
sleep 10
|
2026-06-08 11:47:00 +08:00
|
|
|
SystemdNotifier.notify('WATCHDOG=1') if defined?(SystemdNotifier)
|
|
|
|
|
next unless ServerStatusDetector::MinecraftServer.self_diag[:type] == true
|
|
|
|
|
|
|
|
|
|
record('info', 'reset', 'Server recovered! Resuming normal monitoring.')
|
|
|
|
|
@@rst_counter = 0
|
|
|
|
|
return
|
2026-06-07 11:05:26 +08:00
|
|
|
end
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
@@rst_counter += 1
|
|
|
|
|
|
2026-06-08 11:47:00 +08:00
|
|
|
unless Process.euid.zero?
|
|
|
|
|
record('error', 'reset', 'Permission denied: root required to manage systemd services.')
|
2026-06-08 10:30:38 +08:00
|
|
|
raise Errno::EPERM
|
|
|
|
|
end
|
2026-06-07 11:05:26 +08:00
|
|
|
|
2026-06-08 11:47:00 +08:00
|
|
|
record('info', 'reset', "Spawning 'systemctl restart minecraft' (PID will be captured)...")
|
|
|
|
|
pid = spawn 'systemctl restart minecraft'
|
|
|
|
|
record('debug', 'reset', "systemctl restart spawned with PID #{pid}.")
|
2026-06-08 10:30:38 +08:00
|
|
|
tick_beg = Time.now
|
2026-06-07 11:05:26 +08:00
|
|
|
|
2026-06-08 11:47:00 +08:00
|
|
|
# rubocop:disable Metrics/BlockLength
|
|
|
|
|
loop do
|
2026-06-08 10:30:38 +08:00
|
|
|
sleep 0.5
|
2026-06-08 11:47:00 +08:00
|
|
|
SystemdNotifier.notify('WATCHDOG=1')
|
2026-06-08 10:30:38 +08:00
|
|
|
proc_status = Process.waitpid2(pid, Process::WNOHANG)
|
|
|
|
|
if proc_status # 子进程已退出,返回 [pid, status]
|
|
|
|
|
_, status = proc_status
|
|
|
|
|
elapsed = (Time.now - tick_beg).round(2)
|
|
|
|
|
if status.success?
|
2026-06-08 11:47:00 +08:00
|
|
|
record('info', 'reset', "'systemctl restart' succeeded in #{elapsed}s.")
|
2026-06-08 10:30:38 +08:00
|
|
|
@@rst_counter = 0 # 成功时重置计数器
|
|
|
|
|
else
|
2026-06-08 11:47:00 +08:00
|
|
|
record('error', 'reset',
|
|
|
|
|
"'systemctl restart' failed with exitcode #{status.exitstatus} after #{elapsed}s.")
|
2026-06-07 11:05:26 +08:00
|
|
|
end
|
2026-06-08 11:47:00 +08:00
|
|
|
break
|
2026-06-08 10:30:38 +08:00
|
|
|
else # 子进程仍在运行 (WNOHANG 返回 nil)
|
2026-06-08 11:47:00 +08:00
|
|
|
if Time.now - tick_beg > PERSISTENCE_PERIOD
|
|
|
|
|
record('warn', 'reset',
|
|
|
|
|
"'systemctl restart' timed out after #{PERSISTENCE_PERIOD}s. Force-killing PID #{pid}...")
|
2026-06-08 10:30:38 +08:00
|
|
|
system "kill -9 #{pid}"
|
2026-06-08 11:47:00 +08:00
|
|
|
record('warn', 'reset', "Running 'systemctl kill minecraft' (last resort)...")
|
|
|
|
|
system 'systemctl kill minecraft' # 没招了,毁灭吧
|
2026-06-08 10:30:38 +08:00
|
|
|
sleep 0.5
|
2026-06-08 11:47:00 +08:00
|
|
|
record('info', 'reset', "Running 'systemctl start minecraft' (rebirth)...")
|
|
|
|
|
system 'systemctl start minecraft' # 重生
|
|
|
|
|
record('info', 'reset', 'Force-reset sequence completed.')
|
2026-06-08 10:30:38 +08:00
|
|
|
break
|
2026-06-07 11:05:26 +08:00
|
|
|
end
|
|
|
|
|
end
|
2026-06-08 11:47:00 +08:00
|
|
|
# rubocop:enable Metrics/BlockLength
|
2026-06-08 10:30:38 +08:00
|
|
|
end
|
2026-06-07 11:05:26 +08:00
|
|
|
end
|
2026-06-08 11:47:00 +08:00
|
|
|
# rubocop:enable Metrics/MethodLength, Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
|
|
|
|
|
# rubocop:enable Style/ClassVars
|
2026-06-08 07:30:21 +08:00
|
|
|
end
|