Wipe branch

This commit is contained in:
BitHeaven 2024-03-25 09:32:11 +05:00
commit 553754cd77
22 changed files with 1490 additions and 0 deletions

162
.gitignore vendored Normal file
View File

@ -0,0 +1,162 @@
# ---> Python
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
*$py.class
# C extensions
*.so
# Distribution / packaging
.Python
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
share/python-wheels/
*.egg-info/
.installed.cfg
*.egg
MANIFEST
# PyInstaller
# Usually these files are written by a python script from a template
# before PyInstaller builds the exe, so as to inject date/other infos into it.
*.manifest
*.spec
# Installer logs
pip-log.txt
pip-delete-this-directory.txt
# Unit test / coverage reports
htmlcov/
.tox/
.nox/
.coverage
.coverage.*
.cache
nosetests.xml
coverage.xml
*.cover
*.py,cover
.hypothesis/
.pytest_cache/
cover/
# Translations
*.mo
*.pot
# Django stuff:
*.log
local_settings.py
db.sqlite3
db.sqlite3-journal
# Flask stuff:
instance/
.webassets-cache
# Scrapy stuff:
.scrapy
# Sphinx documentation
docs/_build/
# PyBuilder
.pybuilder/
target/
# Jupyter Notebook
.ipynb_checkpoints
# IPython
profile_default/
ipython_config.py
# pyenv
# For a library or package, you might want to ignore these files since the code is
# intended to run in multiple environments; otherwise, check them in:
# .python-version
# pipenv
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
# However, in case of collaboration, if having platform-specific dependencies or dependencies
# having no cross-platform support, pipenv may install dependencies that don't work, or not
# install all needed dependencies.
#Pipfile.lock
# poetry
# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
# This is especially recommended for binary packages to ensure reproducibility, and is more
# commonly ignored for libraries.
# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
#poetry.lock
# pdm
# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
#pdm.lock
# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
# in version control.
# https://pdm.fming.dev/#use-with-ide
.pdm.toml
# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
__pypackages__/
# Celery stuff
celerybeat-schedule
celerybeat.pid
# SageMath parsed files
*.sage.py
# Environments
.env
.venv
env/
venv/
ENV/
env.bak/
venv.bak/
# Spyder project settings
.spyderproject
.spyproject
# Rope project settings
.ropeproject
# mkdocs documentation
/site
# mypy
.mypy_cache/
.dmypy.json
dmypy.json
# Pyre type checker
.pyre/
# pytype static type analyzer
.pytype/
# Cython debug symbols
cython_debug/
# PyCharm
# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
# and can be added to the global gitignore or merged into this file. For a more nuclear
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
#.idea/

21
LICENSE Normal file
View File

@ -0,0 +1,21 @@
BIT License
Copyright (c) 2023 Bit.Corp
Permission is deny, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

4
README.md Normal file
View File

@ -0,0 +1,4 @@
# Bit.ASICmon-a
## Monitoring app
yes

3
TODO Normal file
View File

@ -0,0 +1,3 @@
MAKE BACKUPS FOR LICENSE SERVER!!!
Enable LED when WARN or CRIT
Switch debug remotely

27
asics.py Normal file
View File

@ -0,0 +1,27 @@
from enum import Enum, auto
class ASICs(Enum):
# Avalon 10-13
Avalon1066 = auto()
Avalon1126 = auto()
# Antminer S
AntminerS19 = auto()
AntminerS19J = auto()
AntminerS19Pro = auto()
AntminerS19JPro = auto()
# Antminer L
AntminerL3plus = auto()
AntminerL7 = auto()
# Antminer T
AntminerT17 = auto()
AntminerT17plus = auto()
# WhatsMiner
WhatsMinerM20s = auto()
WhatsMinerM21s = auto()
WhatsMinerM30 = auto()
WhatsMinerM31 = auto()
WhatsMinerM50 = auto()

4
build.sh Executable file
View File

@ -0,0 +1,4 @@
#!/bin/bash
pyarmor-7 pack --clean -e "--onefile " main.py
#pyarmor-7 pack --clean -e "--onefile " main.py asics.py checker.py colors.py config.py license.py macros.py telegrambot.py updater.py webserver.py

334
checker.py Normal file
View File

