/Users/buildslave/jenkins/workspace/coverage/llvm-project/lldb/tools/lldb-server/lldb-gdbserver.cpp
Line | Count | Source (jump to first uncovered line) |
1 | | //===-- lldb-gdbserver.cpp --------------------------------------*- C++ -*-===// |
2 | | // |
3 | | // Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions. |
4 | | // See https://llvm.org/LICENSE.txt for license information. |
5 | | // SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception |
6 | | // |
7 | | //===----------------------------------------------------------------------===// |
8 | | |
9 | | #include <cerrno> |
10 | | #include <cstdint> |
11 | | #include <cstdio> |
12 | | #include <cstdlib> |
13 | | #include <cstring> |
14 | | |
15 | | #ifndef _WIN32 |
16 | | #include <csignal> |
17 | | #include <unistd.h> |
18 | | #endif |
19 | | |
20 | | #include "LLDBServerUtilities.h" |
21 | | #include "Plugins/Process/gdb-remote/GDBRemoteCommunicationServerLLGS.h" |
22 | | #include "Plugins/Process/gdb-remote/ProcessGDBRemoteLog.h" |
23 | | #include "lldb/Host/Config.h" |
24 | | #include "lldb/Host/ConnectionFileDescriptor.h" |
25 | | #include "lldb/Host/FileSystem.h" |
26 | | #include "lldb/Host/Pipe.h" |
27 | | #include "lldb/Host/Socket.h" |
28 | | #include "lldb/Host/common/NativeProcessProtocol.h" |
29 | | #include "lldb/Target/Process.h" |
30 | | #include "lldb/Utility/LLDBLog.h" |
31 | | #include "lldb/Utility/Status.h" |
32 | | #include "llvm/ADT/StringRef.h" |
33 | | #include "llvm/Option/ArgList.h" |
34 | | #include "llvm/Option/OptTable.h" |
35 | | #include "llvm/Option/Option.h" |
36 | | #include "llvm/Support/Errno.h" |
37 | | #include "llvm/Support/Error.h" |
38 | | #include "llvm/Support/WithColor.h" |
39 | | |
40 | | #if defined(__linux__) |
41 | | #include "Plugins/Process/Linux/NativeProcessLinux.h" |
42 | | #elif defined(__FreeBSD__) |
43 | | #include "Plugins/Process/FreeBSD/NativeProcessFreeBSD.h" |
44 | | #elif defined(__NetBSD__) |
45 | | #include "Plugins/Process/NetBSD/NativeProcessNetBSD.h" |
46 | | #elif defined(_WIN32) |
47 | | #include "Plugins/Process/Windows/Common/NativeProcessWindows.h" |
48 | | #endif |
49 | | |
50 | | #ifndef LLGS_PROGRAM_NAME |
51 | 0 | #define LLGS_PROGRAM_NAME "lldb-server" |
52 | | #endif |
53 | | |
54 | | #ifndef LLGS_VERSION_STR |
55 | 0 | #define LLGS_VERSION_STR "local_build" |
56 | | #endif |
57 | | |
58 | | using namespace llvm; |
59 | | using namespace lldb; |
60 | | using namespace lldb_private; |
61 | | using namespace lldb_private::lldb_server; |
62 | | using namespace lldb_private::process_gdb_remote; |
63 | | |
64 | | namespace { |
65 | | #if defined(__linux__) |
66 | | typedef process_linux::NativeProcessLinux::Manager NativeProcessManager; |
67 | | #elif defined(__FreeBSD__) |
68 | | typedef process_freebsd::NativeProcessFreeBSD::Manager NativeProcessManager; |
69 | | #elif defined(__NetBSD__) |
70 | | typedef process_netbsd::NativeProcessNetBSD::Manager NativeProcessManager; |
71 | | #elif defined(_WIN32) |
72 | | typedef NativeProcessWindows::Manager NativeProcessManager; |
73 | | #else |
74 | | // Dummy implementation to make sure the code compiles |
75 | | class NativeProcessManager : public NativeProcessProtocol::Manager { |
76 | | public: |
77 | | NativeProcessManager(MainLoop &mainloop) |
78 | 0 | : NativeProcessProtocol::Manager(mainloop) {} |
79 | | |
80 | | llvm::Expected<std::unique_ptr<NativeProcessProtocol>> |
81 | | Launch(ProcessLaunchInfo &launch_info, |
82 | 0 | NativeProcessProtocol::NativeDelegate &native_delegate) override { |
83 | 0 | llvm_unreachable("Not implemented"); |
84 | 0 | } |
85 | | llvm::Expected<std::unique_ptr<NativeProcessProtocol>> |
86 | | Attach(lldb::pid_t pid, |
87 | 0 | NativeProcessProtocol::NativeDelegate &native_delegate) override { |
88 | 0 | llvm_unreachable("Not implemented"); |
89 | 0 | } |
90 | | }; |
91 | | #endif |
92 | | } |
93 | | |
94 | | #ifndef _WIN32 |
95 | | // Watch for signals |
96 | | static int g_sighup_received_count = 0; |
97 | | |
98 | 0 | static void sighup_handler(MainLoopBase &mainloop) { |
99 | 0 | ++g_sighup_received_count; |
100 | |
|
101 | 0 | Log *log = GetLog(LLDBLog::Process); |
102 | 0 | LLDB_LOGF(log, "lldb-server:%s swallowing SIGHUP (receive count=%d)", |
103 | 0 | __FUNCTION__, g_sighup_received_count); |
104 | |
|
105 | 0 | if (g_sighup_received_count >= 2) |
106 | 0 | mainloop.RequestTermination(); |
107 | 0 | } |
108 | | #endif // #ifndef _WIN32 |
109 | | |
110 | | void handle_attach_to_pid(GDBRemoteCommunicationServerLLGS &gdb_server, |
111 | 0 | lldb::pid_t pid) { |
112 | 0 | Status error = gdb_server.AttachToProcess(pid); |
113 | 0 | if (error.Fail()) { |
114 | 0 | fprintf(stderr, "error: failed to attach to pid %" PRIu64 ": %s\n", pid, |
115 | 0 | error.AsCString()); |
116 | 0 | exit(1); |
117 | 0 | } |
118 | 0 | } |
119 | | |
120 | | void handle_attach_to_process_name(GDBRemoteCommunicationServerLLGS &gdb_server, |
121 | 0 | const std::string &process_name) { |
122 | | // FIXME implement. |
123 | 0 | } |
124 | | |
125 | | void handle_attach(GDBRemoteCommunicationServerLLGS &gdb_server, |
126 | 0 | const std::string &attach_target) { |
127 | 0 | assert(!attach_target.empty() && "attach_target cannot be empty"); |
128 | | |
129 | | // First check if the attach_target is convertible to a long. If so, we'll use |
130 | | // it as a pid. |
131 | 0 | char *end_p = nullptr; |
132 | 0 | const long int pid = strtol(attach_target.c_str(), &end_p, 10); |
133 | | |
134 | | // We'll call it a match if the entire argument is consumed. |
135 | 0 | if (end_p && |
136 | 0 | static_cast<size_t>(end_p - attach_target.c_str()) == |
137 | 0 | attach_target.size()) |
138 | 0 | handle_attach_to_pid(gdb_server, static_cast<lldb::pid_t>(pid)); |
139 | 0 | else |
140 | 0 | handle_attach_to_process_name(gdb_server, attach_target); |
141 | 0 | } |
142 | | |
143 | | void handle_launch(GDBRemoteCommunicationServerLLGS &gdb_server, |
144 | 0 | llvm::ArrayRef<llvm::StringRef> Arguments) { |
145 | 0 | ProcessLaunchInfo info; |
146 | 0 | info.GetFlags().Set(eLaunchFlagStopAtEntry | eLaunchFlagDebug | |
147 | 0 | eLaunchFlagDisableASLR); |
148 | 0 | info.SetArguments(Args(Arguments), true); |
149 | |
|
150 | 0 | llvm::SmallString<64> cwd; |
151 | 0 | if (std::error_code ec = llvm::sys::fs::current_path(cwd)) { |
152 | 0 | llvm::errs() << "Error getting current directory: " << ec.message() << "\n"; |
153 | 0 | exit(1); |
154 | 0 | } |
155 | 0 | FileSpec cwd_spec(cwd); |
156 | 0 | FileSystem::Instance().Resolve(cwd_spec); |
157 | 0 | info.SetWorkingDirectory(cwd_spec); |
158 | 0 | info.GetEnvironment() = Host::GetEnvironment(); |
159 | |
|
160 | 0 | gdb_server.SetLaunchInfo(info); |
161 | |
|
162 | 0 | Status error = gdb_server.LaunchProcess(); |
163 | 0 | if (error.Fail()) { |
164 | 0 | llvm::errs() << llvm::formatv("error: failed to launch '{0}': {1}\n", |
165 | 0 | Arguments[0], error); |
166 | 0 | exit(1); |
167 | 0 | } |
168 | 0 | } |
169 | | |
170 | 0 | Status writeSocketIdToPipe(Pipe &port_pipe, llvm::StringRef socket_id) { |
171 | 0 | size_t bytes_written = 0; |
172 | | // Write the port number as a C string with the NULL terminator. |
173 | 0 | return port_pipe.Write(socket_id.data(), socket_id.size() + 1, bytes_written); |
174 | 0 | } |
175 | | |
176 | | Status writeSocketIdToPipe(const char *const named_pipe_path, |
177 | 0 | llvm::StringRef socket_id) { |
178 | 0 | Pipe port_name_pipe; |
179 | | // Wait for 10 seconds for pipe to be opened. |
180 | 0 | auto error = port_name_pipe.OpenAsWriterWithTimeout(named_pipe_path, false, |
181 | 0 | std::chrono::seconds{10}); |
182 | 0 | if (error.Fail()) |
183 | 0 | return error; |
184 | 0 | return writeSocketIdToPipe(port_name_pipe, socket_id); |
185 | 0 | } |
186 | | |
187 | | Status writeSocketIdToPipe(lldb::pipe_t unnamed_pipe, |
188 | 0 | llvm::StringRef socket_id) { |
189 | 0 | Pipe port_pipe{LLDB_INVALID_PIPE, unnamed_pipe}; |
190 | 0 | return writeSocketIdToPipe(port_pipe, socket_id); |
191 | 0 | } |
192 | | |
193 | | void ConnectToRemote(MainLoop &mainloop, |
194 | | GDBRemoteCommunicationServerLLGS &gdb_server, |
195 | | bool reverse_connect, llvm::StringRef host_and_port, |
196 | | const char *const progname, const char *const subcommand, |
197 | | const char *const named_pipe_path, pipe_t unnamed_pipe, |
198 | 0 | int connection_fd) { |
199 | 0 | Status error; |
200 | |
|
201 | 0 | std::unique_ptr<Connection> connection_up; |
202 | 0 | std::string url; |
203 | |
|
204 | 0 | if (connection_fd != -1) { |
205 | 0 | url = llvm::formatv("fd://{0}", connection_fd).str(); |
206 | | |
207 | | // Create the connection. |
208 | 0 | #if LLDB_ENABLE_POSIX && !defined _WIN32 |
209 | 0 | ::fcntl(connection_fd, F_SETFD, FD_CLOEXEC); |
210 | 0 | #endif |
211 | 0 | } else if (!host_and_port.empty()) { |
212 | 0 | llvm::Expected<std::string> url_exp = |
213 | 0 | LLGSArgToURL(host_and_port, reverse_connect); |
214 | 0 | if (!url_exp) { |
215 | 0 | llvm::errs() << llvm::formatv("error: invalid host:port or URL '{0}': " |
216 | 0 | "{1}\n", |
217 | 0 | host_and_port, |
218 | 0 | llvm::toString(url_exp.takeError())); |
219 | 0 | exit(-1); |
220 | 0 | } |
221 | | |
222 | 0 | url = std::move(url_exp.get()); |
223 | 0 | } |
224 | | |
225 | 0 | if (!url.empty()) { |
226 | | // Create the connection or server. |
227 | 0 | std::unique_ptr<ConnectionFileDescriptor> conn_fd_up{ |
228 | 0 | new ConnectionFileDescriptor}; |
229 | 0 | auto connection_result = conn_fd_up->Connect( |
230 | 0 | url, |
231 | 0 | [named_pipe_path, unnamed_pipe](llvm::StringRef socket_id) { |
232 | | // If we have a named pipe to write the socket id back to, do that |
233 | | // now. |
234 | 0 | if (named_pipe_path && named_pipe_path[0]) { |
235 | 0 | Status error = writeSocketIdToPipe(named_pipe_path, socket_id); |
236 | 0 | if (error.Fail()) |
237 | 0 | llvm::errs() << llvm::formatv( |
238 | 0 | "failed to write to the named peipe '{0}': {1}\n", |
239 | 0 | named_pipe_path, error.AsCString()); |
240 | 0 | } |
241 | | // If we have an unnamed pipe to write the socket id back to, do |
242 | | // that now. |
243 | 0 | else if (unnamed_pipe != LLDB_INVALID_PIPE) { |
244 | 0 | Status error = writeSocketIdToPipe(unnamed_pipe, socket_id); |
245 | 0 | if (error.Fail()) |
246 | 0 | llvm::errs() << llvm::formatv( |
247 | 0 | "failed to write to the unnamed pipe: {0}\n", error); |
248 | 0 | } |
249 | 0 | }, |
250 | 0 | &error); |
251 | |
|
252 | 0 | if (error.Fail()) { |
253 | 0 | llvm::errs() << llvm::formatv( |
254 | 0 | "error: failed to connect to client at '{0}': {1}\n", url, error); |
255 | 0 | exit(-1); |
256 | 0 | } |
257 | 0 | if (connection_result != eConnectionStatusSuccess) { |
258 | 0 | llvm::errs() << llvm::formatv( |
259 | 0 | "error: failed to connect to client at '{0}' " |
260 | 0 | "(connection status: {1})\n", |
261 | 0 | url, static_cast<int>(connection_result)); |
262 | 0 | exit(-1); |
263 | 0 | } |
264 | 0 | connection_up = std::move(conn_fd_up); |
265 | 0 | } |
266 | 0 | error = gdb_server.InitializeConnection(std::move(connection_up)); |
267 | 0 | if (error.Fail()) { |
268 | 0 | llvm::errs() << llvm::formatv("failed to initialize connection\n", error); |
269 | 0 | exit(-1); |
270 | 0 | } |
271 | 0 | llvm::outs() << "Connection established.\n"; |
272 | 0 | } |
273 | | |
274 | | namespace { |
275 | | using namespace llvm::opt; |
276 | | |
277 | | enum ID { |
278 | | OPT_INVALID = 0, // This is not an option ID. |
279 | | #define OPTION(...) LLVM_MAKE_OPT_ID(__VA_ARGS__), |
280 | | #include "LLGSOptions.inc" |
281 | | #undef OPTION |
282 | | }; |
283 | | |
284 | | #define PREFIX(NAME, VALUE) \ |
285 | | constexpr llvm::StringLiteral NAME##_init[] = VALUE; \ |
286 | | constexpr llvm::ArrayRef<llvm::StringLiteral> NAME( \ |
287 | | NAME##_init, std::size(NAME##_init) - 1); |
288 | | #include "LLGSOptions.inc" |
289 | | #undef PREFIX |
290 | | |
291 | | static constexpr opt::OptTable::Info InfoTable[] = { |
292 | | #define OPTION(...) LLVM_CONSTRUCT_OPT_INFO(__VA_ARGS__), |
293 | | #include "LLGSOptions.inc" |
294 | | #undef OPTION |
295 | | }; |
296 | | |
297 | | class LLGSOptTable : public opt::GenericOptTable { |
298 | | public: |
299 | 4 | LLGSOptTable() : opt::GenericOptTable(InfoTable) {} |
300 | | |
301 | 0 | void PrintHelp(llvm::StringRef Name) { |
302 | 0 | std::string Usage = |
303 | 0 | (Name + " [options] [[host]:port] [[--] program args...]").str(); |
304 | 0 | OptTable::printHelp(llvm::outs(), Usage.c_str(), "lldb-server"); |
305 | 0 | llvm::outs() << R"( |
306 | 0 | DESCRIPTION |
307 | 0 | lldb-server connects to the LLDB client, which drives the debugging session. |
308 | 0 | If no connection options are given, the [host]:port argument must be present |
309 | 0 | and will denote the address that lldb-server will listen on. [host] defaults |
310 | 0 | to "localhost" if empty. Port can be zero, in which case the port number will |
311 | 0 | be chosen dynamically and written to destinations given by --named-pipe and |
312 | 0 | --pipe arguments. |
313 | 0 |
|
314 | 0 | If no target is selected at startup, lldb-server can be directed by the LLDB |
315 | 0 | client to launch or attach to a process. |
316 | 0 |
|
317 | 0 | )"; |
318 | 0 | } |
319 | | }; |
320 | | } // namespace |
321 | | |
322 | 4 | int main_gdbserver(int argc, char *argv[]) { |
323 | 4 | Status error; |
324 | 4 | MainLoop mainloop; |
325 | 4 | #ifndef _WIN32 |
326 | | // Setup signal handlers first thing. |
327 | 4 | signal(SIGPIPE, SIG_IGN); |
328 | 4 | MainLoop::SignalHandleUP sighup_handle = |
329 | 4 | mainloop.RegisterSignal(SIGHUP, sighup_handler, error); |
330 | 4 | #endif |
331 | | |
332 | 4 | const char *progname = argv[0]; |
333 | 4 | const char *subcommand = argv[1]; |
334 | 4 | std::string attach_target; |
335 | 4 | std::string named_pipe_path; |
336 | 4 | std::string log_file; |
337 | 4 | StringRef |
338 | 4 | log_channels; // e.g. "lldb process threads:gdb-remote default:linux all" |
339 | 4 | lldb::pipe_t unnamed_pipe = LLDB_INVALID_PIPE; |
340 | 4 | bool reverse_connect = false; |
341 | 4 | int connection_fd = -1; |
342 | | |
343 | | // ProcessLaunchInfo launch_info; |
344 | 4 | ProcessAttachInfo attach_info; |
345 | | |
346 | 4 | LLGSOptTable Opts; |
347 | 4 | llvm::BumpPtrAllocator Alloc; |
348 | 4 | llvm::StringSaver Saver(Alloc); |
349 | 4 | bool HasError = false; |
350 | 4 | opt::InputArgList Args = Opts.parseArgs(argc - 1, argv + 1, OPT_UNKNOWN, |
351 | 4 | Saver, [&](llvm::StringRef Msg) { |
352 | 2 | WithColor::error() << Msg << "\n"; |
353 | 2 | HasError = true; |
354 | 2 | }); |
355 | 4 | std::string Name = |
356 | 4 | (llvm::sys::path::filename(argv[0]) + " g[dbserver]").str(); |
357 | 4 | std::string HelpText = |
358 | 4 | "Use '" + Name + " --help' for a complete list of options.\n"; |
359 | 4 | if (HasError) { |
360 | 2 | llvm::errs() << HelpText; |
361 | 2 | return 1; |
362 | 2 | } |
363 | | |
364 | 2 | if (Args.hasArg(OPT_help)) { |
365 | 0 | Opts.PrintHelp(Name); |
366 | 0 | return 0; |
367 | 0 | } |
368 | | |
369 | 2 | #ifndef _WIN32 |
370 | 2 | if (Args.hasArg(OPT_setsid)) { |
371 | | // Put llgs into a new session. Terminals group processes |
372 | | // into sessions and when a special terminal key sequences |
373 | | // (like control+c) are typed they can cause signals to go out to |
374 | | // all processes in a session. Using this --setsid (-S) option |
375 | | // will cause debugserver to run in its own sessions and be free |
376 | | // from such issues. |
377 | | // |
378 | | // This is useful when llgs is spawned from a command |
379 | | // line application that uses llgs to do the debugging, |
380 | | // yet that application doesn't want llgs receiving the |
381 | | // signals sent to the session (i.e. dying when anyone hits ^C). |
382 | 0 | { |
383 | 0 | const ::pid_t new_sid = setsid(); |
384 | 0 | if (new_sid == -1) { |
385 | 0 | WithColor::warning() |
386 | 0 | << llvm::formatv("failed to set new session id for {0} ({1})\n", |
387 | 0 | LLGS_PROGRAM_NAME, llvm::sys::StrError()); |
388 | 0 | } |
389 | 0 | } |
390 | 0 | } |
391 | 2 | #endif |
392 | | |
393 | 2 | log_file = Args.getLastArgValue(OPT_log_file).str(); |
394 | 2 | log_channels = Args.getLastArgValue(OPT_log_channels); |
395 | 2 | named_pipe_path = Args.getLastArgValue(OPT_named_pipe).str(); |
396 | 2 | reverse_connect = Args.hasArg(OPT_reverse_connect); |
397 | 2 | attach_target = Args.getLastArgValue(OPT_attach).str(); |
398 | 2 | if (Args.hasArg(OPT_pipe)) { |
399 | 0 | uint64_t Arg; |
400 | 0 | if (!llvm::to_integer(Args.getLastArgValue(OPT_pipe), Arg)) { |
401 | 0 | WithColor::error() << "invalid '--pipe' argument\n" << HelpText; |
402 | 0 | return 1; |
403 | 0 | } |
404 | 0 | unnamed_pipe = (pipe_t)Arg; |
405 | 0 | } |
406 | 2 | if (Args.hasArg(OPT_fd)) { |
407 | 1 | if (!llvm::to_integer(Args.getLastArgValue(OPT_fd), connection_fd)) { |
408 | 1 | WithColor::error() << "invalid '--fd' argument\n" << HelpText; |
409 | 1 | return 1; |
410 | 1 | } |
411 | 1 | } |
412 | | |
413 | 1 | if (!LLDBServerUtilities::SetupLogging( |
414 | 1 | log_file, log_channels, |
415 | 1 | LLDB_LOG_OPTION_PREPEND_TIMESTAMP | |
416 | 1 | LLDB_LOG_OPTION_PREPEND_FILE_FUNCTION)) |
417 | 0 | return -1; |
418 | | |
419 | 1 | std::vector<llvm::StringRef> Inputs; |
420 | 1 | for (opt::Arg *Arg : Args.filtered(OPT_INPUT)) |
421 | 0 | Inputs.push_back(Arg->getValue()); |
422 | 1 | if (opt::Arg *Arg = Args.getLastArg(OPT_REM)) { |
423 | 0 | for (const char *Val : Arg->getValues()) |
424 | 0 | Inputs.push_back(Val); |
425 | 0 | } |
426 | 1 | if (Inputs.empty() && connection_fd == -1) { |
427 | 1 | WithColor::error() << "no connection arguments\n" << HelpText; |
428 | 1 | return 1; |
429 | 1 | } |
430 | | |
431 | 0 | NativeProcessManager manager(mainloop); |
432 | 0 | GDBRemoteCommunicationServerLLGS gdb_server(mainloop, manager); |
433 | |
|
434 | 0 | llvm::StringRef host_and_port; |
435 | 0 | if (!Inputs.empty()) { |
436 | 0 | host_and_port = Inputs.front(); |
437 | 0 | Inputs.erase(Inputs.begin()); |
438 | 0 | } |
439 | | |
440 | | // Any arguments left over are for the program that we need to launch. If |
441 | | // there |
442 | | // are no arguments, then the GDB server will start up and wait for an 'A' |
443 | | // packet |
444 | | // to launch a program, or a vAttach packet to attach to an existing process, |
445 | | // unless |
446 | | // explicitly asked to attach with the --attach={pid|program_name} form. |
447 | 0 | if (!attach_target.empty()) |
448 | 0 | handle_attach(gdb_server, attach_target); |
449 | 0 | else if (!Inputs.empty()) |
450 | 0 | handle_launch(gdb_server, Inputs); |
451 | | |
452 | | // Print version info. |
453 | 0 | printf("%s-%s\n", LLGS_PROGRAM_NAME, LLGS_VERSION_STR); |
454 | |
|
455 | 0 | ConnectToRemote(mainloop, gdb_server, reverse_connect, host_and_port, |
456 | 0 | progname, subcommand, named_pipe_path.c_str(), |
457 | 0 | unnamed_pipe, connection_fd); |
458 | |
|
459 | 0 | if (!gdb_server.IsConnected()) { |
460 | 0 | fprintf(stderr, "no connection information provided, unable to run\n"); |
461 | 0 | return 1; |
462 | 0 | } |
463 | | |
464 | 0 | Status ret = mainloop.Run(); |
465 | 0 | if (ret.Fail()) { |
466 | 0 | fprintf(stderr, "lldb-server terminating due to error: %s\n", |
467 | 0 | ret.AsCString()); |
468 | 0 | return 1; |
469 | 0 | } |
470 | 0 | fprintf(stderr, "lldb-server exiting...\n"); |
471 | |
|
472 | 0 | return 0; |
473 | 0 | } |