summaryrefslogtreecommitdiffstats
path: root/rlz/mac/lib/rlz_value_store_mac.mm
blob: 5313a3013a9b80a016434799a01b475891bb8877 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
// Copyright (c) 2012 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include "rlz/mac/lib/rlz_value_store_mac.h"

#include "base/mac/foundation_util.h"
#include "base/file_path.h"
#include "base/logging.h"
#include "base/sys_string_conversions.h"
#include "rlz/lib/assert.h"
#include "rlz/lib/lib_values.h"
#include "rlz/lib/rlz_lib.h"

#import <Foundation/Foundation.h>
#include <pthread.h>

using base::mac::ObjCCast;

namespace rlz_lib {

// These are written to disk and should not be changed.
NSString* const kPingTimeKey = @"pingTime";
NSString* const kAccessPointKey = @"accessPoints";
NSString* const kProductEventKey = @"productEvents";
NSString* const kStatefulEventKey = @"statefulEvents";

namespace {

NSString* GetNSProductName(Product product) {
  return base::SysUTF8ToNSString(GetProductName(product));
}

NSString* GetNSAccessPointName(AccessPoint p) {
  return base::SysUTF8ToNSString(GetAccessPointName(p));
}

// Retrieves a subdictionary in |p| for key |k|, creating it if necessary.
// If the dictionary contains an object for |k| that is not a mutable
// dictionary, that object is replaced with an empty mutable dictinary.
NSMutableDictionary* GetOrCreateDict(
    NSMutableDictionary* p, NSString* k) {
  NSMutableDictionary* d = ObjCCast<NSMutableDictionary>([p objectForKey:k]);
  if (!d) {
    d = [NSMutableDictionary dictionaryWithCapacity:0];
    [p setObject:d forKey:k];
  }
  return d;
}

}  // namespace

RlzValueStoreMac::RlzValueStoreMac(NSMutableDictionary* dict,
                                   NSString* plist_path)
  : dict_([dict retain]), plist_path_([plist_path retain]) {
}

RlzValueStoreMac::~RlzValueStoreMac() {
}

bool RlzValueStoreMac::HasAccess(AccessType type) {
  NSFileManager* manager = [NSFileManager defaultManager];
  switch (type) {
    case kReadAccess:  return [manager isReadableFileAtPath:plist_path_];
    case kWriteAccess: return [manager isWritableFileAtPath:plist_path_];
  }
}

bool RlzValueStoreMac::WritePingTime(Product product, int64 time) {
  NSNumber* n = [NSNumber numberWithLongLong:time];
  [ProductDict(product) setObject:n forKey:kPingTimeKey];
  return true;
}

bool RlzValueStoreMac::ReadPingTime(Product product, int64* time) {
  if (NSNumber* n =
      ObjCCast<NSNumber>([ProductDict(product) objectForKey:kPingTimeKey])) {
    *time = [n longLongValue];
    return true;
  }
  return false;
}

bool RlzValueStoreMac::ClearPingTime(Product product) {
  [ProductDict(product) removeObjectForKey:kPingTimeKey];
  return true;
}


bool RlzValueStoreMac::WriteAccessPointRlz(AccessPoint access_point,
                                           const char* new_rlz) {
  NSMutableDictionary* d = GetOrCreateDict(WorkingDict(), kAccessPointKey);
  [d setObject:base::SysUTF8ToNSString(new_rlz)
      forKey:GetNSAccessPointName(access_point)];
  return true;
}

bool RlzValueStoreMac::ReadAccessPointRlz(AccessPoint access_point,
                                          char* rlz,
                                          size_t rlz_size) {
  // Reading a non-existent access point counts as success.
  if (NSDictionary* d = ObjCCast<NSDictionary>(
        [WorkingDict() objectForKey:kAccessPointKey])) {
    NSString* val = ObjCCast<NSString>(
        [d objectForKey:GetNSAccessPointName(access_point)]);
    if (!val) {
      if (rlz_size > 0)
        rlz[0] = '\0';
      return true;
    }

    std::string s = base::SysNSStringToUTF8(val);
    if (s.size() >= rlz_size) {
      rlz[0] = 0;
      ASSERT_STRING("GetAccessPointRlz: Insufficient buffer size");
      return false;
    }
    strncpy(rlz, s.c_str(), rlz_size);
    return true;
  }
  if (rlz_size > 0)
    rlz[0] = '\0';
  return true;
}

bool RlzValueStoreMac::ClearAccessPointRlz(AccessPoint access_point) {
  if (NSMutableDictionary* d = ObjCCast<NSMutableDictionary>(
      [WorkingDict() objectForKey:kAccessPointKey])) {
    [d removeObjectForKey:GetNSAccessPointName(access_point)];
  }
  return true;
}


bool RlzValueStoreMac::AddProductEvent(Product product,
                                       const char* event_rlz) {
  [GetOrCreateDict(ProductDict(product), kProductEventKey)
      setObject:[NSNumber numberWithBool:YES]
      forKey:base::SysUTF8ToNSString(event_rlz)];
  return true;
}

bool RlzValueStoreMac::ReadProductEvents(Product product,
                                         std::vector<std::string>* events) {
  if (NSDictionary* d = ObjCCast<NSDictionary>(
      [ProductDict(product) objectForKey:kProductEventKey])) {
    for (NSString* s in d)
      events->push_back(base::SysNSStringToUTF8(s));
    return true;
  }
  return true;
}

bool RlzValueStoreMac::ClearProductEvent(Product product,
                                         const char* event_rlz) {
  if (NSMutableDictionary* d = ObjCCast<NSMutableDictionary>(
      [ProductDict(product) objectForKey:kProductEventKey])) {
    [d removeObjectForKey:base::SysUTF8ToNSString(event_rlz)];
    return true;
  }
  return false;
}

bool RlzValueStoreMac::ClearAllProductEvents(Product product) {
  [ProductDict(product) removeObjectForKey:kProductEventKey];
  return true;
}


bool RlzValueStoreMac::AddStatefulEvent(Product product,
                                        const char* event_rlz) {
  [GetOrCreateDict(ProductDict(product), kStatefulEventKey)
      setObject:[NSNumber numberWithBool:YES]
      forKey:base::SysUTF8ToNSString(event_rlz)];
  return true;
}

bool RlzValueStoreMac::IsStatefulEvent(Product product,
                                       const char* event_rlz) {
  if (NSDictionary* d = ObjCCast<NSDictionary>(
        [ProductDict(product) objectForKey:kStatefulEventKey])) {
    return [d objectForKey:base::SysUTF8ToNSString(event_rlz)] != nil;
  }
  return false;
}

bool RlzValueStoreMac::ClearAllStatefulEvents(Product product) {
  [ProductDict(product) removeObjectForKey:kStatefulEventKey];
  return true;
}


void RlzValueStoreMac::CollectGarbage() {
  NOTIMPLEMENTED();
}

NSDictionary* RlzValueStoreMac::dictionary() {
  return dict_.get();
}

NSMutableDictionary* RlzValueStoreMac::WorkingDict() {
  std::string brand(SupplementaryBranding::GetBrand());
  if (brand.empty())
    return dict_;

  NSString* brand_ns =
      [@"brand_" stringByAppendingString:base::SysUTF8ToNSString(brand)];

  return GetOrCreateDict(dict_.get(), brand_ns);
}

NSMutableDictionary* RlzValueStoreMac::ProductDict(Product p) {
  return GetOrCreateDict(WorkingDict(), GetNSProductName(p));
}


namespace {

// Creating a recursive cross-process mutex on windows is one line. On mac,
// there's no primitve for that, so this lock is emulated by an in-process
// mutex to get the recursive part, followed by a cross-process lock for the
// cross-process part.

// This is a struct so that it doesn't need a static initializer.
struct RecursiveCrossProcessLock {
  // Tries to acquire a recursive cross-process lock. Note that this _always_
  // acquires the in-process lock (if it wasn't already acquired). The parent
  // directory of |lock_file| must exist.
  bool TryGetCrossProcessLock(NSString* lock_filename);

