#!/usr/bin/python3

import concurrent.futures
import glob
import http.server
import logging
import os
import re
import subprocess
import sys
import threading
import time
from typing import Any, Dict, List, Set, Tuple, Union

KEY_VALUE_SPLITTER = re.compile(":\s+")
METRIC_NAME_RE = re.compile("[a-zA-Z_:][a-zA-Z0-9_:]*")
METRIC_LABEL_NAME_RE = re.compile("[a-zA-Z_][a-zA-Z0-9_]*")

UNITLESS_TOTAL_SUFFIX = "_total"
SECONDS_TOTAL_SUFFIX = "_seconds_total"
BYTES_SUFFIX = "_bytes"
RATIO_SUFFIX = "_ratio"

smartmon_prog = r"""#!/bin/bash
# Script informed by the collectd monitoring script for smartmontools (using smartctl)
# by Samuel B. <samuel_._behan_(at)_dob_._sk> (c) 2012
# source at: http://devel.dob.sk/collectd-scripts/

# TODO: This probably needs to be a little more complex.  The raw numbers can have more
#       data in them than you'd think.
#       http://arstechnica.com/civis/viewtopic.php?p=22062211

# Formatting done via shfmt -i 2
# https://github.com/mvdan/sh

parse_smartctl_attributes_awk="$(
  cat <<'SMARTCTLAWK'
$1 ~ /^ *[0-9]+$/ && $2 ~ /^[a-zA-Z0-9_-]+$/ {
  gsub(/-/, "_");
  printf "%s_value{%s,smart_id=\"%s\"} %d\n", $2, labels, $1, $4
  printf "%s_worst{%s,smart_id=\"%s\"} %d\n", $2, labels, $1, $5
  printf "%s_threshold{%s,smart_id=\"%s\"} %d\n", $2, labels, $1, $6
  printf "%s_raw_value{%s,smart_id=\"%s\"} %e\n", $2, labels, $1, $10
}
SMARTCTLAWK
)"

smartmon_attrs="$(
  cat <<'SMARTMONATTRS'
airflow_temperature_cel
command_timeout
current_pending_sector
end_to_end_error
erase_fail_count
g_sense_error_rate
hardware_ecc_recovered
host_reads_mib
host_reads_32mib
host_writes_mib
host_writes_32mib
load_cycle_count
media_wearout_indicator
wear_leveling_count
nand_writes_1gib
offline_uncorrectable
power_cycle_count
power_on_hours
program_fail_count
raw_read_error_rate
reallocated_sector_ct
reported_uncorrect
sata_downshift_count
spin_retry_count
spin_up_time
start_stop_count
temperature_case
temperature_celsius
temperature_internal
total_lbas_read
total_lbas_written
udma_crc_error_count
unsafe_shutdown_count
workld_host_reads_perc
workld_media_wear_indic
workload_minutes
SMARTMONATTRS
)"
smartmon_attrs="$(echo ${smartmon_attrs} | xargs | tr ' ' '|')"

parse_smartctl_attributes() {
  local disk="$1"
  local diskb=$(basename "$disk")
  local disk_type="$2"
  local labels="device=\"${diskb}\",type=\"${disk_type}\""
  local vars="$(echo "${smartmon_attrs}" | xargs | tr ' ' '|')"
  sed 's/^ \+//g' |
    awk -v labels="${labels}" "${parse_smartctl_attributes_awk}" 2>/dev/null |
    tr A-Z a-z |
    grep -E "(${smartmon_attrs})"
}

parse_smartctl_scsi_attributes() {
  local disk="$1"
  local diskb=$(basename "$disk")
  local disk_type="$2"
  local labels="device=\"${diskb}\",type=\"${disk_type}\""
  while read line; do
    attr_type="$(echo "${line}" | tr '=' ':' | cut -f1 -d: | sed 's/^ \+//g' | tr ' ' '_')"
    attr_value="$(echo "${line}" | tr '=' ':' | cut -f2 -d: | sed 's/^ \+//g')"
    case "${attr_type}" in
    number_of_hours_powered_up_) power_on="$(echo "${attr_value}" | awk '{ printf "%e\n", $1 }')" ;;
    Current_Drive_Temperature) temp_cel="$(echo ${attr_value} | cut -f1 -d' ' | awk '{ printf "%e\n", $1 }')" ;;
    Blocks_read_from_cache_and_sent_to_initiator_) lbas_read="$(echo ${attr_value} | awk '{ printf "%e\n", $1 }')" ;;
    Accumulated_start-stop_cycles) power_cycle="$(echo ${attr_value} | awk '{ printf "%e\n", $1 }')" ;;
    Elements_in_grown_defect_list) grown_defects="$(echo ${attr_value} | awk '{ printf "%e\n", $1 }')" ;;
    esac
  done
  [ ! -z "$power_on" ] && echo "power_on_hours_raw_value{${labels},smart_id=\"9\"} ${power_on}"
  [ ! -z "$temp_cel" ] && echo "temperature_celsius_raw_value{${labels},smart_id=\"194\"} ${temp_cel}"
  [ ! -z "$lbas_read" ] && echo "total_lbas_read_raw_value{${labels},smart_id=\"242\"} ${lbas_read}"
  [ ! -z "$power_cycle" ] && echo "power_cycle_count_raw_value{${labels},smart_id=\"12\"} ${power_cycle}"
  [ ! -z "$grown_defects" ] && echo "grown_defects_count_raw_value{${labels},smart_id=\"12\"} ${grown_defects}"
}

parse_smartctl_info() {
  local -i smart_available=0 smart_enabled=0 smart_healthy=0
  local disk="$1" disk_type="$2"
  local diskb=$(basename "$disk")
  local model_family='' device_model='' serial_number='' fw_version='' vendor='' product='' revision='' lun_id='' temp_1='' temp_2=''
  while read line; do
    info_type="$(echo "${line}" | cut -f1 -d: | tr ' ' '_')"
    info_value="$(echo "${line}" | cut -f2- -d: | sed 's/^ \+//g' | sed 's/"/\\"/')"
    case "${info_type}" in
    Model_Family) model_family="${info_value}" ;;
    Device_Model) device_model="${info_value}" ;;
    Serial_Number) serial_number="${info_value}" ;;
    Firmware_Version) fw_version="${info_value}" ;;
    Vendor) vendor="${info_value}" ;;
    Product) product="${info_value}" ;;
    Revision) revision="${info_value}" ;;
    Logical_Unit_id) lun_id="${info_value}" ;;
    Temperature) temp_1="$( echo ${info_value} | cut -d ' ' -f 1 )" ;;
    esac
    if [[ "${info_type}" == 'SMART_support_is' ]]; then
      case "${info_value:0:7}" in
      Enabled) smart_enabled=1 ;;
      Availab) smart_available=1 ;;
      Unavail) smart_available=0 ;;
      esac
    fi
    if [[ "${info_type}" == 'SMART_overall-health_self-assessment_test_result' ]]; then
      case "${info_value:0:6}" in
      PASSED) smart_healthy=1 ;;
      esac
    elif [[ "${info_type}" == 'SMART_Health_Status' ]]; then
      case "${info_value:0:2}" in
      OK) smart_healthy=1 ;;
      esac
    fi
  done
  echo "device_info{device=\"${diskb}\",type=\"${disk_type}\",vendor=\"${vendor}\",product=\"${product}\",revision=\"${revision}\",lun_id=\"${lun_id}\",model_family=\"${model_family}\",device_model=\"${device_model}\",serial_number=\"${serial_number}\",firmware_version=\"${fw_version}\"} 1"
  echo "device_smart_available{device=\"${diskb}\",type=\"${disk_type}\"} ${smart_available}"
  echo "device_smart_enabled{device=\"${diskb}\",type=\"${disk_type}\"} ${smart_enabled}"
  if [ "${temp_1}" != "" ] ; then
    echo "smartmon_temperature_celsius_value{device=\"${diskb}\",type=\"${disk_type}\"} ${temp_1}"
  fi
}

output_format_awk="$(
  cat <<'OUTPUTAWK'
BEGIN { v = "" }
v != $1 {
  print "# HELP smartmon_" $1 " SMART metric " $1;
  print "# TYPE smartmon_" $1 " gauge";
  v = $1
}
{print "smartmon_" $0}
OUTPUTAWK
)"

format_output() {
  sort |
    awk -F'{' "${output_format_awk}"
}

# smartctl_version="$(/usr/sbin/smartctl -V | head -n1 | awk '$1 == "smartctl" {print $2}')"
# echo "smartctl_version{version=\"${smartctl_version}\"} 1" | format_output | format_output
# if [[ "$(expr "${smartctl_version}" : '\([0-9]*\)\..*')" -lt 6 ]]; then
#   exit
# fi

device_list="$(/usr/sbin/smartctl --scan-open -n standby | awk '/^\/dev/{print $1 "|" $3}')"

do_drive() {
  local disk=$1
  diskb=$(basename "$disk")
  local type=$2

  echo "smartctl_run{device=\"${diskb}\",type=\"${type}\"}" "$(TZ=UTC date '+%s')" | format_output
  # Get the SMART information and health.
  out=$(/usr/sbin/smartctl -i -H -d "${type}" "${disk}")
  ret=$?
  echo "smartctl_return_code{device=\"${diskb}\",type=\"${type}\"}" "$ret" | format_output
  echo "$out" | parse_smartctl_info "${diskb}" "${type}" | format_output
  echo "# Fetching attributes for disk ${diskb} of type ${type}."
  # Get the SMART attributes
  case ${type} in
  sat) /usr/sbin/smartctl -A -d "${type}" "${disk}" | parse_smartctl_attributes "${diskb}" "${type}" | format_output ;;
  sat+megaraid*) /usr/sbin/smartctl -A -d "${type}" "${disk}" | parse_smartctl_attributes "${diskb}" "${type}" | format_output ;;
  scsi) /usr/sbin/smartctl -A -d "${type}" "${disk}" | parse_smartctl_scsi_attributes "${diskb}" "${type}" | format_output ;;
  nvme) /usr/sbin/smartctl -A -d "${type}" "${disk}" | parse_smartctl_attributes "${diskb}" "${type}" | format_output ;;
  megaraid*) /usr/sbin/smartctl -A -d "${type}" "${disk}" | parse_smartctl_scsi_attributes "${diskb}" "${type}" | format_output ;;
  *)
    echo "# disk type is not sat, scsi or megaraid but ${type}" \>\&2
    return
    ;;
  esac
  echo "smartctl_all_attributes_run{device=\"${diskb}\",type=\"${type}\"}" "$(TZ=UTC date '+%s')" | format_output
}

disk="$0"
type="$1"

do_drive ${disk} ${type}
"""


