ProcessUtil.cpp
Go to the documentation of this file.
1/*
2 * This file is part of ArmarX.
3 *
4 * ArmarX is free software; you can redistribute it and/or modify
5 * it under the terms of the GNU General Public License version 2 as
6 * published by the Free Software Foundation.
7 *
8 * ArmarX is distributed in the hope that it will be useful, but
9 * WITHOUT ANY WARRANTY; without even the implied warranty of
10 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 * GNU General Public License for more details.
12 *
13 * You should have received a copy of the GNU General Public License
14 * along with this program. If not, see <http://www.gnu.org/licenses/>.
15 *
16 * @package ArmarXCore::core
17 * @copyright http://www.gnu.org/licenses/gpl-2.0.txt
18 * GNU General Public License
19 */
20
21#include "ProcessUtil.h"
22
23#include <algorithm>
24#include <cctype>
25#include <filesystem>
26#include <fstream>
27#include <optional>
28#include <string>
29#include <system_error>
30#include <thread>
31
33{
34 namespace
35 {
36 std::string
37 ProcPath(int pid, const std::string& entry = "")
38 {
39 return "/proc/" + std::to_string(pid) + entry;
40 }
41
42 std::string
43 BaseName(const std::string& path)
44 {
45 const auto lastSeparator = path.rfind('/');
46 return lastSeparator == std::string::npos ? path : path.substr(lastSeparator + 1);
47 }
48
49 /**
50 * Parses a /proc/<pid> directory name into a pid. Returns -1 for the non-numeric entries
51 * (self, thread-self, sys, ...).
52 */
53 int
54 PidFromProcEntry(const std::string& entry)
55 {
56 if (entry.empty() ||
57 !std::all_of(entry.begin(), entry.end(), [](unsigned char c)
58 { return std::isdigit(c) != 0; }))
59 {
60 return -1;
61 }
62
63 try
64 {
65 return std::stoi(entry);
66 }
67 catch (const std::exception&)
68 {
69 // A pid that does not fit into an int is not one of ours.
70 return -1;
71 }
72 }
73 } // namespace
74
75 bool
76 IsZombie(int pid)
77 {
78 if (pid <= 0)
79 {
80 return false;
81 }
82
83 // /proc/<pid>/stat is "<pid> (<comm>) <state> ...". The comm field is the only one that may
84 // contain spaces and parentheses, so the state is the character two positions after the
85 // *last* ')'.
86 std::ifstream stat(ProcPath(pid, "/stat"));
87 if (!stat.is_open())
88 {
89 return false;
90 }
91
92 std::string line;
93 if (!std::getline(stat, line))
94 {
95 return false;
96 }
97
98 const auto commEnd = line.rfind(')');
99 if (commEnd == std::string::npos || commEnd + 2 >= line.size())
100 {
101 return false;
102 }
103
104 return line[commEnd + 2] == 'Z';
105 }
106
107 bool
108 IsAlive(int pid)
109 {
110 if (pid <= 0)
111 {
112 return false;
113 }
114
115 std::error_code ec;
116 if (!std::filesystem::exists(ProcPath(pid), ec))
117 {
118 return false;
119 }
120
121 // A /proc entry that is only a corpse waiting to be reaped is not a live process. Counting
122 // one as alive is what made a stopped application hang the stop for the full termination
123 // timeout and then sit in Unknown, unstartable, for good: the process was dead, but its
124 // entry never went away because nothing waits for the applications we launch.
125 return !IsZombie(pid);
126 }
127
128 std::string
130 {
131 if (pid <= 0)
132 {
133 return {};
134 }
135
136 const std::vector<std::string> arguments = GetProcessArguments(pid);
137 return arguments.empty() ? std::string{} : BaseName(arguments.front());
138 }
139
140 std::vector<std::string>
142 {
143 if (pid <= 0)
144 {
145 return {};
146 }
147
148 std::ifstream cmdline(ProcPath(pid, "/cmdline"), std::ios::binary);
149 if (!cmdline.is_open())
150 {
151 return {};
152 }
153
154 // /proc/<pid>/cmdline is a sequence of NUL terminated strings; the first one is argv[0].
155 // It is empty for zombies, kernel threads and processes that have not exec'd yet.
156 std::vector<std::string> arguments;
157 std::string argument;
158 while (std::getline(cmdline, argument, '\0'))
159 {
160 arguments.push_back(argument);
161 }
162
163 if (arguments.empty() || arguments.front().empty())
164 {
165 return {};
166 }
167
168 return arguments;
169 }
170
171 std::vector<ProcessInfo>
173 {
174 std::vector<ProcessInfo> processes;
175
176 std::error_code ec;
177 std::filesystem::directory_iterator entries("/proc", ec);
178 if (ec)
179 {
180 return processes;
181 }
182
183 for (const auto& entry : entries)
184 {
185 const int pid = PidFromProcEntry(entry.path().filename().string());
186 if (pid < 0)
187 {
188 continue;
189 }
190
191 // Read the arguments before comparing anything: a process that exits while we walk
192 // simply yields an empty vector and is skipped.
193 std::vector<std::string> arguments = GetProcessArguments(pid);
194 if (arguments.empty())
195 {
196 continue;
197 }
198
199 std::string name = BaseName(arguments.front());
200 processes.push_back(ProcessInfo{pid, std::move(name), std::move(arguments)});
201 }
202
203 return processes;
204 }
205
206 std::optional<std::string>
207 FindIceConfigArgument(const std::vector<std::string>& arguments)
208 {
209 static const std::string prefix = "--Ice.Config=";
210 for (const std::string& argument : arguments)
211 {
212 if (argument.rfind(prefix, 0) == 0)
213 {
214 return argument.substr(prefix.size());
215 }
216 }
217 return std::nullopt;
218 }
219
221 IdentifyProcess(int pid, const std::string& executableName, const std::string& configPath)
222 {
223 if (pid <= 0 || executableName.empty())
224 {
226 }
227
228 const std::vector<std::string> arguments = GetProcessArguments(pid);
229 if (arguments.empty())
230 {
231 // Nothing to compare against. Saying "Differs" here would throw away the pid of a
232 // running application every time /proc could not be read, which is the very bug the
233 // Alive/Dead/Indeterminate split was introduced to fix.
235 }
236
237 if (BaseName(arguments.front()) != executableName)
238 {
239 return Identity::Differs;
240 }
241
242 // Same binary - but possibly a different scenario or instance of it. The config it was
243 // launched with is what tells them apart.
244 if (!configPath.empty())
245 {
246 const std::optional<std::string> configArgument = FindIceConfigArgument(arguments);
247 if (configArgument.has_value() &&
248 configArgument->find(configPath) == std::string::npos)
249 {
250 return Identity::Differs;
251 }
252 }
253
254 return Identity::Matches;
255 }
256
257 bool
258 WaitForExit(int pid, std::chrono::milliseconds timeout, std::chrono::milliseconds pollInterval)
259 {
260 const auto deadline = std::chrono::steady_clock::now() + timeout;
261 while (IsAlive(pid))
262 {
263 if (std::chrono::steady_clock::now() >= deadline)
264 {
265 return !IsAlive(pid);
266 }
267 std::this_thread::sleep_for(pollInterval);
268 }
269 return true;
270 }
271} // namespace ScenarioManager::Exec::ProcessUtil
constexpr T c
Identity
Whether the process behind a pid still is the one we think it is.
@ Unverifiable
argv could not be read, so nothing can be concluded either way.
@ Matches
argv says this is the expected executable, and it does not claim a foreign config.
@ Differs
argv says this is a different program, or the same program serving a different config.
std::string GetProcessName(int pid)
Reads the name of the executable behind pid (the basename of argv[0]).
Identity IdentifyProcess(int pid, const std::string &executableName, const std::string &configPath)
Checks whether the process with the given pid is still the application it is supposed to be.
std::optional< std::string > FindIceConfigArgument(const std::vector< std::string > &arguments)
bool WaitForExit(int pid, std::chrono::milliseconds timeout, std::chrono::milliseconds pollInterval)
Blocks until the process with the given pid has disappeared, at most for timeout.
std::vector< std::string > GetProcessArguments(int pid)
Reads the full argument vector of pid from /proc/<pid>/cmdline.
std::vector< ProcessInfo > ScanProcesses()
Walks /proc and returns every process whose argument vector could be read.
A live process as seen by ScanProcesses().
Definition ProcessUtil.h:73