primitive 0.2.11__tar.gz → 0.2.13__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.
Files changed (115) hide show
  1. {primitive-0.2.11 → primitive-0.2.13}/.vscode/settings.json +4 -3
  2. {primitive-0.2.11 → primitive-0.2.13}/PKG-INFO +1 -1
  3. {primitive-0.2.11 → primitive-0.2.13}/src/primitive/__about__.py +1 -1
  4. primitive-0.2.13/src/primitive/agent/actions.py +110 -0
  5. {primitive-0.2.11 → primitive-0.2.13}/src/primitive/agent/commands.py +2 -1
  6. {primitive-0.2.11 → primitive-0.2.13}/src/primitive/agent/runner.py +14 -2
  7. {primitive-0.2.11 → primitive-0.2.13}/src/primitive/agent/uploader.py +2 -2
  8. {primitive-0.2.11 → primitive-0.2.13}/src/primitive/daemons/actions.py +37 -6
  9. {primitive-0.2.11 → primitive-0.2.13}/src/primitive/daemons/launch_agents.py +8 -18
  10. {primitive-0.2.11 → primitive-0.2.13}/src/primitive/daemons/launch_service.py +6 -13
  11. {primitive-0.2.11 → primitive-0.2.13}/src/primitive/db/models.py +15 -5
  12. {primitive-0.2.11 → primitive-0.2.13}/src/primitive/db/sqlite.py +9 -2
  13. {primitive-0.2.11 → primitive-0.2.13}/src/primitive/hardware/actions.py +7 -2
  14. {primitive-0.2.11/src/primitive/agent → primitive-0.2.13/src/primitive/monitor}/actions.py +110 -81
  15. primitive-0.2.11/src/primitive/monitor/actions.py +0 -102
  16. {primitive-0.2.11 → primitive-0.2.13}/.git-hooks/pre-commit +0 -0
  17. {primitive-0.2.11 → primitive-0.2.13}/.gitattributes +0 -0
  18. {primitive-0.2.11 → primitive-0.2.13}/.github/workflows/lint.yml +0 -0
  19. {primitive-0.2.11 → primitive-0.2.13}/.github/workflows/publish.yml +0 -0
  20. {primitive-0.2.11 → primitive-0.2.13}/.github/workflows/pyright.yml +0 -0
  21. {primitive-0.2.11 → primitive-0.2.13}/.gitignore +0 -0
  22. {primitive-0.2.11 → primitive-0.2.13}/.vscode/extensions.json +0 -0
  23. {primitive-0.2.11 → primitive-0.2.13}/LICENSE.txt +0 -0
  24. {primitive-0.2.11 → primitive-0.2.13}/Makefile +0 -0
  25. {primitive-0.2.11 → primitive-0.2.13}/README.md +0 -0
  26. {primitive-0.2.11 → primitive-0.2.13}/linux setup.md +0 -0
  27. {primitive-0.2.11 → primitive-0.2.13}/pyproject.toml +0 -0
  28. {primitive-0.2.11 → primitive-0.2.13}/src/primitive/__init__.py +0 -0
  29. {primitive-0.2.11 → primitive-0.2.13}/src/primitive/agent/__init__.py +0 -0
  30. {primitive-0.2.11 → primitive-0.2.13}/src/primitive/auth/__init__.py +0 -0
  31. {primitive-0.2.11 → primitive-0.2.13}/src/primitive/auth/actions.py +0 -0
  32. {primitive-0.2.11 → primitive-0.2.13}/src/primitive/auth/commands.py +0 -0
  33. {primitive-0.2.11 → primitive-0.2.13}/src/primitive/auth/graphql/__init__.py +0 -0
  34. {primitive-0.2.11 → primitive-0.2.13}/src/primitive/auth/graphql/queries.py +0 -0
  35. {primitive-0.2.11 → primitive-0.2.13}/src/primitive/cli.py +0 -0
  36. {primitive-0.2.11 → primitive-0.2.13}/src/primitive/client.py +0 -0
  37. {primitive-0.2.11 → primitive-0.2.13}/src/primitive/daemons/__init__.py +0 -0
  38. {primitive-0.2.11 → primitive-0.2.13}/src/primitive/daemons/commands.py +0 -0
  39. {primitive-0.2.11 → primitive-0.2.13}/src/primitive/daemons/ui.py +0 -0
  40. {primitive-0.2.11 → primitive-0.2.13}/src/primitive/db/base.py +0 -0
  41. {primitive-0.2.11 → primitive-0.2.13}/src/primitive/exec/__init__.py +0 -0
  42. {primitive-0.2.11 → primitive-0.2.13}/src/primitive/exec/actions.py +0 -0
  43. {primitive-0.2.11 → primitive-0.2.13}/src/primitive/exec/commands.py +0 -0
  44. {primitive-0.2.11 → primitive-0.2.13}/src/primitive/exec/interactive.py +0 -0
  45. {primitive-0.2.11 → primitive-0.2.13}/src/primitive/files/__init__.py +0 -0
  46. {primitive-0.2.11 → primitive-0.2.13}/src/primitive/files/actions.py +0 -0
  47. {primitive-0.2.11 → primitive-0.2.13}/src/primitive/files/commands.py +0 -0
  48. {primitive-0.2.11 → primitive-0.2.13}/src/primitive/files/graphql/__init__.py +0 -0
  49. {primitive-0.2.11 → primitive-0.2.13}/src/primitive/files/graphql/fragments.py +0 -0
  50. {primitive-0.2.11 → primitive-0.2.13}/src/primitive/files/graphql/mutations.py +0 -0
  51. {primitive-0.2.11 → primitive-0.2.13}/src/primitive/files/graphql/queries.py +0 -0
  52. {primitive-0.2.11 → primitive-0.2.13}/src/primitive/git/__init__.py +0 -0
  53. {primitive-0.2.11 → primitive-0.2.13}/src/primitive/git/actions.py +0 -0
  54. {primitive-0.2.11 → primitive-0.2.13}/src/primitive/git/commands.py +0 -0
  55. {primitive-0.2.11 → primitive-0.2.13}/src/primitive/git/graphql/__init__.py +0 -0
  56. {primitive-0.2.11 → primitive-0.2.13}/src/primitive/git/graphql/queries.py +0 -0
  57. {primitive-0.2.11 → primitive-0.2.13}/src/primitive/graphql/__init__.py +0 -0
  58. {primitive-0.2.11 → primitive-0.2.13}/src/primitive/graphql/relay.py +0 -0
  59. {primitive-0.2.11 → primitive-0.2.13}/src/primitive/graphql/sdk.py +0 -0
  60. {primitive-0.2.11 → primitive-0.2.13}/src/primitive/graphql/utility_fragments.py +0 -0
  61. {primitive-0.2.11 → primitive-0.2.13}/src/primitive/hardware/__init__.py +0 -0
  62. {primitive-0.2.11 → primitive-0.2.13}/src/primitive/hardware/android.py +0 -0
  63. {primitive-0.2.11 → primitive-0.2.13}/src/primitive/hardware/commands.py +0 -0
  64. {primitive-0.2.11 → primitive-0.2.13}/src/primitive/hardware/graphql/__init__.py +0 -0
  65. {primitive-0.2.11 → primitive-0.2.13}/src/primitive/hardware/graphql/fragments.py +0 -0
  66. {primitive-0.2.11 → primitive-0.2.13}/src/primitive/hardware/graphql/mutations.py +0 -0
  67. {primitive-0.2.11 → primitive-0.2.13}/src/primitive/hardware/graphql/queries.py +0 -0
  68. {primitive-0.2.11 → primitive-0.2.13}/src/primitive/hardware/ui.py +0 -0
  69. {primitive-0.2.11 → primitive-0.2.13}/src/primitive/jobs/__init__.py +0 -0
  70. {primitive-0.2.11 → primitive-0.2.13}/src/primitive/jobs/actions.py +0 -0
  71. {primitive-0.2.11 → primitive-0.2.13}/src/primitive/jobs/commands.py +0 -0
  72. {primitive-0.2.11 → primitive-0.2.13}/src/primitive/jobs/graphql/__init__.py +0 -0
  73. {primitive-0.2.11 → primitive-0.2.13}/src/primitive/jobs/graphql/fragments.py +0 -0
  74. {primitive-0.2.11 → primitive-0.2.13}/src/primitive/jobs/graphql/mutations.py +0 -0
  75. {primitive-0.2.11 → primitive-0.2.13}/src/primitive/jobs/graphql/queries.py +0 -0
  76. {primitive-0.2.11 → primitive-0.2.13}/src/primitive/monitor/commands.py +0 -0
  77. {primitive-0.2.11 → primitive-0.2.13}/src/primitive/organizations/__init__.py +0 -0
  78. {primitive-0.2.11 → primitive-0.2.13}/src/primitive/organizations/actions.py +0 -0
  79. {primitive-0.2.11 → primitive-0.2.13}/src/primitive/organizations/commands.py +0 -0
  80. {primitive-0.2.11 → primitive-0.2.13}/src/primitive/organizations/graphql/__init__.py +0 -0
  81. {primitive-0.2.11 → primitive-0.2.13}/src/primitive/organizations/graphql/fragments.py +0 -0
  82. {primitive-0.2.11 → primitive-0.2.13}/src/primitive/organizations/graphql/mutations.py +0 -0
  83. {primitive-0.2.11 → primitive-0.2.13}/src/primitive/organizations/graphql/queries.py +0 -0
  84. {primitive-0.2.11 → primitive-0.2.13}/src/primitive/projects/__init__.py +0 -0
  85. {primitive-0.2.11 → primitive-0.2.13}/src/primitive/projects/actions.py +0 -0
  86. {primitive-0.2.11 → primitive-0.2.13}/src/primitive/projects/commands.py +0 -0
  87. {primitive-0.2.11 → primitive-0.2.13}/src/primitive/projects/graphql/__init__.py +0 -0
  88. {primitive-0.2.11 → primitive-0.2.13}/src/primitive/projects/graphql/fragments.py +0 -0
  89. {primitive-0.2.11 → primitive-0.2.13}/src/primitive/projects/graphql/mutations.py +0 -0
  90. {primitive-0.2.11 → primitive-0.2.13}/src/primitive/projects/graphql/queries.py +0 -0
  91. {primitive-0.2.11 → primitive-0.2.13}/src/primitive/provisioning/__init__.py +0 -0
  92. {primitive-0.2.11 → primitive-0.2.13}/src/primitive/provisioning/actions.py +0 -0
  93. {primitive-0.2.11 → primitive-0.2.13}/src/primitive/provisioning/graphql/__init__.py +0 -0
  94. {primitive-0.2.11 → primitive-0.2.13}/src/primitive/provisioning/graphql/queries.py +0 -0
  95. {primitive-0.2.11 → primitive-0.2.13}/src/primitive/reservations/__init__.py +0 -0
  96. {primitive-0.2.11 → primitive-0.2.13}/src/primitive/reservations/actions.py +0 -0
  97. {primitive-0.2.11 → primitive-0.2.13}/src/primitive/reservations/commands.py +0 -0
  98. {primitive-0.2.11 → primitive-0.2.13}/src/primitive/reservations/graphql/__init__.py +0 -0
  99. {primitive-0.2.11 → primitive-0.2.13}/src/primitive/reservations/graphql/fragments.py +0 -0
  100. {primitive-0.2.11 → primitive-0.2.13}/src/primitive/reservations/graphql/mutations.py +0 -0
  101. {primitive-0.2.11 → primitive-0.2.13}/src/primitive/reservations/graphql/queries.py +0 -0
  102. {primitive-0.2.11 → primitive-0.2.13}/src/primitive/utils/__init__.py +0 -0
  103. {primitive-0.2.11 → primitive-0.2.13}/src/primitive/utils/actions.py +0 -0
  104. {primitive-0.2.11 → primitive-0.2.13}/src/primitive/utils/auth.py +0 -0
  105. {primitive-0.2.11 → primitive-0.2.13}/src/primitive/utils/cache.py +0 -0
  106. {primitive-0.2.11 → primitive-0.2.13}/src/primitive/utils/chunk_size.py +0 -0
  107. {primitive-0.2.11 → primitive-0.2.13}/src/primitive/utils/config.py +0 -0
  108. {primitive-0.2.11 → primitive-0.2.13}/src/primitive/utils/daemons.py +1 -1
  109. {primitive-0.2.11 → primitive-0.2.13}/src/primitive/utils/exceptions.py +0 -0
  110. {primitive-0.2.11 → primitive-0.2.13}/src/primitive/utils/memory_size.py +0 -0
  111. {primitive-0.2.11 → primitive-0.2.13}/src/primitive/utils/printer.py +0 -0
  112. {primitive-0.2.11 → primitive-0.2.13}/src/primitive/utils/shell.py +0 -0
  113. {primitive-0.2.11 → primitive-0.2.13}/src/primitive/utils/text.py +0 -0
  114. {primitive-0.2.11 → primitive-0.2.13}/tests/__init__.py +0 -0
  115. {primitive-0.2.11 → primitive-0.2.13}/uv.lock +0 -0
