├── .github └── FUNDING.yml ├── .gitignore ├── .shellcheckrc ├── ANALYSIS.md ├── CHANGELOG.md ├── INSTALLATION.md ├── LICENCE.md ├── README.md ├── bench_cpu.sh ├── cake-autorate.sh ├── cake-autorate.template ├── config.primary.sh ├── defaults.sh ├── example-uci-config.txt ├── fn_parse_autorate_log.m ├── images ├── bandwidth-compromise.png ├── cake-bandwidth-adaptation.png └── cake-bandwidth-autorate-rate-control.png ├── launcher.sh.template ├── lib.sh ├── maint ├── README.md ├── mdformat.sh ├── octave_formatter.py ├── update_setupsh_branch.sh └── version_bump.sh ├── setup.sh └── uninstall.sh /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: lynxthecat 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # for working with fn_parse_autorate_log.m 2 | cake-autorate.*.log 3 | cake-autorate.*.log.gz 4 | cake-autorate.*.log.mat 5 | output.*.* 6 | /.gitattributes 7 | 8 | # benchmarking scripts (tend to be named with a 1, 2, or 3 letter prefix) 9 | [a-z].sh 10 | [a-z][a-z].sh 11 | [a-z][a-z][a-z].sh 12 | -------------------------------------------------------------------------------- /.shellcheckrc: -------------------------------------------------------------------------------- 1 | # Double quote to prevent globbing and word splitting. 2 | disable=SC2086 3 | 4 | # Prefer double quoting even when variables don't contain special characters. 5 | disable=SC2248 6 | 7 | # Prefer explicit -n to check non-empty string (or use =/-ne to check boolean/integer). 8 | disable=SC2244 9 | 10 | # Multiple redirections compete for stdout. Use cat, tee, or pass filenames instead. 11 | disable=SC2261 12 | 13 | # Consider invoking this command separately to avoid masking its return value (or use '|| true' to ignore). 14 | disable=SC2312 15 | 16 | # Quote expansions in case patterns to match literally rather than as a glob. 17 | disable=SC2254 18 | 19 | # Enable all other optional checks 20 | enable=all 21 | 22 | # Allow shellcheck to access arbitrary files 23 | external-sources=true 24 | -------------------------------------------------------------------------------- /ANALYSIS.md: -------------------------------------------------------------------------------- 1 | # Analyzing cake-autorate data 2 | 3 | **cake-autorate** is a script that minimizes latency by adjusting CAKE 4 | bandwidth settings based on traffic load and round-trip time 5 | measurements. See the main [README](./README.md) page for more details 6 | of the algorithm. 7 | 8 | ## Viewing a simple summary of key statistics on the command line 9 | 10 | A simple summary of the key statistics can be generated on the command 11 | line so long as `output_reflector_stats` is enabled using e.g.: 12 | 13 | ```bash 14 | tail -f /var/log/cake-autorate.primary.log | grep -e SUMMARY 15 | ``` 16 | 17 | ## Logging options for a more detailed analysis 18 | 19 | Enabling `output_processing_stats` and `output_load_stats` is 20 | recommended for any detailed analysis and indeed necessary for either 21 | plotting tool described below. 22 | 23 | ## Exporting a Log File 24 | 25 | Extract a compressed log file from a running cake-autorate instance 26 | using one of these methods: 27 | 28 | 1. Run the auto-generated _log_file_export_ script inside the run 29 | directory: 30 | 31 | ```bash 32 | /var/run/cake-autorate/*/log_file_export 33 | ``` 34 | 35 | ... or ... 36 | 37 | 1. Send a USR1 signal to the main log file process(es) using: 38 | 39 | ```bash 40 | awk -F= '/^maintain_log_file=/ {print $2}' /var/run/cake-autorate/*/proc_pids | xargs kill -USR1 41 | ``` 42 | 43 | Either will place a compressed log file in _/var/log_ with the date 44 | and time in its filename. 45 | 46 | ## Resetting the Log File 47 | 48 | Force a log file reset on a running cake-autorate instance by using 49 | one of these methods: 50 | 51 | 1. Run the auto-generated log_file_rotate script inside the run 52 | directory: 53 | 54 | ```bash 55 | /var/run/cake-autorate/*/log_file_reset 56 | ``` 57 | 58 | ... or ... 59 | 60 | 1. Send a USR2 signal to the main log file process(es) using: 61 | 62 | ```bash 63 | awk -F= '/^maintain_log_file=/ {print $2}' /var/run/cake-autorate/*/proc_pids | xargs kill -USR2 64 | ``` 65 | 66 | ## Plotting the Log File 67 | 68 | The excellent Octave/Matlab program _fn_parse_autorate_log.m_ by 69 | @moeller0 of OpenWrt can read an exported log file and produce a 70 | helpful graph like this: 71 | 72 | 73 | 74 | The command below will run the Octave program (see the introductory 75 | notes in _fn_parse_autorate_log.m_ for more details): 76 | 77 | ```bash 78 | octave -qf --eval 'fn_parse_autorate_log("./log.gz", "./output.png")' 79 | ``` 80 | 81 | The script below can be run on a remote machine to extract the log 82 | from the router and generate the pdfs for viewing from the remote 83 | machine: 84 | 85 | ```bash 86 | log_file=$(ssh root@192.168.1.1 '/var/run/cake-autorate/primary/log_file_export 1>/dev/null && cat /var/run/cake-autorate/primary/last_log_file_export') && scp -O root@192.168.1.1:${log_file} . && ssh root@192.168.1.1 "rm ${log_file}" 87 | octave -qf --eval 'fn_parse_autorate_log("./*primary*log.gz", "./output.png")' 88 | ``` 89 | 90 | ### Prometheus cake-autorate exporter 91 | 92 | Check out [bairhys](https://github.com/bairhys)' 93 | [prometheus-cake-autorate-exporter](https://github.com/bairhys/prometheus-cake-autorate-exporter) 94 | for beautiful, continuous plotting of cake-autorate statistics: 95 | 96 | 97 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | **cake-autorate** is a script that minimizes latency by adjusting CAKE 4 | bandwidth settings based on traffic load and one-way-delay or 5 | round-trip time measurements. Read the [README](./README.md) file for 6 | more about cake-autorate. This is the history of changes. 7 | 8 | 9 | 10 | ## 2024-06-17 - Version 3.3.0 11 | 12 | - Improve shaper rate controller by reducing the shaper rate increases 13 | on high load as the average OWD delta approaches the delay threshold. 14 | For this purpose, a new lower average OWD delta threshold has been 15 | introduced beyond which the shaper rate increases are reduced from 16 | a maximum increase rate at the threshold to a minimum increase rate 17 | at the delay treshold. 18 | - Rename the various thresholds in the light of the new lower average 19 | OWD threshold for readability and consistency and add appropriate 20 | documentation for their use. 21 | - Include option to output CPU stats showing percentage usage per core. 22 | 23 | ## 2024-05-18 - Version 3.2.1 24 | 25 | - This release fixes setup.sh on OpenWRT and some documentation 26 | issues. If your installation worked fine there is no need to switch 27 | to this release as it only affects very specific and recent uclient 28 | versions only when downloading the tarballs. It would fail with an 29 | obvious error (`SSL error: SSL - Bad input parameters to function`). 30 | 31 | ## 2024-05-13 - Version 3.2.0 32 | 33 | - This version focuses on reducing CPU usage for everyday use. 34 | - Fold pinger parsing and pinger maintenance processes into main 35 | process thereby to reduce IPC overhead. This involved a significant 36 | restructure of the code. 37 | - Reduce overhead associated with achieved rate monitoring. 38 | - Improve IPC read efficiency of the main process. 39 | - Use buffered log file writes. 40 | - Replace costly regex with alternatives. 41 | - Disable costly logging options by default. 42 | - Improve efficiency of load classification. 43 | - Reduce frequency of shaper rate updates on high load by only 44 | permitting shaper rate increases on load updates and increasing the 45 | default shaper rate increase factor to give the same overall 46 | increase rate. 47 | - Only print log file headers to log file corresponding with toggled 48 | logging stats. 49 | - Remove Starlink satellite switching code given insufficient evidence 50 | that it helps especially given recent improvements to Starlink 51 | connections. 52 | - Add simple CPU benchmark script. 53 | - Add support for devices running Asus Merlin. 54 | - Other minor fixes and improvements. 55 | 56 | ## 2023-09-19 - Version 3.1.1 57 | 58 | - Update the example Starlink config to be compatible with v3.1 59 | 60 | ## 2023-09-18 - Version 3.1.0 61 | 62 | - Removed consulting the achieved rate when setting the new shaper 63 | rate on detection of bufferbloat. Whilst the achieved transfer rate 64 | on bufferbloat detection can give insight into the connection 65 | capacity, leveraging this effectively proved troublesome. 66 | - Introduced scaling of shaper rate reduction on bufferbloat based on 67 | the average OWD delta taken across the bufferbloat detection window 68 | as a portion of a user configurable average OWD delta threshold. 69 | - Amended existing DATA log lines for more consistency and to 70 | incorporate the average OWD deltas and compensated thresholds. 71 | - Introduced new SUMMARY log lines to offer a simple way to see a 72 | summary of key statistics using: `grep -e SUMMARY`. 73 | - Utilities that read from log file(s) will need to be updated to take 74 | into account the changes to the logging. 75 | - Fixed startup crash when log to file is disabled. 76 | - Fixed minor issue relating to parser termination traps. 77 | 78 | ## 2023-07-08 - Version 3.0.0 79 | 80 | - Version 3.0.0 of cake-autorate is the culmination of dozens of 81 | experiments, iterative improvements, and testing that are described 82 | in the 2.0.0 section below. To indicate that the current code 83 | contains significant enhancements (and avoid confusion), we decided 84 | to release this code with a new version number. 85 | 86 | ## 2023-07-05 - Version 2.0.0 87 | 88 | - This version restructures the bash code for improved robustness, 89 | stability and performance. 90 | - Employ FIFOs for passing not only data, but also instructions, 91 | between the major processes, obviating costly reliance on temporary 92 | files. A side effect of this is that now /var/run/cake-autorate is 93 | mostly empty during runs. 94 | - Significantly reduced CPU consumption - cake-autorate can now run 95 | successfully on older routers. 96 | - Introduce support for one way delays (OWDs) using the 'tsping' 97 | binary developed by @Lochnair. This works with ICMP type 13 98 | (timestamp) requests to ascertain the delay in each direction (i.e. 99 | OWDs). 100 | - Many changes to help catch and handle or expose unusual error 101 | conditions. 102 | - Fixed eternal sleep issue. 103 | - Introduce more user-friendly config format by introducing 104 | defaults.sh and config.X.sh with the basics (interface names, 105 | whether to adjust the shaper rates and the min, base and max shaper 106 | rates) and any overrides from the defaults defined in defaults.sh. 107 | - More intelligent check for another running instance. 108 | - Introduce more user-friendly log file exports by automatically 109 | generating an export script and a log reset script for each running 110 | cake-autorate instance inside /var/run/cake-autorate/\*/. 111 | - Added config file validation that checks all config file entries 112 | against those provided in defaults.sh. Firstly, the validation 113 | checks that the config file key finds a corresponding key in 114 | defaults.sh. And secondly, it checks that the value is of the same 115 | type out of array, integer, float, string, etc. Any identified 116 | problematic keys or values are reported to the user to assist with 117 | resolving any bad entries. 118 | - Improved installer and new uninstaller. 119 | - Many more fixes and improvements. 120 | - Particular thanks to @rany2 for his input on this version. 121 | 122 | ## 2022-12-13 - Version 1.2 123 | 124 | - cake-autorate now includes a sophisticated offline log file analysis 125 | utility written in Matlab/Octave: 'fn_parse_autorate_log.m' and 126 | maintained by @moeller0. This utility takes in a cake-autorate 127 | generated log file (in compressed or uncompressed format), which can 128 | be generated on the fly by sending an appropriate signal, and 129 | presents beautiful plots that depict latency and bandwidth over time 130 | together with many important cake-autorate vitals. This gratly 131 | simplifies assessing the efficacy of cake-autorate and associated 132 | settings on a given connection. 133 | - Multiple instances of cake-autorate is now supported. cake-autorate 134 | can now be run on multiple interfaces such as in the case of mwan3 135 | failover. The interface is assigned by designating an appropaite 136 | interface identifier 'X' in the config file in the form 137 | cake-autorate_config.X.sh. A launcher script has been created that 138 | creates one cake-autorate instance per cake-autorate_config file 139 | placed inside /root/cake-autorate/. Log files are generated for each 140 | instance using the form /var/log/cake-autorate.X.log. The interface 141 | identifier 'X' cannot be empty. 142 | - Improved reflector management. With a relatively high frequency 143 | (default 1 minute) cake-autorate now compares reflector baselines 144 | and deltas and rotates out reflectors with either baselines that are 145 | excessively higher than the minimum or deltas that are too close to 146 | the trigger threshold. And with a relatively low frequency (default 147 | 60 minutes), cake-autorate now randomly rotates out a reflector from 148 | the presently active list. This simple algorithm is intended to 149 | converge upon a set of good reflectors from the intitial starting 150 | set. The initial starting set is now also randomized from the 151 | provided list of reflectors. The user is still encouraged to test 152 | the initial reflector list to rule out any particularly far away or 153 | highly variable reflectors. 154 | - Reflector stats may now optionally be printed to help monitor the 155 | efficacy of the reflector management and quality of the present 156 | reflectors. 157 | - LOAD stats may now optionally be printed to monitor achieved rates 158 | during sleep periods when pingers are shutdown. 159 | - For each new sample, the baseline is now subtracted after having 160 | been updated rather than before having been updated. 161 | - Pinger prefix and arguments are now facilitated for the chosen 162 | pinger binary to help improve compatibility with mwan3. 163 | - Consideration was afforded to switching over to the use of SMA 164 | rather than EWMA for reflector baselines, but SMA was found to offer 165 | minimal improvement as compared to EWMA with appropriately chosen 166 | alpha values. The present use of EWMA with multiple alphas for 167 | increase and decrease enables tracking of either reflector owd 168 | minimums (conservative default) or averages (by setting alphas to 169 | around e.g. 0.095). 170 | - User can now specify own log path, e.g. in case of logging out to 171 | cloud mount using rclone or USB stick 172 | 173 | ## 2022-09-28 - Version 1.1 174 | 175 | Implemented several new features such as: 176 | 177 | - Switch default pinger binary to fping - it was identified that using 178 | concurrent instances of iputils-ping resulted in drift between ICMP 179 | requests, and fping solves this because it offers round robin 180 | pinging to multiple reflectors with tightly controlled timing 181 | between requests 182 | - Generalised pinger functions to support wrappers for different ping 183 | binaries - fping and iputils-ping now specifically supported and 184 | handled, and new ping binaries can easily be added by including 185 | appropriate wrapper functions. 186 | - Generalised code to work with one way delays (OWDs) from RTTs in 187 | preparation to use ICMP type 13 requests 188 | - Only use capacity estimate on bufferbloat detection where the 189 | adjusted shaper rate based thereon would exceed the minimum 190 | configured shaper rate (avoiding the situation where e.g. idle load 191 | on download during upload-related bufferbloat would cause download 192 | shaper rate to get punished all the way down to the minimum) 193 | - Stall detection and handling 194 | - Much better log file handling including defaulting to logging, 195 | supporting logging even when running from console, log file rotation 196 | on configured time elapsed or configured bytes written to 197 | 198 | ## 2022-08-21 - Version 1.0 199 | 200 | - New installer script - cake-autorate-setup.sh - now installs all 201 | required files 202 | - Installer checks for presence of previous config and asks whether to 203 | overwrite 204 | - Installer also copies the service script into 205 | `/etc/init.d/cake-autorate` 206 | - Installer does NOT start the software, but displays instructions for 207 | config and starting 208 | - At startup, display version number and interface name and configured 209 | speeds 210 | - Abort if the configured interfaces do not exist 211 | - Style guide: the name of the algorithm and repo is "cake-autorate" 212 | - All "cake-autorate..." filenames are lower case 213 | - New log_msg() function that places a simple time stamp on the each 214 | line 215 | - Moved images to their own directory 216 | - No other new/interesting functionality 217 | 218 | ## 2022-07-01 219 | 220 | - Significant testing with a Starlink connection (thanks to @gba) 221 | - Have added code to compensate for Starlink satelite switch times to 222 | preemptively reduce shaper rates prior to switch thereby to help 223 | prevent or at least reduce the otherwise large RTT spikes associate 224 | with the switching 225 | 226 | ## 2022-06-07 227 | 228 | - Add optional startup delay 229 | - Fix octal/base issue on calculation of loads by forcing base 10 230 | - Prevent crash on interface reset in which rx/tx_bytes counters are 231 | reset by checking for negative achieved rates and setting to zero 232 | - Verify interfaces are up on startup and on main loop exit (and wait 233 | as necessary for them to come up) 234 | 235 | ## 2022-06-02 236 | 237 | - No further changes - author now runs this code 24/7 as a service and 238 | it seems to **just work** 239 | 240 | ## 2022-04-25 241 | 242 | - Included reflector health monitoring and support for reflector 243 | rotation upon detection of bad reflectors 244 | - **Overall the code now seems to work very well and seems to have 245 | reached a mature stage** 246 | 247 | ## 2022-04-19 248 | 249 | - Many further optimizations to reduce CPU use and improve performance 250 | - Replaced coreutils-sleep with 'read -t' on dummy fifo to use bash 251 | inbuilt 252 | - Added various features to help with weaker LTE connections 253 | - Implemented significant number of robustifications 254 | 255 | ## 2022-03-21 256 | 257 | - Huge reworking of cake-autorate. Now individual processes ping a 258 | reflector, maintain a baseline, and write out result lines to a 259 | common FIFO that is read in by a main loop and processed. Several 260 | optimisations have been effected to reduce CPU load. Sleep 261 | functionality has been added to put the pinging processes to sleep 262 | when the connection is not being used and to wake back up when the 263 | connection is used again - this saves unecessary CPU cycles and 264 | issuing pings throughout the 'wee' hours of the night. 265 | - This script seems to be working very well on the author's LTE 266 | conneciton. The author personally uses it as a service 24/7 now. 267 | 268 | ## 2022-02-18 269 | 270 | - Altered cake-autorate to employ inotifywait for main loop ticks 271 | - Now main loops ticks are triggered either by a delay event or tick 272 | trigger (whichever comes first) 273 | 274 | ## 2022-02-17 275 | 276 | - Completed and uploaded to new cake-autorate branch completely new 277 | bash implementation 278 | - This will likely be the future for this project 279 | 280 | ## 2022-02-04 281 | 282 | - Created new experimental-rapid-tick branch in which pings are made 283 | asynchronous with the main loop offering significantly more rapid 284 | ticks 285 | - Corrected main and both experimental branches to work with min RTT 286 | output from each ping call (not average) 287 | 288 | ## 2021-12-11 289 | 290 | - Modified tick duration to 1s and timeout duration to 0.8 seconds in 291 | 'owd' code 292 | - This seems to give an owd routine that mostly works 293 | - Tested how 'owd' codes under independent upload and ownload 294 | saturations and it seems to work well 295 | - Much optimisation still needed 296 | 297 | ## 2021-12-10 298 | 299 | - Extensive development of 'owd' code 300 | - Noticed tick duration 0.5s would result in slowdown during heavy 301 | usage owing to hping3 1s timeout 302 | - Implemented timeout functionality to kill hping3 calls that take 303 | longer than 0.X seconds 304 | - @Failsafe's awk parser a total joy to use! 305 | 306 | ## 2021-12-9 307 | 308 | - Based on discussion in OpenWrt CAKE /w Adaptive Bandwidth thread 309 | created new 'owd' branch 310 | - Adapted code to employ timestamp ICMP type 13 requests to try to 311 | ascertain direction of bufferbloat 312 | - On OpenWrt CAKE /w Adaptive Bandwidth thread much testing/discussion 313 | around various ping utilities 314 | - nping found to support ICMP type 13 but slow and unreliable 315 | - settled on hping3 as identified by @Locknair (OpenWrt forum) as ery 316 | efficient and timing information proves reliable 317 | - @Failsafe demonstrated awk mastery by writing awk parser to handle 318 | output of hping3 319 | 320 | ## 2021-12-6 321 | 322 | - Reverted to old behaviour of decrementing both downlink and uplink 323 | rates upon bufferbloat detection 324 | - Whilst guestimating direction of bufferbloat based on load is a nice 325 | idea/hack, it proved dangerous and unreliable 326 | - Namely, suppose downlink load is 0.8 and uplink load is 0.4 and it 327 | is uplink that causes bufferbloat 328 | - In this situation, decrementing downlink rate (because this is the 329 | heavily loaded direction) does not solve 330 | - The bufferbloat, and this could result in downlink bandwidth being 331 | punished down to zero 332 | 333 | ## 2021-12-4 334 | 335 | - @richb-hanover encourages use of single rate rather than min/max 336 | rates to help simplify things 337 | - 'experimental' branch created that takes single uplink and downlink 338 | rates and adjusts rates based on those 339 | - Seems to work but needs optimisation 340 | - Tried out idea in 'experimental' branch of decrementing only 341 | direction that is heavily loaded upon detection of bufferbloat 342 | - It mostly works, but edge cases may break it 343 | 344 | ## 2021-11-30 and early December 345 | 346 | - @richb-hanover encourages use of documentation and helps with 347 | creation of readme. 348 | - Readme developed to help users 349 | 350 | ## 2021-11-23 351 | 352 | - Mysterious individual @dim-geo helpfuly replaces bc calls with awk 353 | calls 354 | - And also simplifies awk calls 355 | 356 | ## 2021-late October to early Novermver 357 | 358 | - Basic routine tested and adjusted based on testing on 4G connection 359 | - @moeller0 helps tidy up code 360 | 361 | ## 2021-10-19 362 | 363 | - sqm-autorate is born! 364 | - A brief history: 365 | - @Lynx (OpenWrt forum) wondered about simple algorith along the 366 | lines: 367 | - if load \< 50% of minimum set load then assume no load and update 368 | moving average of unloaded ping to 8.8.8.8 if load > 50% of minimum 369 | set load acquire set of sample points by pinging 8.8.8.8 and acquire 370 | sample mean measure bufferbloat by subtracting moving average of 371 | unloaded ping from sample mean ascertain load during sample 372 | acquisition and make bandwidth increase or decrease decision based 373 | on determined load and determination of bufferbloat or not 374 | - And @Lynx asked SQM/CAKE expert @moeller0 (OpenWrt forum) to suggest 375 | a basic algorithm. 376 | - @moeller0 suggested the following approach: 377 | 378 | - @Lynx wrote a shell script to implement this routine 379 | -------------------------------------------------------------------------------- /INSTALLATION.md: -------------------------------------------------------------------------------- 1 | # Installing cake-autorate 2 | 3 | **cake-autorate** is a script that minimizes latency by adjusting CAKE 4 | bandwidth settings based on traffic load and round-trip time 5 | measurements. See the main [README](./README.md) page for more details 6 | of the algorithm. 7 | 8 | ## Installation Steps (OpenWrt) 9 | 10 | cake-autorate provides an installation script that installs all the 11 | required tools. To use it: 12 | 13 | - Install SQM (`luci-app-sqm`) and enable and configure `cake` Queue 14 | Discipline on the interface(s) as described in the 15 | [OpenWrt SQM documentation](https://openwrt.org/docs/guide-user/network/traffic-shaping/sqm) 16 | 17 | - Alternatively, and especially if you may have more complex 18 | networking needs: 19 | 20 | - DSCPs - consider 21 | [cake-simple-qos](https://github.com/lynxthecat/cake-qos-simple); 22 | - WireGuard with PBR - consider 23 | [cake-dual-ifb](https://github.com/lynxthecat/cake-dual-ifb). 24 | 25 | - [SSH into the router](https://openwrt.org/docs/guide-quick-start/sshadministration) 26 | 27 | - Ensure `bash` and `fping` are installed. 28 | 29 | On most OpenWrt installations, you can install them by running: 30 | 31 | ```bash 32 | opkg update 33 | opkg install bash fping 34 | ``` 35 | 36 | If the `opkg` command is not found, you may need to use `apk` 37 | instead: 38 | 39 | ```bash 40 | apk update 41 | apk add bash fping 42 | ``` 43 | 44 | - Use the installer script by copying and pasting each of the commands 45 | below. The commands retrieve the current version from this repo: 46 | 47 | ```bash 48 | wget -O /tmp/cake-autorate_setup.sh https://raw.githubusercontent.com/lynxthecat/cake-autorate/master/setup.sh 49 | sh /tmp/cake-autorate_setup.sh 50 | ``` 51 | 52 | - The installer script will detect a previous configuration file, and 53 | ask whether to preserve it. 54 | 55 | ## Installation Steps (Asus Merlin) 56 | 57 | - From the Asus Merlin GUI: enable adaptive QOS and select cake. 58 | 59 | - [SSH into the router](https://github.com/RMerl/asuswrt-merlin.ng/wiki/SSHD) 60 | 61 | - Make sure these are installed: entware; coreutils-mktemp; jsonfilter; bash; 62 | and iputils-ping or fping. 63 | 64 | - Firstly, if not already installed, 65 | [install entware](https://github.com/RMerl/asuswrt-merlin.ng/wiki/Entware); 66 | - and then run: 67 | 68 | ```bash 69 | opkg update 70 | opkg install coreutils-mktemp jsonfilter bash fping 71 | ``` 72 | 73 | - Use the installer script by copying and pasting each of the commands 74 | below. The commands retrieve the current version from this repo: 75 | 76 | ```bash 77 | wget -O /tmp/cake-autorate_setup.sh https://raw.githubusercontent.com/lynxthecat/cake-autorate/master/setup.sh 78 | sh /tmp/cake-autorate_setup.sh 79 | ``` 80 | 81 | ## Initial Configuration Steps (OpenWrt and Asus Merlin) 82 | 83 | - For a fresh install, you will need to undertake the following steps. 84 | 85 | - Edit the _config.primary.sh_ script using vi or nano to set the 86 | configuration parameters below (see comments in _config.primary.sh_ 87 | for details). 88 | 89 | - **OpenWrt:** in the _/root/cake-autorate_ directory 90 | - **Asus Merlin:** in the _/jffs/configs/cake-autorate_ directory 91 | 92 | In the configuration file: 93 | 94 | - Change `dl_if` and `ul_if` to match the names of the upload and 95 | download interfaces to which CAKE is applied. 96 | 97 | | Variable | Setting | 98 | | -------: | :----------------------------------------------- | 99 | | `dl_if` | Interface that downloads data (often _ifb4-wan_) | 100 | | `ul_if` | Interface that uploads (often _wan_) | 101 | 102 | - For OpenWrt installations, these can be obtained, for example, by 103 | consulting the configured SQM settings in LuCi or by examining the 104 | output of `tc qdisc ls`. 105 | 106 | - For Asus Merlin the requisite interfaces can also be obtained by 107 | examining the output of `tc qdisc ls`. These are most likely: 108 | 109 | ```bash 110 | dl_if=ifb4eth0 # download interface 111 | ul_if=eth0 # upload interface 112 | ``` 113 | 114 | - Choose whether cake-autorate should adjust the shaper rates (disable 115 | for monitoring only): 116 | 117 | | Variable | Setting | 118 | | ----------------------: | :----------------------------------------- | 119 | | `adjust_dl_shaper_rate` | enable (1) or disable (0) download shaping | 120 | | `adjust_ul_shaper_rate` | enable (1) or disable (0) upload shaping | 121 | 122 | - Set bandwidth variables as described in _config.primary.sh_. 123 | 124 | | Type | Download | Upload | 125 | | ---: | :------------------------- | :------------------------- | 126 | | Min. | `min_dl_shaper_rate_kbps` | `min_ul_shaper_rate_kbps` | 127 | | Base | `base_dl_shaper_rate_kbps` | `base_ul_shaper_rate_kbps` | 128 | | Max. | `max_dl_shaper_rate_kbps` | `max_ul_shaper_rate_kbps` | 129 | 130 | - Set connection idle variable as described in _config.primary.sh_. 131 | 132 | | Variable | Setting | 133 | | ---------------------------: | :------------------------------------------------------- | 134 | | `connection_active_thr_kbps` | threshold in Kbit/s below which dl/ul is considered idle | 135 | 136 | ## Configuration of cake-autorate 137 | 138 | cake-autorate is highly configurable and almost every aspect of it can 139 | be (and is ideally) fine-tuned. 140 | 141 | - The file _defaults.sh_ has sensible default settings. After 142 | cake-autorate has been installed, you may wish to override some of 143 | these by providing corresponding entries inside _config.primary.sh_. 144 | 145 | - For example, to set a different `dl_owd_delta_delay_thr_ms`, then 146 | add a line to the config file _config.primary.sh_ like: 147 | 148 | ```bash 149 | dl_owd_delta_delay_thr_ms=100.0 150 | ``` 151 | 152 | - Users are encouraged to look at _defaults.sh_, which documents the 153 | many configurable parameters of cake-autorate. 154 | 155 | - The type of variable: integer, float, string used in any config file 156 | must reflect the same type used in _defaults.sh_, and otherwise 157 | cake-autorate will throw an error on startup. 158 | 159 | ## Delay thresholds 160 | 161 | - At least the following variables relating to the delay thresholds 162 | may warrant overriding depending on the connection particulars. 163 | 164 | | Variable | Setting | 165 | | ------------------------: | :----------------------------------------------------------------------------------------------------------- | 166 | | `dl_owd_delta_delay_thr_ms` | extent of download OWD increase to classify as a delay | 167 | | `ul_owd_delta_delay_thr_ms` | extent of upload OWD increase to classify as a delay | 168 | | `dl_avg_owd_delta_max_adjust_up_thr_ms` | average download OWD threshold across reflectors at which maximum upward shaper rate adjustment is applied | 169 | | `ul_avg_owd_delta_max_adjust_up_thr_ms` | average upload OWD threshold across reflectors at which maximum upward shaper rate adjustment is applied | 170 | | `dl_avg_owd_delta_max_adjust_down_thr_ms` | average download OWD threshold across reflectors at which maximum downward shaper rate adjustment is applied | 171 | | `ul_avg_owd_delta_max_adjust_down_thr_ms` | average upload OWD threshold across reflectors at which maximum downward shaper rate adjustment is applied | 172 | 173 | 174 | An OWD measurement to an individual reflector that exceeds 175 | `xl_owd_delta_delay_thr_ms` from its baseline is classified as a 176 | delay. Bufferbloat is detected when there are 177 | `bufferbloat_detection_thr` delays out of the last 178 | `bufferbloat_detection_window` reflector responses. 179 | 180 | Prior to bufferbloat detection, the extent of the average OWD 181 | delta taken across the reflectors governs how much the shaper 182 | rate is adjusted up. The adjustment is scaled linearly from 183 | `shaper_rate_max_adjust_up_load_high` (at or below 184 | xl_avg_owd_delta_max_adjust_up_thr_ms) 185 | to `shaper_rate_min_adjust_up_load_high` (at 186 | xl_owd_delta_thr_ms). 187 | 188 | Upon bufferbloat detection, the extent of the average OWD delta 189 | taken across the reflectors governs how much the shaper rate is 190 | adjusted down. The adjustment is scaled linearly from 191 | `shaper_rate_min_adjust_down_bufferbloat` (at 192 | xl_owd_delta_thr_ms) 193 | to `shaper_rate_min_adjust_down_bufferbloat` (at or above 194 | xl_avg_owd_delta_max_adjust_down_thr_ms). 195 | 196 | Avoiding bufferbloat requires throttling the connection, and thus 197 | there is a trade-off between bandwidth and latency. 198 | 199 | The delay thresholds affect how much the shaper rate is punished 200 | responsive to latency increase. Users that want very low latency 201 | at all times (at the expense of bandwidth) will want lower values. 202 | Users that can tolerate higher latency excursions (facilitating 203 | greater bandwidth). 204 | 205 | Although the default parameters have been designed to offer 206 | something that might work out of the box for certain connections, 207 | some analysis is likely required to optimize cake-autorate for the 208 | specific use-case. 209 | 210 | Read about this in the [ANALYSIS](./ANALYSIS.md) page. 211 | 212 | ## Reflectors 213 | 214 | - Additionally, the following variables relating to reflectors may 215 | also warrant overriding: 216 | 217 | | Variable | Setting | 218 | | ------------------------: | :-------------------------------------- | 219 | | `reflectors` | list of reflectors | 220 | | `no_pingers` | number of reflectors to ping | 221 | | `reflector_ping_interval` | interval between pinging each reflector | 222 | 223 | Reflector choice is a crucial parameter for cake-autorate. 224 | 225 | By default, cake-autorate sends ICMPs to various large anycast DNS 226 | hosts (Cloudflare, Google, Quad9, etc.). 227 | 228 | It is the responsibility of the user to ensure that the configured 229 | reflectors provide stable, low-latency responses. 230 | 231 | Some governments appear to block DNS hosts like Google. Users 232 | affected by the same will need to determine appropriate 233 | alternative reflectors. 234 | 235 | cake-autorate monitors the responses from reflectors and 236 | automatically kicks out bad reflectors. The parameters governing 237 | the same are configurable in the config file (see _defaults.sh_). 238 | 239 | ## Logging 240 | 241 | - The following variables control logging: 242 | 243 | | Variable | Setting | 244 | | -----------------------------: | :---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | 245 | | `output_processing_stats` | If non-zero, log the results of every iteration through the process | 246 | | `output_load_stats` | If non-zero, log the log the measured achieved rates of upload and download | 247 | | `output_reflector_stats` | If non-zero, log the statistics generated in respect of reflector health monitoring | 248 | | `output_summary_stats` | If non-zero, log a summary with the key statistics | 249 | | `output_cake_changes` | If non-zero, log when changes are made to CAKE settings via `tc` - this shows when cake-autorate is adjusting the shaper | 250 | | `output_cpu_stats` | If non-zero, monitor and log CPU usage percentages across the detected cores | 251 | | `output_cpu_raw_stats` | If non-zero, log the raw CPU usage lines obtained during CPU usage monitoring | 252 | | `debug` | If non-zero, debug lines will be output | 253 | | `log_DEBUG_messages_to_syslog` | If non-zero, log lines will also get sent to the system log | 254 | | `log_to_file` | If non-zero, log lines will be sent to /tmp/cake-autorate.log regardless of whether printing to console `log_file_max_time_mins` have elapsed or `log_file_max_size_KB` has been exceeded | 255 | | `log_file_max_time_mins` | Number of minutes to elapse between log file rotaton | 256 | | `log_file_max_size_KB` | Number of KB (i.e. bytes/1024) worth of log lines between log file rotations | 257 | 258 | ## Manual testing 259 | 260 | To start the `cake-autorate.sh` script and watch the logged output as 261 | it adjusts the CAKE parameters, run these commands: 262 | 263 | ```bash 264 | cd /root/cake-autorate # to the cake-autorate directory 265 | ./cake-autorate.sh 266 | ``` 267 | 268 | - Monitor the script output to see how it adjusts the download and 269 | upload rates as you use the connection. 270 | - Press ^C to halt the process. 271 | 272 | ## Install as a service (OpenWrt) 273 | 274 | You can install cake-autorate as a service that starts up the autorate 275 | process whenever the router reboots. To do this: 276 | 277 | - [SSH into the router](https://openwrt.org/docs/guide-quick-start/sshadministration) 278 | 279 | - Run these commands to enable and start the service file: 280 | 281 | ```bash 282 | # the setup.sh script already installed the service file 283 | service cake-autorate enable 284 | service cake-autorate start 285 | ``` 286 | 287 | If you edit any of the configuration files, you will need to restart 288 | the service with `service cake-autorate restart` 289 | 290 | When running as a service, the `cake-autorate.sh` script outputs to 291 | _/var/log/cake-autorate.primary.log_ (observing the instance 292 | identifier _cake-autorate_config.identifier.sh_ set in the config file 293 | name). 294 | 295 | WARNING: Take care to ensure sufficient free memory exists on router 296 | to handle selected logging parameters. Consider disabling logging or 297 | adjusting logging parameters such as `log_file_max_time_mins` or 298 | `log_file_max_size_KB` if necessary. 299 | 300 | ## Launch on Boot (Asus Merlin) 301 | 302 | cake-autorate can be launched on boot by adding an appropriate entry 303 | to e.g. post-mount - see 304 | [here](https://github.com/RMerl/asuswrt-merlin.ng/wiki/User-scripts). 305 | 306 | For example, add these lines to /jffs/scripts/post-mount: 307 | 308 | ```bash 309 | source /etc/profile 310 | /jffs/scripts/cake-autorate/launcher.sh 311 | ``` 312 | 313 | ## Preserving cake-autorate files for backup or upgrades (OpenWrt) 314 | 315 | OpenWrt devices can save files across upgrades. Read the 316 | [Backup and Restore page on the OpenWrt wiki](https://openwrt.org/docs/guide-user/troubleshooting/backup_restore#customize_and_verify) 317 | for details. 318 | 319 | To ensure the cake-autorate script and configuration files are 320 | preserved, enter the files below to the OpenWrt router's 321 | [Configuration tab](https://openwrt.org/docs/guide-user/troubleshooting/backup_restore#back_up) 322 | 323 | ```bash 324 | /root/cake-autorate 325 | /etc/init.d/cake-autorate 326 | ``` 327 | 328 | ## Multi-WAN Setups 329 | 330 | - cake-autorate has been designed to run multiple instances 331 | simultaneously. 332 | - cake-autorate will run one instance per config file present in the 333 | _/root/cake-autorate/_ directory in the form: 334 | 335 | ```bash 336 | config.instance.sh 337 | ``` 338 | 339 | where 'instance' is replaced with e.g. 'primary', 'secondary', etc. 340 | 341 | ## Selecting a "ping binary" 342 | 343 | cake-autorate reads the `$pinger_binary` variable in the config file 344 | to select the ping binary. Choices include: 345 | 346 | - **fping** (DEFAULT) round robin pinging to multiple reflectors with 347 | tightly controlled timings 348 | - **tsping** round robin ICMP type 13 pinging to multiple reflectors 349 | with tightly controlled timings 350 | - **iputils-ping** more advanced pinging than the default busybox ping 351 | with sub 1s ping frequency 352 | 353 | **About tsping** @Lochnair has coded up an elegant ping utility in C 354 | that sends out ICMP type 13 requests in a round robin manner, thereby 355 | facilitating determination of one way delays (OWDs), i.e. not just 356 | round trip time (RTT), but the constituent download and upload delays, 357 | relative to multiple reflectors. Presently this must be compiled 358 | manually (although we can expect an official OpenWrt package soon). 359 | 360 | Instructions for building a `tsping` OpenWrt package are available 361 | [from github.](https://github.com/Lochnair/tsping) 362 | -------------------------------------------------------------------------------- /LICENCE.md: -------------------------------------------------------------------------------- 1 | # GNU General Public License 2 | 3 | _Version 2, June 1991_\ 4 | _Copyright © 1989, 1991 Free Software 5 | Foundation, Inc.,_\ 6 | _51 Franklin Street, Fifth Floor, Boston, MA 7 | 02110-1301 USA_ 8 | 9 | Everyone is permitted to copy and distribute verbatim copies of this 10 | license document, but changing it is not allowed. 11 | 12 | ### Preamble 13 | 14 | The licenses for most software are designed to take away your freedom 15 | to share and change it. By contrast, the GNU General Public License is 16 | intended to guarantee your freedom to share and change free 17 | software--to make sure the software is free for all its users. This 18 | General Public License applies to most of the Free Software 19 | Foundation's software and to any other program whose authors commit to 20 | using it. (Some other Free Software Foundation software is covered by 21 | the GNU Lesser General Public License instead.) You can apply it to 22 | your programs, too. 23 | 24 | When we speak of free software, we are referring to freedom, not 25 | price. Our General Public Licenses are designed to make sure that you 26 | have the freedom to distribute copies of free software (and charge for 27 | this service if you wish), that you receive source code or can get it 28 | if you want it, that you can change the software or use pieces of it 29 | in new free programs; and that you know you can do these things. 30 | 31 | To protect your rights, we need to make restrictions that forbid 32 | anyone to deny you these rights or to ask you to surrender the rights. 33 | These restrictions translate to certain responsibilities for you if 34 | you distribute copies of the software, or if you modify it. 35 | 36 | For example, if you distribute copies of such a program, whether 37 | gratis or for a fee, you must give the recipients all the rights that 38 | you have. You must make sure that they, too, receive or can get the 39 | source code. And you must show them these terms so they know their 40 | rights. 41 | 42 | We protect your rights with two steps: **(1)** copyright the software, 43 | and **(2)** offer you this license which gives you legal permission to 44 | copy, distribute and/or modify the software. 45 | 46 | Also, for each author's protection and ours, we want to make certain 47 | that everyone understands that there is no warranty for this free 48 | software. If the software is modified by someone else and passed on, 49 | we want its recipients to know that what they have is not the 50 | original, so that any problems introduced by others will not reflect 51 | on the original authors' reputations. 52 | 53 | Finally, any free program is threatened constantly by software 54 | patents. We wish to avoid the danger that redistributors of a free 55 | program will individually obtain patent licenses, in effect making the 56 | program proprietary. To prevent this, we have made it clear that any 57 | patent must be licensed for everyone's free use or not licensed at 58 | all. 59 | 60 | The precise terms and conditions for copying, distribution and 61 | modification follow. 62 | 63 | ### TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 64 | 65 | **0.** This License applies to any program or other work which 66 | contains a notice placed by the copyright holder saying it may be 67 | distributed under the terms of this General Public License. The 68 | “Program”, below, refers to any such program or work, and a “work 69 | based on the Program” means either the Program or any derivative work 70 | under copyright law: that is to say, a work containing the Program or 71 | a portion of it, either verbatim or with modifications and/or 72 | translated into another language. (Hereinafter, translation is 73 | included without limitation in the term “modification”.) Each licensee 74 | is addressed as “you”. 75 | 76 | Activities other than copying, distribution and modification are not 77 | covered by this License; they are outside its scope. The act of 78 | running the Program is not restricted, and the output from the Program 79 | is covered only if its contents constitute a work based on the Program 80 | (independent of having been made by running the Program). Whether that 81 | is true depends on what the Program does. 82 | 83 | **1.** You may copy and distribute verbatim copies of the Program's 84 | source code as you receive it, in any medium, provided that you 85 | conspicuously and appropriately publish on each copy an appropriate 86 | copyright notice and disclaimer of warranty; keep intact all the 87 | notices that refer to this License and to the absence of any warranty; 88 | and give any other recipients of the Program a copy of this License 89 | along with the Program. 90 | 91 | You may charge a fee for the physical act of transferring a copy, and 92 | you may at your option offer warranty protection in exchange for a 93 | fee. 94 | 95 | **2.** You may modify your copy or copies of the Program or any 96 | portion of it, thus forming a work based on the Program, and copy and 97 | distribute such modifications or work under the terms of Section 1 98 | above, provided that you also meet all of these conditions: 99 | 100 | - **a)** You must cause the modified files to carry prominent notices 101 | stating that you changed the files and the date of any change. 102 | - **b)** You must cause any work that you distribute or publish, that 103 | in whole or in part contains or is derived from the Program or any 104 | part thereof, to be licensed as a whole at no charge to all third 105 | parties under the terms of this License. 106 | - **c)** If the modified program normally reads commands interactively 107 | when run, you must cause it, when started running for such 108 | interactive use in the most ordinary way, to print or display an 109 | announcement including an appropriate copyright notice and a notice 110 | that there is no warranty (or else, saying that you provide a 111 | warranty) and that users may redistribute the program under these 112 | conditions, and telling the user how to view a copy of this License. 113 | (Exception: if the Program itself is interactive but does not 114 | normally print such an announcement, your work based on the Program 115 | is not required to print an announcement.) 116 | 117 | These requirements apply to the modified work as a whole. If 118 | identifiable sections of that work are not derived from the Program, 119 | and can be reasonably considered independent and separate works in 120 | themselves, then this License, and its terms, do not apply to those 121 | sections when you distribute them as separate works. But when you 122 | distribute the same sections as part of a whole which is a work based 123 | on the Program, the distribution of the whole must be on the terms of 124 | this License, whose permissions for other licensees extend to the 125 | entire whole, and thus to each and every part regardless of who wrote 126 | it. 127 | 128 | Thus, it is not the intent of this section to claim rights or contest 129 | your rights to work written entirely by you; rather, the intent is to 130 | exercise the right to control the distribution of derivative or 131 | collective works based on the Program. 132 | 133 | In addition, mere aggregation of another work not based on the Program 134 | with the Program (or with a work based on the Program) on a volume of 135 | a storage or distribution medium does not bring the other work under 136 | the scope of this License. 137 | 138 | **3.** You may copy and distribute the Program (or a work based on it, 139 | under Section 2) in object code or executable form under the terms of 140 | Sections 1 and 2 above provided that you also do one of the following: 141 | 142 | - **a)** Accompany it with the complete corresponding machine-readable 143 | source code, which must be distributed under the terms of Sections 1 144 | and 2 above on a medium customarily used for software interchange; 145 | or, 146 | - **b)** Accompany it with a written offer, valid for at least three 147 | years, to give any third party, for a charge no more than your cost 148 | of physically performing source distribution, a complete 149 | machine-readable copy of the corresponding source code, to be 150 | distributed under the terms of Sections 1 and 2 above on a medium 151 | customarily used for software interchange; or, 152 | - **c)** Accompany it with the information you received as to the 153 | offer to distribute corresponding source code. (This alternative is 154 | allowed only for noncommercial distribution and only if you received 155 | the program in object code or executable form with such an offer, in 156 | accord with Subsection b above.) 157 | 158 | The source code for a work means the preferred form of the work for 159 | making modifications to it. For an executable work, complete source 160 | code means all the source code for all modules it contains, plus any 161 | associated interface definition files, plus the scripts used to 162 | control compilation and installation of the executable. However, as a 163 | special exception, the source code distributed need not include 164 | anything that is normally distributed (in either source or binary 165 | form) with the major components (compiler, kernel, and so on) of the 166 | operating system on which the executable runs, unless that component 167 | itself accompanies the executable. 168 | 169 | If distribution of executable or object code is made by offering 170 | access to copy from a designated place, then offering equivalent 171 | access to copy the source code from the same place counts as 172 | distribution of the source code, even though third parties are not 173 | compelled to copy the source along with the object code. 174 | 175 | **4.** You may not copy, modify, sublicense, or distribute the Program 176 | except as expressly provided under this License. Any attempt otherwise 177 | to copy, modify, sublicense or distribute the Program is void, and 178 | will automatically terminate your rights under this License. However, 179 | parties who have received copies, or rights, from you under this 180 | License will not have their licenses terminated so long as such 181 | parties remain in full compliance. 182 | 183 | **5.** You are not required to accept this License, since you have not 184 | signed it. However, nothing else grants you permission to modify or 185 | distribute the Program or its derivative works. These actions are 186 | prohibited by law if you do not accept this License. Therefore, by 187 | modifying or distributing the Program (or any work based on the 188 | Program), you indicate your acceptance of this License to do so, and 189 | all its terms and conditions for copying, distributing or modifying 190 | the Program or works based on it. 191 | 192 | **6.** Each time you redistribute the Program (or any work based on 193 | the Program), the recipient automatically receives a license from the 194 | original licensor to copy, distribute or modify the Program subject to 195 | these terms and conditions. You may not impose any further 196 | restrictions on the recipients' exercise of the rights granted herein. 197 | You are not responsible for enforcing compliance by third parties to 198 | this License. 199 | 200 | **7.** If, as a consequence of a court judgment or allegation of 201 | patent infringement or for any other reason (not limited to patent 202 | issues), conditions are imposed on you (whether by court order, 203 | agreement or otherwise) that contradict the conditions of this 204 | License, they do not excuse you from the conditions of this License. 205 | If you cannot distribute so as to satisfy simultaneously your 206 | obligations under this License and any other pertinent obligations, 207 | then as a consequence you may not distribute the Program at all. For 208 | example, if a patent license would not permit royalty-free 209 | redistribution of the Program by all those who receive copies directly 210 | or indirectly through you, then the only way you could satisfy both it 211 | and this License would be to refrain entirely from distribution of the 212 | Program. 213 | 214 | If any portion of this section is held invalid or unenforceable under 215 | any particular circumstance, the balance of the section is intended to 216 | apply and the section as a whole is intended to apply in other 217 | circumstances. 218 | 219 | It is not the purpose of this section to induce you to infringe any 220 | patents or other property right claims or to contest validity of any 221 | such claims; this section has the sole purpose of protecting the 222 | integrity of the free software distribution system, which is 223 | implemented by public license practices. Many people have made 224 | generous contributions to the wide range of software distributed 225 | through that system in reliance on consistent application of that 226 | system; it is up to the author/donor to decide if he or she is willing 227 | to distribute software through any other system and a licensee cannot 228 | impose that choice. 229 | 230 | This section is intended to make thoroughly clear what is believed to 231 | be a consequence of the rest of this License. 232 | 233 | **8.** If the distribution and/or use of the Program is restricted in 234 | certain countries either by patents or by copyrighted interfaces, the 235 | original copyright holder who places the Program under this License 236 | may add an explicit geographical distribution limitation excluding 237 | those countries, so that distribution is permitted only in or among 238 | countries not thus excluded. In such case, this License incorporates 239 | the limitation as if written in the body of this License. 240 | 241 | **9.** The Free Software Foundation may publish revised and/or new 242 | versions of the General Public License from time to time. Such new 243 | versions will be similar in spirit to the present version, but may 244 | differ in detail to address new problems or concerns. 245 | 246 | Each version is given a distinguishing version number. If the Program 247 | specifies a version number of this License which applies to it and 248 | “any later version”, you have the option of following the terms and 249 | conditions either of that version or of any later version published by 250 | the Free Software Foundation. If the Program does not specify a 251 | version number of this License, you may choose any version ever 252 | published by the Free Software Foundation. 253 | 254 | **10.** If you wish to incorporate parts of the Program into other 255 | free programs whose distribution conditions are different, write to 256 | the author to ask for permission. For software which is copyrighted by 257 | the Free Software Foundation, write to the Free Software Foundation; 258 | we sometimes make exceptions for this. Our decision will be guided by 259 | the two goals of preserving the free status of all derivatives of our 260 | free software and of promoting the sharing and reuse of software 261 | generally. 262 | 263 | ### NO WARRANTY 264 | 265 | **11.** BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO 266 | WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. 267 | EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR 268 | OTHER PARTIES PROVIDE THE PROGRAM “AS IS” WITHOUT WARRANTY OF ANY 269 | KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE 270 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR 271 | PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE 272 | PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME 273 | THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 274 | 275 | **12.** IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN 276 | WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY 277 | AND/OR REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU 278 | FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR 279 | CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE 280 | PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING 281 | RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A 282 | FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF 283 | SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH 284 | DAMAGES. 285 | 286 | END OF TERMS AND CONDITIONS 287 | 288 | ### How to Apply These Terms to Your New Programs 289 | 290 | If you develop a new program, and you want it to be of the greatest 291 | possible use to the public, the best way to achieve this is to make it 292 | free software which everyone can redistribute and change under these 293 | terms. 294 | 295 | To do so, attach the following notices to the program. It is safest to 296 | attach them to the start of each source file to most effectively 297 | convey the exclusion of warranty; and each file should have at least 298 | the “copyright” line and a pointer to where the full notice is found. 299 | 300 | ``` 301 | 302 | Copyright (C) 303 | 304 | This program is free software; you can redistribute it and/or modify 305 | it under the terms of the GNU General Public License as published by 306 | the Free Software Foundation; either version 2 of the License, or 307 | (at your option) any later version. 308 | 309 | This program is distributed in the hope that it will be useful, 310 | but WITHOUT ANY WARRANTY; without even the implied warranty of 311 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 312 | GNU General Public License for more details. 313 | 314 | You should have received a copy of the GNU General Public License along 315 | with this program; if not, write to the Free Software Foundation, Inc., 316 | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. 317 | ``` 318 | 319 | Also add information on how to contact you by electronic and paper 320 | mail. 321 | 322 | If the program is interactive, make it output a short notice like this 323 | when it starts in an interactive mode: 324 | 325 | ``` 326 | Gnomovision version 69, Copyright (C) year name of author 327 | Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. 328 | This is free software, and you are welcome to redistribute it 329 | under certain conditions; type `show c' for details. 330 | ``` 331 | 332 | The hypothetical commands `show w` and `show c` should show the 333 | appropriate parts of the General Public License. Of course, the 334 | commands you use may be called something other than `show w` and 335 | `show c`; they could even be mouse-clicks or menu items--whatever 336 | suits your program. 337 | 338 | You should also get your employer (if you work as a programmer) or 339 | your school, if any, to sign a “copyright disclaimer” for the program, 340 | if necessary. Here is a sample; alter the names: 341 | 342 | ``` 343 | Yoyodyne, Inc., hereby disclaims all copyright interest in the program 344 | `Gnomovision' (which makes passes at compilers) written by James Hacker. 345 | 346 | , 1 April 1989 347 | Ty Coon, President of Vice 348 | ``` 349 | 350 | This General Public License does not permit incorporating your program 351 | into proprietary programs. If your program is a subroutine library, 352 | you may consider it more useful to permit linking proprietary 353 | applications with the library. If this is what you want to do, use the 354 | GNU Lesser General Public License instead of this License. 355 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ⚡CAKE with Adaptive Bandwidth - "cake-autorate" 2 | 3 | **cake-autorate** is a script that minimizes latency in routers 4 | by adjusting CAKE bandwidth settings. 5 | It uses traffic load, one-way-delay, and 6 | round-trip time measurements to adjust the CAKE parameters. 7 | **cake-autorate** is intended for variable 8 | bandwidth connections such as LTE, Starlink, and cable modems and is 9 | not generally required for use on connections that have a stable, 10 | fixed bandwidth. 11 | 12 | [CAKE](https://www.bufferbloat.net/projects/codel/wiki/Cake/) is an 13 | algorithm that manages the buffering of data being sent/received by a 14 | device so that no more 15 | data is queued than is necessary, minimizing the latency 16 | ("bufferbloat") and improving the responsiveness of a network. An 17 | instance of cake on an interface is set up with a certain bandwidth. 18 | Although this bandwidth can be changed, the cake algorithm itself has 19 | no reliable means to adjust the bandwidth on the fly. 20 | **cake-autorate** bridges this gap. 21 | 22 | **cake-autorate** presently supports installation on devices running 23 | on an [OpenWrt router](https://openwrt.org) or an 24 | [Asus Merlin router](https://www.asuswrt-merlin.net/). 25 | 26 | ### Status 27 | 28 | This is the **development** (`master`) branch. New work on 29 | cake-autorate appears here. It is not guaranteed to be stable. 30 | 31 | The **stable version** for production/every day use is 32 | 3.2.1 available from the 33 | [v3.2 branch](https://github.com/lynxthecat/cake-autorate/tree/v3.2). 34 | 35 | If you like cake-autorate and can benefit from it, then please leave a 36 | ⭐ (top right) and become a 37 | [stargazer](https://github.com/lynxthecat/cake-autorate/stargazers)! 38 | And feel free to post any feedback on the official OpenWrt thread 39 | [here](https://forum.openwrt.org/t/cake-w-adaptive-bandwidth/191049). 40 | Thank you for your support. 41 | 42 | ## The Problem: CAKE on variable speed connections forces an unpalatable compromise 43 | 44 | The CAKE algorithm uses static upload and download bandwidth settings 45 | to manage its queues. Variable bandwidth connections present a 46 | challenge because the actual bandwidth at any given moment is not 47 | known. 48 | 49 | Because CAKE works with fixed bandwidth parameters, the user must 50 | choose a single compromise bandwidth setting. This compromise is not 51 | ideal: setting the parameter too low means the connection is 52 | unnecessarily throttled to the compromise setting even when the 53 | available link speed is higher (yellow). Setting the rate too high, 54 | for times when the usable line rate falls below the compromise value, 55 | means that the link is not throttled enough (green) resulting in 56 | bufferbloat. 57 | 58 | 59 | 60 | ## The Solution: Set CAKE parameters based on load and latency 61 | 62 | The cake-autorate script continually measures the load and One Way 63 | Delay (OWD) or Round-Trip-Time (RTT) to adjust the upload and download 64 | settings for the CAKE algorithm. 65 | 66 | ### Theory of Operation 67 | 68 | `cake-autorate.sh` monitors load (receive and transmit utilization) 69 | and ping response times from one or more reflectors (hosts on the 70 | internet), and adjusts the download and upload rate (bandwidth) settings for 71 | CAKE. 72 | 73 | cake-autorate uses this algorithm for each direction of traffic: 74 | 75 | - In periods of high traffic, ramp up the rate setting 76 | toward the configured maximum 77 | to take advantage of the increase throughput 78 | - Anytime bufferbloat (increased latency) is detected, 79 | ramp down the rate setting until the latency stabilizes, 80 | but not below the configured minimum 81 | - In periods of low traffic, gradually ramp the rate setting 82 | back toward the configured baseline. 83 | A subsequent burst of traffic will begin a new search for 84 | the proper rate setting. 85 | - This algorithm typically adjusts to new traffic conditions 86 | in well under one second. 87 | To avoid oscillation, there is a _refractory period_ 88 | during which no further change will be made. 89 | 90 | 91 | 92 | cake-autorate requires three configuration values for each direction, 93 | upload and download. 94 | 95 | **Setting the minimum bandwidth:** Set the minimum value to the lowest 96 | possible observed bufferbloat-free bandwidth. Ideally this setting 97 | should never result in bufferbloat even under the worst conditions. 98 | This is a hard minimum - the script will never reduce the bandwidth 99 | below this level. 100 | 101 | **Setting the baseline bandwidth:** This is the steady state bandwidth 102 | to be maintained under no or low load. This is likely the compromise 103 | bandwidth described above, i.e. the value you would set CAKE to that 104 | is bufferbloat-free most, but not necessarily all, of the time. 105 | 106 | **Setting the maximum bandwidth:** The maximum bandwidth should be set 107 | to the maximum bandwidth the connection can provide (or slightly lower). 108 | When there is heavy traffic, the script will adjust the bandwidth up to 109 | this limit, and then back off if an OWD or RTT spike is detected. 110 | Since the algorithm repeatedly tests for the maximum rate available, 111 | it may permit some excess latency at a traffic peak. 112 | Reducing the cake-autorate maximum to a value 113 | slightly below the link's maximum has the 114 | benefit of avoiding that excess latency, 115 | and may allow the traffic to cruise along with low latency 116 | at that configured maximum, 117 | even though the true connection capacity might be slightly higher. 118 | 119 | To elaborate on setting the minimum and maximum, a variable bandwidth 120 | connection may be most ideally divided up into a known fixed, stable 121 | component, on top of which is provided an unknown variable component: 122 | 123 | ![image of cake bandwidth adaptation](images/cake-bandwidth-adaptation.png) 124 | 125 | The minimum bandwidth is then set to (or slightly below) the fixed 126 | component, and the maximum bandwidth may be set to (or slightly above) 127 | the maximum observed bandwidth (if maximum bandwidth is desired) or 128 | lower than the maximum observed bandwidth (if the user is willing to 129 | sacrifice some bandwidth in favour of reduced latency associated with 130 | always testing for the true maximum as explained above). 131 | 132 | The baseline bandwidth is likely optimally either the minimum 133 | bandwidth or somewhere close thereto (e.g. the compromise bandwidth). 134 | 135 | ## Installation on OpenWrt or Asus Merlin 136 | 137 | Read the installation instructions in the separate 138 | [INSTALLATION](./INSTALLATION.md) page. 139 | 140 | ## Analysis of the cake-autorate logs 141 | 142 | cake-autorate maintains a detailed log file that is helpful in 143 | examining performance. 144 | 145 | Read about this in the [ANALYSIS](./ANALYSIS.md) page. 146 | 147 | ## CPU usage monitoring 148 | 149 | The user should verify that total CPU usage is kept within acceptable 150 | ranges, especially for higher bandwidth connections and devices with 151 | weaker CPUs. On CPU saturation, bandwidth on a running CAKE qdisc is 152 | throttled. A CAKE qdisc is run on a specific CPU core and thus care 153 | should be taken to ensure that the CPU core(s) on which CAKE qdiscs 154 | are run are not saturated during normal use. 155 | 156 | cake-autorate includes logging options `output_cpu_stats` and 157 | `output_cpu_raw_stats` to monitor and log CPU total usage across all 158 | detected CPU cores. This can be leveraged to verify that sufficient 159 | spare CPU cycles exist for CAKE to avoid any bandwidth throttling. 160 | 161 | cake-autorate uses inter-process communication between multiple 162 | concurrent processes and incorporates various optimisations to reduce 163 | the CPU load needed to perform its many tasks. A call to 164 | `ps |grep -e bash -e fping` reveals the presence of the multiple 165 | concurrent processes for each cake-autorate instance. This is normal 166 | and expected behaviour. 167 | 168 | ```bash 169 | root@OpenWrt-1:~# ps |grep -e bash -e fping 170 | 1731 root 2468 S bash /root/cake-autorate/launcher.sh 171 | 1733 root 3412 S bash /root/cake-autorate/cake-autorate.sh /root/cake-autorate/config.primary.sh 172 | 1862 root 3020 S bash /root/cake-autorate/cake-autorate.sh /root/cake-autorate/config.primary.sh 173 | 1866 root 2976 S bash /root/cake-autorate/cake-autorate.sh /root/cake-autorate/config.primary.sh 174 | 1878 root 3200 S bash /root/cake-autorate/cake-autorate.sh /root/cake-autorate/config.primary.sh 175 | 2785 root 1988 S fping --timestamp --loop --period 300 --interval 50 --timeout 10000 1.1.1.1 1.0.0.1 8.8.8.8 176 | ``` 177 | 178 | Process IDs can be checked using 179 | `cat /var/run/cake-autorate/primary/proc_pids`, e.g.: 180 | 181 | ```bash 182 | root@OpenWrt-1:~# cat /var/run/cake-autorate/primary/proc_pids 183 | intercept_stderr=1862 184 | maintain_log_file=1866 185 | fping_pinger=2785 186 | monitor_achieved_rates=1878 187 | main=1733 188 | ``` 189 | 190 | It is useful to keep an htop or atop instance running and run some 191 | speed tests and check the maximum CPU utilisation of the processes: 192 | 193 | ![image](https://github.com/lynxthecat/cake-autorate/assets/10721999/732ecdc0-e847-48db-baa5-c10616c2ad1b) 194 | 195 | CPU load is proportional to the frequency of ping responses. Reducing 196 | the number of pingers or pinger interval will therefore significantly 197 | reduce CPU usage. The default ping response rate is 20 Hz (6 pingers 198 | with 0.3 seconds between pings). Reducing the number of pingers to 199 | three will give a ping response rate of 10 Hz and approximately half 200 | the CPU load. 201 | 202 | Also, for everyday use, consider disabling any unnecessary logging 203 | options, and especially: `output_summary_stats`, 204 | `output_processing_stats` and `output_load_stats`. 205 | 206 | ## :stars: Stargazers 207 | 208 | [![Star History Chart](https://api.star-history.com/svg?repos=lynxthecat/cake-autorate&type=Date)](https://star-history.com/#lynxthecat/cake-autorate&Date) 209 | -------------------------------------------------------------------------------- /bench_cpu.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # Simple CPU benchmark for cake-autorate on OpenWrt 4 | 5 | test_period_s=${1:-60} # Number of seconds to run CPU usage test 6 | 7 | printf "Running CPU benchmark for test period of: %ds.\n...\n" "${test_period_s}" 8 | 9 | service cake-autorate stop 2> /dev/null 10 | service cake-autorate start 11 | 12 | sleep 10 # Give 10 seconds for CPU usage to settle 13 | 14 | tstart=${EPOCHREALTIME/.} 15 | cstart=$(awk 'NR==2,NR==3{sum+=$2};END{print sum;}' /sys/fs/cgroup/services/cake-autorate/cpu.stat) 16 | 17 | sleep "${test_period_s}" 18 | 19 | tstop=${EPOCHREALTIME/.} 20 | cstop=$(awk 'NR==2,NR==3{sum+=$2};END{print sum;}' /sys/fs/cgroup/services/cake-autorate/cpu.stat) 21 | 22 | (( cpu_usage=(100000*(cstop - cstart)) / (tstop - tstart) )) 23 | 24 | cpu_usage=000${cpu_usage} 25 | 26 | printf "Average CPU usage over test period of %ds was: %.3f%%\n" "${test_period_s}" "${cpu_usage::-3}.${cpu_usage: -3}" 27 | 28 | service cake-autorate stop 29 | -------------------------------------------------------------------------------- /cake-autorate.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # cake-autorate automatically adjusts CAKE bandwidth(s) 4 | # in dependence on: a) receive and transmit transfer rates; and b) latency 5 | # (or can just be used to monitor and log transfer rates and latency) 6 | 7 | # requires: bash; and one of the supported ping binaries 8 | 9 | # each cake-autorate instance must be configured using a corresponding config file 10 | 11 | # Project homepage: https://github.com/lynxthecat/cake-autorate 12 | # Licence details: https://github.com/lynxthecat/cake-autorate/blob/master/LICENCE.md 13 | 14 | # Author and maintainer: lynxthecat 15 | # Contributors: rany2; moeller0; richb-hanover 16 | 17 | cake_autorate_version="3.3.0-PRERELEASE" 18 | 19 | ## cake-autorate uses multiple asynchronous processes including: 20 | ## main - main process 21 | ## monitor_achieved_rates - monitor network transfer rates 22 | ## maintain_log_file - maintain and rotate log file 23 | ## 24 | ## IPC is facilitated via FIFOs in the form of anonymous pipes 25 | ## thereby to enable transferring data between processes 26 | 27 | # Set the IFS to space and comma 28 | IFS=" ," 29 | 30 | # Initialize file descriptors 31 | ## -1 signifies that the log file fd will not be used and 32 | ## that the log file will be written to directly 33 | log_fd=-1 34 | exec {main_fd}<> <(:) 35 | 36 | # process pids are stored below in the form 37 | # proc_pids['process_identifier']=${!} 38 | declare -A proc_pids 39 | 40 | # Bash correctness options 41 | ## Disable globbing (expansion of *). 42 | set -f 43 | ## Forbid using unset variables. 44 | set -u 45 | ## The exit status of a pipeline is the status of the last 46 | ## command to exit with a non-zero status, or zero if no 47 | ## command exited with a non-zero status. 48 | set -o pipefail 49 | 50 | ## Errors are intercepted via intercept_stderr below 51 | ## and sent to the log file and system log 52 | 53 | # Possible performance improvement 54 | export LC_ALL=C 55 | 56 | # Set SCRIPT_PREFIX and CONFIG_PREFIX 57 | POSSIBLE_SCRIPT_PREFIXES=( 58 | "${CAKE_AUTORATE_SCRIPT_PREFIX:-}" # User defined 59 | "/jffs/scripts/cake-autorate" # Asuswrt-Merlin 60 | "/opt/cake-autorate" 61 | "/usr/lib/cake-autorate" 62 | "/root/cake-autorate" 63 | ) 64 | for SCRIPT_PREFIX in "${POSSIBLE_SCRIPT_PREFIXES[@]}" 65 | do 66 | [[ -d ${SCRIPT_PREFIX} ]] && break 67 | done 68 | if [[ -z ${SCRIPT_PREFIX} || ! -d ${SCRIPT_PREFIX} ]] 69 | then 70 | printf "ERROR: Unable to find a working SCRIPT_PREFIX for cake-autorate. Exiting now.\n" >&2 71 | printf "ERROR: Please set the CAKE_AUTORATE_SCRIPT_PREFIX environment variable to the correct path.\n" >&2 72 | exit 1 73 | fi 74 | POSSIBLE_CONFIG_PREFIXES=( 75 | "${CAKE_AUTORATE_CONFIG_PREFIX:-}" # User defined 76 | "/jffs/configs/cake-autorate" # Asuswrt-Merlin 77 | "${SCRIPT_PREFIX}" # Default 78 | ) 79 | for CONFIG_PREFIX in "${POSSIBLE_CONFIG_PREFIXES[@]}" 80 | do 81 | [[ -d ${CONFIG_PREFIX} ]] && break 82 | done 83 | if [[ -z ${CONFIG_PREFIX} || ! -d ${CONFIG_PREFIX} ]] 84 | then 85 | printf "ERROR: Unable to find a working CONFIG_PREFIX for cake-autorate. Exiting now.\n" >&2 86 | printf "ERROR: Please set the CAKE_AUTORATE_CONFIG_PREFIX environment variable to the correct path.\n" >&2 87 | exit 1 88 | fi 89 | 90 | # shellcheck source=lib.sh 91 | . "${SCRIPT_PREFIX}/lib.sh" 92 | # shellcheck source=defaults.sh 93 | . "${SCRIPT_PREFIX}/defaults.sh" 94 | # get valid config overrides 95 | mapfile -t valid_config_entries < <(grep -E '^[^(#| )].*=' "${SCRIPT_PREFIX}/defaults.sh" | sed -e 's/[\t ]*\#.*//g' -e 's/=.*//g') 96 | 97 | trap cleanup_and_killall INT TERM EXIT 98 | 99 | cleanup_and_killall() 100 | { 101 | # Do not fail on error for this critical cleanup code 102 | set +e 103 | 104 | trap : INT TERM EXIT 105 | 106 | log_msg "DEBUG" "Starting: ${FUNCNAME[0]} with PID: ${BASHPID}" 107 | 108 | log_msg "INFO" "Stopping cake-autorate with PID: ${BASHPID} and config: ${config_path}" 109 | 110 | log_msg "INFO" "Killing all background processes and cleaning up temporary files." 111 | 112 | terminate "${proc_pids['monitor_achieved_rates']:-}" 113 | 114 | terminate "${pinger_pids[*]}" 115 | 116 | ((terminate_maintain_log_file_timeout_ms=log_file_buffer_timeout_ms+500)) 117 | terminate "${proc_pids['maintain_log_file']}" "${terminate_maintain_log_file_timeout_ms}" 118 | 119 | [[ -d ${run_path} ]] && rm -r "${run_path}" 120 | rmdir /var/run/cake-autorate 2>/dev/null 121 | 122 | # give some time for processes to gracefully exit 123 | sleep_s 1 124 | 125 | # terminate any processes that remain, save for main and intercept_stderr 126 | unset "proc_pids[main]" 127 | intercept_stderr_pid=${proc_pids[intercept_stderr]:-} 128 | if [[ -n ${intercept_stderr_pid} ]] 129 | then 130 | unset "proc_pids[intercept_stderr]" 131 | fi 132 | terminate "${proc_pids[*]}" 133 | 134 | # restore original stderr, and terminate intercept_stderr 135 | if [[ -n ${intercept_stderr_pid} ]] 136 | then 137 | exec 2>&"${original_stderr_fd}" 138 | terminate "${intercept_stderr_pid}" 139 | fi 140 | 141 | log_msg "SYSLOG" "Stopped cake-autorate with PID: ${BASHPID} and config: ${config_path}" 142 | 143 | trap - INT TERM EXIT 144 | exit 145 | } 146 | 147 | log_msg() 148 | { 149 | # send logging message to terminal, log file fifo, log file and/or system logger 150 | 151 | local type=${1} msg=${2} instance_id=${instance_id:-"unknown"} log_timestamp=${EPOCHREALTIME} 152 | 153 | case ${type} in 154 | 155 | DEBUG) 156 | ((debug == 0)) && return # skip over DEBUG messages where debug disabled 157 | ((log_DEBUG_messages_to_syslog && use_logger)) && \ 158 | logger -t "cake-autorate.${instance_id}" "${type}: ${log_timestamp} ${msg}" 159 | ;; 160 | 161 | ERROR) 162 | ((use_logger)) && \ 163 | logger -t "cake-autorate.${instance_id}" "${type}: ${log_timestamp} ${msg}" 164 | ;; 165 | 166 | SYSLOG) 167 | ((use_logger)) && \ 168 | logger -t "cake-autorate.${instance_id}" "INFO: ${log_timestamp} ${msg}" 169 | ;; 170 | 171 | *) 172 | ;; 173 | esac 174 | 175 | printf -v msg '%s; %(%F-%H:%M:%S)T; %s; %s\n' "${type}" -1 "${log_timestamp}" "${msg}" 176 | ((terminal)) && printf '%s' "${msg}" 177 | 178 | # Output to the log file fifo if available (for rotation handling) 179 | # else output directly to the log file 180 | ((log_to_file)) || return 181 | if (( log_fd >= 0 )) 182 | then 183 | printf '%s' "${msg}" >&"${log_fd}" 184 | else 185 | printf '%s' "${msg}" >> "${log_file_path}" 186 | fi 187 | } 188 | 189 | print_headers() 190 | { 191 | log_msg "DEBUG" "Starting: ${FUNCNAME[0]} with PID: ${BASHPID}" 192 | 193 | if ((output_processing_stats)) 194 | then 195 | header="DATA_HEADER; LOG_DATETIME; LOG_TIMESTAMP; PROC_TIME_US; DL_ACHIEVED_RATE_KBPS; UL_ACHIEVED_RATE_KBPS; DL_LOAD_PERCENT; UL_LOAD_PERCENT; ICMP_TIMESTAMP; REFLECTOR; SEQUENCE; DL_OWD_BASELINE; DL_OWD_US; DL_OWD_DELTA_EWMA_US; DL_OWD_DELTA_US; DL_ADJ_DELAY_THR; UL_OWD_BASELINE; UL_OWD_US; UL_OWD_DELTA_EWMA_US; UL_OWD_DELTA_US; UL_ADJ_DELAY_THR; DL_SUM_DELAYS; DL_AVG_OWD_DELTA_US; DL_ADJ_MAX_ADJUST_UP_THR_US; DL_ADJ_MAX_ADJUST_DOWN_THR_US; UL_SUM_DELAYS; UL_AVG_OWD_DELTA_US; UL_ADJ_MAX_ADJUST_UP_THR_US; UL_ADJ_MAX_ADJUST_DOWN_THR_US; DL_LOAD_CONDITION; UL_LOAD_CONDITION; CAKE_DL_RATE_KBPS; CAKE_UL_RATE_KBPS" 196 | ((log_to_file)) && printf '%s\n' "${header}" >&${log_file_fd} 197 | ((terminal)) && printf '%s\n' "${header}" 198 | fi 199 | 200 | if ((output_load_stats)) 201 | then 202 | header="LOAD_HEADER; LOG_DATETIME; LOG_TIMESTAMP; PROC_TIME_US; DL_ACHIEVED_RATE_KBPS; UL_ACHIEVED_RATE_KBPS; CAKE_DL_RATE_KBPS; CAKE_UL_RATE_KBPS" 203 | ((log_to_file)) && printf '%s\n' "${header}" >&${log_file_fd} 204 | ((terminal)) && printf '%s\n' "${header}" 205 | fi 206 | 207 | if ((output_reflector_stats)) 208 | then 209 | header="REFLECTOR_HEADER; LOG_DATETIME; LOG_TIMESTAMP; PROC_TIME_US; REFLECTOR; MIN_SUM_OWD_BASELINES_US; SUM_OWD_BASELINES_US; SUM_OWD_BASELINES_DELTA_US; SUM_OWD_BASELINES_DELTA_THR_US; MIN_DL_DELTA_EWMA_US; DL_DELTA_EWMA_US; DL_DELTA_EWMA_DELTA_US; DL_DELTA_EWMA_DELTA_THR; MIN_UL_DELTA_EWMA_US; UL_DELTA_EWMA_US; UL_DELTA_EWMA_DELTA_US; UL_DELTA_EWMA_DELTA_THR" 210 | ((log_to_file)) && printf '%s\n' "${header}" >&${log_file_fd} 211 | ((terminal)) && printf '%s\n' "${header}" 212 | fi 213 | 214 | if ((output_summary_stats)) 215 | then 216 | header="SUMMARY_HEADER; LOG_DATETIME; LOG_TIMESTAMP; DL_ACHIEVED_RATE_KBPS; UL_ACHIEVED_RATE_KBPS; DL_SUM_DELAYS; UL_SUM_DELAYS; DL_AVG_OWD_DELTA_US; UL_AVG_OWD_DELTA_US; DL_LOAD_CONDITION; UL_LOAD_CONDITION; CAKE_DL_RATE_KBPS; CAKE_UL_RATE_KBPS" 217 | ((log_to_file)) && printf '%s\n' "${header}" >&${log_file_fd} 218 | ((terminal)) && printf '%s\n' "${header}" 219 | fi 220 | 221 | if ((output_cpu_stats)) 222 | then 223 | header="CPU_HEADER; LOG_DATETIME; LOG_TIMESTAMP; STATS_READ_TIME; ${cpu_ids// /_USAGE; }_USAGE" 224 | ((log_to_file)) && printf '%s\n' "${header}" >&${log_file_fd} 225 | ((terminal)) && printf '%s\n' "${header}" 226 | fi 227 | 228 | if ((output_cpu_raw_stats)) 229 | then 230 | header="CPU_RAW_HEADER; LOG_DATETIME; LOG_TIMESTAMP; STATS_READ_TIME; CPU_ID; USER; NICE; SYSTEM; IDLE; IOWAIT; IRQ; SIRQ; STEAL; GUEST; GUEST_NICE" 231 | ((log_to_file)) && printf '%s\n' "${header}" >&${log_file_fd} 232 | ((terminal)) && printf '%s\n' "${header}" 233 | fi 234 | 235 | } 236 | 237 | # MAINTAIN_LOG_FILE + HELPER FUNCTIONS 238 | 239 | rotate_log_file() 240 | { 241 | log_msg "DEBUG" "Starting: ${FUNCNAME[0]} with PID: ${BASHPID}" 242 | 243 | [[ -f ${log_file_path} ]] || return 244 | cat "${log_file_path}" > "${log_file_path}.old" 245 | : > "${log_file_path}" 246 | } 247 | 248 | reset_log_file() 249 | { 250 | log_msg "DEBUG" "Starting: ${FUNCNAME[0]} with PID: ${BASHPID}" 251 | 252 | rm -f "${log_file_path}.old" 253 | : > "${log_file_path}" 254 | } 255 | 256 | generate_log_file_scripts() 257 | { 258 | cat > "${run_path}/log_file_export" <<- EOT 259 | #!${BASH} 260 | 261 | timeout_s=\${1:-20} 262 | 263 | if kill -USR1 "${proc_pids['maintain_log_file']}" 264 | then 265 | printf "Successfully signalled maintain_log_file process to request log file export.\n" 266 | else 267 | printf "ERROR: Failed to signal maintain_log_file process.\n" >&2 268 | exit 1 269 | fi 270 | rm -f "${run_path}/last_log_file_export" 271 | 272 | read_try=0 273 | 274 | while [[ ! -f "${run_path}/last_log_file_export" ]] 275 | do 276 | sleep 1 277 | if (( ++read_try >= \${timeout_s} )) 278 | then 279 | printf "ERROR: Timeout (\${timeout_s}s) reached before new log file export identified.\n" >&2 280 | exit 1 281 | fi 282 | done 283 | 284 | read -r log_file_export_path < "${run_path}/last_log_file_export" 285 | 286 | printf "Log file export complete.\n" 287 | 288 | printf "Log file available at location: " 289 | printf "\${log_file_export_path}\n" 290 | EOT 291 | 292 | cat > "${run_path}/log_file_reset" <<- EOT 293 | #!${BASH} 294 | 295 | if kill -USR2 "${proc_pids['maintain_log_file']}" 296 | then 297 | printf "Successfully signalled maintain_log_file process to request log file reset.\n" 298 | else 299 | printf "ERROR: Failed to signal maintain_log_file process.\n" >&2 300 | exit 1 301 | fi 302 | EOT 303 | 304 | chmod +x "${run_path}/log_file_export" "${run_path}/log_file_reset" 305 | } 306 | 307 | export_log_file() 308 | { 309 | log_msg "DEBUG" "Starting: ${FUNCNAME[0]} with PID: ${BASHPID}" 310 | 311 | printf -v log_file_export_datetime '%(%Y_%m_%d_%H_%M_%S)T' 312 | log_file_export_path="${log_file_path/.log/_${log_file_export_datetime}.log}" 313 | log_msg "DEBUG" "Exporting log file with path: ${log_file_path/.log/_${log_file_export_datetime}.log}" 314 | 315 | flush_log_pipe 316 | 317 | # Now export with or without compression to the appropriate export path 318 | if ((log_file_export_compress)) 319 | then 320 | log_file_export_path="${log_file_export_path}.gz" 321 | export_cmd=("gzip" "-c") 322 | else 323 | export_cmd=("cat") 324 | fi 325 | 326 | if [[ -f ${log_file_path}.old ]] 327 | then 328 | "${export_cmd[@]}" "${log_file_path}.old" > "${log_file_export_path}" 329 | "${export_cmd[@]}" "${log_file_path}" >> "${log_file_export_path}" 330 | else 331 | "${export_cmd[@]}" "${log_file_path}" > "${log_file_export_path}" 332 | fi 333 | printf '%s' "${log_file_export_path}" > "${run_path}/last_log_file_export" 334 | } 335 | 336 | flush_log_pipe() 337 | { 338 | log_msg "DEBUG" "Starting: ${FUNCNAME[0]} with PID: ${BASHPID}" 339 | while read -r -t 0 -u "${log_fd}" 340 | do 341 | read -r -u "${log_fd}" log_line 342 | printf '%s\n' "${log_line}" >&${log_file_fd} 343 | ((log_file_size_bytes+=${#log_line})) 344 | done 345 | } 346 | 347 | maintain_log_file() 348 | { 349 | signal="" 350 | trap '' INT 351 | trap 'signal+=KILL' TERM EXIT 352 | trap 'signal+=EXPORT' USR1 353 | trap 'signal+=RESET' USR2 354 | 355 | log_msg "DEBUG" "Starting: ${FUNCNAME[0]} with PID: ${BASHPID}" 356 | 357 | printf -v log_file_buffer_timeout_s %.1f "${log_file_buffer_timeout_ms}e-3" 358 | 359 | while : 360 | do 361 | exec {log_file_fd}> "${log_file_path}" 362 | 363 | print_headers 364 | log_file_size_bytes=$(wc -c "${log_file_path}" 2>/dev/null | awk '{print $1}') 365 | log_file_size_bytes=${log_file_size_bytes:-0} 366 | 367 | t_log_file_start_s=${SECONDS} 368 | 369 | while : 370 | do 371 | read -r -N "${log_file_buffer_size_B}" -t "${log_file_buffer_timeout_s}" -u "${log_fd}" log_chunk 372 | 373 | printf '%s' "${log_chunk}" >&${log_file_fd} 374 | 375 | ((log_file_size_bytes+=${#log_chunk})) 376 | 377 | # Verify log file time < configured maximum 378 | if (( SECONDS - t_log_file_start_s > log_file_max_time_s )) 379 | then 380 | log_msg "DEBUG" "log file maximum time: ${log_file_max_time_mins} minutes has elapsed so flushing and rotating log file." 381 | flush_log_pipe 382 | rotate_log_file 383 | break 384 | # Verify log file size < configured maximum 385 | elif (( log_file_size_bytes > log_file_max_size_bytes )) 386 | then 387 | ((log_file_size_KB=log_file_size_bytes/1024)) 388 | log_msg "DEBUG" "log file size: ${log_file_size_KB} KB has exceeded configured maximum: ${log_file_max_size_KB} KB so flushing and rotating log file." 389 | flush_log_pipe 390 | rotate_log_file 391 | break 392 | fi 393 | 394 | # Check for signals 395 | case ${signal-} in 396 | 397 | "") 398 | ;; 399 | *KILL*) 400 | log_msg "DEBUG" "received log file kill signal so flushing log and exiting." 401 | flush_log_pipe 402 | trap - TERM EXIT 403 | exit 404 | ;; 405 | *EXPORT*) 406 | log_msg "DEBUG" "received log file export signal so exporting log file." 407 | export_log_file 408 | signal="${signal//EXPORT}" 409 | ;; 410 | *RESET*) 411 | log_msg "DEBUG" "received log file reset signal so flushing log and resetting log file." 412 | flush_log_pipe 413 | reset_log_file 414 | signal="${signal//RESET}" 415 | break 416 | ;; 417 | *) 418 | signal="" 419 | log_msg "ERROR" "processed unknown signal(s): ${signal}." 420 | ;; 421 | esac 422 | done 423 | 424 | exec {log_file_fd}>&- 425 | done 426 | } 427 | 428 | export_proc_pids() 429 | { 430 | log_msg "DEBUG" "Starting: ${FUNCNAME[0]} with PID: ${BASHPID}" 431 | 432 | : > "${run_path}/proc_pids" 433 | for proc_pid in "${!proc_pids[@]}" 434 | do 435 | printf "%s=%s\n" "${proc_pid}" "${proc_pids[${proc_pid}]}" >> "${run_path}/proc_pids" 436 | done 437 | } 438 | 439 | monitor_achieved_rates() 440 | { 441 | trap '' INT 442 | 443 | # track rx and tx bytes transfered and divide by time since last update 444 | # to determine achieved dl and ul transfer rates 445 | 446 | local rx_bytes_path=${1} tx_bytes_path=${2} monitor_achieved_rates_interval_us=${3} 447 | 448 | log_msg "DEBUG" "Starting: ${FUNCNAME[0]} with PID: ${BASHPID}" 449 | 450 | compensated_monitor_achieved_rates_interval_us=${monitor_achieved_rates_interval_us} 451 | 452 | { read -r prev_rx_bytes < "${rx_bytes_path}"; } 2> /dev/null || prev_rx_bytes=0 453 | { read -r prev_tx_bytes < "${tx_bytes_path}"; } 2> /dev/null || prev_tx_bytes=0 454 | 455 | sleep_duration_s=0 t_start_us=0 456 | 457 | declare -A achieved_rate_kbps load_percent 458 | 459 | while : 460 | do 461 | t_start_us=${EPOCHREALTIME/.} 462 | 463 | # read in rx/tx bytes file, and if this fails then set to prev_bytes 464 | # this addresses interfaces going down and back up 465 | { read -r rx_bytes < "${rx_bytes_path}"; } 2> /dev/null || rx_bytes=${prev_rx_bytes} 466 | { read -r tx_bytes < "${tx_bytes_path}"; } 2> /dev/null || tx_bytes=${prev_tx_bytes} 467 | 468 | (( 469 | achieved_rate_kbps[dl] = 8000*(rx_bytes - prev_rx_bytes) / compensated_monitor_achieved_rates_interval_us, 470 | achieved_rate_kbps[ul] = 8000*(tx_bytes - prev_tx_bytes) / compensated_monitor_achieved_rates_interval_us, 471 | 472 | achieved_rate_kbps[dl]<0 && (achieved_rate_kbps[dl]=0), 473 | achieved_rate_kbps[ul]<0 && (achieved_rate_kbps[ul]=0), 474 | 475 | prev_rx_bytes=rx_bytes, 476 | prev_tx_bytes=tx_bytes, 477 | 478 | compensated_monitor_achieved_rates_interval_us = monitor_achieved_rates_interval_us>(10*max_wire_packet_rtt_us) ? monitor_achieved_rates_interval_us : 10*max_wire_packet_rtt_us 479 | )) 480 | 481 | printf "SARS %s %s\n" "${achieved_rate_kbps[dl]}" "${achieved_rate_kbps[ul]}" >&${main_fd} 482 | 483 | sleep_remaining_tick_time "${t_start_us}" "${compensated_monitor_achieved_rates_interval_us}" 484 | done 485 | } 486 | 487 | # GENERIC PINGER START AND STOP FUNCTIONS 488 | 489 | start_pinger() 490 | { 491 | local pinger=${1} 492 | 493 | log_msg "DEBUG" "Starting: ${FUNCNAME[0]} with PID: ${BASHPID}" 494 | 495 | case ${pinger_binary} in 496 | 497 | tsping) 498 | # accommodate present tsping interval/sleep handling to prevent ping flood with only one pinger 499 | (( tsping_sleep_time = no_pingers == 1 ? ping_response_interval_ms : 0 )) 500 | ${ping_prefix_string} tsping ${ping_extra_args} --print-timestamps --machine-readable=, --sleep-time "${tsping_sleep_time}" --target-spacing "${ping_response_interval_ms}" "${reflectors[@]:0:${no_pingers}}" 2>/dev/null >&"${main_fd}" & 501 | pinger_pids[0]=${!} 502 | proc_pids['tsping_pinger']=${pinger_pids[0]} 503 | ;; 504 | fping) 505 | ${ping_prefix_string} fping ${ping_extra_args} --timestamp --loop --period "${reflector_ping_interval_ms}" --interval "${ping_response_interval_ms}" --timeout 10000 "${reflectors[@]:0:${no_pingers}}" 2> /dev/null >&"${main_fd}" & 506 | pinger_pids[0]=${!} 507 | proc_pids['fping_pinger']=${pinger_pids[0]} 508 | ;; 509 | ping) 510 | sleep_until_next_pinger_time_slot "${pinger}" 511 | ${ping_prefix_string} ping ${ping_extra_args} -D -i "${reflector_ping_interval_s}" "${reflectors[pinger]}" 2> /dev/null >&"${main_fd}" & 512 | pinger_pids[pinger]=${!} 513 | proc_pids["ping_${pinger}_pinger"]=${pinger_pids[0]} 514 | ;; 515 | *) 516 | log_msg "ERROR" "Unknown pinger binary: ${pinger_binary}" 517 | kill $$ 2>/dev/null 518 | ;; 519 | esac 520 | 521 | export_proc_pids 522 | } 523 | 524 | start_pingers() 525 | { 526 | log_msg "DEBUG" "Starting: ${FUNCNAME[0]} with PID: ${BASHPID}" 527 | 528 | ((pingers_active)) && return 529 | case ${pinger_binary} in 530 | 531 | tsping|fping) 532 | start_pinger 0 533 | ;; 534 | ping) 535 | for ((pinger=0; pinger < no_pingers; pinger++)) 536 | do 537 | start_pinger "${pinger}" 538 | done 539 | ;; 540 | *) 541 | log_msg "ERROR" "Unknown pinger binary: ${pinger_binary}" 542 | kill $$ 2>/dev/null 543 | ;; 544 | esac 545 | pingers_active=1 546 | } 547 | 548 | sleep_until_next_pinger_time_slot() 549 | { 550 | # wait until next pinger time slot and start pinger in its slot 551 | # this allows pingers to be stopped and started (e.g. during sleep or reflector rotation) 552 | # whilst ensuring pings will remain spaced out appropriately to maintain granularity 553 | 554 | local pinger=${1} 555 | 556 | t_start_us=${EPOCHREALTIME/.} 557 | (( time_to_next_time_slot_us = (reflector_ping_interval_us-(t_start_us-pingers_t_start_us)%reflector_ping_interval_us) + pinger*ping_response_interval_us )) 558 | sleep_remaining_tick_time "${t_start_us}" "${time_to_next_time_slot_us}" 559 | } 560 | 561 | kill_pinger() 562 | { 563 | local pinger=${1} 564 | 565 | log_msg "DEBUG" "Starting: ${FUNCNAME[0]} with PID: ${BASHPID}" 566 | 567 | case ${pinger_binary} in 568 | tsping|fping) 569 | pinger=0 570 | ;; 571 | 572 | *) 573 | ;; 574 | esac 575 | 576 | terminate "${pinger_pids[pinger]}" 577 | } 578 | 579 | stop_pingers() 580 | { 581 | log_msg "DEBUG" "Starting: ${FUNCNAME[0]} with PID: ${BASHPID}" 582 | 583 | ((pingers_active)) || return 584 | case ${pinger_binary} in 585 | 586 | tsping|fping) 587 | log_msg "DEBUG" "Killing ${pinger_binary} instance." 588 | kill_pinger 0 589 | ;; 590 | ping) 591 | for (( pinger=0; pinger < no_pingers; pinger++)) 592 | do 593 | log_msg "DEBUG" "Killing pinger instance: ${pinger}" 594 | kill_pinger "${pinger}" 595 | done 596 | ;; 597 | *) 598 | log_msg "ERROR" "Unknown pinger binary: ${pinger_binary}" 599 | kill $$ 2>/dev/null 600 | ;; 601 | esac 602 | pingers_active=0 603 | } 604 | 605 | 606 | replace_pinger_reflector() 607 | { 608 | # pingers always use reflectors[0]..[no_pingers-1] as the initial set 609 | # and the additional reflectors are spare reflectors should any from initial set go stale 610 | # a bad reflector in the initial set is replaced with ${reflectors[no_pingers]} 611 | # ${reflectors[no_pingers]} is then unset 612 | # and the the bad reflector moved to the back of the queue (last element in ${reflectors[]}) 613 | # and finally the indices for ${reflectors} are updated to reflect the new order 614 | 615 | local pinger=${1} 616 | 617 | log_msg "DEBUG" "Starting: ${FUNCNAME[0]} with PID: ${BASHPID}" 618 | 619 | if ((no_reflectors > no_pingers)) 620 | then 621 | log_msg "DEBUG" "replacing reflector: ${reflectors[pinger]} with ${reflectors[no_pingers]}." 622 | kill_pinger "${pinger}" 623 | bad_reflector=${reflectors[pinger]} 624 | # overwrite the bad reflector with the reflector that is next in the queue (the one after 0..${no_pingers}-1) 625 | reflectors[pinger]=${reflectors[no_pingers]} 626 | # remove the new reflector from the list of additional reflectors beginning from ${reflectors[no_pingers]} 627 | unset "reflectors[no_pingers]" 628 | # bad reflector goes to the back of the queue 629 | # shellcheck disable=SC2206 630 | reflectors+=(${bad_reflector}) 631 | # reset array indices 632 | mapfile -t reflectors < <(for i in "${reflectors[@]}"; do printf '%s\n' "${i}"; done) 633 | # set up the new pinger with the new reflector and retain pid 634 | dl_owd_baselines_us[${reflectors[pinger]}]=${dl_owd_baselines_us[${reflectors[pinger]}]:-100000} \ 635 | ul_owd_baselines_us[${reflectors[pinger]}]=${ul_owd_baselines_us[${reflectors[pinger]}]:-100000} \ 636 | dl_owd_delta_ewmas_us[${reflectors[pinger]}]=${dl_owd_delta_ewmas_us[${reflectors[pinger]}]:-0} \ 637 | ul_owd_delta_ewmas_us[${reflectors[pinger]}]=${ul_owd_delta_ewmas_us[${reflectors[pinger]}]:-0} \ 638 | last_timestamp_reflectors_us[${reflectors[pinger]}]=${t_start_us} 639 | 640 | start_pinger "${pinger}" 641 | else 642 | log_msg "DEBUG" "No additional reflectors specified so just retaining: ${reflectors[pinger]}." 643 | fi 644 | 645 | log_msg "DEBUG" "Resetting reflector offences associated with reflector: ${reflectors[pinger]}." 646 | declare -n reflector_offences="reflector_${pinger}_offences" 647 | for ((i=0; i /dev/null 666 | else 667 | ((output_cake_changes)) && log_msg "DEBUG" "adjust_${direction}_shaper_rate set to 0 in config, so skipping the corresponding tc qdisc change call." 668 | fi 669 | 670 | # Compensate for delays imposed by active traffic shaper 671 | # This will serve to increase the delay thr at rates below around 12Mbit/s 672 | (( 673 | dl_compensation_us=(1000*dl_max_wire_packet_size_bits)/shaper_rate_kbps[dl], 674 | ul_compensation_us=(1000*ul_max_wire_packet_size_bits)/shaper_rate_kbps[ul], 675 | 676 | compensated_avg_owd_delta_max_adjust_up_thr_us[dl]=dl_avg_owd_delta_max_adjust_up_thr_us + dl_compensation_us, 677 | compensated_avg_owd_delta_max_adjust_up_thr_us[ul]=ul_avg_owd_delta_max_adjust_up_thr_us + ul_compensation_us, 678 | 679 | compensated_owd_delta_delay_thr_us[dl]=dl_owd_delta_delay_thr_us + dl_compensation_us, 680 | compensated_owd_delta_delay_thr_us[ul]=ul_owd_delta_delay_thr_us + ul_compensation_us, 681 | 682 | compensated_avg_owd_delta_max_adjust_down_thr_us[dl]=dl_avg_owd_delta_max_adjust_down_thr_us + dl_compensation_us, 683 | compensated_avg_owd_delta_max_adjust_down_thr_us[ul]=ul_avg_owd_delta_max_adjust_down_thr_us + ul_compensation_us, 684 | 685 | max_wire_packet_rtt_us=(1000*dl_max_wire_packet_size_bits)/shaper_rate_kbps[dl] + (1000*ul_max_wire_packet_size_bits)/shaper_rate_kbps[ul], 686 | 687 | last_shaper_rate_kbps[${direction}]=${shaper_rate_kbps[${direction}]} 688 | )) 689 | } 690 | 691 | get_max_wire_packet_size_bits() 692 | { 693 | local interface=${1} 694 | local -n max_wire_packet_size_bits=${2} 695 | 696 | read -r max_wire_packet_size_bits < "/sys/class/net/${interface:?}/mtu" 697 | [[ $(tc qdisc show dev "${interface}") =~ (atm|noatm)[[:space:]]overhead[[:space:]]([0-9]+) ]] 698 | (( max_wire_packet_size_bits=8*(max_wire_packet_size_bits+BASH_REMATCH[2]) )) 699 | # atm compensation = 53*ceil(X/48) bytes = 8*53*((X+8*(48-1)/(8*48)) bits = 424*((X+376)/384) bits 700 | [[ ${BASH_REMATCH[1]:-} == "atm" ]] && (( max_wire_packet_size_bits=424*((max_wire_packet_size_bits+376)/384) )) 701 | } 702 | 703 | verify_ifs_up() 704 | { 705 | # Check the rx/tx paths exist and give extra time for ifb's to come up if needed 706 | # This will block if ifs never come up 707 | log_msg "DEBUG" "Starting: ${FUNCNAME[0]} with PID: ${BASHPID}" 708 | 709 | while [[ ! -f ${rx_bytes_path} || ! -f ${tx_bytes_path} ]] 710 | do 711 | [[ -f ${rx_bytes_path} ]] || log_msg "DEBUG" "Warning: The configured download interface: '${dl_if}' does not appear to be present. Waiting ${if_up_check_interval_s} seconds for the interface to come up." 712 | [[ -f ${tx_bytes_path} ]] || log_msg "DEBUG" "Warning: The configured upload interface: '${ul_if}' does not appear to be present. Waiting ${if_up_check_interval_s} seconds for the interface to come up." 713 | sleep_s "${if_up_check_interval_s}" 714 | done 715 | } 716 | 717 | change_state_main() 718 | { 719 | local main_next_state=${1} 720 | 721 | log_msg "DEBUG" "Starting: ${FUNCNAME[0]} with PID: ${BASHPID}" 722 | 723 | case ${main_next_state} in 724 | 725 | ${main_state}) 726 | log_msg "ERROR" "Received request to change main state to existing state." 727 | ;; 728 | 729 | RUNNING|IDLE|STALL) 730 | 731 | log_msg "DEBUG" "Changing main state from: ${main_state} to: ${main_next_state}" 732 | main_state=${main_next_state} 733 | ;; 734 | 735 | *) 736 | 737 | log_msg "ERROR" "Received unrecognized main state change request: ${main_next_state}. Exiting now." 738 | kill $$ 2>/dev/null 739 | ;; 740 | esac 741 | } 742 | 743 | intercept_stderr() 744 | { 745 | # send stderr to log_msg and exit cake-autorate 746 | # use with redirection: exec 2> >(intercept_stderr) 747 | 748 | while read -r error 749 | do 750 | log_msg "ERROR" "${error}" 751 | kill $$ 2>/dev/null 752 | done 753 | } 754 | 755 | # shellcheck disable=SC1090,SC2311 756 | validate_config_entry() { 757 | # Must be called before loading config_path into the global scope. 758 | # 759 | # When the entry is invalid, two types are returned with the first type 760 | # being the invalid user type and second type is the default type with 761 | # the user needing to adapt the config file so that the entry uses the 762 | # default type. 763 | # 764 | # When the entry is valid, one type is returned and it will be the 765 | # the type of either the default or user type. However because in that 766 | # case they are both valid. It doesn't matter as they'd both have the 767 | # same type. 768 | 769 | local config_path=${1} 770 | 771 | local user_type 772 | local valid_type 773 | 774 | user_type=$(unset "${2}" && . "${config_path}" && typeof "${2}") 775 | valid_type=$(typeof "${2}") 776 | 777 | if [[ ${user_type} != "${valid_type}" ]] 778 | then 779 | printf '%s' "${user_type} ${valid_type}" 780 | return 781 | elif [[ ${user_type} != "string" ]] 782 | then 783 | printf '%s' "${valid_type}" 784 | return 785 | fi 786 | 787 | # extra validation for string, check for empty string 788 | local -n default_value=${2} 789 | local user_value 790 | user_value=$(. "${config_path}" && local -n x="${2}" && printf '%s' "${x}") 791 | 792 | # if user is empty but default is not, invalid entry 793 | if [[ -z ${user_value} && -n ${default_value} ]] 794 | then 795 | printf '%s' "${user_type} ${valid_type}" 796 | else 797 | printf '%s' "${valid_type}" 798 | fi 799 | } 800 | 801 | # ======= Start of the Main Routine ======== 802 | 803 | [[ -t 1 ]] && terminal=1 || terminal=0 804 | 805 | type logger &> /dev/null && use_logger=1 || use_logger=0 # only perform the test once. 806 | 807 | log_file_path=/var/log/cake-autorate.log 808 | 809 | # *** WARNING: take great care if attempting to alter the run_path! *** 810 | # *** cake-autorate issues mkdir -p ${run_path} and rm -r ${run_path} on exit. *** 811 | run_path=/var/run/cake-autorate/ 812 | 813 | # cake-autorate first argument is config file path 814 | if [[ -n ${1-} ]] 815 | then 816 | config_path="${1}" 817 | else 818 | config_path="${CONFIG_PREFIX}/config.primary.sh" 819 | fi 820 | 821 | if [[ ! -f ${config_path} ]] 822 | then 823 | log_msg "ERROR" "No config file found. Exiting now." 824 | exit 1 825 | fi 826 | 827 | # validate config entries before loading 828 | mapfile -t user_config < <(grep -E '^[^(#| )].*=' "${config_path}" | sed -e 's/[\t ]*\#.*//g' -e 's/=.*//g') 829 | config_error_count=0 830 | for key in "${user_config[@]}" 831 | do 832 | # Despite the fact that config_file_check is no longer required, 833 | # we make an exemption just in this case as that variable in 834 | # particular does not have any real impact to the operation 835 | # of the script. 836 | [[ ${key} == "config_file_check" ]] && continue 837 | 838 | # shellcheck disable=SC2076 839 | if [[ ! " ${valid_config_entries[*]} " =~ " ${key} " ]] 840 | then 841 | ((config_error_count++)) 842 | log_msg "ERROR" "The key: '${key}' in config file: '${config_path}' is not a valid config entry." 843 | else 844 | # shellcheck disable=SC2311 845 | read -r user supposed <<< "$(validate_config_entry "${config_path}" "${key}")" 846 | if [[ -n "${supposed}" ]] 847 | then 848 | error_msg="The value of '${key}' in config file: '${config_path}' is not a valid value of type: '${supposed}'." 849 | 850 | case ${user} in 851 | negative-*) error_msg="${error_msg} Also, negative numbers are not supported." ;; 852 | *) ;; 853 | esac 854 | 855 | log_msg "ERROR" "${error_msg}" 856 | unset error_msg 857 | 858 | ((config_error_count++)) 859 | fi 860 | unset user supposed 861 | fi 862 | done 863 | if ((config_error_count)) 864 | then 865 | log_msg "ERROR" "The config file: '${config_path}' contains ${config_error_count} error(s). Exiting now." 866 | exit 1 867 | fi 868 | unset valid_config_entries user_config config_error_count key 869 | 870 | # shellcheck source=config.primary.sh 871 | . "${config_path}" 872 | 873 | if [[ ${config_path} =~ config\.(.*)\.sh ]] 874 | then 875 | instance_id=${BASH_REMATCH[1]} run_path="/var/run/cake-autorate/${instance_id}" 876 | else 877 | log_msg "ERROR" "Instance identifier 'X' set by config.X.sh cannot be empty. Exiting now." 878 | exit 1 879 | fi 880 | 881 | if [[ -n ${log_file_path_override-} ]] 882 | then 883 | if [[ ! -d ${log_file_path_override} ]] 884 | then 885 | broken_log_file_path_override="${log_file_path_override}" 886 | log_file_path="/var/log/cake-autorate${instance_id:+.${instance_id}}.log" 887 | log_msg "ERROR" "Log file path override: '${broken_log_file_path_override}' does not exist. Exiting now." 888 | exit 1 889 | fi 890 | log_file_path="${log_file_path_override}/cake-autorate${instance_id:+.${instance_id}}.log" 891 | else 892 | log_file_path="/var/log/cake-autorate${instance_id:+.${instance_id}}.log" 893 | fi 894 | 895 | rotate_log_file 896 | 897 | # save stderr fd, redirect stderr to intercept_stderr 898 | # intercept_stderr sends stderr to log_msg and exits cake-autorate 899 | exec {original_stderr_fd}>&2 2> >(intercept_stderr) 900 | 901 | proc_pids['intercept_stderr']=${!} 902 | 903 | log_msg "SYSLOG" "Starting cake-autorate with PID: ${BASHPID} and config: ${config_path}" 904 | 905 | # ${run_path}/ is used to store temporary files 906 | # it should not exist on startup so if it does exit, else create the directory 907 | if [[ -d ${run_path} ]] 908 | then 909 | if [[ -f ${run_path}/proc_pids ]] && running_main_pid=$(awk -F= '/^main=/ {print $2}' "${run_path}/proc_pids") && [[ -d /proc/${running_main_pid} ]] 910 | then 911 | log_msg "ERROR" "${run_path} already exists and an instance appears to be running with main process pid ${running_main_pid}. Exiting script." 912 | trap - INT TERM EXIT 913 | exit 1 914 | else 915 | log_msg "DEBUG" "${run_path} already exists but no instance is running. Removing and recreating." 916 | rm -r "${run_path}" 917 | mkdir -p "${run_path}" 918 | fi 919 | else 920 | mkdir -p "${run_path}" 921 | fi 922 | 923 | proc_pids['main']=${BASHPID} 924 | 925 | no_reflectors=${#reflectors[@]} 926 | 927 | # Check ping binary exists 928 | command -v "${pinger_binary}" &> /dev/null || { log_msg "ERROR" "ping binary ${pinger_binary} does not exist. Exiting script."; exit 1; } 929 | 930 | # Check no_pingers <= no_reflectors 931 | (( no_pingers > no_reflectors )) && { log_msg "ERROR" "number of pingers cannot be greater than number of reflectors. Exiting script."; exit 1; } 932 | 933 | # Check dl/if interface not the same 934 | [[ "${dl_if}" == "${ul_if}" ]] && { log_msg "ERROR" "download interface and upload interface are both set to: '${dl_if}', but cannot be the same. Exiting script."; exit 1; } 935 | 936 | # Check bufferbloat detection threshold not greater than window length 937 | (( bufferbloat_detection_thr > bufferbloat_detection_window )) && { log_msg "ERROR" "bufferbloat_detection_thr cannot be greater than bufferbloat_detection_window. Exiting script."; exit 1; } 938 | 939 | # Check if connection_active_thr_kbps is greater than min dl/ul shaper rate 940 | (( connection_active_thr_kbps > min_dl_shaper_rate_kbps )) && { log_msg "ERROR" "connection_active_thr_kbps cannot be greater than min_dl_shaper_rate_kbps. Exiting script."; exit 1; } 941 | (( connection_active_thr_kbps > min_ul_shaper_rate_kbps )) && { log_msg "ERROR" "connection_active_thr_kbps cannot be greater than min_ul_shaper_rate_kbps. Exiting script."; exit 1; } 942 | 943 | # Passed error checks 944 | 945 | cpu_idx=0 946 | while : 947 | do 948 | read -r cpu_id rem 949 | case ${cpu_id} in 950 | cpu*) 951 | cpu_ids[cpu_idx]=${cpu_id^^} 952 | ((cpu_idx++)) 953 | ;; 954 | *) 955 | break 956 | ;; 957 | esac 958 | done <(:) 973 | maintain_log_file & 974 | proc_pids['maintain_log_file']=${!} 975 | fi 976 | 977 | # test if stdout is a tty (terminal) 978 | if ! ((terminal)) 979 | then 980 | echo "stdout not a terminal so redirecting output to: ${log_file_path}" 981 | ((log_to_file)) && exec 1>&${log_fd} 982 | fi 983 | 984 | # Initialize rx_bytes_path and tx_bytes_path if not set 985 | if [[ -z ${rx_bytes_path-} ]] 986 | then 987 | case ${dl_if} in 988 | veth*) 989 | rx_bytes_path="/sys/class/net/${dl_if}/statistics/tx_bytes" 990 | ;; 991 | ifb*) 992 | rx_bytes_path="/sys/class/net/${dl_if}/statistics/tx_bytes" 993 | ;; 994 | *) 995 | rx_bytes_path="/sys/class/net/${dl_if}/statistics/tx_bytes" 996 | ;; 997 | esac 998 | fi 999 | if [[ -z ${tx_bytes_path-} ]] 1000 | then 1001 | case ${ul_if} in 1002 | veth*) 1003 | tx_bytes_path="/sys/class/net/${ul_if}/statistics/rx_bytes" 1004 | ;; 1005 | ifb*) 1006 | tx_bytes_path="/sys/class/net/${ul_if}/statistics/rx_bytes" 1007 | ;; 1008 | *) 1009 | tx_bytes_path="/sys/class/net/${ul_if}/statistics/tx_bytes" 1010 | ;; 1011 | esac 1012 | fi 1013 | 1014 | if ((debug)) 1015 | then 1016 | log_msg "DEBUG" "CAKE-autorate version: ${cake_autorate_version}" 1017 | log_msg "DEBUG" "config_path: ${config_path}" 1018 | log_msg "DEBUG" "run_path: ${run_path}" 1019 | log_msg "DEBUG" "log_file_path: ${log_file_path}" 1020 | log_msg "DEBUG" "pinger_binary:${pinger_binary}" 1021 | log_msg "DEBUG" "download interface: ${dl_if} (${min_dl_shaper_rate_kbps} / ${base_dl_shaper_rate_kbps} / ${max_dl_shaper_rate_kbps})" 1022 | log_msg "DEBUG" "upload interface: ${ul_if} (${min_ul_shaper_rate_kbps} / ${base_ul_shaper_rate_kbps} / ${max_ul_shaper_rate_kbps})" 1023 | log_msg "DEBUG" "rx_bytes_path: ${rx_bytes_path}" 1024 | log_msg "DEBUG" "tx_bytes_path: ${tx_bytes_path}" 1025 | fi 1026 | 1027 | # Check interfaces are up and wait if necessary for them to come up 1028 | verify_ifs_up 1029 | 1030 | # Initialize variables 1031 | 1032 | # Convert human readable parameters to values that work with integer arithmetic 1033 | 1034 | printf -v dl_avg_owd_delta_max_adjust_up_thr_us %.0f "${dl_avg_owd_delta_max_adjust_up_thr_ms}e3" 1035 | printf -v ul_avg_owd_delta_max_adjust_up_thr_us %.0f "${ul_avg_owd_delta_max_adjust_up_thr_ms}e3" 1036 | printf -v dl_owd_delta_delay_thr_us %.0f "${dl_owd_delta_delay_thr_ms}e3" 1037 | printf -v ul_owd_delta_delay_thr_us %.0f "${ul_owd_delta_delay_thr_ms}e3" 1038 | printf -v dl_avg_owd_delta_max_adjust_down_thr_us %.0f "${dl_avg_owd_delta_max_adjust_down_thr_ms}e3" 1039 | printf -v ul_avg_owd_delta_max_adjust_down_thr_us %.0f "${ul_avg_owd_delta_max_adjust_down_thr_ms}e3" 1040 | printf -v alpha_baseline_increase %.0f "${alpha_baseline_increase}e6" 1041 | printf -v alpha_baseline_decrease %.0f "${alpha_baseline_decrease}e6" 1042 | printf -v alpha_delta_ewma %.0f "${alpha_delta_ewma}e6" 1043 | printf -v shaper_rate_min_adjust_down_bufferbloat %.0f "${shaper_rate_min_adjust_down_bufferbloat}e3" 1044 | printf -v shaper_rate_max_adjust_down_bufferbloat %.0f "${shaper_rate_max_adjust_down_bufferbloat}e3" 1045 | printf -v shaper_rate_min_adjust_up_load_high %.0f "${shaper_rate_min_adjust_up_load_high}e3" 1046 | printf -v shaper_rate_max_adjust_up_load_high %.0f "${shaper_rate_max_adjust_up_load_high}e3" 1047 | printf -v shaper_rate_adjust_down_load_low %.0f "${shaper_rate_adjust_down_load_low}e3" 1048 | printf -v shaper_rate_adjust_up_load_low %.0f "${shaper_rate_adjust_up_load_low}e3" 1049 | printf -v high_load_thr_percent %.0f "${high_load_thr}e2" 1050 | printf -v reflector_ping_interval_ms %.0f "${reflector_ping_interval_s}e3" 1051 | printf -v reflector_ping_interval_us %.0f "${reflector_ping_interval_s}e6" 1052 | printf -v reflector_health_check_interval_us %.0f "${reflector_health_check_interval_s}e6" 1053 | printf -v monitor_achieved_rates_interval_us %.0f "${monitor_achieved_rates_interval_ms}e3" 1054 | printf -v monitor_cpu_usage_interval_us %.0f "${monitor_cpu_usage_interval_ms}e3" 1055 | printf -v sustained_idle_sleep_thr_us %.0f "${sustained_idle_sleep_thr_s}e6" 1056 | printf -v reflector_response_deadline_us %.0f "${reflector_response_deadline_s}e6" 1057 | printf -v reflector_sum_owd_baselines_delta_thr_us %.0f "${reflector_sum_owd_baselines_delta_thr_ms}e3" 1058 | printf -v reflector_owd_delta_ewma_delta_thr_us %.0f "${reflector_owd_delta_ewma_delta_thr_ms}e3" 1059 | printf -v startup_wait_us %.0f "${startup_wait_s}e6" 1060 | printf -v global_ping_response_timeout_us %.0f "${global_ping_response_timeout_s}e6" 1061 | printf -v bufferbloat_refractory_period_us %.0f "${bufferbloat_refractory_period_ms}e3" 1062 | printf -v decay_refractory_period_us %.0f "${decay_refractory_period_ms}e3" 1063 | 1064 | (( 1065 | reflector_replacement_interval_us=reflector_replacement_interval_mins*60*1000000, 1066 | reflector_comparison_interval_us=reflector_comparison_interval_mins*60*1000000, 1067 | 1068 | ping_response_interval_us=reflector_ping_interval_us/no_pingers, 1069 | ping_response_interval_ms=ping_response_interval_us/1000, 1070 | 1071 | stall_detection_timeout_us=stall_detection_thr*ping_response_interval_us 1072 | )) 1073 | 1074 | printf -v stall_detection_timeout_s %.2f "${stall_detection_timeout_us}e-6" 1075 | 1076 | declare -A achieved_rate_kbps \ 1077 | achieved_rate_updated \ 1078 | bufferbloat_detected \ 1079 | load_percent \ 1080 | load_condition \ 1081 | t_last_bufferbloat_us \ 1082 | t_last_decay_us \ 1083 | shaper_rate_kbps \ 1084 | last_shaper_rate_kbps \ 1085 | base_shaper_rate_kbps \ 1086 | min_shaper_rate_kbps \ 1087 | max_shaper_rate_kbps \ 1088 | interface \ 1089 | adjust_shaper_rate \ 1090 | avg_owd_delta_us \ 1091 | avg_owd_delta_max_adjust_up_thr_us \ 1092 | avg_owd_delta_max_adjust_down_thr_us \ 1093 | compensated_owd_delta_delay_thr_us \ 1094 | compensated_avg_owd_delta_max_adjust_up_thr_us \ 1095 | compensated_avg_owd_delta_max_adjust_down_thr_us \ 1096 | dl_owd_baselines_us \ 1097 | ul_owd_baselines_us \ 1098 | dl_owd_delta_ewmas_us \ 1099 | ul_owd_delta_ewmas_us \ 1100 | last_timestamp_reflectors_us 1101 | 1102 | base_shaper_rate_kbps[dl]=${base_dl_shaper_rate_kbps} base_shaper_rate_kbps[ul]=${base_ul_shaper_rate_kbps} \ 1103 | min_shaper_rate_kbps[dl]=${min_dl_shaper_rate_kbps} min_shaper_rate_kbps[ul]=${min_ul_shaper_rate_kbps} \ 1104 | max_shaper_rate_kbps[dl]=${max_dl_shaper_rate_kbps} max_shaper_rate_kbps[ul]=${max_ul_shaper_rate_kbps} \ 1105 | shaper_rate_kbps[dl]=${base_dl_shaper_rate_kbps} shaper_rate_kbps[ul]=${base_ul_shaper_rate_kbps} \ 1106 | achieved_rate_kbps[dl]=0 achieved_rate_kbps[ul]=0 \ 1107 | achieved_rate_updated[dl]=0 achieved_rate_updated[ul]=0 \ 1108 | last_shaper_rate_kbps[dl]=0 last_shaper_rate_kbps[ul]=0 \ 1109 | interface[dl]=${dl_if} interface[ul]=${ul_if} \ 1110 | adjust_shaper_rate[dl]=${adjust_dl_shaper_rate} adjust_shaper_rate[ul]=${adjust_ul_shaper_rate} \ 1111 | dl_max_wire_packet_size_bits=0 ul_max_wire_packet_size_bits=0 1112 | 1113 | get_max_wire_packet_size_bits "${dl_if}" dl_max_wire_packet_size_bits 1114 | get_max_wire_packet_size_bits "${ul_if}" ul_max_wire_packet_size_bits 1115 | 1116 | avg_owd_delta_us[dl]=0 avg_owd_delta_us[ul]=0 1117 | 1118 | # shellcheck disable=SC2034 1119 | avg_owd_delta_max_adjust_up_thr_us[dl]=${dl_avg_owd_delta_max_adjust_up_thr_us} avg_owd_delta_max_adjust_up_thr_us[ul]=${ul_avg_owd_delta_max_adjust_up_thr_us} \ 1120 | avg_owd_delta_max_adjust_down_thr_us[dl]=${dl_avg_owd_delta_max_adjust_down_thr_us} avg_owd_delta_max_adjust_down_thr_us[ul]=${ul_avg_owd_delta_max_adjust_down_thr_us} 1121 | 1122 | set_shaper_rate "dl" 1123 | set_shaper_rate "ul" 1124 | 1125 | dl_rate_load_condition="idle" ul_rate_load_condition="idle" 1126 | 1127 | mapfile -t dl_delays < <(for ((i=0; i < bufferbloat_detection_window; i++)); do echo 0; done) 1128 | mapfile -t ul_delays < <(for ((i=0; i < bufferbloat_detection_window; i++)); do echo 0; done) 1129 | mapfile -t dl_owd_deltas_us < <(for ((i=0; i < bufferbloat_detection_window; i++)); do echo 0; done) 1130 | mapfile -t ul_owd_deltas_us < <(for ((i=0; i < bufferbloat_detection_window; i++)); do echo 0; done) 1131 | 1132 | delays_idx=0 sum_dl_delays=0 sum_ul_delays=0 sum_dl_owd_deltas_us=0 sum_ul_owd_deltas_us=0 1133 | 1134 | # Randomize reflectors array providing randomize_reflectors set to 1 1135 | ((randomize_reflectors)) && randomize_array reflectors 1136 | 1137 | for (( reflector=0; reflector 0 1170 | if ((startup_wait_us>0)) 1171 | then 1172 | log_msg "DEBUG" "Waiting ${startup_wait_s} seconds before startup." 1173 | sleep_us "${startup_wait_us}" 1174 | fi 1175 | 1176 | t_start_us=${EPOCHREALTIME/.} \ 1177 | t_last_cpu_usage_check_us=${t_start_us} \ 1178 | t_last_bufferbloat_us[dl]=${t_start_us} t_last_bufferbloat_us[ul]=${t_start_us} \ 1179 | t_last_decay_us[dl]=${t_start_us} t_last_decay_us[ul]=${t_start_us} \ 1180 | t_last_reflector_health_check_us=${t_start_us} \ 1181 | t_sustained_connection_idle_us=0 t_last_connection_idle_us=${t_start_us} reflectors_last_timestamp_us=${t_start_us} \ 1182 | pingers_t_start_us=${t_start_us} t_last_reflector_replacement_us=${t_start_us} t_last_reflector_comparison_us=${t_start_us} 1183 | 1184 | for ((reflector=0; reflector < no_reflectors; reflector++)) 1185 | do 1186 | last_timestamp_reflectors_us[${reflectors[reflector]}]=${t_start_us} 1187 | done 1188 | 1189 | # For each pinger initialize record of offences 1190 | for ((pinger=0; pinger < no_pingers; pinger++)) 1191 | do 1192 | # shellcheck disable=SC2178 1193 | declare -n reflector_offences=reflector_${pinger}_offences 1194 | for ((i=0; i high_load_thr_percent )) 1226 | then 1227 | dl_rate_load_condition="dl_high" 1228 | elif (( achieved_rate_kbps[dl] > connection_active_thr_kbps )) 1229 | then 1230 | dl_rate_load_condition="dl_low" 1231 | else 1232 | dl_rate_load_condition="dl_idle" 1233 | fi 1234 | 1235 | if (( load_percent[ul] > high_load_thr_percent )) 1236 | then 1237 | ul_rate_load_condition="ul_high" 1238 | elif (( achieved_rate_kbps[ul] > connection_active_thr_kbps )) 1239 | then 1240 | ul_rate_load_condition="ul_low" 1241 | else 1242 | ul_rate_load_condition="ul_idle" 1243 | fi 1244 | fi 1245 | ;; 1246 | *) 1247 | case "${pinger_binary}" in 1248 | 1249 | tsping) 1250 | if ((${#command[@]} == 10)) 1251 | then 1252 | timestamp=${command[0]} reflector=${command[1]} seq=${command[2]} dl_owd_ms=${command[8]} ul_owd_ms=${command[9]} reflector_response=1 1253 | fi 1254 | ;; 1255 | fping) 1256 | if ((${#command[@]} == 12)) 1257 | then 1258 | timestamp=${command[0]} reflector=${command[1]} seq=${command[3]} rtt_ms=${command[6]} reflector_response=1 1259 | fi 1260 | ;; 1261 | ping) 1262 | if ((${#command[@]} == 9)) 1263 | then 1264 | timestamp=${command[0]} reflector=${command[4]} seq=${command[5]} rtt_ms=${command[7]} reflector_response=1 1265 | fi 1266 | ;; 1267 | *) 1268 | log_msg "ERROR" "Unknown pinger binary: ${pinger_binary}" 1269 | kill $$ 2>/dev/null 1270 | ;; 1271 | esac 1272 | ;; 1273 | esac 1274 | 1275 | t_start_us=${EPOCHREALTIME/.} 1276 | 1277 | case ${main_state} in 1278 | 1279 | RUNNING) 1280 | if ((reflector_response)) 1281 | then 1282 | # parse pinger response according to pinger binary 1283 | case ${pinger_binary} in 1284 | tsping) 1285 | dl_owd_us=${dl_owd_ms}000 ul_owd_us=${ul_owd_ms}000 1286 | 1287 | (( 1288 | dl_owd_delta_us=dl_owd_us - dl_owd_baselines_us[${reflector}], 1289 | ul_owd_delta_us=ul_owd_us - ul_owd_baselines_us[${reflector}] 1290 | )) 1291 | 1292 | # tsping employs ICMP type 13 and works with timestamps: Originate; Received; Transmit; and Finished, such that: 1293 | # 1294 | # dl_owd_us = Finished - Transmit 1295 | # ul_owd_us = Received - Originate 1296 | # 1297 | # The timestamps are supposed to relate to milliseconds past midnight UTC, albeit implementation varies, and, 1298 | # in any case, timestamps rollover at the local and/or remote ends, and the rollover may not be synchronized. 1299 | # 1300 | # Such an event would result in a huge spike in dl_owd_us or ul_owd_us and a large delta relative to the baseline. 1301 | # 1302 | # So, to compensate, in the event that delta > 50 mins, immediately reset the baselines to the new dl_owd_us and ul_owd_us. 1303 | # 1304 | # Happilly, the sum of dl_owd_baseline_us and ul_owd_baseline_us will roughly equal rtt_baseline_us. 1305 | # And since Transmit is approximately equal to Received, RTT is approximately equal to Finished - Originate. 1306 | # And thus the sum of dl_owd_baseline_us and ul_owd_baseline_us should not be affected by the rollover/compensation. 1307 | # Hence working with this sum, rather than the individual components, is useful for the reflector health check. 1308 | 1309 | if (( (${dl_owd_delta_us#-} + ${ul_owd_delta_us#-}) < 3000000000 )) 1310 | then 1311 | 1312 | (( 1313 | dl_alpha = dl_owd_us >= dl_owd_baselines_us[${reflector}] ? alpha_baseline_increase : alpha_baseline_decrease, 1314 | ul_alpha = ul_owd_us >= ul_owd_baselines_us[${reflector}] ? alpha_baseline_increase : alpha_baseline_decrease, 1315 | 1316 | dl_owd_baselines_us[${reflector}]=(dl_alpha*dl_owd_us+(1000000-dl_alpha)*dl_owd_baselines_us[${reflector}])/1000000, 1317 | ul_owd_baselines_us[${reflector}]=(ul_alpha*ul_owd_us+(1000000-ul_alpha)*ul_owd_baselines_us[${reflector}])/1000000, 1318 | 1319 | dl_owd_delta_us=dl_owd_us - dl_owd_baselines_us[${reflector}], 1320 | ul_owd_delta_us=ul_owd_us - ul_owd_baselines_us[${reflector}] 1321 | )) 1322 | else 1323 | dl_owd_baselines_us[${reflector}]=${dl_owd_us} ul_owd_baselines_us[${reflector}]=${ul_owd_us} dl_owd_delta_us=0 ul_owd_delta_us=0 1324 | fi 1325 | 1326 | if (( load_percent[dl] < high_load_thr_percent && load_percent[ul] < high_load_thr_percent)) 1327 | then 1328 | (( 1329 | dl_owd_delta_ewmas_us[${reflector}]=(alpha_delta_ewma*dl_owd_delta_us+(1000000-alpha_delta_ewma)*dl_owd_delta_ewmas_us[${reflector}])/1000000, 1330 | ul_owd_delta_ewmas_us[${reflector}]=(alpha_delta_ewma*ul_owd_delta_us+(1000000-alpha_delta_ewma)*ul_owd_delta_ewmas_us[${reflector}])/1000000 1331 | )) 1332 | fi 1333 | 1334 | timestamp_us=${timestamp//[.]} 1335 | 1336 | ;; 1337 | fping) 1338 | seq=${seq//[\[\]]} 1339 | printf -v rtt_us %.3f "${rtt_ms}" 1340 | 1341 | (( 1342 | dl_owd_us=10#${rtt_us//.}/2, 1343 | ul_owd_us=dl_owd_us, 1344 | dl_alpha = dl_owd_us >= dl_owd_baselines_us[${reflector}] ? alpha_baseline_increase : alpha_baseline_decrease, 1345 | 1346 | dl_owd_baselines_us[${reflector}]=(dl_alpha*dl_owd_us+(1000000-dl_alpha)*dl_owd_baselines_us[${reflector}])/1000000, 1347 | ul_owd_baselines_us[${reflector}]=dl_owd_baselines_us[${reflector}], 1348 | 1349 | dl_owd_delta_us=dl_owd_us - dl_owd_baselines_us[${reflector}], 1350 | ul_owd_delta_us=dl_owd_delta_us 1351 | )) 1352 | 1353 | if (( load_percent[dl] < high_load_thr_percent && load_percent[ul] < high_load_thr_percent)) 1354 | then 1355 | (( 1356 | dl_owd_delta_ewmas_us[${reflector}]=(alpha_delta_ewma*dl_owd_delta_us+(1000000-alpha_delta_ewma)*dl_owd_delta_ewmas_us[${reflector}])/1000000, 1357 | ul_owd_delta_ewmas_us[${reflector}]=dl_owd_delta_ewmas_us[${reflector}] 1358 | )) 1359 | fi 1360 | 1361 | timestamp_us=${timestamp//[\[\].]}0 1362 | 1363 | ;; 1364 | ping) 1365 | reflector=${reflector//:/} seq=${seq//icmp_seq=} rtt_ms=${rtt_ms//time=} 1366 | 1367 | printf -v rtt_us %.3f "${rtt_ms}" 1368 | 1369 | (( 1370 | dl_owd_us=10#${rtt_us//.}/2, 1371 | ul_owd_us=dl_owd_us, 1372 | 1373 | dl_alpha = dl_owd_us >= dl_owd_baselines_us[${reflector}] ? alpha_baseline_increase : alpha_baseline_decrease, 1374 | 1375 | dl_owd_baselines_us[${reflector}]=(dl_alpha*dl_owd_us+(1000000-dl_alpha)*dl_owd_baselines_us[${reflector}])/1000000, 1376 | ul_owd_baselines_us[${reflector}]=dl_owd_baselines_us[${reflector}], 1377 | 1378 | dl_owd_delta_us=dl_owd_us - dl_owd_baselines_us[${reflector}], 1379 | ul_owd_delta_us=dl_owd_delta_us 1380 | )) 1381 | 1382 | if (( load_percent[dl] < high_load_thr_percent && load_percent[ul] < high_load_thr_percent)) 1383 | then 1384 | (( 1385 | dl_owd_delta_ewmas_us[${reflector}]=(alpha_delta_ewma*dl_owd_delta_us+(1000000-alpha_delta_ewma)*dl_owd_delta_ewmas_us[${reflector}])/1000000, 1386 | ul_owd_delta_ewmas_us[${reflector}]=dl_owd_delta_ewmas_us[${reflector}] 1387 | )) 1388 | fi 1389 | 1390 | timestamp_us=${timestamp//[\[\].]} 1391 | 1392 | ;; 1393 | *) 1394 | log_msg "ERROR" "Unknown pinger binary: ${pinger_binary}" 1395 | exit 1 1396 | ;; 1397 | esac 1398 | 1399 | timestamp=${timestamp//[\[\]]} 1400 | 1401 | last_timestamp_reflectors_us[${reflector}]=${timestamp_us} reflectors_last_timestamp_us=${timestamp_us} 1402 | 1403 | if (( (t_start_us - 10#${reflectors_last_timestamp_us})>500000 )) 1404 | then 1405 | log_msg "DEBUG" "processed response from [${reflector}] that is > 500ms old. Skipping." 1406 | continue 1407 | fi 1408 | 1409 | # Keep track of delays across detection window, detect any bufferbloat and determine load percentages 1410 | (( 1411 | dl_delays[delays_idx] && (sum_dl_delays--), 1412 | dl_delays[delays_idx] = dl_owd_delta_us > compensated_owd_delta_delay_thr_us[dl] ? 1 : 0, 1413 | dl_delays[delays_idx] && (sum_dl_delays++), 1414 | 1415 | sum_dl_owd_deltas_us -= dl_owd_deltas_us[delays_idx], 1416 | dl_owd_deltas_us[delays_idx] = dl_owd_delta_us, 1417 | sum_dl_owd_deltas_us += dl_owd_delta_us, 1418 | 1419 | ul_delays[delays_idx] && (sum_ul_delays--), 1420 | ul_delays[delays_idx] = ul_owd_delta_us > compensated_owd_delta_delay_thr_us[ul] ? 1 : 0, 1421 | ul_delays[delays_idx] && (sum_ul_delays++), 1422 | 1423 | sum_ul_owd_deltas_us -= ul_owd_deltas_us[delays_idx], 1424 | ul_owd_deltas_us[delays_idx] = ul_owd_delta_us, 1425 | sum_ul_owd_deltas_us += ul_owd_delta_us, 1426 | 1427 | delays_idx=(delays_idx+1)%bufferbloat_detection_window, 1428 | 1429 | avg_owd_delta_us[dl] = sum_dl_owd_deltas_us / bufferbloat_detection_window, 1430 | avg_owd_delta_us[ul] = sum_ul_owd_deltas_us / bufferbloat_detection_window, 1431 | 1432 | bufferbloat_detected[dl] = sum_dl_delays >= bufferbloat_detection_thr ? 1 : 0, 1433 | bufferbloat_detected[ul] = sum_ul_delays >= bufferbloat_detection_thr ? 1 : 0, 1434 | 1435 | load_percent[dl]=100*achieved_rate_kbps[dl]/shaper_rate_kbps[dl], 1436 | load_percent[ul]=100*achieved_rate_kbps[ul]/shaper_rate_kbps[ul] 1437 | )) 1438 | 1439 | load_condition[dl]=${dl_rate_load_condition} load_condition[ul]=${ul_rate_load_condition} 1440 | 1441 | ((bufferbloat_detected[dl])) && load_condition[dl]+=_bb 1442 | ((bufferbloat_detected[ul])) && load_condition[ul]+=_bb 1443 | 1444 | # Update shaper rates 1445 | for direction in dl ul 1446 | do 1447 | case ${load_condition[${direction}]} in 1448 | 1449 | # bufferbloat detected, so decrease the rate providing not inside bufferbloat refractory period 1450 | *bb*) 1451 | if (( t_start_us > (t_last_bufferbloat_us[${direction}]+bufferbloat_refractory_period_us) )) 1452 | then 1453 | if (( compensated_avg_owd_delta_max_adjust_down_thr_us[${direction}] <= compensated_owd_delta_delay_thr_us[${direction}] )) 1454 | then 1455 | shaper_rate_adjust_down_factor=1000 1456 | elif (( (avg_owd_delta_us[${direction}]-compensated_owd_delta_delay_thr_us[${direction}]) > 0 )) 1457 | then 1458 | (( 1459 | shaper_rate_adjust_down_factor=1000*(avg_owd_delta_us[${direction}]-compensated_owd_delta_delay_thr_us[${direction}])/(compensated_avg_owd_delta_max_adjust_down_thr_us[${direction}]-compensated_owd_delta_delay_thr_us[${direction}]), 1460 | shaper_rate_adjust_down_factor > 1000 && (shaper_rate_adjust_down_factor=1000) 1461 | )) 1462 | else 1463 | shaper_rate_adjust_down_factor=0 1464 | fi 1465 | (( 1466 | shaper_rate_adjust_down=1000*shaper_rate_min_adjust_down_bufferbloat-shaper_rate_adjust_down_factor*(shaper_rate_min_adjust_down_bufferbloat-shaper_rate_max_adjust_down_bufferbloat), 1467 | shaper_rate_kbps[${direction}]=shaper_rate_kbps[${direction}]*shaper_rate_adjust_down/1000000, 1468 | t_last_bufferbloat_us[${direction}]=t_start_us, 1469 | t_last_decay_us[${direction}]=t_start_us 1470 | )) 1471 | fi 1472 | ;; 1473 | # high load, so increase rate providing not inside bufferbloat refractory period 1474 | *high*) 1475 | if (( achieved_rate_updated[${direction}] && t_start_us > (t_last_bufferbloat_us[${direction}]+bufferbloat_refractory_period_us) )) 1476 | then 1477 | if (( compensated_owd_delta_delay_thr_us[${direction}] <= compensated_avg_owd_delta_max_adjust_up_thr_us[${direction}] )) 1478 | then 1479 | shaper_rate_adjust_up_factor=1000 1480 | elif (( (compensated_owd_delta_delay_thr_us[${direction}]-avg_owd_delta_us[${direction}]) > 0 )) 1481 | then 1482 | (( 1483 | shaper_rate_adjust_up_factor=1000*(compensated_owd_delta_delay_thr_us[${direction}]-avg_owd_delta_us[${direction}])/(compensated_owd_delta_delay_thr_us[${direction}]-compensated_avg_owd_delta_max_adjust_up_thr_us[${direction}]), 1484 | shaper_rate_adjust_up_factor > 1000 && (shaper_rate_adjust_up_factor=1000) 1485 | )) 1486 | else 1487 | shaper_rate_adjust_up_factor=0 1488 | fi 1489 | 1490 | (( 1491 | shaper_rate_adjust_up=1000*shaper_rate_min_adjust_up_load_high-shaper_rate_adjust_up_factor*(shaper_rate_min_adjust_up_load_high-shaper_rate_max_adjust_up_load_high), 1492 | shaper_rate_kbps[${direction}]=shaper_rate_kbps[${direction}]*shaper_rate_adjust_up/1000000, 1493 | achieved_rate_updated[${direction}]=0, 1494 | t_last_decay_us[${direction}]=t_start_us 1495 | )) 1496 | fi 1497 | ;; 1498 | # low or idle load, so determine whether to decay down towards base rate, decay up towards base rate, or set as base rate 1499 | *low*|*idle*) 1500 | if (( t_start_us > (t_last_decay_us[${direction}]+decay_refractory_period_us) )) 1501 | then 1502 | 1503 | if ((shaper_rate_kbps[${direction}] > base_shaper_rate_kbps[${direction}])) 1504 | then 1505 | (( 1506 | decayed_shaper_rate_kbps=(shaper_rate_kbps[${direction}]*shaper_rate_adjust_down_load_low)/1000, 1507 | shaper_rate_kbps[${direction}]=decayed_shaper_rate_kbps > base_shaper_rate_kbps[${direction}] ? decayed_shaper_rate_kbps : base_shaper_rate_kbps[${direction}] 1508 | )) 1509 | elif ((shaper_rate_kbps[${direction}] < base_shaper_rate_kbps[${direction}])) 1510 | then 1511 | (( 1512 | decayed_shaper_rate_kbps=(shaper_rate_kbps[${direction}]*shaper_rate_adjust_up_load_low)/1000, 1513 | shaper_rate_kbps[${direction}] = decayed_shaper_rate_kbps < base_shaper_rate_kbps[${direction}] ? decayed_shaper_rate_kbps : base_shaper_rate_kbps[${direction}] 1514 | )) 1515 | fi 1516 | 1517 | t_last_decay_us[${direction}]=${t_start_us} 1518 | fi 1519 | ;; 1520 | *) 1521 | log_msg "ERROR" "unknown load condition: ${load_condition[${direction}]}" 1522 | kill $$ 2>/dev/null 1523 | ;; 1524 | esac 1525 | done 1526 | 1527 | # make sure that updated shaper rates fall between configured minimum and maximum shaper rates 1528 | (( 1529 | shaper_rate_kbps[dl] < min_shaper_rate_kbps[dl] && (shaper_rate_kbps[dl]=${min_shaper_rate_kbps[dl]}) || 1530 | shaper_rate_kbps[dl] > max_shaper_rate_kbps[dl] && (shaper_rate_kbps[dl]=${max_shaper_rate_kbps[dl]}), 1531 | shaper_rate_kbps[ul] < min_shaper_rate_kbps[ul] && (shaper_rate_kbps[ul]=${min_shaper_rate_kbps[ul]}) || 1532 | shaper_rate_kbps[ul] > max_shaper_rate_kbps[ul] && (shaper_rate_kbps[ul]=${max_shaper_rate_kbps[ul]}) 1533 | )) 1534 | 1535 | set_shaper_rate "dl" 1536 | set_shaper_rate "ul" 1537 | 1538 | # update CPU usage stats if CPU monitoring interval exceeded 1539 | if (( (output_cpu_stats || output_cpu_raw_stats) && t_start_us > t_last_cpu_usage_check_us + monitor_cpu_usage_interval_us )) 1540 | then 1541 | stats_read_time_us=${EPOCHREALTIME} 1542 | for (( cpu_idx=0; cpu_idx<=cpu_cores; cpu_idx++ )) 1543 | do 1544 | read -r -a cpu_stats 1545 | if (( output_cpu_stats )) 1546 | then 1547 | cpu_stats_vals=${cpu_stats[@]:1} 1548 | (( 1549 | cpu_sum=${cpu_stats_vals// /+}, 1550 | cpu_delta=cpu_sum-last_cpu_sum[cpu_idx], 1551 | cpu_idle=${cpu_stats[4]}-last_cpu_idle[cpu_idx], 1552 | cpu_usage[cpu_idx]=(100*(cpu_delta-cpu_idle)/cpu_delta), 1553 | last_cpu_sum[cpu_idx]=cpu_sum, 1554 | last_cpu_idle[cpu_idx]=${cpu_stats[4]} 1555 | )) 1556 | fi 1557 | if (( output_cpu_raw_stats )) 1558 | then 1559 | cpu_raw_stats="${cpu_stats[@]}" cpu_raw_stats="${stats_read_time_us}; ${cpu_raw_stats// /; }" 1560 | log_msg "CPU_RAW" "${cpu_raw_stats}" 1561 | fi 1562 | done sustained_idle_sleep_thr_us)) 1598 | then 1599 | change_state_main "IDLE" 1600 | 1601 | log_msg "DEBUG" "Connection idle. Waiting for minimum load." 1602 | 1603 | if ((min_shaper_rates_enforcement)) 1604 | then 1605 | log_msg "DEBUG" "Enforcing minimum shaper rates." 1606 | shaper_rate_kbps[dl]=${min_dl_shaper_rate_kbps} shaper_rate_kbps[ul]=${min_ul_shaper_rate_kbps} 1607 | set_shaper_rate "dl" 1608 | set_shaper_rate "ul" 1609 | fi 1610 | 1611 | stop_pingers 1612 | 1613 | t_sustained_connection_idle_us=0 sustained_connection_idle=0 1614 | fi 1615 | ;; 1616 | *) 1617 | t_sustained_connection_idle_us=0 sustained_connection_idle=0 1618 | ;; 1619 | esac 1620 | fi 1621 | elif (( (t_start_us - reflectors_last_timestamp_us) > stall_detection_timeout_us )) 1622 | then 1623 | 1624 | log_msg "DEBUG" "Warning: no reflector response within: ${stall_detection_timeout_s} seconds. Checking loads." 1625 | 1626 | log_msg "DEBUG" "load check is: (( ${achieved_rate_kbps[dl]} kbps > ${connection_stall_thr_kbps} kbps for download && ${achieved_rate_kbps[ul]} kbps > ${connection_stall_thr_kbps} kbps for upload ))" 1627 | 1628 | # non-zero load so despite no reflector response within stall interval, the connection not considered to have stalled 1629 | # and therefore resume normal operation 1630 | if (( achieved_rate_kbps[dl] > connection_stall_thr_kbps && achieved_rate_kbps[ul] > connection_stall_thr_kbps )) 1631 | then 1632 | 1633 | log_msg "DEBUG" "load above connection stall threshold so resuming normal operation." 1634 | else 1635 | change_state_main "STALL" 1636 | 1637 | t_connection_stall_time_us=${t_start_us} global_ping_response_timeout=0 1638 | fi 1639 | 1640 | fi 1641 | 1642 | if (( t_start_us > t_last_reflector_health_check_us + reflector_health_check_interval_us )) 1643 | then 1644 | if (( t_start_us>(t_last_reflector_replacement_us+reflector_replacement_interval_us) )) 1645 | then 1646 | ((pinger=RANDOM%no_pingers)) 1647 | log_msg "DEBUG" "reflector: ${reflectors[pinger]} randomly selected for replacement." 1648 | replace_pinger_reflector "${pinger}" 1649 | t_last_reflector_replacement_us=${t_start_us} 1650 | continue 1651 | fi 1652 | 1653 | if (( t_start_us>(t_last_reflector_comparison_us+reflector_comparison_interval_us) )) 1654 | then 1655 | 1656 | t_last_reflector_comparison_us=${t_start_us} 1657 | 1658 | [[ "${dl_owd_baselines_us[${reflectors[0]}]:-}" && "${dl_owd_baselines_us[${reflectors[0]}]:-}" && "${ul_owd_baselines_us[${reflectors[0]}]:-}" && "${ul_owd_baselines_us[${reflectors[0]}]:-}" ]] || continue 1659 | 1660 | (( 1661 | min_sum_owd_baselines_us = dl_owd_baselines_us[${reflectors[0]}] + ul_owd_baselines_us[${reflectors[0]}], 1662 | min_dl_owd_delta_ewma_us=dl_owd_delta_ewmas_us[${reflectors[0]}], 1663 | min_ul_owd_delta_ewma_us=ul_owd_delta_ewmas_us[${reflectors[0]}] 1664 | )) 1665 | 1666 | for ((pinger=0; pinger < no_pingers; pinger++)) 1667 | do 1668 | [[ ${dl_owd_baselines_us[${reflectors[pinger]}]:-} && ${dl_owd_delta_ewmas_us[${reflectors[pinger]}]:-} && ${ul_owd_baselines_us[${reflectors[pinger]}]:-} && ${ul_owd_delta_ewmas_us[${reflectors[pinger]}]:-} ]] || continue 2 1669 | 1670 | (( 1671 | sum_owd_baselines_us[pinger] = dl_owd_baselines_us[${reflectors[pinger]}] + ul_owd_baselines_us[${reflectors[pinger]}], 1672 | sum_owd_baselines_us[pinger] < min_sum_owd_baselines_us && (min_sum_owd_baselines_us=sum_owd_baselines_us[pinger]), 1673 | dl_owd_delta_ewmas_us[${reflectors[pinger]}] < min_dl_owd_delta_ewma_us && (min_dl_owd_delta_ewma_us=dl_owd_delta_ewmas_us[${reflectors[pinger]}]), 1674 | ul_owd_delta_ewmas_us[${reflectors[pinger]}] < min_ul_owd_delta_ewma_us && (min_ul_owd_delta_ewma_us=ul_owd_delta_ewmas_us[${reflectors[pinger]}]) 1675 | 1676 | )) 1677 | done 1678 | 1679 | for ((pinger=0; pinger < no_pingers; pinger++)) 1680 | do 1681 | 1682 | (( 1683 | sum_owd_baselines_delta_us = sum_owd_baselines_us[pinger] - min_sum_owd_baselines_us, 1684 | dl_owd_delta_ewma_delta_us = dl_owd_delta_ewmas_us[${reflectors[pinger]}] - min_dl_owd_delta_ewma_us, 1685 | ul_owd_delta_ewma_delta_us = ul_owd_delta_ewmas_us[${reflectors[pinger]}] - min_ul_owd_delta_ewma_us 1686 | )) 1687 | 1688 | if ((output_reflector_stats)) 1689 | then 1690 | printf -v reflector_stats '%s; %s; %s; %s; %s; %s; %s; %s; %s; %s; %s; %s; %s; %s' "${EPOCHREALTIME}" "${reflectors[pinger]}" "${min_sum_owd_baselines_us}" "${sum_owd_baselines_us[pinger]}" "${sum_owd_baselines_delta_us}" "${reflector_sum_owd_baselines_delta_thr_us}" "${min_dl_owd_delta_ewma_us}" "${dl_owd_delta_ewmas_us[${reflectors[pinger]}]}" "${dl_owd_delta_ewma_delta_us}" "${reflector_owd_delta_ewma_delta_thr_us}" "${min_ul_owd_delta_ewma_us}" "${ul_owd_delta_ewmas_us[${reflectors[pinger]}]}" "${ul_owd_delta_ewma_delta_us}" "${reflector_owd_delta_ewma_delta_thr_us}" 1691 | log_msg "REFLECTOR" "${reflector_stats}" 1692 | fi 1693 | 1694 | if (( sum_owd_baselines_delta_us > reflector_sum_owd_baselines_delta_thr_us )) 1695 | then 1696 | log_msg "DEBUG" "Warning: reflector: ${reflectors[pinger]} sum_owd_baselines_us exceeds the minimum by set threshold." 1697 | replace_pinger_reflector "${pinger}" 1698 | continue 2 1699 | fi 1700 | 1701 | if (( dl_owd_delta_ewma_delta_us > reflector_owd_delta_ewma_delta_thr_us )) 1702 | then 1703 | log_msg "DEBUG" "Warning: reflector: ${reflectors[pinger]} dl_owd_delta_ewma_us exceeds the minimum by set threshold." 1704 | replace_pinger_reflector "${pinger}" 1705 | continue 2 1706 | fi 1707 | 1708 | if (( ul_owd_delta_ewma_delta_us > reflector_owd_delta_ewma_delta_thr_us )) 1709 | then 1710 | log_msg "DEBUG" "Warning: reflector: ${reflectors[pinger]} ul_owd_delta_ewma_us exceeds the minimum by set threshold." 1711 | replace_pinger_reflector "${pinger}" 1712 | continue 2 1713 | fi 1714 | done 1715 | 1716 | fi 1717 | 1718 | replace_pinger_reflector_enabled=1 1719 | 1720 | for ((pinger=0; pinger < no_pingers; pinger++)) 1721 | do 1722 | # shellcheck disable=SC2178 1723 | declare -n reflector_offences="reflector_${pinger}_offences" 1724 | 1725 | (( 1726 | reflector_offences[reflector_offences_idx] && (sum_reflector_offences[pinger]--), 1727 | reflector_offences[reflector_offences_idx] = (t_start_us-last_timestamp_reflectors_us[${reflectors[pinger]}]) > reflector_response_deadline_us ? 1 : 0 1728 | )) 1729 | 1730 | if (( reflector_offences[reflector_offences_idx] )) 1731 | then 1732 | ((sum_reflector_offences[pinger]++)) 1733 | log_msg "DEBUG" "no ping response from reflector: ${reflectors[pinger]} within reflector_response_deadline: ${reflector_response_deadline_s}s" 1734 | log_msg "DEBUG" "reflector=${reflectors[pinger]}, sum_reflector_offences=${sum_reflector_offences[pinger]} and reflector_misbehaving_detection_thr=${reflector_misbehaving_detection_thr}" 1735 | fi 1736 | 1737 | if (( sum_reflector_offences[pinger] >= reflector_misbehaving_detection_thr )) 1738 | then 1739 | 1740 | log_msg "DEBUG" "Warning: reflector: ${reflectors[pinger]} seems to be misbehaving." 1741 | if ((replace_pinger_reflector_enabled)) 1742 | then 1743 | replace_pinger_reflector "${pinger}" 1744 | replace_pinger_reflector_enabled=0 1745 | else 1746 | log_msg "DEBUG" "Warning: skipping replacement of reflector: ${reflectors[pinger]} given prior replacement within this reflector health check cycle." 1747 | fi 1748 | fi 1749 | done 1750 | (( 1751 | reflector_offences_idx=(reflector_offences_idx+1)%reflector_misbehaving_detection_window, 1752 | t_last_reflector_health_check_us=t_start_us 1753 | )) 1754 | fi 1755 | ;; 1756 | IDLE) 1757 | if (( achieved_rate_kbps[dl] > connection_active_thr_kbps || achieved_rate_kbps[ul] > connection_active_thr_kbps )) 1758 | then 1759 | log_msg "DEBUG" "dl achieved rate: ${achieved_rate_kbps[dl]} kbps or ul achieved rate: ${achieved_rate_kbps[ul]} kbps exceeded connection active threshold: ${connection_active_thr_kbps} kbps. Resuming normal operation." 1760 | change_state_main "RUNNING" 1761 | start_pingers 1762 | t_sustained_connection_idle_us=0 1763 | # Give some time to enable pingers to get set up 1764 | (( 1765 | reflectors_last_timestamp_us = t_start_us + 2*reflector_ping_interval_us, 1766 | t_last_reflector_health_check_us=reflectors_last_timestamp_us 1767 | )) 1768 | fi 1769 | ;; 1770 | STALL) 1771 | ((reflector_response)) && reflectors_last_timestamp_us=${t_start_us} 1772 | 1773 | if (( reflector_response || achieved_rate_kbps[dl] > connection_stall_thr_kbps && achieved_rate_kbps[ul] > connection_stall_thr_kbps )) 1774 | then 1775 | if ((reflector_response)) 1776 | then 1777 | log_msg "DEBUG" "Reflector response detected." 1778 | else 1779 | log_msg "DEBUG" "dl achieved rate: ${achieved_rate_kbps[dl]} kbps and ul achieved rate: ${achieved_rate_kbps[ul]} kbps exceeded connection stall threshold: ${connection_stall_thr_kbps} kbps." 1780 | fi 1781 | log_msg "DEBUG" "Connection stall ended. Resuming normal operation." 1782 | change_state_main "RUNNING" 1783 | fi 1784 | 1785 | if (( global_ping_response_timeout==0 && t_start_us > (t_connection_stall_time_us + global_ping_response_timeout_us - stall_detection_timeout_us) )) 1786 | then 1787 | global_ping_response_timeout=1 1788 | ((min_shaper_rates_enforcement)) && set_min_shaper_rates 1789 | log_msg "SYSLOG" "Warning: Configured global ping response timeout: ${global_ping_response_timeout_s} seconds exceeded." 1790 | log_msg "DEBUG" "Restarting pingers." 1791 | stop_pingers 1792 | start_pingers 1793 | fi 1794 | ;; 1795 | *) 1796 | log_msg "ERROR" "Unrecognized main state: ${main_state}. Exiting now." 1797 | exit 1 1798 | ;; 1799 | esac 1800 | done 1801 | -------------------------------------------------------------------------------- /cake-autorate.template: -------------------------------------------------------------------------------- 1 | #!/bin/sh /etc/rc.common 2 | 3 | START=97 4 | STOP=4 5 | USE_PROCD=1 6 | 7 | start_service() { 8 | procd_open_instance 9 | procd_set_param command %%SCRIPT_PREFIX%%/launcher.sh 10 | procd_set_param respawn 11 | procd_close_instance 12 | } 13 | -------------------------------------------------------------------------------- /config.primary.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # *** INSTANCE-SPECIFIC CONFIGURATION OPTIONS *** 4 | # 5 | # cake-autorate will run one instance per config file present in the directory where 6 | # cake-autorate is found (typically /root/cake-autorate). The config files must be in 7 | # the directory in the form: config.instance.sh. Thus multiple instances of cake-autorate 8 | # can be established by setting up appropriate config files like config.primary.sh and 9 | # config.secondary.sh for the respective first and second instances of cake-autorate. 10 | 11 | ### For multihomed setups, it is the responsibility of the user to ensure that the probes 12 | ### sent by this instance of cake-autorate actually travel through these interfaces. 13 | ### See ping_extra_args and ping_prefix_string 14 | 15 | dl_if=ifb-wan # download interface 16 | ul_if=wan # upload interface 17 | 18 | # Set either of the below to 0 to adjust one direction only 19 | # or alternatively set both to 0 to simply use cake-autorate to monitor a connection 20 | adjust_dl_shaper_rate=1 # enable (1) or disable (0) actually changing the dl shaper rate 21 | adjust_ul_shaper_rate=1 # enable (1) or disable (0) actually changing the ul shaper rate 22 | 23 | min_dl_shaper_rate_kbps=5000 # minimum bandwidth for download (Kbit/s) 24 | base_dl_shaper_rate_kbps=20000 # steady state bandwidth for download (Kbit/s) 25 | max_dl_shaper_rate_kbps=80000 # maximum bandwidth for download (Kbit/s) 26 | 27 | min_ul_shaper_rate_kbps=5000 # minimum bandwidth for upload (Kbit/s) 28 | base_ul_shaper_rate_kbps=20000 # steady state bandwidth for upload (KBit/s) 29 | max_ul_shaper_rate_kbps=35000 # maximum bandwidth for upload (Kbit/s) 30 | 31 | connection_active_thr_kbps=2000 # threshold in Kbit/s below which dl/ul is considered idle 32 | 33 | # Logging toggles for various stats 34 | output_processing_stats=0 # enable (1) or disable (0) output monitoring lines showing processing stats 35 | output_load_stats=0 # enable (1) or disable (0) output monitoring lines showing achieved loads 36 | output_reflector_stats=0 # enable (1) or disable (0) output monitoring lines showing reflector stats 37 | output_summary_stats=0 # enable (1) or disable (0) output monitoring lines showing summary stats 38 | output_cpu_stats=0 # enable (1) or disable (0) output monitoring lines showing CPU usage percentages 39 | output_cpu_raw_stats=0 # enable (1) or disable (0) output monitoring lines showing raw CPU usage lines 40 | 41 | # *** OVERRIDES *** 42 | 43 | ### See defaults.sh for additional configuration options 44 | ### that can be set in this configuration file to override the defaults. 45 | ### Place any such overrides below this line. 46 | -------------------------------------------------------------------------------- /defaults.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # defaults.sh -- default configuration values for cake-autorate.sh 4 | # 5 | # This file is part of cake-autorate. 6 | # 7 | # CAKE-AUTORATE IS HIGHLY CONFIGURABLE AND THIS FILE MAY BE 8 | # CONSULTED IN RESPECT OF OVERRIDING VARIABLES IN A CONFIG FILE. 9 | # 10 | # DO NOT MODIFY THIS FILE. ANY CHANGES NEED TO BE MADE TO 11 | # THE CONFIG FILE FOR A GIVEN INSTANCE OF CAKE-AUTORATE. 12 | # MODIFYING THIS FILE WILL RESULT IN THE LOSS OF ANY CHANGES 13 | # DURING AN UPDATE OR UNEXPECTED BEHAVIOR AFTER AN UPDATE 14 | # IF THE OLD DEFAULT FILE WAS IN USE. 15 | 16 | # *** OUTPUT AND LOGGING OPTIONS *** 17 | 18 | output_processing_stats=0 # enable (1) or disable (0) output monitoring lines showing processing stats 19 | output_load_stats=0 # enable (1) or disable (0) output monitoring lines showing achieved loads 20 | output_reflector_stats=0 # enable (1) or disable (0) output monitoring lines showing reflector stats 21 | output_summary_stats=0 # enable (1) or disable (0) output monitoring lines showing summary stats 22 | output_cake_changes=0 # enable (1) or disable (0) output monitoring lines showing cake bandwidth changes 23 | output_cpu_stats=0 # enable (1) or disable (0) output monitoring lines showing CPU usage percentages 24 | output_cpu_raw_stats=0 # enable (1) or disable (0) output monitoring lines showing raw CPU usage lines 25 | debug=1 # enable (1) or disable (0) out of debug lines 26 | 27 | # This can generate a LOT of records so be careful: 28 | log_DEBUG_messages_to_syslog=0 # enable (1) or disable (0) logging of all DEBUG records into the system log. 29 | 30 | # ** Take care with these settings to ensure you won't run into OOM issues on your router *** 31 | # every write the cumulative write time and bytes associated with each log line are checked 32 | # and if either exceeds the configured values below, the log file is rotated 33 | log_to_file=1 # enable (1) or disable (0) output logging to file (/tmp/cake-autorate.log) 34 | log_file_max_time_mins=10 # maximum time between log file rotations 35 | log_file_max_size_KB=2000 # maximum KB (i.e. bytes/1024) worth of log lines between log file rotations 36 | 37 | # log file path defaults to /var/log/ 38 | # or, if set below, then ${log_file_path_override} 39 | log_file_path_override="" 40 | 41 | # *** STANDARD CONFIGURATION OPTIONS *** 42 | 43 | ### For multihomed setups, it is the responsibility of the user to ensure that the probes 44 | ### sent by this instance of cake-autorate actually travel through these interfaces. 45 | ### See ping_extra_args and ping_prefix_string 46 | 47 | dl_if=ifb-wan # download interface 48 | ul_if=wan # upload interface 49 | 50 | # pinger binary selection can be any of: 51 | # fping - round robin pinging (rtts) 52 | # tsping - round robin pinging using ICMP type 13 (owds) 53 | # ping - (iputils-ping) individual pinging (rtts) 54 | pinger_binary=fping 55 | 56 | # list of reflectors to use and number of pingers to initiate 57 | # pingers will be initiated with reflectors in the order specified in the list 58 | # additional reflectors will be used to replace any reflectors that go stale 59 | # so e.g. if 6 reflectors are specified and the number of pingers is set to 4, the first 4 reflectors will be used initially 60 | # and the remaining 2 reflectors in the list will be used in the event any of the first 4 go bad 61 | # a bad reflector will go to the back of the queue on reflector rotation 62 | reflectors=( 63 | "1.1.1.1" "1.0.0.1" # Cloudflare 64 | "8.8.8.8" "8.8.4.4" # Google 65 | "9.9.9.9" "9.9.9.10" "9.9.9.11" # Quad9 66 | "94.140.14.15" "94.140.14.140" "94.140.14.141" "94.140.15.15" "94.140.15.16" # AdGuard 67 | "64.6.65.6" "156.154.70.1" "156.154.70.2" "156.154.70.3" "156.154.70.4" "156.154.70.5" "156.154.71.1" "156.154.71.2" "156.154.71.3" "156.154.71.4" "156.154.71.5" # Neustar 68 | "208.67.220.2" "208.67.220.123" "208.67.220.220" "208.67.222.2" "208.67.222.123" # OpenDNS 69 | "185.228.168.9" "185.228.168.10" # CleanBrowsing 70 | ) 71 | 72 | randomize_reflectors=1 # enable (1) or disable (0) randomization of reflectors on startup 73 | 74 | # Think carefully about the following settings 75 | # to avoid excessive CPU use (proportional with ping interval / number of pingers) 76 | # and to avoid abusive network activity (excessive ICMP frequency to one reflector) 77 | # The author has found an ICMP rate of 1/(0.2/4) = 20 Hz to give satisfactory performance on 4G 78 | no_pingers=6 # number of pingers to maintain 79 | reflector_ping_interval_s=0.3 # (seconds, e.g. 0.2s or 2s) 80 | 81 | # average owd delta threshold in ms up to which the maximum adjust_up_load_high is applied to the shaper rate adjustment 82 | # for average owd deltas between avg_owd_delta_max_adjust_up_thr_ms and owd_delta_thr_ms, the adjustment is scaled linearly 83 | # from max_adjust_up_load_high (at avg_owd_delta_max_adjust_up_thr_ms) to min_adjust_up_load_high (at owd_delta_thr_ms) 84 | dl_avg_owd_delta_max_adjust_up_thr_ms=10.0 # (milliseconds) 85 | ul_avg_owd_delta_max_adjust_up_thr_ms=10.0 # (milliseconds) 86 | 87 | # owd delta threshold in ms is the extent of OWD increase to classify as a delay 88 | # these are automatically adjusted based on maximum on the wire packet size 89 | # (adjustment significant at sub 12Mbit/s rates, else negligible) 90 | dl_owd_delta_delay_thr_ms=30.0 # (milliseconds) 91 | ul_owd_delta_delay_thr_ms=30.0 # (milliseconds) 92 | 93 | # average owd delta threshold in ms beyond which the maximum adjust_down_bufferbloat is applied to the shaper rate adjustment 94 | # for average owd deltas between owd_delta_thr_ms and avg_owd_delta_max_adjust_up_thr_ms, the adjustment is scaled linearly 95 | # from min_adjust_down_bufferbloat (at owd_delta_thr_ms) to min_adjust_up_load_high (at avg_owd_delta_max_adjust_down_thr_ms) 96 | dl_avg_owd_delta_max_adjust_down_thr_ms=60.0 # (milliseconds) 97 | ul_avg_owd_delta_max_adjust_down_thr_ms=60.0 # (milliseconds) 98 | 99 | # Set either of the below to 0 to adjust one direction only 100 | # or alternatively set both to 0 to simply use cake-autorate to monitor a connection 101 | adjust_dl_shaper_rate=1 # enable (1) or disable (0) actually changing the dl shaper rate 102 | adjust_ul_shaper_rate=1 # enable (1) or disable (0) actually changing the ul shaper rate 103 | 104 | min_dl_shaper_rate_kbps=5000 # minimum bandwidth for download (Kbit/s) 105 | base_dl_shaper_rate_kbps=20000 # steady state bandwidth for download (Kbit/s) 106 | max_dl_shaper_rate_kbps=80000 # maximum bandwidth for download (Kbit/s) 107 | 108 | min_ul_shaper_rate_kbps=5000 # minimum bandwidth for upload (Kbit/s) 109 | base_ul_shaper_rate_kbps=20000 # steady state bandwidth for upload (KBit/s) 110 | max_ul_shaper_rate_kbps=35000 # maximum bandwidth for upload (Kbit/s) 111 | 112 | # sleep functionality saves unecessary pings and CPU cycles by 113 | # pausing all active pingers when connection is not in active use 114 | enable_sleep_function=1 # enable (1) or disable (0) sleep functonality 115 | connection_active_thr_kbps=2000 # threshold in Kbit/s below which dl/ul is considered idle 116 | sustained_idle_sleep_thr_s=60.0 # time threshold to put pingers to sleep on sustained dl/ul achieved rate < idle_thr (seconds) 117 | 118 | min_shaper_rates_enforcement=0 # enable (1) or disable (0) dropping down to minimum shaper rates on connection idle or stall 119 | 120 | startup_wait_s=0.0 # number of seconds to wait on startup (e.g. to wait for things to settle on router reboot) 121 | 122 | # *** ADVANCED CONFIGURATION OPTIONS *** 123 | 124 | log_file_buffer_size_B=512 # log file buffer size in bytes 125 | log_file_buffer_timeout_ms=500 # log file buffer timeout in milliseconds 126 | 127 | log_file_export_compress=1 # compress log file exports using gzip and append .gz to export filename 128 | 129 | ### In multi-homed setups, it is mandatory to use either ping_extra_args 130 | ### or ping_prefix_string to direct the pings through $dl_if and $ul_if. 131 | ### No universal recommendation exists, because there are multiple 132 | ### policy-routing packages available (e.g. vpn-policy-routing and mwan3). 133 | ### Typically they either react to a firewall mark set on the pings, or 134 | ### provide a convenient wrapper. 135 | ### 136 | ### In a traditional single-homed setup, there is usually no need for these parameters. 137 | ### 138 | ### These arguments can also be used for any other purpose - e.g. for setting a 139 | ### particular QoS mark. 140 | 141 | # extra arguments for ping or fping 142 | # e.g., here is how you can set the correct outgoing interface and 143 | # the firewall mark for ping: 144 | # ping_extra_args="-I wwan0 -m $((0x300))" 145 | # Unfortunately, fping does not offer a command line switch to set 146 | # the firewall mark. 147 | # WARNING: no error checking so use at own risk! 148 | ping_extra_args="" 149 | 150 | # a wrapper for ping binary - used as a prefix for the real command 151 | # e.g., when using mwan3, it is recommended to set it like this: 152 | # ping_prefix_string="mwan3 use gpon exec" 153 | # WARNING: the wrapper must exec ping as the final step, not run it as a subprocess. 154 | # Running ping or fping as a subprocess will lead to problems stopping it. 155 | # WARNING: no error checking - so use at own risk! 156 | ping_prefix_string="" 157 | 158 | # interval in ms for monitoring achieved rx/tx rates 159 | # this is automatically adjusted based on maximum on the wire packet size 160 | # (adjustment significant at sub 12Mbit/s rates, else negligible) 161 | monitor_achieved_rates_interval_ms=200 # (milliseconds) 162 | 163 | # interval in ms for monitoring CPU usage 164 | monitor_cpu_usage_interval_ms=2000 165 | 166 | # bufferbloat is detected when (bufferbloat_detection_thr) samples 167 | # out of the last (bufferbloat detection window) samples are delayed 168 | bufferbloat_detection_window=6 # number of samples to retain in detection window 169 | bufferbloat_detection_thr=3 # number of delayed samples for bufferbloat detection 170 | 171 | # OWD baseline against which to measure delays 172 | # the idea is that the baseline is allowed to increase slowly to allow for path changes 173 | # and slowly enough such that bufferbloat will be corrected well before the baseline increases, 174 | # but it will decrease very rapidly to ensure delays are measured against the shortest path 175 | alpha_baseline_increase=0.001 # how rapidly baseline RTT is allowed to increase 176 | alpha_baseline_decrease=0.9 # how rapidly baseline RTT is allowed to decrease 177 | 178 | # OWD delta from baseline is tracked using ewma with alpha set below 179 | alpha_delta_ewma=0.095 180 | 181 | # rate adjustment parameters 182 | # shaper rate is adjusted by a maximum of shaper_rate_max_adjust_down_bufferbloat on detection of bufferbloat 183 | # and this is scaled by the average delta owd / average owd delta threshold 184 | # otherwise shaper rate is adjusted up on load high, and down on load idle or low 185 | shaper_rate_min_adjust_down_bufferbloat=0.99 # how rapidly to reduce shaper rate upon detection of bufferbloat (min reduction) 186 | shaper_rate_max_adjust_down_bufferbloat=0.75 # how rapidly to reduce shaper rate upon detection of bufferbloat (max reduction) 187 | shaper_rate_min_adjust_up_load_high=1.0 # how rapidly to increase shaper rate upon high load detected (min increase) 188 | shaper_rate_max_adjust_up_load_high=1.04 # how rapidly to increase shaper rate upon high load detected (max increase) 189 | shaper_rate_adjust_down_load_low=0.99 # how rapidly to return down to base shaper rate upon idle or low load detected 190 | shaper_rate_adjust_up_load_low=1.01 # how rapidly to return up to base shaper rate upon idle or low load detected 191 | 192 | # the load is categoried as low if < high_load_thr and high if > high_load_thr relative to the current shaper rate 193 | high_load_thr=0.75 # % of currently set bandwidth for detecting high load 194 | 195 | # refractory periods between successive bufferbloat/decay rate changes 196 | # the bufferbloat refractory period should be greater than the 197 | # average time it would take to replace the bufferbloat 198 | # detection window with new samples upon a bufferbloat event 199 | bufferbloat_refractory_period_ms=300 # (milliseconds) 200 | decay_refractory_period_ms=1000 # (milliseconds) 201 | 202 | # interval for checking reflector health 203 | reflector_health_check_interval_s=1.0 # (seconds) 204 | # deadline for reflector response not to be classified as an offence against reflector 205 | reflector_response_deadline_s=1.0 # (seconds) 206 | 207 | # reflector misbehaving is detected when $reflector_misbehaving_detection_thr samples 208 | # out of the last (reflector misbehaving detection window) samples are offences 209 | # thus with a 1s interval, window 60 and detection_thr 3, this is tantamount to 210 | # 3 offences within the last 60s 211 | reflector_misbehaving_detection_window=60 212 | reflector_misbehaving_detection_thr=3 213 | 214 | reflector_replacement_interval_mins=60 # how often to replace a random reflector from the present list 215 | 216 | reflector_comparison_interval_mins=1 # how often to compare reflectors 217 | reflector_sum_owd_baselines_delta_thr_ms=20.0 # max increase from min sum owd baselines before reflector rotated 218 | reflector_owd_delta_ewma_delta_thr_ms=10.0 # max increase from min delta ewma before reflector rotated 219 | 220 | # stall is detected when the following two conditions are met: 221 | # 1) no reflector responses within $stall_detection_thr*$ping_response_interval_us; and 222 | # 2) either $rx_achieved_rate or $tx_achieved_rate < $connection_stall_thr 223 | stall_detection_thr=5 224 | connection_stall_thr_kbps=10 225 | 226 | global_ping_response_timeout_s=10.0 # timeout to set shaper rates to min on no ping response whatsoever (seconds) 227 | 228 | if_up_check_interval_s=10.0 # time to wait before re-checking if rx/tx bytes files exist (e.g. from boot state or sleep recovery) 229 | -------------------------------------------------------------------------------- /example-uci-config.txt: -------------------------------------------------------------------------------- 1 | config cake_autorate 'primary' 2 | option enabled '0' 3 | option dl_if 'ifb-wan' 4 | option ul_if 'wan' 5 | option adjust_dl_shaper_rate '1' 6 | option adjust_ul_shaper_rate '1' 7 | option min_dl_shaper_rate_kbps '5000' 8 | option base_dl_shaper_rate_kbps '20000' 9 | option max_dl_shaper_rate_kbps '80000' 10 | option min_ul_shaper_rate_kbps '5000' 11 | option base_ul_shaper_rate_kbps '20000' 12 | option max_ul_shaper_rate_kbps '35000' 13 | 14 | config cake_autorate 'secondary' 15 | option enabled '1' 16 | option dl_if 'ifb-secondary' 17 | option ul_if 'secondary' 18 | option adjust_dl_shaper_rate '1' 19 | option adjust_ul_shaper_rate '0' 20 | option min_dl_shaper_rate_kbps '3000' 21 | option base_dl_shaper_rate_kbps '15000' 22 | option max_dl_shaper_rate_kbps '70000' 23 | option min_ul_shaper_rate_kbps '3000' 24 | option base_ul_shaper_rate_kbps '15000' 25 | option max_ul_shaper_rate_kbps '30000' 26 | -------------------------------------------------------------------------------- /images/bandwidth-compromise.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lynxthecat/cake-autorate/4b65783779bda27e484f1ac6efd463f55e0e61a8/images/bandwidth-compromise.png -------------------------------------------------------------------------------- /images/cake-bandwidth-adaptation.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lynxthecat/cake-autorate/4b65783779bda27e484f1ac6efd463f55e0e61a8/images/cake-bandwidth-adaptation.png -------------------------------------------------------------------------------- /images/cake-bandwidth-autorate-rate-control.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lynxthecat/cake-autorate/4b65783779bda27e484f1ac6efd463f55e0e61a8/images/cake-bandwidth-autorate-rate-control.png -------------------------------------------------------------------------------- /launcher.sh.template: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | cake_instances=() 4 | if command -v uci >/dev/null 2>&1 && uci show cake-autorate >/dev/null 2>&1 5 | then 6 | cake_uci_sections=$(uci show cake-autorate | grep -oE "^cake-autorate\..*\.enabled=" | cut -d. -f2 | cut -d= -f1) 7 | for cake_uci_section in ${cake_uci_sections} 8 | do 9 | enabled=$(uci get cake-autorate.${cake_uci_section}.enabled) 10 | [[ "${enabled}" != "1" ]] && continue # Skip disabled instances 11 | 12 | printf '%s\n' "# This file is automatically generated. Do not edit!" > "%%CONFIG_PREFIX%%/config.${cake_uci_section}.sh.tmp" 13 | printf '%s\n\n' "# You can edit the UCI configuration file /etc/config/cake-autorate." >> "%%CONFIG_PREFIX%%/config.${cake_uci_section}.sh.tmp" 14 | uci show cake-autorate.${cake_uci_section} >> "%%CONFIG_PREFIX%%/config.${cake_uci_section}.sh.tmp" 15 | sed -i "s/^cake-autorate.${cake_uci_section}.//g" "%%CONFIG_PREFIX%%/config.${cake_uci_section}.sh.tmp" 16 | sed -i -E "/^(enabled=|cake_autorate$)/d" "%%CONFIG_PREFIX%%/config.${cake_uci_section}.sh.tmp" 17 | mv "%%CONFIG_PREFIX%%/config.${cake_uci_section}.sh.tmp" "%%CONFIG_PREFIX%%/config.${cake_uci_section}.sh" 18 | cake_instances+=("%%CONFIG_PREFIX%%/config.${cake_uci_section}.sh") 19 | done 20 | else 21 | cake_instances=(%%CONFIG_PREFIX%%/config.*.sh) 22 | fi 23 | cake_instance_pids=() 24 | 25 | trap kill_cake_instances INT TERM EXIT 26 | 27 | kill_cake_instances() 28 | { 29 | trap - INT TERM EXIT 30 | 31 | echo "Killing all instances of cake one-by-one now." 32 | 33 | for ((cake_instance=0; cake_instance<${#cake_instances[@]}; cake_instance++)) 34 | do 35 | kill "${cake_instance_pids[${cake_instance}]}" 2>/dev/null || true 36 | done 37 | wait 38 | } 39 | 40 | for cake_instance in "${cake_instances[@]}" 41 | do 42 | %%SCRIPT_PREFIX%%/cake-autorate.sh "${cake_instance}" & 43 | cake_instance_pids+=(${!}) 44 | done 45 | wait 46 | -------------------------------------------------------------------------------- /lib.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # lib.sh -- common functions for use by cake-autorate.sh 4 | # 5 | # This file is part of cake-autorate. 6 | 7 | __set_e=0 8 | if [[ ! ${-} =~ e ]] 9 | then 10 | set -e 11 | __set_e=1 12 | fi 13 | 14 | if [[ -z ${__sleep_fd:-} ]] 15 | then 16 | exec {__sleep_fd}<> <(:) 17 | fi 18 | 19 | typeof() 20 | { 21 | # typeof -- returns the type of a variable 22 | 23 | local type_sig 24 | type_sig=$(declare -p "${1}" 2>/dev/null) 25 | if [[ "${type_sig}" =~ "declare --" ]] 26 | then 27 | str_type "${1}" 28 | elif [[ "${type_sig}" =~ "declare -a" ]] 29 | then 30 | printf "array" 31 | elif [[ "${type_sig}" =~ "declare -A" ]] 32 | then 33 | printf "map" 34 | else 35 | printf "none" 36 | fi 37 | } 38 | 39 | str_type() 40 | { 41 | # str_type -- returns the type of a string 42 | 43 | local -n str=${1} 44 | 45 | if [[ "${str}" =~ ^[0-9]+$ ]] 46 | then 47 | printf "integer" 48 | elif [[ "${str}" =~ ^[0-9]*\.[0-9]+$ ]] 49 | then 50 | printf "float" 51 | elif [[ "${str}" =~ ^-[0-9]+$ ]] 52 | then 53 | printf "negative-integer" 54 | elif [[ "${str}" =~ ^-[0-9]*\.[0-9]+$ ]] 55 | then 56 | printf "negative-float" 57 | else 58 | # technically not validated, user is just trusted to call 59 | # this function with valid strings 60 | printf "string" 61 | fi 62 | } 63 | 64 | sleep_s() 65 | { 66 | # Calling the external sleep binary could be rather slow, 67 | # especially as it is called very frequently and typically on mediocre hardware. 68 | # 69 | # bash's loadable sleep module is not typically available 70 | # in OpenWRT and most embedded systems, and use of the bash 71 | # read command with a timeout offers performance that is 72 | # at least on a par with bash's sleep module. 73 | # 74 | # For benchmarks, check the following links: 75 | # - https://github.com/lynxthecat/cake-autorate/issues/174#issuecomment-1460057382 76 | # - https://github.com/lynxthecat/cake-autorate/issues/174#issuecomment-1460074498 77 | 78 | # ${1} = sleep_duration_s (seconds, e.g. 0.5, 1 or 1.5) 79 | 80 | read -r -t "${1}" -u "${__sleep_fd}" || : 81 | } 82 | 83 | sleep_us() 84 | { 85 | # ${1} = sleep_duration_us (microseconds) 86 | 87 | printf -v sleep_duration_s %.1f "${1}e-6" 88 | read -r -t "${sleep_duration_s}" -u "${__sleep_fd}" || : 89 | } 90 | 91 | sleep_remaining_tick_time() 92 | { 93 | # sleeps until the end of the tick duration 94 | 95 | # ${1} = t_start_us (microseconds) 96 | # ${2} = tick_duration_us (microseconds) 97 | 98 | # shellcheck disable=SC2154 99 | (( 100 | sleep_duration_us=${1} + ${2} - ${EPOCHREALTIME/.}, 101 | sleep_duration_us < 0 && (sleep_duration_us=0) 102 | )) 103 | 104 | printf -v sleep_duration_s %.1f "${sleep_duration_us}e-6" 105 | read -r -t "${sleep_duration_s}" -u "${__sleep_fd}" || : 106 | } 107 | 108 | randomize_array() 109 | { 110 | # randomize the order of the elements of an array 111 | 112 | local -n array=${1} 113 | 114 | subset=("${array[@]}") 115 | array=() 116 | for ((set=${#subset[@]}; set>0; set--)) 117 | do 118 | idx=$((RANDOM%set)) 119 | array+=("${subset[idx]}") 120 | unset "subset[idx]" 121 | subset=("${subset[@]}") 122 | done 123 | } 124 | 125 | terminate() 126 | { 127 | # Send regular kill to processes and monitor terminations; 128 | # return as soon as all of the active processes terminate; 129 | # if any processes remain active after timeout (defaults to one second), 130 | # then kill with fire using kill -9; 131 | # and, finally, call wait on all processes to reap any zombie processes. 132 | 133 | local pids=${1} timeout_ms=${2:-1000} 134 | 135 | read -r -a pids <<< "${pids}" 136 | 137 | kill "${pids[@]}" 2> /dev/null 138 | 139 | for ((i=0; i /dev/null || unset "pids[${process}]" 144 | done 145 | [[ "${pids[*]}" ]] || return 146 | sleep_s 0.1 147 | done 148 | 149 | kill -9 "${pids[@]}" 2> /dev/null 150 | } 151 | 152 | if (( __set_e == 1 )) 153 | then 154 | set +e 155 | fi 156 | unset __set_e 157 | -------------------------------------------------------------------------------- /maint/README.md: -------------------------------------------------------------------------------- 1 | # maint README 2 | 3 | These scripts are for maintainers of `cake-autorate` to simplify some 4 | housekeeping tasks. If you are a `cake-autorate` user, you should have 5 | no interest in these scripts. 6 | 7 | ## Octave Formatter Guide 8 | 9 | [@moeller0](https://github.com/moeller0) is the primary developer of 10 | the `fn_parse_autorate_log.m` script. However, due to inconsistencies 11 | in the formatting of the script, it is difficult to track changes in 12 | the script using `git`. 13 | 14 | While it would be ideal that the script is written in a consistent 15 | style, we do not want to impose a style on anyone and break anybody's 16 | workflow. Instead, we can use a `textconv` filter to convert the 17 | script to a consistent style when it is displayed in `git diff`. 18 | 19 | The `maint/octave_formatter.py` script is used as the `textconv` 20 | filter. It is invoked by `git` to convert the script to a consistent 21 | style. 22 | 23 | In order to set it up, you need to add the following to your 24 | `.git/config` file: 25 | 26 | ```ini 27 | [diff "octave"] 28 | textconv = python3 maint/octave_formatter.py 29 | ``` 30 | 31 | You also need to add the following to your `.gitattributes` file: 32 | 33 | ```ini 34 | *.m diff=octave 35 | ``` 36 | -------------------------------------------------------------------------------- /maint/mdformat.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | if ! mdformat --help | tail -1 | grep -q ' mdformat_gfm:' 4 | then 5 | cat >&2 <<-EOF 6 | You must install both mdformat and mdformat-gfm. 7 | 8 | Using PIP: 9 | pip install mdformat mdformat-gfm 10 | 11 | Using PIPX: 12 | pipx install mdformat 13 | pipx inject mdformat mdformat-gfm 14 | 15 | EOF 16 | 17 | exit 1 18 | fi 19 | 20 | exec mdformat --wrap=70 . 21 | -------------------------------------------------------------------------------- /maint/octave_formatter.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | """ 4 | Modified from matlab-formatter-vscode to add GNU Octave support and other features 5 | which are not available in the original version. 6 | 7 | Based on https://github.com/affenwiesel/matlab-formatter-vscode commit 43d7224. 8 | 9 | For reference on the differences between GNU Octave and MATLAB, see: 10 | https://en.wikibooks.org/wiki/MATLAB_Programming/Differences_between_Octave_and_MATLAB 11 | 12 | Copyright(C) 2019-2021 Benjamin "Mogli" Mann 13 | Copyright(C) 2022 Linuxbckp 14 | Copyright(C) 2024 Rany 15 | 16 | This program is free software: you can redistribute it and/or modify 17 | it under the terms of the GNU General Public License as published by 18 | the Free Software Foundation, either version 3 of the License, or 19 | (at your option) any later version. 20 | 21 | This program is distributed in the hope that it will be useful, 22 | but WITHOUT ANY WARRANTY; without even the implied warranty of 23 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 24 | GNU General Public License for more details. 25 | 26 | You should have received a copy of the GNU General Public License 27 | along with this program. If not, see . 28 | """ 29 | 30 | import argparse 31 | import re 32 | import sys 33 | 34 | 35 | class Formatter: 36 | # control sequences 37 | ctrl_1line = re.compile( 38 | r"(\s*)(if|while|for|try)(\W\s*\S.*\W)" 39 | r"((end|endif|endwhile|endfor|end_try_catch);?)" 40 | r"(\s+\S.*|\s*$)" 41 | ) 42 | ctrl_1line_dountil = re.compile(r"(^|\s*)(do)(\W\S.*\W)(until)\s*(\W\S.*|\s*$)") 43 | fcnstart = re.compile(r"(\s*)(function|classdef)\s*(\W\s*\S.*|\s*$)") 44 | ctrlstart = re.compile( 45 | r"(\s*)(if|while|for|parfor|try|methods|properties|events|arguments|enumeration|do|spmd)" 46 | r"\s*(\W\s*\S.*|\s*$)" 47 | ) 48 | ctrl_ignore = re.compile(r"(\s*)(import|clear|clearvars)(.*$)") 49 | ctrlstart_2 = re.compile(r"(\s*)(switch)\s*(\W\s*\S.*|\s*$)") 50 | ctrlcont = re.compile(r"(\s*)(elseif|else|case|otherwise|catch)\s*(\W\s*\S.*|\s*$)") 51 | ctrlend = re.compile( 52 | r"(\s*)((end|endfunction|endif|endwhile|endfor|end_try_catch|endswitch|until|endclassdef|" 53 | r"endmethods|endproperties);?)(\s+\S.*|\s*$)" 54 | ) 55 | linecomment = re.compile(r"(\s*)[%#].*$") 56 | ellipsis = re.compile(r".*\.\.\..*$") 57 | blockcomment_open = re.compile(r"(\s*)%\{\s*$") 58 | blockcomment_close = re.compile(r"(\s*)%\}\s*$") 59 | block_close = re.compile(r"\s*[\)\]\}].*$") 60 | ignore_command = re.compile(r".*formatter\s+ignore\s+(\d*).*$") 61 | 62 | # patterns 63 | p_string = re.compile( 64 | r"(.*?[\(\[\{,;=\+\-\*\/\|\&\s]|^)\s*(\'([^\']|\'\')+\')([\)\}\]\+\-\*\/=\|\&,;].*|\s+.*|$)" 65 | ) 66 | p_string_dq = re.compile( 67 | r"(.*?[\(\[\{,;=\+\-\*\/\|\&\s]|^)\s*(\"([^\"])*\")([\)\}\]\+\-\*\/=\|\&,;].*|\s+.*|$)" 68 | ) 69 | p_comment = re.compile(r"(.*\S|^)\s*([%#].*)") 70 | p_blank = re.compile(r"^\s+$") 71 | p_num_sc = re.compile(r"(.*?\W|^)\s*(\d+\.?\d*)([eE][+-]?)(\d+)(.*)") 72 | p_num_R = re.compile(r"(.*?\W|^)\s*(\d+)\s*(\/)\s*(\d+)(.*)") 73 | p_incr = re.compile(r"(.*?\S|^)\s*(\+|\-)\s*(\+|\-)\s*([\)\]\},;].*|$)") 74 | p_sign = re.compile(r"(.*?[\(\[\{,;:=\*/\s]|^)\s*(\+|\-)(\w.*)") 75 | p_colon = re.compile(r"(.*?\S|^)\s*(:)\s*(\S.*|$)") 76 | p_ellipsis = re.compile(r"(.*?\S|^)\s*(\.\.\.)\s*(\S.*|$)") 77 | p_op_dot = re.compile(r"(.*?\S|^)\s*(\.)\s*(\+|\-|\*|/|\^)\s*(=)\s*(\S.*|$)") 78 | p_pow_dot = re.compile(r"(.*?\S|^)\s*(\.)\s*(\^)\s*(\S.*|$)") 79 | p_pow = re.compile(r"(.*?\S|^)\s*(\^)\s*(\S.*|$)") 80 | p_op_comb = re.compile( 81 | r"(.*?\S|^)\s*(\.|\+|\-|\*|\\|/|=|<|>|\||\&|!|~|\^)\s*(<|>|=|\+|\-|\*|/|\&|\|)\s*(\S.*|$)" 82 | ) 83 | p_not = re.compile(r"(.*?\S|^)\s*(!|~)\s*(\S.*|$)") 84 | p_op = re.compile(r"(.*?\S|^)\s*(\+|\-|\*|\\|/|=|!|~|<|>|\||\&)\s*(\S.*|$)") 85 | p_func = re.compile(r"(.*?\w)(\()\s*(\S.*|$)") 86 | p_open = re.compile(r"(.*?)(\(|\[|\{)\s*(\S.*|$)") 87 | p_close = re.compile(r"(.*?\S|^)\s*(\)|\]|\})(.*|$)") 88 | p_comma = re.compile(r"(.*?\S|^)\s*(,|;)\s*(\S.*|$)") 89 | p_multiws = re.compile(r"(.*?\S|^)(\s{2,})(\S.*|$)") 90 | 91 | def cell_indent(self, line, cell_open: str, cell_close: str, indent): 92 | # clean line from strings and comments 93 | pattern = re.compile(rf"(\s*)((\S.*)?)(\{cell_open}.*$)") 94 | line = self.clean_line_from_strings_and_comments(line) 95 | opened = line.count(cell_open) - line.count(cell_close) 96 | if opened > 0: 97 | m = pattern.match(line) 98 | n = len(m.group(2)) 99 | indent = (n + 1) if self.matrix_indent else self.iwidth 100 | elif opened < 0: 101 | indent = 0 102 | return (opened, indent) 103 | 104 | def multilinematrix(self, line): 105 | tmp, self.matrix = self.cell_indent(line, "[", "]", self.matrix) 106 | return tmp 107 | 108 | def cellarray(self, line): 109 | tmp, self.cell = self.cell_indent(line, "{", "}", self.cell) 110 | return tmp 111 | 112 | # indentation 113 | ilvl = 0 114 | istep = [] 115 | fstep = [] 116 | iwidth = 0 117 | matrix = 0 118 | cell = 0 119 | isblockcomment = 0 120 | islinecomment = 0 121 | longline = 0 122 | continueline = 0 123 | separate_blocks = False 124 | ignore_lines = 0 125 | 126 | def __init__( 127 | self, 128 | *, 129 | indent_width, 130 | separate_blocks, 131 | indent_mode, 132 | operator_sep, 133 | matrix_indent, 134 | ): 135 | self.iwidth = indent_width 136 | self.separate_blocks = separate_blocks 137 | self.indent_mode = indent_mode 138 | self.operator_sep = operator_sep 139 | self.matrix_indent = matrix_indent 140 | 141 | def clean_line_from_strings_and_comments(self, line): 142 | split = self.extract_string_comment(line) 143 | if split: 144 | return ( 145 | f"{self.clean_line_from_strings_and_comments(split[0])}" 146 | " " 147 | f"{self.clean_line_from_strings_and_comments(split[2])}" 148 | ) 149 | return line 150 | 151 | # divide string into three parts by extracting and formatting certain 152 | # expressions 153 | 154 | def extract_string_comment(self, part): 155 | # comment 156 | m = self.p_comment.match(part) 157 | if m: 158 | part = f"{m.group(1)} {m.group(2)}" 159 | 160 | # string 161 | m = self.p_string.match(part) 162 | m2 = self.p_string_dq.match(part) 163 | # choose longer string to avoid extracting subexpressions 164 | if m2 and (not m or len(m.group(2)) < len(m2.group(2))): 165 | m = m2 166 | if m: 167 | return (m.group(1), m.group(2), m.group(4)) 168 | 169 | return 0 170 | 171 | def extract(self, part): 172 | # whitespace only 173 | m = self.p_blank.match(part) 174 | if m: 175 | return ("", " ", "") 176 | 177 | # string, comment 178 | string_or_comment = self.extract_string_comment(part) 179 | if string_or_comment: 180 | return string_or_comment 181 | 182 | # decimal number (e.g. 5.6E-3) 183 | m = self.p_num_sc.match(part) 184 | if m: 185 | return ( 186 | f"{m.group(1)}{m.group(2)}", 187 | m.group(3), 188 | f"{m.group(4)}{m.group(5)}", 189 | ) 190 | 191 | # rational number (e.g. 1/4) 192 | m = self.p_num_R.match(part) 193 | if m: 194 | return ( 195 | f"{m.group(1)}{m.group(2)}", 196 | m.group(3), 197 | f"{m.group(4)}{m.group(5)}", 198 | ) 199 | 200 | # incrementor (++ or --) 201 | m = self.p_incr.match(part) 202 | if m: 203 | return (m.group(1), f"{m.group(2)}{m.group(3)}", m.group(4)) 204 | 205 | # signum (unary - or +) 206 | m = self.p_sign.match(part) 207 | if m: 208 | return (m.group(1), m.group(2), m.group(3)) 209 | 210 | # colon 211 | m = self.p_colon.match(part) 212 | if m: 213 | return (m.group(1), m.group(2), m.group(3)) 214 | 215 | # dot-operator-assignment (e.g. .+=) 216 | m = self.p_op_dot.match(part) 217 | if m: 218 | sep = " " if self.operator_sep > 0 else "" 219 | return ( 220 | f"{m.group(1)}{sep}", 221 | f"{m.group(2)}{m.group(3)}{m.group(4)}", 222 | f"{sep}{m.group(5)}", 223 | ) 224 | 225 | # .power (.^) 226 | m = self.p_pow_dot.match(part) 227 | if m: 228 | sep = " " if self.operator_sep > 0.5 else "" 229 | return ( 230 | f"{m.group(1)}{sep}", 231 | f"{m.group(2)}{m.group(3)}", 232 | f"{sep}{m.group(4)}", 233 | ) 234 | 235 | # power (^) 236 | m = self.p_pow.match(part) 237 | if m: 238 | sep = " " if self.operator_sep > 0.5 else "" 239 | return (f"{m.group(1)}{sep}", m.group(2), f"{sep}{m.group(3)}") 240 | 241 | # combined operator (e.g. +=, .+, etc.) 242 | m = self.p_op_comb.match(part) 243 | if m: 244 | sep = " " if self.operator_sep > 0 else "" 245 | return ( 246 | f"{m.group(1)}{sep}", 247 | f"{m.group(2)}{m.group(3)}", 248 | f"{sep}{m.group(4)}", 249 | ) 250 | 251 | # not (~ or !) 252 | m = self.p_not.match(part) 253 | if m: 254 | return (f"{m.group(1)} ", m.group(2), m.group(3)) 255 | 256 | # single operator (e.g. +, -, etc.) 257 | m = self.p_op.match(part) 258 | if m: 259 | sep = " " if self.operator_sep > 0 else "" 260 | return (f"{m.group(1)}{sep}", m.group(2), f"{sep}{m.group(3)}") 261 | 262 | # function call 263 | m = self.p_func.match(part) 264 | if m: 265 | return (m.group(1), m.group(2), m.group(3)) 266 | 267 | # parenthesis open 268 | m = self.p_open.match(part) 269 | if m: 270 | return (m.group(1), m.group(2), m.group(3)) 271 | 272 | # parenthesis close 273 | m = self.p_close.match(part) 274 | if m: 275 | return (m.group(1), m.group(2), m.group(3)) 276 | 277 | # comma/semicolon 278 | m = self.p_comma.match(part) 279 | if m: 280 | return (m.group(1), m.group(2), f" {m.group(3)}") 281 | 282 | # ellipsis 283 | m = self.p_ellipsis.match(part) 284 | if m: 285 | return (f"{m.group(1)} ", m.group(2), f" {m.group(3)}") 286 | 287 | # multiple whitespace 288 | m = self.p_multiws.match(part) 289 | if m: 290 | return (m.group(1), " ", m.group(3)) 291 | 292 | return 0 293 | 294 | # recursively format string 295 | def format(self, part): 296 | m = self.extract(part) 297 | if m: 298 | return f"{self.format(m[0])}{m[1]}{self.format(m[2])}" 299 | return part 300 | 301 | # compute indentation 302 | def indent(self, add_space=0): 303 | return ((self.ilvl + self.continueline) * self.iwidth + add_space) * " " 304 | 305 | # take care of indentation and call format(line) 306 | def format_line(self, line): 307 | 308 | if self.ignore_lines > 0: 309 | self.ignore_lines -= 1 310 | return (0, f"{self.indent()}{line.strip()}") 311 | 312 | # determine if linecomment 313 | if re.match(self.linecomment, line): 314 | self.islinecomment = 2 315 | else: 316 | # we also need to track whether the previous line was a commment 317 | self.islinecomment = max(0, self.islinecomment - 1) 318 | 319 | # determine if blockcomment 320 | if re.match(self.blockcomment_open, line): 321 | self.isblockcomment = float("inf") 322 | elif re.match(self.blockcomment_close, line): 323 | self.isblockcomment = 1 324 | else: 325 | self.isblockcomment = max(0, self.isblockcomment - 1) 326 | 327 | # find ellipsis 328 | stripped_line = self.clean_line_from_strings_and_comments(line) 329 | ellipsis_in_comment = self.islinecomment == 2 or self.isblockcomment 330 | if re.match(self.block_close, stripped_line) or ellipsis_in_comment: 331 | self.continueline = 0 332 | else: 333 | self.continueline = self.longline 334 | if re.match(self.ellipsis, stripped_line) and not ellipsis_in_comment: 335 | self.longline = 1 336 | else: 337 | self.longline = 0 338 | 339 | # find comments 340 | if self.isblockcomment: 341 | return (0, line.rstrip()) # don't modify indentation in block comments 342 | if self.islinecomment == 2: 343 | # check for ignore statement 344 | m = re.match(self.ignore_command, line) 345 | if m: 346 | if m.group(1) and int(m.group(1)) > 1: 347 | self.ignore_lines = int(m.group(1)) 348 | else: 349 | self.ignore_lines = 1 350 | return (0, f"{self.indent()}{line.strip()}") 351 | 352 | # find imports, clear, etc. 353 | m = re.match(self.ctrl_ignore, line) 354 | if m: 355 | return (0, f"{self.indent()}{line.strip()}") 356 | 357 | # find matrices 358 | tmp = self.matrix 359 | if self.multilinematrix(line) or tmp: 360 | return (0, f"{self.indent(tmp)}{self.format(line).strip()}") 361 | 362 | # find cell arrays 363 | tmp = self.cell 364 | if self.cellarray(line) or tmp: 365 | return (0, f"{self.indent(tmp)}{self.format(line).strip()}") 366 | 367 | # find control structures 368 | m = re.match(self.ctrl_1line, line) 369 | if m: 370 | return ( 371 | 0, 372 | f"{self.indent()}{m.group(2)} {self.format(m.group(3)).strip()} " 373 | f"{m.group(4)} {self.format(m.group(6)).strip()}", 374 | ) 375 | 376 | m = re.match(self.fcnstart, line) 377 | if m: 378 | offset = self.indent_mode 379 | self.fstep.append(1) 380 | if self.indent_mode == -1: 381 | offset = int(len(self.fstep) > 1) 382 | return ( 383 | offset, 384 | f"{self.indent()}{m.group(2)} {self.format(m.group(3)).strip()}", 385 | ) 386 | 387 | m = re.match(self.ctrl_1line_dountil, line) 388 | if m: 389 | return ( 390 | 0, 391 | f"{self.indent()}{m.group(2)} {self.format(m.group(3)).strip()} " 392 | f"{m.group(4)} {m.group(5)}", 393 | ) 394 | 395 | m = re.match(self.ctrl_1line_dountil, line) 396 | if m: 397 | return ( 398 | 0, 399 | f"{self.indent()}{m.group(2)} {self.format(m.group(3)).strip()} " 400 | f"{m.group(4)} {m.group(5)}", 401 | ) 402 | 403 | m = re.match(self.ctrlstart, line) 404 | if m: 405 | self.istep.append(1) 406 | return ( 407 | 1, 408 | f"{self.indent()}{m.group(2)} {self.format(m.group(3)).strip()}", 409 | ) 410 | 411 | m = re.match(self.ctrlstart_2, line) 412 | if m: 413 | self.istep.append(2) 414 | return ( 415 | 2, 416 | f"{self.indent()}{m.group(2)} {self.format(m.group(3)).strip()}", 417 | ) 418 | 419 | m = re.match(self.ctrlcont, line) 420 | if m: 421 | return ( 422 | 0, 423 | f"{self.indent(-self.iwidth)}{m.group(2)} {self.format(m.group(3)).strip()}", 424 | ) 425 | 426 | m = re.match(self.ctrlend, line) 427 | if m: 428 | if len(self.istep) > 0: 429 | step = self.istep.pop() 430 | elif len(self.fstep) > 0: 431 | step = self.fstep.pop() 432 | else: 433 | print("There are more end-statements than blocks!", file=sys.stderr) 434 | step = 0 435 | return ( 436 | -step, 437 | f"{self.indent(-step * self.iwidth)}{m.group(2)} " 438 | f"{self.format(m.group(4)).strip()}", 439 | ) 440 | 441 | return (0, f"{self.indent()}{self.format(line).strip()}") 442 | 443 | # format file from line 'start' to line 'end' 444 | def format_file(self, *, filename, start, end, inplace): 445 | # read lines from file 446 | wlines = rlines = [] 447 | 448 | with ( 449 | sys.stdin if filename == "-" else open(filename, "r", encoding="UTF-8") 450 | ) as f: 451 | rlines = f.readlines()[start - 1 : end] 452 | 453 | # take care of empty input 454 | if not rlines: 455 | rlines = [""] 456 | 457 | # get initial indent lvl 458 | p = r"(\s*)(.*)" 459 | m = re.match(p, rlines[0]) 460 | if m: 461 | self.ilvl = len(m.group(1)) // self.iwidth 462 | rlines[0] = m.group(2) 463 | 464 | blank = True 465 | for line in rlines: 466 | # remove additional newlines 467 | if re.match(r"^\s*$", line): 468 | if not blank: 469 | blank = True 470 | wlines.append("") 471 | continue 472 | 473 | # format line 474 | (offset, line) = self.format_line(line) 475 | 476 | # adjust indent lvl 477 | self.ilvl = max(0, self.ilvl + offset) 478 | 479 | # add newline before block 480 | if ( 481 | self.separate_blocks 482 | and offset > 0 483 | and not blank 484 | and not self.islinecomment 485 | ): 486 | wlines.append("") 487 | 488 | # add formatted line 489 | wlines.append(line.rstrip()) 490 | 491 | # add newline after block 492 | blank = self.separate_blocks and offset < 0 493 | if blank: 494 | wlines.append("") 495 | 496 | # remove last line if blank 497 | while wlines and not wlines[-1]: 498 | wlines.pop() 499 | 500 | # take care of empty output 501 | if not wlines: 502 | wlines = [""] 503 | 504 | # write output 505 | if inplace: 506 | if filename == "-": 507 | print("Cannot write inplace to stdin!", file=sys.stderr) 508 | return 509 | 510 | if not (start == 1 and end is None): 511 | print("Cannot write inplace to a slice of a file!", file=sys.stderr) 512 | return 513 | 514 | with open(filename, "w", encoding="UTF-8") as f: 515 | for line in wlines: 516 | f.write(f"{line}\n") 517 | else: 518 | for line in wlines: 519 | print(line) 520 | 521 | 522 | def main(): 523 | parser = argparse.ArgumentParser(description="MATLAB formatter") 524 | parser.add_argument("filename", help="input file") 525 | parser.add_argument("--start-line", type=int, default=1, help="start line") 526 | parser.add_argument("--end-line", type=int, help="end line") 527 | parser.add_argument("--indent-width", type=int, default=4, help="indent width") 528 | parser.add_argument( 529 | "--separate-blocks", action="store_true", help="separate blocks" 530 | ) 531 | parser.add_argument( 532 | "--indent-mode", 533 | choices=["all_functions", "only_nested_functions", "classic"], 534 | default="all_functions", 535 | help="indent mode", 536 | ) 537 | parser.add_argument( 538 | "--add-space", 539 | choices=["all_operators", "exclude_pow", "no_spaces"], 540 | default="exclude_pow", 541 | help="add space", 542 | ) 543 | parser.add_argument( 544 | "--matrix-indent", 545 | choices=["aligned", "simple"], 546 | default="aligned", 547 | help="matrix indentation", 548 | ) 549 | parser.add_argument("--inplace", action="store_true", help="modify file in place") 550 | args = parser.parse_args() 551 | 552 | indent_modes = {"all_functions": 1, "only_nested_functions": -1, "classic": 0} 553 | operator_spaces = {"all_operators": 1, "exclude_pow": 0.5, "no_spaces": 0} 554 | matrix_indentation = {"aligned": 1, "simple": 0} 555 | 556 | formatter = Formatter( 557 | indent_width=args.indent_width, 558 | separate_blocks=args.separate_blocks, 559 | indent_mode=indent_modes.get(args.indent_mode, indent_modes["all_functions"]), 560 | operator_sep=operator_spaces.get( 561 | args.add_space, operator_spaces["exclude_pow"] 562 | ), 563 | matrix_indent=matrix_indentation.get( 564 | args.matrix_indent, matrix_indentation["aligned"] 565 | ), 566 | ) 567 | 568 | formatter.format_file( 569 | filename=args.filename, 570 | start=args.start_line, 571 | end=args.end_line, 572 | inplace=args.inplace, 573 | ) 574 | 575 | 576 | if __name__ == "__main__": 577 | main() 578 | -------------------------------------------------------------------------------- /maint/update_setupsh_branch.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -e 4 | 5 | asker() { 6 | read -r -p "$1" yn 7 | case ${yn} in 8 | [yY]) return 0 ;; 9 | [nN]) return 1 ;; 10 | *) asker "$@" ;; 11 | esac 12 | } 13 | 14 | asker "Would you like to proceed? This script will erase all your work. (y/n) " || exit 1 15 | git reset --hard 16 | 17 | branch=${1:?} 18 | 19 | if sed -E 's|(BRANCH=\"\$\{CAKE_AUTORATE_BRANCH:-\$\{2-)[^\}]+(\}\}\")|\1'"${branch}"'\2|' -i setup.sh 20 | then 21 | git add setup.sh 22 | git commit -sm "Update setup.sh branch for release" 23 | fi 24 | -------------------------------------------------------------------------------- /maint/version_bump.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -e 4 | 5 | asker() { 6 | read -r -p "$1" yn 7 | case ${yn} in 8 | [yY]) return 0 ;; 9 | [nN]) return 1 ;; 10 | *) asker "$@" ;; 11 | esac 12 | } 13 | 14 | asker "Would you like to proceed? This script will erase all your work. (y/n) " || exit 1 15 | git reset --hard 16 | 17 | version=${1:?} 18 | is_latest=${2:-1} 19 | EDITOR=${EDITOR:-nano} 20 | version_major=$(cut -d. -f1,2 <<<"${version}") 21 | 22 | if asker "Would you like to create a new changelog entry? (y/n) " 23 | then 24 | cur_date=$(date -I) 25 | sed -e '/Zep7RkGZ52/a\' -e '\n\n\#\# '"${cur_date}"' - Version '"${version}"'\n\n**Release notes here**' -i CHANGELOG.md 26 | fi 27 | ${EDITOR} CHANGELOG.md 28 | ( git add CHANGELOG.md && git commit -sm "Updated CHANGELOG for ${version}"; ) || : 29 | 30 | if sed -E 's/(^cake_autorate_version=\")[^\"]+(\"$)/\1'"${version}"'\2/' -i cake-autorate.sh 31 | then 32 | echo Cake autorate version updated in cake-autorate.sh 33 | ( git add cake-autorate.sh 34 | git commit -sm "Updated cake-autorate.sh version to ${version}"; ) || : 35 | fi 36 | 37 | if ((is_latest)) 38 | then 39 | if sed -E -e 's|()[^\<]+()|\1'"${version}"'\2|' \ 40 | -e 's|\[v[^\<]+( branch[^\<]+tree\/v)([^\)]+)|\[v'${version_major}'\1'${version_major}'|' -i README.md 41 | then 42 | echo Latest cake autorate version updated in README.md 43 | fi 44 | ( git add README.md 45 | git commit -sm "Updated latest version in README.md to ${version}"; ) || : 46 | fi 47 | -------------------------------------------------------------------------------- /setup.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # Capabilities: 4 | # - CAKE_AUTORATE_SETUP_SH_SUPPORT_SELF_REPLACING 5 | 6 | # Installation script for cake-autorate 7 | # 8 | # See https://github.com/lynxthecat/cake-autorate for more details 9 | 10 | # This needs to be encapsulated into a function so that we are sure that 11 | # sh reads all the contents of the shell file before we potentially erase it. 12 | # 13 | # Otherwise the read operation might fail and it won't be able to proceed with 14 | # the script as expected. 15 | main() { 16 | # Set correctness options 17 | set -eu 18 | 19 | # Setup dependencies to check for 20 | DEPENDENCIES="jsonfilter wget tar grep cmp mktemp bash" 21 | 22 | # Set up remote locations and branch 23 | BRANCH="${CAKE_AUTORATE_BRANCH:-${2-master}}" 24 | REPOSITORY="${CAKE_AUTORATE_REPO:-${1-lynxthecat/cake-autorate}}" 25 | SRC_DIR="https://codeload.github.com/${REPOSITORY}/tar.gz" 26 | API_URL="https://api.github.com/repos/${REPOSITORY}/commits/${BRANCH}" 27 | DOC_URL="https://github.com/${REPOSITORY}/tree/${BRANCH}#installation-on-openwrt" 28 | 29 | # Set SCRIPT_PREFIX and CONFIG_PREFIX 30 | SCRIPT_PREFIX=${CAKE_AUTORATE_SCRIPT_PREFIX:-} 31 | CONFIG_PREFIX=${CAKE_AUTORATE_CONFIG_PREFIX:-} 32 | 33 | # Store what OS we are running on 34 | MY_OS=unknown 35 | 36 | # Check if OS is OpenWRT or derivative 37 | unset ID_LIKE 38 | 39 | # The following formulations seem to be problematic for some sh versions: 40 | # - `. /not_found || true` 41 | # - set +e; . /etc/os-release 2>/dev/null; set -e 42 | # but this seems to work OK: 43 | [ -e "/etc/os-release" ] && . /etc/os-release 44 | 45 | for x in ${ID_LIKE:-} 46 | do 47 | if [ "${x}" = "openwrt" ] 48 | then 49 | MY_OS=openwrt 50 | [ -z "${SCRIPT_PREFIX}" ] && SCRIPT_PREFIX=/root/cake-autorate 51 | [ -z "${CONFIG_PREFIX}" ] && CONFIG_PREFIX=/root/cake-autorate 52 | break 53 | fi 54 | done 55 | 56 | # Check if OS is ASUSWRT-Merlin 57 | if [ "$(uname -o)" = "ASUSWRT-Merlin" ] 58 | then 59 | MY_OS=asuswrt 60 | [ -z "${SCRIPT_PREFIX}" ] && SCRIPT_PREFIX=/jffs/scripts/cake-autorate 61 | [ -z "${CONFIG_PREFIX}" ] && CONFIG_PREFIX=/jffs/configs/cake-autorate 62 | fi 63 | 64 | # If we are not running on OpenWRT or ASUSWRT-Merlin, exit 65 | if [ "${MY_OS}" = "unknown" ] 66 | then 67 | printf "This script requires OpenWrt or ASUSWRT-Merlin\n" >&2 68 | return 1 69 | fi 70 | 71 | # Check if an instance of cake-autorate is already running and exit if so 72 | if [ -d /var/run/cake-autorate ] 73 | then 74 | printf "At least one instance of cake-autorate appears to be running - exiting\n" >&2 75 | printf "If you want to install a new version, first stop any running instance of cake-autorate\n" >&2 76 | printf "If you are sure that no instance of cake-autorate is running, delete the /var/run/cake-autorate directory\n" >&2 77 | exit 1 78 | fi 79 | 80 | # Check for required setup.sh script dependencies 81 | exit_now=0 82 | for dep in ${DEPENDENCIES} 83 | do 84 | if ! type "${dep}" >/dev/null 2>&1; then 85 | printf >&2 "%s is required, please install it and rerun the script!\n" "${dep}" 86 | exit_now=1 87 | fi 88 | done 89 | [ "${exit_now}" -ge 1 ] && exit "${exit_now}" 90 | 91 | # Check for fping, which is required by default 92 | if ! type "fping" >/dev/null 2>&1; then 93 | printf "Warning, fping is required by default, but it is not installed.\n" 94 | printf "So cake-autorate will not run with default settings.\n" 95 | printf "Either install fping or ensure that one of the other supported ping binaries is installed and selected in the config(s).\n" 96 | fi 97 | 98 | # Create the cake-autorate directory if it does not exist 99 | mkdir -p "${SCRIPT_PREFIX}" "${CONFIG_PREFIX}" 100 | 101 | # Get the latest commit to download 102 | [ -z "${__CAKE_AUTORATE_SETUP_SH_EXEC_COMMIT:-}" ] && \ 103 | commit=$(wget -qO- "${API_URL}" | jsonfilter -e @.sha) || \ 104 | commit="${__CAKE_AUTORATE_SETUP_SH_EXEC_COMMIT}" 105 | if [ -z "${commit:-}" ]; 106 | then 107 | printf >&2 "Invalid operation occurred, commit variable should not be empty" 108 | exit 1 109 | fi 110 | 111 | printf "Detected Operating System: %s\n" "${MY_OS}" 112 | printf "Installation directories for detected Operating System:\n" 113 | printf " - Script prefix: %s\n" "${SCRIPT_PREFIX}" 114 | printf " - Config prefix: %s\n" "${CONFIG_PREFIX}" 115 | 116 | printf "Continue with installation? [Y/n] " 117 | 118 | read -r continue_installation 119 | if [ "${continue_installation}" = "N" ] || [ "${continue_installation}" = "n" ] 120 | then 121 | exit 122 | fi 123 | 124 | printf "Installing cake-autorate using %s (script) and %s (config) directories...\n" "${SCRIPT_PREFIX}" "${CONFIG_PREFIX}" 125 | 126 | # Download the files of the latest version of cake-autorate to a temporary directory, so we can move them to the cake-autorate directory 127 | if [ -z "${__CAKE_AUTORATE_SETUP_SH_EXEC_TMP:-}" ] 128 | then 129 | tmp=$(mktemp -d) 130 | wget -qO- "${SRC_DIR}/${commit}" | tar -xozf - -C "${tmp}" 131 | mv "${tmp}/cake-autorate-"*/* "${tmp}" 132 | else 133 | tmp="${__CAKE_AUTORATE_SETUP_SH_EXEC_TMP}" 134 | fi 135 | trap 'rm -rf "${tmp}"' EXIT INT TERM 136 | 137 | # Compare local setup.sh with the one from the repository 138 | if [ -e "${tmp}/setup.sh" ] && [ -e "${0}" ] && ! cmp -s "${0}" "${tmp}/setup.sh" 139 | then 140 | printf "Local setup.sh differs from the one in the repository\n" 141 | if grep -q ' CAKE_AUTORATE_SETUP_SH_SUPPORT_SELF_REPLACING$' "${tmp}/setup.sh" 142 | then 143 | printf "Self-replacing setup.sh and restarting while preserving the current environment...\n" 144 | trap - EXIT INT TERM 145 | __CAKE_AUTORATE_SETUP_SH_EXEC_TMP="${tmp}" \ 146 | __CAKE_AUTORATE_SETUP_SH_EXEC_COMMIT="${commit}" \ 147 | exec "${tmp}/setup.sh" "${REPOSITORY}" "${BRANCH}" 148 | else 149 | printf "Self-replacing not fully supported. Restarting with the new setup.sh...\n" 150 | rm -rf "${tmp}" 151 | exec "${tmp}/setup.sh" "${REPOSITORY}" "${BRANCH}" 152 | fi 153 | 154 | exit "${?}" # should not reach this point 155 | fi 156 | 157 | # Migrate old configuration (and new file) files if present 158 | cd "${CONFIG_PREFIX}" 159 | for file in cake-autorate_config.*.sh* 160 | do 161 | [ -e "${file}" ] || continue # handle case where there are no old config files 162 | new_fname="$(printf '%s\n' "${file}" | cut -c15-)" 163 | mv "${file}" "${new_fname}" 164 | done 165 | 166 | # Check if a configuration file exists, and ask whether to keep it 167 | cd "${CONFIG_PREFIX}" 168 | editmsg="\nNow edit the config.primary.sh file as described in:\n ${DOC_URL}" 169 | if [ -f config.primary.sh ] 170 | then 171 | printf "Previous configuration present - keep it? [Y/n] " 172 | read -r keep_previous_configuration 173 | if [ "${keep_previous_configuration}" = "N" ] || [ "${keep_previous_configuration}" = "n" ]; then 174 | mv "${tmp}/config.primary.sh" config.primary.sh 175 | rm -f config.primary.sh.new # delete config.primary.sh.new if exists 176 | else 177 | editmsg="Using prior configuration" 178 | mv "${tmp}/config.primary.sh" config.primary.sh.new 179 | fi 180 | else 181 | mv "${tmp}/config.primary.sh" config.primary.sh 182 | fi 183 | 184 | # remove old program files from cake-autorate directory 185 | cd "${SCRIPT_PREFIX}" 186 | old_fnames="cake-autorate.sh cake-autorate_defaults.sh cake-autorate_launcher.sh cake-autorate_lib.sh cake-autorate_setup.sh" 187 | for file in ${old_fnames} 188 | do 189 | rm -f "${file}" 190 | done 191 | 192 | # move the program files to the cake-autorate directory 193 | # scripts that need to be executable are already marked as such in the tarball 194 | cd "${SCRIPT_PREFIX}" 195 | files="cake-autorate.sh defaults.sh lib.sh setup.sh uninstall.sh" 196 | for file in ${files} 197 | do 198 | mv "${tmp}/${file}" "${file}" 199 | done 200 | 201 | # Generate a launcher.sh file from the launcher.sh.template file 202 | sed -e "s|%%SCRIPT_PREFIX%%|${SCRIPT_PREFIX}|g" -e "s|%%CONFIG_PREFIX%%|${CONFIG_PREFIX}|g" \ 203 | "${tmp}/launcher.sh.template" > "${SCRIPT_PREFIX}/launcher.sh" 204 | chmod +x "${SCRIPT_PREFIX}/launcher.sh" 205 | 206 | # Also for OpenWrt generate the service file from cake-autorate.template but DO NOT ACTIVATE IT 207 | if [ "${MY_OS}" = "openwrt" ] 208 | then 209 | sed "s|%%SCRIPT_PREFIX%%|${SCRIPT_PREFIX}|g" "${tmp}/cake-autorate.template" > /etc/init.d/cake-autorate 210 | chmod +x /etc/init.d/cake-autorate 211 | fi 212 | 213 | # Get version and generate a file containing version information 214 | cd "${SCRIPT_PREFIX}" 215 | version=$(grep -m 1 ^cake_autorate_version= "${SCRIPT_PREFIX}/cake-autorate.sh" | cut -d= -f2 | cut -d'"' -f2) 216 | cat > version.txt <<-EOF 217 | version=${version} 218 | commit=${commit} 219 | EOF 220 | 221 | # Tell how to handle the config file - use old, or edit the new one 222 | # shellcheck disable=SC2059 223 | printf "${editmsg}\n" 224 | 225 | printf '\n%s\n\n' "${version} successfully installed, but not yet running" 226 | printf '%s\n' "Start the software manually with:" 227 | printf '%s\n' " cd ${SCRIPT_PREFIX}; ./cake-autorate.sh" 228 | 229 | case "${MY_OS}" in 230 | 231 | "openwrt") 232 | printf '%s\n' "Run as a service with:" 233 | printf '%s\n\n' " service cake-autorate enable; service cake-autorate start" 234 | ;; 235 | "asuswrt") 236 | printf '%s\n' "Launch script on boot with:" 237 | printf '%s\n' " echo ${SCRIPT_PREFIX}/launcher.sh > /opt/etc/init.d/S99cake-autorate" 238 | printf '%s\n' " chmod +x /opt/etc/init.d/S99cake-autorate" 239 | printf '%s\n\n' "See also: https://github.com/RMerl/asuswrt-merlin.ng/wiki/User-scripts" 240 | ;; 241 | *) 242 | ;; 243 | esac 244 | } 245 | 246 | # Now that we are sure all code is loaded, we could execute the function 247 | main "${@}" 248 | -------------------------------------------------------------------------------- /uninstall.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # Uninstall script for cake-autorate 4 | # 5 | # See https://github.com/lynxthecat/cake-autorate for more details 6 | 7 | # This needs to be encapsulated into a function so that we are sure that 8 | # sh reads all the contents of the shell file before we potentially erase it. 9 | # 10 | # Otherwise the read operation might fail and it won't be able to proceed with 11 | # the script as expected. 12 | main() { 13 | # Set correctness options 14 | set -eu 15 | 16 | # Set SCRIPT_PREFIX and CONFIG_PREFIX 17 | SCRIPT_PREFIX=${CAKE_AUTORATE_SCRIPT_PREFIX:-} 18 | CONFIG_PREFIX=${CAKE_AUTORATE_CONFIG_PREFIX:-} 19 | 20 | # Store what OS we are running on 21 | MY_OS=unknown 22 | 23 | # Check if OS is OpenWRT or derivative 24 | unset ID_LIKE 25 | # We do `set +/-e` here because in some Busybox sh versions 26 | # `. /not_found || true` doesn't do anything 27 | set +e; . /etc/os-release 2>/dev/null; set -e 28 | for x in ${ID_LIKE:-} 29 | do 30 | if [ "${x}" = "openwrt" ] 31 | then 32 | MY_OS=openwrt 33 | [ -z "${SCRIPT_PREFIX}" ] && SCRIPT_PREFIX=/root/cake-autorate 34 | [ -z "${CONFIG_PREFIX}" ] && CONFIG_PREFIX=/root/cake-autorate 35 | break 36 | fi 37 | done 38 | 39 | # Check if OS is ASUSWRT-Merlin 40 | if [ "$(uname -o)" = "ASUSWRT-Merlin" ] 41 | then 42 | MY_OS=asuswrt 43 | [ -z "${SCRIPT_PREFIX}" ] && SCRIPT_PREFIX=/jffs/scripts/cake-autorate 44 | [ -z "${CONFIG_PREFIX}" ] && CONFIG_PREFIX=/jffs/configs/cake-autorate 45 | fi 46 | 47 | # If we are not running on OpenWRT or ASUSWRT-Merlin, exit 48 | if [ "${MY_OS}" = "unknown" ] 49 | then 50 | printf "This script requires OpenWrt or ASUSWRT-Merlin\n" >&2 51 | return 1 52 | fi 53 | 54 | # Stop cake-autorate before continueing 55 | if [ -x /etc/init.d/cake-autorate ] 56 | then 57 | /etc/init.d/cake-autorate stop || : 58 | fi 59 | rm -f /etc/init.d/cake-autorate /etc/rc.d/*cake-autorate 60 | 61 | # Check if an instance of cake-autorate is already running and exit if so 62 | if [ -d /var/run/cake-autorate ] 63 | then 64 | printf "At least one instance of cake-autorate appears to be running - exiting\n" >&2 65 | printf "If you want to uninstall a cake-autorate, first stop any running instance of cake-autorate\n" >&2 66 | printf "If you are sure that no instance of cake-autorate is running, delete the /var/run/cake-autorate directory\n" >&2 67 | exit 1 68 | fi 69 | 70 | # remove configuration files if user does not want to keep them 71 | cd "${CONFIG_PREFIX}" 72 | keepIt='' 73 | for file in *config.*.sh* 74 | do 75 | [ -e "${file}" ] || continue # handle case where there are no old config files 76 | if [ -z "${keepIt:-}" ] 77 | then 78 | printf "Would you like to keep your configs? [Y/n]" 79 | read -r keepIt 80 | [ -z "${keepIt:-}" ] && keepIt=Y 81 | fi 82 | 83 | if [ "${keepIt}" = "N" ] || [ "${keepIt}" = "n" ]; then 84 | rm -f "${file}" 85 | fi 86 | done 87 | 88 | # remove old program files from cake-autorate directory 89 | cd "${SCRIPT_PREFIX}" 90 | old_fnames="cake-autorate.sh cake-autorate_defaults.sh cake-autorate_launcher.sh cake-autorate_lib.sh cake-autorate_setup.sh" 91 | for file in ${old_fnames} 92 | do 93 | rm -f "${file}" 94 | done 95 | 96 | # remove current program files from the cake-autorate directory 97 | cd "${SCRIPT_PREFIX}" 98 | files="cake-autorate.sh defaults.sh launcher.sh lib.sh setup.sh uninstall.sh" 99 | for file in ${files} 100 | do 101 | rm -f "${file}" 102 | done 103 | 104 | # remove ${SCRIPT_PREFIX} and ${CONFIG_PREFIX} directories if empty 105 | rmdir "${SCRIPT_PREFIX}" "${CONFIG_PREFIX}" 2>/dev/null || : 106 | 107 | printf '%s\n' "cake-autorate was uninstalled" 108 | } 109 | 110 | # Now that we are sure all code is loaded, we could execute the function 111 | main "${@}" 112 | --------------------------------------------------------------------------------