class CacheEntry:
    def __init__(self) -> None:
        self.output = b""
        self.lock = threading.Lock()
        self.time = time.time()


class Cache:
    def __init__(self, timeout: int) -> None:
        self.lock = threading.Lock()
        self.items: Dict[str, CacheEntry] = {}
        self.timeout = timeout


class LogEachMessageOnce(logging.Filter):
    """Context manager to log each message only once per logger.

    Track each message template on a per-logger basis (or, if unspecified,
    the root logger by default) and, if a message is seen more than once
    traversing that logger (or its children), then forego logging it.

    Only active during the context in which this context manager is active.

    Usage:

        with LogEachMessageOnce(): # automatically apply to root logger
            logging.warning("This message only logs once per device %s", device)
            logging.warning("This message only logs once per device %s", device)
    """

    msgs: Dict[
        logging.Logger, Set[str]
    ] = {}  # Shared between instances from beginning to end of program.
    lock = threading.Lock()

    def __init__(self, logger: Union[logging.Logger, None] = None) -> None:
        self.logger = logger or logging.getLogger()

    def filter(self, record: logging.LogRecord) -> bool:
        msg = str(record.msg)
        with self.lock:
            is_duplicate = self.logger in self.msgs and msg in self.msgs[self.logger]
            if not is_duplicate:
                if self.logger not in self.msgs:
                    self.msgs[self.logger] = set()
                self.msgs[self.logger].add(msg)
        return not is_duplicate

    def __enter__(self) -> None:
        self.logger.addFilter(self)

    def __exit__(self, exc_type: Any, exc_val: Any, exc_tb: Any) -> None:
        self.logger.removeFilter(self)