@@ -6,14 +6,15 @@
6
6
  "Mbit",
7
7
  "mbps",
8
8
  "pkey",
9
+ "plutil",
10
+ "procs",
11
+ "sessionmaker",
9
12
  "testsuites",
10
13
  "untar",
11
14
  "Untaring",
12
15
  "VERIBLE",
13
16
  "verilog",
14
- "workdir",
15
- "plutil",
16
- "procs"
17
+ "workdir"
17
18
  ],
18
19
  "python.languageServer": "Pylance",
19
20
  "[python]": {
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: primitive
3
- Version: 0.2.11
3
+ Version: 0.2.13
4
4
  Project-URL: Documentation, https://github.com//primitivecorp/primitive-cli#readme
5
5
  Project-URL: Issues, https://github.com//primitivecorp/primitive-cli/issues
6
6
  Project-URL: Source, https://github.com//primitivecorp/primitive-cli
@@ -1,4 +1,4 @@
1
1
  # SPDX-FileCopyrightText: 2024-present Dylan Stein <dylan@primitive.tech>
2
2
  #
3
3
  # SPDX-License-Identifier: MIT
4
- __version__ = "0.2.11"
4
+ __version__ = "0.2.13"
@@ -0,0 +1,110 @@
1
+ import sys
2
+ from time import sleep
3
+
4
+ from loguru import logger
5
+
6
+ from primitive.__about__ import __version__
7
+ from primitive.utils.actions import BaseAction
8
+
9
+ from ..db import sqlite
10
+ from ..db.models import JobRun
11
+ from .runner import Runner
12
+ from .uploader import Uploader
13
+
14
+
15
+ class Agent(BaseAction):
16
+ def execute(
17
+ self,
18
+ ):
19
+ logger.remove()
20
+ logger.add(
21
+ sink=sys.stderr,
22
+ format="<green>{time:YYYY-MM-DD HH:mm:ss.SSS}</green> | <level>{level: <8}</level> | <level>{message}</level>",
23
+ backtrace=True,
24
+ diagnose=True,
25
+ level="DEBUG" if self.primitive.DEBUG else "INFO",
26
+ )
27
+ logger.info("[*] primitive agent")
28
+ logger.info(f"[*] Version: {__version__}")
29
+
30
+ # Initialize the database
31
+ sqlite.init()
32
+
33
+ # Create uploader
34
+ uploader = Uploader(primitive=self.primitive)
35
+
36
+ try:
37
+ while True:
38
+ logger.debug("Scanning for files to upload...")
39
+ uploader.scan()
40
+
41
+ db_job_run = JobRun.objects.first()
42
+
43
+ if not db_job_run:
44
+ sleep_amount = 5
45
+ logger.debug(
46
+ f"No pending job runs... [sleeping {sleep_amount} seconds]"
47
+ )
48
+ sleep(sleep_amount)
49
+ continue
50
+
51
+ api_job_run_data = self.primitive.jobs.get_job_run(
52
+ id=db_job_run.job_run_id,
53
+ )
54
+
55
+ if not api_job_run_data or not api_job_run_data.data:
56
+ logger.error(
57
+ f"Job Run {db_job_run.job_run_id} not found in API, deleting from DB"
58
+ )
59
+ JobRun.objects.filter_by(job_run_id=db_job_run.job_run_id).delete()
60
+ continue
61
+
62
+ api_job_run = api_job_run_data.data["jobRun"]
63
+
64
+ logger.debug("Found pending Job Run")
65
+ logger.debug(f"Job Run ID: {api_job_run.get('id')}")
66
+ logger.debug(f"Job Name: {api_job_run.get('name')}")
67
+
68
+ runner = Runner(
69
+ primitive=self.primitive,
70
+ job_run=api_job_run,
71
+ # max_log_size=500 * 1024,
72
+ )
73
+
74
+ try:
75
+ runner.setup()
76
+ except Exception as exception:
77
+ logger.exception(
78
+ f"Exception while initializing runner: {exception}"
79
+ )
80
+ self.primitive.jobs.job_run_update(
81
+ id=api_job_run.get("id"),
82
+ status="request_completed",
83
+ conclusion="failure",
84
+ )
85
+ JobRun.objects.filter_by(job_run_id=api_job_run.get("id")).delete()
86
+ continue
87
+
88
+ try:
89
+ runner.execute()
90
+ except Exception as exception:
91
+ logger.exception(f"Exception while executing job: {exception}")
92
+ self.primitive.jobs.job_run_update(
93
+ id=api_job_run.get("id"),
94
+ status="request_completed",
95
+ conclusion="failure",
96
+ )
97
+ finally:
98
+ runner.cleanup()
99
+
100
+ # NOTE: also run scan here to force upload of artifacts
101
+ # This should probably eventually be another daemon?
102
+ uploader.scan()
103
+
104
+ JobRun.objects.filter_by(
105
+ job_run_id=api_job_run.get("id"),
106
+ ).delete()
107
+
108
+ sleep(5)
109
+ except KeyboardInterrupt:
110
+ logger.info("[*] Stopping primitive agent...")
@@ -1,6 +1,7 @@
1
- import click
2
1
  import typing
3
2
 
3
+ import click
4
+
4
5
  if typing.TYPE_CHECKING:
5
6
  from ..client import Primitive
6
7
 
@@ -7,11 +7,11 @@ from abc import abstractmethod
7
7
  from enum import Enum, IntEnum
8
8
  from pathlib import Path, PurePath
9
9
  from typing import Dict, List, TypedDict
10
- from ..db.models import JobRun
11
10
 
12
11
  import yaml
13
12
  from loguru import logger
14
13
 
14
+ from ..db.models import JobRun
15
15
  from ..utils.cache import get_artifacts_cache, get_logs_cache, get_sources_cache
16
16
  from ..utils.shell import env_to_dict
17
17
 
@@ -157,8 +157,11 @@ class Runner:
157
157
 
158
158
  task_failed = False
159
159
  cancelled = False
160
- conclusion = "success"
160
+
161
161
  for task in self.config["executes"]:
162
+ # the get status check here is to ensure that if cancel is called
163
+ # while one task is running, we do not run any OTHER laebeled tasks
164
+ # THIS is required for MULTI STEP JOBS
162
165
  status = self.primitive.jobs.get_job_status(self.job_run["id"])
163
166
  status_value = status.data["jobRun"]["status"]
164
167
  conclusion_value = status.data["jobRun"]["conclusion"]
@@ -177,6 +180,14 @@ class Runner:
177
180
  f"Produced {number_of_files_produced} files for {self.job['slug']} job"
178
181
  )