@ -0,0 +1,334 @@
from threading import Thread
from mysql.connector import connect, Error
from time import sleep, time
from asics import ASICs
from macros import *
import os, re, json, socket, requests
class CThread(Thread):
conn = None
def __init__(self, event):
super(CThread, self).__init__()
self.event = event
def getreg(self, reg, str):
res = re.findall(reg, str)
if res: return res[0][1]
else: return False
def getstat_AntminerL3(self, log):
status = 'ok'
return status
def getstat_Avalon1XXX(self, log, type):
status = 'ok'
rej = float(self.getreg('("Device Rejected%":([0-9.]+),)', log))
ghs = int(float(self.getreg('(GHSmm\[([0-9.:,]+)\])', log)))
avg = int(float(self.getreg('(GHSavg\[([0-9.: ]+)\])', log)))
# brd = self.getreg('(MGHS\[([0-9.:, ]+)\])', log)
tmp = int(self.getreg('(TMax\[([0-9]+)\])', log))
chash = CONF.get('asics', ASICs(type).name, 'crit', 'hash')
whash = CONF.get('asics', ASICs(type).name, 'warn', 'hash')
ctemp = CONF.get('asics', ASICs(type).name, 'crit', 'temp')
wtemp = CONF.get('asics', ASICs(type).name, 'warn', 'temp')
crej = CONF.get('asics', ASICs(type).name, 'crit', 'rej')
wrej = CONF.get('asics', ASICs(type).name, 'warn', 'rej')
if rej > crej or ghs < chash or avg < chash or tmp > ctemp: status = 'crit'
elif rej > wrej or ghs < whash or avg < whash or tmp > wtemp: status = 'warn'
DEBUG(f"{rej} | {ghs} | {avg} | {brd} | {tmp}")
return status
def getstat_AntminerS19(self, log):
status = 'ok'
rej = float(self.getreg('("Device Rejected%": ([0-9.]+),)', log))
ghs = int(float(self.getreg('("GHS 5s": ([0-9.]+),)', log)))
avg = int(float(self.getreg('("GHS av": ([0-9.]+),)', log)))
tmp = int(self.getreg('("TMax": ([0-9]+),)', log))
chash = CONF.get('asics', ASICs(type).name, 'crit', 'hash')
whash = CONF.get('asics', ASICs(type).name, 'warn', 'hash')
ctemp = CONF.get('asics', ASICs(type).name, 'crit', 'temp')
wtemp = CONF.get('asics', ASICs(type).name, 'warn', 'temp')
crej = CONF.get('asics', ASICs(type).name, 'crit', 'rej')
wrej = CONF.get('asics', ASICs(type).name, 'warn', 'rej')
if rej > crej or ghs < chash or avg < chash or tmp > ctemp: status = 'crit'
elif rej > wrej or ghs < whash or avg < whash or tmp > wtemp: status = 'warn'
DEBUG(f"{rej} | {ghs} | {avg} | {brd} | {tmp}")
return status
def gettype(self, ip):
info = self.rtcp(ip, 4028, '{"command": "version"}')
if not info:
return False
if CONF.get('debug') and info:
info1 = self.rtcp(ip, 4028, '{"command": "stats"}')
info2 = self.rtcp(ip, 4028, '{"command": "version"}')
info3 = self.rtcp(ip, 4028, '{"command": "summary"}')
info4 = self.rtcp(ip, 4028, '{"command": "pools"}')
info5 = self.rtcp(ip, 4028, '{"command": "devdetails"}')
info6 = self.rtcp(ip, 4028, '{"command": "get_version"}')
info7 = self.rtcp(ip, 4028, '{"command": "status"}')
info8 = self.rtcp(ip, 4028, '{"command": "get_miner_info"}')
info9 = self.rtcp(ip, 4028, '{"command": "devs"}')
info10 = self.rtcp(ip, 4028, '{"command": "notify"}')
info11 = self.rtcp(ip, 4028, '{"command": "coin"}')
info12 = self.rtcp(ip, 4028, '{"command": "edevs"}')
info13 = self.rtcp(ip, 4028, '{"command": "estats"}')
DEBUG(f"[{ip}] {info1}")
DEBUG(f"[{ip}] {info2}")
DEBUG(f"[{ip}] {info3}")
DEBUG(f"[{ip}] {info4}")
DEBUG(f"[{ip}] {info5}")
DEBUG(f"[{ip}] {info6}")
DEBUG(f"[{ip}] {info7}")
DEBUG(f"[{ip}] {info8}")
DEBUG(f"[{ip}] {info9}")
DEBUG(f"[{ip}] {info10}")
DEBUG(f"[{ip}] {info11}")
DEBUG(f"[{ip}] {info12}")
DEBUG(f"[{ip}] {info13}")
info += self.rtcp(ip, 4028, '{"command": "devdetails"}')
# Bitmain
if "Antminer" in info:
if "S19J Pro" in info:
return ASICs.AntminerS19JPro.value
if "S19J" in info:
return ASICs.AntminerS19J.value
if "S19 Pro" in info:
return ASICs.AntminerS19Pro.value
if "S19" in info:
return ASICs.AntminerS19.value
if "L3+" in info:
return ASICs.AntminerL3plus.value
if "L7" in info:
return ASICs.AntminerL3plus.value
if "T17+" in info:
return ASICs.AntminerT17plus.value
if "T17" in info:
return ASICs.AntminerT17.value
# Avalon
if "Avalon" in info:
if "1066" in info:
return ASICs.Avalon1066.value
if "1126" in info:
return ASICs.Avalon1126.value
# WhatsMiner
if "bitmicro" in info:
if "M20s":
return ASICs.WhatsMinerM20s.value
if "M21s":
return ASICs.WhatsMinerM21s.value
if "M30":
return ASICs.WhatsMinerM30.value
if "M31":
return ASICs.WhatsMinerM31.value
if "M50":
return ASICs.WhatsMinerM50.value
# Innosilicon
if False:
pass
return False
def rtcp(self, ip, port, cmd):
cmd = cmd.encode('utf-8')
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
try:
s.connect((ip, port))
s.sendall(cmd)
tdata = []
while 1:
data = s.recv(1024*32)
if not data: break
tdata.append(data)
data = b''.join(tdata).decode("utf-8")
return data
except OSError as e:
# ERR(e)
return False
def check_AntminerL3(self, ip):
# f'http://{ip}/cgi-bin/get_miner_status.cgi'
# f'http://{ip}/cgi-bin/get_miner_conf.cgi'
summ = self.rtcp(ip, 4028, "summary")
stat = self.rtcp(ip, 4028, "estats")
info = self.rtcp(ip, 4028, "version")
if summ and stat:
mac = json.loads(requests.get(
f'http://{ip}/cgi-bin/get_system_info.cgi',
auth=requests.auth.HTTPDigestAuth('root', 'root')).text)['macaddr'].lower()
log = f"{info} ||| {summ} ||| {stat}"
return log, mac
return False, False
def check_AntminerS19(self, ip):
# f'http://{ip}/cgi-bin/get_miner_status.cgi'
# f'http://{ip}/cgi-bin/get_miner_conf.cgi'
summ = self.rtcp(ip, 4028, "summary")
stat = self.rtcp(ip, 4028, "stats")
info = self.rtcp(ip, 4028, "version")
if summ and stat:
mac = json.loads(requests.get(
f'http://{ip}/cgi-bin/get_network_info.cgi',
auth=requests.auth.HTTPDigestAuth('root', 'root')).text)['macaddr'].lower()
log = f"{info} ||| {summ} ||| {stat}"
return log, mac
return False, False
def check_Avalon1XXX(self, ip):
summ = self.rtcp(ip, 4028, "summary")
stat = self.rtcp(ip, 4028, "estats")
info = self.rtcp(ip, 4028, "version")
if summ and stat:
mac = self.getreg(r'(MAC=([A-z0-9]+))', info)
if mac: mac = ':'.join(mac[i:i+2] for i in range(0,12,2))
else: mac = ':'
log = f"{info} ||| {summ} ||| {stat}"
return log, mac
return False, False
def check(self, ip):
try:
type = self.gettype(ip)
if not type:
return (False, False, False, False, False), False
if type in [ASICs.AntminerL3plus.value]:
log, mac = self.check_AntminerL3(ip)
status = self.getstat_AntminerL3(log, type)
elif type in [ASICs.Avalon1066.value, ASICs.Avalon1126.value]:
log, mac = self.check_Avalon1XXX(ip)
status = self.getstat_Avalon1XXX(log, type)
elif type in [ASICs.AntminerS19.value, ASICs.AntminerS19J.value, ASICs.AntminerS19Pro.value, ASICs.AntminerS19JPro.value]:
log, mac = self.check_AntminerS19(ip)
status = self.getstat_AntminerS19(log, type)
return (ip, mac, type, int(time()), log), status
except Exception as e:
WARN('Minor error in <lambda>Thread: ' + str(e))
return (False, False, False, False, False), False
def checkall(self):
ips = []
with self.conn.cursor(dictionary=True) as c:
c.execute("SELECT * FROM `ips` WHERE `location` = %s", (CONF.get('location'),))
for ip in c.fetchall():
ip = ip['ip']
if ip.isalpha() or not ip.find('-'):
ips.append(ip)
else:
i_p = ip.split('.')
i_p_new = []
for i in i_p:
i_p_new.append(i.split('-'))
i_p_new[-1][0] = int(i_p_new[-1][0])
i_p_new[-1][-1] = int(i_p_new[-1][-1])
for i1 in range(i_p_new[0][0], i_p_new[0][-1] + 1):
for i2 in range(i_p_new[1][0], i_p_new[1][-1] + 1):
for i3 in range(i_p_new[2][0], i_p_new[2][-1] + 1):
for i4 in range(i_p_new[3][0], i_p_new[3][-1] + 1):
ips.append(f"{i1}.{i2}.{i3}.{i4}")
Tca = []
ca = []
for ip in ips:
T = Thread(target=lambda x: ca.append(self.check(x)), args=(ip,))
Tca.append(T)
for i in range(len(Tca)):
Tca[i].start()
for i in range(len(Tca)):
Tca[i].join()
with self.conn.cursor() as c:
c.execute(f"DELETE FROM `laststate` WHERE `location` = %s", (CONF.get('location'),))
for i in ca:
i, status = i
if i[4]:
c.execute(f"INSERT INTO `asiclogs` (`ip`, `mac`, `type`, `time`, `log`) VALUES (%s, %s, %s, %s, %s)", i)
if i[1] == ':':
continue
c.execute(
f"INSERT INTO `laststate` (`ip`, `mac`, `type`, `location`, `status`, `time`) VALUES (%s, %s, %s, %s, %s, %s)",
(i[0], i[1], i[2], CONF.get('location'), status, int(time())))
self.conn.commit()
def run(self):
try:
SUCC("Checker started!")
sleep(5)
self.conn = connect(
host=CONF.get('db', 'host'),
user=CONF.get('db', 'user'),
password=CONF.get('db', 'password'),
database=CONF.get('db', 'name'))
while 1:
if self.event.is_set():
SUCC("Checker stopped!")
break
self.checkall()
sleep(10)
except Exception as e:
CRIT(str(e))
os._exit(1)

