#include <assert.h>
#include <dirent.h>
#include <errno.h>
#include <fcntl.h>
#include <execinfo.h>
#include <libgen.h>
#include <mach-o/dyld.h>
#include <mach-o/dyld-interposing.h>
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <sys/event.h>
#include <sys/mman.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <sys/time.h>
#include <tr1/memory> // for std::tr1::shared_ptr
#include <unistd.h>
#include <string>
#include <vector>
#include <map>
#define DISALLOW_COPY_AND_ASSIGN(TypeName) \
TypeName(const TypeName&); \
const TypeName& operator=(const TypeName&)
extern "C" {
int accept$NOCANCEL (int, struct sockaddr * __restrict, socklen_t * __restrict);
int close$NOCANCEL(int);
int open$NOCANCEL(const char *, int, ...);
int __open_extended(const char *, int, uid_t, gid_t, int, struct kauth_filesec *);
}
namespace fd_interposing {
class String
{
public:
String () :
m_str (NULL)
{}
String (const char *format, ...) :
m_str (NULL)
{
va_list args;
va_start (args, format);
vprintf (format, args);
va_end (args);
}
~String()
{
reset();
}
void
reset (char *s = NULL)
{
if (m_str)
::free (m_str);
m_str = s;
}
const char *
c_str () const
{
return m_str;
}
void
printf (const char *format, ...)
{
va_list args;
va_start (args, format);
vprintf (format, args);
va_end (args);
}
void
vprintf (const char *format, va_list args)
{
reset();
::vasprintf (&m_str, format, args);
}
void
log (int log_fd)
{
if (m_str && log_fd >= 0)
{
const int len = strlen(m_str);
if (len > 0)
{
write (log_fd, m_str, len);
const char last_char = m_str[len-1];
if (!(last_char == '\n' || last_char == '\r'))
write (log_fd, "\n", 1);
}
}
}
protected:
char *m_str;
private:
DISALLOW_COPY_AND_ASSIGN (String);
};
typedef std::vector<void *> Frames;
class FDEvent;
typedef std::vector<void *> Frames;
typedef std::tr1::shared_ptr<FDEvent> FDEventSP;
typedef std::tr1::shared_ptr<String> StringSP;
class FDEvent
{
public:
FDEvent (int fd, int err, const StringSP &string_sp, bool is_create, const Frames& frames) :
m_string_sp (string_sp),
m_frames (frames.begin(), frames.end()),
m_fd (fd),
m_err (err),
m_is_create (is_create)
{}
~FDEvent () {}
bool
IsCreateEvent() const
{
return m_is_create;
}
bool
IsDeleteEvent() const
{
return !m_is_create;
}
Frames &
GetFrames ()
{
return m_frames;
}
const Frames &
GetFrames () const
{
return m_frames;
}
int
GetFD () const
{
return m_fd;
}
int
GetError () const
{
return m_err;
}
void
Dump (int log_fd) const;
void
SetCreateEvent (FDEventSP &create_event_sp)
{
m_create_event_sp = create_event_sp;
}
private:
StringSP m_string_sp;
Frames m_frames;
FDEventSP m_create_event_sp;
int m_fd;
int m_err;
bool m_is_create;
};
template <int INVALID>
class Errno
{
public:
Errno (int value) :
m_saved_errno ((value == INVALID) ? errno : 0),
m_restore (value == INVALID)
{
}
~Errno()
{
if (m_restore)
errno = m_saved_errno;
}
int
get_errno() const
{
return m_saved_errno;
}
protected:
const int m_saved_errno;
const bool m_restore;
};
typedef Errno<-1> InvalidFDErrno;
typedef Errno<-1> NegativeErrorErrno;
typedef std::vector<FDEventSP> FDEventArray;
typedef std::map<int, FDEventArray> FDEventMap;
static FDEventMap g_fd_event_map;
static pthread_mutex_t g_mutex = PTHREAD_MUTEX_INITIALIZER;
static int g_log_all_calls = 1;
static int g_compact = 1;
static int g_pid = -1;
static bool g_enabled = true;
class Locker
{
public:
Locker (pthread_mutex_t *mutex_ptr) :
m_mutex_ptr(mutex_ptr)
{
::pthread_mutex_lock (m_mutex_ptr);
}
Locker (pthread_mutex_t *mutex_ptr, bool &lock_acquired) :
m_mutex_ptr(NULL)
{
lock_acquired = ::pthread_mutex_trylock(mutex_ptr) == 0;
if (lock_acquired)
m_mutex_ptr = mutex_ptr;
}
~Locker ()
{
if (m_mutex_ptr)
::pthread_mutex_unlock (m_mutex_ptr);
}
protected:
pthread_mutex_t *m_mutex_ptr;
};
static void
log (const char *format, ...) __attribute__ ((format (printf, 1, 2)));
static void
log (int log_fd, const FDEvent *event, const char *format, ...) __attribute__ ((format (printf, 3, 4)));
static void
backtrace_log (const char *format, ...) __attribute__ ((format (printf, 1, 2)));
static void
backtrace_error (const char *format, ...) __attribute__ ((format (printf, 1, 2)));
static void
log_to_fd (int log_fd, const char *format, ...) __attribute__ ((format (printf, 2, 3)));
static inline size_t
get_backtrace (Frames &frame_buffer, size_t frames_to_remove)
{
void *frames[2048];
int count = ::backtrace (&frames[0], sizeof(frames)/sizeof(void*));
if (count > frames_to_remove)
frame_buffer.assign (&frames[frames_to_remove], &frames[count]);
else
frame_buffer.assign (&frames[0], &frames[count]);
while (frame_buffer.back() < (void *)1024)
frame_buffer.pop_back();
return frame_buffer.size();
}
static int g_log_fd = STDOUT_FILENO;
static int g_initialized = 0;
const char *
get_process_fullpath (bool force = false)
{
static char g_process_fullpath[PATH_MAX] = {0};
if (force || g_process_fullpath[0] == '\0')
{
uint32_t len = sizeof(g_process_fullpath);
if (_NSGetExecutablePath (g_process_fullpath, &len) != 0)
strncpy (g_process_fullpath, "<error>", sizeof(g_process_fullpath));
}
return g_process_fullpath;
}
static int
get_interposed_pid()
{
if (!g_enabled)
return -1;
const pid_t pid = getpid();
if (g_pid != pid)
{
if (g_pid == -1)
{
g_pid = pid;
log ("Interposing file descriptor create and delete functions for %s (pid=%i)\n", get_process_fullpath (true), pid);
}
else
{
log ("pid=%i: disabling interposing file descriptor create and delete functions for child process %s (pid=%i)\n", g_pid, get_process_fullpath (true), pid);
g_enabled = false;
return -1;
}
}
return g_pid;
}
static int
get_logging_fd ()
{
if (!g_enabled)
return -1;
if (!g_initialized)
{
g_initialized = 1;
const pid_t pid = get_interposed_pid();
if (g_enabled)
{
if (getenv("FileDescriptorStackLoggingNoCompact"))
g_compact = 0;
if (getenv("FileDescriptorMinimalLogging"))
g_log_all_calls = 0;
const char *log_path = getenv ("FileDescriptorLogFile");
if (log_path)
g_log_fd = ::creat (log_path, 0660);
else
g_log_fd = STDOUT_FILENO;
::unsetenv ("DYLD_INSERT_LIBRARIES");
}
else
{
log ("pid=%i: logging disabled\n", getpid());
}
}
return g_log_fd;
}
void
log_to_fd (int log_fd, const char *format, va_list args)
{
if (format && format[0] && log_fd >= 0)
{
char buffer[PATH_MAX];
const int count = ::vsnprintf (buffer, sizeof(buffer), format, args);
if (count > 0)
write (log_fd, buffer, count);
}
}
void
log_to_fd (int log_fd, const char *format, ...)
{
if (format && format[0])
{
va_list args;
va_start (args, format);
log_to_fd (log_fd, format, args);
va_end (args);
}
}
void
log (const char *format, va_list args)
{
log_to_fd (get_logging_fd (), format, args);
}
void
log (const char *format, ...)
{
if (format && format[0])
{
va_list args;
va_start (args, format);
log (format, args);
va_end (args);
}
}
void
log (int log_fd, const FDEvent *event, const char *format, ...)
{
if (format && format[0])
{
va_list args;
va_start (args, format);
log_to_fd (log_fd, format, args);
va_end (args);
}
if (event)
event->Dump(log_fd);
}
void
FDEvent::Dump (int log_fd) const
{
if (log_fd >= 0)
{
log_to_fd (log_fd, "%s\n", m_string_sp->c_str());
if (!m_frames.empty())
::backtrace_symbols_fd (m_frames.data(), m_frames.size(), log_fd);
if (m_create_event_sp)
{
log_to_fd (log_fd, "\nfd=%i was created with this event:\n", m_fd);
m_create_event_sp->Dump (log_fd);
log_to_fd (log_fd, "\n");
}
}
}
void
backtrace_log (const char *format, ...)
{
const int log_fd = get_logging_fd ();
if (log_fd >= 0)
{
if (format && format[0])
{
va_list args;
va_start (args, format);
log (format, args);
va_end (args);
}
Frames frames;
if (get_backtrace(frames, 2))
::backtrace_symbols_fd (frames.data(), frames.size(), log_fd);
}
}
void
backtrace_error (const char *format, ...)
{
const int pid = get_interposed_pid();
if (pid >= 0)
{
const int log_fd = get_logging_fd ();
if (log_fd >= 0)
{
log ("\nerror: %s (pid=%i): ", get_process_fullpath (), pid);
if (format && format[0])
{
va_list args;
va_start (args, format);
log (format, args);
va_end (args);
}
Frames frames;
if (get_backtrace(frames, 2))
::backtrace_symbols_fd (frames.data(), frames.size(), log_fd);
}
}
}
void
save_backtrace (int fd, int err, const StringSP &string_sp, bool is_create)
{
Frames frames;
get_backtrace(frames, 2);
FDEventSP fd_event_sp (new FDEvent (fd, err, string_sp, is_create, frames));
FDEventMap::iterator pos = g_fd_event_map.find (fd);
if (pos != g_fd_event_map.end())
{
FDEventArray &event_array = g_fd_event_map[fd];
if (fd_event_sp->IsCreateEvent())
{
if (event_array.back()->IsCreateEvent())
{
const int log_fd = get_logging_fd();
log (log_fd, fd_event_sp.get(), "\nwarning: unmatched file descriptor create event fd=%i (we missed a file descriptor close event):\n", fd);
}
else if (g_compact)
{
event_array.pop_back();
}
}
else
{
if (event_array.back()->IsDeleteEvent())
{
const int log_fd = get_logging_fd();
log (log_fd, fd_event_sp.get(), "\nwarning: unmatched file descriptor close event for fd=%d (we missed the file descriptor create event):\n", fd);
}
else if (g_compact)
{
fd_event_sp->SetCreateEvent(event_array.back());
event_array.pop_back();
}
}
event_array.push_back(fd_event_sp);
}
else
{
g_fd_event_map[fd].push_back(fd_event_sp);
}
}
extern "C" int
socket$__interposed__ (int domain, int type, int protocol)
{
const int pid = get_interposed_pid();
if (pid >= 0)
{
Locker locker (&g_mutex);
const int fd = ::socket (domain, type, protocol);
InvalidFDErrno fd_errno(fd);
StringSP description_sp(new String);
if (fd == -1)
description_sp->printf("pid=%i: socket (domain = %i, type = %i, protocol = %i) => fd=%i errno = %i", pid, domain, type, protocol, fd, fd_errno.get_errno());
else
description_sp->printf("pid=%i: socket (domain = %i, type = %i, protocol = %i) => fd=%i", pid, domain, type, protocol, fd);
if (g_log_all_calls)
description_sp->log (get_logging_fd());
if (fd >= 0)
save_backtrace (fd, fd_errno.get_errno(), description_sp, true);
return fd;
}
else
{
return ::socket (domain, type, protocol);
}
}
extern "C" int
socketpair$__interposed__ (int domain, int type, int protocol, int fds[2])
{
const int pid = get_interposed_pid();
if (pid >= 0)
{
Locker locker (&g_mutex);
fds[0] = -1;
fds[1] = -1;
const int err = socketpair (domain, type, protocol, fds);
NegativeErrorErrno err_errno(err);
StringSP description_sp(new String ("pid=%i: socketpair (domain=%i, type=%i, protocol=%i, {fd=%i, fd=%i}) -> err=%i", pid, domain, type, protocol, fds[0], fds[1], err));
if (g_log_all_calls)
description_sp->log (get_logging_fd());
if (fds[0] >= 0)
save_backtrace (fds[0], err_errno.get_errno(), description_sp, true);
if (fds[1] >= 0)
save_backtrace (fds[1], err_errno.get_errno(), description_sp, true);
return err;
}
else
{
return socketpair (domain, type, protocol, fds);
}
}
extern "C" int
open$__interposed__ (const char *path, int oflag, int mode)
{
const int pid = get_interposed_pid();
if (pid >= 0)
{
Locker locker (&g_mutex);
int fd = -2;
StringSP description_sp(new String);
if (oflag & O_CREAT)
{
fd = ::open (path, oflag, mode);
description_sp->printf("pid=%i: open (path = '%s', oflag = %i, mode = %i) -> fd=%i", pid, path, oflag, mode, fd);
}
else
{
fd = ::open (path, oflag);
description_sp->printf("pid=%i: open (path = '%s', oflag = %i) -> fd=%i", pid, path, oflag, fd);
}
InvalidFDErrno fd_errno(fd);
if (g_log_all_calls)
description_sp->log (get_logging_fd());
if (fd >= 0)
save_backtrace (fd, fd_errno.get_errno(), description_sp, true);
return fd;
}
else
{
return ::open (path, oflag, mode);
}
}
extern "C" int
open$NOCANCEL$__interposed__ (const char *path, int oflag, int mode)
{
const int pid = get_interposed_pid();
if (pid >= 0)
{
Locker locker (&g_mutex);
const int fd = ::open$NOCANCEL (path, oflag, mode);
InvalidFDErrno fd_errno(fd);
StringSP description_sp(new String ("pid=%i: open$NOCANCEL (path = '%s', oflag = %i, mode = %i) -> fd=%i", pid, path, oflag, mode, fd));
if (g_log_all_calls)
description_sp->log (get_logging_fd());
if (fd >= 0)
save_backtrace (fd, fd_errno.get_errno(), description_sp, true);
return fd;
}
else
{
return ::open$NOCANCEL (path, oflag, mode);
}
}
extern "C" int
__open_extended$__interposed__ (const char *path, int oflag, uid_t uid, gid_t gid, int mode, struct kauth_filesec *fsacl)
{
const int pid = get_interposed_pid();
if (pid >= 0)
{
Locker locker (&g_mutex);
const int fd = ::__open_extended (path, oflag, uid, gid, mode, fsacl);
InvalidFDErrno fd_errno(fd);
StringSP description_sp(new String ("pid=%i: __open_extended (path='%s', oflag=%i, uid=%i, gid=%i, mode=%i, fsacl=%p) -> fd=%i", pid, path, oflag, uid, gid, mode, fsacl, fd));
if (g_log_all_calls)
description_sp->log (get_logging_fd());
if (fd >= 0)
save_backtrace (fd, fd_errno.get_errno(), description_sp, true);
return fd;
}
else
{
return ::__open_extended (path, oflag, uid, gid, mode, fsacl);
}
}
extern "C" int
kqueue$__interposed__ (void)
{
const int pid = get_interposed_pid();
if (pid >= 0)
{
Locker locker (&g_mutex);
const int fd = ::kqueue ();
InvalidFDErrno fd_errno(fd);
StringSP description_sp(new String ("pid=%i: kqueue () -> fd=%i", pid, fd));
if (g_log_all_calls)
description_sp->log (get_logging_fd());
if (fd >= 0)
save_backtrace (fd, fd_errno.get_errno(), description_sp, true);
return fd;
}
else
{
return ::kqueue ();
}
}
extern "C" int
shm_open$__interposed__ (const char *path, int oflag, int mode)
{
const int pid = get_interposed_pid();
if (pid >= 0)
{
Locker locker (&g_mutex);
const int fd = ::shm_open (path, oflag, mode);
InvalidFDErrno fd_errno(fd);
StringSP description_sp(new String ("pid=%i: shm_open (path = '%s', oflag = %i, mode = %i) -> fd=%i", pid, path, oflag, mode, fd));
if (g_log_all_calls)
description_sp->log (get_logging_fd());
if (fd >= 0)
save_backtrace (fd, fd_errno.get_errno(), description_sp, true);
return fd;
}
else
{
return ::shm_open (path, oflag, mode);
}
}
extern "C" int
accept$__interposed__ (int socket, struct sockaddr *address, socklen_t *address_len)
{
const int pid = get_interposed_pid();
if (pid >= 0)
{
Locker locker (&g_mutex);
const int fd = ::accept (socket, address, address_len);
InvalidFDErrno fd_errno(fd);
StringSP description_sp(new String ("pid=%i: accept (socket=%i, ...) -> fd=%i", pid, socket, fd));
if (g_log_all_calls)
description_sp->log (get_logging_fd());
if (fd >= 0)
save_backtrace (fd, fd_errno.get_errno(), description_sp, true);
return fd;
}
else
{
return ::accept (socket, address, address_len);
}
}
extern "C" int
accept$NOCANCEL$__interposed__ (int socket, struct sockaddr *address, socklen_t *address_len)
{
const int pid = get_interposed_pid();
if (pid >= 0)
{
Locker locker (&g_mutex);
const int fd = ::accept$NOCANCEL (socket, address, address_len);
InvalidFDErrno fd_errno(fd);
StringSP description_sp(new String ("pid=%i: accept$NOCANCEL (socket=%i, ...) -> fd=%i", pid, socket, fd));
if (g_log_all_calls)
description_sp->log (get_logging_fd());
if (fd >= 0)
save_backtrace (fd, fd_errno.get_errno(), description_sp, true);
return fd;
}
else
{
return ::accept$NOCANCEL (socket, address, address_len);
}
}
extern "C" int
dup$__interposed__ (int fd2)
{
const int pid = get_interposed_pid();
if (pid >= 0)
{
Locker locker (&g_mutex);
const int fd = ::dup (fd2);
InvalidFDErrno fd_errno(fd);
StringSP description_sp(new String ("pid=%i: dup (fd2=%i) -> fd=%i", pid, fd2, fd));
if (g_log_all_calls)
description_sp->log (get_logging_fd());
if (fd >= 0)
save_backtrace (fd, fd_errno.get_errno(), description_sp, true);
return fd;
}
else
{
return ::dup (fd2);
}
}
extern "C" int
dup2$__interposed__ (int fd1, int fd2)
{
const int pid = get_interposed_pid();
if (pid >= 0)
{
Locker locker (&g_mutex);
FDEventMap::iterator pos = g_fd_event_map.find (fd2);
StringSP dup2_close_description_sp(new String ("pid=%i: dup2 (fd1=%i, fd2=%i) -> will close (fd=%i)", pid, fd1, fd2, fd2));
if (pos != g_fd_event_map.end() && pos->second.back()->IsCreateEvent())
save_backtrace (fd2, 0, dup2_close_description_sp, false);
const int fd = ::dup2(fd1, fd2);
InvalidFDErrno fd_errno(fd);
StringSP description_sp(new String ("pid=%i: dup2 (fd1=%i, fd2=%i) -> fd=%i", pid, fd1, fd2, fd));
if (g_log_all_calls)
description_sp->log (get_logging_fd());
if (fd >= 0)
save_backtrace (fd, fd_errno.get_errno(), description_sp, true);
return fd;
}
else
{
return ::dup2(fd1, fd2);
}
}
extern "C" int
close$__interposed__ (int fd)
{
const int pid = get_interposed_pid();
if (pid >= 0)
{
Locker locker (&g_mutex);
const int err = close(fd);
NegativeErrorErrno err_errno(err);
StringSP description_sp (new String);
if (err == -1)
description_sp->printf("pid=%i: close (fd=%i) => %i errno = %i (%s))", pid, fd, err, err_errno.get_errno(), strerror(err_errno.get_errno()));
else
description_sp->printf("pid=%i: close (fd=%i) => %i", pid, fd, err);
if (g_log_all_calls)
description_sp->log (get_logging_fd());
if (err == 0)
{
if (fd >= 0)
save_backtrace (fd, err, description_sp, false);
}
else if (err == -1)
{
if (err_errno.get_errno() == EBADF && fd != -1)
{
backtrace_error ("close (fd=%d) resulted in EBADF:\n", fd);
FDEventMap::iterator pos = g_fd_event_map.find (fd);
if (pos != g_fd_event_map.end())
{
log (get_logging_fd(), pos->second.back().get(), "\nfd=%d was previously %s with this event:\n", fd, pos->second.back()->IsCreateEvent() ? "opened" : "closed");
}
}
}
return err;
}
else
{
return close (fd);
}
}
extern "C" int
close$NOCANCEL$__interposed__ (int fd)
{
const int pid = get_interposed_pid();
if (pid >= 0)
{
Locker locker (&g_mutex);
const int err = close$NOCANCEL(fd);
NegativeErrorErrno err_errno(err);
StringSP description_sp (new String);
if (err == -1)
description_sp->printf("pid=%i: close$NOCANCEL (fd=%i) => %i errno = %i (%s))", pid, fd, err, err_errno.get_errno(), strerror(err_errno.get_errno()));
else
description_sp->printf("pid=%i: close$NOCANCEL (fd=%i) => %i", pid, fd, err);
if (g_log_all_calls)
description_sp->log (get_logging_fd());
if (err == 0)
{
if (fd >= 0)
save_backtrace (fd, err, description_sp, false);
}
else if (err == -1)
{
if (err_errno.get_errno() == EBADF && fd != -1)
{
backtrace_error ("close$NOCANCEL (fd=%d) resulted in EBADF\n:", fd);
FDEventMap::iterator pos = g_fd_event_map.find (fd);
if (pos != g_fd_event_map.end())
{
log (get_logging_fd(), pos->second.back().get(), "\nfd=%d was previously %s with this event:\n", fd, pos->second.back()->IsCreateEvent() ? "opened" : "closed");
}
}
}
return err;
}
else
{
return close$NOCANCEL(fd);
}
}
extern "C" int
pipe$__interposed__ (int fds[2])
{
const int pid = get_interposed_pid();
if (pid >= 0)
{
Locker locker (&g_mutex);
fds[0] = -1;
fds[1] = -1;
const int err = pipe (fds);
const int saved_errno = errno;
StringSP description_sp(new String ("pid=%i: pipe ({fd=%i, fd=%i}) -> err=%i", pid, fds[0], fds[1], err));
if (g_log_all_calls)
description_sp->log (get_logging_fd());
if (fds[0] >= 0)
save_backtrace (fds[0], saved_errno, description_sp, true);
if (fds[1] >= 0)
save_backtrace (fds[1], saved_errno, description_sp, true);
errno = saved_errno;
return err;
}
else
{
return pipe (fds);
}
}
extern "C" void
get_fd_history (int log_fd, int fd)
{
if (log_fd >= 0)
{
bool got_lock = false;
Locker locker (&g_mutex, got_lock);
if (got_lock)
{
FDEventMap::iterator pos = g_fd_event_map.find (fd);
log_to_fd (log_fd, "Dumping file descriptor history for fd=%i:\n", fd);
if (pos != g_fd_event_map.end())
{
FDEventArray &event_array = g_fd_event_map[fd];
const size_t num_events = event_array.size();
for (size_t i=0; i<num_events; ++i)
event_array[i]->Dump (log_fd);
}
else
{
log_to_fd (log_fd, "error: no file descriptor events found for fd=%i\n", fd);
}
}
else
{
log_to_fd (log_fd, "error: fd event mutex is locked...\n");
}
}
}
DYLD_INTERPOSE(accept$__interposed__, accept);
DYLD_INTERPOSE(accept$NOCANCEL$__interposed__, accept$NOCANCEL);
DYLD_INTERPOSE(dup$__interposed__, dup);
DYLD_INTERPOSE(dup2$__interposed__, dup2);
DYLD_INTERPOSE(kqueue$__interposed__, kqueue);
DYLD_INTERPOSE(open$__interposed__, open);
DYLD_INTERPOSE(open$NOCANCEL$__interposed__, open$NOCANCEL);
DYLD_INTERPOSE(__open_extended$__interposed__, __open_extended);
DYLD_INTERPOSE(pipe$__interposed__, pipe);
DYLD_INTERPOSE(shm_open$__interposed__, shm_open);
DYLD_INTERPOSE(socket$__interposed__, socket);
DYLD_INTERPOSE(socketpair$__interposed__, socketpair);
DYLD_INTERPOSE(close$__interposed__, close);
DYLD_INTERPOSE(close$NOCANCEL$__interposed__, close$NOCANCEL);
}