aboutsummaryrefslogtreecommitdiffstats
path: root/global-functions.rsc
diff options
context:
space:
mode:
Diffstat (limited to 'global-functions.rsc')
-rw-r--r--global-functions.rsc550
1 files changed, 395 insertions, 155 deletions
diff --git a/global-functions.rsc b/global-functions.rsc
index 47a69c4..829cbf2 100644
--- a/global-functions.rsc
+++ b/global-functions.rsc
@@ -1,18 +1,21 @@
#!rsc by RouterOS
# RouterOS script: global-functions
-# Copyright (c) 2013-2024 Christian Hesse <mail@eworm.de>
+# Copyright (c) 2013-2025 Christian Hesse <mail@eworm.de>
# Michael Gisbers <michael@gisbers.de>
-# https://git.eworm.de/cgit/routeros-scripts/about/COPYING.md
+# https://rsc.eworm.de/COPYING.md
#
-# requires RouterOS, version=7.14
+# requires RouterOS, version=7.15
+# requires device-mode, fetch, scheduler
#
# global functions
-# https://git.eworm.de/cgit/routeros-scripts/about/
+# https://rsc.eworm.de/
:local ScriptName [ :jobname ];
-# expected configuration version
-:global ExpectedConfigVersion 131;
+# Git commit id & info, expected configuration version
+:global CommitId "unknown";
+:global CommitInfo "unknown";
+:global ExpectedConfigVersion 138;
# global variables not to be changed by user
:global GlobalFunctionsReady false;
@@ -32,8 +35,11 @@
:global DownloadPackage;
:global EitherOr;
:global EscapeForRegEx;
+:global ExitError;
:global FetchHuge;
:global FetchUserAgentStr;
+:global FileExists;
+:global FileGet;
:global FormatLine;
:global FormatMultiLines;
:global GetMacVendor;
@@ -51,6 +57,7 @@
:global IsTimeSync;
:global LogPrint;
:global LogPrintOnce;
+:global LogPrintVerbose;
:global MAX;
:global MIN;
:global MkDir;
@@ -61,6 +68,8 @@
:global ProtocolStrip;
:global RandomDelay;
:global RequiredRouterOS;
+:global RmDir;
+:global RmFile;
:global ScriptFromTerminal;
:global ScriptInstallUpdate;
:global ScriptLock;
@@ -112,6 +121,11 @@
:return false;
}
+ :if (([ /certificate/settings/get ]->"builtin-trust-anchors") = "trusted" && \
+ [[ :parse (":return [ :len [ /certificate/builtin/find where common-name=\"" . $CommonName . "\" ] ]") ]] > 0) do={
+ :return true;
+ }
+
:if ([ :len [ /certificate/find where common-name=$CommonName ] ] = 0) do={
$LogPrint info $0 ("Certificate with CommonName '" . $CommonName . "' not available.");
:if ([ $CertificateDownload $CommonName ] = false) do={
@@ -145,6 +159,7 @@
:global CleanName;
:global FetchUserAgentStr;
:global LogPrint;
+ :global RmFile;
:global WaitForFile;
$LogPrint info $0 ("Downloading and importing certificate with " . \
@@ -159,8 +174,8 @@
$LogPrint warning $0 ("Failed downloading certificate with CommonName '" . $CommonName . \
"' from repository! Trying fallback to mkcert.org...");
:do {
- :if ([ $CertificateAvailable "ISRG Root X1" ] = false) do={
- $LogPrint error $0 ("Downloading required certificate failed.");
+ :if ([ :len [ /certificate/find where common-name="ISRG Root X1" ] ] = 0) do={
+ $LogPrint error $0 ("Required certificate is not available.");
:return false;
}
/tool/fetch check-certificate=yes-without-crl http-header-field=({ [ $FetchUserAgentStr $0 ] }) \
@@ -168,7 +183,7 @@
dst-path=$FileName as-value;
$WaitForFile $FileName;
:if ([ /file/get $FileName size ] = 0) do={
- /file/remove $FileName;
+ $RmFile $FileName;
:error false;
}
} on-error={
@@ -179,7 +194,7 @@
/certificate/import file-name=$FileName passphrase="" as-value;
:delay 1s;
- /file/remove [ find where name=$FileName ];
+ $RmFile $FileName;
:if ([ :len [ /certificate/find where common-name=$CommonName ] ] = 0) do={
/certificate/remove [ find where name~("^" . $FileName . "_[0-9]+\$") ];
@@ -195,12 +210,19 @@
# name a certificate by its common-name
:set CertificateNameByCN do={
- :local CommonName [ :tostr $1 ];
+ :local Match [ :tostr $1 ];
:global CleanName;
+ :global LogPrint;
- :local Cert [ /certificate/find where common-name=$CommonName ];
+ :local Cert ([ /certificate/find where (common-name=$Match or fingerprint=$Match or name=$Match) ]->0);
+ :if ([ :len $Cert ] = 0) do={
+ $LogPrint warning $0 ("No matching certificate found.");
+ :return false;
+ }
+ :local CommonName [ /certificate/get $Cert common-name ];
/certificate/set $Cert name=[ $CleanName $CommonName ];
+ :return true;
}
# multiply given character(s)
@@ -279,6 +301,8 @@
# get readable device info
:set DeviceInfo do={
+ :global CommitId;
+ :global CommitInfo;
:global ExpectedConfigVersion;
:global Identity;
@@ -300,16 +324,19 @@
([ $FormatLine "Location" ($Snmp->"location") ] . "\n") ] . \
[ $IfThenElse ([ :len ($Snmp->"contact") ] > 0) \
([ $FormatLine "Contact" ($Snmp->"contact") ] . "\n") ] . \
- [ $FormatLine "Board name" ($Resource->"board-name") ] . "\n" . \
- [ $FormatLine "Architecture" ($Resource->"architecture-name") ] . "\n" . \
+ "Hardware:\n" . \
+ [ $FormatLine " Board" ($Resource->"board-name") ] . "\n" . \
+ [ $FormatLine " Arch" ($Resource->"architecture-name") ] . "\n" . \
[ $IfThenElse ($RouterBoard->"routerboard" = true) \
- ([ $FormatLine "Model" ($RouterBoard->"model") ] . \
+ ([ $FormatLine " Model" ($RouterBoard->"model") ] . \
[ $IfThenElse ([ :len ($RouterBoard->"revision") ] > 0) \
(" " . $RouterBoard->"revision") ] . "\n" . \
- [ $FormatLine "Serial number" ($RouterBoard->"serial-number") ] . "\n") ] . \
- [ $IfThenElse ([ :len ($License->"level") ] > 0) \
- ([ $FormatLine "License" ($License->"level") ] . "\n") ] . \
+ [ $FormatLine " Serial" ($RouterBoard->"serial-number") ] . "\n") ] . \
+ [ $IfThenElse ([ :len ($License->"nlevel") ] > 0) \
+ ([ $FormatLine " License" ("level " . ($License->"nlevel")) ] . "\n") ] . \
"RouterOS:\n" . \
+ [ $IfThenElse ([ :len ($License->"level") ] > 0) \
+ ([ $FormatLine " License" ("level " . ($License->"level")) ] . "\n") ] . \
[ $FormatLine " Channel" ($Update->"channel") ] . "\n" . \
[ $FormatLine " Installed" ($Update->"installed-version") ] . "\n" . \
[ $IfThenElse ([ :typeof ($Update->"latest-version") ] != "nothing" && \
@@ -319,6 +346,8 @@
$RouterBoard->"current-firmware" != $RouterBoard->"upgrade-firmware") \
([ $FormatLine " Firmware" ($RouterBoard->"current-firmware") ] . "\n") ] . \
"RouterOS-Scripts:\n" . \
+ [ $IfThenElse ($CommitId != "unknown") \
+ ([ $FormatLine " Commit" ($CommitInfo . "/" . [ :pick $CommitId 0 8 ]) ] . "\n") ] . \
[ $FormatLine " Version" $ExpectedConfigVersion ]);
}
@@ -336,8 +365,10 @@
:global CertificateAvailable;
:global CleanFilePath;
+ :global FileExists;
:global LogPrint;
:global MkDir;
+ :global RmFile;
:global WaitForFile;
:if ([ :len $PkgName ] = 0) do={ :return false; }
@@ -355,7 +386,7 @@
:return false;
}
- :if ([ :len [ /file/find where name=$PkgDest type="package" ] ] > 0) do={
+ :if ([ $FileExists $PkgDest "package" ] = true) do={
$LogPrint info $0 ("Package file " . $PkgName . " already exists.");
:return true;
}
@@ -368,25 +399,22 @@
:local Url ("https://upgrade.mikrotik.com/routeros/" . $PkgVer . "/" . $PkgFile);
$LogPrint info $0 ("Downloading package file '" . $PkgName . "'...");
$LogPrint debug $0 ("... from url: " . $Url);
- :local Retry 3;
- :while ($Retry > 0) do={
- :do {
- /tool/fetch check-certificate=yes-without-crl $Url dst-path=$PkgDest;
- $WaitForFile $PkgDest;
- :if ([ /file/get [ find where name=$PkgDest ] type ] = "package") do={
- :return true;
- }
- } on-error={
- $LogPrint debug $0 ("Downloading package file failed.");
- }
+ :onerror Err {
+ /tool/fetch check-certificate=yes-without-crl $Url dst-path=$PkgDest;
+ $WaitForFile $PkgDest;
+ } do={
+ $LogPrint warning $0 ("Downloading package file '" . $PkgName . "' failed: " . $Err);
+ :return false;
+ }
- /file/remove [ find where name=$PkgDest ];
- :set Retry ($Retry - 1);
+ :if ([ $FileExists $PkgDest "package" ] = false) do={
+ $LogPrint warning $0 ("Downloaded file is not a package, removing.");
+ $RmFile $PkgDest;
+ :return false;
}
- $LogPrint warning $0 ("Downloading package file '" . $PkgName . "' failed.");
- :return false;
+ :return true;
}
# return either first (if "true") or second
@@ -425,11 +453,27 @@
:return $Return;
}
+# simple macro to print error message on unintentional error
+:set ExitError do={
+ :local ExitOK [ :tostr $1 ];
+ :local Name [ :tostr $2 ];
+ :local Error [ :tostr $3 ];
+
+ :global IfThenElse;
+ :global LogPrint;
+
+ :if ($ExitOK = "false") do={
+ $LogPrint error $Name ([ $IfThenElse ([ :pick $Name 0 1 ] = "\$") \
+ "Function" "Script" ] . " '" . $Name . "' exited with error" . \
+ [ $IfThenElse (!($Error ~ "^(|true|false)\$")) (": " . $Error) "." ]);
+ }
+}
+
# fetch huge data to file, read in chunks
:set FetchHuge do={
- :local ScriptName [ :tostr $1 ];
- :local Url [ :tostr $2 ];
- :local CheckCert [ :tobool $3 ];
+ :local ScriptName [ :tostr $1 ];
+ :local Url [ :tostr $2 ];
+ :local CheckCert [ :tostr $3 ];
:global CleanName;
:global FetchUserAgentStr;
@@ -437,9 +481,11 @@
:global IfThenElse;
:global LogPrint;
:global MkDir;
+ :global RmDir;
+ :global RmFile;
:global WaitForFile;
- :set CheckCert [ $IfThenElse ($CheckCert = false) "no" "yes-without-crl" ];
+ :set CheckCert [ $IfThenElse ($CheckCert = "false") "no" "yes-without-crl" ];
:local DirName ("tmpfs/" . [ $CleanName $ScriptName ]);
:if ([ $MkDir $DirName ] = false) do={
@@ -448,15 +494,15 @@
}
:local FileName ($DirName . "/" . [ $CleanName $0 ] . "-" . [ $GetRandom20CharAlNum ]);
- :do {
+ :onerror Err {
/tool/fetch check-certificate=$CheckCert $Url dst-path=$FileName \
http-header-field=({ [ $FetchUserAgentStr $ScriptName ] }) as-value;
- } on-error={
+ } do={
:if ([ $WaitForFile $FileName 500ms ] = true) do={
- /file/remove $FileName;
+ $RmFile $FileName;
}
- $LogPrint debug $0 ("Failed downloading from: " . $Url);
- /file/remove $DirName;
+ $LogPrint debug $0 ("Failed downloading from " . $Url . " - " . $Err);
+ $RmDir $DirName;
:return false;
}
$WaitForFile $FileName;
@@ -464,11 +510,15 @@
:local FileSize [ /file/get $FileName size ];
:local Return "";
:local VarSize 0;
- :while ($VarSize < $FileSize) do={
+ :while ($VarSize != $FileSize) do={
:set Return ($Return . ([ /file/read offset=$VarSize chunk-size=32768 file=$FileName as-value ]->"data"));
+ :set FileSize [ /file/get $FileName size ];
:set VarSize [ :len $Return ];
+ :if ($VarSize > $FileSize) do={
+ :delay 100ms;
+ }
}
- /file/remove $DirName;
+ $RmDir $DirName;
:return $Return;
}
@@ -482,6 +532,47 @@
$Resource->"architecture-name" . " " . $Caller . "/Fetch (https://rsc.eworm.de/)");
}
+# check for existence of file, optionally with type
+:set FileExists do={
+ :local FileName [ :tostr $1 ];
+ :local Type [ :tostr $2 ];
+
+ :global FileGet;
+
+ :local FileVal [ $FileGet $FileName ];
+ :if ($FileVal = false) do={
+ :return false;
+ }
+
+ :if ([ :len ($FileVal->"size") ] = 0) do={
+ :return false;
+ }
+
+ :if ([ :len $Type ] = 0 || $FileVal->"type" = $Type) do={
+ :return true;
+ }
+
+ :return false;
+}
+
+# get file properties in array, or false on error
+:set FileGet do={
+ :local FileName [ :tostr $1 ];
+
+ :global WaitForFile;
+
+ :if ([ $WaitForFile $FileName 0s ] = false) do={
+ :return false;
+ }
+
+ :local FileVal false;
+ :do {
+ :set FileVal [ /file/get $FileName ];
+ } on-error={ }
+
+ :return $FileVal;
+}
+
# format a line for output
:set FormatLine do={
:local Key [ :tostr $1 ];
@@ -544,12 +635,12 @@
("https://api.macvendors.com/" . [ :pick $Mac 0 8 ]) output=user as-value ]->"data");
:return $Vendor;
} on-error={
- :do {
+ :onerror Err {
/tool/fetch check-certificate=yes-without-crl ("https://api.macvendors.com/") \
output=none as-value;
$LogPrint debug $0 ("The mac vendor is not known in database.");
- } on-error={
- $LogPrint warning $0 ("Failed getting mac vendor.");
+ } do={
+ $LogPrint warning $0 ("Failed getting mac vendor: " . $Err);
}
:return "unknown vendor";
}
@@ -813,6 +904,9 @@
:return true;
}
+# The function $LogPrintVerbose is declared, but has no code, intentionally.
+# https://rsc.eworm.de/DEBUG.md#verbose-output
+
# get max value
:set MAX do={
:if ($1 > $2) do={ :return $1; }
@@ -830,7 +924,9 @@
:local Path [ :tostr $1 ];
:global CleanFilePath;
+ :global FileGet;
:global LogPrint;
+ :global RmDir;
:global WaitForFile;
:local MkTmpfs do={
@@ -847,12 +943,12 @@
}
$LogPrint info $0 ("Creating disk of type tmpfs.");
- /file/remove [ find where name="tmpfs" type="directory" ];
- :do {
+ $RmDir "tmpfs";
+ :onerror Err {
/disk/add slot=tmpfs type=tmpfs tmpfs-max-size=([ /system/resource/get total-memory ] / 3);
$WaitForFile "tmpfs";
- } on-error={
- $LogPrint warning $0 ("Creating disk of type tmpfs failed!");
+ } do={
+ $LogPrint warning $0 ("Creating disk of type tmpfs failed: " . $Err);
:return false;
}
:return true;
@@ -864,7 +960,11 @@
:return true;
}
- :if ([ :len [ /file/find where name=$Path type="directory" ] ] = 1) do={
+ $LogPrint debug $0 ("Making directory: " . $Path);
+
+ :local PathVal [ $FileGet $Path ];
+ :if ($PathVal->"type" = "directory") do={
+ $LogPrint debug $0 ("... which already exists.");
:return true;
}
@@ -874,13 +974,11 @@
}
}
- :do {
- :local File ($Path . "/file");
- /file/add name=$File;
- $WaitForFile $File;
- /file/remove $File;
- } on-error={
- $LogPrint warning $0 ("Making directory '" . $Path . "' failed!");
+ :onerror Err {
+ /file/add type="directory" name=$Path;
+ $WaitForFile $Path;
+ } do={
+ $LogPrint warning $0 ("Making directory '" . $Path . "' failed: " . $Err);
:return false;
}
@@ -904,14 +1002,24 @@
# parse key value store
:set ParseKeyValueStore do={
:local Source $1;
+
+ :if ([ :pick $Source 0 1 ] = "{") do={
+ :do {
+ :return [ :deserialize from=json $Source ];
+ } on-error={ }
+ }
+
:if ([ :typeof $Source ] != "array") do={
:set Source [ :tostr $1 ];
}
:local Result ({});
:foreach KeyValue in=[ :toarray $Source ] do={
:if ([ :find $KeyValue "=" ]) do={
- :set ($Result->[ :pick $KeyValue 0 [ :find $KeyValue "=" ] ]) \
- [ :pick $KeyValue ([ :find $KeyValue "=" ] + 1) [ :len $KeyValue ] ];
+ :local Key [ :pick $KeyValue 0 [ :find $KeyValue "=" ] ];
+ :local Value [ :pick $KeyValue ([ :find $KeyValue "=" ] + 1) [ :len $KeyValue ] ];
+ :if ($Value="true") do={ :set Value true; }
+ :if ($Value="false") do={ :set Value false; }
+ :set ($Result->$Key) $Value;
} else={
:set ($Result->$KeyValue) true;
}
@@ -976,6 +1084,64 @@
:return true;
}
+# remove directory
+:set RmDir do={
+ :local DirName [ :tostr $1 ];
+
+ :global FileGet;
+ :global LogPrint;
+
+ $LogPrint debug $0 ("Removing directory: ". $DirName);
+
+ :local DirVal [ $FileGet $DirName ];
+ :if ($DirVal = false) do={
+ $LogPrint debug $0 ("... which does not exist.");
+ :return true;
+ }
+
+ :if ($DirVal->"type" != "directory") do={
+ $LogPrint error $0 ("Directory '" . $DirName . "' is not a directory.");
+ :return false;
+ }
+
+ :onerror Err {
+ /file/remove $DirName;
+ } do={
+ $LogPrint error $0 ("Removing directory '" . $DirName . "' failed: " . $Err);
+ :return false;
+ }
+ :return true;
+}
+
+# remove file
+:set RmFile do={
+ :local FileName [ :tostr $1 ];
+
+ :global FileGet;
+ :global LogPrint;
+
+ $LogPrint debug $0 ("Removing file: ". $FileName);
+
+ :local FileVal [ $FileGet $FileName ];
+ :if ($FileVal = false) do={
+ $LogPrint debug $0 ("... which does not exist.");
+ :return true;
+ }
+
+ :if ($FileVal->"type" = "directory" || $FileVal->"type" = "disk") do={
+ $LogPrint error $0 ("File '" . $FileName . "' is not a file.");
+ :return false;
+ }
+
+ :onerror Err {
+ /file/remove $FileName;
+ } do={
+ $LogPrint error $0 ("Removing file '" . $FileName . "' failed: " . $Err);
+ :return false;
+ }
+ :return true;
+}
+
# check if script is run from terminal
:set ScriptFromTerminal do={
:local Script [ :tostr $1 ];
@@ -1003,11 +1169,15 @@
}
# install new scripts, update existing scripts
-:set ScriptInstallUpdate do={
+:set ScriptInstallUpdate do={ :onerror Err {
:local Scripts [ :toarray $1 ];
:local NewComment [ :tostr $2 ];
+ :global CommitId;
+ :global CommitInfo;
:global ExpectedConfigVersion;
+ :global GlobalConfigReady;
+ :global GlobalFunctionsReady;
:global Identity;
:global IDonate;
:global NoNewsAndChangesNotification;
@@ -1039,9 +1209,18 @@
}
}
+ :local CommitIdBefore $CommitId;
:local ExpectedConfigVersionBefore $ExpectedConfigVersion;
- :local ReloadGlobalFunctions false;
- :local ReloadGlobalConfig false;
+ :local ReloadGlobal false;
+ :local DeviceMode [ /system/device-mode/get ];
+
+ :local CheckSums ({});
+ :do {
+ :local Url ($ScriptUpdatesBaseUrl . "checksums.json" . $ScriptUpdatesUrlSuffix);
+ $LogPrint debug $0 ("Fetching checksums from url: " . $Url);
+ :set CheckSums [ :deserialize from=json ([ /tool/fetch check-certificate=yes-without-crl \
+ http-header-field=({ [ $FetchUserAgentStr $0 ] }) $Url output=user as-value ]->"data") ];
+ } on-error={ }
:foreach Script in=[ /system/script/find where source~"^#!rsc by RouterOS\r?\n" ] do={
:local ScriptVal [ /system/script/get $Script ];
@@ -1056,8 +1235,26 @@
}
}
- :if (!($ScriptInfo->"ignore" = true)) do={
- :do {
+ :do {
+ :if ($ScriptInfo->"ignore" = true) do={
+ $LogPrint debug $0 ("Ignoring script '" . $ScriptVal->"name" . "', as requested.");
+ :error true;
+ }
+
+ :local CheckSum ($CheckSums->($ScriptVal->"name"));
+ :if ([ :len ($ScriptInfo->"base-url") ] = 0 && [ :len ($ScriptInfo->"url-suffix") ] = 0 && \
+ [ :convert transform=md5 to=hex [ :tolf ($ScriptVal->"source") ] ] = $CheckSum) do={
+ $LogPrint debug $0 ("Checksum for script '" . $ScriptVal->"name" . "' matches, ignoring.");
+ :error true;
+ }
+
+ :if ([ :len ($ScriptInfo->"certificate") ] > 0) do={
+ :if ([ $CertificateAvailable ($ScriptInfo->"certificate") ] = false) do={
+ $LogPrint warning $0 ("Downloading certificate failed, trying without.");
+ }
+ }
+
+ :onerror Err {
:local BaseUrl [ $EitherOr ($ScriptInfo->"base-url") $ScriptUpdatesBaseUrl ];
:local UrlSuffix [ $EitherOr ($ScriptInfo->"url-suffix") $ScriptUpdatesUrlSuffix ];
:local Url ($BaseUrl . $ScriptVal->"name" . ".rsc" . $UrlSuffix);
@@ -1067,70 +1264,84 @@
:if ($Result->"status" = "finished") do={
:set SourceNew [ :tolf ($Result->"data") ];
}
- } on-error={
+ } do={
+ $LogPrint warning $0 ("Failed fetching script '" . $ScriptVal->"name" . "': " . $Err);
:if ($ScriptVal->"source" = "#!rsc by RouterOS\n") do={
- $LogPrint warning $0 ("Failed fetching script '" . $ScriptVal->"name" . \
- "', removing dummy. Typo on installation?");
+ $LogPrint warning $0 ("Removing dummy. Typo on installation?");
/system/script/remove $Script;
- } else={
- $LogPrint warning $0 ("Failed fetching script '" . $ScriptVal->"name" . "'!");
}
+ :error false;
+ }
+
+ :if ([ :len $SourceNew ] = 0) do={
+ $LogPrint debug $0 ("No update for script '" . $ScriptVal->"name" . "'.");
+ :error false;
}
- }
- :if ([ :len $SourceNew ] > 0) do={
:local SourceCRLF [ :tocrlf $SourceNew ];
- :if ($SourceNew != $ScriptVal->"source" && $SourceCRLF != $ScriptVal->"source") do={
- :if ([ :pick $SourceNew 0 18 ] = "#!rsc by RouterOS\n") do={
- :local Required ([ $ParseKeyValueStore [ $Grep $SourceNew ("\23 requires RouterOS, ") ] ]->"version");
- :if ([ $RequiredRouterOS $0 [ $EitherOr $Required "0.0" ] false ] = true) do={
- :if ([ $ValidateSyntax $SourceNew ] = true) do={
- $LogPrint info $0 ("Updating script: " . $ScriptVal->"name");
- /system/script/set owner=($ScriptVal->"name") \
- source=[ $IfThenElse ($ScriptUpdatesCRLF = true) $SourceCRLF $SourceNew ] $Script;
- :if ($ScriptVal->"name" = "global-config") do={
- :set ReloadGlobalConfig true;
- }
- :if ($ScriptVal->"name" = "global-functions" || $ScriptVal->"name" ~ ("^mod/.")) do={
- :set ReloadGlobalFunctions true;
- }
- } else={
- $LogPrint warning $0 ("Syntax validation for script '" . $ScriptVal->"name" . \
- "' failed! Ignoring!");
- }
- } else={
- $LogPrintOnce warning $0 ("The script '" . $ScriptVal->"name" . "' requires RouterOS " . \
- $Required . ", which is not met by your installation. Ignoring!");
- }
- } else={
- $LogPrint warning $0 ("Looks like new script '" . $ScriptVal->"name" . \
+ :if ($SourceNew = $ScriptVal->"source" || $SourceCRLF = $ScriptVal->"source") do={
+ $LogPrint debug $0 ("Script '" . $ScriptVal->"name" . "' did not change.");
+ :error false;
+ }
+
+ :if ([ :pick $SourceNew 0 18 ] != "#!rsc by RouterOS\n") do={
+ $LogPrint warning $0 ("Looks like new script '" . $ScriptVal->"name" . \
"' is not valid (missing shebang). Ignoring!");
+ :error false;
+ }
+
+ :local RequiredROS ([ $ParseKeyValueStore [ $Grep $SourceNew ("\23 requires RouterOS, ") ] ]->"version");
+ :if ([ $RequiredRouterOS $0 [ $EitherOr $RequiredROS "0.0" ] false ] = false) do={
+ $LogPrintOnce warning $0 ("The script '" . $ScriptVal->"name" . "' requires RouterOS " . \
+ $RequiredROS . ", which is not met by your installation. Ignoring!");
+ :error false;
+ }
+
+ :local RequiredDM [ $ParseKeyValueStore [ $Grep $SourceNew ("\23 requires device-mode, ") ] ];
+ :local MissingDM ({});
+ :foreach Feature,Value in=$RequiredDM do={
+ :if ([ :typeof ($DeviceMode->$Feature) ] = "bool" && ($DeviceMode->$Feature) = false) do={
+ :set MissingDM ($MissingDM, $Feature);
}
- } else={
- $LogPrint debug $0 ("Script '" . $ScriptVal->"name" . "' did not change.");
}
- } else={
- $LogPrint debug $0 ("No update for script '" . $ScriptVal->"name" . "'.");
- }
+ :if ([ :len $MissingDM ] > 0) do={
+ $LogPrintOnce warning $0 ("The script '" . $ScriptVal->"name" . "' requires disabled " . \
+ "device-mode features (" . [ :tostr $MissingDM ] . "). Ignoring!");
+ :error false;
+ }
+
+ :if ([ $ValidateSyntax $SourceNew ] = false) do={
+ $LogPrint warning $0 ("Syntax validation for script '" . $ScriptVal->"name" . "' failed! Ignoring!");
+ :error false;
+ }
+
+ $LogPrint info $0 ("Updating script: " . $ScriptVal->"name");
+ /system/script/set owner=($ScriptVal->"name") \
+ source=[ $IfThenElse ($ScriptUpdatesCRLF = true) $SourceCRLF $SourceNew ] $Script;
+ :if ($ScriptVal->"name" = "global-config" || \
+ $ScriptVal->"name" = "global-functions" || \
+ $ScriptVal->"name" ~ ("^mod/.")) do={
+ :set ReloadGlobal true;
+ }
+ } on-error={ }
}
- :if ($ReloadGlobalFunctions = true) do={
- $LogPrint info $0 ("Reloading global functions.");
- :do {
+ :if ($ReloadGlobal = true) do={
+ $LogPrint info $0 ("Reloading global configuration and functions.");
+ :set GlobalConfigReady false;
+ :set GlobalFunctionsReady false;
+ :delay 1s;
+
+ :onerror Err {
+ /system/script/run global-config;
/system/script/run global-functions;
- } on-error={
- $LogPrint error $0 ("Reloading global functions failed!");
+ } do={
+ $LogPrint error $0 ("Reloading global configuration and functions failed! " . $Err);
}
}
- :if ($ReloadGlobalConfig = true) do={
- $LogPrint info $0 ("Reloading global configuration.");
- :do {
- /system/script/run global-config;
- } on-error={
- $LogPrint error $0 ("Reloading global configuration failed!" . \
- " Syntax error or missing overlay?");
- }
+ :if ($CommitId != "unknown" && $CommitIdBefore != $CommitId) do={
+ $LogPrint info $0 ("Updated to commit: " . $CommitInfo . "/" . [ :pick $CommitId 0 8 ]);
}
:if ($ExpectedConfigVersionBefore > $ExpectedConfigVersion) do={
@@ -1144,7 +1355,7 @@
:global GlobalConfigMigration;
:local ChangeLogCode;
- :do {
+ :onerror Err {
:local Url ($ScriptUpdatesBaseUrl . "news-and-changes.rsc" . $ScriptUpdatesUrlSuffix);
$LogPrint debug $0 ("Fetching news, changes and migration: " . $Url);
:local Result [ /tool/fetch check-certificate=yes-without-crl \
@@ -1152,16 +1363,16 @@
:if ($Result->"status" = "finished") do={
:set ChangeLogCode ($Result->"data");
}
- } on-error={
- $LogPrint warning $0 ("Failed fetching news, changes and migration!");
+ } do={
+ $LogPrint warning $0 ("Failed fetching news, changes and migration: " . $Err);
}
:if ([ :len $ChangeLogCode ] > 0) do={
:if ([ $ValidateSyntax $ChangeLogCode ] = true) do={
- :do {
+ :onerror Err {
[ :parse $ChangeLogCode ];
- } on-error={
- $LogPrint warning $0 ("The changelog failed to run!");
+ } do={
+ $LogPrint warning $0 ("The changelog failed to run: " . $Err);
}
} else={
$LogPrint warning $0 ("The changelog failed syntax validation!");
@@ -1171,18 +1382,24 @@
:if ([ :len $GlobalConfigMigration ] > 0) do={
:for I from=($ExpectedConfigVersionBefore + 1) to=$ExpectedConfigVersion do={
:local Migration ($GlobalConfigMigration->[ :tostr $I ]);
- :if ([ :typeof $Migration ] = "str") do={
- :if ([ $ValidateSyntax $Migration ] = true) do={
- $LogPrint info $0 ("Applying migration for change " . $I . ": " . $Migration);
- :do {
- [ :parse $Migration ];
- } on-error={
- $LogPrint warning $0 ("Migration code for change " . $I . " failed to run!");
- }
- } else={
+ :do {
+ :if ([ :typeof $Migration ] != "str") do={
+ $LogPrint debug $0 ("Migration code for change " . $I . " is not available.");
+ :error false;
+ }
+
+ :if ([ $ValidateSyntax $Migration ] = false) do={
$LogPrint warning $0 ("Migration code for change " . $I . " failed syntax validation!");
+ :error false;
}
- }
+
+ $LogPrint info $0 ("Applying migration for change " . $I . ": " . $Migration);
+ :onerror Err {
+ [ :parse $Migration ];
+ } do={
+ $LogPrint warning $0 ("Migration code for change " . $I . " failed to run: " . $Err);
+ }
+ } on-error={ }
}
}
@@ -1222,12 +1439,14 @@
:set GlobalConfigChanges;
:set GlobalConfigMigration;
}
-}
+} do={
+ :global ExitError; $ExitError false $0 $Err;
+} }
# lock script against multiple invocation
:set ScriptLock do={
- :local Script [ :tostr $1 ];
- :local WaitMax ([ :tonum $3 ] * 10);
+ :local Script [ :tostr $1 ];
+ :local WaitMax [ :totime $2 ];
:global GetRandom20CharAlNum;
:global IfThenElse;
@@ -1316,6 +1535,10 @@
:set ($ScriptLockOrder->$Script) ({});
}
+ :if ([ :typeof $WaitMax ] = "nil" ) do={
+ :set WaitMax 0s;
+ }
+
:if ([ :len [ /system/script/find where name=$Script ] ] = 0) do={
$LogPrint error $0 ("A script named '" . $Script . "' does not exist!");
:error false;
@@ -1335,12 +1558,13 @@
:local MyTicket [ $GetRandom20CharAlNum 6 ];
$AddTicket $Script $MyTicket;
- :local WaitCount 0;
- :while ($WaitMax > $WaitCount && \
+ :local WaitInterval ($WaitMax / 20);
+ :local WaitTime $WaitMax;
+ :while ($WaitTime > 0 && \
([ $IsFirstTicket $Script $MyTicket ] = false || \
[ $TicketCount $Script ] < [ $JobCount $Script ])) do={
- :set WaitCount ($WaitCount + 1);
- :delay 100ms;
+ :set WaitTime ($WaitTime - $WaitInterval);
+ :delay $WaitInterval;
}
:if ([ $IsFirstTicket $Script $MyTicket ] = true && \
@@ -1352,16 +1576,18 @@
$RemoveTicket $Script $MyTicket;
$LogPrint debug $0 ("Script '" . $Script . "' started more than once" . \
- [ $IfThenElse ($WaitCount > 0) " and timed out waiting for lock" "" ] . "...");
+ [ $IfThenElse ($WaitTime < $WaitMax) " and timed out waiting for lock" "" ] . "...");
:return false;
}
# send notification via NotificationFunctions - expects at least two string arguments
-:set SendNotification do={
+:set SendNotification do={ :onerror Err {
:global SendNotification2;
$SendNotification2 ({ origin=$0; subject=$1; message=$2; link=$3; silent=$4 });
-}
+} do={
+ :global ExitError; $ExitError false $0 $Err;
+} }
# send notification via NotificationFunctions - expects one array argument
:set SendNotification2 do={
@@ -1479,9 +1705,12 @@
:set ValidateSyntax do={
:local Code [ :tostr $1 ];
- :do {
+ :global LogPrint;
+
+ :onerror Err {
[ :parse (":local Validate do={\n" . $Code . "\n}") ];
- } on-error={
+ } do={
+ $LogPrint debug $0 ("Valdation failed: " . $Err);
:return false;
}
:return true;
@@ -1548,17 +1777,28 @@
:global MAX;
:set FileName [ $CleanFilePath $FileName ];
- :local I 1;
- :local Delay ([ $MAX [ $EitherOr $WaitTime 2s ] 100ms ] / 10);
+ :local Delay ([ $MAX [ $EitherOr $WaitTime 2s ] 100ms ] / 9);
- :while ([ :len [ /file/find where name=$FileName ] ] = 0) do={
- :if ($I >= 10) do={
- :return false;
- }
+ :do {
+ :retry {
+ :if ([ :len [ /file/find where name=$FileName ] ] = 0) do={
+ :error false;
+ }
+ } delay=$Delay max=10;
+ } on-error={
+ :return false;
+ }
+
+ :while ([ :len [ /file/find where name=$FileName ] ] > 0) do={
+ :do {
+ /file/get $FileName;
+ :return true;
+ } on-error={ }
:delay $Delay;
- :set I ($I + 1);
+ :set Delay ($Delay * 3 / 2);
}
- :return true;
+
+ :return false;
}
# wait to be fully connected (default route is reachable, time is sync, DNS resolves)
@@ -1585,10 +1825,10 @@
:foreach Script in=[ /system/script/find where name ~ "^mod/." ] do={
:local ScriptVal [ /system/script/get $Script ];
:if ([ $ValidateSyntax ($ScriptVal->"source") ] = true) do={
- :do {
+ :onerror Err {
/system/script/run $Script;
- } on-error={
- $LogPrint error $0 ("Module '" . $ScriptVal->"name" . "' failed to run.");
+ } do={
+ $LogPrint error $0 ("Module '" . $ScriptVal->"name" . "' failed to run: " . $Err);
}
} else={
$LogPrint error $0 ("Module '" . $ScriptVal->"name" . "' failed syntax validation, skipping.");