Skip to content
Snippets Groups Projects
Commit 99752ce1 authored by Cam Saül's avatar Cam Saül
Browse files

Integrate Metabase OS X repo.

parent 166bb509
No related merge requests found
Showing
with 1350 additions and 0 deletions
File deleted
......@@ -26,3 +26,12 @@ profiles.clj
/resources/sample-dataset.db.trace.db
/deploy/artifacts/*
/resources/version.properties
xcuserdata
xcshareddata
*.xcworkspacedata
OSX/Metabase/jre
OSX/Resources/metabase.jar
OSX/build
/osx-artifacts
OSX/dsa_priv.pem
bin/config.json
[submodule "OSX/Vendor/INAppStoreWindow"]
path = OSX/Vendor/INAppStoreWindow
url = git@github.com:indragiek/INAppStoreWindow.git
This diff is collapsed.
//
// AppDelegate.h
// Metabase
//
// Created by Cam Saul on 9/21/15.
// Copyright (c) 2015 Metabase. All rights reserved.
//
@import Cocoa;
@interface AppDelegate : NSObject <NSApplicationDelegate>
+ (AppDelegate *)instance;
@property (readonly) NSUInteger port;
@end
//
// AppDelegate.m
// Metabase
//
// Created by Cam Saul on 9/21/15.
// Copyright (c) 2015 Metabase. All rights reserved.
//
#import <Sparkle/Sparkle.h>
#import "AppDelegate.h"
#import "MainViewController.h"
#import "MetabaseTask.h"
#import "TaskHealthChecker.h"
@interface AppDelegate ()
@property (weak) IBOutlet NSWindow *window;
@property (strong, nonatomic) MetabaseTask *task;
@property (strong, nonatomic) TaskHealthChecker *healthChecker;
@end
static AppDelegate *sInstance = nil;
@implementation AppDelegate
+ (AppDelegate *)instance {
return sInstance;
}
- (id)init {
if (self = [super init]) {
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(taskTimedOut:) name:MetabaseTaskTimedOutNotification object:nil];
}
return self;
}
- (void)dealloc {
[[NSNotificationCenter defaultCenter] removeObserver:self];
}
- (void)applicationDidFinishLaunching:(NSNotification *)aNotification {
sInstance = self;
[[SUUpdater sharedUpdater] checkForUpdatesInBackground];
self.task = [MetabaseTask task];
self.healthChecker.port = self.task.port;
[self.healthChecker start];
}
- (void)applicationDidBecomeActive:(NSNotification *)notification {
// re-start the health checker if it's not checking like it should be : the HEALTH CHECKER HEALTH CHECKER
if (self.healthChecker.lastCheckTime) {
const CFTimeInterval timeSinceLastHealthCheck = CFAbsoluteTimeGetCurrent() - self.healthChecker.lastCheckTime;
if (timeSinceLastHealthCheck > 5.0f) {
NSLog(@"Last health check was %.0f ago, restarting health checker!", timeSinceLastHealthCheck);
[self.healthChecker start];
}
}
// (re)start the health checker just to be extra double-safe it's still running
}
- (void)applicationWillTerminate:(NSNotification *)notification {
self.task = nil;
}
#pragma mark - Notifications
- (void)taskTimedOut:(NSNotification *)notification {
NSLog(@"Metabase task timed out. Restarting...");
[self.healthChecker resetTimeout];
self.task = [MetabaseTask task];
}
#pragma mark - Getters / Setters
- (TaskHealthChecker *)healthChecker {
if (!_healthChecker) {
_healthChecker = [[TaskHealthChecker alloc] init];
}
return _healthChecker;
}
- (void)setTask:(MetabaseTask *)task {
[_task terminate];
_task = task;
[task launch];
}
- (NSUInteger)port {
return self.task.port;
}
@end
//
// MetabaseTask.h
// Metabase
//
// Created by Cam Saul on 10/9/15.
// Copyright (c) 2015 Metabase. All rights reserved.
//
@interface MetabaseTask : NSObject
/// Create (and launch) a task to run the Metabase backend server.
+ (MetabaseTask *)task;
- (void)launch;
- (void)terminate;
- (NSUInteger)port;
@end
//
// MetabaseTask.m
// Metabase
//
// Created by Cam Saul on 10/9/15.
// Copyright (c) 2015 Metabase. All rights reserved.
//
#import "MetabaseTask.h"
#define ENABLE_JAR_UNPACKING 0
@interface MetabaseTask ()
@property (strong, nonatomic) NSTask *task;
@property (strong, nonatomic) NSPipe *pipe;
@property (strong, nonatomic) NSFileHandle *readHandle;
@property (strong, readonly) NSString *javaPath;
@property (strong, readonly) NSString *jarPath;
@property (strong, readonly) NSString *dbPath;
#if ENABLE_JAR_UNPACKING
@property (strong, readonly) NSString *unpack200Path;
#endif
@property (nonatomic) NSUInteger port;
@end
@implementation MetabaseTask
+ (MetabaseTask *)task {
return [[MetabaseTask alloc] init];
}
#pragma mark - Lifecycle
- (instancetype)init {
if (self = [super init]) {
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(fileHandleCompletedRead:) name:NSFileHandleReadCompletionNotification object:nil];
}
return self;
}
- (void)dealloc {
[[NSNotificationCenter defaultCenter] removeObserver:self];
[self terminate];
}
#pragma mark - Notifications
- (void)fileHandleCompletedRead:(NSNotification *)notification {
if (!self.readHandle) return;
__weak MetabaseTask *weakSelf = self;
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0), ^{
if (!weakSelf) return;
@try {
NSString *message = [[NSString alloc] initWithData:weakSelf.readHandle.availableData encoding:NSUTF8StringEncoding];
// skip calls to health endpoint
if ([message rangeOfString:@"GET /api/health"].location == NSNotFound) {
// strip off the timestamp that comes back from the backend so we don't get double-timestamps when NSLog adds its own
NSRegularExpression *regex = [NSRegularExpression regularExpressionWithPattern:@"^[\\d-:,\\s]+(.*$)" options:NSRegularExpressionAnchorsMatchLines|NSRegularExpressionAllowCommentsAndWhitespace error:nil];
message = [regex stringByReplacingMatchesInString:message options:0 range:NSMakeRange(0, message.length) withTemplate:@"$1"];
// remove control codes used to color output
regex = [NSRegularExpression regularExpressionWithPattern:@"\\[\\d+m" options:0 error:nil];
message = [regex stringByReplacingMatchesInString:message options:0 range:NSMakeRange(0, message.length) withTemplate:@""];
NSLog(@"%@", message);
}
} @catch (NSException *) {}
dispatch_async(dispatch_get_main_queue(), ^{
[weakSelf.readHandle readInBackgroundAndNotify];
});
});
}
#pragma mark - Local Methods
#if ENABLE_JAR_UNPACKING
/// unpack the jars in the BG if needed the first time around
- (void)unpackJars {
[self.packedJarPaths enumerateObjectsWithOptions:NSEnumerationConcurrent usingBlock:^(NSString *packedFilename, NSUInteger idx, BOOL *stop) {
NSString *jarName = [packedFilename stringByReplacingOccurrencesOfString:@".pack.gz" withString:@".jar"];
if (![[NSFileManager defaultManager] fileExistsAtPath:jarName]) {
NSLog(@"Unpacking %@ ->\n\t%@...", packedFilename, jarName);
NSTask *task = [[NSTask alloc] init];
task.launchPath = self.unpack200Path;
task.arguments = @[packedFilename, jarName];
[task launch];
[task waitUntilExit];
}
}];
}
#endif
- (void)deleteOldDBLockFilesIfNeeded {
NSString *lockFile = [self.dbPath stringByAppendingString:@".lock.db"];
NSString *traceFile = [self.dbPath stringByAppendingString:@".trace.db"];
for (NSString *file in @[lockFile, traceFile]) {
if ([[NSFileManager defaultManager] fileExistsAtPath:file]) {
NSLog(@"Deleting %@...", file);
NSError *error = nil;
[[NSFileManager defaultManager] removeItemAtPath:file error:&error];
if (error) {
NSLog(@"Error deleting %@: %@", file, error.localizedDescription);
}
}
}
}
- (void)launch {
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
#if ENABLE_JAR_UNPACKING
[self unpackJars];
#endif
[self deleteOldDBLockFilesIfNeeded];
NSLog(@"Starting MetabaseTask @ 0x%zx...", (size_t)self);
self.task = [[NSTask alloc] init];
self.task.launchPath = self.javaPath;
self.task.environment = @{@"MB_DB_FILE": self.dbPath,
@"MB_JETTY_PORT": @(self.port)};
self.task.arguments = @[@"-jar", self.jarPath];
self.pipe = [NSPipe pipe];
self.task.standardOutput = self.pipe;
self.task.standardError = self.pipe;
__weak MetabaseTask *weakSelf = self;
self.task.terminationHandler = ^(NSTask *task){
NSLog(@"\n\n!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! Task terminated with exit code %d !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!", task.terminationStatus);
dispatch_async(dispatch_get_main_queue(), ^{
if ([[NSAlert alertWithMessageText:@"Fatal Error" defaultButton:@"Restart" alternateButton:@"Quit" otherButton:nil informativeTextWithFormat:@"The Metabase server terminated unexpectedly."] runModal] == NSAlertDefaultReturn) {
[weakSelf launch];
} else {
exit(task.terminationStatus);
}
});
};
self.readHandle = self.pipe.fileHandleForReading;
dispatch_async(dispatch_get_main_queue(), ^{
[self.readHandle readInBackgroundAndNotify];
});
NSLog(@"%@ -jar %@", self.javaPath, self.jarPath);
[self.task launch];
});
}
- (void)terminate {
if (!self.task) return; // already dead
NSLog(@"Killing MetabaseTask @ 0x%zx...", (size_t)self);
self.task = nil;
_port = 0;
}
#pragma mark - Getters / Setters
- (void)setTask:(NSTask *)task {
self.pipe = nil;
[_task terminate];
_task = task;
}
- (void)setPipe:(NSPipe *)pipe {
self.readHandle = nil;
_pipe = pipe;
}
- (void)setReadHandle:(NSFileHandle *)readHandle {
[_readHandle closeFile];
_readHandle = readHandle;
}
- (NSString *)javaPath {
return [[NSBundle mainBundle] pathForResource:@"java" ofType:nil inDirectory:@"jre/bin"];
}
- (NSString *)jarPath {
return [[NSBundle mainBundle] pathForResource:@"metabase" ofType:@"jar"];
}
- (NSString *)dbPath {
NSString *applicationSupportDir = [NSSearchPathForDirectoriesInDomains(NSApplicationSupportDirectory, NSUserDomainMask, YES)[0] stringByAppendingPathComponent:@"Metabase"];
if (![[NSFileManager defaultManager] fileExistsAtPath:applicationSupportDir]) {
NSError *error = nil;
[[NSFileManager defaultManager] createDirectoryAtPath:applicationSupportDir withIntermediateDirectories:YES attributes:nil error:&error];
if (error) {
NSLog(@"Error creating %@: %@", applicationSupportDir, error.localizedDescription);
}
}
return [applicationSupportDir stringByAppendingPathComponent:@"metabase.db"];
}
- (NSUInteger)port {
if (!_port) {
srand((unsigned)time(NULL));
_port = (rand() % 1000) + 13000;
NSLog(@"Using port %lu", _port);
}
return _port;
}
#if ENABLE_JAR_UNPACKING
- (NSArray *)packedJarPaths {
return [[NSBundle mainBundle] pathsForResourcesOfType:@"pack.gz" inDirectory:@"jre/lib"];
}
- (NSString *)unpack200Path {
return [[NSBundle mainBundle] pathForResource:@"unpack200" ofType:nil inDirectory:@"jre/bin"];
}
#endif
@end
//
// SettingsManager.h
// Metabase
//
// Created by Cam Saul on 9/22/15.
// Copyright (c) 2015 Metabase. All rights reserved.
//
NSString *LocalHostBaseURL();
@interface SettingsManager : NSObject
+ (instancetype)instance;
@property (copy) NSString *baseURL;
@end
//
// SettingsManager.m
// Metabase
//
// Created by Cam Saul on 9/22/15.
// Copyright (c) 2015 Metabase. All rights reserved.
//
#import "AppDelegate.h"
#import "SettingsManager.h"
static SettingsManager *sSettingsManager = nil;
static NSString * const BaseURLUserDefaultsKey = @"com.metabase.baseURL";
NSString *LocalHostBaseURL() {
return [NSString stringWithFormat:@"http://localhost:%lu", [AppDelegate instance].port];
}
@implementation SettingsManager
+ (instancetype)instance {
@synchronized(self) {
if (!sSettingsManager) sSettingsManager = [[SettingsManager alloc] init];
}
return sSettingsManager;
}
#pragma mark - Getters / Setters
- (NSString *)baseURL {
return [[NSUserDefaults standardUserDefaults] objectForKey:BaseURLUserDefaultsKey];
}
- (void)setBaseURL:(NSString *)baseURL {
[[NSUserDefaults standardUserDefaults] setObject:[baseURL copy] forKey:BaseURLUserDefaultsKey];
[[NSUserDefaults standardUserDefaults] synchronize];
}
@end
//
// TaskHealthChecker.h
// Metabase
//
// Created by Cam Saul on 10/9/15.
// Copyright (c) 2015 Metabase. All rights reserved.
//
static NSString * const MetabaseTaskBecameHealthyNotification = @"MetabaseTaskBecameHealthyNotification";
static NSString * const MetabaseTaskBecameUnhealthyNotification = @"MetabaseTaskBecameUnhealthyNotification";
static NSString * const MetabaseTaskTimedOutNotification = @"MetabaseTaskTimedOutNotification";
@interface TaskHealthChecker : NSObject
@property () NSUInteger port;
- (void)start;
- (void)stop;
- (void)resetTimeout;
- (CFAbsoluteTime)lastCheckTime;
@end
//
// TaskHealthChecker.m
// Metabase
//
// Created by Cam Saul on 10/9/15.
// Copyright (c) 2015 Metabase. All rights reserved.
//
#import "TaskHealthChecker.h"
/// Check out health every this many seconds
static const CGFloat HealthCheckIntervalSeconds = 1.2f;
/// This number should be lower than HealthCheckIntervalSeconds so requests don't end up piling up
static const CGFloat HealthCheckRequestTimeout = 0.25f;
/// After this many seconds of being unhealthy, consider the task timed out so it can be killed
static const CFTimeInterval TimeoutIntervalSeconds = 10.0f;
@interface TaskHealthChecker ()
@property (strong, nonatomic) NSOperationQueue *healthCheckOperationQueue;
@property (strong, nonatomic) NSTimer *healthCheckTimer;
@property (nonatomic) BOOL healthy;
@property CFAbsoluteTime lastHealthyTime;
@property CFAbsoluteTime lastCheckTime;
/// Set this to YES after server has started successfully one time
/// we'll hold of on the whole killing the Metabase server until it launches one time, I guess
@property (nonatomic) BOOL hasEverBeenHealthy;
@end
@implementation TaskHealthChecker
- (void)dealloc {
[self stop];
}
#pragma mark - Local Methods
- (void)resetTimeout {
self.lastHealthyTime = CFAbsoluteTimeGetCurrent();
}
- (void)start {
NSLog(@"(re)starting health checker @ 0x%zx...", (size_t)self);
self.healthCheckOperationQueue = [[NSOperationQueue alloc] init];
self.healthCheckOperationQueue.maxConcurrentOperationCount = 1;
[self resetTimeout];
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
self.healthCheckTimer = [NSTimer timerWithTimeInterval:HealthCheckIntervalSeconds target:self selector:@selector(checkHealth) userInfo:nil repeats:YES];
self.healthCheckTimer.tolerance = HealthCheckIntervalSeconds / 2.0f;
[[NSRunLoop mainRunLoop] addTimer:self.healthCheckTimer forMode:NSRunLoopCommonModes];
// self.healthCheckTimer = [NSTimer scheduledTimerWithTimeInterval:HealthCheckIntervalSeconds target:self selector:@selector(checkHealth) userInfo:nil repeats:YES];
// self.healthCheckTimer.tolerance = HealthCheckIntervalSeconds / 2.0f; // the timer doesn't need to fire exactly on the intervals, so give it a loose tolerance which will improve power savings, etc.
});}
- (void)stop {
self.healthCheckTimer = nil;
self.healthCheckOperationQueue = nil;
}
- (void)checkHealth:(void(^)(BOOL healthy))completion {
self.lastCheckTime = CFAbsoluteTimeGetCurrent();
/// Cancel any pending checks so they don't pile up indefinitely
[self.healthCheckOperationQueue cancelAllOperations];
NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:[NSURL URLWithString:[NSString stringWithFormat:@"http://localhost:%lu/api/health", self.port]] cachePolicy:NSURLRequestReloadIgnoringCacheData timeoutInterval:HealthCheckRequestTimeout];
[NSURLConnection sendAsynchronousRequest:request queue:self.healthCheckOperationQueue completionHandler:^(NSURLResponse *response, NSData *data, NSError *connectionError) {
if (connectionError) {
completion(NO);
return;
}
NSError *jsonError = nil;
NSDictionary *json = [NSJSONSerialization JSONObjectWithData:data options:NSJSONReadingAllowFragments error:&jsonError];
if (jsonError) {
completion(NO);
return;
}
completion([json[@"status"] isEqualToString:@"ok"]);
}];
}
- (void)checkHealth {
__weak TaskHealthChecker *weakSelf = self;
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0), ^{
[weakSelf checkHealth:^(BOOL healthy) {
if (!healthy) NSLog(@"😷");
if (healthy && !weakSelf.healthy) NSLog(@"✅");
weakSelf.healthy = healthy;
}];
});
}
#pragma mark - Getters / Setters
- (void)setHealthCheckTimer:(NSTimer *)healthCheckTimer {
[_healthCheckTimer invalidate];
_healthCheckTimer = healthCheckTimer;
}
- (void)setHealthCheckOperationQueue:(NSOperationQueue *)healthCheckOperationQueue {
[_healthCheckOperationQueue cancelAllOperations];
_healthCheckOperationQueue = healthCheckOperationQueue;
}
- (void)setHealthy:(BOOL)healthy {
if (healthy) {
self.lastHealthyTime = CFAbsoluteTimeGetCurrent();
} else {
const CFTimeInterval timeSinceWasLastHealthy = CFAbsoluteTimeGetCurrent() - self.lastHealthyTime;
if (timeSinceWasLastHealthy >= TimeoutIntervalSeconds) {
__weak TaskHealthChecker *weakSelf = self;
if (!self.hasEverBeenHealthy) {
NSLog(@"We've been waiting %.0f seconds, what's going on?", timeSinceWasLastHealthy);
return;
}
[[NSNotificationCenter defaultCenter] postNotificationName:MetabaseTaskTimedOutNotification object:weakSelf];
}
}
if (_healthy == healthy) return;
_healthy = healthy;
NSLog(@"\n\n"
"+--------------------------------------------------------------------+\n"
"| Server status has transitioned to: %@ |\n"
"+--------------------------------------------------------------------+\n\n", (healthy ? @"HEALTHY " : @"NOT HEALTHY"));
__weak TaskHealthChecker *weakSelf = self;
NSString *notification = healthy ? MetabaseTaskBecameHealthyNotification : MetabaseTaskBecameUnhealthyNotification;
[[NSNotificationCenter defaultCenter] postNotificationName:notification object:weakSelf];
}
@end
{
"images" : [
{
"size" : "16x16",
"idiom" : "mac",
"filename" : "Logo_16.png",
"scale" : "1x"
},
{
"size" : "16x16",
"idiom" : "mac",
"filename" : "Logo_32.png",
"scale" : "2x"
},
{
"size" : "32x32",
"idiom" : "mac",
"filename" : "Logo_32.png",
"scale" : "1x"
},
{
"size" : "32x32",
"idiom" : "mac",
"filename" : "Logo_64.png",
"scale" : "2x"
},
{
"size" : "128x128",
"idiom" : "mac",
"filename" : "Logo_128.png",
"scale" : "1x"
},
{
"size" : "128x128",
"idiom" : "mac",
"filename" : "Logo_256.png",
"scale" : "2x"
},
{
"size" : "256x256",
"idiom" : "mac",
"filename" : "Logo_256.png",
"scale" : "1x"
},
{
"size" : "256x256",
"idiom" : "mac",
"filename" : "Logo_512.png",
"scale" : "2x"
},
{
"size" : "512x512",
"idiom" : "mac",
"filename" : "Logo_512.png",
"scale" : "1x"
},
{
"size" : "512x512",
"idiom" : "mac",
"filename" : "Logo_1024.png",
"scale" : "2x"
}
],
"info" : {
"version" : 1,
"author" : "xcode"
}
}
\ No newline at end of file
OSX/Metabase/Images.xcassets/AppIcon.appiconset/Logo_1024.png

300 KiB

OSX/Metabase/Images.xcassets/AppIcon.appiconset/Logo_128.png

18.8 KiB

OSX/Metabase/Images.xcassets/AppIcon.appiconset/Logo_16.png

3.32 KiB

OSX/Metabase/Images.xcassets/AppIcon.appiconset/Logo_256.png

42 KiB

OSX/Metabase/Images.xcassets/AppIcon.appiconset/Logo_32.png

4.92 KiB

OSX/Metabase/Images.xcassets/AppIcon.appiconset/Logo_512.png

107 KiB

OSX/Metabase/Images.xcassets/AppIcon.appiconset/Logo_64.png

9.12 KiB

0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment