Reticulum/RNS/Interfaces/PipeInterface.py
2024-10-08 22:03:30 +02:00

189 lines
6.3 KiB
Python

# MIT License
#
# Copyright (c) 2016-2022 Mark Qvist / unsigned.io
#
# Permission is hereby granted, 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.
from .Interface import Interface
from time import sleep
import sys
import threading
import time
import RNS
import subprocess
import shlex
class HDLC():
# The Pipe Interface packetizes data using
# simplified HDLC framing, similar to PPP
FLAG = 0x7E
ESC = 0x7D
ESC_MASK = 0x20
@staticmethod
def escape(data):
data = data.replace(bytes([HDLC.ESC]), bytes([HDLC.ESC, HDLC.ESC^HDLC.ESC_MASK]))
data = data.replace(bytes([HDLC.FLAG]), bytes([HDLC.ESC, HDLC.FLAG^HDLC.ESC_MASK]))
return data
class PipeInterface(Interface):
MAX_CHUNK = 32768
BITRATE_GUESS = 1*1000*1000
owner = None
command = None
def __init__(self, owner, name, command, respawn_delay):
if respawn_delay == None:
respawn_delay = 5
super().__init__()
self.HW_MTU = 1064
self.owner = owner
self.name = name
self.command = command
self.process = None
self.timeout = 100
self.online = False
self.pipe_is_open = False
self.bitrate = PipeInterface.BITRATE_GUESS
self.respawn_delay = respawn_delay
try:
self.open_pipe()
except Exception as e:
RNS.log(f"Could connect pipe for interface {self}", RNS.LOG_ERROR)
raise e
if self.pipe_is_open:
self.configure_pipe()
else:
raise OSError("Could not connect pipe")
def open_pipe(self):
RNS.log(f"Connecting subprocess pipe for {self}...", RNS.LOG_VERBOSE)
try:
self.process = subprocess.Popen(shlex.split(self.command), stdin=subprocess.PIPE, stdout=subprocess.PIPE)
self.pipe_is_open = True
except Exception as e:
raise e
self.pipe_is_open = False
def configure_pipe(self):
sleep(0.01)
thread = threading.Thread(target=self.readLoop)
thread.daemon = True
thread.start()
self.online = True
RNS.log(f"Subprocess pipe for {self} is now connected", RNS.LOG_VERBOSE)
def processIncoming(self, data):
self.rxb += len(data)
self.owner.inbound(data, self)
def processOutgoing(self,data):
if self.online:
data = bytes([HDLC.FLAG])+HDLC.escape(data)+bytes([HDLC.FLAG])
written = self.process.stdin.write(data)
self.process.stdin.flush()
self.txb += len(data)
if written != len(data):
raise OSError(f"Pipe interface only wrote {written} bytes of {len(data)}")
def readLoop(self):
try:
in_frame = False
escape = False
data_buffer = b""
last_read_ms = int(time.time()*1000)
while True:
process_output = self.process.stdout.read(1)
if len(process_output) == 0 and self.process.poll() is not None:
break
else:
byte = ord(process_output)
last_read_ms = int(time.time()*1000)
if (in_frame and byte == HDLC.FLAG):
in_frame = False
self.processIncoming(data_buffer)
elif (byte == HDLC.FLAG):
in_frame = True
data_buffer = b""
elif (in_frame and len(data_buffer) < self.HW_MTU):
if (byte == HDLC.ESC):
escape = True
else:
if (escape):
if (byte == HDLC.FLAG ^ HDLC.ESC_MASK):
byte = HDLC.FLAG
if (byte == HDLC.ESC ^ HDLC.ESC_MASK):
byte = HDLC.ESC
escape = False
data_buffer = data_buffer+bytes([byte])
RNS.log(f"Subprocess terminated on {self}")
self.process.kill()
except Exception as e:
self.online = False
try:
self.process.kill()
except Exception as e:
pass
RNS.log(f"A pipe error occurred, the contained exception was: {e}", RNS.LOG_ERROR)
RNS.log(f"The interface {self} experienced an unrecoverable error and is now offline.", RNS.LOG_ERROR)
if RNS.Reticulum.panic_on_interface_error:
RNS.panic()
RNS.log("Reticulum will attempt to reconnect the interface periodically.", RNS.LOG_ERROR)
self.online = False
self.reconnect_pipe()
def reconnect_pipe(self):
while not self.online:
try:
time.sleep(self.respawn_delay)
RNS.log(f"Attempting to respawn subprocess for {self}...", RNS.LOG_VERBOSE)
self.open_pipe()
if self.pipe_is_open:
self.configure_pipe()
except Exception as e:
RNS.log(f"Error while spawning subprocess, the contained exception was: {e}", RNS.LOG_ERROR)
RNS.log(f"Reconnected pipe for {self}")
def __str__(self):
return f"PipeInterface[{self.name}]"