master
cpp 282 lines 9.48 KB
Raw
1 /*++
2
3 Copyright (c) Microsoft. All rights reserved.
4
5 Module Name:
6
7 telemetry.cpp
8
9 Abstract:
10
11 This file contains the telemetry agent implementation.
12
13 --*/
14
15 #include "common.h"
16 #include <dirent.h>
17 #include <errno.h>
18 #include <libgen.h>
19 #include <stdio.h>
20 #include <string.h>
21 #include <unistd.h>
22 #include <linux/connector.h>
23 #include <linux/netlink.h>
24 #include <linux/cn_proc.h>
25 #include <string>
26 #include <lxwil.h>
27 #include "util.h"
28 #include "mountutil.h"
29 #include "SocketChannel.h"
30 #include "message.h"
31
32 namespace {
33
34 #pragma GCC diagnostic push
35 #pragma GCC diagnostic ignored "-Wgnu-variable-sized-type-not-at-end"
36
37 union messageBuffer
38 {
39 struct
40 {
41 nlmsghdr netlinkHeader;
42 cn_msg connectorMessage;
43 proc_cn_mcast_op operation;
44 } Send;
45
46 struct
47 {
48 nlmsghdr netlinkHeader;
49 cn_msg connectorMessage;
50 proc_event event;
51 } Receive;
52 };
53
54 #pragma GCC diagnostic pop
55
56 // Map containing the binaries and commands that are filesystem-intensive that we want to warn users against using in DrvFs.
57 const std::map<std::string, std::string> g_drvFsUsageMap = {
58 {"git", "clone"}, {"node", "/usr/bin/npm install"}, {"cargo", "build"}};
59
60 bool g_drvFsUsageEnabled = true;
61
62 } // namespace
63
64 std::pair<std::string, bool> GetProcessInformation(int pid)
65 {
66 // N.B. Procfs files may no longer be present for short-lived processes that exit before the
67 // process creation notification can be processed.
68 const std::string procPidPath = std::format("/proc/{}", pid);
69 wil::unique_fd fd{open(std::format("{}/cmdline", procPidPath).c_str(), O_RDONLY)};
70 if (!fd)
71 {
72 return {};
73 }
74
75 // /proc/pid/cmdline contains all the arguments separated by NULL characters.
76 LX_MINI_INIT_TELEMETRY_MESSAGE message{};
77 message.Header.MessageSize = sizeof(message);
78 message.Header.MessageType = LX_MINI_INIT_TELEMETRY_MESSAGE::Type;
79
80 std::string commandLine(256, '\0');
81 auto bytesRead = TEMP_FAILURE_RETRY(read(fd.get(), commandLine.data(), commandLine.size()));
82 if (bytesRead <= 0)
83 {
84 return {};
85 }
86
87 commandLine.resize(bytesRead - 1);
88 const auto* executable = basename(commandLine.data());
89
90 bool showDrvfsNotification = false;
91
92 // Determine if the DrvFs perf notification should be displayed.
93 if (g_drvFsUsageEnabled)
94 {
95 // Check the if the binary name and first argument are in the list of scenarios.
96 auto found = g_drvFsUsageMap.find(executable);
97 if (found != g_drvFsUsageMap.end())
98 {
99 auto length = strlen(executable);
100 if (bytesRead > length + 1)
101 {
102 auto argument = std::string_view(commandLine.data(), bytesRead).substr(length + 1);
103 if (strcmp(argument.data(), found->second.c_str()) == 0)
104 {
105 // Determine if the current working directory is a DrvFs mount.
106 std::error_code errorCode;
107 auto cwd = std::filesystem::read_symlink(std::format("{}/cwd", procPidPath), errorCode);
108 if (!errorCode)
109 {
110 auto mountInfo = std::format("{}{}", procPidPath, MOUNT_INFO_FILE_NAME);
111 size_t prefixLength;
112 auto drvFsPrefix = UtilFindMount(mountInfo.c_str(), cwd.c_str(), false, &prefixLength);
113 if (!drvFsPrefix.empty())
114 {
115 showDrvfsNotification = true;
116 g_drvFsUsageEnabled = false;
117 }
118 }
119 }
120 }
121 }
122 }
123
124 return std::make_pair(executable, showDrvfsNotification);
125 }
126
127 unsigned int StartTelemetryAgent()
128 {
129 try
130 {
131 constexpr auto flushPeriod = std::chrono::minutes(30);
132
133 // The telemetry agent is only supported on VM mode.
134 if (!UtilIsUtilityVm())
135 {
136 return 1;
137 }
138
139 // Initialize logging.
140 InitializeLogging(false);
141
142 // Open and bind a netlink socket.
143 wil::unique_fd fd = socket(PF_NETLINK, SOCK_DGRAM, NETLINK_CONNECTOR);
144 THROW_LAST_ERROR_IF(!fd);
145
146 sockaddr_nl address{};
147 address.nl_family = AF_NETLINK;
148 address.nl_groups = CN_IDX_PROC;
149 address.nl_pid = getpid();
150 THROW_LAST_ERROR_IF(bind(fd.get(), reinterpret_cast<sockaddr*>(&address), sizeof(address)) < 0);
151
152 // Fill in the netlink header and connector message and send a message.
153 messageBuffer buffer{};
154 buffer.Send.netlinkHeader.nlmsg_len = sizeof(buffer.Send);
155 buffer.Send.netlinkHeader.nlmsg_type = NLMSG_DONE;
156 buffer.Send.netlinkHeader.nlmsg_pid = getpid();
157 buffer.Send.connectorMessage.id.idx = CN_IDX_PROC;
158 buffer.Send.connectorMessage.id.val = CN_VAL_PROC;
159 buffer.Send.connectorMessage.len = sizeof(buffer.Send.operation);
160 buffer.Send.operation = proc_cn_mcast_op::PROC_CN_MCAST_LISTEN;
161 auto bytes = send(fd.get(), &buffer, sizeof(buffer.Send), 0);
162 THROW_LAST_ERROR_IF(bytes != sizeof(buffer.Send));
163
164 // Set the receive timeout to 1 minute so the thread has an opportunity to flush even when no events are received.
165 timeval tv{};
166 tv.tv_sec = 10;
167 THROW_LAST_ERROR_IF(setsockopt(fd.get(), SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof(tv)) < 0);
168
169 wsl::shared::SocketChannel channel({STDOUT_FILENO}, "Telemetry");
170
171 std::map<std::string, size_t> events;
172 std::optional<std::string> drvfsNotifyCommand;
173
174 // Schedule the next flush in 30 seconds so that some events are captured even if WSL shuts down quickly.
175 auto nextFlush = std::chrono::steady_clock::now() + std::chrono::seconds(30);
176
177 // Begin reading netlink messages.
178 for (;;)
179 {
180 memset(&buffer, 0, sizeof(buffer));
181 sockaddr_nl fromAddress{};
182 fromAddress.nl_family = AF_NETLINK;
183 fromAddress.nl_groups = CN_IDX_PROC;
184 fromAddress.nl_pid = 1;
185 socklen_t addressLength = sizeof(fromAddress);
186 bytes = TEMP_FAILURE_RETRY(recvfrom(fd.get(), &buffer, sizeof(buffer), 0, reinterpret_cast<sockaddr*>(&fromAddress), &addressLength));
187
188 if (bytes <= 0)
189 {
190 THROW_LAST_ERROR_IF(errno != ETIMEDOUT && errno != EAGAIN);
191 }
192 else
193 {
194 for (nlmsghdr* netlinkHeader = &buffer.Receive.netlinkHeader; NLMSG_OK(netlinkHeader, bytes);
195 netlinkHeader = NLMSG_NEXT(netlinkHeader, bytes))
196 {
197 if ((netlinkHeader->nlmsg_type == NLMSG_ERROR) || (netlinkHeader->nlmsg_type == NLMSG_OVERRUN))
198 {
199 break;
200 }
201
202 if (netlinkHeader->nlmsg_type == NLMSG_NOOP)
203 {
204 continue;
205 }
206
207 // For exec events, log app usage telemetry.
208 auto event = reinterpret_cast<proc_event*>(((cn_msg*)NLMSG_DATA(netlinkHeader))->data);
209 if (event->what == PROC_EVENT_EXEC)
210 {
211 auto [executable, showNotification] = GetProcessInformation(event->event_data.exec.process_pid);
212 if (showNotification)
213 {
214 drvfsNotifyCommand = executable;
215 }
216
217 // Make sure the name doesn't contain a '/' so it doesn't break our message format.
218 if (!executable.empty() && executable.find("/") == std::string::npos)
219 {
220 auto it = events.find(executable);
221 if (it == events.end())
222 {
223 events.emplace(std::move(executable), 1);
224 }
225 else
226 {
227 it->second++;
228 }
229 }
230 }
231
232 if (netlinkHeader->nlmsg_type == NLMSG_DONE)
233 {
234 break;
235 }
236 }
237 }
238
239 // Regularly flush messages back to the service.
240
241 auto now = std::chrono::steady_clock::now();
242 if (drvfsNotifyCommand.has_value() || now > nextFlush)
243 {
244 if (!events.empty())
245 {
246 std::stringstream content;
247
248 if (drvfsNotifyCommand.has_value())
249 {
250 content << drvfsNotifyCommand.value() << "/1/";
251 }
252
253 for (auto [executable, count] : events)
254 {
255 // Having an extra ',' at the end makes parsing simpler.
256 content << executable << '/' << count << '/';
257 }
258
259 wsl::shared::MessageWriter<LX_MINI_INIT_TELEMETRY_MESSAGE> message;
260 message->ShowDrvFsNotification = drvfsNotifyCommand.has_value();
261 message.WriteString(content.str());
262
263 channel.SendMessage<LX_MINI_INIT_TELEMETRY_MESSAGE>(message.Span());
264
265 events.clear();
266 drvfsNotifyCommand = {};
267 }
268
269 nextFlush = now + flushPeriod;
270 }
271 }
272
273 return 0;
274 }
275 catch (...)
276 {
277 LOG_CAUGHT_EXCEPTION();
278 return 1;
279 }
280
281 return 0;
282 }