def is_standby(device: str) -> bool:
    try:
        out = subprocess.check_output(
            ["/usr/sbin/hdparm", "-C", device],
            universal_newlines=True,
        )
    except subprocess.CalledProcessError as exc:
        if exc.returncode == 25:
            # The drive does not support checking power status.
            # We pretend it's active as a response.
            return False
        raise
    logging.debug(out)
    idle = "drive state is:  standby" in out
    if idle:
        logging.debug("Device %s is in standby.", device)
    else:
        logging.debug("Device %s is active.", device)
    return idle


def normalize_metric_name(m: str) -> str:
    # FIXME remove all characters that cannot be used in label names
    # in Prometheus according to the spec, then remove consecutive __s .
    x = m.lower().replace(" ", "_").replace("/", "_")
    while "__" in x:
        x = x.replace("__", "_")
    return x


def format_labelpair(label: str, value: str) -> str:
    escaped_value = value.replace("\\", "\\\\").replace('"', '\\"')
    return f'{label}="{escaped_value}"'


def format_metric(
    metric: str, labelpairs: Dict[str, str], value: Union[float, int]
) -> Union[str, None]:
    if not METRIC_NAME_RE.match(metric):
        logging.warning("Cannot serialize metric %s: invalid metric name", metric)
        return None

    for p in labelpairs:
        if not METRIC_LABEL_NAME_RE.match(p):
            logging.warning(
                "Cannot serialize metric %s: invalid metric label %s", metric, p
            )
            return None

    formatted_labelpairs = (
        ("{" + ", ".join(format_labelpair(k, v) for k, v in labelpairs.items()) + "} ")
        if labelpairs
        else ""
    )

    return f"{metric}{formatted_labelpairs} {value}"


