Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
42 changes: 27 additions & 15 deletions robotpy_installer/cacheserver.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
from http.server import SimpleHTTPRequestHandler
from typing import Dict

from robotpy_installer.sshcontroller import SshController
from robotpy_installer.sshcontroller import ControllerProtocol

logger = logging.getLogger("cacheserver")

Expand All @@ -28,24 +28,25 @@ def translate_path(self, path):


class CacheServer:
def __init__(self, ssh_controller: SshController, cache_root: pathlib.Path):
def __init__(self, ssh_controller: ControllerProtocol, cache_root: pathlib.Path):
self.controller = ssh_controller
self.cache_root = cache_root

self.transport = self.controller.client.get_transport()
assert self.transport is not None
self.port = self.transport.request_port_forward("", 0)

self.mapped_files: Dict[str, str] = {}
self._closed = threading.Event()
self.port = self.controller.cache_listen()

def add_mapping(self, fname: str, local_file: str):
self.mapped_files[fname] = local_file

def start(self):
t = threading.Thread(target=self._handle_requests)
t.setDaemon(True)
t.daemon = True
t.start()

def close(self):
self._closed.set()
self.controller.cache_close()

def process_request(self, request):
client_address = request.getpeername()
try:
Expand All @@ -56,18 +57,29 @@ def process_request(self, request):
server=None,
directory=self.cache_root,
).handle()
except OSError as e:
if str(e) == "File is closed":
except (OSError, ValueError) as e:
if str(e) in ("File is closed", "readline of closed file"):
return
raise
finally:
request.close()

def _handle_requests(self):
request = self.transport.accept()
while not self._closed.is_set():
try:
request = self.controller.cache_accept()
except OSError:
if self._closed.is_set():
return
raise

if request is None:
return

if self._closed.is_set():
request.close()
return

while request is not None:
t = threading.Thread(target=self.process_request, args=[request])
t.setDaemon(True)
t.daemon = True
t.start()

request = self.transport.accept()
152 changes: 136 additions & 16 deletions robotpy_installer/cli_deploy.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,14 +18,17 @@
from . import pypackages, pyproject, robot_utils, sshcontroller
from .installer import PipInstallError, PythonMissingError, RobotpyInstaller
from .installer import _ROBOTPY_PYTHON_VERSION_TUPLE as required_pyversion
from .installer import _ROBOT_VENV_PYTHON
from .installer import _ROBOT_VENV, _ROBOT_VENV_PYTHON
from .errors import Error
from .utils import handle_cli_error, print_err, yesno

import logging

logger = logging.getLogger("deploy")

_LOCAL_DEFAULT_CACHE_ROOT = pathlib.Path("/opt/blocks/cache")
_NO_VERIFY_WITHOUT_WARNING = object()


@contextlib.contextmanager
def wrap_ssh_error(msg: str):
Expand Down Expand Up @@ -136,6 +139,20 @@ def __init__(self, parser: argparse.ArgumentParser):
"--team", default=None, type=int, help="Set team number to deploy robot for"
)

robot_args.add_argument(
"--local",
action="store_true",
default=False,
help="Deploy to the current SystemCore without SSH",
)

parser.add_argument(
"--cache-root",
type=pathlib.Path,
default=None,
help="Override RobotPy installer cache location; defaults to /opt/blocks/cache with --local",
)

