diff options
author | 2017-06-28 16:05:23 +0200 | |
---|---|---|
committer | 2017-06-29 09:25:27 +0200 | |
commit | f07023471d261abbefee65adb2ca769b1da0ba42 (patch) | |
tree | 5450696a88dacabfcfa65a008e9edb0a4b1450ff /src/main/native/windows_processes.cc | |
parent | 1e0628d18c2e3defaddd935ffb41edbf3ef40339 (diff) |
Windows, JNI: move around sources
Move the Windows JNI C++ sources to a separate
package and separate namespace.
This no-op refactoring allows other build rules
than Bazel's client library to depend on file I/O
and/or JNI functionality.
A follow-up commit will split the
//src/main/native/windows:processes library into
:jni-processes and :jni-file.
Change-Id: I33c5f8ebd8961cc440db3b4a95ff78024d7c1d74
PiperOrigin-RevId: 160404298
Diffstat (limited to 'src/main/native/windows_processes.cc')
-rw-r--r-- | src/main/native/windows_processes.cc | 647 |
1 files changed, 0 insertions, 647 deletions
diff --git a/src/main/native/windows_processes.cc b/src/main/native/windows_processes.cc deleted file mode 100644 index cf0b677c93..0000000000 --- a/src/main/native/windows_processes.cc +++ /dev/null @@ -1,647 +0,0 @@ -// Copyright 2016 The Bazel Authors. All rights reserved. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -#define WINVER 0x0601 -#define _WIN32_WINNT 0x0601 - -#include <ctype.h> -#include <jni.h> -#include <stdlib.h> -#include <string.h> -#include <wchar.h> -#include <windows.h> - -#include <atomic> -#include <functional> -#include <memory> -#include <string> -#include <type_traits> // static_assert - -#include "src/main/native/windows_file_operations.h" -#include "src/main/native/windows_util.h" - -// Ensure we can safely cast (const) jchar* to (const) WCHAR* and LP(C)WSTR. -// This is true with MSVC but not always with GCC. -static_assert(sizeof(jchar) == sizeof(WCHAR), - "jchar and WCHAR should be the same size"); - -extern "C" JNIEXPORT jint JNICALL -Java_com_google_devtools_build_lib_windows_WindowsProcesses_nativeGetpid( - JNIEnv* env, jclass clazz) { - return GetCurrentProcessId(); -} - -struct NativeOutputStream { - HANDLE handle_; - std::string error_; - std::atomic<bool> closed_; - NativeOutputStream() - : handle_(INVALID_HANDLE_VALUE), - error_(""), - closed_(false) {} - - void close() { - closed_.store(true); - if (handle_ == INVALID_HANDLE_VALUE) { - return; - } - - CancelIoEx(handle_, NULL); - CloseHandle(handle_); - handle_ = INVALID_HANDLE_VALUE; - } -}; - -struct NativeProcess { - HANDLE stdin_; - NativeOutputStream stdout_; - NativeOutputStream stderr_; - HANDLE process_; - HANDLE job_; - DWORD pid_; - std::string error_; - - NativeProcess() - : stdin_(INVALID_HANDLE_VALUE), - stdout_(), - stderr_(), - process_(INVALID_HANDLE_VALUE), - job_(INVALID_HANDLE_VALUE), - error_("") {} -}; - -class JavaByteArray { - public: - JavaByteArray(JNIEnv* env, jbyteArray java_array) - : env_(env), - array_(java_array), - size_(java_array != nullptr ? env->GetArrayLength(java_array) : 0), - ptr_(java_array != nullptr ? env->GetByteArrayElements(java_array, NULL) - : nullptr) {} - - ~JavaByteArray() { - if (array_ != nullptr) { - env_->ReleaseByteArrayElements(array_, ptr_, 0); - array_ = nullptr; - size_ = 0; - ptr_ = nullptr; - } - } - - jsize size() { return size_; } - jbyte* ptr() { return ptr_; } - - private: - JNIEnv* env_; - jbyteArray array_; - jsize size_; - jbyte* ptr_; -}; - -static bool NestedJobsSupported() { - OSVERSIONINFOEX version_info; - version_info.dwOSVersionInfoSize = sizeof(version_info); - if (!GetVersionEx(reinterpret_cast<OSVERSIONINFO*>(&version_info))) { - return false; - } - - return version_info.dwMajorVersion > 6 || - version_info.dwMajorVersion == 6 && version_info.dwMinorVersion >= 2; -} - -static void MaybeReportLastError(const std::string& reason, JNIEnv* env, - jobjectArray error_msg_holder) { - if (error_msg_holder != nullptr && - env->GetArrayLength(error_msg_holder) > 0) { - std::string error_str = windows_util::GetLastErrorString(reason); - jstring error_msg = env->NewStringUTF(error_str.c_str()); - env->SetObjectArrayElement(error_msg_holder, 0, error_msg); - } -} - -static std::string GetJavaUTFString(JNIEnv* env, jstring str) { - std::string result; - if (str != nullptr) { - const char* jstr = env->GetStringUTFChars(str, nullptr); - result.assign(jstr); - env->ReleaseStringUTFChars(str, jstr); - } - return result; -} - -static std::wstring GetJavaWstring(JNIEnv* env, jstring str) { - std::wstring result; - if (str != nullptr) { - const jchar* jstr = env->GetStringChars(str, nullptr); - // We can safely reinterpret_cast because of the static_assert checking that - // sizeof(jchar) = sizeof(WCHAR). - result.assign(reinterpret_cast<const WCHAR*>(jstr)); - env->ReleaseStringChars(str, jstr); - } - return result; -} - -static std::wstring AddUncPrefixMaybe(const std::wstring& path) { - return (path.size() >= MAX_PATH) ? (std::wstring(L"\\\\?\\") + path) : path; -} - -static jlong PtrAsJlong(void* p) { return reinterpret_cast<jlong>(p); } - -static std::string AsExecutableForCreateProcess(JNIEnv* env, jstring path, - std::string* result) { - return windows_util::AsExecutablePathForCreateProcess( - GetJavaUTFString(env, path), - [env, path]() { return GetJavaWstring(env, path); }, result); -} - -extern "C" JNIEXPORT jlong JNICALL -Java_com_google_devtools_build_lib_windows_WindowsProcesses_nativeCreateProcess( - JNIEnv* env, jclass clazz, jstring java_argv0, jstring java_argv_rest, - jbyteArray java_env, jstring java_cwd, jstring java_stdout_redirect, - jstring java_stderr_redirect) { - NativeProcess* result = new NativeProcess(); - - std::string argv0; - std::string error_msg(AsExecutableForCreateProcess(env, java_argv0, &argv0)); - if (!error_msg.empty()) { - result->error_ = error_msg; - return PtrAsJlong(result); - } - - std::string commandline = argv0 + " " + GetJavaUTFString(env, java_argv_rest); - std::wstring stdout_redirect = - AddUncPrefixMaybe(GetJavaWstring(env, java_stdout_redirect)); - std::wstring stderr_redirect = - AddUncPrefixMaybe(GetJavaWstring(env, java_stderr_redirect)); - std::string cwd; - error_msg = windows_util::AsShortPath( - GetJavaUTFString(env, java_cwd), - [env, java_cwd]() { return GetJavaWstring(env, java_cwd); }, - &cwd); - if (!error_msg.empty()) { - result->error_ = error_msg; - return PtrAsJlong(result); - } - - std::unique_ptr<char[]> mutable_commandline(new char[commandline.size() + 1]); - strncpy(mutable_commandline.get(), commandline.c_str(), - commandline.size() + 1); - - SECURITY_ATTRIBUTES sa = {0}; - sa.nLength = sizeof(SECURITY_ATTRIBUTES); - sa.bInheritHandle = TRUE; - - // Standard file handles are closed even if the process was successfully - // created. If this was not so, operations on these file handles would not - // return immediately if the process is terminated. - // Therefore we make these handles auto-closing (by using AutoHandle). - windows_util::AutoHandle stdin_process; - windows_util::AutoHandle stdout_process; - windows_util::AutoHandle stderr_process; - windows_util::AutoHandle thread; - PROCESS_INFORMATION process_info = {0}; - STARTUPINFOA startup_info = {0}; - JOBOBJECT_EXTENDED_LIMIT_INFORMATION job_info = {0}; - - JavaByteArray env_map(env, java_env); - if (env_map.ptr() != nullptr) { - if (env_map.size() < 2) { - result->error_ = "The environment must be at least two bytes long"; - return PtrAsJlong(result); - } else if (env_map.ptr()[env_map.size() - 1] != 0 || - env_map.ptr()[env_map.size() - 2] != 0) { - result->error_ = "Environment array must end with two null bytes"; - return PtrAsJlong(result); - } - } - - if (!CreatePipe(&stdin_process.handle, &result->stdin_, &sa, 0)) { - result->error_ = windows_util::GetLastErrorString("CreatePipe(stdin)"); - return PtrAsJlong(result); - } - - if (!stdout_redirect.empty()) { - result->stdout_.close(); - - stdout_process.handle = CreateFileW( - /* lpFileName */ stdout_redirect.c_str(), - /* dwDesiredAccess */ FILE_APPEND_DATA, - /* dwShareMode */ 0, - /* lpSecurityAttributes */ &sa, - /* dwCreationDisposition */ OPEN_ALWAYS, - /* dwFlagsAndAttributes */ FILE_ATTRIBUTE_NORMAL, - /* hTemplateFile */ NULL); - - if (!stdout_process.IsValid()) { - result->error_ = windows_util::GetLastErrorString("CreateFile(stdout)"); - return PtrAsJlong(result); - } - } else { - if (!CreatePipe(&result->stdout_.handle_, &stdout_process.handle, &sa, 0)) { - result->error_ = windows_util::GetLastErrorString("CreatePipe(stdout)"); - return PtrAsJlong(result); - } - } - - // The value of the stderr HANDLE. - // If stdout and stderr are redirected to the same file, this will be equal to - // stdout_process.handle and stderr_process.handle will be - // INVALID_HANDLE_VALUE, so the two AutoHandle objects' d'tors will not - // attempt to close stdout's handle twice. - // If stdout != stderr, then stderr_handle = stderr_process.handle, and these - // are distinct from stdout_process.handle, so again the d'tors will do the - // right thing, closing the handles. - // In both cases, we DO NOT close stderr_handle, since it's either - // stdout_process's or stderr_process's d'tor doing so. - HANDLE stderr_handle = INVALID_HANDLE_VALUE; - - if (!stderr_redirect.empty()) { - result->stderr_.close(); - if (stdout_redirect == stderr_redirect) { - stderr_handle = stdout_process.handle; - // do not set stderr_process.handle; it equals stdout_process.handle and - // the AutoHandle d'tor would attempt to close it again - } else { - stderr_handle = CreateFileW( - /* lpFileName */ stderr_redirect.c_str(), - /* dwDesiredAccess */ FILE_APPEND_DATA, - /* dwShareMode */ 0, - /* lpSecurityAttributes */ &sa, - /* dwCreationDisposition */ OPEN_ALWAYS, - /* dwFlagsAndAttributes */ FILE_ATTRIBUTE_NORMAL, - /* hTemplateFile */ NULL); - - if (stderr_handle == INVALID_HANDLE_VALUE) { - result->error_ = windows_util::GetLastErrorString("CreateFile(stderr)"); - return PtrAsJlong(result); - } - // stderr_process != stdout_process, so set its handle, so the AutoHandle - // d'tor will close it - stderr_process.handle = stderr_handle; - } - } else { - if (!CreatePipe(&result->stderr_.handle_, &stderr_handle, &sa, 0)) { - result->error_ = windows_util::GetLastErrorString("CreatePipe(stderr)"); - return PtrAsJlong(result); - } - stderr_process.handle = stderr_handle; - } - - // MDSN says that the default for job objects is that breakaway is not - // allowed. Thus, we don't need to do any more setup here. - HANDLE job = CreateJobObject(NULL, NULL); - if (job == NULL) { - result->error_ = windows_util::GetLastErrorString("CreateJobObject()"); - return PtrAsJlong(result); - } - - result->job_ = job; - - job_info.BasicLimitInformation.LimitFlags = - JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE; - if (!SetInformationJobObject( - job, - JobObjectExtendedLimitInformation, - &job_info, - sizeof(job_info))) { - result->error_ = - windows_util::GetLastErrorString("SetInformationJobObject()"); - return PtrAsJlong(result); - } - - startup_info.hStdInput = stdin_process.handle; - startup_info.hStdOutput = stdout_process.handle; - startup_info.hStdError = stderr_handle; - startup_info.dwFlags |= STARTF_USESTDHANDLES; - - BOOL ok = CreateProcessA( - /* lpApplicationName */ NULL, - /* lpCommandLine */ mutable_commandline.get(), - /* lpProcessAttributes */ NULL, - /* lpThreadAttributes */ NULL, - /* bInheritHandles */ TRUE, - /* dwCreationFlags */ CREATE_NO_WINDOW // Don't create a console window - | CREATE_NEW_PROCESS_GROUP // So that Ctrl-Break is not propagated - | CREATE_SUSPENDED, // So that it doesn't start a new job itself - /* lpEnvironment */ env_map.ptr(), - /* lpCurrentDirectory */ cwd.empty() ? nullptr : cwd.c_str(), - /* lpStartupInfo */ &startup_info, - /* lpProcessInformation */ &process_info); - - if (!ok) { - result->error_ = windows_util::GetLastErrorString("CreateProcess()"); - return PtrAsJlong(result); - } - - result->pid_ = process_info.dwProcessId; - result->process_ = process_info.hProcess; - thread.handle = process_info.hThread; - - if (!AssignProcessToJobObject(result->job_, result->process_)) { - BOOL is_in_job = false; - if (IsProcessInJob(result->process_, NULL, &is_in_job) - && is_in_job - && !NestedJobsSupported()) { - // We are on a pre-Windows 8 system and the Bazel is already in a job. - // We can't create nested jobs, so just revert to TerminateProcess() and - // hope for the best. In batch mode, the launcher puts Bazel in a job so - // that will take care of cleanup once the command finishes. - CloseHandle(result->job_); - result->job_ = INVALID_HANDLE_VALUE; - } else { - result->error_ = - windows_util::GetLastErrorString("AssignProcessToJobObject()"); - return PtrAsJlong(result); - } - } - - // Now that we put the process in a new job object, we can start executing it - if (ResumeThread(thread) == -1) { - result->error_ = windows_util::GetLastErrorString("ResumeThread()"); - return PtrAsJlong(result); - } - - result->error_ = ""; - return PtrAsJlong(result); -} - -extern "C" JNIEXPORT jint JNICALL -Java_com_google_devtools_build_lib_windows_WindowsProcesses_nativeWriteStdin( - JNIEnv *env, jclass clazz, jlong process_long, jbyteArray java_bytes, - jint offset, jint length) { - NativeProcess* process = reinterpret_cast<NativeProcess*>(process_long); - - JavaByteArray bytes(env, java_bytes); - if (offset < 0 || length <= 0 || offset > bytes.size() - length) { - process->error_ = "Array index out of bounds"; - return -1; - } - - DWORD bytes_written; - - if (!::WriteFile(process->stdin_, bytes.ptr() + offset, length, - &bytes_written, NULL)) { - process->error_ = windows_util::GetLastErrorString("WriteFile()"); - bytes_written = -1; - } - - process->error_ = ""; - return bytes_written; -} - -extern "C" JNIEXPORT jlong JNICALL -Java_com_google_devtools_build_lib_windows_WindowsProcesses_nativeGetStdout( - JNIEnv* env, jclass clazz, jlong process_long) { - NativeProcess* process = reinterpret_cast<NativeProcess*>(process_long); - return PtrAsJlong(&process->stdout_); -} - -extern "C" JNIEXPORT jlong JNICALL -Java_com_google_devtools_build_lib_windows_WindowsProcesses_nativeGetStderr( - JNIEnv* env, jclass clazz, jlong process_long) { - NativeProcess* process = reinterpret_cast<NativeProcess*>(process_long); - return PtrAsJlong(&process->stderr_); -} - -extern "C" JNIEXPORT jint JNICALL -Java_com_google_devtools_build_lib_windows_WindowsProcesses_nativeReadStream( - JNIEnv* env, jclass clazz, jlong stream_long, jbyteArray java_bytes, - jint offset, jint length) { - NativeOutputStream* stream = - reinterpret_cast<NativeOutputStream*>(stream_long); - - JavaByteArray bytes(env, java_bytes); - if (offset < 0 || length <= 0 || offset > bytes.size() - length) { - stream->error_ = "Array index out of bounds"; - return -1; - } - - if (stream->handle_ == INVALID_HANDLE_VALUE || stream->closed_.load()) { - stream->error_ = ""; - return 0; - } - - DWORD bytes_read; - if (!::ReadFile(stream->handle_, bytes.ptr() + offset, length, &bytes_read, - NULL)) { - // Check if either the other end closed the pipe or we did it with - // NativeOutputStream.close() . In the latter case, we'll get a "system - // call interrupted" error. - if (GetLastError() == ERROR_BROKEN_PIPE || stream->closed_.load()) { - // End of file. - stream->error_ = ""; - bytes_read = 0; - } else { - stream->error_ = windows_util::GetLastErrorString("ReadFile()"); - bytes_read = -1; - } - } else { - stream->error_ = ""; - } - - return bytes_read; -} - -extern "C" JNIEXPORT jint JNICALL -Java_com_google_devtools_build_lib_windows_WindowsProcesses_nativeGetExitCode( - JNIEnv *env, jclass clazz, jlong process_long) { - NativeProcess* process = reinterpret_cast<NativeProcess*>(process_long); - DWORD exit_code; - if (!GetExitCodeProcess(process->process_, &exit_code)) { - process->error_ = windows_util::GetLastErrorString("GetExitCodeProcess()"); - return -1; - } - - return exit_code; -} - -// return values: -// 0: Wait completed successfully -// 1: Timeout -// 2: Wait returned with an error -extern "C" JNIEXPORT jint JNICALL -Java_com_google_devtools_build_lib_windows_WindowsProcesses_nativeWaitFor( - JNIEnv *env, jclass clazz, jlong process_long, jlong java_timeout) { - NativeProcess* process = reinterpret_cast<NativeProcess*>(process_long); - HANDLE handles[1] = { process->process_ }; - DWORD win32_timeout = java_timeout < 0 ? INFINITE : java_timeout; - jint result; - switch (WaitForMultipleObjects(1, handles, FALSE, win32_timeout)) { - case 0: - result = 0; - break; - - case WAIT_TIMEOUT: - result = 1; - break; - - case WAIT_FAILED: - result = 2; - break; - - default: - process->error_ = "WaitForMultipleObjects() returned unknown result"; - result = 2; - break; - } - - // Close the pipe handles so that any pending nativeReadStream() calls - // return. This will call CancelIoEx() on the file handles in order to make - // ReadFile() in nativeReadStream() return; otherwise, CloseHandle() would - // hang. - // - // This protects against a subprocess being created, it passing the write - // side of the stdout/stderr pipes to a subprocess, then dying. In that case, - // if we didn't do this, the Java side of the code would hang waiting for the - // streams to finish. - // - // An alternative implementation would be to rely on job control terminating - // the subprocesses, but we don't want to assume that it's always available. - process->stdout_.close(); - process->stderr_.close(); - - if (process->stdin_ != INVALID_HANDLE_VALUE) { - CloseHandle(process->stdin_); - process->stdin_ = INVALID_HANDLE_VALUE; - } - - return result; -} - -extern "C" JNIEXPORT jint JNICALL -Java_com_google_devtools_build_lib_windows_WindowsProcesses_nativeGetProcessPid( - JNIEnv *env, jclass clazz, jlong process_long) { - NativeProcess* process = reinterpret_cast<NativeProcess*>(process_long); - process->error_ = ""; - return GetProcessId(process->process_); // MSDN says that this cannot fail -} - -extern "C" JNIEXPORT jboolean JNICALL -Java_com_google_devtools_build_lib_windows_WindowsProcesses_nativeTerminate( - JNIEnv *env, jclass clazz, jlong process_long) { - static const UINT exit_code = 130; // 128 + SIGINT, like on Linux - NativeProcess* process = reinterpret_cast<NativeProcess*>(process_long); - - if (process->job_ != INVALID_HANDLE_VALUE) { - if (!TerminateJobObject(process->job_, exit_code)) { - process->error_ = - windows_util::GetLastErrorString("TerminateJobObject()"); - return JNI_FALSE; - } - } else if (process->process_ != INVALID_HANDLE_VALUE) { - if (!TerminateProcess(process->process_, exit_code)) { - process->error_ = windows_util::GetLastErrorString("TerminateProcess()"); - return JNI_FALSE; - } - } - - process->error_ = ""; - return JNI_TRUE; -} - -extern "C" JNIEXPORT void JNICALL -Java_com_google_devtools_build_lib_windows_WindowsProcesses_nativeDeleteProcess( - JNIEnv* env, jclass clazz, jlong process_long) { - NativeProcess* process = reinterpret_cast<NativeProcess*>(process_long); - - if (process->stdin_ != INVALID_HANDLE_VALUE) { - CloseHandle(process->stdin_); - } - - process->stdout_.close(); - process->stderr_.close(); - - if (process->process_ != INVALID_HANDLE_VALUE) { - CloseHandle(process->process_); - } - - if (process->job_ != INVALID_HANDLE_VALUE) { - CloseHandle(process->job_); - } - - delete process; -} - -extern "C" JNIEXPORT void JNICALL -Java_com_google_devtools_build_lib_windows_WindowsProcesses_nativeCloseStream( - JNIEnv* env, jclass clazz, jlong stream_long) { - NativeOutputStream* stream = - reinterpret_cast<NativeOutputStream*>(stream_long); - stream->close(); -} - -extern "C" JNIEXPORT jstring JNICALL -Java_com_google_devtools_build_lib_windows_WindowsProcesses_nativeProcessGetLastError( - JNIEnv* env, jclass clazz, jlong process_long) { - NativeProcess* process = reinterpret_cast<NativeProcess*>(process_long); - jstring result = env->NewStringUTF(process->error_.c_str()); - process->error_ = ""; - return result; -} - -extern "C" JNIEXPORT jstring JNICALL -Java_com_google_devtools_build_lib_windows_WindowsProcesses_nativeStreamGetLastError( - JNIEnv* env, jclass clazz, jlong stream_long) { - NativeOutputStream* stream = - reinterpret_cast<NativeOutputStream*>(stream_long); - jstring result = env->NewStringUTF(stream->error_.c_str()); - stream->error_ = ""; - return result; -} - -extern "C" JNIEXPORT jint JNICALL -Java_com_google_devtools_build_lib_windows_WindowsFileOperations_nativeIsJunction( - JNIEnv* env, jclass clazz, jstring path, jobjectArray error_msg_holder) { - int result = windows_util::IsJunctionOrDirectorySymlink( - GetJavaWstring(env, path).c_str()); - if (result == windows_util::IS_JUNCTION_ERROR) { - MaybeReportLastError( - std::string("GetFileAttributes(") + GetJavaUTFString(env, path) + ")", - env, error_msg_holder); - } - return result; -} - -extern "C" JNIEXPORT jboolean JNICALL -Java_com_google_devtools_build_lib_windows_WindowsFileOperations_nativeGetLongPath( - JNIEnv* env, jclass clazz, jstring path, jobjectArray result_holder, - jobjectArray error_msg_holder) { - std::unique_ptr<WCHAR[]> result; - bool success = - windows_util::GetLongPath(GetJavaWstring(env, path).c_str(), &result); - if (!success) { - MaybeReportLastError( - std::string("GetLongPathName(") + GetJavaUTFString(env, path) + ")", - env, error_msg_holder); - return JNI_FALSE; - } - env->SetObjectArrayElement( - result_holder, 0, - env->NewString(reinterpret_cast<const jchar*>(result.get()), - wcslen(result.get()))); - return JNI_TRUE; -} - -extern "C" JNIEXPORT jboolean JNICALL -Java_com_google_devtools_build_lib_windows_WindowsFileOperations_nativeCreateJunction( - JNIEnv* env, jclass clazz, jstring name, jstring target, - jobjectArray error_msg_holder) { - std::string error = windows_util::CreateJunction(GetJavaWstring(env, name), - GetJavaWstring(env, target)); - if (!error.empty()) { - MaybeReportLastError(error, env, error_msg_holder); - return JNI_FALSE; - } - return JNI_TRUE; -} |