def convert_percent_to_ratio(m: str) -> float:
    assert m.endswith("%")
    return float(m[:-1])


def convert_to_int(m: str, base: Union[int, None] = None) -> int:
    return int(m.split()[0].replace(",", ""), base=base or 10)


def render_nvme_metrics(
    device_path: str, type_: str
) -> List[Tuple[str, Dict[str, str], Union[float, int]]]:
    """
    Documented at
    https://media.kingston.com/support/downloads/MKP_521.6_SMART-DCP1000_attribute.pdf

    Sample output:

    === START OF INFORMATION SECTION ===
    Model Number:                       Vi3000 Internal PCIe NVMe M.2 SSD 256GB
    Serial Number:                      493733094833604
    Firmware Version:                   H220902a
    PCI Vendor/Subsystem ID:            0x1e4b
    IEEE OUI Identifier:                0x000000
    Total NVM Capacity:                 256,060,514,304 [256 GB]
    Unallocated NVM Capacity:           0
    Controller ID:                      0
    NVMe Version:                       1.4
    Number of Namespaces:               1
    Namespace 1 Size/Capacity:          256,060,514,304 [256 GB]
    Namespace 1 Formatted LBA Size:     512
    Namespace 1 IEEE EUI-64:            000000 3094833604
    Local Time is:                      Tue Dec  5 01:57:48 2023 UTC

    === START OF SMART DATA SECTION ===
    SMART overall-health self-assessment test result: PASSED

    SMART/Health Information (NVMe Log 0x02)
    Critical Warning:                   0x00
    Temperature:                        45 Celsius
    Available Spare:                    100%
    Available Spare Threshold:          1%
    Percentage Used:                    0%
    Data Units Read:                    312,243 [159 GB]
    Data Units Written:                 638,685 [327 GB]
    Host Read Commands:                 2,489,053
    Host Write Commands:                4,688,856
    Controller Busy Time:               19
    Power Cycles:                       41
    Power On Hours:                     103
    Unsafe Shutdowns:                   30
    Media and Data Integrity Errors:    0
    Error Information Log Entries:      0
    Warning  Comp. Temperature Time:    0
    Critical Comp. Temperature Time:    0
    Temperature Sensor 1:               45 Celsius
    Temperature Sensor 2:               39 Celsius
    """
    output = subprocess.check_output(
        ["/usr/sbin/smartctl", "-i", "-H", "-a", "-d", type_, device_path],
        universal_newlines=True,
    )
    metrics: List[Tuple[str, Dict[str, str], Union[float, int]]] = []

    info = output.split("=== START OF INFORMATION SECTION ===\n")[1].split("\n\n===")[0]

    device_info_table, device_info_labels = (
        {
            "Model Number": "device_model",
            "Serial Number": "serial_number",
            "Firmware Version": "firmware_version",
            "PCI Vendor/Subsystem ID": "pci_vendor_subsystem_id",
            "NVMe Version": "nvme_version",
        },
        {},
    )
    for line in info.splitlines():
        try:
            key, value = KEY_VALUE_SPLITTER.split(line)
        except ValueError:
            continue
        if key in device_info_table:
            device_info_labels[device_info_table[key]] = value
        elif key == "Total NVM Capacity":
            metrics.append(
                ("nvm_capacity_total" + BYTES_SUFFIX, {}, convert_to_int(value))
            )
        elif key == "Unallocated NVM Capacity":
            metrics.append(
                ("nvm_capacity_unallocated" + BYTES_SUFFIX, {}, convert_to_int(value))
            )
        elif key == "Number of Namespaces":
            metrics.append(("nvm_namespace_count", {}, convert_to_int(value)))
        elif key.startswith("Namespace"):
            number = key.split()[1]
            if "Size" in key:
                metrics.append(
                    (
                        "nvm_namespace_size" + BYTES_SUFFIX,
                        {"namespace": number},
                        convert_to_int(value),
                    )
                )
            elif "Formatted LBA Size" in key:
                metrics.append(
                    (
                        "nvm_namespace_lba_size" + BYTES_SUFFIX,
                        {"namespace": number},
                        convert_to_int(value),
                    )
                )
        else:
            logging.warning(
                f"Metric {key} from device %s not understood",
                device_path,
            )
    if device_info_labels:
        metrics.insert(0, ("device_info", device_info_labels, 1))

    metrics.append(("enabled", {}, 0))
    metrics.append(("available", {}, 0))
    smartdata = (
        output.split("=== START OF SMART DATA SECTION ===")[1]
        if "START OF SMART DATA SECTION" in output
        else ""
    )
    for line in smartdata.splitlines():
        try:
            key, value = KEY_VALUE_SPLITTER.split(line)
        except ValueError:
            continue
        if "SMART overall-health" in key:
            metrics.append(("healthy", {}, (1 if value == "PASSED" else 0)))
            metrics.append(("enabled", {}, 1))
            metrics.append(("available", {}, 1))
        elif key == "Critical Warning":
            value = convert_to_int(value, 16)
            for bit, label in [
                (1, "spare space low"),
                (2, "temperature outside operating range"),
                (3, "read only mode"),
                (4, "volatile memory backup failed"),
            ]:
                metrics.append(
                    ("critical_warning", {"category": label}, 1 if value & bit else 0)
                )
        elif key == "Temperature":
            metrics.append(
                (
                    normalize_metric_name(key + "_" + value.split()[1]),
                    {},
                    convert_to_int(value),
                )
            )
        elif "Spare" in key and "%" in value:
            metrics.append(
                (
                    normalize_metric_name(key) + RATIO_SUFFIX,
                    {},
                    convert_percent_to_ratio(value),
                )
            )
        elif key == "Percentage Used":
            metrics.append(("used" + RATIO_SUFFIX, {}, convert_percent_to_ratio(value)))
        elif key.startswith("Data Units"):
            metrics.append(
                (
                    normalize_metric_name(key.split()[2] + " units"),
                    {},
                    convert_to_int(value),
                )
            )
        elif key.startswith("Host") and "Commands" in key:
            metrics.append(
                (
                    (normalize_metric_name(key) + UNITLESS_TOTAL_SUFFIX),
                    {},
                    convert_to_int(value),
                )
            )
        elif key == "Controller Busy Time" or "Temperature Time" in key:
            key = (
                normalize_metric_name(
                    key.replace("Comp.", "Composite").replace(" Time", "")
                )
                + SECONDS_TOTAL_SUFFIX
            )
            metrics.append((key, {}, convert_to_int(value) * 60))
        elif key in (
            "Power Cycles",
            "Unsafe Shutdowns",
            "Media and Data Integrity Errors",
        ):
            metrics.append(
                (
                    normalize_metric_name(key) + UNITLESS_TOTAL_SUFFIX,
                    {},
                    convert_to_int(value),
                )
            )

        elif key == "Power On Hours":
            metrics.append(
                (
                    "power_on" + SECONDS_TOTAL_SUFFIX,
                    {},
                    convert_to_int(value) * 3600,
                )
            )
        elif key.startswith("Temperature Sensor"):
            sensor = key.split()[2]
            key = normalize_metric_name("temperature_sensor_" + value.split()[1])
            metrics.append((key, {"sensor": sensor}, convert_to_int(value)))
        else:
            logging.warning(
                f"Metric {key} from device %s not understood",
                device_path,
            )

    deduped_metrics: Dict[str, Tuple[str, Dict[str, str], Union[float, int]]] = {}

    for mname, labels, value in metrics:
        deduped_metrics[f"{mname}-{labels}"] = (mname, labels, value)

    return list(deduped_metrics.values())


