bool Channel::CreatePipe(const IPC::ChannelHandle &channel_handle) { assert(INVALID_HANDLE_VALUE == pipe_); std::wstring pipe_name; // If we already have a valid pipe for channel just copy it. if (channel_handle.pipe.handle) { assert(channel_handle.name.empty()); pipe_name = L"Not Available"; // Just used for LOG // Check that the given pipe confirms to the specified mode. We can // only check for PIPE_TYPE_MESSAGE & PIPE_SERVER_END flags since the // other flags (PIPE_TYPE_BYTE, and PIPE_CLIENT_END) are defined as 0. DWORD flags = 0; GetNamedPipeInfo(channel_handle.pipe.handle, &flags, NULL, NULL, NULL); assert(!(flags & PIPE_TYPE_MESSAGE)); if (!DuplicateHandle(GetCurrentProcess(), channel_handle.pipe.handle, GetCurrentProcess(), &pipe_, 0, FALSE, DUPLICATE_SAME_ACCESS)) { //LOG(WARNING) << "DuplicateHandle failed. Error :" << GetLastError(); return false; } } else { assert(!channel_handle.pipe.handle); pipe_name = PipeName(channel_handle.name, &client_secret_); //Ïȳ¢ÊÔ´´½¨ const DWORD open_mode = PIPE_ACCESS_DUPLEX | FILE_FLAG_OVERLAPPED | FILE_FLAG_FIRST_PIPE_INSTANCE; validate_client_ = !!client_secret_; pipe_ = CreateNamedPipeW(pipe_name.c_str(), open_mode, PIPE_TYPE_BYTE | PIPE_READMODE_BYTE, 1, kReadBufferSize, kReadBufferSize, 5000, NULL); if (pipe_ == INVALID_HANDLE_VALUE) { pipe_ = CreateFileW(pipe_name.c_str(), GENERIC_READ | GENERIC_WRITE, 0, NULL, OPEN_EXISTING, SECURITY_SQOS_PRESENT | SECURITY_IDENTIFICATION | FILE_FLAG_OVERLAPPED, NULL); waiting_connect_ = false; } } if (pipe_ == INVALID_HANDLE_VALUE) { // If this process is being closed, the pipe may be gone already. //LOG(WARNING) << "Unable to create pipe \"" << pipe_name << // "\" in " << (mode & MODE_SERVER_FLAG ? "server" : "client") // << " mode. Error :" << GetLastError(); return false; } // Create the Hello message to be sent when Connect is called Message* m = new Message(MSG_ROUTING_NONE, HELLO_MESSAGE_TYPE, IPC::Message::PRIORITY_NORMAL); m->AddRef(); // Don't send the secret to the untrusted process, and don't send a secret // if the value is zero (for IPC backwards compatability). int32 secret = validate_client_ ? 0 : client_secret_; if (!m->WriteInt(GetCurrentProcessId()) || (secret && !m->WriteUInt32(secret))) { CloseHandle(pipe_); pipe_ = INVALID_HANDLE_VALUE; m->Release(); return false; } output_queue_.push(m); return true; }
// FIXME: remove socket option functions? static PRStatus nsNamedPipeGetSocketOption(PRFileDesc* aFd, PRSocketOptionData* aData) { MOZ_ASSERT(PR_GetCurrentThread() == gSocketThread); MOZ_ASSERT(aFd); MOZ_ASSERT(aData); switch (aData->option) { case PR_SockOpt_Nonblocking: aData->value.non_blocking = GetNamedPipeInfo(aFd)->IsNonblocking() ? PR_TRUE : PR_FALSE; break; case PR_SockOpt_Keepalive: aData->value.keep_alive = PR_TRUE; break; case PR_SockOpt_NoDelay: aData->value.no_delay = PR_TRUE; break; default: PR_SetError(PR_INVALID_METHOD_ERROR, 0); return PR_FAILURE; } return PR_SUCCESS; }
fcgi_request *fcgi_init_request(int listen_socket, void(*on_accept)(), void(*on_read)(), void(*on_close)()) { fcgi_request *req = calloc(1, sizeof(fcgi_request)); req->listen_socket = listen_socket; req->fd = -1; req->id = -1; /* req->in_len = 0; req->in_pad = 0; req->out_hdr = NULL; #ifdef TCP_NODELAY req->nodelay = 0; #endif req->env = NULL; req->has_env = 0; */ req->out_pos = req->out_buf; req->hook.on_accept = on_accept ? on_accept : fcgi_hook_dummy; req->hook.on_read = on_read ? on_read : fcgi_hook_dummy; req->hook.on_close = on_close ? on_close : fcgi_hook_dummy; #ifdef _WIN32 req->tcp = !GetNamedPipeInfo((HANDLE)_get_osfhandle(req->listen_socket), NULL, NULL, NULL, NULL); #endif fcgi_hash_init(&req->env); return req; }
fcgi_request *fcgi_init_request(int listen_socket) { fcgi_request *req = (fcgi_request*)calloc(1, sizeof(fcgi_request)); req->listen_socket = listen_socket; req->fd = -1; req->id = -1; req->in_len = 0; req->in_pad = 0; req->out_hdr = NULL; req->out_pos = req->out_buf; #ifdef _WIN32 req->tcp = !GetNamedPipeInfo((HANDLE)_get_osfhandle(req->listen_socket), NULL, NULL, NULL, NULL); #endif #ifdef TCP_NODELAY req->nodelay = 0; #endif fcgi_hash_init(&req->env); return req; }
/* Return true if HD refers to a socket. */ static int is_socket (HANDLE hd) { /* We need to figure out whether we are working on a socket or on a handle. A trivial way would be to check for the return code of recv and see if it is WSAENOTSOCK. However the recv may block after the server process died and thus the destroy_reader will hang. Another option is to use getsockopt to test whether it is a socket. The bug here is that once a socket with a certain values has been opened, closed and later a CreatePipe returned the same value (i.e. handle), getsockopt still believes it is a socket. What we do now is to use a combination of GetFileType and GetNamedPipeInfo. The specs say that the latter may be used on anonymous pipes as well. Note that there are claims that since winsocket version 2 ReadFile may be used on a socket but only if it is supported by the service provider. Tests on a stock XP using a local TCP socket show that it does not work. */ DWORD dummyflags, dummyoutsize, dummyinsize, dummyinst; if (GetFileType (hd) == FILE_TYPE_PIPE && !GetNamedPipeInfo (hd, &dummyflags, &dummyoutsize, &dummyinsize, &dummyinst)) return 1; /* Function failed; thus we assume it is a socket. */ else return 0; /* Success; this is not a socket. */ }
fcgi_request *fcgi_init_request(fcgi_request *req, int listen_socket) { memset(req, 0, sizeof(fcgi_request)); req->listen_socket = listen_socket; req->fd = -1; req->id = -1; /* req->in_len = 0; req->in_pad = 0; req->out_hdr = NULL; #ifdef TCP_NODELAY req->nodelay = 0; #endif req->env = NULL; req->has_env = 0; */ req->out_pos = req->out_buf; req->hook.on_accept = fcgi_hook_dummy; req->hook.on_read = fcgi_hook_dummy; req->hook.on_close = fcgi_hook_dummy; #ifdef _WIN32 req->tcp = !GetNamedPipeInfo((HANDLE)_get_osfhandle(req->listen_socket), NULL, NULL, NULL, NULL); #endif fcgi_hash_init(&req->env); return req; }
static PRInt32 nsNamedPipeRecv(PRFileDesc* aFd, void* aBuffer, PRInt32 aAmount, PRIntn aFlags, PRIntervalTime aTimeout) { MOZ_ASSERT(PR_GetCurrentThread() == gSocketThread); Unused << aTimeout; NamedPipeInfo* info = GetNamedPipeInfo(aFd); if (!info) { PR_SetError(PR_BAD_DESCRIPTOR_ERROR, 0); return -1; } if (aFlags) { if (aFlags != PR_MSG_PEEK) { PR_SetError(PR_UNKNOWN_ERROR, 0); return -1; } return info->Peek(aBuffer, aAmount); } return info->Read(aBuffer, aAmount); }
static PRInt64 nsNamedPipeAvailable64(PRFileDesc* aFd) { MOZ_ASSERT(PR_GetCurrentThread() == gSocketThread); NamedPipeInfo* info = GetNamedPipeInfo(aFd); if (!info) { PR_SetError(PR_BAD_DESCRIPTOR_ERROR, 0); return -1; } return static_cast<PRInt64>(info->Available()); }
HRESULT NamedPipeChannel::GetMaxInstances(DWORD* count) const { if (count == nullptr) return E_POINTER; if (!IsValid()) return E_HANDLE; if (!GetNamedPipeInfo(handle_, nullptr, nullptr, nullptr, count)) return HRESULT_FROM_WIN32(GetLastError()); return S_OK; }
static PRInt16 nsNamedPipePoll(PRFileDesc* aFd, PRInt16 aInFlags, PRInt16* aOutFlags) { MOZ_ASSERT(PR_GetCurrentThread() == gSocketThread); NamedPipeInfo* info = GetNamedPipeInfo(aFd); if (!info) { PR_SetError(PR_BAD_DESCRIPTOR_ERROR, 0); return 0; } return info->GetPollFlags(aInFlags, aOutFlags); }
static PRStatus nsNamedPipeSync(PRFileDesc* aFd) { MOZ_ASSERT(PR_GetCurrentThread() == gSocketThread); NamedPipeInfo* info = GetNamedPipeInfo(aFd); if (!info) { PR_SetError(PR_BAD_DESCRIPTOR_ERROR, 0); return PR_FAILURE; } return info->Sync(0) ? PR_SUCCESS : PR_FAILURE; }
static inline PRInt32 nsNamedPipeWrite(PRFileDesc* aFd, const void* aBuffer, PRInt32 aAmount) { MOZ_ASSERT(PR_GetCurrentThread() == gSocketThread); NamedPipeInfo* info = GetNamedPipeInfo(aFd); if (!info) { PR_SetError(PR_BAD_DESCRIPTOR_ERROR, 0); return -1; } return info->Write(aBuffer, aAmount); }
void fcgi_init_request(fcgi_request *req, int listen_socket) { memset(req, 0, sizeof(fcgi_request)); req->listen_socket = listen_socket; req->fd = -1; req->id = -1; req->in_len = 0; req->in_pad = 0; req->out_hdr = NULL; req->out_pos = req->out_buf; #ifdef _WIN32 req->tcp = !GetNamedPipeInfo((HANDLE)_get_osfhandle(req->listen_socket), NULL, NULL, NULL, NULL); #endif }
static PRStatus nsNamedPipeClose(PRFileDesc* aFd) { MOZ_ASSERT(PR_GetCurrentThread() == gSocketThread); if (aFd->secret && PR_GetLayersIdentity(aFd) == nsNamedPipeLayerIdentity) { RefPtr<NamedPipeInfo> info = dont_AddRef(GetNamedPipeInfo(aFd)); info->Disconnect(); aFd->secret = nullptr; aFd->identity = PR_INVALID_IO_LAYER; } MOZ_ASSERT(!aFd->lower); PR_DELETE(aFd); return PR_SUCCESS; }
static PRInt32 nsNamedPipeSend(PRFileDesc* aFd, const void* aBuffer, PRInt32 aAmount, PRIntn aFlags, PRIntervalTime aTimeout) { MOZ_ASSERT(PR_GetCurrentThread() == gSocketThread); Unused << aFlags; Unused << aTimeout; NamedPipeInfo* info = GetNamedPipeInfo(aFd); if (!info) { PR_SetError(PR_BAD_DESCRIPTOR_ERROR, 0); return -1; } return info->Write(aBuffer, aAmount); }
static PRStatus nsNamedPipeConnect(PRFileDesc* aFd, const PRNetAddr* aAddr, PRIntervalTime aTimeout) { MOZ_ASSERT(PR_GetCurrentThread() == gSocketThread); NamedPipeInfo* info = GetNamedPipeInfo(aFd); if (!info) { PR_SetError(PR_BAD_DESCRIPTOR_ERROR, 0); return PR_FAILURE; } if (NS_WARN_IF(NS_FAILED(info->Connect( nsDependentCString(aAddr->local.path))))) { return PR_FAILURE; } return PR_SUCCESS; }
static PRStatus nsNamedPipeSetSocketOption(PRFileDesc* aFd, const PRSocketOptionData* aData) { MOZ_ASSERT(PR_GetCurrentThread() == gSocketThread); MOZ_ASSERT(aFd); MOZ_ASSERT(aData); switch (aData->option) { case PR_SockOpt_Nonblocking: GetNamedPipeInfo(aFd)->SetNonblocking(aData->value.non_blocking); break; case PR_SockOpt_Keepalive: case PR_SockOpt_NoDelay: break; default: PR_SetError(PR_INVALID_METHOD_ERROR, 0); return PR_FAILURE; } return PR_SUCCESS; }
RTDECL(int) RTPipeFromNative(PRTPIPE phPipe, RTHCINTPTR hNativePipe, uint32_t fFlags) { AssertPtrReturn(phPipe, VERR_INVALID_POINTER); AssertReturn(!(fFlags & ~RTPIPE_N_VALID_MASK), VERR_INVALID_PARAMETER); AssertReturn(!!(fFlags & RTPIPE_N_READ) != !!(fFlags & RTPIPE_N_WRITE), VERR_INVALID_PARAMETER); /* * Get and validate the pipe handle info. */ HANDLE hNative = (HANDLE)hNativePipe; AssertReturn(GetFileType(hNative) == FILE_TYPE_PIPE, VERR_INVALID_HANDLE); DWORD cMaxInstances; DWORD fInfo; if (!GetNamedPipeInfo(hNative, &fInfo, NULL, NULL, &cMaxInstances)) return RTErrConvertFromWin32(GetLastError()); AssertReturn(!(fInfo & PIPE_TYPE_MESSAGE), VERR_INVALID_HANDLE); AssertReturn(cMaxInstances == 1, VERR_INVALID_HANDLE); DWORD cInstances; DWORD fState; if (!GetNamedPipeHandleState(hNative, &fState, &cInstances, NULL, NULL, NULL, 0)) return RTErrConvertFromWin32(GetLastError()); AssertReturn(!(fState & PIPE_NOWAIT), VERR_INVALID_HANDLE); AssertReturn(!(fState & PIPE_READMODE_MESSAGE), VERR_INVALID_HANDLE); AssertReturn(cInstances <= 1, VERR_INVALID_HANDLE); /* * Looks kind of OK, create a handle so we can try rtPipeQueryInfo on it * and see if we need to duplicate it to make that call work. */ RTPIPEINTERNAL *pThis = (RTPIPEINTERNAL *)RTMemAllocZ(sizeof(RTPIPEINTERNAL)); if (!pThis) return VERR_NO_MEMORY; int rc = RTCritSectInit(&pThis->CritSect); if (RT_SUCCESS(rc)) { pThis->Overlapped.hEvent = CreateEvent(NULL, TRUE /*fManualReset*/, TRUE /*fInitialState*/, NULL /*pName*/); if (pThis->Overlapped.hEvent != NULL) { pThis->u32Magic = RTPIPE_MAGIC; pThis->hPipe = hNative; pThis->fRead = !!(fFlags & RTPIPE_N_READ); //pThis->fIOPending = false; //pThis->fZeroByteRead = false; //pThis->fBrokenPipe = false; //pThisR->fPromisedWritable= false; //pThis->cUsers = 0; //pThis->pbBounceBuf = NULL; //pThis->cbBounceBufUsed = 0; //pThis->cbBounceBufAlloc= 0; pThis->hPollSet = NIL_RTPOLLSET; HANDLE hNative2 = INVALID_HANDLE_VALUE; FILE_PIPE_LOCAL_INFORMATION Info; if (rtPipeQueryInfo(pThis, &Info)) rc = VINF_SUCCESS; else { if (DuplicateHandle(GetCurrentProcess() /*hSrcProcess*/, hNative /*hSrcHandle*/, GetCurrentProcess() /*hDstProcess*/, &hNative2 /*phDstHandle*/, pThis->fRead ? GENERIC_READ : GENERIC_WRITE | FILE_READ_ATTRIBUTES /*dwDesiredAccess*/, !!(fFlags & RTPIPE_N_INHERIT) /*fInheritHandle*/, 0 /*dwOptions*/)) { pThis->hPipe = hNative2; if (rtPipeQueryInfo(pThis, &Info)) rc = VINF_SUCCESS; else { rc = VERR_ACCESS_DENIED; CloseHandle(hNative2); } } else hNative2 = INVALID_HANDLE_VALUE; } if (RT_SUCCESS(rc)) { /* * Verify the pipe state and correct the inheritability. */ AssertStmt( Info.NamedPipeState == FILE_PIPE_CONNECTED_STATE || Info.NamedPipeState == FILE_PIPE_CLOSING_STATE || Info.NamedPipeState == FILE_PIPE_DISCONNECTED_STATE, VERR_INVALID_HANDLE); AssertStmt( Info.NamedPipeConfiguration == ( Info.NamedPipeEnd == FILE_PIPE_SERVER_END ? (pThis->fRead ? FILE_PIPE_INBOUND : FILE_PIPE_OUTBOUND) : (pThis->fRead ? FILE_PIPE_OUTBOUND : FILE_PIPE_INBOUND) ), VERR_INVALID_HANDLE); if ( RT_SUCCESS(rc) && hNative2 == INVALID_HANDLE_VALUE && !SetHandleInformation(hNative, HANDLE_FLAG_INHERIT /*dwMask*/, fFlags & RTPIPE_N_INHERIT ? HANDLE_FLAG_INHERIT : 0)) { rc = RTErrConvertFromWin32(GetLastError()); AssertMsgFailed(("%Rrc\n", rc)); } if (RT_SUCCESS(rc)) { /* * Ok, we're good! */ if (hNative2 != INVALID_HANDLE_VALUE) CloseHandle(hNative); *phPipe = pThis; return VINF_SUCCESS; } } /* Bail out. */ if (hNative2 != INVALID_HANDLE_VALUE) CloseHandle(hNative2); CloseHandle(pThis->Overlapped.hEvent); } RTCritSectDelete(&pThis->CritSect); } RTMemFree(pThis); return rc; }
RTDECL(int) RTHandleGetStandard(RTHANDLESTD enmStdHandle, PRTHANDLE ph) { /* * Validate and convert input. */ AssertPtrReturn(ph, VERR_INVALID_POINTER); DWORD dwStdHandle; switch (enmStdHandle) { case RTHANDLESTD_INPUT: dwStdHandle = STD_INPUT_HANDLE; break; case RTHANDLESTD_OUTPUT: dwStdHandle = STD_OUTPUT_HANDLE; break; case RTHANDLESTD_ERROR: dwStdHandle = STD_ERROR_HANDLE; break; default: AssertFailedReturn(VERR_INVALID_PARAMETER); } /* * Is the requested descriptor valid and which IPRT handle type does it * best map on to? */ HANDLE hNative = GetStdHandle(dwStdHandle); if (hNative == INVALID_HANDLE_VALUE) return RTErrConvertFromWin32(GetLastError()); DWORD dwInfo; if (!GetHandleInformation(hNative, &dwInfo)) return RTErrConvertFromWin32(GetLastError()); bool const fInherit = RT_BOOL(dwInfo & HANDLE_FLAG_INHERIT); RTHANDLE h; DWORD dwType = GetFileType(hNative); switch (dwType & ~FILE_TYPE_REMOTE) { default: case FILE_TYPE_UNKNOWN: case FILE_TYPE_CHAR: case FILE_TYPE_DISK: h.enmType = RTHANDLETYPE_FILE; break; case FILE_TYPE_PIPE: { DWORD cMaxInstances; DWORD fInfo; if (!GetNamedPipeInfo(hNative, &fInfo, NULL, NULL, &cMaxInstances)) h.enmType = RTHANDLETYPE_SOCKET; else h.enmType = RTHANDLETYPE_PIPE; break; } } /* * Create the IPRT handle. */ int rc; switch (h.enmType) { case RTHANDLETYPE_FILE: rc = RTFileFromNative(&h.u.hFile, (RTHCUINTPTR)hNative); break; case RTHANDLETYPE_PIPE: rc = RTPipeFromNative(&h.u.hPipe, (RTHCUINTPTR)hNative, (enmStdHandle == RTHANDLESTD_INPUT ? RTPIPE_N_READ : RTPIPE_N_WRITE) | (fInherit ? RTPIPE_N_INHERIT : 0)); break; case RTHANDLETYPE_SOCKET: rc = rtSocketCreateForNative(&h.u.hSocket, (RTHCUINTPTR)hNative); break; default: /* shut up gcc */ return VERR_INTERNAL_ERROR; } if (RT_SUCCESS(rc)) *ph = h; return rc; }
ssize_t rpl_write (int fd, const void *buf, size_t count) { for (;;) { ssize_t ret = write_nothrow (fd, buf, count); if (ret < 0) { # if GNULIB_NONBLOCKING if (errno == ENOSPC) { HANDLE h = (HANDLE) _get_osfhandle (fd); if (GetFileType (h) == FILE_TYPE_PIPE) { /* h is a pipe or socket. */ DWORD state; if (GetNamedPipeHandleState (h, &state, NULL, NULL, NULL, NULL, 0) && (state & PIPE_NOWAIT) != 0) { /* h is a pipe in non-blocking mode. We can get here in four situations: 1. When the pipe buffer is full. 2. When count <= pipe_buf_size and the number of free bytes in the pipe buffer is < count. 3. When count > pipe_buf_size and the number of free bytes in the pipe buffer is > 0, < pipe_buf_size. 4. When count > pipe_buf_size and the pipe buffer is entirely empty. The cases 1 and 2 are POSIX compliant. In cases 3 and 4 POSIX specifies that write() must split the request and succeed with a partial write. We fix case 4. We don't fix case 3 because it is not essential for programs. */ DWORD out_size; /* size of the buffer for outgoing data */ DWORD in_size; /* size of the buffer for incoming data */ if (GetNamedPipeInfo (h, NULL, &out_size, &in_size, NULL)) { size_t reduced_count = count; /* In theory we need only one of out_size, in_size. But I don't know which of the two. The description is ambiguous. */ if (out_size != 0 && out_size < reduced_count) reduced_count = out_size; if (in_size != 0 && in_size < reduced_count) reduced_count = in_size; if (reduced_count < count) { /* Attempt to write only the first part. */ count = reduced_count; continue; } } /* Change errno from ENOSPC to EAGAIN. */ errno = EAGAIN; } } } else # endif { # if GNULIB_SIGPIPE if (GetLastError () == ERROR_NO_DATA && GetFileType ((HANDLE) _get_osfhandle (fd)) == FILE_TYPE_PIPE) { /* Try to raise signal SIGPIPE. */ raise (SIGPIPE); /* If it is currently blocked or ignored, change errno from EINVAL to EPIPE. */ errno = EPIPE; } # endif } } return ret; } }
void uv_pipe_connect(uv_connect_t* req, uv_pipe_t* handle, const char* name, uv_connect_cb cb) { uv_loop_t* loop = handle->loop; int errno, nameSize; handle->handle = INVALID_HANDLE_VALUE; uv_req_init(loop, (uv_req_t*) req); req->type = UV_CONNECT; req->handle = (uv_stream_t*) handle; req->cb = cb; /* Convert name to UTF16. */ nameSize = uv_utf8_to_utf16(name, NULL, 0) * sizeof(wchar_t); handle->name = (wchar_t*)malloc(nameSize); if (!handle->name) { uv_fatal_error(ERROR_OUTOFMEMORY, "malloc"); } if (!uv_utf8_to_utf16(name, handle->name, nameSize / sizeof(wchar_t))) { errno = GetLastError(); goto error; } if (open_named_pipe(handle) != 0) { if (GetLastError() == ERROR_PIPE_BUSY) { /* Wait for the server to make a pipe instance available. */ if (!QueueUserWorkItem(&pipe_connect_thread_proc, req, WT_EXECUTELONGFUNCTION)) { errno = GetLastError(); goto error; } handle->reqs_pending++; return; } errno = GetLastError(); goto error; } assert(handle->handle != INVALID_HANDLE_VALUE); /* Ensure that what we just opened is actually a pipe */ if (!GetNamedPipeInfo(handle->handle, NULL, NULL, NULL, NULL)) { errno = WSAENOTSOCK; goto error; } if (uv_set_pipe_handle(loop, (uv_pipe_t*)req->handle, handle->handle)) { errno = GetLastError(); goto error; } SET_REQ_SUCCESS(req); uv_insert_pending_req(loop, (uv_req_t*) req); handle->reqs_pending++; return; error: if (handle->name) { free(handle->name); handle->name = NULL; } if (handle->handle != INVALID_HANDLE_VALUE) { CloseHandle(handle->handle); handle->handle = INVALID_HANDLE_VALUE; } /* Make this req pending reporting an error. */ SET_REQ_ERROR(req, errno); uv_insert_pending_req(loop, (uv_req_t*) req); handle->reqs_pending++; return; }