Skip to content
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

feat: local network process MGMT CLI #2545

Merged
merged 26 commits into from
Mar 14, 2025
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
25 changes: 25 additions & 0 deletions docs/userguides/networks.md
Original file line number Diff line number Diff line change
Expand Up @@ -525,6 +525,14 @@ To run a network with a process, use the `ape networks run` command:
ape networks run
```

This launches a development node in the current working terminal session.
To continue developing, you will have to launch a new terminal session.
Alternatively, you can use the `--background` flag to background the process:

```shell
ape networks run --background
```

By default, `ape networks run` runs a development Node (geth) process.
To use a different network, such as `hardhat` or Anvil nodes, use the `--network` flag:

Expand All @@ -534,6 +542,23 @@ ape networks run --network ethereum:local:foundry

To configure the network's block time, use the `--block-time` option.

```shell
ape networks run --network ethereum:local:foundry --block-time 10
```

Once you are done with your node, you can simply exit the process to tear it down.
Or, if you used `--background` or lost the process some other way, you can stop the node using the `kill` command:

```shell
ape networks kill --all
```

To list all running networks, use the `list --running` command:

```shell
ape networks list --running
```

## Provider Interaction

Once you are connected to a network, you now have access to a `.provider`.
Expand Down
2 changes: 1 addition & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@
"hypothesis-jsonschema==0.19.0", # JSON Schema fuzzer extension
],
"lint": [
"ruff>=0.9.10", # Unified linter and formatter
"ruff>=0.9.10,<0.10", # Unified linter and formatter
"mypy>=1.15.0,<1.16.0", # Static type analyzer
"types-PyYAML", # Needed due to mypy typeshed
"types-requests", # Needed due to mypy typeshed
Expand Down
44 changes: 40 additions & 4 deletions src/ape/api/providers.py
Original file line number Diff line number Diff line change
Expand Up @@ -253,6 +253,13 @@ def disconnect(self):
Disconnect from a provider, such as tear-down a process or quit an HTTP session.
"""

@property
def ipc_path(self) -> Optional[Path]:
"""
Return the IPC path for the provider, if supported.
"""
return None

@property
def http_uri(self) -> Optional[str]:
"""
Expand Down Expand Up @@ -984,7 +991,9 @@ class SubprocessProvider(ProviderAPI):
"""

PROCESS_WAIT_TIMEOUT: int = 15
background: bool = False
process: Optional[Popen] = None
allow_start: bool = True
is_stopping: bool = False

stdout_queue: Optional[JoinableQueue] = None
Expand Down Expand Up @@ -1059,7 +1068,7 @@ def connect(self):
or self.config_manager.get_config("test").disconnect_providers_after
)
if disconnect_after:
atexit.register(self.disconnect)
atexit.register(self._disconnect_atexit)

# Register handlers to ensure atexit handlers are called when Python dies.
def _signal_handler(signum, frame):
Expand All @@ -1069,6 +1078,12 @@ def _signal_handler(signum, frame):
signal(SIGINT, _signal_handler)
signal(SIGTERM, _signal_handler)

def _disconnect_atexit(self):
if self.background:
return

self.disconnect()

def disconnect(self):
"""
Stop the process if it exists.
Expand All @@ -1078,25 +1093,38 @@ def disconnect(self):
if self.process:
self.stop()

# Delete entry from managed list of running nodes.
self.network_manager.running_nodes.remove_provider(self)

def start(self, timeout: int = 20):
"""Start the process and wait for its RPC to be ready."""

if self.is_connected:
logger.info(f"Connecting to existing '{self.process_name}' process.")
self.process = None # Not managing the process.
else:

elif self.allow_start:
logger.info(f"Starting '{self.process_name}' process.")
pre_exec_fn = _linux_set_death_signal if platform.uname().system == "Linux" else None
self.stderr_queue = JoinableQueue()
self.stdout_queue = JoinableQueue()
out_file = PIPE if logger.level <= LogLevel.DEBUG else DEVNULL

if self.background or logger.level > LogLevel.DEBUG:
out_file = DEVNULL
else:
out_file = PIPE

cmd = self.build_command()
self.process = Popen(cmd, preexec_fn=pre_exec_fn, stdout=out_file, stderr=out_file)
process = popen(cmd, preexec_fn=pre_exec_fn, stdout=out_file, stderr=out_file)
self.process = process
spawn(self.produce_stdout_queue)
spawn(self.produce_stderr_queue)
spawn(self.consume_stdout_queue)
spawn(self.consume_stderr_queue)

# Cache the process so we can manage it even if lost.
self.network_manager.running_nodes.cache_provider(self)

with RPCTimeoutError(self, seconds=timeout) as _timeout:
while True:
if self.is_connected:
Expand All @@ -1105,6 +1133,9 @@ def start(self, timeout: int = 20):
time.sleep(0.1)
_timeout.check()

else:
raise ProviderError("Process not started and cannot connect to existing process.")

def produce_stdout_queue(self):
process = self.process
if self.stdout_queue is None or process is None:
Expand Down Expand Up @@ -1250,3 +1281,8 @@ def _linux_set_death_signal():
# the second argument is what signal to send to child subprocesses
libc = ctypes.CDLL("libc.so.6")
return libc.prctl(1, SIGTERM)


def popen(cmd: list[str], **kwargs):
# Abstracted for testing purporses.
return Popen(cmd, **kwargs)
Loading
Loading