  // Releases the lock. Should always be called, even if
  // TryGetCrossProcessLock() returns false.
  void ReleaseLock();

  pthread_mutex_t recursive_lock_;
  pthread_t locking_thread_;

  NSDistributedLock* file_lock_;
} g_recursive_lock = {
  // PTHREAD_RECURSIVE_MUTEX_INITIALIZER doesn't exist before 10.7 and is buggy
  // on 10.7 (http://gcc.gnu.org/bugzilla/show_bug.cgi?id=51906#c34), so emulate
  // recursive locking with a normal non-recursive mutex.
  PTHREAD_MUTEX_INITIALIZER
};

bool RecursiveCrossProcessLock::TryGetCrossProcessLock(
    NSString* lock_filename) {
  bool just_got_lock = false;

  // Emulate a recursive mutex with a non-recursive one.
  if (pthread_mutex_trylock(&recursive_lock_) == EBUSY) {
    if (pthread_equal(pthread_self(), locking_thread_) == 0) {
      // Some other thread has the lock, wait for it.
      pthread_mutex_lock(&recursive_lock_);
      CHECK(locking_thread_ == 0);
      just_got_lock = true;
    }
  } else {
    just_got_lock = true;
  }

  locking_thread_ = pthread_self();

  // Try to acquire file lock.
  if (just_got_lock) {
    const int kMaxTimeoutMS = 5000;  // Matches windows.
    const int kSleepPerTryMS = 200;

    CHECK(!file_lock_);
    file_lock_ = [[NSDistributedLock alloc] initWithPath:lock_filename];

    BOOL got_file_lock = NO;
    int elapsedMS = 0;
    while (!(got_file_lock = [file_lock_ tryLock]) &&
           elapsedMS < kMaxTimeoutMS) {
      usleep(kSleepPerTryMS * 1000);
      elapsedMS += kSleepPerTryMS;
    }

    if (!got_file_lock) {
      [file_lock_ release];
      file_lock_ = nil;
      return false;
    }
    return true;
  } else {
    return file_lock_ != nil;
  }
}

void RecursiveCrossProcessLock::ReleaseLock() {
  if (file_lock_) {
    [file_lock_ unlock];
    [file_lock_ release];
    file_lock_ = nil;
  }

  locking_thread_ = 0;
  pthread_mutex_unlock(&recursive_lock_);
}


// This is set during test execution, to write RLZ files into a temporary
// directory instead of the user's Application Support folder.
NSString* g_test_folder;

// RlzValueStoreMac keeps its data in memory and only writes it to disk when
// ScopedRlzValueStoreLock goes out of scope. Hence, if several
// ScopedRlzValueStoreLocks are nested, they all need to use the same store
// object.

// This counts the nesting depth.
int g_lock_depth = 0;

// This is the store object that might be shared. Only set if g_lock_depth > 0.
RlzValueStoreMac* g_store_object = NULL;


NSString* CreateRlzDirectory() {
  NSFileManager* manager = [NSFileManager defaultManager];
  NSArray* paths = NSSearchPathForDirectoriesInDomains(
      NSApplicationSupportDirectory, NSUserDomainMask, /*expandTilde=*/YES);
  NSString* folder = nil;
  if ([paths count] > 0)
    folder = ObjCCast<NSString>([paths objectAtIndex:0]);
  if (!folder)
    folder = [@"~/Library/Application Support" stringByStandardizingPath];
  folder = [folder stringByAppendingPathComponent:@"Google/RLZ"];

  if (g_test_folder)
    folder = [g_test_folder stringByAppendingPathComponent:folder];

  [manager createDirectoryAtPath:folder
     withIntermediateDirectories:YES
                      attributes:nil
                           error:nil];
  return folder;
}

// Returns the path of the rlz plist store, also creates the parent directory
// path if it doesn't exist.
NSString* RlzPlistFilename() {
  NSString* const kRlzFile = @"RlzStore.plist";
  return [CreateRlzDirectory() stringByAppendingPathComponent:kRlzFile];
}

// Returns the path of the rlz lock file, also creates the parent directory
// path if it doesn't exist.
NSString* RlzLockFilename() {
  NSString* const kRlzFile = @"lockfile";
  return [CreateRlzDirectory() stringByAppendingPathComponent:kRlzFile];
}

}  // namespace

ScopedRlzValueStoreLock::ScopedRlzValueStoreLock() {
  bool got_distributed_lock =
      g_recursive_lock.TryGetCrossProcessLock(RlzLockFilename());
  // At this point, we hold the in-process lock, no matter the value of
  // |got_distributed_lock|.

  ++g_lock_depth;

  if (!got_distributed_lock) {
    // Give up. |store_| isn't set, which signals to callers that acquiring
    // the lock failed. |g_recursive_lock| will be released by the
    // destructor.
    CHECK(!g_store_object);
    return;
  }

  if (g_lock_depth > 1) {
    // Reuse the already existing store object.
    CHECK(g_store_object);
    store_.reset(g_store_object);
    return;
  }

  CHECK(!g_store_object);

  NSString* plist = RlzPlistFilename();

  // Create an empty file if none exists yet.
  NSFileManager* manager = [NSFileManager defaultManager];
  if (![manager fileExistsAtPath:plist isDirectory:NULL])
    [[NSDictionary dictionary] writeToFile:plist atomically:YES];

  NSMutableDictionary* dict =
      [NSMutableDictionary dictionaryWithContentsOfFile:plist];
  VERIFY(dict);

  if (dict) {
    store_.reset(new RlzValueStoreMac(dict, plist));
    g_store_object = (RlzValueStoreMac*)store_.get();
  }
}

ScopedRlzValueStoreLock::~ScopedRlzValueStoreLock() {
  --g_lock_depth;
  CHECK(g_lock_depth >= 0);

  if (g_lock_depth > 0) {
    // Other locks are still using store_, don't free it yet.
    ignore_result(store_.release());
    return;
  }

  if (store_.get()) {
    g_store_object = NULL;

    NSDictionary* dict =
        static_cast<RlzValueStoreMac*>(store_.get())->dictionary();
    VERIFY([dict writeToFile:RlzPlistFilename() atomically:YES]);
  }

  // Check that "store_ set" => "file_lock acquired". The converse isn't true,
  // for example if the rlz data file can't be read.
  if (store_.get())
    CHECK(g_recursive_lock.file_lock_);
  if (!g_recursive_lock.file_lock_)
    CHECK(!store_.get());

  g_recursive_lock.ReleaseLock();
}

RlzValueStore* ScopedRlzValueStoreLock::GetStore() {
  return store_.get();
}

namespace testing {

void SetRlzStoreDirectory(const FilePath& directory) {
  base::mac::ScopedNSAutoreleasePool pool;

  [g_test_folder release];
  if (directory.empty()) {
    g_test_folder = nil;
  } else {
    // Not Unsafe on OS X.
    g_test_folder =
      [[NSString alloc] initWithUTF8String:directory.AsUTF8Unsafe().c_str()];
  }
}

}  // namespace testing

}  // namespace rlz_lib