From 06c2e2fbe2659d67b0efa4f3295c3713fff75008 Mon Sep 17 00:00:00 2001 From: Yuanle Song Date: Thu, 14 Nov 2019 15:06:06 +0800 Subject: [PATCH] moved some functions to Lib.fs; logger functions now support variable number of arguments. --- Lib.fs | 72 ++++++++++++++++++++++++++++ Program.fs | 91 +++++++----------------------------- mbackup-for-windows.fsproj | 1 + mbackup-tests/MbackupTest.fs | 22 ++++++++- operational | 87 ++++++++++++++++++++++++++++++++++ 5 files changed, 197 insertions(+), 76 deletions(-) create mode 100644 Lib.fs diff --git a/Lib.fs b/Lib.fs new file mode 100644 index 0000000..2583a92 --- /dev/null +++ b/Lib.fs @@ -0,0 +1,72 @@ +module Mbackup.Lib + +open System +open System.Text.RegularExpressions + +type Logger() = + let mutable level = Logger.DEBUG + + static member DEBUG = 10 + static member INFO = 20 + static member WARNING = 30 + static member ERROR = 40 + static member LevelToString level = + match level with + | 10 -> "DEBUG" + | 20 -> "INFO" + | 30 -> "WARNING" + | 40 -> "ERROR" + | _ -> failwith (sprintf "Unknown log level: %d" level) + + member this.Level = level + + member this.LogMaybe level fmt = + let doAfter s = + if this.Level <= level then + let ts = DateTime.Now.ToString("s") + printf "%s %s %s\n" ts (Logger.LevelToString level) s + else + printf "" + Printf.ksprintf doAfter fmt + + member this.SetLevel level = + this.Level = level + member this.Debug fmt = this.LogMaybe Logger.DEBUG fmt + member this.Info fmt = this.LogMaybe Logger.INFO fmt + member this.Warning fmt = this.LogMaybe Logger.WARNING fmt + member this.Error fmt = this.LogMaybe Logger.ERROR fmt + +// append string s to list if pred is true +let appendWhen (pred: bool) (lst: string list) (s: string) = if pred then List.append lst [s] else lst + +let GetEnv (varName: string) = Environment.GetEnvironmentVariable varName + +let GetEnvDefault (varName: string) (defaultValue: string) = + let value = Environment.GetEnvironmentVariable varName + match value with + | null -> defaultValue + | "" -> defaultValue + | _ -> value + +// Convert windows path to Mingw64 path. +// Supported windows path: C:\foo, C:/foo, /c/foo +// MingwPath format: /cygdrive/c/foo +let ToMingwPath (windowsPath: string) = + let pattern = Regex("^/([c-zC-Z])/", RegexOptions.None) + let result = + if pattern.IsMatch(windowsPath) then + "/cygdrive" + windowsPath + else + let pattern = Regex("^([c-zC-Z]):", RegexOptions.None) + if pattern.IsMatch(windowsPath) then + let result = windowsPath.Replace('\\', '/') + "/cygdrive/" + result.Substring(0, 1).ToLower() + result.Substring(2) + else + windowsPath + result + +let EnsureDir (path: string) = if path.EndsWith "/" then path else path + "/" +let EnsureWinDir (path: string) = if path.EndsWith "\\" then path else path + "\\" + +let parseMbackupConfig fn = + () diff --git a/Program.fs b/Program.fs index d1f7442..ba7d1ff 100644 --- a/Program.fs +++ b/Program.fs @@ -8,16 +8,18 @@ // /%appdata%/mbackup/mbackup-default.exclude // /%appdata%/mbackup/local.exclude (optional) -module Mbackup +module Mbackup.Program open System open System.IO open System.Diagnostics -open System.Text.RegularExpressions; +open System.Text.RegularExpressions open System.Diagnostics.CodeAnalysis open Argu +open Mbackup.Lib + let ExitBadParam = 1 let ExitTimeout = 2 let ExitIOError = 3 @@ -41,69 +43,6 @@ with | Node_Name _ -> "local node's name, used in remote logging" | Ssh_Key _ -> "ssh private key, used when backup to remote ssh node" -type Logger() = - let mutable level = Logger.DEBUG - - static member DEBUG = 10 - static member INFO = 20 - static member WARNING = 30 - static member ERROR = 40 - static member LevelToString level = - match level with - | 10 -> "DEBUG" - | 20 -> "INFO" - | 30 -> "WARNING" - | 40 -> "ERROR" - | _ -> failwith (sprintf "Unknown log level: %d" level) - - member this.Level = level - - member this.LogMaybe level fmt = - if this.Level <= level then - Printf.ksprintf ( - fun s -> - let time = DateTime.Now - printfn "%02d:%02d:%02d %s %s" time.Hour time.Minute time.Second (Logger.LevelToString level) s) - fmt - else - Printf.ksprintf (fun _ -> printfn "") fmt - - member this.SetLevel level = - this.Level = level - member this.Debug = this.LogMaybe Logger.DEBUG - member this.Info = this.LogMaybe Logger.INFO - member this.Warning = this.LogMaybe Logger.WARNING - member this.Error = this.LogMaybe Logger.ERROR - -let GetEnv (varName: string) = Environment.GetEnvironmentVariable varName - -let GetEnvDefault (varName: string) (defaultValue: string) = - let value = Environment.GetEnvironmentVariable varName - match value with - | null -> defaultValue - | "" -> defaultValue - | _ -> value - -// Convert windows path to Mingw64 path. -// Supported windows path: C:\foo, C:/foo, /c/foo -// MingwPath format: /cygdrive/c/foo -let ToMingwPath (windowsPath: string) = - let pattern = Regex("^/([c-zC-Z])/", RegexOptions.None) - let result = - if pattern.IsMatch(windowsPath) then - "/cygdrive" + windowsPath - else - let pattern = Regex("^([c-zC-Z]):", RegexOptions.None) - if pattern.IsMatch(windowsPath) then - let result = windowsPath.Replace('\\', '/') - "/cygdrive/" + result.Substring(0, 1).ToLower() + result.Substring(2) - else - windowsPath - result - -let EnsureDir (path: string) = if path.EndsWith "/" then path else path + "/" -let EnsureWinDir (path: string) = if path.EndsWith "\\" then path else path + "\\" - let appDataRoamingDir = Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData) |> ToMingwPath |> EnsureDir let programDataDirWin = GetEnv "PROGRAMDATA" |> EnsureWinDir let programDataDir = ToMingwPath programDataDirWin @@ -119,12 +58,14 @@ let userHomeWin = let userHome = userHomeWin |> ToMingwPath -//let userConfigDir = appDataRoamingDir + "mbackup/" let userConfigDirWin = programDataDirWin + "mbackup\\" let userConfigDir = programDataDir + "mbackup/" let runtimeDirWin = appDataLocalDirWin + "mbackup\\" let runtimeDir = appDataLocalDir + "mbackup/" +let mbackupConfigFile = userConfigDirWin + "mbackup.ini" +let mbackupConfig: unit = parseMbackupConfig mbackupConfigFile + let isLocalTarget (target: string) = target.StartsWith "/" // expand user file to mingw64 rsync supported path. @@ -186,16 +127,16 @@ let generateMbackupList (logger: Logger) = // skip and give a warning on non-absolute path. // For user-default.list, auto prefix user's home dir, auto expand Documents, Downloads etc special folder. File.WriteAllLines(mbackupList, allLines) - logger.Info "%s written" mbackupList + logger.Info "mbackup.list file written: %s" mbackupList true with + | :? System.IO.IOException as ex -> + logger.Error "Read/write file failed: %s %s" ex.Source ex.Message + false | ex -> logger.Error "Read/write mbackup list file failed: %s" ex.Message false -// append string s to list if pred is true -let appendWhen (pred: bool) (lst: string list) (s: string) = if pred then List.append lst [s] else lst - [] let main argv = let errorHandler = ProcessExiter(colorizer = function ErrorCode.HelpText -> None | _ -> Some ConsoleColor.Red) @@ -206,8 +147,8 @@ let main argv = let logger = Logger() - logger.Info "using user config dir: %s" userConfigDirWin - logger.Info "using runtime dir: %s" runtimeDirWin + logger.Info "user config dir: %s" userConfigDirWin + logger.Info "runtime dir: %s" runtimeDirWin let rsyncCmd: string list = [] let rsyncCmd = appendWhen dryRun rsyncCmd "--dry-run" @@ -257,16 +198,16 @@ let main argv = let rsyncCmd = List.append rsyncCmd ["/"] let rsyncCmd = List.append rsyncCmd [backupTarget] let rsyncArgs = rsyncCmd |> String.concat " " - // TODO print rsyncArgs in the same Info call when Info supports multiple params. - logger.Info "Note: if you run the following rsync command yourself, make sure the generated file list (%s) is up-to-date." mbackupFile + let rsyncExe = mbackupInstallDirWinTest + "rsync-w64\\usr\\bin\\rsync.exe" let echoExe = "C:\\Program Files\\Git\\usr\\bin\\echo.exe" try IO.Directory.CreateDirectory(runtimeDir) |> ignore IO.Directory.CreateDirectory(userConfigDir) |> ignore let proc = Process.Start(rsyncExe, rsyncArgs) - logger.Info "%s" (rsyncExe + " " + rsyncArgs) + logger.Info "Note: if you run the following rsync command yourself, make sure the generated file list (%s) is up-to-date.\n%s" mbackupFile (rsyncExe + " " + rsyncArgs) if proc.WaitForExit Int32.MaxValue then + logger.Info "mbackup exit" proc.ExitCode else logger.Error "mbackup timed out while waiting for rsync to complete%s" "" diff --git a/mbackup-for-windows.fsproj b/mbackup-for-windows.fsproj index 349c578..32482b9 100644 --- a/mbackup-for-windows.fsproj +++ b/mbackup-for-windows.fsproj @@ -7,6 +7,7 @@ + diff --git a/mbackup-tests/MbackupTest.fs b/mbackup-tests/MbackupTest.fs index f10b626..3efab48 100644 --- a/mbackup-tests/MbackupTest.fs +++ b/mbackup-tests/MbackupTest.fs @@ -1,7 +1,7 @@ module MbackupTests open NUnit.Framework -open Mbackup +open Mbackup.Lib [] let Setup () = @@ -25,3 +25,23 @@ let TestToMingwPath () = Assert.That("/cygdrive/c/foo", Is.EqualTo(ToMingwPath "/c/foo")) Assert.That("/cygdrive/D/foo", Is.EqualTo(ToMingwPath "/D/foo")) Assert.That("/var/log", Is.EqualTo(ToMingwPath "/var/log")) + +let mysprintf fmt = sprintf fmt + +[] +let TestMyprintf () = + Assert.That("123", Is.EqualTo(mysprintf "123")) + Assert.That("123", Is.EqualTo(mysprintf "%d" 123)) + Assert.That("123 456", Is.EqualTo(mysprintf "%d %d" 123 456)) + +let mylogger fmt = + let doAfter s = + "INFO " + s + Printf.ksprintf doAfter fmt + +[] +let TestMylogger () = + Assert.That("INFO 123", Is.EqualTo(mylogger "123")) + Assert.That("INFO 123", Is.EqualTo(mylogger "%d" 123)) + Assert.That("INFO 123 456", Is.EqualTo(mylogger "%d %d" 123 456)) + Assert.That("INFO 123 456 a", Is.EqualTo(mylogger "%d %d %s" 123 456 "a")) diff --git a/operational b/operational index 7483c5a..d28b4da 100644 --- a/operational +++ b/operational @@ -38,6 +38,9 @@ remote logging works. https://docs.microsoft.com/en-us/dotnet/fsharp/language-reference/ F# Collection Types - F# | Microsoft Docs https://docs.microsoft.com/en-us/dotnet/fsharp/language-reference/fsharp-collection-types + Modules - F# | Microsoft Docs + https://docs.microsoft.com/en-us/dotnet/fsharp/language-reference/modules + https://docs.microsoft.com/en-us/dotnet/fsharp/language-reference/namespaces fsharp-cheatsheet https://dungpa.github.io/fsharp-cheatsheet/ Literals - F# | Microsoft Docs @@ -54,20 +57,41 @@ remote logging works. MyPictures DesktopDirectory Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData) + DateTime.ToString Method (System) | Microsoft Docs + https://docs.microsoft.com/en-us/dotnet/api/system.datetime.tostring?view=netframework-4.8 + Standard Date and Time Format Strings | Microsoft Docs + https://docs.microsoft.com/en-us/dotnet/standard/base-types/standard-date-and-time-format-strings?view=netframework-4.8 - FSharpLint http://fsprojects.github.io/FSharpLint/ http://fsprojects.github.io/FSharpLint/index.html +- FSharp.Configuration + http://fsprojects.github.io/FSharp.Configuration/ + dotnet add package FSharp.Configuration --version 1.5.0 + http://fsprojects.github.io/FSharp.Configuration/IniTypeProvider.html + I will use the ini provider. + user don't need to escape string or pay attension to spaces in ini file. + this lib doesn't work with dotnet core 3. +- Formatted text using printf | F# for fun and profit + https://fsharpforfunandprofit.com/posts/printf/ +- + ** 2019-11-13 install dir layout. C:\Program Files\mbackup\rsync-w64\usr\bin\rsync.exe C:\Program Files\mbackup\rsync-w64\usr\bin\ssh.exe C:\ProgramData\mbackup\mbackup-default.exclude C:\ProgramData\mbackup\mbackup-default.list C:\ProgramData\mbackup\user-default.list +C:\ProgramData\mbackup\mbackup.ini * later :entry: ** 2019-11-14 supports expand Downloads dir in user-default.list * current :entry: ** +** 2019-11-14 support mbackup.ini config. +options to support in ini file. +[mbackup] +target=user@host:port/path/ + ** 2019-11-13 next todos - DONE fix TODOs in F# code - DONE add rsync command and arguments for running in windows. @@ -83,6 +107,69 @@ C:\ProgramData\mbackup\user-default.list command line param > env var > mbackup default value. support --node-name param. - test run in console and scheduled task. + run in console works. + try run in scheduled task. + + SCHTASKS /Create /? + # run mbackup 15m after user logon. + SCHTASKS /Create /U /SC ONLOGON /TN mbackup-logon /TR "\"\" \"args\"" /DELAY 15:00 + # run mbackup at 10am and 4pm. + SCHTASKS /Create /U /SC DAILY /TN mbackup-morning /TR "\"\" \"args\"" /ST 10:00 /ET 13:00 /K + SCHTASKS /Create /U /SC DAILY /TN mbackup-afternoon /TR "\"\" \"args\"" /ST 16:00 /ET 19:00 /K + + # debug purpose, one time only + SCHTASKS /Create /U /SC ONCE /TN mbackup-afternoon /TR "\"\" \"args\"" /ST 12:03 + + - problems + - how many scheduled task to run on a multi-user PC? + each user have its own user-default.list expansion. + should I iter over all users on PC? + I think only current user can get it's profile dir and special dirs. + - maybe config mbackup to run after user logon. with 15minute delay. + always run as current user. + - how to not require any param when running mbackup.exe? + put TARGET and other option in a config file? + define system level TARGET env variable. + + use config file is easier for user to edit and making the change effective. + userConfigDir / mbackup.conf + + search: F# read config file + FSharp.Configuration + http://fsprojects.github.io/FSharp.Configuration/ + - FSharp.Configuration missing reference to System.Runtime.Caching + search: how to reference System.Runtime.Caching for dotnet core project + https://www.nuget.org/packages/System.Runtime.Caching/ + dotnet add package System.Runtime.Caching --version 4.6.0 + should I add 4.0.0? Can I use a higher version? + + still not compatible. + search: use FSharp.Configuration with dotnet core 3 + - give up on FSharp.Configuration. + - try this: + FsConfig + https://www.demystifyfp.com/FsConfig/ + AppSettings is only supported in V0.0.6 or below. + try F# AppSettings directly. + If nothing is easy to use, write my own parser. + Support similar config format as python wells lib. + - ConfigurationManager.AppSettings Property (System.Configuration) | Microsoft Docs + https://docs.microsoft.com/en-us/dotnet/api/system.configuration.configurationmanager.appsettings?view=netframework-4.8 + where should I save the App.config xml file? + search: what is app.config + App.Config: Basics and Best Practices - SubMain Blog + https://blog.submain.com/app-config-basics-best-practices/ + What is App.config in C#.NET? How to use it? - Stack Overflow + https://stackoverflow.com/questions/13043530/what-is-app-config-in-c-net-how-to-use-it + Okay. This is not what I want. + This is for application configuration (rarely change), not for user configuration (can change any time). + dotnet will create .exe.config from your App.config file. + - how to use multiple files in F# dotnet core project? + search: f# module and namespace + search: f# module + Modules - F# | Microsoft Docs + https://docs.microsoft.com/en-us/dotnet/fsharp/language-reference/modules + - create an installer. The installer should add scheduled task on install and delete scheduled task on removal. -- GitLab