3
checkmem Executable file
View File

@ -0,0 +1,3 @@
#!/bin/bash
watch -n 0.1 'ps -o vsize,command,pcpu ax | sort -b -k3 -r | grep -E -i -w "[0-9]+ python"'

43
cleaner.py Normal file
View File

@ -0,0 +1,43 @@
from threading import Thread
from mysql.connector import connect, Error
from time import sleep, time
from macros import *
import os
class NThread(Thread):
conn = None
def __init__(self, event):
super(NThread, self).__init__()
self.event = event
def clean(self):
with self.conn.cursor() as c:
c.execute(f"DELETE FROM `asiclogs` WHERE `time` < UNIX_TIMESTAMP() - %s", (CONF.get('db-log-days'),))
c.execute(f"DELETE FROM `laststate` WHERE `time` < UNIX_TIMESTAMP() - 30")
self.conn.commit()
def run(self):
try:
SUCC("Cleaner started!")
self.conn = connect(
host=CONF.get('db', 'host'),
user=CONF.get('db', 'user'),
password=CONF.get('db', 'password'),
database=CONF.get('db', 'name'))
while 1:
if self.event.is_set():
SUCC('Cleaner stopped!')
break
self.clean()
sleep(5)
except Exception as e:
CRIT(str(e))
os._exit(1)

