Skip to content

Commit 2ebebfc

Browse files
committed
Merge branch 'mc/tr2-process-ancestry-cleanup' into next
Add process ancestry data to trace2 on macOS to match what we already do on Linux and Windows. Also adjust the way Windows implementation reports this information to match the other two. * mc/tr2-process-ancestry-cleanup: t0213: add trace2 cmd_ancestry tests test-tool: extend trace2 helper with 400ancestry trace2: emit cmd_ancestry data for Windows trace2: refactor Windows process ancestry trace2 event build: include procinfo.c impl for macOS trace2: add macOS process ancestry tracing
2 parents 221d062 + 3c8c638 commit 2ebebfc

9 files changed

Lines changed: 379 additions & 27 deletions

File tree

compat/darwin/procinfo.c

Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
#include "git-compat-util.h"
2+
#include "strbuf.h"
3+
#include "strvec.h"
4+
#include "trace2.h"
5+
#include <sys/sysctl.h>
6+
7+
/*
8+
* An arbitrarily chosen value to limit the depth of the ancestor chain.
9+
*/
10+
#define NR_PIDS_LIMIT 10
11+
12+
/*
13+
* Get the process name and parent PID for a given PID using sysctl().
14+
* Returns 0 on success, -1 on failure.
15+
*/
16+
static int get_proc_info(pid_t pid, struct strbuf *name, pid_t *ppid)
17+
{
18+
int mib[4];
19+
struct kinfo_proc proc;
20+
size_t size = sizeof(proc);
21+
22+
mib[0] = CTL_KERN;
23+
mib[1] = KERN_PROC;
24+
mib[2] = KERN_PROC_PID;
25+
mib[3] = pid;
26+
27+
if (sysctl(mib, 4, &proc, &size, NULL, 0) < 0)
28+
return -1;
29+
30+
if (size == 0)
31+
return -1;
32+
33+
strbuf_addstr(name, proc.kp_proc.p_comm);
34+
*ppid = proc.kp_eproc.e_ppid;
35+
36+
return 0;
37+
}
38+
39+
/*
40+
* Recursively push process names onto the ancestry array.
41+
* We guard against cycles by limiting the depth to NR_PIDS_LIMIT.
42+
*/
43+
static void push_ancestry_name(struct strvec *names, pid_t pid, int depth)
44+
{
45+
struct strbuf name = STRBUF_INIT;
46+
pid_t ppid;
47+
48+
if (depth >= NR_PIDS_LIMIT)
49+
return;
50+
51+
if (pid <= 0)
52+
return;
53+
54+
if (get_proc_info(pid, &name, &ppid) < 0)
55+
goto cleanup;
56+
57+
strvec_push(names, name.buf);
58+
59+
/*
60+
* Recurse to the parent process. Stop if ppid not valid
61+
* or if we've reached ourselves (cycle).
62+
*/
63+
if (ppid && ppid != pid)
64+
push_ancestry_name(names, ppid, depth + 1);
65+
66+
cleanup:
67+
strbuf_release(&name);
68+
}
69+
70+
void trace2_collect_process_info(enum trace2_process_info_reason reason)
71+
{
72+
struct strvec names = STRVEC_INIT;
73+
74+
if (!trace2_is_enabled())
75+
return;
76+
77+
switch (reason) {
78+
case TRACE2_PROCESS_INFO_STARTUP:
79+
push_ancestry_name(&names, getppid(), 0);
80+
if (names.nr)
81+
trace2_cmd_ancestry(names.v);
82+
83+
strvec_clear(&names);
84+
break;
85+
86+
case TRACE2_PROCESS_INFO_EXIT:
87+
/*
88+
* The Windows version of this calls its
89+
* get_peak_memory_info() here. We may want to insert
90+
* similar process-end statistics here in the future.
91+
*/
92+
break;
93+
94+
default:
95+
BUG("trace2_collect_process_info: unknown reason '%d'", reason);
96+
}
97+
}

compat/win32/trace2_win32_process_info.c

Lines changed: 33 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
#include "../../git-compat-util.h"
44
#include "../../json-writer.h"
55
#include "../../repository.h"
6+
#include "../../strvec.h"
67
#include "../../trace2.h"
78
#include "lazyload.h"
89
#include <psapi.h>
@@ -32,12 +33,7 @@ static int find_pid(DWORD pid, HANDLE hSnapshot, PROCESSENTRY32 *pe32)
3233
}
3334

