Add auto-restart of process. Add GracefulKiller

This commit is contained in:
Luke Van Seters 2021-09-13 20:33:00 -04:00
parent 84e1a62d43
commit 474f775c8a
21 changed files with 588 additions and 10 deletions

View File

@ -18,4 +18,4 @@ COPY . /app
# easter eggs 😝 # easter eggs 😝
RUN echo "PS1='🕵️:\[\033[1;36m\]\h \[\033[1;34m\]\W\[\033[0;35m\]\[\033[1;36m\]$ \[\033[0m\]'" >> ~/.bashrc RUN echo "PS1='🕵️:\[\033[1;36m\]\h \[\033[1;34m\]\W\[\033[0;35m\]\[\033[1;36m\]$ \[\033[0m\]'" >> ~/.bashrc
CMD [ "python", "run.py"] ENTRYPOINT [ "/app/run.sh"]

View File

@ -1,22 +1,25 @@
load('ext://helm_remote', 'helm_remote') load('ext://helm_remote', 'helm_remote')
load('ext://restart_process', 'docker_build_with_restart')
load('ext://secret', 'secret_from_dict')
helm_remote("postgresql", helm_remote("postgresql",
repo_name='bitnami', repo_name='bitnami',
repo_url='https://charts.bitnami.com/bitnami', repo_url='https://charts.bitnami.com/bitnami',
set=["postgresqlPassword=password", "postgresqlDatabase=mev_inspect"], set=["postgresqlPassword=password", "postgresqlDatabase=mev_inspect"],
) )
load('ext://secret', 'secret_from_dict')
k8s_yaml(secret_from_dict("mev-inspect-db-credentials", inputs = { k8s_yaml(secret_from_dict("mev-inspect-db-credentials", inputs = {
"username" : "postgres", "username" : "postgres",
"password": "password", "password": "password",
})) }))
docker_build('mev-inspect-py', '.', docker_build_with_restart('mev-inspect-py', '.',
entrypoint="/app/run.sh",
live_update=[ live_update=[
sync('.', '/app'), sync('.', '/app'),
run('cd /app && poetry install', run('cd /app && poetry install',
trigger='./pyproject.toml'), trigger='./pyproject.toml'),
], ],
platform='linux/arm64',
) )
k8s_yaml("k8s/app.yaml") k8s_yaml("k8s/app.yaml")

View File

@ -17,7 +17,7 @@ spec:
containers: containers:
- name: mev-inspect - name: mev-inspect
image: mev-inspect-py image: mev-inspect-py
command: [ "python", "run.py"] command: [ "/app/run.sh" ]
env: env:
- name: POSTGRES_USER - name: POSTGRES_USER
valueFrom: valueFrom:

View File