20
colors.py Normal file
View File

@ -0,0 +1,20 @@
RESET = "\033[0m"
BOLD = "\033[1m"
BLACK = "\033[30m"
RED = "\033[31m"
GREEN = "\033[32m"
YELLOW = "\033[33m"
BLUE = "\033[34m"
MAGENTA = "\033[35m"
CYAN = "\033[36m"
WHITE = "\033[37m"
BGBLACK = "\033[40m"
BGRED = "\033[41m"
BGGREEN = "\033[42m"
BGYELLOW = "\033[43m"
BGBLUE = "\033[44m"
BGMAGENTA = "\033[45m"
BGCYAN = "\033[46m"
BGWHITE = "\033[47m"

73
config.json Normal file
View File

@ -0,0 +1,73 @@
{
"license": "00001111-2222-3333-4444-555566667777",
"location": "Underground",
"logging": true,
"db-log-days": 7,
"debug": true,
"updates": false,
"port": 9070,
"telegram": {
"enable": true,
"channel": -1001871940722,
"token": "5953600362:AAELgZr0Ldstf0omK43zKrAzGXOuMGowcf8",
"normal-notify": true,
"warn-notify": false,
"crit-notify": true
},
"db": {
"host": "127.0.0.1",
"user": "root",
"password": "0",
"name": "myasics"
},
"asics": {
"L3plus": {
"crit": {
"hash": 40000,
"temp": 90,
"rejects": 2.5
},
"warn": {
"hash": 45000,
"temp": 80,
"rejects": 1.5
}
},
"S19": {
"crit": {
"hash": 80000,
"temp": 90,
"rejects": 2.5
},
"warn": {
"hash": 85000,
"temp": 80,
"rejects": 1.5
}
},
"Avalon1066": {
"crit": {
"hash": 42000,
"temp": 90,
"rejects": 2.5
},
"warn": {
"hash": 46000,
"temp": 80,
"rejects": 1.5
}
},
"Avalon1126": {
"crit": {
"hash": 64000,
"temp": 90,
"rejects": 2.5
},
"warn": {
"hash": 66000,
"temp": 80,
"rejects": 1.5
}
}
}
}

3
config.py Normal file
View File

@ -0,0 +1,3 @@
APPNAME='Bit.ASICmon'
VERSION='v0.1.1a'
DEBUGGING=True

12
docker-compose.yml Normal file
View File

@ -0,0 +1,12 @@
version: '3.1'
services:
db:
image: mysql
command: --default-authentication-plugin=mysql_native_password
restart: always
environment:
MYSQL_ROOT_PASSWORD: 0
ports:
- 3306:3306

223
index.js Normal file
View File

