olas-operate-middleware 0.1.0rc59__py3-none-any.whl → 0.13.2__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (98) hide show
  1. olas_operate_middleware-0.13.2.dist-info/METADATA +75 -0
  2. olas_operate_middleware-0.13.2.dist-info/RECORD +101 -0
  3. {olas_operate_middleware-0.1.0rc59.dist-info → olas_operate_middleware-0.13.2.dist-info}/WHEEL +1 -1
  4. operate/__init__.py +17 -0
  5. operate/account/user.py +35 -9
  6. operate/bridge/bridge_manager.py +470 -0
  7. operate/bridge/providers/lifi_provider.py +377 -0
  8. operate/bridge/providers/native_bridge_provider.py +677 -0
  9. operate/bridge/providers/provider.py +469 -0
  10. operate/bridge/providers/relay_provider.py +457 -0
  11. operate/cli.py +1565 -417
  12. operate/constants.py +60 -12
  13. operate/data/README.md +19 -0
  14. operate/data/contracts/{service_staking_token → dual_staking_token}/__init__.py +2 -2
  15. operate/data/contracts/dual_staking_token/build/DualStakingToken.json +443 -0
  16. operate/data/contracts/dual_staking_token/contract.py +132 -0
  17. operate/data/contracts/dual_staking_token/contract.yaml +23 -0
  18. operate/{ledger/base.py → data/contracts/foreign_omnibridge/__init__.py} +2 -19
  19. operate/data/contracts/foreign_omnibridge/build/ForeignOmnibridge.json +1372 -0
  20. operate/data/contracts/foreign_omnibridge/contract.py +130 -0
  21. operate/data/contracts/foreign_omnibridge/contract.yaml +23 -0
  22. operate/{ledger/solana.py → data/contracts/home_omnibridge/__init__.py} +2 -20
  23. operate/data/contracts/home_omnibridge/build/HomeOmnibridge.json +1421 -0
  24. operate/data/contracts/home_omnibridge/contract.py +80 -0
  25. operate/data/contracts/home_omnibridge/contract.yaml +23 -0
  26. operate/data/contracts/l1_standard_bridge/__init__.py +20 -0
  27. operate/data/contracts/l1_standard_bridge/build/L1StandardBridge.json +831 -0
  28. operate/data/contracts/l1_standard_bridge/contract.py +158 -0
  29. operate/data/contracts/l1_standard_bridge/contract.yaml +23 -0
  30. operate/data/contracts/l2_standard_bridge/__init__.py +20 -0
  31. operate/data/contracts/l2_standard_bridge/build/L2StandardBridge.json +626 -0
  32. operate/data/contracts/l2_standard_bridge/contract.py +130 -0
  33. operate/data/contracts/l2_standard_bridge/contract.yaml +23 -0
  34. operate/data/contracts/mech_activity/__init__.py +20 -0
  35. operate/data/contracts/mech_activity/build/MechActivity.json +111 -0
  36. operate/data/contracts/mech_activity/contract.py +44 -0
  37. operate/data/contracts/mech_activity/contract.yaml +23 -0
  38. operate/data/contracts/optimism_mintable_erc20/__init__.py +20 -0
  39. operate/data/contracts/optimism_mintable_erc20/build/OptimismMintableERC20.json +491 -0
  40. operate/data/contracts/optimism_mintable_erc20/contract.py +45 -0
  41. operate/data/contracts/optimism_mintable_erc20/contract.yaml +23 -0
  42. operate/data/contracts/recovery_module/__init__.py +20 -0
  43. operate/data/contracts/recovery_module/build/RecoveryModule.json +811 -0
  44. operate/data/contracts/recovery_module/contract.py +61 -0
  45. operate/data/contracts/recovery_module/contract.yaml +23 -0
  46. operate/data/contracts/requester_activity_checker/__init__.py +20 -0
  47. operate/data/contracts/requester_activity_checker/build/RequesterActivityChecker.json +111 -0
  48. operate/data/contracts/requester_activity_checker/contract.py +33 -0
  49. operate/data/contracts/requester_activity_checker/contract.yaml +23 -0
  50. operate/data/contracts/staking_token/__init__.py +20 -0
  51. operate/data/contracts/staking_token/build/StakingToken.json +1336 -0
  52. operate/data/contracts/{service_staking_token → staking_token}/contract.py +27 -13
  53. operate/data/contracts/staking_token/contract.yaml +23 -0
  54. operate/data/contracts/uniswap_v2_erc20/contract.yaml +3 -1
  55. operate/data/contracts/uniswap_v2_erc20/tests/__init__.py +20 -0
  56. operate/data/contracts/uniswap_v2_erc20/tests/test_contract.py +363 -0
  57. operate/keys.py +118 -33
  58. operate/ledger/__init__.py +159 -56
  59. operate/ledger/profiles.py +321 -18
  60. operate/migration.py +555 -0
  61. operate/{http → operate_http}/__init__.py +3 -2
  62. operate/{http → operate_http}/exceptions.py +6 -4
  63. operate/operate_types.py +544 -0
  64. operate/pearl.py +13 -1
  65. operate/quickstart/analyse_logs.py +118 -0
  66. operate/quickstart/claim_staking_rewards.py +104 -0
  67. operate/quickstart/reset_configs.py +106 -0
  68. operate/quickstart/reset_password.py +70 -0
  69. operate/quickstart/reset_staking.py +145 -0
  70. operate/quickstart/run_service.py +726 -0
  71. operate/quickstart/stop_service.py +72 -0
  72. operate/quickstart/terminate_on_chain_service.py +83 -0
  73. operate/quickstart/utils.py +298 -0
  74. operate/resource.py +62 -3
  75. operate/services/agent_runner.py +202 -0
  76. operate/services/deployment_runner.py +868 -0
  77. operate/services/funding_manager.py +929 -0
  78. operate/services/health_checker.py +280 -0
  79. operate/services/manage.py +2356 -620
  80. operate/services/protocol.py +1246 -340
  81. operate/services/service.py +756 -391
  82. operate/services/utils/mech.py +103 -0
  83. operate/services/utils/tendermint.py +86 -12
  84. operate/settings.py +70 -0
  85. operate/utils/__init__.py +135 -0
  86. operate/utils/gnosis.py +407 -80
  87. operate/utils/single_instance.py +226 -0
  88. operate/utils/ssl.py +133 -0
  89. operate/wallet/master.py +708 -123
  90. operate/wallet/wallet_recovery_manager.py +507 -0
  91. olas_operate_middleware-0.1.0rc59.dist-info/METADATA +0 -304
  92. olas_operate_middleware-0.1.0rc59.dist-info/RECORD +0 -41
  93. operate/data/contracts/service_staking_token/build/ServiceStakingToken.json +0 -1273
  94. operate/data/contracts/service_staking_token/contract.yaml +0 -23
  95. operate/ledger/ethereum.py +0 -48
  96. operate/types.py +0 -260
  97. {olas_operate_middleware-0.1.0rc59.dist-info → olas_operate_middleware-0.13.2.dist-info}/entry_points.txt +0 -0
  98. {olas_operate_middleware-0.1.0rc59.dist-info → olas_operate_middleware-0.13.2.dist-info/licenses}/LICENSE +0 -0