@ -16,7 +16,6 @@ from mev_inspect.crud.miner_payments import (
write_miner_payments, write_miner_payments,
) )
from mev_inspect.crud.swaps import delete_swaps_for_block, write_swaps from mev_inspect.crud.swaps import delete_swaps_for_block, write_swaps
from mev_inspect.db import get_session
from mev_inspect.miner_payments import get_miner_payments from mev_inspect.miner_payments import get_miner_payments
from mev_inspect.swaps import get_swaps from mev_inspect.swaps import get_swaps
@ -45,8 +44,6 @@ def inspect_block(
classified_traces = trace_clasifier.classify(block.traces) classified_traces = trace_clasifier.classify(block.traces)
print(f"Returned {len(classified_traces)} classified traces") print(f"Returned {len(classified_traces)} classified traces")
db_session = get_session()
if should_write_classified_traces: if should_write_classified_traces:
delete_classified_traces_for_block(db_session, block_number) delete_classified_traces_for_block(db_session, block_number)
write_classified_traces(db_session, classified_traces) write_classified_traces(db_session, classified_traces)

36
run.py
View File

@ -1,5 +1,37 @@
import logging
import signal
import time import time
logging.basicConfig(filename="app.log", level=logging.DEBUG)
logger = logging.getLogger(__name__)
class GracefulKiller:
"""
handle sigint / sigterm gracefully
taken from https://stackoverflow.com/a/31464349
"""
signal_names = {signal.SIGINT: "SIGINT", signal.SIGTERM: "SIGTERM"}
def __init__(self):
self.kill_now = False
signal.signal(signal.SIGINT, self.exit_gracefully)
signal.signal(signal.SIGTERM, self.exit_gracefully)
def exit_gracefully(self, signum, frame): # pylint: disable=unused-argument
signal_name = self.signal_names[signum]
print(f"Received {signal_name} signal")
print("Cleaning up resources. End of process")
self.kill_now = True
if __name__ == "__main__": if __name__ == "__main__":
while True: logger.info("Starting...")
time.sleep(30) killer = GracefulKiller()
while not killer.kill_now:
logger.info("Running...")
time.sleep(5)
logger.info("Stopping...")

3
run.sh Executable file
View File

@ -0,0 +1,3 @@
#!/bin/bash
python run.py

View File

@ -14,6 +14,11 @@
"Name": "secret", "Name": "secret",
"ExtensionRegistry": "https://github.com/tilt-dev/tilt-extensions", "ExtensionRegistry": "https://github.com/tilt-dev/tilt-extensions",
"TimeFetched": "2021-09-09T08:57:26.199313-06:00" "TimeFetched": "2021-09-09T08:57:26.199313-06:00"
},
{
"Name": "restart_process",
"ExtensionRegistry": "https://github.com/tilt-dev/tilt-extensions",
"TimeFetched": "2021-09-13T20:14:11.011803-04:00"
} }
] ]
} }

View File

@ -0,0 +1 @@
tilt-restart-wrapper

View File

@ -0,0 +1,15 @@
FROM alpine/git
RUN apk update && apk add make
RUN apk add build-base
RUN git clone https://github.com/eradman/entr.git /entr
WORKDIR /entr
RUN git checkout c564e6bdca1dfe2177d1224363cad734158863ad
RUN ./configure; CFLAGS="-static" make install
FROM scratch
COPY --from=0 /usr/local/bin/entr /
ADD tilt-restart-wrapper /

View File

