import asyncio
import logging
import os
import pygtail
import random
import socket
import ssl
import capnp
import hidiocore.schema.hidio_capnp as hidio_capnp
logger = logging.getLogger(__name__)
class NodesSubscriberImpl(hidio_capnp.HidIo.NodesSubscriber.Server):
def __init__(self, hidioclient):
hidio_capnp.HidIo.NodesSubscriber.Server.__init__(self)
self.hidioclient = hidioclient
def nodesUpdate(self, nodes, **kwargs):
self.hidioclient.on_nodesupdate(nodes)
class HidIoClient:
AUTH_NONE = 'None'
AUTH_BASIC = 'Basic'
AUTH_ADMIN = 'Admin'
def __init__(self, client_name):
self.retry_task = None
self.retry_connection = True
self.addr = None
self.port = None
self.ctx = None
self.reader = None
self.writer = None
self.client = None
self.cap = None
self.overalltasks = []
self.auth = self.AUTH_NONE
self.loop = None
self.client_name = client_name
self.version_info = None
self.daemon_name = None
self.core_log_files = None
self.core_current_log_file = None
self.serial = "{} - pid:{}".format(random.getrandbits(64), os.getpid())
def __del__(self):
if not self.loop:
self.loop = asyncio.get_event_loop()
asyncio.ensure_future(self.disconnect(), loop=self.loop)
async def socketreader(self):
while self.retry_task:
try:
data = await asyncio.wait_for(
self.reader.read(4096),
timeout=5.0
)
except asyncio.TimeoutError:
logger.debug("socketreader timeout.")
continue
except Exception as err:
logger.error("Unknown socketreader err: %s", err)
return False
self.client.write(data)
logger.debug("socketreader done.")
return True
async def socketwriter(self):
while self.retry_task:
try:
data = await asyncio.wait_for(
self.client.read(4096),
timeout=5.0
)
self.writer.write(data.tobytes())
except asyncio.TimeoutError:
logger.debug("socketwriter timeout.")
continue
except Exception as err:
logger.error("Unknown socketwriter err: %s", err)
return False
logger.debug("socketwriter done.")
return True
async def socketwatcher(self):
while self.retry_task:
try:
await asyncio.wait_for(
self.cap.alive().a_wait(),
timeout=1.0
)
logger.debug("Server connection ok.")
await asyncio.sleep(2)
except asyncio.TimeoutError:
logging.debug("Server connection failed, disconnecting.")
await self.disconnect(retry_connection=True)
return False
except Exception as err:
logger.error("Unknown socketwatcher err: %s", err)
return False
logger.debug("socketwatcher done.")
return True
async def nodeswatcher(self):
try:
subscriber = NodesSubscriberImpl(self)
promise = (await self.auth_promise().a_wait()).port.subscribeNodes(subscriber)
await promise.a_wait()
while self.retry_task:
await asyncio.sleep(1)
except Exception as err:
logger.error("Unknown nodeswatcher err: %s", err)
return False
logger.debug("nodeswatcher done.")
return True
def reset_corelog_followposition(self):
try:
os.remove(self.core_current_log_file_offset)
except Exception as err:
logger.warn("No hid-io-core log offset file: %s", err)
async def corelogwatcher(self):
while self.retry_task:
if self.core_current_log_file:
try:
for line in pygtail.Pygtail(
self.core_current_log_file,
offset_file=self.core_current_log_file_offset
):
self.on_core_log_entry(line)
except Exception as err:
logger.error(err)
await asyncio.sleep(0.5)
else:
await asyncio.sleep(1)
async def socketconnection(self):
self.retry_task = True
self.ctx = ssl.SSLContext(protocol=ssl.PROTOCOL_TLSv1_2)
try:
logging.debug("Try IPv4 (may autodetect IPv6)")
self.reader, self.writer = await asyncio.wait_for(
asyncio.open_connection(
self.addr, self.port,
ssl=self.ctx,
),
timeout=1.0,
)
except (asyncio.TimeoutError, OSError):
logging.debug("Try IPv6")
try:
self.reader, self.writer = await asyncio.wait_for(
asyncio.open_connection(
self.addr, self.port,
ssl=self.ctx,
family=socket.AF_INET6
),
timeout=1.0,
)
except (asyncio.TimeoutError, OSError):
logger.debug(
"Retrying port connection {}:{} auth level {}".format(
self.addr,
self.port,
self.auth
)
)
return False
self.overalltasks = []
logging.debug("Backgrounding socket reader and writer functions")
coroutines = [self.socketreader(), self.socketwriter()]
self.overalltasks.append(asyncio.gather(*coroutines, return_exceptions=True))
logging.debug("Starting TwoPartyClient")
self.client = capnp.TwoPartyClient()
logging.debug("Starting Bootstrap")
self.cap = self.client.bootstrap().cast_as(hidio_capnp.HidIoServer)
logging.debug("Backgrounding socketwatcher")
watcher = [self.socketwatcher()]
self.overalltasks.append(asyncio.gather(*watcher, return_exceptions=True))
logging.debug("Backgrounding corelogwatcher")
watcher = [self.corelogwatcher()]
self.overalltasks.append(asyncio.gather(*watcher, return_exceptions=True))
self.version_info = (await self.cap.version().a_wait()).version
logger.info(self.version_info)
self.uid_info = (await self.cap.id().a_wait()).id
logger.info("uid: %s", self.uid_info)
self.daemon_name = (await self.cap.name().a_wait()).name
logger.info("name: %s", self.daemon_name)
self.core_log_files = (await self.cap.logFiles().a_wait()).paths
logger.info("hid-io-core log files: %s", self.core_log_files)
try:
self.core_current_log_file = [f for f in self.core_log_files if 'rCURRENT' in f][0]
self.core_current_log_file_offset = "{}.offset.{}".format(
self.core_current_log_file,
self.uid_info
)
self.reset_corelog_followposition()
except IndexError:
self.core_current_log_file = None
logger.warn("Could not find current hid-io-core log file...")
cap_auth = None
if self.auth:
self.key_info = (await self.cap.key().a_wait()).key
logger.info(self.key_info)
key_lookup = {
self.AUTH_BASIC: self.key_info.basicKeyPath,
self.AUTH_ADMIN: self.key_info.authKeyPath,
}
key_location = key_lookup[self.auth]
self.key = None
try:
with open(key_location, 'r') as myfile:
self.key = myfile.read()
except OSError as err:
logger.error("Could not read keyfile. This usually means insufficient permissions.")
logger.error(err)
await self.disconnect()
return False
logger.info("Key: %s", self.key)
cap_auth = await self.capability_authenticate()
if not cap_auth:
await self.disconnect()
return False
logger.debug("Authenticated with %s", self.auth)
background_tasks = [self.nodeswatcher()]
self.overalltasks.append(asyncio.gather(*background_tasks, return_exceptions=True))
await self.on_connect(self.cap, cap_auth)
while self.retry_task:
await asyncio.sleep(1)
logger.debug("socketconnection done.")
async def connect(self, auth=AUTH_NONE, addr='localhost', port='7185'):
self.addr = addr
self.port = port
self.auth = auth
self.loop = asyncio.get_event_loop()
logger.info("Connecting to {}:{} with auth level {}".format(self.addr, self.port, self.auth))
self.retry_task = True
self.retry_connection = True
while self.retry_connection:
try:
await self.socketconnection()
except Exception as err:
logger.error("Unhandled Exception")
logger.error(err)
await asyncio.sleep(1)
logger.debug("connect retry: %s", self.retry_connection)
self.loop = None
logger.debug("Connection ended")
async def disconnect(self, retry_connection=False):
logger.info("Disconnecting from {}:{} (auth level {})".format(self.addr, self.port, self.auth))
await self.on_disconnect()
if not retry_connection:
self.retry_connection = False
self.retry_task = False
logger.debug("Tasks open: %s", len(self.overalltasks))
for index, task in enumerate(self.overalltasks):
logger.debug("Ending task: %s", index)
await task
self.reader = None
self.writer = None
self.ctx = None
self.client = None
self.cap = None
self.version_info = None
self.daemon_name = None
if retry_connection:
logger.debug("Retrying connection.")
return
logger.debug("Stopping client.")
async def on_connect(self, cap, cap_auth):
async def on_disconnect(self):
def on_nodesupdate(self, nodes):
def on_core_log_entry(self, entry):
def capability_hidioserver(self):
return self.cap
def auth_promise(self):
if self.auth:
key_usage = {
self.AUTH_BASIC: self.cap.basic_request(),
self.AUTH_ADMIN: self.cap.auth_request(),
}
request = key_usage[self.auth]
request.key = self.key
request.info.type = 'hidioApi'
request.info.name = self.client_name
request.info.serial = self.serial
request.info.id = self.uid_info
return request.send()
return None
async def capability_authenticate(self):
if self.auth:
promise = self.auth_promise()
try:
cap_auth = (await promise.a_wait()).port
except Exception:
logger.error("Invalid auth key!")
return None
return cap_auth
return None
def retry_connection_status(self):
return self.retry_connection
def version(self):
return self.version_info
def name(self):
return self.daemon_name
async def nodes(self):
if self.auth:
nodes = (
await self.auth_promise().port.nodes().a_wait()
)
logger.info("nodes: %s", nodes)
return nodes
return []