primitive 0.2.10__py3-none-any.whl → 0.2.12__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.
- primitive/__about__.py +1 -1
- primitive/agent/actions.py +62 -120
- primitive/agent/commands.py +2 -1
- primitive/agent/runner.py +43 -34
- primitive/agent/uploader.py +2 -2
- primitive/cli.py +2 -0
- primitive/client.py +41 -16
- primitive/daemons/actions.py +48 -62
- primitive/daemons/commands.py +68 -22
- primitive/daemons/launch_agents.py +205 -122
- primitive/daemons/launch_service.py +224 -164
- primitive/daemons/ui.py +41 -0
- primitive/db/base.py +5 -0
- primitive/db/models.py +88 -0
- primitive/db/sqlite.py +34 -0
- primitive/exec/actions.py +0 -1
- primitive/files/actions.py +0 -1
- primitive/hardware/actions.py +11 -10
- primitive/hardware/commands.py +1 -68
- primitive/hardware/ui.py +67 -0
- primitive/monitor/actions.py +199 -0
- primitive/monitor/commands.py +13 -0
- primitive/reservations/actions.py +0 -2
- primitive/utils/auth.py +0 -2
- primitive/utils/daemons.py +54 -0
- {primitive-0.2.10.dist-info → primitive-0.2.12.dist-info}/METADATA +3 -1
- {primitive-0.2.10.dist-info → primitive-0.2.12.dist-info}/RECORD +30 -22
- {primitive-0.2.10.dist-info → primitive-0.2.12.dist-info}/WHEEL +0 -0
- {primitive-0.2.10.dist-info → primitive-0.2.12.dist-info}/entry_points.txt +0 -0
- {primitive-0.2.10.dist-info → primitive-0.2.12.dist-info}/licenses/LICENSE.txt +0 -0
primitive/daemons/commands.py
CHANGED
@@ -1,7 +1,10 @@
|
|
1
1
|
import click
|
2
2
|
|
3
|
-
from ..utils.printer import print_result
|
4
3
|
import typing
|
4
|
+
from typing import Optional
|
5
|
+
from .ui import render_daemon_list
|
6
|
+
|
7
|
+
from loguru import logger
|
5
8
|
|
6
9
|
if typing.TYPE_CHECKING:
|
7
10
|
from ..client import Primitive
|
@@ -16,50 +19,93 @@ def cli(context):
|
|
16
19
|
|
17
20
|
@cli.command("install")
|
18
21
|
@click.pass_context
|
19
|
-
|
22
|
+
@click.argument(
|
23
|
+
"name",
|
24
|
+
type=str,
|
25
|
+
required=False,
|
26
|
+
)
|
27
|
+
def install_daemon_command(context, name: Optional[str]):
|
20
28
|
"""Install the full primitive daemon"""
|
21
29
|
primitive: Primitive = context.obj.get("PRIMITIVE")
|
22
|
-
|
23
|
-
|
30
|
+
installed = primitive.daemons.install(name=name)
|
31
|
+
|
32
|
+
if installed:
|
33
|
+
logger.info(":white_check_mark: daemon(s) installed successfully!")
|
34
|
+
else:
|
35
|
+
logger.error("Unable to install daemon(s).")
|
24
36
|
|
25
37
|
|
26
38
|
@cli.command("uninstall")
|
27
39
|
@click.pass_context
|
28
|
-
|
40
|
+
@click.argument(
|
41
|
+
"name",
|
42
|
+
type=str,
|
43
|
+
required=False,
|
44
|
+
)
|
45
|
+
def uninstall_daemon_command(context, name: Optional[str]):
|
29
46
|
"""Uninstall the full primitive Daemon"""
|
30
47
|
primitive: Primitive = context.obj.get("PRIMITIVE")
|
31
|
-
|
32
|
-
|
48
|
+
uninstalled = primitive.daemons.uninstall(name=name)
|
49
|
+
|
50
|
+
if uninstalled:
|
51
|
+
logger.info(":white_check_mark: daemon(s) uninstalled successfully!")
|
52
|
+
else:
|
53
|
+
logger.error("Unable to uninstall daemon(s).")
|
33
54
|
|
34
55
|
|
35
56
|
@cli.command("stop")
|
36
57
|
@click.pass_context
|
37
|
-
|
58
|
+
@click.argument(
|
59
|
+
"name",
|
60
|
+
type=str,
|
61
|
+
required=False,
|
62
|
+
)
|
63
|
+
def stop_daemon_command(context, name: Optional[str]):
|
38
64
|
"""Stop primitive Daemon"""
|
39
65
|
primitive: Primitive = context.obj.get("PRIMITIVE")
|
40
|
-
|
41
|
-
|
42
|
-
if
|
43
|
-
|
44
|
-
|
66
|
+
stopped = primitive.daemons.stop(name=name)
|
67
|
+
|
68
|
+
if stopped:
|
69
|
+
logger.info(":white_check_mark: daemon(s) stopped successfully!")
|
70
|
+
else:
|
71
|
+
logger.error("Unable to stop daemon(s).")
|
45
72
|
|
46
73
|
|
47
74
|
@cli.command("start")
|
48
75
|
@click.pass_context
|
49
|
-
|
76
|
+
@click.argument(
|
77
|
+
"name",
|
78
|
+
type=str,
|
79
|
+
required=False,
|
80
|
+
)
|
81
|
+
def start_daemon_command(context, name: Optional[str]):
|
50
82
|
"""Start primitive Daemon"""
|
51
83
|
primitive: Primitive = context.obj.get("PRIMITIVE")
|
52
|
-
|
53
|
-
|
54
|
-
if
|
55
|
-
|
56
|
-
|
84
|
+
started = primitive.daemons.start(name=name)
|
85
|
+
|
86
|
+
if started:
|
87
|
+
logger.info(":white_check_mark: daemon(s) started successfully!")
|
88
|
+
else:
|
89
|
+
logger.error("Unable to start daemon(s).")
|
57
90
|
|
58
91
|
|
59
92
|
@cli.command("logs")
|
60
93
|
@click.pass_context
|
61
|
-
|
94
|
+
@click.argument(
|
95
|
+
"name",
|
96
|
+
type=str,
|
97
|
+
required=True,
|
98
|
+
)
|
99
|
+
def log_daemon_command(context, name: str):
|
62
100
|
"""Logs from primitive Daemon"""
|
63
101
|
primitive: Primitive = context.obj.get("PRIMITIVE")
|
64
|
-
|
65
|
-
|
102
|
+
primitive.daemons.logs(name=name)
|
103
|
+
|
104
|
+
|
105
|
+
@cli.command("list")
|
106
|
+
@click.pass_context
|
107
|
+
def list_daemon_command(context):
|
108
|
+
"""List all daemons"""
|
109
|
+
primitive: Primitive = context.obj.get("PRIMITIVE")
|
110
|
+
daemon_list = primitive.daemons.list()
|
111
|
+
render_daemon_list(daemons=daemon_list)
|
@@ -1,102 +1,130 @@
|
|
1
1
|
import os
|
2
2
|
from pathlib import Path
|
3
3
|
import subprocess
|
4
|
+
from loguru import logger
|
5
|
+
from ..utils.daemons import Daemon
|
4
6
|
|
5
7
|
HOME_DIRECTORY = Path.home()
|
6
8
|
CURRENT_USER = str(HOME_DIRECTORY.expanduser()).lstrip("/Users/")
|
7
|
-
|
8
9
|
PRIMITIVE_BINARY_PATH = Path(HOME_DIRECTORY / ".pyenv" / "shims" / "primitive")
|
9
10
|
|
10
|
-
|
11
|
-
|
12
|
-
)
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
return
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
def
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
11
|
+
|
12
|
+
class LaunchAgent(Daemon):
|
13
|
+
def __init__(self, label: str):
|
14
|
+
self.label = label
|
15
|
+
self.name = label.split(".")[-1]
|
16
|
+
|
17
|
+
@property
|
18
|
+
def file_path(self) -> Path:
|
19
|
+
return Path(HOME_DIRECTORY / "Library" / "LaunchAgents" / f"{self.label}.plist")
|
20
|
+
|
21
|
+
@property
|
22
|
+
def logs(self) -> Path:
|
23
|
+
return Path(HOME_DIRECTORY / "Library" / "Logs" / f"{self.label}.log")
|
24
|
+
|
25
|
+
@property
|
26
|
+
def cmd(self) -> str:
|
27
|
+
return self.label.split(".")[-1]
|
28
|
+
|
29
|
+
def stop(self, unload: bool = True) -> bool:
|
30
|
+
try:
|
31
|
+
stop_existing_process = f"launchctl stop {self.label}"
|
32
|
+
subprocess.check_output(
|
33
|
+
stop_existing_process.split(" "), stderr=subprocess.DEVNULL
|
34
|
+
)
|
35
|
+
logger.info(f":white_check_mark: {self.label} stopped successfully!")
|
36
|
+
if unload:
|
37
|
+
self.unload() # Need to unload with KeepAlive = true or else launchctl will try to pick it up again
|
38
|
+
return True
|
39
|
+
except subprocess.CalledProcessError as exception:
|
40
|
+
if exception.returncode == 3:
|
41
|
+
logger.debug(f"{self.label} is not running or does not exist.")
|
42
|
+
return True
|
43
|
+
else:
|
44
|
+
logger.error(f"Unable to stop {self.label}, {exception.returncode}")
|
45
|
+
logger.error(exception)
|
46
|
+
return False
|
47
|
+
|
48
|
+
def start(self, load: bool = True) -> bool:
|
49
|
+
if load:
|
50
|
+
self.load()
|
51
|
+
try:
|
52
|
+
start_new_agent = f"launchctl start {self.label}"
|
53
|
+
subprocess.check_output(
|
54
|
+
start_new_agent.split(" "), stderr=subprocess.DEVNULL
|
55
|
+
)
|
56
|
+
logger.info(f":white_check_mark: {self.label} started successfully!")
|
57
|
+
return True
|
58
|
+
except subprocess.CalledProcessError as exception:
|
59
|
+
logger.error(f"Unable to start {self.label}")
|
60
|
+
logger.error(exception)
|
61
|
+
return False
|
62
|
+
|
63
|
+
def unload(self) -> bool:
|
64
|
+
try:
|
65
|
+
remove_existing_agent = f"launchctl unload -w {self.file_path}"
|
66
|
+
subprocess.check_output(
|
67
|
+
remove_existing_agent.split(" "), stderr=subprocess.DEVNULL
|
68
|
+
)
|
69
|
+
return True
|
70
|
+
except subprocess.CalledProcessError as exception:
|
71
|
+
logger.error(f"Unable to unload {self.label}")
|
72
|
+
logger.error(exception)
|
73
|
+
return False
|
74
|
+
|
75
|
+
def load(self) -> bool:
|
76
|
+
try:
|
77
|
+
load_new_plist = f"launchctl load -w {self.file_path}"
|
78
|
+
subprocess.check_output(
|
79
|
+
load_new_plist.split(" "), stderr=subprocess.DEVNULL
|
80
|
+
)
|
81
|
+
return True
|
82
|
+
except subprocess.CalledProcessError as exception:
|
83
|
+
logger.error(f"Unable to load {self.label}")
|
84
|
+
logger.error(exception)
|
85
|
+
return False
|
86
|
+
|
87
|
+
def verify(self) -> bool:
|
88
|
+
plutil_check = f"plutil -lint {self.file_path}"
|
89
|
+
try:
|
90
|
+
subprocess.check_output(plutil_check.split(" "), stderr=subprocess.DEVNULL)
|
91
|
+
return True
|
92
|
+
except subprocess.CalledProcessError as exception:
|
93
|
+
logger.error(f"Unable to verify {self.label}")
|
94
|
+
logger.error(exception)
|
89
95
|
return False
|
90
96
|
|
91
|
-
|
92
|
-
f"
|
97
|
+
def view_logs(self) -> None:
|
98
|
+
follow_logs = f"tail -f -n +1 {self.logs}"
|
99
|
+
os.system(follow_logs)
|
100
|
+
|
101
|
+
def populate(self) -> bool:
|
102
|
+
self.logs.parent.mkdir(parents=True, exist_ok=True)
|
103
|
+
self.logs.touch()
|
104
|
+
|
105
|
+
if self.file_path.exists():
|
106
|
+
self.file_path.unlink()
|
107
|
+
self.file_path.parent.mkdir(parents=True, exist_ok=True)
|
108
|
+
self.file_path.touch()
|
109
|
+
|
110
|
+
found_primitive_binary_path = PRIMITIVE_BINARY_PATH
|
111
|
+
if not PRIMITIVE_BINARY_PATH.exists():
|
112
|
+
result = subprocess.run(["which", "primitive"], capture_output=True)
|
113
|
+
if result.returncode == 0:
|
114
|
+
found_primitive_binary_path = result.stdout.decode().rstrip("\n")
|
115
|
+
else:
|
116
|
+
logger.error("primitive binary not found")
|
117
|
+
return False
|
118
|
+
|
119
|
+
self.file_path.write_text(
|
120
|
+
f"""<?xml version="1.0" encoding="UTF-8"?>
|
93
121
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
94
122
|
<plist version="1.0">
|
95
123
|
<dict>
|
96
124
|
<key>KeepAlive</key>
|
97
125
|
<true/>
|
98
126
|
<key>Label</key>
|
99
|
-
<string>{
|
127
|
+
<string>{self.label}</string>
|
100
128
|
<key>LimitLoadToSessionType</key>
|
101
129
|
<array>
|
102
130
|
<string>Aqua</string>
|
@@ -107,48 +135,103 @@ def populate_fresh_launch_agent():
|
|
107
135
|
<key>ProgramArguments</key>
|
108
136
|
<array>
|
109
137
|
<string>{found_primitive_binary_path}</string>
|
110
|
-
<string>
|
138
|
+
<string>{self.cmd}</string>
|
111
139
|
</array>
|
112
140
|
<key>RunAtLoad</key>
|
113
141
|
<true/>
|
114
142
|
<key>StandardErrorPath</key>
|
115
|
-
<string>{
|
143
|
+
<string>{self.logs}</string>
|
116
144
|
<key>StandardOutPath</key>
|
117
|
-
<string>{
|
145
|
+
<string>{self.logs}</string>
|
118
146
|
</dict>
|
119
|
-
</plist>"""
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
|
127
|
-
|
128
|
-
|
129
|
-
|
130
|
-
|
131
|
-
|
132
|
-
|
133
|
-
|
134
|
-
|
135
|
-
|
136
|
-
|
137
|
-
|
138
|
-
|
139
|
-
|
140
|
-
|
141
|
-
|
142
|
-
|
143
|
-
|
144
|
-
|
145
|
-
|
146
|
-
|
147
|
-
|
148
|
-
|
149
|
-
def
|
150
|
-
|
151
|
-
|
152
|
-
|
153
|
-
|
154
|
-
|
147
|
+
</plist>"""
|
148
|
+
)
|
149
|
+
self.file_path.chmod(0o644)
|
150
|
+
return self.verify()
|
151
|
+
|
152
|
+
def create_stdout_file(self) -> bool:
|
153
|
+
try:
|
154
|
+
if not self.logs.exists():
|
155
|
+
self.logs.parent.mkdir(parents=True, exist_ok=True)
|
156
|
+
self.logs.touch()
|
157
|
+
return True
|
158
|
+
except Exception as e:
|
159
|
+
logger.error(
|
160
|
+
f"Unable to create log file at {self.logs} for daemon {self.label}"
|
161
|
+
)
|
162
|
+
logger.error(e)
|
163
|
+
return False
|
164
|
+
|
165
|
+
def delete_stdout_file(self) -> bool:
|
166
|
+
try:
|
167
|
+
if self.logs.exists():
|
168
|
+
self.logs.unlink()
|
169
|
+
return True
|
170
|
+
except Exception as e:
|
171
|
+
logger.error(
|
172
|
+
f"Unable to delete log file at {self.logs} for daemon {self.label}"
|
173
|
+
)
|
174
|
+
logger.error(e)
|
175
|
+
return False
|
176
|
+
|
177
|
+
def delete_plist_file(self) -> bool:
|
178
|
+
try:
|
179
|
+
if self.file_path.exists():
|
180
|
+
self.file_path.unlink()
|
181
|
+
return True
|
182
|
+
except Exception as e:
|
183
|
+
logger.error(
|
184
|
+
f"Unable to delete log file at {self.logs} for daemon {self.label}"
|
185
|
+
)
|
186
|
+
logger.error(e)
|
187
|
+
return False
|
188
|
+
|
189
|
+
def install(self) -> bool:
|
190
|
+
return all(
|
191
|
+
[
|
192
|
+
self.stop(),
|
193
|
+
self.unload(),
|
194
|
+
self.populate(),
|
195
|
+
self.create_stdout_file(),
|
196
|
+
self.load(),
|
197
|
+
self.start(load=False),
|
198
|
+
]
|
199
|
+
)
|
200
|
+
|
201
|
+
def uninstall(self) -> bool:
|
202
|
+
return all(
|
203
|
+
[
|
204
|
+
self.stop(unload=False),
|
205
|
+
self.unload(),
|
206
|
+
self.delete_plist_file(),
|
207
|
+
self.delete_stdout_file(),
|
208
|
+
]
|
209
|
+
)
|
210
|
+
|
211
|
+
def is_active(self) -> bool:
|
212
|
+
if not self.is_installed():
|
213
|
+
return False
|
214
|
+
|
215
|
+
try:
|
216
|
+
is_service_active = f"launchctl list {self.label}" # noqa
|
217
|
+
output = (
|
218
|
+
subprocess.check_output(is_service_active.split(" ")).decode().strip()
|
219
|
+
)
|
220
|
+
return "PID" in output
|
221
|
+
except subprocess.CalledProcessError as exception:
|
222
|
+
logger.error(f"Unable to check if {self.label} active")
|
223
|
+
logger.error(exception)
|
224
|
+
return False
|
225
|
+
|
226
|
+
def is_installed(self) -> bool:
|
227
|
+
try:
|
228
|
+
is_service_active = f"launchctl list {self.label}" # noqa
|
229
|
+
subprocess.check_output(
|
230
|
+
is_service_active.split(" "), stderr=subprocess.DEVNULL
|
231
|
+
)
|
232
|
+
return True
|
233
|
+
except subprocess.CalledProcessError as exception:
|
234
|
+
if exception.returncode != 113:
|
235
|
+
logger.error(f"Unable to check if {self.label} enabled")
|
236
|
+
logger.error(exception)
|
237
|
+
return False
|