Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Sheep compatiability #21

Merged
merged 35 commits into from
Aug 28, 2022
Merged
Show file tree
Hide file tree
Changes from 13 commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
9902110
serve sheep and docs
shardros Aug 17, 2022
3878147
Added install script
shardros Aug 17, 2022
3a188cd
make script gracefully fail
shardros Aug 17, 2022
9837d53
tests for `files/`
shardros Aug 18, 2022
e13f469
uploading and running compatiability with sheep
shardros Aug 19, 2022
d328fc8
add tests for creation and deletion
shardros Aug 19, 2022
4144d52
include test_editor.py
shardros Aug 19, 2022
6a7bded
include test_editor.py
shardros Aug 19, 2022
6caa63d
automate python install in `install.sh`
shardros Aug 19, 2022
9a6e12b
fetch new packages
shardros Aug 19, 2022
f4b54c9
Document install.sh
shardros Aug 19, 2022
ddc7604
fix problems with running on fresh install
shardros Aug 19, 2022
18c9b54
allow install.sh to fix broken env
shardros Aug 20, 2022
361fdca
fix tests
shardros Aug 20, 2022
5d1e0f6
basic framework for starting usercode (#27)
shardros Aug 20, 2022
8f57339
create requirements.txt
shardros Aug 22, 2022
1e6ac46
remove hashes from requirements.txt
shardros Aug 22, 2022
f67c9c9
add logging
shardros Aug 23, 2022
2afa680
Fix flakeyness of "test_kill_user_code"
shardros Aug 23, 2022
039d580
add logging convience method
shardros Aug 24, 2022
018ebb6
automate python install in `install.sh`
shardros Aug 19, 2022
37326af
fetch new packages
shardros Aug 19, 2022
75dbbb6
Document install.sh
shardros Aug 19, 2022
fe1f957
fix problems with running on fresh install
shardros Aug 19, 2022
8dd50b2
allow install.sh to fix broken env
shardros Aug 20, 2022
0f45d41
fix tests
shardros Aug 20, 2022
45214cd
basic framework for starting usercode (#27)
shardros Aug 20, 2022
e16d673
create requirements.txt
shardros Aug 22, 2022
0780c3b
remove hashes from requirements.txt
shardros Aug 22, 2022
b699d8a
add logging
shardros Aug 23, 2022
0f69ff5
Fix flakeyness of "test_kill_user_code"
shardros Aug 23, 2022
41decea
add logging convience method
shardros Aug 24, 2022
2341afa
Merge branch 'sheep-compatiability' of github.com:systemetric/shepher…
shardros Aug 24, 2022
3ac9c33
kill user code in tests using psutil
shardros Aug 24, 2022
980520f
Tidy up for review
shardros Aug 28, 2022
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -144,5 +144,9 @@ cython_debug/
# Non-generic gitignores
##################################################################

usercode/*
usercode/round/*
usercode/editable/*
!usercode/editable/main.py
!usercode/editable/minimal_example.py
!usercode/editable/snippests.py
logs.txt
37 changes: 31 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,24 @@ Additionally there are a whole load of features which a new shepherd could suppo

## Getting started

## Use the script

Automatically install everything and set up the virtual env:

```
sh install.sh
```

If the script fails because you do not have a new enough python version you can
follow the steps here:
- Guide: https://realpython.com/installing-python/#how-to-build-python-from-source-code
- Offical python docs: https://docs.python.org/3/using/unix.html

If building on a pi you will need to increase your swap space. 2GB has been
tested to work on a pi 3A+.

## Manual install

Dependencies are managed using poetry which you can get
[here](https://python-poetry.org/docs/master/#installing-with-the-official-installer)

Expand All @@ -39,16 +57,23 @@ You can enter this virtual environment by:
poetry shell
```

Start the server in development mode (hot reload)
## Running shepherd

Start the server in development mode (hot reload):
```
dev = "poetry run uvicorn app:app --reload"
poetry run uvicorn app:app --reload
```

Run tests
To deploy run (chosen port and ip are optional):
```
pytest -x
poetry run uvicorn app:app --host 0.0.0.0 --port 80
```

## Documentation
## Tests

Tests are located in `test/`

Documentation is available for this project here: TODO
Run all tests and stop on the first fail:
```
pytest -x
```
66 changes: 59 additions & 7 deletions app/config.py
Original file line number Diff line number Diff line change
@@ -1,16 +1,17 @@
import os

from pydantic import BaseSettings
from pathlib import Path


class Settings(BaseSettings):
class Settings:

output_file_path: Path = Path("logs.txt").absolute()
usercode_path: Path = Path("usercode/").absolute()
usercode_entry_point = Path("main.py")
usercode_entry_path: Path = (usercode_path / usercode_entry_point).absolute()
sheep_files_path: Path = Path("static/editor").absolute()
round_path: Path = Path("usercode/round").absolute()
round_entry_point = Path("main.py")
round_entry_path: Path = (round_path / round_entry_point).absolute()

editor_path: Path = Path("static/editor/").absolute()
docs_path: Path = Path("static/docs/").absolute()

round_len: float = 180.0 # seconds
reap_grace_time: float = 5.0 # seconds
Expand All @@ -20,7 +21,58 @@ class Settings(BaseSettings):
robot_env: dict = dict(os.environ)
on_brain: bool = False

upload_tmp_dir: str = "shepherd-user-code-"
game_control_path: Path = Path('/media/ArenaUSB')
teamname_file: Path = Path('/home/pi/teamname.txt')

def __init__(self):
self._init_usercode()

def _get_team_specifics(self):
"""Find infomation set on each brain about the team"""
# Teamname should be set on a per brain basis before shipping
# Its purpose is to allow the setting of specific graphics for help identifing teams in the arena.
# Graphics are loaded from the ArenaUSB stick if available, or standard graphics from the stick are used.
# this used to be in rc.local, but the looks of shame and dissapointment
# got the better of me

if teamname_file.exists():
teamname_jpg = teamname_file.read_text().replace('\n', '') + '.jpg'
else:
teamname_jpg = 'none'

# Pick a start imapge in order of preference :
# 1) We have a team corner image on the USB
# 2) The team have uploaded their own image to the robot
# 3) We have a generic corner image on the USB
# 4) The game image
start_graphic = game_control_path / teamname_jpg
if not start_graphic.exists():
# attempt to find the team specific corner graphic from the ArenaUSB
start_graphic = Path('robotsrc/team_logo.jpg')
if not start_graphic.exists():
# attempt to find the default corner graphic from ArenaUSB
start_graphic = game_control_path / 'Corner.jpg'
if not start_graphic.exists():
# finally look for a game specific logo
start_graphic = Path('/home/pi/game_logo.jpg')

if config.start_graphic.exists():
static_graphic = Path('shepherd/static/image.jpg')
static_graphic.write_bytes(start_graphic.read_bytes())

def _init_usercode(self):
"""Ensure that the saved usercode has a main.py and blocks.json"""
self.usr_src_path = Path("usercode/editable").absolute()
if not self.usr_src_path.exists():
os.mkdir(self.usr_src_path)

self.usr_src_main_path = self.usr_src_path / Path('main.py')
with open(self.usr_src_main_path, 'w') as main_file:
main_file.write('# DO NOT DELETE\n')
self.blocks_path = self.usr_src_path / Path('blocks.json')

if self.round_path.exists() is False:
os.mkdir(self.round_path)


config = Settings()
Expand Down
69 changes: 69 additions & 0 deletions app/editor.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
"""Functions used by endpoints for sheep"""
import logging
from app.config import config
import os
import json
import re
from pathlib import Path


_logger = logging.getLogger(__name__)


def get_files() -> dict:
project_paths = [f for f in os.listdir(config.usr_src_path)
if os.path.isfile(os.path.join(config.usr_src_path, f))
and (f.endswith('.py') or f.endswith(".xml") or f == "blocks.json")
and f != 'main.py']

def read_project(project_path: Path) -> dict:
with open(config.usr_src_path / project_path, 'r') as project_file:
content = project_file.read()
return {
'filename': project_path,
'content': content
}

blocks = {}
if config.blocks_path.exists():
with open(config.blocks_path, 'r') as blocks_file:
try:
blocks = json.load(blocks_file)
except ValueError:
pass

if "requires" not in blocks:
blocks["requires"] = []
if "header" not in blocks:
blocks["header"] = ""
if "footer" not in blocks:
blocks["footer"] = ""
if "blocks" not in blocks:
blocks["blocks"] = []

return {
'main': config.usr_src_main_path.absolute(),
'blocks': blocks,
'projects': [read_project(p) for p in project_paths]
}


def save_file(filename, body):
dots = len(re.findall("\\.", filename))
if dots == 1:
with open(os.path.join(config.usr_src_path, filename), 'w') as f:
f.write(body.decode('utf-8'))
else:
_logger.warn("A file was attempted to be saved with too many dots: "
f"{filename}")


def delete_file(filename):
if filename == "blocks.json":
return ""
dots = len(re.findall("\\.", filename))
if dots == 1:
os.unlink(os.path.join(config.usr_src_path, filename))
else:
_logger.warn("A file was attempted to be saved with too many dots: "
f"{filename}")
49 changes: 23 additions & 26 deletions app/main.py
Original file line number Diff line number Diff line change
@@ -1,24 +1,12 @@
from fastapi import FastAPI
from fastapi.staticfiles import StaticFiles
import uvicorn

from app import routers
from app.runner import runner
from app.routers import runner_router, upload_router, files_router
from app.run import runner
from app.config import config
from app.upload import increase_max_file_size

def increase_max_file_size():
"""This is the maximum filesize which can be uploaded to shepherd.
see app.upload._fix_bad_zips for more info.

starlette which FastAPI uses does not allow for us to set the spool_max_size
in the constructor instead defining it as a class attribute so we override
this class attribute
"""
from starlette.datastructures import UploadFile as StarletteUploadFile
# the original size * a big number
StarletteUploadFile.spool_max_size = (1024 * 1024) * 99999999999999999


# Call before any chance of any UploadFiles being created
increase_max_file_size()

app = FastAPI(
title="Shepherd",
Expand All @@ -27,20 +15,29 @@ def increase_max_file_size():
redoc_url="/api_redoc",
)

app.include_router(routers.runner_router)
app.include_router(routers.upload_router)
app.include_router(runner_router)
app.include_router(upload_router)
app.include_router(files_router)

app.mount("/editor", StaticFiles(directory=config.editor_path, html=True), name="editor")
app.mount("/docs", StaticFiles(directory=config.docs_path, html=True), name="docs")


@app.on_event("startup")
def startup_event():
increase_max_file_size()

@app.get("/")
def root():
return "Root of shepherd-2"

@app.on_event("shutdown")
def shutdown_event():
"""Make sure that we kill any running usercode
Might not work if we crash or die
"""
"""Kill any running usercode"""
runner.shutdown()


@app.get("/")
def root():
return "Root of shepherd-2"


if __name__ == '__main__':
uvicorn.run(app, port=8080, host='0.0.0.0')
uvicorn.run(app, port=8080, host='0.0.0.0')
51 changes: 39 additions & 12 deletions app/routers.py
Original file line number Diff line number Diff line change
@@ -1,23 +1,27 @@
from fastapi import APIRouter, HTTPException, UploadFile, File
from fastapi.responses import StreamingResponse
import logging
from fastapi import APIRouter, HTTPException, UploadFile, File, Request
from pydantic import BaseModel

from app.runner import States
from app.runner import runner
from app.run import States
from app.run import runner
import app.editor
import app.upload

_logger = logging.getLogger(__name__)

# ==============================================================================
# Runner router
# ==============================================================================

runner_router = APIRouter()
runner_router = APIRouter(prefix="/run")


@runner_router.get("/stop")
@runner_router.post("/stop")
def stop():
runner.state = States.STOPPED


@runner_router.get("/start")
@runner_router.post("/start")
def start():
"""Start the robot, really the check and the start should be in a lock"""
# TODO: https://github.com/systemetric/shepherd-2/issues/18
Expand All @@ -43,13 +47,36 @@ def output():
# ==============================================================================


upload_router = APIRouter()
upload_router = APIRouter(prefix="/upload")

@upload_router.post("/upload", status_code=201)
def upload_file(file: UploadFile = File(...)):
print("file upload triggered")
app.upload.process_uploaded_file(file)
def upload_file(uploaded_file: UploadFile = File(...)):
_logger.info("File uploaded to staging")
app.upload.process_uploaded_file(uploaded_file)
return {
"filename": file.filename,
"filename": uploaded_file.filename,
}

# ==============================================================================
# Files router
# ==============================================================================

files_router = APIRouter(prefix="/files")

class SheepFile(BaseModel):
contents: str


@files_router.get('/')
def get_files():
return app.editor.get_files()


@files_router.post("/save/{filename}")
async def save_file(filename: str, request: Request):
app.editor.save_file(filename, await request.body())


@files_router.delete("/delete/{filename}")
def delete_file(filename: str):
app.editor.delete_file(filename)
Loading