179
182
 
183
+ # FOR NONE MULTI STEP JOBS
184
+ # we still have to check that the job was cancelled here as well
185
+ status = self.primitive.jobs.get_job_status(self.job_run["id"])
186
+ status_value = status.data["jobRun"]["status"]
187
+ conclusion_value = status.data["jobRun"]["conclusion"]
188
+ if status_value == "completed" and conclusion_value == "cancelled":
189
+ cancelled = True
190
+
180
191
  if cancelled:
181
192
  logger.warning("Job cancelled by user")
182
193
  self.primitive.jobs.job_run_update(
@@ -185,6 +196,7 @@ class Runner:
185
196
  )
186
197
  return
187
198
 
199
+ conclusion = "success"
188
200
  if task_failed:
189
201
  conclusion = "failure"
190
202
  else:
@@ -50,8 +50,8 @@ class Uploader:
50
50
  path=file,
51
51
  key_prefix=str(PurePath(file).relative_to(cache.parent).parent),
52
52
  )
53
- except Exception as e:
54
- if "is empty" in str(e):
53
+ except Exception as exception:
54
+ if "is empty" in str(exception):
55
55
  logger.warning(f"{file} is empty, skipping upload")
56
56
  continue
57
57
 
@@ -1,13 +1,18 @@
1
1
  import platform
