Add serverside type checking

This commit is contained in:
Marks Polakovs 2020-04-07 12:19:28 +02:00
parent 7b5315d470
commit d18550e247
5 changed files with 94 additions and 33 deletions

3
.gitignore vendored
View file

@ -24,3 +24,6 @@ yarn-error.log*
.env.local .env.local
.yarn/ .yarn/
.mypy_cache/
env/

31
Jenkinsfile vendored
View file

@ -7,11 +7,37 @@ pipeline {
stages { stages {
stage('Install dependencies') { stage('Install dependencies') {
steps { parallel {
sh 'CI=true npm_config_python=/usr/local/bin/python2.7 yarn --no-progress --non-interactive --skip-integrity-check --frozen-lockfile install' stage('JavaScript') {
steps {
sh 'CI=true npm_config_python=/usr/local/bin/python2.7 yarn --no-progress --non-interactive --skip-integrity-check --frozen-lockfile install'
}
}
stage('Python') {
steps {
sh '/usr/local/bin/python3.7 -m venv env'
sh 'source env/bin/activate'
sh 'pip install -r requirements.txt'
}
}
} }
} }
stage('Type checks') {
parallel {
stage('TypeScript') {
steps {
sh 'node_modules/.bin/tsc -p tsconfig.json --noEmit --extendedDiagnostics'
}
}
stage('MyPy') {
steps {
sh 'mypy server.py'
}
}
}
}
stage('Build and deploy to dev instance') { stage('Build and deploy to dev instance') {
steps { steps {
sh 'sed -i -e \'s/ury.org.uk\\/webstudio/ury.org.uk\\/webstudio-dev/\' package.json' sh 'sed -i -e \'s/ury.org.uk\\/webstudio/ury.org.uk\\/webstudio-dev/\' package.json'
@ -21,6 +47,7 @@ pipeline {
} }
} }
} }
stage('Build and deploy for production') { stage('Build and deploy for production') {
when { when {
branch 'production' branch 'production'

2
mypy.ini Normal file
View file

@ -0,0 +1,2 @@
[mypy]
strict = True

View file

@ -5,9 +5,21 @@ cffi==1.14.0
crc32c==2.0 crc32c==2.0
cryptography==2.8 cryptography==2.8
JACK-Client==0.5.2 JACK-Client==0.5.2
jedi==0.15.2
mypy==0.770
mypy-extensions==0.4.3
netifaces==0.10.9 netifaces==0.10.9
parso==0.6.2
pluggy==0.13.1
pycparser==2.20 pycparser==2.20
pyee==7.0.1 pyee==7.0.1
pylibsrtp==0.6.6 pylibsrtp==0.6.6
pyls==0.1.6
pyls-mypy==0.1.8
python-jsonrpc-server==0.3.4
python-language-server==0.31.9
six==1.14.0 six==1.14.0
typed-ast==1.4.1
typing-extensions==3.7.4.2
ujson==1.35
websockets==8.1 websockets==8.1

View file

@ -2,20 +2,21 @@ import asyncio
import websockets import websockets
import json import json
import uuid import uuid
import av import av # type: ignore
import struct import struct
from aiortc import MediaStreamTrack, RTCPeerConnection, RTCSessionDescription from aiortc import MediaStreamTrack, RTCPeerConnection, RTCSessionDescription # type: ignore
from aiortc.contrib.media import MediaBlackhole, MediaPlayer from aiortc.contrib.media import MediaBlackhole, MediaPlayer # type: ignore
import jack as Jack import jack as Jack # type: ignore
import os import os
import re import re
from datetime import datetime from datetime import datetime
from typing import Optional, Any
file_contents_ex = re.compile(r"^ws=\d$") file_contents_ex = re.compile(r"^ws=\d$")
def write_ob_status(status): def write_ob_status(status: bool) -> None:
if not os.path.exists("/music/ob_state.conf"): if not os.path.exists("/music/ob_state.conf"):
print("OB State file does not exist. Bailing.") print("OB State file does not exist. Bailing.")
return return
@ -26,19 +27,19 @@ def write_ob_status(status):
else: else:
if content[len(content) - 1] != "\n": if content[len(content) - 1] != "\n":
content += "\n" content += "\n"
content += "ws=" + (1 if status else 0) + "\n" content += "ws=" + str(1 if status else 0) + "\n"
fd.seek(0) fd.seek(0)
fd.write(content) fd.write(content)
fd.truncate() fd.truncate()
@Jack.set_error_function @Jack.set_error_function # type: ignore
def error(msg): def error(msg: str) -> None:
print("Error:", msg) print("Error:", msg)
@Jack.set_info_function @Jack.set_info_function # type: ignore
def info(msg): def info(msg: str) -> None:
print("Info:", msg) print("Info:", msg)
@ -46,8 +47,10 @@ jack = Jack.Client("webstudio")
out1 = jack.outports.register("out_0") out1 = jack.outports.register("out_0")
out2 = jack.outports.register("out_1") out2 = jack.outports.register("out_1")
transfer_buffer1: Any = None
transfer_buffer2: Any = None
def init_buffers(): def init_buffers() -> None:
global transfer_buffer1, transfer_buffer2 global transfer_buffer1, transfer_buffer2
transfer_buffer1 = Jack.RingBuffer(jack.samplerate * 10) transfer_buffer1 = Jack.RingBuffer(jack.samplerate * 10)
transfer_buffer2 = Jack.RingBuffer(jack.samplerate * 10) transfer_buffer2 = Jack.RingBuffer(jack.samplerate * 10)
@ -56,8 +59,8 @@ def init_buffers():
init_buffers() init_buffers()
@jack.set_process_callback @jack.set_process_callback # type: ignore
def process(frames): def process(frames: int) -> None:
buf1 = out1.get_buffer() buf1 = out1.get_buffer()
piece1 = transfer_buffer1.read(len(buf1)) piece1 = transfer_buffer1.read(len(buf1))
buf1[: len(piece1)] = piece1 buf1[: len(piece1)] = piece1
@ -67,15 +70,16 @@ def process(frames):
class JackSender(object): class JackSender(object):
def __init__(self, track): resampler: Any
def __init__(self, track: MediaStreamTrack) -> None:
self.track = track self.track = track
self.resampler = None self.resampler = None
self.ended = False self.ended = False
def end(): def end(self) -> None:
self.ended = True self.ended = True
async def process(self): async def process(self) -> None:
while True: while True:
if self.ended: if self.ended:
break break
@ -99,13 +103,18 @@ current_session = None
class Session(object): class Session(object):
def __init__(self): websocket: Optional[websockets.WebSocketServerProtocol]
sender: Optional[JackSender]
connection_state: Optional[str]
pc: Optional[Any]
def __init__(self) -> None:
self.websocket = None self.websocket = None
self.sender = None self.sender = None
self.pc = None self.pc = None
self.connection_state = None self.connection_state = None
async def end(): async def end(self) -> None:
print(self.connection_id, "going away") print(self.connection_id, "going away")
if self.sender is not None: if self.sender is not None:
self.sender.end() self.sender.end()
@ -116,18 +125,21 @@ class Session(object):
if self.websocket is not None: if self.websocket is not None:
await self.websocket.send(json.dumps({"kind": "REPLACED"})) await self.websocket.send(json.dumps({"kind": "REPLACED"}))
def create_peerconnection(self): def create_peerconnection(self) -> None:
self.pc = RTCPeerConnection() self.pc = RTCPeerConnection()
assert self.pc is not None
@self.pc.on("signalingstatechange") @self.pc.on("signalingstatechange") # type: ignore
async def on_signalingstatechange(): async def on_signalingstatechange() -> None:
assert self.pc is not None
print( print(
self.connection_id, self.connection_id,
"Signaling state is {}".format(self.pc.signalingState), "Signaling state is {}".format(self.pc.signalingState),
) )
@self.pc.on("iceconnectionstatechange") @self.pc.on("iceconnectionstatechange") # type: ignore
async def on_iceconnectionstatechange(): async def on_iceconnectionstatechange() -> None:
assert self.pc is not None
print( print(
self.connection_id, self.connection_id,
"ICE connection state is {}".format(self.pc.iceConnectionState), "ICE connection state is {}".format(self.pc.iceConnectionState),
@ -135,18 +147,19 @@ class Session(object):
if self.pc.iceConnectionState == "failed": if self.pc.iceConnectionState == "failed":
await self.pc.close() await self.pc.close()
self.pc = None self.pc = None
await self.websocket.close(1008) if self.websocket is not None:
await self.websocket.close(1008)
return return
@self.pc.on("track") @self.pc.on("track") # type: ignore
async def on_track(track): async def on_track(track: MediaStreamTrack) -> None:
global current_session global current_session
print(self.connection_id, "Received track") print(self.connection_id, "Received track")
if track.kind == "audio": if track.kind == "audio":
print(self.connection_id, "Adding to Jack.") print(self.connection_id, "Adding to Jack.")
@track.on("ended") @track.on("ended") # type: ignore
async def on_ended(): async def on_ended() -> None:
print(self.connection_id, "Track {} ended".format(track.kind)) print(self.connection_id, "Track {} ended".format(track.kind))
# TODO: this doesn't exactly handle reconnecting gracefully # TODO: this doesn't exactly handle reconnecting gracefully
self.end() self.end()
@ -158,16 +171,20 @@ class Session(object):
write_ob_status(True) write_ob_status(True)
await self.sender.process() await self.sender.process()
async def process_ice(self, message): async def process_ice(self, message: Any) -> None:
if self.connection_state == "HELLO" and message["kind"] == "OFFER": if self.connection_state == "HELLO" and message["kind"] == "OFFER":
offer = RTCSessionDescription(sdp=message["sdp"], type=message["type"]) offer = RTCSessionDescription(sdp=message["sdp"], type=message["type"])
print(self.connection_id, "Received offer") print(self.connection_id, "Received offer")
self.create_peerconnection() self.create_peerconnection()
assert self.pc is not None
await self.pc.setRemoteDescription(offer) await self.pc.setRemoteDescription(offer)
answer = await self.pc.createAnswer() answer = await self.pc.createAnswer()
await self.pc.setLocalDescription(answer) await self.pc.setLocalDescription(answer)
assert self.websocket is not None
await self.websocket.send( await self.websocket.send(
json.dumps( json.dumps(
{ {
@ -187,7 +204,7 @@ class Session(object):
), ),
) )
async def connect(self, websocket): async def connect(self, websocket: websockets.WebSocketServerProtocol) -> None:
self.websocket = websocket self.websocket = websocket
self.connection_id = uuid.uuid4() self.connection_id = uuid.uuid4()
self.connection_state = "HELLO" self.connection_state = "HELLO"
@ -210,7 +227,7 @@ class Session(object):
) )
async def serve(websocket, path): async def serve(websocket: websockets.WebSocketServerProtocol, path: str) -> None:
if path == "/stream": if path == "/stream":
session = Session() session = Session()
await session.connect(websocket) await session.connect(websocket)