1 : // Copyright 2012 Google Inc. All Rights Reserved.
2 : //
3 : // Licensed under the Apache License, Version 2.0 (the "License");
4 : // you may not use this file except in compliance with the License.
5 : // You may obtain a copy of the License at
6 : //
7 : // http://www.apache.org/licenses/LICENSE-2.0
8 : //
9 : // Unless required by applicable law or agreed to in writing, software
10 : // distributed under the License is distributed on an "AS IS" BASIS,
11 : // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 : // See the License for the specific language governing permissions and
13 : // limitations under the License.
14 : //
15 : // This file defines the trace::agent_logger::LoggerApp class which implements
16 : // the LoggerApp RPC interface.
17 :
18 : #include "syzygy/trace/agent_logger/agent_logger_app.h"
19 :
20 : #include "base/bind.h"
21 : #include "base/environment.h"
22 : #include "base/path_service.h"
23 : #include "base/rand_util.h"
24 : #include "base/files/file_util.h"
25 : #include "base/process/kill.h"
26 : #include "base/process/launch.h"
27 : #include "base/process/process.h"
28 : #include "base/strings/string_util.h"
29 : #include "base/strings/stringprintf.h"
30 : #include "base/strings/utf_string_conversions.h"
31 : #include "base/win/scoped_handle.h"
32 : #include "syzygy/common/rpc/helpers.h"
33 : #include "syzygy/trace/agent_logger/agent_logger.h"
34 : #include "syzygy/trace/agent_logger/agent_logger_rpc_impl.h"
35 : #include "syzygy/trace/common/service_util.h"
36 : #include "syzygy/trace/protocol/call_trace_defs.h"
37 : #include "syzygy/trace/rpc/logger_rpc.h"
38 :
39 : namespace trace {
40 : namespace agent_logger {
41 :
42 : namespace {
43 :
44 : using ::common::rpc::GetInstanceString;
45 :
46 : // The usage string for the logger app.
47 : const char kUsageFormatStr[] =
48 : "Usage: %ls [options] ACTION [-- command]\n"
49 : " Supported actions:\n"
50 : " start Run a new logger instance in the foreground (blocking). You\n"
51 : " may optionally specify an external command which will be\n"
52 : " run behind the logger. The logger will return once the\n"
53 : " external command has terminated or the logger is externally\n"
54 : " stopped. If no command is specified, Ctrl-C or an invocation\n"
55 : " of the stop action will stop the logger.\n"
56 : " spawn Run a new logger instance in the background (non-blocking).\n"
57 : " stop Stop a separately running logger instance.\n"
58 : "\n"
59 : " Options:\n"
60 : " --append Append to (instead of truncating) the output\n"
61 : " file. This option is valid for the start and\n"
62 : " spawn actions.\n"
63 : " --instance-id=ID A unique (up to 16 character) ID to identify\n"
64 : " the logger instance.\n"
65 : " --minidump-dir=PATH The directory path in which minidumps, if any,\n"
66 : " should be generated.\n"
67 : " --output-file=PATH The file path to which logs should be written.\n"
68 : " This may be stdout (the default), stderr or a\n"
69 : " file path. This option is valid for the start\n"
70 : " and spawn actions.\n"
71 : " --unique-instance-id Automatically generate a unique ID for the\n"
72 : " logger instance.\n";
73 :
74 : // Names for kernel objects used to synchronize with a logger singleton.
75 : const wchar_t kLoggerMutexRoot[] = L"syzygy-logger-mutex";
76 : const wchar_t kLoggerStartEventRoot[] = L"syzygy-logger-started";
77 : const wchar_t kLoggerStopEventRoot[] = L"syzygy-logger-stopped";
78 :
79 : // A static location to which the current instance id can be saved. We
80 : // persist it here so that OnConsoleCtrl can have access to the instance
81 : // id when it is invoked on the signal handler thread.
82 : wchar_t saved_instance_id[LoggerApp::kMaxInstanceIdLength + 1] = { 0 };
83 :
84 : // Send a stop request via RPC to the logger instance given by @p instance_id.
85 : // If the process is remote and |handle| is specified, returns a handle to it.
86 : bool SendStopRequest(const base::StringPiece16& instance_id,
87 E : base::win::ScopedHandle* handle) {
88 E : std::wstring protocol(kLoggerRpcProtocol);
89 E : std::wstring endpoint(GetInstanceString(kLoggerRpcEndpointRoot, instance_id));
90 :
91 E : LOG(INFO) << "Stopping logging service instance at '"
92 : << endpoint << "' via " << protocol << '.';
93 :
94 E : handle_t binding = NULL;
95 E : if (!::common::rpc::CreateRpcBinding(protocol, endpoint, &binding)) {
96 i : LOG(ERROR) << "Failed to connect to logging service.";
97 i : return false;
98 : }
99 :
100 : // Get the process ID, and a handle to that process.
101 E : auto result = ::common::rpc::InvokeRpc(LoggerClient_GetProcessId, binding);
102 E : if (!result.succeeded() || result.result == 0) {
103 i : LOG(ERROR) << "Failed to get logging service process id.";
104 i : return false;
105 : }
106 E : if (handle != nullptr && result.result != ::GetCurrentProcessId())
107 E : handle->Set(::OpenProcess(SYNCHRONIZE, FALSE, result.result));
108 :
109 E : if (!::common::rpc::InvokeRpc(LoggerClient_Stop, binding).succeeded()) {
110 i : LOG(ERROR) << "Failed to stop logging service.";
111 i : return false;
112 : }
113 :
114 E : LOG(INFO) << "Logging service shutdown has been requested.";
115 :
116 E : return true;
117 E : }
118 :
119 : // Handler function to be called on exit signals (Ctrl-C, TERM, etc...).
120 i : BOOL WINAPI OnConsoleCtrl(DWORD ctrl_type) {
121 i : if (ctrl_type != CTRL_LOGOFF_EVENT) {
122 i : SendStopRequest(saved_instance_id, nullptr);
123 i : return TRUE;
124 : }
125 i : return FALSE;
126 i : }
127 :
128 : // A helper function to signal an event. This is passable as a callback to
129 : // a Logger instance to be called on logger start/stop.
130 E : bool SignalEvent(HANDLE event_handle, trace::common::Service* /* logger */) {
131 E : DCHECK_NE(INVALID_HANDLE_VALUE, event_handle);
132 E : if (!::SetEvent(event_handle))
133 i : return false;
134 E : return true;
135 E : }
136 :
137 : // A helper function which sets the Syzygy RPC instance id environment variable
138 : // then runs a given command line to completion.
139 : bool RunApp(const base::CommandLine& command_line,
140 : const std::wstring& instance_id,
141 : HANDLE interruption_event,
142 E : int* exit_code) {
143 E : DCHECK(exit_code != NULL);
144 E : std::unique_ptr<base::Environment> env(base::Environment::Create());
145 E : CHECK(env != NULL);
146 :
147 : // Put |instance_id| as the first value.
148 E : std::string env_var;
149 E : env->GetVar(kSyzygyRpcInstanceIdEnvVar, &env_var);
150 E : env->SetVar(kSyzygyRpcInstanceIdEnvVar,
151 : base::WideToUTF8(instance_id) + ";" + env_var);
152 :
153 E : LOG(INFO) << "Launching '" << command_line.GetProgram().value() << "'.";
154 E : VLOG(1) << "Command Line: " << command_line.GetCommandLineString();
155 :
156 E : *exit_code = 0;
157 :
158 : // Launch a new process in the background.
159 E : base::LaunchOptions options;
160 E : options.start_hidden = false;
161 E : base::Process process = base::LaunchProcess(command_line, options);
162 E : if (!process.IsValid()) {
163 i : LOG(ERROR)
164 : << "Failed to launch '" << command_line.GetProgram().value() << "'.";
165 i : return false;
166 : }
167 :
168 E : HANDLE objects[] = {process.Handle(), interruption_event};
169 E : DWORD num_objects = arraysize(objects);
170 E : switch (::WaitForMultipleObjects(num_objects, objects, FALSE, INFINITE)) {
171 : case WAIT_OBJECT_0 + 0: {
172 : // The client process has finished.
173 : DWORD temp_exit_code;
174 E : ::GetExitCodeProcess(process.Handle(), &temp_exit_code);
175 E : *exit_code = temp_exit_code;
176 E : process.Close();
177 E : return true;
178 : }
179 :
180 : case WAIT_OBJECT_0 + 1: {
181 : // The logger has been shutdown. Kill the client process.
182 i : process.Terminate(1, true);
183 i : process.Close();
184 i : *exit_code = 1;
185 i : return true;
186 : }
187 : }
188 :
189 : // If we get here then an error has occurred (since the timeout is infinite).
190 i : DWORD error = ::GetLastError();
191 i : LOG(ERROR) << "Error waiting for shutdown event " << ::common::LogWe(error)
192 : << ".";
193 i : return false;
194 E : }
195 :
196 : } // namespace
197 :
198 : // Keywords appearing on the command-line
199 : const wchar_t LoggerApp::kSpawn[] = L"spawn";
200 : const wchar_t LoggerApp::kStart[] = L"start";
201 : const wchar_t LoggerApp::kStatus[] = L"status";
202 : const wchar_t LoggerApp::kStop[] = L"stop";
203 : const char LoggerApp::kInstanceId[] = "instance-id";
204 : const char LoggerApp::kUniqueInstanceId[] = "unique-instance-id";
205 : const char LoggerApp::kOutputFile[] = "output-file";
206 : const char LoggerApp::kMiniDumpDir[] = "minidump-dir";
207 : const char LoggerApp::kAppend[] = "append";
208 : const wchar_t LoggerApp::kStdOut[] = L"stdout";
209 : const wchar_t LoggerApp::kStdErr[] = L"stderr";
210 :
211 : // A table mapping action keywords to their handler implementations.
212 : const LoggerApp::ActionTableEntry LoggerApp::kActionTable[] = {
213 E : { LoggerApp::kSpawn, &LoggerApp::Spawn },
214 E : { LoggerApp::kStart, &LoggerApp::Start },
215 E : { LoggerApp::kStatus, &LoggerApp::Status },
216 E : { LoggerApp::kStop, &LoggerApp::Stop },
217 E : };
218 :
219 : LoggerApp::LoggerApp()
220 E : : ::application::AppImplBase("AgentLogger"),
221 E : logger_command_line_(base::CommandLine::NO_PROGRAM),
222 E : action_handler_(NULL),
223 E : append_(false) {
224 E : }
225 :
226 E : LoggerApp::~LoggerApp() {
227 E : }
228 :
229 E : bool LoggerApp::ParseCommandLine(const base::CommandLine* command_line) {
230 E : DCHECK(command_line != NULL);
231 :
232 E : if (!trace::common::SplitCommandLine(
233 : command_line,
234 : &logger_command_line_,
235 : &app_command_line_)) {
236 i : LOG(ERROR) << "Failed to split command_line into logger and app parts.";
237 i : return false;
238 : }
239 :
240 : // Save the command-line in case we need to spawn.
241 E : command_line = &logger_command_line_;
242 :
243 E : if (command_line->HasSwitch(kInstanceId) &&
244 : command_line->HasSwitch(kUniqueInstanceId)) {
245 i : return Usage(command_line,
246 : base::StringPrintf("--%s and --%s are mutually exclusive.",
247 : kInstanceId,
248 : kUniqueInstanceId));
249 : }
250 :
251 : // Parse the instance id.
252 E : instance_id_ = command_line->GetSwitchValueNative(kInstanceId);
253 E : if (instance_id_.length() > kMaxInstanceIdLength) {
254 i : return Usage(command_line,
255 : base::StringPrintf("The instance id '%ls' is too long. "
256 : "The max length is %d characters.",
257 : instance_id_.c_str(),
258 : kMaxInstanceIdLength));
259 : }
260 :
261 : // Save the output file parameter.
262 E : output_file_path_ = command_line->GetSwitchValuePath(kOutputFile);
263 :
264 : // Save the minidump-dir parameter.
265 E : mini_dump_dir_ = command_line->GetSwitchValuePath(kMiniDumpDir);
266 E : if (mini_dump_dir_.empty()) {
267 E : CHECK(PathService::Get(base::DIR_CURRENT, &mini_dump_dir_));
268 E : } else {
269 E : mini_dump_dir_ = base::MakeAbsoluteFilePath(mini_dump_dir_);
270 E : if (mini_dump_dir_.empty())
271 i : return Usage(command_line, "The minidump-dir parameter is invalid.");
272 :
273 E : if (!base::DirectoryExists(mini_dump_dir_) &&
274 : !base::CreateDirectory(mini_dump_dir_)) {
275 i : LOG(ERROR) << "Failed to create minidump-dir "
276 : << mini_dump_dir_.value();
277 : }
278 : }
279 :
280 : // Make sure there's exactly one action.
281 E : if (command_line->GetArgs().size() != 1) {
282 E : return Usage(command_line,
283 : "Exactly 1 action is expected on the command line.");
284 : }
285 :
286 : // Check for the append flag.
287 E : append_ = command_line->HasSwitch(kAppend);
288 :
289 : // Parse the action.
290 E : action_ = command_line->GetArgs()[0];
291 E : const ActionTableEntry* entry = FindActionHandler(action_);
292 E : if (entry == NULL) {
293 E : return Usage(
294 : command_line,
295 : base::StringPrintf("Unrecognized action: %s.", action_.c_str()));
296 : }
297 :
298 E : if (command_line->HasSwitch(kUniqueInstanceId)) {
299 E : DWORD process_id = ::GetCurrentProcessId();
300 E : DWORD timestamp = ::GetTickCount();
301 E : base::SStringPrintf(&instance_id_, L"%08x%08x", process_id, timestamp);
302 E : DCHECK_EQ(kMaxInstanceIdLength, instance_id_.length());
303 : }
304 :
305 E : LOG(INFO) << "Using logger instance ID: '" << instance_id_ << "'.";
306 E : LOG(INFO) << "Writing minidumps to: " << mini_dump_dir_.value();
307 :
308 : // Setup the action handler.
309 E : DCHECK(entry->handler != NULL);
310 E : action_handler_ = entry->handler;
311 :
312 E : return true;
313 E : }
314 :
315 E : int LoggerApp::Run() {
316 E : DCHECK(action_handler_ != NULL);
317 E : if (!(this->*action_handler_)())
318 i : return 1;
319 E : return 0;
320 E : }
321 :
322 : // A helper function to find the handler method for a given action.
323 : const LoggerApp::ActionTableEntry* LoggerApp::FindActionHandler(
324 E : const base::StringPiece16& action) {
325 E : for (size_t i = 0; i < arraysize(kActionTable); ++i) {
326 E : if (::_wcsicmp(kActionTable[i].action, action.data()) == 0)
327 E : return &kActionTable[i];
328 E : }
329 E : return NULL;
330 E : }
331 :
332 E : bool LoggerApp::Start() {
333 E : std::wstring logger_name(
334 : GetInstanceString(kLoggerRpcEndpointRoot, instance_id_));
335 :
336 : // Acquire the logger mutex.
337 E : base::win::ScopedHandle mutex;
338 E : std::wstring mutex_name(GetInstanceString(kLoggerMutexRoot, instance_id_));
339 E : if (!trace::common::AcquireMutex(mutex_name, &mutex))
340 i : return false;
341 :
342 : // Setup the start event.
343 E : base::win::ScopedHandle start_event;
344 E : std::wstring start_event_name(
345 : GetInstanceString(kLoggerStartEventRoot, instance_id_));
346 E : if (!trace::common::InitEvent(start_event_name, &start_event)) {
347 i : LOG(ERROR) << "Unable to init start event for '" << logger_name << "'.";
348 i : return false;
349 : }
350 :
351 : // Setup the stop event.
352 E : base::win::ScopedHandle stop_event;
353 E : std::wstring stop_event_name(
354 : GetInstanceString(kLoggerStopEventRoot, instance_id_));
355 E : if (!trace::common::InitEvent(stop_event_name, &stop_event)) {
356 i : LOG(ERROR) << "Unable to init stop event for '" << logger_name << "'.";
357 i : return false;
358 : }
359 :
360 : // Setup an anonymous event to notify us if the logger has been
361 : // asynchronously asked to shutdown.
362 E : base::win::ScopedHandle interrupt_event;
363 E : if (!trace::common::InitEvent(L"", &interrupt_event)) {
364 i : LOG(ERROR) << "Unable to init interrupt event for '" << logger_name << "'.";
365 i : return false;
366 : }
367 :
368 : // Get the log file output_file.
369 E : FILE* output_file = NULL;
370 E : bool must_close_output_file = false;
371 E : base::ScopedFILE auto_close;
372 E : if (!OpenOutputFile(&output_file, &must_close_output_file)) {
373 i : LOG(ERROR) << "Unable to open '" << output_file_path_.value() << "'.";
374 i : return false;
375 : }
376 :
377 : // Setup auto_close as appropriate.
378 E : if (must_close_output_file)
379 E : auto_close.reset(output_file);
380 :
381 : // Initialize the logger instance.
382 E : AgentLogger logger;
383 E : logger.set_destination(output_file);
384 E : logger.set_minidump_dir(mini_dump_dir_);
385 E : logger.set_instance_id(instance_id_);
386 E : logger.set_started_callback(
387 : base::Bind(&SignalEvent, start_event.Get()));
388 E : logger.set_stopped_callback(
389 : base::Bind(&SignalEvent, stop_event.Get()));
390 E : logger.set_interrupted_callback(
391 : base::Bind(&SignalEvent, interrupt_event.Get()));
392 :
393 : // Save the instance_id for the Ctrl-C handler.
394 E : ::wcsncpy_s(saved_instance_id,
395 : arraysize(saved_instance_id),
396 : instance_id_.c_str(),
397 : _TRUNCATE);
398 :
399 : // Register the handler for Ctrl-C.
400 E : if (!SetConsoleCtrlHandler(&OnConsoleCtrl, TRUE)) {
401 i : DWORD error = ::GetLastError();
402 i : LOG(ERROR) << "Failed to register shutdown handler: "
403 : << ::common::LogWe(error) << ".";
404 i : return false;
405 : }
406 :
407 : // Start the logger.
408 E : RpcLoggerInstanceManager instance_manager(&logger);
409 E : if (!logger.Start()) {
410 i : LOG(ERROR) << "Failed to start '" << logger_name << "'.";
411 i : return false;
412 : }
413 :
414 E : bool error = false;
415 :
416 : // Run the logger, either standalone or as the parent of some application.
417 E : trace::common::ScopedConsoleCtrlHandler ctrl_handler;
418 E : if (app_command_line_.get() != NULL) {
419 : // We have a command to run, so launch that command and when it finishes
420 : // stop the logger.
421 E : int exit_code = 0;
422 : if (!RunApp(*app_command_line_, instance_id_, interrupt_event.Get(),
423 E : &exit_code) ||
424 : exit_code != 0) {
425 i : error = true;
426 : }
427 E : ignore_result(logger.Stop());
428 E : } else {
429 : // There is no command to wait for, so just register the control handler
430 : // (we stop the logger if this fails) and then let the logger run until
431 : // the control handler stops it or someone externally stops it using the
432 : // stop command.
433 E : if (!ctrl_handler.Init(&OnConsoleCtrl)) {
434 i : ignore_result(logger.Stop());
435 i : error = true;
436 : }
437 : }
438 :
439 : // Run the logger to completion.
440 E : if (!logger.Join()) {
441 i : LOG(ERROR) << "Failed running to completion '" << logger_name << "'.";
442 i : error = true;
443 : }
444 :
445 : // And we're done.
446 E : return !error;
447 E : }
448 :
449 i : bool LoggerApp::Status() {
450 : // TODO(rogerm): Implement me.
451 i : return false;
452 i : }
453 :
454 i : bool LoggerApp::Spawn() {
455 i : std::wstring logger_name(
456 : GetInstanceString(kLoggerRpcEndpointRoot, instance_id_));
457 :
458 i : LOG(INFO) << "Launching background logging service '" << logger_name << "'.";
459 :
460 : // Get the path to ourselves.
461 i : base::FilePath self_path;
462 i : PathService::Get(base::FILE_EXE, &self_path);
463 :
464 : // Build a command line for starting a new instance of the logger.
465 i : base::CommandLine new_command_line(self_path);
466 i : new_command_line.AppendArg("start");
467 :
468 : // Copy over any other switches.
469 : base::CommandLine::SwitchMap::const_iterator it =
470 i : logger_command_line_.GetSwitches().begin();
471 i : for (; it != logger_command_line_.GetSwitches().end(); ++it)
472 i : new_command_line.AppendSwitchNative(it->first, it->second);
473 :
474 : // Launch a new process in the background.
475 i : base::LaunchOptions options;
476 i : options.start_hidden = true;
477 i : base::Process process = base::LaunchProcess(new_command_line, options);
478 i : if (!process.IsValid()) {
479 i : LOG(ERROR) << "Failed to launch process.";
480 i : return false;
481 : }
482 :
483 : // Setup the start event.
484 i : base::win::ScopedHandle start_event;
485 i : std::wstring start_event_name(
486 : GetInstanceString(kLoggerStartEventRoot, instance_id_));
487 i : if (!trace::common::InitEvent(start_event_name, &start_event)) {
488 i : LOG(ERROR) << "Unable to init start event for '" << logger_name << "'.";
489 i : return false;
490 : }
491 :
492 : // We wait on both the start event and the process, as if the process fails
493 : // for any reason, it'll exit and its handle will become signaled.
494 i : HANDLE handles[] = {start_event.Get(), process.Handle()};
495 : if (::WaitForMultipleObjects(arraysize(handles),
496 : handles,
497 : FALSE,
498 i : INFINITE) != WAIT_OBJECT_0) {
499 i : LOG(ERROR) << "The logger '" << logger_name << "' exited in error.";
500 i : return false;
501 : }
502 :
503 i : LOG(INFO) << "Background logger '" << logger_name << "' is running.";
504 :
505 i : return true;
506 i : }
507 :
508 E : bool LoggerApp::Stop() {
509 E : std::wstring logger_name(
510 : GetInstanceString(kLoggerRpcEndpointRoot, instance_id_));
511 :
512 : // Setup the stop event.
513 E : base::win::ScopedHandle stop_event;
514 E : std::wstring stop_event_name(
515 : GetInstanceString(kLoggerStopEventRoot, instance_id_));
516 E : if (!trace::common::InitEvent(stop_event_name, &stop_event)) {
517 i : LOG(ERROR) << "Unable to init stop event for '" << logger_name << "'.";
518 i : return false;
519 : }
520 :
521 : // Send the stop request.
522 E : base::win::ScopedHandle process;
523 E : if (!SendStopRequest(instance_id_, &process))
524 i : return false;
525 :
526 : // Wait for the RPC event that indicates an orderly shutdown.
527 E : if (::WaitForSingleObject(stop_event.Get(), INFINITE) != WAIT_OBJECT_0) {
528 i : LOG(ERROR) << "Timed out waiting for '" << logger_name << "' to stop.";
529 i : return false;
530 : }
531 :
532 : // Wait on the process itself terminating.
533 E : if (process.IsValid()) {
534 E : if (::WaitForSingleObject(process.Get(), INFINITE) != WAIT_OBJECT_0) {
535 i : LOG(ERROR) << "Timed out waiting for logger process to terminate.";
536 i : return false;
537 : }
538 : }
539 :
540 E : LOG(INFO) << "The logger instance has stopped.";
541 :
542 E : return true;
543 E : }
544 :
545 : // Helper to resolve @p path to an open file. This will set @p must_close
546 : // to true if @path denotes a newly opened file, and false if it denotes
547 : // stderr or stdout.
548 E : bool LoggerApp::OpenOutputFile(FILE** output_file, bool* must_close) {
549 E : DCHECK(output_file != NULL);
550 E : DCHECK(must_close != NULL);
551 :
552 E : *output_file = NULL;
553 E : *must_close = false;
554 :
555 : // Check for stdout.
556 E : if (output_file_path_.empty() ||
557 : ::_wcsnicmp(output_file_path_.value().c_str(),
558 : kStdOut,
559 : arraysize(kStdOut)) == 0) {
560 E : *output_file = stdout;
561 E : return true;
562 : }
563 :
564 : // Check for stderr.
565 : if (::_wcsnicmp(output_file_path_.value().c_str(),
566 : kStdErr,
567 E : arraysize(kStdErr)) == 0) {
568 i : *output_file = stderr;
569 i : return true;
570 : }
571 :
572 : // Setup the write mode.
573 E : const char* mode = "wb";
574 E : if (append_)
575 i : mode = "ab";
576 :
577 : // Create a new file, which the caller is responsible for closing.
578 E : *output_file = base::OpenFile(output_file_path_, mode);
579 E : if (*output_file == NULL)
580 i : return false;
581 :
582 E : *must_close = true;
583 E : return true;
584 E : }
585 :
586 : // Print the usage/help text, plus an optional @p message.
587 : bool LoggerApp::Usage(const base::CommandLine* command_line,
588 E : const base::StringPiece& message) const {
589 E : if (!message.empty()) {
590 E : ::fwrite(message.data(), 1, message.length(), err());
591 E : ::fprintf(err(), "\n\n");
592 : }
593 :
594 E : ::fprintf(err(),
595 : kUsageFormatStr,
596 : command_line->GetProgram().BaseName().value().c_str());
597 :
598 E : return false;
599 E : }
600 :
601 : } // namespace agent_logger
602 : } // namespace trace
|