def render_smart_data(device_path: str, name: str, type_: str) -> bytes:
    if type_ == "nvme":
        metrics = render_nvme_metrics(device_path, type_)
        rendered: List[str] = []
        for key, labels, value in metrics:
            labels["device"] = name
            labels["type"] = type_
            m = format_metric("smartmon_" + key, labels, value)
            if m is not None:
                rendered.append(m)
        return "\n".join(rendered).encode("utf-8")
    else:
        return subprocess.check_output(
            ["bash", "-c", smartmon_prog, device_path, type_]
        )


old_devlist: Union[List[str], None] = None
old_device_type_list: List[List[str]] = []


def get_device_list() -> List[List[str]]:
    global old_devlist, old_device_type_list

    devlist: List[str] = []
    for fn in sorted(glob.glob("/sys/block/*/dev")):
        with open(fn, "r") as f:
            devlist.append(f.read().strip())

    logging.debug("Obtained device major/minor list: %s", devlist)

    if old_devlist == devlist:
        # Prevent scan since nothing has changed.
        logging.debug("Eliding disk scan.")
        return old_device_type_list

    logging.debug("Devices have changed.  Asking smartctl to scan devices again.")
    device_type_list = [
        [x.strip().split()[0], x.strip().split()[2]]
        for x in subprocess.check_output(
            ["/usr/sbin/smartctl", "--scan-open"], universal_newlines=True
        ).splitlines()
        if x.strip()
    ]
    old_devlist = devlist
    old_device_type_list = device_type_list
    logging.info("Devices discovered:")
    for d, t in device_type_list:
        logging.info("* %s (type %s)", d, t)
    return device_type_list


