Мой маленький мониторинг
Делюсь очередной полезной штукой из личных разработок, на этот раз на тему мониторинга температуры в компьютере.
Задача
Честно говоря, задача мониторинга температуры это такой своеобразный «неуловимый Джо» — ненужная мелкая ерунда, реализуемая в больших системах для галочки и по остаточному принципу.
Ну и конечно отображение температуры с датчиков персонального компьютера давным давно реализовано и присутствует в виде виджета во всех основных графических окружениях и для любых ОС.
Чаще всего это выглядит как-то так:
Это все к вопросу об уникальности, важности и полезности решения, как бы заранее отвечая на самый важный вопрос бытия: «нах#я я все это читаю».
Кстати одна из первых разработанных автором систем была как раз на тему мониторинга температуры у парка серверов.
Дело было давно и сервера (как и сетевое оборудование) стояли не в охлаждаемых и чистых серверных, а где придется.
С соответствующими последствиями по перегреву.
Так выглядит виновник сегодняшнего торжества:
Исходный код можно посмотреть чуть ниже в статье или вот тут в виде gist. Теперь рассказываю подробнее как оно все работает.
Кстати думаю для многих станет сюрпризом, что X-сервер позволяет рисовать графические элементы и текст в чужих окнах.
Без предупреждения, без индикации, без каких-то отдельных прав и тд.
Очко сотрудников СБ только что заиграло новыми красками.
Реализация
Поскольку у автора используется тот еще зоопарк разных систем, хотелось сделать максимально переносимое решение.
Хардкодить все на чистом С я посчитал излишним, поэтому был взят Python и библиотека Xlib, которая есть везде, куда еще не добрались проклятые зумеры со своим Wayland.
К моему великому сожалению, оказалось что знаменитая связка Tcl/Tk не умеет работать с root window, поэтому использовать их не получится.
Именно поэтому был взят петон, да.
Собственно эта библиотека является единственной внешней зависимостью в этом проекте, для FreeBSD устанавливается вот так:
pkg install py311-python-xlib
Аналогичные пакеты есть в любом линуксе и BSD. Теперь стоит рассказать про сами датчики.
Датчики температуры в случае FreeBSD отдают свои значения через sysctl и требуют подгрузки специального модуля ядра.
Чаще всего это будет coretemp:
kldload coretemp
Чтение значений датчиков будет выглядеть как-то так:
Для процессоров AMD есть отдельный модуль amdtemp:
kldload amdtemp
Названия датчиков отличаются, поэтому для чтения используется немного другой паттерн:
sysctl dev.amdtemp.0 |grep core
В случае линукса температуру с датчиков можно получить из специальной виртуальной файловой системы /dev:
cat /sys/class/thermal/thermal_zone*/temp
Исходный код
Ниже представлен полный исходный код моей утилиты, технически это shebang — самозапускаемый скрипт на Python, поэтому его нужно сохранить с расширением .sh и поставить бит запуска.
#!/usr/bin/env python3
import Xlib
from Xlib import display, X # display и X - не импортируются автоматически
import subprocess,time,logging
#настройки логирования
logging.basicConfig(level=logging.INFO)
#logging.basicConfig(level=logging.DEBUG)
# координаты на экране для отображения
POS_X = 150
POS_Y = 50
# для FreeBSD и машины с AMD
PATTERN = 'sysctl dev.amdtemp.0 |grep core'
# для FreeBSD с модулем coretemp
#PATTERN = 'sysctl dev.cpu |grep temperature'
# для Linux
#PATTERN = "cat /sys/class/thermal/thermal_zone*/temp | awk '{ print \"temp: \" ($1 / 1000) \"C\" }'"
# шрифт
FONT = '-misc-fixed-medium-r-normal--13-120-75-75-c-70-iso8859-1'
# период обновления
REFRESH_SECS = 15
last_dim = [0,0]
# определение рабочего стола
# d - display (экран)s
def get_root_window(d):
screen = d.screen()
# root window - рабочий стол по-умолчанию
root = screen.root
# Получаем ID всех окон верхнего уровня
windowIDs = root.get_full_property(d.intern_atom('_NET_CLIENT_LIST'),
X.AnyPropertyType).value
logging.debug('Found %d windows.',len(windowIDs))
for windowID in windowIDs:
# Create a window object from the ID to access its properties
window = d.create_resource_object('window', windowID)
try:
# Get the window title (WM_NAME or _NET_WM_NAME)
# Use get_wm_name() for simplicity,
# or look up EWMH properties for better compatibility
window_name = window.get_wm_name()
if window_name:
# в продвинутых DE вроде KDE/Xfce за рабочий стол отвечает
# отдельное окно и рисовать придется в нем
if 'Desktop' in window_name:
logging.debug("Found desktop ID: %d - Name: %s",
windowID,window_name)
return window
else:
# Бывает, что у окна нет заголовка
logging.debug("ID: %d - Name: None (no WM_NAME property)",
windowID)
except X.BadWindow:
# Обработка ситуации, когда считываемое окно больше не существует
logging.debug("ID: %d - Window no longer exists",windowID)
# если отдельного окна с именем Desktop не обнаружено - рисуем
# прямо на root window
return root
# очистка области
def clear_rect(msg):
global last_dim
# если есть сохраненные размеры - используем их
if last_dim[0] > 0:
root.fill_rectangle(gc2, POS_X,POS_Y-last_dim[1],
last_dim[0]-20, last_dim[1]+5)
else:
# расчет размеров надписи в пикселях
text_extents = font.query_text_extents(msg)
tw = text_extents.overall_width
th = text_extents.font_ascent + text_extents.font_descent
# очистка происходит через отрисовку черного прямоугольника
# clear_area плохо работает с KDE
root.fill_rectangle(gc2, POS_X,POS_Y-th, tw-20, th+5)
# запоминаем размеры надписи для следующей очистки
last_dim = [tw,th]
# отрисовка сообщения на экране
# msg - текст сообщения
def draw_message(msg):
# очистка области
clear_rect(msg)
# отрисовка текста
root.draw_text(gc, POS_X, POS_Y, msg)
display.flush()
# инициализация подключения к Х-серверу
display = Xlib.display.Display()
screen = display.screen()
root = get_root_window(display)
# Access the window ID (an integer)
root_id = root.id
logging.debug("Root window ID: %d",root_id)
# для реакции на события
root.change_attributes(event_mask=X.ExposureMask) # "adds" this event mask
# создание графического контекста, белый текст на черном фоне
gc = root.create_gc(foreground = screen.white_pixel,
background = screen.black_pixel)
# дополнительный контекст для заливки области черным
colormap = screen.default_colormap
color = colormap.alloc_named_color('black')
gc2 = root.create_gc(foreground=color.pixel)
# загружаем шрифт, которым будет отрисовываться текст
# если шрифт с таким названием не будет найден - вылетит ошибка
try:
font = display.open_font(FONT)
gc.font = font.id
except Exception as e:
logging.exception(e)
exit(1)
try:
# бесконечный цикл, в котором происходит все действо
while 1:
# запуск процесса для получения значений датчиков
process = subprocess.Popen(PATTERN,
shell=True, text=True,
stdout=subprocess.PIPE)
# в этой переменной будет массив строк со значениями
stdout_list = process.communicate()[0].split('\n')
out = ''
# делаем чистку
for s in stdout_list:
# убираем ошибочные строки, если нет : - нет и значения
if ':' not in s: continue
kv = s.split(':')
# добавляем запятую в качестве разделителя
if len(out) > 0: out+= ','
# добавляем значение датчика в строку
out+= kv[1]
logging.debug(out)
# отрисовываем полученную строку
draw_message(out.encode())
# задержка между итерациями
time.sleep(REFRESH_SECS)
except KeyboardInterrupt:
# при нажатии Ctrl-C делаем очистку области экрана, где
# происходила отрисовка виджета
x = POS_X // 2
y = POS_Y // 2
root.clear_area(x,y,last_dim[0]+x,last_dim[1]+y,True)
display.flush()Как видите тут нет ООП и нет многопоточности — реализация максимально упрощена. Также не стал городить считывание параметров, поэтому ключевые настройки находятся в самом скрипте.
logging.basicConfig(level=logging.INFO) #logging.basicConfig(level=logging.DEBUG)
Координаты для отображения на экране (левый нижний угол):
POS_X = 150 POS_Y = 50
Выбор паттерна для считывания значений датчиков:
# для FreeBSD и машины с AMD
PATTERN = 'sysctl dev.amdtemp.0 |grep core'
# для FreeBSD с модулем coretemp
PATTERN = 'sysctl dev.cpu |grep temperature'
# для Linux
PATTERN = "cat /sys/class/thermal/thermal_zone*/temp | awk '{ print \"temp: \" ($1 / 1000) \"C\" }'"Шрифт, которым будут отображаться значения температуры, указывается в специальном формате:
FONT = '-misc-fixed-medium-r-normal--13-120-75-75-c-70-iso8859-1'
Частота обновлений в секундах:
REFRESH_SECS = 15
Так виджет выглядит на Ubuntu и Xfce: