├── .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 | 
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 | 
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 | [](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 |
--------------------------------------------------------------------------------