models.py 24 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544
  1. __package__ = 'archivebox.machine'
  2. import sys
  3. import os
  4. import signal
  5. import socket
  6. import subprocess
  7. import multiprocessing
  8. from datetime import timedelta
  9. from pathlib import Path
  10. from django.db import models
  11. from django.utils import timezone
  12. from django.utils.functional import cached_property
  13. import abx
  14. import archivebox
  15. from abx_pkg import Binary, BinProvider
  16. from archivebox.base_models.models import ABIDModel, ABIDField, AutoDateTimeField, ModelWithHealthStats
  17. from .detect import get_host_guid, get_os_info, get_vm_info, get_host_network, get_host_stats
  18. _CURRENT_MACHINE = None # global cache for the current machine
  19. _CURRENT_INTERFACE = None # global cache for the current network interface
  20. _CURRENT_BINARIES = {} # global cache for the currently installed binaries
  21. MACHINE_RECHECK_INTERVAL = 7 * 24 * 60 * 60 # 1 week (how often should we check for OS/hardware changes?)
  22. NETWORK_INTERFACE_RECHECK_INTERVAL = 1 * 60 * 60 # 1 hour (how often should we check for public IP/private IP/DNS changes?)
  23. INSTALLED_BINARY_RECHECK_INTERVAL = 1 * 30 * 60 # 30min (how often should we check for changes to locally installed binaries?)
  24. class MachineManager(models.Manager):
  25. def current(self) -> 'Machine':
  26. return Machine.current()
  27. class Machine(ABIDModel, ModelWithHealthStats):
  28. """Audit log entry for a physical machine that was used to do archiving."""
  29. abid_prefix = 'mcn_'
  30. abid_ts_src = 'self.created_at'
  31. abid_uri_src = 'self.guid'
  32. abid_subtype_src = '"01"'
  33. abid_rand_src = 'self.id'
  34. abid_drift_allowed = False
  35. read_only_fields = ('id', 'abid', 'created_at', 'guid', 'hw_in_docker', 'hw_in_vm', 'hw_manufacturer', 'hw_product', 'hw_uuid', 'os_arch', 'os_family')
  36. id = models.UUIDField(primary_key=True, default=None, null=False, editable=False, unique=True, verbose_name='ID')
  37. abid = ABIDField(prefix=abid_prefix)
  38. created_at = AutoDateTimeField(default=None, null=False, db_index=True)
  39. modified_at = models.DateTimeField(auto_now=True)
  40. # IMMUTABLE PROPERTIES
  41. guid = models.CharField(max_length=64, default=None, null=False, unique=True, editable=False) # 64char sha256 hash of machine's unique hardware ID
  42. # MUTABLE PROPERTIES
  43. hostname = models.CharField(max_length=63, default=None, null=False) # e.g. somehost.subdomain.example.com
  44. hw_in_docker = models.BooleanField(default=False, null=False) # e.g. False
  45. hw_in_vm = models.BooleanField(default=False, null=False) # e.g. False
  46. hw_manufacturer = models.CharField(max_length=63, default=None, null=False) # e.g. Apple
  47. hw_product = models.CharField(max_length=63, default=None, null=False) # e.g. Mac Studio Mac13,1
  48. hw_uuid = models.CharField(max_length=255, default=None, null=False) # e.g. 39A12B50-...-...-...-...
  49. os_arch = models.CharField(max_length=15, default=None, null=False) # e.g. arm64
  50. os_family = models.CharField(max_length=15, default=None, null=False) # e.g. darwin
  51. os_platform = models.CharField(max_length=63, default=None, null=False) # e.g. macOS-14.6.1-arm64-arm-64bit
  52. os_release = models.CharField(max_length=63, default=None, null=False) # e.g. macOS 14.6.1
  53. os_kernel = models.CharField(max_length=255, default=None, null=False) # e.g. Darwin Kernel Version 23.6.0: Mon Jul 29 21:14:30 PDT 2024; root:xnu-10063.141.2~1/RELEASE_ARM64_T6000
  54. # STATS COUNTERS
  55. stats = models.JSONField(default=dict, null=False) # e.g. {"cpu_load": [1.25, 2.4, 1.4], "mem_swap_used_pct": 56, ...}
  56. # num_uses_failed = models.PositiveIntegerField(default=0) # from ModelWithHealthStats
  57. # num_uses_succeeded = models.PositiveIntegerField(default=0)
  58. objects: MachineManager = MachineManager()
  59. networkinterface_set: models.Manager['NetworkInterface']
  60. @classmethod
  61. def current(cls) -> 'Machine':
  62. """Get the current machine that ArchiveBox is running on."""
  63. global _CURRENT_MACHINE
  64. if _CURRENT_MACHINE:
  65. expires_at = _CURRENT_MACHINE.modified_at + timedelta(seconds=MACHINE_RECHECK_INTERVAL)
  66. if timezone.now() < expires_at:
  67. # assume current machine cant change *while archivebox is actively running on it*
  68. # it's not strictly impossible to swap hardware while code is running,
  69. # but its rare and unusual so we check only once per week
  70. # (e.g. VMWare can live-migrate a VM to a new host while it's running)
  71. return _CURRENT_MACHINE
  72. else:
  73. _CURRENT_MACHINE = None
  74. _CURRENT_MACHINE, _created = cls.objects.update_or_create(
  75. guid=get_host_guid(),
  76. defaults={
  77. 'hostname': socket.gethostname(),
  78. **get_os_info(),
  79. **get_vm_info(),
  80. 'stats': get_host_stats(),
  81. },
  82. )
  83. _CURRENT_MACHINE.save() # populate ABID
  84. return _CURRENT_MACHINE
  85. class NetworkInterfaceManager(models.Manager):
  86. def current(self) -> 'NetworkInterface':
  87. return NetworkInterface.current()
  88. class NetworkInterface(ABIDModel, ModelWithHealthStats):
  89. """Audit log entry for a physical network interface / internet connection that was used to do archiving."""
  90. abid_prefix = 'net_'
  91. abid_ts_src = 'self.machine.created_at'
  92. abid_uri_src = 'self.machine.guid'
  93. abid_subtype_src = 'self.iface'
  94. abid_rand_src = 'self.id'
  95. abid_drift_allowed = False
  96. read_only_fields = ('id', 'abid', 'created_at', 'machine', 'mac_address', 'ip_public', 'ip_local', 'dns_server')
  97. id = models.UUIDField(primary_key=True, default=None, null=False, editable=False, unique=True, verbose_name='ID')
  98. abid = ABIDField(prefix=abid_prefix)
  99. created_at = AutoDateTimeField(default=None, null=False, db_index=True)
  100. modified_at = models.DateTimeField(auto_now=True)
  101. machine = models.ForeignKey(Machine, on_delete=models.CASCADE, default=None, null=False) # e.g. Machine(id=...)
  102. # IMMUTABLE PROPERTIES
  103. mac_address = models.CharField(max_length=17, default=None, null=False, editable=False) # e.g. ab:cd:ef:12:34:56
  104. ip_public = models.GenericIPAddressField(default=None, null=False, editable=False) # e.g. 123.123.123.123 or 2001:0db8:85a3:0000:0000:8a2e:0370:7334
  105. ip_local = models.GenericIPAddressField(default=None, null=False, editable=False) # e.g. 192.168.2.18 or 2001:0db8:85a3:0000:0000:8a2e:0370:7334
  106. dns_server = models.GenericIPAddressField(default=None, null=False, editable=False) # e.g. 8.8.8.8 or 2001:0db8:85a3:0000:0000:8a2e:0370:7334
  107. # MUTABLE PROPERTIES
  108. hostname = models.CharField(max_length=63, default=None, null=False) # e.g. somehost.sub.example.com
  109. iface = models.CharField(max_length=15, default=None, null=False) # e.g. en0
  110. isp = models.CharField(max_length=63, default=None, null=False) # e.g. AS-SONICTELECOM
  111. city = models.CharField(max_length=63, default=None, null=False) # e.g. Berkeley
  112. region = models.CharField(max_length=63, default=None, null=False) # e.g. California
  113. country = models.CharField(max_length=63, default=None, null=False) # e.g. United States
  114. # STATS COUNTERS (inherited from ModelWithHealthStats)
  115. # num_uses_failed = models.PositiveIntegerField(default=0)
  116. # num_uses_succeeded = models.PositiveIntegerField(default=0)
  117. objects: NetworkInterfaceManager = NetworkInterfaceManager()
  118. class Meta:
  119. unique_together = (
  120. # if *any* of these change, it's considered a different interface
  121. # because we might get different downloaded content as a result,
  122. # this forces us to store an audit trail whenever these things change
  123. ('machine', 'ip_public', 'ip_local', 'mac_address', 'dns_server'),
  124. )
  125. @classmethod
  126. def current(cls) -> 'NetworkInterface':
  127. """Get the current network interface for the current machine."""
  128. global _CURRENT_INTERFACE
  129. if _CURRENT_INTERFACE:
  130. # assume the current network interface (public IP, DNS servers, etc.) wont change more than once per hour
  131. expires_at = _CURRENT_INTERFACE.modified_at + timedelta(seconds=NETWORK_INTERFACE_RECHECK_INTERVAL)
  132. if timezone.now() < expires_at:
  133. return _CURRENT_INTERFACE
  134. else:
  135. _CURRENT_INTERFACE = None
  136. machine = Machine.objects.current()
  137. net_info = get_host_network()
  138. _CURRENT_INTERFACE, _created = cls.objects.update_or_create(
  139. machine=machine,
  140. ip_public=net_info.pop('ip_public'),
  141. ip_local=net_info.pop('ip_local'),
  142. mac_address=net_info.pop('mac_address'),
  143. dns_server=net_info.pop('dns_server'),
  144. defaults=net_info,
  145. )
  146. _CURRENT_INTERFACE.save() # populate ABID
  147. return _CURRENT_INTERFACE
  148. class InstalledBinaryManager(models.Manager):
  149. def get_from_db_or_cache(self, binary: Binary) -> 'InstalledBinary':
  150. """Get or create an InstalledBinary record for a Binary on the local machine"""
  151. global _CURRENT_BINARIES
  152. cached_binary = _CURRENT_BINARIES.get(binary.name)
  153. if cached_binary:
  154. expires_at = cached_binary.modified_at + timedelta(seconds=INSTALLED_BINARY_RECHECK_INTERVAL)
  155. if timezone.now() < expires_at:
  156. is_loaded = binary.abspath and binary.version and binary.sha256
  157. if is_loaded:
  158. # if the caller took did the (expensive) job of loading the binary from the filesystem already
  159. # then their in-memory version is certainly more up-to-date than any potential cached version
  160. # use this opportunity to invalidate the cache in case if anything has changed
  161. is_different_from_cache = (
  162. binary.abspath != cached_binary.abspath
  163. or binary.version != cached_binary.version
  164. or binary.sha256 != cached_binary.sha256
  165. )
  166. if is_different_from_cache:
  167. _CURRENT_BINARIES.pop(binary.name)
  168. else:
  169. return cached_binary
  170. else:
  171. # if they have not yet loaded the binary
  172. # but our cache is recent enough and not expired, assume cached version is good enough
  173. # it will automatically reload when the cache expires
  174. # cached_binary will be stale/bad for up to 30min if binary was updated/removed on host system
  175. return cached_binary
  176. else:
  177. # cached binary is too old, reload it from scratch
  178. _CURRENT_BINARIES.pop(binary.name)
  179. if not binary.abspath or not binary.version or not binary.sha256:
  180. # if binary was not yet loaded from filesystem, do it now
  181. # this is expensive, we have to find it's abspath, version, and sha256, but it's necessary
  182. # to make sure we have a good, up-to-date record of it in the DB & in-memroy cache
  183. binary = archivebox.pm.hook.binary_load(binary=binary, fresh=True)
  184. assert binary.loaded_binprovider and binary.loaded_abspath and binary.loaded_version and binary.loaded_sha256, f'Failed to load binary {binary.name} abspath, version, and sha256'
  185. _CURRENT_BINARIES[binary.name], _created = self.update_or_create(
  186. machine=Machine.objects.current(),
  187. name=binary.name,
  188. binprovider=binary.loaded_binprovider.name,
  189. version=str(binary.loaded_version),
  190. abspath=str(binary.loaded_abspath),
  191. sha256=str(binary.loaded_sha256),
  192. )
  193. cached_binary = _CURRENT_BINARIES[binary.name]
  194. cached_binary.save() # populate ABID
  195. # if we get this far make sure DB record matches in-memroy cache
  196. assert str(cached_binary.binprovider) == str(binary.loaded_binprovider.name)
  197. assert str(cached_binary.abspath) == str(binary.loaded_abspath)
  198. assert str(cached_binary.version) == str(binary.loaded_version)
  199. assert str(cached_binary.sha256) == str(binary.loaded_sha256)
  200. return cached_binary
  201. class InstalledBinary(ABIDModel, ModelWithHealthStats):
  202. abid_prefix = 'bin_'
  203. abid_ts_src = 'self.machine.created_at'
  204. abid_uri_src = 'self.machine.guid'
  205. abid_subtype_src = 'self.binprovider'
  206. abid_rand_src = 'self.id'
  207. abid_drift_allowed = False
  208. read_only_fields = ('id', 'abid', 'created_at', 'machine', 'name', 'binprovider', 'abspath', 'version', 'sha256')
  209. id = models.UUIDField(primary_key=True, default=None, null=False, editable=False, unique=True, verbose_name='ID')
  210. abid = ABIDField(prefix=abid_prefix)
  211. created_at = AutoDateTimeField(default=None, null=False, db_index=True)
  212. modified_at = models.DateTimeField(auto_now=True)
  213. # IMMUTABLE PROPERTIES
  214. machine = models.ForeignKey(Machine, on_delete=models.CASCADE, default=None, null=False, blank=True)
  215. name = models.CharField(max_length=63, default=None, null=False, blank=True)
  216. binprovider = models.CharField(max_length=31, default=None, null=False, blank=True)
  217. abspath = models.CharField(max_length=255, default=None, null=False, blank=True)
  218. version = models.CharField(max_length=32, default=None, null=False, blank=True)
  219. sha256 = models.CharField(max_length=64, default=None, null=False, blank=True)
  220. # MUTABLE PROPERTIES (TODO)
  221. # is_pinned = models.BooleanField(default=False) # i.e. should this binary superceede other binaries with the same name on the host?
  222. # is_valid = models.BooleanField(default=True) # i.e. is this binary still available on the host?
  223. # STATS COUNTERS (inherited from ModelWithHealthStats)
  224. # num_uses_failed = models.PositiveIntegerField(default=0)
  225. # num_uses_succeeded = models.PositiveIntegerField(default=0)
  226. objects: InstalledBinaryManager = InstalledBinaryManager()
  227. class Meta:
  228. verbose_name = 'Installed Binary'
  229. verbose_name_plural = 'Installed Binaries'
  230. unique_together = (
  231. ('machine', 'name', 'abspath', 'version', 'sha256'),
  232. )
  233. def __str__(self) -> str:
  234. return f'{self.name}@{self.binprovider}+{self.abspath}@{self.version}'
  235. def clean(self, *args, **kwargs) -> None:
  236. assert self.name or self.abspath
  237. self.name = str(self.name or self.abspath)
  238. assert self.name
  239. if not hasattr(self, 'machine'):
  240. self.machine = Machine.objects.current()
  241. if not self.binprovider:
  242. all_known_binproviders = list(abx.as_dict(archivebox.pm.hook.get_BINPROVIDERS()).values())
  243. binary = archivebox.pm.hook.binary_load(binary=Binary(name=self.name, binproviders=all_known_binproviders), fresh=True)
  244. self.binprovider = binary.loaded_binprovider.name if binary.loaded_binprovider else None
  245. if not self.abspath:
  246. self.abspath = self.BINPROVIDER.get_abspath(self.name)
  247. if not self.version:
  248. self.version = self.BINPROVIDER.get_version(self.name, abspath=self.abspath)
  249. if not self.sha256:
  250. self.sha256 = self.BINPROVIDER.get_sha256(self.name, abspath=self.abspath)
  251. super().clean(*args, **kwargs)
  252. @cached_property
  253. def BINARY(self) -> Binary:
  254. for binary in abx.as_dict(archivebox.pm.hook.get_BINARIES()).values():
  255. if binary.name == self.name:
  256. return binary
  257. raise Exception(f'Orphaned InstalledBinary {self.name} {self.binprovider} was found in DB, could not find any plugin that defines it')
  258. # TODO: we could technically reconstruct it from scratch, but why would we ever want to do that?
  259. @cached_property
  260. def BINPROVIDER(self) -> BinProvider:
  261. for binprovider in abx.as_dict(archivebox.pm.hook.get_BINPROVIDERS()).values():
  262. if binprovider.name == self.binprovider:
  263. return binprovider
  264. raise Exception(f'Orphaned InstalledBinary(name={self.name}) was found in DB, could not find any plugin that defines BinProvider(name={self.binprovider})')
  265. # maybe not a good idea to provide this? Binary in DB is a record of the binary's config
  266. # whereas a loaded binary is a not-yet saved instance that may not have the same config
  267. # why would we want to load a binary record from the db when it could be freshly loaded?
  268. def load_from_db(self) -> Binary:
  269. # TODO: implement defaults arg in abx_pkg
  270. # return self.BINARY.load(defaults={
  271. # 'binprovider': self.BINPROVIDER,
  272. # 'abspath': Path(self.abspath),
  273. # 'version': self.version,
  274. # 'sha256': self.sha256,
  275. # })
  276. return Binary.model_validate({
  277. **self.BINARY.model_dump(),
  278. 'abspath': self.abspath and Path(self.abspath),
  279. 'version': self.version,
  280. 'sha256': self.sha256,
  281. 'loaded_binprovider': self.BINPROVIDER,
  282. 'binproviders_supported': self.BINARY.binproviders_supported,
  283. 'overrides': self.BINARY.overrides,
  284. })
  285. def load_fresh(self) -> Binary:
  286. return archivebox.pm.hook.binary_load(binary=self.BINARY, fresh=True)
  287. def spawn_process(proc_id: str):
  288. proc = Process.objects.get(id=proc_id)
  289. proc.spawn()
  290. class ProcessManager(models.Manager):
  291. pass
  292. class ProcessQuerySet(models.QuerySet):
  293. """
  294. Enhanced QuerySet for Process model, usage:
  295. Process.objects.queued() -> QuerySet[Process] [Process(pid=None, returncode=None), Process(pid=None, returncode=None)]
  296. Process.objects.running() -> QuerySet[Process] [Process(pid=123, returncode=None), Process(pid=456, returncode=None)]
  297. Process.objects.exited() -> QuerySet[Process] [Process(pid=789, returncode=0), Process(pid=101, returncode=1)]
  298. Process.objects.running().pids() -> [456]
  299. Process.objects.kill() -> 1
  300. """
  301. def queued(self):
  302. return self.filter(pid__isnull=True, returncode__isnull=True)
  303. def running(self):
  304. return self.filter(pid__isnull=False, returncode__isnull=True)
  305. def exited(self):
  306. return self.filter(returncode__isnull=False)
  307. def kill(self):
  308. total_killed = 0
  309. for proc in self.running():
  310. proc.kill()
  311. total_killed += 1
  312. return total_killed
  313. def pids(self):
  314. return self.values_list('pid', flat=True)
  315. class Process(ABIDModel):
  316. abid_prefix = 'pid_'
  317. abid_ts_src = 'self.created_at'
  318. abid_uri_src = 'self.cmd'
  319. abid_subtype_src = 'self.actor_type or "00"'
  320. abid_rand_src = 'self.id'
  321. abid_drift_allowed = False
  322. read_only_fields = ('id', 'abid', 'created_at', 'cmd', 'cwd', 'actor_type', 'timeout')
  323. id = models.UUIDField(primary_key=True, default=None, null=False, editable=False, unique=True, verbose_name='ID')
  324. abid = ABIDField(prefix=abid_prefix)
  325. # immutable state
  326. cmd = models.JSONField(default=list) # shell argv
  327. cwd = models.CharField(max_length=255) # working directory
  328. actor_type = models.CharField(max_length=255, null=True) # python ActorType that this process is running
  329. timeout = models.PositiveIntegerField(null=True, default=None) # seconds to wait before killing the process if it's still running
  330. created_at = models.DateTimeField(null=False, default=timezone.now, editable=False)
  331. modified_at = models.DateTimeField(null=False, default=timezone.now, editable=False)
  332. # mutable fields
  333. machine = models.ForeignKey(Machine, on_delete=models.CASCADE)
  334. pid = models.IntegerField(null=True)
  335. launched_at = models.DateTimeField(null=True)
  336. finished_at = models.DateTimeField(null=True)
  337. returncode = models.IntegerField(null=True)
  338. stdout = models.TextField(default='', null=False)
  339. stderr = models.TextField(default='', null=False)
  340. machine_id: str
  341. # optional mutable state that can be used to trace what the process is doing
  342. # active_event = models.ForeignKey('Event', null=True, on_delete=models.SET_NULL)
  343. emitted_events: models.RelatedManager['Event']
  344. claimed_events: models.RelatedManager['Event']
  345. objects: ProcessManager = ProcessManager.from_queryset(ProcessQuerySet)()
  346. @classmethod
  347. def current(cls) -> 'Process':
  348. proc_id = os.environ.get('PROCESS_ID', '').strip()
  349. if not proc_id:
  350. proc = cls.objects.create(
  351. cmd=sys.argv,
  352. cwd=os.getcwd(),
  353. actor_type=None,
  354. timeout=None,
  355. machine=Machine.objects.current(),
  356. pid=os.getpid(),
  357. launched_at=timezone.now(),
  358. finished_at=None,
  359. returncode=None,
  360. stdout='',
  361. stderr='',
  362. )
  363. os.environ['PROCESS_ID'] = str(proc.id)
  364. return proc
  365. proc = cls.objects.get(id=proc_id)
  366. if proc.pid:
  367. assert os.getpid() == proc.pid, f'Process ID mismatch: {proc.pid} != {os.getpid()}'
  368. else:
  369. proc.pid = os.getpid()
  370. proc.machine = Machine.current()
  371. proc.cwd = os.getcwd()
  372. proc.cmd = sys.argv
  373. proc.launched_at = proc.launched_at or timezone.now()
  374. proc.save()
  375. return proc
  376. @classmethod
  377. def create_and_fork(cls, **kwargs):
  378. proc = cls.objects.create(**kwargs)
  379. proc.fork()
  380. return proc
  381. def fork(self):
  382. if self.pid:
  383. raise Exception(f'Process is already running, cannot fork again: {self}')
  384. # fork the process in the background
  385. multiprocessing.Process(target=spawn_process, args=(self.id,)).start()
  386. def spawn(self):
  387. if self.pid:
  388. raise Exception(f'Process already running, cannot spawn again: {self}')
  389. # spawn the process in the foreground and block until it exits
  390. proc = subprocess.Popen(self.cmd, cwd=self.cwd, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True)
  391. self.pid = proc.pid
  392. self.launched_at = timezone.now()
  393. self.save()
  394. # Event.dispatch('PROC_UPDATED', {'process_id': self.id})
  395. # block until the process exits
  396. proc.wait()
  397. self.finished_at = timezone.now()
  398. self.returncode = proc.returncode
  399. self.stdout = proc.stdout.read()
  400. self.stderr = proc.stderr.read()
  401. self.pid = None
  402. self.save()
  403. # Event.dispatch('PROC_UPDATED', {'process_id': self.id})
  404. def kill(self):
  405. if not self.is_running: return
  406. assert self.machine == Machine.current(), f'Cannot kill actor on another machine: {self.machine_id} != {Machine.current().id}'
  407. os.kill(self.pid, signal.SIGKILL)
  408. self.pid = None
  409. self.save()
  410. # Event.dispatch('PROC_UPDATED', {'process_id': self.id})
  411. @property
  412. def is_pending(self):
  413. return (self.pid is None) and (self.returncode is None)
  414. @property
  415. def is_running(self):
  416. return (self.pid is not None) and (self.returncode is None)
  417. @property
  418. def is_failed(self):
  419. return self.returncode not in (None, 0)
  420. @property
  421. def is_succeeded(self):
  422. return self.returncode == 0
  423. # @property
  424. # def is_idle(self):
  425. # if not self.actor_type:
  426. # raise Exception(f'Process {self.id} has no actor_type set, can only introspect active events if Process.actor_type is set to the Actor its running')
  427. # return self.active_event is None