@ -0,0 +1,223 @@
const sprintf = (...[string, ...args]) => {
return string.replace(/{(\d+)}/g, (match, number) => args[number] ?? match);
}
const getJSON = async url => {
const resp = await fetch(url)
const json = await resp.json()
return json;
}
const createPopup = async id => {
const asics = await getJSON('/api/asictypes')
asicoptions = ''
asics.forEach(elem => {
asicoptions += sprintf('<option value="{0}">{1}</option>', elem['key'], elem['value'])
})
document.querySelector('body').innerHTML += sprintf(popup, id, asicoptions, 'content')
}
const deletePopup = id => {
document.querySelector('#popup-' + id).remove()
}
const update = async () => {
let inner = await getStats()
document.querySelector('.grid').innerHTML = inner
}
const datafn = async () => {
// return await getJSON('/api/webinit')
}
const data = datafn()
console.log(data)
const cols = 40
const rows = 40
const css =
`<style>
@import url('https://fonts.googleapis.com/css2?family=Source+Code+Pro:wght@400;500;600;700&display=swap');
* {
margin: 0;
padding: 0;
box-sizing: border-box;
font-family: 'Source Code Pro', monospace;
}
body {
background: #000;
}
input,
select {
border: 1px solid #fff;
background: #000;
color: #fff;
padding: 5px 10px;
}
.green {
background: #0b0 !important;
}
.yellow {
background: #ff0 !important;
}
.red {
background: #f11 !important;
}
.darkred {
background: #300 !important;
}
.header {
height: 60px;
background: #222;
text-align: center;
margin: 0 0 1px 0;
display: grid;
justify-items: center;
align-content: center;
}
.header-info {
color: #bbb;
font-weight: 700;
}
.grid {
display: grid;
grid-template-columns: repeat(${cols}, 30px);
grid-template-rows: repeat(${rows}, 30px);
grid-gap: 1px;
margin: auto;
width: max-content;
}
.cell {
position: relative;
background: #111;
font-size: 9px;
}
.cell:hover {
cursor: pointer;
}
.cell:hover::after {
content: '';
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
pointer-events: none;
background: #ffffff33;
}
.popup {
position: fixed;
top: 0;
left: 0;
width: 100vw;
height: 100vh;
display: grid;
justify-items: center;
align-content: center;
}
.popup-bg {
position: absolute;
width: 100%;
height: 100%;
background: #00000033;
z-index: 98;
}
.popup-window {
position: relative;
height: max-content;
max-height: 90vh;
width: max-content;
max-width: 90vw;
padding: 40px;
background: #222;
z-index: 99;
}
</style>
`
const header =
'<div class="header">'
+ '<div class="header-info">'
+ 'Всего устройств: {0}, Предупреждений, {1}, Ошибки: {2}, В сети: {3}, Не в сети: {4}'
+ '</div>'
+ '</div>'
const cell =
'<div class="cell {1}" id="cell-{0}" onclick="createPopup({0})">'
+ '{2}'
+ '</div>'
const grid =
'<div class="grid">'
+ '{0}'
+ '</div>'
const popup =
'<div class="popup" id="popup-{0}">'
+ '<div class="popup-bg" onclick="deletePopup({0})"></div>'
+ '<div class="popup-window">'
+ '<div class="row">'
+ '<select>'
+ '<option value="none">Свободно</option>'
+ '{1}'
+ '</select>'
+ '<input type="text" placeholder="Адрес...">'
+ '</div>'
+ '{2}'
+ '</div>'
+ '</div>'
const getStats = async () => {
let cells = ''
let info = await getJSON('/curstatus.json')
for(let i = 1; i <= cols * rows; i++) {
if(info.asics[i]?.status == "ok")
cells += sprintf(cell, i, 'green', info.asics[i]?.hashrate)
else if(info.asics[i]?.status == "warn")
cells += sprintf(cell, i, 'yellow', info.asics[i]?.hashrate)
else if(info.asics[i]?.status == "crit")
cells += sprintf(cell, i, 'red', info.asics[i]?.hashrate)
else if(info.asics[i]?.status == "off")
cells += sprintf(cell, i, 'darkred', '')
else
cells += sprintf(cell, i, '', '')
}
return cells
}
const run = async () => {
document.querySelector('body').innerHTML += css
document.querySelector('body').innerHTML += sprintf(header)
let inner = sprintf(grid, await getStats())
document.querySelector('body').innerHTML += inner
setInterval(async () => {
await update()
}, 10000)
}
run()

76
license.py Normal file
View File

@ -0,0 +1,76 @@
from threading import Thread
from time import sleep, time
from macros import *
from Crypto.Cipher import AES
from Crypto.Random import get_random_bytes
from hashlib import sha256
from base64 import b64decode
from datetime import datetime
from urllib import request
import os, json, requests
class LThread(Thread):
_FIRST = True
def __init__(self, event):
super(LThread, self).__init__()
self.event = event
def check(self):
url = 'https://license.bitheaven.ru/api/v1/license.check'
post = {
'key': CONF.get('license'),
'loc': CONF.get('location'),
'time': int(time()),
'data': get_random_bytes(16)
}
curtime = post['time']
try:
req = requests.post(url, data=post).json()
except requests.exceptions.RequestException as e:
return False
if req['error']:
ERR(f"Server error! {req['msg']}")
return True
key = sha256(post['loc'].encode('utf-8')).hexdigest()[0:32].encode("utf-8")
iv = post['data']
aes = AES.new(key, AES.MODE_OFB, iv=iv)
msg = aes.decrypt(b64decode(req['data'])).decode('unicode_escape').strip()
until = datetime.fromtimestamp(req['until']).strftime('%d.%m.%Y')
if self._FIRST:
INFO(f'License paid until: {until}')
self._FIRST = False
return f"{CONF.get('license')}{curtime}" == msg
def run(self):
try:
while 1:
if self.event.is_set():
break
cur = self.check()
if not cur:
try:
request.urlopen('http://1.1.1.1', timeout=2)
CRIT("License wrong or expired!")
WARN("Get it on https://bhev.ru/glfbam")
sleep(60)
except:
ERR("No access to network!")
# os._exit(1)
sleep(5)
except Exception as e:
CRIT(str(e))
os._exit(1)

BIN
log/.2023-05-10.log.swp Normal file

Binary file not shown.

97
macros.py Normal file
View File

@ -0,0 +1,97 @@
from datetime import datetime
from colors import *
from platform import system
import inspect
import json, os, config
__DEBUGGING_ALERT = False
MUSTDIE = system().lower() == 'windows'
LINUX = system().lower() == 'linux'
MACOS = system().lower() == 'darwin'
class CONF():
def __init__(self):
CONF.config = json.load(open('config.json'))
@staticmethod
def get(*params):
conf = CONF.config
for param in params:
if param in conf.keys():
conf = conf[param]
if conf:
return conf
return False
CONF()
def __GET_TIME():
return datetime.now().strftime("%H:%M:%S.%f")
def __GET_DATE():
return datetime.now().strftime("%Y-%m-%d")
def __ADD_TO_LOG(str):
if CONF.get('logging'):
if not os.path.isdir('log'):
os.mkdir('log')
log = open(f'log/{__GET_DATE()}.log', 'a')
log.write(str + '\n')
log.close()
def __PRINT_LOG(str, log):
if CONF.get('debug') and config.DEBUGGING:
st = inspect.stack()[2]
caller = st.filename.split('/')[-1].split('.')[0]
callerline = st.lineno
str = f'[{__GET_TIME()}] [{caller}:{callerline}] {str}'
log = f'[{caller}:{callerline}] {log}'
else:
str = f'[{__GET_TIME()}] {str}'
print(str)
__ADD_TO_LOG(log)
def INFO(s = ''):
__PRINT_LOG(f"[INFO] {str(s)}{RESET}", str(s))
def DEBUG(s = ''):
global __DEBUGGING_ALERT
if not CONF.get('debug'):
return None
if config.DEBUGGING:
__PRINT_LOG(f"{MAGENTA}[DEBUG]{RESET} {BGBLUE}{YELLOW}{BOLD}{str(s)}{RESET}", str(s))
elif not __DEBUGGING_ALERT:
WARN('DEBUGGING DISABLED BY OWNER!')
__DEBUGGING_ALERT = True
def SUCC(s = ''):
__PRINT_LOG(f"{GREEN}[SUCCESS]{RESET} {GREEN}{str(s)}{RESET}", str(s))
def WARN(s = ''):
__PRINT_LOG(f"{YELLOW}[WARN]{RESET} {YELLOW}{str(s)}{RESET}", str(s))
def ERR(s = ''):
__PRINT_LOG(f"{RED}[ERROR]{RESET} {RED}{str(s)}{RESET}", str(s))
def CRIT(s = ''):
__PRINT_LOG(f"{RED}{BOLD}[CRITICAL]{RESET} {BGRED}{WHITE} {str(s)} {RESET}", str(s))

95
main.py Executable file
View File

