Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 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
43 changes: 40 additions & 3 deletions packages/prime-tunnel/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,9 +33,9 @@ await tunnel.restart()
```

A tunnel that was deleted, expired, or stayed disconnected for too long cannot
be restarted: `restart()` raises `TunnelGoneError`, and `tunnel.is_gone` turns
true as soon as the tunnel service rejects the client. Stop it and start a new
tunnel, which gets a new URL. `await tunnel.status()` reports the client
be restarted: `restart()` raises `TunnelGoneError`, and `tunnel.state` turns
`TunnelState.DEAD` as soon as the tunnel service rejects the client. Stop it
and start a new tunnel, which gets a new URL. `await tunnel.status()` reports the client
process and the service's record of the tunnel:

```python
Expand All @@ -44,6 +44,43 @@ if status.gone:
await tunnel.stop()
```

### Keeping a tunnel up

Pass `supervise=True` to have the tunnel watch and repair itself after
`start()`. It restarts its client under the same URL when the client exits or
when requests through the URL stop reaching your service, and it marks the
tunnel dead when the registration is gone:

```python
tunnel = Tunnel(local_port=8765, supervise=True, on_state_change=print)
await tunnel.start()

if await tunnel.wait_dead(): # False if the tunnel was stopped instead
await tunnel.stop()
# start a new Tunnel; it gets a new URL
```

`tunnel.state` is `up`, `recovering`, `dead` or `stopped`, and
`on_state_change` is called with each new state.

The supervisor sends a GET through the URL every 15 seconds. It blames the
tunnel only if the same request sent straight to your service succeeds, so a
service that is down or returns errors itself does not cause restarts. Pass a
`Supervision` instead of `True` to choose what it requests:

```python
from prime_tunnel import Supervision, Tunnel

tunnel = Tunnel(
local_port=8765,
supervise=Supervision(probe_path="/health", probe_header="X-My-Service"),
)
```

`probe_header` names a header your service sets on every response. A response
without it is then known not to come from your service, whatever its status
code.

## CLI Usage

```bash
Expand Down
5 changes: 4 additions & 1 deletion packages/prime-tunnel/src/prime_tunnel/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,8 @@
TunnelLimitReachedError,
TunnelTimeoutError,
)
from prime_tunnel.models import TunnelInfo, TunnelListPage, TunnelStatus
from prime_tunnel.models import TunnelInfo, TunnelListPage, TunnelState, TunnelStatus
from prime_tunnel.supervisor import Supervision

logging.getLogger("prime_tunnel").addHandler(logging.NullHandler())

Expand All @@ -25,9 +26,11 @@
"TunnelClient",
# Main interface
"Tunnel",
"Supervision",
# Models
"TunnelInfo",
"TunnelListPage",
"TunnelState",
"TunnelStatus",
# Exceptions
"BinaryDownloadError",
Expand Down
14 changes: 14 additions & 0 deletions packages/prime-tunnel/src/prime_tunnel/models.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
from datetime import datetime
from enum import Enum
from typing import List, Optional

from pydantic import BaseModel, Field
Expand Down Expand Up @@ -40,6 +41,19 @@ class Config:
from_attributes = True


class TunnelState(str, Enum):
"""Where a Tunnel is in its life, as far as the client knows."""

# Not started, or stopped.
STOPPED = "stopped"
# Started and not known to be down.
UP = "up"
# Not serving; the supervisor is restarting frpc under the same URL.
RECOVERING = "recovering"
# The registration is gone. Only a new tunnel, with a new URL, helps.
DEAD = "dead"


class TunnelStatus(BaseModel):
"""What is known about a started tunnel: the local frpc and the service's record."""

Expand Down
Loading
Loading