-
-
Notifications
You must be signed in to change notification settings - Fork 37.2k
Use Unix socket for Supervisor communication #163907
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
Changes from 3 commits
275374e
68d94ba
c588908
72db92b
b6be7a1
c3be74c
95d76e8
f499a0b
ea556d6
cccb252
da29f06
fdde931
88b9e6c
d93b45f
58d8824
f0c56d7
03817cc
63bc456
0888dcc
4ce712b
b975a89
2d67754
8f886ae
4bd744f
d4e71ab
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -20,14 +20,20 @@ | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| from homeassistant.auth.const import GROUP_ID_READ_ONLY | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| from homeassistant.auth.models import User | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| from homeassistant.components import websocket_api | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| from homeassistant.const import HASSIO_USER_NAME | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| from homeassistant.core import HomeAssistant, callback | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| from homeassistant.helpers.http import current_request | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| from homeassistant.helpers.json import json_bytes | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| from homeassistant.helpers.network import is_cloud_connection | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| from homeassistant.helpers.storage import Store | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| from homeassistant.util.network import is_local | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| from .const import KEY_AUTHENTICATED, KEY_HASS_REFRESH_TOKEN_ID, KEY_HASS_USER | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| from .const import ( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| KEY_AUTHENTICATED, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| KEY_HASS_REFRESH_TOKEN_ID, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| KEY_HASS_USER, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| is_unix_socket_request, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| _LOGGER = logging.getLogger(__name__) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -117,7 +123,7 @@ def async_user_not_allowed_do_auth( | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return "User cannot authenticate remotely" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| async def async_setup_auth( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| async def async_setup_auth( # noqa: C901 | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| hass: HomeAssistant, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| app: Application, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ) -> None: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
frenck marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -207,14 +213,39 @@ def async_validate_signed_request(request: Request) -> bool: | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| request[KEY_HASS_REFRESH_TOKEN_ID] = refresh_token.id | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return True | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| supervisor_user_id: str | None = None | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| async def async_authenticate_unix_socket(request: Request) -> bool: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
edenhaus marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| """Authenticate a request from a Unix socket as the Supervisor user.""" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
edenhaus marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| nonlocal supervisor_user_id | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| # Fast path: use cached user ID | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if supervisor_user_id is not None: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if user := await hass.auth.async_get_user(supervisor_user_id): | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| request[KEY_HASS_USER] = user | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return True | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| supervisor_user_id = None | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+237
to
+241
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| # Slow path: find the Supervisor user by name | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| for user in await hass.auth.async_get_users(): | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if user.system_generated and user.name == HASSIO_USER_NAME: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| supervisor_user_id = user.id | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| request[KEY_HASS_USER] = user | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+225
to
+249
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| async def async_authenticate_unix_socket(request: Request) -> bool: | |
| """Authenticate a request from a Unix socket as the Supervisor user.""" | |
| nonlocal supervisor_user_id | |
| # Fast path: use cached user ID | |
| if supervisor_user_id is not None: | |
| if user := await hass.auth.async_get_user(supervisor_user_id): | |
| request[KEY_HASS_USER] = user | |
| return True | |
| supervisor_user_id = None | |
| # Slow path: find the Supervisor user by name | |
| for user in await hass.auth.async_get_users(): | |
| if user.system_generated and user.name == HASSIO_USER_NAME: | |
| supervisor_user_id = user.id | |
| request[KEY_HASS_USER] = user | |
| supervisor_refresh_token_id: str | None = None | |
| async def async_authenticate_unix_socket(request: Request) -> bool: | |
| """Authenticate a request from a Unix socket as the Supervisor user.""" | |
| nonlocal supervisor_user_id, supervisor_refresh_token_id | |
| # Fast path: use cached user and refresh token IDs | |
| if supervisor_user_id is not None and supervisor_refresh_token_id is not None: | |
| user = await hass.auth.async_get_user(supervisor_user_id) | |
| refresh_token = hass.auth.async_get_refresh_token( | |
| supervisor_refresh_token_id | |
| ) | |
| if user is not None and refresh_token is not None: | |
| request[KEY_HASS_USER] = user | |
| request[KEY_HASS_REFRESH_TOKEN_ID] = refresh_token.id | |
| return True | |
| supervisor_user_id = None | |
| supervisor_refresh_token_id = None | |
| # Slow path: find the Supervisor user by name and associate a refresh token | |
| for user in await hass.auth.async_get_users(): | |
| if user.system_generated and user.name == HASSIO_USER_NAME: | |
| # Reuse an existing refresh token for this user if available | |
| refresh_token = next(iter(user.refresh_tokens.values()), None) | |
| if refresh_token is None: | |
| # Without a refresh token ID, we cannot safely authenticate | |
| return False | |
| supervisor_user_id = user.id | |
| supervisor_refresh_token_id = refresh_token.id | |
| request[KEY_HASS_USER] = user | |
| request[KEY_HASS_REFRESH_TOKEN_ID] = refresh_token.id |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'd prefer if we can avoid setting KEY_HASS_REFRESH_TOKEN_ID. From what I can tell, the two possible locations seem safe for the Supervisor case:
- onboarding/views.py:286 — IntegrationOnboardingView.post() — requires auth, only called by the frontend during onboarding. Would KeyError on a Unix socket request, but Supervisor has no reason to call this.
- auth.py:71 — async_sign_path() — uses KEY_HASS_REFRESH_TOKEN_ID in request (with an in check, not direct access), so it gracefully falls back to the content user if the key isn't present. Safe.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What's the reason we want to avoid setting KEY_HASS_REFRESH_TOKEN_ID?
I'd suggest to add a comment here explaining we intentionally don't set KEY_HASS_REFRESH_TOKEN_ID.
WRT to the IntegrationOnboardingView it seems there's some missing error handling there and it should check that there's a KEY_HASS_REFRESH_TOKEN_ID in the request? Could it be added to the schema, or is that wrong?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What's the reason we want to avoid setting
KEY_HASS_REFRESH_TOKEN_ID?
Really only to avoid having to maintain a "fake" authentication and refresh token. I've added a comment.
WRT to the
IntegrationOnboardingViewit seems there's some missing error handling there and it should check that there's aKEY_HASS_REFRESH_TOKEN_IDin the request? Could it be added to the schema, or is that wrong?
The schema is for payload, but KEY_HASS_REFRESH_TOKEN_ID is data added to the request by our authentication system. So no, we can't add this to schema. The Supervisor should never use this view though, and the view is only active during onboarding. I've added a check for completeness.
edenhaus marked this conversation as resolved.
Show resolved
Hide resolved
edenhaus marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
edenhaus marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -1,10 +1,22 @@ | ||||||||||||||||||||||||||||||||||||||||||||
| """HTTP specific constants.""" | ||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||
| import socket | ||||||||||||||||||||||||||||||||||||||||||||
| from typing import Final | ||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||
| from aiohttp.web import Request | ||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||
| from homeassistant.helpers.http import KEY_AUTHENTICATED, KEY_HASS # noqa: F401 | ||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||
| DOMAIN: Final = "http" | ||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||
| KEY_HASS_USER: Final = "hass_user" | ||||||||||||||||||||||||||||||||||||||||||||
| KEY_HASS_REFRESH_TOKEN_ID: Final = "hass_refresh_token_id" | ||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||
| def is_unix_socket_request(request: Request) -> bool: | ||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||
| refresh_token = None | |
| if (hassio_user := config_store.data.hassio_user) is not None: | |
| user = await hass.auth.async_get_user(hassio_user) | |
| if user and user.refresh_tokens: | |
| refresh_token = list(user.refresh_tokens.values())[0] | |
| # Migrate old Hass.io users to be admin. | |
| if not user.is_admin: | |
| await hass.auth.async_update_user(user, group_ids=[GROUP_ID_ADMIN]) | |
| # Migrate old name | |
| if user.name == "Hass.io": | |
| await hass.auth.async_update_user(user, name=HASSIO_USER_NAME) | |
| if refresh_token is None: | |
| user = await hass.auth.async_create_system_user( | |
| HASSIO_USER_NAME, group_ids=[GROUP_ID_ADMIN] | |
| ) | |
| refresh_token = await hass.auth.async_create_refresh_token(user) | |
| config_store.update(hassio_user=user.id) |
We then send this token to (hopfully) the Supervisor:
| await supervisor_client.homeassistant.set_options(options) |
We hand it out to whoever is answering to the IP address learned from the SUPERVISOR environment variable. So in other words, today, whoever controls the environment, has the power to learn the authentication token to be "Supervisor". This authentication token is just like any regular Home Assistant (admin) user can be used to authenticate remotely (via http) too.
By creating a new environment variable SUPERVISOR_CORE_API_SOCKET, which offers a local only way to connect Supervisor to Core, we essentially don't increase the attack surface: Controlling environment still means you have the means to be "Supervisor". But since its a Unix socket which only works locally per-se, and there is no authentication token which works via TCP/IP remotely too, the overall attack surface is lowered.
Note that in general Unix security, controlling environment means anyways game over: Via LD_PRELOAD one can load external code into a process, and hence control the process anyways.
The only risk here is that some other process connects to the SUPERVISOR_CORE_API_SOCKET Unix socket. But this will be mitigated by pointing it to a location which is only available to the host OS and the Supervisor container (see the current implementation on Supervisor side https://github.com/home-assistant/supervisor/blob/bffc3e80a044b4226800277a108a65d05fed3de5/supervisor/docker/homeassistant.py#L187-L188).
In the end this is a bit a chicken-egg problem: We want to systems to talk to each other, how do we make sure they can trust each other? There will always be some inherent trust required, be it Core "trusts" that Supervisor is at the other end of SUPERVISOR env IP, or Core trusts whoever connects to the SUPERVISOR_CORE_API_SOCKET socket to be Supervisor. What this is really doing is simplifying this inherit trust, and with Unix socket use a local only communication channel, which his essentially best practice for this type of communication (Docker socket does a similar thing, it is an unauthenticated Unix socket typically at /var/run/docker.socket, which allows to control any container on the system).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is there a way to check if the request belongs to the unix socket specified in SUPERVISOR_CORE_API_SOCKET?
I don't like that we have a generic function checking for a Unix socket, and then we imply that only the supervisor is using it. That's true for the moment, but it can change in the future.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hm, yeah that is a good idea, in fact yes, transport.get_extra_info("sockname") returns the Unix socket path. So we can definitely say this request came from that (Supervisor) Unix socket. Since we only have one Unix socket today, this does not really give us much. But we might add Unix sockets for other use cases later, or a custom components might do things... So agreed, worth doing. I'll add that extra check to the renamed is_(supervisor_)unix_socket_request.
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -3,6 +3,7 @@ | |||||||||||||||||||||||
| from __future__ import annotations | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| import asyncio | ||||||||||||||||||||||||
| from pathlib import Path | ||||||||||||||||||||||||
| from ssl import SSLContext | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| from aiohttp import web | ||||||||||||||||||||||||
|
|
@@ -68,3 +69,46 @@ async def start(self) -> None: | |||||||||||||||||||||||
| reuse_address=self._reuse_address, | ||||||||||||||||||||||||
| reuse_port=self._reuse_port, | ||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| class HomeAssistantUnixSite(web.BaseSite): | ||||||||||||||||||||||||
| """HomeAssistant specific aiohttp UnixSite. | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| Listens on a Unix socket for local inter-process communication, | ||||||||||||||||||||||||
| used for Supervisor to Core communication. | ||||||||||||||||||||||||
| """ | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| __slots__ = ("_path",) | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| def __init__( | ||||||||||||||||||||||||
| self, | ||||||||||||||||||||||||
| runner: web.BaseRunner, | ||||||||||||||||||||||||
| path: Path, | ||||||||||||||||||||||||
| *, | ||||||||||||||||||||||||
| backlog: int = 128, | ||||||||||||||||||||||||
| ) -> None: | ||||||||||||||||||||||||
| """Initialize HomeAssistantUnixSite.""" | ||||||||||||||||||||||||
| super().__init__( | ||||||||||||||||||||||||
| runner, | ||||||||||||||||||||||||
| backlog=backlog, | ||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||
| self._path = path | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| @property | ||||||||||||||||||||||||
| def name(self) -> str: | ||||||||||||||||||||||||
| """Return server URL.""" | ||||||||||||||||||||||||
| return f"http://unix:{self._path}:" | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| async def start(self) -> None: | ||||||||||||||||||||||||
| """Start server.""" | ||||||||||||||||||||||||
| await super().start() | ||||||||||||||||||||||||
| self._path.parent.mkdir(parents=True, exist_ok=True) | ||||||||||||||||||||||||
| self._path.unlink(missing_ok=True) | ||||||||||||||||||||||||
| loop = asyncio.get_running_loop() | ||||||||||||||||||||||||
| server = self._runner.server | ||||||||||||||||||||||||
| assert server is not None | ||||||||||||||||||||||||
| self._server = await loop.create_unix_server( | ||||||||||||||||||||||||
| server, | ||||||||||||||||||||||||
| self._path, | ||||||||||||||||||||||||
| backlog=self._backlog, | ||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||
agners marked this conversation as resolved.
Show resolved
Hide resolved
Comment on lines
+129
to
+131
|
||||||||||||||||||||||||
| self._server = await loop.create_unix_server( | |
| server, sock=sock, backlog=self._backlog | |
| ) | |
| try: | |
| self._server = await loop.create_unix_server( | |
| server, sock=sock, backlog=self._backlog | |
| ) | |
| except Exception: | |
| sock.close() | |
| self._path.unlink(missing_ok=True) | |
| raise |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
_create_unix_socket() handles stale entries gracefully. This seems unnecessary to me.
Uh oh!
There was an error while loading. Please reload this page.