go-astty 0.1.0a0__tar.gz
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.
- go_astty-0.1.0a0/.gitignore +19 -0
- go_astty-0.1.0a0/LICENSE +21 -0
- go_astty-0.1.0a0/PKG-INFO +147 -0
- go_astty-0.1.0a0/README.md +127 -0
- go_astty-0.1.0a0/pyproject.toml +33 -0
- go_astty-0.1.0a0/src/goastty/__init__.py +5 -0
- go_astty-0.1.0a0/src/goastty/asyncrun.py +72 -0
- go_astty-0.1.0a0/src/goastty/base.py +211 -0
- go_astty-0.1.0a0/src/goastty/gitfy.py +189 -0
- go_astty-0.1.0a0/src/goastty/syncrun.py +50 -0
- go_astty-0.1.0a0/tests/async/test.py +29 -0
- go_astty-0.1.0a0/tests/sync/test.py +31 -0
go_astty-0.1.0a0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Fyllus
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: go-astty
|
|
3
|
+
Version: 0.1.0a0
|
|
4
|
+
Summary: go-astty is modular process orchestration gate for seamless synchronous and asynchronous command-line executions in Python.
|
|
5
|
+
Project-URL: Codeberg, https://codeberg.org/Fyllus/go-astty
|
|
6
|
+
Project-URL: Github, https://github.com/Fyllus/go-astty
|
|
7
|
+
Author-email: Fyllus <Fyllus@git.com>
|
|
8
|
+
License: MIT
|
|
9
|
+
License-File: LICENSE
|
|
10
|
+
Classifier: Development Status :: 3 - Alpha
|
|
11
|
+
Classifier: Intended Audience :: Developers
|
|
12
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
16
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
17
|
+
Requires-Python: >=3.11
|
|
18
|
+
Requires-Dist: pygit2>=1.14.0
|
|
19
|
+
Description-Content-Type: text/markdown
|
|
20
|
+
|
|
21
|
+
# go-astty
|
|
22
|
+
|
|
23
|
+
`go-astty` or `Gate of Asyncronous and Syncronous TTY`, is a modular process orchestration gate for seamless synchronous and asynchronous command-line executions in Python.
|
|
24
|
+
|
|
25
|
+
`go-astty` decouples process lifecycle management from standard I/O streams by introducing structured, data-driven pipelines. By abstracting execution payload contexts into self-contained Object vectors, it shifts execution responsibility from external engine hooks directly into individual Task runtimes.
|
|
26
|
+
|
|
27
|
+
## Architecture
|
|
28
|
+
|
|
29
|
+
The framework splits execution into two unified, object-oriented components:
|
|
30
|
+
|
|
31
|
+
* **Tasks (`_BaseTask`)**: Extended list containers acting as payload data vectors. They isolate the executable binary context, handle target system constraints via explicit pre-runtime structural gates (`validation`), and natively invoke their own execution cycles via `.run()`.
|
|
32
|
+
* **Pipers (`_BasePiper`)**: Isolated state engines tied directly to specific task envelopes to capture and track operational boundaries (`stdin`, `stdout`, `stderr`, paths, and exit return codes).
|
|
33
|
+
|
|
34
|
+
---
|
|
35
|
+
|
|
36
|
+
## Features
|
|
37
|
+
|
|
38
|
+
* **Self-Contained Runtimes**: Tasks are no longer passive configuration blocks passed to functional routines; execution logic is encapsulated directly within the task objects (`task.run()`).
|
|
39
|
+
* **Pre-Runtime Validation Gates**: Safe assertion tracks (`shutil.which`) evaluate process structure and binary integrity before booting processes to enforce immediate fail-fast mechanics.
|
|
40
|
+
* **Dual Object Engine Layout**: Mirrored execution architectures separating blocking synchronous behaviors (`syncrun.SyncTask`) and non-blocking asynchronous event routines (`asyncrun.AsyncTask`) cleanly under a predictable interface.
|
|
41
|
+
|
|
42
|
+
---
|
|
43
|
+
|
|
44
|
+
## Installation
|
|
45
|
+
|
|
46
|
+
To install directly from the source repository:
|
|
47
|
+
|
|
48
|
+
**Clone from Codeberg:**
|
|
49
|
+
|
|
50
|
+
```bash
|
|
51
|
+
git clone https://codeberg.org/Fyllus/go-astty.git
|
|
52
|
+
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
**Clone from GitHub:**
|
|
56
|
+
|
|
57
|
+
```bash
|
|
58
|
+
git clone https://github.com/fyllus/go-astty.git
|
|
59
|
+
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
**Install:**
|
|
63
|
+
|
|
64
|
+
```bash
|
|
65
|
+
cd goastty
|
|
66
|
+
pip install .
|
|
67
|
+
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
---
|
|
71
|
+
|
|
72
|
+
## Usage Guide
|
|
73
|
+
|
|
74
|
+
### 1. Asynchronous Execution Pipeline
|
|
75
|
+
|
|
76
|
+
Perfect for long-running CLI integrations, microservices, or concurrent network-bound stream tracking.
|
|
77
|
+
|
|
78
|
+
```python
|
|
79
|
+
import asyncio
|
|
80
|
+
from pathlib import Path
|
|
81
|
+
from goastty import asyncrun
|
|
82
|
+
|
|
83
|
+
async def main():
|
|
84
|
+
# Instantiate asynchronous task with payload arguments
|
|
85
|
+
task = asyncrun.AsyncTask("ping", "-c", "5", "google.com")
|
|
86
|
+
task.piper.path = Path.home()
|
|
87
|
+
|
|
88
|
+
# Fire the self-contained non-blocking runtime
|
|
89
|
+
await task.run()
|
|
90
|
+
|
|
91
|
+
# Evaluate context matrices safely
|
|
92
|
+
if task.piper.returncode == 0:
|
|
93
|
+
print(task.piper.stdout.decode("utf-8"))
|
|
94
|
+
else:
|
|
95
|
+
print(f"Error: {task.piper.stderr.decode('utf-8')}")
|
|
96
|
+
|
|
97
|
+
if __name__ == "__main__":
|
|
98
|
+
asyncio.run(main())
|
|
99
|
+
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
### 2. Synchronous Execution Pipeline
|
|
103
|
+
|
|
104
|
+
Ideal for local scripts, standard automation sequences, or linear operational pipelines.
|
|
105
|
+
|
|
106
|
+
```python
|
|
107
|
+
from pathlib import Path
|
|
108
|
+
from goastty import syncrun
|
|
109
|
+
|
|
110
|
+
def run_backup():
|
|
111
|
+
# Build standard array configuration payload
|
|
112
|
+
task = syncrun.SyncTask("tar", "-czf", "backup.tar.gz", "src/")
|
|
113
|
+
task.piper.path = Path.cwd()
|
|
114
|
+
|
|
115
|
+
# Invoke execution directly from the task payload instance
|
|
116
|
+
task.run()
|
|
117
|
+
|
|
118
|
+
print(f"Process finalized with code: {task.piper.returncode}")
|
|
119
|
+
|
|
120
|
+
if __name__ == "__main__":
|
|
121
|
+
run_backup()
|
|
122
|
+
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
---
|
|
126
|
+
|
|
127
|
+
## API Specification
|
|
128
|
+
|
|
129
|
+
### Core Classes
|
|
130
|
+
|
|
131
|
+
#### `_BasePiper`
|
|
132
|
+
|
|
133
|
+
The logical data matrix tracking standard streams and execution boundaries.
|
|
134
|
+
|
|
135
|
+
* `stdout` / `stderr`: Automatic validation and mutation of incremental stream chunks (`bytearray`).
|
|
136
|
+
* `returncode`: Tracking vector for process termination status.
|
|
137
|
+
* `path`: Explicit execution context location directory (`pathlib.Path`).
|
|
138
|
+
* `shell`: Evaluates whether execution requires a target environment shell gateway.
|
|
139
|
+
|
|
140
|
+
#### `_BaseTask(list)`
|
|
141
|
+
|
|
142
|
+
An extended list structure executing process payload vectors.
|
|
143
|
+
|
|
144
|
+
* `prog`: Tracks the execution binary anchor context (`self[0]`).
|
|
145
|
+
* `args`: Slices away argument payloads safely (`self[1:]`).
|
|
146
|
+
* `validation()`: Evaluates process layout constraints and structural target command existence before booting.
|
|
147
|
+
* `run(stdin=None, kwargs)`: Abstract gateway implemented by runtime engines to drive process setups natively.
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
# go-astty
|
|
2
|
+
|
|
3
|
+
`go-astty` or `Gate of Asyncronous and Syncronous TTY`, is a modular process orchestration gate for seamless synchronous and asynchronous command-line executions in Python.
|
|
4
|
+
|
|
5
|
+
`go-astty` decouples process lifecycle management from standard I/O streams by introducing structured, data-driven pipelines. By abstracting execution payload contexts into self-contained Object vectors, it shifts execution responsibility from external engine hooks directly into individual Task runtimes.
|
|
6
|
+
|
|
7
|
+
## Architecture
|
|
8
|
+
|
|
9
|
+
The framework splits execution into two unified, object-oriented components:
|
|
10
|
+
|
|
11
|
+
* **Tasks (`_BaseTask`)**: Extended list containers acting as payload data vectors. They isolate the executable binary context, handle target system constraints via explicit pre-runtime structural gates (`validation`), and natively invoke their own execution cycles via `.run()`.
|
|
12
|
+
* **Pipers (`_BasePiper`)**: Isolated state engines tied directly to specific task envelopes to capture and track operational boundaries (`stdin`, `stdout`, `stderr`, paths, and exit return codes).
|
|
13
|
+
|
|
14
|
+
---
|
|
15
|
+
|
|
16
|
+
## Features
|
|
17
|
+
|
|
18
|
+
* **Self-Contained Runtimes**: Tasks are no longer passive configuration blocks passed to functional routines; execution logic is encapsulated directly within the task objects (`task.run()`).
|
|
19
|
+
* **Pre-Runtime Validation Gates**: Safe assertion tracks (`shutil.which`) evaluate process structure and binary integrity before booting processes to enforce immediate fail-fast mechanics.
|
|
20
|
+
* **Dual Object Engine Layout**: Mirrored execution architectures separating blocking synchronous behaviors (`syncrun.SyncTask`) and non-blocking asynchronous event routines (`asyncrun.AsyncTask`) cleanly under a predictable interface.
|
|
21
|
+
|
|
22
|
+
---
|
|
23
|
+
|
|
24
|
+
## Installation
|
|
25
|
+
|
|
26
|
+
To install directly from the source repository:
|
|
27
|
+
|
|
28
|
+
**Clone from Codeberg:**
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
git clone https://codeberg.org/Fyllus/go-astty.git
|
|
32
|
+
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
**Clone from GitHub:**
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
git clone https://github.com/fyllus/go-astty.git
|
|
39
|
+
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
**Install:**
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
cd goastty
|
|
46
|
+
pip install .
|
|
47
|
+
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
---
|
|
51
|
+
|
|
52
|
+
## Usage Guide
|
|
53
|
+
|
|
54
|
+
### 1. Asynchronous Execution Pipeline
|
|
55
|
+
|
|
56
|
+
Perfect for long-running CLI integrations, microservices, or concurrent network-bound stream tracking.
|
|
57
|
+
|
|
58
|
+
```python
|
|
59
|
+
import asyncio
|
|
60
|
+
from pathlib import Path
|
|
61
|
+
from goastty import asyncrun
|
|
62
|
+
|
|
63
|
+
async def main():
|
|
64
|
+
# Instantiate asynchronous task with payload arguments
|
|
65
|
+
task = asyncrun.AsyncTask("ping", "-c", "5", "google.com")
|
|
66
|
+
task.piper.path = Path.home()
|
|
67
|
+
|
|
68
|
+
# Fire the self-contained non-blocking runtime
|
|
69
|
+
await task.run()
|
|
70
|
+
|
|
71
|
+
# Evaluate context matrices safely
|
|
72
|
+
if task.piper.returncode == 0:
|
|
73
|
+
print(task.piper.stdout.decode("utf-8"))
|
|
74
|
+
else:
|
|
75
|
+
print(f"Error: {task.piper.stderr.decode('utf-8')}")
|
|
76
|
+
|
|
77
|
+
if __name__ == "__main__":
|
|
78
|
+
asyncio.run(main())
|
|
79
|
+
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
### 2. Synchronous Execution Pipeline
|
|
83
|
+
|
|
84
|
+
Ideal for local scripts, standard automation sequences, or linear operational pipelines.
|
|
85
|
+
|
|
86
|
+
```python
|
|
87
|
+
from pathlib import Path
|
|
88
|
+
from goastty import syncrun
|
|
89
|
+
|
|
90
|
+
def run_backup():
|
|
91
|
+
# Build standard array configuration payload
|
|
92
|
+
task = syncrun.SyncTask("tar", "-czf", "backup.tar.gz", "src/")
|
|
93
|
+
task.piper.path = Path.cwd()
|
|
94
|
+
|
|
95
|
+
# Invoke execution directly from the task payload instance
|
|
96
|
+
task.run()
|
|
97
|
+
|
|
98
|
+
print(f"Process finalized with code: {task.piper.returncode}")
|
|
99
|
+
|
|
100
|
+
if __name__ == "__main__":
|
|
101
|
+
run_backup()
|
|
102
|
+
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
---
|
|
106
|
+
|
|
107
|
+
## API Specification
|
|
108
|
+
|
|
109
|
+
### Core Classes
|
|
110
|
+
|
|
111
|
+
#### `_BasePiper`
|
|
112
|
+
|
|
113
|
+
The logical data matrix tracking standard streams and execution boundaries.
|
|
114
|
+
|
|
115
|
+
* `stdout` / `stderr`: Automatic validation and mutation of incremental stream chunks (`bytearray`).
|
|
116
|
+
* `returncode`: Tracking vector for process termination status.
|
|
117
|
+
* `path`: Explicit execution context location directory (`pathlib.Path`).
|
|
118
|
+
* `shell`: Evaluates whether execution requires a target environment shell gateway.
|
|
119
|
+
|
|
120
|
+
#### `_BaseTask(list)`
|
|
121
|
+
|
|
122
|
+
An extended list structure executing process payload vectors.
|
|
123
|
+
|
|
124
|
+
* `prog`: Tracks the execution binary anchor context (`self[0]`).
|
|
125
|
+
* `args`: Slices away argument payloads safely (`self[1:]`).
|
|
126
|
+
* `validation()`: Evaluates process layout constraints and structural target command existence before booting.
|
|
127
|
+
* `run(stdin=None, kwargs)`: Abstract gateway implemented by runtime engines to drive process setups natively.
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "go-astty"
|
|
7
|
+
version = "0.1.0-a"
|
|
8
|
+
description = "go-astty is modular process orchestration gate for seamless synchronous and asynchronous command-line executions in Python."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
authors = [
|
|
11
|
+
{ name = "Fyllus", email = "Fyllus@git.com" }
|
|
12
|
+
]
|
|
13
|
+
classifiers = [
|
|
14
|
+
"Development Status :: 3 - Alpha",
|
|
15
|
+
"Intended Audience :: Developers",
|
|
16
|
+
"License :: OSI Approved :: MIT License",
|
|
17
|
+
"Programming Language :: Python :: 3.11",
|
|
18
|
+
"Programming Language :: Python :: 3.12",
|
|
19
|
+
"Programming Language :: Python :: 3.13",
|
|
20
|
+
"Topic :: Software Development :: Libraries :: Python Modules",
|
|
21
|
+
]
|
|
22
|
+
requires-python = ">=3.11"
|
|
23
|
+
dependencies = [
|
|
24
|
+
"pygit2>=1.14.0",
|
|
25
|
+
]
|
|
26
|
+
license = { text = "MIT" }
|
|
27
|
+
|
|
28
|
+
[project.urls]
|
|
29
|
+
Codeberg = "https://codeberg.org/Fyllus/go-astty"
|
|
30
|
+
Github = "https://github.com/Fyllus/go-astty"
|
|
31
|
+
|
|
32
|
+
[tool.hatch.build.targets.wheel]
|
|
33
|
+
packages = ["src/goastty"]
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
from asyncio import create_subprocess_exec as async_exec
|
|
3
|
+
from asyncio import create_subprocess_shell as async_shell
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
from . import base
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class AsyncPiper(base._BasePiper):
|
|
10
|
+
"""Asynchronous stream reader and state manager for running sub-processes."""
|
|
11
|
+
def __init__(self) -> None:
|
|
12
|
+
super().__init__()
|
|
13
|
+
|
|
14
|
+
async def stream_stdout(self, stream: asyncio.StreamReader | None) -> None:
|
|
15
|
+
"""Consume and append chunks from the asynchronous stdout stream buffer."""
|
|
16
|
+
if not isinstance(stream, asyncio.StreamReader):
|
|
17
|
+
raise TypeError('<stream> must be StreamReader ')
|
|
18
|
+
while True:
|
|
19
|
+
data = await stream.read(4096)
|
|
20
|
+
if not data:
|
|
21
|
+
break
|
|
22
|
+
self.stdout = data
|
|
23
|
+
|
|
24
|
+
async def stream_stderr(self, stream: asyncio.StreamReader | None) -> None:
|
|
25
|
+
"""Consume and append chunks from the asynchronous stderr stream buffer."""
|
|
26
|
+
if not isinstance(stream, asyncio.StreamReader):
|
|
27
|
+
raise TypeError('<stream> must be StreamReader ')
|
|
28
|
+
while True:
|
|
29
|
+
data = await stream.read(4096)
|
|
30
|
+
if not data:
|
|
31
|
+
break
|
|
32
|
+
self.stderr = data
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class AsyncTask(base._BaseTask):
|
|
36
|
+
"""Asynchronous executable command vector managing an AsyncPiper context."""
|
|
37
|
+
def __init__(self, *args: str) -> None:
|
|
38
|
+
super().__init__(*args)
|
|
39
|
+
|
|
40
|
+
@property
|
|
41
|
+
def piper(self) -> AsyncPiper:
|
|
42
|
+
"""Get or initialize the asynchronous stream pipeline manager instance."""
|
|
43
|
+
if not hasattr(self, '_piper'):
|
|
44
|
+
setattr(self, '_piper', AsyncPiper())
|
|
45
|
+
return getattr(self, '_piper')
|
|
46
|
+
|
|
47
|
+
async def run(self, stdin: Any = None, **kwargs: Any) -> None:
|
|
48
|
+
"""Execute an AsyncTask asynchronously using either shell or executive sub-processes."""
|
|
49
|
+
self.validation()
|
|
50
|
+
|
|
51
|
+
if stdin is not None:
|
|
52
|
+
self.piper.stdin_pipe = stdin
|
|
53
|
+
|
|
54
|
+
kwargs.setdefault('stdin', self.piper.stdin_pipe)
|
|
55
|
+
kwargs.setdefault('stdout', self.piper.stdout_pipe)
|
|
56
|
+
kwargs.setdefault('stderr', self.piper.stderr_pipe)
|
|
57
|
+
kwargs.setdefault('cwd', self.piper.path)
|
|
58
|
+
|
|
59
|
+
if self.piper.shell:
|
|
60
|
+
cmd_str = " ".join(self)
|
|
61
|
+
process = await async_shell(cmd_str, **kwargs)
|
|
62
|
+
else:
|
|
63
|
+
process = await async_exec(self.prog, *self.args, **kwargs)
|
|
64
|
+
|
|
65
|
+
await asyncio.gather(
|
|
66
|
+
self.piper.stream_stdout(process.stdout),
|
|
67
|
+
self.piper.stream_stderr(process.stderr)
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
await process.wait()
|
|
71
|
+
if process.returncode is not None:
|
|
72
|
+
self.piper.returncode = process.returncode
|
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
import shutil
|
|
2
|
+
import subprocess
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from typing import Any, Callable, List, TypeVar
|
|
5
|
+
|
|
6
|
+
PIPE = subprocess.PIPE
|
|
7
|
+
STDOUT = subprocess.STDOUT
|
|
8
|
+
DEVNULL = subprocess.DEVNULL
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class AssignmentError(Exception):
|
|
12
|
+
"""Exception raised for invalid property or attribute assignments."""
|
|
13
|
+
def __init__(self, case: str, obj: object, *expected: type) -> None:
|
|
14
|
+
expected_types = ", ".join(f"<{t.__name__}>" for t in expected)
|
|
15
|
+
self._cases = {
|
|
16
|
+
'none': f'Property/Attribute cannot be None: expected {expected_types}, got <{type(obj).__name__}>',
|
|
17
|
+
'unable': f'Unable to assign type <{type(obj).__name__}>: expected {expected_types}',
|
|
18
|
+
'invalid': f'Invalid Assignment: expected {expected_types}, cannot use <{type(obj).__name__}>'
|
|
19
|
+
}
|
|
20
|
+
super().__init__(self._cases.get(case, f"Unknown error case with type <{type(obj).__name__}>"))
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class TaskError(Exception):
|
|
24
|
+
"""Exception raised for validation failures before task runtime execution."""
|
|
25
|
+
def __init__(self, flag: str, task: "_BaseTask") -> None:
|
|
26
|
+
self._cases = {
|
|
27
|
+
'empty': f'Unable to run empty task: {task}',
|
|
28
|
+
'invalid_command': f'Not found or unknown command {task.prog}'
|
|
29
|
+
}
|
|
30
|
+
super().__init__(self._cases.get(flag, f'Unknown error case with task {task}'))
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
BaseTask = TypeVar('BaseTask', bound='_BaseTask')
|
|
34
|
+
BasePiper = TypeVar('BasePiper', bound='_BasePiper')
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class _BasePiper:
|
|
38
|
+
"""Manage stream pipes, exit status, and execution context for a process."""
|
|
39
|
+
def __init__(self) -> None:
|
|
40
|
+
pass
|
|
41
|
+
|
|
42
|
+
def __getitem__(self, name: str) -> Any:
|
|
43
|
+
return getattr(self, name)
|
|
44
|
+
|
|
45
|
+
def __setitem__(self, name: str, value: Any) -> None:
|
|
46
|
+
if not hasattr(self, name):
|
|
47
|
+
raise AttributeError(f"'{type(self).__name__}' object has no attribute '{name}'")
|
|
48
|
+
setattr(self, name, value)
|
|
49
|
+
|
|
50
|
+
# ---------- class property -------------------
|
|
51
|
+
@property
|
|
52
|
+
def stdout(self) -> bytearray:
|
|
53
|
+
"""Get the accumulated standard output bytes."""
|
|
54
|
+
if not hasattr(self, '_stdout'):
|
|
55
|
+
setattr(self, '_stdout', bytearray())
|
|
56
|
+
return getattr(self, '_stdout')
|
|
57
|
+
|
|
58
|
+
@stdout.setter
|
|
59
|
+
def stdout(self, value: bytes | bytearray) -> None:
|
|
60
|
+
if not isinstance(value, (bytes, bytearray)):
|
|
61
|
+
raise AssignmentError('invalid', value, bytes, bytearray)
|
|
62
|
+
self.stdout.extend(value)
|
|
63
|
+
|
|
64
|
+
@property
|
|
65
|
+
def stderr(self) -> bytearray:
|
|
66
|
+
"""Get the accumulated standard error bytes."""
|
|
67
|
+
if not hasattr(self, '_stderr'):
|
|
68
|
+
setattr(self, '_stderr', bytearray())
|
|
69
|
+
return getattr(self, '_stderr')
|
|
70
|
+
|
|
71
|
+
@stderr.setter
|
|
72
|
+
def stderr(self, value: bytes | bytearray) -> None:
|
|
73
|
+
if not isinstance(value, (bytes, bytearray)):
|
|
74
|
+
raise AssignmentError('invalid', value, bytes, bytearray)
|
|
75
|
+
self.stderr.extend(value)
|
|
76
|
+
|
|
77
|
+
@property
|
|
78
|
+
def returncode(self) -> int | None:
|
|
79
|
+
"""Get the process exit code integer after termination."""
|
|
80
|
+
if not hasattr(self, '_returncode'):
|
|
81
|
+
setattr(self, '_returncode', None)
|
|
82
|
+
return getattr(self, '_returncode')
|
|
83
|
+
|
|
84
|
+
@returncode.setter
|
|
85
|
+
def returncode(self, value: int) -> None:
|
|
86
|
+
if not isinstance(value, int):
|
|
87
|
+
raise AssignmentError('unable', value, int)
|
|
88
|
+
self._returncode = value
|
|
89
|
+
|
|
90
|
+
@property
|
|
91
|
+
def stdout_pipe(self) -> int:
|
|
92
|
+
"""Get the internal target destination descriptor for stdout."""
|
|
93
|
+
if not hasattr(self, '_stdout_pipe'):
|
|
94
|
+
setattr(self, '_stdout_pipe', PIPE)
|
|
95
|
+
return getattr(self, '_stdout_pipe')
|
|
96
|
+
|
|
97
|
+
@property
|
|
98
|
+
def stderr_pipe(self) -> int:
|
|
99
|
+
"""Get the internal target destination descriptor for stderr."""
|
|
100
|
+
if not hasattr(self, '_stderr_pipe'):
|
|
101
|
+
setattr(self, '_stderr_pipe', PIPE)
|
|
102
|
+
return getattr(self, '_stderr_pipe')
|
|
103
|
+
|
|
104
|
+
@property
|
|
105
|
+
def stdin_pipe(self) -> Any:
|
|
106
|
+
"""Get the source input stream pipeline anchor for stdin."""
|
|
107
|
+
if not hasattr(self, '_stdin'):
|
|
108
|
+
setattr(self, '_stdin', None)
|
|
109
|
+
return getattr(self, '_stdin')
|
|
110
|
+
|
|
111
|
+
@stdin_pipe.setter
|
|
112
|
+
def stdin_pipe(self, value: Any) -> None:
|
|
113
|
+
setattr(self, '_stdin', value)
|
|
114
|
+
|
|
115
|
+
@property
|
|
116
|
+
def path(self) -> Path:
|
|
117
|
+
"""Get the filesystem directory context where execution occurs."""
|
|
118
|
+
if not hasattr(self, '_path'):
|
|
119
|
+
setattr(self, '_path', Path.cwd())
|
|
120
|
+
return getattr(self, '_path')
|
|
121
|
+
|
|
122
|
+
@path.setter
|
|
123
|
+
def path(self, value: Path) -> None:
|
|
124
|
+
if value is None:
|
|
125
|
+
raise AssignmentError('none', value, Path)
|
|
126
|
+
if not isinstance(value, Path):
|
|
127
|
+
raise AssignmentError('unable', value, Path)
|
|
128
|
+
setattr(self, '_path', value)
|
|
129
|
+
|
|
130
|
+
@property
|
|
131
|
+
def shell(self) -> bool:
|
|
132
|
+
"""Check whether direct system shell execution is enabled."""
|
|
133
|
+
if not hasattr(self, '_shell'):
|
|
134
|
+
setattr(self, '_shell', False)
|
|
135
|
+
return getattr(self, '_shell')
|
|
136
|
+
|
|
137
|
+
@shell.setter
|
|
138
|
+
def shell(self, value: bool) -> None:
|
|
139
|
+
if not isinstance(value, bool):
|
|
140
|
+
raise AssignmentError('unable', value, bool)
|
|
141
|
+
setattr(self, '_shell', value)
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
class _BaseTask(list):
|
|
145
|
+
"""Represent an executable command payload structure as an array of arguments."""
|
|
146
|
+
def __init__(self, *args: str) -> None:
|
|
147
|
+
super().__init__()
|
|
148
|
+
if args:
|
|
149
|
+
self.extend(args)
|
|
150
|
+
|
|
151
|
+
def __bool__(self) -> bool:
|
|
152
|
+
return len(self) > 0
|
|
153
|
+
|
|
154
|
+
# ---------- class property -------------------
|
|
155
|
+
@property
|
|
156
|
+
def prog(self) -> str:
|
|
157
|
+
"""Get the root binary filename executable anchor of the command."""
|
|
158
|
+
if not self:
|
|
159
|
+
return ''
|
|
160
|
+
return self[0]
|
|
161
|
+
|
|
162
|
+
@property
|
|
163
|
+
def args(self) -> list:
|
|
164
|
+
"""Get the trailing arguments list passed to the executable payload."""
|
|
165
|
+
if not len(self) > 1:
|
|
166
|
+
return []
|
|
167
|
+
return self[1:]
|
|
168
|
+
|
|
169
|
+
@property
|
|
170
|
+
def piper(self) -> Any:
|
|
171
|
+
"""Get the stream pipeline manager tied to this specific task execution context."""
|
|
172
|
+
if not hasattr(self, '_piper'):
|
|
173
|
+
setattr(self, '_piper', _BasePiper())
|
|
174
|
+
return getattr(self, '_piper')
|
|
175
|
+
|
|
176
|
+
@piper.setter
|
|
177
|
+
def piper(self, value: Any) -> None:
|
|
178
|
+
if not isinstance(value, _BasePiper):
|
|
179
|
+
raise AssignmentError('unable', value, _BasePiper)
|
|
180
|
+
setattr(self, '_piper', value)
|
|
181
|
+
|
|
182
|
+
@property
|
|
183
|
+
def callback(self) -> Any:
|
|
184
|
+
"""Get the execution hook function triggered upon process completion."""
|
|
185
|
+
return getattr(self, '_callback', None)
|
|
186
|
+
|
|
187
|
+
@callback.setter
|
|
188
|
+
def callback(self, value: Any) -> None:
|
|
189
|
+
if not callable(value):
|
|
190
|
+
raise AssignmentError('invalid', value, Callable)
|
|
191
|
+
setattr(self, '_callback', value)
|
|
192
|
+
|
|
193
|
+
# --------------- main methods -------------------------------
|
|
194
|
+
def append(self, value: str) -> None:
|
|
195
|
+
"""Append a safe string argument entry to the command vector."""
|
|
196
|
+
if not isinstance(value, str):
|
|
197
|
+
raise ValueError('Value must be <str>')
|
|
198
|
+
super().append(value)
|
|
199
|
+
|
|
200
|
+
def extend(self, value: List[str]) -> None:
|
|
201
|
+
"""Extend the command argument payload using an array of string slices."""
|
|
202
|
+
if not all(isinstance(arg_item, str) for arg_item in value):
|
|
203
|
+
raise ValueError('Value must be <List[str]>')
|
|
204
|
+
super().extend(value)
|
|
205
|
+
|
|
206
|
+
def validation(self) -> None:
|
|
207
|
+
"""Pre-Runtime validation gate to check structural faults before process boot."""
|
|
208
|
+
if not self:
|
|
209
|
+
raise TaskError('empty', self)
|
|
210
|
+
if not self.piper.shell and not shutil.which(self.prog):
|
|
211
|
+
raise TaskError('invalid_command', self)
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
from pathlib import Path
|
|
2
|
+
from typing import (
|
|
3
|
+
List,
|
|
4
|
+
Optional,
|
|
5
|
+
Self,
|
|
6
|
+
)
|
|
7
|
+
|
|
8
|
+
from pygit2 import (
|
|
9
|
+
Commit,
|
|
10
|
+
GitError,
|
|
11
|
+
Index,
|
|
12
|
+
Repository,
|
|
13
|
+
Signature,
|
|
14
|
+
clone_repository,
|
|
15
|
+
init_repository,
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class GitRepoError(Exception):
|
|
20
|
+
def __init__(self, *args: object) -> None:
|
|
21
|
+
super().__init__(*args)
|
|
22
|
+
|
|
23
|
+
def open_repo(repo: Repository | Path) -> Repository:
|
|
24
|
+
"""Open an existing git repository from a Path or Repository object."""
|
|
25
|
+
if isinstance(repo, Repository):
|
|
26
|
+
return repo
|
|
27
|
+
if not isinstance(repo, Path):
|
|
28
|
+
raise GitRepoError(f'Invalid repository type <{type(repo).__name__}>, expected <RepositoryOrPath>')
|
|
29
|
+
try:
|
|
30
|
+
return Repository(str(repo))
|
|
31
|
+
except (KeyError, GitError):
|
|
32
|
+
raise GitRepoError('Not a valid repository inited.')
|
|
33
|
+
|
|
34
|
+
def is_git(path: Path) -> bool:
|
|
35
|
+
"""Check if the given path is a valid git repository."""
|
|
36
|
+
try:
|
|
37
|
+
open_repo(repo=path)
|
|
38
|
+
return True
|
|
39
|
+
except GitRepoError:
|
|
40
|
+
return False
|
|
41
|
+
|
|
42
|
+
class GitRepo:
|
|
43
|
+
"""
|
|
44
|
+
TODO: log, status, tag
|
|
45
|
+
"""
|
|
46
|
+
def __init__(self, repo: Repository | Path) -> None:
|
|
47
|
+
self.repo = repo
|
|
48
|
+
|
|
49
|
+
# ---------------------- class property ---------------------------
|
|
50
|
+
@property
|
|
51
|
+
def index(self) -> Index:
|
|
52
|
+
"""Get the repository index (staging area)."""
|
|
53
|
+
return self.repo.index
|
|
54
|
+
|
|
55
|
+
@property
|
|
56
|
+
def repo(self) -> Repository:
|
|
57
|
+
"""Get the underlying native repository instance."""
|
|
58
|
+
if not hasattr(self, '_repo'):
|
|
59
|
+
setattr(self, '_repo', None)
|
|
60
|
+
return getattr(self, '_repo')
|
|
61
|
+
|
|
62
|
+
@property
|
|
63
|
+
def workdir(self) -> Path:
|
|
64
|
+
"""Get the absolute path to the working directory."""
|
|
65
|
+
return Path(self.repo.workdir).resolve()
|
|
66
|
+
|
|
67
|
+
@repo.setter
|
|
68
|
+
def repo(self, repo: Repository | Path) -> None:
|
|
69
|
+
new_repo = open_repo(repo=repo)
|
|
70
|
+
setattr(self, '_repo', new_repo)
|
|
71
|
+
|
|
72
|
+
# ---------------- repo methods ------------------
|
|
73
|
+
@classmethod
|
|
74
|
+
def init(cls, path: Path, bare: bool = False, **kwargs) -> "GitRepo":
|
|
75
|
+
"""Initialize a new git repository or open it if already initialized."""
|
|
76
|
+
if is_git(path):
|
|
77
|
+
return cls(open_repo(repo=path))
|
|
78
|
+
if not path.exists():
|
|
79
|
+
raise GitRepoError(f"Path does not exist: invalid try to init a repo at '{path}'.")
|
|
80
|
+
native_repo = init_repository(str(path), bare=bare, **kwargs)
|
|
81
|
+
return cls(native_repo)
|
|
82
|
+
|
|
83
|
+
@classmethod
|
|
84
|
+
def clone(cls, url: str, path: Path, **kwargs) -> "GitRepo":
|
|
85
|
+
"""Clone a remote git repository into a local directory."""
|
|
86
|
+
if is_git(path):
|
|
87
|
+
raise GitRepoError(f"Cannot clone: a valid git repository already exists at '{path}'.")
|
|
88
|
+
native_repo = clone_repository(url, str(path), **kwargs)
|
|
89
|
+
return cls(native_repo)
|
|
90
|
+
|
|
91
|
+
# ------------- main methods -----------------------------
|
|
92
|
+
def add(self, *files: Path) -> Self:
|
|
93
|
+
"""Stage files or all modifications to the repository index."""
|
|
94
|
+
if len(files) == 0:
|
|
95
|
+
self.index.add_all()
|
|
96
|
+
else:
|
|
97
|
+
for file_path in files:
|
|
98
|
+
abs_path = file_path.resolve()
|
|
99
|
+
try:
|
|
100
|
+
rel_path = abs_path.relative_to(self.workdir)
|
|
101
|
+
self.index.add(str(rel_path))
|
|
102
|
+
except ValueError:
|
|
103
|
+
raise ValueError(
|
|
104
|
+
f"File {abs_path} out of repository {self.workdir}"
|
|
105
|
+
)
|
|
106
|
+
self.index.write()
|
|
107
|
+
return self
|
|
108
|
+
|
|
109
|
+
def remove(self, *files: Path, staged_only: bool = False) -> Self:
|
|
110
|
+
"""Remove files from the repository index and optionally from the working directory."""
|
|
111
|
+
for file_path in files:
|
|
112
|
+
rel_path = file_path.resolve().relative_to(self.workdir)
|
|
113
|
+
str_path = str(rel_path)
|
|
114
|
+
|
|
115
|
+
if str_path in self.index:
|
|
116
|
+
self.index.remove(str_path)
|
|
117
|
+
|
|
118
|
+
if not staged_only:
|
|
119
|
+
abs_path = self.workdir / rel_path
|
|
120
|
+
if abs_path.exists():
|
|
121
|
+
abs_path.unlink()
|
|
122
|
+
|
|
123
|
+
self.index.write()
|
|
124
|
+
return self
|
|
125
|
+
|
|
126
|
+
def move(self, source: Path, destination: Path) -> Self:
|
|
127
|
+
"""Move or rename a file within the working directory and update the index."""
|
|
128
|
+
abs_src = source.resolve()
|
|
129
|
+
abs_dst = destination.resolve()
|
|
130
|
+
|
|
131
|
+
rel_src = abs_src.relative_to(self.workdir)
|
|
132
|
+
rel_dst = abs_dst.relative_to(self.workdir)
|
|
133
|
+
|
|
134
|
+
if abs_src.exists():
|
|
135
|
+
abs_dst.parent.mkdir(parents=True, exist_ok=True)
|
|
136
|
+
abs_src.rename(abs_dst)
|
|
137
|
+
|
|
138
|
+
str_src = str(rel_src)
|
|
139
|
+
if str_src in self.index:
|
|
140
|
+
self.index.remove(str_src)
|
|
141
|
+
|
|
142
|
+
self.index.add(str(rel_dst))
|
|
143
|
+
self.index.write()
|
|
144
|
+
return self
|
|
145
|
+
|
|
146
|
+
def restore(self, *files: Path, ref: str = 'HEAD') -> Self:
|
|
147
|
+
"""Discard local modifications by restoring files from a specific commit reference."""
|
|
148
|
+
try:
|
|
149
|
+
commit = self.repo.revparse_single(ref)
|
|
150
|
+
commit_tree = commit.tree
|
|
151
|
+
except KeyError:
|
|
152
|
+
return self
|
|
153
|
+
|
|
154
|
+
if len(files) == 0:
|
|
155
|
+
self.repo.checkout_tree(commit_tree, strategy=1, directory=str(self.workdir))
|
|
156
|
+
self.index.read()
|
|
157
|
+
else:
|
|
158
|
+
for file_path in files:
|
|
159
|
+
rel_path = file_path.resolve().relative_to(self.workdir)
|
|
160
|
+
str_path = str(rel_path)
|
|
161
|
+
|
|
162
|
+
self.repo.checkout_tree(
|
|
163
|
+
commit_tree,
|
|
164
|
+
strategy=1,
|
|
165
|
+
paths=[str_path],
|
|
166
|
+
directory=str(self.workdir)
|
|
167
|
+
)
|
|
168
|
+
|
|
169
|
+
self.index.read()
|
|
170
|
+
return self
|
|
171
|
+
|
|
172
|
+
def commit(self,
|
|
173
|
+
author: Signature, committer: Signature,
|
|
174
|
+
message: str, parents: Optional[List[Commit]] = None,
|
|
175
|
+
ref: str = 'HEAD'
|
|
176
|
+
) -> Self:
|
|
177
|
+
"""Commit staged changes to the repository history."""
|
|
178
|
+
tree_oid = self.index.write_tree()
|
|
179
|
+
if parents is None:
|
|
180
|
+
parents = []
|
|
181
|
+
if len(parents) == 0:
|
|
182
|
+
try:
|
|
183
|
+
current_tip = self.repo.revparse_single(ref)
|
|
184
|
+
if isinstance(current_tip, Commit):
|
|
185
|
+
parents = [current_tip]
|
|
186
|
+
except KeyError:
|
|
187
|
+
pass
|
|
188
|
+
self.repo.create_commit(ref, author, committer, message, tree_oid, parents)
|
|
189
|
+
return self
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
from subprocess import run as shell
|
|
2
|
+
from typing import Any
|
|
3
|
+
|
|
4
|
+
from . import base
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class SyncPiper(base._BasePiper):
|
|
8
|
+
"""Synchronous stream reader and state manager for running sub-processes."""
|
|
9
|
+
def __init__(self) -> None:
|
|
10
|
+
super().__init__()
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class SyncTask(base._BaseTask):
|
|
14
|
+
"""Synchronous executable command vector managing a SyncPiper context."""
|
|
15
|
+
def __init__(self, *args: str) -> None:
|
|
16
|
+
super().__init__(*args)
|
|
17
|
+
|
|
18
|
+
@property
|
|
19
|
+
def piper(self) -> SyncPiper:
|
|
20
|
+
"""Get or initialize the synchronous stream pipeline manager instance."""
|
|
21
|
+
if not hasattr(self, '_piper'):
|
|
22
|
+
setattr(self, '_piper', SyncPiper())
|
|
23
|
+
return getattr(self, '_piper')
|
|
24
|
+
|
|
25
|
+
def run(self, stdin: Any = None, **kwargs: Any) -> None:
|
|
26
|
+
"""Execute a SyncTask synchronously using either shell or executive sub-processes."""
|
|
27
|
+
self.validation()
|
|
28
|
+
|
|
29
|
+
if stdin is not None:
|
|
30
|
+
self.piper.stdin_pipe = stdin
|
|
31
|
+
|
|
32
|
+
kwargs.setdefault('stdin', self.piper.stdin_pipe)
|
|
33
|
+
kwargs.setdefault('stdout', self.piper.stdout_pipe)
|
|
34
|
+
kwargs.setdefault('stderr', self.piper.stderr_pipe)
|
|
35
|
+
kwargs.setdefault('cwd', self.piper.path)
|
|
36
|
+
kwargs.setdefault('shell', self.piper.shell)
|
|
37
|
+
|
|
38
|
+
if not self.piper.shell:
|
|
39
|
+
cmd = list(self)
|
|
40
|
+
process = shell(cmd, **kwargs)
|
|
41
|
+
else:
|
|
42
|
+
cmd_str = ' '.join(self)
|
|
43
|
+
process = shell(cmd_str, **kwargs)
|
|
44
|
+
|
|
45
|
+
if process.stdout is not None:
|
|
46
|
+
self.piper.stdout = process.stdout
|
|
47
|
+
if process.stderr is not None:
|
|
48
|
+
self.piper.stderr = process.stderr
|
|
49
|
+
|
|
50
|
+
self.piper.returncode = process.returncode
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
|
|
4
|
+
from goastty import asyncrun
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
async def main(c: int = 5) -> None:
|
|
8
|
+
# Safe argument array context for native ping execution
|
|
9
|
+
task = asyncrun.AsyncTask("ping", "-Dv", "-c", str(c), "google.com")
|
|
10
|
+
task.piper.path = Path.home()
|
|
11
|
+
|
|
12
|
+
print({"status": "starting", "command": list(task)})
|
|
13
|
+
|
|
14
|
+
# Dispatch the execution coroutine to the background event loop
|
|
15
|
+
run_task = asyncio.create_task(task.run())
|
|
16
|
+
|
|
17
|
+
# Monitor engine execution while returncode remains unassigned
|
|
18
|
+
while task.piper.returncode is None:
|
|
19
|
+
print("[Async Engine] Awaiting ping response... process actively running.")
|
|
20
|
+
await asyncio.sleep(0.5)
|
|
21
|
+
|
|
22
|
+
await run_task
|
|
23
|
+
|
|
24
|
+
print(f"\n[Async Engine] Process finalized with code: {task.piper.returncode}")
|
|
25
|
+
print(f"Accumulated STDOUT buffer:\n{task.piper.stdout.decode('utf-8', errors='ignore')}")
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
if __name__ == "__main__":
|
|
29
|
+
asyncio.run(main(5))
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import threading
|
|
2
|
+
import time
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
from goastty import syncrun
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def main(c: int=5) -> None:
|
|
9
|
+
# Initialize sequential task vector context
|
|
10
|
+
task = syncrun.SyncTask("ping", "-Dv", "-c", str(c), "google.com")
|
|
11
|
+
task.piper.path = Path.home()
|
|
12
|
+
|
|
13
|
+
print({"status": "starting", "command": list(task)})
|
|
14
|
+
|
|
15
|
+
# Dispatch the blocking execution thread to decouple I/O boundary tracking
|
|
16
|
+
worker_thread = threading.Thread(target=task.run)
|
|
17
|
+
worker_thread.start()
|
|
18
|
+
|
|
19
|
+
# Core main loop monitors the state engine without stopping the thread runtime
|
|
20
|
+
while task.piper.returncode is None:
|
|
21
|
+
print("[Sync Engine] Processing execution pipeline... task locked in worker thread.")
|
|
22
|
+
time.sleep(0.5)
|
|
23
|
+
|
|
24
|
+
worker_thread.join()
|
|
25
|
+
|
|
26
|
+
print(f"\n[Sync Engine] Process finalized with code: {task.piper.returncode}")
|
|
27
|
+
print(f"Accumulated STDOUT buffer:\n{task.piper.stdout.decode('utf-8', errors='ignore')}")
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
if __name__ == "__main__":
|
|
31
|
+
main(5)
|