2
+ import subprocess
2
3
  import typing
3
- from typing import Dict, Optional, List
4
+ from pathlib import Path
5
+ from typing import Dict, List, Optional
4
6
 
5
7
  if typing.TYPE_CHECKING:
6
8
  from ..client import Primitive
7
9
 
10
+ from ..utils.daemons import Daemon
8
11
  from .launch_agents import LaunchAgent
9
12
  from .launch_service import LaunchService
10
- from ..utils.daemons import Daemon
13
+
14
+ HOME_DIRECTORY = Path.home()
15
+ PRIMITIVE_BINARY_PATH = Path(HOME_DIRECTORY / ".pyenv" / "shims" / "primitive")
11
16
 
12
17
 
13
18
  class Daemons:
@@ -15,16 +20,42 @@ class Daemons:
15
20
  self.primitive: Primitive = primitive
16
21
  self.os_family = platform.system()
17
22
 
23
+ found_primitive_binary_path = PRIMITIVE_BINARY_PATH
24
+ if not PRIMITIVE_BINARY_PATH.exists():
25
+ result = subprocess.run(["which", "primitive"], capture_output=True)
26
+ if result.returncode == 0:
27
+ found_primitive_binary_path = result.stdout.decode().rstrip("\n")
28
+ else:
29
+ raise Exception(
30
+ f"primitive binary not found at {PRIMITIVE_BINARY_PATH}"
31
+ )
32
+
33
+ base_primitive_command = f'/bin/sh -lc "{found_primitive_binary_path} "'
34
+
18
35
  match self.os_family:
19
36
  case "Darwin":