parser.add_argument(
"--no-resolve",
action="store_true",
Expand Down Expand Up @@ -166,6 +183,8 @@ def run(
robot: typing.Optional[str],
team: typing.Optional[int],
no_resolve: bool,
local: bool,
cache_root: typing.Optional[pathlib.Path],
):
if main_file.parent == pathlib.Path.home():
print_err(
Expand Down Expand Up @@ -227,9 +246,10 @@ def run(
logger.info("- %s", package)

if no_verify:
logger.warning(
"Not checking to see if they are installed on SystemCore"
)
if no_verify is not _NO_VERIFY_WITHOUT_WARNING:
logger.warning(
"Not checking to see if they are installed on SystemCore"
)
else:
requirements_met, desc = project.are_local_requirements_met()
if not requirements_met:
Expand All @@ -248,13 +268,21 @@ def run(
)
raise Error(msg)

installer = RobotpyInstaller()
ssh: typing.Optional[sshcontroller.ControllerProtocol] = None
if local:
if cache_root is None:
cache_root = _LOCAL_DEFAULT_CACHE_ROOT
ssh = sshcontroller.LocalController()

installer = RobotpyInstaller(cache_root=cache_root)

with installer.connect_to_robot(
project_path=project_path,
main_file=main_file,
robot_or_team=robot or team,
ignore_image_version=ignore_image_version,
no_resolve=no_resolve,
ssh=ssh,
) as ssh:
self._ensure_requirements(
project,
Expand Down Expand Up @@ -350,7 +378,7 @@ def _get_cached_packages(self, installer: RobotpyInstaller) -> pypackages.Packag
return self._packages_in_cache

def _get_robot_packages(
self, ssh: sshcontroller.SshController
self, ssh: sshcontroller.ControllerProtocol
) -> pypackages.Packages:
if self._robot_packages is None:
rio_packages = robot_utils.get_robot_py_packages(ssh)
Expand All @@ -366,7 +394,7 @@ def _ensure_requirements(
self,
project: typing.Optional[pyproject.RobotPyProjectToml],
installer: RobotpyInstaller,
ssh: sshcontroller.SshController,
ssh: sshcontroller.ControllerProtocol,
no_install: bool,
force_install: bool,
no_uninstall: bool,
Expand Down Expand Up @@ -529,19 +557,13 @@ def _ensure_requirements(
pypackages.robot_env(),
pypackages.make_cache_extra_resolver(cached),
)
# The user may have deleted something from the project
# requirements so the only way to ensure the exact
# environment is to first clear the environment.
# - can't do a partial uninstall without completely
# resolving everything
self._clear_pip_packages(installer)

try:
packages = project.get_deploy_list(cached)
except KeyError as e:
raise Error(str(e)) from e

if not no_uninstall:
if not no_uninstall and ssh.sftp_remote_file_exists(_ROBOT_VENV):
logger.info(
"Clearing existing packages on robot before install (specify --no-uninstall to not do this)"
)
Expand All @@ -563,7 +585,7 @@ def _ensure_requirements(

def _do_deploy(
self,
ssh: sshcontroller.SshController,
ssh: sshcontroller.ControllerProtocol,
debug: bool,
nc: bool,
nc_ds: bool,
Expand Down Expand Up @@ -656,7 +678,7 @@ def _do_deploy(

return True

def _start_nc(self, ssh: sshcontroller.SshController, nc_ds: bool):
def _start_nc(self, ssh: sshcontroller.ControllerProtocol, nc_ds: bool):
from netconsole import run # type: ignore

nc_event = threading.Event()
Expand Down Expand Up @@ -701,3 +723,101 @@ def _copy_to_tmpdir(
shutil.copy(fname, tmp_dir / prefix / filename)

return upload_files


class LocalDeploy(Deploy):
"""
Uploads code to the current SystemCore without importing robot code locally or
running tests.
"""

def __init__(self, parser: argparse.ArgumentParser):
parser.add_argument(
"--debug",
action="store_true",
default=False,
help="If specified, runs the code in debug mode (which only currently enables verbose logging)",
)

parser.add_argument(
"--ignore-image-version",
action="store_true",
default=False,
help="Ignore SystemCore image version",
)

install_args = parser.add_mutually_exclusive_group()

install_args.add_argument(
"--no-install",
action="store_true",
default=False,
help="If specified, do not use pyproject.toml to install packages on the robot before deploy",
)

install_args.add_argument(
"--force-install",
action="store_true",
default=False,
help="Force installation of packages required by pyproject.toml",
)

parser.add_argument(
"--no-uninstall",
action="store_true",
default=False,
help="Do not uninstall packages from the SystemCore",
)

parser.add_argument(
"--large",
action="store_true",
default=False,
help="If specified, allow uploading large files (> 250k) to the SystemCore",
)

parser.add_argument(
"--cache-root",
type=pathlib.Path,
default=None,
help="Override RobotPy installer cache location; defaults to /opt/blocks/cache",
)

self._packages_in_cache: typing.Optional[pypackages.Packages] = None
self._robot_packages: typing.Optional[pypackages.Packages] = None

@handle_cli_error
def run(
self,
main_file: pathlib.Path,
project_path: pathlib.Path,
debug: bool,
ignore_image_version: bool,
no_install: bool,
no_uninstall: bool,
force_install: bool,
large: bool,
cache_root: typing.Optional[pathlib.Path],
):
return Deploy.run(
self,
main_file=main_file,
project_path=project_path,
robot_class=None,
builtin=False,
skip_tests=True,
debug=debug,
nc=False,
nc_ds=False,
ignore_image_version=ignore_image_version,
no_install=no_install,
no_verify=_NO_VERIFY_WITHOUT_WARNING,
no_uninstall=no_uninstall,
force_install=force_install,
large=large,
robot=None,
team=None,
no_resolve=False,
local=True,
cache_root=cache_root,
)
Loading
Loading