diff --git a/README.md b/README.md index 0f91320..769be16 100644 --- a/README.md +++ b/README.md @@ -97,6 +97,8 @@ Following keys are currently used: - `upload_delta`: This is the time delta data should be uploaded in seconds. - `api_url`: The url endpoint the data should be uploaded to. You can use the https://github.com/green-coding-berlin/green-metrics-tool if you want but also write/ use your own backend. - `web_url`: The url where the analytics can be found. We will append the machine ID to this so make sure the end of the string is a `=` +- `resolve_coalitions`: The way macOS works is that it looks as apps and not processes. So it can happen that when you look at your power data you see your shell as the main power hog. + This is because your shell has probably spawn the process that is using a lot of resources. Please add the name of the coalition to this list to resolve this error. ## The desktop App @@ -122,6 +124,17 @@ All data is saved in an sqlite database that is located under: /Library/Application Support/berlin.green-coding.hog/db.db ``` +## Updating + +We currently don't support an automatic update. You will have to: + +- Download the current app and move it into your Applications folder from https://github.com/green-coding-berlin/hog/releases . The file will be called `hog.app.zip` +- Rerun in the install script which will overwrite any custom changes you have made! +``` +sudo mv /etc/hog_settings.ini /etc/hog_settings.ini.back +curl -fsSL https://raw.githubusercontent.com/green-coding-berlin/hog/main/install.sh | sudo bash +``` + ## Contributing PRs are always welcome. Feel free to drop us an email or look into the issues. @@ -141,4 +154,4 @@ The hog is developed to not need any dependencies. - If you can't see the hog logo in the menu bar because of the notch there are multiple solutions. - you can use a tool like https://www.macbartender.com/Bartender4/ - - you can use the the command `$ sudo /usr/local/bin/hog/power_logger.py -w` to display the url. \ No newline at end of file + - you can use the the command `$ sudo /usr/local/bin/hog/power_logger.py -w` to display the url. diff --git a/app/hog/hog.xcodeproj/project.pbxproj b/app/hog/hog.xcodeproj/project.pbxproj index 356336f..202ec8a 100644 --- a/app/hog/hog.xcodeproj/project.pbxproj +++ b/app/hog/hog.xcodeproj/project.pbxproj @@ -7,13 +7,9 @@ objects = { /* Begin PBXBuildFile section */ - 0A21F0132AC6D0DA0036252A /* WidgetKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 0A21F0122AC6D0DA0036252A /* WidgetKit.framework */; }; - 0A21F0152AC6D0DA0036252A /* SwiftUI.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 0A21F0142AC6D0DA0036252A /* SwiftUI.framework */; }; - 0A21F0182AC6D0DA0036252A /* widgetBundle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0A21F0172AC6D0DA0036252A /* widgetBundle.swift */; }; - 0A21F01A2AC6D0DA0036252A /* widget.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0A21F0192AC6D0DA0036252A /* widget.swift */; }; - 0A21F01C2AC6D0DA0036252A /* AppIntent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0A21F01B2AC6D0DA0036252A /* AppIntent.swift */; }; - 0A21F01E2AC6D0DB0036252A /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 0A21F01D2AC6D0DB0036252A /* Assets.xcassets */; }; - 0A21F0232AC6D0DB0036252A /* widgetExtension.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = 0A21F0102AC6D0DA0036252A /* widgetExtension.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; + 0A1636632AE05DA2001C38B4 /* SettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0A1636622AE05DA2001C38B4 /* SettingsView.swift */; }; + 0A1636652AE05E3E001C38B4 /* InstallView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0A1636642AE05E3E001C38B4 /* InstallView.swift */; }; + 0A16366E2AE097CC001C38B4 /* UpdateView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0A16366D2AE097CC001C38B4 /* UpdateView.swift */; }; 0A69EDE42AA0820E00F4A364 /* DetailView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0A69EDE32AA0820E00F4A364 /* DetailView.swift */; }; 0A6C64572AAF5F9A00664D98 /* demo_db.db in Resources */ = {isa = PBXBuildFile; fileRef = 0A6C64562AAF5F9A00664D98 /* demo_db.db */; }; 0AEC07772A40D4C2003C82E7 /* hogApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0AEC07762A40D4C2003C82E7 /* hogApp.swift */; }; @@ -25,13 +21,6 @@ /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ - 0A21F0212AC6D0DB0036252A /* PBXContainerItemProxy */ = { - isa = PBXContainerItemProxy; - containerPortal = 0AEC076B2A40D4C2003C82E7 /* Project object */; - proxyType = 1; - remoteGlobalIDString = 0A21F00F2AC6D0DA0036252A; - remoteInfo = widgetExtension; - }; 0AEC07852A40D4C3003C82E7 /* PBXContainerItemProxy */ = { isa = PBXContainerItemProxy; containerPortal = 0AEC076B2A40D4C2003C82E7 /* Project object */; @@ -55,7 +44,6 @@ dstPath = ""; dstSubfolderSpec = 13; files = ( - 0A21F0232AC6D0DB0036252A /* widgetExtension.appex in Embed Foundation Extensions */, ); name = "Embed Foundation Extensions"; runOnlyForDeploymentPostprocessing = 0; @@ -63,15 +51,11 @@ /* End PBXCopyFilesBuildPhase section */ /* Begin PBXFileReference section */ - 0A21F0102AC6D0DA0036252A /* widgetExtension.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = widgetExtension.appex; sourceTree = BUILT_PRODUCTS_DIR; }; + 0A1636622AE05DA2001C38B4 /* SettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsView.swift; sourceTree = ""; }; + 0A1636642AE05E3E001C38B4 /* InstallView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InstallView.swift; sourceTree = ""; }; + 0A16366D2AE097CC001C38B4 /* UpdateView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UpdateView.swift; sourceTree = ""; }; 0A21F0122AC6D0DA0036252A /* WidgetKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = WidgetKit.framework; path = System/Library/Frameworks/WidgetKit.framework; sourceTree = SDKROOT; }; 0A21F0142AC6D0DA0036252A /* SwiftUI.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = SwiftUI.framework; path = System/Library/Frameworks/SwiftUI.framework; sourceTree = SDKROOT; }; - 0A21F0172AC6D0DA0036252A /* widgetBundle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = widgetBundle.swift; sourceTree = ""; }; - 0A21F0192AC6D0DA0036252A /* widget.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = widget.swift; sourceTree = ""; }; - 0A21F01B2AC6D0DA0036252A /* AppIntent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppIntent.swift; sourceTree = ""; }; - 0A21F01D2AC6D0DB0036252A /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; - 0A21F01F2AC6D0DB0036252A /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; - 0A21F0202AC6D0DB0036252A /* widget.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = widget.entitlements; sourceTree = ""; }; 0A69EDE32AA0820E00F4A364 /* DetailView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DetailView.swift; sourceTree = ""; }; 0A6C64552AACBA5A00664D98 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; path = Info.plist; sourceTree = ""; }; 0A6C64562AAF5F9A00664D98 /* demo_db.db */ = {isa = PBXFileReference; lastKnownFileType = text; path = demo_db.db; sourceTree = ""; }; @@ -88,15 +72,6 @@ /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ - 0A21F00D2AC6D0DA0036252A /* Frameworks */ = { - isa = PBXFrameworksBuildPhase; - buildActionMask = 2147483647; - files = ( - 0A21F0152AC6D0DA0036252A /* SwiftUI.framework in Frameworks */, - 0A21F0132AC6D0DA0036252A /* WidgetKit.framework in Frameworks */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; 0AEC07702A40D4C2003C82E7 /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; @@ -130,26 +105,12 @@ name = Frameworks; sourceTree = ""; }; - 0A21F0162AC6D0DA0036252A /* widget */ = { - isa = PBXGroup; - children = ( - 0A21F0172AC6D0DA0036252A /* widgetBundle.swift */, - 0A21F0192AC6D0DA0036252A /* widget.swift */, - 0A21F01B2AC6D0DA0036252A /* AppIntent.swift */, - 0A21F01D2AC6D0DB0036252A /* Assets.xcassets */, - 0A21F01F2AC6D0DB0036252A /* Info.plist */, - 0A21F0202AC6D0DB0036252A /* widget.entitlements */, - ); - path = widget; - sourceTree = ""; - }; 0AEC076A2A40D4C2003C82E7 = { isa = PBXGroup; children = ( 0AEC07752A40D4C2003C82E7 /* hog */, 0AEC07872A40D4C3003C82E7 /* hogTests */, 0AEC07912A40D4C3003C82E7 /* hogUITests */, - 0A21F0162AC6D0DA0036252A /* widget */, 0A21F0112AC6D0DA0036252A /* Frameworks */, 0AEC07742A40D4C2003C82E7 /* Products */, ); @@ -161,7 +122,6 @@ 0AEC07732A40D4C2003C82E7 /* hog.app */, 0AEC07842A40D4C3003C82E7 /* hogTests.xctest */, 0AEC078E2A40D4C3003C82E7 /* hogUITests.xctest */, - 0A21F0102AC6D0DA0036252A /* widgetExtension.appex */, ); name = Products; sourceTree = ""; @@ -172,6 +132,9 @@ 0A6C64552AACBA5A00664D98 /* Info.plist */, 0AEC07762A40D4C2003C82E7 /* hogApp.swift */, 0A69EDE32AA0820E00F4A364 /* DetailView.swift */, + 0A1636622AE05DA2001C38B4 /* SettingsView.swift */, + 0A1636642AE05E3E001C38B4 /* InstallView.swift */, + 0A16366D2AE097CC001C38B4 /* UpdateView.swift */, 0AEC077A2A40D4C3003C82E7 /* Assets.xcassets */, 0AEC077F2A40D4C3003C82E7 /* hog.entitlements */, 0AEC077C2A40D4C3003C82E7 /* Preview Content */, @@ -208,23 +171,6 @@ /* End PBXGroup section */ /* Begin PBXNativeTarget section */ - 0A21F00F2AC6D0DA0036252A /* widgetExtension */ = { - isa = PBXNativeTarget; - buildConfigurationList = 0A21F0272AC6D0DB0036252A /* Build configuration list for PBXNativeTarget "widgetExtension" */; - buildPhases = ( - 0A21F00C2AC6D0DA0036252A /* Sources */, - 0A21F00D2AC6D0DA0036252A /* Frameworks */, - 0A21F00E2AC6D0DA0036252A /* Resources */, - ); - buildRules = ( - ); - dependencies = ( - ); - name = widgetExtension; - productName = widgetExtension; - productReference = 0A21F0102AC6D0DA0036252A /* widgetExtension.appex */; - productType = "com.apple.product-type.app-extension"; - }; 0AEC07722A40D4C2003C82E7 /* hog */ = { isa = PBXNativeTarget; buildConfigurationList = 0AEC07982A40D4C3003C82E7 /* Build configuration list for PBXNativeTarget "hog" */; @@ -237,7 +183,6 @@ buildRules = ( ); dependencies = ( - 0A21F0222AC6D0DB0036252A /* PBXTargetDependency */, ); name = hog; productName = hog; @@ -287,12 +232,12 @@ isa = PBXProject; attributes = { BuildIndependentTargetsInParallel = 1; + KnownAssetTags = ( + New, + ); LastSwiftUpdateCheck = 1500; LastUpgradeCheck = 1500; TargetAttributes = { - 0A21F00F2AC6D0DA0036252A = { - CreatedOnToolsVersion = 15.0; - }; 0AEC07722A40D4C2003C82E7 = { CreatedOnToolsVersion = 14.3.1; }; @@ -322,20 +267,11 @@ 0AEC07722A40D4C2003C82E7 /* hog */, 0AEC07832A40D4C3003C82E7 /* hogTests */, 0AEC078D2A40D4C3003C82E7 /* hogUITests */, - 0A21F00F2AC6D0DA0036252A /* widgetExtension */, ); }; /* End PBXProject section */ /* Begin PBXResourcesBuildPhase section */ - 0A21F00E2AC6D0DA0036252A /* Resources */ = { - isa = PBXResourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - 0A21F01E2AC6D0DB0036252A /* Assets.xcassets in Resources */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; 0AEC07712A40D4C2003C82E7 /* Resources */ = { isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; @@ -363,21 +299,14 @@ /* End PBXResourcesBuildPhase section */ /* Begin PBXSourcesBuildPhase section */ - 0A21F00C2AC6D0DA0036252A /* Sources */ = { - isa = PBXSourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - 0A21F0182AC6D0DA0036252A /* widgetBundle.swift in Sources */, - 0A21F01A2AC6D0DA0036252A /* widget.swift in Sources */, - 0A21F01C2AC6D0DA0036252A /* AppIntent.swift in Sources */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; 0AEC076F2A40D4C2003C82E7 /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + 0A1636632AE05DA2001C38B4 /* SettingsView.swift in Sources */, 0A69EDE42AA0820E00F4A364 /* DetailView.swift in Sources */, + 0A16366E2AE097CC001C38B4 /* UpdateView.swift in Sources */, + 0A1636652AE05E3E001C38B4 /* InstallView.swift in Sources */, 0AEC07772A40D4C2003C82E7 /* hogApp.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; @@ -402,11 +331,6 @@ /* End PBXSourcesBuildPhase section */ /* Begin PBXTargetDependency section */ - 0A21F0222AC6D0DB0036252A /* PBXTargetDependency */ = { - isa = PBXTargetDependency; - target = 0A21F00F2AC6D0DA0036252A /* widgetExtension */; - targetProxy = 0A21F0212AC6D0DB0036252A /* PBXContainerItemProxy */; - }; 0AEC07862A40D4C3003C82E7 /* PBXTargetDependency */ = { isa = PBXTargetDependency; target = 0AEC07722A40D4C2003C82E7 /* hog */; @@ -420,69 +344,6 @@ /* End PBXTargetDependency section */ /* Begin XCBuildConfiguration section */ - 0A21F0252AC6D0DB0036252A /* Debug */ = { - isa = XCBuildConfiguration; - buildSettings = { - ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; - ASSETCATALOG_COMPILER_WIDGET_BACKGROUND_COLOR_NAME = WidgetBackground; - CODE_SIGN_ENTITLEMENTS = widget/widget.entitlements; - CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 1; - DEVELOPMENT_TEAM = SBWA476E6F; - ENABLE_HARDENED_RUNTIME = YES; - GCC_C_LANGUAGE_STANDARD = gnu17; - GENERATE_INFOPLIST_FILE = YES; - INFOPLIST_FILE = widget/Info.plist; - INFOPLIST_KEY_CFBundleDisplayName = widget; - INFOPLIST_KEY_NSHumanReadableCopyright = ""; - LD_RUNPATH_SEARCH_PATHS = ( - "$(inherited)", - "@executable_path/../Frameworks", - "@executable_path/../../../../Frameworks", - ); - LOCALIZATION_PREFERS_STRING_CATALOGS = YES; - MACOSX_DEPLOYMENT_TARGET = 14.0; - MARKETING_VERSION = 1.0; - PRODUCT_BUNDLE_IDENTIFIER = "berlin.green-coding.hog.widget"; - PRODUCT_NAME = "$(TARGET_NAME)"; - SKIP_INSTALL = YES; - SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)"; - SWIFT_EMIT_LOC_STRINGS = YES; - SWIFT_VERSION = 5.0; - }; - name = Debug; - }; - 0A21F0262AC6D0DB0036252A /* Release */ = { - isa = XCBuildConfiguration; - buildSettings = { - ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; - ASSETCATALOG_COMPILER_WIDGET_BACKGROUND_COLOR_NAME = WidgetBackground; - CODE_SIGN_ENTITLEMENTS = widget/widget.entitlements; - CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 1; - DEVELOPMENT_TEAM = SBWA476E6F; - ENABLE_HARDENED_RUNTIME = YES; - GCC_C_LANGUAGE_STANDARD = gnu17; - GENERATE_INFOPLIST_FILE = YES; - INFOPLIST_FILE = widget/Info.plist; - INFOPLIST_KEY_CFBundleDisplayName = widget; - INFOPLIST_KEY_NSHumanReadableCopyright = ""; - LD_RUNPATH_SEARCH_PATHS = ( - "$(inherited)", - "@executable_path/../Frameworks", - "@executable_path/../../../../Frameworks", - ); - LOCALIZATION_PREFERS_STRING_CATALOGS = YES; - MACOSX_DEPLOYMENT_TARGET = 14.0; - MARKETING_VERSION = 1.0; - PRODUCT_BUNDLE_IDENTIFIER = "berlin.green-coding.hog.widget"; - PRODUCT_NAME = "$(TARGET_NAME)"; - SKIP_INSTALL = YES; - SWIFT_EMIT_LOC_STRINGS = YES; - SWIFT_VERSION = 5.0; - }; - name = Release; - }; 0AEC07962A40D4C3003C82E7 /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { @@ -615,7 +476,7 @@ "CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development"; CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; - CURRENT_PROJECT_VERSION = 3; + CURRENT_PROJECT_VERSION = 1; DEAD_CODE_STRIPPING = YES; DEVELOPMENT_ASSET_PATHS = "\"hog/Preview Content\""; DEVELOPMENT_TEAM = SBWA476E6F; @@ -631,7 +492,8 @@ "$(inherited)", "@executable_path/../Frameworks", ); - MARKETING_VERSION = 0.2; + MACOSX_DEPLOYMENT_TARGET = 13.0; + MARKETING_VERSION = 0.3; PRODUCT_BUNDLE_IDENTIFIER = "berlin.green-coding.hog"; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_EMIT_LOC_STRINGS = YES; @@ -650,7 +512,7 @@ "CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development"; CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; - CURRENT_PROJECT_VERSION = 3; + CURRENT_PROJECT_VERSION = 1; DEAD_CODE_STRIPPING = YES; DEVELOPMENT_ASSET_PATHS = "\"hog/Preview Content\""; DEVELOPMENT_TEAM = SBWA476E6F; @@ -666,7 +528,8 @@ "$(inherited)", "@executable_path/../Frameworks", ); - MARKETING_VERSION = 0.2; + MACOSX_DEPLOYMENT_TARGET = 13.0; + MARKETING_VERSION = 0.3; PRODUCT_BUNDLE_IDENTIFIER = "berlin.green-coding.hog"; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_EMIT_LOC_STRINGS = YES; @@ -753,15 +616,6 @@ /* End XCBuildConfiguration section */ /* Begin XCConfigurationList section */ - 0A21F0272AC6D0DB0036252A /* Build configuration list for PBXNativeTarget "widgetExtension" */ = { - isa = XCConfigurationList; - buildConfigurations = ( - 0A21F0252AC6D0DB0036252A /* Debug */, - 0A21F0262AC6D0DB0036252A /* Release */, - ); - defaultConfigurationIsVisible = 0; - defaultConfigurationName = Release; - }; 0AEC076E2A40D4C2003C82E7 /* Build configuration list for PBXProject "hog" */ = { isa = XCConfigurationList; buildConfigurations = ( diff --git a/app/hog/hog.xcodeproj/project.xcworkspace/xcuserdata/didi.xcuserdatad/UserInterfaceState.xcuserstate b/app/hog/hog.xcodeproj/project.xcworkspace/xcuserdata/didi.xcuserdatad/UserInterfaceState.xcuserstate index 4446bd4..8951365 100644 Binary files a/app/hog/hog.xcodeproj/project.xcworkspace/xcuserdata/didi.xcuserdatad/UserInterfaceState.xcuserstate and b/app/hog/hog.xcodeproj/project.xcworkspace/xcuserdata/didi.xcuserdatad/UserInterfaceState.xcuserstate differ diff --git a/app/hog/hog/Assets.xcassets/logo_bw_bar.imageset/Contents.json b/app/hog/hog/Assets.xcassets/logo_bw_bar.imageset/Contents.json new file mode 100644 index 0000000..8ebf0a5 --- /dev/null +++ b/app/hog/hog/Assets.xcassets/logo_bw_bar.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "filename" : "logo_bw_bar.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/app/hog/hog/Assets.xcassets/logo_bw_bar.imageset/logo_bw_bar.png b/app/hog/hog/Assets.xcassets/logo_bw_bar.imageset/logo_bw_bar.png new file mode 100644 index 0000000..dfb1c88 Binary files /dev/null and b/app/hog/hog/Assets.xcassets/logo_bw_bar.imageset/logo_bw_bar.png differ diff --git a/app/hog/hog/DetailView.swift b/app/hog/hog/DetailView.swift index 7640c8c..ddbe867 100644 --- a/app/hog/hog/DetailView.swift +++ b/app/hog/hog/DetailView.swift @@ -13,6 +13,7 @@ import SwiftUI import SQLite3 import Charts import AppKit +import Combine var db_path = "/Library/Application Support/berlin.green-coding.hog/db.db" @@ -145,93 +146,42 @@ func checkDB() -> Bool { return fileManager.fileExists(atPath: db_path) } -class SettingsManager: ObservableObject { - var lookBackTime:Int = 0 - - @Published var machine_uuid: String = "Loading ..." - @Published var powermetrics: Int = 0 - @Published var api_url: String = "Loading ..." - @Published var web_url: String = "Loading ..." - @Published var upload_data: Bool = true - @Published var upload_backlog: Int = 0 +class LoadingClass { + + var lookBackTime:Int = 0 + @Published var isLoading: Bool = false + + func loadDataFrom() { + fatalError("loadDataFrom() must be overridden in subclasses") + } + + public func refreshData(lookBackTime: Int = 0) -> Void{ + if self.isLoading == true { + return + } - init(){ self.isLoading = true + self.lookBackTime = lookBackTime DispatchQueue.global(qos: .userInitiated).async { self.loadDataFrom() DispatchQueue.main.async { self.isLoading = false } - } - } - func loadDataFrom() { - var db: OpaquePointer? - - if sqlite3_open(db_path, &db) != SQLITE_OK { // Open database - print("error opening database") - return - } - - let lastMeasurementQuery = "SELECT machine_uuid, powermetrics, api_url, web_url, upload_data FROM settings ORDER BY time DESC LIMIT 1;" - var queryStatement: OpaquePointer? - - var new_machine_uuid = "Loading ..." - var new_powermetrics: Int = 0 - var new_api_url = "Loading ..." - var new_web_url = "Loading ..." - var upload_data = true - - if sqlite3_prepare_v2(db, lastMeasurementQuery, -1, &queryStatement, nil) == SQLITE_OK { - if sqlite3_step(queryStatement) == SQLITE_ROW { - new_machine_uuid = String(cString: sqlite3_column_text(queryStatement, 0)) - new_powermetrics = Int(sqlite3_column_int(queryStatement, 1)) - new_api_url = String(cString: sqlite3_column_text(queryStatement, 2)) - new_web_url = String(cString: sqlite3_column_text(queryStatement, 3)) - upload_data = sqlite3_column_int(queryStatement, 4) != 0 // assuming it's stored as 0 for false, non-0 for true - } - sqlite3_finalize(queryStatement) } - - let uploadCountQuery = "SELECT COUNT(*) FROM measurements WHERE uploaded = 0;" - var new_upload_backlog: Int = 0 - - if sqlite3_prepare_v2(db, uploadCountQuery, -1, &queryStatement, nil) == SQLITE_OK { - if sqlite3_step(queryStatement) == SQLITE_ROW { - new_upload_backlog = Int(sqlite3_column_int(queryStatement, 0)) - } - sqlite3_finalize(queryStatement) // Always finalize your statement when done - } else { - print("SELECT statement could not be prepared") - } - - sqlite3_close(db) - - DispatchQueue.main.async { - self.machine_uuid = new_machine_uuid - self.powermetrics = new_powermetrics - self.api_url = new_api_url - self.web_url = new_web_url - self.upload_data = upload_data - self.upload_backlog = new_upload_backlog - } - } - } - -class ValueManager: ObservableObject { - var lookBackTime:Int = 0 +class ValueManager: LoadingClass, ObservableObject{ + @Published var energy: Int64 = 0 @Published var providerRunning: Bool = false @Published var topApp: String = "Loading..." - @Published var isLoading: Bool = true enum ValueType { @@ -239,17 +189,7 @@ class ValueManager: ObservableObject { case string } - public func refreshData(lookBackTime: Int = 0) -> Void{ - - self.isLoading = true - self.lookBackTime = lookBackTime - - DispatchQueue.global(qos: .userInitiated).async { - self.loadDataFrom() - } - } - - func loadDataFrom() { + override func loadDataFrom() { var db: OpaquePointer? @@ -304,7 +244,6 @@ class ValueManager: ObservableObject { self.energy = newEnergy self.providerRunning = newScriptRunning self.topApp = newTopApp - self.isLoading = false } sqlite3_close(db) @@ -335,6 +274,7 @@ class ValueManager: ObservableObject { return nil } } + struct TopProcess: Codable, Identifiable { let id: UUID = UUID() // Add this line if you want a unique identifier let name: String @@ -346,8 +286,7 @@ struct TopProcess: Codable, Identifiable { } } -class TopProcessData: Identifiable, ObservableObject, RandomAccessCollection { - var lookBackTime:Int = 0 +class TopProcessData: LoadingClass, Identifiable, ObservableObject, RandomAccessCollection { typealias Element = TopProcess typealias Index = Array.Index @@ -356,8 +295,6 @@ class TopProcessData: Identifiable, ObservableObject, RandomAccessCollection { var startIndex: Index { lines.startIndex } var endIndex: Index { lines.endIndex } - @Published var isLoading: Bool = true - subscript(position: Index) -> Element { lines[position] } @@ -379,16 +316,8 @@ class TopProcessData: Identifiable, ObservableObject, RandomAccessCollection { } } - public func refreshData(lookBackTime: Int = 0) -> Void{ - self.isLoading = true - self.lookBackTime = lookBackTime - DispatchQueue.global(qos: .userInitiated).async { - self.loadDataFrom() - } - } - - private func loadDataFrom() { + override func loadDataFrom() { var db: OpaquePointer? @@ -433,8 +362,6 @@ class TopProcessData: Identifiable, ObservableObject, RandomAccessCollection { } DispatchQueue.main.async { self.lines = newLines - self.isLoading = false - } } @@ -459,8 +386,7 @@ struct DataPoint: Codable, Identifiable { } } -class ChartData: ObservableObject, RandomAccessCollection { - var lookBackTime:Int = 0 +class ChartData: LoadingClass, ObservableObject, RandomAccessCollection { typealias Element = DataPoint typealias Index = Array.Index @@ -469,25 +395,12 @@ class ChartData: ObservableObject, RandomAccessCollection { var startIndex: Index { points.startIndex } var endIndex: Index { points.endIndex } - @Published var isLoading: Bool = false - subscript(position: Index) -> Element { points[position] } - public func refreshData(lookBackTime: Int = 0) -> Void{ - self.isLoading = true - self.lookBackTime = lookBackTime - DispatchQueue.global(qos: .userInitiated).async { - self.loadDataFrom() - DispatchQueue.main.async { - self.isLoading = false - } - } - } - - private func loadDataFrom() { + override func loadDataFrom() { var db: OpaquePointer? // SQLite database object @@ -544,18 +457,14 @@ struct PointsGraph: View { .padding() } else { VStack { - if chartData.isEmpty { - Text("No Data! Please enable provider app.").font(.largeTitle) - }else{ - Chart(chartData) { - BarMark( - x: .value("Time", $0.time!), - y: .value("Energy", $0.combined_energy) - ) - } - .chartYAxisLabel("mJ") - .chartXAxisLabel("Time") + Chart(chartData) { + BarMark( + x: .value("Time", $0.time!), + y: .value("Energy", $0.combined_energy) + ) } + .chartYAxisLabel("mJ") + .chartXAxisLabel("Time") } } } @@ -637,102 +546,123 @@ struct TextInputView: View { } } -struct DataView: View { - @State var chartData = ChartData() - @State var lineData = TopProcessData() - @ObservedObject var valueManager = ValueManager() +struct DataView: View { + + @StateObject var chartData = ChartData() + @StateObject var lineData = TopProcessData() + @StateObject var valueManager = ValueManager() + @StateObject var settingsManager = SettingsManager() + @State private var isHovering = false @State private var refreshFlag = false - var settingsManager = SettingsManager() - var lookBackTime: Int + var viewModel: ViewModel + var whoAmI: TabSelection + @State private var text: String = "" @State private var isTextInputViewPresented: Bool = false - - init(lookBackTime: Int = 0) { - self.lookBackTime = lookBackTime + func refresh() { self.chartData.refreshData(lookBackTime: self.lookBackTime) self.lineData.refreshData(lookBackTime: self.lookBackTime) self.valueManager.refreshData(lookBackTime: self.lookBackTime) } + init(lookBackTime: Int = 0, viewModel: ViewModel, whoAmI: TabSelection) { + self.lookBackTime = lookBackTime + self.viewModel = viewModel + self.whoAmI = whoAmI + } + var body: some View { VStack(){ - - HStack { - VStack(alignment: .leading, spacing: 8) { - Text("This is a very minimalistic overview of your energy usage.") - } - - Spacer(minLength: 10) - - Button(action: { - self.chartData.refreshData(lookBackTime: self.lookBackTime) - self.lineData.refreshData(lookBackTime: self.lookBackTime) - self.valueManager.refreshData(lookBackTime: self.lookBackTime) - }) { - Image(systemName: "goforward") + if chartData.isLoading == false && chartData.isEmpty { + Text("No Data for this timeframe!").font(.largeTitle) + Text("Please make sure the data collection app is running! For more details please check out the documentation under:") + Link(destination: URL(string: "https://github.com/green-coding-berlin/hog#power-logger")!) { + Text("https://github.com/green-coding-berlin/hog#power-logger") } - Button(action: { - exit(0) - }) { - Image(systemName: "x.circle") + }else{ + HStack { + VStack(alignment: .leading, spacing: 8) { + Text("This is a very minimalistic overview of your energy usage.") + } + + Spacer(minLength: 10) + + Button(action: { + self.refresh() + }) { + Image(systemName: "goforward") + } + Button(action: { + exit(0) + }) { + Image(systemName: "x.circle") + } + } - - } - VStack{ - - VStack(spacing: 0) { - if valueManager.isLoading { - Text("Loading") - } else { - ProcessBadge(title: "App with the highest energy usage", color: Color("chartColor2"), process: valueManager.topApp) - EnergyBadge(title: "System energy usage", color: Color("chartColor2"), image: "clock.badge.checkmark", value: valueManager.energy) - if valueManager.providerRunning { - TextBadge(title: "", color: Color("chartColor2"), image: "checkmark.seal", value: "All measurement systems are functional") + VStack{ + + VStack(spacing: 0) { + if valueManager.isLoading { + Text("Loading") } else { - HStack{ - TextBadge(title: "", color: Color("redish"), image: "exclamationmark.octagon", value: "Measurement systems not running!") - Link(destination: URL(string: "https://github.com/green-coding-berlin/hog#power-logger")!) { - Image(systemName: "questionmark.circle.fill") - .font(.system(size: 24)) + ProcessBadge(title: "App with the highest energy usage", color: Color("chartColor2"), process: valueManager.topApp) + EnergyBadge(title: "System energy usage", color: Color("chartColor2"), image: "clock.badge.checkmark", value: valueManager.energy) + if valueManager.providerRunning { + TextBadge(title: "", color: Color("chartColor2"), image: "checkmark.seal", value: "All measurement systems are functional") + } else { + HStack{ + TextBadge(title: "", color: Color("redish"), image: "exclamationmark.octagon", value: "Measurement systems not running!") + Link(destination: URL(string: "https://github.com/green-coding-berlin/hog#power-logger")!) { + Image(systemName: "questionmark.circle.fill") + .font(.system(size: 24)) + } } } + // HStack{ + // TextBadge(title: "", color: Color("menuTab"), image: "person.crop.circle.badge.clock", value: "Project: Hog Development") + // Button(action: { + // isTextInputViewPresented = true + // }) { + // Image(systemName: "pencil.circle") + // } + // + // } + // .sheet(isPresented: $isTextInputViewPresented) { + // TextInputView(text: $text, isPresented: $isTextInputViewPresented) + // } } -// HStack{ -// TextBadge(title: "", color: Color("menuTab"), image: "person.crop.circle.badge.clock", value: "No project set") -// Button(action: { -// isTextInputViewPresented = true -// }) { -// Image(systemName: "pencil.circle") -// } -// } -// .sheet(isPresented: $isTextInputViewPresented) { -// TextInputView(text: $text, isPresented: $isTextInputViewPresented) -// } - } - Button(action: { - if let url = URL(string: "\(settingsManager.web_url)\(settingsManager.machine_uuid)") { - NSWorkspace.shared.open(url) + Button(action: { + if let url = URL(string: "\(settingsManager.web_url)\(settingsManager.machine_uuid)") { + NSWorkspace.shared.open(url) + } + }) { + Text("View Detailed Analytics") + .padding(10) } - }) { - Text("View Detailed Analytics") - .padding(10) + + } - - + + PointsGraph(chartData: chartData) + TopProcessTable(tpData: lineData) + } + } - PointsGraph(chartData: chartData) - TopProcessTable(tpData: lineData) - + } + .padding() + .onReceive(viewModel.$selectedTab) { tab in + if tab == self.whoAmI { + self.refresh() } + } - }.padding() } } @@ -803,78 +733,17 @@ func TextBadge(title: String, color: Color, image: String, value: String)->some .frame(maxWidth: .infinity, alignment: .leading) } -struct CopyPasteTextField: NSViewRepresentable { - @Binding var text: String - - func makeNSView(context: Context) -> NSTextField { - let textField = NSTextField() - textField.isBordered = true - textField.backgroundColor = NSColor.textBackgroundColor - return textField - } - - func updateNSView(_ nsView: NSTextField, context: Context) { - nsView.stringValue = text - } -} -struct OneView: View { +class WindowFocusTracker: ObservableObject { + @Published var isKeyWindow: Bool = false + private var cancellables: Set = [] - var body: some View { - Text("1) Open Terminal").font(.headline) - HStack{ - Button(action: { - let workspace = NSWorkspace.shared - if let url = URL(string: "file:///System/Applications/Utilities/Terminal.app") { - let configuration = NSWorkspace.OpenConfiguration() - workspace.open(url, configuration: configuration, completionHandler: nil) - } - }) { - HStack { - Image(systemName: "terminal") // This is a symbolic representation, actual symbol might differ. - Text("Terminal") - } - } - - Text("If the button does not work please look under the Utilities folder in your Apps and start the terminal.") - }.padding() - } -} - -struct TwoView: View { - @State private var text = "curl -fsSL https://raw.githubusercontent.com/green-coding-berlin/hog/main/install.sh | sudo bash" - - var body: some View { - Text("2) Run this command").font(.headline) - - HStack(spacing: 20) { - CopyPasteTextField(text: $text) - - Button("Copy Text") { - let pasteboard = NSPasteboard.general - pasteboard.clearContents() - pasteboard.setString(text, forType: .string) - } - }.padding() - } -} -struct ThreeView: View { - @State private var showInfo: Bool = false - - var body: some View { - VStack(alignment: .leading, spacing: 10) { - HStack { - Text("3) You will need to enter your password and install xcode tools.").font(.headline) - Button(action: { - showInfo.toggle() - }) { - Image(systemName: "info.circle") - } - } - if showInfo { - Text("We are very aware that this is a risky operation. The problem is that the program needs to run as root and also the installer needs to activate the program.") + init() { + NSApplication.shared.publisher(for: \.keyWindow) + .sink { [weak self] keyWindow in + self?.isKeyWindow = (keyWindow != nil) } - } + .store(in: &cancellables) } } @@ -883,7 +752,7 @@ enum TabSelection: Hashable { } -class InstallViewModel: ObservableObject { +class ViewModel: ObservableObject { @Published var renderToggle: Bool = false @Published var selectedTab: TabSelection = .last5Minutes @@ -892,148 +761,53 @@ class InstallViewModel: ObservableObject { } } -struct StepsView: View { - var body: some View { - HStack{ - Image("logo2") - .resizable() - .scaledToFit() - .frame(width: 50, height: 50) - Text("Welcome to the hog").font(.title) - Spacer() - Button(action: { - exit(0) - }) { - Image(systemName: "x.circle") - } - - } - Text("It looks like you haven't installed the program that we need to collect the power measurments. Please follow these steps:") - Divider() - OneView() - TwoView() - ThreeView() - Text("4) All done. Now check").font(.headline) - - } -} - - -struct InstallView: View { - @ObservedObject var viewModel: InstallViewModel - - @State private var showingAlert = false - - var body: some View { - VStack(alignment: .leading, spacing: 10) { // Set alignment to .leading - - StepsView() - Button("Re-check if the power data is reported") { - viewModel.toggleRender() - - }.padding() - Divider() - Text("If you just want to see the interface you can also load some demo data visualises what is possible with the hog.") - Button("View with demo data") { - guard let sourceURL = Bundle.main.url(forResource: "demo_db", withExtension: "db") else { - print("Source file not found!") - return - } - db_path = sourceURL.path() - viewModel.selectedTab = .allTime - viewModel.toggleRender() - } - .alert(isPresented: $showingAlert) { - Alert(title: Text("There was an error copying demo data."), - message: Text("Please look at the logs and submit an issue! https://github.com/green-coding-berlin/hog/issues/new"), - dismissButton: .default(Text("Got it!"))) - } - Divider() - Text("You can also read our documentation for all the details under: https://github.com/green-coding-berlin/hog") - - }.padding() - - } - -} - -private struct SettingDetailView: View { - let title: String - let value: String - - var body: some View { - Group { - Text(title) - .bold() - Text(value) - .padding(.bottom, 10) - } - } -} -struct SettingsView: View { - - @ObservedObject var settingsManager = SettingsManager() - - var body: some View { - VStack(alignment: .leading) { - Text("Settings") - .font(.headline) - .bold() - Text("These are the settings that are set by the power logger.\nPlease refer to https://github.com/green-coding-berlin/hog#settings") - Divider().padding() - SettingDetailView(title: "Machine ID:", value: settingsManager.machine_uuid) - SettingDetailView(title: "Powermetrics Intervall:", value: "\(settingsManager.powermetrics)") - SettingDetailView(title: "Upload to URL:", value: settingsManager.api_url) - SettingDetailView(title: "Web View URL:", value: settingsManager.web_url) - SettingDetailView(title: "Upload data:", value: settingsManager.upload_data ? "Yes" : "No") - SettingDetailView(title: "Upload Backlog Count:", value: "\(settingsManager.upload_backlog)") - - } - .padding() - } -} - - - - struct DetailView: View { - - @ObservedObject var viewModel = InstallViewModel() + + @ObservedObject var windowFocusTracker = WindowFocusTracker() + @ObservedObject var viewModel = ViewModel() @Environment(\.colorScheme) var colorScheme var body: some View { - if checkDB() { - TabView(selection: $viewModel.selectedTab) { - DataView(lookBackTime: 300000) - .tabItem { - Label("Last 5 Minutes", systemImage: "list.dash") - } - .tag(TabSelection.last5Minutes) - - DataView(lookBackTime: 86400000) - .tabItem { - Label("Last 24 Hours", systemImage: "square.and.pencil") - } - .tag(TabSelection.last24Hours) - - DataView() - .tabItem { - Label("All Time", systemImage: "square.and.pencil") - } - .tag(TabSelection.allTime) - - SettingsView() - .tabItem { - Label("Settings", systemImage: "square.and.pencil") - } - .tag(TabSelection.settings) + if checkDB(){ + if windowFocusTracker.isKeyWindow{ + ReleaseCheckerView() + TabView(selection: $viewModel.selectedTab) { + DataView(lookBackTime: 300000, viewModel: viewModel, whoAmI: TabSelection.last5Minutes) + .tabItem { + Label("Last 5 Minutes", systemImage: "list.dash") + } + .tag(TabSelection.last5Minutes) + + + DataView(lookBackTime: 86400000, viewModel: viewModel, whoAmI: TabSelection.last24Hours) + .tabItem { + Label("Last 24 Hours", systemImage: "square.and.pencil") + } + .tag(TabSelection.last24Hours) + + + DataView(viewModel: viewModel, whoAmI: TabSelection.allTime) + .tabItem { + Label("All Time", systemImage: "square.and.pencil") + } + .tag(TabSelection.allTime) + + + SettingsView(viewModel: viewModel, whoAmI: TabSelection.settings) + .tabItem { + Label("Settings", systemImage: "square.and.pencil") + } + .tag(TabSelection.settings) + + } + .padding() + .background(colorScheme == .light ? Color.white : Color.black) + }else{ + Text("Please return focus to window to display data. You can do this by clicking here.") } - .padding() - .background(colorScheme == .light ? Color.white : Color.black) } else { InstallView(viewModel: viewModel) } - } } diff --git a/app/hog/hog/InstallView.swift b/app/hog/hog/InstallView.swift new file mode 100644 index 0000000..4419bee --- /dev/null +++ b/app/hog/hog/InstallView.swift @@ -0,0 +1,150 @@ +// +// InstallView.swift +// hog +// +// Created by Didi Hoffmann on 18.10.23. +// + +import SwiftUI + +struct OneView: View { + + var body: some View { + Text("1) Open Terminal").font(.headline) + HStack{ + Button(action: { + let workspace = NSWorkspace.shared + if let url = URL(string: "file:///System/Applications/Utilities/Terminal.app") { + let configuration = NSWorkspace.OpenConfiguration() + workspace.open(url, configuration: configuration, completionHandler: nil) + } + }) { + HStack { + Image(systemName: "terminal") // This is a symbolic representation, actual symbol might differ. + Text("Terminal") + } + } + + Text("If the button does not work please look under the Utilities folder in your Apps and start the terminal.") + }.padding() + } +} + +struct CopyPasteTextField: NSViewRepresentable { + @Binding var text: String + + func makeNSView(context: Context) -> NSTextField { + let textField = NSTextField() + textField.isBordered = true + textField.backgroundColor = NSColor.textBackgroundColor + return textField + } + + func updateNSView(_ nsView: NSTextField, context: Context) { + nsView.stringValue = text + } +} + + +struct TwoView: View { + @State private var text = "curl -fsSL https://raw.githubusercontent.com/green-coding-berlin/hog/main/install.sh | sudo bash" + + var body: some View { + Text("2) Run this command").font(.headline) + + HStack(spacing: 20) { + CopyPasteTextField(text: $text) + + Button("Copy Text") { + let pasteboard = NSPasteboard.general + pasteboard.clearContents() + pasteboard.setString(text, forType: .string) + } + }.padding() + } +} +struct ThreeView: View { + @State private var showInfo: Bool = false + + var body: some View { + VStack(alignment: .leading, spacing: 10) { + HStack { + Text("3) You will need to enter your password and install xcode tools.").font(.headline) + Button(action: { + showInfo.toggle() + }) { + Image(systemName: "info.circle") + } + } + if showInfo { + Text("We are very aware that this is a risky operation. The problem is that the program needs to run as root and also the installer needs to activate the program.") + } + } + } +} + + +struct StepsView: View { + var body: some View { + HStack{ + Image("logo2") + .resizable() + .scaledToFit() + .frame(width: 50, height: 50) + Text("Welcome to the hog").font(.title) + Spacer() + Button(action: { + exit(0) + }) { + Image(systemName: "x.circle") + } + + } + Text("It looks like you haven't installed the program that we need to collect the power measurments. Please follow these steps:") + Divider() + OneView() + TwoView() + ThreeView() + Text("4) All done. Now check").font(.headline) + + } +} + + +struct InstallView: View { + @ObservedObject var viewModel: ViewModel + + @State private var showingAlert = false + + var body: some View { + VStack(alignment: .leading, spacing: 10) { // Set alignment to .leading + + StepsView() + Button("Re-check if the power data is reported") { + viewModel.toggleRender() + + }.padding() + Divider() + Text("If you just want to see the interface you can also load some demo data visualises what is possible with the hog.") + Button("View with demo data") { + guard let sourceURL = Bundle.main.url(forResource: "demo_db", withExtension: "db") else { + print("Source file not found!") + return + } + db_path = sourceURL.path() + viewModel.selectedTab = .allTime + viewModel.toggleRender() + } + .alert(isPresented: $showingAlert) { + Alert(title: Text("There was an error copying demo data."), + message: Text("Please look at the logs and submit an issue! https://github.com/green-coding-berlin/hog/issues/new"), + dismissButton: .default(Text("Got it!"))) + } + Divider() + Text("You can also read our documentation for all the details under: https://github.com/green-coding-berlin/hog") + + }.padding() + + } + +} diff --git a/app/hog/hog/SettingsView.swift b/app/hog/hog/SettingsView.swift new file mode 100644 index 0000000..ccfd016 --- /dev/null +++ b/app/hog/hog/SettingsView.swift @@ -0,0 +1,165 @@ +// +// SettingsView.swift +// hog +// +// Created by Didi Hoffmann on 18.10.23. +// + +import SwiftUI +import SQLite3 +import Charts +import AppKit + + +class SettingsManager: ObservableObject { + + @Published var machine_uuid: String = "Loading ..." + @Published var powermetrics: Int = 0 + @Published var api_url: String = "Loading ..." + @Published var web_url: String = "Loading ..." + @Published var upload_data: Bool = true + + @Published var upload_backlog: Int = 0 + + @Published var isLoading: Bool = false + + + init(){ + self.isLoading = true + + DispatchQueue.global(qos: .userInitiated).async { + self.loadDataFrom() + DispatchQueue.main.async { + self.isLoading = false + } + } + } + + func loadDataFrom() { + var db: OpaquePointer? + + if sqlite3_open(db_path, &db) != SQLITE_OK { // Open database + print("error opening database") + return + } + + let lastMeasurementQuery = "SELECT machine_uuid, powermetrics, api_url, web_url, upload_data FROM settings ORDER BY time DESC LIMIT 1;" + var queryStatement: OpaquePointer? + + var new_machine_uuid = "Loading ..." + var new_powermetrics: Int = 0 + var new_api_url = "Loading ..." + var new_web_url = "Loading ..." + var upload_data = true + + if sqlite3_prepare_v2(db, lastMeasurementQuery, -1, &queryStatement, nil) == SQLITE_OK { + if sqlite3_step(queryStatement) == SQLITE_ROW { + new_machine_uuid = String(cString: sqlite3_column_text(queryStatement, 0)) + new_powermetrics = Int(sqlite3_column_int(queryStatement, 1)) + new_api_url = String(cString: sqlite3_column_text(queryStatement, 2)) + new_web_url = String(cString: sqlite3_column_text(queryStatement, 3)) + upload_data = sqlite3_column_int(queryStatement, 4) != 0 // assuming it's stored as 0 for false, non-0 for true + } + sqlite3_finalize(queryStatement) + } + + let uploadCountQuery = "SELECT COUNT(*) FROM measurements WHERE uploaded = 0;" + var new_upload_backlog: Int = 0 + + if sqlite3_prepare_v2(db, uploadCountQuery, -1, &queryStatement, nil) == SQLITE_OK { + if sqlite3_step(queryStatement) == SQLITE_ROW { + new_upload_backlog = Int(sqlite3_column_int(queryStatement, 0)) + } + sqlite3_finalize(queryStatement) // Always finalize your statement when done + } else { + print("SELECT statement could not be prepared") + } + + sqlite3_close(db) + + DispatchQueue.main.async { + self.machine_uuid = new_machine_uuid + self.powermetrics = new_powermetrics + self.api_url = new_api_url + self.web_url = new_web_url + self.upload_data = upload_data + self.upload_backlog = new_upload_backlog + } + + } + +} + +private struct SettingDetailView: View { + let title: String + let value: String + + var body: some View { + Group { + Text(title) + .bold() + Text(value) + .padding(.bottom, 10) + } + } +} + + +struct SettingsView: View { + + @ObservedObject var settingsManager = SettingsManager() + var viewModel: ViewModel + var whoAmI: TabSelection + + init(viewModel: ViewModel, whoAmI: TabSelection){ + self.viewModel = viewModel + self.whoAmI = whoAmI + } + + var body: some View { + VStack(alignment: .leading) { + HStack { + VStack(alignment: .leading, spacing: 8) { + Text("Settings") + .font(.headline) + .bold() + } + + Spacer(minLength: 10) + + Button(action: { + self.settingsManager.loadDataFrom() + }) { + Image(systemName: "goforward") + } + Button(action: { + exit(0) + }) { + Image(systemName: "x.circle") + } + + } + + if settingsManager.isLoading { + Text("Loading") + } else { + Text("These are the settings that are set by the power logger.\nPlease refer to https://github.com/green-coding-berlin/hog#settings") + Divider().padding() + SettingDetailView(title: "Machine ID:", value: settingsManager.machine_uuid) + SettingDetailView(title: "Powermetrics Intervall:", value: "\(settingsManager.powermetrics)") + SettingDetailView(title: "Upload to URL:", value: settingsManager.api_url) + SettingDetailView(title: "Web View URL:", value: settingsManager.web_url) + SettingDetailView(title: "Upload data:", value: settingsManager.upload_data ? "Yes" : "No") + SettingDetailView(title: "Upload Backlog Count:", value: "\(settingsManager.upload_backlog)") + } + + } + .padding() + .onReceive(viewModel.$selectedTab) { tab in + if tab == self.whoAmI { + self.settingsManager.loadDataFrom() + } + } + + } +} diff --git a/app/hog/hog/UpdateView.swift b/app/hog/hog/UpdateView.swift new file mode 100644 index 0000000..944d592 --- /dev/null +++ b/app/hog/hog/UpdateView.swift @@ -0,0 +1,55 @@ +// +// UpdateView.swift +// hog +// +// Created by Didi Hoffmann on 19.10.23. +// + +import SwiftUI +import Combine + +struct ReleaseCheckerView: View { + @State private var hasNewRelease: Bool = false + @State private var latestVersion: String = "" + + let currentVersion = (Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String) ?? "0.1" + + var body: some View { + VStack { + if hasNewRelease { + Text("A new Hog version is available. Please update!") + Text("See https://github.com/green-coding-berlin/hog/blob/main/README.md#updating") + } + } + .task { + await checkLatestRelease() + } + } + + func fetchLatestRelease() async throws -> GitHubRelease? { + let url = URL(string: "https://api.github.com/repos/green-coding-berlin/hog/releases/latest")! + + let (data, response) = try await URLSession.shared.data(from: url) + + guard let httpResponse = response as? HTTPURLResponse, httpResponse.statusCode == 200 else { + return nil + } + + return try JSONDecoder().decode(GitHubRelease.self, from: data) + } + + func checkLatestRelease() async { + if let releaseData = try? await fetchLatestRelease() { + self.latestVersion = releaseData.tagName + self.hasNewRelease = self.latestVersion > self.currentVersion + } + } +} + +struct GitHubRelease: Decodable { + let tagName: String + + enum CodingKeys: String, CodingKey { + case tagName = "tag_name" + } +} diff --git a/app/hog/hog/hog.entitlements b/app/hog/hog/hog.entitlements index 852fa1a..ee95ab7 100644 --- a/app/hog/hog/hog.entitlements +++ b/app/hog/hog/hog.entitlements @@ -4,5 +4,7 @@ com.apple.security.app-sandbox + com.apple.security.network.client + diff --git a/app/hog/hog/hogApp.swift b/app/hog/hog/hogApp.swift index a77be75..ba40e45 100644 --- a/app/hog/hog/hogApp.swift +++ b/app/hog/hog/hogApp.swift @@ -9,12 +9,12 @@ import SwiftUI @main struct hogApp: App { - var body: some Scene { - - MenuBarExtra("QuickView", image: "logo") { - DetailView().frame( - minWidth: 600, minHeight: 850) + @State var observer: NSKeyValueObservation? + var body: some Scene { + MenuBarExtra("QuickView", image: "logo_bw_bar") { + DetailView() + .frame(minWidth: 600, minHeight: 850) }.menuBarExtraStyle(WindowMenuBarExtraStyle()) } } diff --git a/app/hog/widget/AppIntent.swift b/app/hog/widget/AppIntent.swift deleted file mode 100644 index b6c1cb1..0000000 --- a/app/hog/widget/AppIntent.swift +++ /dev/null @@ -1,18 +0,0 @@ -// -// AppIntent.swift -// widget -// -// Created by Didi Hoffmann on 29.09.23. -// - -import WidgetKit -import AppIntents - -struct ConfigurationAppIntent: WidgetConfigurationIntent { - static var title: LocalizedStringResource = "Configuration" - static var description = IntentDescription("This is an example widget.") - - // An example configurable parameter. - @Parameter(title: "Favorite Emoji", default: "😃") - var favoriteEmoji: String -} diff --git a/app/hog/widget/Assets.xcassets/AccentColor.colorset/Contents.json b/app/hog/widget/Assets.xcassets/AccentColor.colorset/Contents.json deleted file mode 100644 index eb87897..0000000 --- a/app/hog/widget/Assets.xcassets/AccentColor.colorset/Contents.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "colors" : [ - { - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/app/hog/widget/Assets.xcassets/AppIcon.appiconset/Contents.json b/app/hog/widget/Assets.xcassets/AppIcon.appiconset/Contents.json deleted file mode 100644 index 3f00db4..0000000 --- a/app/hog/widget/Assets.xcassets/AppIcon.appiconset/Contents.json +++ /dev/null @@ -1,58 +0,0 @@ -{ - "images" : [ - { - "idiom" : "mac", - "scale" : "1x", - "size" : "16x16" - }, - { - "idiom" : "mac", - "scale" : "2x", - "size" : "16x16" - }, - { - "idiom" : "mac", - "scale" : "1x", - "size" : "32x32" - }, - { - "idiom" : "mac", - "scale" : "2x", - "size" : "32x32" - }, - { - "idiom" : "mac", - "scale" : "1x", - "size" : "128x128" - }, - { - "idiom" : "mac", - "scale" : "2x", - "size" : "128x128" - }, - { - "idiom" : "mac", - "scale" : "1x", - "size" : "256x256" - }, - { - "idiom" : "mac", - "scale" : "2x", - "size" : "256x256" - }, - { - "idiom" : "mac", - "scale" : "1x", - "size" : "512x512" - }, - { - "idiom" : "mac", - "scale" : "2x", - "size" : "512x512" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/app/hog/widget/Assets.xcassets/Contents.json b/app/hog/widget/Assets.xcassets/Contents.json deleted file mode 100644 index 73c0059..0000000 --- a/app/hog/widget/Assets.xcassets/Contents.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/app/hog/widget/Assets.xcassets/WidgetBackground.colorset/Contents.json b/app/hog/widget/Assets.xcassets/WidgetBackground.colorset/Contents.json deleted file mode 100644 index eb87897..0000000 --- a/app/hog/widget/Assets.xcassets/WidgetBackground.colorset/Contents.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "colors" : [ - { - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/app/hog/widget/Info.plist b/app/hog/widget/Info.plist deleted file mode 100644 index 0f118fb..0000000 --- a/app/hog/widget/Info.plist +++ /dev/null @@ -1,11 +0,0 @@ - - - - - NSExtension - - NSExtensionPointIdentifier - com.apple.widgetkit-extension - - - diff --git a/app/hog/widget/widget.entitlements b/app/hog/widget/widget.entitlements deleted file mode 100644 index 852fa1a..0000000 --- a/app/hog/widget/widget.entitlements +++ /dev/null @@ -1,8 +0,0 @@ - - - - - com.apple.security.app-sandbox - - - diff --git a/app/hog/widget/widget.swift b/app/hog/widget/widget.swift deleted file mode 100644 index 4c9a719..0000000 --- a/app/hog/widget/widget.swift +++ /dev/null @@ -1,63 +0,0 @@ -// -// widget.swift -// widget -// -// Created by Didi Hoffmann on 29.09.23. -// - -import WidgetKit -import SwiftUI - -struct Provider: AppIntentTimelineProvider { - func placeholder(in context: Context) -> SimpleEntry { - SimpleEntry(date: Date(), configuration: ConfigurationAppIntent()) - } - - func snapshot(for configuration: ConfigurationAppIntent, in context: Context) async -> SimpleEntry { - SimpleEntry(date: Date(), configuration: configuration) - } - - func timeline(for configuration: ConfigurationAppIntent, in context: Context) async -> Timeline { - var entries: [SimpleEntry] = [] - - // Generate a timeline consisting of five entries an hour apart, starting from the current date. - let currentDate = Date() - for hourOffset in 0 ..< 5 { - let entryDate = Calendar.current.date(byAdding: .hour, value: hourOffset, to: currentDate)! - let entry = SimpleEntry(date: entryDate, configuration: configuration) - entries.append(entry) - } - - return Timeline(entries: entries, policy: .atEnd) - } -} - -struct SimpleEntry: TimelineEntry { - let date: Date - let configuration: ConfigurationAppIntent -} - -struct widgetEntryView : View { - var entry: Provider.Entry - - var body: some View { - VStack { - Text("Time:") - Text(entry.date, style: .time) - - Text("Favorite Emoji:") - Text(entry.configuration.favoriteEmoji) - } - } -} - -struct widget: Widget { - let kind: String = "widget" - - var body: some WidgetConfiguration { - AppIntentConfiguration(kind: kind, intent: ConfigurationAppIntent.self, provider: Provider()) { entry in - widgetEntryView(entry: entry) - .containerBackground(.fill.tertiary, for: .widget) - } - } -} diff --git a/app/hog/widget/widgetBundle.swift b/app/hog/widget/widgetBundle.swift deleted file mode 100644 index 79d5c4a..0000000 --- a/app/hog/widget/widgetBundle.swift +++ /dev/null @@ -1,16 +0,0 @@ -// -// widgetBundle.swift -// widget -// -// Created by Didi Hoffmann on 29.09.23. -// - -import WidgetKit -import SwiftUI - -@main -struct widgetBundle: WidgetBundle { - var body: some Widget { - widget() - } -} diff --git a/install.sh b/install.sh old mode 100644 new mode 100755 index 074962c..f1b2a00 --- a/install.sh +++ b/install.sh @@ -33,6 +33,7 @@ install_xcode_clt() { # Call the function to ensure Xcode Command Line Tools are installed install_xcode_clt +# Checks if the hog is already running hog_running_output=$(launchctl list | grep berlin.green-coding.hog || echo "") if [[ ! -z "$hog_running_output" ]]; then @@ -40,6 +41,10 @@ if [[ ! -z "$hog_running_output" ]]; then rm -f /tmp/latest_release.zip fi +### +# Downloads and moves the code +### + ZIP_LOCATION=$(curl -s https://api.github.com/repos/green-coding-berlin/hog/releases/latest | grep -o 'https://[^"]*/hog_power_logger.zip') curl -fLo /tmp/latest_release.zip $ZIP_LOCATION @@ -52,6 +57,36 @@ chmod 755 /usr/local/bin/hog chmod -R 755 /usr/local/bin/hog/ chmod +x /usr/local/bin/hog/power_logger.py +### +# Writing the config file +### + +read -p "In order for the app to work with all features please allow us to upload some data. [Y/n]: " upload_data +upload_data=${upload_data:-Y} +upload_data=$(echo "$upload_data" | tr '[:upper:]' '[:lower:]') + +if [[ $upload_data == "y" || $upload_data == "yes" ]]; then + upload_flag="true" +else + upload_flag="false" +fi + +cat > /etc/hog_settings.ini << EOF +[DEFAULT] +api_url = https://api.green-coding.berlin/v1/hog/add +web_url = https://metrics.green-coding.berlin/hog-details.html?machine_uuid= +upload_delta = 300 +powermetrics = 5000 +upload_data = $upload_flag +resolve_coalitions=com.googlecode.iterm2,com.apple.Terminal,com.vix.cron +EOF + +echo "Configuration written to /etc/hog_settings.ini" + +### +# Setting up the background demon +### + mv -f /usr/local/bin/hog/berlin.green-coding.hog.plist /Library/LaunchDaemons/berlin.green-coding.hog.plist sed -i '' "s|PATH_PLEASE_CHANGE|/usr/local/bin/hog/|g" /Library/LaunchDaemons/berlin.green-coding.hog.plist diff --git a/power_logger.py b/power_logger.py index d1fb2b1..80c8964 100755 --- a/power_logger.py +++ b/power_logger.py @@ -19,15 +19,21 @@ import configparser import sqlite3 import http +import threading +import logging +import select + from datetime import timezone from pathlib import Path from libs import caribou -VERSION = '0.2.1' +VERSION = '0.3' + +LOG_LEVELS = ['debug', 'info', 'warning', 'error', 'critical'] # Shared variable to signal the thread to stop -stop_signal = False +stop_signal = threading.Event() stats = { 'combined_energy': 0, @@ -39,16 +45,17 @@ def sigint_handler(_, __): global stop_signal - if stop_signal: + if stop_signal.is_set(): # If you press CTR-C the second time we bail - sys.exit() + sys.exit(2) - stop_signal = True - print('Received stop signal. Terminating all processes.') + stop_signal.set() + logging.info('❗ Terminating all processes. Please be patient, this might take a few seconds.') def siginfo_handler(_, __): print(SETTINGS) print(stats) + logging.info(f"System stats:\n{stats}\n{SETTINGS}") signal.signal(signal.SIGINT, sigint_handler) signal.signal(signal.SIGTERM, sigint_handler) @@ -70,6 +77,7 @@ def siginfo_handler(_, __): 'api_url': 'https://api.green-coding.berlin/v1/hog/add', 'web_url': 'https://metrics.green-coding.berlin/hog-details.html?machine_uuid=', 'upload_data': True, + 'resolve_coalitions': ['com.googlecode.iterm2,com.apple.Terminal,com.vix.cron'] } script_dir = os.path.dirname(os.path.realpath(__file__)) @@ -92,74 +100,98 @@ def siginfo_handler(_, __): 'api_url': config['DEFAULT'].get('api_url', default_settings['api_url']), 'web_url': config['DEFAULT'].get('web_url', default_settings['web_url']), 'upload_data': bool(config['DEFAULT'].getboolean('upload_data', default_settings['upload_data'])), + 'resolve_coalitions': config['DEFAULT'].get('resolve_coalitions', default_settings['resolve_coalitions']), } + SETTINGS['resolve_coalitions'] = [x.strip().lower() for x in SETTINGS['resolve_coalitions'].split(',')] else: SETTINGS = default_settings - - machine_uuid = None conn = sqlite3.connect(DATABASE_FILE) c = conn.cursor() +# This is a replacement for time.sleep as we need to check periodically if we need to exit +# We choose a max exit time of one second as we don't want to wake up too often. +def sleeper(stop_event, duration): + end_time = time.time() + duration + while time.time() < end_time: + if stop_event.is_set(): + return + time.sleep(1) -def run_powermetrics(debug: bool, filename: str = None): - - def process_lines(lines, debug): - buffer = [] - last_upload_time = time.time() - for line in lines: - line = line.strip().replace('&', '&') - buffer.append(line) - if line == '': - parse_powermetrics_output(''.join(buffer)) - if debug: - print(stats) - sys.stdout.flush() +def run_powermetrics(local_stop_signal, filename: str = None): + buffer = [] - buffer = [] + def process_line(line, buffer): + line = line.strip().replace('&', '&') + buffer.append(line) - if SETTINGS['upload_data']: - current_time = time.time() - if current_time - last_upload_time >= SETTINGS['upload_delta']: - upload_data_to_endpoint() - last_upload_time = current_time + if line == '': + logging.debug('Parsing new input') + parse_powermetrics_output(''.join(buffer)) + buffer.clear() - if stop_signal: - break + logging.info(stats) if filename: + logging.info(f"Reading file {filename}") with open(filename, 'r', encoding='utf-8') as file: - lines = file.readlines() - process_lines(lines, debug) + for line in file.readlines(): + process_line(line, buffer) + else: cmd = ['powermetrics', '--show-all', '-i', str(SETTINGS['powermetrics']), '-f', 'plist'] - with subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.DEVNULL, text=True) as process: - process_lines(process.stdout, debug) + logging.info(f"Starting powermetrics process: {' '.join(cmd)}") - if stop_signal: - process.terminate() - - # Make sure that all data has been uploaded when exiting - upload_data_to_endpoint() + with subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.DEVNULL, text=True) as process: -def upload_data_to_endpoint(): - retry_counter = 0 - while True: - retry_counter += 1 + os.set_blocking(process.stdout.fileno(), False) + + partial_buffer = '' + while not local_stop_signal.is_set(): + # Make sure that the timeout is greater than the output is coming in + rlist, _, _ = select.select([process.stdout], [], [], int(SETTINGS['powermetrics'] / 1_000 * 2 )) + if rlist: + # This is a little hacky. The problem is that select just reads data and doesn't respect the lines + # so it happens that we read in the middle of a line. + data = rlist[0].read() + data = partial_buffer + data + lines = data.splitlines() + try: + if not data.endswith('\n'): + partial_buffer = lines.pop() + else: + partial_buffer = '' + + for line in lines: + process_line(line, buffer) + except IndexError: + # This happens when the process is killed before we exit here so stop_signal should be set. If not + # there is a problem with powermetrics and we should report and exit. + if not local_stop_signal.is_set(): + logging.error('The pipe to powermetrics has been closed. Exiting') + local_stop_signal.set() + + +def upload_data_to_endpoint(local_stop_signal): + thread_conn = sqlite3.connect(DATABASE_FILE) + tc = thread_conn.cursor() + + while not local_stop_signal.is_set(): # We need to limit the amount of data here as otherwise the payload becomes to big - c.execute('SELECT id, time, data FROM measurements WHERE uploaded = 0 LIMIT 10;') - rows = c.fetchall() + tc.execute('SELECT id, time, data FROM measurements WHERE uploaded = 0 LIMIT 10;') + rows = tc.fetchall() - if not rows or retry_counter > 3: - retry_counter = 0 - break + # When everything is uploaded we sleep + if not rows: + sleeper(local_stop_signal, SETTINGS['upload_delta']) + continue payload = [] for row in rows: @@ -178,32 +210,44 @@ def upload_data_to_endpoint(): 'machine_uuid': machine_uuid, 'row_id': row_id }) + request_data = json.dumps(payload).encode('utf-8') req = urllib.request.Request(url=SETTINGS['api_url'], data=request_data, headers={'content-type': 'application/json'}, method='POST') + + logging.info(f"Uploading {len(payload)} rows to: {SETTINGS['api_url']}") + try: with urllib.request.urlopen(req) as response: if response.status == 204: for p in payload: - c.execute('UPDATE measurements SET uploaded = ?, data = NULL WHERE id = ?;', (int(time.time()), p['row_id'])) - conn.commit() + tc.execute('UPDATE measurements SET uploaded = ?, data = NULL WHERE id = ?;', (int(time.time()), p['row_id'])) + thread_conn.commit() + logging.debug('Upload 👌') else: - print(f"Failed to upload data: {payload}\n HTTP status: {response.status}") + logging.info(f"Failed to upload data: {payload}\n HTTP status: {response.status}") + sleeper(local_stop_signal, SETTINGS['upload_delta']) # Sleep if there is an error + except (urllib.error.HTTPError, ConnectionRefusedError, urllib.error.URLError, http.client.RemoteDisconnected, - ConnectionResetError): - break + ConnectionResetError) as exc: + logging.debug(f"Upload exception: {exc}") + sleeper(local_stop_signal, SETTINGS['upload_delta']) # Sleep if there is an error + + thread_conn.close() + + def find_top_processes(data: list, elapsed_ns:int): # As iterm2 will probably show up as it spawns the processes called from the shell we look at the tasks new_data = [] for coalition in data: - if coalition['name'] == 'com.googlecode.iterm2' or coalition['name'].strip() == '': + if coalition['name'].lower() in SETTINGS['resolve_coalitions'] or coalition['name'].strip() == '': new_data.extend(coalition['tasks']) else: new_data.append(coalition) @@ -232,7 +276,7 @@ def parse_powermetrics_output(output: str): data['timezone'] = time.tzname data['timestamp'] = int(data['timestamp'].replace(tzinfo=timezone.utc).timestamp() * 1e3) except xml.parsers.expat.ExpatError as exc: - print(data) + logging.error(f"XML Error:\n{data}") raise exc compressed_data = zlib.compress(str(json.dumps(data)).encode()) @@ -281,6 +325,7 @@ def parse_powermetrics_output(output: str): (data['timestamp'], process['name'], process['energy_impact'], cpu_per)) conn.commit() + logging.debug('Data added to the DB') def save_settings(): global machine_uuid @@ -296,7 +341,7 @@ def save_settings(): last_web_url.strip() == SETTINGS['web_url'].strip() and last_upload_delta == SETTINGS['upload_delta'] and last_upload_data == SETTINGS['upload_data']): - return + return False else: machine_uuid = str(uuid.uuid1()) @@ -313,19 +358,72 @@ def save_settings(): )) conn.commit() + logging.debug(f"Saved Settings:\n{SETTINGS}") + + return True + +def check_DB(local_stop_signal): + # The powermetrics script should return ever SETTINGS['powermetrics'] ms but because of the way we batch things + # we will not get values every n ms so we have quite a big value here. + # powermetrics = 5000 ms in production and 1000 in dev mode + + interval_sec = SETTINGS['powermetrics'] * 20 / 1_000 + + # We first sleep for quite some time to give the program some time to add data to the DB + sleeper(local_stop_signal, interval_sec) + + thread_conn = sqlite3.connect(DATABASE_FILE) + tc = thread_conn.cursor() + + while not local_stop_signal.is_set(): + n_ago = int((time.time() - interval_sec) * 1_000) + + tc.execute('SELECT MAX(time) FROM measurements') + result = tc.fetchone() + + if result and result[0]: + if result[0] < n_ago: + logging.error('No new data in DB. Exiting to be restarted by the os') + local_stop_signal.set() + else: + logging.error('We are not getting values from the DB for checker thread.') + + logging.debug('DB Check ✅') + sleeper(local_stop_signal, interval_sec) + + thread_conn.close() + + +def is_power_logger_running(): + try: + subprocess.check_output(['pgrep', '-f', sys.argv[0]]) + logging.error(f"There is already a {sys.argv[0]} process running! Maybe check launchctl?") + sys.exit(4) + except subprocess.CalledProcessError: + return False if __name__ == '__main__': parser = argparse.ArgumentParser(description= - '''A powermetrics wrapper that does simple parsing and writes to a file.''') - parser.add_argument('-d', '--debug', action='store_true', help='Enable debug/ development mode') + ''' + A power collection script that records a multitude of metrics and saves them to + a database. Also uploads the data to a server. + Exit codes: + 1 - run as root + 2 - force quit + 3 - db not updated + 4 - already a power_logger process is running + ''') + parser.add_argument('-d', '--dev', action='store_true', help='Enable development mode api endpoints and log level.') parser.add_argument('-w', '--website', action='store_true', help='Shows the website URL') parser.add_argument('-f', '--file', type=str, help='Path to the input file') + parser.add_argument('-v', '--log-level', choices=LOG_LEVELS, default='info', help='Logging level (debug, info, warning, error, critical)') + parser.add_argument('-o', '--output-file', type=str, help='Path to the output log file.') args = parser.parse_args() - if args.debug: + if args.dev: SETTINGS = { 'powermetrics' : 1000, 'upload_delta': 5, @@ -333,11 +431,25 @@ def save_settings(): 'web_url': 'http://metrics.green-coding.internal:9142/hog-details.html?machine_uuid=', 'upload_data': True, } + args.log_level = 'debug' + + log_level = getattr(logging, args.log_level.upper()) + + if args.output_file: + logging.basicConfig(filename=args.output_file, level=log_level, format='[%(levelname)s] %(asctime)s - %(message)s') + else: + logging.basicConfig(level=log_level, format='[%(levelname)s] %(asctime)s - %(message)s') + + logging.debug('Program started 🎉') + logging.debug(f"Using db: {DATABASE_FILE}") + if os.geteuid() != 0: - print('The script needs to be run as root!') + logging.error('The script needs to be run as root!') sys.exit(1) + is_power_logger_running() + # Make sure that everyone can write to the DB os.chmod(DATABASE_FILE, stat.S_IRUSR | stat.S_IWUSR | stat.S_IRGRP | stat.S_IWGRP | @@ -347,13 +459,24 @@ def save_settings(): # Make sure the DB is migrated caribou.upgrade(DATABASE_FILE, MIGRATIONS_PATH) - save_settings() + if not save_settings(): + logging.debug(f"Setting: {SETTINGS}") + if args.website: print('Please visit this url for detailed analytics:') print(f"{SETTINGS['web_url']}{machine_uuid}") - sys.exit() + sys.exit(0) + + if SETTINGS['upload_data']: + upload_thread = threading.Thread(target=upload_data_to_endpoint, args=(stop_signal,)) + upload_thread.start() + logging.debug('Upload thread started') + + db_checker_thread = threading.Thread(target=check_DB, args=(stop_signal,), daemon=True) + db_checker_thread.start() + logging.debug('DB checker thread started') - run_powermetrics(args.debug, args.file) + run_powermetrics(stop_signal, args.file) c.close() diff --git a/settings.ini b/settings.ini index 1c63343..b639bd8 100644 --- a/settings.ini +++ b/settings.ini @@ -4,3 +4,4 @@ web_url = https://metrics.green-coding.berlin/hog-details.html?machine_uuid= upload_delta = 300 powermetrics = 5000 upload_data = true +resolve_coalitions=com.googlecode.iterm2,com.apple.Terminal,com.vix.cron \ No newline at end of file