20
37
  self.daemons: Dict[str, Daemon] = {
21
- "agent": LaunchAgent("tech.primitive.agent"),
22
- "monitor": LaunchAgent("tech.primitive.monitor"),
38
+ "agent": LaunchAgent(
39
+ "tech.primitive.agent",
40
+ executable=str(found_primitive_binary_path),
41
+ command="agent --debug",
42
+ ),
43
+ "monitor": LaunchAgent(
44
+ "tech.primitive.monitor",
45
+ executable=str(found_primitive_binary_path),
46
+ command="monitor --debug",
47
+ ),
23
48
  }
24
49
  case "Linux":
25
50
  self.daemons: Dict[str, Daemon] = {
26
- "agent": LaunchService("tech.primitive.agent"),
27
- "monitor": LaunchService("tech.primitive.monitor"),
51
+ "agent": LaunchService(
52
+ "tech.primitive.agent",
53
+ command=f"{base_primitive_command} agent --debug",
54
+ ),
55
+ "monitor": LaunchService(
56
+ "tech.primitive.monitor",
57
+ command=f"{base_primitive_command} monitor --debug",
58
+ ),
28
59
  }
29
60
  case _:
30
61
  raise NotImplementedError(f"{self.os_family} is not supported.")
@@ -1,18 +1,21 @@
1
1
  import os
2
- from pathlib import Path
3
2
  import subprocess
3
+ from pathlib import Path
4
+
4
5
  from loguru import logger
6
+
5
7
  from ..utils.daemons import Daemon
6
8
 
7
9
  HOME_DIRECTORY = Path.home()
8
10
  CURRENT_USER = str(HOME_DIRECTORY.expanduser()).lstrip("/Users/")
9
- PRIMITIVE_BINARY_PATH = Path(HOME_DIRECTORY / ".pyenv" / "shims" / "primitive")
10
11
 
11
12
 
12
13
  class LaunchAgent(Daemon):
13
- def __init__(self, label: str):
14
+ def __init__(self, label: str, executable: str, command: str):
14
15
  self.label = label
15
16
  self.name = label.split(".")[-1]
17
+ self.executable = executable
18
+ self.command = command
16
19
 
17
20
  @property
18
21
  def file_path(self) -> Path:
@@ -22,10 +25,6 @@ class LaunchAgent(Daemon):
22
25
  def logs(self) -> Path:
23
26
  return Path(HOME_DIRECTORY / "Library" / "Logs" / f"{self.label}.log")
24
27
 
25
- @property
26
- def cmd(self) -> str:
27
- return self.label.split(".")[-1]
28
-
29
28
  def stop(self, unload: bool = True) -> bool:
30
29
  try:
31
30
  stop_existing_process = f"launchctl stop {self.label}"
@@ -107,15 +106,6 @@ class LaunchAgent(Daemon):
107
106
  self.file_path.parent.mkdir(parents=True, exist_ok=True)
108
107
  self.file_path.touch()
