added support for mac agent
This commit is contained in:
parent
c43f64e785
commit
e49b8c2349
4 changed files with 346 additions and 133 deletions
344
agent/agent_darwin.go
Normal file
344
agent/agent_darwin.go
Normal file
|
|
@ -0,0 +1,344 @@
|
||||||
|
/*
|
||||||
|
Copyright 2022 AmidaWare LLC.
|
||||||
|
|
||||||
|
Licensed under the Tactical RMM License Version 1.0 (the “License”).
|
||||||
|
You may only use the Licensed Software in accordance with the License.
|
||||||
|
A copy of the License is available at:
|
||||||
|
|
||||||
|
https://license.tacticalrmm.com
|
||||||
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
package agent
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bufio"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"runtime"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"syscall"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
rmm "github.com/amidaware/rmmagent/shared"
|
||||||
|
ps "github.com/elastic/go-sysinfo"
|
||||||
|
"github.com/jaypipes/ghw"
|
||||||
|
"github.com/kardianos/service"
|
||||||
|
"github.com/shirou/gopsutil/v3/cpu"
|
||||||
|
"github.com/shirou/gopsutil/v3/disk"
|
||||||
|
psHost "github.com/shirou/gopsutil/v3/host"
|
||||||
|
"github.com/spf13/viper"
|
||||||
|
trmm "github.com/wh1te909/trmm-shared"
|
||||||
|
)
|
||||||
|
|
||||||
|
func ShowStatus(version string) {
|
||||||
|
fmt.Println(version)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *Agent) GetDisks() []trmm.Disk {
|
||||||
|
ret := make([]trmm.Disk, 0)
|
||||||
|
partitions, err := disk.Partitions(false)
|
||||||
|
if err != nil {
|
||||||
|
a.Logger.Debugln(err)
|
||||||
|
return ret
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, p := range partitions {
|
||||||
|
if strings.Contains(p.Device, "dev/loop") {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
usage, err := disk.Usage(p.Mountpoint)
|
||||||
|
if err != nil {
|
||||||
|
a.Logger.Debugln(err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
d := trmm.Disk{
|
||||||
|
Device: p.Device,
|
||||||
|
Fstype: p.Fstype,
|
||||||
|
Total: ByteCountSI(usage.Total),
|
||||||
|
Used: ByteCountSI(usage.Used),
|
||||||
|
Free: ByteCountSI(usage.Free),
|
||||||
|
Percent: int(usage.UsedPercent),
|
||||||
|
}
|
||||||
|
ret = append(ret, d)
|
||||||
|
|
||||||
|
}
|
||||||
|
return ret
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *Agent) SystemRebootRequired() (bool, error) { return false, nil }
|
||||||
|
|
||||||
|
func (a *Agent) LoggedOnUser() string { return "" }
|
||||||
|
|
||||||
|
func (a *Agent) osString() string {
|
||||||
|
h, err := psHost.Info()
|
||||||
|
if err != nil {
|
||||||
|
return "error getting host info"
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("%s %s %s %s", strings.Title(h.Platform), h.PlatformVersion, h.KernelArch, h.KernelVersion)
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewAgentConfig() *rmm.AgentConfig {
|
||||||
|
viper.SetConfigName("tacticalagent")
|
||||||
|
viper.SetConfigType("json")
|
||||||
|
viper.AddConfigPath("/etc/")
|
||||||
|
viper.AddConfigPath(".")
|
||||||
|
err := viper.ReadInConfig()
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return &rmm.AgentConfig{}
|
||||||
|
}
|
||||||
|
|
||||||
|
agentpk := viper.GetString("agentpk")
|
||||||
|
pk, _ := strconv.Atoi(agentpk)
|
||||||
|
|
||||||
|
ret := &rmm.AgentConfig{
|
||||||
|
BaseURL: viper.GetString("baseurl"),
|
||||||
|
AgentID: viper.GetString("agentid"),
|
||||||
|
APIURL: viper.GetString("apiurl"),
|
||||||
|
Token: viper.GetString("token"),
|
||||||
|
AgentPK: agentpk,
|
||||||
|
PK: pk,
|
||||||
|
Cert: viper.GetString("cert"),
|
||||||
|
Proxy: viper.GetString("proxy"),
|
||||||
|
CustomMeshDir: viper.GetString("meshdir"),
|
||||||
|
}
|
||||||
|
return ret
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *Agent) RunScript(code string, shell string, args []string, timeout int) (stdout, stderr string, exitcode int, e error) {
|
||||||
|
code = removeWinNewLines(code)
|
||||||
|
content := []byte(code)
|
||||||
|
|
||||||
|
f, err := createTmpFile()
|
||||||
|
if err != nil {
|
||||||
|
a.Logger.Errorln("RunScript createTmpFile()", err)
|
||||||
|
return "", err.Error(), 85, err
|
||||||
|
}
|
||||||
|
defer os.Remove(f.Name())
|
||||||
|
|
||||||
|
if _, err := f.Write(content); err != nil {
|
||||||
|
a.Logger.Errorln(err)
|
||||||
|
return "", err.Error(), 85, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := f.Close(); err != nil {
|
||||||
|
a.Logger.Errorln(err)
|
||||||
|
return "", err.Error(), 85, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := os.Chmod(f.Name(), 0770); err != nil {
|
||||||
|
a.Logger.Errorln(err)
|
||||||
|
return "", err.Error(), 85, err
|
||||||
|
}
|
||||||
|
|
||||||
|
opts := a.NewCMDOpts()
|
||||||
|
opts.IsScript = true
|
||||||
|
opts.Shell = f.Name()
|
||||||
|
opts.Args = args
|
||||||
|
opts.Timeout = time.Duration(timeout)
|
||||||
|
out := a.CmdV2(opts)
|
||||||
|
retError := ""
|
||||||
|
if out.Status.Error != nil {
|
||||||
|
retError += CleanString(out.Status.Error.Error())
|
||||||
|
retError += "\n"
|
||||||
|
}
|
||||||
|
if len(out.Stderr) > 0 {
|
||||||
|
retError += out.Stderr
|
||||||
|
}
|
||||||
|
return out.Stdout, retError, out.Status.Exit, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func SetDetached() *syscall.SysProcAttr {
|
||||||
|
return &syscall.SysProcAttr{Setpgid: true}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *Agent) AgentUpdate(url, inno, version string) {}
|
||||||
|
|
||||||
|
func (a *Agent) AgentUninstall(code string) {}
|
||||||
|
|
||||||
|
func (a *Agent) NixMeshNodeID() string { return "" }
|
||||||
|
|
||||||
|
func (a *Agent) getMeshNodeID() (string, error) { return "", nil }
|
||||||
|
|
||||||
|
func (a *Agent) RecoverMesh() {}
|
||||||
|
|
||||||
|
func (a *Agent) GetWMIInfo() map[string]interface{} {
|
||||||
|
wmiInfo := make(map[string]interface{})
|
||||||
|
ips := make([]string, 0)
|
||||||
|
disks := make([]string, 0)
|
||||||
|
cpus := make([]string, 0)
|
||||||
|
gpus := make([]string, 0)
|
||||||
|
|
||||||
|
// local ips
|
||||||
|
host, err := ps.Host()
|
||||||
|
if err != nil {
|
||||||
|
a.Logger.Errorln("GetWMIInfo() ps.Host()", err)
|
||||||
|
} else {
|
||||||
|
for _, ip := range host.Info().IPs {
|
||||||
|
if strings.Contains(ip, "127.0.") || strings.Contains(ip, "::1/128") {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
ips = append(ips, ip)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
wmiInfo["local_ips"] = ips
|
||||||
|
|
||||||
|
// disks
|
||||||
|
block, err := ghw.Block(ghw.WithDisableWarnings())
|
||||||
|
if err != nil {
|
||||||
|
a.Logger.Errorln("ghw.Block()", err)
|
||||||
|
} else {
|
||||||
|
for _, disk := range block.Disks {
|
||||||
|
if disk.IsRemovable || strings.Contains(disk.Name, "ram") {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
ret := fmt.Sprintf("%s %s %s %s %s %s", disk.Vendor, disk.Model, disk.StorageController, disk.DriveType, disk.Name, ByteCountSI(disk.SizeBytes))
|
||||||
|
ret = strings.TrimSpace(strings.ReplaceAll(ret, "unknown", ""))
|
||||||
|
disks = append(disks, ret)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
wmiInfo["disks"] = disks
|
||||||
|
|
||||||
|
// cpus
|
||||||
|
cpuInfo, err := cpu.Info()
|
||||||
|
if err != nil {
|
||||||
|
a.Logger.Errorln("cpu.Info()", err)
|
||||||
|
} else {
|
||||||
|
if len(cpuInfo) > 0 {
|
||||||
|
if cpuInfo[0].ModelName != "" {
|
||||||
|
cpus = append(cpus, cpuInfo[0].ModelName)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
wmiInfo["cpus"] = cpus
|
||||||
|
|
||||||
|
// make/model
|
||||||
|
wmiInfo["make_model"] = ""
|
||||||
|
chassis, err := ghw.Chassis(ghw.WithDisableWarnings())
|
||||||
|
if err != nil {
|
||||||
|
a.Logger.Errorln("ghw.Chassis()", err)
|
||||||
|
} else {
|
||||||
|
if chassis.Vendor != "" || chassis.Version != "" {
|
||||||
|
wmiInfo["make_model"] = fmt.Sprintf("%s %s", chassis.Vendor, chassis.Version)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// gfx cards
|
||||||
|
|
||||||
|
gpu, err := ghw.GPU(ghw.WithDisableWarnings())
|
||||||
|
if err != nil {
|
||||||
|
a.Logger.Errorln("ghw.GPU()", err)
|
||||||
|
} else {
|
||||||
|
for _, i := range gpu.GraphicsCards {
|
||||||
|
if i.DeviceInfo != nil {
|
||||||
|
ret := fmt.Sprintf("%s %s", i.DeviceInfo.Vendor.Name, i.DeviceInfo.Product.Name)
|
||||||
|
gpus = append(gpus, ret)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
wmiInfo["gpus"] = gpus
|
||||||
|
|
||||||
|
// temp hack for ARM cpu/make/model if rasp pi
|
||||||
|
var makeModel string
|
||||||
|
if strings.Contains(runtime.GOARCH, "arm") {
|
||||||
|
file, _ := os.Open("/proc/cpuinfo")
|
||||||
|
scanner := bufio.NewScanner(file)
|
||||||
|
for scanner.Scan() {
|
||||||
|
if strings.Contains(strings.ToLower(scanner.Text()), "raspberry") {
|
||||||
|
model := strings.Split(scanner.Text(), ":")
|
||||||
|
if len(model) == 2 {
|
||||||
|
makeModel = strings.TrimSpace(model[1])
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(cpus) == 0 {
|
||||||
|
wmiInfo["cpus"] = []string{makeModel}
|
||||||
|
}
|
||||||
|
if makeModel != "" && (wmiInfo["make_model"] == "" || wmiInfo["make_model"] == "unknown unknown") {
|
||||||
|
wmiInfo["make_model"] = makeModel
|
||||||
|
}
|
||||||
|
if len(gpus) == 1 && gpus[0] == "unknown unknown" {
|
||||||
|
wmiInfo["gpus"] = ""
|
||||||
|
}
|
||||||
|
|
||||||
|
return wmiInfo
|
||||||
|
}
|
||||||
|
|
||||||
|
// windows only below TODO add into stub file
|
||||||
|
|
||||||
|
func (a *Agent) PlatVer() (string, error) { return "", nil }
|
||||||
|
|
||||||
|
func (a *Agent) SendSoftware() {}
|
||||||
|
|
||||||
|
func (a *Agent) UninstallCleanup() {}
|
||||||
|
|
||||||
|
func (a *Agent) RunMigrations() {}
|
||||||
|
|
||||||
|
func GetServiceStatus(name string) (string, error) { return "", nil }
|
||||||
|
|
||||||
|
func (a *Agent) GetPython(force bool) {}
|
||||||
|
|
||||||
|
type SchedTask struct{ Name string }
|
||||||
|
|
||||||
|
func (a *Agent) PatchMgmnt(enable bool) error { return nil }
|
||||||
|
|
||||||
|
func (a *Agent) CreateSchedTask(st SchedTask) (bool, error) { return false, nil }
|
||||||
|
|
||||||
|
func DeleteSchedTask(name string) error { return nil }
|
||||||
|
|
||||||
|
func ListSchedTasks() []string { return []string{} }
|
||||||
|
|
||||||
|
func (a *Agent) GetEventLog(logName string, searchLastDays int) []rmm.EventLogMsg {
|
||||||
|
return []rmm.EventLogMsg{}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *Agent) GetServiceDetail(name string) trmm.WindowsService { return trmm.WindowsService{} }
|
||||||
|
|
||||||
|
func (a *Agent) ControlService(name, action string) rmm.WinSvcResp {
|
||||||
|
return rmm.WinSvcResp{Success: false, ErrorMsg: "/na"}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *Agent) EditService(name, startupType string) rmm.WinSvcResp {
|
||||||
|
return rmm.WinSvcResp{Success: false, ErrorMsg: "/na"}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *Agent) GetInstalledSoftware() []trmm.WinSoftwareList { return []trmm.WinSoftwareList{} }
|
||||||
|
|
||||||
|
func (a *Agent) ChecksRunning() bool { return false }
|
||||||
|
|
||||||
|
func (a *Agent) RunTask(id int) error { return nil }
|
||||||
|
|
||||||
|
func (a *Agent) InstallChoco() {}
|
||||||
|
|
||||||
|
func (a *Agent) InstallWithChoco(name string) (string, error) { return "", nil }
|
||||||
|
|
||||||
|
func (a *Agent) GetWinUpdates() {}
|
||||||
|
|
||||||
|
func (a *Agent) InstallUpdates(guids []string) {}
|
||||||
|
|
||||||
|
func (a *Agent) installMesh(meshbin, exe, proxy string) (string, error) {
|
||||||
|
return "not implemented", nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func CMDShell(shell string, cmdArgs []string, command string, timeout int, detached bool) (output [2]string, e error) {
|
||||||
|
return [2]string{"", ""}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func CMD(exe string, args []string, timeout int, detached bool) (output [2]string, e error) {
|
||||||
|
return [2]string{"", ""}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *Agent) GetServices() []trmm.WindowsService { return []trmm.WindowsService{} }
|
||||||
|
|
||||||
|
func (a *Agent) Start(_ service.Service) error { return nil }
|
||||||
|
|
||||||
|
func (a *Agent) Stop(_ service.Service) error { return nil }
|
||||||
|
|
||||||
|
func (a *Agent) InstallService() error { return nil }
|
||||||
|
|
@ -1,133 +0,0 @@
|
||||||
/*
|
|
||||||
Copyright 2022 AmidaWare LLC.
|
|
||||||
|
|
||||||
Licensed under the Tactical RMM License Version 1.0 (the “License”).
|
|
||||||
You may only use the Licensed Software in accordance with the License.
|
|
||||||
A copy of the License is available at:
|
|
||||||
|
|
||||||
https://license.tacticalrmm.com
|
|
||||||
|
|
||||||
*/
|
|
||||||
|
|
||||||
package agent
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"strings"
|
|
||||||
"syscall"
|
|
||||||
|
|
||||||
rmm "github.com/amidaware/rmmagent/shared"
|
|
||||||
"github.com/kardianos/service"
|
|
||||||
psHost "github.com/shirou/gopsutil/v3/host"
|
|
||||||
trmm "github.com/wh1te909/trmm-shared"
|
|
||||||
)
|
|
||||||
|
|
||||||
func ShowStatus(version string) {
|
|
||||||
fmt.Println(version)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (a *Agent) GetDisks() []trmm.Disk { return nil }
|
|
||||||
|
|
||||||
func (a *Agent) SystemRebootRequired() (bool, error) { return false, nil }
|
|
||||||
|
|
||||||
func (a *Agent) LoggedOnUser() string { return "" }
|
|
||||||
|
|
||||||
func (a *Agent) osString() string {
|
|
||||||
h, err := psHost.Info()
|
|
||||||
if err != nil {
|
|
||||||
return "error getting host info"
|
|
||||||
}
|
|
||||||
return fmt.Sprintf("%s %s %s %s", strings.Title(h.Platform), h.PlatformVersion, h.KernelArch, h.KernelVersion)
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewAgentConfig() *rmm.AgentConfig { return nil }
|
|
||||||
|
|
||||||
func (a *Agent) RunScript(code string, shell string, args []string, timeout int) (stdout, stderr string, exitcode int, e error) {
|
|
||||||
return "", "", 0, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func SetDetached() *syscall.SysProcAttr { return nil }
|
|
||||||
|
|
||||||
func (a *Agent) AgentUpdate(url, inno, version string) {}
|
|
||||||
|
|
||||||
func (a *Agent) AgentUninstall(code string) {}
|
|
||||||
|
|
||||||
func (a *Agent) NixMeshNodeID() string { return "" }
|
|
||||||
|
|
||||||
func (a *Agent) getMeshNodeID() (string, error) { return "", nil }
|
|
||||||
|
|
||||||
func (a *Agent) RecoverMesh() {}
|
|
||||||
|
|
||||||
func (a *Agent) GetWMIInfo() map[string]interface{} { return nil }
|
|
||||||
|
|
||||||
// windows only below TODO add into stub file
|
|
||||||
|
|
||||||
func (a *Agent) PlatVer() (string, error) { return "", nil }
|
|
||||||
|
|
||||||
func (a *Agent) SendSoftware() {}
|
|
||||||
|
|
||||||
func (a *Agent) UninstallCleanup() {}
|
|
||||||
|
|
||||||
func (a *Agent) RunMigrations() {}
|
|
||||||
|
|
||||||
func GetServiceStatus(name string) (string, error) { return "", nil }
|
|
||||||
|
|
||||||
func (a *Agent) GetPython(force bool) {}
|
|
||||||
|
|
||||||
type SchedTask struct{ Name string }
|
|
||||||
|
|
||||||
func (a *Agent) PatchMgmnt(enable bool) error { return nil }
|
|
||||||
|
|
||||||
func (a *Agent) CreateSchedTask(st SchedTask) (bool, error) { return false, nil }
|
|
||||||
|
|
||||||
func DeleteSchedTask(name string) error { return nil }
|
|
||||||
|
|
||||||
func ListSchedTasks() []string { return []string{} }
|
|
||||||
|
|
||||||
func (a *Agent) GetEventLog(logName string, searchLastDays int) []rmm.EventLogMsg {
|
|
||||||
return []rmm.EventLogMsg{}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (a *Agent) GetServiceDetail(name string) trmm.WindowsService { return trmm.WindowsService{} }
|
|
||||||
|
|
||||||
func (a *Agent) ControlService(name, action string) rmm.WinSvcResp {
|
|
||||||
return rmm.WinSvcResp{Success: false, ErrorMsg: "/na"}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (a *Agent) EditService(name, startupType string) rmm.WinSvcResp {
|
|
||||||
return rmm.WinSvcResp{Success: false, ErrorMsg: "/na"}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (a *Agent) GetInstalledSoftware() []trmm.WinSoftwareList { return []trmm.WinSoftwareList{} }
|
|
||||||
|
|
||||||
func (a *Agent) ChecksRunning() bool { return false }
|
|
||||||
|
|
||||||
func (a *Agent) RunTask(id int) error { return nil }
|
|
||||||
|
|
||||||
func (a *Agent) InstallChoco() {}
|
|
||||||
|
|
||||||
func (a *Agent) InstallWithChoco(name string) (string, error) { return "", nil }
|
|
||||||
|
|
||||||
func (a *Agent) GetWinUpdates() {}
|
|
||||||
|
|
||||||
func (a *Agent) InstallUpdates(guids []string) {}
|
|
||||||
|
|
||||||
func (a *Agent) installMesh(meshbin, exe, proxy string) (string, error) {
|
|
||||||
return "not implemented", nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func CMDShell(shell string, cmdArgs []string, command string, timeout int, detached bool) (output [2]string, e error) {
|
|
||||||
return [2]string{"", ""}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func CMD(exe string, args []string, timeout int, detached bool) (output [2]string, e error) {
|
|
||||||
return [2]string{"", ""}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (a *Agent) GetServices() []trmm.WindowsService { return []trmm.WindowsService{} }
|
|
||||||
|
|
||||||
func (a *Agent) Start(_ service.Service) error { return nil }
|
|
||||||
|
|
||||||
func (a *Agent) Stop(_ service.Service) error { return nil }
|
|
||||||
|
|
||||||
func (a *Agent) InstallService() error { return nil }
|
|
||||||
2
main.go
2
main.go
|
|
@ -185,6 +185,8 @@ func setupLogging(level, to *string) {
|
||||||
logFile, _ = os.OpenFile(filepath.Join(os.Getenv("ProgramFiles"), "TacticalAgent", "agent.log"), os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0664)
|
logFile, _ = os.OpenFile(filepath.Join(os.Getenv("ProgramFiles"), "TacticalAgent", "agent.log"), os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0664)
|
||||||
case "linux":
|
case "linux":
|
||||||
logFile, _ = os.OpenFile(filepath.Join("/var/log/", "tacticalagent.log"), os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0664)
|
logFile, _ = os.OpenFile(filepath.Join("/var/log/", "tacticalagent.log"), os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0664)
|
||||||
|
case "darwin":
|
||||||
|
logFile, _ = os.OpenFile(filepath.Join("/var/log/", "tacticalagent.log"), os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0664)
|
||||||
}
|
}
|
||||||
log.SetOutput(logFile)
|
log.SetOutput(logFile)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue