Compare commits

...

7 Commits

Author SHA1 Message Date
Brett Rowan
cd52d3aeaf version: bump version number 2025-09-23 12:40:02 -06:00
Brett Rowan
66c9b3663e feature: add serial number for antminers 2025-09-23 12:39:16 -06:00
James Hilliard
5f0e1da938 Update dependencies 2025-09-22 18:34:03 -06:00
Brett Rowan
2bd031b33d version: bump version number 2025-09-19 15:49:29 -06:00
James Hilliard
e2f07818cc Fix race conditions in RPC and web API multicommand methods
Multiple multicommand methods were double-awaiting tasks - first via
asyncio.gather() with return_exceptions=True, then calling .result() on
the same tasks. This caused ConnectionResetError and other exceptions
when connections were lost.

Changed to properly use the results from gather() instead of calling
.result() on completed tasks, preventing exceptions from being raised
after they were already caught.

Fixed in:
- pyasic/rpc/base.py:144 - RPC _send_split_multicommand
- pyasic/web/espminer.py:79 - ESPMiner multicommand
- pyasic/web/auradine.py:149 - Auradine multicommand
2025-09-19 14:31:33 -06:00
Brett Rowan
75056cfff5 version: bump version number 2025-09-19 14:18:36 -06:00
James Hilliard
7fbcb0dbd2 Fix race condition in BOSer multicommand causing CancelledError
The multicommand method was double-awaiting tasks - first via
asyncio.gather() with return_exceptions=True, then trying to await
the same tasks again. This caused CancelledError when gRPC connections
were lost.

Changed to properly use the results from gather() instead of
re-awaiting completed tasks, preventing the race condition and
properly handling exceptions.

Fixes StreamTerminatedError occurring in pyasic/web/braiins_os/boser.py:91
2025-09-19 14:17:59 -06:00
12 changed files with 547 additions and 507 deletions

View File

@@ -5,13 +5,13 @@ ci:
- generate-docs
repos:
- repo: https://github.com/python-poetry/poetry
rev: 2.1.2
rev: 2.2.1
hooks:
- id: poetry-check
- id: poetry-lock
- id: poetry-install
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v5.0.0
rev: v6.0.0
hooks:
- id: trailing-whitespace
- id: check-yaml
@@ -23,7 +23,7 @@ repos:
exclude: ^mkdocs\.yml$
- id: check-added-large-files
- repo: https://github.com/psf/black
rev: 25.1.0
rev: 25.9.0
hooks:
- id: black
- repo: https://github.com/pycqa/isort

968
poetry.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -83,6 +83,7 @@ class MinerData(BaseModel):
# about
device_info: DeviceInfo | None = None
serial_number: str | None = None
mac: str | None = None
api_ver: str | None = None
fw_ver: str | None = None

View File

@@ -39,6 +39,10 @@ from pyasic.web.antminer import AntminerModernWebAPI, AntminerOldWebAPI
ANTMINER_MODERN_DATA_LOC = DataLocations(
**{
str(DataOptions.SERIAL_NUMBER): DataFunction(
"_get_serial_number",
[WebAPICommand("web_get_system_info", "get_system_info")],
),
str(DataOptions.MAC): DataFunction(
"_get_mac",
[WebAPICommand("web_get_system_info", "get_system_info")],
@@ -360,6 +364,21 @@ class AntminerModern(BMMiner):
except LookupError:
pass
async def _get_serial_number(
self, web_get_system_info: dict = None
) -> Optional[str]:
if web_get_system_info is None:
try:
web_get_system_info = await self.web.get_system_info()
except APIError:
pass
if web_get_system_info is not None:
try:
return web_get_system_info["serinum"]
except LookupError:
pass
async def set_static_ip(
self,
ip: str,

View File

@@ -209,6 +209,14 @@ class MinerProtocol(Protocol):
### DATA GATHERING FUNCTIONS (get_{some_data}) ###
##################################################
async def get_serial_number(self) -> Optional[str]:
"""Get the serial number of the miner and return it as a string.
Returns:
A string representing the serial number of the miner.
"""
return await self._get_serial_number()
async def get_mac(self) -> Optional[str]:
"""Get the MAC address of the miner and return it as a string.
@@ -379,6 +387,9 @@ class MinerProtocol(Protocol):
"""
return await self._get_pools()
async def _get_serial_number(self) -> Optional[str]:
pass
async def _get_mac(self) -> Optional[str]:
pass

View File

@@ -20,6 +20,7 @@ from typing import List, Union
class DataOptions(Enum):
SERIAL_NUMBER = "serial_number"
MAC = "mac"
API_VERSION = "api_ver"
FW_VERSION = "fw_ver"

View File

@@ -136,17 +136,16 @@ class BaseMinerRPCAPI:
self.send_command(cmd, allow_warning=allow_warning)
)
await asyncio.gather(*[tasks[cmd] for cmd in tasks], return_exceptions=True)
results = await asyncio.gather(
*[tasks[cmd] for cmd in tasks], return_exceptions=True
)
data = {}
for cmd in tasks:
try:
result = tasks[cmd].result()
for cmd, result in zip(tasks.keys(), results):
if not isinstance(result, (APIError, Exception)):
if result is None or result == {}:
result = {}
data[cmd] = [result]
except APIError:
pass
return data

View File

@@ -141,17 +141,16 @@ class AuradineWebAPI(BaseWebAPI):
self.send_command(cmd, allow_warning=allow_warning)
)
await asyncio.gather(*[tasks[cmd] for cmd in tasks], return_exceptions=True)
results = await asyncio.gather(
*[tasks[cmd] for cmd in tasks], return_exceptions=True
)
data = {"multicommand": True}
for cmd in tasks:
try:
result = tasks[cmd].result()
for cmd, result in zip(tasks.keys(), results):
if not isinstance(result, (APIError, Exception)):
if result is None or result == {}:
result = {}
data[cmd] = result
except APIError:
pass
return data

View File

@@ -84,13 +84,13 @@ class BOSerWebAPI(BaseWebAPI):
except AttributeError:
pass
await asyncio.gather(*[t for t in tasks.values()], return_exceptions=True)
results = await asyncio.gather(
*[t for t in tasks.values()], return_exceptions=True
)
for cmd in tasks:
try:
result[cmd] = await tasks[cmd]
except (GRPCError, APIError, ConnectionError):
pass
for cmd, task_result in zip(tasks.keys(), results):
if not isinstance(task_result, (GRPCError, APIError, ConnectionError)):
result[cmd] = task_result
return result

View File

@@ -71,17 +71,16 @@ class ESPMinerWebAPI(BaseWebAPI):
self.send_command(cmd, allow_warning=allow_warning)
)
await asyncio.gather(*[tasks[cmd] for cmd in tasks], return_exceptions=True)
results = await asyncio.gather(
*[tasks[cmd] for cmd in tasks], return_exceptions=True
)
data = {"multicommand": True}
for cmd in tasks:
try:
result = tasks[cmd].result()
for cmd, result in zip(tasks.keys(), results):
if not isinstance(result, (APIError, Exception)):
if result is None or result == {}:
result = {}
data[cmd] = result
except APIError:
pass
return data

View File

@@ -1,6 +1,6 @@
[project]
name = "pyasic"
version = "0.76.7"
version = "0.77.0"
description = "A simplified and standardized interface for Bitcoin ASICs."
authors = [{name = "UpstreamData", email = "brett@upstreamdata.ca"}]

View File

@@ -92,6 +92,7 @@ class MinersTest(unittest.TestCase):
"hashrate",
"hostname",
"is_mining",
"serial_number",
"mac",
"expected_hashrate",
"uptime",