109
108
 
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
109
  self.file_path.write_text(
120
110
  f"""<?xml version="1.0" encoding="UTF-8"?>
121
111
  <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
@@ -134,8 +124,8 @@ class LaunchAgent(Daemon):
134
124
  </array>
135
125
  <key>ProgramArguments</key>
136
126
  <array>
137
- <string>{found_primitive_binary_path}</string>
138
- <string>{self.cmd}</string>
127
+ <string>{self.executable}</string>
128
+ <string>{self.command}</string>
139
129
  </array>
140
130
  <key>RunAtLoad</key>
141
131
  <true/>
@@ -1,18 +1,20 @@
1
- import os
2
1
  import configparser
2
+ import os
3
3
  import subprocess
4
4
  from pathlib import Path
5
+
5
6
  from loguru import logger
7
+
6
8
  from ..utils.daemons import Daemon
7
9
 
8
10
  HOME_DIRECTORY = Path.home()
9
- PRIMITIVE_BINARY_PATH = Path(HOME_DIRECTORY / ".pyenv" / "shims" / "primitive")
10
11
 
11
12
 
12
13
  class LaunchService(Daemon):
13
- def __init__(self, label: str):
14
+ def __init__(self, label: str, command: str):
14
15
  self.label = label
15
16
  self.name = label.split(".")[-1]
17
+ self.command = command
16
18
 
17
19
  @property
18
20
  def service_name(self) -> str:
@@ -119,17 +121,8 @@ class LaunchService(Daemon):
119
121
  "After": "network.target",
120
122
  }
121
123
 
122
- found_primitive_binary_path = PRIMITIVE_BINARY_PATH
123
- if not PRIMITIVE_BINARY_PATH.exists():
124
- result = subprocess.run(["which", "primitive"], capture_output=True)
125
- if result.returncode == 0:
126
- found_primitive_binary_path = result.stdout.decode().rstrip("\n")
127
- else:
128
- print("primitive binary not found")
129
- return False
130
-
131
124
  config["Service"] = {
132
- "ExecStart": f'/bin/sh -lc "{found_primitive_binary_path} agent"',
125
+ "ExecStart": self.command,
133
126
  "Restart": "always",
134
127
  "StandardError": f"append:{self.logs}",
135
128
  "StandardOutput": f"append:{self.logs}",
@@ -1,8 +1,10 @@
1
+ from typing import Any, Callable, Dict, Generic, List, Optional, Type, TypeVar, Union
2
+
1
3
  from sqlalchemy import Column, Integer, String
2
- from sqlalchemy.orm import Mapped, mapped_column, Query
3
- from typing import Optional, Generic, TypeVar, Callable, Type, List, Union, Dict, Any
4
- from .sqlite import Session
4
+ from sqlalchemy.orm import Mapped, Query, mapped_column
5
+
5
6
  from .base import Base
7
+ from .sqlite import Session
6
8
 
7
9
  T = TypeVar("T", bound="Base")
8
10
 
@@ -25,6 +27,14 @@ class Manager(Generic[T]):
25
27
  self.filters = kwargs
26
28
  return self
27
29
 
30
+ def exists(self) -> bool:
31
+ with Session() as session:
32
+ model = self.model_cls_lambda()
33
+ query = session.query(model)
34
+ query.filter_by(**self.filters)
35
+ self.filters.clear()
36
+ return query.count() > 0
37
+
28
38
  def all(self) -> List[T]:
29
39
  with Session() as session:
30
40
  model = self.model_cls_lambda()
@@ -51,7 +61,7 @@ class Manager(Generic[T]):
51
61
  session.commit()
52
62
  return query
53
63
  else:
54
- raise ValueError(f"{model.__name__} not found")
64
+ raise ValueError(f"Update failed, {model.__name__} not found")
55
65
 
56
66
  def delete(self) -> None:
57
67
  with Session() as session:
@@ -62,7 +72,7 @@ class Manager(Generic[T]):
62
72
  query.delete()
63
73
  session.commit()
64
74
  else:
65
- raise ValueError(f"{model.__name__} not found")
75
+ raise ValueError(f"Delete failed, {model.__name__} not found")
66
76
 
67
77
 
68
78
  class JobRun(Base):
@@ -1,6 +1,9 @@
1
1
  from pathlib import Path
2
- from sqlalchemy import create_engine, Engine
2
+
3
+ from loguru import logger
4
+ from sqlalchemy import Engine, create_engine
3
5
  from sqlalchemy.orm import Session as SQLAlchemySession
6
+
4
7
  from ..utils.cache import get_cache_dir
5
8
  from .base import Base
6
9
 
@@ -9,9 +12,13 @@ def init() -> None:
9
12
  db_path: Path = get_cache_dir() / "primitive.sqlite3"
10
13
 
11
14
  # Drop DB existing database if it exists
15
+ # if db_path.exists():
16
+ # logger.warning(f"[*] Deleting existing SQLite database at {db_path}")
17
+ # db_path.unlink()
12
18
  if db_path.exists():
13
- db_path.unlink()
19
+ return
14
20
 
21
+ logger.info(f"[*] Initializing SQLite database at {db_path}")
15
22
  engine = create_engine(f"sqlite:///{db_path}", echo=False)
16
23
  Base.metadata.create_all(engine)
17
24
 
@@ -592,12 +592,17 @@ class Hardware(BaseAction):
592
592
  pass
593
593
 
594
594
  @guard
595
- def _sync_children(self):
595
+ def _sync_children(self, hardware: Optional[Dict[str, str]] = None):
596
596
  # get the existing children if any from the hardware details
597
597
  # get the latest children from the node
598
598
  # compare the two and update the node with the latest children
599
599
  # remove any children from remote that are not in the latest children
600
- hardware = self.primitive.hardware.get_own_hardware_details()
600
+ if not hardware:
601
+ hardware = self.primitive.hardware.get_own_hardware_details()
602
+ if not hardware:
603
+ logger.error("No hardware found.")
604
+ return
605
+
601
606
  remote_children = hardware.get("children", [])
602
607
  local_children = self.primitive.hardware._list_local_children()
603
608
 
@@ -1,21 +1,18 @@
1
1
  import sys
2
2
  from time import sleep
3
3
 
4
+ import psutil
4
5
  from loguru import logger
5
6
 
6
7
  from primitive.__about__ import __version__
8
+ from primitive.db import sqlite
9
+ from primitive.db.models import JobRun
7
10
  from primitive.utils.actions import BaseAction
11
+ from primitive.utils.exceptions import P_CLI_100
8
12
 
9
- from .runner import Runner
10
- from .uploader import Uploader
11
- from ..db import sqlite
12
- from ..db.models import JobRun
13
13
 
14
-
15
- class Agent(BaseAction):
16
- def execute(
17
- self,
18
- ):
14
+ class Monitor(BaseAction):
15
+ def start(self):
19
16
  logger.remove()
20
17
  logger.add(
21
18
  sink=sys.stderr,
@@ -24,16 +21,9 @@ class Agent(BaseAction):
24
21
  diagnose=True,
25
22
  level="DEBUG" if self.primitive.DEBUG else "INFO",
26
23
  )
27
- logger.info("[*] primitive agent")
24
+ logger.info("[*] primitive monitor")
28
25
  logger.info(f"[*] Version: {__version__}")
29
26
 
30
- # Initialize the database
31
- sqlite.init()
32
-
33
- # Create uploader
34
- uploader = Uploader(primitive=self.primitive)
35
-
36
- # self.primitive.hardware.update_hardware_system_info()
37
27
  try:
38
28
  # hey stupid:
39
29
  # do not set is_available to True here, it will mess up the reservation logic
@@ -45,22 +35,74 @@ class Agent(BaseAction):
45
35
  logger.exception(f"Error checking in hardware: {exception}")
46
36
  sys.exit(1)
47
37
 
38
+ # Initialize the database
39
+ sqlite.init()
40
+
48
41
  try:
49
42
  active_reservation_id = None
50
43
  active_reservation_pk = None
51
44
 
52
45
  while True:
53
- logger.debug("Scanning for files to upload...")
54
- uploader.scan()
46
+ # FIRST, check for jobs in the database that are running
47
+ db_job_runs = JobRun.objects.all()
48
+ for job_run in db_job_runs:
49
+ if job_run.pid is None:
50
+ pid_sleep_amount = 0.1
51
+ logger.debug(
52
+ f"Job run {job_run.job_run_id} has no PID. Agent has not started."
53
+ )
54
+ logger.debug(
55
+ f"Sleeping {pid_sleep_amount} seconds before checking again..."
56
+ )
57
+ sleep(pid_sleep_amount)
58
+ continue
55
59
 
56
- hardware = self.primitive.hardware.get_own_hardware_details()
60
+ logger.debug(
61
+ f"Checking process PID {job_run.pid} for JobRun {job_run.job_run_id}..."
62
+ )
63
+
64
+ status = self.primitive.jobs.get_job_status(job_run.job_run_id)
65
+ if status is None or status.data is None:
66
+ logger.error(
67
+ f"Error fetching status of <JobRun {job_run.job_run_id}>."
68
+ )
69
+ continue
70
+
71
+ status_value = status.data["jobRun"]["status"]
72
+ conclusion_value = status.data["jobRun"]["conclusion"]
73
+
74
+ logger.debug(f"- Status: {status_value}")
75
+ logger.debug(f"- Conclusion: {conclusion_value}")
76
+
77
+ try:
78
+ parent = psutil.Process(job_run.pid)
79
+ except psutil.NoSuchProcess:
80
+ logger.debug("Process not found")
81
+ continue
82
+
83
+ children = parent.children(recursive=True)
84
+
85
+ if status_value == "completed" and conclusion_value == "cancelled":
86
+ logger.warning("Job cancelled by user")
87
+ for child in children:
88
+ logger.debug(f"Killing child process {child.pid}...")
89
+ child.kill()
90
+
91
+ logger.debug(f"Killing parent process {parent.pid}...")
92
+ parent.kill()
57
93
 
94
+ if status != "completed":
95
+ sleep(1)
96
+ continue
97
+
98
+ # Second, check for active reservations
99
+ hardware = self.primitive.hardware.get_own_hardware_details()
58
100
  if hardware["activeReservation"]:
59
101
  if (
60
102
  hardware["activeReservation"]["id"] != active_reservation_id
61
103
  or hardware["activeReservation"]["pk"] != active_reservation_pk
62
104
  ):
63
- logger.warning("New reservation for this hardware.")
105
+ logger.info("New reservation for this hardware.")
64
106
  active_reservation_id = hardware["activeReservation"]["id"]
65
107
  active_reservation_pk = hardware["activeReservation"]["pk"]
66
108
  logger.debug("Active Reservation:")
@@ -71,6 +113,20 @@ class Agent(BaseAction):
71
113
  self.primitive.provisioning.add_reservation_authorized_keys(
72
114
  reservation_id=active_reservation_id
73
115
  )
116
+
117
+ if not active_reservation_id:
118
+ self.primitive.hardware.check_in_http(
119
+ is_available=True, is_online=True
120
+ )
121
+ logger.debug("Syncing children...")
122
+ self.primitive.hardware._sync_children(hardware=hardware)
123
+
124
+ sleep_amount = 5
125
+ logger.debug(
126
+ f"No active reservation found... [sleeping {sleep_amount} seconds]"
127
+ )
128
+ sleep(sleep_amount)
129
+ continue
74
130
  else:
75
131
  if (
76
132
  hardware["activeReservation"] is None
@@ -89,27 +145,32 @@ class Agent(BaseAction):
89
145
  active_reservation_id = None
90
146
  active_reservation_pk = None
91
147
 
92
- if not active_reservation_id:
93
- self.primitive.hardware.check_in_http(
94
- is_available=True, is_online=True
95
- )
148
+ # Third, see if the active reservation has any pending job runs
149
+ job_runs_for_reservation = self.primitive.jobs.get_job_runs(
150
+ status="pending", first=1, reservation_id=active_reservation_id
151
+ )
152
+
153
+ if (
154
+ job_runs_for_reservation is None
155
+ or job_runs_for_reservation.data is None
156
+ ):
157
+ logger.error("Error fetching job runs.")
96
158
  sleep_amount = 5
97
159
  logger.debug(
98
- f"No active reservation found... [sleeping {sleep_amount} seconds]"
160
+ f"Error fetching job runs... [sleeping {sleep_amount} seconds]"
99
161
  )
100
162
  sleep(sleep_amount)
101
163
  continue
102
164
 
103
- job_runs_result = self.primitive.jobs.get_job_runs(
104
- status="pending", first=1, reservation_id=active_reservation_id
105
- )
106
-
107
165
  pending_job_runs = [
108
- edge["node"] for edge in job_runs_result.data["jobRuns"]["edges"]
166
+ edge["node"]
167
+ for edge in job_runs_for_reservation.data["jobRuns"]["edges"]
109
168
  ]
110
169
 
111
170
  if not pending_job_runs:
112
- self.primitive.hardware.check_in_http(is_online=True)
171
+ self.primitive.hardware.check_in_http(
172
+ is_available=False, is_online=True
173
+ )
113
174
  sleep_amount = 5
114
175
  logger.debug(
115
176
  f"Waiting for Job Runs... [sleeping {sleep_amount} seconds]"
@@ -117,54 +178,22 @@ class Agent(BaseAction):
117
178
  sleep(sleep_amount)
118
179
  continue
119
180
 
120
- for job_run in pending_job_runs:
121
- logger.debug("Found pending Job Run")
122
- logger.debug(f"Job Run ID: {job_run['id']}")
123
- logger.debug(f"Job Name: {job_run['job']['name']}")
124
-
125
- JobRun.objects.create(
126
- job_run_id=job_run["id"],
127
- pid=None,
128
- )
129
-
130
- runner = Runner(
131
- primitive=self.primitive,
132
- job_run=job_run,
133
- max_log_size=500 * 1024,
134
- )
181
+ # If we did find a pending job run, check if it exists in the database
182
+ # and create it if it doesn't.
183
+ # This will trigger the agent to start the job run.
184
+ job_run = pending_job_runs[0]
185
+ if not JobRun.objects.filter_by(job_run_id=job_run["id"]).exists():
186
+ JobRun.objects.create(job_run_id=job_run["id"], pid=None)
187
+ logger.debug(f"Creating job run in database: {job_run['id']}")
135
188
 
136
- try:
137
- runner.setup()
138
- except Exception as exception:
139
- logger.exception(
140
- f"Exception while initializing runner: {exception}"
141
- )
142
- self.primitive.jobs.job_run_update(
143
- id=job_run["id"],
144
- status="request_completed",
145
- conclusion="failure",
146
- )
147
- JobRun.objects.filter_by(job_run_id=job_run["id"]).delete()
148
- continue
149
-
150
- try:
151
- runner.execute()
152
- except Exception as exception:
153
- logger.exception(f"Exception while executing job: {exception}")
154
- self.primitive.jobs.job_run_update(
155
- id=job_run["id"],
156
- status="request_completed",
157
- conclusion="failure",
158
- )
159
- finally:
160
- runner.cleanup()
161
-
162
- # NOTE: also run scan here to force upload of artifacts
163
- # This should probably eventually be another daemon?
164
- uploader.scan()
165
-
166
- JobRun.objects.filter_by(job_run_id=job_run["id"]).delete()
167
-
168
- sleep(5)
169
189
  except KeyboardInterrupt:
170
- logger.info("[*] Stopping primitive agent...")
190
+ logger.info("[*] Stopping primitive monitor...")
191
+ try:
192
+ self.primitive.hardware.check_in_http(
193
+ is_available=False, is_online=False, stopping_agent=True
194
+ )
195
+
196
+ except P_CLI_100 as exception:
197
+ logger.error("[*] Error stopping primitive monitor.")
198
+ logger.error(str(exception))
199
+ sys.exit()
@@ -1,102 +0,0 @@
1
- from primitive.utils.actions import BaseAction
2
- from loguru import logger
3
- from primitive.__about__ import __version__
4
- from ..utils.exceptions import P_CLI_100
5
- import sys
6
- import psutil
7
- from ..db import sqlite
8
- from ..db.models import JobRun
9
- from time import sleep
10
-
11
-
12
- class Monitor(BaseAction):
13
- def start(self):
14
- logger.remove()
15
- logger.add(
16
- sink=sys.stderr,
17
- format="<green>{time:YYYY-MM-DD HH:mm:ss.SSS}</green> | <level>{level: <8}</level> | <level>{message}</level>",
18
- backtrace=True,
19
- diagnose=True,
20
- level="DEBUG" if self.primitive.DEBUG else "INFO",
21
- )
22
- logger.info("[*] primitive monitor")
23
- logger.info(f"[*] Version: {__version__}")
24
-
25
- # Initialize the database
26
- sqlite.init()
27
-
28
- try:
29
- # hey stupid:
30
- # do not set is_available to True here, it will mess up the reservation logic
31
- # only set is_available after we've checked that no active reservation is present
32
- # setting is_available of the parent also effects the children,
33
- # which may have active reservations as well
34
- self.primitive.hardware.check_in_http(is_online=True)
35
- except Exception as exception:
36
- logger.exception(f"Error checking in hardware: {exception}")
37
- sys.exit(1)
38
-
39
- try:
40
- while True:
41
- logger.debug("Syncing children...")
42
- self.primitive.hardware._sync_children()
43
-
44
- # Look for entries in the database
45
- procs = JobRun.objects.all()
46
-
47
- # No procs in the database => nothing to monitor
48
- if len(procs) == 0:
49
- sleep_amount = 5
50
- logger.debug(
51
- f"No active processes found... [sleeping {sleep_amount} seconds]"
52
- )
53
- sleep(sleep_amount)
54
- continue
55
-
56
- # If there is a process in the database, take over check in from agent
57
- try:
58
- self.primitive.hardware.check_in_http(is_online=True)
59
- except Exception as exception:
60
- logger.exception(f"Error checking in hardware: {exception}")
61
-
62
- # For each process, check status and kill if cancelled
63
- for proc in procs:
64
- logger.debug(f"Checking process {proc.pid}...")
65
-
66
- status = self.primitive.jobs.get_job_status(proc.job_run_id)
67
- status_value = status.data["jobRun"]["status"]
68
- conclusion_value = status.data["jobRun"]["conclusion"]
69
-
70
- logger.debug(f"- Status: {status_value}")
71
- logger.debug(f"- Conclusion: {conclusion_value}")
72
-
73
- try:
74
- parent = psutil.Process(proc.pid)
75
- except psutil.NoSuchProcess:
76
- logger.debug("Process not found")
77
- continue
78
-
79
- children = parent.children(recursive=True)
80
-
81
- if status_value == "completed" and conclusion_value == "cancelled":
82
- logger.warning("Job cancelled by user")
83
- for child in children:
84
- logger.debug(f"Killing child process {child.pid}...")
85
- child.kill()
86
-
87
- logger.debug(f"Killing parent process {parent.pid}...")
88
- parent.kill()
89
-
90
- sleep(5)
91
-
92
- except KeyboardInterrupt:
93
- logger.info("[*] Stopping primitive monitor...")
94
- try:
95
- self.primitive.hardware.check_in_http(
96
- is_available=False, is_online=False, stopping_agent=True
97
- )
98
-
99
- except P_CLI_100 as exception:
100
- logger.error("[*] Error stopping primitive monitor.")
101
- logger.error(str(exception))
102
- sys.exit()
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
@@ -1,5 +1,5 @@
1
- from pathlib import Path
2
1
  from abc import ABC, abstractmethod
2
+ from pathlib import Path
3
3
 
4
4
 
5
5
  class Daemon(ABC):
File without changes
File without changes