├── Makefile ├── README.md └── main.cpp /Makefile: -------------------------------------------------------------------------------- 1 | CXX=cl 2 | CXXFLAGS=/std:c++20 /W4 /EHsc /nologo 3 | LIBS=psapi.lib 4 | 5 | OBJS=main.obj 6 | TARGET=tm.exe 7 | 8 | all: $(TARGET) 9 | 10 | $(TARGET): $(OBJS) 11 | $(CXX) $(CXXFLAGS) /Fe$@ $(OBJS) /link $(LIBS) 12 | 13 | main.obj: main.cpp 14 | $(CXX) $(CXXFLAGS) /c main.cpp 15 | 16 | clean: 17 | del /q $(OBJS) $(TARGET) 2>nul 18 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # tm — lightweight console Task Manager for Windows 2 | 3 | `tm` is a small C++20 command-line tool that mimics a BSD-style `ps`/`top` view on Windows. It enumerates processes, shows per-process CPU% (sampled over time), working-set memory, thread count, and parent PID, and can optionally terminate a process. 4 | 5 | The default run prints one snapshot sized to fit your console without scrolling. Top mode refreshes the display at a user-controlled interval. 6 | 7 | ## Features 8 | - One-shot snapshot similar to `ps` with a concise summary header. 9 | - Top mode that refreshes in-place (default every 1 second). 10 | - Console-aware row limiting to avoid scrolling; override with `-n/--numprocs`. 11 | - Kill a process by PID with `-k/--kill`. 12 | - C++20 implementation using Win32 APIs (`Toolhelp32Snapshot`, `GetProcessTimes`, `GetSystemTimes`, `GetProcessMemoryInfo`). 13 | 14 | ## Usage 15 | ``` 16 | tm.exe [options] 17 | 18 | Options: 19 | -t, --top Refresh continuously (top mode) 20 | -s, --seconds Seconds between refreshes (implies -t) 21 | -n, --numprocs Max processes to display (default: fit console) 22 | -k, --kill Terminate a process 23 | -h, -?, --help Show this help 24 | ``` 25 | 26 | Examples: 27 | - One-shot snapshot: `tm.exe` 28 | - Top mode, default 1s refresh: `tm.exe -t` 29 | - Top mode every 2.5 seconds: `tm.exe -s 2.5` 30 | - Limit rows to 30: `tm.exe -n 30` 31 | - Kill PID 1234, then exit: `tm.exe -k 1234` 32 | - Kill and then stay in top mode: `tm.exe -k 1234 -t` 33 | 34 | ## How it works (brief) 35 | - Processes are enumerated with `CreateToolhelp32Snapshot`. 36 | - Per-process CPU% is approximated by comparing `GetProcessTimes` deltas to `GetSystemTimes` deltas across samples. 37 | - Memory comes from `GetProcessMemoryInfo` (working set in MB). 38 | - Console height is read via `GetConsoleScreenBufferInfo`; the tool subtracts header lines so the summary stays visible. 39 | 40 | ## Building on Windows (novice-friendly) 41 | You need the Microsoft C++ toolchain (Visual Studio or Build Tools) and `nmake`. 42 | 43 | 1) Install: 44 | - Install **Visual Studio 2022** (any edition) with the **Desktop development with C++** workload; or 45 | - Install **Build Tools for Visual Studio 2022** with the same C++ workload. 46 | 47 | 2) Open a **Developer Command Prompt for VS 2022** (or x64 Native Tools Command Prompt). This preconfigures `cl` and `nmake`. 48 | 49 | 3) In the project folder, build: 50 | ``` 51 | nmake 52 | ``` 53 | This uses the provided `Makefile` (targets `tm.exe`, links `psapi.lib`). 54 | 55 | Alternate: run the helper script that sets up the environment then builds: 56 | ``` 57 | cmd /c build.cmd 58 | ``` 59 | (This calls `vcvarsall.bat x64` and then `nmake`.) 60 | 61 | 4) Run: 62 | ``` 63 | tm.exe 64 | ``` 65 | 66 | ## File overview 67 | - `main.cpp` — the entire application (argument parsing, sampling, rendering). 68 | - `Makefile` — `nmake` build rules using `cl` and `psapi.lib`. 69 | - `build.cmd` — convenience script to load the MSVC environment and invoke `nmake`. 70 | 71 | ## Notes and limitations 72 | - CPU% is sampled; very short-lived processes might show 0% or not appear. 73 | - Requires access rights to query or terminate target processes; some system processes may not open or kill. 74 | - Output is UTF-8; executable names are narrow-converted from wide strings. 75 | 76 | Enjoy! Contributions and tweaks are welcome.*** 77 | -------------------------------------------------------------------------------- /main.cpp: -------------------------------------------------------------------------------- 1 | // *************************************************************************** 2 | // 3 | // ░██████████░███ ░███ 4 | // ░██ ░████ ░████ 5 | // ░██ ░██░██ ░██░██ ░███████ ░██ ░██ ░███████ 6 | // ░██ ░██ ░████ ░██ ░██ ░██ ░██ ░██ ░██ ░██ 7 | // ░██ ░██ ░██ ░██ ░█████████ ░█████ ░█████████ 8 | // ░██ ░██ ░██ ░██ ░██ ░██ ░██ 9 | // ░██ ░██ ░██ ░██ ░███████ ░██ ░██ ░███████ 10 | // 11 | // 12 | // ════════════════════════════════════════════════════════════════════════ 13 | // 14 | // PROGRAM: CONSOLE BASED TASK MANAGER 15 | // MODULE: MAIN.CPP 16 | // VERSION: 0.2 17 | // DATE: DECEMBER 2025 18 | // 19 | // ════════════════════════════════════════════════════════════════════════ 20 | // 21 | // DESCRIPTION: 22 | // 23 | // main processes options, spawns top mode or single snapshot, and 24 | // handles process termination requests. 25 | // 26 | // ════════════════════════════════════════════════════════════════════════ 27 | // 28 | // AUTHOR: DAVE PLUMMER AND VARIOUS AI ASSISTS 29 | // LICENSE: GPL 2.0 30 | // 31 | // *************************************************************************** 32 | 33 | #define NOMINMAX 34 | #include 35 | #include 36 | #include 37 | #include 38 | #include 39 | #include 40 | #include 41 | #include 42 | #include 43 | #include 44 | #include 45 | #include 46 | #include 47 | #include 48 | 49 | struct Options { 50 | bool topMode{false}; 51 | double intervalSeconds{1.0}; 52 | std::optional killPid; 53 | size_t maxProcs{0}; // 0 = auto-fit to console height 54 | }; 55 | 56 | // Converts FILETIME to a 64-bit tick count. 57 | uint64_t fileTimeToUint64(const FILETIME &ft) { 58 | ULARGE_INTEGER li; 59 | li.LowPart = ft.dwLowDateTime; 60 | li.HighPart = ft.dwHighDateTime; 61 | return li.QuadPart; 62 | } 63 | 64 | // Reads total non-idle system time ticks for CPU% calculations. 65 | uint64_t readSystemTime() { 66 | FILETIME idle{}, kernel{}, user{}; 67 | if (!GetSystemTimes(&idle, &kernel, &user)) { 68 | return 0; 69 | } 70 | const uint64_t kernelTime = fileTimeToUint64(kernel); 71 | const uint64_t userTime = fileTimeToUint64(user); 72 | const uint64_t idleTime = fileTimeToUint64(idle); 73 | return (kernelTime + userTime) - idleTime; 74 | } 75 | 76 | // Converts wide strings to UTF-8 narrow strings. 77 | std::string narrow(const wchar_t *wstr) { 78 | if (!wstr) { 79 | return {}; 80 | } 81 | const int len = WideCharToMultiByte(CP_UTF8, 0, wstr, -1, nullptr, 0, nullptr, nullptr); 82 | if (len <= 0) { 83 | return {}; 84 | } 85 | std::string result(static_cast(len - 1), '\0'); 86 | WideCharToMultiByte(CP_UTF8, 0, wstr, -1, result.data(), len, nullptr, nullptr); 87 | return result; 88 | } 89 | 90 | // Builds a readable Windows error message. 91 | std::string formatError(DWORD code) { 92 | LPWSTR buffer = nullptr; 93 | const DWORD flags = FORMAT_MESSAGE_FROM_SYSTEM | FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_IGNORE_INSERTS; 94 | const DWORD length = FormatMessageW(flags, nullptr, code, 0, reinterpret_cast(&buffer), 0, nullptr); 95 | if (length == 0 || buffer == nullptr) { 96 | return "unknown error"; 97 | } 98 | std::wstring message(buffer, buffer + length); 99 | LocalFree(buffer); 100 | std::string result = narrow(message.c_str()); 101 | while (!result.empty() && (result.back() == '\r' || result.back() == '\n')) { 102 | result.pop_back(); 103 | } 104 | return result; 105 | } 106 | 107 | // Formats uptime in a friendly d/h/m/s string. 108 | std::string formatUptime(uint64_t milliseconds) { 109 | uint64_t totalSeconds = milliseconds / 1000; 110 | const uint64_t days = totalSeconds / 86400; 111 | totalSeconds %= 86400; 112 | const uint64_t hours = totalSeconds / 3600; 113 | totalSeconds %= 3600; 114 | const uint64_t minutes = totalSeconds / 60; 115 | const uint64_t seconds = totalSeconds % 60; 116 | std::ostringstream oss; 117 | if (days > 0) { 118 | oss << days << "d "; 119 | } 120 | oss << hours << "h " << minutes << "m " << seconds << "s"; 121 | return oss.str(); 122 | } 123 | 124 | struct ProcessRow { 125 | DWORD pid{0}; 126 | DWORD ppid{0}; 127 | double cpuPercent{0.0}; 128 | double workingSetMb{0.0}; 129 | DWORD threads{0}; 130 | std::string name; 131 | }; 132 | 133 | // Tracks per-process CPU deltas between samples to approximate top-like CPU%. 134 | class ProcessSampler { 135 | public: 136 | // Collects process info and computes CPU% based on previous sample. 137 | std::vector sample() { 138 | const uint64_t systemTime = readSystemTime(); 139 | std::unordered_map currentTimes; 140 | HANDLE snapshot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0); 141 | if (snapshot == INVALID_HANDLE_VALUE) { 142 | return {}; 143 | } 144 | 145 | PROCESSENTRY32W entry{}; 146 | entry.dwSize = sizeof(entry); 147 | BOOL hasProcess = Process32FirstW(snapshot, &entry); 148 | std::vector rows; 149 | rows.reserve(128); 150 | 151 | while (hasProcess) { 152 | ProcessRow row{}; 153 | row.pid = entry.th32ProcessID; 154 | row.ppid = entry.th32ParentProcessID; 155 | row.threads = entry.cntThreads; 156 | row.name = narrow(entry.szExeFile); 157 | 158 | uint64_t procTime = 0; 159 | SIZE_T workingSet = 0; 160 | HANDLE handle = OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION | PROCESS_VM_READ, FALSE, entry.th32ProcessID); 161 | if (handle) { 162 | FILETIME create{}, exit{}, kernel{}, user{}; 163 | if (GetProcessTimes(handle, &create, &exit, &kernel, &user)) { 164 | procTime = fileTimeToUint64(kernel) + fileTimeToUint64(user); 165 | } 166 | 167 | PROCESS_MEMORY_COUNTERS pmc{}; 168 | if (GetProcessMemoryInfo(handle, &pmc, sizeof(pmc))) { 169 | workingSet = pmc.WorkingSetSize; 170 | } 171 | CloseHandle(handle); 172 | } 173 | 174 | currentTimes[row.pid] = procTime; 175 | 176 | double cpu = 0.0; 177 | if (prevSystemTime_ != 0 && systemTime > prevSystemTime_) { 178 | const auto it = prevProcTimes_.find(row.pid); 179 | if (it != prevProcTimes_.end() && procTime >= it->second) { 180 | const uint64_t procDelta = procTime - it->second; 181 | const uint64_t sysDelta = systemTime - prevSystemTime_; 182 | if (sysDelta > 0) { 183 | cpu = 100.0 * static_cast(procDelta) / static_cast(sysDelta); 184 | } 185 | } 186 | } 187 | 188 | row.cpuPercent = cpu; 189 | row.workingSetMb = static_cast(workingSet) / (1024.0 * 1024.0); 190 | rows.push_back(std::move(row)); 191 | hasProcess = Process32NextW(snapshot, &entry); 192 | } 193 | 194 | CloseHandle(snapshot); 195 | prevProcTimes_ = std::move(currentTimes); 196 | prevSystemTime_ = systemTime; 197 | lastProcessCount_ = rows.size(); 198 | 199 | std::sort(rows.begin(), rows.end(), [](const ProcessRow &a, const ProcessRow &b) { 200 | if (a.cpuPercent == b.cpuPercent) { 201 | return a.pid < b.pid; 202 | } 203 | return a.cpuPercent > b.cpuPercent; 204 | }); 205 | return rows; 206 | } 207 | 208 | // Returns number of processes seen in the last sample. 209 | size_t lastProcessCount() const { return lastProcessCount_; } 210 | 211 | private: 212 | uint64_t prevSystemTime_{0}; 213 | std::unordered_map prevProcTimes_{}; 214 | size_t lastProcessCount_{0}; 215 | }; 216 | 217 | // Prints usage/help text. 218 | void printUsage(const char *exe) { 219 | std::cout << "Usage: " << exe << " [options]\n\n" 220 | << " -t, --top Refresh continuously (top mode)\n" 221 | << " -s, --seconds Seconds between refreshes (implies -t)\n" 222 | << " -n, --numprocs Max processes to display (default: fit console)\n" 223 | << " -k, --kill Terminate a process\n" 224 | << " -h, -?, --help Show this help\n"; 225 | } 226 | 227 | // Parses seconds value and validates positivity. 228 | bool parseSeconds(const std::string &value, double &out) { 229 | try { 230 | out = std::stod(value); 231 | return out > 0.0; 232 | } catch (...) { 233 | return false; 234 | } 235 | } 236 | 237 | // Parses a PID as unsigned integer. 238 | bool parsePid(const std::string &value, DWORD &pid) { 239 | try { 240 | const unsigned long parsed = std::stoul(value); 241 | pid = static_cast(parsed); 242 | return true; 243 | } catch (...) { 244 | return false; 245 | } 246 | } 247 | 248 | // Parses a positive count for row limiting. 249 | bool parseCount(const std::string &value, size_t &count) { 250 | try { 251 | const unsigned long parsed = std::stoul(value); 252 | if (parsed == 0) { 253 | return false; 254 | } 255 | count = static_cast(parsed); 256 | return true; 257 | } catch (...) { 258 | return false; 259 | } 260 | } 261 | 262 | // Terminates a process by PID with error reporting. 263 | bool killProcess(DWORD pid, std::string &error) { 264 | HANDLE handle = OpenProcess(PROCESS_TERMINATE, FALSE, pid); 265 | if (!handle) { 266 | error = "OpenProcess failed: " + formatError(GetLastError()); 267 | return false; 268 | } 269 | const BOOL terminated = TerminateProcess(handle, 1); 270 | CloseHandle(handle); 271 | if (!terminated) { 272 | error = "TerminateProcess failed: " + formatError(GetLastError()); 273 | return false; 274 | } 275 | return true; 276 | } 277 | 278 | // Prints the top-style summary header (uptime, mem, CPU count, procs). 279 | void printSummary(double intervalSeconds, size_t processCount) { 280 | SYSTEM_INFO sysInfo{}; 281 | GetSystemInfo(&sysInfo); 282 | MEMORYSTATUSEX mem{}; 283 | mem.dwLength = sizeof(mem); 284 | GlobalMemoryStatusEx(&mem); 285 | 286 | const double totalMb = static_cast(mem.ullTotalPhys) / (1024.0 * 1024.0); 287 | const double usedMb = static_cast(mem.ullTotalPhys - mem.ullAvailPhys) / (1024.0 * 1024.0); 288 | 289 | const uint64_t uptimeMs = GetTickCount64(); 290 | const auto now = std::chrono::system_clock::now(); 291 | const std::time_t nowTime = std::chrono::system_clock::to_time_t(now); 292 | 293 | std::cout << "top-like view (interval " << std::fixed << std::setprecision(2) << intervalSeconds 294 | << "s) | procs: " << processCount 295 | << " | uptime: " << formatUptime(uptimeMs) << '\n'; 296 | 297 | std::tm local{}; 298 | #if defined(_WIN32) 299 | localtime_s(&local, &nowTime); 300 | #else 301 | local = *std::localtime(&nowTime); 302 | #endif 303 | std::cout << "Time: " << std::put_time(&local, "%Y-%m-%d %H:%M:%S") 304 | << " | Mem: " << std::setprecision(1) << usedMb << "MB/" << totalMb << "MB" 305 | << " | Logical CPUs: " << sysInfo.dwNumberOfProcessors << "\n\n"; 306 | } 307 | 308 | // Prints the process table up to maxRows entries. 309 | void printTable(const std::vector &rows, size_t maxRows) { 310 | std::cout << std::left << std::setw(7) << "PID" 311 | << std::setw(7) << "PPID" 312 | << std::right << std::setw(8) << "CPU%" 313 | << std::setw(12) << "MEM(MB)" 314 | << std::setw(9) << "THREADS" 315 | << " NAME" << '\n'; 316 | std::cout << std::string(60, '-') << '\n'; 317 | std::cout << std::fixed << std::setprecision(1); 318 | 319 | const size_t limit = maxRows > 0 ? std::min(maxRows, rows.size()) : rows.size(); 320 | for (size_t idx = 0; idx < limit; ++idx) { 321 | const auto &row = rows[idx]; 322 | std::cout << std::left << std::setw(7) << row.pid 323 | << std::setw(7) << row.ppid 324 | << std::right << std::setw(8) << std::setprecision(1) << row.cpuPercent 325 | << std::setw(12) << std::setprecision(1) << row.workingSetMb 326 | << std::setw(9) << row.threads 327 | << " " << row.name << '\n'; 328 | } 329 | } 330 | 331 | // Parses all CLI options and detects bad/ help cases. 332 | Options parseOptions(int argc, char *argv[], bool &showHelp, bool &badArgs) { 333 | Options opts{}; 334 | bool secondsProvided = false; 335 | for (int i = 1; i < argc; ++i) { 336 | std::string arg = argv[i]; 337 | if (arg == "-h" || arg == "-?" || arg == "--help") { 338 | showHelp = true; 339 | return opts; 340 | } else if (arg == "-t" || arg == "--top") { 341 | opts.topMode = true; 342 | } else if (arg.rfind("--top=", 0) == 0) { 343 | const auto value = arg.substr(std::string("--top=").size()); 344 | const auto lower = [value]() { 345 | std::string v = value; 346 | std::transform(v.begin(), v.end(), v.begin(), [](unsigned char c) { return static_cast(std::tolower(c)); }); 347 | return v; 348 | }(); 349 | if (lower == "1" || lower == "true" || lower == "yes" || lower == "on") { 350 | opts.topMode = true; 351 | } else if (lower == "0" || lower == "false" || lower == "no" || lower == "off") { 352 | opts.topMode = false; 353 | } else { 354 | badArgs = true; 355 | return opts; 356 | } 357 | } else if (arg == "-s" || arg == "--seconds") { 358 | if (i + 1 >= argc) { 359 | badArgs = true; 360 | return opts; 361 | } 362 | double seconds = 0.0; 363 | if (!parseSeconds(argv[++i], seconds)) { 364 | badArgs = true; 365 | return opts; 366 | } 367 | opts.intervalSeconds = seconds; 368 | secondsProvided = true; 369 | } else if (arg == "-k" || arg == "--kill") { 370 | if (i + 1 >= argc) { 371 | badArgs = true; 372 | return opts; 373 | } 374 | DWORD pid = 0; 375 | if (!parsePid(argv[++i], pid)) { 376 | badArgs = true; 377 | return opts; 378 | } 379 | opts.killPid = pid; 380 | } else if (arg == "-n" || arg == "--numprocs") { 381 | if (i + 1 >= argc) { 382 | badArgs = true; 383 | return opts; 384 | } 385 | size_t count = 0; 386 | if (!parseCount(argv[++i], count)) { 387 | badArgs = true; 388 | return opts; 389 | } 390 | opts.maxProcs = count; 391 | } else if (arg.rfind("--numprocs=", 0) == 0) { 392 | size_t count = 0; 393 | if (!parseCount(arg.substr(std::string("--numprocs=").size()), count)) { 394 | badArgs = true; 395 | return opts; 396 | } 397 | opts.maxProcs = count; 398 | } else { 399 | badArgs = true; 400 | return opts; 401 | } 402 | } 403 | 404 | if (secondsProvided) { 405 | opts.topMode = true; 406 | } 407 | return opts; 408 | } 409 | 410 | // Reads the number of visible console rows (0 if unavailable). 411 | size_t detectConsoleRows() { 412 | const HANDLE hOut = GetStdHandle(STD_OUTPUT_HANDLE); 413 | CONSOLE_SCREEN_BUFFER_INFO info{}; 414 | if (hOut == INVALID_HANDLE_VALUE || !GetConsoleScreenBufferInfo(hOut, &info)) { 415 | return 0; 416 | } 417 | const SHORT rows = info.srWindow.Bottom - info.srWindow.Top + 1; 418 | return rows > 0 ? static_cast(rows) : 0; 419 | } 420 | 421 | // Determines how many process rows to show based on console and user cap. 422 | size_t resolveMaxRows(const Options &opts) { 423 | size_t rows = detectConsoleRows(); 424 | // summary block (2 lines + trailing blank) + table header (2 lines) + one extra buffer row 425 | constexpr size_t summaryLines = 3; 426 | constexpr size_t tableHeaderLines = 2; 427 | constexpr size_t extraPad = 1; 428 | const size_t reserved = summaryLines + tableHeaderLines + extraPad; 429 | if (rows == 0) { 430 | rows = 24; // fallback when console size unknown 431 | } 432 | const size_t visibleCapacity = rows > reserved ? rows - reserved : 0; 433 | 434 | if (opts.maxProcs > 0) { 435 | // Honor user cap but do not exceed visible rows when known. 436 | if (visibleCapacity > 0) { 437 | return std::min(visibleCapacity, opts.maxProcs); 438 | } 439 | return opts.maxProcs; 440 | } 441 | return visibleCapacity; 442 | } 443 | 444 | // Runs a single snapshot: warm up sampling, wait briefly, then print. 445 | void renderOnce(ProcessSampler &sampler, double initialDelaySeconds, const Options &opts) { 446 | sampler.sample(); 447 | std::this_thread::sleep_for(std::chrono::duration(initialDelaySeconds)); 448 | const auto rows = sampler.sample(); 449 | const size_t maxRows = resolveMaxRows(opts); 450 | printSummary(initialDelaySeconds, rows.size()); 451 | printTable(rows, maxRows); 452 | } 453 | 454 | // Runs continuous top-style refresh until interrupted. 455 | void renderTop(ProcessSampler &sampler, double intervalSeconds, const Options &opts) { 456 | sampler.sample(); 457 | while (true) { 458 | std::this_thread::sleep_for(std::chrono::duration(intervalSeconds)); 459 | const auto rows = sampler.sample(); 460 | system("cls"); 461 | const size_t maxRows = resolveMaxRows(opts); 462 | printSummary(intervalSeconds, rows.size()); 463 | printTable(rows, maxRows); 464 | } 465 | } 466 | 467 | // Entry point: parse options, handle kill, then render snapshot or top mode. 468 | int main(int argc, char *argv[]) { 469 | std::ios::sync_with_stdio(false); 470 | 471 | bool showHelp = false; 472 | bool badArgs = false; 473 | Options opts = parseOptions(argc, argv, showHelp, badArgs); 474 | 475 | if (showHelp) { 476 | printUsage(argv[0]); 477 | return 0; 478 | } 479 | if (badArgs) { 480 | printUsage(argv[0]); 481 | return 1; 482 | } 483 | 484 | if (opts.killPid) { 485 | std::string error; 486 | if (killProcess(*opts.killPid, error)) { 487 | std::cout << "Killed PID " << *opts.killPid << "\n"; 488 | } else { 489 | std::cerr << error << "\n"; 490 | return 1; 491 | } 492 | if (!opts.topMode) { 493 | return 0; 494 | } 495 | } 496 | 497 | ProcessSampler sampler; 498 | constexpr double initialDeltaSeconds = 0.2; 499 | 500 | if (opts.topMode) { 501 | renderTop(sampler, opts.intervalSeconds, opts); 502 | } else { 503 | renderOnce(sampler, initialDeltaSeconds, opts); 504 | } 505 | return 0; 506 | } 507 | --------------------------------------------------------------------------------