diff --git a/API.md b/API.md index 4d6206e92..c1395573b 100644 --- a/API.md +++ b/API.md @@ -355,6 +355,7 @@ Get all available addons. "logo": "bool", "audio": "bool", "gpio": "bool", + "stdin": "bool", "hassio_api": "bool", "homeassistant_api": "bool" } @@ -395,6 +396,7 @@ Get all available addons. "logo": "bool", "hassio_api": "bool", "homeassistant_api": "bool", + "stdin": "bool", "webui": "null|http(s)://[HOST]:port/xy/zx", "gpio": "bool", "audio": "bool", @@ -458,6 +460,10 @@ Output is the raw Docker log. Only supported for local build addons +- POST `/addons/{addon}/stdin` + +Write data to add-on stdin + ## Host Control Communicate over UNIX socket with a host daemon. diff --git a/hassio/addons/addon.py b/hassio/addons/addon.py index be6044e7d..3c13ecd8b 100644 --- a/hassio/addons/addon.py +++ b/hassio/addons/addon.py @@ -22,7 +22,7 @@ from ..const import ( STATE_STARTED, STATE_STOPPED, STATE_NONE, ATTR_USER, ATTR_SYSTEM, ATTR_STATE, ATTR_TIMEOUT, ATTR_AUTO_UPDATE, ATTR_NETWORK, ATTR_WEBUI, ATTR_HASSIO_API, ATTR_AUDIO, ATTR_AUDIO_OUTPUT, ATTR_AUDIO_INPUT, - ATTR_GPIO, ATTR_HOMEASSISTANT_API) + ATTR_GPIO, ATTR_HOMEASSISTANT_API, ATTR_STDIN) from .util import check_installed from ..dock.addon import DockerAddon from ..tools import write_json_file, read_json_file @@ -270,6 +270,11 @@ class Addon(object): """Return True if the add-on access to Home-Assistant api proxy.""" return self._mesh[ATTR_HOMEASSISTANT_API] + @property + def with_stdin(self): + """Return True if the add-on access use stdin input.""" + return self._mesh[ATTR_STDIN] + @property def with_gpio(self): """Return True if the add-on access to gpio interface.""" @@ -561,6 +566,18 @@ class Addon(object): await self.docker.run() return True + @check_installed + async def write_stdin(self, data): + """Write data to add-on stdin. + + Return a coroutine. + """ + if not self.with_stdin: + _LOGGER.error("Add-on don't support write to stdin!") + return False + + return await self.docker.write_stdin(data) + @check_installed async def snapshot(self, tar_file): """Snapshot a state of a addon.""" diff --git a/hassio/addons/validate.py b/hassio/addons/validate.py index 56abfe4a7..f44f9571e 100644 --- a/hassio/addons/validate.py +++ b/hassio/addons/validate.py @@ -14,7 +14,7 @@ from ..const import ( ATTR_LOCATON, ATTR_REPOSITORY, ATTR_TIMEOUT, ATTR_NETWORK, ATTR_AUTO_UPDATE, ATTR_WEBUI, ATTR_AUDIO, ATTR_AUDIO_INPUT, ATTR_AUDIO_OUTPUT, ATTR_HASSIO_API, ATTR_BUILD_FROM, ATTR_SQUASH, - ATTR_ARGS, ATTR_GPIO, ATTR_HOMEASSISTANT_API) + ATTR_ARGS, ATTR_GPIO, ATTR_HOMEASSISTANT_API, ATTR_STDIN) from ..validate import NETWORK_PORT, DOCKER_PORTS, ALSA_CHANNEL @@ -98,6 +98,7 @@ SCHEMA_ADDON_CONFIG = vol.Schema({ vol.Optional(ATTR_GPIO, default=False): vol.Boolean(), vol.Optional(ATTR_HASSIO_API, default=False): vol.Boolean(), vol.Optional(ATTR_HOMEASSISTANT_API, default=False): vol.Boolean(), + vol.Optional(ATTR_STDIN, default=False): vol.Boolean(), vol.Required(ATTR_OPTIONS): dict, vol.Required(ATTR_SCHEMA): vol.Any(vol.Schema({ vol.Coerce(str): vol.Any(SCHEMA_ELEMENT, [ diff --git a/hassio/api/__init__.py b/hassio/api/__init__.py index 627ca7eac..449974ef7 100644 --- a/hassio/api/__init__.py +++ b/hassio/api/__init__.py @@ -104,6 +104,7 @@ class RestAPI(object): '/addons/{addon}/rebuild', api_addons.rebuild) self.webapp.router.add_get('/addons/{addon}/logs', api_addons.logs) self.webapp.router.add_get('/addons/{addon}/logo', api_addons.logo) + self.webapp.router.add_post('/addons/{addon}/stdin', api_addons.stdin) def register_security(self): """Register security function.""" diff --git a/hassio/api/addons.py b/hassio/api/addons.py index 7126b66f6..24fb7b77a 100644 --- a/hassio/api/addons.py +++ b/hassio/api/addons.py @@ -13,7 +13,7 @@ from ..const import ( ATTR_SOURCE, ATTR_REPOSITORIES, ATTR_ADDONS, ATTR_ARCH, ATTR_MAINTAINER, ATTR_INSTALLED, ATTR_LOGO, ATTR_WEBUI, ATTR_DEVICES, ATTR_PRIVILEGED, ATTR_AUDIO, ATTR_AUDIO_INPUT, ATTR_AUDIO_OUTPUT, ATTR_HASSIO_API, - ATTR_GPIO, ATTR_HOMEASSISTANT_API, BOOT_AUTO, BOOT_MANUAL, + ATTR_GPIO, ATTR_HOMEASSISTANT_API, ATTR_STDIN, BOOT_AUTO, BOOT_MANUAL, CONTENT_TYPE_PNG, CONTENT_TYPE_BINARY) from ..validate import DOCKER_PORTS @@ -78,6 +78,7 @@ class APIAddons(object): ATTR_DEVICES: self._pretty_devices(addon), ATTR_URL: addon.url, ATTR_LOGO: addon.with_logo, + ATTR_STDIN: addon.with_stdin, ATTR_HASSIO_API: addon.access_hassio_api, ATTR_HOMEASSISTANT_API: addon.access_homeassistant_api, ATTR_AUDIO: addon.with_audio, @@ -129,6 +130,7 @@ class APIAddons(object): ATTR_DEVICES: self._pretty_devices(addon), ATTR_LOGO: addon.with_logo, ATTR_WEBUI: addon.webui, + ATTR_STDIN: addon.with_stdin, ATTR_HASSIO_API: addon.access_hassio_api, ATTR_HOMEASSISTANT_API: addon.access_homeassistant_api, ATTR_GPIO: addon.with_gpio, @@ -242,3 +244,13 @@ class APIAddons(object): with addon.path_logo.open('rb') as png: return png.read() + + @api_process + async def stdin(self, request): + """Write to stdin of addon.""" + addon = self._extract_addon(request) + if not addon.with_stdin: + raise RuntimeError("STDIN not supported by addons") + + data = await request.read() + return asyncio.shield(addon.write_stdin(data), loop=self.loop) diff --git a/hassio/const.py b/hassio/const.py index e50f1209d..e055ea3f0 100644 --- a/hassio/const.py +++ b/hassio/const.py @@ -86,6 +86,7 @@ ATTR_STATE = 'state' ATTR_SCHEMA = 'schema' ATTR_IMAGE = 'image' ATTR_LOGO = 'logo' +ATTR_STDIN = 'stdin' ATTR_ADDONS_REPOSITORIES = 'addons_repositories' ATTR_REPOSITORY = 'repository' ATTR_REPOSITORIES = 'repositories' diff --git a/hassio/dock/addon.py b/hassio/dock/addon.py index b320ffa89..2c63fe817 100644 --- a/hassio/dock/addon.py +++ b/hassio/dock/addon.py @@ -1,5 +1,6 @@ """Init file for HassIO addon docker object.""" import logging +import os import docker import requests @@ -171,6 +172,7 @@ class DockerAddon(DockerInterface): name=self.name, hostname=self.hostname, detach=True, + stdin_open=self.addon.with_stdin, network_mode=self.network_mode, ports=self.ports, extra_hosts=self.network_mapping, @@ -278,3 +280,35 @@ class DockerAddon(DockerInterface): """ self._stop() return self._run() + + @docker_process + def write_stdin(self, data): + """Write to add-on stdin.""" + return self.loop.run_in_executor(None, self._write_stdin, data) + + def _write_stdin(self, data): + """Write to add-on stdin. + + Need run inside executor. + """ + if not self._is_running(): + return False + + try: + # load needed docker objects + container = self.docker.containers.get(self.name) + socket = container.attach_socket(params={'stdin': 1, 'stream': 1}) + except docker.errors.DockerException as err: + _LOGGER.error("Can't attach to %s stdin -> %s", self.name, err) + return False + + try: + # write to stdin + data += b"\n" + os.write(socket.fileno(), data) + socket.close() + except OSError as err: + _LOGGER.error("Can't write to %s stdin -> %s", self.name, err) + return False + + return True