def render_output(cache: Cache) -> bytes:
    def produce_output_for_a_drive(
        kache: Cache,
        device_path: str,
        type_: str,
    ) -> List[bytes]:
        buf: List[bytes] = []

        def buffer(o: Union[str, bytes]) -> None:
            buf.append(o if isinstance(o, bytes) else o.encode("utf-8"))

        name = os.path.basename(device_path)
        idle = is_standby(device_path)
        buffer(
            'smartmon_disk_standby{device="%s",type="%s"} %s' % (name, type_, int(idle))
        )

        kache.lock.acquire()
        if kache.items.get(device_path) and (
            idle or (kache.timeout > (time.time() - kache.items[device_path].time))
        ):
            logging.debug(
                "Device %s is idle or metrics cache is not expired.  Using cache.",
                device_path,
            )
            entry = kache.items[device_path]
            with entry.lock:
                kache.lock.release()
                cache_age = time.time() - entry.time
                o = entry.output

            buffer(
                'smartmon_cache_age_seconds{device="%s",type="%s"} %s'
                % (name, type_, cache_age)
            )
            buffer(
                'smartmon_data_from_cache{device="%s",type="%s"} %s' % (name, type_, 1)
            )

        else:
            logging.debug(
                "Device %s is active or metrics cache is expired.  Retrieving metrics.",
                name,
            )

            entry = CacheEntry()
            kache.items[device_path] = entry
            with entry.lock:
                kache.lock.release()
                with LogEachMessageOnce():
                    o = render_smart_data(device_path, name, type_)
                entry.output = o

            buffer(
                'smartmon_cache_age_seconds{device="%s",type="%s"} %s'
                % (name, type_, 0)
            )
            buffer(
                'smartmon_data_from_cache{device="%s",type="%s"} %s' % (name, type_, 0)
            )
            logging.debug("Finished retrieving metrics from device %s.", name)

        buffer(o.strip())
        return buf

    device_list = get_device_list()
    per_device_output: List[bytes] = [
        ("smartmon_total_device_count %s" % len(device_list)).encode("utf-8")
    ]

    with concurrent.futures.ThreadPoolExecutor() as executor:
        future_to_url = {
            executor.submit(produce_output_for_a_drive, cache, *devtyp): devtyp
            for devtyp in device_list
        }

        for future in concurrent.futures.as_completed(future_to_url):
            buffered = future.result()
            per_device_output.extend(buffered)

    return b"\n".join(per_device_output)


