bagger.cpp
Go to the documentation of this file.
1 /*
2  * Software License Agreement (BSD License)
3  *
4  * Copyright (c) 2018, Square Robot, Inc.
5  * All rights reserved.
6  *
7  * Redistribution and use in source and binary forms, with or without
8  * modification, are permitted provided that the following conditions
9  * are met:
10  *
11  * * Redistributions of source code must retain the above copyright
12  * notice, this list of conditions and the following disclaimer.
13  * * Redistributions in binary form must reproduce the above
14  * copyright notice, this list of conditions and the following
15  * disclaimer in the documentation and/or other materials provided
16  * with the distribution.
17  * * Neither the name of Square Robot, Inc. nor the names of its
18  * contributors may be used to endorse or promote products derived
19  * from this software without specific prior written permission.
20  *
21  * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
22  * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
23  * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
24  * FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
25  * COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
26  * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
27  * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
28  * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
29  * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
30  * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
31  * ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
32  * POSSIBILITY OF SUCH DAMAGE.
33  */
34 
35 #include <bagger/bagger.h>
36 
37 #include <fcntl.h>
38 #include <stdio.h>
39 #include <sys/stat.h>
40 #include <sys/wait.h>
41 #include <unistd.h>
42 
43 #include <boost/algorithm/string.hpp>
44 #include <boost/algorithm/string/split.hpp>
45 
47  name_ = "";
48  record_options_ = "";
49  record_pid_ = 0;
50  recording_ = false;
51 }
52 
53 RecordProcess::RecordProcess(std::string name, std::string record_options) {
54  name_ = name;
55  record_options_ = record_options;
56  record_pid_ = 0;
57  recording_ = false;
58 }
59 
61 }
62 
63 std::string RecordProcess::getName() {
64  return name_;
65 }
66 
68  return record_pid_;
69 }
70 
71 std::vector<std::string> RecordProcess::getRecordOptionsVector() {
72  // Split the options string at the spaces
73  std::vector<std::string> options_strings;
74  boost::split(options_strings, record_options_, boost::is_any_of(" "));
75 
76  std::vector<std::string> rov;
77 
78  rov.push_back("rosrun");
79  rov.push_back("rosbag");
80  rov.push_back("record");
81 
82  for (auto it = options_strings.begin(); it != options_strings.end(); ++it) {
83  rov.push_back(*it);
84  }
85 
86  return rov;
87 }
88 
90  return recording_;
91 }
92 
93 void RecordProcess::setName(std::string name) {
94  name_ = name;
95 }
96 
97 void RecordProcess::setRecording(bool recording) {
98  recording_ = recording;
99 }
100 
102  record_pid_ = pid;
103 }
104 
105 void RecordProcess::setRecordOptionString(std::string record_options) {
106  record_options_ = record_options;
107 }
108 
109 bool Bagger::onBagStateSrv(bagger::SetBagState::Request &request, bagger::SetBagState::Response &response) {
110  bool success = false;
111 
112  // double check that there is an entry for the specified profile in the profile name to options string map
113  if (profile_name_to_record_options_map_.find(request.bag_profile_name) != profile_name_to_record_options_map_.end()) {
114  // if trying to start a record, make sure that a bag isn't currently being recorded for the specified profile
115  if (request.set) { // request to start the bag
116  if (!profile_name_to_record_process_map_[request.bag_profile_name].getRecording()) {
117  // Start recording
118  pid_t pid_rr = fork();
119  if (pid_rr == 0) {
120  // Child process - successful fork - pass the vector's internal array to execvp
121  std::vector<std::string> command_strings =
122  profile_name_to_record_process_map_[request.bag_profile_name].getRecordOptionsVector();
123  std::vector<char *> commands;
124 
125  for (auto it = command_strings.begin(); it != command_strings.end(); ++it) {
126  commands.push_back(const_cast<char *>(it->c_str()));
127  }
128 
129  // add null to end (execvp expects null as last element)
130  commands.push_back(NULL);
131 
132  // redirect console output to /dev/null
133  int dev_null_fd = open("/dev/null", O_WRONLY);
134  // suppress console output from the rosbag record subprocess
135  dup2(dev_null_fd, 1);
136  dup2(dev_null_fd, 2);
137  close(dev_null_fd);
138 
139  if (execvp(commands[0], &commands[0]) != 0) {
140  // just ros error about it, its the child process, so we shouldn't interact with any service / message
141  ROS_ERROR("execvp failed! %s", strerror(errno));
142  }
143  } else if (pid_rr > 0) {
144  // parent process - successful fork
145  profile_name_to_record_process_map_[request.bag_profile_name].setRecordPID(pid_rr);
146  profile_name_to_record_process_map_[request.bag_profile_name].setRecording(true);
147  ROS_INFO("successfully started the record. profile: %s", request.bag_profile_name.c_str());
148  success = true;
149 
150  // updateBagNames();
151 
152  } else {
153  // fork failed!
154  ROS_ERROR("fork() failed\n");
155  }
156 
157  } else {
158  // already recording, - false
159  ROS_WARN("record profile %s already recording", request.bag_profile_name.c_str());
160  }
161  } else {
162  // request to stop the record
163  // make sure the requested record is recording
164  if (profile_name_to_record_process_map_[request.bag_profile_name].getRecording()) {
165  if (kill(profile_name_to_record_process_map_[request.bag_profile_name].getRecordPID(), SIGINT) == 0) {
166  ROS_INFO("successfully stopped the record. profile: %s", request.bag_profile_name.c_str());
167  profile_name_to_record_process_map_[request.bag_profile_name].setRecording(false);
168  success = true;
169  } else {
170  ROS_ERROR("kill() when stopping record profile %s failed", request.bag_profile_name.c_str());
171  }
172  } else {
173  // not recording, don't need to stop
174  ROS_WARN("record profile %s already not recording", request.bag_profile_name.c_str());
175  }
176  }
177  } else {
178  // The requested profile does not exist
179  ROS_ERROR("Requested record profile: %s does not exist. Check the configured "
180  "profile names",
181  request.bag_profile_name.c_str());
182  }
183 
184  response.success = success;
185 
186  // Publish the current states
187  publishBaggingStates();
188 
189  return true;
190 }
191 
193  bagger::BaggingState bs_msg;
194  bs_msg.header.stamp = ros::Time::now();
195 
196  // Sleep for a quarter second to make sure the new bag exists
197  ros::Duration(0.25).sleep();
198 
199  // First, fill out the profile names and whether or not each one has an active rosbag recording Second, for active
200  // profiles fill out the currently recording bag path and name
201  for (auto it = profile_name_to_record_process_map_.begin(); it != profile_name_to_record_process_map_.end(); ++it) {
202  bs_msg.bag_profile_names.push_back(it->first);
203  bs_msg.bagging.push_back(it->second.getRecording());
204 
205  if (it->second.getRecording()) {
206  // Get the record options for the profile name
207  bs_msg.bag_file_names.push_back(getBagNameFromRecordOptions(profile_name_to_record_options_map_[it->first]));
208  } else {
209  bs_msg.bag_file_names.push_back("");
210  }
211  }
212  bagging_state_publisher_.publish(bs_msg);
213 }
214 
215 std::string Bagger::getBagNameFromRecordOptions(std::string record_opts) {
216  std::string bag_name = "";
217  std::string cwd_string = getCurrentWorkingDirectory();
218 
219  if (record_opts.find("-o") != std::string::npos) {
220  // -o is the prefix argument. It means that rosbag record will record bags beginning with the passed prefix and
221  // ending with the date time, followed by .bag. If no directory exists in the passed prefix, The bag will be
222  // recorded in the current working directory.
223 
224  // Get the passed -o argument.
225  size_t start_index = record_opts.find("-o ") + 3;
226  size_t next_space_index = record_opts.find(" ", start_index);
227  std::string file_path = record_opts.substr(start_index, next_space_index - start_index);
228 
229  // If no directory is present in the argument, look up cwd and prepend it, resulting in file path without ".bag"
230  if (file_path.find("/") == std::string::npos) {
231  file_path = cwd_string + file_path;
232  }
233 
234  boost::filesystem::path full_file_name(file_path);
235 
236  // look in the file's directory for all files that contain the file's prefix
237  std::vector<boost::filesystem::path> matches =
238  getMatchingFilePathsInDirectory(full_file_name.parent_path(), full_file_name.stem().string());
239 
240  if (matches.size() > 0) {
241  boost::filesystem::path latest = matches[0];
242  for (auto i : matches) {
243  std::string s = i.stem().string();
244  s.erase(std::remove_if(s.begin(), s.end(), std::not1(std::ptr_fun((int (*)(int)) std::isdigit))), s.end());
245 
246  std::string latest_digits(latest.string());
247  latest_digits.erase(std::remove_if(latest_digits.begin(), latest_digits.end(),
248  std::not1(std::ptr_fun((int (*)(int)) std::isdigit))),
249  latest_digits.end());
250  if (s > latest_digits) {
251  latest = i;
252  }
253  }
254  bag_name = latest.string();
255  }
256  } else if (record_opts.find("-O") != std::string::npos) {
257  // The -O argument means that the bag's name is specified. If the passed name does not end in .bag, .bag is
258  // appended. If multiple bags are recorded with the same name, when rosbag record stops, it will append the .active
259  // bag to the existing .bag file. If no directory is passed as part of the name, the bag will be recorded in the
260  // current working dir
261 
262  size_t start_index = record_opts.find("-O ") + 3;
263  size_t next_space_index = record_opts.find(" ", start_index);
264  std::string file_path = record_opts.substr(start_index, next_space_index - start_index);
265 
266  // If the passed arg doesn't end in .bag, append it
267  if (file_path.find(".bag") == std::string::npos) {
268  file_path += ".bag";
269  }
270 
271  // If no directory is present in the argument, look up cwd and prepend it
272  if (file_path.find("/") == std::string::npos) {
273  file_path = cwd_string + file_path;
274  }
275  bag_name = file_path;
276 
277  } else {
278  // if nothing is passed, a dated rosbag will be created in the cwd. Find the most recent rosbag in the cwd without a
279  // prefix
280  const boost::filesystem::path cwd_path(cwd_string);
281  std::vector<boost::filesystem::path> bag_matches = getMatchingFilePathsInDirectory(cwd_path, ".bag.active");
282 
283  if (bag_matches.size() > 0) {
284  boost::filesystem::path latest("");
285 
286  for (auto i : bag_matches) {
287  std::string s = i.stem().string();
288  if (std::isdigit(s[0])) {
289  if (latest.string() == "") {
290  latest = i;
291  }
292  std::string latest_digits(latest.stem().string());
293  latest_digits.erase(std::remove_if(latest_digits.begin(), latest_digits.end(),
294  std::not1(std::ptr_fun((int (*)(int)) std::isdigit))),
295  latest_digits.end());
296 
297  s.erase(std::remove_if(s.begin(), s.end(), std::not1(std::ptr_fun((int (*)(int)) std::isdigit))), s.end());
298 
299  if (s > latest_digits) {
300  latest = i;
301  }
302  }
303  }
304 
305  bag_name = latest.string();
306  }
307  }
308 
309  // If the --split flag was specified, remove the tailing '_N'
310  if (record_opts.find("--split") != std::string::npos) {
311  auto last_underscore_index = bag_name.rfind('_');
312  if (last_underscore_index != std::string::npos) {
313  bag_name.erase(last_underscore_index);
314  }
315  }
316 
317  bag_name = removeSuffix(bag_name, ".active");
318  return removeSuffix(bag_name, ".bag");
319 }
320 
321 std::vector<boost::filesystem::path> Bagger::getMatchingFilePathsInDirectory(const boost::filesystem::path &dir_path,
322  std::string match_string) {
323  std::vector<boost::filesystem::path> paths;
324  if (exists(dir_path)) {
325  for (auto &entry : boost::make_iterator_range(boost::filesystem::directory_iterator(dir_path), {})) {
326  if (entry.path().string().find(match_string) != std::string::npos) {
327  paths.push_back(entry.path());
328  }
329  }
330  }
331  return paths;
332 }
333 
334 std::string Bagger::removeSuffix(std::string s, std::string suffix) {
335  if (s != suffix && s.size() > suffix.size() && s.substr(s.size() - suffix.size()) == suffix) {
336  s = s.substr(0, s.size() - suffix.size());
337  }
338  return s;
339 }
340 
342  char path[FILENAME_MAX];
343  if (!getcwd(path, sizeof(path))) {
344  return "";
345  }
346  return std::string(path) + "/";
347 }
348 
350  for (auto &pn_pair : profile_name_to_record_process_map_) {
351  int status;
352  pid_t wait_result = waitpid(pn_pair.second.getRecordPID(), &status, WNOHANG);
353  if (wait_result > 0) {
354  ROS_DEBUG_STREAM("Record for profile " << pn_pair.first << " has completed");
355  // In case the record process died without bagger killing it, ensure that bagger updates properly
356  pn_pair.second.setRecording(false);
357  publishBaggingStates();
358  }
359  }
360 }
361 
363  node_handle_ = ros::NodeHandlePtr(new ros::NodeHandle("bagger"));
364 
365  private_node_handle_ = ros::NodeHandlePtr(new ros::NodeHandle("~"));
366 
367  // Publish bag states - latching = true;
368  bagging_state_publisher_ = node_handle_->advertise<bagger::BaggingState>("bag_states", 1, true);
369 
370  bag_state_service_ = node_handle_->advertiseService("set_bag_state", &Bagger::onBagStateSrv, this);
371 
372  // Make sure that there is at least one pair of name and options, then read it in
373  if (private_node_handle_->hasParam("profile_name_to_record_options_map")) {
374  private_node_handle_->getParam("profile_name_to_record_options_map", profile_name_to_record_options_map_);
375  } else {
376  ROS_ERROR("Failed to find profile_name_to_bag_options_map in param server - "
377  "using default");
378  profile_name_to_record_options_map_["everything"] = "-a -O /tmp/everything.bag";
379  }
380 
381  // Go through the profile_name_to_record_options_map and set relevant info in the profile_name_to_record_process_map_
382  for (auto it = profile_name_to_record_options_map_.begin(); it != profile_name_to_record_options_map_.end(); ++it) {
383  RecordProcess rp(it->first, it->second);
384  profile_name_to_record_process_map_[it->first] = rp;
385  }
386 
387  publishBaggingStates();
388 }
389 int main(int argc, char **argv) {
390  ros::init(argc, argv, "bagger");
391  Bagger bg;
392  ros::Rate r(20);
393  while (ros::ok()) {
394  ros::spinOnce();
395  // clean up any child records that have completed
397  r.sleep();
398  }
399 }
int main(int argc, char **argv)
Definition: bagger.cpp:389
pid_t getRecordPID()
Definition: bagger.cpp:67
Definition: bagger.h:88
void attemptRecordCleanup()
Definition: bagger.cpp:349
XmlRpcServer s
bool sleep() const
ROSCPP_DECL void init(int &argc, char **argv, const std::string &name, uint32_t options=0)
bool getRecording()
Definition: bagger.cpp:89
boost::shared_ptr< NodeHandle > NodeHandlePtr
std::vector< std::string > getRecordOptionsVector()
Definition: bagger.cpp:71
#define ROS_WARN(...)
Bagger()
Definition: bagger.cpp:362
void setRecording(bool recording)
Definition: bagger.cpp:97
#define ROS_INFO(...)
bool recording_
whether or not the rosbag record process is currently recording
Definition: bagger.h:83
ROSCPP_DECL bool ok()
std::string getName()
Getters.
Definition: bagger.cpp:63
std::string getBagNameFromRecordOptions(std::string record_opts)
Utility function which infers the most likely rosbag name for the passed record options.
Definition: bagger.cpp:215
#define ROS_DEBUG_STREAM(args)
Utility class for managing each spawned rosbag record process.
Definition: bagger.h:59
bool sleep()
std::string removeSuffix(std::string s, std::string suffix)
Returns a version of the passed string with the passed suffix removed.
Definition: bagger.cpp:334
void publishBaggingStates()
Convenience function for publishing the recording states for each record profile. ...
Definition: bagger.cpp:192
static Time now()
bool onBagStateSrv(bagger::SetBagState::Request &request, bagger::SetBagState::Response &response)
Callback for receipt of set bag state service calls.
Definition: bagger.cpp:109
ROSCPP_DECL bool exists(const std::string &service_name, bool print_failure_reason)
std::vector< boost::filesystem::path > getMatchingFilePathsInDirectory(const boost::filesystem::path &dir_path, std::string match_string)
Definition: bagger.cpp:321
void setRecordOptionString(std::string record_options)
Definition: bagger.cpp:105
ROSCPP_DECL void spinOnce()
~RecordProcess()
Definition: bagger.cpp:60
#define ROS_ERROR(...)
void setName(std::string name)
Definition: bagger.cpp:93
pid_t record_pid_
pid of the spawned process - used to eventually terminate it
Definition: bagger.h:85
std::string getCurrentWorkingDirectory()
Utility function which returns the current working directory of the executing program.
Definition: bagger.cpp:341
std::string name_
name associated with the process - comes from the set record_profiles
Definition: bagger.h:79
std::string record_options_
record options associated with the process - comes from the set record_profiles
Definition: bagger.h:81
void setRecordPID(pid_t pid)
Setters.
Definition: bagger.cpp:101


bagger
Author(s): Brenden Gibbons
autogenerated on Tue Apr 27 2021 02:29:15