diff --git a/app/modules/battery.js b/app/modules/battery.js index 1629f49..32265e9 100644 --- a/app/modules/battery.js +++ b/app/modules/battery.js @@ -1,9 +1,9 @@ // Command line interactors const { exec } = require('node:child_process') const sudo = require( 'sudo-prompt' ) -const { log, alert } = require( './helpers' ) +const { log, alert, wait } = require( './helpers' ) const { USER } = process.env -const path_fix = 'PATH=$PATH:/bin:/usr/bin:/usr/local/bin:/usr/sbin:/opt/homebrew' +const path_fix = 'PATH=$PATH:/bin:/usr/bin:/usr/local/bin:/usr/sbin:/opt/homebrew:/usr/bin/' const battery = `${ path_fix } battery` const { app } = require( 'electron' ) const shell_options = { @@ -12,13 +12,13 @@ const shell_options = { } // Execute without sudo -const exec_async = command => new Promise( ( resolve, reject ) => { +const exec_async_no_timeout = command => new Promise( ( resolve, reject ) => { log( `Executing ${ command }` ) exec( command, shell_options, ( error, stdout, stderr ) => { - if( error ) return reject( error ) + if( error ) return reject( error, stderr, stdout ) if( stderr ) return reject( stderr ) if( stdout ) return resolve( stdout ) @@ -26,6 +26,13 @@ const exec_async = command => new Promise( ( resolve, reject ) => { } ) +const exec_async = ( command, timeout_in_ms=2000, throw_on_timeout=false ) => Promise.race( [ + exec_async_no_timeout( command ), + wait( timeout_in_ms ).then( () => { + if( throw_on_timeout ) throw new Error( `${ command } timed out` ) + } ) +] ) + // Execute with sudo const exec_sudo_async = async command => new Promise( async ( resolve, reject ) => { @@ -47,7 +54,10 @@ const exec_sudo_async = async command => new Promise( async ( resolve, reject ) const enable_battery_limiter = async () => { try { - await exec_async( `${ battery } maintain 80` ) + // Start battery maintainer + const status = await get_battery_status() + await exec_async( `${ battery } maintain ${ status?.maintain_percentage || 80 }` ) + log( `enable_battery_limiter exec complete` ) } catch( e ) { log( 'Error enabling battery: ', e ) alert( e.message ) @@ -70,6 +80,13 @@ const update_or_install_battery = async () => { try { + // Check for network + const online = await Promise.race( [ + exec_async( `${ path_fix } curl icanhasip.com &> /dev/null` ).then( () => true ).catch( () => false ), + exec_async( `${ path_fix } curl github.com &> /dev/null` ).then( () => true ).catch( () => false ) + ] ) + log( `Internet online: ${ online }` ) + // Check if xcode build tools are installed const xcode_installed = await exec_async( `${ path_fix } which git` ).catch( () => false ) if( !xcode_installed ) { @@ -96,8 +113,15 @@ const update_or_install_battery = async () => { const is_installed = battery_installed && smc_installed log( 'Is installed? ', is_installed ) + // Kill running instances of battery + const processes = await exec_async( `ps aux | grep "/usr/local/bin/battery " | wc -l | grep -Eo "\\d*"` ) + log( `Found ${ `${ processes }`.replace( /\n/, '' ) } battery related processed to kill` ) + await exec_async( `${ battery } maintain stop` ) + await exec_async( `pkill -f "/usr/local/bin/battery.*"` ).catch( e => log( `Error killing existing battery progesses, usually means no running processes` ) ) + // If installed, update if( is_installed && visudo_complete ) { + if( !online ) return log( `Skipping battery update because we are offline` ) log( `Updating battery...` ) const result = await exec_async( `${ battery } update silent` ) log( `Update result: `, result ) @@ -106,6 +130,7 @@ const update_or_install_battery = async () => { // If not installed, run install script if( !is_installed || !visudo_complete ) { log( `Installing battery for ${ USER }...` ) + if( !online ) return alert( `Battery needs an internet connection to download the latest version, please connect to the internet and open the app again.` ) await alert( `Welcome to the Battery limiting tool. The app needs to install/update some components, so it will ask for your password. This should only be needed once.` ) const result = await exec_sudo_async( `curl -s https://raw.githubusercontent.com/actuallymentor/battery/main/setup.sh | bash -s -- $USER` ) log( `Install result: `, result ) @@ -125,6 +150,7 @@ const is_limiter_enabled = async () => { try { const message = await exec_async( `${ battery } status` ) + log( `Limiter status message: `, message ) return message.includes( 'being maintained at' ) } catch( e ) { log( `Error getting battery status: `, e ) @@ -138,6 +164,8 @@ const get_battery_status = async () => { try { const message = await exec_async( `${ battery } status_csv` ) let [ percentage, remaining, charging, discharging, maintain_percentage ] = message.split( ',' ) + maintain_percentage = maintain_percentage.trim() + maintain_percentage = maintain_percentage.length ? maintain_percentage : undefined charging = charging == 'enabled' discharging = discharging == 'discharging' remaining = remaining.match( /\d{1,2}:\d{1,2}/ ) ? remaining : 'unknown' @@ -147,7 +175,7 @@ const get_battery_status = async () => { if( discharging ) daemon_state += `forcing discharge to 80%` else daemon_state += `smc charging ${ charging ? 'enabled' : 'disabled' }` - return [ battery_state, daemon_state ] + return [ battery_state, daemon_state, maintain_percentage ] } catch( e ) { log( `Error getting battery status: `, e ) diff --git a/app/modules/helpers.js b/app/modules/helpers.js index 6ab7ee8..97f5844 100644 --- a/app/modules/helpers.js +++ b/app/modules/helpers.js @@ -24,8 +24,12 @@ const log = async ( ...messages ) => { const { dialog } = require('electron') const alert = ( message ) => dialog.showMessageBox( { message } ) +const wait = time_in_ms => new Promise( resolve => { + setTimeout( resolve, time_in_ms ) +} ) module.exports = { log, - alert + alert, + wait } \ No newline at end of file diff --git a/app/modules/interface.js b/app/modules/interface.js index d4b334e..3abc0e6 100644 --- a/app/modules/interface.js +++ b/app/modules/interface.js @@ -1,6 +1,6 @@ const { shell, app, Tray, Menu } = require( 'electron' ) const { enable_battery_limiter, disable_battery_limiter, update_or_install_battery, is_limiter_enabled, get_battery_status } = require('./battery') -const { log } = require("./helpers") +const { log, wait } = require("./helpers") const { get_inactive_logo, get_active_logo } = require('./theme') /* /////////////////////////////// @@ -11,88 +11,102 @@ let tray = undefined // Set interface to usable const generate_app_menu = async () => { - // Get battery and daemon status - const [ battery_state, daemon_state ] = await get_battery_status() + try { + // Get battery and daemon status + const [ battery_state, daemon_state, maintain_percentage=80 ] = await get_battery_status() - // Check if limiter is on - const limiter_on = await is_limiter_enabled() + // Check if limiter is on + const limiter_on = await is_limiter_enabled() - // Set tray icon - tray.setImage( limiter_on ? get_active_logo() : get_inactive_logo() ) + // Set tray icon + tray.setImage( limiter_on ? get_active_logo() : get_inactive_logo() ) - // Build menu - return Menu.buildFromTemplate( [ + // Build menu + return Menu.buildFromTemplate( [ - { - label: 'Enable 80% battery limit', - type: 'radio', - checked: limiter_on, - click: enable_limiter - }, - { - sublabel: 'thing', - label: 'Disable 80% battery limit', - type: 'radio', - checked: !limiter_on, - click: disable_limiter - }, - { - type: 'separator' - }, - { - label: `Battery: ${ battery_state }`, - enabled: false - }, - { - label: `Power: ${ daemon_state }`, - enabled: false - }, - { - type: 'separator' - }, - { - label: `About v${ app.getVersion() }`, - submenu: [ - { - label: `Check for updates`, - click: () => shell.openExternal( `https://github.com/actuallymentor/battery/releases` ) - }, - { - label: `User manual`, - click: () => shell.openExternal( `https://github.com/actuallymentor/battery#readme` ) - }, - { - type: 'normal', - label: 'Command-line usage', - click: () => shell.openExternal( `https://github.com/actuallymentor/battery#-command-line-version` ) - }, - { - type: 'normal', - label: 'Help and feature requests', - click: () => shell.openExternal( `https://github.com/actuallymentor/battery/issues` ) + { + label: `Enable ${ maintain_percentage }% battery limit`, + type: 'radio', + checked: limiter_on, + click: enable_limiter + }, + { + sublabel: 'thing', + label: `Disable ${ maintain_percentage }% battery limit`, + type: 'radio', + checked: !limiter_on, + click: disable_limiter + }, + { + type: 'separator' + }, + { + label: `Battery: ${ battery_state }`, + enabled: false + }, + { + label: `Power: ${ daemon_state }`, + enabled: false + }, + { + type: 'separator' + }, + { + label: `About v${ app.getVersion() }`, + submenu: [ + { + label: `Check for updates`, + click: () => shell.openExternal( `https://github.com/actuallymentor/battery/releases` ) + }, + { + label: `User manual`, + click: () => shell.openExternal( `https://github.com/actuallymentor/battery#readme` ) + }, + { + type: 'normal', + label: 'Command-line usage', + click: () => shell.openExternal( `https://github.com/actuallymentor/battery#-command-line-version` ) + }, + { + type: 'normal', + label: 'Help and feature requests', + click: () => shell.openExternal( `https://github.com/actuallymentor/battery/issues` ) + } + ] + }, + { + label: 'Quit', + click: () => { + tray.destroy() + app.quit() } - ] - }, - { - label: 'Quit', - click: () => { - tray.destroy() - app.quit() } - } - - ] ) + + ] ) + } catch( e ) { + log( `Error generating menu: `, e ) + } } // Refresh tray with battery status values -const refresh_tray = async () => { +const refresh_tray = async ( force_interactive_refresh = false ) => { log( "Refreshing tray icon..." ) - tray.setContextMenu( await generate_app_menu() ) + const new_menu = await generate_app_menu() + if( force_interactive_refresh ) { + log( `Forcing interactive refresh ${ force_interactive_refresh }` ) + tray.closeContextMenu() + tray.popUpContextMenu( new_menu ) + } + tray.setContextMenu( new_menu ) } // Refresh app logo -const refresh_logo = async () => { +const refresh_logo = async ( force ) => { + + if( force == 'active' ) return tray.setImage( get_active_logo() ) + if( force == 'inactive' ) return tray.setImage( get_inactive_logo() ) + const is_enabled = await is_limiter_enabled() if( is_enabled ) return tray.setImage( get_active_logo() ) return tray.setImage( get_inactive_logo() ) @@ -114,7 +128,10 @@ async function set_initial_interface() { log( "Triggering boot-time auto-update" ) await update_or_install_battery() - log( "Update process complete" ) + log( "App initialisation process complete" ) + + // Start battery handler + await enable_battery_limiter() // Set tray styles @@ -122,7 +139,8 @@ async function set_initial_interface() { await refresh_tray() // Set tray open listener - tray.on( 'mouse-enter', refresh_tray ) + tray.on( 'mouse-enter', () => refresh_tray() ) + tray.on( 'click', () => refresh_tray() ) } @@ -132,19 +150,27 @@ async function set_initial_interface() { // /////////////////////////////*/ async function enable_limiter() { - log( 'Enable limiter' ) - await enable_battery_limiter() - await refresh_tray() - await refresh_logo() + try { + log( 'Enable limiter' ) + await refresh_logo( 'active' ) + await enable_battery_limiter() + await refresh_tray() + } catch( e ) { + log( `Error in enable_limiter: `, e ) + } } async function disable_limiter() { - log( 'Disable limiter' ) - await disable_battery_limiter() - await refresh_tray() - await refresh_logo() + try { + log( 'Disable limiter' ) + await refresh_logo( 'inactive' ) + await disable_battery_limiter() + await refresh_tray() + } catch( e ) { + log( `Error in disable_limiter: `, e ) + } } diff --git a/app/package.json b/app/package.json index 57a3394..e390fac 100644 --- a/app/package.json +++ b/app/package.json @@ -1,7 +1,7 @@ { "name": "battery", "version": "1.0.5", - "description": "A battery charge limiter for M1 Mac devices", + "description": "A battery charge limiter for M1/2 Mac devices", "main": "main.js", "build": { "appId": "co.palokaj.battery", diff --git a/battery.sh b/battery.sh index e81e265..8afcd55 100755 --- a/battery.sh +++ b/battery.sh @@ -226,6 +226,7 @@ if [[ "$action" == "uninstall" ]]; then disable_discharging battery remove_daemon sudo rm -v "$binfolder/smc" "$binfolder/battery" + pkill -f "/usr/local/bin/battery.*" exit 0 fi