class SimplePromHandler(http.server.BaseHTTPRequestHandler):
    def handle_non_metrics(self) -> bool:
        if self.path == "/":
            self.send_response(200)
            self.send_header("Content-Type", "text/html; charset=utf-8")
            self.end_headers()
            self.wfile.write(
                (
                    "<html><body>%s exporter.  "
                    "<a href='/metrics'>Metrics.</a>"
                    "<body></html>" % os.path.basename(__file__)
                ).encode("utf-8")
            )
            return True
        elif self.path != "/metrics":
            self.send_response(404)
            self.end_headers()
            return True
        return False

    def log_request(
        self, code: Union[int, str] = "-", size: Union[int, str] = "-"
    ) -> None:
        pass

    def do_GET(self) -> None:
        if self.handle_non_metrics():
            return

        global cache

        try:
            output = render_output(cache)
            self.send_response(200)
            self.send_header("Content-Type", "text/plain; version=0.0.4; charset=utf-8")
            self.end_headers()
            self.wfile.write(output)

        except subprocess.CalledProcessError:
            self.send_response(500)


logging.basicConfig(level=logging.DEBUG if os.getenv("DEBUG") else logging.INFO)

if len(sys.argv) == 1:
    print(render_output(Cache(0)).decode("utf-8"))
    # print(get_device_list())
    # for dev, typ in get_device_list():
    #    print(dev, is_standby(dev))
    #    print(
    #        subprocess.check_output(["bash", "-xc", smartmon_prog, dev, typ]).decode(
    #            "utf-8"
    #        )
    #    )
else:
    cache = Cache(int(sys.argv[2]))
    server = http.server.HTTPServer(("", int(sys.argv[1])), SimplePromHandler)
    logging.info("Serving on TCP port %s", sys.argv[1])
    server.serve_forever()
