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.
@@ -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
- def install_daemon_command(context):
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
- result = primitive.daemons.install()
23
- print_result(message=result, context=context)
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
- def uninstall_daemon_command(context):
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
- result = primitive.daemons.uninstall()
32
- print_result(message=result, context=context)
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
- def stop_daemon_command(context):
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
- result = primitive.daemons.stop()
41
- message = "stopping primitive daemon"
42
- if context.obj["JSON"]:
43
- message = result
44
- print_result(message=message, context=context)
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
- def start_daemon_command(context):
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
- result = primitive.daemons.start()
53
- message = "starting primitive daemon"
54
- if context.obj["JSON"]:
55
- message = result
56
- print_result(message=message, context=context)
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
- def log_daemon_command(context):
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
- result = primitive.daemons.logs()
65
- print_result(message=result, context=context)
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
- PRIMITIVE_LAUNCH_AGENT_FILEPATH = Path(
11
- HOME_DIRECTORY / "Library" / "LaunchAgents" / "tech.primitive.agent.plist"
12
- )
13
- PRIMITIVE_LAUNCH_AGENT_LOGS = Path(
14
- HOME_DIRECTORY / "Library" / "Logs" / "tech.primitive.agent.log"
15
- )
16
- PRIMITIVE_LAUNCH_AGENT_LABEL = "tech.primitive.agent"
17
- PRIMITIVE_LAUNCH_AGENT_WORKING_DIR = Path(
18
- HOME_DIRECTORY / "Logs" / "tech.primitive.agent.log"
19
- )
20
-
21
-
22
- def stop_launch_agent():
23
- try:
24
- stop_existing_process = f"launchctl stop {PRIMITIVE_LAUNCH_AGENT_LABEL}"
25
- subprocess.check_output(stop_existing_process.split(" "))
26
- return True
27
- except subprocess.CalledProcessError as exception:
28
- print("stop_launch_agent: ", exception)
29
- return False
30
-
31
-
32
- def start_launch_agent():
33
- try:
34
- start_new_agent = f"launchctl start {PRIMITIVE_LAUNCH_AGENT_LABEL}"
35
- subprocess.check_output(start_new_agent.split(" "))
36
- return True
37
- except subprocess.CalledProcessError as exception:
38
- print("start_launch_agent: ", exception)
39
- return False
40
-
41
-
42
- def unload_launch_agent():
43
- try:
44
- remove_existing_agent = f"launchctl unload -w {PRIMITIVE_LAUNCH_AGENT_FILEPATH}"
45
- subprocess.check_output(remove_existing_agent.split(" "))
46
- return True
47
- except subprocess.CalledProcessError as exception:
48
- print("remove_launch_agent: ", exception)
49
- return False
50
-
51
-
52
- def load_launch_agent():
53
- try:
54
- load_new_plist = f"launchctl load -w {PRIMITIVE_LAUNCH_AGENT_FILEPATH}"
55
- subprocess.check_output(load_new_plist.split(" "))
56
- return True
57
- except subprocess.CalledProcessError as exception:
58
- print("load_launch_agent: ", exception)
59
- return False
60
-
61
-
62
- def create_stdout_file():
63
- if not PRIMITIVE_LAUNCH_AGENT_LOGS.exists():
64
- PRIMITIVE_LAUNCH_AGENT_LOGS.parent.mkdir(parents=True, exist_ok=True)
65
- PRIMITIVE_LAUNCH_AGENT_LOGS.touch()
66
-
67
-
68
- def delete_stdout_file():
69
- if PRIMITIVE_LAUNCH_AGENT_LOGS.exists():
70
- PRIMITIVE_LAUNCH_AGENT_LOGS.unlink()
71
-
72
-
73
- def populate_fresh_launch_agent():
74
- PRIMITIVE_LAUNCH_AGENT_LOGS.parent.mkdir(parents=True, exist_ok=True)
75
- PRIMITIVE_LAUNCH_AGENT_LOGS.touch()
76
-
77
- if PRIMITIVE_LAUNCH_AGENT_FILEPATH.exists():
78
- PRIMITIVE_LAUNCH_AGENT_FILEPATH.unlink()
79
- PRIMITIVE_LAUNCH_AGENT_FILEPATH.parent.mkdir(parents=True, exist_ok=True)
80
- PRIMITIVE_LAUNCH_AGENT_FILEPATH.touch()
81
-
82
- found_primitive_binary_path = PRIMITIVE_BINARY_PATH
83
- if not PRIMITIVE_BINARY_PATH.exists():
84
- result = subprocess.run(["which", "primitive"], capture_output=True)
85
- if result.returncode == 0:
86
- found_primitive_binary_path = result.stdout.decode().rstrip("\n")
87
- else:
88
- print("primitive binary not found")
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
- PRIMITIVE_LAUNCH_AGENT_FILEPATH.write_text(
92
- f"""<?xml version="1.0" encoding="UTF-8"?>
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>{PRIMITIVE_LAUNCH_AGENT_LABEL}</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>agent</string>
138
+ <string>{self.cmd}</string>
111
139
  </array>
112
140
  <key>RunAtLoad</key>
113
141
  <true/>
114
142
  <key>StandardErrorPath</key>
115
- <string>{PRIMITIVE_LAUNCH_AGENT_LOGS}</string>
143
+ <string>{self.logs}</string>
116
144
  <key>StandardOutPath</key>
117
- <string>{PRIMITIVE_LAUNCH_AGENT_LOGS}</string>
145
+ <string>{self.logs}</string>
118
146
  </dict>
119
- </plist>""" # noqa: E501
120
- )
121
- PRIMITIVE_LAUNCH_AGENT_FILEPATH.chmod(0o644)
122
- verify_launch_agent()
123
-
124
-
125
- def verify_launch_agent():
126
- plutil_check = f"plutil -lint {PRIMITIVE_LAUNCH_AGENT_FILEPATH}"
127
- try:
128
- subprocess.check_output(plutil_check.split(" "))
129
- return True
130
- except subprocess.CalledProcessError as exception:
131
- print("verify_launch_agent: ", exception)
132
- return False
133
-
134
-
135
- def view_launch_agent_logs():
136
- follow_logs = f"tail -f -n +1 {PRIMITIVE_LAUNCH_AGENT_LOGS}"
137
- os.system(follow_logs)
138
-
139
-
140
- def full_launch_agent_install():
141
- stop_launch_agent()
142
- unload_launch_agent()
143
- populate_fresh_launch_agent()
144
- create_stdout_file()
145
- load_launch_agent()
146
- start_launch_agent()
147
-
148
-
149
- def full_launch_agent_uninstall():
150
- stop_launch_agent()
151
- unload_launch_agent()
152
- if PRIMITIVE_LAUNCH_AGENT_FILEPATH.exists():
153
- PRIMITIVE_LAUNCH_AGENT_FILEPATH.unlink()
154
- delete_stdout_file()
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