@@ -0,0 +1,868 @@
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+ # ------------------------------------------------------------------------------
4
+ #
5
+ # Copyright 2024 Valory AG
6
+ #
7
+ # Licensed under the Apache License, Version 2.0 (the "License");
8
+ # you may not use this file except in compliance with the License.
9
+ # You may obtain a copy of the License at
10
+ #
11
+ # http://www.apache.org/licenses/LICENSE-2.0
12
+ #
13
+ # Unless required by applicable law or agreed to in writing, software
14
+ # distributed under the License is distributed on an "AS IS" BASIS,
15
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16
+ # See the License for the specific language governing permissions and
17
+ # limitations under the License.
18
+ #
19
+ # ------------------------------------------------------------------------------
20
+ """Source code to run and stop deployments created."""
21
+ import ctypes
22
+ import json
23
+ import multiprocessing
24
+ import os
25
+ import platform
26
+ import shutil # nosec
27
+ import subprocess # nosec
28
+ import sys # nosec
29
+ import time
30
+ import typing as t
31
+ from abc import ABC, ABCMeta, abstractmethod
32
+ from contextlib import suppress
33
+ from enum import Enum
34
+ from io import TextIOWrapper
35
+ from pathlib import Path
36
+ from traceback import print_exc
37
+ from typing import Any, Dict, List, Type
38
+ from venv import main as venv_cli
39
+
40
+ import psutil
41
+ import requests
42
+ from aea.__version__ import __version__ as aea_version
43
+ from aea.helpers.logging import setup_logger
44
+ from autonomy.__version__ import __version__ as autonomy_version
45
+
46
+ from operate import constants
47
+
48
+ from .agent_runner import get_agent_runner_path
49
+
50
+
51
+ class AbstractDeploymentRunner(ABC):
52
+ """Abstract deployment runner."""
53
+
54
+ def __init__(self, work_directory: Path) -> None:
55
+ """Init the deployment runner."""
56
+ self._work_directory = work_directory
57
+
58
+ @abstractmethod
59
+ def start(self, password: str) -> None:
60
+ """Start the deployment."""
61
+
62
+ @abstractmethod
63
+ def stop(self) -> None:
64
+ """Stop the deployment."""
65
+
66
+
67
+ def _kill_process(pid: int) -> None:
68
+ """Kill process."""
69
+ while True:
70
+ if not psutil.pid_exists(pid=pid):
71
+ return
72
+ process = psutil.Process(pid=pid)
73
+ if process.status() in (
74
+ psutil.STATUS_DEAD,
75
+ psutil.STATUS_ZOMBIE,
76
+ ):
77
+ return
78
+ try:
79
+ process.kill()
80
+ except OSError:
81
+ return
82
+ except psutil.AccessDenied:
83
+ return
84
+ time.sleep(1)
85
+
86
+
87
+ def kill_process(pid: int) -> None:
88
+ """Kill the process and all children first."""
89
+ if not psutil.pid_exists(pid=pid):
90
+ return
91
+ current_process = psutil.Process(pid=pid)
92
+ children = list(reversed(current_process.children(recursive=True)))
93
+ for child in children:
94
+ _kill_process(child.pid)
95
+ _kill_process(child.pid)
96
+ _kill_process(pid)
97
+ _kill_process(pid)
98
+
99
+
100
+ class BaseDeploymentRunner(AbstractDeploymentRunner, metaclass=ABCMeta):
101
+ """Base deployment with aea support."""
102
+
103
+ TM_CONTROL_URL = constants.TM_CONTROL_URL
104
+ SLEEP_BEFORE_TM_KILL = 2 # seconds
105
+ START_TRIES = constants.DEPLOYMENT_START_TRIES_NUM
106
+ logger = setup_logger(name="operate.base_deployment_runner")
107
+
108
+ def __init__(self, work_directory: Path, is_aea: bool) -> None:
109
+ """Initialize the deployment runner."""
110
+ super().__init__(work_directory)
111
+ self._is_aea = is_aea
112
+
113
+ def _open_agent_runner_log_file(self) -> TextIOWrapper:
114
+ """Open agent_runner.log file."""
115
+ return (self._get_operate_dir() / "agent_runner.log").open("w+")
116
+
117
+ def _open_tendermint_log_file(self) -> TextIOWrapper:
118
+ """Open tm.log file."""
119
+ return (self._get_operate_dir() / "tm.log").open("w+")
120
+
121
+ def _get_operate_dir(self) -> Path:
122
+ """Get .operate dir."""
123
+ return Path(self._work_directory).parent.parent.parent
124
+
125
+ def _run_aea_command(self, *args: str, cwd: Path) -> Any:
126
+ """Run aea command."""
127
+ no_password_args = []
128
+ for i, arg in enumerate(args):
129
+ if i > 0 and args[i - 1] == "--password":
130
+ no_password_args.append("******")
131
+ elif arg.startswith("--password="):
132
+ no_password_args.append("--password=******")
133
+ else:
134
+ no_password_args.append(arg)
135
+
136
+ self.logger.info(
137
+ f"Running aea command: {' '.join(no_password_args)} at {str(cwd)}"
138
+ )
139
+ p = multiprocessing.Process(
140
+ target=self.__class__._call_aea_command, # pylint: disable=protected-access
141
+ args=(cwd, args),
142
+ )
143
+ p.start()
144
+ p.join()
145
+ if p.exitcode != 0:
146
+ raise RuntimeError(
147
+ f"aea command `{' '.join(no_password_args)}` execution failed with exit code: {p.exitcode}"
148
+ )
149
+
150
+ @staticmethod
151
+ def _call_aea_command(cwd: str | Path, args: List[str]) -> None:
152
+ try:
153
+ import os # pylint: disable=redefined-outer-name,reimported,import-outside-toplevel
154
+
155
+ os.chdir(cwd)
156
+ # pylint: disable-next=import-outside-toplevel
157
+ from aea.cli.core import cli as call_aea
158
+
159
+ call_aea( # pylint: disable=unexpected-keyword-arg, no-value-for-parameter
160
+ args, standalone_mode=False
161
+ )
162
+ except Exception:
163
+ print(f"Error on calling aea command: {args}")
164
+ print_exc()
165
+ raise
166
+
167
+ def _run_cmd(self, args: t.List[str], cwd: t.Optional[Path] = None) -> None:
168
+ """Run command in a subprocess."""
169
+ self.logger.info(f"Running: {' '.join(args)}")
170
+ self.logger.info(f"Working dir: {os.getcwd()}")
171
+ result = subprocess.run( # pylint: disable=subprocess-run-check # nosec
172
+ args=args,
173
+ cwd=cwd,
174
+ stdout=subprocess.PIPE,
175
+ stderr=subprocess.PIPE,
176
+ )
177
+ if result.returncode != 0:
178
+ raise RuntimeError(
179
+ f"Error running: {args} @ {cwd}\n{result.stderr.decode()}"
180
+ )
181
+
182
+ def _prepare_agent_env(self) -> Any:
183
+ """Prepare agent env, add keys, run aea commands."""
184
+ working_dir = self._work_directory
185
+ env = json.loads((working_dir / "agent.json").read_text(encoding="utf-8"))
186
+
187
+ # TODO: Dynamic port allocation, backport to service builder
188
+ env["PYTHONUTF8"] = "1"
189
+ for var in env:
190
+ # Fix tendermint connection params
191
+ if var.endswith("MODELS_PARAMS_ARGS_TENDERMINT_COM_URL"):
192
+ env[var] = self.TM_CONTROL_URL
193
+
194
+ if var.endswith("MODELS_PARAMS_ARGS_TENDERMINT_URL"):
195
+ env[var] = "http://localhost:26657"
196
+
197
+ if var.endswith("MODELS_PARAMS_ARGS_TENDERMINT_P2P_URL"):
198
+ env[var] = "localhost:26656"
199
+
200
+ (working_dir / "agent.json").write_text(
201
+ json.dumps(env, indent=4),
202
+ encoding="utf-8",
203
+ )
204
+ return env
205
+
206
+ def _setup_agent(self, password: str) -> None:
207
+ """Setup agent with retries for network operations."""
208
+ max_attempts = 10
209
+ for attempt in range(1, max_attempts + 1):
210
+ try:
211
+ working_dir = self._work_directory
212
+ env = self._prepare_agent_env()
213
+
214
+ # Clear agent directory before each attempt to avoid partial state
215
+ agent_alias_name = "agent"
216
+ agent_dir_full_path = Path(working_dir) / agent_alias_name
217
+ if agent_dir_full_path.exists():
218
+ with suppress(Exception):
219
+ shutil.rmtree(agent_dir_full_path, ignore_errors=True)
220
+
221
+ self._run_aea_command(
222
+ "init",
223
+ "--reset",
224
+ "--author",
225
+ "valory",
226
+ "--remote",
227
+ "--ipfs",
228
+ "--ipfs-node",
229
+ "/dns/registry.autonolas.tech/tcp/443/https",
230
+ cwd=working_dir,
231
+ )
232
+
233
+ self._run_aea_command(
234
+ "-s",
235
+ "fetch",
236
+ env["AEA_AGENT"],
237
+ "--alias",
238
+ agent_alias_name,
239
+ cwd=working_dir,
240
+ )
241
+
242
+ # Add keys
243
+ shutil.copy(
244
+ working_dir / "ethereum_private_key.txt",
245
+ working_dir / "agent" / "ethereum_private_key.txt",
246
+ )
247
+
248
+ self._run_aea_command(
249
+ "-s",
250
+ "add-key",
251
+ "--password",
252
+ password,
253
+ "ethereum",
254
+ cwd=working_dir / "agent",
255
+ )
256
+ self._run_aea_command(
257
+ "-s",
258
+ "add-key",
259
+ "--password",
260
+ password,
261
+ "ethereum",
262
+ "--connection",
263
+ cwd=working_dir / "agent",
264
+ )
265
+
266
+ self._run_aea_command(
267
+ "-s",
268
+ "issue-certificates",
269
+ "--password",
270
+ password,
271
+ cwd=working_dir / "agent",
272
+ )
273
+
274
+ # Success - break out of retry loop
275
+ self.logger.info(
276
+ f"Agent setup completed successfully on attempt {attempt}"
277
+ )
278
+ break
279
+
280
+ except Exception as e: # pylint: disable=broad-except
281
+ self.logger.warning(
282
+ f"Agent setup attempt {attempt}/{max_attempts} failed: {e}"
283
+ )
284
+ if attempt < max_attempts:
285
+ sleep_time = attempt * 5
286
+ self.logger.info(f"Retrying agent setup in {sleep_time} seconds...")
287
+ time.sleep(sleep_time)
288
+ else:
289
+ self.logger.error(f"All {max_attempts} agent setup attempts failed")
290
+ raise
291
+
292
+ def start(self, password: str) -> None:
293
+ """Start the deployment with retries."""
294
+ for _ in range(self.START_TRIES):
295
+ try:
296
+ self._start(password=password)
297
+ return
298
+ except Exception as e: # pylint: disable=broad-except
299
+ self.logger.exception(f"Error on starting deployment: {e}")
300
+ raise RuntimeError(
301
+ f"Failed to start the deployment after {self.START_TRIES} attempts! Check logs"
302
+ )
303
+
304
+ def _start(self, password: str) -> None:
305
+ """Start the deployment."""
306
+ self._setup_agent(password=password)
307
+ if self._is_aea:
308
+ self._start_tendermint()
309
+
310
+ self._start_agent(password=password)
311
+
312
+ def stop(self) -> None:
313
+ """Stop the deployment."""
314
+ self._stop_agent()
315
+ if self._is_aea:
316
+ self._stop_tendermint()
317
+
318
+ def _stop_agent(self) -> None:
319
+ """Start process."""
320
+ pid = self._work_directory / "agent.pid"
321
+ if not pid.exists():
322
+ return
323
+ kill_process(int(pid.read_text(encoding="utf-8")))
324
+
325
+ def _get_tm_exit_url(self) -> str:
326
+ return f"{self.TM_CONTROL_URL}/exit"
327
+
328
+ def _stop_tendermint(self) -> None:
329
+ """Stop tendermint process."""
330
+ try:
331
+ requests.get(self._get_tm_exit_url(), timeout=(1, 10))
332
+ time.sleep(self.SLEEP_BEFORE_TM_KILL)
333
+ except requests.ConnectionError:
334
+ self.logger.error(
335
+ f"No Tendermint process listening on {self._get_tm_exit_url()}."
336
+ )
337
+ except Exception: # pylint: disable=broad-except
338
+ self.logger.exception("Exception on tendermint stop!")
339
+
340
+ pid = self._work_directory / "tendermint.pid"
341
+ if not pid.exists():
342
+ return
343
+ kill_process(int(pid.read_text(encoding="utf-8")))
344
+
345
+ @abstractmethod
346
+ def _start_tendermint(self) -> None:
347
+ """Start tendermint process."""
348
+
349
+ @abstractmethod
350
+ def _start_agent(self, password: str) -> None:
351
+ """Start aea process."""
352
+
353
+ @property
354
+ @abstractmethod
355
+ def _agent_runner_bin(self) -> str:
356
+ """Return aea_bin path."""
357
+ raise NotImplementedError
358
+
359
+ def get_agent_start_args(self, password: str) -> List[str]:
360
+ """Return agent start arguments."""
361
+ return (
362
+ [self._agent_runner_bin]
363
+ + (
364
+ [
365
+ "-s",
366
+ "run",
367
+ ]
368
+ if self._is_aea
369
+ else []
370
+ )
371
+ + [
372
+ "--password",
373
+ password,
374
+ ]
375
+ )
376
+
377
+
378
+ class PyInstallerHostDeploymentRunner(BaseDeploymentRunner):
379
+ """Deployment runner within pyinstaller env."""
380
+
381
+ @property
382
+ def _agent_runner_bin(self) -> str:
383
+ """Return aea_bin path."""
384
+ service_dir = self._work_directory.parent
385
+ agent_runner_bin = get_agent_runner_path(service_dir=service_dir)
386
+ return str(agent_runner_bin)
387
+
388
+ @property
389
+ def _tendermint_bin(self) -> str:
390
+ """Return tendermint path."""
391
+ return str(Path(os.path.dirname(sys.executable)) / "tendermint_bin") # type: ignore # pylint: disable=protected-access
392
+
393
+ def _start_agent(self, password: str) -> None:
394
+ """Start agent process."""
395
+ working_dir = self._work_directory
396
+ env = json.loads((working_dir / "agent.json").read_text(encoding="utf-8"))
397
+ env["PYTHONUTF8"] = "1"
398
+ env["PYTHONIOENCODING"] = "utf8"
399
+ env = {**os.environ, **env}
400
+
401
+ process = self._start_agent_process(
402
+ env=env, working_dir=working_dir, password=password
403
+ )
404
+ (working_dir / "agent.pid").write_text(
405
+ data=str(process.pid),
406
+ encoding="utf-8",
407
+ )
408
+
409
+ def _start_agent_process(
410
+ self, env: Dict, working_dir: Path, password: str
411
+ ) -> subprocess.Popen:
412
+ """Start agent process."""
413
+ raise NotImplementedError
414
+
415
+ def _start_tendermint(self) -> None:
416
+ """Start tendermint process."""
417
+ working_dir = self._work_directory
418
+ env = json.loads((working_dir / "tendermint.json").read_text(encoding="utf-8"))
419
+ env["PYTHONUTF8"] = "1"
420
+ env["PYTHONIOENCODING"] = "utf8"
421
+
422
+ env = {
423
+ **os.environ,
424
+ **env,
425
+ }
426
+
427
+ process = self._start_tendermint_process(env=env, working_dir=working_dir)
428
+
429
+ (working_dir / "tendermint.pid").write_text(
430
+ data=str(process.pid),
431
+ encoding="utf-8",
432
+ )
433
+
434
+ def _start_tendermint_process(
435
+ self, env: Dict, working_dir: Path
436
+ ) -> subprocess.Popen:
437
+ raise NotImplementedError
438
+
439
+
440
+ class PyInstallerHostDeploymentRunnerMac(PyInstallerHostDeploymentRunner):
441
+ """Mac deployment runner."""
442
+
443
+ def _start_agent_process(
444
+ self, env: Dict, working_dir: Path, password: str
445
+ ) -> subprocess.Popen:
446
+ """Start agent process."""
447
+ agent_runner_log_file = self._open_agent_runner_log_file()
448
+ process = subprocess.Popen( # pylint: disable=consider-using-with,subprocess-popen-preexec-fn # nosec
449
+ args=self.get_agent_start_args(password=password),
450
+ cwd=working_dir / "agent",
451
+ stdout=agent_runner_log_file,
452
+ stderr=agent_runner_log_file,
453
+ env=env,
454
+ preexec_fn=os.setpgrp,
455
+ )
456
+ return process
457
+
458
+ def _start_tendermint_process(
459
+ self, env: Dict, working_dir: Path
460
+ ) -> subprocess.Popen:
461
+ """Start tendermint process."""
462
+ env = {
463
+ **env,
464
+ }
465
+ env["PATH"] = os.path.dirname(sys.executable) + ":" + os.environ["PATH"]
466
+ tm_log_file = self._open_tendermint_log_file()
467
+ process = subprocess.Popen( # pylint: disable=consider-using-with,subprocess-popen-preexec-fn # nosec
468
+ args=[self._tendermint_bin],
469
+ cwd=working_dir,
470
+ stdout=tm_log_file,
471
+ stderr=tm_log_file,
472
+ env=env,
473
+ preexec_fn=os.setpgrp, # pylint: disable=subprocess-popen-preexec-fn # nosec
474
+ )
475
+ return process
476
+
477
+
478
+ class PyInstallerHostDeploymentRunnerWindows(PyInstallerHostDeploymentRunner):
479
+ """Windows deployment runner."""
480
+
481
+ def __init__(self, work_directory: Path, is_aea: bool) -> None:
482
+ """Init the runner."""
483
+ super().__init__(work_directory, is_aea=is_aea)
484
+ self._job = self.set_windows_object_job()
485
+
486
+ @staticmethod
487
+ def set_windows_object_job() -> Any:
488
+ """Set windows job object to handle sub processes."""
489
+ from ctypes import ( # type: ignore # pylint:disable=import-outside-toplevel,reimported
490
+ wintypes,
491
+ )
492
+
493
+ kernel32 = ctypes.windll.kernel32 # type: ignore
494
+
495
+ class JOBOBJECT_BASIC_LIMIT_INFORMATION(
496
+ ctypes.Structure
497
+ ): # pylint: disable=missing-class-docstring
498
+ _fields_ = [
499
+ ("PerProcessUserTimeLimit", wintypes.LARGE_INTEGER),
500
+ ("PerJobUserTimeLimit", wintypes.LARGE_INTEGER),
501
+ ("LimitFlags", wintypes.DWORD),
502
+ ("MinimumWorkingSetSize", ctypes.c_size_t),
503
+ ("MaximumWorkingSetSize", ctypes.c_size_t),
504
+ ("ActiveProcessLimit", wintypes.DWORD),
505
+ ("Affinity", ctypes.POINTER(wintypes.ULONG)),
506
+ ("PriorityClass", wintypes.DWORD),
507
+ ("SchedulingClass", wintypes.DWORD),
508
+ ]
509
+
510
+ class IO_COUNTERS(ctypes.Structure): # pylint: disable=missing-class-docstring
511
+ _fields_ = [
512
+ ("ReadOperationCount", ctypes.c_ulonglong),
513
+ ("WriteOperationCount", ctypes.c_ulonglong),
514
+ ("OtherOperationCount", ctypes.c_ulonglong),
515
+ ("ReadTransferCount", ctypes.c_ulonglong),
516
+ ("WriteTransferCount", ctypes.c_ulonglong),
517
+ ("OtherTransferCount", ctypes.c_ulonglong),
518
+ ]
519
+
520
+ class JOBOBJECT_EXTENDED_LIMIT_INFORMATION(
521
+ ctypes.Structure
522
+ ): # pylint: disable=missing-class-docstring
523
+ _fields_ = [
524
+ ("BasicLimitInformation", JOBOBJECT_BASIC_LIMIT_INFORMATION),
525
+ ("IoInfo", IO_COUNTERS),
526
+ ("ProcessMemoryLimit", ctypes.c_size_t),
527
+ ("JobMemoryLimit", ctypes.c_size_t),
528
+ ("PeakProcessMemoryUsed", ctypes.c_size_t),
529
+ ("PeakJobMemoryUsed", ctypes.c_size_t),
530
+ ]
531
+
532
+ # Создаем Job Object
533
+ job = kernel32.CreateJobObjectW(None, None)
534
+ if not job:
535
+ raise ctypes.WinError() # type: ignore
536
+
537
+ # Настраиваем автоматическое завершение процессов при закрытии Job
538
+ info = JOBOBJECT_EXTENDED_LIMIT_INFORMATION()
539
+ info.BasicLimitInformation.LimitFlags = (
540
+ 0x2000 # JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE
541
+ )
542
+
543
+ if not kernel32.SetInformationJobObject(
544
+ job,
545
+ 9, # JobObjectExtendedLimitInformation
546
+ ctypes.byref(info),
547
+ ctypes.sizeof(info),
548
+ ):
549
+ kernel32.CloseHandle(job)
550
+ raise ctypes.WinError() # type: ignore
551
+
552
+ return job
553
+
554
+ def assign_to_job(self, pid: int) -> None:
555
+ """Windows-only: привязывает процесс к Job Object."""
556
+ ctypes.windll.kernel32.AssignProcessToJobObject(self._job, pid) # type: ignore
557
+
558
+ @property
559
+ def _tendermint_bin(self) -> str:
560
+ """Return tendermint path."""
561
+ return str(Path(os.path.dirname(sys.executable)) / "tendermint_win.exe") # type: ignore # pylint: disable=protected-access
562
+
563
+ def _start_agent_process(
564
+ self, env: Dict, working_dir: Path, password: str
565
+ ) -> subprocess.Popen:
566
+ """Start agent process."""
567
+ agent_runner_log_file = self._open_agent_runner_log_file()
568
+ process = subprocess.Popen( # pylint: disable=consider-using-with # nosec
569
+ args=self.get_agent_start_args(password=password),
570
+ cwd=working_dir / "agent",
571
+ stdout=agent_runner_log_file,
572
+ stderr=agent_runner_log_file,
573
+ env=env,
574
+ creationflags=0x00000200, # Detach process from the main process
575
+ )
576
+ self.assign_to_job(process._handle) # type: ignore # pylint: disable=protected-access
577
+ return process
578
+
579
+ def _start_tendermint_process(
580
+ self, env: Dict, working_dir: Path
581
+ ) -> subprocess.Popen:
582
+ """Start tendermint process."""
583
+ env = {
584
+ **env,
585
+ }
586
+ tm_log_file = self._open_tendermint_log_file()
587
+ env["PATH"] = os.path.dirname(sys.executable) + ";" + os.environ["PATH"]
588
+
589
+ process = subprocess.Popen( # pylint: disable=consider-using-with # nosec
590
+ args=[self._tendermint_bin],
591
+ cwd=working_dir,
592
+ stdout=tm_log_file,
593
+ stderr=tm_log_file,
594
+ env=env,
595
+ creationflags=0x00000200, # Detach process from the main process
596
+ )
597
+ self.assign_to_job(process._handle) # type: ignore # pylint: disable=protected-access
598
+ return process
599
+
600
+
601
+ class HostPythonHostDeploymentRunner(BaseDeploymentRunner):
602
+ """Deployment runner for host installed python."""
603
+
604
+ @property
605
+ def _agent_runner_bin(self) -> str:
606
+ """Return aea_bin path."""
607
+ if self._is_aea:
608
+ return str(self._venv_dir / "bin" / "aea")
609
+
610
+ service_dir = self._work_directory.parent
611
+ agent_runner_bin = get_agent_runner_path(service_dir=service_dir)
612
+ return str(agent_runner_bin)
613
+
614
+ def _start_agent(self, password: str) -> None:
615
+ """Start agent process."""
616
+ working_dir = self._work_directory
617
+ env = json.loads((working_dir / "agent.json").read_text(encoding="utf-8"))
618
+ env["PYTHONUTF8"] = "1"
619
+ env["PYTHONIOENCODING"] = "utf8"
620
+ agent_runner_log_file = self._open_agent_runner_log_file()
621
+
622
+ process = subprocess.Popen( # pylint: disable=consider-using-with # nosec
623
+ args=self.get_agent_start_args(password=password),
624
+ cwd=str(working_dir / "agent"),
625
+ env={**os.environ, **env},
626
+ stdout=agent_runner_log_file,
627
+ stderr=agent_runner_log_file,
628
+ creationflags=(
629
+ 0x00000008 if platform.system() == "Windows" else 0
630
+ ), # Detach process from the main process
631
+ )
632
+ (working_dir / "agent.pid").write_text(
633
+ data=str(process.pid),
634
+ encoding="utf-8",
635
+ )
636
+
637
+ def _start_tendermint(self) -> None:
638
+ """Start tendermint process."""
639
+ working_dir = self._work_directory
640
+ env = json.loads((working_dir / "tendermint.json").read_text(encoding="utf-8"))
641
+ env["PYTHONUTF8"] = "1"
642
+ env["PYTHONIOENCODING"] = "utf8"
643
+
644
+ process = subprocess.Popen( # pylint: disable=consider-using-with # nosec
645
+ args=[
646
+ str(self._venv_dir / "bin" / "flask"),
647
+ "run",
648
+ "--host",
649
+ "localhost",
650
+ "--port",
651
+ "8080",
652
+ ],
653
+ cwd=working_dir,
654
+ stdout=subprocess.DEVNULL,
655
+ stderr=subprocess.DEVNULL,
656
+ env={**os.environ, **env},
657
+ creationflags=(
658
+ 0x00000008 if platform.system() == "Windows" else 0
659
+ ), # Detach process from the main process
660
+ )
661
+ (working_dir / "tendermint.pid").write_text(
662
+ data=str(process.pid),
663
+ encoding="utf-8",
664
+ )
665
+
666
+ @property
667
+ def _venv_dir(self) -> Path:
668
+ """Get venv dir for aea."""
669
+ return self._work_directory / "venv"
670
+
671
+ def _setup_venv(self) -> None:
672
+ """Perform venv setup, install deps."""
673
+ if not self._is_aea:
674
+ return
675
+
676
+ self._venv_dir.mkdir(exist_ok=True)
677
+ venv_cli(args=[str(self._venv_dir)])
678
+ pbin = str(self._venv_dir / "bin" / "python")
679
+ # Install agent dependencies
680
+ self._run_cmd(
681
+ args=[
682
+ pbin,
683
+ "-m",
684
+ "pip",
685
+ "install",
686
+ f"open-autonomy[all]=={autonomy_version}",
687
+ f"open-aea-ledger-ethereum=={aea_version}",
688
+ f"open-aea-ledger-ethereum-flashbots=={aea_version}",
689
+ f"open-aea-ledger-cosmos=={aea_version}",
690
+ # Install tendermint dependencies
691
+ "flask",
692
+ "requests",
693
+ "multiaddr==0.0.9", # TODO: remove when pinned on open-aea
694
+ ],
695
+ )
696
+
697
+ def _setup_agent(self, password: str) -> None:
698
+ """Prepare agent."""
699
+ multiprocessing.set_start_method("spawn")
700
+ self._setup_venv()
701
+ super()._setup_agent(password=password)
702
+ if not self._is_aea:
703
+ return
704
+
705
+ # Install agent dependencies
706
+ self._run_cmd(
707
+ args=[
708
+ self._agent_runner_bin,
709
+ "-v",
710
+ "debug",
711
+ "install",
712
+ "--timeout",
713
+ "600",
714
+ ],
715
+ cwd=self._work_directory / "agent",
716
+ )
717
+
718
+
719
+ class States(Enum):
720
+ """Service deployment states."""
721
+
722
+ NONE = 0
723
+ STARTING = 1
724
+ STARTED = 2
725
+ STOPPING = 3
726
+ STOPPED = 4
727
+ ERROR = 5
728
+
729
+
730
+ class DeploymentManager:
731
+ """Deployment manager to run and stop deployments."""
732
+
733
+ def __init__(self) -> None:
734
+ """Init the deployment manager."""
735
+ self._deployment_runner_class = self._get_host_deployment_runner_class()
736
+ self._is_stopping = False
737
+ self.logger = setup_logger(name="operate.deployment_manager")
738
+ self._states: Dict[Path, States] = {}
739
+
740
+ def _get_deployment_runner(
741
+ self, build_dir: Path, is_aea: bool
742
+ ) -> BaseDeploymentRunner:
743
+ """Get deploymnent runner instance."""
744
+ return self._deployment_runner_class(build_dir, is_aea=is_aea)
745
+
746
+ @staticmethod
747
+ def _get_host_deployment_runner_class() -> Type[BaseDeploymentRunner]:
748
+ """Return depoyment runner class according to running env."""
749
+
750
+ if getattr(sys, "frozen", False) and hasattr(sys, "_MEIPASS"):
751
+ # pyinstaller inside!
752
+ if platform.system() == "Darwin":
753
+ return PyInstallerHostDeploymentRunnerMac
754
+ if platform.system() == "Windows":
755
+ return PyInstallerHostDeploymentRunnerWindows
756
+ raise ValueError(f"Platform not supported {platform.system()}")
757
+
758
+ return HostPythonHostDeploymentRunner
759
+
760
+ def stop(self) -> None:
761
+ """Stop deploment manager."""
762
+ self.logger.info("Stop deployment manager")
763
+ self._is_stopping = True
764
+
765
+ def get_state(self, build_dir: Path) -> States:
766
+ """Get state of the deployment."""
767
+ return self._states.get(build_dir) or States.NONE
768
+
769
+ def check_ipfs_connection_works(self) -> None:
770
+ """Check ipfs works and there is a good net connection."""
771
+ self.logger.info("Doing network connection check by test call to ipfs server.")
772
+ for i in range(3):
773
+ try:
774
+ requests.get(constants.IPFS_CHECK_URL, timeout=60)
775
+ return
776
+ except OSError:
777
+ self.logger.exception(
778
+ "failed to connect to ipfs to test connection. OSError, critical!"
779
+ )
780
+ raise
781
+ except Exception: # pylint: disable=broad-except
782
+ self.logger.exception(
783
+ "failed to connect to ipfs to test connection. do another try"
784
+ )
785
+ time.sleep(i * 5)
786
+ self.logger.error(
787
+ "failed to connect to ipfs to test connection. no attempts left. raise error"
788
+ )
789
+ raise RuntimeError(
790
+ "Failed to perform test connection to ipfs to check network connection!"
791
+ )
792
+
793
+ def run_deployment(
794
+ self, build_dir: Path, password: str, is_aea: bool = True
795
+ ) -> None:
796
+ """Run deployment."""
797
+ if self._is_stopping:
798
+ raise RuntimeError("deployment manager stopped")
799
+ if self.get_state(build_dir=build_dir) in [States.STARTING, States.STOPPING]:
800
+ raise ValueError("Service already in transition")
801
+
802
+ # doing pre check for ipfs works fine, also network connection is ok.
803
+ self.check_ipfs_connection_works()
804
+
805
+ self.logger.info(f"Starting deployment {build_dir}...")
806
+ self._states[build_dir] = States.STARTING
807
+ try:
808
+ deployment_runner = self._get_deployment_runner(
809
+ build_dir=build_dir, is_aea=is_aea
810
+ )
811
+ deployment_runner.start(password=password)
812
+ self.logger.info(f"Started deployment {build_dir}")
813
+ self._states[build_dir] = States.STARTED
814
+ except Exception: # pylint: disable=broad-except
815
+ self.logger.exception(
816
+ f"Starting deployment failed {build_dir}. so try to stop"
817
+ )
818
+ self._states[build_dir] = States.ERROR
819
+ self.stop_deployment(build_dir=build_dir, force=True)
820
+
821
+ if self._is_stopping:
822
+ self.logger.warning(
823
+ f"Deployment at {build_dir} started when it was going to stop, so stop it"
824
+ )
825
+ self.stop_deployment(build_dir=build_dir, force=True)
826
+
827
+ def stop_deployment(
828
+ self, build_dir: Path, force: bool = False, is_aea: bool = True
829
+ ) -> None:
830
+ """Stop the deployment."""
831
+ if (
832
+ self.get_state(build_dir=build_dir) in [States.STARTING, States.STOPPING]
833
+ and not force
834
+ ):
835
+ raise ValueError("Service already in transition")
836
+ self.logger.info(f"Stopping deployment {build_dir}...")
837
+ self._states[build_dir] = States.STOPPING
838
+ deployment_runner = self._get_deployment_runner(
839
+ build_dir=build_dir, is_aea=is_aea
840
+ )
841
+ try:
842
+ deployment_runner.stop()
843
+ self.logger.info(f"Stopped deployment {build_dir}...")
844
+ self._states[build_dir] = States.STOPPED
845
+ except Exception:
846
+ self.logger.exception(f"Stopping deployment failed {build_dir}...")
847
+ self._states[build_dir] = States.ERROR
848
+ raise
849
+
850
+
851
+ deployment_manager = DeploymentManager()
852
+
853
+
854
+ def run_host_deployment(build_dir: Path, password: str, is_aea: bool = True) -> None:
855
+ """Run host deployment."""
856
+ deployment_manager.run_deployment(
857
+ build_dir=build_dir, password=password, is_aea=is_aea
858
+ )
859
+
860
+
861
+ def stop_host_deployment(build_dir: Path, is_aea: bool = True) -> None:
862
+ """Stop host deployment."""
863
+ deployment_manager.stop_deployment(build_dir=build_dir, is_aea=is_aea)
864
+
865
+
866
+ def stop_deployment_manager() -> None:
867
+ """Stop deployment manager."""
868
+ deployment_manager.stop()