首页 > 其他分享 >使用interface化解一场因操作系统不同导致的编译问题

使用interface化解一场因操作系统不同导致的编译问题

时间:2024-05-23 15:07:22浏览次数:25  
标签:Object return 操作系统 err windows cmd 编译 getUsedPortsCmd interface

场景描述


起因

因项目需求,需要编写一个agent, 需支持Linux和Windows操作系统。 Agent里面有一个功能需要获取到服务器上所有已经被占用的端口。

实现方式:针对不同的操作系统,实现方式有所不同

  • linux: 使用服务器自带的 netstat 指令,然后使用 os/exec 库来调用 shell脚本实现
  • windows: windows系统不同在于,使用 exec.Command指令后,需要调用 syscall.SysProcAttrsyscall.LoadDLL, 而这两个方法是windows系统下的专用库。

问题: 这里会出出现一个问题,虽然程序在编译的时候可以通过GOOS来区分编译到指定的操作系统的二进制包, 但是在编译过程中,编译器会进行代码检查,也会加载windows的代码逻辑。


编译争端

初始代码如下:

  1. tools.go
    // get address
    func getAddress(addr string) string {
        var address string
        if strings.Contains(addr, "tcp") {
            address = strings.TrimRight(addr, "tcp")
        } else {
            address = strings.TrimRight(addr, "udp")
        }
        return address
    }
    
    // CollectServerUsedPorts, collect all of the ports that have been used
    func CollectServerUsedPorts(platform string) string {
        var (
            platformLower = strings.ToLower(platform)
            cmd           *exec.Cmd
            err           error
            cmdOutPut     []byte
        )
    
        if platformLower == "linux" {
            // 执行 shell 指令, 获取tcp协议占用的端口
            getUsedPortsCmd := `netstat -tln | awk '{print $4}' | awk -F: '{print $NF}' | egrep -o '[0-9]+' | sort -n | uniq | paste -s -d ","`
            cmd = exec.Command("bash", "-c", getUsedPortsCmd)
    
        } else if platformLower == "windows" {
            // 执行 powershell指令获取已经占用的端口号
            getUsedPortsCmd := SelectScriptByWindowsVersion()
    
            cmd = exec.Command("powershell", "-Command", getUsedPortsCmd)
            cmd.SysProcAttr = &syscall.SysProcAttr{HideWindow: true}
    
        } else {
            cmd = nil
        }
    
        if cmd != nil {
            if cmdOutPut, err = cmd.Output(); err != nil {
                log.Errorf("err to execute command %s,  %s", cmd.String(), err.Error())
                return ""
            }
            return strings.Trim(string(cmdOutPut), "\n")
        }
        return ""
    }
    
    func SelectScriptByWindowsVersion() string {
        var getUsedPortsCmd string
        version, err := getWindowsVersion()
        if err != nil {
            log.Errorf("无法获取Windows版本信息: %s", err.Error())
            return ""
        }
    
        // if system version is lower than windows 8
        if version < 6.2 {
            log.Warnf("Windows 版本低于 Windows 8")
            getUsedPortsCmd = `(netstat -an | Select-String 'LISTENING' | ForEach-Object { $_ -replace '\s+', ' ' } | ForEach-Object { ($_ -split ' ')[2] } | Where-Object {$_ -match '\d'} | ForEach-Object {[int]($_ -split ':')[-1]} | Sort-Object | Get-Unique ) -join ",".Replace("r","")`
        } else {
            getUsedPortsCmd = `((Get-NetTCPConnection | Where-Object {$_.State -eq 'Listen'} | Select-Object -ExpandProperty LocalPort) | Sort-Object {[int]$_} | Get-Unique) -join ",".Replace("r","")`
        }
    
        return getUsedPortsCmd
    }
    
    func getWindowsVersion() (float64, error) {
    
        mod, err := syscall.LoadDLL("kernel32.dll")
        if err != nil {
            return 0, err
        }
        defer func() {
            _ = mod.Release()
        }()
    
        proc, err := mod.FindProc("GetVersion")
        if err != nil {
            return 0, err
        }
    
        version, _, _ := proc.Call()
        majorVersion := byte(version)
        minorVersion := byte(version >> 8)
    
        return float64(majorVersion) + float64(minorVersion)/10, nil
    }
    
  2. 上面代码编译成windows没问题,但是编译linux二进制文件时,会提示:
    # 编译linux二进制文件
    go build -ldflags "-linkmode external -extldflags '-static'" -tags musl -o  main main.go
    
    # 错误输出如下
    windows.go:31:22: undefined: syscall.LoadDLL
    windows.go:56:41: unknown field 'HideWindow' in struct literal of type syscall.SysProcAttr
    
    # 错误原因
    - 内置库syscall,在linux编译时,其syscall.SysProcAttr 结构体并没有`HideWindow`字段;
    - linux 下也没有 syscall.LoadDLL方法
    - 编译和代码执行逻辑不一样,虽然代码有检查系统服务器类型的逻辑,但是编译时需要加载代码中的每一行代码逻辑,
    - 将其编译成汇编,然后再交给计算机执行,所以会出现编译错误
    

矛盾化解

Go语言在编译时除了有对整个项目编译的 参数控制 , 如 参数GOOS=windows表示编译成widnwos系统下的二进制文件。 但是这个参数只能控制项目级别的, 对于上面这种情况,需要控制文件级别的编译, 当然 Go也是支持的,在提取出 windows 逻辑的代码为独立文件,在文件开头使用 // + build windows 语法。修改如下:

  1. used_ports/windows.go如下:
      //  +build windows
    func SelectScriptByWindowsVersion() string {
        var getUsedPortsCmd string
        version, err := getWindowsVersion()
        if err != nil {
            log.Errorf("无法获取Windows版本信息: %s", err.Error())
            return ""
        }
    
        // if system version is lower than windows 8
        if version < 6.2 {
            log.Warnf("Windows 版本低于 Windows 8")
            getUsedPortsCmd = `(netstat -an | Select-String 'LISTENING' | ForEach-Object { $_ -replace '\s+', ' ' } | ForEach-Object { ($_ -split ' ')[2] } | Where-Object {$_ -match '\d'} | ForEach-Object {[int]($_ -split ':')[-1]} | Sort-Object | Get-Unique ) -join ",".Replace("r","")`
        } else {
            getUsedPortsCmd = `((Get-NetTCPConnection | Where-Object {$_.State -eq 'Listen'} | Select-Object -ExpandProperty LocalPort) | Sort-Object {[int]$_} | Get-Unique) -join ",".Replace("r","")`
        }
    
        return getUsedPortsCmd
    }
    
    func getWindowsVersion() (float64, error) {
    
        mod, err := syscall.LoadDLL("kernel32.dll")
        if err != nil {
            return 0, err
        }
        defer func() {
            _ = mod.Release()
        }()
    
        proc, err := mod.FindProc("GetVersion")
        if err != nil {
            return 0, err
        }
    
        version, _, _ := proc.Call()
        majorVersion := byte(version)
        minorVersion := byte(version >> 8)
    
        return float64(majorVersion) + float64(minorVersion)/10, nil
    }
    

  1. 将原来逻辑改成如下:

    • windows.go
      // CollectServerUsedPorts, collect all of the ports that have been used
      func CollectServerUsedPorts(platform string) string {
          var (
              platformLower = strings.ToLower(platform)
              cmd           *exec.Cmd
              err           error
              cmdOutPut     []byte
          )
      
          if platformLower == "linux" {
              // 执行 shell 指令, 获取tcp协议占用的端口
              getUsedPortsCmd := `netstat -tln | awk '{print $4}' | awk -F: '{print $NF}' | egrep -o '[0-9]+' | sort -n | uniq | paste -s -d ","`
              cmd = exec.Command("bash", "-c", getUsedPortsCmd)
          
          } else if platformLower == "windows" {
              // todo: 需要优化,通过接口映射避免编译的问题
              // Linux 编译时需要隐藏下面代码,
              getUsedPortsCmd := used_ports.CollectWindowsUsedPorts()
              cmd = exec.Command("powershell", "-Command", getUsedPortsCmd)
              cmd.SysProcAttr = &syscall.SysProcAttr{HideWindow: true}
      
          } else {
              cmd = nil
          }
      
          if cmd != nil {
              if cmdOutPut, err = cmd.Output(); err != nil {
                  log.Errorf("err to execute command %s,  %s", cmd.String(), err.Error())
                  return ""
              }
              return strings.Trim(string(cmdOutPut), "\n")
          }
          return ""
      }
      

  2. 光修改了上面的逻辑也不行,因为编译的时候代码依然会执行,此时会报如下错误

    tools.go:121:15: undefined: used_ports.CollectWindowsUsedPorts
    
    # 这是因为虽然linux编译时,不会编译 windows.go的文件,同时会导致 模块下的
    #CollectWindowsUsedPorts 方法不存在
    

  3. 最终修复方式如下

    • 编译时考虑使用 定义接口的方式, 针对不同操作系统使用不同的 结构体,然后通过结构实现接口的方式来使其两种操作系统方法来指向同一个接口
    • 使用 接口字典的方式,实现策略模式,不再使用显示的 if 判断语法来做显示判断,这样可以避免编译时显示加载因操作系统带来的编译冲突
    • 使用init() 方式初始化 接口实现
    • 最终代码如下
      /* 实现接口目录如下
      ├── used_ports
      │   ├── linux.go
      │   ├── used_ports.go
      │   └── windows.go
      */
      
    • used_ports.go
      package used_ports
      
      import "os/exec"
      
      type UsedPortCollector interface {
          CollectHaveUsedPorts() *exec.Cmd
      }
      
      var UsedPortCollectorMap = make(map[string]UsedPortCollector, 2)
      
      func Register(platformOS string, collector UsedPortCollector) {
          if _, ok := Find(platformOS); ok {
              return
          }
      
          UsedPortCollectorMap[platformOS] = collector
      }
      
      func Find(platformOS string) (UsedPortCollector, bool) {
      
          c, ok := UsedPortCollectorMap[platformOS]
          return c, ok
      }
      
    • linux.go
      package used_ports
        
      import (
          "os/exec"
      )
        
      func init() {
          Register("linux", newLinuxUsedPorts())
      }
        
      type linuxPortCollectorImpl struct{}
        
      func (w linuxPortCollectorImpl) CollectHaveUsedPorts() *exec.Cmd {
          // 执行 shell 指令, 获取tcp协议占用的端口
          getUsedPortsCmd := `netstat -tln | awk '{print $4}' | awk -F: '{print $NF}' | egrep -o '[0-9]+' | sort -n | uniq | paste -s -d ","`
          cmd := exec.Command("bash", "-c", getUsedPortsCmd)
          return cmd
      }
        
         // newLinuxUsedPorts 返回Windows系统下的端口收集器实例
      func newLinuxUsedPorts() UsedPortCollector {
          return linuxPortCollectorImpl{}
      }
      
      • windows.go
      //  +build windows
      
      package used_ports
      
      import (
          "os/exec"
          "syscall"
      )
      
      func init() {
          Register("windows", newWindowsCollector())
      }
      
      // 结构体
      type windowsPortCollectorImpl struct{}
      
      // 实现接口方法
      func (w windowsPortCollectorImpl) CollectHaveUsedPorts() *exec.Cmd {
          // 执行 powershell指令获取已经占用的端口号
          getUsedPortsCmd := selectScriptByWindowsVersion()
      
          cmd := exec.Command("powershell", "-Command", getUsedPortsCmd)
          cmd.SysProcAttr = &syscall.SysProcAttr{HideWindow: true}
      
          return cmd
      }
      
      // newWindowsCollector 返回Windows系统下的端口收集器实例,算是工厂方法
      func newWindowsCollector() UsedPortCollector {
          return windowsPortCollectorImpl{}
      }
      
      func selectScriptByWindowsVersion() string {
          var getUsedPortsCmd string
          version, err := getWindowsVersion()
          if err != nil {
              return ""
          }
      
          // if system version is lower than windows 8
          if version < 6.2 {
              getUsedPortsCmd = `(netstat -an | Select-String 'LISTENING' | ForEach-Object { $_ -replace '\s+', ' ' } | ForEach-Object { ($_ -split ' ')[2] } | Where-Object {$_ -match '\d'} | ForEach-Object {[int]($_ -split ':')[-1]} | Sort-Object | Get-Unique ) -join ",".Replace("r","")`
          } else {
              getUsedPortsCmd = `((Get-NetTCPConnection | Where-Object {$_.State -eq 'Listen'} | Select-Object -ExpandProperty LocalPort) | Sort-Object {[int]$_} | Get-Unique) -join ",".Replace("r","")`
          }
      
          return getUsedPortsCmd
      }
      
      func getWindowsVersion() (float64, error) {
      
          mod, err := syscall.LoadDLL("kernel32.dll")
          if err != nil {
              return 0, err
          }
          defer func() {
              _ = mod.Release()
          }()
      
          proc, err := mod.FindProc("GetVersion")
          if err != nil {
              return 0, err
          }
      
          version, _, _ := proc.Call()
          majorVersion := byte(version)
          minorVersion := byte(version >> 8)
      
          return float64(majorVersion) + float64(minorVersion)/10, nil
      }
      
    • tools.go
      ...
      
      // CollectServerUsedPorts, collect all of the ports that have been used
      func CollectServerUsedPorts(platform string) string {
          var (
              platformLower = strings.ToLower(platform)
              cmd           *exec.Cmd
              err           error
              cmdOutPut     []byte
          )
         
         // 策略方法,获取操作系统对应的实例(接口)  
          if portCollector, ok := used_ports.Find(platformLower); ok {
              cmd = portCollector.CollectHaveUsedPorts()
          }
      
          if cmd != nil {
              if cmdOutPut, err = cmd.Output(); err != nil {
                  log.Errorf("err to execute command %s,  %s", cmd.String(), err.Error())
                  return ""
              }
              return strings.Trim(string(cmdOutPut), "\n")
          }
      
          return ""
      }
      

