whatap-python 2.2.0__py3-none-any.whl → 2.2.1rc1__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.
whatap/__init__.py CHANGED
@@ -537,9 +537,14 @@ def agent():
537
537
  llm_enabled = preview_whatap_conf("llm_enabled")
538
538
 
539
539
  if apm_enabled == 'true':
540
- t = threading.Thread(target=go)
541
- t.setDaemon(True)
542
- t.start()
540
+ if sys.platform.startswith('linux'):
541
+ # Publish native ownership before a short-lived interpreter
542
+ # can exit. This does not wait for collector/TCP readiness.
543
+ go()
544
+ else:
545
+ t = threading.Thread(target=go)
546
+ t.setDaemon(True)
547
+ t.start()
543
548
 
544
549
  if llm_enabled == 'true':
545
550
  # llm_enabled=true 라도 LLM 채널 자격증명(llm_accesskey)이 없으면 LLM
@@ -571,7 +576,60 @@ AGENT_NAME = 'whatap_python'
571
576
 
572
577
 
573
578
 
579
+ def _native_agent_spec(opts):
580
+ from whatap.util.native_process import AgentSpec
581
+ source = os.path.join(os.path.dirname(__file__), 'agent', 'linux',
582
+ ARCH[platform.machine()], AGENT_NAME)
583
+ env = os.environ.copy()
584
+ env.update(opts)
585
+ return AgentSpec(os.environ['WHATAP_HOME'], source, env)
586
+
587
+
588
+ def _native_agent_log_path():
589
+ return os.path.join(os.environ.get('WHATAP_HOME', '.'), 'logs', AGENT_NAME + '.log')
590
+
591
+
592
+ def _open_native_agent_log():
593
+ # managed 경로는 PIPE 를 읽지 않으므로 네이티브 stdout/stderr 를 파일로 남겨
594
+ # 기동 직후 실패 원인을 추적할 수 있게 한다.
595
+ try:
596
+ log_path = _native_agent_log_path()
597
+ os.makedirs(os.path.dirname(log_path), exist_ok=True)
598
+ return open(log_path, 'ab')
599
+ except Exception as e:
600
+ whatap_print('WHATAP: native agent log open failed: %s' % e)
601
+ return subprocess.DEVNULL
602
+
603
+
574
604
  def go(batch=False, opts={}, llm=False):
605
+ if sys.platform.startswith('linux') and not batch and not llm:
606
+ from whatap.util.native_process import FAILED, ensure_agent_running
607
+
608
+ def on_child_exit(pid, exit_code):
609
+ whatap_error_log(
610
+ 'WHATAP: native APM exited (PID: {}, exit code: {}, log: {})'.format(
611
+ pid, exit_code, _native_agent_log_path()),
612
+ log_id='WHATAP_AGENT_EXITED')
613
+
614
+ try:
615
+ spec = _native_agent_spec(opts)
616
+ result = ensure_agent_running(
617
+ spec, lambda: _start_go(batch, opts, llm, managed=True),
618
+ on_child_exit=on_child_exit)
619
+ if result.status == FAILED:
620
+ whatap_error_log('WHATAP: native APM startup failed',
621
+ log_id='WHATAP_AGENT_START_FAILED')
622
+ else:
623
+ whatap_print('WHATAP: native APM {} (PID: {})'.format(
624
+ result.status, result.pid))
625
+ except Exception as e:
626
+ whatap_error_log('WHATAP: native APM startup failed: {}'.format(e),
627
+ log_id='WHATAP_AGENT_START_FAILED')
628
+ return
629
+ return _start_go(batch, opts, llm)
630
+
631
+
632
+ def _start_go(batch=False, opts={}, llm=False, managed=False):
575
633
  # LLM 에이전트는 Linux/macOS 를 지원한다(전용 whatap_python 이
576
634
  # $WHATAP_HOME/<os>/<arch> 에 배포됨 — linux/darwin × amd64/arm64).
577
635
  # 그 외 OS(Windows 등)는 전용 바이너리가 없으므로 우회적 "binary not found"
@@ -625,7 +683,7 @@ def go(batch=False, opts={}, llm=False):
625
683
  return ''
