| 1 | ////////////////////////////////////////////////////////////////////////// |
|---|
| 2 | // Service Helpers. |
|---|
| 3 | // |
|---|
| 4 | // Functions to stop/start/install and uninstall a service. |
|---|
| 5 | // |
|---|
| 6 | // Copyright (c) 2004 MySolutions NORDIC (http://www.medin.nu) |
|---|
| 7 | // |
|---|
| 8 | // Date: 2004-03-13 |
|---|
| 9 | // Author: Michael Medin (mickem@medin.nu) |
|---|
| 10 | // |
|---|
| 11 | // This software is provided "AS IS", without a warranty of any kind. |
|---|
| 12 | // You are free to use/modify this code but leave this header intact. |
|---|
| 13 | // |
|---|
| 14 | ////////////////////////////////////////////////////////////////////////// |
|---|
| 15 | |
|---|
| 16 | #include <windows.h> |
|---|
| 17 | #include <tchar.h> |
|---|
| 18 | #include "ServiceCmd.h" |
|---|
| 19 | #include <strEx.h> |
|---|
| 20 | #include <tchar.h> |
|---|
| 21 | #include <iostream> |
|---|
| 22 | #include <error.hpp> |
|---|
| 23 | |
|---|
| 24 | namespace serviceControll { |
|---|
| 25 | /** |
|---|
| 26 | * Installs the service |
|---|
| 27 | * |
|---|
| 28 | * @param szName |
|---|
| 29 | * @param szDisplayName |
|---|
| 30 | * @param szDependencies |
|---|
| 31 | * |
|---|
| 32 | * @author mickem |
|---|
| 33 | * |
|---|
| 34 | * @date 03-13-2004 |
|---|
| 35 | * |
|---|
| 36 | */ |
|---|
| 37 | void Install(std::wstring szName, std::wstring szDisplayName, std::wstring szDependencies, DWORD dwServiceType, std::wstring args, std::wstring exe) { |
|---|
| 38 | SC_HANDLE schService; |
|---|
| 39 | SC_HANDLE schSCManager; |
|---|
| 40 | |
|---|
| 41 | if (exe.empty()) { |
|---|
| 42 | TCHAR szPath[512]; |
|---|
| 43 | if ( GetModuleFileName( NULL, szPath, 512 ) == 0 ) |
|---|
| 44 | throw SCException(_T("Could not get module")); |
|---|
| 45 | exe = szPath; |
|---|
| 46 | } |
|---|
| 47 | |
|---|
| 48 | std::wstring bin = _T("\"") + exe + _T("\""); |
|---|
| 49 | if (!args.empty()) |
|---|
| 50 | bin += _T(" ") + args; |
|---|
| 51 | schSCManager = OpenSCManager(NULL, NULL, SC_MANAGER_ALL_ACCESS); |
|---|
| 52 | if (!schSCManager) |
|---|
| 53 | throw SCException(_T("OpenSCManager failed:") + error::lookup::last_error()); |
|---|
| 54 | schService = CreateService( |
|---|
| 55 | schSCManager, // SCManager database |
|---|
| 56 | szName.c_str(), // name of service |
|---|
| 57 | szDisplayName.c_str(), // name to display |
|---|
| 58 | SERVICE_ALL_ACCESS, // desired access |
|---|
| 59 | dwServiceType, // service type |
|---|
| 60 | SERVICE_AUTO_START, // start type |
|---|
| 61 | SERVICE_ERROR_NORMAL, // error control type |
|---|
| 62 | bin.c_str(), // service's binary |
|---|
| 63 | NULL, // no load ordering group |
|---|
| 64 | NULL, // no tag identifier |
|---|
| 65 | szDependencies.c_str(), // dependencies |
|---|
| 66 | NULL, // LocalSystem account |
|---|
| 67 | NULL); // no password |
|---|
| 68 | |
|---|
| 69 | if (!schService) { |
|---|
| 70 | DWORD err = GetLastError(); |
|---|
| 71 | CloseServiceHandle(schSCManager); |
|---|
| 72 | if (err==ERROR_SERVICE_EXISTS) { |
|---|
| 73 | throw SCException(_T("Service already installed!")); |
|---|
| 74 | } |
|---|
| 75 | throw SCException(_T("Unable to install service.") + error::lookup::last_error(err)); |
|---|
| 76 | } |
|---|
| 77 | std::wcout << _T("Service ") << szName << _T(" (") << bin << _T(") installed...") << std::endl;; |
|---|
| 78 | CloseServiceHandle(schService); |
|---|
| 79 | CloseServiceHandle(schSCManager); |
|---|
| 80 | } |
|---|
| 81 | |
|---|
| 82 | void ModifyServiceType(LPCTSTR szName, DWORD dwServiceType) { |
|---|
| 83 | SC_HANDLE schService; |
|---|
| 84 | SC_HANDLE schSCManager; |
|---|
| 85 | TCHAR szPath[512]; |
|---|
| 86 | |
|---|
| 87 | if ( GetModuleFileName( NULL, szPath, 512 ) == 0 ) |
|---|
| 88 | throw SCException(_T("Could not get module")); |
|---|
| 89 | |
|---|
| 90 | schSCManager = OpenSCManager(NULL, NULL, SC_MANAGER_ALL_ACCESS); |
|---|
| 91 | if (!schSCManager) |
|---|
| 92 | throw SCException(_T("OpenSCManager failed: ") + error::lookup::last_error()); |
|---|
| 93 | schService = OpenService(schSCManager, szName, SERVICE_ALL_ACCESS); |
|---|
| 94 | if (!schService) { |
|---|
| 95 | DWORD err = GetLastError(); |
|---|
| 96 | CloseServiceHandle(schSCManager); |
|---|
| 97 | throw SCException(_T("Unable to open service: ") + error::lookup::last_error(err)); |
|---|
| 98 | } |
|---|
| 99 | BOOL result = ChangeServiceConfig(schService, dwServiceType, SERVICE_NO_CHANGE, SERVICE_NO_CHANGE , NULL, NULL, NULL, |
|---|
| 100 | NULL, NULL, NULL, NULL); |
|---|
| 101 | CloseServiceHandle(schService); |
|---|
| 102 | CloseServiceHandle(schSCManager); |
|---|
| 103 | if (result != TRUE) |
|---|
| 104 | throw SCException(_T("Could not change service information")); |
|---|
| 105 | } |
|---|
| 106 | |
|---|
| 107 | DWORD GetServiceType(LPCTSTR szName) { |
|---|
| 108 | LPQUERY_SERVICE_CONFIG lpqscBuf = (LPQUERY_SERVICE_CONFIG) LocalAlloc(LPTR, 4096); |
|---|
| 109 | if (lpqscBuf == NULL) { |
|---|
| 110 | throw SCException(_T("Could not allocate memory")); |
|---|
| 111 | } |
|---|
| 112 | SC_HANDLE schService; |
|---|
| 113 | SC_HANDLE schSCManager; |
|---|
| 114 | TCHAR szPath[512]; |
|---|
| 115 | |
|---|
| 116 | if ( GetModuleFileName( NULL, szPath, 512 ) == 0 ) |
|---|
| 117 | throw SCException(_T("Could not get module")); |
|---|
| 118 | |
|---|
| 119 | schSCManager = OpenSCManager(NULL, NULL, SC_MANAGER_ALL_ACCESS); |
|---|
| 120 | if (!schSCManager) |
|---|
| 121 | throw SCException(_T("OpenSCManager failed.")); |
|---|
| 122 | schService = OpenService(schSCManager, szName, SERVICE_ALL_ACCESS); |
|---|
| 123 | if (!schService) { |
|---|
| 124 | DWORD err = GetLastError(); |
|---|
| 125 | CloseServiceHandle(schSCManager); |
|---|
| 126 | throw SCException(_T("Unable to open service: ") + error::lookup::last_error(err)); |
|---|
| 127 | } |
|---|
| 128 | |
|---|
| 129 | |
|---|
| 130 | DWORD dwBytesNeeded = 0; |
|---|
| 131 | BOOL success = QueryServiceConfig(schService, lpqscBuf, 4096, &dwBytesNeeded); |
|---|
| 132 | CloseServiceHandle(schService); |
|---|
| 133 | CloseServiceHandle(schSCManager); |
|---|
| 134 | if (success != TRUE) |
|---|
| 135 | throw SCException(_T("Could not query service information")); |
|---|
| 136 | DWORD ret = lpqscBuf->dwServiceType; |
|---|
| 137 | LocalFree(lpqscBuf); |
|---|
| 138 | return ret; |
|---|
| 139 | } |
|---|
| 140 | |
|---|
| 141 | /** |
|---|
| 142 | * Stars the service. |
|---|
| 143 | * |
|---|
| 144 | * @param name The name of the service to start |
|---|
| 145 | * |
|---|
| 146 | * @author mickem |
|---|
| 147 | * |
|---|
| 148 | * @date 03-13-2004 |
|---|
| 149 | * |
|---|
| 150 | */ |
|---|
| 151 | void Start(std::wstring name) { |
|---|
| 152 | SC_HANDLE schService; |
|---|
| 153 | SC_HANDLE schSCManager; |
|---|
| 154 | SERVICE_STATUS ssStatus; |
|---|
| 155 | |
|---|
| 156 | schSCManager = OpenSCManager(NULL, NULL, SC_MANAGER_ALL_ACCESS ); |
|---|
| 157 | if (!schSCManager) |
|---|
| 158 | throw SCException(_T("OpenSCManager failed: ") + error::lookup::last_error()); |
|---|
| 159 | schService = OpenService(schSCManager, name.c_str(), SERVICE_ALL_ACCESS); |
|---|
| 160 | if (schService) { |
|---|
| 161 | // try to stop the service |
|---|
| 162 | if ( StartService(schService,0,NULL) ) { |
|---|
| 163 | std::wcout << _T("Starting ") << name; |
|---|
| 164 | Sleep( 1000 ); |
|---|
| 165 | while( QueryServiceStatus( schService, &ssStatus ) ) { |
|---|
| 166 | if ( ssStatus.dwCurrentState == SERVICE_START_PENDING ) { |
|---|
| 167 | std::wcout << _T("."); |
|---|
| 168 | Sleep( 1000 ); |
|---|
| 169 | } else |
|---|
| 170 | break; |
|---|
| 171 | } |
|---|
| 172 | if ( ssStatus.dwCurrentState != SERVICE_RUNNING ) { |
|---|
| 173 | CloseServiceHandle(schService); |
|---|
| 174 | CloseServiceHandle(schSCManager); |
|---|
| 175 | throw SCException(_T("Service '") +name + _T("' failed to start.")); |
|---|
| 176 | } |
|---|
| 177 | } |
|---|
| 178 | CloseServiceHandle(schService); |
|---|
| 179 | } else { |
|---|
| 180 | std::wstring err = _T("OpenService on '") + name + _T("' failed: ") + error::lookup::last_error(); |
|---|
| 181 | CloseServiceHandle(schSCManager); |
|---|
| 182 | throw SCException(err); |
|---|
| 183 | } |
|---|
| 184 | CloseServiceHandle(schSCManager); |
|---|
| 185 | } |
|---|
| 186 | bool isInstalled(std::wstring name) { |
|---|
| 187 | SC_HANDLE schService; |
|---|
| 188 | SC_HANDLE schSCManager; |
|---|
| 189 | schSCManager = OpenSCManager(NULL, NULL, SC_MANAGER_ALL_ACCESS); |
|---|
| 190 | if (!schSCManager) |
|---|
| 191 | throw SCException(_T("OpenSCManager failed: ") + error::lookup::last_error()); |
|---|
| 192 | schService = OpenService(schSCManager, name.c_str(), SERVICE_ALL_ACCESS); |
|---|
| 193 | if (schService) { |
|---|
| 194 | CloseServiceHandle(schService); |
|---|
| 195 | CloseServiceHandle(schSCManager); |
|---|
| 196 | return true; |
|---|
| 197 | } else { |
|---|
| 198 | CloseServiceHandle(schSCManager); |
|---|
| 199 | return false; |
|---|
| 200 | } |
|---|
| 201 | } |
|---|
| 202 | std::wstring get_exe_path(std::wstring svc_name) { |
|---|
| 203 | std::wstring ret; |
|---|
| 204 | SC_HANDLE schService; |
|---|
| 205 | SC_HANDLE schSCManager; |
|---|
| 206 | schSCManager = OpenSCManager(NULL, NULL, SC_MANAGER_ALL_ACCESS); |
|---|
| 207 | if (!schSCManager) |
|---|
| 208 | throw SCException(_T("OpenSCManager failed: ") + error::lookup::last_error()); |
|---|
| 209 | schService = OpenService(schSCManager, svc_name.c_str(), SERVICE_QUERY_CONFIG); |
|---|
| 210 | if (!schService) { |
|---|
| 211 | CloseServiceHandle(schSCManager); |
|---|
| 212 | throw SCException(_T("Failed to open service: ") + svc_name + _T(" because ") + error::lookup::last_error()); |
|---|
| 213 | } |
|---|
| 214 | DWORD dwBytesNeeded=0; |
|---|
| 215 | DWORD lErr; |
|---|
| 216 | if (QueryServiceConfig(schService,NULL,0, &dwBytesNeeded) || ((lErr = GetLastError()) != ERROR_INSUFFICIENT_BUFFER)) { |
|---|
| 217 | CloseServiceHandle(schService); |
|---|
| 218 | CloseServiceHandle(schSCManager); |
|---|
| 219 | throw SCException(_T("Failed to query service information: ") + svc_name + _T(" because ") + error::lookup::last_error(lErr)); |
|---|
| 220 | } |
|---|
| 221 | |
|---|
| 222 | LPQUERY_SERVICE_CONFIG lpqscBuf = (LPQUERY_SERVICE_CONFIG)LocalAlloc(LPTR, dwBytesNeeded+10); |
|---|
| 223 | BOOL bRet = (lpqscBuf!=NULL) && (QueryServiceConfig(schService,lpqscBuf,dwBytesNeeded, &dwBytesNeeded)==TRUE); |
|---|
| 224 | if (!bRet) |
|---|
| 225 | lErr = GetLastError(); |
|---|
| 226 | else { |
|---|
| 227 | ret = lpqscBuf->lpBinaryPathName; |
|---|
| 228 | } |
|---|
| 229 | LocalFree(lpqscBuf); |
|---|
| 230 | CloseServiceHandle(schService); |
|---|
| 231 | CloseServiceHandle(schSCManager); |
|---|
| 232 | if (!bRet) |
|---|
| 233 | throw SCException(_T("Failed to query service information: ") + svc_name + _T(" because ") + error::lookup::last_error(lErr)); |
|---|
| 234 | return ret; |
|---|
| 235 | } |
|---|
| 236 | |
|---|
| 237 | bool isStarted(std::wstring name) { |
|---|
| 238 | SC_HANDLE schService; |
|---|
| 239 | SC_HANDLE schSCManager; |
|---|
| 240 | SERVICE_STATUS ssStatus; |
|---|
| 241 | bool ret = false; |
|---|
| 242 | |
|---|
| 243 | schSCManager = OpenSCManager(NULL, NULL, SC_MANAGER_ALL_ACCESS ); |
|---|
| 244 | if (!schSCManager) |
|---|
| 245 | throw SCException(_T("OpenSCManager failed: ") + error::lookup::last_error()); |
|---|
| 246 | schService = OpenService(schSCManager, name.c_str(), SERVICE_ALL_ACCESS); |
|---|
| 247 | if (schService) { |
|---|
| 248 | if ( QueryServiceStatus( schService, &ssStatus ) ) { |
|---|
| 249 | if ( ssStatus.dwCurrentState == SERVICE_RUNNING ) { |
|---|
| 250 | ret = true; |
|---|
| 251 | } else if ( ssStatus.dwCurrentState == SERVICE_START_PENDING ) { |
|---|
| 252 | ret = true; |
|---|
| 253 | } |
|---|
| 254 | } |
|---|
| 255 | CloseServiceHandle(schService); |
|---|
| 256 | } else { |
|---|
| 257 | CloseServiceHandle(schSCManager); |
|---|
| 258 | ret = false; |
|---|
| 259 | } |
|---|
| 260 | CloseServiceHandle(schSCManager); |
|---|
| 261 | return ret; |
|---|
| 262 | } |
|---|
| 263 | |
|---|
| 264 | /** |
|---|
| 265 | * Stops and removes the service |
|---|
| 266 | * |
|---|
| 267 | * @param name The name of the service to uninstall |
|---|
| 268 | * |
|---|
| 269 | * @author mickem |
|---|
| 270 | * |
|---|
| 271 | * @date 03-13-2004 |
|---|
| 272 | * |
|---|
| 273 | */ |
|---|
| 274 | void Uninstall(std::wstring name) { |
|---|
| 275 | SC_HANDLE schService; |
|---|
| 276 | SC_HANDLE schSCManager; |
|---|
| 277 | Stop(name); |
|---|
| 278 | |
|---|
| 279 | schSCManager = OpenSCManager(NULL, NULL, SC_MANAGER_ALL_ACCESS); |
|---|
| 280 | if (!schSCManager) |
|---|
| 281 | throw SCException(_T("OpenSCManager failed: ") + error::lookup::last_error()); |
|---|
| 282 | schService = OpenService(schSCManager, name.c_str(), SERVICE_ALL_ACCESS); |
|---|
| 283 | if (schService) { |
|---|
| 284 | if(!DeleteService(schService)) { |
|---|
| 285 | std::wstring err = _T("DeleteService failed: ") + error::lookup::last_error(); |
|---|
| 286 | CloseServiceHandle(schService); |
|---|
| 287 | CloseServiceHandle(schSCManager); |
|---|
| 288 | throw SCException(err); |
|---|
| 289 | } |
|---|
| 290 | CloseServiceHandle(schService); |
|---|
| 291 | } else { |
|---|
| 292 | std::wstring err = _T("OpenService failed: ") + error::lookup::last_error(); |
|---|
| 293 | CloseServiceHandle(schSCManager); |
|---|
| 294 | throw SCException(err); |
|---|
| 295 | } |
|---|
| 296 | CloseServiceHandle(schSCManager); |
|---|
| 297 | } |
|---|
| 298 | |
|---|
| 299 | /** |
|---|
| 300 | * Stops the service |
|---|
| 301 | * |
|---|
| 302 | * @param name The name of the serive to stop |
|---|
| 303 | * |
|---|
| 304 | * @author MickeM |
|---|
| 305 | * |
|---|
| 306 | * @date 03-13-2004 |
|---|
| 307 | * |
|---|
| 308 | */ |
|---|
| 309 | void Stop(std::wstring name) { |
|---|
| 310 | SC_HANDLE schService; |
|---|
| 311 | SC_HANDLE schSCManager; |
|---|
| 312 | SERVICE_STATUS ssStatus; |
|---|
| 313 | |
|---|
| 314 | schSCManager = OpenSCManager(NULL, NULL, SC_MANAGER_ALL_ACCESS); |
|---|
| 315 | if (!schSCManager) |
|---|
| 316 | throw SCException(_T("OpenSCManager failed: ") + error::lookup::last_error()); |
|---|
| 317 | schService = OpenService(schSCManager, name.c_str(), SERVICE_ALL_ACCESS); |
|---|
| 318 | if (schService) { |
|---|
| 319 | // try to stop the service |
|---|
| 320 | if ( ControlService( schService, SERVICE_CONTROL_STOP, &ssStatus ) ) { |
|---|
| 321 | std::wcout << _T("Stopping service."); |
|---|
| 322 | Sleep( 1000 ); |
|---|
| 323 | while( QueryServiceStatus( schService, &ssStatus ) ) { |
|---|
| 324 | if ( ssStatus.dwCurrentState == SERVICE_STOP_PENDING ) { |
|---|
| 325 | std::wcout << _T("."); |
|---|
| 326 | Sleep( 1000 ); |
|---|
| 327 | } else |
|---|
| 328 | break; |
|---|
| 329 | } |
|---|
| 330 | std::wcout << std::endl; |
|---|
| 331 | if ( ssStatus.dwCurrentState != SERVICE_STOPPED ) { |
|---|
| 332 | CloseServiceHandle(schService); |
|---|
| 333 | CloseServiceHandle(schSCManager); |
|---|
| 334 | throw SCException(_T("Service failed to stop.")); |
|---|
| 335 | } |
|---|
| 336 | } |
|---|
| 337 | CloseServiceHandle(schService); |
|---|
| 338 | } else { |
|---|
| 339 | std::wstring err = _T("OpenService failed: ") + error::lookup::last_error(); |
|---|
| 340 | CloseServiceHandle(schSCManager); |
|---|
| 341 | throw SCException(err); |
|---|
| 342 | } |
|---|
| 343 | CloseServiceHandle(schSCManager); |
|---|
| 344 | } |
|---|
| 345 | |
|---|
| 346 | void StopNoWait(std::wstring name) { |
|---|
| 347 | SC_HANDLE schService; |
|---|
| 348 | SC_HANDLE schSCManager; |
|---|
| 349 | SERVICE_STATUS ssStatus; |
|---|
| 350 | |
|---|
| 351 | schSCManager = OpenSCManager(NULL, NULL, SC_MANAGER_ALL_ACCESS); |
|---|
| 352 | if (!schSCManager) |
|---|
| 353 | throw SCException(_T("OpenSCManager failed.")); |
|---|
| 354 | schService = OpenService(schSCManager, name.c_str(), SERVICE_ALL_ACCESS); |
|---|
| 355 | if (schService) { |
|---|
| 356 | // try to stop the service |
|---|
| 357 | ControlService( schService, SERVICE_CONTROL_STOP, &ssStatus ); |
|---|
| 358 | CloseServiceHandle(schService); |
|---|
| 359 | } else { |
|---|
| 360 | CloseServiceHandle(schSCManager); |
|---|
| 361 | throw SCException(_T("OpenService failed.")); |
|---|
| 362 | } |
|---|
| 363 | CloseServiceHandle(schSCManager); |
|---|
| 364 | } |
|---|
| 365 | |
|---|
| 366 | |
|---|
| 367 | typedef BOOL (WINAPI*PFChangeServiceConfig2)(SC_HANDLE hService,DWORD dwInfoLevel,LPVOID lpInfo); |
|---|
| 368 | |
|---|
| 369 | void SetDescription(std::wstring name, std::wstring desc) { |
|---|
| 370 | PFChangeServiceConfig2 FChangeServiceConfig2; |
|---|
| 371 | HMODULE ADVAPI= ::LoadLibrary(_T("Advapi32")); |
|---|
| 372 | if (!ADVAPI) { |
|---|
| 373 | throw SCException(_T("Couldn't set extended service info (ignore this on NT4).")); |
|---|
| 374 | } |
|---|
| 375 | #ifdef UNICODE |
|---|
| 376 | FChangeServiceConfig2 = (PFChangeServiceConfig2)::GetProcAddress(ADVAPI, "ChangeServiceConfig2W"); |
|---|
| 377 | #else |
|---|
| 378 | FChangeServiceConfig2 = (PFChangeServiceConfig2)::GetProcAddress(ADVAPI, _TEXT("ChangeServiceConfig2A")); |
|---|
| 379 | #endif |
|---|
| 380 | if (!FChangeServiceConfig2) { |
|---|
| 381 | FreeLibrary(ADVAPI); |
|---|
| 382 | throw SCException(_T("Couldn't set extended service info (ignore this on NT4).")); |
|---|
| 383 | } |
|---|
| 384 | SERVICE_DESCRIPTION descr; |
|---|
| 385 | SC_HANDLE schSCManager = OpenSCManager(NULL, NULL, SC_MANAGER_ALL_ACCESS); |
|---|
| 386 | if (!schSCManager) |
|---|
| 387 | throw SCException(_T("OpenSCManager failed.")); |
|---|
| 388 | SC_HANDLE schService = OpenService(schSCManager, name.c_str(), SERVICE_ALL_ACCESS); |
|---|
| 389 | if (!schService) { |
|---|
| 390 | FreeLibrary(ADVAPI); |
|---|
| 391 | CloseServiceHandle(schSCManager); |
|---|
| 392 | throw SCException(_T("OpenService failed.")); |
|---|
| 393 | } |
|---|
| 394 | |
|---|
| 395 | TCHAR* d = new TCHAR[desc.length()+2]; |
|---|
| 396 | wcsncpy(d, desc.c_str(), desc.length()+1); |
|---|
| 397 | descr.lpDescription = d; |
|---|
| 398 | BOOL bResult = FChangeServiceConfig2(schService, SERVICE_CONFIG_DESCRIPTION, &descr); |
|---|
| 399 | delete [] d; |
|---|
| 400 | FreeLibrary(ADVAPI); |
|---|
| 401 | CloseServiceHandle(schService); |
|---|
| 402 | CloseServiceHandle(schSCManager); |
|---|
| 403 | if (!bResult) |
|---|
| 404 | throw SCException(_T("ChangeServiceConfig2 failed.")); |
|---|
| 405 | } |
|---|
| 406 | } |
|---|