总结

  1. 程序级别的控制可以在 编译时使用 GOOS=windows 来区分编译成对应操作系统的二进制文件
  2. 文件级别的控制可以在文件头上使用 // + build windows进行控制
  3. 代码级别的控制,可以是使用 结构体映射接口的方式进行区分
  4. init()初始化方法的使用
  5. 不同结构体只要实现了同一个接口的所有方法,那么可以使用 字典接口来实现代码层面的控制

标签:Object,return,操作系统,err,windows,cmd,编译,getUsedPortsCmd,interface
From: https://www.cnblogs.com/failymao/p/18208515

相关文章

  • socketserver模块、操作系统、操作系统的发展史
    【一】socketserver模块【1】简介socketserver中包含了两种类,一种为服务类(serverclass):前者提供了许多方法像绑定,监听,运行……(也就是建立连接的过程)。一种为请求处理类(requesthandleclass)专注于如何处理用户所发送的数据(也就是事务逻辑)。......
  • LaTeX 交叉引用的三次编译
    源文件main.tex\documentclass{article}\begin{document}Hereisacitation\cite{example}.\bibliographystyle{plain}\bibliography{references}\end{document}references.bib@article{example,author={AuthorName},title={TitleofthePap......
  • 3562-Qt工程编译说明、GPU核心使用说明
     ......
  • RK3308 SDK 编译 --- ubuntu 22
    ./build.shbuildroot编译问题controller-enumtypes.c:6:1:error:stray'\'inprogram\#include"gstinterpolationcontrolsource.h"^controller-enumtypes.c:6:2:error:stray'#'inprogram\#include"gstinterpolationco......
  • ​一款开源的.NET程序集反编译、编辑和调试神器----dnSpyEx
    思维导航前言dnSpyEx主要功能工具源代码工具下载安装运行创建测试项目并编译成dll程序集使用工具调试程序集中的代码使用工具编辑程序集中的代码使用工具编辑程序集中的IL指令项目源码地址优秀项目和框架精选DotNetGuide技术社区交流群前言说到.NET相关的反编......
  • m基于BP译码算法的LDPC编译码matlab误码率仿真,对比不同的码率
    1.算法仿真效果matlab2022a仿真结果如下:   2.算法涉及理论知识概要      低密度奇偶校验码(Low-DensityParity-CheckCode,LDPC码)是一种高效的前向纠错码,广泛应用于无线通信、数据存储等领域。BP(BeliefPropagation)译码算法,又称为消息传递算法,是LDPC码最常用......
  • 实现“代码可视化”需要了解的前置知识-编译器中端
    1.前言前文实现“代码可视化”需要了解的前置知识-编译器前端介绍了编译器前端知识并附带了小练习,本文将继续介绍编译器中端相关的知识,还是概念+练习的学习方式。中间代码是用来进行程序分析和实现代码可视化的关键数据,了解其生成和优化方式能更好的帮助我们理解程序的执行逻辑,......
  • 创建hello_world节点、编译、运行
    1、在功能包的src目录下创建源文件cd hello_world_ws/src/hello_world_pkg/srctouch hello_world.cpp#include"rclcpp/rclcpp.hpp"intmain(intargc,char**argv){  rclcpp::init(argc,argv);  autonode=std::make_shared<rclcpp::Node>("hello_w......
  • FFMPEG windows版本编译
    安装MSYS2:更新MSYS2系统:pacman-Syu打开正确的终端:?如果您要编译64位版本的FFmpeg)或MSYS2MinGW32-bit(如果您需要编译32位版本)。安装所需组件:?在打开的MinGW-w64终端中,安装编译FFmpeg所需的工具链、开发工具和依赖库:pacman-Sbase-develgitmingw-w64-x86_64-toolchainm......
  • cnetos 7.9 编译安装 jemalloc
    centos7.9编译安装jemalloc编译安装下载地址本人安装的是5.3.0版本下载后解压,cd到解压目录执行:./autogen.sh--prefix=/xx/xxxmakemakeinstall使用编译程序时加上-L/xx/xxx/lib-ljemalloc-Wl,-rpath=/xx/xxx/lib-I/xx/xxx/include/jemalloc程序中的mallo......