├── .codacy.yml ├── .gitattributes ├── .github ├── FUNDING.yml └── workflows │ └── shellcheck.yml ├── .gitignore ├── .htmlhintrc ├── LICENSE ├── README.md ├── S77chronyd ├── S77ntpd ├── chrony.conf ├── ntp.conf ├── ntpdstats_www.asp ├── ntpdstats_www.css ├── ntpdstats_www.js ├── ntpmerlin.sh └── timeserverd /.codacy.yml: -------------------------------------------------------------------------------- 1 | exclude_paths: 2 | - '**.js' 3 | - '**.css' 4 | - '**.asp' 5 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | * text eol=lf 2 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] 4 | patreon: # Replace with a single Patreon username 5 | open_collective: # Replace with a single Open Collective username 6 | ko_fi: # Replace with a single Ko-fi username 7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 9 | liberapay: # Replace with a single Liberapay username 10 | issuehunt: # Replace with a single IssueHunt username 11 | otechie: # Replace with a single Otechie username 12 | custom: ["https://www.paypal.com/donate/?hosted_button_id=47UTYVRBDKSTL"] 13 | -------------------------------------------------------------------------------- /.github/workflows/shellcheck.yml: -------------------------------------------------------------------------------- 1 | on: push 2 | 3 | name: 'ShellCheck' 4 | 5 | jobs: 6 | shellcheck: 7 | name: Shellcheck 8 | runs-on: ubuntu-latest 9 | steps: 10 | - uses: actions/checkout@v2 11 | - name: Run ShellCheck 12 | uses: ludeeus/action-shellcheck@master 13 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.tags 2 | *.tags1 3 | -------------------------------------------------------------------------------- /.htmlhintrc: -------------------------------------------------------------------------------- 1 | { 2 | "tagname-lowercase": true, 3 | "attr-lowercase": true, 4 | "attr-value-double-quotes": true, 5 | "doctype-first": true, 6 | "tag-pair": true, 7 | "spec-char-escape": false, 8 | "id-unique": true, 9 | "src-not-empty": true, 10 | "attr-no-duplication": true, 11 | "title-require": true 12 | } 13 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | GNU GENERAL PUBLIC LICENSE 2 | Version 3, 29 June 2007 3 | 4 | Copyright (C) 2007 Free Software Foundation, Inc. 5 | Everyone is permitted to copy and distribute verbatim copies 6 | of this license document, but changing it is not allowed. 7 | 8 | Preamble 9 | 10 | The GNU General Public License is a free, copyleft license for 11 | software and other kinds of works. 12 | 13 | The licenses for most software and other practical works are designed 14 | to take away your freedom to share and change the works. By contrast, 15 | the GNU General Public License is intended to guarantee your freedom to 16 | share and change all versions of a program--to make sure it remains free 17 | software for all its users. We, the Free Software Foundation, use the 18 | GNU General Public License for most of our software; it applies also to 19 | any other work released this way by its authors. You can apply it to 20 | your programs, too. 21 | 22 | When we speak of free software, we are referring to freedom, not 23 | price. Our General Public Licenses are designed to make sure that you 24 | have the freedom to distribute copies of free software (and charge for 25 | them if you wish), that you receive source code or can get it if you 26 | want it, that you can change the software or use pieces of it in new 27 | free programs, and that you know you can do these things. 28 | 29 | To protect your rights, we need to prevent others from denying you 30 | these rights or asking you to surrender the rights. Therefore, you have 31 | certain responsibilities if you distribute copies of the software, or if 32 | you modify it: responsibilities to respect the freedom of others. 33 | 34 | For example, if you distribute copies of such a program, whether 35 | gratis or for a fee, you must pass on to the recipients the same 36 | freedoms that you received. You must make sure that they, too, receive 37 | or can get the source code. And you must show them these terms so they 38 | know their rights. 39 | 40 | Developers that use the GNU GPL protect your rights with two steps: 41 | (1) assert copyright on the software, and (2) offer you this License 42 | giving you legal permission to copy, distribute and/or modify it. 43 | 44 | For the developers' and authors' protection, the GPL clearly explains 45 | that there is no warranty for this free software. For both users' and 46 | authors' sake, the GPL requires that modified versions be marked as 47 | changed, so that their problems will not be attributed erroneously to 48 | authors of previous versions. 49 | 50 | Some devices are designed to deny users access to install or run 51 | modified versions of the software inside them, although the manufacturer 52 | can do so. This is fundamentally incompatible with the aim of 53 | protecting users' freedom to change the software. The systematic 54 | pattern of such abuse occurs in the area of products for individuals to 55 | use, which is precisely where it is most unacceptable. Therefore, we 56 | have designed this version of the GPL to prohibit the practice for those 57 | products. If such problems arise substantially in other domains, we 58 | stand ready to extend this provision to those domains in future versions 59 | of the GPL, as needed to protect the freedom of users. 60 | 61 | Finally, every program is threatened constantly by software patents. 62 | States should not allow patents to restrict development and use of 63 | software on general-purpose computers, but in those that do, we wish to 64 | avoid the special danger that patents applied to a free program could 65 | make it effectively proprietary. To prevent this, the GPL assures that 66 | patents cannot be used to render the program non-free. 67 | 68 | The precise terms and conditions for copying, distribution and 69 | modification follow. 70 | 71 | TERMS AND CONDITIONS 72 | 73 | 0. Definitions. 74 | 75 | "This License" refers to version 3 of the GNU General Public License. 76 | 77 | "Copyright" also means copyright-like laws that apply to other kinds of 78 | works, such as semiconductor masks. 79 | 80 | "The Program" refers to any copyrightable work licensed under this 81 | License. Each licensee is addressed as "you". "Licensees" and 82 | "recipients" may be individuals or organizations. 83 | 84 | To "modify" a work means to copy from or adapt all or part of the work 85 | in a fashion requiring copyright permission, other than the making of an 86 | exact copy. The resulting work is called a "modified version" of the 87 | earlier work or a work "based on" the earlier work. 88 | 89 | A "covered work" means either the unmodified Program or a work based 90 | on the Program. 91 | 92 | To "propagate" a work means to do anything with it that, without 93 | permission, would make you directly or secondarily liable for 94 | infringement under applicable copyright law, except executing it on a 95 | computer or modifying a private copy. Propagation includes copying, 96 | distribution (with or without modification), making available to the 97 | public, and in some countries other activities as well. 98 | 99 | To "convey" a work means any kind of propagation that enables other 100 | parties to make or receive copies. Mere interaction with a user through 101 | a computer network, with no transfer of a copy, is not conveying. 102 | 103 | An interactive user interface displays "Appropriate Legal Notices" 104 | to the extent that it includes a convenient and prominently visible 105 | feature that (1) displays an appropriate copyright notice, and (2) 106 | tells the user that there is no warranty for the work (except to the 107 | extent that warranties are provided), that licensees may convey the 108 | work under this License, and how to view a copy of this License. If 109 | the interface presents a list of user commands or options, such as a 110 | menu, a prominent item in the list meets this criterion. 111 | 112 | 1. Source Code. 113 | 114 | The "source code" for a work means the preferred form of the work 115 | for making modifications to it. "Object code" means any non-source 116 | form of a work. 117 | 118 | A "Standard Interface" means an interface that either is an official 119 | standard defined by a recognized standards body, or, in the case of 120 | interfaces specified for a particular programming language, one that 121 | is widely used among developers working in that language. 122 | 123 | The "System Libraries" of an executable work include anything, other 124 | than the work as a whole, that (a) is included in the normal form of 125 | packaging a Major Component, but which is not part of that Major 126 | Component, and (b) serves only to enable use of the work with that 127 | Major Component, or to implement a Standard Interface for which an 128 | implementation is available to the public in source code form. A 129 | "Major Component", in this context, means a major essential component 130 | (kernel, window system, and so on) of the specific operating system 131 | (if any) on which the executable work runs, or a compiler used to 132 | produce the work, or an object code interpreter used to run it. 133 | 134 | The "Corresponding Source" for a work in object code form means all 135 | the source code needed to generate, install, and (for an executable 136 | work) run the object code and to modify the work, including scripts to 137 | control those activities. However, it does not include the work's 138 | System Libraries, or general-purpose tools or generally available free 139 | programs which are used unmodified in performing those activities but 140 | which are not part of the work. For example, Corresponding Source 141 | includes interface definition files associated with source files for 142 | the work, and the source code for shared libraries and dynamically 143 | linked subprograms that the work is specifically designed to require, 144 | such as by intimate data communication or control flow between those 145 | subprograms and other parts of the work. 146 | 147 | The Corresponding Source need not include anything that users 148 | can regenerate automatically from other parts of the Corresponding 149 | Source. 150 | 151 | The Corresponding Source for a work in source code form is that 152 | same work. 153 | 154 | 2. Basic Permissions. 155 | 156 | All rights granted under this License are granted for the term of 157 | copyright on the Program, and are irrevocable provided the stated 158 | conditions are met. This License explicitly affirms your unlimited 159 | permission to run the unmodified Program. The output from running a 160 | covered work is covered by this License only if the output, given its 161 | content, constitutes a covered work. This License acknowledges your 162 | rights of fair use or other equivalent, as provided by copyright law. 163 | 164 | You may make, run and propagate covered works that you do not 165 | convey, without conditions so long as your license otherwise remains 166 | in force. You may convey covered works to others for the sole purpose 167 | of having them make modifications exclusively for you, or provide you 168 | with facilities for running those works, provided that you comply with 169 | the terms of this License in conveying all material for which you do 170 | not control copyright. Those thus making or running the covered works 171 | for you must do so exclusively on your behalf, under your direction 172 | and control, on terms that prohibit them from making any copies of 173 | your copyrighted material outside their relationship with you. 174 | 175 | Conveying under any other circumstances is permitted solely under 176 | the conditions stated below. Sublicensing is not allowed; section 10 177 | makes it unnecessary. 178 | 179 | 3. Protecting Users' Legal Rights From Anti-Circumvention Law. 180 | 181 | No covered work shall be deemed part of an effective technological 182 | measure under any applicable law fulfilling obligations under article 183 | 11 of the WIPO copyright treaty adopted on 20 December 1996, or 184 | similar laws prohibiting or restricting circumvention of such 185 | measures. 186 | 187 | When you convey a covered work, you waive any legal power to forbid 188 | circumvention of technological measures to the extent such circumvention 189 | is effected by exercising rights under this License with respect to 190 | the covered work, and you disclaim any intention to limit operation or 191 | modification of the work as a means of enforcing, against the work's 192 | users, your or third parties' legal rights to forbid circumvention of 193 | technological measures. 194 | 195 | 4. Conveying Verbatim Copies. 196 | 197 | You may convey verbatim copies of the Program's source code as you 198 | receive it, in any medium, provided that you conspicuously and 199 | appropriately publish on each copy an appropriate copyright notice; 200 | keep intact all notices stating that this License and any 201 | non-permissive terms added in accord with section 7 apply to the code; 202 | keep intact all notices of the absence of any warranty; and give all 203 | recipients a copy of this License along with the Program. 204 | 205 | You may charge any price or no price for each copy that you convey, 206 | and you may offer support or warranty protection for a fee. 207 | 208 | 5. Conveying Modified Source Versions. 209 | 210 | You may convey a work based on the Program, or the modifications to 211 | produce it from the Program, in the form of source code under the 212 | terms of section 4, provided that you also meet all of these conditions: 213 | 214 | a) The work must carry prominent notices stating that you modified 215 | it, and giving a relevant date. 216 | 217 | b) The work must carry prominent notices stating that it is 218 | released under this License and any conditions added under section 219 | 7. This requirement modifies the requirement in section 4 to 220 | "keep intact all notices". 221 | 222 | c) You must license the entire work, as a whole, under this 223 | License to anyone who comes into possession of a copy. This 224 | License will therefore apply, along with any applicable section 7 225 | additional terms, to the whole of the work, and all its parts, 226 | regardless of how they are packaged. This License gives no 227 | permission to license the work in any other way, but it does not 228 | invalidate such permission if you have separately received it. 229 | 230 | d) If the work has interactive user interfaces, each must display 231 | Appropriate Legal Notices; however, if the Program has interactive 232 | interfaces that do not display Appropriate Legal Notices, your 233 | work need not make them do so. 234 | 235 | A compilation of a covered work with other separate and independent 236 | works, which are not by their nature extensions of the covered work, 237 | and which are not combined with it such as to form a larger program, 238 | in or on a volume of a storage or distribution medium, is called an 239 | "aggregate" if the compilation and its resulting copyright are not 240 | used to limit the access or legal rights of the compilation's users 241 | beyond what the individual works permit. Inclusion of a covered work 242 | in an aggregate does not cause this License to apply to the other 243 | parts of the aggregate. 244 | 245 | 6. Conveying Non-Source Forms. 246 | 247 | You may convey a covered work in object code form under the terms 248 | of sections 4 and 5, provided that you also convey the 249 | machine-readable Corresponding Source under the terms of this License, 250 | in one of these ways: 251 | 252 | a) Convey the object code in, or embodied in, a physical product 253 | (including a physical distribution medium), accompanied by the 254 | Corresponding Source fixed on a durable physical medium 255 | customarily used for software interchange. 256 | 257 | b) Convey the object code in, or embodied in, a physical product 258 | (including a physical distribution medium), accompanied by a 259 | written offer, valid for at least three years and valid for as 260 | long as you offer spare parts or customer support for that product 261 | model, to give anyone who possesses the object code either (1) a 262 | copy of the Corresponding Source for all the software in the 263 | product that is covered by this License, on a durable physical 264 | medium customarily used for software interchange, for a price no 265 | more than your reasonable cost of physically performing this 266 | conveying of source, or (2) access to copy the 267 | Corresponding Source from a network server at no charge. 268 | 269 | c) Convey individual copies of the object code with a copy of the 270 | written offer to provide the Corresponding Source. This 271 | alternative is allowed only occasionally and noncommercially, and 272 | only if you received the object code with such an offer, in accord 273 | with subsection 6b. 274 | 275 | d) Convey the object code by offering access from a designated 276 | place (gratis or for a charge), and offer equivalent access to the 277 | Corresponding Source in the same way through the same place at no 278 | further charge. You need not require recipients to copy the 279 | Corresponding Source along with the object code. If the place to 280 | copy the object code is a network server, the Corresponding Source 281 | may be on a different server (operated by you or a third party) 282 | that supports equivalent copying facilities, provided you maintain 283 | clear directions next to the object code saying where to find the 284 | Corresponding Source. Regardless of what server hosts the 285 | Corresponding Source, you remain obligated to ensure that it is 286 | available for as long as needed to satisfy these requirements. 287 | 288 | e) Convey the object code using peer-to-peer transmission, provided 289 | you inform other peers where the object code and Corresponding 290 | Source of the work are being offered to the general public at no 291 | charge under subsection 6d. 292 | 293 | A separable portion of the object code, whose source code is excluded 294 | from the Corresponding Source as a System Library, need not be 295 | included in conveying the object code work. 296 | 297 | A "User Product" is either (1) a "consumer product", which means any 298 | tangible personal property which is normally used for personal, family, 299 | or household purposes, or (2) anything designed or sold for incorporation 300 | into a dwelling. In determining whether a product is a consumer product, 301 | doubtful cases shall be resolved in favor of coverage. For a particular 302 | product received by a particular user, "normally used" refers to a 303 | typical or common use of that class of product, regardless of the status 304 | of the particular user or of the way in which the particular user 305 | actually uses, or expects or is expected to use, the product. A product 306 | is a consumer product regardless of whether the product has substantial 307 | commercial, industrial or non-consumer uses, unless such uses represent 308 | the only significant mode of use of the product. 309 | 310 | "Installation Information" for a User Product means any methods, 311 | procedures, authorization keys, or other information required to install 312 | and execute modified versions of a covered work in that User Product from 313 | a modified version of its Corresponding Source. The information must 314 | suffice to ensure that the continued functioning of the modified object 315 | code is in no case prevented or interfered with solely because 316 | modification has been made. 317 | 318 | If you convey an object code work under this section in, or with, or 319 | specifically for use in, a User Product, and the conveying occurs as 320 | part of a transaction in which the right of possession and use of the 321 | User Product is transferred to the recipient in perpetuity or for a 322 | fixed term (regardless of how the transaction is characterized), the 323 | Corresponding Source conveyed under this section must be accompanied 324 | by the Installation Information. But this requirement does not apply 325 | if neither you nor any third party retains the ability to install 326 | modified object code on the User Product (for example, the work has 327 | been installed in ROM). 328 | 329 | The requirement to provide Installation Information does not include a 330 | requirement to continue to provide support service, warranty, or updates 331 | for a work that has been modified or installed by the recipient, or for 332 | the User Product in which it has been modified or installed. Access to a 333 | network may be denied when the modification itself materially and 334 | adversely affects the operation of the network or violates the rules and 335 | protocols for communication across the network. 336 | 337 | Corresponding Source conveyed, and Installation Information provided, 338 | in accord with this section must be in a format that is publicly 339 | documented (and with an implementation available to the public in 340 | source code form), and must require no special password or key for 341 | unpacking, reading or copying. 342 | 343 | 7. Additional Terms. 344 | 345 | "Additional permissions" are terms that supplement the terms of this 346 | License by making exceptions from one or more of its conditions. 347 | Additional permissions that are applicable to the entire Program shall 348 | be treated as though they were included in this License, to the extent 349 | that they are valid under applicable law. If additional permissions 350 | apply only to part of the Program, that part may be used separately 351 | under those permissions, but the entire Program remains governed by 352 | this License without regard to the additional permissions. 353 | 354 | When you convey a copy of a covered work, you may at your option 355 | remove any additional permissions from that copy, or from any part of 356 | it. (Additional permissions may be written to require their own 357 | removal in certain cases when you modify the work.) You may place 358 | additional permissions on material, added by you to a covered work, 359 | for which you have or can give appropriate copyright permission. 360 | 361 | Notwithstanding any other provision of this License, for material you 362 | add to a covered work, you may (if authorized by the copyright holders of 363 | that material) supplement the terms of this License with terms: 364 | 365 | a) Disclaiming warranty or limiting liability differently from the 366 | terms of sections 15 and 16 of this License; or 367 | 368 | b) Requiring preservation of specified reasonable legal notices or 369 | author attributions in that material or in the Appropriate Legal 370 | Notices displayed by works containing it; or 371 | 372 | c) Prohibiting misrepresentation of the origin of that material, or 373 | requiring that modified versions of such material be marked in 374 | reasonable ways as different from the original version; or 375 | 376 | d) Limiting the use for publicity purposes of names of licensors or 377 | authors of the material; or 378 | 379 | e) Declining to grant rights under trademark law for use of some 380 | trade names, trademarks, or service marks; or 381 | 382 | f) Requiring indemnification of licensors and authors of that 383 | material by anyone who conveys the material (or modified versions of 384 | it) with contractual assumptions of liability to the recipient, for 385 | any liability that these contractual assumptions directly impose on 386 | those licensors and authors. 387 | 388 | All other non-permissive additional terms are considered "further 389 | restrictions" within the meaning of section 10. If the Program as you 390 | received it, or any part of it, contains a notice stating that it is 391 | governed by this License along with a term that is a further 392 | restriction, you may remove that term. If a license document contains 393 | a further restriction but permits relicensing or conveying under this 394 | License, you may add to a covered work material governed by the terms 395 | of that license document, provided that the further restriction does 396 | not survive such relicensing or conveying. 397 | 398 | If you add terms to a covered work in accord with this section, you 399 | must place, in the relevant source files, a statement of the 400 | additional terms that apply to those files, or a notice indicating 401 | where to find the applicable terms. 402 | 403 | Additional terms, permissive or non-permissive, may be stated in the 404 | form of a separately written license, or stated as exceptions; 405 | the above requirements apply either way. 406 | 407 | 8. Termination. 408 | 409 | You may not propagate or modify a covered work except as expressly 410 | provided under this License. Any attempt otherwise to propagate or 411 | modify it is void, and will automatically terminate your rights under 412 | this License (including any patent licenses granted under the third 413 | paragraph of section 11). 414 | 415 | However, if you cease all violation of this License, then your 416 | license from a particular copyright holder is reinstated (a) 417 | provisionally, unless and until the copyright holder explicitly and 418 | finally terminates your license, and (b) permanently, if the copyright 419 | holder fails to notify you of the violation by some reasonable means 420 | prior to 60 days after the cessation. 421 | 422 | Moreover, your license from a particular copyright holder is 423 | reinstated permanently if the copyright holder notifies you of the 424 | violation by some reasonable means, this is the first time you have 425 | received notice of violation of this License (for any work) from that 426 | copyright holder, and you cure the violation prior to 30 days after 427 | your receipt of the notice. 428 | 429 | Termination of your rights under this section does not terminate the 430 | licenses of parties who have received copies or rights from you under 431 | this License. If your rights have been terminated and not permanently 432 | reinstated, you do not qualify to receive new licenses for the same 433 | material under section 10. 434 | 435 | 9. Acceptance Not Required for Having Copies. 436 | 437 | You are not required to accept this License in order to receive or 438 | run a copy of the Program. Ancillary propagation of a covered work 439 | occurring solely as a consequence of using peer-to-peer transmission 440 | to receive a copy likewise does not require acceptance. However, 441 | nothing other than this License grants you permission to propagate or 442 | modify any covered work. These actions infringe copyright if you do 443 | not accept this License. Therefore, by modifying or propagating a 444 | covered work, you indicate your acceptance of this License to do so. 445 | 446 | 10. Automatic Licensing of Downstream Recipients. 447 | 448 | Each time you convey a covered work, the recipient automatically 449 | receives a license from the original licensors, to run, modify and 450 | propagate that work, subject to this License. You are not responsible 451 | for enforcing compliance by third parties with this License. 452 | 453 | An "entity transaction" is a transaction transferring control of an 454 | organization, or substantially all assets of one, or subdividing an 455 | organization, or merging organizations. If propagation of a covered 456 | work results from an entity transaction, each party to that 457 | transaction who receives a copy of the work also receives whatever 458 | licenses to the work the party's predecessor in interest had or could 459 | give under the previous paragraph, plus a right to possession of the 460 | Corresponding Source of the work from the predecessor in interest, if 461 | the predecessor has it or can get it with reasonable efforts. 462 | 463 | You may not impose any further restrictions on the exercise of the 464 | rights granted or affirmed under this License. For example, you may 465 | not impose a license fee, royalty, or other charge for exercise of 466 | rights granted under this License, and you may not initiate litigation 467 | (including a cross-claim or counterclaim in a lawsuit) alleging that 468 | any patent claim is infringed by making, using, selling, offering for 469 | sale, or importing the Program or any portion of it. 470 | 471 | 11. Patents. 472 | 473 | A "contributor" is a copyright holder who authorizes use under this 474 | License of the Program or a work on which the Program is based. The 475 | work thus licensed is called the contributor's "contributor version". 476 | 477 | A contributor's "essential patent claims" are all patent claims 478 | owned or controlled by the contributor, whether already acquired or 479 | hereafter acquired, that would be infringed by some manner, permitted 480 | by this License, of making, using, or selling its contributor version, 481 | but do not include claims that would be infringed only as a 482 | consequence of further modification of the contributor version. For 483 | purposes of this definition, "control" includes the right to grant 484 | patent sublicenses in a manner consistent with the requirements of 485 | this License. 486 | 487 | Each contributor grants you a non-exclusive, worldwide, royalty-free 488 | patent license under the contributor's essential patent claims, to 489 | make, use, sell, offer for sale, import and otherwise run, modify and 490 | propagate the contents of its contributor version. 491 | 492 | In the following three paragraphs, a "patent license" is any express 493 | agreement or commitment, however denominated, not to enforce a patent 494 | (such as an express permission to practice a patent or covenant not to 495 | sue for patent infringement). To "grant" such a patent license to a 496 | party means to make such an agreement or commitment not to enforce a 497 | patent against the party. 498 | 499 | If you convey a covered work, knowingly relying on a patent license, 500 | and the Corresponding Source of the work is not available for anyone 501 | to copy, free of charge and under the terms of this License, through a 502 | publicly available network server or other readily accessible means, 503 | then you must either (1) cause the Corresponding Source to be so 504 | available, or (2) arrange to deprive yourself of the benefit of the 505 | patent license for this particular work, or (3) arrange, in a manner 506 | consistent with the requirements of this License, to extend the patent 507 | license to downstream recipients. "Knowingly relying" means you have 508 | actual knowledge that, but for the patent license, your conveying the 509 | covered work in a country, or your recipient's use of the covered work 510 | in a country, would infringe one or more identifiable patents in that 511 | country that you have reason to believe are valid. 512 | 513 | If, pursuant to or in connection with a single transaction or 514 | arrangement, you convey, or propagate by procuring conveyance of, a 515 | covered work, and grant a patent license to some of the parties 516 | receiving the covered work authorizing them to use, propagate, modify 517 | or convey a specific copy of the covered work, then the patent license 518 | you grant is automatically extended to all recipients of the covered 519 | work and works based on it. 520 | 521 | A patent license is "discriminatory" if it does not include within 522 | the scope of its coverage, prohibits the exercise of, or is 523 | conditioned on the non-exercise of one or more of the rights that are 524 | specifically granted under this License. You may not convey a covered 525 | work if you are a party to an arrangement with a third party that is 526 | in the business of distributing software, under which you make payment 527 | to the third party based on the extent of your activity of conveying 528 | the work, and under which the third party grants, to any of the 529 | parties who would receive the covered work from you, a discriminatory 530 | patent license (a) in connection with copies of the covered work 531 | conveyed by you (or copies made from those copies), or (b) primarily 532 | for and in connection with specific products or compilations that 533 | contain the covered work, unless you entered into that arrangement, 534 | or that patent license was granted, prior to 28 March 2007. 535 | 536 | Nothing in this License shall be construed as excluding or limiting 537 | any implied license or other defenses to infringement that may 538 | otherwise be available to you under applicable patent law. 539 | 540 | 12. No Surrender of Others' Freedom. 541 | 542 | If conditions are imposed on you (whether by court order, agreement or 543 | otherwise) that contradict the conditions of this License, they do not 544 | excuse you from the conditions of this License. If you cannot convey a 545 | covered work so as to satisfy simultaneously your obligations under this 546 | License and any other pertinent obligations, then as a consequence you may 547 | not convey it at all. For example, if you agree to terms that obligate you 548 | to collect a royalty for further conveying from those to whom you convey 549 | the Program, the only way you could satisfy both those terms and this 550 | License would be to refrain entirely from conveying the Program. 551 | 552 | 13. Use with the GNU Affero General Public License. 553 | 554 | Notwithstanding any other provision of this License, you have 555 | permission to link or combine any covered work with a work licensed 556 | under version 3 of the GNU Affero General Public License into a single 557 | combined work, and to convey the resulting work. The terms of this 558 | License will continue to apply to the part which is the covered work, 559 | but the special requirements of the GNU Affero General Public License, 560 | section 13, concerning interaction through a network will apply to the 561 | combination as such. 562 | 563 | 14. Revised Versions of this License. 564 | 565 | The Free Software Foundation may publish revised and/or new versions of 566 | the GNU General Public License from time to time. Such new versions will 567 | be similar in spirit to the present version, but may differ in detail to 568 | address new problems or concerns. 569 | 570 | Each version is given a distinguishing version number. If the 571 | Program specifies that a certain numbered version of the GNU General 572 | Public License "or any later version" applies to it, you have the 573 | option of following the terms and conditions either of that numbered 574 | version or of any later version published by the Free Software 575 | Foundation. If the Program does not specify a version number of the 576 | GNU General Public License, you may choose any version ever published 577 | by the Free Software Foundation. 578 | 579 | If the Program specifies that a proxy can decide which future 580 | versions of the GNU General Public License can be used, that proxy's 581 | public statement of acceptance of a version permanently authorizes you 582 | to choose that version for the Program. 583 | 584 | Later license versions may give you additional or different 585 | permissions. However, no additional obligations are imposed on any 586 | author or copyright holder as a result of your choosing to follow a 587 | later version. 588 | 589 | 15. Disclaimer of Warranty. 590 | 591 | THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY 592 | APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT 593 | HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY 594 | OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, 595 | THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR 596 | PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM 597 | IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF 598 | ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 599 | 600 | 16. Limitation of Liability. 601 | 602 | IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING 603 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS 604 | THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY 605 | GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE 606 | USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF 607 | DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD 608 | PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), 609 | EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF 610 | SUCH DAMAGES. 611 | 612 | 17. Interpretation of Sections 15 and 16. 613 | 614 | If the disclaimer of warranty and limitation of liability provided 615 | above cannot be given local legal effect according to their terms, 616 | reviewing courts shall apply local law that most closely approximates 617 | an absolute waiver of all civil liability in connection with the 618 | Program, unless a warranty or assumption of liability accompanies a 619 | copy of the Program in return for a fee. 620 | 621 | END OF TERMS AND CONDITIONS 622 | 623 | How to Apply These Terms to Your New Programs 624 | 625 | If you develop a new program, and you want it to be of the greatest 626 | possible use to the public, the best way to achieve this is to make it 627 | free software which everyone can redistribute and change under these terms. 628 | 629 | To do so, attach the following notices to the program. It is safest 630 | to attach them to the start of each source file to most effectively 631 | state the exclusion of warranty; and each file should have at least 632 | the "copyright" line and a pointer to where the full notice is found. 633 | 634 | 635 | Copyright (C) 636 | 637 | This program is free software: you can redistribute it and/or modify 638 | it under the terms of the GNU General Public License as published by 639 | the Free Software Foundation, either version 3 of the License, or 640 | (at your option) any later version. 641 | 642 | This program is distributed in the hope that it will be useful, 643 | but WITHOUT ANY WARRANTY; without even the implied warranty of 644 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 645 | GNU General Public License for more details. 646 | 647 | You should have received a copy of the GNU General Public License 648 | along with this program. If not, see . 649 | 650 | Also add information on how to contact you by electronic and paper mail. 651 | 652 | If the program does terminal interaction, make it output a short 653 | notice like this when it starts in an interactive mode: 654 | 655 | Copyright (C) 656 | This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. 657 | This is free software, and you are welcome to redistribute it 658 | under certain conditions; type `show c' for details. 659 | 660 | The hypothetical commands `show w' and `show c' should show the appropriate 661 | parts of the General Public License. Of course, your program's commands 662 | might be different; for a GUI interface, you would use an "about box". 663 | 664 | You should also get your employer (if you work as a programmer) or school, 665 | if any, to sign a "copyright disclaimer" for the program, if necessary. 666 | For more information on this, and how to apply and follow the GNU GPL, see 667 | . 668 | 669 | The GNU General Public License does not permit incorporating your program 670 | into proprietary programs. If your program is a subroutine library, you 671 | may consider it more useful to permit linking proprietary applications with 672 | the library. If this is what you want to do, use the GNU Lesser General 673 | Public License instead of this License. But first, please read 674 | . 675 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ntpMerlin 2 | [![Codacy Badge](https://api.codacy.com/project/badge/Grade/1bc89c12c4bf44b49b28161f328e49b0)](https://www.codacy.com/app/jackyaz/ntpMerlin?utm_source=github.com&utm_medium=referral&utm_content=jackyaz/ntpMerlin&utm_campaign=Badge_Grade) 3 | ![Shellcheck](https://github.com/jackyaz/ntpmerlin/actions/workflows/shellcheck.yml/badge.svg) 4 | 5 | ## v3.4.5 6 | ### Updated on 2021-08-05 7 | ## About 8 | ntpMerlin implements an NTP time server for AsusWRT Merlin with charts for daily, weekly and monthly summaries of performance. A choice between ntpd and chrony is available. 9 | 10 | ntpMerlin is free to use under the [GNU General Public License version 3](https://opensource.org/licenses/GPL-3.0) (GPL 3.0). 11 | 12 | ### Supporting development 13 | Love the script and want to support future development? Any and all donations gratefully received! 14 | 15 | | [![paypal](https://www.paypalobjects.com/en_GB/i/btn/btn_donate_LG.gif)](https://www.paypal.com/donate/?hosted_button_id=47UTYVRBDKSTL)

[**PayPal donation**](https://www.paypal.com/donate/?hosted_button_id=47UTYVRBDKSTL) | [![paypal](https://puu.sh/IAhtp/3788f3a473.png)](https://www.paypal.com/donate/?hosted_button_id=47UTYVRBDKSTL) | 16 | | :----: | --- | 17 | 18 | ## Supported firmware versions 19 | You must be running firmware Merlin 384.15/384.13_4 or Fork 43E5 (or later) [Asuswrt-Merlin](https://asuswrt.lostrealm.ca/) 20 | 21 | ## Installation 22 | Using your preferred SSH client/terminal, copy and paste the following command, then press Enter: 23 | 24 | ```sh 25 | /usr/sbin/curl --retry 3 "https://raw.githubusercontent.com/jackyaz/ntpMerlin/master/ntpmerlin.sh" -o "/jffs/scripts/ntpmerlin" && chmod 0755 /jffs/scripts/ntpmerlin && /jffs/scripts/ntpmerlin install 26 | ``` 27 | 28 | ## Usage 29 | ### WebUI 30 | ntpMerlin can be configured via the WebUI, in the Addons section. 31 | 32 | ### Command Line 33 | To launch the ntpMerlin menu after installation, use: 34 | ```sh 35 | ntpmerlin 36 | ``` 37 | 38 | If this does not work, you will need to use the full path: 39 | ```sh 40 | /jffs/scripts/ntpmerlin 41 | ``` 42 | 43 | ## Screenshots 44 | 45 | ![WebUI](https://puu.sh/HF2uc/396909c6c7.png) 46 | 47 | ![CLI UI](https://puu.sh/HF2u3/02f06c84a4.png) 48 | 49 | ## Help 50 | Please post about any issues and problems here: [Asuswrt-Merlin AddOns on SNBForums](https://www.snbforums.com/forums/asuswrt-merlin-addons.60/?prefix_id=22) 51 | -------------------------------------------------------------------------------- /S77chronyd: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # shellcheck disable=SC2034 3 | ln -s /jffs/addons/ntpmerlin.d/timeserverd /opt/bin 2>/dev/null 4 | chmod 0755 /opt/bin/timeserverd 5 | 6 | if [ "$1" = "restart" ] || [ "$1" = "stop" ] || [ "$1" = "kill" ]; then 7 | killall -q timeserverd 8 | killall -q chronyd 9 | fi 10 | 11 | 12 | ENABLED=yes 13 | PROCS=timeserverd 14 | ARGS="S77chronyd" 15 | PREARGS="" 16 | PRECMD="killall ntp && killall ntpd" 17 | POSTCMD="" 18 | DESC=$PROCS 19 | PATH=/opt/sbin:/opt/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin 20 | 21 | . /opt/etc/init.d/rc.func 22 | -------------------------------------------------------------------------------- /S77ntpd: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # shellcheck disable=SC2034 3 | ln -s /jffs/addons/ntpmerlin.d/timeserverd /opt/bin 2>/dev/null 4 | chmod 0755 /opt/bin/timeserverd 5 | 6 | if [ "$1" = "restart" ] || [ "$1" = "stop" ] || [ "$1" = "kill" ]; then 7 | killall -q timeserverd 8 | killall -q ntpd 9 | fi 10 | 11 | ENABLED=yes 12 | PROCS=timeserverd 13 | ARGS="S77ntpd" 14 | PREARGS="" 15 | PRECMD="killall ntp && killall ntpd" 16 | POSTCMD="" 17 | DESC=$PROCS 18 | PATH=/opt/sbin:/opt/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin 19 | 20 | . /opt/etc/init.d/rc.func 21 | -------------------------------------------------------------------------------- /chrony.conf: -------------------------------------------------------------------------------- 1 | ####################################################################### 2 | # 3 | # This is a chrony configuration file. You can edit the options that you 4 | # want to enable. The more obscure options are not included. Refer 5 | # to the documentation for these. 6 | # 7 | ####################################################################### 8 | ### COMMENTS 9 | # Any of the following lines are comments (you have a choice of 10 | # comment start character): 11 | # a comment 12 | % a comment 13 | ! a comment 14 | ; a comment 15 | # 16 | # Below, the '!' form is used for lines that you might want to 17 | # uncomment and edit to make your own chrony.conf file. 18 | # 19 | ####################################################################### 20 | ####################################################################### 21 | ### SPECIFY YOUR NTP SERVERS 22 | # Most computers using chrony will send measurement requests to one or 23 | # more 'NTP servers'. You will probably find that your Internet Service 24 | # Provider or company have one or more NTP servers that you can specify. 25 | # Failing that, there are a lot of public NTP servers. There is a list 26 | # you can access at http://support.ntp.org/bin/view/Servers/WebHome or 27 | # you can use servers from the pool.ntp.org project. 28 | 29 | ! pool time.cloudflare.com iburst nts 30 | 31 | ! server 0.pool.ntp.org iburst 32 | ! server 1.pool.ntp.org iburst 33 | ! server 2.pool.ntp.org iburst 34 | ! server 3.pool.ntp.org iburst 35 | 36 | pool pool.ntp.org iburst 37 | 38 | ####################################################################### 39 | ### AVOIDING POTENTIALLY BOGUS CHANGES TO YOUR CLOCK 40 | # 41 | # To avoid changes being made to your computer's gain/loss compensation 42 | # when the measurement history is too erratic 43 | 44 | maxupdateskew 5 45 | 46 | # If you want to increase the minimum number of selectable sources 47 | # required to update the system clock in order to make the 48 | # synchronisation more reliable, uncomment (and edit) the following 49 | # line. 50 | 51 | ! minsources 2 52 | 53 | # If your computer has a good stable clock (e.g. it is not a virtual 54 | # machine), you might also want to reduce the maximum assumed drift 55 | # (frequency error) of the clock (the value is specified in ppm). 56 | 57 | ! maxdrift 100 58 | 59 | ####################################################################### 60 | ### FILENAMES ETC 61 | # Chrony likes to keep information about your computer's clock in files. 62 | # The 'driftfile' stores the computer's clock gain/loss rate in parts 63 | # per million. When chronyd starts, the system clock can be tuned 64 | # immediately so that it doesn't gain or lose any more time. You 65 | # generally want this, so it is uncommented. 66 | 67 | driftfile /opt/var/lib/chrony/drift 68 | 69 | # If you want to enable NTP authentication with symmetric keys, you will need 70 | # to uncomment the following line and edit the file to set up the keys. 71 | 72 | ! keyfile /opt/etc/chrony/chrony.keys 73 | 74 | # chronyd can save the measurement history for the servers to files when 75 | # it it exits. This is useful on Linux, if you stop chronyd and restart 76 | # tt with '-r' (e.g. after # an upgrade), the old measurements will still 77 | # be relevant when chronyd is restarted. This will reduce the time needed 78 | # to get accurate gain/loss measurements, especially with a dial-up link. 79 | # 80 | # Enable these two options to use this. 81 | 82 | dumponexit 83 | dumpdir /opt/var/lib/chrony 84 | 85 | # chronyd writes its process ID to a file. If you try to start a second 86 | # copy of chronyd, it will detect that the process named in the file is 87 | # still running and bail out. If you want to change the path to the PID 88 | # file, uncomment this line and edit it. The default path is shown. 89 | 90 | pidfile /opt/var/run/chrony/chronyd.pid 91 | 92 | # NTS dir for keys and cookies 93 | 94 | ! ntsdumpdir /opt/var/lib/chrony 95 | 96 | ####################################################################### 97 | ### INITIAL CLOCK CORRECTION 98 | # This option is useful to quickly correct the clock on start if it's 99 | # off by a large amount. The value '1.0' means that if the error is less 100 | # than 1 second, it will be gradually removed by speeding up or slowing 101 | # down your computer's clock until it is correct. If the error is above 102 | # 1 second, an immediate time jump will be applied to correct it. The 103 | # value '3' means the step is allowed only in the first three updates of 104 | # the clock. Some software can get upset if the system clock jumps 105 | # (especially backwards), so be careful! 106 | 107 | ! makestep 1.0 3 108 | 109 | ####################################################################### 110 | ### LOGGING 111 | # If you want to log information about the time measurements chronyd has 112 | # gathered, you might want to enable the following lines. You probably 113 | # only need this if you really enjoy looking at the logs, you want to 114 | # produce some graphs of your system's timekeeping performance, or you 115 | # need help in debugging a problem. 116 | 117 | ! logdir /opt/var/log/ 118 | ! log measurements statistics tracking 119 | 120 | # If you have real time clock support enabled (see below), you might want 121 | # this line instead: 122 | 123 | ! log measurements statistics tracking rtc 124 | 125 | ####################################################################### 126 | ### ACTING AS AN NTP SERVER 127 | # You might want the computer to be an NTP server for other computers. 128 | # e.g. you might be running chronyd on a dial-up machine that has a LAN 129 | # sitting behind it with several 'satellite' computers on it. 130 | # 131 | # By default, chronyd does not allow any clients to access it. You need 132 | # to explicitly enable access using 'allow' and 'deny' directives. 133 | # 134 | # e.g. to enable client access from the 192.168.*.* class B subnet, 135 | 136 | allow 10.0.0.0/8 137 | allow 172.16.0.0/12 138 | allow 192.168.0.0/16 139 | 140 | # .. but disallow the 192.168.100.* subnet of that, 141 | 142 | ! deny 192.168.100/24 143 | 144 | # You can have as many allow and deny directives as you need. The order 145 | # is unimportant. 146 | 147 | # If you want chronyd to act as an NTP broadcast server, enable and edit 148 | # (and maybe copy) the following line. This means that a broadcast 149 | # packet is sent to the address 192.168.1.255 every 60 seconds. The 150 | # address MUST correspond to the broadcast address of one of the network 151 | # interfaces on your machine. If you have multiple network interfaces, 152 | # add a broadcast line for each. 153 | 154 | ! broadcast 60 192.168.1.255 155 | 156 | # If you want to present your computer's time for others to synchronise 157 | # with, even if you don't seem to be synchronised to any NTP servers 158 | # yourself, enable the following line. The value 10 may be varied 159 | # between 1 and 15. You should avoid small values because you will look 160 | # like a real NTP server. The value 10 means that you appear to be 10 161 | # NTP 'hops' away from an authoritative source (atomic clock, GPS 162 | # receiver, radio clock etc). 163 | 164 | local stratum 10 165 | 166 | # Normally, chronyd will keep track of how many times each client 167 | # machine accesses it. The information can be accessed by the 'clients' 168 | # command of chronyc. You can disable this facility by uncommenting the 169 | # following line. This will save a bit of memory if you have many 170 | # clients and it will also disable support for the interleaved mode. 171 | 172 | ! noclientlog 173 | 174 | # The clientlog size is limited to 512KB by default. If you have many 175 | # clients, you might want to increase the limit. 176 | 177 | ! clientloglimit 4194304 178 | 179 | # By default, chronyd tries to respond to all valid NTP requests from 180 | # allowed addresses. If you want to limit the response rate for NTP 181 | # clients that are sending requests too frequently, uncomment and edit 182 | # the following line. 183 | 184 | ratelimit interval 3 burst 8 185 | 186 | ####################################################################### 187 | ### REPORTING BIG CLOCK CHANGES 188 | # Perhaps you want to know if chronyd suddenly detects any large error 189 | # in your computer's clock. This might indicate a fault or a problem 190 | # with the server(s) you are using, for example. 191 | # 192 | # The next option causes a message to be written to syslog when chronyd 193 | # has to correct an error above 0.5 seconds (you can use any amount you 194 | # like). 195 | 196 | logchange 0.5 197 | 198 | # The next option will send email to the named person when chronyd has 199 | # to correct an error above 0.5 seconds. (If you need to send mail to 200 | # several people, you need to set up a mailing list or sendmail alias 201 | # for them and use the address of that.) 202 | 203 | ! mailonchange wibble@foo.example.net 0.5 204 | 205 | ####################################################################### 206 | ### COMMAND ACCESS 207 | # The program chronyc is used to show the current operation of chronyd 208 | # and to change parts of its configuration whilst it is running. 209 | 210 | # By default chronyd binds to the loopback interface. Uncomment the 211 | # following lines to allow receiving command packets from remote hosts. 212 | 213 | ! bindcmdaddress 0.0.0.0 214 | ! bindcmdaddress :: 215 | 216 | # Normally, chronyd will only allow connections from chronyc on the same 217 | # machine as itself. This is for security. If you have a subnet 218 | # 192.168.*.* and you want to be able to use chronyc from any machine on 219 | # it, you could uncomment the following line. (Edit this to your own 220 | # situation.) 221 | 222 | ! cmdallow 192.168/16 223 | 224 | # You can add as many 'cmdallow' and 'cmddeny' lines as you like. The 225 | # syntax and meaning is the same as for 'allow' and 'deny', except that 226 | # 'cmdallow' and 'cmddeny' control access to the chronyd's command port. 227 | 228 | # Rate limiting can be enabled also for command packets. (Note, 229 | # commands from localhost are never limited.) 230 | 231 | ! cmdratelimit interval -4 burst 16 232 | 233 | ####################################################################### 234 | ### REAL TIME SCHEDULER 235 | # This directive tells chronyd to use the real-time FIFO scheduler with the 236 | # specified priority (which must be between 0 and 100). This should result 237 | # in reduced latency. You don't need it unless you really have a requirement 238 | # for extreme clock stability. Works only on Linux. Note that the "-P" 239 | # command-line switch will override this. 240 | 241 | ! sched_priority 1 242 | 243 | ####################################################################### 244 | ### LOCKING CHRONYD INTO RAM 245 | # This directive tells chronyd to use the mlockall() syscall to lock itself 246 | # into RAM so that it will never be paged out. This should result in reduced 247 | # latency. You don't need it unless you really have a requirement 248 | # for extreme clock stability. Works only on Linux. Note that the "-m" 249 | # command-line switch will also enable this feature. 250 | 251 | lock_all 252 | -------------------------------------------------------------------------------- /ntp.conf: -------------------------------------------------------------------------------- 1 | 2 | # replace the following with time servers or pools close to you 3 | # see http://support.ntp.org/bin/view/Servers/NTPPoolServers 4 | pool pool.ntp.org iburst 5 | 6 | interface ignore wildcard 7 | interface listen br0 8 | 9 | logfile /opt/var/spool/ntp/ntp.log 10 | driftfile /opt/var/spool/ntp/ntp.drift 11 | #leapfile /opt/var/spool/ntp/leap-seconds.list # https://hpiers.obspm.fr/iers/bul/bulc/ntp/leap-seconds.list 12 | 13 | restrict default limited kod nomodify notrap nopeer noquery # restrictive default IPv4 14 | restrict -6 default limited kod nomodify notrap nopeer noquery # restrictive default IPv6 15 | restrict source nomodify notrap noquery # required for pool directive if using restrictive default permissions 16 | restrict 127.0.0.1 # permissive localhost IPv4 17 | restrict -6 ::1 # permissive localhost IPv6 18 | 19 | disable auth stats 20 | -------------------------------------------------------------------------------- /ntpdstats_www.asp: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | ntpMerlin 11 | 12 | 13 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 46 | 47 | 48 |
49 | 50 | 51 |
52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | "> 61 | "> 62 | 63 | 64 | 65 | 66 | 69 | 289 | 290 |
  67 | 68 | 70 | 71 | 72 | 73 | 286 | 287 |
74 | 75 | 76 | 77 | 282 | 283 | 284 |
78 |
 
79 |
ntpMerlin
80 |
Stats last updated:
81 |
82 |
ntpMerlin implements an NTP time server for AsusWRT Merlin with charts for daily, weekly and monthly summaries of performance. A choice between ntpd and chrony is available.
83 | 84 | 85 | 86 | 87 | 88 | 89 | 99 | 100 | 101 | 102 | 108 | 109 | 110 | 111 | 114 | 115 |
Utilities (click to expand/collapse)
Version information 90 | 91 |     92 | 93 |     94 | 95 | 96 | 97 |     98 |
Update stats 103 | 104 | 105 |     106 | 107 |
Export 112 | 113 |
116 |
 
117 | 118 | 119 | 120 | 121 | 122 | 123 | 129 | 130 | 131 | 132 | 138 | 139 | 140 | 141 | 147 | 148 | 149 | 150 | 154 | 155 | 156 | 157 | 161 | 162 | 163 | 166 | 167 |
Configuration (click to expand/collapse)
Time Output Mode
(for CSV export)
124 | 125 | 126 | 127 | 128 |
Data Storage Location 133 | 134 | 135 | 136 | 137 |
Timeserver 142 | 143 | 144 | 145 | 146 |
Last X results to display 151 | 152 |  results (between 1 and 100, default: 10) 153 |
Number of days of data to keep 158 | 159 |  days (between 30 and 365, default: 30) 160 |
164 | 165 |
168 |
 
169 | 170 | 171 | 172 | 173 | 174 | 177 | 178 |
Latest timeserver stats (click to expand/collapse)
175 |
176 |
179 |
 
180 | 181 | 182 | 183 | 184 | 185 | 186 | 279 | 280 |
Charts (click to expand/collapse)
187 | 188 | 189 | 190 | 191 | 192 | 193 | 199 | 200 | 201 | 210 | 211 |
Chart Display Options (click to expand/collapse)
Time format
(for tooltips and Last 24h chart axis)
194 | 198 |
202 | 203 |       204 | 205 |       206 | 207 |       208 | 209 |
212 |
 
213 | 214 | 215 | 216 | 217 | 218 | 219 | 220 | 221 | 228 | 229 | 230 | 231 | 238 | 239 | 240 | 243 | 244 |
Offset (click to expand/collapse)
Data interval 222 | 227 |
Period to display 232 | 237 |
241 |
242 |
245 |
 
246 | 247 | 248 | 249 | 250 | 251 | 252 | 253 | 254 | 261 | 262 | 263 | 264 | 271 | 272 | 273 | 276 | 277 |
Drift (click to expand/collapse)
Data interval 255 | 260 |
Period to display 265 | 270 |
274 |
275 |
278 |
281 |
285 |
288 |
291 |
292 |
293 | "> 294 | 295 | 296 | 297 | 298 | 299 |
300 | 301 | 302 | 303 | -------------------------------------------------------------------------------- /ntpdstats_www.css: -------------------------------------------------------------------------------- 1 | p { 2 | font-weight: bolder; 3 | } 4 | 5 | thead.collapsible-jquery { 6 | color: white; 7 | padding: 0px; 8 | width: 100%; 9 | border: none; 10 | text-align: left; 11 | outline: none; 12 | cursor: pointer; 13 | } 14 | 15 | td.nodata { 16 | font-size: 48px !important; 17 | font-weight: bolder !important; 18 | height: 65px !important; 19 | font-family: Arial !important; 20 | border: none !important; 21 | text-align: center !important; 22 | } 23 | 24 | .SettingsTable { 25 | text-align: left; 26 | } 27 | 28 | .SettingsTable input { 29 | text-align: left; 30 | margin-left: 3px !important; 31 | } 32 | 33 | .SettingsTable input.savebutton { 34 | text-align: center; 35 | margin-top: 5px; 36 | margin-bottom: 5px; 37 | border-right: solid 1px black; 38 | border-left: solid 1px black; 39 | border-bottom: solid 1px black; 40 | } 41 | 42 | .SettingsTable td.savebutton { 43 | border-right: solid 1px black; 44 | border-left: solid 1px black; 45 | border-bottom: solid 1px black; 46 | background-color:rgb(77,89,93); 47 | } 48 | 49 | .SettingsTable .cronbutton { 50 | text-align: center; 51 | min-width: 50px; 52 | width: 50px; 53 | height: 23px; 54 | vertical-align: middle; 55 | } 56 | 57 | .SettingsTable select { 58 | margin-left: 3px !important; 59 | } 60 | 61 | .SettingsTable label { 62 | margin-right: 10px !important; 63 | vertical-align: top !important; 64 | } 65 | 66 | .SettingsTable th { 67 | background-color: #1F2D35 !important; 68 | background: #2F3A3E !important; 69 | border-bottom: none !important; 70 | border-top: none !important; 71 | font-size: 12px !important; 72 | color: white !important; 73 | padding: 4px !important; 74 | font-weight: bolder !important; 75 | padding: 0px !important; 76 | } 77 | 78 | .SettingsTable td { 79 | word-wrap: break-word !important; 80 | overflow-wrap: break-word !important; 81 | border-right: none; 82 | border-left: none; 83 | } 84 | 85 | .SettingsTable span.settingname { 86 | background-color: #1F2D35 !important; 87 | background: #2F3A3E !important; 88 | } 89 | 90 | .SettingsTable td.settingname { 91 | border-right: solid 1px black; 92 | border-left: solid 1px black; 93 | background-color: #1F2D35 !important; 94 | background: #2F3A3E !important; 95 | width: 35% !important; 96 | } 97 | 98 | .SettingsTable td.settingvalue { 99 | text-align: left !important; 100 | border-right: solid 1px black; 101 | } 102 | 103 | .SettingsTable th:first-child{ 104 | border-left: none !important; 105 | } 106 | 107 | .SettingsTable th:last-child { 108 | border-right: none !important; 109 | } 110 | 111 | .SettingsTable .invalid { 112 | background-color: darkred !important; 113 | } 114 | 115 | .SettingsTable .disabled { 116 | background-color: #CCCCCC !important; 117 | color: #888888 !important; 118 | } 119 | 120 | .removespacing { 121 | padding-left: 0px !important; 122 | margin-left: 0px !important; 123 | margin-bottom: 5px !important; 124 | text-align: center !important; 125 | } 126 | 127 | div.sortTableContainer { 128 | height: 300px; 129 | overflow-y: scroll; 130 | width: 745px; 131 | border: 1px solid #000; 132 | } 133 | 134 | .sortTable { 135 | table-layout: fixed !important; 136 | border: none; 137 | } 138 | 139 | thead.sortTableHeader th { 140 | background-image: linear-gradient(rgb(146, 160, 165) 0%, rgb(102, 117, 124) 100%); 141 | border-top: none !important; 142 | border-left: none !important; 143 | border-right: none !important; 144 | border-bottom: 1px solid #000 !important; 145 | font-weight: bolder; 146 | padding: 2px; 147 | text-align: center; 148 | color: #fff; 149 | position: sticky; 150 | top: 0; 151 | font-size: 11px !important; 152 | } 153 | 154 | thead.sortTableHeader th:first-child, 155 | thead.sortTableHeader th:last-child { 156 | border-right: none !important; 157 | } 158 | 159 | thead.sortTableHeader th:last-child { 160 | /*padding-left: 4px !important;*/ 161 | } 162 | 163 | thead.sortTableHeader th:first-child, 164 | thead.sortTableHeader td:first-child { 165 | border-left: none !important; 166 | } 167 | 168 | tbody.sortTableContent td:last-child, tbody.sortTableContent tr.sortNormalRow td:last-child, tbody.sortTableContent tr.sortAlternateRow td:last-child { 169 | /*padding-left: 4px !important;*/ 170 | } 171 | 172 | tbody.sortTableContent td{ 173 | border-bottom: 1px solid #000 !important; 174 | border-left: none !important; 175 | border-right: 1px solid #000 !important; 176 | border-top: none !important; 177 | padding: 2px; 178 | text-align: center; 179 | overflow: hidden !important; 180 | white-space: nowrap !important; 181 | font-size: 12px !important; 182 | } 183 | 184 | tbody.sortTableContent tr.sortRow:nth-child(odd) td { 185 | background-color: #2F3A3E !important; 186 | } 187 | 188 | tbody.sortTableContent tr.sortRow:nth-child(even) td { 189 | background-color: #475A5F !important; 190 | } 191 | 192 | th.sortable { 193 | cursor: pointer; 194 | } 195 | -------------------------------------------------------------------------------- /ntpdstats_www.js: -------------------------------------------------------------------------------- 1 | var $j = jQuery.noConflict(); //avoid conflicts on John's fork (state.js) 2 | 3 | var arraysortlistlines = []; 4 | var sortname = 'Time'; 5 | var sortdir = 'desc'; 6 | 7 | var maxNoCharts = 18; 8 | var currentNoCharts = 0; 9 | 10 | var ShowLines = GetCookie('ShowLines','string'); 11 | var ShowFill = GetCookie('ShowFill','string'); 12 | 13 | var DragZoom = true; 14 | var ChartPan = false; 15 | 16 | Chart.defaults.global.defaultFontColor = '#CCC'; 17 | Chart.Tooltip.positioners.cursor = function(chartElements,coordinates){ 18 | return coordinates; 19 | }; 20 | 21 | var dataintervallist = ['raw','hour','day']; 22 | var metriclist = ['Offset','Drift']; 23 | var measureunitlist = ['ms','ppm']; 24 | var chartlist = ['daily','weekly','monthly']; 25 | var timeunitlist = ['hour','day','day']; 26 | var intervallist = [24,7,30]; 27 | var bordercolourlist = ['#fc8500','#ffffff']; 28 | var backgroundcolourlist = ['rgba(252,133,0,0.5)','rgba(255,255,255,0.5)']; 29 | 30 | function keyHandler(e){ 31 | if(e.keyCode == 82){ 32 | $j(document).off('keydown'); 33 | ResetZoom(); 34 | } 35 | else if(e.keyCode == 68){ 36 | $j(document).off('keydown'); 37 | ToggleDragZoom(document.form.btnDragZoom); 38 | } 39 | else if(e.keyCode == 70){ 40 | $j(document).off('keydown'); 41 | ToggleFill(); 42 | } 43 | else if(e.keyCode == 76){ 44 | $j(document).off('keydown'); 45 | ToggleLines(); 46 | } 47 | } 48 | 49 | $j(document).keydown(function(e){keyHandler(e);}); 50 | $j(document).keyup(function(e){ 51 | $j(document).keydown(function(e){ 52 | keyHandler(e); 53 | }); 54 | }); 55 | 56 | function Draw_Chart_NoData(txtchartname,texttodisplay){ 57 | document.getElementById('divLineChart_'+txtchartname).width='730'; 58 | document.getElementById('divLineChart_'+txtchartname).height='500'; 59 | document.getElementById('divLineChart_'+txtchartname).style.width='730px'; 60 | document.getElementById('divLineChart_'+txtchartname).style.height='500px'; 61 | var ctx = document.getElementById('divLineChart_'+txtchartname).getContext('2d'); 62 | ctx.save(); 63 | ctx.textAlign = 'center'; 64 | ctx.textBaseline = 'middle'; 65 | ctx.font = 'normal normal bolder 48px Arial'; 66 | ctx.fillStyle = 'white'; 67 | ctx.fillText(texttodisplay,365,250); 68 | ctx.restore(); 69 | } 70 | 71 | function Draw_Chart(txtchartname,txttitle,txtunity,bordercolourname,backgroundcolourname){ 72 | var chartperiod = getChartPeriod($j('#'+txtchartname+'_Period option:selected').val()); 73 | var chartinterval = getChartInterval($j('#' + txtchartname + '_Interval option:selected').val()); 74 | var txtunitx = timeunitlist[$j('#'+txtchartname+'_Period option:selected').val()]; 75 | var numunitx = intervallist[$j('#'+txtchartname+'_Period option:selected').val()]; 76 | var zoompanxaxismax = moment(); 77 | var chartxaxismax = null; 78 | var chartxaxismin = moment().subtract(numunitx,txtunitx+'s'); 79 | var charttype = 'line'; 80 | var dataobject = window[txtchartname+'_'+chartinterval+'_'+chartperiod]; 81 | 82 | if(typeof dataobject === 'undefined' || dataobject === null){ Draw_Chart_NoData(txtchartname,'No data to display'); return; } 83 | if(dataobject.length == 0){ Draw_Chart_NoData(txtchartname,'No data to display'); return; } 84 | 85 | var chartLabels = dataobject.map(function(d){return d.Metric}); 86 | var chartData = dataobject.map(function(d){return {x: d.Time,y: d.Value}}); 87 | var objchartname = window['LineChart_'+txtchartname]; 88 | 89 | var timeaxisformat = getTimeFormat($j('#Time_Format option:selected').val(),'axis'); 90 | var timetooltipformat = getTimeFormat($j('#Time_Format option:selected').val(),'tooltip'); 91 | 92 | if(chartinterval == 'day'){ 93 | charttype = 'bar'; 94 | chartxaxismax = moment().endOf('day').subtract(9,'hours'); 95 | chartxaxismin = moment().startOf('day').subtract(numunitx-1,txtunitx+'s').subtract(12,'hours'); 96 | zoompanxaxismax = chartxaxismax; 97 | } 98 | 99 | if(chartperiod == 'daily' && chartinterval == 'day'){ 100 | txtunitx = 'day'; 101 | numunitx = 1; 102 | chartxaxismax = moment().endOf('day').subtract(9,'hours'); 103 | chartxaxismin = moment().startOf('day').subtract(12,'hours'); 104 | zoompanxaxismax = chartxaxismax; 105 | } 106 | 107 | factor=0; 108 | if(txtunitx=='hour'){ 109 | factor=60*60*1000; 110 | } 111 | else if(txtunitx=='day'){ 112 | factor=60*60*24*1000; 113 | } 114 | if(objchartname != undefined) objchartname.destroy(); 115 | var ctx = document.getElementById('divLineChart_'+txtchartname).getContext('2d'); 116 | var lineOptions = { 117 | segmentShowStroke: false, 118 | segmentStrokeColor: '#000', 119 | animationEasing: 'easeOutQuart', 120 | animationSteps: 100, 121 | maintainAspectRatio: false, 122 | animateScale: true, 123 | hover: { mode: 'point' }, 124 | legend: { display: false,position: 'bottom',onClick: null }, 125 | title: { display: true,text: txttitle }, 126 | tooltips: { 127 | callbacks: { 128 | title: function (tooltipItem,data){ 129 | if(chartinterval == 'day'){ 130 | return moment(tooltipItem[0].xLabel,'X').format('YYYY-MM-DD'); 131 | } 132 | else{ 133 | return moment(tooltipItem[0].xLabel,'X').format(timetooltipformat); 134 | } 135 | }, 136 | label: function (tooltipItem,data){ return round(data.datasets[tooltipItem.datasetIndex].data[tooltipItem.index].y,3).toFixed(3)+' '+txtunity;} 137 | }, 138 | mode: 'point', 139 | position: 'cursor', 140 | intersect: true 141 | }, 142 | scales: { 143 | xAxes: [{ 144 | type: 'time', 145 | gridLines: { display: true,color: '#282828' }, 146 | ticks: { 147 | min: chartxaxismin, 148 | max: chartxaxismax, 149 | display: true 150 | }, 151 | time: { 152 | parser: 'X', 153 | unit: txtunitx, 154 | stepSize: 1, 155 | displayFormats: timeaxisformat 156 | } 157 | }], 158 | yAxes: [{ 159 | gridLines: { display: false,color: '#282828' }, 160 | scaleLabel: { display: false,labelString: txtunity }, 161 | ticks: { 162 | display: true, 163 | callback: function (value,index,values){ 164 | return round(value,3).toFixed(3)+' '+txtunity; 165 | } 166 | } 167 | }] 168 | }, 169 | plugins: { 170 | zoom: { 171 | pan: { 172 | enabled: ChartPan, 173 | mode: 'xy', 174 | rangeMin: { 175 | x: chartxaxismin, 176 | y: getLimit(chartData,'y','min',false) - Math.sqrt(Math.pow(getLimit(chartData,'y','min',false),2))*0.1 177 | }, 178 | rangeMax: { 179 | x: zoompanxaxismax, 180 | y: getLimit(chartData,'y','max',false)+getLimit(chartData,'y','max',false)*0.1 181 | }, 182 | }, 183 | zoom: { 184 | enabled: true, 185 | drag: DragZoom, 186 | mode: 'xy', 187 | rangeMin: { 188 | x: chartxaxismin, 189 | y: getLimit(chartData,'y','min',false) - Math.sqrt(Math.pow(getLimit(chartData,'y','min',false),2))*0.1 190 | }, 191 | rangeMax: { 192 | x: zoompanxaxismax, 193 | y: getLimit(chartData,'y','max',false)+getLimit(chartData,'y','max',false)*0.1 194 | }, 195 | speed: 0.1 196 | }, 197 | } 198 | }, 199 | annotation: { 200 | drawTime: 'afterDatasetsDraw', 201 | annotations: [{ 202 | //id: 'avgline', 203 | type: ShowLines, 204 | mode: 'horizontal', 205 | scaleID: 'y-axis-0', 206 | value: getAverage(chartData), 207 | borderColor: bordercolourname, 208 | borderWidth: 1, 209 | borderDash: [5,5], 210 | label: { 211 | backgroundColor: 'rgba(0,0,0,0.3)', 212 | fontFamily: 'sans-serif', 213 | fontSize: 10, 214 | fontStyle: 'bold', 215 | fontColor: '#fff', 216 | xPadding: 6, 217 | yPadding: 6, 218 | cornerRadius: 6, 219 | position: 'center', 220 | enabled: true, 221 | xAdjust: 0, 222 | yAdjust: 0, 223 | content: 'Avg='+round(getAverage(chartData),3).toFixed(3)+txtunity 224 | } 225 | }, 226 | { 227 | //id: 'maxline', 228 | type: ShowLines, 229 | mode: 'horizontal', 230 | scaleID: 'y-axis-0', 231 | value: getLimit(chartData,'y','max',true), 232 | borderColor: bordercolourname, 233 | borderWidth: 1, 234 | borderDash: [5,5], 235 | label: { 236 | backgroundColor: 'rgba(0,0,0,0.3)', 237 | fontFamily: 'sans-serif', 238 | fontSize: 10, 239 | fontStyle: 'bold', 240 | fontColor: '#fff', 241 | xPadding: 6, 242 | yPadding: 6, 243 | cornerRadius: 6, 244 | position: 'right', 245 | enabled: true, 246 | xAdjust: 15, 247 | yAdjust: 0, 248 | content: 'Max='+round(getLimit(chartData,'y','max',true),3).toFixed(3)+txtunity 249 | } 250 | }, 251 | { 252 | //id: 'minline', 253 | type: ShowLines, 254 | mode: 'horizontal', 255 | scaleID: 'y-axis-0', 256 | value: getLimit(chartData,'y','min',true), 257 | borderColor: bordercolourname, 258 | borderWidth: 1, 259 | borderDash: [5,5], 260 | label: { 261 | backgroundColor: 'rgba(0,0,0,0.3)', 262 | fontFamily: 'sans-serif', 263 | fontSize: 10, 264 | fontStyle: 'bold', 265 | fontColor: '#fff', 266 | xPadding: 6, 267 | yPadding: 6, 268 | cornerRadius: 6, 269 | position: 'left', 270 | enabled: true, 271 | xAdjust: 15, 272 | yAdjust: 0, 273 | content: 'Min='+round(getLimit(chartData,'y','min',true),3).toFixed(3)+txtunity 274 | } 275 | }] 276 | } 277 | }; 278 | var lineDataset = { 279 | labels: chartLabels, 280 | datasets: [{data: chartData, 281 | borderWidth: 1, 282 | pointRadius: 1, 283 | lineTension: 0, 284 | fill: ShowFill, 285 | backgroundColor: backgroundcolourname, 286 | borderColor: bordercolourname 287 | }] 288 | }; 289 | objchartname = new Chart(ctx,{ 290 | type: charttype, 291 | data: lineDataset, 292 | options: lineOptions 293 | }); 294 | window['LineChart_'+txtchartname] = objchartname; 295 | } 296 | 297 | function getLimit(datasetname,axis,maxmin,isannotation){ 298 | var limit = 0; 299 | var values; 300 | if(axis == 'x'){ 301 | values = datasetname.map(function(o){ return o.x } ); 302 | } 303 | else{ 304 | values = datasetname.map(function(o){ return o.y } ); 305 | } 306 | 307 | if(maxmin == 'max'){ 308 | limit=Math.max.apply(Math,values); 309 | } 310 | else{ 311 | limit=Math.min.apply(Math,values); 312 | } 313 | if(maxmin == 'max' && limit == 0 && isannotation == false){ 314 | limit = 1; 315 | } 316 | return limit; 317 | } 318 | 319 | function getAverage(datasetname){ 320 | var total = 0; 321 | for(var i = 0; i < datasetname.length; i++){ 322 | total += (datasetname[i].y*1); 323 | } 324 | var avg = total / datasetname.length; 325 | return avg; 326 | } 327 | 328 | function round(value,decimals){ 329 | return Number(Math.round(value+'e'+decimals)+'e-'+decimals); 330 | } 331 | 332 | function ToggleLines(){ 333 | if(ShowLines == ''){ 334 | ShowLines = 'line'; 335 | SetCookie('ShowLines','line'); 336 | } 337 | else{ 338 | ShowLines = ''; 339 | SetCookie('ShowLines',''); 340 | } 341 | for(var i = 0; i < metriclist.length; i++){ 342 | for(var i3 = 0; i3 < 3; i3++){ 343 | window['LineChart_'+metriclist[i]].options.annotation.annotations[i3].type=ShowLines; 344 | } 345 | window['LineChart_'+metriclist[i]].update(); 346 | } 347 | } 348 | 349 | function ToggleFill(){ 350 | if(ShowFill == 'false'){ 351 | ShowFill = 'origin'; 352 | SetCookie('ShowFill','origin'); 353 | } 354 | else{ 355 | ShowFill = 'false'; 356 | SetCookie('ShowFill','false'); 357 | } 358 | for(var i = 0; i < metriclist.length; i++){ 359 | window['LineChart_'+metriclist[i]].data.datasets[0].fill=ShowFill; 360 | window['LineChart_'+metriclist[i]].update(); 361 | } 362 | } 363 | 364 | function RedrawAllCharts(){ 365 | for(var i = 0; i < metriclist.length; i++){ 366 | Draw_Chart_NoData(metriclist[i],'Data loading...'); 367 | for(var i2 = 0; i2 < chartlist.length; i2++){ 368 | for(var i3 = 0; i3 < dataintervallist.length; i3++){ 369 | d3.csv('/ext/ntpmerlin/csv/'+metriclist[i]+'_'+dataintervallist[i3]+'_'+chartlist[i2]+'.htm').then(SetGlobalDataset.bind(null,metriclist[i]+'_'+dataintervallist[i3]+'_'+chartlist[i2])); 370 | } 371 | } 372 | } 373 | } 374 | 375 | function SetGlobalDataset(txtchartname,dataobject){ 376 | window[txtchartname] = dataobject; 377 | currentNoCharts++; 378 | if(currentNoCharts == maxNoCharts){ 379 | document.getElementById('ntpupdate_text').innerHTML = ''; 380 | showhide('imgNTPUpdate',false); 381 | showhide('ntpupdate_text',false); 382 | showhide('btnUpdateStats',true); 383 | for(var i = 0; i < metriclist.length; i++){ 384 | $j('#'+metriclist[i]+'_Interval').val(GetCookie(metriclist[i]+'_Interval','number')); 385 | changePeriod(document.getElementById(metriclist[i]+'_Interval')); 386 | $j('#'+metriclist[i]+'_Period').val(GetCookie(metriclist[i]+'_Period','number')); 387 | Draw_Chart(metriclist[i],metriclist[i],measureunitlist[i],bordercolourlist[i],backgroundcolourlist[i]); 388 | } 389 | AddEventHandlers(); 390 | get_lastx_file(); 391 | } 392 | } 393 | 394 | function getTimeFormat(value,format){ 395 | var timeformat; 396 | 397 | if(format == 'axis'){ 398 | if(value == 0){ 399 | timeformat = { 400 | millisecond: 'HH:mm:ss.SSS', 401 | second: 'HH:mm:ss', 402 | minute: 'HH:mm', 403 | hour: 'HH:mm' 404 | } 405 | } 406 | else if(value == 1){ 407 | timeformat = { 408 | millisecond: 'h:mm:ss.SSS A', 409 | second: 'h:mm:ss A', 410 | minute: 'h:mm A', 411 | hour: 'h A' 412 | } 413 | } 414 | } 415 | else if(format == 'tooltip'){ 416 | if(value == 0){ 417 | timeformat = 'YYYY-MM-DD HH:mm:ss'; 418 | } 419 | else if(value == 1){ 420 | timeformat = 'YYYY-MM-DD h:mm:ss A'; 421 | } 422 | } 423 | 424 | return timeformat; 425 | } 426 | 427 | function GetCookie(cookiename,returntype){ 428 | var s; 429 | if((s = cookie.get('ntp_'+cookiename)) != null){ 430 | return cookie.get('ntp_'+cookiename); 431 | } 432 | else{ 433 | if(returntype == 'string'){ 434 | return ''; 435 | } 436 | else if(returntype == 'number'){ 437 | return 0; 438 | } 439 | } 440 | } 441 | 442 | function SetCookie(cookiename,cookievalue){ 443 | cookie.set('ntp_'+cookiename,cookievalue,10 * 365); 444 | } 445 | 446 | function AddEventHandlers(){ 447 | $j('.collapsible-jquery').off('click').on('click',function(){ 448 | $j(this).siblings().toggle('fast',function(){ 449 | if($j(this).css('display') == 'none'){ 450 | SetCookie($j(this).siblings()[0].id,'collapsed'); 451 | } 452 | else{ 453 | SetCookie($j(this).siblings()[0].id,'expanded'); 454 | } 455 | }) 456 | }); 457 | 458 | $j('.collapsible-jquery').each(function(index,element){ 459 | if(GetCookie($j(this)[0].id,'string') == 'collapsed'){ 460 | $j(this).siblings().toggle(false); 461 | } 462 | else{ 463 | $j(this).siblings().toggle(true); 464 | } 465 | }); 466 | } 467 | 468 | $j.fn.serializeObject = function(){ 469 | var o = custom_settings; 470 | var a = this.serializeArray(); 471 | $j.each(a,function(){ 472 | if(o[this.name] !== undefined && this.name.indexOf('ntpmerlin') != -1 && this.name.indexOf('version') == -1){ 473 | if(!o[this.name].push){ 474 | o[this.name] = [o[this.name]]; 475 | } 476 | o[this.name].push(this.value || ''); 477 | } else if(this.name.indexOf('ntpmerlin') != -1 && this.name.indexOf('version') == -1){ 478 | o[this.name] = this.value || ''; 479 | } 480 | }); 481 | return o; 482 | }; 483 | 484 | function SetCurrentPage(){ 485 | document.form.next_page.value = window.location.pathname.substring(1); 486 | document.form.current_page.value = window.location.pathname.substring(1); 487 | } 488 | 489 | function ErrorCSVExport(){ 490 | document.getElementById('aExport').href='javascript:alert(\'Error exporting CSV,please refresh the page and try again\')'; 491 | } 492 | 493 | function ParseCSVExport(data){ 494 | var csvContent = 'Timestamp,Offset,Frequency,Sys_Jitter,Clk_Jitter,Clk_Wander,Rootdisp\n'; 495 | for(var i = 0; i < data.length; i++){ 496 | var dataString = data[i].Timestamp+','+data[i].Offset+','+data[i].Frequency+','+data[i].Sys_Jitter+','+data[i].Clk_Jitter+','+data[i].Clk_Wander+','+data[i].Rootdisp; 497 | csvContent += i < data.length-1 ? dataString+'\n' : dataString; 498 | } 499 | document.getElementById('aExport').href='data:text/csv;charset=utf-8,'+encodeURIComponent(csvContent); 500 | } 501 | 502 | function initial(){ 503 | SetCurrentPage(); 504 | LoadCustomSettings(); 505 | show_menu(); 506 | $j('#sortTableContainer').empty(); 507 | $j('#sortTableContainer').append(BuildLastXTableNoData()); 508 | get_conf_file(); 509 | d3.csv('/ext/ntpmerlin/csv/CompleteResults.htm').then(function(data){ParseCSVExport(data);}).catch(function(){ErrorCSVExport();}); 510 | $j('#Time_Format').val(GetCookie('Time_Format','number')); 511 | ScriptUpdateLayout(); 512 | get_statstitle_file(); 513 | RedrawAllCharts(); 514 | } 515 | 516 | function ScriptUpdateLayout(){ 517 | var localver = GetVersionNumber('local'); 518 | var serverver = GetVersionNumber('server'); 519 | $j('#ntpmerlin_version_local').text(localver); 520 | 521 | if(localver != serverver && serverver != 'N/A'){ 522 | $j('#ntpmerlin_version_server').text('Updated version available: '+serverver); 523 | showhide('btnChkUpdate',false); 524 | showhide('ntpmerlin_version_server',true); 525 | showhide('btnDoUpdate',true); 526 | } 527 | } 528 | 529 | function reload(){ 530 | location.reload(true); 531 | } 532 | 533 | function Validate_Number_Setting(forminput,upperlimit,lowerlimit){ 534 | var inputname = forminput.name; 535 | var inputvalue = forminput.value*1; 536 | 537 | if(inputvalue > upperlimit || inputvalue < lowerlimit){ 538 | $j(forminput).addClass('invalid'); 539 | return false; 540 | } 541 | else{ 542 | $j(forminput).removeClass('invalid'); 543 | return true; 544 | } 545 | } 546 | 547 | function Format_Number_Setting(forminput){ 548 | var inputname = forminput.name; 549 | var inputvalue = forminput.value*1; 550 | 551 | if(forminput.value.length == 0 || inputvalue == NaN){ 552 | return false; 553 | } 554 | else{ 555 | forminput.value = parseInt(forminput.value); 556 | return true; 557 | } 558 | } 559 | 560 | function Validate_All(){ 561 | var validationfailed = false; 562 | if(! Validate_Number_Setting(document.form.ntpmerlin_lastxresults,100,10)){validationfailed=true;} 563 | if(! Validate_Number_Setting(document.form.ntpmerlin_daystokeep,365,30)){validationfailed=true;} 564 | 565 | if(validationfailed){ 566 | alert('Validation for some fields failed. Please correct invalid values and try again.'); 567 | return false; 568 | } 569 | else{ 570 | return true; 571 | } 572 | } 573 | 574 | function getChartPeriod(period){ 575 | var chartperiod = 'daily'; 576 | if(period == 0) chartperiod = 'daily'; 577 | else if(period == 1) chartperiod = 'weekly'; 578 | else if(period == 2) chartperiod = 'monthly'; 579 | return chartperiod; 580 | } 581 | 582 | function getChartInterval(layout){ 583 | var charttype = 'raw'; 584 | if(layout == 0) charttype = 'raw'; 585 | else if(layout == 1) charttype = 'hour'; 586 | else if(layout == 2) charttype = 'day'; 587 | return charttype; 588 | } 589 | 590 | function changePeriod(e){ 591 | value = e.value * 1; 592 | name = e.id.substring(0,e.id.indexOf('_')); 593 | if(value == 2){ 594 | $j('select[id="'+name+'_Period"] option:contains(24)').text('Today'); 595 | } 596 | else{ 597 | $j('select[id="'+name+'_Period"] option:contains("Today")').text('Last 24 hours'); 598 | } 599 | } 600 | 601 | function ResetZoom(){ 602 | for(var i = 0; i < metriclist.length; i++){ 603 | var chartobj = window['LineChart_'+metriclist[i]]; 604 | if(typeof chartobj === 'undefined' || chartobj === null){ continue; } 605 | chartobj.resetZoom(); 606 | } 607 | } 608 | 609 | function ToggleDragZoom(button){ 610 | var drag = true; 611 | var pan = false; 612 | var buttonvalue = ''; 613 | if(button.value.indexOf('On') != -1){ 614 | drag = false; 615 | pan = true; 616 | DragZoom = false; 617 | ChartPan = true; 618 | buttonvalue = 'Drag Zoom Off'; 619 | } 620 | else{ 621 | drag = true; 622 | pan = false; 623 | DragZoom = true; 624 | ChartPan = false; 625 | buttonvalue = 'Drag Zoom On'; 626 | } 627 | 628 | for(var i = 0; i < metriclist.length; i++){ 629 | var chartobj = window['LineChart_'+metriclist[i]]; 630 | if(typeof chartobj === 'undefined' || chartobj === null){ continue; } 631 | chartobj.options.plugins.zoom.zoom.drag = drag; 632 | chartobj.options.plugins.zoom.pan.enabled = pan; 633 | button.value = buttonvalue; 634 | chartobj.update(); 635 | } 636 | } 637 | 638 | function update_status(){ 639 | $j.ajax({ 640 | url: '/ext/ntpmerlin/detect_update.js', 641 | dataType: 'script', 642 | error: function(xhr){ 643 | setTimeout(update_status,1000); 644 | }, 645 | success: function(){ 646 | if(updatestatus == 'InProgress'){ 647 | setTimeout(update_status,1000); 648 | } 649 | else{ 650 | document.getElementById('imgChkUpdate').style.display = 'none'; 651 | showhide('ntpmerlin_version_server',true); 652 | if(updatestatus != 'None'){ 653 | $j('#ntpmerlin_version_server').text('Updated version available: '+updatestatus); 654 | showhide('btnChkUpdate',false); 655 | showhide('btnDoUpdate',true); 656 | } 657 | else{ 658 | $j('#ntpmerlin_version_server').text('No update available'); 659 | showhide('btnChkUpdate',true); 660 | showhide('btnDoUpdate',false); 661 | } 662 | } 663 | } 664 | }); 665 | } 666 | 667 | function CheckUpdate(){ 668 | showhide('btnChkUpdate',false); 669 | document.formScriptActions.action_script.value = 'start_ntpmerlincheckupdate' 670 | document.formScriptActions.submit(); 671 | document.getElementById('imgChkUpdate').style.display = ''; 672 | setTimeout(update_status,2000); 673 | } 674 | 675 | function DoUpdate(){ 676 | document.form.action_script.value = 'start_ntpmerlindoupdate'; 677 | document.form.action_wait.value = 10; 678 | showLoading(); 679 | document.form.submit(); 680 | } 681 | 682 | function update_ntpstats(){ 683 | $j.ajax({ 684 | url: '/ext/ntpmerlin/detect_ntpmerlin.js', 685 | dataType: 'script', 686 | error: function(xhr){ 687 | setTimeout(update_ntpstats,1000); 688 | }, 689 | success: function(){ 690 | if(ntpstatus == 'InProgress'){ 691 | setTimeout(update_ntpstats,1000); 692 | } 693 | else if(ntpstatus == 'GenerateCSV'){ 694 | document.getElementById('ntpupdate_text').innerHTML = 'Retrieving data for charts...'; 695 | setTimeout(update_ntpstats,1000); 696 | } 697 | else if(ntpstatus == 'Done'){ 698 | document.getElementById('ntpupdate_text').innerHTML = 'Refreshing charts...'; 699 | PostNTPUpdate(); 700 | } 701 | } 702 | }); 703 | } 704 | 705 | function PostNTPUpdate(){ 706 | currentNoCharts = 0; 707 | $j('#Time_Format').val(GetCookie('Time_Format','number')); 708 | get_statstitle_file(); 709 | setTimeout(RedrawAllCharts,3000); 710 | } 711 | 712 | function UpdateStats(){ 713 | showhide('btnUpdateStats',false); 714 | document.formScriptActions.action_script.value='start_ntpmerlin'; 715 | document.formScriptActions.submit(); 716 | document.getElementById('ntpupdate_text').innerHTML = 'Retrieving timeserver stats'; 717 | showhide('imgNTPUpdate',true); 718 | showhide('ntpupdate_text',true); 719 | setTimeout(update_ntpstats,5000); 720 | } 721 | 722 | function SaveConfig(){ 723 | document.getElementById('amng_custom').value = JSON.stringify($j('form').serializeObject()) 724 | document.form.action_script.value = 'start_ntpmerlinconfig'; 725 | document.form.action_wait.value = 10; 726 | showLoading(); 727 | document.form.submit(); 728 | } 729 | 730 | function GetVersionNumber(versiontype){ 731 | var versionprop; 732 | if(versiontype == 'local'){ 733 | versionprop = custom_settings.ntpmerlin_version_local; 734 | } 735 | else if(versiontype == 'server'){ 736 | versionprop = custom_settings.ntpmerlin_version_server; 737 | } 738 | 739 | if(typeof versionprop == 'undefined' || versionprop == null){ 740 | return 'N/A'; 741 | } 742 | else{ 743 | return versionprop; 744 | } 745 | } 746 | 747 | function get_conf_file(){ 748 | $j.ajax({ 749 | url: '/ext/ntpmerlin/config.htm', 750 | dataType: 'text', 751 | error: function(xhr){ 752 | setTimeout(get_conf_file,1000); 753 | }, 754 | success: function(data){ 755 | var configdata=data.split('\n'); 756 | configdata = configdata.filter(Boolean); 757 | 758 | for(var i = 0; i < configdata.length; i++){ 759 | eval('document.form.ntpmerlin_'+configdata[i].split('=')[0].toLowerCase()).value = configdata[i].split('=')[1].replace(/(\r\n|\n|\r)/gm,''); 760 | } 761 | } 762 | }); 763 | } 764 | 765 | 766 | function get_statstitle_file(){ 767 | $j.ajax({ 768 | url: '/ext/ntpmerlin/ntpstatstext.js', 769 | dataType: 'script', 770 | error: function(xhr){ 771 | setTimeout(get_statstitle_file,1000); 772 | }, 773 | success: function(){ 774 | SetNTPDStatsTitle(); 775 | } 776 | }); 777 | } 778 | 779 | function get_lastx_file(){ 780 | $j.ajax({ 781 | url: '/ext/ntpmerlin/lastx.htm', 782 | dataType: 'text', 783 | error: function(xhr){ 784 | setTimeout(get_lastx_file,1000); 785 | }, 786 | success: function(data){ 787 | ParseLastXData(data); 788 | } 789 | }); 790 | } 791 | 792 | function ParseLastXData(data){ 793 | var arraysortlines = data.split('\n'); 794 | arraysortlines = arraysortlines.filter(Boolean); 795 | arraysortlistlines = []; 796 | for(var i = 0; i < arraysortlines.length; i++){ 797 | try{ 798 | var resultfields = arraysortlines[i].split(','); 799 | var parsedsortline = new Object(); 800 | parsedsortline.Time = moment.unix(resultfields[0].trim()).format('YYYY-MM-DD HH:mm:ss'); 801 | parsedsortline.Offset = resultfields[1].trim(); 802 | parsedsortline.Drift = resultfields[2].trim(); 803 | arraysortlistlines.push(parsedsortline); 804 | } 805 | catch{ 806 | //do nothing,continue 807 | } 808 | } 809 | SortTable(sortname+' '+sortdir.replace('desc','↑').replace('asc','↓').trim()); 810 | } 811 | 812 | function SortTable(sorttext){ 813 | sortname = sorttext.replace('↑','').replace('↓','').trim(); 814 | var sorttype = 'number'; 815 | var sortfield = sortname; 816 | switch(sortname){ 817 | case 'Time': 818 | sorttype = 'date'; 819 | break; 820 | } 821 | 822 | if(sorttype == 'string'){ 823 | if(sorttext.indexOf('↓') == -1 && sorttext.indexOf('↑') == -1){ 824 | eval('arraysortlistlines = arraysortlistlines.sort((a,b) => (a.'+sortfield+' > b.'+sortfield+') ? 1 : ((b.'+sortfield+' > a.'+sortfield+') ? -1 : 0));'); 825 | sortdir = 'asc'; 826 | } 827 | else if(sorttext.indexOf('↓') != -1){ 828 | eval('arraysortlistlines = arraysortlistlines.sort((a,b) => (a.'+sortfield+' > b.'+sortfield+') ? 1 : ((b.'+sortfield+' > a.'+sortfield+') ? -1 : 0));'); 829 | sortdir = 'asc'; 830 | } 831 | else{ 832 | eval('arraysortlistlines = arraysortlistlines.sort((a,b) => (a.'+sortfield+' < b.'+sortfield+') ? 1 : ((b.'+sortfield+' < a.'+sortfield+') ? -1 : 0));'); 833 | sortdir = 'desc'; 834 | } 835 | } 836 | else if(sorttype == 'number'){ 837 | if(sorttext.indexOf('↓') == -1 && sorttext.indexOf('↑') == -1){ 838 | eval('arraysortlistlines = arraysortlistlines.sort((a,b) => parseFloat(a.'+sortfield+'.replace("m","000")) - parseFloat(b.'+sortfield+'.replace("m","000")));'); 839 | sortdir = 'asc'; 840 | } 841 | else if(sorttext.indexOf('↓') != -1){ 842 | eval('arraysortlistlines = arraysortlistlines.sort((a,b) => parseFloat(a.'+sortfield+'.replace("m","000")) - parseFloat(b.'+sortfield+'.replace("m","000"))); '); 843 | sortdir = 'asc'; 844 | } 845 | else{ 846 | eval('arraysortlistlines = arraysortlistlines.sort((a,b) => parseFloat(b.'+sortfield+'.replace("m","000")) - parseFloat(a.'+sortfield+'.replace("m","000")));'); 847 | sortdir = 'desc'; 848 | } 849 | } 850 | else if(sorttype == 'date'){ 851 | if(sorttext.indexOf('↓') == -1 && sorttext.indexOf('↑') == -1){ 852 | eval('arraysortlistlines = arraysortlistlines.sort((a,b) => new Date(a.'+sortfield+') - new Date(b.'+sortfield+'));'); 853 | sortdir = 'asc'; 854 | } 855 | else if(sorttext.indexOf('↓') != -1){ 856 | eval('arraysortlistlines = arraysortlistlines.sort((a,b) => new Date(a.'+sortfield+') - new Date(b.'+sortfield+'));'); 857 | sortdir = 'asc'; 858 | } 859 | else{ 860 | eval('arraysortlistlines = arraysortlistlines.sort((a,b) => new Date(b.'+sortfield+') - new Date(a.'+sortfield+'));'); 861 | sortdir = 'desc'; 862 | } 863 | } 864 | 865 | $j('#sortTableContainer').empty(); 866 | $j('#sortTableContainer').append(BuildLastXTable()); 867 | 868 | $j('.sortable').each(function(index,element){ 869 | if(element.innerHTML.replace(/ \(.*\)/,'').replace(' ','') == sortname){ 870 | if(sortdir == 'asc'){ 871 | element.innerHTML = element.innerHTML+' ↑'; 872 | } 873 | else{ 874 | element.innerHTML = element.innerHTML+' ↓'; 875 | } 876 | } 877 | }); 878 | } 879 | 880 | function BuildLastXTableNoData(){ 881 | var tablehtml=''; 882 | tablehtml += ''; 883 | tablehtml += ''; 886 | tablehtml += ''; 887 | tablehtml += '
'; 884 | tablehtml += 'Data loading...'; 885 | tablehtml += '
'; 888 | return tablehtml; 889 | } 890 | 891 | function BuildLastXTable(){ 892 | var tablehtml=''; 893 | 894 | tablehtml += ''; 895 | tablehtml += ''; 896 | tablehtml += ''; 897 | tablehtml += ''; 898 | tablehtml += ''; 899 | tablehtml += ''; 900 | tablehtml += ''; 901 | tablehtml += ''; 902 | tablehtml += ''; 903 | tablehtml += ''; 904 | tablehtml += ''; 905 | for(var i = 0; i < arraysortlistlines.length; i++){ 906 | tablehtml += ''; 907 | tablehtml += ''; 908 | tablehtml += ''; 909 | tablehtml += ''; 910 | tablehtml += ''; 911 | } 912 | 913 | tablehtml += ''; 914 | tablehtml += '
TimeOffset (ms)Drift (ppm)
'+arraysortlistlines[i].Time+''+arraysortlistlines[i].Offset+''+arraysortlistlines[i].Drift+'
'; 915 | return tablehtml; 916 | } 917 | 918 | function changeChart(e){ 919 | value = e.value * 1; 920 | name = e.id.substring(0,e.id.indexOf('_')); 921 | SetCookie(e.id,value); 922 | 923 | if(name == 'Offset'){ 924 | Draw_Chart('Offset',metriclist[0],measureunitlist[0],bordercolourlist[0],backgroundcolourlist[0]); 925 | } 926 | else if(name == 'Drift'){ 927 | Draw_Chart('Drift',metriclist[1],measureunitlist[1],bordercolourlist[1],backgroundcolourlist[1]); 928 | } 929 | } 930 | 931 | function changeAllCharts(e){ 932 | value = e.value * 1; 933 | name = e.id.substring(0,e.id.indexOf('_')); 934 | SetCookie(e.id,value); 935 | for(var i = 0; i < metriclist.length; i++){ 936 | Draw_Chart(metriclist[i],metriclist[i],measureunitlist[i],bordercolourlist[i],backgroundcolourlist[i]); 937 | } 938 | } 939 | -------------------------------------------------------------------------------- /ntpmerlin.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | ############################################################## 4 | ## _ __ __ _ _ ## 5 | ## | | | \/ | | |(_) ## 6 | ## _ __ | |_ _ __ | \ / | ___ _ __ | | _ _ __ ## 7 | ## | '_ \ | __|| '_ \ | |\/| | / _ \| '__|| || || '_ \ ## 8 | ## | | | || |_ | |_) || | | || __/| | | || || | | | ## 9 | ## |_| |_| \__|| .__/ |_| |_| \___||_| |_||_||_| |_| ## 10 | ## | | ## 11 | ## |_| ## 12 | ## ## 13 | ## https://github.com/jackyaz/ntpMerlin ## 14 | ## ## 15 | ############################################################## 16 | 17 | ############### Shellcheck directives ############# 18 | # shellcheck disable=SC2009 19 | # shellcheck disable=SC2012 20 | # shellcheck disable=SC2016 21 | # shellcheck disable=SC2018 22 | # shellcheck disable=SC2019 23 | # shellcheck disable=SC2059 24 | # shellcheck disable=SC2086 25 | # shellcheck disable=SC2155 26 | ############################################################## 27 | 28 | ### Start of script variables ### 29 | readonly SCRIPT_NAME="ntpMerlin" 30 | readonly SCRIPT_NAME_LOWER="$(echo "$SCRIPT_NAME" | tr 'A-Z' 'a-z' | sed 's/d//')" 31 | readonly SCRIPT_VERSION="v3.4.5" 32 | SCRIPT_BRANCH="master" 33 | SCRIPT_REPO="https://raw.githubusercontent.com/jackyaz/$SCRIPT_NAME/$SCRIPT_BRANCH" 34 | readonly SCRIPT_DIR="/jffs/addons/$SCRIPT_NAME_LOWER.d" 35 | readonly SCRIPT_WEBPAGE_DIR="$(readlink /www/user)" 36 | readonly SCRIPT_WEB_DIR="$SCRIPT_WEBPAGE_DIR/$SCRIPT_NAME_LOWER" 37 | readonly SHARED_DIR="/jffs/addons/shared-jy" 38 | readonly SHARED_REPO="https://raw.githubusercontent.com/jackyaz/shared-jy/master" 39 | readonly SHARED_WEB_DIR="$SCRIPT_WEBPAGE_DIR/shared-jy" 40 | [ -z "$(nvram get odmpid)" ] && ROUTER_MODEL=$(nvram get productid) || ROUTER_MODEL=$(nvram get odmpid) 41 | [ -f /opt/bin/sqlite3 ] && SQLITE3_PATH=/opt/bin/sqlite3 || SQLITE3_PATH=/usr/sbin/sqlite3 42 | ### End of script variables ### 43 | 44 | ### Start of output format variables ### 45 | readonly CRIT="\\e[41m" 46 | readonly ERR="\\e[31m" 47 | readonly WARN="\\e[33m" 48 | readonly PASS="\\e[32m" 49 | readonly BOLD="\\e[1m" 50 | readonly SETTING="${BOLD}\\e[36m" 51 | readonly CLEARFORMAT="\\e[0m" 52 | ### End of output format variables ### 53 | 54 | # $1 = print to syslog, $2 = message to print, $3 = log level 55 | Print_Output(){ 56 | if [ "$1" = "true" ]; then 57 | logger -t "$SCRIPT_NAME" "$2" 58 | fi 59 | printf "${BOLD}${3}%s${CLEARFORMAT}\\n\\n" "$2" 60 | } 61 | 62 | Firmware_Version_Check(){ 63 | if nvram get rc_support | grep -qF "am_addons"; then 64 | return 0 65 | else 66 | return 1 67 | fi 68 | } 69 | 70 | ### Code for these functions inspired by https://github.com/Adamm00 - credit to @Adamm ### 71 | Check_Lock(){ 72 | if [ -f "/tmp/$SCRIPT_NAME.lock" ]; then 73 | ageoflock=$(($(date +%s) - $(date +%s -r /tmp/$SCRIPT_NAME.lock))) 74 | if [ "$ageoflock" -gt 600 ]; then 75 | Print_Output true "Stale lock file found (>600 seconds old) - purging lock" "$ERR" 76 | kill "$(sed -n '1p' /tmp/$SCRIPT_NAME.lock)" >/dev/null 2>&1 77 | Clear_Lock 78 | echo "$$" > "/tmp/$SCRIPT_NAME.lock" 79 | return 0 80 | else 81 | Print_Output true "Lock file found (age: $ageoflock seconds) - stopping to prevent duplicate runs" "$ERR" 82 | if [ -z "$1" ]; then 83 | exit 1 84 | else 85 | return 1 86 | fi 87 | fi 88 | else 89 | echo "$$" > "/tmp/$SCRIPT_NAME.lock" 90 | return 0 91 | fi 92 | } 93 | 94 | Clear_Lock(){ 95 | rm -f "/tmp/$SCRIPT_NAME.lock" 2>/dev/null 96 | return 0 97 | } 98 | ############################################################################ 99 | 100 | Set_Version_Custom_Settings(){ 101 | SETTINGSFILE="/jffs/addons/custom_settings.txt" 102 | case "$1" in 103 | local) 104 | if [ -f "$SETTINGSFILE" ]; then 105 | if [ "$(grep -c "ntpmerlin_version_local" $SETTINGSFILE)" -gt 0 ]; then 106 | if [ "$2" != "$(grep "ntpmerlin_version_local" /jffs/addons/custom_settings.txt | cut -f2 -d' ')" ]; then 107 | sed -i "s/ntpmerlin_version_local.*/ntpmerlin_version_local $2/" "$SETTINGSFILE" 108 | fi 109 | else 110 | echo "ntpmerlin_version_local $2" >> "$SETTINGSFILE" 111 | fi 112 | else 113 | echo "ntpmerlin_version_local $2" >> "$SETTINGSFILE" 114 | fi 115 | ;; 116 | server) 117 | if [ -f "$SETTINGSFILE" ]; then 118 | if [ "$(grep -c "ntpmerlin_version_server" $SETTINGSFILE)" -gt 0 ]; then 119 | if [ "$2" != "$(grep "ntpmerlin_version_server" /jffs/addons/custom_settings.txt | cut -f2 -d' ')" ]; then 120 | sed -i "s/ntpmerlin_version_server.*/ntpmerlin_version_server $2/" "$SETTINGSFILE" 121 | fi 122 | else 123 | echo "ntpmerlin_version_server $2" >> "$SETTINGSFILE" 124 | fi 125 | else 126 | echo "ntpmerlin_version_server $2" >> "$SETTINGSFILE" 127 | fi 128 | ;; 129 | esac 130 | } 131 | 132 | Update_Check(){ 133 | echo 'var updatestatus = "InProgress";' > "$SCRIPT_WEB_DIR/detect_update.js" 134 | doupdate="false" 135 | localver=$(grep "SCRIPT_VERSION=" "/jffs/scripts/$SCRIPT_NAME_LOWER" | grep -m1 -oE 'v[0-9]{1,2}([.][0-9]{1,2})([.][0-9]{1,2})') 136 | /usr/sbin/curl -fsL --retry 3 "$SCRIPT_REPO/$SCRIPT_NAME_LOWER.sh" | grep -qF "jackyaz" || { Print_Output true "404 error detected - stopping update" "$ERR"; return 1; } 137 | serverver=$(/usr/sbin/curl -fsL --retry 3 "$SCRIPT_REPO/$SCRIPT_NAME_LOWER.sh" | grep "SCRIPT_VERSION=" | grep -m1 -oE 'v[0-9]{1,2}([.][0-9]{1,2})([.][0-9]{1,2})') 138 | if [ "$localver" != "$serverver" ]; then 139 | doupdate="version" 140 | Set_Version_Custom_Settings server "$serverver" 141 | echo 'var updatestatus = "'"$serverver"'";' > "$SCRIPT_WEB_DIR/detect_update.js" 142 | else 143 | localmd5="$(md5sum "/jffs/scripts/$SCRIPT_NAME_LOWER" | awk '{print $1}')" 144 | remotemd5="$(curl -fsL --retry 3 "$SCRIPT_REPO/$SCRIPT_NAME_LOWER.sh" | md5sum | awk '{print $1}')" 145 | if [ "$localmd5" != "$remotemd5" ]; then 146 | doupdate="md5" 147 | Set_Version_Custom_Settings server "$serverver-hotfix" 148 | echo 'var updatestatus = "'"$serverver-hotfix"'";' > "$SCRIPT_WEB_DIR/detect_update.js" 149 | fi 150 | fi 151 | if [ "$doupdate" = "false" ]; then 152 | echo 'var updatestatus = "None";' > "$SCRIPT_WEB_DIR/detect_update.js" 153 | fi 154 | echo "$doupdate,$localver,$serverver" 155 | } 156 | 157 | Update_Version(){ 158 | if [ -z "$1" ]; then 159 | updatecheckresult="$(Update_Check)" 160 | isupdate="$(echo "$updatecheckresult" | cut -f1 -d',')" 161 | localver="$(echo "$updatecheckresult" | cut -f2 -d',')" 162 | serverver="$(echo "$updatecheckresult" | cut -f3 -d',')" 163 | 164 | if [ "$isupdate" = "version" ]; then 165 | Print_Output true "New version of $SCRIPT_NAME available - $serverver" "$PASS" 166 | elif [ "$isupdate" = "md5" ]; then 167 | Print_Output true "MD5 hash of $SCRIPT_NAME does not match - hotfix available - $serverver" "$PASS" 168 | fi 169 | 170 | if [ "$isupdate" != "false" ]; then 171 | printf "\\n${BOLD}Do you want to continue with the update? (y/n)${CLEARFORMAT} " 172 | read -r confirm 173 | case "$confirm" in 174 | y|Y) 175 | printf "\\n" 176 | Update_File shared-jy.tar.gz 177 | Update_File timeserverd 178 | TIMESERVER_NAME="$(TimeServer check)" 179 | if [ "$TIMESERVER_NAME" = "ntpd" ]; then 180 | Update_File S77ntpd 181 | Update_File ntp.conf 182 | elif [ "$TIMESERVER_NAME" = "chronyd" ]; then 183 | Update_File S77chronyd 184 | Update_File chrony.conf 185 | fi 186 | 187 | Update_File ntpdstats_www.asp 188 | 189 | /usr/sbin/curl -fsL --retry 3 "$SCRIPT_REPO/$SCRIPT_NAME_LOWER.sh" -o "/jffs/scripts/$SCRIPT_NAME_LOWER" && Print_Output true "$SCRIPT_NAME successfully updated" 190 | chmod 0755 "/jffs/scripts/$SCRIPT_NAME_LOWER" 191 | Set_Version_Custom_Settings local "$serverver" 192 | Set_Version_Custom_Settings server "$serverver" 193 | Clear_Lock 194 | PressEnter 195 | exec "$0" 196 | exit 0 197 | ;; 198 | *) 199 | printf "\\n" 200 | Clear_Lock 201 | return 1 202 | ;; 203 | esac 204 | else 205 | Print_Output true "No updates available - latest is $localver" "$WARN" 206 | Clear_Lock 207 | fi 208 | fi 209 | 210 | if [ "$1" = "force" ]; then 211 | serverver=$(/usr/sbin/curl -fsL --retry 3 "$SCRIPT_REPO/$SCRIPT_NAME_LOWER.sh" | grep "SCRIPT_VERSION=" | grep -m1 -oE 'v[0-9]{1,2}([.][0-9]{1,2})([.][0-9]{1,2})') 212 | Print_Output true "Downloading latest version ($serverver) of $SCRIPT_NAME" "$PASS" 213 | Update_File shared-jy.tar.gz 214 | Update_File timeserverd 215 | TIMESERVER_NAME="$(TimeServer check)" 216 | if [ "$TIMESERVER_NAME" = "ntpd" ]; then 217 | Update_File ntp.conf 218 | Update_File S77ntpd 219 | elif [ "$TIMESERVER_NAME" = "chronyd" ]; then 220 | Update_File chrony.conf 221 | Update_File S77chronyd 222 | fi 223 | Update_File ntpdstats_www.asp 224 | /usr/sbin/curl -fsL --retry 3 "$SCRIPT_REPO/$SCRIPT_NAME_LOWER.sh" -o "/jffs/scripts/$SCRIPT_NAME_LOWER" && Print_Output true "$SCRIPT_NAME successfully updated" 225 | chmod 0755 "/jffs/scripts/$SCRIPT_NAME_LOWER" 226 | Set_Version_Custom_Settings local "$serverver" 227 | Set_Version_Custom_Settings server "$serverver" 228 | Clear_Lock 229 | if [ -z "$2" ]; then 230 | PressEnter 231 | exec "$0" 232 | elif [ "$2" = "unattended" ]; then 233 | exec "$0" postupdate 234 | fi 235 | exit 0 236 | fi 237 | } 238 | 239 | Update_File(){ 240 | if [ "$1" = "S77ntpd" ] || [ "$1" = "S77chronyd" ]; then 241 | tmpfile="/tmp/$1" 242 | Download_File "$SCRIPT_REPO/$1" "$tmpfile" 243 | if ! diff -q "$tmpfile" "/opt/etc/init.d/$1" >/dev/null 2>&1; then 244 | Print_Output true "New version of $1 downloaded" "$PASS" 245 | TimeServer_Customise 246 | fi 247 | rm -f "$tmpfile" 248 | elif [ "$1" = "ntp.conf" ] || [ "$1" = "chrony.conf" ]; then 249 | tmpfile="/tmp/$1" 250 | Download_File "$SCRIPT_REPO/$1" "$tmpfile" 251 | if [ ! -f "$SCRIPT_STORAGE_DIR/$1" ]; then 252 | Download_File "$SCRIPT_REPO/$1" "$SCRIPT_STORAGE_DIR/$1.default" 253 | Download_File "$SCRIPT_REPO/$1" "$SCRIPT_STORAGE_DIR/$1" 254 | Print_Output true "$SCRIPT_STORAGE_DIR/$1 does not exist, downloading now." "$PASS" 255 | elif [ -f "$SCRIPT_STORAGE_DIR/$1.default" ]; then 256 | if ! diff -q "$tmpfile" "$SCRIPT_STORAGE_DIR/$1.default" >/dev/null 2>&1; then 257 | Download_File "$SCRIPT_REPO/$1" "$SCRIPT_STORAGE_DIR/$1.default" 258 | Print_Output true "New default version of $1 downloaded to $SCRIPT_STORAGE_DIR/$1.default, please compare against your $SCRIPT_STORAGE_DIR/$1" "$PASS" 259 | fi 260 | else 261 | Download_File "$SCRIPT_REPO/$1" "$SCRIPT_STORAGE_DIR/$1.default" 262 | Print_Output true "$SCRIPT_STORAGE_DIR/$1.default does not exist, downloading now. Please compare against your $SCRIPT_STORAGE_DIR/$1" "$PASS" 263 | fi 264 | rm -f "$tmpfile" 265 | elif [ "$1" = "ntpdstats_www.asp" ]; then 266 | tmpfile="/tmp/$1" 267 | Download_File "$SCRIPT_REPO/$1" "$tmpfile" 268 | if [ -f "$SCRIPT_DIR/$1" ]; then 269 | if ! diff -q "$tmpfile" "$SCRIPT_DIR/$1" >/dev/null 2>&1; then 270 | Get_WebUI_Page "$SCRIPT_DIR/$1" 271 | sed -i "\\~$MyPage~d" /tmp/menuTree.js 272 | rm -f "$SCRIPT_WEBPAGE_DIR/$MyPage" 2>/dev/null 273 | Download_File "$SCRIPT_REPO/$1" "$SCRIPT_DIR/$1" 274 | Print_Output true "New version of $1 downloaded" "$PASS" 275 | Mount_WebUI 276 | fi 277 | else 278 | Download_File "$SCRIPT_REPO/$1" "$SCRIPT_DIR/$1" 279 | Print_Output true "New version of $1 downloaded" "$PASS" 280 | Mount_WebUI 281 | fi 282 | rm -f "$tmpfile" 283 | elif [ "$1" = "timeserverd" ]; then 284 | tmpfile="/tmp/$1" 285 | Download_File "$SCRIPT_REPO/$1" "$tmpfile" 286 | if ! diff -q "$tmpfile" "$SCRIPT_DIR/$1" >/dev/null 2>&1; then 287 | Download_File "$SCRIPT_REPO/$1" "$SCRIPT_DIR/$1" 288 | chmod 0755 "$SCRIPT_DIR/$1" 289 | Print_Output true "New version of $1 downloaded" "$PASS" 290 | TimeServer_Customise 291 | fi 292 | rm -f "$tmpfile" 293 | elif [ "$1" = "shared-jy.tar.gz" ]; then 294 | if [ ! -f "$SHARED_DIR/$1.md5" ]; then 295 | Download_File "$SHARED_REPO/$1" "$SHARED_DIR/$1" 296 | Download_File "$SHARED_REPO/$1.md5" "$SHARED_DIR/$1.md5" 297 | tar -xzf "$SHARED_DIR/$1" -C "$SHARED_DIR" 298 | rm -f "$SHARED_DIR/$1" 299 | Print_Output true "New version of $1 downloaded" "$PASS" 300 | else 301 | localmd5="$(cat "$SHARED_DIR/$1.md5")" 302 | remotemd5="$(curl -fsL --retry 3 "$SHARED_REPO/$1.md5")" 303 | if [ "$localmd5" != "$remotemd5" ]; then 304 | Download_File "$SHARED_REPO/$1" "$SHARED_DIR/$1" 305 | Download_File "$SHARED_REPO/$1.md5" "$SHARED_DIR/$1.md5" 306 | tar -xzf "$SHARED_DIR/$1" -C "$SHARED_DIR" 307 | rm -f "$SHARED_DIR/$1" 308 | Print_Output true "New version of $1 downloaded" "$PASS" 309 | fi 310 | fi 311 | else 312 | return 1 313 | fi 314 | } 315 | 316 | Validate_Number(){ 317 | if [ "$1" -eq "$1" ] 2>/dev/null; then 318 | return 0 319 | else 320 | return 1 321 | fi 322 | } 323 | 324 | Conf_FromSettings(){ 325 | SETTINGSFILE="/jffs/addons/custom_settings.txt" 326 | TMPFILE="/tmp/ntpmerlin_settings.txt" 327 | if [ -f "$SETTINGSFILE" ]; then 328 | if [ "$(grep "ntpmerlin_" $SETTINGSFILE | grep -v "version" -c)" -gt 0 ]; then 329 | Print_Output true "Updated settings from WebUI found, merging into $SCRIPT_CONF" "$PASS" 330 | cp -a "$SCRIPT_CONF" "$SCRIPT_CONF.bak" 331 | grep "ntpmerlin_" "$SETTINGSFILE" | grep -v "version" > "$TMPFILE" 332 | sed -i "s/ntpmerlin_//g;s/ /=/g" "$TMPFILE" 333 | while IFS='' read -r line || [ -n "$line" ]; do 334 | SETTINGNAME="$(echo "$line" | cut -f1 -d'=' | awk '{ print toupper($1) }')" 335 | SETTINGVALUE="$(echo "$line" | cut -f2 -d'=')" 336 | sed -i "s/$SETTINGNAME=.*/$SETTINGNAME=$SETTINGVALUE/" "$SCRIPT_CONF" 337 | done < "$TMPFILE" 338 | grep 'ntpmerlin_version' "$SETTINGSFILE" > "$TMPFILE" 339 | sed -i "\\~ntpmerlin_~d" "$SETTINGSFILE" 340 | mv "$SETTINGSFILE" "$SETTINGSFILE.bak" 341 | cat "$SETTINGSFILE.bak" "$TMPFILE" > "$SETTINGSFILE" 342 | rm -f "$TMPFILE" 343 | rm -f "$SETTINGSFILE.bak" 344 | 345 | ScriptStorageLocation "$(ScriptStorageLocation check)" 346 | Create_Symlinks 347 | 348 | Generate_CSVs 349 | 350 | TimeServer "$(TimeServer check)" 351 | 352 | Print_Output true "Merge of updated settings from WebUI completed successfully" "$PASS" 353 | else 354 | Print_Output false "No updated settings from WebUI found, no merge into $SCRIPT_CONF necessary" "$PASS" 355 | fi 356 | fi 357 | } 358 | 359 | Create_Dirs(){ 360 | if [ ! -d "$SCRIPT_DIR" ]; then 361 | mkdir -p "$SCRIPT_DIR" 362 | fi 363 | 364 | if [ ! -d "$SCRIPT_STORAGE_DIR" ]; then 365 | mkdir -p "$SCRIPT_STORAGE_DIR" 366 | fi 367 | 368 | if [ ! -d "$CSV_OUTPUT_DIR" ]; then 369 | mkdir -p "$CSV_OUTPUT_DIR" 370 | fi 371 | 372 | if [ ! -d "$SHARED_DIR" ]; then 373 | mkdir -p "$SHARED_DIR" 374 | fi 375 | 376 | if [ ! -d "$SCRIPT_WEBPAGE_DIR" ]; then 377 | mkdir -p "$SCRIPT_WEBPAGE_DIR" 378 | fi 379 | 380 | if [ ! -d "$SCRIPT_WEB_DIR" ]; then 381 | mkdir -p "$SCRIPT_WEB_DIR" 382 | fi 383 | } 384 | 385 | Create_Symlinks(){ 386 | rm -rf "${SCRIPT_WEB_DIR:?}/"* 2>/dev/null 387 | 388 | ln -s /tmp/detect_ntpmerlin.js "$SCRIPT_WEB_DIR/detect_ntpmerlin.js" 2>/dev/null 389 | ln -s "$SCRIPT_STORAGE_DIR/ntpstatstext.js" "$SCRIPT_WEB_DIR/ntpstatstext.js" 2>/dev/null 390 | ln -s "$SCRIPT_STORAGE_DIR/lastx.csv" "$SCRIPT_WEB_DIR/lastx.htm" 2>/dev/null 391 | 392 | ln -s "$SCRIPT_CONF" "$SCRIPT_WEB_DIR/config.htm" 2>/dev/null 393 | 394 | ln -s "$CSV_OUTPUT_DIR" "$SCRIPT_WEB_DIR/csv" 2>/dev/null 395 | 396 | if [ ! -d "$SHARED_WEB_DIR" ]; then 397 | ln -s "$SHARED_DIR" "$SHARED_WEB_DIR" 2>/dev/null 398 | fi 399 | } 400 | 401 | Conf_Exists(){ 402 | if [ -f "$SCRIPT_CONF" ]; then 403 | dos2unix "$SCRIPT_CONF" 404 | chmod 0644 "$SCRIPT_CONF" 405 | sed -i -e 's/"//g' "$SCRIPT_CONF" 406 | if grep -q "OUTPUTDATAMODE" "$SCRIPT_CONF"; then 407 | sed -i '/OUTPUTDATAMODE/d;' "$SCRIPT_CONF" 408 | fi 409 | if ! grep -q "DAYSTOKEEP" "$SCRIPT_CONF"; then 410 | echo "DAYSTOKEEP=30" >> "$SCRIPT_CONF" 411 | fi 412 | if ! grep -q "LASTXRESULTS" "$SCRIPT_CONF"; then 413 | echo "LASTXRESULTS=10" >> "$SCRIPT_CONF" 414 | fi 415 | return 0 416 | else 417 | { echo "OUTPUTTIMEMODE=unix"; echo "STORAGELOCATION=jffs"; echo "TIMESERVER=ntpd"; echo "DAYSTOKEEP=30"; echo "LASTXRESULTS=10"; } > "$SCRIPT_CONF" 418 | return 1 419 | fi 420 | } 421 | 422 | Auto_ServiceEvent(){ 423 | case $1 in 424 | create) 425 | if [ -f /jffs/scripts/service-event ]; then 426 | STARTUPLINECOUNT=$(grep -c '# '"$SCRIPT_NAME" /jffs/scripts/service-event) 427 | STARTUPLINECOUNTEX=$(grep -cx 'if echo "$2" | /bin/grep -q "'"$SCRIPT_NAME_LOWER"'"; then { /jffs/scripts/'"$SCRIPT_NAME_LOWER"' service_event "$@" & }; fi # '"$SCRIPT_NAME" /jffs/scripts/service-event) 428 | 429 | if [ "$STARTUPLINECOUNT" -gt 1 ] || { [ "$STARTUPLINECOUNTEX" -eq 0 ] && [ "$STARTUPLINECOUNT" -gt 0 ]; }; then 430 | sed -i -e '/# '"$SCRIPT_NAME"'/d' /jffs/scripts/service-event 431 | fi 432 | 433 | if [ "$STARTUPLINECOUNTEX" -eq 0 ]; then 434 | echo 'if echo "$2" | /bin/grep -q "'"$SCRIPT_NAME_LOWER"'"; then { /jffs/scripts/'"$SCRIPT_NAME_LOWER"' service_event "$@" & }; fi # '"$SCRIPT_NAME" >> /jffs/scripts/service-event 435 | fi 436 | else 437 | echo "#!/bin/sh" > /jffs/scripts/service-event 438 | echo "" >> /jffs/scripts/service-event 439 | echo 'if echo "$2" | /bin/grep -q "'"$SCRIPT_NAME_LOWER"'"; then { /jffs/scripts/'"$SCRIPT_NAME_LOWER"' service_event "$@" & }; fi # '"$SCRIPT_NAME" >> /jffs/scripts/service-event 440 | chmod 0755 /jffs/scripts/service-event 441 | fi 442 | ;; 443 | delete) 444 | if [ -f /jffs/scripts/service-event ]; then 445 | STARTUPLINECOUNT=$(grep -c '# '"$SCRIPT_NAME" /jffs/scripts/service-event) 446 | 447 | if [ "$STARTUPLINECOUNT" -gt 0 ]; then 448 | sed -i -e '/# '"$SCRIPT_NAME"'/d' /jffs/scripts/service-event 449 | fi 450 | fi 451 | ;; 452 | esac 453 | } 454 | 455 | Auto_DNSMASQ(){ 456 | case $1 in 457 | create) 458 | if [ -f /jffs/configs/dnsmasq.conf.add ]; then 459 | STARTUPLINECOUNT=$(grep -c '# '"$SCRIPT_NAME" /jffs/configs/dnsmasq.conf.add) 460 | STARTUPLINECOUNTEX=$(grep -cx "dhcp-option=lan,42,$(nvram get lan_ipaddr)"' # '"$SCRIPT_NAME" /jffs/configs/dnsmasq.conf.add) 461 | 462 | if [ "$STARTUPLINECOUNT" -gt 1 ] || { [ "$STARTUPLINECOUNTEX" -eq 0 ] && [ "$STARTUPLINECOUNT" -gt 0 ]; }; then 463 | sed -i -e '/# '"$SCRIPT_NAME"'/d' /jffs/configs/dnsmasq.conf.add 464 | fi 465 | 466 | if [ "$STARTUPLINECOUNTEX" -eq 0 ]; then 467 | echo "dhcp-option=lan,42,$(nvram get lan_ipaddr)"' # '"$SCRIPT_NAME" >> /jffs/configs/dnsmasq.conf.add 468 | service restart_dnsmasq >/dev/null 2>&1 469 | fi 470 | else 471 | echo "" >> /jffs/configs/dnsmasq.conf.add 472 | echo "dhcp-option=lan,42,$(nvram get lan_ipaddr)"' # '"$SCRIPT_NAME" >> /jffs/configs/dnsmasq.conf.add 473 | chmod 0644 /jffs/configs/dnsmasq.conf.add 474 | service restart_dnsmasq >/dev/null 2>&1 475 | fi 476 | ;; 477 | delete) 478 | if [ -f /jffs/configs/dnsmasq.conf.add ]; then 479 | STARTUPLINECOUNT=$(grep -c '# '"$SCRIPT_NAME" /jffs/configs/dnsmasq.conf.add) 480 | 481 | if [ "$STARTUPLINECOUNT" -gt 0 ]; then 482 | sed -i -e '/# '"$SCRIPT_NAME"'/d' /jffs/configs/dnsmasq.conf.add 483 | service restart_dnsmasq >/dev/null 2>&1 484 | fi 485 | fi 486 | ;; 487 | esac 488 | } 489 | 490 | Auto_Startup(){ 491 | case $1 in 492 | create) 493 | if [ -f /jffs/scripts/services-start ]; then 494 | STARTUPLINECOUNT=$(grep -c '# '"$SCRIPT_NAME" /jffs/scripts/services-start) 495 | 496 | if [ "$STARTUPLINECOUNT" -gt 0 ]; then 497 | sed -i -e '/# '"$SCRIPT_NAME"'/d' /jffs/scripts/services-start 498 | fi 499 | fi 500 | if [ -f /jffs/scripts/post-mount ]; then 501 | STARTUPLINECOUNT=$(grep -c '# '"$SCRIPT_NAME" /jffs/scripts/post-mount) 502 | STARTUPLINECOUNTEX=$(grep -cx "/jffs/scripts/$SCRIPT_NAME_LOWER startup"' "$@" & # '"$SCRIPT_NAME" /jffs/scripts/post-mount) 503 | 504 | if [ "$STARTUPLINECOUNT" -gt 1 ] || { [ "$STARTUPLINECOUNTEX" -eq 0 ] && [ "$STARTUPLINECOUNT" -gt 0 ]; }; then 505 | sed -i -e '/# '"$SCRIPT_NAME"'/d' /jffs/scripts/post-mount 506 | fi 507 | 508 | if [ "$STARTUPLINECOUNTEX" -eq 0 ]; then 509 | echo "/jffs/scripts/$SCRIPT_NAME_LOWER startup"' "$@" & # '"$SCRIPT_NAME" >> /jffs/scripts/post-mount 510 | fi 511 | else 512 | echo "#!/bin/sh" > /jffs/scripts/post-mount 513 | echo "" >> /jffs/scripts/post-mount 514 | echo "/jffs/scripts/$SCRIPT_NAME_LOWER startup"' "$@" & # '"$SCRIPT_NAME" >> /jffs/scripts/post-mount 515 | chmod 0755 /jffs/scripts/post-mount 516 | fi 517 | ;; 518 | delete) 519 | if [ -f /jffs/scripts/services-start ]; then 520 | STARTUPLINECOUNT=$(grep -c '# '"$SCRIPT_NAME" /jffs/scripts/services-start) 521 | 522 | if [ "$STARTUPLINECOUNT" -gt 0 ]; then 523 | sed -i -e '/# '"$SCRIPT_NAME"'/d' /jffs/scripts/services-start 524 | fi 525 | fi 526 | if [ -f /jffs/scripts/post-mount ]; then 527 | STARTUPLINECOUNT=$(grep -c '# '"$SCRIPT_NAME" /jffs/scripts/post-mount) 528 | 529 | if [ "$STARTUPLINECOUNT" -gt 0 ]; then 530 | sed -i -e '/# '"$SCRIPT_NAME"'/d' /jffs/scripts/post-mount 531 | fi 532 | fi 533 | ;; 534 | esac 535 | } 536 | 537 | Auto_NAT(){ 538 | case $1 in 539 | create) 540 | if [ -f /jffs/scripts/nat-start ]; then 541 | STARTUPLINECOUNT=$(grep -c '# '"$SCRIPT_NAME" /jffs/scripts/nat-start) 542 | STARTUPLINECOUNTEX=$(grep -cx "/jffs/scripts/$SCRIPT_NAME_LOWER ntpredirect"' # '"$SCRIPT_NAME" /jffs/scripts/nat-start) 543 | 544 | if [ "$STARTUPLINECOUNT" -gt 1 ] || { [ "$STARTUPLINECOUNTEX" -eq 0 ] && [ "$STARTUPLINECOUNT" -gt 0 ]; }; then 545 | sed -i -e '/# '"$SCRIPT_NAME"'/d' /jffs/scripts/nat-start 546 | fi 547 | 548 | if [ "$STARTUPLINECOUNTEX" -eq 0 ]; then 549 | echo "/jffs/scripts/$SCRIPT_NAME_LOWER ntpredirect"' # '"$SCRIPT_NAME" >> /jffs/scripts/nat-start 550 | fi 551 | else 552 | echo "#!/bin/sh" > /jffs/scripts/nat-start 553 | echo "" >> /jffs/scripts/nat-start 554 | echo "/jffs/scripts/$SCRIPT_NAME_LOWER ntpredirect"' # '"$SCRIPT_NAME" >> /jffs/scripts/nat-start 555 | chmod 0755 /jffs/scripts/nat-start 556 | fi 557 | ;; 558 | delete) 559 | if [ -f /jffs/scripts/nat-start ]; then 560 | STARTUPLINECOUNT=$(grep -c '# '"$SCRIPT_NAME" /jffs/scripts/nat-start) 561 | 562 | if [ "$STARTUPLINECOUNT" -gt 0 ]; then 563 | sed -i -e '/# '"$SCRIPT_NAME"'/d' /jffs/scripts/nat-start 564 | fi 565 | fi 566 | ;; 567 | check) 568 | if [ -f /jffs/scripts/nat-start ]; then 569 | STARTUPLINECOUNT=$(grep -c '# '"$SCRIPT_NAME" /jffs/scripts/nat-start) 570 | 571 | if [ "$STARTUPLINECOUNT" -gt 0 ]; then 572 | return 0 573 | else 574 | return 1 575 | fi 576 | else 577 | return 1 578 | fi 579 | ;; 580 | esac 581 | } 582 | 583 | Auto_Cron(){ 584 | case $1 in 585 | create) 586 | STARTUPLINECOUNT=$(cru l | grep -c "$SCRIPT_NAME") 587 | 588 | if [ "$STARTUPLINECOUNT" -eq 0 ]; then 589 | cru a "$SCRIPT_NAME" "*/10 * * * * /jffs/scripts/$SCRIPT_NAME_LOWER generate" 590 | fi 591 | ;; 592 | delete) 593 | STARTUPLINECOUNT=$(cru l | grep -c "$SCRIPT_NAME") 594 | 595 | if [ "$STARTUPLINECOUNT" -gt 0 ]; then 596 | cru d "$SCRIPT_NAME" 597 | fi 598 | ;; 599 | esac 600 | } 601 | 602 | Download_File(){ 603 | /usr/sbin/curl -fsL --retry 3 "$1" -o "$2" 604 | } 605 | 606 | NTP_Redirect(){ 607 | case $1 in 608 | create) 609 | for ACTION in -D -I; do 610 | iptables -t nat "$ACTION" PREROUTING -i br0 -p udp --dport 123 -j DNAT --to "$(nvram get lan_ipaddr)" 2>/dev/null 611 | iptables -t nat "$ACTION" PREROUTING -i br0 -p tcp --dport 123 -j DNAT --to "$(nvram get lan_ipaddr)" 2>/dev/null 612 | 613 | ## drop attempts for clients trying to avoid redirect 614 | if [ "$ACTION" = "-I" ]; then 615 | FWRDSTART="$(iptables -nvL FORWARD --line | grep -E "all.*state RELATED,ESTABLISHED" | tail -1 | awk '{print $1}')" 616 | if [ -n "$(iptables -nvL FORWARD --line | grep -E "YazFiFORWARD" | tail -1 | awk '{print $1}')" ]; then 617 | FWRDSTART="$(($(iptables -nvL FORWARD --line | grep -E "YazFiFORWARD" | tail -1 | awk '{print $1}') + 1))" 618 | fi 619 | iptables "$ACTION" FORWARD "$FWRDSTART" -i br0 -p tcp --dport 123 -j REJECT 2>/dev/null 620 | iptables "$ACTION" FORWARD "$FWRDSTART" -i br0 -p udp --dport 123 -j REJECT 2>/dev/null 621 | fi 622 | ip6tables "$ACTION" FORWARD -i br0 -p tcp --dport 123 -j REJECT 2>/dev/null 623 | ip6tables "$ACTION" FORWARD -i br0 -p udp --dport 123 -j REJECT 2>/dev/null 624 | ## 625 | done 626 | Auto_DNSMASQ create 2>/dev/null 627 | ;; 628 | delete) 629 | iptables -t nat -D PREROUTING -i br0 -p udp --dport 123 -j DNAT --to "$(nvram get lan_ipaddr)" 2>/dev/null 630 | iptables -t nat -D PREROUTING -i br0 -p tcp --dport 123 -j DNAT --to "$(nvram get lan_ipaddr)" 2>/dev/null 631 | 632 | iptables -D FORWARD -i br0 -p tcp --dport 123 -j REJECT 2>/dev/null 633 | iptables -D FORWARD -i br0 -p udp --dport 123 -j REJECT 2>/dev/null 634 | ip6tables -D FORWARD -i br0 -p tcp --dport 123 -j REJECT 2>/dev/null 635 | ip6tables -D FORWARD -i br0 -p udp --dport 123 -j REJECT 2>/dev/null 636 | 637 | Auto_DNSMASQ delete 2>/dev/null 638 | ;; 639 | esac 640 | } 641 | 642 | NTP_Firmware_Check(){ 643 | ENABLED_NTPD="$(nvram get ntpd_enable)" 644 | if ! Validate_Number "$ENABLED_NTPD"; then ENABLED_NTPD=0; fi 645 | 646 | if [ "$ENABLED_NTPD" -eq 1 ]; then 647 | Print_Output true "Built-in ntpd is enabled and will conflict, it will be disabled" "$WARN" 648 | nvram set ntpd_enable=0 649 | nvram set ntpd_server_redir=0 650 | nvram commit 651 | service restart_ntpd 652 | service restart_firewall 653 | return 1 654 | else 655 | return 0 656 | fi 657 | } 658 | 659 | Get_WebUI_Page(){ 660 | MyPage="none" 661 | for i in 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20; do 662 | page="/www/user/user$i.asp" 663 | if [ -f "$page" ] && [ "$(md5sum < "$1")" = "$(md5sum < "$page")" ]; then 664 | MyPage="user$i.asp" 665 | return 666 | elif [ "$MyPage" = "none" ] && [ ! -f "$page" ]; then 667 | MyPage="user$i.asp" 668 | fi 669 | done 670 | } 671 | 672 | ### function based on @dave14305's FlexQoS webconfigpage function ### 673 | Get_WebUI_URL(){ 674 | urlpage="" 675 | urlproto="" 676 | urldomain="" 677 | urlport="" 678 | 679 | urlpage="$(sed -nE "/$SCRIPT_NAME/ s/.*url\: \"(user[0-9]+\.asp)\".*/\1/p" /tmp/menuTree.js)" 680 | if [ "$(nvram get http_enable)" -eq 1 ]; then 681 | urlproto="https" 682 | else 683 | urlproto="http" 684 | fi 685 | if [ -n "$(nvram get lan_domain)" ]; then 686 | urldomain="$(nvram get lan_hostname).$(nvram get lan_domain)" 687 | else 688 | urldomain="$(nvram get lan_ipaddr)" 689 | fi 690 | if [ "$(nvram get ${urlproto}_lanport)" -eq 80 ] || [ "$(nvram get ${urlproto}_lanport)" -eq 443 ]; then 691 | urlport="" 692 | else 693 | urlport=":$(nvram get ${urlproto}_lanport)" 694 | fi 695 | 696 | if echo "$urlpage" | grep -qE "user[0-9]+\.asp"; then 697 | echo "${urlproto}://${urldomain}${urlport}/${urlpage}" | tr "A-Z" "a-z" 698 | else 699 | echo "WebUI page not found" 700 | fi 701 | } 702 | ### ### 703 | 704 | ### locking mechanism code credit to Martineau (@MartineauUK) ### 705 | Mount_WebUI(){ 706 | Print_Output true "Mounting WebUI tab for $SCRIPT_NAME" "$PASS" 707 | LOCKFILE=/tmp/addonwebui.lock 708 | FD=386 709 | eval exec "$FD>$LOCKFILE" 710 | flock -x "$FD" 711 | Get_WebUI_Page "$SCRIPT_DIR/ntpdstats_www.asp" 712 | if [ "$MyPage" = "none" ]; then 713 | Print_Output true "Unable to mount $SCRIPT_NAME WebUI page, exiting" "$CRIT" 714 | flock -u "$FD" 715 | return 1 716 | fi 717 | 718 | cp -f "$SCRIPT_DIR/ntpdstats_www.asp" "$SCRIPT_WEBPAGE_DIR/$MyPage" 719 | echo "$SCRIPT_NAME" > "$SCRIPT_WEBPAGE_DIR/$(echo $MyPage | cut -f1 -d'.').title" 720 | 721 | if [ "$(uname -o)" = "ASUSWRT-Merlin" ]; then 722 | if [ ! -f /tmp/index_style.css ]; then 723 | cp -f /www/index_style.css /tmp/ 724 | fi 725 | 726 | if ! grep -q '.menu_Addons' /tmp/index_style.css ; then 727 | echo ".menu_Addons { background: url(ext/shared-jy/addons.png); }" >> /tmp/index_style.css 728 | fi 729 | 730 | umount /www/index_style.css 2>/dev/null 731 | mount -o bind /tmp/index_style.css /www/index_style.css 732 | 733 | if [ ! -f /tmp/menuTree.js ]; then 734 | cp -f /www/require/modules/menuTree.js /tmp/ 735 | fi 736 | 737 | sed -i "\\~$MyPage~d" /tmp/menuTree.js 738 | 739 | if ! grep -q 'menuName: "Addons"' /tmp/menuTree.js ; then 740 | lineinsbefore="$(( $(grep -n "exclude:" /tmp/menuTree.js | cut -f1 -d':') - 1))" 741 | sed -i "$lineinsbefore"'i,\n{\nmenuName: "Addons",\nindex: "menu_Addons",\ntab: [\n{url: "javascript:var helpwindow=window.open('"'"'/ext/shared-jy/redirect.htm'"'"')", tabName: "Help & Support"},\n{url: "NULL", tabName: "__INHERIT__"}\n]\n}' /tmp/menuTree.js 742 | fi 743 | 744 | sed -i "/url: \"javascript:var helpwindow=window.open('\/ext\/shared-jy\/redirect.htm'/i {url: \"$MyPage\", tabName: \"$SCRIPT_NAME\"}," /tmp/menuTree.js 745 | 746 | umount /www/require/modules/menuTree.js 2>/dev/null 747 | mount -o bind /tmp/menuTree.js /www/require/modules/menuTree.js 748 | fi 749 | flock -u "$FD" 750 | Print_Output true "Mounted $SCRIPT_NAME WebUI page as $MyPage" "$PASS" 751 | } 752 | 753 | TimeServer_Customise(){ 754 | TIMESERVER_NAME="$(TimeServer check)" 755 | if [ -f "/opt/etc/init.d/S77$TIMESERVER_NAME" ]; then 756 | "/opt/etc/init.d/S77$TIMESERVER_NAME" stop >/dev/null 2>&1 757 | fi 758 | rm -f "/opt/etc/init.d/S77$TIMESERVER_NAME" 759 | Download_File "$SCRIPT_REPO/S77$TIMESERVER_NAME" "/opt/etc/init.d/S77$TIMESERVER_NAME" 760 | chmod +x "/opt/etc/init.d/S77$TIMESERVER_NAME" 761 | if [ "$TIMESERVER_NAME" = "chronyd" ]; then 762 | mkdir -p /opt/var/lib/chrony 763 | mkdir -p /opt/var/run/chrony 764 | chown -R nobody:nobody /opt/var/lib/chrony 765 | chown -R nobody:nobody /opt/var/run/chrony 766 | chmod -R 770 /opt/var/lib/chrony 767 | chmod -R 770 /opt/var/run/chrony 768 | fi 769 | "/opt/etc/init.d/S77$TIMESERVER_NAME" start >/dev/null 2>&1 770 | } 771 | 772 | ScriptStorageLocation(){ 773 | case "$1" in 774 | usb) 775 | TIMESERVER_NAME="$(TimeServer check)" 776 | sed -i 's/^STORAGELOCATION.*$/STORAGELOCATION=usb/' "$SCRIPT_CONF" 777 | mkdir -p "/opt/share/$SCRIPT_NAME_LOWER.d/" 778 | mv "/jffs/addons/$SCRIPT_NAME_LOWER.d/csv" "/opt/share/$SCRIPT_NAME_LOWER.d/" 2>/dev/null 779 | mv "/jffs/addons/$SCRIPT_NAME_LOWER.d/config" "/opt/share/$SCRIPT_NAME_LOWER.d/" 2>/dev/null 780 | mv "/jffs/addons/$SCRIPT_NAME_LOWER.d/config.bak" "/opt/share/$SCRIPT_NAME_LOWER.d/" 2>/dev/null 781 | mv "/jffs/addons/$SCRIPT_NAME_LOWER.d/ntpstatstext.js" "/opt/share/$SCRIPT_NAME_LOWER.d/" 2>/dev/null 782 | mv "/jffs/addons/$SCRIPT_NAME_LOWER.d/ntpdstats.db" "/opt/share/$SCRIPT_NAME_LOWER.d/" 2>/dev/null 783 | mv "/jffs/addons/$SCRIPT_NAME_LOWER.d/ntp.conf" "/opt/share/$SCRIPT_NAME_LOWER.d/" 2>/dev/null 784 | mv "/jffs/addons/$SCRIPT_NAME_LOWER.d/ntp.conf.default" "/opt/share/$SCRIPT_NAME_LOWER.d/" 2>/dev/null 785 | mv "/jffs/addons/$SCRIPT_NAME_LOWER.d/chrony.conf" "/opt/share/$SCRIPT_NAME_LOWER.d/" 2>/dev/null 786 | mv "/jffs/addons/$SCRIPT_NAME_LOWER.d/chrony.conf.default" "/opt/share/$SCRIPT_NAME_LOWER.d/" 2>/dev/null 787 | mv "/jffs/addons/$SCRIPT_NAME_LOWER.d/.chronyugraded" "/opt/share/$SCRIPT_NAME_LOWER.d/" 2>/dev/null 788 | mv "/jffs/addons/$SCRIPT_NAME_LOWER.d/.indexcreated" "/opt/share/$SCRIPT_NAME_LOWER.d/" 2>/dev/null 789 | "/opt/etc/init.d/S77$TIMESERVER_NAME" restart >/dev/null 2>&1 790 | SCRIPT_CONF="/opt/share/$SCRIPT_NAME_LOWER.d/config" 791 | ScriptStorageLocation load 792 | ;; 793 | jffs) 794 | TIMESERVER_NAME="$(TimeServer check)" 795 | sed -i 's/^STORAGELOCATION.*$/STORAGELOCATION=jffs/' "$SCRIPT_CONF" 796 | mkdir -p "/jffs/addons/$SCRIPT_NAME_LOWER.d/" 797 | mv "/opt/share/$SCRIPT_NAME_LOWER.d/csv" "/jffs/addons/$SCRIPT_NAME_LOWER.d/" 2>/dev/null 798 | mv "/opt/share/$SCRIPT_NAME_LOWER.d/config" "/jffs/addons/$SCRIPT_NAME_LOWER.d/" 2>/dev/null 799 | mv "/opt/share/$SCRIPT_NAME_LOWER.d/config.bak" "/jffs/addons/$SCRIPT_NAME_LOWER.d/" 2>/dev/null 800 | mv "/opt/share/$SCRIPT_NAME_LOWER.d/ntpstatstext.js" "/jffs/addons/$SCRIPT_NAME_LOWER.d/" 2>/dev/null 801 | mv "/opt/share/$SCRIPT_NAME_LOWER.d/ntpdstats.db" "/jffs/addons/$SCRIPT_NAME_LOWER.d/" 2>/dev/null 802 | mv "/opt/share/$SCRIPT_NAME_LOWER.d/ntp.conf" "/jffs/addons/$SCRIPT_NAME_LOWER.d/" 2>/dev/null 803 | mv "/opt/share/$SCRIPT_NAME_LOWER.d/ntp.conf.default" "/jffs/addons/$SCRIPT_NAME_LOWER.d/" 2>/dev/null 804 | mv "/opt/share/$SCRIPT_NAME_LOWER.d/chrony.conf" "/jffs/addons/$SCRIPT_NAME_LOWER.d/" 2>/dev/null 805 | mv "/opt/share/$SCRIPT_NAME_LOWER.d/chrony.conf.default" "/jffs/addons/$SCRIPT_NAME_LOWER.d/" 2>/dev/null 806 | mv "/opt/share/$SCRIPT_NAME_LOWER.d/.chronyugraded" "/jffs/addons/$SCRIPT_NAME_LOWER.d/" 2>/dev/null 807 | mv "/opt/share/$SCRIPT_NAME_LOWER.d/.indexcreated" "/jffs/addons/$SCRIPT_NAME_LOWER.d/" 2>/dev/null 808 | "/opt/etc/init.d/S77$TIMESERVER_NAME" restart >/dev/null 2>&1 809 | SCRIPT_CONF="/jffs/addons/$SCRIPT_NAME_LOWER.d/config" 810 | ScriptStorageLocation load 811 | ;; 812 | check) 813 | STORAGELOCATION=$(grep "STORAGELOCATION" "$SCRIPT_CONF" | cut -f2 -d"=") 814 | echo "$STORAGELOCATION" 815 | ;; 816 | load) 817 | STORAGELOCATION=$(grep "STORAGELOCATION" "$SCRIPT_CONF" | cut -f2 -d"=") 818 | if [ "$STORAGELOCATION" = "usb" ]; then 819 | SCRIPT_STORAGE_DIR="/opt/share/$SCRIPT_NAME_LOWER.d" 820 | elif [ "$STORAGELOCATION" = "jffs" ]; then 821 | SCRIPT_STORAGE_DIR="/jffs/addons/$SCRIPT_NAME_LOWER.d" 822 | fi 823 | 824 | CSV_OUTPUT_DIR="$SCRIPT_STORAGE_DIR/csv" 825 | ;; 826 | esac 827 | } 828 | 829 | OutputTimeMode(){ 830 | case "$1" in 831 | unix) 832 | sed -i 's/^OUTPUTTIMEMODE.*$/OUTPUTTIMEMODE=unix/' "$SCRIPT_CONF" 833 | Generate_CSVs 834 | ;; 835 | non-unix) 836 | sed -i 's/^OUTPUTTIMEMODE.*$/OUTPUTTIMEMODE=non-unix/' "$SCRIPT_CONF" 837 | Generate_CSVs 838 | ;; 839 | check) 840 | OUTPUTTIMEMODE=$(grep "OUTPUTTIMEMODE" "$SCRIPT_CONF" | cut -f2 -d"=") 841 | echo "$OUTPUTTIMEMODE" 842 | ;; 843 | esac 844 | } 845 | 846 | TimeServer(){ 847 | case "$1" in 848 | ntpd) 849 | sed -i 's/^TIMESERVER.*$/TIMESERVER=ntpd/' "$SCRIPT_CONF" 850 | /opt/etc/init.d/S77chronyd stop >/dev/null 2>&1 851 | rm -f /opt/etc/init.d/S77chronyd 852 | if [ ! -f /opt/sbin/ntpd ]; then 853 | opkg update 854 | opkg install ntp-utils 855 | opkg install ntpd 856 | fi 857 | Update_File ntp.conf >/dev/null 2>&1 858 | Update_File S77ntpd >/dev/null 2>&1 859 | ;; 860 | chronyd) 861 | sed -i 's/^TIMESERVER.*$/TIMESERVER=chronyd/' "$SCRIPT_CONF" 862 | /opt/etc/init.d/S77ntpd stop >/dev/null 2>&1 863 | rm -f /opt/etc/init.d/S77ntpd 864 | if [ ! -f /opt/sbin/chronyd ]; then 865 | opkg update 866 | if [ -n "$(opkg info chrony-nts)" ]; then 867 | opkg install chrony-nts 868 | touch "$SCRIPT_STORAGE_DIR/.chronyugraded" 869 | else 870 | opkg install chrony 871 | touch "$SCRIPT_STORAGE_DIR/.chronyugraded" 872 | fi 873 | fi 874 | Update_File chrony.conf >/dev/null 2>&1 875 | Update_File S77chronyd >/dev/null 2>&1 876 | ;; 877 | check) 878 | TIMESERVER=$(grep "TIMESERVER" "$SCRIPT_CONF" | cut -f2 -d"=") 879 | echo "$TIMESERVER" 880 | ;; 881 | esac 882 | } 883 | 884 | DaysToKeep(){ 885 | case "$1" in 886 | update) 887 | daystokeep=30 888 | exitmenu="" 889 | ScriptHeader 890 | while true; do 891 | printf "\\n${BOLD}Please enter the desired number of days\\nto keep data for (30-365 days):${CLEARFORMAT} " 892 | read -r daystokeep_choice 893 | 894 | if [ "$daystokeep_choice" = "e" ]; then 895 | exitmenu="exit" 896 | break 897 | elif ! Validate_Number "$daystokeep_choice"; then 898 | printf "\\n${ERR}Please enter a valid number (30-365)${CLEARFORMAT}\\n" 899 | elif [ "$daystokeep_choice" -lt 30 ] || [ "$daystokeep_choice" -gt 365 ]; then 900 | printf "\\n${ERR}Please enter a number between 30 and 365${CLEARFORMAT}\\n" 901 | else 902 | daystokeep="$daystokeep_choice" 903 | printf "\\n" 904 | break 905 | fi 906 | done 907 | 908 | if [ "$exitmenu" != "exit" ]; then 909 | sed -i 's/^DAYSTOKEEP.*$/DAYSTOKEEP='"$daystokeep"'/' "$SCRIPT_CONF" 910 | return 0 911 | else 912 | printf "\\n" 913 | return 1 914 | fi 915 | ;; 916 | check) 917 | DAYSTOKEEP=$(grep "DAYSTOKEEP" "$SCRIPT_CONF" | cut -f2 -d"=") 918 | echo "$DAYSTOKEEP" 919 | ;; 920 | esac 921 | } 922 | 923 | LastXResults(){ 924 | case "$1" in 925 | update) 926 | lastxresults=10 927 | exitmenu="" 928 | ScriptHeader 929 | while true; do 930 | printf "\\n${BOLD}Please enter the desired number of results\\nto display in the WebUI (1-100):${CLEARFORMAT} " 931 | read -r lastx_choice 932 | 933 | if [ "$lastx_choice" = "e" ]; then 934 | exitmenu="exit" 935 | break 936 | elif ! Validate_Number "$lastx_choice"; then 937 | printf "\\n${ERR}Please enter a valid number (1-100)${CLEARFORMAT}\\n" 938 | elif [ "$lastx_choice" -lt 1 ] || [ "$lastx_choice" -gt 100 ]; then 939 | printf "\\n${ERR}Please enter a number between 1 and 100${CLEARFORMAT}\\n" 940 | else 941 | lastxresults="$lastx_choice" 942 | printf "\\n" 943 | break 944 | fi 945 | done 946 | 947 | if [ "$exitmenu" != "exit" ]; then 948 | sed -i 's/^LASTXRESULTS.*$/LASTXRESULTS='"$lastxresults"'/' "$SCRIPT_CONF" 949 | Generate_LastXResults 950 | return 0 951 | else 952 | printf "\\n" 953 | return 1 954 | fi 955 | ;; 956 | check) 957 | LASTXRESULTS=$(grep "LASTXRESULTS" "$SCRIPT_CONF" | cut -f2 -d"=") 958 | echo "$LASTXRESULTS" 959 | ;; 960 | esac 961 | } 962 | 963 | WriteStats_ToJS(){ 964 | echo "function $3(){" > "$2" 965 | html='document.getElementById("'"$4"'").innerHTML="' 966 | while IFS='' read -r line || [ -n "$line" ]; do 967 | html="${html}${line}\\r\\n" 968 | done < "$1" 969 | html="$html"'"' 970 | printf "%s\\r\\n}\\r\\n" "$html" >> "$2" 971 | } 972 | 973 | #$1 fieldname $2 tablename $3 frequency (hours) $4 length (days) $5 outputfile $6 outputfrequency $7 sqlfile $8 timestamp 974 | WriteSql_ToFile(){ 975 | timenow="$8" 976 | maxcount="$(echo "$3" "$4" | awk '{printf ((24*$2)/$1)}')" 977 | 978 | if ! echo "$5" | grep -q "day"; then 979 | { 980 | echo ".mode csv" 981 | echo ".headers on" 982 | echo ".output ${5}_${6}.htm" 983 | echo "SELECT '$1' Metric,Min(strftime('%s',datetime(strftime('%Y-%m-%d %H:00:00',datetime([Timestamp],'unixepoch'))))) Time,IFNULL(printf('%f',Avg($1)),'NaN') Value FROM $2 WHERE ([Timestamp] >= strftime('%s',datetime($timenow,'unixepoch','-$maxcount hour'))) GROUP BY strftime('%m',datetime([Timestamp],'unixepoch')),strftime('%d',datetime([Timestamp],'unixepoch')),strftime('%H',datetime([Timestamp],'unixepoch')) ORDER BY [Timestamp] DESC;" 984 | } > "$7" 985 | else 986 | { 987 | echo ".mode csv" 988 | echo ".headers on" 989 | echo ".output ${5}_${6}.htm" 990 | echo "SELECT '$1' Metric,Max(strftime('%s',datetime([Timestamp],'unixepoch','localtime','start of day','utc'))) Time,IFNULL(printf('%f',Avg($1)),'NaN') Value FROM $2 WHERE ([Timestamp] > strftime('%s',datetime($timenow,'unixepoch','localtime','start of day','utc','+1 day','-$maxcount day'))) GROUP BY strftime('%m',datetime([Timestamp],'unixepoch','localtime')),strftime('%d',datetime([Timestamp],'unixepoch','localtime')) ORDER BY [Timestamp] DESC;" 991 | } > "$7" 992 | fi 993 | } 994 | 995 | Get_TimeServer_Stats(){ 996 | if [ ! -f /opt/bin/xargs ]; then 997 | Print_Output true "Installing findutils from Entware" 998 | opkg update 999 | opkg install findutils 1000 | fi 1001 | if [ -n "$PPID" ]; then 1002 | ps | grep -v grep | grep -v $$ | grep -v "$PPID" | grep -i "$SCRIPT_NAME" | grep generate | awk '{print $1}' | xargs kill -9 >/dev/null 2>&1 1003 | else 1004 | ps | grep -v grep | grep -v $$ | grep -i "$SCRIPT_NAME" | grep generate | awk '{print $1}' | xargs kill -9 >/dev/null 2>&1 1005 | fi 1006 | Create_Dirs 1007 | Conf_Exists 1008 | Auto_Startup create 2>/dev/null 1009 | Auto_Cron create 2>/dev/null 1010 | Auto_ServiceEvent create 2>/dev/null 1011 | NTP_Firmware_Check 1012 | ScriptStorageLocation load 1013 | Create_Symlinks 1014 | 1015 | echo 'var ntpstatus = "InProgress";' > /tmp/detect_ntpmerlin.js 1016 | 1017 | killall ntp 2>/dev/null 1018 | 1019 | TIMESERVER="$(TimeServer check)" 1020 | if [ "$TIMESERVER" = "ntpd" ]; then 1021 | tmpfile=/tmp/ntp-stats.$$ 1022 | ntpq -4 -c rv | awk 'BEGIN{ RS=","}{ print }' > "$tmpfile" 1023 | 1024 | [ -n "$(grep offset "$tmpfile" | awk 'BEGIN{FS="="}{print $2}')" ] && NOFFSET=$(grep offset "$tmpfile" | awk 'BEGIN{FS="="}{print $2}') || NOFFSET=0 1025 | [ -n "$(grep frequency "$tmpfile" | awk 'BEGIN{FS="="}{print $2}')" ] && NFREQ=$(grep frequency "$tmpfile" | awk 'BEGIN{FS="="}{print $2}') || NFREQ=0 1026 | [ -n "$(grep sys_jitter "$tmpfile" | awk 'BEGIN{FS="="}{print $2}')" ] && NSJIT=$(grep sys_jitter "$tmpfile" | awk 'BEGIN{FS="="}{print $2}') || NSJIT=0 1027 | [ -n "$(grep clk_jitter "$tmpfile" | awk 'BEGIN{FS="="}{print $2}')" ] && NCJIT=$(grep clk_jitter "$tmpfile" | awk 'BEGIN{FS="="}{print $2}') || NCJIT=0 1028 | [ -n "$(grep clk_wander "$tmpfile" | awk 'BEGIN{FS="="}{print $2}')" ] && NWANDER=$(grep clk_wander "$tmpfile" | awk 'BEGIN{FS="="}{print $2}') || NWANDER=0 1029 | [ -n "$(grep rootdisp "$tmpfile" | awk 'BEGIN{FS="="}{print $2}')" ] && NDISPER=$(grep rootdisp "$tmpfile" | awk 'BEGIN{FS="="}{print $2}') || NDISPER=0 1030 | rm -f "$tmpfile" 1031 | elif [ "$TIMESERVER" = "chronyd" ]; then 1032 | tmpfile=/tmp/chrony-stats.$$ 1033 | chronyc tracking > "$tmpfile" 1034 | 1035 | [ -n "$(grep "Last offset" "$tmpfile" | awk '{print $4}')" ] && NOFFSET=$(grep Last "$tmpfile" | awk '{print $4}') || NOFFSET=0 1036 | [ -n "$(grep Frequency "$tmpfile" | awk '{print $3}')" ] && NFREQ=$(grep Frequency "$tmpfile" | awk '{print $3}') || NFREQ=0 1037 | [ -n "$(grep System "$tmpfile" | awk '{print $4}')" ] && NSJIT=$(grep System "$tmpfile" | awk '{print $4}') || NSJIT=0 1038 | [ -n "$(grep Skew "$tmpfile" | awk '{print $3}')" ] && NWANDER=$(grep Skew "$tmpfile" | awk '{print $3}') || NWANDER=0 1039 | [ -n "$(grep dispersion "$tmpfile" | awk '{print $4}')" ] && NDISPER=$(grep dispersion "$tmpfile" | awk '{print $4}') || NDISPER=0 1040 | 1041 | NOFFSET="$(echo "$NOFFSET" | awk '{printf ($1*1000)}')" 1042 | NSJIT="$(echo "$NSJIT" | awk '{printf ($1*1000)}')" 1043 | NCJIT=0 1044 | NDISPER="$(echo "$NDISPER" | awk '{printf ($1*1000)}')" 1045 | rm -f "$tmpfile" 1046 | fi 1047 | 1048 | TZ=$(cat /etc/TZ) 1049 | export TZ 1050 | timenow=$(date +"%s") 1051 | timenowfriendly=$(date +"%c") 1052 | 1053 | Process_Upgrade 1054 | 1055 | { 1056 | echo "CREATE TABLE IF NOT EXISTS [ntpstats] ([StatID] INTEGER PRIMARY KEY NOT NULL,[Timestamp] NUMERIC NOT NULL,[Offset] REAL NOT NULL,[Frequency] REAL NOT NULL,[Sys_Jitter] REAL NOT NULL,[Clk_Jitter] REAL NOT NULL,[Clk_Wander] REAL NOT NULL,[Rootdisp] REAL NOT NULL);" 1057 | echo "INSERT INTO ntpstats ([Timestamp],[Offset],[Frequency],[Sys_Jitter],[Clk_Jitter],[Clk_Wander],[Rootdisp]) values($timenow,$NOFFSET,$NFREQ,$NSJIT,$NCJIT,$NWANDER,$NDISPER);" 1058 | } > /tmp/ntp-stats.sql 1059 | "$SQLITE3_PATH" "$SCRIPT_STORAGE_DIR/ntpdstats.db" < /tmp/ntp-stats.sql 1060 | 1061 | { 1062 | echo "DELETE FROM [ntpstats] WHERE [Timestamp] < strftime('%s',datetime($timenow,'unixepoch','-$(DaysToKeep check) day'));" 1063 | echo "PRAGMA analysis_limit=0;" 1064 | echo "PRAGMA cache_size=-20000;" 1065 | echo "ANALYZE ntpstats;" 1066 | } > /tmp/ntp-stats.sql 1067 | "$SQLITE3_PATH" "$SCRIPT_STORAGE_DIR/ntpdstats.db" < /tmp/ntp-stats.sql >/dev/null 2>&1 1068 | rm -f /tmp/ntp-stats.sql 1069 | 1070 | echo 'var ntpstatus = "GenerateCSV";' > /tmp/detect_ntpmerlin.js 1071 | 1072 | Generate_CSVs 1073 | 1074 | echo "Stats last updated: $timenowfriendly" > /tmp/ntpstatstitle.txt 1075 | WriteStats_ToJS /tmp/ntpstatstitle.txt "$SCRIPT_STORAGE_DIR/ntpstatstext.js" SetNTPDStatsTitle statstitle 1076 | rm -f /tmp/ntpstatstitle.txt 1077 | 1078 | echo 'var ntpstatus = "Done";' > /tmp/detect_ntpmerlin.js 1079 | } 1080 | 1081 | Generate_CSVs(){ 1082 | Process_Upgrade 1083 | 1084 | renice 15 $$ 1085 | 1086 | OUTPUTTIMEMODE="$(OutputTimeMode check)" 1087 | TZ=$(cat /etc/TZ) 1088 | export TZ 1089 | timenow=$(date +"%s") 1090 | timenowfriendly=$(date +"%c") 1091 | 1092 | metriclist="Offset Frequency" 1093 | 1094 | for metric in $metriclist; do 1095 | FILENAME="$metric" 1096 | if [ "$metric" = "Frequency" ]; then 1097 | FILENAME="Drift" 1098 | fi 1099 | { 1100 | echo ".mode csv" 1101 | echo ".headers on" 1102 | echo ".output $CSV_OUTPUT_DIR/${FILENAME}_raw_daily.htm" 1103 | echo "SELECT '$metric' Metric,[Timestamp] Time,printf('%f', $metric) Value FROM ntpstats WHERE ([Timestamp] >= strftime('%s',datetime($timenow,'unixepoch','-1 day'))) ORDER BY [Timestamp] DESC;" 1104 | } > /tmp/ntp-stats.sql 1105 | "$SQLITE3_PATH" "$SCRIPT_STORAGE_DIR/ntpdstats.db" < /tmp/ntp-stats.sql 1106 | 1107 | { 1108 | echo ".mode csv" 1109 | echo ".headers on" 1110 | echo ".output $CSV_OUTPUT_DIR/${FILENAME}_raw_weekly.htm" 1111 | echo "SELECT '$metric' Metric,[Timestamp] Time,printf('%f', $metric) Value FROM ntpstats WHERE ([Timestamp] >= strftime('%s',datetime($timenow,'unixepoch','-7 day'))) ORDER BY [Timestamp] DESC;" 1112 | } > /tmp/ntp-stats.sql 1113 | "$SQLITE3_PATH" "$SCRIPT_STORAGE_DIR/ntpdstats.db" < /tmp/ntp-stats.sql 1114 | 1115 | { 1116 | echo ".mode csv" 1117 | echo ".headers on" 1118 | echo ".output $CSV_OUTPUT_DIR/${FILENAME}_raw_monthly.htm" 1119 | echo "SELECT '$metric' Metric,[Timestamp] Time,printf('%f', $metric) Value FROM ntpstats WHERE ([Timestamp] >= strftime('%s',datetime($timenow,'unixepoch','-30 day'))) ORDER BY [Timestamp] DESC;" 1120 | } > /tmp/ntp-stats.sql 1121 | "$SQLITE3_PATH" "$SCRIPT_STORAGE_DIR/ntpdstats.db" < /tmp/ntp-stats.sql 1122 | 1123 | WriteSql_ToFile "$metric" ntpstats 1 1 "$CSV_OUTPUT_DIR/${FILENAME}_hour" daily /tmp/ntp-stats.sql "$timenow" 1124 | "$SQLITE3_PATH" "$SCRIPT_STORAGE_DIR/ntpdstats.db" < /tmp/ntp-stats.sql 1125 | 1126 | WriteSql_ToFile "$metric" ntpstats 1 7 "$CSV_OUTPUT_DIR/${FILENAME}_hour" weekly /tmp/ntp-stats.sql "$timenow" 1127 | "$SQLITE3_PATH" "$SCRIPT_STORAGE_DIR/ntpdstats.db" < /tmp/ntp-stats.sql 1128 | 1129 | WriteSql_ToFile "$metric" ntpstats 1 30 "$CSV_OUTPUT_DIR/${FILENAME}_hour" monthly /tmp/ntp-stats.sql "$timenow" 1130 | "$SQLITE3_PATH" "$SCRIPT_STORAGE_DIR/ntpdstats.db" < /tmp/ntp-stats.sql 1131 | 1132 | WriteSql_ToFile "$metric" ntpstats 24 1 "$CSV_OUTPUT_DIR/${FILENAME}_day" daily /tmp/ntp-stats.sql "$timenow" 1133 | "$SQLITE3_PATH" "$SCRIPT_STORAGE_DIR/ntpdstats.db" < /tmp/ntp-stats.sql 1134 | 1135 | WriteSql_ToFile "$metric" ntpstats 24 7 "$CSV_OUTPUT_DIR/${FILENAME}_day" weekly /tmp/ntp-stats.sql "$timenow" 1136 | "$SQLITE3_PATH" "$SCRIPT_STORAGE_DIR/ntpdstats.db" < /tmp/ntp-stats.sql 1137 | 1138 | WriteSql_ToFile "$metric" ntpstats 24 30 "$CSV_OUTPUT_DIR/${FILENAME}_day" monthly /tmp/ntp-stats.sql "$timenow" 1139 | "$SQLITE3_PATH" "$SCRIPT_STORAGE_DIR/ntpdstats.db" < /tmp/ntp-stats.sql 1140 | 1141 | rm -f "$CSV_OUTPUT_DIR/${FILENAME}daily.htm" 1142 | rm -f "$CSV_OUTPUT_DIR/${FILENAME}weekly.htm" 1143 | rm -f "$CSV_OUTPUT_DIR/${FILENAME}monthly.htm" 1144 | done 1145 | 1146 | rm -f /tmp/ntp-stats.sql 1147 | 1148 | Generate_LastXResults 1149 | 1150 | { 1151 | echo ".mode csv" 1152 | echo ".headers on" 1153 | echo ".output $CSV_OUTPUT_DIR/CompleteResults.htm" 1154 | echo "SELECT [Timestamp],[Offset],[Frequency],[Sys_Jitter],[Clk_Jitter],[Clk_Wander],[Rootdisp] FROM ntpstats WHERE ([Timestamp] >= strftime('%s',datetime($timenow,'unixepoch','-$(DaysToKeep check) day'))) ORDER BY [Timestamp] DESC;" 1155 | } > /tmp/ntp-complete.sql 1156 | "$SQLITE3_PATH" "$SCRIPT_STORAGE_DIR/ntpdstats.db" < /tmp/ntp-complete.sql 1157 | rm -f /tmp/ntp-complete.sql 1158 | 1159 | dos2unix "$CSV_OUTPUT_DIR/"*.htm 1160 | 1161 | tmpoutputdir="/tmp/${SCRIPT_NAME_LOWER}results" 1162 | mkdir -p "$tmpoutputdir" 1163 | mv "$CSV_OUTPUT_DIR/CompleteResults.htm" "$tmpoutputdir/CompleteResults.htm" 1164 | 1165 | if [ "$OUTPUTTIMEMODE" = "unix" ]; then 1166 | find "$tmpoutputdir/" -name '*.htm' -exec sh -c 'i="$1"; mv -- "$i" "${i%.htm}.csv"' _ {} \; 1167 | elif [ "$OUTPUTTIMEMODE" = "non-unix" ]; then 1168 | for i in "$tmpoutputdir/"*".htm"; do 1169 | awk -F"," 'NR==1 {OFS=","; print} NR>1 {OFS=","; $1=strftime("%Y-%m-%d %H:%M:%S", $1); print }' "$i" > "$i.out" 1170 | done 1171 | 1172 | find "$tmpoutputdir/" -name '*.htm.out' -exec sh -c 'i="$1"; mv -- "$i" "${i%.htm.out}.csv"' _ {} \; 1173 | rm -f "$tmpoutputdir/"*.htm 1174 | fi 1175 | 1176 | mv "$tmpoutputdir/CompleteResults.csv" "$CSV_OUTPUT_DIR/CompleteResults.htm" 1177 | rm -f "$CSV_OUTPUT_DIR/ntpmerlindata.zip" 1178 | rm -rf "$tmpoutputdir" 1179 | 1180 | renice 0 $$ 1181 | } 1182 | 1183 | Generate_LastXResults(){ 1184 | rm -f "$SCRIPT_STORAGE_DIR/lastx.htm" 1185 | { 1186 | echo ".mode csv" 1187 | echo ".output /tmp/ntp-lastx.csv" 1188 | echo "SELECT [Timestamp],[Offset],[Frequency] FROM ntpstats ORDER BY [Timestamp] DESC LIMIT $(LastXResults check);" 1189 | } > /tmp/ntp-lastx.sql 1190 | "$SQLITE3_PATH" "$SCRIPT_STORAGE_DIR/ntpdstats.db" < /tmp/ntp-lastx.sql 1191 | rm -f /tmp/ntp-lastx.sql 1192 | sed -i 's/"//g' /tmp/ntp-lastx.csv 1193 | mv /tmp/ntp-lastx.csv "$SCRIPT_STORAGE_DIR/lastx.csv" 1194 | } 1195 | 1196 | Reset_DB(){ 1197 | SIZEAVAIL="$(df -P -k "$SCRIPT_STORAGE_DIR" | awk '{print $4}' | tail -n 1)" 1198 | SIZEDB="$(ls -l "$SCRIPT_STORAGE_DIR/ntpdstats.db" | awk '{print $5}')" 1199 | if [ "$SIZEDB" -gt "$((SIZEAVAIL*1024))" ]; then 1200 | Print_Output true "Database size exceeds available space. $(ls -lh "$SCRIPT_STORAGE_DIR/ntpdstats.db" | awk '{print $5}')B is required to create backup." "$ERR" 1201 | return 1 1202 | else 1203 | Print_Output true "Sufficient free space to back up database, proceeding..." "$PASS" 1204 | if ! cp -a "$SCRIPT_STORAGE_DIR/ntpdstats.db" "$SCRIPT_STORAGE_DIR/ntpdstats.db.bak"; then 1205 | Print_Output true "Database backup failed, please check storage device" "$WARN" 1206 | fi 1207 | 1208 | echo "DELETE FROM [ntpstats];" > /tmp/ntpmerlin-stats.sql 1209 | "$SQLITE3_PATH" "$SCRIPT_STORAGE_DIR/ntpdstats.db" < /tmp/ntpmerlin-stats.sql 1210 | rm -f /tmp/ntpmerlin-stats.sql 1211 | 1212 | Print_Output true "Database reset complete" "$WARN" 1213 | fi 1214 | } 1215 | 1216 | Shortcut_Script(){ 1217 | case $1 in 1218 | create) 1219 | if [ -d /opt/bin ] && [ ! -f "/opt/bin/$SCRIPT_NAME_LOWER" ] && [ -f "/jffs/scripts/$SCRIPT_NAME_LOWER" ]; then 1220 | ln -s "/jffs/scripts/$SCRIPT_NAME_LOWER" /opt/bin 1221 | chmod 0755 "/opt/bin/$SCRIPT_NAME_LOWER" 1222 | fi 1223 | ;; 1224 | delete) 1225 | if [ -f "/opt/bin/$SCRIPT_NAME_LOWER" ]; then 1226 | rm -f "/opt/bin/$SCRIPT_NAME_LOWER" 1227 | fi 1228 | ;; 1229 | esac 1230 | } 1231 | 1232 | Process_Upgrade(){ 1233 | rm -f "$SCRIPT_STORAGE_DIR/.tableupgraded" 1234 | if [ ! -f "$SCRIPT_STORAGE_DIR/.chronyugraded" ]; then 1235 | if [ "$(TimeServer check)" = "chronyd" ]; then 1236 | Print_Output true "Checking if chrony-nts is available for your router..." "$PASS" 1237 | opkg update >/dev/null 2>&1 1238 | if [ -n "$(opkg info chrony-nts)" ]; then 1239 | Print_Output true "chrony-nts is available, replacing chrony with chrony-nts..." "$PASS" 1240 | /opt/etc/init.d/S77chronyd stop >/dev/null 2>&1 1241 | rm -f /opt/etc/init.d/S77chronyd 1242 | opkg remove chrony >/dev/null 2>&1 1243 | opkg install chrony-nts >/dev/null 2>&1 1244 | Update_File chrony.conf >/dev/null 2>&1 1245 | Update_File S77chronyd >/dev/null 2>&1 1246 | else 1247 | Print_Output true "chrony-nts not found in Entware for your router" "$WARN" 1248 | fi 1249 | touch "$SCRIPT_STORAGE_DIR/.chronyugraded" 1250 | fi 1251 | fi 1252 | if [ ! -f "$SCRIPT_STORAGE_DIR/.indexcreated" ]; then 1253 | renice 15 $$ 1254 | Print_Output true "Creating database table indexes..." "$PASS" 1255 | echo "CREATE INDEX IF NOT EXISTS idx_time_offset ON ntpstats (Timestamp,Offset);" > /tmp/ntp-upgrade.sql 1256 | while ! "$SQLITE3_PATH" "$SCRIPT_STORAGE_DIR/ntpdstats.db" < /tmp/ntp-upgrade.sql >/dev/null 2>&1; do 1257 | sleep 1 1258 | done 1259 | echo "CREATE INDEX IF NOT EXISTS idx_time_frequency ON ntpstats (Timestamp,Frequency);" > /tmp/ntp-upgrade.sql 1260 | while ! "$SQLITE3_PATH" "$SCRIPT_STORAGE_DIR/ntpdstats.db" < /tmp/ntp-upgrade.sql >/dev/null 2>&1; do 1261 | sleep 1 1262 | done 1263 | rm -f /tmp/ntp-upgrade.sql 1264 | touch "$SCRIPT_STORAGE_DIR/.indexcreated" 1265 | Print_Output true "Database ready, continuing..." "$PASS" 1266 | renice 0 $$ 1267 | fi 1268 | if [ ! -f "$SCRIPT_STORAGE_DIR/lastx.csv" ]; then 1269 | Generate_LastXResults 1270 | fi 1271 | } 1272 | 1273 | PressEnter(){ 1274 | while true; do 1275 | printf "Press enter to continue..." 1276 | read -r key 1277 | case "$key" in 1278 | *) 1279 | break 1280 | ;; 1281 | esac 1282 | done 1283 | return 0 1284 | } 1285 | 1286 | ScriptHeader(){ 1287 | clear 1288 | DST_ENABLED="$(nvram get time_zone_dst)" 1289 | if ! Validate_Number "$DST_ENABLED"; then DST_ENABLED=0; fi 1290 | if [ "$DST_ENABLED" -eq 0 ]; then 1291 | DST_ENABLED="Inactive" 1292 | else 1293 | DST_ENABLED="Active" 1294 | fi 1295 | 1296 | DST_SETTING="$(nvram get time_zone_dstoff)" 1297 | DST_SETTING="$(echo "$DST_SETTING" | sed 's/M//g')" 1298 | DST_START="$(echo "$DST_SETTING" | cut -f1 -d",")" 1299 | DST_START="Month $(echo "$DST_START" | cut -f1 -d".") Week $(echo "$DST_START" | cut -f2 -d".") Weekday $(echo "$DST_START" | cut -f3 -d"." | cut -f1 -d"/") Hour $(echo "$DST_START" | cut -f3 -d"." | cut -f2 -d"/")" 1300 | DST_END="$(echo "$DST_SETTING" | cut -f2 -d",")" 1301 | DST_END="Month $(echo "$DST_END" | cut -f1 -d".") Week $(echo "$DST_END" | cut -f2 -d".") Weekday $(echo "$DST_END" | cut -f3 -d"." | cut -f1 -d"/") Hour $(echo "$DST_END" | cut -f3 -d"." | cut -f2 -d"/")" 1302 | 1303 | printf "\\n" 1304 | printf "${BOLD}##############################################################${CLEARFORMAT}\\n" 1305 | printf "${BOLD}## ##${CLEARFORMAT}\\n" 1306 | printf "${BOLD}## _ __ __ _ _ ##${CLEARFORMAT}\\n" 1307 | printf "${BOLD}## | | | \/ | | |(_) ##${CLEARFORMAT}\\n" 1308 | printf "${BOLD}## _ __ | |_ _ __ | \ / | ___ _ __ | | _ _ __ ##${CLEARFORMAT}\\n" 1309 | printf "${BOLD}## | '_ \ | __|| '_ \ | |\/| | / _ \| '__|| || || '_ \ ##${CLEARFORMAT}\\n" 1310 | printf "${BOLD}## | | | || |_ | |_) || | | || __/| | | || || | | | ##${CLEARFORMAT}\\n" 1311 | printf "${BOLD}## |_| |_| \__|| .__/ |_| |_| \___||_| |_||_||_| |_| ##${CLEARFORMAT}\\n" 1312 | printf "${BOLD}## | | ##${CLEARFORMAT}\\n" 1313 | printf "${BOLD}## |_| ##${CLEARFORMAT}\\n" 1314 | printf "${BOLD}## ##${CLEARFORMAT}\\n" 1315 | printf "${BOLD}## %s on %-11s ##${CLEARFORMAT}\\n" "$SCRIPT_VERSION" "$ROUTER_MODEL" 1316 | printf "${BOLD}## ##${CLEARFORMAT}\\n" 1317 | printf "${BOLD}## https://github.com/jackyaz/ntpMerlin ##${CLEARFORMAT}\\n" 1318 | printf "${BOLD}## ##${CLEARFORMAT}\\n" 1319 | printf "${BOLD}## DST is currently %-8s ##${CLEARFORMAT}\\n" "$DST_ENABLED" 1320 | printf "${BOLD}## ##${CLEARFORMAT}\\n" 1321 | printf "${BOLD}## DST starts on %-33s ##${CLEARFORMAT}\\n" "$DST_START" 1322 | printf "${BOLD}## DST ends on %-33s ##${CLEARFORMAT}\\n" "$DST_END" 1323 | printf "${BOLD}## ##${CLEARFORMAT}\\n" 1324 | printf "${BOLD}##############################################################${CLEARFORMAT}\\n" 1325 | printf "\\n" 1326 | } 1327 | 1328 | MainMenu(){ 1329 | NTP_REDIRECT_ENABLED="" 1330 | if Auto_NAT check; then 1331 | NTP_REDIRECT_ENABLED="Enabled" 1332 | else 1333 | NTP_REDIRECT_ENABLED="Disabled" 1334 | fi 1335 | TIMESERVER_NAME_MENU="$(TimeServer check)" 1336 | CONFFILE_MENU="" 1337 | if [ "$TIMESERVER_NAME_MENU" = "ntpd" ]; then 1338 | CONFFILE_MENU="$SCRIPT_STORAGE_DIR/ntp.conf" 1339 | elif [ "$TIMESERVER_NAME_MENU" = "chronyd" ]; then 1340 | CONFFILE_MENU="$SCRIPT_STORAGE_DIR/chrony.conf" 1341 | fi 1342 | 1343 | printf "WebUI for %s is available at:\\n${SETTING}%s${CLEARFORMAT}\\n\\n" "$SCRIPT_NAME" "$(Get_WebUI_URL)" 1344 | printf "1. Update timeserver stats now\\n\\n" 1345 | printf "2. Toggle redirect of all NTP traffic to %s\\n Currently: ${SETTING}%s${CLEARFORMAT}\\n\\n" "$SCRIPT_NAME" "$NTP_REDIRECT_ENABLED" 1346 | printf "3. Edit ${SETTING}%s${CLEARFORMAT} config\\n\\n" "$(TimeServer check)" 1347 | printf "4. Toggle time output mode\\n Currently ${SETTING}%s${CLEARFORMAT} time values will be used for CSV exports\\n\\n" "$(OutputTimeMode check)" 1348 | printf "5. Set number of timeserver stats to show in WebUI\\n Currently: ${SETTING}%s results will be shown${CLEARFORMAT}\\n\\n" "$(LastXResults check)" 1349 | printf "6. Set number of days data to keep in database\\n Currently: ${SETTING}%s days data will be kept${CLEARFORMAT}\\n\\n" "$(DaysToKeep check)" 1350 | printf "s. Toggle storage location for stats and config\\n Current location is ${SETTING}%s${CLEARFORMAT} \\n\\n" "$(ScriptStorageLocation check)" 1351 | printf "t. Switch timeserver between ntpd and chronyd\\n Currently using ${SETTING}%s${CLEARFORMAT}\\n Config location: ${SETTING}%s${CLEARFORMAT}\\n\\n" "$(TimeServer check)" "$CONFFILE_MENU" 1352 | printf "r. Restart ${SETTING}%s${CLEARFORMAT}\\n\\n" "$(TimeServer check)" 1353 | printf "u. Check for updates\\n" 1354 | printf "uf. Update %s with latest version (force update)\\n\\n" "$SCRIPT_NAME" 1355 | printf "rs. Reset %s database / delete all data\\n\\n" "$SCRIPT_NAME" 1356 | printf "e. Exit %s\\n\\n" "$SCRIPT_NAME" 1357 | printf "z. Uninstall %s\\n" "$SCRIPT_NAME" 1358 | printf "\\n" 1359 | printf "${BOLD}##############################################################${CLEARFORMAT}\\n" 1360 | printf "\\n" 1361 | 1362 | while true; do 1363 | printf "Choose an option: " 1364 | read -r menu 1365 | case "$menu" in 1366 | 1) 1367 | printf "\\n" 1368 | if Check_Lock menu; then 1369 | Get_TimeServer_Stats 1370 | Clear_Lock 1371 | fi 1372 | PressEnter 1373 | break 1374 | ;; 1375 | 2) 1376 | printf "\\n" 1377 | if Auto_NAT check; then 1378 | Auto_NAT delete 1379 | NTP_Redirect delete 1380 | printf "${BOLD}NTP Redirect has been disabled${CLEARFORMAT}\\n\\n" 1381 | else 1382 | Auto_NAT create 1383 | NTP_Redirect create 1384 | printf "${BOLD}NTP Redirect has been enabled${CLEARFORMAT}\\n\\n" 1385 | fi 1386 | PressEnter 1387 | break 1388 | ;; 1389 | 3) 1390 | printf "\\n" 1391 | if Check_Lock menu; then 1392 | Menu_Edit 1393 | fi 1394 | PressEnter 1395 | break 1396 | ;; 1397 | 4) 1398 | printf "\\n" 1399 | if [ "$(OutputTimeMode check)" = "unix" ]; then 1400 | OutputTimeMode non-unix 1401 | elif [ "$(OutputTimeMode check)" = "non-unix" ]; then 1402 | OutputTimeMode unix 1403 | fi 1404 | break 1405 | ;; 1406 | 5) 1407 | printf "\\n" 1408 | LastXResults update 1409 | PressEnter 1410 | break 1411 | ;; 1412 | 6) 1413 | printf "\\n" 1414 | DaysToKeep update 1415 | PressEnter 1416 | break 1417 | ;; 1418 | s) 1419 | printf "\\n" 1420 | if [ "$(ScriptStorageLocation check)" = "jffs" ]; then 1421 | ScriptStorageLocation usb 1422 | Create_Symlinks 1423 | elif [ "$(ScriptStorageLocation check)" = "usb" ]; then 1424 | ScriptStorageLocation jffs 1425 | Create_Symlinks 1426 | fi 1427 | break 1428 | ;; 1429 | t) 1430 | printf "\\n" 1431 | if Check_Lock menu; then 1432 | if [ "$(TimeServer check)" = "ntpd" ]; then 1433 | TimeServer chronyd 1434 | elif [ "$(TimeServer check)" = "chronyd" ]; then 1435 | TimeServer ntpd 1436 | fi 1437 | Clear_Lock 1438 | fi 1439 | PressEnter 1440 | break 1441 | ;; 1442 | r) 1443 | printf "\\n" 1444 | TIMESERVER_NAME="$(TimeServer check)" 1445 | Print_Output true "Restarting $TIMESERVER_NAME..." "$PASS" 1446 | "/opt/etc/init.d/S77$TIMESERVER_NAME" restart >/dev/null 2>&1 1447 | PressEnter 1448 | break 1449 | ;; 1450 | u) 1451 | printf "\\n" 1452 | if Check_Lock menu; then 1453 | Update_Version 1454 | Clear_Lock 1455 | fi 1456 | PressEnter 1457 | break 1458 | ;; 1459 | uf) 1460 | printf "\\n" 1461 | if Check_Lock menu; then 1462 | Update_Version force 1463 | Clear_Lock 1464 | fi 1465 | PressEnter 1466 | break 1467 | ;; 1468 | rs) 1469 | printf "\\n" 1470 | if Check_Lock menu; then 1471 | Menu_ResetDB 1472 | Clear_Lock 1473 | fi 1474 | PressEnter 1475 | break 1476 | ;; 1477 | e) 1478 | ScriptHeader 1479 | printf "\\n${BOLD}Thanks for using %s!${CLEARFORMAT}\\n\\n\\n" "$SCRIPT_NAME" 1480 | exit 0 1481 | ;; 1482 | z) 1483 | while true; do 1484 | printf "\\n${BOLD}Are you sure you want to uninstall %s? (y/n)${CLEARFORMAT} " "$SCRIPT_NAME" 1485 | read -r confirm 1486 | case "$confirm" in 1487 | y|Y) 1488 | Menu_Uninstall 1489 | exit 0 1490 | ;; 1491 | *) 1492 | break 1493 | ;; 1494 | esac 1495 | done 1496 | ;; 1497 | *) 1498 | printf "\\nPlease choose a valid option\\n\\n" 1499 | ;; 1500 | esac 1501 | done 1502 | 1503 | ScriptHeader 1504 | MainMenu 1505 | } 1506 | 1507 | Check_Requirements(){ 1508 | CHECKSFAILED="false" 1509 | 1510 | if [ "$(nvram get jffs2_scripts)" -ne 1 ]; then 1511 | nvram set jffs2_scripts=1 1512 | nvram commit 1513 | Print_Output true "Custom JFFS Scripts enabled" "$WARN" 1514 | fi 1515 | 1516 | if [ ! -f /opt/bin/opkg ]; then 1517 | Print_Output false "Entware not detected!" "$ERR" 1518 | CHECKSFAILED="true" 1519 | fi 1520 | 1521 | if ! Firmware_Version_Check install ; then 1522 | Print_Output false "Unsupported firmware version detected" "$ERR" 1523 | Print_Output false "$SCRIPT_NAME requires Merlin 384.15/384.13_4 or Fork 43E5 (or later)" "$ERR" 1524 | CHECKSFAILED="true" 1525 | fi 1526 | 1527 | NTP_Firmware_Check 1528 | 1529 | if [ "$CHECKSFAILED" = "false" ]; then 1530 | Print_Output false "Installing required packages from Entware" "$PASS" 1531 | opkg update 1532 | opkg install sqlite3-cli 1533 | opkg install ntp-utils 1534 | opkg install ntpd 1535 | opkg install findutils 1536 | return 0 1537 | else 1538 | return 1 1539 | fi 1540 | } 1541 | 1542 | Menu_Install(){ 1543 | ScriptHeader 1544 | Print_Output true "Welcome to $SCRIPT_NAME $SCRIPT_VERSION, a script by JackYaz" 1545 | sleep 1 1546 | 1547 | Print_Output false "Checking your router meets the requirements for $SCRIPT_NAME" 1548 | 1549 | if ! Check_Requirements; then 1550 | Print_Output false "Requirements for $SCRIPT_NAME not met, please see above for the reason(s)" "$CRIT" 1551 | PressEnter 1552 | Clear_Lock 1553 | rm -f "/jffs/scripts/$SCRIPT_NAME_LOWER" 2>/dev/null 1554 | exit 1 1555 | fi 1556 | 1557 | Create_Dirs 1558 | Conf_Exists 1559 | Set_Version_Custom_Settings local "$SCRIPT_VERSION" 1560 | Set_Version_Custom_Settings server "$SCRIPT_VERSION" 1561 | ScriptStorageLocation load 1562 | Create_Symlinks 1563 | 1564 | Update_File ntp.conf 1565 | Update_File ntpdstats_www.asp 1566 | Update_File shared-jy.tar.gz 1567 | Update_File timeserverd 1568 | 1569 | Auto_Startup create 2>/dev/null 1570 | Auto_Cron create 2>/dev/null 1571 | Auto_ServiceEvent create 2>/dev/null 1572 | Shortcut_Script create 1573 | TimeServer_Customise 1574 | 1575 | echo "CREATE TABLE IF NOT EXISTS [ntpstats] ([StatID] INTEGER PRIMARY KEY NOT NULL,[Timestamp] NUMERIC NOT NULL,[Offset] REAL NOT NULL,[Frequency] REAL NOT NULL,[Sys_Jitter] REAL NOT NULL,[Clk_Jitter] REAL NOT NULL,[Clk_Wander] REAL NOT NULL,[Rootdisp] REAL NOT NULL);" > /tmp/ntp-stats.sql 1576 | "$SQLITE3_PATH" "$SCRIPT_STORAGE_DIR/ntpdstats.db" < /tmp/ntp-stats.sql 1577 | rm -f /tmp/ntp-stats.sql 1578 | touch "$SCRIPT_STORAGE_DIR/lastx.csv" 1579 | Process_Upgrade 1580 | 1581 | Get_TimeServer_Stats 1582 | Clear_Lock 1583 | 1584 | ScriptHeader 1585 | MainMenu 1586 | } 1587 | 1588 | Menu_Startup(){ 1589 | if [ -z "$1" ]; then 1590 | Print_Output true "Missing argument for startup, not starting $SCRIPT_NAME" "$WARN" 1591 | exit 1 1592 | elif [ "$1" != "force" ]; then 1593 | if [ ! -f "$1/entware/bin/opkg" ]; then 1594 | Print_Output true "$1 does not contain Entware, not starting $SCRIPT_NAME" "$WARN" 1595 | exit 1 1596 | else 1597 | Print_Output true "$1 contains Entware, starting $SCRIPT_NAME" "$WARN" 1598 | fi 1599 | fi 1600 | 1601 | NTP_Ready 1602 | 1603 | Check_Lock 1604 | 1605 | if [ "$1" != "force" ]; then 1606 | sleep 7 1607 | fi 1608 | Create_Dirs 1609 | Conf_Exists 1610 | ScriptStorageLocation load 1611 | Create_Symlinks 1612 | Auto_Startup create 2>/dev/null 1613 | Auto_Cron create 2>/dev/null 1614 | Auto_ServiceEvent create 2>/dev/null 1615 | NTP_Firmware_Check 1616 | Shortcut_Script create 1617 | Mount_WebUI 1618 | Clear_Lock 1619 | } 1620 | 1621 | Menu_Edit(){ 1622 | texteditor="" 1623 | exitmenu="false" 1624 | 1625 | printf "\\n${BOLD}A choice of text editors is available:${CLEARFORMAT}\\n" 1626 | printf "1. nano (recommended for beginners)\\n" 1627 | printf "2. vi\\n" 1628 | printf "\\ne. Exit to main menu\\n" 1629 | 1630 | while true; do 1631 | printf "\\n${BOLD}Choose an option:${CLEARFORMAT} " 1632 | read -r editor 1633 | case "$editor" in 1634 | 1) 1635 | texteditor="nano -K" 1636 | break 1637 | ;; 1638 | 2) 1639 | texteditor="vi" 1640 | break 1641 | ;; 1642 | e) 1643 | exitmenu="true" 1644 | break 1645 | ;; 1646 | *) 1647 | printf "\\nPlease choose a valid option\\n\\n" 1648 | ;; 1649 | esac 1650 | done 1651 | 1652 | if [ "$exitmenu" != "true" ]; then 1653 | TIMESERVER_NAME="$(TimeServer check)" 1654 | CONFFILE="" 1655 | if [ "$TIMESERVER_NAME" = "ntpd" ]; then 1656 | CONFFILE="$SCRIPT_STORAGE_DIR/ntp.conf" 1657 | elif [ "$TIMESERVER_NAME" = "chronyd" ]; then 1658 | CONFFILE="$SCRIPT_STORAGE_DIR/chrony.conf" 1659 | fi 1660 | oldmd5="$(md5sum "$CONFFILE" | awk '{print $1}')" 1661 | $texteditor "$CONFFILE" 1662 | newmd5="$(md5sum "$CONFFILE" | awk '{print $1}')" 1663 | if [ "$oldmd5" != "$newmd5" ]; then 1664 | "/opt/etc/init.d/S77$TIMESERVER_NAME" restart >/dev/null 2>&1 1665 | fi 1666 | fi 1667 | Clear_Lock 1668 | } 1669 | 1670 | Menu_ResetDB(){ 1671 | printf "${BOLD}\\e[33mWARNING: This will reset the %s database by deleting all database records.\\n" "$SCRIPT_NAME" 1672 | printf "A backup of the database will be created if you change your mind.${CLEARFORMAT}\\n" 1673 | printf "\\n${BOLD}Do you want to continue? (y/n)${CLEARFORMAT} " 1674 | read -r confirm 1675 | case "$confirm" in 1676 | y|Y) 1677 | printf "\\n" 1678 | Reset_DB 1679 | ;; 1680 | *) 1681 | printf "\\n${BOLD}\\e[33mDatabase reset cancelled${CLEARFORMAT}\\n\\n" 1682 | ;; 1683 | esac 1684 | } 1685 | 1686 | Menu_Uninstall(){ 1687 | if [ -n "$PPID" ]; then 1688 | ps | grep -v grep | grep -v $$ | grep -v "$PPID" | grep -i "$SCRIPT_NAME" | grep generate | awk '{print $1}' | xargs kill -9 >/dev/null 2>&1 1689 | else 1690 | ps | grep -v grep | grep -v $$ | grep -i "$SCRIPT_NAME" | grep generate | awk '{print $1}' | xargs kill -9 >/dev/null 2>&1 1691 | fi 1692 | Print_Output true "Removing $SCRIPT_NAME..." "$PASS" 1693 | Auto_Startup delete 2>/dev/null 1694 | Auto_Cron delete 2>/dev/null 1695 | Auto_ServiceEvent delete 2>/dev/null 1696 | Auto_NAT delete 1697 | NTP_Redirect delete 1698 | 1699 | LOCKFILE=/tmp/addonwebui.lock 1700 | FD=386 1701 | eval exec "$FD>$LOCKFILE" 1702 | flock -x "$FD" 1703 | Get_WebUI_Page "$SCRIPT_DIR/ntpdstats_www.asp" 1704 | if [ -n "$MyPage" ] && [ "$MyPage" != "none" ] && [ -f "/tmp/menuTree.js" ]; then 1705 | sed -i "\\~$MyPage~d" /tmp/menuTree.js 1706 | umount /www/require/modules/menuTree.js 1707 | mount -o bind /tmp/menuTree.js /www/require/modules/menuTree.js 1708 | rm -f "$SCRIPT_WEBPAGE_DIR/$MyPage" 1709 | rm -f "$SCRIPT_WEBPAGE_DIR/$(echo $MyPage | cut -f1 -d'.').title" 1710 | fi 1711 | flock -u "$FD" 1712 | rm -f "$SCRIPT_DIR/ntpdstats_www.asp" 2>/dev/null 1713 | rm -rf "$SCRIPT_WEB_DIR" 2>/dev/null 1714 | 1715 | Shortcut_Script delete 1716 | TIMESERVER_NAME="$(TimeServer check)" 1717 | "/opt/etc/init.d/S77$TIMESERVER_NAME" stop >/dev/null 2>&1 1718 | opkg remove --autoremove ntpd 1719 | opkg remove --autoremove ntp-utils 1720 | opkg remove --autoremove chrony 1721 | 1722 | rm -f /opt/etc/init.d/S77ntpd 1723 | rm -f /opt/etc/init.d/S77chronyd 1724 | 1725 | SETTINGSFILE="/jffs/addons/custom_settings.txt" 1726 | sed -i '/ntpmerlin_version_local/d' "$SETTINGSFILE" 1727 | sed -i '/ntpmerlin_version_server/d' "$SETTINGSFILE" 1728 | 1729 | printf "\\n${BOLD}Do you want to delete %s configuration file and stats? (y/n)${CLEARFORMAT} " "$SCRIPT_NAME" 1730 | read -r confirm 1731 | case "$confirm" in 1732 | y|Y) 1733 | rm -rf "$SCRIPT_DIR" 2>/dev/null 1734 | rm -rf "$SCRIPT_STORAGE_DIR" 2>/dev/null 1735 | ;; 1736 | *) 1737 | : 1738 | ;; 1739 | esac 1740 | 1741 | rm -f "/jffs/scripts/$SCRIPT_NAME_LOWER" 2>/dev/null 1742 | Clear_Lock 1743 | Print_Output true "Uninstall completed" "$PASS" 1744 | } 1745 | 1746 | NTP_Ready(){ 1747 | if [ "$(nvram get ntp_ready)" -eq 0 ]; then 1748 | Check_Lock 1749 | ntpwaitcount=0 1750 | while [ "$(nvram get ntp_ready)" -eq 0 ] && [ "$ntpwaitcount" -lt 600 ]; do 1751 | ntpwaitcount="$((ntpwaitcount + 30))" 1752 | Print_Output true "Waiting for NTP to sync..." "$WARN" 1753 | sleep 30 1754 | done 1755 | if [ "$ntpwaitcount" -ge 600 ]; then 1756 | Print_Output true "NTP failed to sync after 10 minutes. Please resolve!" "$CRIT" 1757 | Clear_Lock 1758 | exit 1 1759 | else 1760 | Print_Output true "NTP synced, $SCRIPT_NAME will now continue" "$PASS" 1761 | Clear_Lock 1762 | fi 1763 | fi 1764 | } 1765 | 1766 | ### function based on @Adamm00's Skynet USB wait function ### 1767 | Entware_Ready(){ 1768 | if [ ! -f /opt/bin/opkg ]; then 1769 | Check_Lock 1770 | sleepcount=1 1771 | while [ ! -f /opt/bin/opkg ] && [ "$sleepcount" -le 10 ]; do 1772 | Print_Output true "Entware not found, sleeping for 10s (attempt $sleepcount of 10)" "$ERR" 1773 | sleepcount="$((sleepcount + 1))" 1774 | sleep 10 1775 | done 1776 | if [ ! -f /opt/bin/opkg ]; then 1777 | Print_Output true "Entware not found and is required for $SCRIPT_NAME to run, please resolve" "$CRIT" 1778 | Clear_Lock 1779 | exit 1 1780 | else 1781 | Print_Output true "Entware found, $SCRIPT_NAME will now continue" "$PASS" 1782 | Clear_Lock 1783 | fi 1784 | fi 1785 | } 1786 | ### ### 1787 | 1788 | Show_About(){ 1789 | cat </dev/null 1850 | Auto_Cron create 2>/dev/null 1851 | Auto_ServiceEvent create 2>/dev/null 1852 | Shortcut_Script create 1853 | Process_Upgrade 1854 | ScriptHeader 1855 | MainMenu 1856 | exit 0 1857 | fi 1858 | 1859 | case "$1" in 1860 | install) 1861 | Check_Lock 1862 | Menu_Install 1863 | exit 0 1864 | ;; 1865 | startup) 1866 | Menu_Startup "$2" 1867 | exit 0 1868 | ;; 1869 | generate) 1870 | NTP_Ready 1871 | Entware_Ready 1872 | Check_Lock 1873 | Get_TimeServer_Stats 1874 | Clear_Lock 1875 | exit 0 1876 | ;; 1877 | outputcsv) 1878 | NTP_Ready 1879 | Entware_Ready 1880 | Check_Lock 1881 | Generate_CSVs 1882 | Clear_Lock 1883 | exit 0 1884 | ;; 1885 | service_event) 1886 | if [ "$2" = "start" ] && [ "$3" = "$SCRIPT_NAME_LOWER" ]; then 1887 | rm -f /tmp/detect_ntpmerlin.js 1888 | Check_Lock 1889 | sleep 3 1890 | Get_TimeServer_Stats 1891 | Clear_Lock 1892 | exit 0 1893 | elif [ "$2" = "start" ] && [ "$3" = "${SCRIPT_NAME_LOWER}config" ]; then 1894 | Conf_FromSettings 1895 | exit 0 1896 | elif [ "$2" = "start" ] && [ "$3" = "${SCRIPT_NAME_LOWER}checkupdate" ]; then 1897 | Update_Check 1898 | exit 0 1899 | elif [ "$2" = "start" ] && [ "$3" = "${SCRIPT_NAME_LOWER}doupdate" ]; then 1900 | Update_Version force unattended 1901 | exit 0 1902 | fi 1903 | exit 0 1904 | ;; 1905 | ntpredirect) 1906 | Print_Output true "Sleeping for 5s to allow firewall/nat startup to be completed..." "$PASS" 1907 | sleep 5 1908 | Auto_NAT create 1909 | NTP_Redirect create 1910 | exit 0 1911 | ;; 1912 | update) 1913 | Update_Version 1914 | exit 0 1915 | ;; 1916 | forceupdate) 1917 | Update_Version force 1918 | exit 0 1919 | ;; 1920 | postupdate) 1921 | Create_Dirs 1922 | Conf_Exists 1923 | ScriptStorageLocation load 1924 | Create_Symlinks 1925 | Auto_Startup create 2>/dev/null 1926 | Auto_Cron create 2>/dev/null 1927 | Auto_ServiceEvent create 2>/dev/null 1928 | Shortcut_Script create 1929 | Process_Upgrade 1930 | if Auto_NAT check; then 1931 | NTP_Redirect create 1932 | fi 1933 | ;; 1934 | checkupdate) 1935 | Update_Check 1936 | exit 0 1937 | ;; 1938 | uninstall) 1939 | Menu_Uninstall 1940 | exit 0 1941 | ;; 1942 | about) 1943 | ScriptHeader 1944 | Show_About 1945 | exit 0 1946 | ;; 1947 | help) 1948 | ScriptHeader 1949 | Show_Help 1950 | exit 0 1951 | ;; 1952 | develop) 1953 | SCRIPT_BRANCH="develop" 1954 | SCRIPT_REPO="https://raw.githubusercontent.com/jackyaz/$SCRIPT_NAME/$SCRIPT_BRANCH" 1955 | Update_Version force 1956 | exit 0 1957 | ;; 1958 | stable) 1959 | SCRIPT_BRANCH="master" 1960 | SCRIPT_REPO="https://raw.githubusercontent.com/jackyaz/$SCRIPT_NAME/$SCRIPT_BRANCH" 1961 | Update_Version force 1962 | exit 0 1963 | ;; 1964 | *) 1965 | ScriptHeader 1966 | Print_Output false "Command not recognised." "$ERR" 1967 | Print_Output false "For a list of available commands run: $SCRIPT_NAME_LOWER help" 1968 | exit 1 1969 | ;; 1970 | esac 1971 | -------------------------------------------------------------------------------- /timeserverd: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | #shellcheck disable=SC2039 3 | trap '' SIGHUP 4 | 5 | # Wait for NTP before starting 6 | logger -st timeserverd "Waiting for NTP to sync before starting..." 7 | ntpwaitcount=0 8 | while [ "$(nvram get ntp_ready)" -eq 0 ] && [ "$ntpwaitcount" -lt 600 ]; do 9 | ntpwaitcount="$((ntpwaitcount + 30))" 10 | logger -st timeserverd "Waiting for NTP to sync..." 11 | sleep 30 12 | done 13 | 14 | if [ "$ntpwaitcount" -ge 600 ]; then 15 | logger -st timeserverd "NTP failed to sync after 10 minutes - please check immediately!" 16 | exit 1 17 | fi 18 | 19 | if [ -f "/opt/share/ntpmerlin.d/config" ]; then 20 | SCRIPT_STORAGE_DIR="/opt/share/ntpmerlin.d" 21 | else 22 | SCRIPT_STORAGE_DIR="/jffs/addons/ntpmerlin.d" 23 | fi 24 | 25 | if [ "$1" = "S77ntpd" ]; then 26 | ntpd -c "$SCRIPT_STORAGE_DIR/ntp.conf" -g > /dev/null 2>&1 & 27 | 28 | while true; do 29 | sleep 5 30 | if [ "$(pidof ntpd | wc -w)" -lt 1 ]; then 31 | logger -t timeserverd "ntpd dead, restarting..." 32 | killall -q ntpd 33 | sleep 5 34 | ntpd -c "$SCRIPT_STORAGE_DIR/ntp.conf" -g > /dev/null 2>&1 & 35 | logger -t timeserverd "ntpd restarted" 36 | fi 37 | done 38 | elif [ "$1" = "S77chronyd" ]; then 39 | if [ -f /opt/etc/init.d/S06chronyd ]; then 40 | /opt/etc/init.d/S06chronyd stop 41 | rm -f /opt/etc/init.d/S06chronyd 42 | fi 43 | 44 | mkdir -p /opt/var/lib/chrony 45 | mkdir -p /opt/var/run/chrony 46 | chown -R nobody:nobody /opt/var/lib/chrony 47 | chown -R nobody:nobody /opt/var/run/chrony 48 | chmod -R 770 /opt/var/lib/chrony 49 | chmod -R 770 /opt/var/run/chrony 50 | 51 | chronyd -r -u nobody -f "$SCRIPT_STORAGE_DIR/chrony.conf" > /dev/null 2>&1 & 52 | 53 | while true; do 54 | sleep 5 55 | if [ "$(pidof chronyd | wc -w)" -lt 1 ]; then 56 | logger -t timeserverd "chronyd dead, restarting..." 57 | killall -q chronyd 58 | sleep 5 59 | chronyd -r -u nobody -f "$SCRIPT_STORAGE_DIR/chrony.conf" > /dev/null 2>&1 & 60 | logger -t timeserverd "chronyd restarted" 61 | fi 62 | done 63 | fi 64 | --------------------------------------------------------------------------------