#!/usr/bin/env ruby

require "socket"
require "fileutils"
require "json"

SOCKET_PATH = "/run/malpd/malpd.sock"

def human_capacity(bytes)
  units = ["B", "KB", "MB", "GB", "TB", "PB"]
  i = 0
  size = bytes.to_f
  while size >= 1000 && i < units.length - 1
    size /= 1000
    i += 1
  end
  precision = i <= 3 ? 0 : 1
  "%g %s" % [size.round(precision), units[i]]
end

def drive_type(name)
  return "nvme" if name.start_with?("nvme")
  rotational = File.read("/sys/block/#{name}/queue/rotational").strip
  rotational == "1" ? "hdd" : "ssd"
end

def read_smart(name)
  out = `smartctl -j -A -H /dev/#{name} 2>/dev/null`
  return nil if out.empty?
  data = JSON.parse(out) rescue nil
  return nil unless data

  result = {}
  passed = data.dig("smart_status", "passed")
  result["passed"] = passed unless passed.nil?

  if (t = data.dig("temperature", "current"))
    result["temperature"] = t
  end
  if (poh = data.dig("power_on_time", "hours"))
    result["power_on_hours"] = poh
  end

  if name.start_with?("nvme")
    log = data["nvme_smart_health_information_log"] || {}
    result["percentage_used"] = log["percentage_used"] if log.key?("percentage_used")
    result["media_errors"]    = log["media_errors"]    if log.key?("media_errors")
    result["critical_warning"] = log["critical_warning"] if log.key?("critical_warning")
  else
    attrs = data.dig("ata_smart_attributes", "table") || []
    attrs.each do |a|
      raw = a.dig("raw", "value")
      next if raw.nil?
      case a["id"]
      when 5   then result["reallocated_sectors"] = raw
      when 10  then result["spin_retry"] = raw
      when 197 then result["pending_sectors"] = raw
      when 199 then result["crc_errors"] = raw
      end
    end
  end

  result
end

def handle_info(conn)
  info = []

  Dir.glob("/sys/block/*").each do |block_path|
    name = File.basename(block_path)
    next unless name.match?(/^(sd[a-z]+|nvme\d+n\d+)$/)

    device_path = File.realpath("#{block_path}/device") rescue next
    next if device_path.include?("/usb")

    model = (File.read("#{block_path}/device/model").strip rescue "Unknown")
    bytes = File.read("#{block_path}/size").strip.to_i * 512

    entry = {
      "type" => "drive",
      "drivetype" => drive_type(name),
      "name" => name,
      "model" => model,
      "capacity" => human_capacity(bytes)
    }
    if (smart = read_smart(name))
      entry["smart"] = smart
    end
    info << entry
  end

  if conn
    conn.puts JSON.generate(info)
  else
    puts JSON.pretty_generate(info)
  end
end

if ARGV.include?("-t")
  handle_info(nil)
  exit
end

if Process.uid != 0
  $stderr.puts "malpd must be run as root"
  exit(1)
end

if ENV["LISTEN_FDS"]
  server = UNIXServer.for_fd(3)
else
  FileUtils.mkdir_p(File.dirname(SOCKET_PATH))
  FileUtils.rm_f(SOCKET_PATH)

  server = UNIXServer.new(SOCKET_PATH)
  File.chmod(0660, SOCKET_PATH)
  FileUtils.chown(nil, "apache", SOCKET_PATH)
end

Signal.trap("TERM") { server.close }
Signal.trap("INT") { server.close }

loop do
  conn = server.accept
  request = conn.gets&.chomp

  begin
    case request
    when nil
      # empty request
    when "info"
      handle_info(conn)
    else
      conn.puts "unknown request: #{request}"
    end
  rescue => e
    $stderr.puts "error handling request: #{e.message}"
  ensure
    conn.close
  end
rescue IOError
  break
end