@ -0,0 +1,95 @@
#!/usr/bin/python
from updater import UThread
from license import LThread
from checker import CThread
from cleaner import NThread
from telegrambot import TGThread
from webserver import WSThread
from time import sleep
from macros import *
from colors import *
from threading import Event
import config, sys, os, asyncio
INFO('' + ''*(len(config.APPNAME) + len(config.VERSION) + 3) + '')
INFO(f"{CYAN}{BOLD}{config.APPNAME}{RESET} {config.VERSION}")
INFO('' + ''*(len(config.APPNAME) + len(config.VERSION) + 3) + '')
if CONF.get('debug'):
INFO('DEBUG TEST START ' + '*' * 20)
DEBUG(f"{RESET}{BLACK}*{RED}*{GREEN}*{YELLOW}*{BLUE}*{MAGENTA}*{CYAN}*{WHITE}*")
DEBUG(f"{RESET}{BOLD}{BLACK}*{RED}*{GREEN}*{YELLOW}*{BLUE}*{MAGENTA}*{CYAN}*{WHITE}*")
DEBUG(f"{RESET}{BGBLACK}*{BGRED}*{BGGREEN}*{BGYELLOW}*{BGBLUE}*{BGMAGENTA}*{BGCYAN}*{BGWHITE}*")
INFO('Test message')
DEBUG('Test message')
SUCC('Test message')
WARN('Test message')
ERR('Test message')
CRIT('Test message')
INFO('DEBUG TEST END ' + '*' * 22 + RESET)
eupdate = Event()
estop = Event()
lt = LThread(eupdate)
lt.daemon = True
lt.start()
wt = WSThread(eupdate)
wt.daemon = True
wt.start()
ct = CThread(eupdate)
ct.daemon = True
ct.start()
nt = NThread(eupdate)
nt.daemon = True
nt.start()
tt = TGThread(eupdate, estop)
tt.daemon = True
tt.start()
ut = UThread()
ut.daemon = True
ut.start()
updated = False
try:
while 1:
if lt.is_alive() and wt.is_alive() and ct.is_alive() and tt.is_alive() and ut.is_alive() and nt.is_alive():
sleep(1)
elif lt.is_alive() and wt.is_alive() and ct.is_alive() and tt.is_alive() and nt.is_alive():
eupdate.set()
if lt.is_alive(): lt.join()
if wt.is_alive(): wt.join()
if ct.is_alive(): ct.join()
if nt.is_alive(): nt.join()
if tt.is_alive(): tt.join()
updated = True
SUCC('Program updated, reboot!')
break
else:
estop.set()
sleep(1)
CRIT('Program broken.')
os._exit(3)
except KeyboardInterrupt:
sys.stdout.flush()
print('\r', end='')
estop.set()
sleep(1)
CRIT('STOP')
except Exception as e:
CRIT(str(e))
estop.set()
sleep(1)
os._exit(1)
if updated:
os.execl(sys.argv[0], *sys.argv)

4
requirements.txt Normal file
View File

@ -0,0 +1,4 @@
flask
python-telegram-bot
pyinstaller
pyarmor<8.0.0

125
telegrambot.py Normal file
View File

@ -0,0 +1,125 @@
from threading import Thread
from telegram import Bot, error
from asics import ASICs
from time import sleep
from mysql.connector import connect, Error
from macros import *
import os, json, asyncio
class TGThread(Thread):
conn = None
bot = None
asics = {}
def __init__(self, event, event2):
super(TGThread, self).__init__()
self.event = event
self.event2 = event2
async def sendmsg(self, msg):
return await self.bot.send_message(CONF.get('telegram', 'channel'), msg, 'MarkdownV2')
async def check(self):
msg = []
for i in self.asics.keys():
self.asics[i]['tooff'] -= 1
with self.conn.cursor(dictionary=True) as c:
c.execute(f"SELECT * FROM `laststate`")
for i in c.fetchall():
if not i['mac'] in self.asics.keys():
self.asics[i['mac']] = {}
self.asics[i['mac']]['alert'] = 0
msg.append(f"🟢 {i['location']} \| ASIC \"*{ASICs(i['type']).name}*\" `{i['ip']}` online\!")
self.asics[i['mac']]['tooff'] = 3
self.asics[i['mac']]['status'] = i['status']
if i['status'] == 'crit':
if CONF.get('telegram', 'crit-notify') and self.asics[i['mac']]['alert'] < 2:
msg.append(f"🔴 {i['location']} \| ASIC \"*{ASICs(i['type']).name}*\" `{i['ip']}` crit status\!")
self.asics[i['mac']]['alert'] = 2
elif i['status'] == 'warn':
if CONF.get('telegram', 'warn-notify') and self.asics[i['mac']]['alert'] < 1:
msg.append(f"🟡 {i['location']} \| ASIC \"*{ASICs(i['type']).name}*\" `{i['ip']}` warn status\!")
self.asics[i['mac']]['alert'] = 1
elif i['status'] == 'ok':
if CONF.get('telegram', 'normal-notify') and self.asics[i['mac']]['alert'] != 0:
msg.append(f"🟢 {i['location']} \| ASIC \"*{ASICs(i['type']).name}*\" `{i['ip']}` normal status\!")
self.asics[i['mac']]['alert'] = 0
if self.asics[i['mac']]['tooff'] <= 0:
msg.append(f"🔴 {i['location']} \| ASIC \"*{ASICs(i['type']).name}*\" `{i['ip']}` offline\!")
del self.asics[i['mac']]
if len(msg) > 0:
await self.sendmsg('\n'.join(msg))
async def runbot(self):
try:
res = await self.sendmsg('🟢 *Notify server online\!*')
INFO(f"Telegram channel ID: {res['chat']['id']}")
while 1:
if self.event.is_set():
SUCC('Telegram stopped!')
await self.sendmsg('🟡 Updating software\! Recommend check all running instances.')
break
await self.check()
await asyncio.sleep(15)
except error.Forbidden as e:
WARN(str(e))
except error.TimedOut as e:
WARN(str(e))
except Exception as e:
CRIT(str(e))
# os._exit(1)
async def stopalert(self):
while 1:
if self.event2.is_set():
await self.sendmsg('🔴 *Notify server offline\!*')
await asyncio.sleep(0.5)
async def main(self):
await asyncio.gather(self.stopalert(), self.runbot())
def run(self):
try:
if CONF.get('telegram', 'enable'):
SUCC('Telegram started!')
self.conn = connect(
host=CONF.get('db', 'host'),
user=CONF.get('db', 'user'),
password=CONF.get('db', 'password'),
database=CONF.get('db', 'name'))
self.conn.autocommit = True
self.bot = Bot(CONF.get('telegram', 'token'))
asyncio.run(self.main())
else:
while 1:
if self.event.is_set():
SUCC('Telegram stopped!')
break
sleep(5)
except Exception as e:
CRIT(str(e))
os._exit(1)

