1dda28197Spatrick //===-- Watchpoint.cpp ----------------------------------------------------===//
2061da546Spatrick //
3061da546Spatrick // Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
4061da546Spatrick // See https://llvm.org/LICENSE.txt for license information.
5061da546Spatrick // SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
6061da546Spatrick //
7061da546Spatrick //===----------------------------------------------------------------------===//
8061da546Spatrick
9061da546Spatrick #include "lldb/Breakpoint/Watchpoint.h"
10061da546Spatrick
11061da546Spatrick #include "lldb/Breakpoint/StoppointCallbackContext.h"
12061da546Spatrick #include "lldb/Core/Value.h"
13061da546Spatrick #include "lldb/Core/ValueObject.h"
14061da546Spatrick #include "lldb/Core/ValueObjectMemory.h"
15061da546Spatrick #include "lldb/Expression/UserExpression.h"
16061da546Spatrick #include "lldb/Symbol/TypeSystem.h"
17061da546Spatrick #include "lldb/Target/Process.h"
18061da546Spatrick #include "lldb/Target/Target.h"
19061da546Spatrick #include "lldb/Target/ThreadSpec.h"
20*f6aab3d8Srobert #include "lldb/Utility/LLDBLog.h"
21061da546Spatrick #include "lldb/Utility/Log.h"
22061da546Spatrick #include "lldb/Utility/Stream.h"
23061da546Spatrick
24061da546Spatrick using namespace lldb;
25061da546Spatrick using namespace lldb_private;
26061da546Spatrick
Watchpoint(Target & target,lldb::addr_t addr,uint32_t size,const CompilerType * type,bool hardware)27061da546Spatrick Watchpoint::Watchpoint(Target &target, lldb::addr_t addr, uint32_t size,
28061da546Spatrick const CompilerType *type, bool hardware)
29be691f3bSpatrick : StoppointSite(0, addr, size, hardware), m_target(target),
30061da546Spatrick m_enabled(false), m_is_hardware(hardware), m_is_watch_variable(false),
31061da546Spatrick m_is_ephemeral(false), m_disabled_count(0), m_watch_read(0),
32061da546Spatrick m_watch_write(0), m_watch_was_read(0), m_watch_was_written(0),
33*f6aab3d8Srobert m_ignore_count(0), m_false_alarms(0), m_being_created(true) {
34061da546Spatrick
35061da546Spatrick if (type && type->IsValid())
36061da546Spatrick m_type = *type;
37061da546Spatrick else {
38061da546Spatrick // If we don't have a known type, then we force it to unsigned int of the
39061da546Spatrick // right size.
40061da546Spatrick auto type_system_or_err =
41061da546Spatrick target.GetScratchTypeSystemForLanguage(eLanguageTypeC);
42061da546Spatrick if (auto err = type_system_or_err.takeError()) {
43*f6aab3d8Srobert LLDB_LOG_ERROR(GetLog(LLDBLog::Watchpoints), std::move(err),
44*f6aab3d8Srobert "Failed to set type.");
45061da546Spatrick } else {
46*f6aab3d8Srobert if (auto ts = *type_system_or_err)
47*f6aab3d8Srobert m_type =
48*f6aab3d8Srobert ts->GetBuiltinTypeForEncodingAndBitSize(eEncodingUint, 8 * size);
49*f6aab3d8Srobert else
50*f6aab3d8Srobert LLDB_LOG_ERROR(GetLog(LLDBLog::Watchpoints), std::move(err),
51*f6aab3d8Srobert "Failed to set type. Typesystem is no longer live.");
52061da546Spatrick }
53061da546Spatrick }
54061da546Spatrick
55061da546Spatrick // Set the initial value of the watched variable:
56061da546Spatrick if (m_target.GetProcessSP()) {
57061da546Spatrick ExecutionContext exe_ctx;
58061da546Spatrick m_target.GetProcessSP()->CalculateExecutionContext(exe_ctx);
59061da546Spatrick CaptureWatchedValue(exe_ctx);
60061da546Spatrick }
61061da546Spatrick m_being_created = false;
62061da546Spatrick }
63061da546Spatrick
64061da546Spatrick Watchpoint::~Watchpoint() = default;
65061da546Spatrick
66061da546Spatrick // This function is used when "baton" doesn't need to be freed
SetCallback(WatchpointHitCallback callback,void * baton,bool is_synchronous)67061da546Spatrick void Watchpoint::SetCallback(WatchpointHitCallback callback, void *baton,
68061da546Spatrick bool is_synchronous) {
69061da546Spatrick // The default "Baton" class will keep a copy of "baton" and won't free or
70061da546Spatrick // delete it when it goes goes out of scope.
71061da546Spatrick m_options.SetCallback(callback, std::make_shared<UntypedBaton>(baton),
72061da546Spatrick is_synchronous);
73061da546Spatrick
74061da546Spatrick SendWatchpointChangedEvent(eWatchpointEventTypeCommandChanged);
75061da546Spatrick }
76061da546Spatrick
77061da546Spatrick // This function is used when a baton needs to be freed and therefore is
78061da546Spatrick // contained in a "Baton" subclass.
SetCallback(WatchpointHitCallback callback,const BatonSP & callback_baton_sp,bool is_synchronous)79061da546Spatrick void Watchpoint::SetCallback(WatchpointHitCallback callback,
80061da546Spatrick const BatonSP &callback_baton_sp,
81061da546Spatrick bool is_synchronous) {
82061da546Spatrick m_options.SetCallback(callback, callback_baton_sp, is_synchronous);
83061da546Spatrick SendWatchpointChangedEvent(eWatchpointEventTypeCommandChanged);
84061da546Spatrick }
85061da546Spatrick
ClearCallback()86061da546Spatrick void Watchpoint::ClearCallback() {
87061da546Spatrick m_options.ClearCallback();
88061da546Spatrick SendWatchpointChangedEvent(eWatchpointEventTypeCommandChanged);
89061da546Spatrick }
90061da546Spatrick
SetDeclInfo(const std::string & str)91061da546Spatrick void Watchpoint::SetDeclInfo(const std::string &str) { m_decl_str = str; }
92061da546Spatrick
GetWatchSpec()93061da546Spatrick std::string Watchpoint::GetWatchSpec() { return m_watch_spec_str; }
94061da546Spatrick
SetWatchSpec(const std::string & str)95061da546Spatrick void Watchpoint::SetWatchSpec(const std::string &str) {
96061da546Spatrick m_watch_spec_str = str;
97061da546Spatrick }
98061da546Spatrick
IsHardware() const99be691f3bSpatrick bool Watchpoint::IsHardware() const {
100be691f3bSpatrick lldbassert(m_is_hardware || !HardwareRequired());
101be691f3bSpatrick return m_is_hardware;
102be691f3bSpatrick }
103061da546Spatrick
IsWatchVariable() const104061da546Spatrick bool Watchpoint::IsWatchVariable() const { return m_is_watch_variable; }
105061da546Spatrick
SetWatchVariable(bool val)106061da546Spatrick void Watchpoint::SetWatchVariable(bool val) { m_is_watch_variable = val; }
107061da546Spatrick
CaptureWatchedValue(const ExecutionContext & exe_ctx)108061da546Spatrick bool Watchpoint::CaptureWatchedValue(const ExecutionContext &exe_ctx) {
109061da546Spatrick ConstString watch_name("$__lldb__watch_value");
110061da546Spatrick m_old_value_sp = m_new_value_sp;
111061da546Spatrick Address watch_address(GetLoadAddress());
112061da546Spatrick if (!m_type.IsValid()) {
113061da546Spatrick // Don't know how to report new & old values, since we couldn't make a
114061da546Spatrick // scalar type for this watchpoint. This works around an assert in
115061da546Spatrick // ValueObjectMemory::Create.
116061da546Spatrick // FIXME: This should not happen, but if it does in some case we care about,
117061da546Spatrick // we can go grab the value raw and print it as unsigned.
118061da546Spatrick return false;
119061da546Spatrick }
120061da546Spatrick m_new_value_sp = ValueObjectMemory::Create(
121061da546Spatrick exe_ctx.GetBestExecutionContextScope(), watch_name.GetStringRef(),
122061da546Spatrick watch_address, m_type);
123061da546Spatrick m_new_value_sp = m_new_value_sp->CreateConstantValue(watch_name);
124061da546Spatrick return (m_new_value_sp && m_new_value_sp->GetError().Success());
125061da546Spatrick }
126061da546Spatrick
IncrementFalseAlarmsAndReviseHitCount()127061da546Spatrick void Watchpoint::IncrementFalseAlarmsAndReviseHitCount() {
128061da546Spatrick ++m_false_alarms;
129061da546Spatrick if (m_false_alarms) {
130be691f3bSpatrick if (m_hit_counter.GetValue() >= m_false_alarms) {
131be691f3bSpatrick m_hit_counter.Decrement(m_false_alarms);
132061da546Spatrick m_false_alarms = 0;
133061da546Spatrick } else {
134be691f3bSpatrick m_false_alarms -= m_hit_counter.GetValue();
135be691f3bSpatrick m_hit_counter.Reset();
136061da546Spatrick }
137061da546Spatrick }
138061da546Spatrick }
139061da546Spatrick
140061da546Spatrick // RETURNS - true if we should stop at this breakpoint, false if we
141061da546Spatrick // should continue.
142061da546Spatrick
ShouldStop(StoppointCallbackContext * context)143061da546Spatrick bool Watchpoint::ShouldStop(StoppointCallbackContext *context) {
144be691f3bSpatrick m_hit_counter.Increment();
145061da546Spatrick
146061da546Spatrick return IsEnabled();
147061da546Spatrick }
148061da546Spatrick
GetDescription(Stream * s,lldb::DescriptionLevel level)149061da546Spatrick void Watchpoint::GetDescription(Stream *s, lldb::DescriptionLevel level) {
150061da546Spatrick DumpWithLevel(s, level);
151061da546Spatrick }
152061da546Spatrick
Dump(Stream * s) const153061da546Spatrick void Watchpoint::Dump(Stream *s) const {
154061da546Spatrick DumpWithLevel(s, lldb::eDescriptionLevelBrief);
155061da546Spatrick }
156061da546Spatrick
157061da546Spatrick // If prefix is nullptr, we display the watch id and ignore the prefix
158061da546Spatrick // altogether.
DumpSnapshots(Stream * s,const char * prefix) const159061da546Spatrick void Watchpoint::DumpSnapshots(Stream *s, const char *prefix) const {
160061da546Spatrick if (!prefix) {
161061da546Spatrick s->Printf("\nWatchpoint %u hit:", GetID());
162061da546Spatrick prefix = "";
163061da546Spatrick }
164061da546Spatrick
165061da546Spatrick if (m_old_value_sp) {
166061da546Spatrick const char *old_value_cstr = m_old_value_sp->GetValueAsCString();
167061da546Spatrick if (old_value_cstr && old_value_cstr[0])
168061da546Spatrick s->Printf("\n%sold value: %s", prefix, old_value_cstr);
169061da546Spatrick else {
170061da546Spatrick const char *old_summary_cstr = m_old_value_sp->GetSummaryAsCString();
171061da546Spatrick if (old_summary_cstr && old_summary_cstr[0])
172061da546Spatrick s->Printf("\n%sold value: %s", prefix, old_summary_cstr);
173061da546Spatrick }
174061da546Spatrick }
175061da546Spatrick
176061da546Spatrick if (m_new_value_sp) {
177061da546Spatrick const char *new_value_cstr = m_new_value_sp->GetValueAsCString();
178061da546Spatrick if (new_value_cstr && new_value_cstr[0])
179061da546Spatrick s->Printf("\n%snew value: %s", prefix, new_value_cstr);
180061da546Spatrick else {
181061da546Spatrick const char *new_summary_cstr = m_new_value_sp->GetSummaryAsCString();
182061da546Spatrick if (new_summary_cstr && new_summary_cstr[0])
183061da546Spatrick s->Printf("\n%snew value: %s", prefix, new_summary_cstr);
184061da546Spatrick }
185061da546Spatrick }
186061da546Spatrick }
187061da546Spatrick
DumpWithLevel(Stream * s,lldb::DescriptionLevel description_level) const188061da546Spatrick void Watchpoint::DumpWithLevel(Stream *s,
189061da546Spatrick lldb::DescriptionLevel description_level) const {
190061da546Spatrick if (s == nullptr)
191061da546Spatrick return;
192061da546Spatrick
193061da546Spatrick assert(description_level >= lldb::eDescriptionLevelBrief &&
194061da546Spatrick description_level <= lldb::eDescriptionLevelVerbose);
195061da546Spatrick
196061da546Spatrick s->Printf("Watchpoint %u: addr = 0x%8.8" PRIx64
197061da546Spatrick " size = %u state = %s type = %s%s",
198061da546Spatrick GetID(), GetLoadAddress(), m_byte_size,
199061da546Spatrick IsEnabled() ? "enabled" : "disabled", m_watch_read ? "r" : "",
200061da546Spatrick m_watch_write ? "w" : "");
201061da546Spatrick
202061da546Spatrick if (description_level >= lldb::eDescriptionLevelFull) {
203061da546Spatrick if (!m_decl_str.empty())
204061da546Spatrick s->Printf("\n declare @ '%s'", m_decl_str.c_str());
205061da546Spatrick if (!m_watch_spec_str.empty())
206061da546Spatrick s->Printf("\n watchpoint spec = '%s'", m_watch_spec_str.c_str());
207061da546Spatrick
208061da546Spatrick // Dump the snapshots we have taken.
209061da546Spatrick DumpSnapshots(s, " ");
210061da546Spatrick
211061da546Spatrick if (GetConditionText())
212061da546Spatrick s->Printf("\n condition = '%s'", GetConditionText());
213061da546Spatrick m_options.GetCallbackDescription(s, description_level);
214061da546Spatrick }
215061da546Spatrick
216061da546Spatrick if (description_level >= lldb::eDescriptionLevelVerbose) {
217061da546Spatrick s->Printf("\n hw_index = %i hit_count = %-4u ignore_count = %-4u",
218061da546Spatrick GetHardwareIndex(), GetHitCount(), GetIgnoreCount());
219061da546Spatrick }
220061da546Spatrick }
221061da546Spatrick
IsEnabled() const222061da546Spatrick bool Watchpoint::IsEnabled() const { return m_enabled; }
223061da546Spatrick
224061da546Spatrick // Within StopInfo.cpp, we purposely turn on the ephemeral mode right before
225061da546Spatrick // temporarily disable the watchpoint in order to perform possible watchpoint
226061da546Spatrick // actions without triggering further watchpoint events. After the temporary
227061da546Spatrick // disabled watchpoint is enabled, we then turn off the ephemeral mode.
228061da546Spatrick
TurnOnEphemeralMode()229061da546Spatrick void Watchpoint::TurnOnEphemeralMode() { m_is_ephemeral = true; }
230061da546Spatrick
TurnOffEphemeralMode()231061da546Spatrick void Watchpoint::TurnOffEphemeralMode() {
232061da546Spatrick m_is_ephemeral = false;
233061da546Spatrick // Leaving ephemeral mode, reset the m_disabled_count!
234061da546Spatrick m_disabled_count = 0;
235061da546Spatrick }
236061da546Spatrick
IsDisabledDuringEphemeralMode()237061da546Spatrick bool Watchpoint::IsDisabledDuringEphemeralMode() {
238061da546Spatrick return m_disabled_count > 1 && m_is_ephemeral;
239061da546Spatrick }
240061da546Spatrick
SetEnabled(bool enabled,bool notify)241061da546Spatrick void Watchpoint::SetEnabled(bool enabled, bool notify) {
242061da546Spatrick if (!enabled) {
243061da546Spatrick if (!m_is_ephemeral)
244061da546Spatrick SetHardwareIndex(LLDB_INVALID_INDEX32);
245061da546Spatrick else
246061da546Spatrick ++m_disabled_count;
247061da546Spatrick
248061da546Spatrick // Don't clear the snapshots for now.
249061da546Spatrick // Within StopInfo.cpp, we purposely do disable/enable watchpoint while
250061da546Spatrick // performing watchpoint actions.
251061da546Spatrick }
252061da546Spatrick bool changed = enabled != m_enabled;
253061da546Spatrick m_enabled = enabled;
254061da546Spatrick if (notify && !m_is_ephemeral && changed)
255061da546Spatrick SendWatchpointChangedEvent(enabled ? eWatchpointEventTypeEnabled
256061da546Spatrick : eWatchpointEventTypeDisabled);
257061da546Spatrick }
258061da546Spatrick
SetWatchpointType(uint32_t type,bool notify)259061da546Spatrick void Watchpoint::SetWatchpointType(uint32_t type, bool notify) {
260061da546Spatrick int old_watch_read = m_watch_read;
261061da546Spatrick int old_watch_write = m_watch_write;
262061da546Spatrick m_watch_read = (type & LLDB_WATCH_TYPE_READ) != 0;
263061da546Spatrick m_watch_write = (type & LLDB_WATCH_TYPE_WRITE) != 0;
264061da546Spatrick if (notify &&
265061da546Spatrick (old_watch_read != m_watch_read || old_watch_write != m_watch_write))
266061da546Spatrick SendWatchpointChangedEvent(eWatchpointEventTypeTypeChanged);
267061da546Spatrick }
268061da546Spatrick
WatchpointRead() const269061da546Spatrick bool Watchpoint::WatchpointRead() const { return m_watch_read != 0; }
270061da546Spatrick
WatchpointWrite() const271061da546Spatrick bool Watchpoint::WatchpointWrite() const { return m_watch_write != 0; }
272061da546Spatrick
GetIgnoreCount() const273061da546Spatrick uint32_t Watchpoint::GetIgnoreCount() const { return m_ignore_count; }
274061da546Spatrick
SetIgnoreCount(uint32_t n)275061da546Spatrick void Watchpoint::SetIgnoreCount(uint32_t n) {
276061da546Spatrick bool changed = m_ignore_count != n;
277061da546Spatrick m_ignore_count = n;
278061da546Spatrick if (changed)
279061da546Spatrick SendWatchpointChangedEvent(eWatchpointEventTypeIgnoreChanged);
280061da546Spatrick }
281061da546Spatrick
InvokeCallback(StoppointCallbackContext * context)282061da546Spatrick bool Watchpoint::InvokeCallback(StoppointCallbackContext *context) {
283061da546Spatrick return m_options.InvokeCallback(context, GetID());
284061da546Spatrick }
285061da546Spatrick
SetCondition(const char * condition)286061da546Spatrick void Watchpoint::SetCondition(const char *condition) {
287061da546Spatrick if (condition == nullptr || condition[0] == '\0') {
288061da546Spatrick if (m_condition_up)
289061da546Spatrick m_condition_up.reset();
290061da546Spatrick } else {
291061da546Spatrick // Pass nullptr for expr_prefix (no translation-unit level definitions).
292061da546Spatrick Status error;
293061da546Spatrick m_condition_up.reset(m_target.GetUserExpressionForLanguage(
294061da546Spatrick condition, llvm::StringRef(), lldb::eLanguageTypeUnknown,
295061da546Spatrick UserExpression::eResultTypeAny, EvaluateExpressionOptions(), nullptr,
296061da546Spatrick error));
297061da546Spatrick if (error.Fail()) {
298061da546Spatrick // FIXME: Log something...
299061da546Spatrick m_condition_up.reset();
300061da546Spatrick }
301061da546Spatrick }
302061da546Spatrick SendWatchpointChangedEvent(eWatchpointEventTypeConditionChanged);
303061da546Spatrick }
304061da546Spatrick
GetConditionText() const305061da546Spatrick const char *Watchpoint::GetConditionText() const {
306061da546Spatrick if (m_condition_up)
307061da546Spatrick return m_condition_up->GetUserText();
308061da546Spatrick else
309061da546Spatrick return nullptr;
310061da546Spatrick }
311061da546Spatrick
SendWatchpointChangedEvent(lldb::WatchpointEventType eventKind)312061da546Spatrick void Watchpoint::SendWatchpointChangedEvent(
313061da546Spatrick lldb::WatchpointEventType eventKind) {
314061da546Spatrick if (!m_being_created &&
315061da546Spatrick GetTarget().EventTypeHasListeners(
316061da546Spatrick Target::eBroadcastBitWatchpointChanged)) {
317061da546Spatrick WatchpointEventData *data =
318061da546Spatrick new Watchpoint::WatchpointEventData(eventKind, shared_from_this());
319061da546Spatrick GetTarget().BroadcastEvent(Target::eBroadcastBitWatchpointChanged, data);
320061da546Spatrick }
321061da546Spatrick }
322061da546Spatrick
SendWatchpointChangedEvent(WatchpointEventData * data)323061da546Spatrick void Watchpoint::SendWatchpointChangedEvent(WatchpointEventData *data) {
324061da546Spatrick if (data == nullptr)
325061da546Spatrick return;
326061da546Spatrick
327061da546Spatrick if (!m_being_created &&
328061da546Spatrick GetTarget().EventTypeHasListeners(Target::eBroadcastBitWatchpointChanged))
329061da546Spatrick GetTarget().BroadcastEvent(Target::eBroadcastBitWatchpointChanged, data);
330061da546Spatrick else
331061da546Spatrick delete data;
332061da546Spatrick }
333061da546Spatrick
WatchpointEventData(WatchpointEventType sub_type,const WatchpointSP & new_watchpoint_sp)334061da546Spatrick Watchpoint::WatchpointEventData::WatchpointEventData(
335061da546Spatrick WatchpointEventType sub_type, const WatchpointSP &new_watchpoint_sp)
336*f6aab3d8Srobert : m_watchpoint_event(sub_type), m_new_watchpoint_sp(new_watchpoint_sp) {}
337061da546Spatrick
338061da546Spatrick Watchpoint::WatchpointEventData::~WatchpointEventData() = default;
339061da546Spatrick
GetFlavorString()340061da546Spatrick ConstString Watchpoint::WatchpointEventData::GetFlavorString() {
341061da546Spatrick static ConstString g_flavor("Watchpoint::WatchpointEventData");
342061da546Spatrick return g_flavor;
343061da546Spatrick }
344061da546Spatrick
GetFlavor() const345061da546Spatrick ConstString Watchpoint::WatchpointEventData::GetFlavor() const {
346061da546Spatrick return WatchpointEventData::GetFlavorString();
347061da546Spatrick }
348061da546Spatrick
GetWatchpoint()349061da546Spatrick WatchpointSP &Watchpoint::WatchpointEventData::GetWatchpoint() {
350061da546Spatrick return m_new_watchpoint_sp;
351061da546Spatrick }
352061da546Spatrick
353061da546Spatrick WatchpointEventType
GetWatchpointEventType() const354061da546Spatrick Watchpoint::WatchpointEventData::GetWatchpointEventType() const {
355061da546Spatrick return m_watchpoint_event;
356061da546Spatrick }
357061da546Spatrick
Dump(Stream * s) const358061da546Spatrick void Watchpoint::WatchpointEventData::Dump(Stream *s) const {}
359061da546Spatrick
360061da546Spatrick const Watchpoint::WatchpointEventData *
GetEventDataFromEvent(const Event * event)361061da546Spatrick Watchpoint::WatchpointEventData::GetEventDataFromEvent(const Event *event) {
362061da546Spatrick if (event) {
363061da546Spatrick const EventData *event_data = event->GetData();
364061da546Spatrick if (event_data &&
365061da546Spatrick event_data->GetFlavor() == WatchpointEventData::GetFlavorString())
366061da546Spatrick return static_cast<const WatchpointEventData *>(event->GetData());
367061da546Spatrick }
368061da546Spatrick return nullptr;
369061da546Spatrick }
370061da546Spatrick
371061da546Spatrick WatchpointEventType
GetWatchpointEventTypeFromEvent(const EventSP & event_sp)372061da546Spatrick Watchpoint::WatchpointEventData::GetWatchpointEventTypeFromEvent(
373061da546Spatrick const EventSP &event_sp) {
374061da546Spatrick const WatchpointEventData *data = GetEventDataFromEvent(event_sp.get());
375061da546Spatrick
376061da546Spatrick if (data == nullptr)
377061da546Spatrick return eWatchpointEventTypeInvalidType;
378061da546Spatrick else
379061da546Spatrick return data->GetWatchpointEventType();
380061da546Spatrick }
381061da546Spatrick
GetWatchpointFromEvent(const EventSP & event_sp)382061da546Spatrick WatchpointSP Watchpoint::WatchpointEventData::GetWatchpointFromEvent(
383061da546Spatrick const EventSP &event_sp) {
384061da546Spatrick WatchpointSP wp_sp;
385061da546Spatrick
386061da546Spatrick const WatchpointEventData *data = GetEventDataFromEvent(event_sp.get());
387061da546Spatrick if (data)
388061da546Spatrick wp_sp = data->m_new_watchpoint_sp;
389061da546Spatrick
390061da546Spatrick return wp_sp;
391061da546Spatrick }
392