626
684
 
627
685
  pid = read_file(home, file_name)
628
- if pid and get_pname(pid).find('whatap_python') >= 0:
686
+ if not managed and pid and get_pname(pid).find('whatap_python') >= 0:
629
687
  if sys.platform == 'win32':
630
688
  # Windows: Don't kill, just skip
631
689
  whatap_print('WHATAP: Agent already running (PID: {}). Skipping duplicate execution.'.format(pid))
@@ -774,6 +832,17 @@ def go(batch=False, opts={}, llm=False):
774
832
  cwd=home_path, env=newenv,
775
833
  creationflags=DETACHED_PROCESS,
776
834
  stdout=subprocess.PIPE, stderr=subprocess.PIPE)
835
+ elif managed:
836
+ go_out = _open_native_agent_log()
837
+ try:
838
+ process = subprocess.Popen(cmd_args,
839
+ cwd=home_path, env=newenv,
840
+ start_new_session=True,
841
+ stdout=go_out, stderr=go_out)
842
+ finally:
843
+ if go_out is not subprocess.DEVNULL:
844
+ go_out.close()
845
+ return process
777
846
  else:
778
847
  process = subprocess.Popen(cmd_args,
779
848
  cwd=home_path, env=newenv,
@@ -1086,6 +1155,22 @@ def get_port_number(port=6600, home=os.environ.get('WHATAP_HOME'), lock_file=Non
1086
1155
 
1087
1156
 
1088
1157
  def configPort():
1158
+ if sys.platform.startswith('linux') and preview_whatap_conf('apm_enabled') == 'true':
1159
+ from whatap.util.native_process import acquire_start_lock, configured_channel, find_existing_agent
1160
+ try:
1161
+ spec = _native_agent_spec({})
1162
+ with acquire_start_lock(spec):
1163
+ if find_existing_agent(spec):
1164
+ # CLI allocation must not rewrite a live bootstrap agent's channel.
1165
+ return configured_channel(spec.home, spec.environment, include_unix=False)[1]
1166
+ return _config_port()
1167
+ except Exception as e:
1168
+ # 락 타임아웃/다중 인스턴스 등은 CLI 를 죽이지 말고 기존 포트 할당으로 폴백
1169
+ whatap_print('WHATAP: live agent check skipped, falling back to port file: %s' % e)
1170
+ return _config_port()
1171
+
1172
+
1173
+ def _config_port():
1089
1174
  # force_net_udp_port가 설정된 경우 lock 파일 무시하고 해당 포트 강제 사용
1090
1175
  force_port_str = preview_whatap_conf("force_net_udp_port")
1091
1176
  if force_port_str and force_port_str not in ('false', 'False'):
whatap/build.py CHANGED
@@ -1,4 +1,4 @@
1
1
  app = 'Python'
2
2
  name = 'whatap-python'
3
- version = '2.2.0'
4
- release_date = '20260901'
3
+ version = '2.2.1rc1'
4
+ release_date = '20260916'
@@ -0,0 +1,458 @@
1
+ """Linux APM native process ownership (not instrumentation configuration)."""
2
+ import os
3
+ import errno
4
+ import fcntl
5
+ import filecmp
6
+ import json
7
+ import re
8
+ import stat
9
+ import subprocess
10
+ import tempfile
11
+ import threading
12
+ import time
13
+ from collections import namedtuple
14
+ from contextlib import contextmanager
15
+ from pathlib import Path
16
+
17
+ STARTED = 'started'
18
+ REUSED = 'reused'
19
+ FAILED = 'failed'
20
+ IN_PROGRESS = 'in_progress'
21
+ StartResult = namedtuple('StartResult', 'status pid')
22
+ DISCOVERY_TIMEOUT_SECONDS = 1.0
23
+ LOCK_TIMEOUT_SECONDS = 1.0
24
+ LOCK_RETRY_SECONDS = 0.01
25
+ DEFAULT_UDP_PORT = 6600
26
+ CONFIG_MAX_BYTES = 1024 * 1024
27
+ RECORD_MAX_BYTES = 8192
28
+ CHILD_STOP_TIMEOUT_SECONDS = 2.0
29
+ STARTUP_GRACE_SECONDS = 0.5
30
+ STARTUP_POLL_SECONDS = 0.05
31
+ _TRUE_VALUES = ('1', 't', 'T', 'true', 'TRUE', 'True')
32
+ _watchers = set()
33
+ _watchers_lock = threading.Lock()
34
+ _start_lock_fds = set()
35
+ _start_lock_fds_lock = threading.Lock()
36
+
37
+
38
+ def _before_fork():
39
+ # Serialize only descriptor bookkeeping, never flock acquisition or yield.
40
+ _start_lock_fds_lock.acquire()
41
+
42
+
43
+ def _after_fork_parent():
44
+ _start_lock_fds_lock.release()
45
+
46
+
47
+ def _after_fork():
48
+ global _watchers, _watchers_lock, _start_lock_fds, _start_lock_fds_lock
49
+ for fd in _start_lock_fds:
50
+ # LOCK_UN would also unlock the parent's shared open-file description.
51
+ try:
52
+ os.close(fd)
53
+ except OSError:
54
+ pass
55
+ _start_lock_fds = set()
56
+ _start_lock_fds_lock = threading.Lock()
57
+ _watchers = set()
58
+ _watchers_lock = threading.Lock()
59
+
60
+
61
+ os.register_at_fork(before=_before_fork, after_in_parent=_after_fork_parent,
62
+ after_in_child=_after_fork)
63
+
64
+
65
+ def watch_owned_child(pid, process=None, on_exit=None):
66
+ if process is None:
67
+ try:
68
+ fields = (Path('/proc') / str(pid) / 'stat').read_text().rsplit(')', 1)[1].split()
69
+ if int(fields[1]) != os.getpid():
70
+ return
71
+ except (FileNotFoundError, ProcessLookupError):
72
+ return
73
+ key = (os.getpid(), pid)
74
+ with _watchers_lock:
75
+ if key in _watchers:
76
+ return
77
+ _watchers.add(key)
78
+
79
+ def wait():
80
+ try:
81
+ if process is not None:
82
+ process.wait()
83
+ else:
84
+ os.waitpid(pid, 0)
85
+ except ChildProcessError:
86
+ # An application may also reap its children. Never wait for -1 or
87
+ # replace SIGCHLD: those are application-owned contracts.
88
+ pass
89
+ finally:
90
+ if on_exit is not None:
91
+ try:
92
+ on_exit()
93
+ except (OSError, RuntimeError):
94
+ # A stale record is safe: it is revalidated on every start.
95
+ pass
96
+ with _watchers_lock:
97
+ _watchers.discard(key)
98
+
99
+ thread = threading.Thread(target=wait, name='whatap-native-wait-{}'.format(pid))
100
+ thread.daemon = True
101
+ try:
102
+ thread.start()
103
+ except Exception:
104
+ with _watchers_lock:
105
+ _watchers.discard(key)
106
+ raise
107
+
108
+
109
+ def configured_channel(home, env, include_unix=True):
110
+ """Read only native transport keys, including native file/ENV precedence.
111
+
112
+ whatap.port is an object identity input, NOT the UDP listener port.
113
+ """
114
+ aliases = {'net_udp_port': 'WHATAP_NET_UDP_PORT',
115
+ 'use_env_first': 'WHATAP_USE_ENV_FIRST'}
116
+ keys = ('net_udp_port', 'use_env_first', 'unix_socket_enabled', 'unix_socket')
117
+ config_home = env.get('WHATAP_CONFIG_HOME') or home
118
+ config_name = env.get('WHATAP_CONFIG') or 'whatap.conf'
119
+ # Preserve Go path.Join semantics before resolving against native cwd.
120
+ config_path = os.path.normpath(config_home + '/' + config_name)
121
+ if not os.path.isabs(config_path):
122
+ config_path = os.path.join(home, config_path)
123
+ values = {}
124
+ try:
125
+ with open(config_path) as stream:
126
+ content = stream.read(CONFIG_MAX_BYTES + 1)
127
+ if len(content) > CONFIG_MAX_BYTES:
128
+ raise RuntimeError('native configuration exceeds startup read limit')
129
+ for line in content.splitlines():
130
+ line = line.strip()
131
+ if not line or line.startswith(('#', '!')):
132
+ continue
133
+ key, separator, value = line.partition('=')
134
+ if separator and key.strip() in keys:
135
+ values[key.strip()] = value.strip()
136
+ except FileNotFoundError:
137
+ pass
138
+
139
+ def env_value(key):
140
+ return (env.get(key) or env.get(aliases.get(key, ''), '')).strip()
141
+
142
+ # The native loader resets UseEnvFirst before reading UseEnvFirst itself.
143
+ env_first = values.get('use_env_first', env_value('use_env_first')) in _TRUE_VALUES
144
+
145
+ def get(key, default):
146
+ value = env_value(key)
147
+ if not (env_first and value):
148
+ value = values.get(key, value)
149
+ return value or default
150
+
151
+ if include_unix and get('unix_socket_enabled', 'false') in _TRUE_VALUES:
152
+ # Go path.Join joins even a leading slash in its second argument.
153
+ path = os.path.normpath(home + '/' + get('unix_socket', 'whatap.sock'))
154
+ return ('unix', path)
155
+ try:
156
+ value = get('net_udp_port', str(DEFAULT_UDP_PORT))
157
+ # Native strconv.ParseInt(value, 10, 32) rejects Python-only syntax.
158
+ if re.fullmatch(r'[+-]?[0-9]+', value) is None:
159
+ raise ValueError('invalid native decimal')
160
+ port = int(value)
161
+ if not -2147483648 <= port <= 2147483647:
162
+ raise ValueError('native decimal exceeds int32')
163
+ except ValueError:
164
+ port = DEFAULT_UDP_PORT
165
+ if not 0 <= port <= 65535:
166
+ raise ValueError('native APM UDP port is out of range')
167
+ return ('udp', port)
168
+
169
+
170
+ def channel_socket_inodes(channel):
171
+ kind, address = channel
172
+ inodes = set()
173
+ tables = ('unix',) if kind == 'unix' else ('udp', 'udp6')
174
+ for table in tables:
175
+ try:
176
+ lines = (Path('/proc/net') / table).read_text().splitlines()[1:]
177
+ except FileNotFoundError:
178
+ if table == 'udp6':
179
+ continue
180
+ raise
181
+ for line in lines:
182
+ fields = line.split(None, 7) if kind == 'unix' else line.split()
183
+ if kind == 'unix':
184
+ if len(fields) == 8 and fields[7] == address:
185
+ inodes.add(fields[6])
186
+ elif address and int(fields[1].rsplit(':', 1)[1], 16) == address:
187
+ inodes.add(fields[9])
188
+ return inodes
189
+
190
+
191
+ def owns_socket(pid, inodes):
192
+ sockets = {'socket:[{}]'.format(inode) for inode in inodes}
193
+ if not sockets:
194
+ return False
195
+ try:
196
+ with os.scandir('/proc/{}/fd'.format(pid)) as descriptors:
197
+ for descriptor in descriptors:
198
+ try:
199
+ sockets.discard(os.readlink(descriptor.path))
200
+ if not sockets:
201
+ return True
202
+ except FileNotFoundError:
203
+ continue
204
+ except FileNotFoundError:
205
+ return False
206
+ return False
207
+
208
+
209
+ class AgentSpec(object):
210
+ def __init__(self, home, executable, env=None):
211
+ self.home = os.path.realpath(home)
212
+ self.executable = os.path.realpath(executable)
213
+ environment = os.environ if env is None else env
214
+ # Retain only transport/config-location keys, never credentials.
215
+ self.environment = {key: environment[key] for key in (
216
+ 'net_udp_port', 'WHATAP_NET_UDP_PORT', 'use_env_first',
217
+ 'WHATAP_USE_ENV_FIRST', 'unix_socket_enabled', 'unix_socket',
218
+ 'WHATAP_CONFIG_HOME', 'WHATAP_CONFIG') if key in environment}
219
+ self.channel = None # Read under the startup lock, not before waiting.
220
+ self.pid_file = Path(self.home) / 'whatap_python.pid'
221
+ namespaces = '{}-{}'.format(os.stat('/proc/self/ns/pid').st_ino,
222
+ os.stat('/proc/self/ns/net').st_ino)
223
+ self.namespaces = namespaces
224
+ self.lock_file = Path(self.home) / ('whatap_python.' + namespaces + '.lock')
225
+ self.record_file = Path(self.home) / ('whatap_python.' + namespaces + '.runtime.json')
226
+
227
+
228
+ def _process_stat(pid):
229
+ # comm can contain spaces/parentheses. After its final ')', state is field 3
230
+ # and the process start time is field 22.
231
+ fields = (Path('/proc') / str(pid) / 'stat').read_text().rsplit(')', 1)[1].split()
232
+ return {'pid': pid, 'state': fields[0], 'ppid': int(fields[1]), 'start_time': fields[19]}
233
+
234
+
235
+ def _read_record(spec):
236
+ try:
237
+ with spec.record_file.open() as stream:
238
+ content = stream.read(RECORD_MAX_BYTES + 1)
239
+ if len(content) > RECORD_MAX_BYTES:
240
+ raise RuntimeError('native APM ownership record is too large')
241
+ value = json.loads(content)
242
+ return value if isinstance(value, dict) else {}
243
+ except (FileNotFoundError, ValueError):
244
+ return {}
245
+
246
+
247
+ def _atomic_write(path, content):
248
+ fd, temporary = tempfile.mkstemp(prefix=path.name + '.', dir=str(path.parent))
249
+ try:
250
+ with os.fdopen(fd, 'w') as stream:
251
+ stream.write(content)
252
+ os.replace(temporary, str(path))
253
+ finally:
254
+ try:
255
+ os.unlink(temporary)
256
+ except FileNotFoundError:
257
+ pass
258
+
259
+
260
+ def write_runtime_record(spec, pid):
261
+ info = _process_stat(pid)
262
+ if info['state'] in ('Z', 'X'):
263
+ raise RuntimeError('native APM exited during startup')
264
+ record = {'pid': pid, 'start_time': info['start_time'],
265
+ 'channel': list(spec.channel), 'namespaces': spec.namespaces}
266
+ if _read_record(spec) != record:
267
+ _atomic_write(spec.record_file, json.dumps(record))
268
+ try:
269
+ unchanged = spec.pid_file.read_text().strip() == str(pid)
270
+ except FileNotFoundError:
271
+ unchanged = False
272
+ if not unchanged:
273
+ _atomic_write(spec.pid_file, str(pid))
274
+ return record
275
+
276
+
277
+ def _clear_record(spec, record):
278
+ with acquire_start_lock(spec):
279
+ if _read_record(spec) == record:
280
+ spec.record_file.unlink()
281
+
282
+
283
+ @contextmanager
284
+ def acquire_start_lock(spec):
285
+ flags = os.O_RDWR | os.O_CREAT | os.O_CLOEXEC | os.O_NOFOLLOW
286
+ owner_pid = os.getpid()
287
+ with _start_lock_fds_lock:
288
+ fd = os.open(str(spec.lock_file), flags, 0o600)
289
+ _start_lock_fds.add(fd)
290
+ try:
291
+ info = os.fstat(fd)
292
+ if not stat.S_ISREG(info.st_mode) or info.st_nlink != 1:
293
+ raise RuntimeError('unsafe native APM lock file')
294
+ deadline = time.monotonic() + LOCK_TIMEOUT_SECONDS
295
+ while True:
296
+ try:
297
+ fcntl.flock(fd, fcntl.LOCK_EX | fcntl.LOCK_NB)
298
+ break
299
+ except OSError as error:
300
+ if error.errno not in (errno.EAGAIN, errno.EACCES):
301
+ raise
302
+ if time.monotonic() >= deadline:
303
+ raise TimeoutError('native APM startup lock is busy')
304
+ time.sleep(LOCK_RETRY_SECONDS)
305
+ yield
306
+ finally:
307
+ # Closing releases the lock. Never unlink: another waiter may have opened
308
+ # this inode already, and a replacement inode would allow two owners.
309
+ if os.getpid() == owner_pid:
310
+ with _start_lock_fds_lock:
311
+ try:
312
+ os.close(fd)
313
+ finally:
314
+ _start_lock_fds.discard(fd)
315
+
316
+
317
+ def _matching_process(pid, spec, record):
318
+ """A name/PID alone is not an identity; do not adopt other applications."""
319
+ root = Path('/proc') / str(pid)
320
+ try:
321
+ info = _process_stat(pid)
322
+ if info['state'] in ('Z', 'X'):
323
+ return False
324
+ if os.path.realpath(os.readlink(str(root / 'cwd'))) != spec.home:
325
+ return False
326
+ args = (root / 'cmdline').read_bytes().split(b'\0')
327
+ if b'--llm' in args:
328
+ return False
329
+ namespaces = '{}-{}'.format(os.stat(str(root / 'ns/pid')).st_ino,
330
+ os.stat(str(root / 'ns/net')).st_ino)
331
+ if namespaces != spec.namespaces:
332
+ return False
333
+ process_exe = str(root / 'exe')
334
+ same_binary = os.path.samefile(process_exe, spec.executable)
335
+ if not same_binary and os.path.realpath(os.readlink(process_exe)) == os.path.join(spec.home, 'whatap_python'):
336
+ # _start_go copies when the filesystem does not support symlinks.
337
+ # Path/name alone is insufficient: compare the actual executable.
338
+ same_binary = filecmp.cmp(process_exe, spec.executable, shallow=False)
339
+ if not same_binary:
340
+ if (root / 'comm').read_text().strip() == 'whatap_python':
341
+ raise RuntimeError('different native APM executable is already running in this home')
342
+ return False
343
+ if record.get('pid') == pid and record.get('start_time') == info['start_time']:
344
+ if record.get('channel') != list(spec.channel):
345
+ raise RuntimeError('running native APM channel differs from current configuration')
346
+ return True
347
+ except (FileNotFoundError, ProcessLookupError):
348
+ return False
349
+
350
+
351
+ def find_existing_agent(spec):
352
+ """Caller holds acquire_start_lock, including during port-file allocation."""
353
+ spec.channel = configured_channel(spec.home, spec.environment)
354
+ record = _read_record(spec)
355
+ recorded_pid = record.get('pid')
356
+ if isinstance(recorded_pid, int) and recorded_pid > 0:
357
+ if _matching_process(recorded_pid, spec, record):
358
+ return recorded_pid
359
+ try:
360
+ pid = int(spec.pid_file.read_text().strip())
361
+ except (FileNotFoundError, ValueError):
362
+ pid = None
363
+ if pid and pid > 0 and _matching_process(pid, spec, record):
364
+ return pid
365
+ matches = []
366
+ deadline = time.monotonic() + DISCOVERY_TIMEOUT_SECONDS
367
+ with os.scandir('/proc') as entries:
368
+ for entry in entries:
369
+ if time.monotonic() > deadline:
370
+ raise RuntimeError('native APM process discovery timed out')
371
+ if not entry.name.isdigit():
372
+ continue
373
+ try:
374
+ comm = (Path(entry.path) / 'comm').read_text().strip()
375
+ if comm != 'whatap_python':
376
+ continue
377
+ candidate = int(entry.name)
378
+ if _matching_process(candidate, spec, record):
379
+ matches.append(candidate)
380
+ except (FileNotFoundError, ProcessLookupError):
381
+ continue
382
+ if len(matches) > 1:
383
+ raise RuntimeError('multiple native APM instances already exist')
384
+ return matches[0] if matches else None
385
+
386
+
387
+ def ensure_agent_running(spec, spawn, on_child_exit=None):
388
+ try:
389
+ with acquire_start_lock(spec):
390
+ return _ensure_locked(spec, spawn, on_child_exit)
391
+ except TimeoutError:
392
+ return StartResult(IN_PROGRESS, None)
393
+
394
+
395
+ def _early_exit_code(process):
396
+ """Return the exit code if the child dies within the startup grace period."""
397
+ deadline = time.monotonic() + STARTUP_GRACE_SECONDS
398
+ while True:
399
+ code = process.poll()
400
+ if isinstance(code, int):
401
+ return code
402
+ if time.monotonic() >= deadline:
403
+ return None
404
+ time.sleep(STARTUP_POLL_SECONDS)
405
+
406
+
407
+ def _owned_child_exited(spec, record, process, on_child_exit):
408
+ try:
409
+ _clear_record(spec, record)
410
+ finally:
411
+ if on_child_exit is not None:
412
+ try:
413
+ on_child_exit(process.pid, process.returncode)
414
+ except Exception:
415
+ pass
416
+
417
+
418
+ def _ensure_locked(spec, spawn, on_child_exit=None):
419
+ pid = find_existing_agent(spec)
420
+ occupied = channel_socket_inodes(spec.channel)
421
+ if occupied and (not pid or not owns_socket(pid, occupied)):
422
+ raise RuntimeError('native APM channel is occupied by another process')
423
+ if pid:
424
+ previous = _read_record(spec)
425
+ known = (previous.get('pid') == pid and
426
+ previous.get('start_time') == _process_stat(pid)['start_time'] and
427
+ previous.get('channel') == list(spec.channel) and
428
+ previous.get('namespaces') == spec.namespaces)
429
+ if not known and not occupied:
430
+ # Legacy native may still be connecting, or use a different channel.
431
+ # Neither duplicate it nor silently certify its current config.
432
+ watch_owned_child(pid)
433
+ return StartResult(IN_PROGRESS, pid)
434
+ record = write_runtime_record(spec, pid)
435
+ watch_owned_child(pid, on_exit=lambda: _clear_record(spec, record))
436
+ return StartResult(REUSED, pid)
437
+ process = spawn()
438
+ if process is None:
439
+ return StartResult(FAILED, None)
440
+ exit_code = _early_exit_code(process)
441
+ if exit_code is not None:
442
+ raise RuntimeError('native APM exited immediately (PID {}, exit code {})'.format(
443
+ process.pid, exit_code))
444
+ try:
445
+ record = write_runtime_record(spec, process.pid)
446
+ watch_owned_child(process.pid, process,
447
+ on_exit=lambda: _owned_child_exited(spec, record, process, on_child_exit))
448
+ except Exception:
449
+ # Only this Popen's child is ours to stop. Never abandon a child after a
450
+ # metadata/thread failure, and never kill a process found by discovery.
451
+ process.terminate()
452
+ try:
453
+ process.wait(timeout=CHILD_STOP_TIMEOUT_SECONDS)
454
+ except subprocess.TimeoutExpired:
455
+ process.kill()
456
+ process.wait(timeout=CHILD_STOP_TIMEOUT_SECONDS)
457
+ raise
458
+ return StartResult(STARTED, process.pid)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: whatap-python
3
- Version: 2.2.0
3
+ Version: 2.2.1rc1
4
4
  Summary: Monitoring and Profiling Service
5
5
  Home-page: https://www.whatap.io
6
6
  Author: whatap
@@ -1,8 +1,8 @@
1
1
  whatap/LICENSE,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
2
  whatap/README.rst,sha256=M4dohffaOQQcSlo02OgWplEr9SlffyjV3v7w9vHf1RI,967
3
- whatap/__init__.py,sha256=cO2inP_mN4OYajp7PrWKIkwQ25cRDGrPOHfGXu50vl8,44743
3
+ whatap/__init__.py,sha256=mfoWRapp9hPl4DsQz-j420MZPie-Jf2RR0VkFIgYi20,48545
4
4
  whatap/__main__.py,sha256=9fgK-V2eh7rZQ0OKWjpYsWRCyhOx35zRK6XUvJXWuUo,64
5
- whatap/build.py,sha256=oTQnQ42pUcKU-D7BsszxZqXBvkJ0f9DjbMhWD4OxC1A,82
5
+ whatap/build.py,sha256=cgdqD1v2H161sBWSa-VHWVtTo9Ba9q6mSOo_16QfF-U,85
6
6
  whatap/whatap.conf,sha256=E3UACbCDQ3-3XgDJWovbAtEN6LVLa7BQuCA0NXtX3CY,457
7
7
  whatap/agent/darwin/amd64/whatap_python,sha256=dLdQ2aVjuinboro4S8600b4sEA73RpFIHw72lvkSvRk,20802384
8
8
  whatap/agent/darwin/arm64/whatap_python,sha256=ar2scLqop8rVxB_ITdOrSIMlyTThQ6rF1V6pf1acqag,19784050
@@ -219,6 +219,7 @@ whatap/util/keygen.py,sha256=l25xGyb4ZUvEK0dtxuEvwwK74hJVL4AEgggZFwbygO8,223
219
219
  whatap/util/linked_list.py,sha256=OkqCYyMKZ63soqyhW8V9IDBqFIEi2HhwCmk2LG61La8,2990
220
220
  whatap/util/linked_map.py,sha256=wDmAS3WwDYsdRBp4QfGwzH4Jm4uCeno_AkRxpEeGSNI,9834
221
221
  whatap/util/metering_util.py,sha256=TGLrmfmhVz1vH6LHSoWXMa3n3eQlajItxO71OV0I-3Q,2857
222
+ whatap/util/native_process.py,sha256=0x7hRYf_zkMXMjanyrSoWvuHCU1hv6SLEjv9weT9XMY,17353
222
223
  whatap/util/request_double_queue.py,sha256=HnpuT9RzWZgRn7foQ15c9Ateb4GOQAny1W4QvaVimDs,1791
223
224
  whatap/util/request_queue.py,sha256=odbkRRx469Il_qT396eEnyS5kPIhfP2h5_WTMW1Rfd8,1585
224
225
  whatap/util/string_util.py,sha256=DgOyXVgCTr2rc4kfRLQZHqkHadjPDM8FM9s7RG1zaD4,313
@@ -250,8 +251,8 @@ whatap/value/text_hash_value.py,sha256=WPsSWzkR17plw3sgGjhJK0heCQiy0XdDYhawvBvQl
250
251
  whatap/value/text_value.py,sha256=8CWF57POGgosr5zEnGc0BdY0pYF0Vh8EsifHaRgfEwU,1148
251
252
  whatap/value/value.py,sha256=puRYlGm7q0iX3TKOqe_e0NdgK9Zj2J1t7NzqaQ8g7bU,578
252
253
  whatap/value/value_enum.py,sha256=AoUU4HUWC1bPan8k8CRBE9ayl0rGvJJHdRCTsamkWxE,2577
253
- whatap_python-2.2.0.dist-info/METADATA,sha256=cfxhCuVlEsg4QWlnF1h2RUJV4stuIYCzfPnacwU2YhY,2244
254
- whatap_python-2.2.0.dist-info/WHEEL,sha256=YVMoNqKzERt-wjUZwJ33xBGAwnFl-4cqbYkTtWa4itE,91
255
- whatap_python-2.2.0.dist-info/entry_points.txt,sha256=lAwpO3oqmPt5riREZO759dHWoM8dqv-vMpDWwDcMiJY,280
256
- whatap_python-2.2.0.dist-info/top_level.txt,sha256=8bRV-cNhEi4cSEIRQm5zDLRqkH1ki9YVrcfLIUVs5jg,7
257
- whatap_python-2.2.0.dist-info/RECORD,,
254
+ whatap_python-2.2.1rc1.dist-info/METADATA,sha256=rxpgsL2NJ_iqb-VqDWdeUY9oO-fKGzivBD7rIcYe7ns,2247
255
+ whatap_python-2.2.1rc1.dist-info/WHEEL,sha256=YVMoNqKzERt-wjUZwJ33xBGAwnFl-4cqbYkTtWa4itE,91
256
+ whatap_python-2.2.1rc1.dist-info/entry_points.txt,sha256=lAwpO3oqmPt5riREZO759dHWoM8dqv-vMpDWwDcMiJY,280
257
+ whatap_python-2.2.1rc1.dist-info/top_level.txt,sha256=8bRV-cNhEi4cSEIRQm5zDLRqkH1ki9YVrcfLIUVs5jg,7
258
+ whatap_python-2.2.1rc1.dist-info/RECORD,,