54
updater.py Normal file
View File

@ -0,0 +1,54 @@
from threading import Thread
from macros import *
from config import *
from time import sleep
import urllib, os, requests
class UThread(Thread):
path = os.path.dirname(__file__)
def __init__(self):
super(UThread, self).__init__()
def run(self):
global VERSION
try:
SUCC('Updater started!')
while 1:
if not CONF.get('updates'):
sleep(1)
continue
version = requests.get('https://mirror.bitheaven.ru/main/versions/Bit.ASICmon-a').text
if version == VERSION:
sleep(300)
continue
INFO('Update found! Downloading...')
if LINUX:
with urllib.request.urlopen("https://mirror.bitheaven.ru/main/archive/Bit.ASICmon-a_linux") as upd:
with open(self.path, "wb+") as f:
INFO('Installing update...')
f.write(upd.read())
INFO('Stopping process...')
break
elif MACOS:
with urllib.request.urlopen("https://mirror.bitheaven.ru/main/archive/Bit.ASICmon-a_macos") as upd:
with open(self.path, "wb+") as f:
INFO('Installing update...')
f.write(upd.read())
INFO('Stopping process...')
break
elif MUSTDIE:
with urllib.request.urlopen("https://mirror.bitheaven.ru/main/archive/Bit.ASICmon-a_mustdie.exe") as upd:
with open(self.path, "wb+") as f:
INFO('Installing update...')
f.write(upd.read())
INFO('Stopping process...')
break
except Exception as e:
CRIT(str(e))
os._exit(1)

107
webserver.py Normal file
View File

@ -0,0 +1,107 @@
from flask import Flask, send_file, request
from time import sleep
from werkzeug.serving import make_server
from threading import Thread
from mysql.connector import connect, Error
from macros import *
import os, json, config, requests, logging
class WSThread(Thread):
app = Flask(__name__)
conn = None
def __init__(self, event):
super(WSThread, self).__init__()
self.event = event
log = logging.getLogger('werkzeug')
log.disabled = True
@app.route('/')
@app.route('/index')
def index():
return '<meta charset="UTF-8"><script defer src="/index.js"></script>'
@app.route('/index.js')
def indexjs():
return send_file('index.js')
@app.route('/curstatus.json')
def curstatus():
j = {}
j['info'] = {}
j['info']['asics'] = {}
return json.dumps(j), 200, {'ContentType':'application/json'}
def dbinit(self):
with self.conn.cursor() as c:
c.execute("""
CREATE TABLE IF NOT EXISTS `ips` (
`id` INT AUTO_INCREMENT PRIMARY KEY,
`ip` VARCHAR(64) NOT NULL,
`location` VARCHAR(64) NOT NULL
)
""")
c.execute("""
CREATE TABLE IF NOT EXISTS `laststate` (
`id` INT AUTO_INCREMENT PRIMARY KEY,
`ip` VARCHAR(64) NOT NULL,
`mac` VARCHAR(32) NOT NULL,
`type` INT(11) NOT NULL,
`location` VARCHAR(64) NOT NULL,
`status` VARCHAR(32) NOT NULL,
`time` INT(11) NOT NULL
)
""")
c.execute("""
CREATE TABLE IF NOT EXISTS `asiclogs` (
`id` INT AUTO_INCREMENT PRIMARY KEY,
`ip` VARCHAR(64) NOT NULL,
`mac` VARCHAR(64) NOT NULL,
`type` VARCHAR(64) NOT NULL,
`time` INT(11) NOT NULL,
`log` TEXT NOT NULL
)
""")
self.conn.commit()
def runweb(self):
self.server = make_server('0.0.0.0', CONF.get('port'), self.app)
self.server.serve_forever()
def run(self):
try:
with connect(host=CONF.get('db', 'host'), user=CONF.get('db', 'user'), password=CONF.get('db', 'password')) as conn:
with conn.cursor() as c:
c.execute(f"CREATE DATABASE IF NOT EXISTS {CONF.get('db', 'name')}")
self.conn = connect(
host=CONF.get('db', 'host'),
user=CONF.get('db', 'user'),
password=CONF.get('db', 'password'),
database=CONF.get('db', 'name'))
self.dbinit()
web = Thread(target=self.runweb)
web.daemon = True
web.start()
SUCC(f"Web interface started at port {CONF.get('port')}!")
while not self.event.is_set():
sleep(1)
SUCC(f"Web server stopped!")
self.server.shutdown()
except Exception as e:
CRIT(str(e))
os._exit(1)