From cb4aff9e294a8d926389dd522df2a657255d6bb2 Mon Sep 17 00:00:00 2001 From: f-trycua Date: Tue, 4 Feb 2025 11:17:12 +0100 Subject: [PATCH] Fix TCP connections reuse --- examples.py | 2 +- poetry.lock | 282 +------------------------------- pylume/client.py | 148 ++++++++--------- pylume/pylume.py | 64 ++++---- pylume/server.py | 413 ++++++++++++++++++----------------------------- pyproject.toml | 31 ++-- 6 files changed, 274 insertions(+), 666 deletions(-) diff --git a/examples.py b/examples.py index 7249dd8..a60936f 100644 --- a/examples.py +++ b/examples.py @@ -10,7 +10,7 @@ async def main(): """Example usage of PyLume.""" - async with PyLume(debug=True) as pylume: + async with PyLume(port=3000, use_existing_server=False) as pylume: # Create a new VM print("\n=== Creating a new VM ===") diff --git a/poetry.lock b/poetry.lock index 2a2e79a..e5e465a 100644 --- a/poetry.lock +++ b/poetry.lock @@ -134,28 +134,6 @@ files = [ {file = "annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89"}, ] -[[package]] -name = "anyio" -version = "4.8.0" -description = "High level compatibility layer for multiple asynchronous event loop implementations" -optional = false -python-versions = ">=3.9" -files = [ - {file = "anyio-4.8.0-py3-none-any.whl", hash = "sha256:b5011f270ab5eb0abf13385f851315585cc37ef330dd88e27ec3d34d651fd47a"}, - {file = "anyio-4.8.0.tar.gz", hash = "sha256:1d9fe889df5212298c0c0723fa20479d1b94883a2df44bd3897aa91083316f7a"}, -] - -[package.dependencies] -exceptiongroup = {version = ">=1.0.2", markers = "python_version < \"3.11\""} -idna = ">=2.8" -sniffio = ">=1.1" -typing_extensions = {version = ">=4.5", markers = "python_version < \"3.13\""} - -[package.extras] -doc = ["Sphinx (>=7.4,<8.0)", "packaging", "sphinx-autodoc-typehints (>=1.2.0)", "sphinx_rtd_theme"] -test = ["anyio[trio]", "coverage[toml] (>=7)", "exceptiongroup (>=1.2.0)", "hypothesis (>=4.0)", "psutil (>=5.9)", "pytest (>=7.0)", "trustme", "truststore (>=0.9.1)", "uvloop (>=0.21)"] -trio = ["trio (>=0.26.1)"] - [[package]] name = "async-timeout" version = "5.0.1" @@ -232,17 +210,6 @@ d = ["aiohttp (>=3.7.4)", "aiohttp (>=3.7.4,!=3.9.0)"] jupyter = ["ipython (>=7.8.0)", "tokenize-rt (>=3.2.0)"] uvloop = ["uvloop (>=0.15.2)"] -[[package]] -name = "certifi" -version = "2025.1.31" -description = "Python package for providing Mozilla's CA Bundle." -optional = false -python-versions = ">=3.6" -files = [ - {file = "certifi-2025.1.31-py3-none-any.whl", hash = "sha256:ca78db4565a652026a4db2bcdf68f2fb589ea80d0be70e03929ed730746b84fe"}, - {file = "certifi-2025.1.31.tar.gz", hash = "sha256:3d5da6925056f6f18f119200434a4780a94263f10d1c21d032a6f6b2baa20651"}, -] - [[package]] name = "click" version = "8.1.8" @@ -383,62 +350,6 @@ files = [ {file = "frozenlist-1.5.0.tar.gz", hash = "sha256:81d5af29e61b9c8348e876d442253723928dce6433e0e76cd925cd83f1b4b817"}, ] -[[package]] -name = "h11" -version = "0.14.0" -description = "A pure-Python, bring-your-own-I/O implementation of HTTP/1.1" -optional = false -python-versions = ">=3.7" -files = [ - {file = "h11-0.14.0-py3-none-any.whl", hash = "sha256:e3fe4ac4b851c468cc8363d500db52c2ead036020723024a109d37346efaa761"}, - {file = "h11-0.14.0.tar.gz", hash = "sha256:8f19fbbe99e72420ff35c00b27a34cb9937e902a8b810e2c88300c6f0a3b699d"}, -] - -[[package]] -name = "httpcore" -version = "1.0.7" -description = "A minimal low-level HTTP client." -optional = false -python-versions = ">=3.8" -files = [ - {file = "httpcore-1.0.7-py3-none-any.whl", hash = "sha256:a3fff8f43dc260d5bd363d9f9cf1830fa3a458b332856f34282de498ed420edd"}, - {file = "httpcore-1.0.7.tar.gz", hash = "sha256:8551cb62a169ec7162ac7be8d4817d561f60e08eaa485234898414bb5a8a0b4c"}, -] - -[package.dependencies] -certifi = "*" -h11 = ">=0.13,<0.15" - -[package.extras] -asyncio = ["anyio (>=4.0,<5.0)"] -http2 = ["h2 (>=3,<5)"] -socks = ["socksio (==1.*)"] -trio = ["trio (>=0.22.0,<1.0)"] - -[[package]] -name = "httpx" -version = "0.26.0" -description = "The next generation HTTP client." -optional = false -python-versions = ">=3.8" -files = [ - {file = "httpx-0.26.0-py3-none-any.whl", hash = "sha256:8915f5a3627c4d47b73e8202457cb28f1266982d1159bd5779d86a80c0eab1cd"}, - {file = "httpx-0.26.0.tar.gz", hash = "sha256:451b55c30d5185ea6b23c2c793abf9bb237d2a7dfb901ced6ff69ad37ec1dfaf"}, -] - -[package.dependencies] -anyio = "*" -certifi = "*" -httpcore = "==1.*" -idna = "*" -sniffio = "*" - -[package.extras] -brotli = ["brotli", "brotlicffi"] -cli = ["click (==8.*)", "pygments (==2.*)", "rich (>=10,<14)"] -http2 = ["h2 (>=3,<5)"] -socks = ["socksio (==1.*)"] - [[package]] name = "idna" version = "3.10" @@ -478,41 +389,6 @@ files = [ [package.extras] colors = ["colorama (>=0.4.6)"] -[[package]] -name = "markdown-it-py" -version = "3.0.0" -description = "Python port of markdown-it. Markdown parsing, done right!" -optional = false -python-versions = ">=3.8" -files = [ - {file = "markdown-it-py-3.0.0.tar.gz", hash = "sha256:e3f60a94fa066dc52ec76661e37c851cb232d92f9886b15cb560aaada2df8feb"}, - {file = "markdown_it_py-3.0.0-py3-none-any.whl", hash = "sha256:355216845c60bd96232cd8d8c40e8f9765cc86f46880e43a8fd22dc1a1a8cab1"}, -] - -[package.dependencies] -mdurl = ">=0.1,<1.0" - -[package.extras] -benchmarking = ["psutil", "pytest", "pytest-benchmark"] -code-style = ["pre-commit (>=3.0,<4.0)"] -compare = ["commonmark (>=0.9,<1.0)", "markdown (>=3.4,<4.0)", "mistletoe (>=1.0,<2.0)", "mistune (>=2.0,<3.0)", "panflute (>=2.3,<3.0)"] -linkify = ["linkify-it-py (>=1,<3)"] -plugins = ["mdit-py-plugins"] -profiling = ["gprof2dot"] -rtd = ["jupyter_sphinx", "mdit-py-plugins", "myst-parser", "pyyaml", "sphinx", "sphinx-copybutton", "sphinx-design", "sphinx_book_theme"] -testing = ["coverage", "pytest", "pytest-cov", "pytest-regressions"] - -[[package]] -name = "mdurl" -version = "0.1.2" -description = "Markdown URL utilities" -optional = false -python-versions = ">=3.7" -files = [ - {file = "mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8"}, - {file = "mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba"}, -] - [[package]] name = "multidict" version = "6.1.0" @@ -617,65 +493,6 @@ files = [ [package.dependencies] typing-extensions = {version = ">=4.1.0", markers = "python_version < \"3.11\""} -[[package]] -name = "mypy" -version = "1.14.1" -description = "Optional static typing for Python" -optional = false -python-versions = ">=3.8" -files = [ - {file = "mypy-1.14.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:52686e37cf13d559f668aa398dd7ddf1f92c5d613e4f8cb262be2fb4fedb0fcb"}, - {file = "mypy-1.14.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:1fb545ca340537d4b45d3eecdb3def05e913299ca72c290326be19b3804b39c0"}, - {file = "mypy-1.14.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:90716d8b2d1f4cd503309788e51366f07c56635a3309b0f6a32547eaaa36a64d"}, - {file = "mypy-1.14.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2ae753f5c9fef278bcf12e1a564351764f2a6da579d4a81347e1d5a15819997b"}, - {file = "mypy-1.14.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:e0fe0f5feaafcb04505bcf439e991c6d8f1bf8b15f12b05feeed96e9e7bf1427"}, - {file = "mypy-1.14.1-cp310-cp310-win_amd64.whl", hash = "sha256:7d54bd85b925e501c555a3227f3ec0cfc54ee8b6930bd6141ec872d1c572f81f"}, - {file = "mypy-1.14.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f995e511de847791c3b11ed90084a7a0aafdc074ab88c5a9711622fe4751138c"}, - {file = "mypy-1.14.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d64169ec3b8461311f8ce2fd2eb5d33e2d0f2c7b49116259c51d0d96edee48d1"}, - {file = "mypy-1.14.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ba24549de7b89b6381b91fbc068d798192b1b5201987070319889e93038967a8"}, - {file = "mypy-1.14.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:183cf0a45457d28ff9d758730cd0210419ac27d4d3f285beda038c9083363b1f"}, - {file = "mypy-1.14.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f2a0ecc86378f45347f586e4163d1769dd81c5a223d577fe351f26b179e148b1"}, - {file = "mypy-1.14.1-cp311-cp311-win_amd64.whl", hash = "sha256:ad3301ebebec9e8ee7135d8e3109ca76c23752bac1e717bc84cd3836b4bf3eae"}, - {file = "mypy-1.14.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:30ff5ef8519bbc2e18b3b54521ec319513a26f1bba19a7582e7b1f58a6e69f14"}, - {file = "mypy-1.14.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:cb9f255c18052343c70234907e2e532bc7e55a62565d64536dbc7706a20b78b9"}, - {file = "mypy-1.14.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8b4e3413e0bddea671012b063e27591b953d653209e7a4fa5e48759cda77ca11"}, - {file = "mypy-1.14.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:553c293b1fbdebb6c3c4030589dab9fafb6dfa768995a453d8a5d3b23784af2e"}, - {file = "mypy-1.14.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fad79bfe3b65fe6a1efaed97b445c3d37f7be9fdc348bdb2d7cac75579607c89"}, - {file = "mypy-1.14.1-cp312-cp312-win_amd64.whl", hash = "sha256:8fa2220e54d2946e94ab6dbb3ba0a992795bd68b16dc852db33028df2b00191b"}, - {file = "mypy-1.14.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:92c3ed5afb06c3a8e188cb5da4984cab9ec9a77ba956ee419c68a388b4595255"}, - {file = "mypy-1.14.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:dbec574648b3e25f43d23577309b16534431db4ddc09fda50841f1e34e64ed34"}, - {file = "mypy-1.14.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8c6d94b16d62eb3e947281aa7347d78236688e21081f11de976376cf010eb31a"}, - {file = "mypy-1.14.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d4b19b03fdf54f3c5b2fa474c56b4c13c9dbfb9a2db4370ede7ec11a2c5927d9"}, - {file = "mypy-1.14.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:0c911fde686394753fff899c409fd4e16e9b294c24bfd5e1ea4675deae1ac6fd"}, - {file = "mypy-1.14.1-cp313-cp313-win_amd64.whl", hash = "sha256:8b21525cb51671219f5307be85f7e646a153e5acc656e5cebf64bfa076c50107"}, - {file = "mypy-1.14.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:7084fb8f1128c76cd9cf68fe5971b37072598e7c31b2f9f95586b65c741a9d31"}, - {file = "mypy-1.14.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:8f845a00b4f420f693f870eaee5f3e2692fa84cc8514496114649cfa8fd5e2c6"}, - {file = "mypy-1.14.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:44bf464499f0e3a2d14d58b54674dee25c031703b2ffc35064bd0df2e0fac319"}, - {file = "mypy-1.14.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c99f27732c0b7dc847adb21c9d47ce57eb48fa33a17bc6d7d5c5e9f9e7ae5bac"}, - {file = "mypy-1.14.1-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:bce23c7377b43602baa0bd22ea3265c49b9ff0b76eb315d6c34721af4cdf1d9b"}, - {file = "mypy-1.14.1-cp38-cp38-win_amd64.whl", hash = "sha256:8edc07eeade7ebc771ff9cf6b211b9a7d93687ff892150cb5692e4f4272b0837"}, - {file = "mypy-1.14.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:3888a1816d69f7ab92092f785a462944b3ca16d7c470d564165fe703b0970c35"}, - {file = "mypy-1.14.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:46c756a444117c43ee984bd055db99e498bc613a70bbbc120272bd13ca579fbc"}, - {file = "mypy-1.14.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:27fc248022907e72abfd8e22ab1f10e903915ff69961174784a3900a8cba9ad9"}, - {file = "mypy-1.14.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:499d6a72fb7e5de92218db961f1a66d5f11783f9ae549d214617edab5d4dbdbb"}, - {file = "mypy-1.14.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:57961db9795eb566dc1d1b4e9139ebc4c6b0cb6e7254ecde69d1552bf7613f60"}, - {file = "mypy-1.14.1-cp39-cp39-win_amd64.whl", hash = "sha256:07ba89fdcc9451f2ebb02853deb6aaaa3d2239a236669a63ab3801bbf923ef5c"}, - {file = "mypy-1.14.1-py3-none-any.whl", hash = "sha256:b66a60cc4073aeb8ae00057f9c1f64d49e90f918fbcef9a977eb121da8b8f1d1"}, - {file = "mypy-1.14.1.tar.gz", hash = "sha256:7ec88144fe9b510e8475ec2f5f251992690fcf89ccb4500b214b4226abcd32d6"}, -] - -[package.dependencies] -mypy_extensions = ">=1.0.0" -tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""} -typing_extensions = ">=4.6.0" - -[package.extras] -dmypy = ["psutil (>=4.0)"] -faster-cache = ["orjson"] -install-types = ["pip"] -mypyc = ["setuptools (>=50)"] -reports = ["lxml"] - [[package]] name = "mypy-extensions" version = "1.0.0" @@ -709,20 +526,6 @@ files = [ {file = "pathspec-0.12.1.tar.gz", hash = "sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712"}, ] -[[package]] -name = "pexpect" -version = "4.9.0" -description = "Pexpect allows easy control of interactive console applications." -optional = false -python-versions = "*" -files = [ - {file = "pexpect-4.9.0-py2.py3-none-any.whl", hash = "sha256:7236d1e080e4936be2dc3e326cec0af72acf9212a7e1d060210e70a47e253523"}, - {file = "pexpect-4.9.0.tar.gz", hash = "sha256:ee7d41123f3c9911050ea2c2dac107568dc43b2d3b0c7557a33212c398ead30f"}, -] - -[package.dependencies] -ptyprocess = ">=0.5" - [[package]] name = "platformdirs" version = "4.3.6" @@ -845,17 +648,6 @@ files = [ {file = "propcache-0.2.1.tar.gz", hash = "sha256:3f77ce728b19cb537714499928fe800c3dda29e8d9428778fc7c186da4c09a64"}, ] -[[package]] -name = "ptyprocess" -version = "0.7.0" -description = "Run a subprocess in a pseudo terminal" -optional = false -python-versions = "*" -files = [ - {file = "ptyprocess-0.7.0-py2.py3-none-any.whl", hash = "sha256:4b41f3967fce3af57cc7e94b888626c18bf37a083e3651ca8feeb66d492fef35"}, - {file = "ptyprocess-0.7.0.tar.gz", hash = "sha256:5c5d0a3b48ceee0b48485e0c26037c0acd7d29765ca3fbb5cb3831d347423220"}, -] - [[package]] name = "pydantic" version = "2.10.6" @@ -988,20 +780,6 @@ files = [ [package.dependencies] typing-extensions = ">=4.6.0,<4.7.0 || >4.7.0" -[[package]] -name = "pygments" -version = "2.19.1" -description = "Pygments is a syntax highlighting package written in Python." -optional = false -python-versions = ">=3.8" -files = [ - {file = "pygments-2.19.1-py3-none-any.whl", hash = "sha256:9ea1544ad55cecf4b8242fab6dd35a93bbce657034b0611ee383099054ab6d8c"}, - {file = "pygments-2.19.1.tar.gz", hash = "sha256:61c16d2a8576dc0649d9f39e089b5f02bcd27fba10d8fb4dcc28173f7a45151f"}, -] - -[package.extras] -windows-terminal = ["colorama (>=0.4.6)"] - [[package]] name = "pytest" version = "7.4.4" @@ -1042,62 +820,6 @@ pytest = ">=7.0.0,<9" docs = ["sphinx (>=5.3)", "sphinx-rtd-theme (>=1.0)"] testing = ["coverage (>=6.2)", "hypothesis (>=5.7.1)"] -[[package]] -name = "rich" -version = "13.9.4" -description = "Render rich text, tables, progress bars, syntax highlighting, markdown and more to the terminal" -optional = false -python-versions = ">=3.8.0" -files = [ - {file = "rich-13.9.4-py3-none-any.whl", hash = "sha256:6049d5e6ec054bf2779ab3358186963bac2ea89175919d699e378b99738c2a90"}, - {file = "rich-13.9.4.tar.gz", hash = "sha256:439594978a49a09530cff7ebc4b5c7103ef57baf48d5ea3184f21d9a2befa098"}, -] - -[package.dependencies] -markdown-it-py = ">=2.2.0" -pygments = ">=2.13.0,<3.0.0" -typing-extensions = {version = ">=4.0.0,<5.0", markers = "python_version < \"3.11\""} - -[package.extras] -jupyter = ["ipywidgets (>=7.5.1,<9)"] - -[[package]] -name = "ruff" -version = "0.1.15" -description = "An extremely fast Python linter and code formatter, written in Rust." -optional = false -python-versions = ">=3.7" -files = [ - {file = "ruff-0.1.15-py3-none-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:5fe8d54df166ecc24106db7dd6a68d44852d14eb0729ea4672bb4d96c320b7df"}, - {file = "ruff-0.1.15-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:6f0bfbb53c4b4de117ac4d6ddfd33aa5fc31beeaa21d23c45c6dd249faf9126f"}, - {file = "ruff-0.1.15-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e0d432aec35bfc0d800d4f70eba26e23a352386be3a6cf157083d18f6f5881c8"}, - {file = "ruff-0.1.15-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:9405fa9ac0e97f35aaddf185a1be194a589424b8713e3b97b762336ec79ff807"}, - {file = "ruff-0.1.15-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c66ec24fe36841636e814b8f90f572a8c0cb0e54d8b5c2d0e300d28a0d7bffec"}, - {file = "ruff-0.1.15-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:6f8ad828f01e8dd32cc58bc28375150171d198491fc901f6f98d2a39ba8e3ff5"}, - {file = "ruff-0.1.15-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:86811954eec63e9ea162af0ffa9f8d09088bab51b7438e8b6488b9401863c25e"}, - {file = "ruff-0.1.15-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:fd4025ac5e87d9b80e1f300207eb2fd099ff8200fa2320d7dc066a3f4622dc6b"}, - {file = "ruff-0.1.15-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b17b93c02cdb6aeb696effecea1095ac93f3884a49a554a9afa76bb125c114c1"}, - {file = "ruff-0.1.15-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:ddb87643be40f034e97e97f5bc2ef7ce39de20e34608f3f829db727a93fb82c5"}, - {file = "ruff-0.1.15-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:abf4822129ed3a5ce54383d5f0e964e7fef74a41e48eb1dfad404151efc130a2"}, - {file = "ruff-0.1.15-py3-none-musllinux_1_2_i686.whl", hash = "sha256:6c629cf64bacfd136c07c78ac10a54578ec9d1bd2a9d395efbee0935868bf852"}, - {file = "ruff-0.1.15-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:1bab866aafb53da39c2cadfb8e1c4550ac5340bb40300083eb8967ba25481447"}, - {file = "ruff-0.1.15-py3-none-win32.whl", hash = "sha256:2417e1cb6e2068389b07e6fa74c306b2810fe3ee3476d5b8a96616633f40d14f"}, - {file = "ruff-0.1.15-py3-none-win_amd64.whl", hash = "sha256:3837ac73d869efc4182d9036b1405ef4c73d9b1f88da2413875e34e0d6919587"}, - {file = "ruff-0.1.15-py3-none-win_arm64.whl", hash = "sha256:9a933dfb1c14ec7a33cceb1e49ec4a16b51ce3c20fd42663198746efc0427360"}, - {file = "ruff-0.1.15.tar.gz", hash = "sha256:f6dfa8c1b21c913c326919056c390966648b680966febcb796cc9d1aaab8564e"}, -] - -[[package]] -name = "sniffio" -version = "1.3.1" -description = "Sniff out which async library your code is running under" -optional = false -python-versions = ">=3.7" -files = [ - {file = "sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2"}, - {file = "sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc"}, -] - [[package]] name = "tomli" version = "2.2.1" @@ -1248,5 +970,5 @@ propcache = ">=0.2.0" [metadata] lock-version = "2.0" -python-versions = "^3.10" -content-hash = "6c85e9c3590e802af691ac5e75557f62b29418858879aa39ab18d700c24b85fa" +python-versions = "^3.9" +content-hash = "be49757026c88523bd6f02c7fbb3e90dc415d9f85a7dc1584a8e1de4b0ba14a7" diff --git a/pylume/client.py b/pylume/client.py index f3d23b9..cbfd2c6 100644 --- a/pylume/client.py +++ b/pylume/client.py @@ -17,7 +17,15 @@ def __init__(self, base_url: str, timeout: aiohttp.ClientTimeout, debug: bool = self.base_url = base_url self.timeout = timeout self.debug = debug - self.session: Optional[aiohttp.ClientSession] = None + + def _create_connector(self) -> aiohttp.TCPConnector: + """Create a new connector for each session.""" + return aiohttp.TCPConnector( + force_close=True, + enable_cleanup_closed=True, + keepalive_timeout=None, + limit=10 + ) def _log_debug(self, message: str, **kwargs) -> None: """Log debug information if debug mode is enabled.""" @@ -26,94 +34,72 @@ def _log_debug(self, message: str, **kwargs) -> None: if kwargs: print(json.dumps(kwargs, indent=2)) - async def _init_session(self) -> aiohttp.ClientSession: - """Initialize aiohttp session if not already initialized.""" - if self.session is None: - self.session = aiohttp.ClientSession(timeout=self.timeout) - return self.session - - async def _handle_api_error(self, e: Exception, operation: str) -> None: - """Handle API errors and raise appropriate custom exceptions.""" - if isinstance(e, aiohttp.ClientConnectionError): - raise LumeConnectionError(f"Failed to connect to PyLume server: {str(e)}") - elif isinstance(e, asyncio.TimeoutError): - raise LumeTimeoutError(f"Request timed out: {str(e)}") - - if not hasattr(e, 'status') and not isinstance(e, aiohttp.ClientResponseError): - raise LumeServerError(f"Unknown error during {operation}: {str(e)}") - - status_code = getattr(e, 'status', 500) - response_text = str(e) - - self._log_debug( - f"{operation} request failed", - status_code=status_code, - response_text=response_text - ) - - if status_code == 404: - raise LumeNotFoundError(f"Resource not found during {operation}") - elif status_code == 400: - raise LumeConfigError(f"Invalid configuration for {operation}: {response_text}") - elif status_code >= 500: - raise LumeServerError( - f"Server error during {operation}", - status_code=status_code, - response_text=response_text - ) - else: - raise LumeServerError( - f"Error during {operation}", - status_code=status_code, - response_text=response_text - ) - async def get(self, path: str, params: Optional[Dict[str, Any]] = None) -> Any: """Make a GET request.""" - try: - session = await self._init_session() - async with session.get(f"{self.base_url}{path}", params=params) as response: - response.raise_for_status() - return await response.json() - except Exception as e: - raise await self._handle_api_error(e, f"GET {path}") + connector = self._create_connector() + async with aiohttp.ClientSession( + timeout=self.timeout, + connector=connector, + headers={'Connection': 'close'} + ) as session: + try: + async with session.get(f"{self.base_url}{path}", params=params) as response: + response.raise_for_status() + return await response.json() + finally: + await connector.close() async def post(self, path: str, data: Optional[Dict[str, Any]] = None, timeout: Optional[aiohttp.ClientTimeout] = None) -> Any: """Make a POST request.""" - try: - session = await self._init_session() - async with session.post( - f"{self.base_url}{path}", - headers={"Content-Type": "application/json"}, - json=data, - timeout=timeout or self.timeout - ) as response: - response.raise_for_status() - return await response.json() if response.content_length else None - except Exception as e: - raise await self._handle_api_error(e, f"POST {path}") + connector = self._create_connector() + async with aiohttp.ClientSession( + timeout=timeout or self.timeout, + connector=connector, + headers={ + 'Content-Type': 'application/json', + 'Connection': 'close' + } + ) as session: + try: + async with session.post( + f"{self.base_url}{path}", + json=data + ) as response: + response.raise_for_status() + return await response.json() if response.content_length else None + finally: + await connector.close() async def patch(self, path: str, data: Dict[str, Any]) -> None: """Make a PATCH request.""" - try: - session = await self._init_session() - async with session.patch( - f"{self.base_url}{path}", - headers={"Content-Type": "application/json"}, - json=data - ) as response: - response.raise_for_status() - except Exception as e: - raise await self._handle_api_error(e, f"PATCH {path}") + connector = self._create_connector() + async with aiohttp.ClientSession( + timeout=self.timeout, + connector=connector, + headers={ + 'Content-Type': 'application/json', + 'Connection': 'close' + } + ) as session: + try: + async with session.patch(f"{self.base_url}{path}", json=data) as response: + response.raise_for_status() + finally: + await connector.close() async def delete(self, path: str) -> None: """Make a DELETE request.""" - try: - session = await self._init_session() - async with session.delete(f"{self.base_url}{path}") as response: - response.raise_for_status() - except Exception as e: - raise await self._handle_api_error(e, f"DELETE {path}") + connector = self._create_connector() + async with aiohttp.ClientSession( + timeout=self.timeout, + connector=connector, + headers={'Connection': 'close'} + ) as session: + try: + async with session.delete(f"{self.base_url}{path}") as response: + response.raise_for_status() + finally: + await connector.close() def print_curl(self, method: str, path: str, data: Optional[Dict[str, Any]] = None) -> None: """Print equivalent curl command for debugging.""" @@ -128,7 +114,5 @@ def print_curl(self, method: str, path: str, data: Optional[Dict[str, Any]] = No print() async def close(self) -> None: - """Close the client session.""" - if self.session: - await self.session.close() - self.session = None \ No newline at end of file + """Close the client resources.""" + pass # No shared resources to clean up \ No newline at end of file diff --git a/pylume/pylume.py b/pylume/pylume.py index 01d20af..01559fc 100644 --- a/pylume/pylume.py +++ b/pylume/pylume.py @@ -8,6 +8,7 @@ from typing import Optional, List, Union, Callable, TypeVar, Any from functools import wraps import re +import signal from .server import LumeServer from .client import LumeClient @@ -48,7 +49,9 @@ def __init__( self, debug: bool = False, auto_start_server: bool = True, - server_start_timeout: int = 60 + server_start_timeout: int = 60, + port: Optional[int] = None, + use_existing_server: bool = False ): """Initialize the async PyLume client. @@ -56,16 +59,31 @@ def __init__( debug: Enable debug logging auto_start_server: Whether to automatically start the lume server if not running server_start_timeout: Timeout in seconds to wait for server to start + port: Port number for the lume server. Required when use_existing_server is True. + use_existing_server: If True, will try to connect to an existing server on the specified port + instead of starting a new one. """ - self.server = LumeServer(debug=debug, server_start_timeout=server_start_timeout) - self.client = None # Will be initialized after server starts - self.auto_start_server = auto_start_server + if use_existing_server and port is None: + raise LumeConfigError("Port must be specified when using an existing server") + + self.server = LumeServer( + debug=debug, + server_start_timeout=server_start_timeout, + port=port, + use_existing_server=use_existing_server + ) + self.client = None async def __aenter__(self) -> 'PyLume': """Async context manager entry.""" - if self.auto_start_server: + if self.server.use_existing_server: + # Just set up the base URL and initialize client for existing server + self.server.port = self.server.requested_port + self.server.base_url = f"http://localhost:{self.server.port}/lume" + else: await self.server.ensure_running() - await self._init_client() + + await self._init_client() return self async def __aexit__(self, exc_type, exc_val, exc_tb) -> None: @@ -78,18 +96,18 @@ async def _init_client(self) -> None: """Initialize the client if not already initialized.""" if self.client is None: client_timeout = aiohttp.ClientTimeout( - total=float(300), # 5 minutes total timeout + total=float(300), connect=30.0, sock_read=float(300), sock_connect=30.0 ) - self.client = LumeClient(base_url=self.server.base_url, timeout=client_timeout, debug=self.server.debug) - - @ensure_server - async def _ensure_client(self) -> None: - """Ensure client is initialized.""" - if self.client is None: - await self._init_client() + if self.server.base_url is None: + raise RuntimeError("Server base URL not set") + self.client = LumeClient( + base_url=self.server.base_url, + timeout=client_timeout, + debug=self.server.debug + ) def _log_debug(self, message: str, **kwargs) -> None: """Log debug information if debug mode is enabled.""" @@ -305,7 +323,6 @@ async def _ensure_server_running(self) -> None: self.output_file = output_file self._log_debug("Server startup completed successfully") - @ensure_server async def create_vm(self, spec: Union[VMConfig, dict]) -> None: """Create a new VM.""" if isinstance(spec, VMConfig): @@ -314,7 +331,6 @@ async def create_vm(self, spec: Union[VMConfig, dict]) -> None: self.client.print_curl("POST", "/vms", spec) await self.client.post("/vms", spec) - @ensure_server async def run_vm(self, name: str, opts: Optional[Union[VMRunOpts, dict]] = None) -> None: """Run a VM.""" if opts is None: @@ -326,19 +342,16 @@ async def run_vm(self, name: str, opts: Optional[Union[VMRunOpts, dict]] = None) self.client.print_curl("POST", f"/vms/{name}/run", payload) await self.client.post(f"/vms/{name}/run", payload) - @ensure_server async def list_vms(self) -> List[VMStatus]: """List all VMs.""" data = await self.client.get("/vms") return [VMStatus.model_validate(vm) for vm in data] - @ensure_server async def get_vm(self, name: str) -> VMStatus: """Get VM details.""" data = await self.client.get(f"/vms/{name}") return VMStatus.model_validate(data) - @ensure_server async def update_vm(self, name: str, params: Union[VMUpdateOpts, dict]) -> None: """Update VM settings.""" if isinstance(params, dict): @@ -348,17 +361,14 @@ async def update_vm(self, name: str, params: Union[VMUpdateOpts, dict]) -> None: self.client.print_curl("PATCH", f"/vms/{name}", payload) await self.client.patch(f"/vms/{name}", payload) - @ensure_server async def stop_vm(self, name: str) -> None: """Stop a VM.""" await self.client.post(f"/vms/{name}/stop") - @ensure_server async def delete_vm(self, name: str) -> None: """Delete a VM.""" await self.client.delete(f"/vms/{name}") - @ensure_server async def pull_image(self, spec: Union[ImageRef, dict, str], name: Optional[str] = None) -> None: """Pull a VM image.""" await self._ensure_client() @@ -398,21 +408,18 @@ async def pull_image(self, spec: Union[ImageRef, dict, str], name: Optional[str] self.client.print_curl("POST", "/pull", payload) await self.client.post("/pull", payload, timeout=pull_timeout) - @ensure_server async def clone_vm(self, name: str, new_name: str) -> None: """Clone a VM with the given name to a new VM with new_name.""" config = CloneSpec(name=name, newName=new_name) self.client.print_curl("POST", "/vms/clone", config.model_dump()) await self.client.post("/vms/clone", config.model_dump()) - @ensure_server async def get_latest_ipsw_url(self) -> str: """Get the latest IPSW URL.""" await self._ensure_client() data = await self.client.get("/ipsw") return data["url"] - @ensure_server async def get_images(self, organization: Optional[str] = None) -> ImageList: """Get list of available images.""" await self._ensure_client() @@ -422,11 +429,12 @@ async def get_images(self, organization: Optional[str] = None) -> ImageList: async def close(self) -> None: """Close the client and stop the server.""" - await self.client.close() - await asyncio.sleep(2) # Give the server 2 seconds to finish any pending operations + if self.client is not None: + await self.client.close() + self.client = None + await asyncio.sleep(1) # Reduced from 2 to 1 second await self.server.stop() - @ensure_server async def _ensure_client(self) -> None: """Ensure client is initialized.""" if self.client is None: diff --git a/pylume/server.py b/pylume/server.py index 4de43c7..ac86d16 100644 --- a/pylume/server.py +++ b/pylume/server.py @@ -6,18 +6,28 @@ import aiohttp import logging import socket -from typing import Optional, Tuple +from typing import Optional import sys +from .exceptions import LumeConnectionError +import signal class LumeServer: - def __init__(self, debug: bool = False, server_start_timeout: int = 60): + def __init__( + self, + debug: bool = False, + server_start_timeout: int = 60, + port: Optional[int] = None, + use_existing_server: bool = False + ): + """Initialize the LumeServer.""" self.debug = debug self.server_start_timeout = server_start_timeout - self.server_process: Optional[subprocess.Popen] = None - self.output_file: Optional[tempfile.NamedTemporaryFile] = None - self._output_task: Optional[asyncio.Task] = None - self.port: Optional[int] = None - self.base_url: Optional[str] = None + self.server_process = None + self.output_file = None + self.requested_port = port + self.port = None + self.base_url = None + self.use_existing_server = use_existing_server # Configure logging self.logger = logging.getLogger('lume_server') @@ -28,197 +38,145 @@ def __init__(self, debug: bool = False, server_start_timeout: int = 60): self.logger.addHandler(handler) self.logger.setLevel(logging.DEBUG if debug else logging.INFO) - def _find_available_port(self, start_port: int = 3000) -> int: - """Find the first available port starting from start_port.""" - port = start_port - while port < 65535: - try: - with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s: - s.bind(('localhost', port)) - return port - except OSError: - port += 1 - raise RuntimeError("No available ports found") - - def _log_debug(self, message: str, **kwargs) -> None: - """Log debug information if debug mode is enabled.""" - if self.debug: - if kwargs: - import json - message = f"{message}\n{json.dumps(kwargs, indent=2)}" - self.logger.debug(message) - - async def _read_output(self) -> None: - """Read and display server output.""" - if not self.server_process: - return - - while True: - if self.server_process.poll() is not None: - self.logger.debug("Server process ended") - break - - # Read stdout - if self.server_process.stdout: - try: - line = self.server_process.stdout.readline() - if line: - line = line.strip() - if line: # Only print non-empty lines - print(f"SERVER OUT: {line}") - except Exception as e: - print(f"Error reading stdout: {e}") - - # Read stderr - if self.server_process.stderr: - try: - line = self.server_process.stderr.readline() - if line: - line = line.strip() - if line: # Only print non-empty lines - print(f"SERVER ERR: {line}") - except Exception as e: - print(f"Error reading stderr: {e}") - - await asyncio.sleep(0.1) - - async def ensure_running(self) -> None: - """Ensure the lume server is running.""" - if self.server_process is None or self.server_process.poll() is not None: - await self._start_server() + def _check_port_available(self, port: int) -> bool: + """Check if a specific port is available.""" + try: + with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s: + s.bind(('localhost', port)) + return True + except OSError: + return False + + def _get_server_port(self) -> int: + """Get and validate the server port. + + Returns: + int: The validated port number + + Raises: + RuntimeError: If no port was specified + LumeConfigError: If the requested port is not available + """ + if self.requested_port is None: + raise RuntimeError("No port specified for lume server") + + if not self._check_port_available(self.requested_port): + from .exceptions import LumeConfigError + raise LumeConfigError(f"Requested port {self.requested_port} is not available") + + return self.requested_port async def _start_server(self) -> None: - """Start the lume server.""" + """Start the lume server using a managed shell script.""" self.logger.debug("Starting PyLume server") lume_path = os.path.join(os.path.dirname(__file__), "lume") if not os.path.exists(lume_path): raise RuntimeError(f"Could not find lume binary at {lume_path}") - # Make sure the file is executable - os.chmod(lume_path, 0o755) - - # Find an available port - self.port = self._find_available_port() - self.base_url = f"http://localhost:{self.port}/lume" - - # Create log file in the same directory as the lume binary - log_file_path = os.path.abspath(os.path.join(os.path.dirname(lume_path), "lume_server.log")) - - if self.debug: - print("\n=== Server Configuration ===") - print(f"Log file path: {log_file_path}") - print(f"Current directory: {os.getcwd()}") - print(f"Lume binary path: {lume_path}") - print(f"Server port: {self.port}") - print("==========================\n") - + script_file = None try: - if self.debug: - # In debug mode, write to log file - self.logger.debug(f"Starting lume server with: {lume_path} serve --port {self.port}") - - # Open log file for both reading and writing - self.output_file = open(log_file_path, 'w+') - self.output_file.write(f"=== Starting Lume Server ===\n") - self.output_file.write(f"Time: {time.strftime('%Y-%m-%d %H:%M:%S')}\n") - self.output_file.write(f"Port: {self.port}\n") - self.output_file.write("=========================\n\n") - self.output_file.flush() - - # Start server process with output going to our managed file - env = os.environ.copy() # Copy current environment - env.update({ - "RUST_LOG": "debug", # Enable Rust debug logging - "RUST_BACKTRACE": "1" # Enable backtraces for better error reporting - }) - - self.server_process = subprocess.Popen( - [lume_path, "serve", "--port", str(self.port)], - stdout=self.output_file, - stderr=subprocess.STDOUT, # Redirect stderr to stdout - cwd=os.path.dirname(lume_path), - start_new_session=True, - env=env - ) - - # Start log reading task - async def tail_log(): - while True: - try: - # Seek to current position - self.output_file.seek(0, os.SEEK_END) - - # Read any new content - line = self.output_file.readline() - if line: - line = line.strip() - if line: # Only print non-empty lines - print(f"SERVER: {line}") - - # Check if process is still running - if self.server_process.poll() is not None: - print("Server process ended") - break - - await asyncio.sleep(0.1) - except Exception as e: - print(f"Error reading log: {e}") - await asyncio.sleep(0.1) - - self._output_task = asyncio.create_task(tail_log()) - print(f"Started reading server logs from: {log_file_path}") - else: - # In non-debug mode, write to a temporary file - self.output_file = tempfile.NamedTemporaryFile(mode='w+', delete=False) - self.logger.debug(f"Using temporary file for server output: {self.output_file.name}") - self.server_process = subprocess.Popen( - [lume_path, "serve", "--port", str(self.port)], - stdout=self.output_file, - stderr=self.output_file, - cwd=os.path.dirname(lume_path), - start_new_session=True - ) - + os.chmod(lume_path, 0o755) + self.port = self._get_server_port() + self.base_url = f"http://localhost:{self.port}/lume" + + # Create shell script with trap for process management + script_content = f"""#!/bin/bash +trap 'kill $(jobs -p)' EXIT +exec {lume_path} serve --port {self.port} +""" + script_dir = os.path.dirname(lume_path) + script_file = tempfile.NamedTemporaryFile( + mode='w', + suffix='.sh', + dir=script_dir, + delete=True + ) + script_file.write(script_content) + script_file.flush() + os.chmod(script_file.name, 0o755) + + # Set up output handling - just use a temp file + self.output_file = tempfile.NamedTemporaryFile(mode='w+', delete=False) + + # Start the managed server process + env = os.environ.copy() + env["RUST_BACKTRACE"] = "1" + + self.server_process = subprocess.Popen( + ['/bin/bash', script_file.name], + stdout=self.output_file, + stderr=subprocess.STDOUT, + cwd=script_dir, + env=env + ) + + # Wait for server to initialize + await asyncio.sleep(2) + await self._wait_for_server() + except Exception as e: await self._cleanup() raise RuntimeError(f"Failed to start lume server process: {str(e)}") - - await self._wait_for_server() + finally: + # Ensure script file is cleaned up + if script_file: + try: + script_file.close() + except: + pass + + async def _tail_log(self) -> None: + """Read and display server log output in debug mode.""" + while True: + try: + self.output_file.seek(0, os.SEEK_END) + line = self.output_file.readline() + if line: + line = line.strip() + if line: + print(f"SERVER: {line}") + if self.server_process.poll() is not None: + print("Server process ended") + break + await asyncio.sleep(0.1) + except Exception as e: + print(f"Error reading log: {e}") + await asyncio.sleep(0.1) async def _wait_for_server(self) -> None: - """Wait for server to start and become responsive.""" + """Wait for server to start and become responsive with increased timeout.""" start_time = time.time() - server_ready = False - last_size = 0 - while time.time() - start_time < self.server_start_timeout: if self.server_process.poll() is not None: - # Process has terminated - error_msg = self._get_error_message() - self._cleanup() + error_msg = await self._get_error_output() + await self._cleanup() raise RuntimeError(error_msg) - server_ready = await self._check_server_output(last_size) - if server_ready: - break - - await asyncio.sleep(1.0) - - if not server_ready: - self._cleanup() - raise RuntimeError( - f"Failed to start lume server after {self.server_start_timeout} seconds. " - "Check the debug output for more details." - ) + try: + await self._verify_server() + self.logger.debug("Server is now responsive") + return + except Exception as e: + self.logger.debug(f"Server not ready yet: {str(e)}") + await asyncio.sleep(1.0) - # Give the server a moment to fully initialize - await asyncio.sleep(2.0) - await self._verify_server() + await self._cleanup() + raise RuntimeError(f"Server failed to start after {self.server_start_timeout} seconds") - def _get_error_message(self) -> str: - """Get error message from output file.""" + async def _verify_server(self) -> None: + """Verify server is responding to requests.""" + try: + timeout = aiohttp.ClientTimeout(total=10.0) + async with aiohttp.ClientSession(timeout=timeout) as client: + await client.get(f"{self.base_url}/vms") + self.logger.debug("PyLume server started successfully") + except Exception as e: + raise RuntimeError(f"Server not responding: {str(e)}") + + async def _get_error_output(self) -> str: + """Get error output from the server process.""" if not self.output_file: - return "No output file available" + return "No output available" self.output_file.seek(0) output = self.output_file.read() return ( @@ -227,88 +185,35 @@ def _get_error_message(self) -> str: f"Output: {output}" ) - async def _check_server_output(self, last_size: int) -> bool: - """Check server output for startup message.""" - if self.debug: - # In debug mode, just check server connection - try: - check_timeout = aiohttp.ClientTimeout(total=5.0) - async with aiohttp.ClientSession(timeout=check_timeout) as check_client: - await check_client.get(f"{self.base_url}/vms") - self.logger.debug("Server is responding to requests") - return True - except (aiohttp.ClientConnectionError, asyncio.TimeoutError): - return False - else: - # In non-debug mode, check the output file - if not self.output_file: - return False - - self.output_file.seek(0, os.SEEK_END) - size = self.output_file.tell() - if size > last_size: - self.output_file.seek(last_size) - new_output = self.output_file.read() - if new_output.strip(): - self.logger.debug(f"Server output: {new_output.strip()}") - if "Server started" in new_output: - self.logger.debug("Server startup detected") - return True - - # Try to connect to the server - try: - check_timeout = aiohttp.ClientTimeout(total=5.0) - async with aiohttp.ClientSession(timeout=check_timeout) as check_client: - await check_client.get(f"{self.base_url}/vms") - self.logger.debug("Server is responding to requests") - return True - except (aiohttp.ClientConnectionError, asyncio.TimeoutError): - return False - - async def _verify_server(self) -> None: - """Verify server is responding to requests.""" - try: - check_timeout = aiohttp.ClientTimeout(total=10.0) - async with aiohttp.ClientSession(timeout=check_timeout) as check_client: - await check_client.get(f"{self.base_url}/vms") - self.logger.debug("PyLume server started successfully") - except (aiohttp.ClientConnectionError, asyncio.TimeoutError) as e: - self.logger.error(f"Server verification failed: {str(e)}") - self._cleanup() - raise RuntimeError(f"Server started but is not responding: {str(e)}") - async def _cleanup(self) -> None: - """Clean up server process and output file.""" - if self._output_task and not self._output_task.done(): - self._output_task.cancel() - try: - await self._output_task - except asyncio.CancelledError: - pass - self._output_task = None - + """Clean up all server resources.""" if self.server_process: - self.server_process.terminate() try: - self.server_process.wait(timeout=5) - except subprocess.TimeoutExpired: - self.server_process.kill() + self.server_process.terminate() + try: + self.server_process.wait(timeout=5) + except subprocess.TimeoutExpired: + self.server_process.kill() + except: + pass self.server_process = None - + + # Clean up output file if self.output_file: - name = self.output_file.name - self.output_file.close() - # Only delete the output file if we're not in debug mode - if not self.debug: - try: - os.unlink(name) - except Exception as e: - print(f"Error removing output file: {e}") - else: - print(f"\nServer log file preserved at: {name}") + try: + self.output_file.close() + os.unlink(self.output_file.name) + except Exception as e: + self.logger.debug(f"Error cleaning up output file: {e}") self.output_file = None + async def ensure_running(self) -> None: + """Start the server if we're managing it.""" + if not self.use_existing_server: + await self._start_server() + async def stop(self) -> None: - """Stop the server.""" - self.logger.debug("Stopping lume server...") - await self._cleanup() \ No newline at end of file + """Stop the server if we're managing it.""" + if not self.use_existing_server: + self.logger.debug("Stopping lume server...") + await self._cleanup() \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index a933529..e23364f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -2,14 +2,13 @@ name = "pylume" version = "0.1.0" description = "Python SDK for lume - run macOS and Linux VMs on Apple Silicon" -authors = ["TryCua "] +authors = ["TryCua "] readme = "README.md" license = "MIT" homepage = "https://github.com/trycua/pylume" repository = "https://github.com/trycua/pylume" keywords = ["macos", "virtualization", "vm", "apple-silicon"] classifiers = [ - "Development Status :: 4 - Beta", "Intended Audience :: Developers", "License :: OSI Approved :: MIT License", "Operating System :: MacOS :: MacOS X", @@ -30,26 +29,16 @@ include = [ ] [tool.poetry.dependencies] -python = "^3.10" -httpx = "^0.26.0" -pydantic = "^2.5.3" -click = "^8.1.7" -rich = "^13.7.0" -pygments = "^2.17.2" -pexpect = "^4.9.0" -aiohttp = "^3.11.11" +python = "^3.9" +aiohttp = "^3.8.0" +pydantic = "^2.0.0" [tool.poetry.group.dev.dependencies] -pytest = "^7.4.4" -pytest-asyncio = "^0.23.3" -black = "^23.12.1" -isort = "^5.13.2" -mypy = "^1.8.0" -ruff = "^0.1.11" +pytest = "^7.0.0" +pytest-asyncio = "^0.23.0" +black = "^23.0.0" +isort = "^5.12.0" -[tool.poetry.scripts] -pylume = "pylume.main:main" [build-system] -requires = ["poetry-core"] -build-backend = "poetry.core.masonry.api" - +requires = ["poetry-core>=1.0.0"] +build-backend = "poetry.core.masonry.api" \ No newline at end of file