mirror of
https://github.com/actuallymentor/battery.git
synced 2026-03-07 18:52:03 +00:00
Merge pull request #446 from base47/root-owned-executables
Fix security issues, simplify app setup/updates, fix calibrate and some minor issues
This commit is contained in:
@@ -4,11 +4,28 @@ const { exec } = require( 'node:child_process' )
|
||||
const { log, alert, wait, confirm } = require( './helpers' )
|
||||
const { get_force_discharge_setting } = require( './settings' )
|
||||
const { USER } = process.env
|
||||
const path_fix = 'PATH=/opt/homebrew/bin:/opt/homebrew/sbin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin'
|
||||
const battery = `${ path_fix } battery`
|
||||
const binfolder = '/usr/local/co.palokaj.battery'
|
||||
const battery = `${ binfolder }/battery`
|
||||
|
||||
/* ///////////////////////////////
|
||||
// Shell-execution helpers
|
||||
// ///////////////////////////////
|
||||
//
|
||||
// exec_async_no_timeout(), exec_async() and exec_sudo_async() are async helper functions
|
||||
// which run bash-shell commands.
|
||||
//
|
||||
// They provide a unified output contract:
|
||||
// Fulfilled result: { stdout: string, stderr: string }
|
||||
// Rejected result: 'Error' object having the following extra properties:
|
||||
// - 'cmd': shell command string
|
||||
// - 'code': shell exit code, 'ETIMEDOUT', 'SIGNAL' or 'UNKNOWN'
|
||||
// - 'output': { stdout: string, stderr: string }
|
||||
//
|
||||
// /////////////////////////////*/
|
||||
|
||||
const shell_options = {
|
||||
shell: '/bin/bash',
|
||||
env: { ...process.env, PATH: `${ process.env.PATH }:/usr/local/bin` }
|
||||
env: { ...process.env, PATH: '/usr/bin:/bin:/usr/sbin:/sbin' }
|
||||
}
|
||||
|
||||
// Execute without sudo
|
||||
@@ -18,21 +35,37 @@ const exec_async_no_timeout = command => new Promise( ( resolve, reject ) => {
|
||||
|
||||
exec( command, shell_options, ( error, stdout, stderr ) => {
|
||||
|
||||
if( error ) return reject( error, stderr, stdout )
|
||||
if( stderr ) return reject( stderr )
|
||||
if( stdout ) return resolve( stdout )
|
||||
if( !stdout ) return resolve( '' )
|
||||
const output = { stdout: stdout ?? '', stderr: stderr ?? '' }
|
||||
if (error) {
|
||||
error.code ??= (error.signal ? 'SIGNAL' : 'UNKNOWN')
|
||||
error.cmd ??= command
|
||||
error.output = output
|
||||
return reject( error )
|
||||
} else {
|
||||
return resolve( output )
|
||||
}
|
||||
|
||||
} )
|
||||
|
||||
} )
|
||||
|
||||
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` )
|
||||
} )
|
||||
] )
|
||||
const exec_async = ( command, timeout_in_ms=0 ) => {
|
||||
|
||||
const workers = [ exec_async_no_timeout( command ) ]
|
||||
if ( timeout_in_ms > 0 ) {
|
||||
workers.push(
|
||||
wait(timeout_in_ms).then( () => {
|
||||
const error = new Error( `${ command } timed out` )
|
||||
error.code = 'ETIMEDOUT'
|
||||
error.cmd = command
|
||||
error.output = { stdout: '', stderr: '' }
|
||||
throw error;
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
return Promise.race( workers )
|
||||
}
|
||||
|
||||
// Execute with sudo
|
||||
const exec_sudo_async = command => new Promise( ( resolve, reject ) => {
|
||||
@@ -42,21 +75,30 @@ const exec_sudo_async = command => new Promise( ( resolve, reject ) => {
|
||||
|
||||
exec( `osascript -e "do shell script \\"${ command }\\" with administrator privileges"`, shell_options, ( error, stdout, stderr ) => {
|
||||
|
||||
if( error ) return reject( error, stderr, stdout )
|
||||
if( stderr ) return reject( stderr )
|
||||
if( stdout ) return resolve( stdout )
|
||||
if( !stdout ) return resolve( '' )
|
||||
const output = { stdout: stdout ?? '', stderr: stderr ?? '' }
|
||||
if (error) {
|
||||
error.code ??= (error.signal ? 'SIGNAL' : 'UNKNOWN')
|
||||
error.cmd ??= command
|
||||
error.output = output
|
||||
return reject(error)
|
||||
} else {
|
||||
return resolve(output)
|
||||
}
|
||||
|
||||
} )
|
||||
|
||||
} )
|
||||
|
||||
/* ///////////////////////////////
|
||||
// Battery cli functions
|
||||
// /////////////////////////////*/
|
||||
|
||||
// Battery status checker
|
||||
const get_battery_status = async () => {
|
||||
|
||||
try {
|
||||
const message = await exec_async( `${ battery } status_csv` )
|
||||
let [ percentage='??', remaining='', charging='', discharging='', maintain_percentage='' ] = message?.split( ',' ) || []
|
||||
const result = await exec_async( `${ battery } status_csv` )
|
||||
let [ percentage='??', remaining='', charging='', discharging='', maintain_percentage='' ] = result.stdout.split( ',' ) || []
|
||||
maintain_percentage = maintain_percentage.trim()
|
||||
maintain_percentage = maintain_percentage.length ? maintain_percentage : undefined
|
||||
charging = charging == 'enabled'
|
||||
@@ -74,23 +116,32 @@ const get_battery_status = async () => {
|
||||
|
||||
} catch ( e ) {
|
||||
log( `Error getting battery status: `, e )
|
||||
alert( `Battery limiter error: ${ e.message }` )
|
||||
await alert( `Battery limiter error: ${ e.message }` )
|
||||
const ERR_COMMAND_NOT_FOUND = 127
|
||||
if ( e.code === ERR_COMMAND_NOT_FOUND ) {
|
||||
// No battery script found. Constant alerts will be preventing a user from quitting, so do it now.
|
||||
// Happens if battery is uninstalled from Terminal while the app is running.
|
||||
app.quit()
|
||||
app.exit()
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/* ///////////////////////////////
|
||||
// Battery cli functions
|
||||
// /////////////////////////////*/
|
||||
const enable_battery_limiter = async () => {
|
||||
|
||||
|
||||
try {
|
||||
// Start battery maintainer
|
||||
const status = await get_battery_status()
|
||||
const allow_force_discharge = get_force_discharge_setting()
|
||||
await exec_async( `${ battery } maintain ${ status?.maintain_percentage || 80 }${ allow_force_discharge ? ' --force-discharge' : '' }` )
|
||||
log( `enable_battery_limiter exec complete` )
|
||||
// 'battery maintain' creates a child process, so when the command exits exec_async does not return.
|
||||
// That's why here we use a timeout and wait for some time.
|
||||
await exec_async(
|
||||
`${ battery } maintain ${ status?.maintain_percentage || 80 }${ allow_force_discharge ? ' --force-discharge' : '' }`,
|
||||
1000 // timeout in milliseconds
|
||||
).catch( e => {
|
||||
if ( e.code !== 'ETIMEDOUT' ) throw e;
|
||||
})
|
||||
log( `enable_battery_limiter exec completed` )
|
||||
return status?.percentage
|
||||
} catch ( e ) {
|
||||
log( 'Error enabling battery: ', e )
|
||||
@@ -112,7 +163,7 @@ const disable_battery_limiter = async () => {
|
||||
|
||||
}
|
||||
|
||||
const low_err_return_false = ( ...errdata ) => {
|
||||
const log_err_return_false = ( ...errdata ) => {
|
||||
log( 'Error in shell call: ', ...errdata )
|
||||
return false
|
||||
}
|
||||
@@ -126,91 +177,73 @@ const initialize_battery = async () => {
|
||||
if( development ) log( `Dev mode on, skip updates: ${ skipupdate }` )
|
||||
|
||||
// Check for network
|
||||
const online = await Promise.race( [
|
||||
exec_async( `${ path_fix } curl -I https://icanhazip.com &> /dev/null` ).then( () => true ).catch( () => false ),
|
||||
exec_async( `${ path_fix } curl -I https://github.com &> /dev/null` ).then( () => true ).catch( () => false )
|
||||
] )
|
||||
log( `Internet online: ${ online }` )
|
||||
const online_check_timeout_millisec = 3000
|
||||
const online = await Promise.any( [
|
||||
exec_async( `curl -I https://icanhazip.com > /dev/null 2>&1`, online_check_timeout_millisec ),
|
||||
exec_async( `curl -I https://github.com > /dev/null 2>&1`, online_check_timeout_millisec )
|
||||
] ).then( () => true ).catch( () => false )
|
||||
log( `Internet online: `, online)
|
||||
|
||||
// Check if battery is installed and visudo entries are complete. New visudo entries are added when we do new `sudo` stuff in battery.sh
|
||||
// note to self: only added a few of the new entries, there is no need to be exhaustive except to make sure all new sudo stuff is covered
|
||||
const smc_commands = [
|
||||
// Old list
|
||||
'-k CH0C -r',
|
||||
'-k CH0I -r',
|
||||
'-k ACLC -r',
|
||||
// Apple introduced the following two in 2025.
|
||||
// It seems to apply to all Apple Silicon MacBooks, regardless of macOS version.
|
||||
'-k CHTE -r',
|
||||
'-k CHIE -r'
|
||||
]
|
||||
// Check if battery background executables are installed and owned by root.
|
||||
// Note: We assume that ownership and permissions of /usr/local folders are SIP protected by macOS.
|
||||
const [
|
||||
battery_installed,
|
||||
smc_installed,
|
||||
charging_in_visudo,
|
||||
discharging_in_visudo,
|
||||
magsafe_led_in_visudo,
|
||||
chte_charging_in_visudo,
|
||||
chie_discharging_in_visudo,
|
||||
bin_dir_root_owned, // This is important. Other software can potentially change the owner allowing for battery executable replacement.
|
||||
battery_installed, // Make sure battery script exists and is root-owned.
|
||||
smc_installed, // Make sure smc binary exists and is root-owned.
|
||||
silent_update_enabled // Make sure visudo config is installed and allows passwordless update
|
||||
] = await Promise.all( [
|
||||
exec_async( `${ path_fix } which battery` ).catch( low_err_return_false ),
|
||||
exec_async( `${ path_fix } which smc` ).catch( low_err_return_false ),
|
||||
...smc_commands.map( cmd => exec_async( `${ path_fix } sudo -n /usr/local/bin/smc ${ cmd }` ).catch( low_err_return_false ) )
|
||||
exec_async( `test ! -L ${ binfolder } && test "$(stat -f '%u' ${ binfolder })" -eq 0` ).then( () => true ).catch( log_err_return_false ),
|
||||
exec_async( `test ! -L ${ battery } && test "$(stat -f '%u' ${ battery })" -eq 0` ).then( () => true ).catch( log_err_return_false ),
|
||||
exec_async( `test ! -L ${ binfolder }/smc && test "$(stat -f '%u' ${ binfolder }/smc)" -eq 0` ).then( () => true ).catch( log_err_return_false ),
|
||||
exec_async( `sudo -n ${ battery } update_silent is_enabled` ).then( () => true ).catch( log_err_return_false )
|
||||
] )
|
||||
|
||||
const visudo_complete = ![ charging_in_visudo, discharging_in_visudo, magsafe_led_in_visudo, chte_charging_in_visudo, chie_discharging_in_visudo ].some( entry => entry === false )
|
||||
const is_installed = battery_installed && smc_installed
|
||||
log( 'Is installed? ', is_installed )
|
||||
log( 'Visudo complete? ', {
|
||||
charging_in_visudo,
|
||||
discharging_in_visudo,
|
||||
magsafe_led_in_visudo,
|
||||
chte_charging_in_visudo,
|
||||
chie_discharging_in_visudo,
|
||||
visudo_complete
|
||||
} )
|
||||
const is_installed = bin_dir_root_owned && battery_installed && smc_installed && silent_update_enabled
|
||||
log( 'Is installed? ', is_installed, 'details: ', bin_dir_root_owned, battery_installed, smc_installed, silent_update_enabled )
|
||||
|
||||
// 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` )
|
||||
if( is_installed ) 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` ) )
|
||||
// Why are we doing this: The maintenance battery process which launches on macOS startup does not update a
|
||||
// pidfile (bug). Consider removing the following lines when process management improves.
|
||||
if( is_installed ) await exec_async( `${ battery } maintain stop` ).catch( log_err_return_false )
|
||||
const battery_process_pattern = `/usr/local/bin/battery.*|${battery.replace(/\./g, '\\.')}.*`
|
||||
const processes = await exec_async( `ps aux | grep -E "${battery_process_pattern}" | grep -v grep | wc -l | grep -Eo "\\d*"` ).catch( e => e.output )
|
||||
log( `Found '${ `${ processes.stdout }`.replace( /\n/, '' ) }' dangling battery processes to kill` )
|
||||
await exec_async( `pkill -f "${battery_process_pattern}"` ).catch( e => log( `Error killing existing battery processes, usually means no running processes` ) )
|
||||
|
||||
// If installed, update
|
||||
if( is_installed && visudo_complete ) {
|
||||
if( !online ) return log( `Skipping battery update because we are offline` )
|
||||
if( skipupdate ) return log( `Skipping update due to environment variable` )
|
||||
log( `Updating battery...` )
|
||||
const result = await exec_async( `${ battery } update silent` ).catch( e => e )
|
||||
log( `Update result: `, result )
|
||||
}
|
||||
|
||||
// If not installed, run install script
|
||||
// Reinstall or try updating
|
||||
if( !is_installed ) {
|
||||
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.` )
|
||||
if( !is_installed ) 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 success `, result )
|
||||
await alert( `Battery background components installed successfully. You can find the battery limiter icon in the top right of your menu bar.` )
|
||||
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.` )
|
||||
try {
|
||||
const result = await exec_sudo_async( `curl -s https://raw.githubusercontent.com/actuallymentor/battery/main/setup.sh | bash -s -- $USER` )
|
||||
log( `Install result success `, result )
|
||||
await alert( `Battery background components installed/updated successfully. You can find the battery limiter icon in the top right of your menu bar.` )
|
||||
} catch ( e ) {
|
||||
log( `Battery setup failed: `, e )
|
||||
await alert( `Failed to install battery background components.\n\n${e.message}`)
|
||||
app.quit()
|
||||
app.exit()
|
||||
}
|
||||
} else {
|
||||
// Try updating to the latest version
|
||||
if( !online ) return log( `Skipping battery update because we are offline` )
|
||||
if( skipupdate ) return log( `Skipping update due to environment variable` )
|
||||
log( `Updating battery...` )
|
||||
try {
|
||||
const result = await exec_async( `sudo -n ${ battery } update_silent` )
|
||||
log( `Update details: `, result )
|
||||
} catch ( e ) {
|
||||
log( `Battery update failed: `, e )
|
||||
await alert( `Couldn’t complete the update.\n\n${e.message}`)
|
||||
}
|
||||
}
|
||||
|
||||
// If visudo entries are incomplete, update
|
||||
if( !visudo_complete ) {
|
||||
await alert( `Battery needs to apply a backwards incompatible update, to do this it will ask for your password. This should not happen frequently.` )
|
||||
await exec_sudo_async( `${ path_fix } battery visudo` )
|
||||
}
|
||||
|
||||
// Recover old battery setting on boot (as we killed all old processes above)
|
||||
await exec_async( `${ battery } maintain recover` )
|
||||
|
||||
// Basic user tracking on app open, run it in the background so it does not cause any delay for the user
|
||||
if( online ) exec_async( `nohup curl "https://unidentifiedanalytics.web.app/touch/?namespace=battery" > /dev/null 2>&1` )
|
||||
|
||||
if( online ) exec_async( `nohup curl "https://unidentifiedanalytics.web.app/touch/?namespace=battery" > /dev/null 2>&1` ).catch(() => {})
|
||||
|
||||
} catch ( e ) {
|
||||
log( `Update/install error: `, e )
|
||||
await alert( `Error installing battery limiter: ${ e.message }` )
|
||||
log( `Error Initializing battery: `, e )
|
||||
await alert( `Battery limiter initialization error: ${ e.message }` )
|
||||
app.quit()
|
||||
app.exit()
|
||||
}
|
||||
@@ -222,7 +255,11 @@ const uninstall_battery = async () => {
|
||||
try {
|
||||
const confirmed = await confirm( `Are you sure you want to uninstall Battery?` )
|
||||
if( !confirmed ) return false
|
||||
await exec_sudo_async( `${ path_fix } sudo battery uninstall silent` )
|
||||
await exec_sudo_async( `sudo ${ battery } uninstall silent` ).catch( e => {
|
||||
// Being killed is an expected successful completion for 'battery uninstall' and the osascript running it.
|
||||
// If you need to change this, check whether 'pkill -f <process cmd pattern>' is still used by 'battery uninstall'.
|
||||
if ( e.code !== 'SIGNAL' ) throw e;
|
||||
})
|
||||
await alert( `Battery is now uninstalled!` )
|
||||
return true
|
||||
} catch ( e ) {
|
||||
@@ -233,13 +270,12 @@ const uninstall_battery = async () => {
|
||||
|
||||
}
|
||||
|
||||
|
||||
const is_limiter_enabled = async () => {
|
||||
|
||||
try {
|
||||
const message = await exec_async( `${ battery } status` )
|
||||
log( `Limiter status message: `, message )
|
||||
return message?.includes( 'being maintained at' )
|
||||
const result = await exec_async( `${ battery } status` )
|
||||
log( `Limiter status message: `, result )
|
||||
return result.stdout.includes( 'being maintained at' )
|
||||
} catch ( e ) {
|
||||
log( `Error getting battery status: `, e )
|
||||
alert( `Battery limiter error: ${ e.message }` )
|
||||
@@ -247,7 +283,6 @@ const is_limiter_enabled = async () => {
|
||||
|
||||
}
|
||||
|
||||
|
||||
module.exports = {
|
||||
enable_battery_limiter,
|
||||
disable_battery_limiter,
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
const { promises: fs } = require( 'fs' )
|
||||
const { HOME } = process.env
|
||||
const util = require( 'util' )
|
||||
|
||||
let has_alerted_user_no_home = false
|
||||
|
||||
const { dialog } = require( 'electron' )
|
||||
@@ -21,7 +23,8 @@ const log = async ( ...messages ) => {
|
||||
try {
|
||||
if( HOME ) {
|
||||
await fs.mkdir( `${ HOME }/.battery/`, { recursive: true } )
|
||||
await fs.appendFile( `${ HOME }/.battery/gui.log`, `${ messages.join( '\n' ) }\n`, 'utf8' )
|
||||
const line = util.format(...messages) + "\n";
|
||||
await fs.appendFile( `${ HOME }/.battery/gui.log`, line, 'utf8' )
|
||||
} else if( !has_alerted_user_no_home ) {
|
||||
alert( `No HOME variable set, this should never happen` )
|
||||
has_alerted_user_no_home = true
|
||||
|
||||
@@ -177,7 +177,7 @@ const refresh_logo = async ( percent=80, force ) => {
|
||||
// /////////////////////////////*/
|
||||
async function set_initial_interface() {
|
||||
|
||||
log( "Starting tray app" )
|
||||
log('\n===\n=== Starting tray app\n===\n')
|
||||
tray = new Tray( get_logo_template( 100, true ) )
|
||||
|
||||
// Set "loading" context
|
||||
|
||||
526
battery.sh
526
battery.sh
@@ -6,13 +6,14 @@
|
||||
## ###############
|
||||
BATTERY_CLI_VERSION="v1.3.2"
|
||||
|
||||
# Path fixes for unexpected environments
|
||||
PATH=/opt/homebrew/bin:/opt/homebrew/sbin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin
|
||||
# If a script may run as root:
|
||||
# - Reset PATH to safe defaults at the very beginning of the script.
|
||||
# - Never include user-owned directories in PATH.
|
||||
PATH=/usr/bin:/bin:/usr/sbin:/sbin
|
||||
|
||||
## ###############
|
||||
## Variables
|
||||
## ###############
|
||||
binfolder=/usr/local/bin
|
||||
visudo_folder=/private/etc/sudoers.d
|
||||
visudo_file=${visudo_folder}/battery
|
||||
configfolder=$HOME/.battery
|
||||
@@ -22,6 +23,7 @@ maintain_percentage_tracker_file=$configfolder/maintain.percentage
|
||||
maintain_voltage_tracker_file=$configfolder/maintain.voltage
|
||||
daemon_path=$HOME/Library/LaunchAgents/battery.plist
|
||||
calibrate_pidfile=$configfolder/calibrate.pid
|
||||
path_configfile=/etc/paths.d/50-battery
|
||||
|
||||
# Voltage limits
|
||||
voltage_min="10.5"
|
||||
@@ -29,6 +31,28 @@ voltage_max="12.6"
|
||||
voltage_hyst_min="0.1"
|
||||
voltage_hyst_max="2"
|
||||
|
||||
# SECURITY NOTES:
|
||||
# - ALWAYS hardcode and use the absolute path to the battery executables to avoid PATH-based spoofing.
|
||||
# Think of the scenario where 'battery update_silent' running as root invokes 'battery visudo' as a
|
||||
# PATH spoofing opportunity example.
|
||||
# - Ensure this script, smc binary and their parent folders are root-owned and not writable by
|
||||
# the user or others.
|
||||
# - Ensure that you are not sourcing any user-writable scripts within this script to avoid overrides of
|
||||
# security critical variables.
|
||||
binfolder="/usr/local/co.palokaj.battery"
|
||||
battery_binary="$binfolder/battery"
|
||||
smc_binary="$binfolder/smc"
|
||||
|
||||
# GitHub URLs for setup and updates.
|
||||
# Temporarily set to your username and branch to test update functionality with your fork.
|
||||
# Security note: Do NOT allow github_user or github_branch to be injected via environment
|
||||
# variables or any other means. Keep them hardcoded.
|
||||
github_user="actuallymentor"
|
||||
github_branch="main"
|
||||
github_url_setup_sh="https://raw.githubusercontent.com/${github_user}/battery/${github_branch}/setup.sh"
|
||||
github_url_update_sh="https://raw.githubusercontent.com/${github_user}/battery/${github_branch}/update.sh"
|
||||
github_url_battery_sh="https://raw.githubusercontent.com/${github_user}/battery/${github_branch}/battery.sh"
|
||||
|
||||
## ###############
|
||||
## Housekeeping
|
||||
## ###############
|
||||
@@ -57,11 +81,11 @@ Usage:
|
||||
|
||||
battery logs LINES[integer, optional]
|
||||
output logs of the battery CLI and GUI
|
||||
eg: battery logs 100
|
||||
eg: battery logs 100
|
||||
|
||||
battery maintain PERCENTAGE[1-100,stop] or RANGE[lower-upper]
|
||||
reboot-persistent battery level maintenance: turn off charging above, and on below a certain value
|
||||
it has the option of a --force-discharge flag that discharges even when plugged in (this does NOT work well with clamshell mode)
|
||||
it has the option of a --force-discharge flag that discharges even when plugged in (this does NOT work well with clamshell mode)
|
||||
eg: battery maintain 80 # maintain at 80%
|
||||
eg: battery maintain 70-80 # maintain between 70-80%
|
||||
eg: battery maintain stop
|
||||
@@ -82,19 +106,17 @@ Usage:
|
||||
|
||||
battery calibrate
|
||||
calibrate the battery by discharging it to 15%, then recharging it to 100%, and keeping it there for 1 hour
|
||||
battery maintenance is restored upon completion
|
||||
menubar battery app execution and/or battery maintain command will interrupt calibration
|
||||
|
||||
battery charge LEVEL[1-100]
|
||||
charge the battery to a certain percentage, and disable charging when that percentage is reached
|
||||
charge the battery to a certain percentage; battery maintenance is restored upon completion
|
||||
eg: battery charge 90
|
||||
|
||||
battery discharge LEVEL[1-100]
|
||||
block power input from the adapter until battery falls to this level
|
||||
block adapter power until the battery reaches the specified level; battery maintenance is restored upon completion
|
||||
eg: battery discharge 90
|
||||
|
||||
battery visudo
|
||||
ensure you don't need to call battery with sudo
|
||||
This is already used in the setup script, so you should't need it.
|
||||
|
||||
battery update
|
||||
update the battery utility to the latest version
|
||||
|
||||
@@ -107,29 +129,37 @@ Usage:
|
||||
"
|
||||
|
||||
# Visudo instructions
|
||||
# File location: /etc/sudoers.d/battery
|
||||
# Purpose:
|
||||
# - Allows this script to execute 'sudo smc -w' commands without requiring a user password.
|
||||
# - Allows passwordless updates.
|
||||
visudoconfig="
|
||||
# Visudo settings for the battery utility installed from https://github.com/actuallymentor/battery
|
||||
# intended to be placed in $visudo_file on a mac
|
||||
Cmnd_Alias BATTERYOFF = $binfolder/smc -k CH0B -w 02, $binfolder/smc -k CH0C -w 02, $binfolder/smc -k CH0B -r, $binfolder/smc -k CH0C -r
|
||||
Cmnd_Alias BATTERYON = $binfolder/smc -k CH0B -w 00, $binfolder/smc -k CH0C -w 00
|
||||
Cmnd_Alias DISCHARGEOFF = $binfolder/smc -k CH0I -w 00, $binfolder/smc -k CH0I -r
|
||||
Cmnd_Alias DISCHARGEON = $binfolder/smc -k CH0I -w 01
|
||||
Cmnd_Alias LEDCONTROL = $binfolder/smc -k ACLC -w 04, $binfolder/smc -k ACLC -w 03, $binfolder/smc -k ACLC -w 02, $binfolder/smc -k ACLC -w 01, $binfolder/smc -k ACLC -w 00, $binfolder/smc -k ACLC -r
|
||||
Cmnd_Alias CHTE = $binfolder/smc -k CHTE -r, $binfolder/smc -k CHTE -w 00000000, $binfolder/smc -k CHTE -w 01000000
|
||||
Cmnd_Alias CHIE = $binfolder/smc -k CHIE -r, $binfolder/smc -k CHIE -w 08, $binfolder/smc -k CHIE -w 00
|
||||
Cmnd_Alias CH0J = $binfolder/smc -k CH0J -r, $binfolder/smc -k CH0J -w 01, $binfolder/smc -k CH0J -w 00
|
||||
ALL ALL = NOPASSWD: BATTERYOFF
|
||||
ALL ALL = NOPASSWD: BATTERYON
|
||||
ALL ALL = NOPASSWD: DISCHARGEOFF
|
||||
ALL ALL = NOPASSWD: DISCHARGEON
|
||||
ALL ALL = NOPASSWD: LEDCONTROL
|
||||
ALL ALL = NOPASSWD: CHTE
|
||||
ALL ALL = NOPASSWD: CHIE
|
||||
ALL ALL = NOPASSWD: CH0J
|
||||
|
||||
# Allow passwordless update (All battery app executables are owned by root to prevent privilege escalation attacks)
|
||||
ALL ALL = NOPASSWD: $battery_binary update_silent
|
||||
ALL ALL = NOPASSWD: $battery_binary update_silent is_enabled
|
||||
|
||||
# Allow passwordless battery-charging–related SMC write commands
|
||||
Cmnd_Alias CHARGING_OFF = $smc_binary -k CH0B -w 02, $smc_binary -k CH0C -w 02, $smc_binary -k CHTE -w 01000000
|
||||
Cmnd_Alias CHARGING_ON = $smc_binary -k CH0B -w 00, $smc_binary -k CH0C -w 00, $smc_binary -k CHTE -w 00000000
|
||||
Cmnd_Alias FORCE_DISCHARGE_OFF = $smc_binary -k CH0I -w 00, $smc_binary -k CHIE -w 00, $smc_binary -k CH0J -w 00
|
||||
Cmnd_Alias FORCE_DISCHARGE_ON = $smc_binary -k CH0I -w 01, $smc_binary -k CHIE -w 08, $smc_binary -k CH0J -w 01
|
||||
Cmnd_Alias LED_CONTROL = $smc_binary -k ACLC -w 04, $smc_binary -k ACLC -w 03, $smc_binary -k ACLC -w 02, $smc_binary -k ACLC -w 01, $smc_binary -k ACLC -w 00
|
||||
ALL ALL = NOPASSWD: CHARGING_OFF
|
||||
ALL ALL = NOPASSWD: CHARGING_ON
|
||||
ALL ALL = NOPASSWD: FORCE_DISCHARGE_OFF
|
||||
ALL ALL = NOPASSWD: FORCE_DISCHARGE_ON
|
||||
ALL ALL = NOPASSWD: LED_CONTROL
|
||||
|
||||
# Temporarily keep passwordless SMC reading commands so the old menubar GUI versions don't ask for password on each launch
|
||||
# trying to execute 'battery visudo'. There is no harm in removing this, so do it as soon as you believe users are no
|
||||
# longer using old versions.
|
||||
ALL ALL = NOPASSWD: $smc_binary -k CH0C -r, $smc_binary -k CH0I -r, $smc_binary -k ACLC -r, $smc_binary -k CHIE -r, $smc_binary -k CHTE -r
|
||||
"
|
||||
|
||||
# Get parameters
|
||||
battery_binary=$0
|
||||
action=$1
|
||||
setting=$2
|
||||
subsetting=$3
|
||||
@@ -139,7 +169,7 @@ subsetting=$3
|
||||
## ###############
|
||||
|
||||
function log() {
|
||||
echo -e "$(date +%D-%T) - $1"
|
||||
echo -e "$(date +%D-%T) [$$]: $*"
|
||||
}
|
||||
|
||||
function valid_percentage() {
|
||||
@@ -187,7 +217,7 @@ function valid_voltage() {
|
||||
|
||||
function smc_read_hex() {
|
||||
key=$1
|
||||
line=$(echo $(sudo smc -k $key -r))
|
||||
line=$(echo $($smc_binary -k $key -r))
|
||||
if [[ $line =~ "no data" ]]; then
|
||||
echo
|
||||
else
|
||||
@@ -198,7 +228,7 @@ function smc_read_hex() {
|
||||
function smc_write_hex() {
|
||||
local key=$1
|
||||
local hex_value=$2
|
||||
if ! sudo smc -k "$key" -w "$hex_value" >/dev/null 2>&1; then
|
||||
if ! sudo $smc_binary -k "$key" -w "$hex_value" >/dev/null 2>&1; then
|
||||
log "⚠️ Failed to write $hex_value to $key"
|
||||
return 1
|
||||
fi
|
||||
@@ -208,11 +238,11 @@ function smc_write_hex() {
|
||||
## #########################
|
||||
## Detect supported SMC keys
|
||||
## #########################
|
||||
[[ $(sudo smc -k CHTE -r) =~ "no data" ]] && smc_supports_tahoe=false || smc_supports_tahoe=true;
|
||||
[[ $(sudo smc -k CH0B -r) =~ "no data" ]] && smc_supports_legacy=false || smc_supports_legacy=true;
|
||||
[[ $(sudo smc -k CHIE -r) =~ "no data" ]] && smc_supports_adapter_chie=false || smc_supports_adapter_chie=true;
|
||||
[[ $(sudo smc -k CH0I -r) =~ "no data" ]] && smc_supports_adapter_ch0i=false || smc_supports_adapter_ch0i=true;
|
||||
[[ $(sudo smc -k CH0J -r) =~ "no data" || $(sudo smc -k CH0J -r) =~ "Error" ]] && smc_supports_adapter_ch0j=false || smc_supports_adapter_ch0j=true;
|
||||
[[ $($smc_binary -k CHTE -r) =~ "no data" ]] && smc_supports_tahoe=false || smc_supports_tahoe=true;
|
||||
[[ $($smc_binary -k CH0B -r) =~ "no data" ]] && smc_supports_legacy=false || smc_supports_legacy=true;
|
||||
[[ $($smc_binary -k CHIE -r) =~ "no data" ]] && smc_supports_adapter_chie=false || smc_supports_adapter_chie=true;
|
||||
[[ $($smc_binary -k CH0I -r) =~ "no data" ]] && smc_supports_adapter_ch0i=false || smc_supports_adapter_ch0i=true;
|
||||
[[ $($smc_binary -k CH0J -r) =~ "no data" || $($smc_binary -k CH0J -r) =~ "Error" ]] && smc_supports_adapter_ch0j=false || smc_supports_adapter_ch0j=true;
|
||||
|
||||
function log_smc_capabilities() {
|
||||
log "SMC capabilities: tahoe=$smc_supports_tahoe legacy=$smc_supports_legacy CHIE=$smc_supports_adapter_chie CH0I=$smc_supports_adapter_ch0i CH0J=$smc_supports_adapter_ch0j"
|
||||
@@ -225,27 +255,20 @@ function log_smc_capabilities() {
|
||||
# Change magsafe color
|
||||
# see community sleuthing: https://github.com/actuallymentor/battery/issues/71
|
||||
function change_magsafe_led_color() {
|
||||
log "MagSafe LED function invoked"
|
||||
color=$1
|
||||
local color=$1
|
||||
|
||||
# Check whether user can run color changes without password (required for backwards compatibility)
|
||||
if sudo -n smc -k ACLC -r &>/dev/null; then
|
||||
log "💡 Setting magsafe color to $color"
|
||||
else
|
||||
log "🚨 Your version of battery is using an old visudo file, please run 'battery visudo' to fix this, until you do battery cannot change magsafe led colors"
|
||||
return
|
||||
fi
|
||||
log "💡 Setting magsafe color to $color"
|
||||
|
||||
if [[ "$color" == "green" ]]; then
|
||||
log "setting LED to green"
|
||||
sudo smc -k ACLC -w 03
|
||||
sudo $smc_binary -k ACLC -w 03
|
||||
elif [[ "$color" == "orange" ]]; then
|
||||
log "setting LED to orange"
|
||||
sudo smc -k ACLC -w 04
|
||||
sudo $smc_binary -k ACLC -w 04
|
||||
else
|
||||
# Default action: reset. Value 00 is a guess and needs confirmation
|
||||
log "resetting LED"
|
||||
sudo smc -k ACLC -w 00
|
||||
sudo $smc_binary -k ACLC -w 00
|
||||
fi
|
||||
}
|
||||
|
||||
@@ -260,7 +283,7 @@ function enable_discharging() {
|
||||
else
|
||||
smc_write_hex CH0I 01
|
||||
fi
|
||||
sudo smc -k ACLC -w 01
|
||||
sudo $smc_binary -k ACLC -w 01
|
||||
}
|
||||
|
||||
function disable_discharging() {
|
||||
@@ -415,43 +438,161 @@ function get_voltage() {
|
||||
echo "$voltage"
|
||||
}
|
||||
|
||||
## ##################
|
||||
## Miscellany helpers
|
||||
## ##################
|
||||
|
||||
function determine_unprivileged_user() {
|
||||
local username="$1"
|
||||
if [[ "$username" == "root" ]]; then
|
||||
log "⚠️ 'battery $action $setting $subsetting': argument user is root, trying to recover" >&2
|
||||
username=""
|
||||
fi
|
||||
if [[ -z "$username" && -n "$SUDO_USER" && "$SUDO_USER" != "root" ]]; then
|
||||
username="$SUDO_USER"
|
||||
fi
|
||||
if [[ -z "$username" && -n "$USER" && "$USER" != "root" ]]; then
|
||||
username="$USER"
|
||||
fi
|
||||
if [[ -z "$username" && "$HOME" == /Users/* ]]; then
|
||||
username="$(basename "$HOME")";
|
||||
fi
|
||||
if [[ -z "$username" ]]; then
|
||||
log "⚠️ 'battery $action $setting $subsetting': unable to determine unprivileged user; falling back to 'logname'" >&2
|
||||
username="$(logname 2>/dev/null || true)"
|
||||
fi
|
||||
echo "$username"
|
||||
}
|
||||
|
||||
function assert_unprivileged_user() {
|
||||
local username="$1"
|
||||
if [[ -z "$username" || "$username" == "root" ]]; then
|
||||
log "❌ 'battery $action $setting $subsetting': failed to determine unprivileged user"
|
||||
exit 11
|
||||
fi
|
||||
}
|
||||
|
||||
function assert_not_running_as_root() {
|
||||
if [[ $EUID -eq 0 ]]; then
|
||||
echo " ❌ The following command should not be executed with root privileges:"
|
||||
echo " battery $action $setting $subsetting"
|
||||
echo " Please, try running without 'sudo'"
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
function assert_running_as_root() {
|
||||
if [[ $EUID -ne 0 ]]; then
|
||||
log "❌ battery $action $setting $subsetting: must be executed with root privileges"
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
function ensure_owner() {
|
||||
local owner="$1" group="$2" path="$3"
|
||||
[[ -e $path ]] || { return 1; }
|
||||
local cur_owner=$(stat -f '%Su' "$path")
|
||||
local cur_group=$(stat -f '%Sg' "$path")
|
||||
if [[ $cur_owner != "$owner" || $cur_group != "$group" ]]; then
|
||||
sudo chown -h "${owner}:${group}" "$path"
|
||||
fi
|
||||
}
|
||||
|
||||
function ensure_owner_mode() {
|
||||
local owner="$1" group="$2" mode="$3" path="$4"
|
||||
ensure_owner "$owner" "$group" "$path" || return
|
||||
local cur_mode=$(stat -f '%Lp' "$path")
|
||||
if [[ $cur_mode != "${mode#0}" ]]; then
|
||||
sudo chmod -h "$mode" "$path"
|
||||
fi
|
||||
}
|
||||
|
||||
# Use the following function to apply any setup related fixes which require root permissions.
|
||||
# This function is executed by 'update_silent' action with EUID==0.
|
||||
function fixup_installation_owner_mode() {
|
||||
local username=$1
|
||||
|
||||
ensure_owner_mode $username staff 755 "$(dirname "$daemon_path")"
|
||||
ensure_owner_mode $username staff 644 "$daemon_path"
|
||||
|
||||
ensure_owner_mode $username staff 755 "$configfolder"
|
||||
ensure_owner_mode $username staff 644 "$pidfile"
|
||||
ensure_owner_mode $username staff 644 "$logfile"
|
||||
ensure_owner_mode $username staff 644 "$maintain_percentage_tracker_file"
|
||||
ensure_owner_mode $username staff 644 "$maintain_voltage_tracker_file"
|
||||
ensure_owner_mode $username staff 644 "$calibrate_pidfile"
|
||||
|
||||
ensure_owner_mode root wheel 755 "$visudo_folder"
|
||||
ensure_owner_mode root wheel 440 "$visudo_file"
|
||||
|
||||
ensure_owner_mode root wheel 755 "$binfolder"
|
||||
ensure_owner_mode root wheel 755 "$battery_binary"
|
||||
ensure_owner_mode root wheel 755 "$smc_binary"
|
||||
|
||||
# Do some cleanup after previous versions
|
||||
sudo rm -f "$configfolder/visudo.tmp"
|
||||
}
|
||||
|
||||
function is_latest_version_installed() {
|
||||
# Check if content is reachable first with HEAD request
|
||||
curl -sSI "$github_url_battery_sh" &>/dev/null || return 0
|
||||
# Start downloading and check version
|
||||
curl -sS "$github_url_battery_sh" 2>/dev/null | grep -q "$BATTERY_CLI_VERSION"
|
||||
}
|
||||
|
||||
## ###############
|
||||
## Actions
|
||||
## ###############
|
||||
|
||||
# If the config folder or log file were just created by the code above while
|
||||
# running as root, set the correct ownership and permissions.
|
||||
if [[ $EUID -eq 0 ]]; then
|
||||
username="$(determine_unprivileged_user "$SUDO_USER")"
|
||||
if [[ -n "$username" && "$username" != "root" ]]; then
|
||||
fixup_installation_owner_mode "$username"
|
||||
fi
|
||||
fi
|
||||
|
||||
# Version message
|
||||
if [[ "$action" == "version" ]] || [[ "$action" == "--version" ]]; then
|
||||
echo "$BATTERY_CLI_VERSION"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Help message
|
||||
if [ -z "$action" ] || [[ "$action" == "help" ]]; then
|
||||
if [ -z "$action" ] || [[ "$action" == "help" ]] || [[ "$action" == "--help" ]]; then
|
||||
echo -e "$helpmessage"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Visudo message
|
||||
# Update '/etc/sudoers.d/battery' config if needed
|
||||
if [[ "$action" == "visudo" ]]; then
|
||||
|
||||
# User to set folder ownership to is $setting if it is defined and $USER otherwise
|
||||
if [[ -z "$setting" ]]; then
|
||||
setting=$USER
|
||||
fi
|
||||
|
||||
# Set visudo tempfile ownership to current user
|
||||
log "Setting visudo file permissions to $setting"
|
||||
sudo chown -R $setting $configfolder
|
||||
# Allocate temp folder
|
||||
tempfolder="$(mktemp -d)"
|
||||
function cleanup() { rm -rf "$tempfolder"; }
|
||||
trap cleanup EXIT
|
||||
|
||||
# Write the visudo file to a tempfile
|
||||
visudo_tmpfile="$configfolder/visudo.tmp"
|
||||
sudo rm $visudo_tmpfile 2>/dev/null
|
||||
visudo_tmpfile="$tempfolder/visudo.tmp"
|
||||
echo -e "$visudoconfig" >$visudo_tmpfile
|
||||
|
||||
# If the visudo folder does not exist, make it
|
||||
if ! test -d "$visudo_folder"; then
|
||||
sudo mkdir -p "$visudo_folder"
|
||||
fi
|
||||
ensure_owner_mode root wheel 755 "$visudo_folder"
|
||||
|
||||
# If the visudo file is the same (no error, exit code 0), set the permissions just
|
||||
if sudo cmp $visudo_file $visudo_tmpfile &>/dev/null; then
|
||||
|
||||
echo "The existing battery visudo file is what it should be for version $BATTERY_CLI_VERSION"
|
||||
echo "☑️ The existing battery visudo file is what it should be for version $BATTERY_CLI_VERSION"
|
||||
|
||||
# Check if file permissions are correct, if not, set them
|
||||
current_visudo_file_permissions=$(stat -f "%Lp" $visudo_file)
|
||||
if [[ "$current_visudo_file_permissions" != "440" ]]; then
|
||||
sudo chmod 440 $visudo_file
|
||||
fi
|
||||
ensure_owner_mode root wheel 440 "$visudo_file"
|
||||
|
||||
# Delete tempfolder
|
||||
rm -rf "$tempfolder"
|
||||
|
||||
# exit because no changes are needed
|
||||
exit 0
|
||||
@@ -461,24 +602,19 @@ if [[ "$action" == "visudo" ]]; then
|
||||
# Validate that the visudo tempfile is valid
|
||||
if sudo visudo -c -f $visudo_tmpfile &>/dev/null; then
|
||||
|
||||
# If the visudo folder does not exist, make it
|
||||
if ! test -d "$visudo_folder"; then
|
||||
sudo mkdir -p "$visudo_folder"
|
||||
fi
|
||||
|
||||
# Copy the visudo file from tempfile to live location
|
||||
sudo cp $visudo_tmpfile $visudo_file
|
||||
|
||||
# Delete tempfile
|
||||
rm $visudo_tmpfile
|
||||
|
||||
# Set correct permissions on visudo file
|
||||
sudo chmod 440 $visudo_file
|
||||
ensure_owner_mode root wheel 440 "$visudo_file"
|
||||
|
||||
echo "Visudo file updated successfully"
|
||||
# Delete tempfolder
|
||||
rm -rf "$tempfolder"
|
||||
|
||||
echo "✅ Visudo file updated successfully"
|
||||
|
||||
else
|
||||
echo "Error validating visudo file, this should never happen:"
|
||||
echo "❌ Error validating visudo file, this should never happen:"
|
||||
sudo visudo -c -f $visudo_tmpfile
|
||||
fi
|
||||
|
||||
@@ -487,37 +623,94 @@ fi
|
||||
|
||||
# Reinstall helper
|
||||
if [[ "$action" == "reinstall" ]]; then
|
||||
echo "This will run curl -sS https://raw.githubusercontent.com/actuallymentor/battery/main/setup.sh | bash"
|
||||
echo "This will run curl -sS ${github_url_setup_sh} | bash"
|
||||
if [[ ! "$setting" == "silent" ]]; then
|
||||
echo "Press any key to continue"
|
||||
read
|
||||
fi
|
||||
curl -sS https://raw.githubusercontent.com/actuallymentor/battery/main/setup.sh | bash
|
||||
curl -sS "$github_url_setup_sh" | bash
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Update helper
|
||||
# Update helper for GUI app
|
||||
if [[ "$action" == "update_silent" ]]; then
|
||||
|
||||
assert_running_as_root
|
||||
|
||||
# Exit with success when the GUI app just checks if passwordless updates are enabled
|
||||
if [[ "$setting" == "is_enabled" ]]; then
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Try updating
|
||||
if ! is_latest_version_installed; then
|
||||
curl -sS "$github_url_update_sh" | bash
|
||||
echo "✅ battery background script was updated to the latest version."
|
||||
else
|
||||
echo "☑️ No updates found"
|
||||
fi
|
||||
|
||||
# Update the visudo configuration on each update ensuring that the latest version
|
||||
# is always installed.
|
||||
# Note: this will overwrite the visudo configuration file only if it is outdated.
|
||||
$battery_binary visudo
|
||||
|
||||
# Determine the name of unprivileged user
|
||||
username="$(determine_unprivileged_user "")"
|
||||
assert_unprivileged_user "$username"
|
||||
|
||||
# Use opportunity to fixup installation
|
||||
fixup_installation_owner_mode "$username"
|
||||
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Update helper for Terminal users
|
||||
if [[ "$action" == "update" ]]; then
|
||||
|
||||
# Check if we have the most recent version
|
||||
if curl -sS https://raw.githubusercontent.com/actuallymentor/battery/main/battery.sh | grep -q "$BATTERY_CLI_VERSION"; then
|
||||
echo "No need to update, offline version number $BATTERY_CLI_VERSION matches remote version number"
|
||||
else
|
||||
echo "This will run curl -sS https://raw.githubusercontent.com/actuallymentor/battery/main/update.sh | bash"
|
||||
if [[ ! "$setting" == "silent" ]]; then
|
||||
echo "Press any key to continue"
|
||||
read
|
||||
fi
|
||||
curl -sS https://raw.githubusercontent.com/actuallymentor/battery/main/update.sh | bash
|
||||
if [[ ! "$setting" == "silent" ]]; then
|
||||
# The setting "silent" is always passed when `battery update` is invoked from the UI app,
|
||||
# which decides whether to invoke `battery visudo` as well. But for Terminal-only users
|
||||
# there is no UI app. So it's either we invoke `battery visudo` here, or assume that users
|
||||
# remember to do it themselves, which did not work in the past.
|
||||
echo "Runnig $battery_binary visudo"
|
||||
$battery_binary visudo
|
||||
fi
|
||||
assert_not_running_as_root
|
||||
|
||||
# The older GUI versions 1_3_2 and below can not run silent passwordless update and
|
||||
# will complain with alert. Just exit with success and let them update themselves.
|
||||
# Remove this condition in future versions when you believe the old UI is not used anymore.
|
||||
if [[ "$setting" == "silent" ]]; then
|
||||
exit 0
|
||||
fi
|
||||
|
||||
if ! curl -fsI "$github_url_battery_sh" &>/dev/null; then
|
||||
echo "❌ Can't check for updates: no internet connection (or GitHub unreachable)."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# The code below repeats integrity checks from GUI app, specifically from
|
||||
# app/modules/battery.js: 'initialize_battery'. Try keeping it consistent.
|
||||
|
||||
function check_installation_integrity() (
|
||||
function not_link_and_root_owned() {
|
||||
[[ ! -L "$1" ]] && [[ $(stat -f '%u' "$1") -eq 0 ]]
|
||||
}
|
||||
|
||||
not_link_and_root_owned "$binfolder" && \
|
||||
not_link_and_root_owned "$battery_binary" && \
|
||||
not_link_and_root_owned "$smc_binary" && \
|
||||
sudo -n "$battery_binary" update_silent is_enabled >/dev/null 2>&1
|
||||
)
|
||||
|
||||
if ! check_installation_integrity; then
|
||||
version_before="0" # Force restart maintenance process
|
||||
echo -e "‼️ The battery installation seems to be broken. Forcing reinstall...\n"
|
||||
$battery_binary reinstall silent
|
||||
else
|
||||
version_before="$($battery_binary version)"
|
||||
sudo $battery_binary update_silent
|
||||
fi
|
||||
|
||||
# Restart background maintenance process if update was installed
|
||||
if [[ -x $battery_binary ]] && [[ "$($battery_binary version)" != "$version_before" ]]; then
|
||||
printf "\n%s\n" "🛠️ Restarting 'battery maintain' ..."
|
||||
$battery_binary maintain recover
|
||||
fi
|
||||
|
||||
exit 0
|
||||
fi
|
||||
|
||||
@@ -529,12 +722,24 @@ if [[ "$action" == "uninstall" ]]; then
|
||||
echo "Press any key to continue"
|
||||
read
|
||||
fi
|
||||
|
||||
$battery_binary maintain stop
|
||||
$battery_binary remove_daemon
|
||||
|
||||
enable_charging
|
||||
disable_discharging
|
||||
$battery_binary remove_daemon
|
||||
sudo rm -v "$binfolder/smc" "$binfolder/battery" $visudo_file
|
||||
sudo rm -v -r "$configfolder"
|
||||
pkill -f "/usr/local/bin/battery.*"
|
||||
|
||||
sudo rm -fv /usr/local/bin/battery
|
||||
sudo rm -fv /usr/local/bin/smc
|
||||
|
||||
sudo rm -fv "$visudo_file"
|
||||
sudo rm -frv "$binfolder"
|
||||
sudo rm -frv "$configfolder"
|
||||
sudo rm -fv "$path_configfile"
|
||||
|
||||
# Ensure no dangling battery processes are left running
|
||||
pkill -f "/usr/local/bin/battery.*|/usr/local/co\.palokaj\.battery/battery.*"
|
||||
|
||||
exit 0
|
||||
fi
|
||||
|
||||
@@ -570,9 +775,9 @@ if [[ "$action" == "adapter" ]]; then
|
||||
|
||||
# Set charging to on and off
|
||||
if [[ "$setting" == "on" ]]; then
|
||||
enable_discharging
|
||||
elif [[ "$setting" == "off" ]]; then
|
||||
disable_discharging
|
||||
elif [[ "$setting" == "off" ]]; then
|
||||
enable_discharging
|
||||
else
|
||||
log "Error: $setting is not \"on\" or \"off\"."
|
||||
exit 1
|
||||
@@ -590,18 +795,17 @@ if [[ "$action" == "charge" ]]; then
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Disable running daemon
|
||||
$battery_binary maintain stop
|
||||
|
||||
# Disable charge blocker if enabled
|
||||
$battery_binary adapter on
|
||||
# Stop battery maintenance if invoked by user from Terminal
|
||||
if [[ "$BATTERY_HELPER_MODE" != "1" ]]; then
|
||||
$battery_binary maintain stop
|
||||
fi
|
||||
|
||||
# Start charging
|
||||
battery_percentage=$(get_battery_percentage)
|
||||
log "Charging to $setting% from $battery_percentage%"
|
||||
enable_charging # also disables discharging
|
||||
|
||||
# Loop until battery percent is exceeded
|
||||
# Loop until battery charging level is reached
|
||||
while [[ "$battery_percentage" -lt "$setting" ]]; do
|
||||
|
||||
if [[ "$battery_percentage" -ge "$((setting - 3))" ]]; then
|
||||
@@ -610,11 +814,18 @@ if [[ "$action" == "charge" ]]; then
|
||||
caffeinate -is sleep 60
|
||||
fi
|
||||
|
||||
battery_percentage=$(get_battery_percentage)
|
||||
|
||||
done
|
||||
|
||||
disable_charging
|
||||
log "Charging completed at $battery_percentage%"
|
||||
|
||||
# Try restoring maintenance if invoked by user from Terminal
|
||||
if [[ "$BATTERY_HELPER_MODE" != "1" ]]; then
|
||||
$battery_binary maintain recover
|
||||
fi
|
||||
|
||||
exit 0
|
||||
|
||||
fi
|
||||
@@ -627,12 +838,17 @@ if [[ "$action" == "discharge" ]]; then
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Start charging
|
||||
# Stop battery maintenance if invoked by user from Terminal
|
||||
if [[ "$BATTERY_HELPER_MODE" != "1" ]]; then
|
||||
$battery_binary maintain stop
|
||||
fi
|
||||
|
||||
# Start discharging
|
||||
battery_percentage=$(get_battery_percentage)
|
||||
log "Discharging to $setting% from $battery_percentage%"
|
||||
enable_discharging
|
||||
|
||||
# Loop until battery percent is exceeded
|
||||
# Loop until battery charging level is reached
|
||||
while [[ "$battery_percentage" -gt "$setting" ]]; do
|
||||
|
||||
log "Battery at $battery_percentage% (target $setting%)"
|
||||
@@ -644,6 +860,13 @@ if [[ "$action" == "discharge" ]]; then
|
||||
disable_discharging
|
||||
log "Discharging completed at $battery_percentage%"
|
||||
|
||||
# Try restoring maintenance if invoked by user from Terminal
|
||||
if [[ "$BATTERY_HELPER_MODE" != "1" ]]; then
|
||||
$battery_binary maintain recover
|
||||
fi
|
||||
|
||||
exit 0
|
||||
|
||||
fi
|
||||
|
||||
# Maintain at level
|
||||
@@ -699,7 +922,7 @@ if [[ "$action" == "maintain_synchronous" ]]; then
|
||||
# Before we start maintaining the battery level, first discharge to the target level
|
||||
discharge_target="$lower_bound"
|
||||
log "Triggering discharge to $discharge_target before enabling charging limiter"
|
||||
$battery_binary discharge "$discharge_target"
|
||||
BATTERY_HELPER_MODE=1 $battery_binary discharge "$discharge_target"
|
||||
log "Discharge pre battery-maintenance complete, continuing to battery maintenance loop"
|
||||
else
|
||||
log "Not triggering discharge as it is not requested"
|
||||
@@ -800,6 +1023,10 @@ fi
|
||||
# Asynchronous battery level maintenance
|
||||
if [[ "$action" == "maintain" ]]; then
|
||||
|
||||
assert_not_running_as_root
|
||||
|
||||
disable_discharging
|
||||
|
||||
# Kill old process silently
|
||||
if test -f "$pidfile"; then
|
||||
log "Killing old maintain process at $(cat $pidfile)"
|
||||
@@ -901,62 +1128,43 @@ if [[ "$action" == "maintain" ]]; then
|
||||
fi
|
||||
|
||||
# Battery calibration
|
||||
if [[ "$action" == "calibrate_synchronous" ]]; then
|
||||
log "Starting calibration"
|
||||
if [[ "$action" == "calibrate" ]]; then
|
||||
|
||||
# Stop the maintaining
|
||||
battery maintain stop
|
||||
$battery_binary maintain stop &>/dev/null
|
||||
|
||||
# Discharge battery to 15%
|
||||
battery discharge 15
|
||||
|
||||
while true; do
|
||||
log "checking if at 100%"
|
||||
# Check if battery level has reached 100%
|
||||
if battery status | head -n 1 | grep -q "Battery at 100%"; then
|
||||
break
|
||||
else
|
||||
sleep 300
|
||||
continue
|
||||
fi
|
||||
done
|
||||
|
||||
# Wait before discharging to target level
|
||||
log "reached 100%, maintaining for 1 hour"
|
||||
sleep 3600
|
||||
|
||||
# Discharge battery to 80%
|
||||
battery discharge 80
|
||||
|
||||
# Recover old maintain status
|
||||
battery maintain recover
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Asynchronous battery level maintenance
|
||||
if [[ "$action" == "calibrate" ]]; then
|
||||
# Kill old process silently
|
||||
if test -f "$calibrate_pidfile"; then
|
||||
pid=$(cat "$calibrate_pidfile" 2>/dev/null)
|
||||
kill $pid &>/dev/null
|
||||
fi
|
||||
echo $$ >$calibrate_pidfile
|
||||
|
||||
if [[ "$setting" == "stop" ]]; then
|
||||
log "Killing running calibration daemon"
|
||||
pid=$(cat "$calibrate_pidfile" 2>/dev/null)
|
||||
kill $pid &>/dev/null
|
||||
rm $calibrate_pidfile 2>/dev/null
|
||||
echo -e "Starting battery calibration\n"
|
||||
|
||||
exit 0
|
||||
fi
|
||||
echo "[ 1 ] Discharging battery to 15%"
|
||||
BATTERY_HELPER_MODE=1 $battery_binary discharge 15 &>/dev/null
|
||||
|
||||
# Start calibration script
|
||||
log "Starting calibration script"
|
||||
nohup battery calibrate_synchronous >>$logfile &
|
||||
echo "[ 2 ] Charging to 100%"
|
||||
BATTERY_HELPER_MODE=1 $battery_binary charge 100 &>/dev/null
|
||||
|
||||
echo "[ 3 ] Reached 100%, waiting for 1 hour"
|
||||
enable_charging &>/dev/null
|
||||
sleep 3600
|
||||
|
||||
echo "[ 4 ] Discharging battery to 80%"
|
||||
BATTERY_HELPER_MODE=1 $battery_binary discharge 80 &>/dev/null
|
||||
|
||||
# Remove pidfile
|
||||
rm -f $calibrate_pidfile
|
||||
|
||||
# Recover old maintain status
|
||||
echo "[ 5 ] Restarting battery maintenance"
|
||||
$battery_binary maintain recover &>/dev/null
|
||||
|
||||
echo -e "\n✅ Done\n"
|
||||
exit 0
|
||||
|
||||
# Store pid of calibration process and setting
|
||||
echo $! >$calibrate_pidfile
|
||||
pid=$(cat "$calibrate_pidfile" 2>/dev/null)
|
||||
fi
|
||||
|
||||
# Status logger
|
||||
@@ -991,6 +1199,8 @@ fi
|
||||
# launchd daemon creator, inspiration: https://www.launchd.info/
|
||||
if [[ "$action" == "create_daemon" ]]; then
|
||||
|
||||
assert_not_running_as_root
|
||||
|
||||
call_action="maintain_synchronous"
|
||||
if test -f "$maintain_voltage_tracker_file"; then
|
||||
call_action="maintain_voltage_synchronous"
|
||||
@@ -1005,7 +1215,7 @@ if [[ "$action" == "create_daemon" ]]; then
|
||||
<string>com.battery.app</string>
|
||||
<key>ProgramArguments</key>
|
||||
<array>
|
||||
<string>$binfolder/battery</string>
|
||||
<string>$battery_binary</string>
|
||||
<string>$call_action</string>
|
||||
<string>recover</string>
|
||||
</array>
|
||||
|
||||
128
setup.sh
128
setup.sh
@@ -1,31 +1,75 @@
|
||||
#!/bin/bash
|
||||
|
||||
#
|
||||
# ‼️ SECURITY NOTES FOR MAINTAINERS:
|
||||
#
|
||||
# This app uses a visudo configuration that allows a background script running as
|
||||
# an unprivileged user to execute battery management commands without requiring a
|
||||
# user password. This requires careful installation and design to avoid potential
|
||||
# privilege-escalation vulnerabilities.
|
||||
#
|
||||
# Rule of thumb:
|
||||
# - Unprivileged users must not be able to modify, replace, or inject any code
|
||||
# that can be executed with root privileges.
|
||||
#
|
||||
# For this reason:
|
||||
# - All battery-related binaries and scripts that can be executed via sudo,
|
||||
# including those that prompt for a user password, must be owned by root.
|
||||
# - They must not be writable by group or others.
|
||||
# - Their parent directories must also be owned by root and not be writable by
|
||||
# unprivileged users, to prevent the replacement of executables.
|
||||
#
|
||||
|
||||
# Reset PATH to minimal safe defaults
|
||||
PATH=/usr/bin:/bin:/usr/sbin:/sbin
|
||||
|
||||
# User welcome message
|
||||
echo -e "\n####################################################################"
|
||||
echo '# 👋 Welcome, this is the setup script for the battery CLI tool.'
|
||||
echo -e "# Note: this script will ask for your password once or multiple times."
|
||||
echo -e "# Note: this script may ask for your password."
|
||||
echo -e "####################################################################\n\n"
|
||||
|
||||
# Set environment variables
|
||||
tempfolder=~/.battery-tmp
|
||||
binfolder=/usr/local/bin
|
||||
mkdir -p $tempfolder
|
||||
# Determine unprivileged user name
|
||||
if [[ -n "$1" ]]; then
|
||||
calling_user="$1"
|
||||
else
|
||||
if [[ -n "$SUDO_USER" ]]; then
|
||||
calling_user=$SUDO_USER
|
||||
else
|
||||
calling_user=$USER
|
||||
fi
|
||||
fi
|
||||
if [[ "$calling_user" == "root" ]]; then
|
||||
echo "❌ Failed to determine unprivileged username"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Set script value
|
||||
calling_user=${1:-"$USER"}
|
||||
# Set variables
|
||||
binfolder=/usr/local/co.palokaj.battery
|
||||
configfolder=/Users/$calling_user/.battery
|
||||
pidfile=$configfolder/battery.pid
|
||||
logfile=$configfolder/battery.log
|
||||
launch_agent_plist=/Users/$calling_user/Library/LaunchAgents/battery.plist
|
||||
path_configfile=/etc/paths.d/50-battery
|
||||
|
||||
# Ask for sudo once, in most systems this will cache the permissions for a bit
|
||||
sudo echo "🔋 Starting battery installation"
|
||||
echo -e "[ 1 ] Superuser permissions acquired."
|
||||
echo "[ 1 ] Superuser permissions acquired."
|
||||
|
||||
# Cleanup after versions 1_3_2 and below
|
||||
sudo rm -f /usr/local/bin/battery
|
||||
sudo rm -f /usr/local/bin/smc
|
||||
|
||||
echo "[ 2 ] Allocate temp folder"
|
||||
tempfolder="$(mktemp -d)"
|
||||
function cleanup() { rm -rf "$tempfolder"; }
|
||||
trap cleanup EXIT
|
||||
|
||||
echo "[ 3 ] Downloading latest version of battery CLI"
|
||||
# Note: github names zips by <reponame>-<branchname>.replace( '/', '-' )
|
||||
update_branch="main"
|
||||
in_zip_folder_name="battery-$update_branch"
|
||||
batteryfolder="$tempfolder/battery"
|
||||
echo "[ 2 ] Downloading latest version of battery CLI"
|
||||
rm -rf $batteryfolder
|
||||
mkdir -p $batteryfolder
|
||||
curl -sSL -o $batteryfolder/repo.zip "https://github.com/actuallymentor/battery/archive/refs/heads/$update_branch.zip"
|
||||
@@ -33,44 +77,56 @@ unzip -qq $batteryfolder/repo.zip -d $batteryfolder
|
||||
cp -r $batteryfolder/$in_zip_folder_name/* $batteryfolder
|
||||
rm $batteryfolder/repo.zip
|
||||
|
||||
# Move built file to bin folder
|
||||
echo "[ 3 ] Move smc to executable folder"
|
||||
sudo mkdir -p $binfolder
|
||||
sudo cp $batteryfolder/dist/smc $binfolder
|
||||
sudo chown $calling_user $binfolder/smc
|
||||
sudo chmod 755 $binfolder/smc
|
||||
sudo chmod +x $binfolder/smc
|
||||
echo "[ 4 ] Make sure $binfolder is recreated and owned by root"
|
||||
sudo rm -rf "$binfolder" # start with an empty $binfolder and ensure there is no symlink or file at the path
|
||||
sudo install -d -m 755 -o root -g wheel "$binfolder"
|
||||
|
||||
echo "[ 4 ] Writing script to $binfolder/battery for user $calling_user"
|
||||
sudo cp $batteryfolder/battery.sh $binfolder/battery
|
||||
echo "[ 5 ] Install prebuilt smc binary into $binfolder"
|
||||
sudo install -m 755 -o root -g wheel "$batteryfolder/dist/smc" "$binfolder/smc"
|
||||
|
||||
echo "[ 5 ] Setting correct file permissions for $calling_user"
|
||||
# Set permissions for battery executables
|
||||
sudo chown -R $calling_user $binfolder/battery
|
||||
sudo chmod 755 $binfolder/battery
|
||||
sudo chmod +x $binfolder/battery
|
||||
echo "[ 6 ] Install battery script into $binfolder"
|
||||
sudo install -m 755 -o root -g wheel "$batteryfolder/battery.sh" "$binfolder/battery"
|
||||
|
||||
# Set permissions for logfiles
|
||||
echo "[ 7 ] Make sure the PATH environment variable includes '$binfolder'"
|
||||
if ! grep -qF "$binfolder" $path_configfile 2>/dev/null; then
|
||||
printf '%s\n' "$binfolder" | sudo tee "$path_configfile" >/dev/null
|
||||
fi
|
||||
sudo chown -h root:wheel $path_configfile
|
||||
sudo chmod -h 644 $path_configfile
|
||||
# Create a symlink for rare shells that do not initialize PATH from /etc/paths.d (including the current one)
|
||||
sudo mkdir -p /usr/local/bin
|
||||
sudo ln -sf "$binfolder/battery" /usr/local/bin/battery
|
||||
sudo chown -h root:wheel /usr/local/bin/battery
|
||||
# Create a link to smc as well to silence older GUI apps running with updated background executables
|
||||
# (consider removing in the next releases)
|
||||
sudo ln -sf "$binfolder/smc" /usr/local/bin/smc
|
||||
sudo chown -h root:wheel /usr/local/bin/smc
|
||||
|
||||
echo "[ 8 ] Set ownership and permissions for $configfolder"
|
||||
mkdir -p $configfolder
|
||||
sudo chown -R $calling_user $configfolder
|
||||
sudo chown -hRP $calling_user $configfolder
|
||||
sudo chmod -h 755 $configfolder
|
||||
|
||||
touch $logfile
|
||||
sudo chown $calling_user $logfile
|
||||
sudo chmod 755 $logfile
|
||||
sudo chown -h $calling_user $logfile
|
||||
sudo chmod -h 644 $logfile
|
||||
|
||||
touch $pidfile
|
||||
sudo chown $calling_user $pidfile
|
||||
sudo chmod 755 $pidfile
|
||||
sudo chown -h $calling_user $pidfile
|
||||
sudo chmod -h 644 $pidfile
|
||||
|
||||
sudo chown $calling_user $binfolder/battery
|
||||
# Fix permissions for 'create_daemon' action
|
||||
echo "[ 9 ] Fix ownership and permissions for $(dirname "$launch_agent_plist")"
|
||||
sudo chown -h $calling_user "$(dirname "$launch_agent_plist")"
|
||||
sudo chmod -h 755 "$(dirname "$launch_agent_plist")"
|
||||
sudo chown -hf $calling_user "$launch_agent_plist" 2>/dev/null
|
||||
|
||||
echo "[ 6 ] Setting up visudo declarations"
|
||||
sudo $batteryfolder/battery.sh visudo $USER
|
||||
sudo chown -R $calling_user $configfolder
|
||||
echo "[ 10 ] Setup visudo configuration"
|
||||
sudo $binfolder/battery visudo
|
||||
|
||||
# Remove tempfiles
|
||||
cd ../..
|
||||
echo "[ 7 ] Removing temp folder $tempfolder"
|
||||
echo "[ 11 ] Remove temp folder $tempfolder"
|
||||
rm -rf $tempfolder
|
||||
|
||||
echo -e "\n🎉 Battery tool installed. Type \"battery help\" for instructions.\n"
|
||||
|
||||
exit 0
|
||||
|
||||
80
update.sh
80
update.sh
@@ -1,32 +1,66 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Force-set path to include sbin
|
||||
PATH="$PATH:/usr/sbin"
|
||||
|
||||
# Set environment variables
|
||||
tempfolder=~/.battery-tmp
|
||||
binfolder=/usr/local/bin
|
||||
batteryfolder="$tempfolder/battery"
|
||||
mkdir -p $batteryfolder
|
||||
|
||||
echo -e "🔋 Starting battery update\n"
|
||||
|
||||
# Write battery function as executable
|
||||
# This script is running as root:
|
||||
# - Reset PATH to safe defaults at the very beginning of the script.
|
||||
# - Never include user-owned directories in PATH.
|
||||
PATH=/usr/bin:/bin:/usr/sbin:/sbin
|
||||
|
||||
echo "[ 1 ] Downloading latest battery version"
|
||||
rm -rf $batteryfolder
|
||||
mkdir -p $batteryfolder
|
||||
curl -sS -o $batteryfolder/battery.sh https://raw.githubusercontent.com/actuallymentor/battery/main/battery.sh
|
||||
# Define the installation directory for the battery background executables
|
||||
binfolder="/usr/local/co.palokaj.battery"
|
||||
|
||||
echo "[ 2 ] Writing script to $binfolder/battery"
|
||||
cp $batteryfolder/battery.sh $binfolder/battery
|
||||
chown $USER $binfolder/battery
|
||||
chmod 755 $binfolder/battery
|
||||
chmod u+x $binfolder/battery
|
||||
function is_launched_by_gui_app() {
|
||||
# Determine the process group ID (PGID) of the current process
|
||||
local this_process_pgid="$(ps -o pgid= -p $$ | tr -d ' ')"
|
||||
# Return 0 if any process in the same process group has battery.app or Electron.app in its command string
|
||||
ps -x -g $this_process_pgid -o command= -ww 2>/dev/null | grep -qE '(battery\.app|Electron\.app)' >&/dev/null
|
||||
}
|
||||
|
||||
# Remove tempfiles
|
||||
cd
|
||||
rm -rf $tempfolder
|
||||
echo "[ 3 ] Removed temporary folder"
|
||||
# If running as an unprivileged user and launched by GUI app
|
||||
if [[ $EUID -ne 0 ]] && is_launched_by_gui_app; then
|
||||
# This execution path is taken when GUI app version 1_3_2 or earlier launches update using
|
||||
# the battery script of the same version. This is expected when a new version of battery.sh
|
||||
# is released but the GUI app has not been updated yet.
|
||||
# Exit successfully with a silent warning to avoid disrupting the existing installation.
|
||||
printf "%s\n%s\n" \
|
||||
"The update to the next version requires root privileges." \
|
||||
"Run the updated menu bar GUI app or issue the 'battery update' command in Terminal."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Trigger reinstall for Terminal users to update from version 1_3_2 or earlier.
|
||||
# Consider removing the following if..fi block in future versions when you believe
|
||||
# that users are no longer using versions 1_3_2 or earlier. New versions of battery.sh are using
|
||||
# more comprehensive checks in 'battery update' in order to trigger 'battery reinstall' when needed.
|
||||
if [[ $EUID -ne 0 && ! -x "$binfolder/battery" ]]; then
|
||||
echo -e "💡 This battery update requires a full reinstall...\n"
|
||||
curl -sS "https://raw.githubusercontent.com/actuallymentor/battery/main/setup.sh" | bash
|
||||
$binfolder/battery maintain recover
|
||||
exit 0
|
||||
fi
|
||||
|
||||
echo -n "[ 1 ] Allocating temp folder: "
|
||||
tempfolder="$(mktemp -d)"
|
||||
echo "$tempfolder"
|
||||
function cleanup() { rm -rf "$tempfolder"; }
|
||||
trap cleanup EXIT
|
||||
|
||||
updatefolder="$tempfolder/battery"
|
||||
mkdir -p $updatefolder
|
||||
|
||||
echo "[ 2 ] Downloading the latest battery version"
|
||||
if ! curl -sS -o $updatefolder/battery.sh https://raw.githubusercontent.com/actuallymentor/battery/main/battery.sh; then
|
||||
err=$?
|
||||
echo -e "\n❌ Failed to download the update.\n"
|
||||
exit $err
|
||||
fi
|
||||
|
||||
echo "[ 3 ] Writing script to $binfolder/battery"
|
||||
sudo install -d -m 755 -o root -g wheel "$binfolder"
|
||||
sudo install -m 755 -o root -g wheel "$updatefolder/battery.sh" "$binfolder/battery"
|
||||
|
||||
echo "[ 4 ] Remove temporary folder"
|
||||
rm -rf "$tempfolder";
|
||||
|
||||
echo -e "\n🎉 Battery tool updated.\n"
|
||||
|
||||
Reference in New Issue
Block a user