3435
/*
35-
* Accumulate JSON array of our parent processes:
36-
* [
37-
* exe-name-parent,
38-
* exe-name-grand-parent,
39-
* ...
40-
* ]
36+
* Accumulate array of our parent process names.
4137
*
4238
* Note: we only report the filename of the process executable; the
4339
* only way to get its full pathname is to use OpenProcess()
@@ -73,7 +69,7 @@ static int find_pid(DWORD pid, HANDLE hSnapshot, PROCESSENTRY32 *pe32)
7369
* simple and avoid the alloc/realloc overhead. It is OK if we
7470
* truncate the search and return a partial answer.
7571
*/
76-
static void get_processes(struct json_writer *jw, HANDLE hSnapshot)
72+
static void get_processes(struct strvec *names, HANDLE hSnapshot)
7773
{
7874
PROCESSENTRY32 pe32;
7975
DWORD pid;
@@ -82,19 +78,19 @@ static void get_processes(struct json_writer *jw, HANDLE hSnapshot)
8278

8379
pid = GetCurrentProcessId();
8480
while (find_pid(pid, hSnapshot, &pe32)) {
85-
/* Only report parents. Omit self from the JSON output. */
81+
/* Only report parents. Omit self from the output. */
8682
if (nr_pids)
87-
jw_array_string(jw, pe32.szExeFile);
83+
strvec_push(names, pe32.szExeFile);
8884

8985
/* Check for cycle in snapshot. (Yes, it happened.) */
9086
for (k = 0; k < nr_pids; k++)
9187
if (pid == pid_list[k]) {
92-
jw_array_string(jw, "(cycle)");
88+
strvec_push(names, "(cycle)");
9389
return;
9490
}
9591

9692
if (nr_pids == NR_PIDS_LIMIT) {
97-
jw_array_string(jw, "(truncated)");
93+
strvec_push(names, "(truncated)");
9894
return;
9995
}
10096

@@ -105,24 +101,14 @@ static void get_processes(struct json_writer *jw, HANDLE hSnapshot)
105101
}
106102

107103
/*
108-
* Emit JSON data for the current and parent processes. Individual
109-
* trace2 targets can decide how to actually print it.
104+
* Collect the list of parent process names.
110105
*/
111-
static void get_ancestry(void)
106+
static void get_ancestry(struct strvec *names)
112107
{
113108
HANDLE hSnapshot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
114109

115110
if (hSnapshot != INVALID_HANDLE_VALUE) {
116-
struct json_writer jw = JSON_WRITER_INIT;
117-
118-
jw_array_begin(&jw, 0);
119-
get_processes(&jw, hSnapshot);
120-
jw_end(&jw);
121-
122-
trace2_data_json("process", the_repository, "windows/ancestry",
123-
&jw);
124-
125-
jw_release(&jw);
111+
get_processes(names, hSnapshot);
126112
CloseHandle(hSnapshot);
127113
}
128114
}
@@ -176,13 +162,35 @@ static void get_peak_memory_info(void)
176162

177163
void trace2_collect_process_info(enum trace2_process_info_reason reason)
178164
{
165+
struct strvec names = STRVEC_INIT;
166+
179167
if (!trace2_is_enabled())
180168
return;
181169

182170
switch (reason) {
183171
case TRACE2_PROCESS_INFO_STARTUP:
184172
get_is_being_debugged();
185-
get_ancestry();
173+
get_ancestry(&names);
174+
if (names.nr) {
175+
/*
176+
Emit the ancestry data as a data_json event to
177+
maintain compatibility for consumers of the older
178+
"windows/ancestry" event.
179+
*/
180+
struct json_writer jw = JSON_WRITER_INIT;
181+
jw_array_begin(&jw, 0);
182+
for (size_t i = 0; i < names.nr; i++)
183+
jw_array_string(&jw, names.v[i]);
184+
jw_end(&jw);
185+
trace2_data_json("process", the_repository,
186+
"windows/ancestry", &jw);
187+
jw_release(&jw);
188+
189+
/* Emit the ancestry data with the new event. */
190+
trace2_cmd_ancestry(names.v);
191+
}
192+
193+
strvec_clear(&names);
186194
return;
187195

188196
case TRACE2_PROCESS_INFO_EXIT:

config.mak.uname

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -149,6 +149,8 @@ ifeq ($(uname_S),Darwin)
149149
HAVE_NS_GET_EXECUTABLE_PATH = YesPlease
150150
CSPRNG_METHOD = arc4random
151151
USE_ENHANCED_BASIC_REGULAR_EXPRESSIONS = YesPlease
152+
HAVE_PLATFORM_PROCINFO = YesPlease
153+
COMPAT_OBJS += compat/darwin/procinfo.o
152154

153155
ifeq ($(uname_M),arm64)
154156
HOMEBREW_PREFIX = /opt/homebrew

contrib/buildsystems/CMakeLists.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -274,6 +274,8 @@ if(CMAKE_SYSTEM_NAME STREQUAL "Windows")
274274
elseif(CMAKE_SYSTEM_NAME STREQUAL "Linux")
275275
add_compile_definitions(PROCFS_EXECUTABLE_PATH="/proc/self/exe" HAVE_DEV_TTY )
276276
list(APPEND compat_SOURCES unix-socket.c unix-stream-server.c compat/linux/procinfo.c)
277+
elseif(CMAKE_SYSTEM_NAME STREQUAL "Darwin")
278+
list(APPEND compat_SOURCES compat/darwin/procinfo.c)
277279
endif()
278280

279281
if(CMAKE_SYSTEM_NAME STREQUAL "Windows")

meson.build

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1294,6 +1294,8 @@ if host_machine.system() == 'linux'
12941294
libgit_sources += 'compat/linux/procinfo.c'
12951295
elif host_machine.system() == 'windows'
12961296
libgit_sources += 'compat/win32/trace2_win32_process_info.c'
1297+
elif host_machine.system() == 'darwin'
1298+
libgit_sources += 'compat/darwin/procinfo.c'
12971299
else
12981300
libgit_sources += 'compat/stub/procinfo.c'
12991301
endif

t/helper/test-trace2.c

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -466,6 +466,63 @@ static int ut_303redact_def_param(int argc, const char **argv)
466466
return 0;
467467
}
468468

469+
/*
470+
* Run a child process with specific trace2 environment settings so that
471+
* we can capture its trace2 output (including cmd_ancestry) in isolation.
472+
*
473+
* test-tool trace2 400ancestry <target> <output_file> [<child_command_line>]
474+
*
475+
* <target> is one of: normal, perf, event
476+
*
477+
* For example:
478+
* test-tool trace2 400ancestry normal out.normal test-tool trace2 001return 0
479+
*
480+
* The child process inherits a controlled trace2 environment where only
481+
* the specified target is directed to <output_file>. The parent's trace2
482+
* environment variables are cleared in the child so that only the child's
483+
* events are captured.
484+
*
485+
* This is used by t0213-trace2-ancestry.sh to test cmd_ancestry events.
486+
* The child process will see "test-tool" as its immediate parent in the
487+
* process ancestry, giving us a predictable value to verify.
488+
*/
489+
static int ut_400ancestry(int argc, const char **argv)
490+
{
491+
struct child_process cmd = CHILD_PROCESS_INIT;
492+
const char *target;
493+
const char *outfile;
494+
int result;
495+
496+
if (argc < 3)
497+
die("expect <target> <output_file> <child_command_line>");
498+
499+
target = argv[0];
500+
outfile = argv[1];
501+
argv += 2;
502+
argc -= 2;
503+
504+
/* Clear all trace2 environment variables in the child. */
505+
strvec_push(&cmd.env, "GIT_TRACE2=");
506+
strvec_push(&cmd.env, "GIT_TRACE2_PERF=");
507+
strvec_push(&cmd.env, "GIT_TRACE2_EVENT=");
508+
strvec_push(&cmd.env, "GIT_TRACE2_BRIEF=1");
509+
510+
/* Set only the requested target. */
511+
if (!strcmp(target, "normal"))
512+
strvec_pushf(&cmd.env, "GIT_TRACE2=%s", outfile);
513+
else if (!strcmp(target, "perf"))
514+
strvec_pushf(&cmd.env, "GIT_TRACE2_PERF=%s", outfile);
515+
else if (!strcmp(target, "event"))
516+
strvec_pushf(&cmd.env, "GIT_TRACE2_EVENT=%s", outfile);
517+
else
518+
die("invalid target '%s', expected: normal, perf, event",
519+
target);
520+
521+
strvec_pushv(&cmd.args, argv);
522+
result = run_command(&cmd);
523+
exit(result);
524+
}
525+
469526
/*
470527
* Usage:
471528
* test-tool trace2 <ut_name_1> <ut_usage_1>
@@ -497,6 +554,8 @@ static struct unit_test ut_table[] = {
497554
{ ut_301redact_child_start, "301redact_child_start", "<argv...>" },
498555
{ ut_302redact_exec, "302redact_exec", "<exe> <argv...>" },
499556
{ ut_303redact_def_param, "303redact_def_param", "<key> <value>" },
557+
558+
{ ut_400ancestry, "400ancestry", "<target> <output_file> [<child_command_line>]" },
500559
};
501560
/* clang-format on */
502561

t/meson.build

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -134,6 +134,7 @@ integration_tests = [
134134
't0210-trace2-normal.sh',
135135
't0211-trace2-perf.sh',
136136
't0212-trace2-event.sh',
137+
't0213-trace2-ancestry.sh',
137138
't0300-credentials.sh',
138139
't0301-credential-cache.sh',
139140
't0302-credential-store.sh',

t/t0210-trace2-normal.sh

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -74,8 +74,9 @@ scrub_normal () {
7474
# This line is only emitted when RUNTIME_PREFIX is defined,
7575
# so just omit it for testing purposes.
7676
#
77-
# 4. 'cmd_ancestry' is not implemented everywhere, so for portability's
78-
# sake, skip it when parsing normal.
77+
# 4. 'cmd_ancestry' output depends on how the test is run and
78+
# is not relevant to the features we are testing here.
79+
# Ancestry tests are covered in t0213-trace2-ancestry.sh instead.
7980
sed \
8081
-e 's/elapsed:[0-9]*\.[0-9][0-9]*\([eE][-+]\{0,1\}[0-9][0-9]*\)\{0,1\}/elapsed:_TIME_/g' \
8182
-e "s/^start '[^']*' \(.*\)/start _EXE_ \1/" \

0 commit comments

Comments
 (0)