1 /** 2 * Copyright © 2016 IBM Corporation 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 #include "config.h" 17 18 #include "mainloop.hpp" 19 20 #include "env.hpp" 21 #include "fan_pwm.hpp" 22 #include "fan_speed.hpp" 23 #include "hwmon.hpp" 24 #include "hwmonio.hpp" 25 #include "sensor.hpp" 26 #include "sensorset.hpp" 27 #include "sysfs.hpp" 28 #include "targets.hpp" 29 #include "thresholds.hpp" 30 #include "util.hpp" 31 32 #include <fmt/format.h> 33 34 #include <cassert> 35 #include <cstdlib> 36 #include <functional> 37 #include <future> 38 #include <iostream> 39 #include <memory> 40 #include <phosphor-logging/elog-errors.hpp> 41 #include <sstream> 42 #include <string> 43 #include <unordered_set> 44 #include <xyz/openbmc_project/Sensor/Device/error.hpp> 45 46 using namespace phosphor::logging; 47 48 // Initialization for Warning Objects 49 decltype(Thresholds<WarningObject>::setLo) Thresholds<WarningObject>::setLo = 50 &WarningObject::warningLow; 51 decltype(Thresholds<WarningObject>::setHi) Thresholds<WarningObject>::setHi = 52 &WarningObject::warningHigh; 53 decltype(Thresholds<WarningObject>::getLo) Thresholds<WarningObject>::getLo = 54 &WarningObject::warningLow; 55 decltype(Thresholds<WarningObject>::getHi) Thresholds<WarningObject>::getHi = 56 &WarningObject::warningHigh; 57 decltype(Thresholds<WarningObject>::alarmLo) 58 Thresholds<WarningObject>::alarmLo = &WarningObject::warningAlarmLow; 59 decltype(Thresholds<WarningObject>::alarmHi) 60 Thresholds<WarningObject>::alarmHi = &WarningObject::warningAlarmHigh; 61 decltype(Thresholds<WarningObject>::getAlarmLow) 62 Thresholds<WarningObject>::getAlarmLow = &WarningObject::warningAlarmLow; 63 decltype(Thresholds<WarningObject>::getAlarmHigh) 64 Thresholds<WarningObject>::getAlarmHigh = &WarningObject::warningAlarmHigh; 65 decltype(Thresholds<WarningObject>::assertLowSignal) 66 Thresholds<WarningObject>::assertLowSignal = 67 &WarningObject::warningLowAlarmAsserted; 68 decltype(Thresholds<WarningObject>::assertHighSignal) 69 Thresholds<WarningObject>::assertHighSignal = 70 &WarningObject::warningHighAlarmAsserted; 71 decltype(Thresholds<WarningObject>::deassertLowSignal) 72 Thresholds<WarningObject>::deassertLowSignal = 73 &WarningObject::warningLowAlarmDeasserted; 74 decltype(Thresholds<WarningObject>::deassertHighSignal) 75 Thresholds<WarningObject>::deassertHighSignal = 76 &WarningObject::warningHighAlarmDeasserted; 77 78 // Initialization for Critical Objects 79 decltype(Thresholds<CriticalObject>::setLo) Thresholds<CriticalObject>::setLo = 80 &CriticalObject::criticalLow; 81 decltype(Thresholds<CriticalObject>::setHi) Thresholds<CriticalObject>::setHi = 82 &CriticalObject::criticalHigh; 83 decltype(Thresholds<CriticalObject>::getLo) Thresholds<CriticalObject>::getLo = 84 &CriticalObject::criticalLow; 85 decltype(Thresholds<CriticalObject>::getHi) Thresholds<CriticalObject>::getHi = 86 &CriticalObject::criticalHigh; 87 decltype(Thresholds<CriticalObject>::alarmLo) 88 Thresholds<CriticalObject>::alarmLo = &CriticalObject::criticalAlarmLow; 89 decltype(Thresholds<CriticalObject>::alarmHi) 90 Thresholds<CriticalObject>::alarmHi = &CriticalObject::criticalAlarmHigh; 91 decltype(Thresholds<CriticalObject>::getAlarmLow) 92 Thresholds<CriticalObject>::getAlarmLow = &CriticalObject::criticalAlarmLow; 93 decltype(Thresholds<CriticalObject>::getAlarmHigh) 94 Thresholds<CriticalObject>::getAlarmHigh = 95 &CriticalObject::criticalAlarmHigh; 96 decltype(Thresholds<CriticalObject>::assertLowSignal) 97 Thresholds<CriticalObject>::assertLowSignal = 98 &CriticalObject::criticalLowAlarmAsserted; 99 decltype(Thresholds<CriticalObject>::assertHighSignal) 100 Thresholds<CriticalObject>::assertHighSignal = 101 &CriticalObject::criticalHighAlarmAsserted; 102 decltype(Thresholds<CriticalObject>::deassertLowSignal) 103 Thresholds<CriticalObject>::deassertLowSignal = 104 &CriticalObject::criticalLowAlarmDeasserted; 105 decltype(Thresholds<CriticalObject>::deassertHighSignal) 106 Thresholds<CriticalObject>::deassertHighSignal = 107 &CriticalObject::criticalHighAlarmDeasserted; 108 109 void updateSensorInterfaces(InterfaceMap& ifaces, SensorValueType value) 110 { 111 for (auto& iface : ifaces) 112 { 113 switch (iface.first) 114 { 115 // clang-format off 116 case InterfaceType::VALUE: 117 { 118 auto& valueIface = 119 std::any_cast<std::shared_ptr<ValueObject>&>(iface.second); 120 valueIface->value(value); 121 } 122 break; 123 // clang-format on 124 case InterfaceType::WARN: 125 checkThresholds<WarningObject>(iface.second, value); 126 break; 127 case InterfaceType::CRIT: 128 checkThresholds<CriticalObject>(iface.second, value); 129 break; 130 default: 131 break; 132 } 133 } 134 } 135 136 std::string MainLoop::getID(SensorSet::container_t::const_reference sensor) 137 { 138 std::string id; 139 140 /* 141 * Check if the value of the MODE_<item><X> env variable for the sensor 142 * is set. If it is, then read the from the <item><X>_<mode> 143 * file. The name of the DBUS object would be the value of the env 144 * variable LABEL_<item><mode value>. If the MODE_<item><X> env variable 145 * doesn't exist, then the name of DBUS object is the value of the env 146 * variable LABEL_<item><X>. 147 * 148 * For example, if MODE_temp1 = "label", then code reads the temp1_label 149 * file. If it has a 5 in it, then it will use the following entry to 150 * name the object: LABEL_temp5 = "My DBus object name". 151 * 152 */ 153 auto mode = env::getEnv("MODE", sensor.first); 154 if (!mode.empty()) 155 { 156 id = env::getIndirectID(_hwmonRoot + '/' + _instance + '/', mode, 157 sensor.first); 158 159 if (id.empty()) 160 { 161 return id; 162 } 163 } 164 165 // Use the ID we looked up above if there was one, 166 // otherwise use the standard one. 167 id = (id.empty()) ? sensor.first.second : id; 168 169 return id; 170 } 171 172 SensorIdentifiers 173 MainLoop::getIdentifiers(SensorSet::container_t::const_reference sensor) 174 { 175 std::string id = getID(sensor); 176 std::string label; 177 178 if (!id.empty()) 179 { 180 // Ignore inputs without a label. 181 label = env::getEnv("LABEL", sensor.first.first, id); 182 } 183 184 return std::make_tuple(std::move(id), std::move(label)); 185 } 186 187 /** 188 * Reads the environment parameters of a sensor and creates an object with 189 * atleast the `Value` interface, otherwise returns without creating the object. 190 * If the `Value` interface is successfully created, by reading the sensor's 191 * corresponding sysfs file's value, the additional interfaces for the sensor 192 * are created and the InterfacesAdded signal is emitted. The object's state 193 * data is then returned for sensor state monitoring within the main loop. 194 */ 195 std::optional<ObjectStateData> 196 MainLoop::getObject(SensorSet::container_t::const_reference sensor) 197 { 198 auto properties = getIdentifiers(sensor); 199 if (std::get<sensorID>(properties).empty() || 200 std::get<sensorLabel>(properties).empty()) 201 { 202 return {}; 203 } 204 205 hwmon::Attributes attrs; 206 if (!hwmon::getAttributes(sensor.first.first, attrs)) 207 { 208 return {}; 209 } 210 211 const auto& [sensorSetKey, sensorAttrs] = sensor; 212 const auto& [sensorSysfsType, sensorSysfsNum] = sensorSetKey; 213 214 /* Note: The sensor objects all share the same ioAccess object. */ 215 auto sensorObj = 216 std::make_unique<sensor::Sensor>(sensorSetKey, _ioAccess, _devPath); 217 218 // Get list of return codes for removing sensors on device 219 auto devRmRCs = env::getEnv("REMOVERCS"); 220 // Add sensor removal return codes defined at the device level 221 sensorObj->addRemoveRCs(devRmRCs); 222 223 std::string objectPath{_root}; 224 objectPath.append(1, '/'); 225 objectPath.append(hwmon::getNamespace(attrs)); 226 objectPath.append(1, '/'); 227 objectPath.append(std::get<sensorLabel>(properties)); 228 229 ObjectInfo info(&_bus, std::move(objectPath), InterfaceMap()); 230 RetryIO retryIO(hwmonio::retries, hwmonio::delay); 231 if (_rmSensors.find(sensorSetKey) != _rmSensors.end()) 232 { 233 // When adding a sensor that was purposely removed, 234 // don't retry on errors when reading its value 235 std::get<size_t>(retryIO) = 0; 236 } 237 auto valueInterface = static_cast<std::shared_ptr<ValueObject>>(nullptr); 238 try 239 { 240 // Add status interface based on _fault file being present 241 sensorObj->addStatus(info); 242 valueInterface = sensorObj->addValue(retryIO, info, _timedoutMap); 243 } 244 catch (const std::system_error& e) 245 { 246 auto file = 247 sysfs::make_sysfs_path(_ioAccess->path(), sensorSysfsType, 248 sensorSysfsNum, hwmon::entry::cinput); 249 250 // Check sensorAdjusts for sensor removal RCs 251 auto& sAdjusts = sensorObj->getAdjusts(); 252 if (sAdjusts.rmRCs.count(e.code().value()) > 0) 253 { 254 // Return code found in sensor return code removal list 255 if (_rmSensors.find(sensorSetKey) == _rmSensors.end()) 256 { 257 // Trace for sensor not already removed from dbus 258 log<level::INFO>("Sensor not added to dbus for read fail", 259 entry("FILE=%s", file.c_str()), 260 entry("RC=%d", e.code().value())); 261 _rmSensors[std::move(sensorSetKey)] = std::move(sensorAttrs); 262 } 263 return {}; 264 } 265 266 using namespace sdbusplus::xyz::openbmc_project::Sensor::Device::Error; 267 report<ReadFailure>( 268 xyz::openbmc_project::Sensor::Device::ReadFailure::CALLOUT_ERRNO( 269 e.code().value()), 270 xyz::openbmc_project::Sensor::Device::ReadFailure:: 271 CALLOUT_DEVICE_PATH(_devPath.c_str())); 272 273 log<level::INFO>(fmt::format("Failing sysfs file: {} errno: {}", file, 274 e.code().value()) 275 .c_str()); 276 exit(EXIT_FAILURE); 277 } 278 auto sensorValue = valueInterface->value(); 279 int64_t scale = sensorObj->getScale(); 280 281 addThreshold<WarningObject>(sensorSysfsType, std::get<sensorID>(properties), 282 sensorValue, info, scale); 283 addThreshold<CriticalObject>(sensorSysfsType, 284 std::get<sensorID>(properties), sensorValue, 285 info, scale); 286 287 auto target = 288 addTarget<hwmon::FanSpeed>(sensorSetKey, _ioAccess, _devPath, info); 289 if (target) 290 { 291 target->enable(); 292 } 293 addTarget<hwmon::FanPwm>(sensorSetKey, _ioAccess, _devPath, info); 294 295 // All the interfaces have been created. Go ahead 296 // and emit InterfacesAdded. 297 valueInterface->emit_object_added(); 298 299 // Save sensor object specifications 300 _sensorObjects[sensorSetKey] = std::move(sensorObj); 301 302 return std::make_pair(std::move(std::get<sensorLabel>(properties)), 303 std::move(info)); 304 } 305 306 MainLoop::MainLoop(sdbusplus::bus::bus&& bus, const std::string& param, 307 const std::string& path, const std::string& devPath, 308 const char* prefix, const char* root, 309 const std::string& instanceId, 310 const hwmonio::HwmonIOInterface* ioIntf) : 311 _bus(std::move(bus)), 312 _manager(_bus, root), _pathParam(param), _hwmonRoot(), _instance(), 313 _devPath(devPath), _prefix(prefix), _root(root), _state(), 314 _instanceId(instanceId), _ioAccess(ioIntf), 315 _event(sdeventplus::Event::get_default()), 316 _timer(_event, std::bind(&MainLoop::read, this)) 317 { 318 // Strip off any trailing slashes. 319 std::string p = path; 320 while (!p.empty() && p.back() == '/') 321 { 322 p.pop_back(); 323 } 324 325 // Given the furthest right /, set instance to 326 // the basename, and hwmonRoot to the leading path. 327 auto n = p.rfind('/'); 328 if (n != std::string::npos) 329 { 330 _instance.assign(p.substr(n + 1)); 331 _hwmonRoot.assign(p.substr(0, n)); 332 } 333 334 assert(!_instance.empty()); 335 assert(!_hwmonRoot.empty()); 336 } 337 338 void MainLoop::shutdown() noexcept 339 { 340 _event.exit(0); 341 } 342 343 void MainLoop::run() 344 { 345 init(); 346 347 std::function<void()> callback(std::bind(&MainLoop::read, this)); 348 try 349 { 350 _timer.restart(std::chrono::microseconds(_interval)); 351 352 // TODO: Issue#6 - Optionally look at polling interval sysfs entry. 353 354 // TODO: Issue#7 - Should probably periodically check the SensorSet 355 // for new entries. 356 357 _bus.attach_event(_event.get(), SD_EVENT_PRIORITY_IMPORTANT); 358 _event.loop(); 359 } 360 catch (const std::exception& e) 361 { 362 log<level::ERR>("Error in sysfs polling loop", 363 entry("ERROR=%s", e.what())); 364 throw; 365 } 366 } 367 368 void MainLoop::init() 369 { 370 // Check sysfs for available sensors. 371 auto sensors = std::make_unique<SensorSet>(_hwmonRoot + '/' + _instance); 372 373 for (const auto& i : *sensors) 374 { 375 auto object = getObject(i); 376 if (object) 377 { 378 // Construct the SensorSet value 379 // std::tuple<SensorSet::mapped_type, 380 // std::string(Sensor Label), 381 // ObjectInfo> 382 auto value = 383 std::make_tuple(std::move(i.second), std::move((*object).first), 384 std::move((*object).second)); 385 386 _state[std::move(i.first)] = std::move(value); 387 } 388 389 // Initialize _averageMap of sensor. e.g. <<power, 1>, <0, 0>> 390 if ((i.first.first == hwmon::type::power) && 391 (phosphor::utility::isAverageEnvSet(i.first))) 392 { 393 _average.setAverageValue(i.first, std::make_pair(0, 0)); 394 } 395 } 396 397 /* If there are no sensors specified by labels, exit. */ 398 if (0 == _state.size()) 399 { 400 exit(0); 401 } 402 403 { 404 std::stringstream ss; 405 std::string id = _instanceId; 406 if (id.empty()) 407 { 408 id = 409 std::to_string(std::hash<std::string>{}(_devPath + _pathParam)); 410 } 411 ss << _prefix << "-" << id << ".Hwmon1"; 412 413 _bus.request_name(ss.str().c_str()); 414 } 415 416 { 417 auto interval = env::getEnv("INTERVAL"); 418 if (!interval.empty()) 419 { 420 _interval = std::strtoull(interval.c_str(), NULL, 10); 421 } 422 } 423 } 424 425 void MainLoop::read() 426 { 427 // TODO: Issue#3 - Need to make calls to the dbus sensor cache here to 428 // ensure the objects all exist? 429 430 // Iterate through all the sensors. 431 for (auto& [sensorSetKey, sensorStateTuple] : _state) 432 { 433 const auto& [sensorSysfsType, sensorSysfsNum] = sensorSetKey; 434 auto& [attrs, unused, objInfo] = sensorStateTuple; 435 436 if (attrs.find(hwmon::entry::input) == attrs.end()) 437 { 438 continue; 439 } 440 441 // Read value from sensor. 442 std::string input = hwmon::entry::input; 443 if (sensorSysfsType == hwmon::type::pwm) 444 { 445 input = ""; 446 } 447 // If type is power and AVERAGE_power* is true in env, use average 448 // instead of input 449 else if ((sensorSysfsType == hwmon::type::power) && 450 (phosphor::utility::isAverageEnvSet(sensorSetKey))) 451 { 452 input = hwmon::entry::average; 453 } 454 455 SensorValueType value; 456 auto& obj = std::get<InterfaceMap>(objInfo); 457 std::unique_ptr<sensor::Sensor>& sensor = _sensorObjects[sensorSetKey]; 458 459 auto& statusIface = std::any_cast<std::shared_ptr<StatusObject>&>( 460 obj[InterfaceType::STATUS]); 461 // As long as addStatus is called before addValue, statusIface 462 // should never be nullptr. 463 assert(statusIface); 464 465 try 466 { 467 if (sensor->hasFaultFile()) 468 { 469 auto fault = _ioAccess->read(sensorSysfsType, sensorSysfsNum, 470 hwmon::entry::fault, 471 hwmonio::retries, hwmonio::delay); 472 // Skip reading from a sensor with a valid fault file 473 // and set the functional property accordingly 474 if (!statusIface->functional((fault == 0) ? true : false)) 475 { 476 continue; 477 } 478 } 479 480 { 481 // RAII object for GPIO unlock / lock 482 auto locker = sensor::gpioUnlock(sensor->getGpio()); 483 484 // For sensors with attribute ASYNC_READ_TIMEOUT, 485 // spawn a thread with timeout 486 auto asyncReadTimeout = 487 env::getEnv("ASYNC_READ_TIMEOUT", sensorSetKey); 488 if (!asyncReadTimeout.empty()) 489 { 490 std::chrono::milliseconds asyncTimeout{ 491 std::stoi(asyncReadTimeout)}; 492 value = sensor::asyncRead( 493 sensorSetKey, _ioAccess, asyncTimeout, _timedoutMap, 494 sensorSysfsType, sensorSysfsNum, input, 495 hwmonio::retries, hwmonio::delay); 496 } 497 else 498 { 499 // Retry for up to a second if device is busy 500 // or has a transient error. 501 value = 502 _ioAccess->read(sensorSysfsType, sensorSysfsNum, input, 503 hwmonio::retries, hwmonio::delay); 504 } 505 506 // Set functional property to true if we could read sensor 507 statusIface->functional(true); 508 509 value = sensor->adjustValue(value); 510 511 if (input == hwmon::entry::average) 512 { 513 // Calculate the values of averageMap based on current 514 // average value, current average_interval value, previous 515 // average value, previous average_interval value 516 int64_t interval = 517 _ioAccess->read(sensorSysfsType, sensorSysfsNum, 518 hwmon::entry::caverage_interval, 519 hwmonio::retries, hwmonio::delay); 520 auto ret = _average.getAverageValue(sensorSetKey); 521 assert(ret); 522 523 const auto& [preAverage, preInterval] = *ret; 524 525 auto calValue = Average::calcAverage( 526 preAverage, preInterval, value, interval); 527 if (calValue) 528 { 529 // Update previous values in averageMap before the 530 // variable value is changed next 531 _average.setAverageValue( 532 sensorSetKey, std::make_pair(value, interval)); 533 // Update value to be calculated average 534 value = calValue.value(); 535 } 536 else 537 { 538 // the value of 539 // power*_average_interval is not changed yet, use the 540 // previous calculated average instead. So skip dbus 541 // update. 542 continue; 543 } 544 } 545 } 546 547 updateSensorInterfaces(obj, value); 548 } 549 catch (const std::system_error& e) 550 { 551 #if UPDATE_FUNCTIONAL_ON_FAIL 552 // If UPDATE_FUNCTIONAL_ON_FAIL is defined and an exception was 553 // thrown, set the functional property to false. 554 // We cannot set this with the 'continue' in the lower block 555 // as the code may exit before reaching it. 556 statusIface->functional(false); 557 #endif 558 auto file = sysfs::make_sysfs_path( 559 _ioAccess->path(), sensorSysfsType, sensorSysfsNum, input); 560 561 // Check sensorAdjusts for sensor removal RCs 562 auto& sAdjusts = _sensorObjects[sensorSetKey]->getAdjusts(); 563 if (sAdjusts.rmRCs.count(e.code().value()) > 0) 564 { 565 // Return code found in sensor return code removal list 566 if (_rmSensors.find(sensorSetKey) == _rmSensors.end()) 567 { 568 // Trace for sensor not already removed from dbus 569 log<level::INFO>("Remove sensor from dbus for read fail", 570 entry("FILE=%s", file.c_str()), 571 entry("RC=%d", e.code().value())); 572 // Mark this sensor to be removed from dbus 573 _rmSensors[sensorSetKey] = attrs; 574 } 575 continue; 576 } 577 #if UPDATE_FUNCTIONAL_ON_FAIL 578 // Do not exit with failure if UPDATE_FUNCTIONAL_ON_FAIL is set 579 continue; 580 #endif 581 using namespace sdbusplus::xyz::openbmc_project::Sensor::Device:: 582 Error; 583 report<ReadFailure>( 584 xyz::openbmc_project::Sensor::Device::ReadFailure:: 585 CALLOUT_ERRNO(e.code().value()), 586 xyz::openbmc_project::Sensor::Device::ReadFailure:: 587 CALLOUT_DEVICE_PATH(_devPath.c_str())); 588 589 log<level::INFO>(fmt::format("Failing sysfs file: {} errno: {}", 590 file, e.code().value()) 591 .c_str()); 592 593 exit(EXIT_FAILURE); 594 } 595 } 596 597 removeSensors(); 598 599 addDroppedSensors(); 600 } 601 602 void MainLoop::removeSensors() 603 { 604 // Remove any sensors marked for removal 605 for (const auto& i : _rmSensors) 606 { 607 // Remove sensor object from dbus using emit_object_removed() 608 auto& objInfo = std::get<ObjectInfo>(_state[i.first]); 609 auto& objPath = std::get<std::string>(objInfo); 610 611 _bus.emit_object_removed(objPath.c_str()); 612 613 // Erase sensor object info 614 _state.erase(i.first); 615 } 616 } 617 618 void MainLoop::addDroppedSensors() 619 { 620 // Attempt to add any sensors that were removed 621 auto it = _rmSensors.begin(); 622 while (it != _rmSensors.end()) 623 { 624 if (_state.find(it->first) == _state.end()) 625 { 626 SensorSet::container_t::value_type ssValueType = 627 std::make_pair(it->first, it->second); 628 629 auto object = getObject(ssValueType); 630 if (object) 631 { 632 // Construct the SensorSet value 633 // std::tuple<SensorSet::mapped_type, 634 // std::string(Sensor Label), 635 // ObjectInfo> 636 auto value = std::make_tuple(std::move(ssValueType.second), 637 std::move((*object).first), 638 std::move((*object).second)); 639 640 _state[std::move(ssValueType.first)] = std::move(value); 641 642 std::string input = hwmon::entry::input; 643 // If type is power and AVERAGE_power* is true in env, use 644 // average instead of input 645 if ((it->first.first == hwmon::type::power) && 646 (phosphor::utility::isAverageEnvSet(it->first))) 647 { 648 input = hwmon::entry::average; 649 } 650 // Sensor object added, erase entry from removal list 651 auto file = 652 sysfs::make_sysfs_path(_ioAccess->path(), it->first.first, 653 it->first.second, input); 654 655 log<level::INFO>("Added sensor to dbus after successful read", 656 entry("FILE=%s", file.c_str())); 657 658 it = _rmSensors.erase(it); 659 } 660 else 661 { 662 ++it; 663 } 664 } 665 else 666 { 667 // Sanity check to remove sensors that were re-added 668 it = _rmSensors.erase(it); 669 } 670 } 671 } 672 673 // vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4 674