@ -0,0 +1,181 @@
# Restart Process
This extension helps create images that can restart on `live_update`:
- `docker_build_with_restart`: wraps a `docker_build` call
- `custom_build_with_restart`: wraps a `custom_build` call
At the end of a `live_update`, the container's process will rerun itself.
(Use it in place of the `restart_container()` Live Update step, which has been deprecated for Kubernetes resources.)
## When to Use
Use this extension when you have an image and you want to re-execute its entrypoint/command as part of a `live_update`.
E.g. if your app is a static binary, you'll probably need to re-execute the binary for any changes you made to take effect.
(If your app has hot reloading capabilities--i.e. it can detect and incorporate changes to its source code without needing to restart--you probably don't need this extension.)
### Unsupported Cases
This extension does NOT support process restarts for:
- Images built with `custom_build` using any of the `skips_local_docker`, `disable_push`, or `tag` parameters.
- Images run in Docker Compose resources (use the [`restart_container()`](https://docs.tilt.dev/api.html#api.restart_container) builtin instead)
- Images without a shell (e.g. `scratch`, `distroless`)
- Container commands specified as `command` in Kubernetes YAML will be overridden by this extension.
- However, the `args` field is still available; [reach out](https://tilt.dev/contact) if you need help navigating the interplay between Tilt and these YAML values
- CRDs
If this extension doesn't work for your use case, [see our docs for alternatives](https://docs.tilt.dev/live_update_reference.html#restarting-your-process).
Run into a bug? Need a use case that we don't yet support? Let us know--[open an issue](https://github.com/tilt-dev/tilt-extensions/issues) or [contact us](https://tilt.dev/contact).
## How to Use
Import this extension by putting the following at the top of your Tiltfile:
```python
load('ext://restart_process', 'docker_build_with_restart')
```
For the image that needs the process restart, replace your existing `docker_build` call:
```python
docker_build(
'foo-image',
'./foo',
arg1=val1,
arg2=val2,
live_update=[x, y, z...]
)
```
with a `docker_build_with_restart` call:
```python
docker_build_with_restart(
'foo-image',
'./foo',
entrypoint='/go/bin/foo',
arg1=val1,
arg2=val2,
live_update=[x, y, z...]
)
```
The call above looks just like the initial `docker_build` call except for one added parameter, `entrypoint` (in this example, `/go/bin/foo`). This is the command that you want to run on container start and _re-run_ on Live Update.
A custom_build call looks similar:
```python
load('ext://restart_process', 'custom_build_with_restart')
custom_build_with_restart(
'foo-image',
'docker build -t $EXPECTED_REF ./foo',
deps=['./foo'],
live_update=[sync(...)]
)
```
### Troubleshooting
#### `failed running [touch /tmp/.restart-proc']`
If you see an error of the form:
```
ERROR: Build Failed: ImageBuild: executor failed running [touch /tmp/.restart-proc']: exit code: 1
```
this often means that your Dockerfile user ([see docs](https://docs.docker.com/engine/reference/builder/#user)) doesn't have permission to write to the file we use to signal a process restart. Use the `restart_file` parameter to specify a file that your Dockerfile user definitely has write access to.
### API
```python
def docker_build_with_restart(ref: str, context: str,
entrypoint: Union[str, List[str]],
live_update: List[LiveUpdateStep],
base_suffix: str = '-base',
restart_file: str = '/.restart-proc',
trigger: Union[str, List[str]] = [],
**kwargs
):
"""Args:
ref: name for this image (e.g. 'myproj/backend' or 'myregistry/myproj/backend'); as the parameter of the same name in docker_build
context: path to use as the Docker build context; as the parameter of the same name in docker_build
entrypoint: the command to be (re-)executed when the container starts or when a live_update is run
live_update: set of steps for updating a running container; as the parameter of the same name in docker_build
base_suffix: suffix for naming the base image, applied as {ref}{base_suffix}
restart_file: file that Tilt will update during a live_update to signal the entrypoint to rerun
trigger: (optional) list of local paths. If specified, the process will ONLY be restarted when there are changes
to the given file(s); as the parameter of the same name in the LiveUpdate `run` step.
**kwargs: will be passed to the underlying `docker_build` call
"""
def custom_build_with_restart(ref: str, command: str, deps: List[str], entrypoint,
entrypoint: Union[str, List[str]],
live_update: List[LiveUpdateStep],
base_suffix: str = '-base',
restart_file: str = '/.restart-proc',
trigger: Union[str, List[str]] = [],
, **kwargs
):
"""
Args:
ref: name for this image (e.g. 'myproj/backend' or 'myregistry/myproj/backend'); as the parameter of the same name in custom_build
command: build command for building your image
deps: source dependencies of the custom build
entrypoint: the command to be (re-)executed when the container starts or when a live_update is run
live_update: set of steps for updating a running container; as the parameter of the same name in custom_build
base_suffix: suffix for naming the base image, applied as {ref}{base_suffix}
restart_file: file that Tilt will update during a live_update to signal the entrypoint to rerun
trigger: (optional) list of local paths. If specified, the process will ONLY be restarted when there are changes
to the given file(s); as the parameter of the same name in the LiveUpdate `run` step.
**kwargs: will be passed to the underlying `custom_build` call
"""
```
## What's Happening Under the Hood
*If you're a casual user/just want to get your app running, you can stop reading now. However, if you want to dig deep and know exactly what's going on, or are trying to debug weird behavior, read on.*
This extension wraps commands in `tilt-restart-wrapper`, which makes use of [`entr`](https://github.com/eradman/entr/)
to run arbitrary commands whenever a specified file changes. Specifically, we override the container's entrypoint with the following:
```
/tilt-restart-wrapper --watch_file='/.restart-proc' <entrypoint>
```
This invocation says:
- when the container starts, run <entrypoint>
- whenever the `/.restart-proc` file changes, re-execute <entrypoint>
We also set the following as the last `live_update` step:
```python
run('date > /.restart-proc')
```
Because `tilt-restart-wrapper` will re-execute the entrypoint whenever `/.restart-proc'` changes, the above `run` step will cause the entrypoint to re-run.
#### Provide `tilt-restart-wrapper`
For this all to work, the `entr` binary must be available on the Docker image. The easiest solution would be to call e.g. `apt-get install entr` in the Dockerfile, but different base images will have different package managers; rather than grapple with that, we've made a statically linked binary available on Docker image: [`tiltdev/entr`](https://hub.docker.com/repository/docker/tiltdev/entr).
To build `image-foo`, this extension will:
- build your image as normal (via `docker_build`, with all of your specified args/kwargs) but with the name `image-foo-base`
- build `image-foo` (the actual image that will be used in your resource) as a _child_ of `image-foo-base`, with the `tilt-process-wrapper` and its dependencies available
Thus, the final image produced is tagged `image-foo` and has all the properties of your original `docker_build`, plus access to the `tilt-restart-wrapper` binary.
#### Why a Wrapper?
Why bother with `tilt-restart-wrapper` rather than just calling `entr` directly?
Because in its canonical invocation, `entr` requires that the file(s) to watch be piped via stdin, i.e. it is invoked like:
```
echo "/.restart-proc" | entr -rz /bin/my-app
```
When specified as a `command` in Kubernetes or Docker Compose YAML (this is how Tilt overrides entrypoints), the above would therefore need to be executed as shell:
```
/bin/sh -c 'echo "/.restart-proc" | entr -rz /bin/my-app'
```
Any `args` specified in Kubernetes/Docker Compose are attached to the end of this call, and therefore in this case would apply TO THE `/bin/sh -c` CALL, rather than to the actual command run by `entr`; that is, any `args` specified by the user would be effectively ignored.
In order to make `entr` usable without a shell, this extension uses [a simple binary](/restart_process/tilt-restart-wrapper.go) that invokes `entr` and writes to its stdin.
Note: ideally `entr` could accept files-to-watch via flag instead of stdin, but (for a number of good reasons) this feature isn't likely to be added any time soon (see [entr#33](https://github.com/eradman/entr/issues/33)).
## For Maintainers: Releasing
If you have push access to the `tiltdev` repository on DockerHub, you can release a new version of the binaries used by this extension like so:
1. run `release.sh` (builds `tilt-restart-wrapper` from source, builds and pushes a Docker image with the new binary and a fresh binary of `entr` also installed from source)
2. update the image tag in the [Tiltfile](/restart_process/Tiltfile) with the tag you just pushed (you'll find the image referenced in the Dockerfile contents of the child image--look for "FROM tiltdev/restart-helper")

View File

@ -0,0 +1,146 @@
RESTART_FILE = '/tmp/.restart-proc'
TYPE_RESTART_CONTAINER_STEP = 'live_update_restart_container_step'
KWARGS_BLACKLIST = [
# since we'll be passing `dockerfile_contents` when building the
# child image, remove any kwargs that might conflict
'dockerfile', 'dockerfile_contents',
# 'target' isn't relevant to our child build--if we pass this arg,
# Docker will just fail to find the specified stage and error out
'target',
]
# Arguments to custom_build that don't apply to the docker_build.
_CUSTOM_BUILD_KWARGS_BLACKLIST = [
'tag',
'command_bat',
'outputs_image_ref_to',
'disable_push',
]
_ext_dir = os.getcwd()
# shared code between the two restart functions
def _helper(base_ref, ref, entrypoint, live_update, restart_file=RESTART_FILE, trigger=None, exit_policy='restart', **kwargs):
if not trigger:
trigger = []
# declare a new docker build that adds a static binary of tilt-restart-wrapper
# (which makes use of `entr` to watch files and restart processes) to the user's image
df = '''
FROM tiltdev/restart-helper:2021-08-09 as restart-helper
FROM {}
RUN ["touch", "{}"]
COPY --from=restart-helper /tilt-restart-wrapper /
COPY --from=restart-helper /entr /
'''.format(base_ref, restart_file)
# Change the entrypoint to use `tilt-restart-wrapper`.
# `tilt-restart-wrapper` makes use of `entr` (https://github.com/eradman/entr/) to
# re-execute $entrypoint whenever $restart_file changes
entrypoint_with_entr = ["/tilt-restart-wrapper", "--watch_file={}".format(restart_file)]
if exit_policy == 'continue':
entrypoint_with_entr = entrypoint_with_entr + ["--entr_flags=-r"]
if type(entrypoint) == type(""):
entrypoint_with_entr = entrypoint_with_entr + ["sh", "-c", entrypoint]
elif type(entrypoint) == type([]):
entrypoint_with_entr = entrypoint_with_entr + entrypoint
else:
fail("`entrypoint` must be a string or list of strings: got {}".format(type(entrypoint)))
# last live_update step should always be to modify $restart_file, which
# triggers the process wrapper to rerun $entrypoint
# NB: write `date` instead of just `touch`ing because `entr` doesn't respond
# to timestamp changes, only writes (see https://github.com/eradman/entr/issues/32)
live_update = live_update + [run('date > {}'.format(restart_file), trigger=trigger)]
# We don't need a real context. See:
# https://github.com/tilt-dev/tilt/issues/3897
context = _ext_dir
docker_build(ref, context, entrypoint=entrypoint_with_entr, dockerfile_contents=df,
live_update=live_update, **kwargs)
def docker_build_with_restart(ref, context, entrypoint, live_update,
base_suffix='-tilt_docker_build_with_restart_base', restart_file=RESTART_FILE,
trigger=None, exit_policy='restart', **kwargs):
"""Wrap a docker_build call and its associated live_update steps so that the last step
of any live update is to rerun the given entrypoint.
Args:
ref: name for this image (e.g. 'myproj/backend' or 'myregistry/myproj/backend'); as the parameter of the same name in docker_build
context: path to use as the Docker build context; as the parameter of the same name in docker_build
entrypoint: the command to be (re-)executed when the container starts or when a live_update is run
live_update: set of steps for updating a running container; as the parameter of the same name in docker_build
base_suffix: suffix for naming the base image, applied as {ref}{base_suffix}
restart_file: file that Tilt will update during a live_update to signal the entrypoint to rerun
trigger: (optional) list of local paths. If specified, the process will ONLY be restarted when there are changes
to the given file(s); as the parameter of the same name in the LiveUpdate `run` step.
**kwargs: will be passed to the underlying `docker_build` call
"""
# first, validate the given live_update steps
if len(live_update) == 0:
fail("`docker_build_with_restart` requires at least one live_update step")
for step in live_update:
if type(step) == TYPE_RESTART_CONTAINER_STEP:
fail("`docker_build_with_restart` is not compatible with live_update step: " +
"`restart_container()` (this extension is meant to REPLACE restart_container() )")
# rename the original image to make it a base image and declare a docker_build for it
base_ref = '{}{}'.format(ref, base_suffix)
docker_build(base_ref, context, **kwargs)
# Clean kwargs for building the child image (which builds on user's specified
# image and copies in Tilt's restart wrapper). In practice, this means removing
# kwargs that were relevant to building the user's specified image but are NOT
# relevant to building the child image / may conflict with args we specifically
# pass for the child image.
cleaned_kwargs = {k: v for k, v in kwargs.items() if k not in KWARGS_BLACKLIST}
_helper(base_ref, ref, entrypoint, live_update, restart_file, trigger, exit_policy, **cleaned_kwargs)
def custom_build_with_restart(ref, command, deps, entrypoint, live_update,
base_suffix='-tilt_docker_build_with_restart_base', restart_file=RESTART_FILE,
trigger=None, exit_policy='restart', **kwargs):
"""Wrap a custom_build call and its associated live_update steps so that the last step
of any live update is to rerun the given entrypoint.
Args:
ref: name for this image (e.g. 'myproj/backend' or 'myregistry/myproj/backend'); as the parameter of the same name in custom_build
command: build command for building your image
deps: source dependencies of the custom build
entrypoint: the command to be (re-)executed when the container starts or when a live_update is run
live_update: set of steps for updating a running container; as the parameter of the same name in custom_build
base_suffix: suffix for naming the base image, applied as {ref}{base_suffix}
restart_file: file that Tilt will update during a live_update to signal the entrypoint to rerun
trigger: (optional) list of local paths. If specified, the process will ONLY be restarted when there are changes
to the given file(s); as the parameter of the same name in the LiveUpdate `run` step.
**kwargs: will be passed to the underlying `custom_build` call
"""
# first, validate the given live_update steps
if len(live_update) == 0:
fail("`custom_build_with_restart` requires at least one live_update step")
for step in live_update:
if type(step) == TYPE_RESTART_CONTAINER_STEP:
fail("`custom_build_with_restart` is not compatible with live_update step: "+
"`restart_container()` (this extension is meant to REPLACE restart_container() )")
for k, v in kwargs.items():
if k == 'skips_local_docker':
fail("`custom_build_with_restart` is not compatible with `skips_local_docker`, because it needs access to the image")
if k == 'disable_push':
fail("`custom_build_with_restart` is not compatible with `disable_push`")
if k == 'tag':
fail("`custom_build_with_restart` renames your base image, so is not compatible with `tag`")
# rename the original image to make it a base image and declare a custom_build for it
base_ref = '{}{}'.format(ref, base_suffix)
custom_build(base_ref, command=command, deps=deps, **kwargs)
# A few arguments aren't applicable to the docker_build, so remove them.
cleaned_kwargs = {k: v for k, v in kwargs.items() if k not in _CUSTOM_BUILD_KWARGS_BLACKLIST}
_helper(base_ref, ref, entrypoint, live_update, restart_file, trigger, exit_policy, **cleaned_kwargs)

View File

@ -0,0 +1,24 @@
#!/bin/bash
set -ex
TIMESTAMP=$(date +'%Y-%m-%d')
IMAGE_NAME='tiltdev/restart-helper'
IMAGE_WITH_TAG=$IMAGE_NAME:$TIMESTAMP
# build binary for tilt-restart-wrapper
env GOOS=linux GOARCH=amd64 go build tilt-restart-wrapper.go
# build Docker image with static binaries of:
# - tilt-restart-wrapper (compiled above)
# - entr (dependency of tilt-restart-wrapper)
docker build . -t "$IMAGE_NAME"
docker push "$IMAGE_NAME"
docker tag "$IMAGE_NAME" "$IMAGE_WITH_TAG"
docker push "$IMAGE_WITH_TAG"
echo "Successfully built and pushed $IMAGE_WITH_TAG"

View File

@ -0,0 +1,5 @@
FROM busybox
COPY fail.sh /
ENTRYPOINT /fail.sh

View File

@ -0,0 +1,5 @@
load('../Tiltfile', 'docker_build_with_restart')
k8s_yaml('job.yaml')
docker_build_with_restart('failing_job', '.', '/fail.sh',
live_update=[sync('./fail.sh', '/fail.sh')])

View File

@ -0,0 +1,9 @@
load('../Tiltfile', 'custom_build_with_restart')
k8s_yaml('job.yaml')
custom_build_with_restart(
'failing_job',
command='docker build -t $EXPECTED_REF .',
deps=['fail.sh'],
entrypoint='/fail.sh',
live_update=[sync('./fail.sh', '/fail.sh')])

View File

@ -0,0 +1,9 @@
#! /bin/sh
echo "Are you there, pod?"
sleep 1
# Exit with a non-zero status code; we check that docker_build_with_restart
# surfaces this error code to k8s, so k8s knows that the job failed.
echo "Exiting with status code 123 😱"
exit 123

View File

@ -0,0 +1,12 @@
apiVersion: batch/v1
kind: Job
metadata:
name: failing-job
spec:
template:
spec:
containers:
- name: failing-job
image: failing_job
restartPolicy: Never
backoffLimit: 4

View File

@ -0,0 +1,21 @@
#!/bin/bash
cd "$(dirname "$0")" || exit 1
set -x
tilt ci -f custom.Tiltfile > tilt.log 2>&1
CI_EXIT=$?
tilt down
if [ $CI_EXIT -eq 0 ]; then
echo "Expected 'tilt ci' to fail, but succeeded."
exit 1
fi
grep -q "Are you there, pod?" tilt.log
GREP_EXIT=$?
rm tilt.log
exit $GREP_EXIT

View File

@ -0,0 +1,29 @@
#!/bin/bash
# Test case for https://github.com/tilt-dev/tilt-extensions/issues/92
#
# This job will always exit with a non-zero status code; make sure
# that docker_build_with_restart surfaces this error code to k8s,
# so k8s knows that the job failed. (Thus, we expect the `tilt ci`
# call to fail.)
cd "$(dirname "$0")" || exit 1
set -x
tilt ci > tilt.log 2>&1
CI_EXIT=$?
tilt down
if [ $CI_EXIT -eq 0 ]; then
echo "Expected 'tilt ci' to fail, but succeeded."
exit 1
fi
grep -q "Are you there, pod?" tilt.log
GREP_EXIT=$?
cat tilt.log
rm tilt.log
exit $GREP_EXIT

View File

@ -0,0 +1,7 @@
#!/bin/bash
set -ex
cd "$(dirname "$0")"
./test-docker.sh
./test-custom.sh

View File

@ -0,0 +1,74 @@
// `tilt-restart-wrapper` wraps `entr` (http://eradman.com/entrproject/) to easily
// rerun a user-specified command when a given file changes.
//
// This is Tilt's recommended way of restarting a process as part of a live_update:
// if your container invokes your app via the restart wrapper (e.g. `tilt-restart-wrapper /bin/my-app`),
// you can trigger re-execution of your app with a live_update `run` step that makes
// a trivial change to the file watched by `entr` (e.g. `run('date > /.restart-proc')`)
//
// This script exists (i.e. we're wrapping `entr` in a binary instead of invoking
// it directly) because in its canonical invocation, `entr` requires that the
// file(s) to watch be piped via stdin, i.e. it is invoked like:
// echo "/.restart-proc" | entr -rz /bin/my-app
//
// When specified as a `command` in Kubernetes or Docker Compose YAML (this is how
// Tilt overrides entrypoints), the above would therefore need to be executed as shell:
// /bin/sh -c 'echo "/.restart-proc" | entr -rz /bin/my-app'
// Any args specified in Kubernetes or Docker Compose YAML are attached to the end
// of this call, and therefore in this case apply TO THE `/bin/sh -c` CALL, rather
// than to the actual command run by `entr`; that is, any `args` specified by the
// user would be effectively ignored.
//
// In order to make `entr` executable as ARGV rather than as shell, we have wrapped it
// in a binary that can be called directly and takes care of the piping under the hood.
//
// Note: ideally `entr` could accept files-to-watch via flag instead of stdin,
// but (for a number of good reasons) this feature isn't likely to be added any
// time soon (see https://github.com/eradman/entr/issues/33).
package main
import (
"flag"
"fmt"
"log"
"os"
"os/exec"
"strings"
"syscall"
)
var watchFile = flag.String("watch_file", "/.restart-proc", "File that entr will watch for changes; changes to this file trigger `entr` to rerun the command(s) passed")
var entrPath = flag.String("entr_path", "/entr", "Path to `entr` executable")
var entrFlags = flag.String("entr_flags", "-rz", "Command line flags to pass to `entr` executable")
func main() {
flag.Parse()
cmd := exec.Command(*entrPath, *entrFlags)
cmd.Stdin = strings.NewReader(fmt.Sprintf("%s\n", *watchFile))
cmd.Args = append(cmd.Args, flag.Args()...)
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
if err := cmd.Run(); err != nil {
if exiterr, ok := err.(*exec.ExitError); ok {
// The program has exited with an exit code != 0
if status, ok := exiterr.Sys().(syscall.WaitStatus); ok {
if len(flag.Args()) == 0 {
log.Println("`tilt-restart-wrapper` requires at least one positional arg " +
"(a command or set of args to be executed / rerun whenever `watch_file` changes)")
}
os.Exit(status.ExitStatus())
}
} else {
log.Fatalf("error running command: %v", err)
}
}
if len(flag.Args()) == 0 {
log.Fatal("`tilt-restart-wrapper` requires at least one positional arg " +
"(will be passed to `entr` and executed / rerun whenever `watch_file` changes)")
}
}