mirror of
https://github.com/SagerNet/sing-box.git
synced 2026-10-21 11:33:20 +00:00
Improve process search
This commit is contained in:
parent
7ad8567e67
commit
827f50cd41
19 changed files with 140 additions and 78 deletions
|
|
@ -112,11 +112,11 @@ type FindConnectionOwnerRequest struct {
|
|||
}
|
||||
|
||||
type ConnectionOwner struct {
|
||||
ProcessID uint32
|
||||
UserId int32
|
||||
UserName string
|
||||
ProcessPath string
|
||||
AndroidPackageNames []string
|
||||
ProcessID uint32
|
||||
UserId int32
|
||||
UserName string
|
||||
ProcessPaths []string
|
||||
PackageNames []string
|
||||
}
|
||||
|
||||
type Notification struct {
|
||||
|
|
|
|||
|
|
@ -519,8 +519,8 @@ func (d *DefaultDialer) dialAttribution(ctx context.Context, destination M.Socks
|
|||
ProcessID: metadata.ProcessInfo.ProcessID,
|
||||
UserID: metadata.ProcessInfo.UserId,
|
||||
UserName: metadata.ProcessInfo.UserName,
|
||||
ProcessPath: metadata.ProcessInfo.ProcessPath,
|
||||
PackageNames: metadata.ProcessInfo.AndroidPackageNames,
|
||||
ProcessPaths: metadata.ProcessInfo.ProcessPaths,
|
||||
PackageNames: metadata.ProcessInfo.PackageNames,
|
||||
}
|
||||
}
|
||||
attribution.Rule = metadata.RouteRule
|
||||
|
|
|
|||
|
|
@ -20,26 +20,28 @@ import (
|
|||
const (
|
||||
darwinSnapshotTTL = 200 * time.Millisecond
|
||||
|
||||
darwinXinpgenSize = 24
|
||||
darwinXsocketOffset = 104
|
||||
darwinXinpcbForeignPort = 16
|
||||
darwinXinpcbLocalPort = 18
|
||||
darwinXinpcbVFlag = 44
|
||||
darwinXinpcbForeignAddr = 48
|
||||
darwinXinpcbLocalAddr = 64
|
||||
darwinXinpcbIPv4Addr = 12
|
||||
darwinXsocketUID = 64
|
||||
darwinXsocketLastPID = 68
|
||||
darwinTCPExtraStructSize = 208
|
||||
darwinXinpgenSize = 24
|
||||
darwinXsocketOffset = 104
|
||||
darwinXinpcbForeignPort = 16
|
||||
darwinXinpcbLocalPort = 18
|
||||
darwinXinpcbVFlag = 44
|
||||
darwinXinpcbForeignAddr = 48
|
||||
darwinXinpcbLocalAddr = 64
|
||||
darwinXinpcbIPv4Addr = 12
|
||||
darwinXsocketUID = 64
|
||||
darwinXsocketLastPID = 68
|
||||
darwinXsocketEffectivePID = 72
|
||||
darwinTCPExtraStructSize = 208
|
||||
)
|
||||
|
||||
type darwinConnectionEntry struct {
|
||||
localAddr netip.Addr
|
||||
remoteAddr netip.Addr
|
||||
localPort uint16
|
||||
remotePort uint16
|
||||
pid uint32
|
||||
uid int32
|
||||
localAddr netip.Addr
|
||||
remoteAddr netip.Addr
|
||||
localPort uint16
|
||||
remotePort uint16
|
||||
pid uint32
|
||||
effectivePid uint32
|
||||
uid int32
|
||||
}
|
||||
|
||||
type darwinConnectionMatchKind uint8
|
||||
|
|
@ -105,7 +107,15 @@ func (f *darwinConnectionFinder) find(network string, source netip.AddrPort, des
|
|||
}
|
||||
processPath, err := getExecPathFromPID(entry.pid)
|
||||
if err == nil {
|
||||
owner.ProcessPath = processPath
|
||||
owner.ProcessID = entry.pid
|
||||
owner.ProcessPaths = []string{processPath}
|
||||
if entry.effectivePid != 0 && entry.effectivePid != entry.pid {
|
||||
effectivePath, effectiveErr := getExecPathFromPID(entry.effectivePid)
|
||||
if effectiveErr == nil {
|
||||
owner.ProcessID = entry.effectivePid
|
||||
owner.ProcessPaths = []string{effectivePath, processPath}
|
||||
}
|
||||
}
|
||||
return owner, nil
|
||||
}
|
||||
if fromCache {
|
||||
|
|
@ -186,10 +196,11 @@ func parseDarwinConnectionEntry(inp []byte, so []byte) (darwinConnectionEntry, b
|
|||
return darwinConnectionEntry{}, false
|
||||
}
|
||||
entry := darwinConnectionEntry{
|
||||
remotePort: binary.BigEndian.Uint16(inp[darwinXinpcbForeignPort : darwinXinpcbForeignPort+2]),
|
||||
localPort: binary.BigEndian.Uint16(inp[darwinXinpcbLocalPort : darwinXinpcbLocalPort+2]),
|
||||
pid: binary.NativeEndian.Uint32(so[darwinXsocketLastPID : darwinXsocketLastPID+4]),
|
||||
uid: int32(binary.NativeEndian.Uint32(so[darwinXsocketUID : darwinXsocketUID+4])),
|
||||
remotePort: binary.BigEndian.Uint16(inp[darwinXinpcbForeignPort : darwinXinpcbForeignPort+2]),
|
||||
localPort: binary.BigEndian.Uint16(inp[darwinXinpcbLocalPort : darwinXinpcbLocalPort+2]),
|
||||
pid: binary.NativeEndian.Uint32(so[darwinXsocketLastPID : darwinXsocketLastPID+4]),
|
||||
effectivePid: binary.NativeEndian.Uint32(so[darwinXsocketEffectivePID : darwinXsocketEffectivePID+4]),
|
||||
uid: int32(binary.NativeEndian.Uint32(so[darwinXsocketUID : darwinXsocketUID+4])),
|
||||
}
|
||||
flag := inp[darwinXinpcbVFlag]
|
||||
switch {
|
||||
|
|
|
|||
|
|
@ -88,7 +88,7 @@ func (s *linuxSearcher) FindProcessInfo(ctx context.Context, network string, sou
|
|||
if err != nil {
|
||||
s.logger.DebugContext(ctx, "find process path: ", err)
|
||||
} else {
|
||||
processInfo.ProcessPath = processPath
|
||||
processInfo.ProcessPaths = []string{processPath}
|
||||
}
|
||||
if s.packageManager != nil {
|
||||
appID := uid % 100000
|
||||
|
|
@ -99,7 +99,7 @@ func (s *linuxSearcher) FindProcessInfo(ctx context.Context, network string, sou
|
|||
if packages, loaded := s.packageManager.PackagesByID(appID); loaded {
|
||||
packageNames = append(packageNames, packages...)
|
||||
}
|
||||
processInfo.AndroidPackageNames = common.Uniq(packageNames)
|
||||
processInfo.PackageNames = common.Uniq(packageNames)
|
||||
}
|
||||
return processInfo, nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ import (
|
|||
"github.com/sagernet/sing/common/winiphlpapi"
|
||||
|
||||
"golang.org/x/sys/windows"
|
||||
"golang.org/x/sys/windows/registry"
|
||||
)
|
||||
|
||||
var _ Searcher = (*windowsSearcher)(nil)
|
||||
|
|
@ -36,15 +37,45 @@ func (s *windowsSearcher) Close() error {
|
|||
}
|
||||
|
||||
func (s *windowsSearcher) FindProcessInfo(ctx context.Context, network string, source netip.AddrPort, destination netip.AddrPort) (*adapter.ConnectionOwner, error) {
|
||||
pid, err := winiphlpapi.FindPid(network, source)
|
||||
owner, err := winiphlpapi.FindSocketOwner(network, source)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
path, err := getProcessPath(pid)
|
||||
path, err := getProcessPath(owner.Pid)
|
||||
if err != nil {
|
||||
return &adapter.ConnectionOwner{ProcessID: pid, UserId: -1}, err
|
||||
return &adapter.ConnectionOwner{ProcessID: owner.Pid, UserId: -1}, err
|
||||
}
|
||||
return &adapter.ConnectionOwner{ProcessID: pid, ProcessPath: path, UserId: -1}, nil
|
||||
processPaths := []string{path}
|
||||
if owner.ServiceName != "" {
|
||||
servicePath, serviceErr := getServiceDLLPath(owner.ServiceName)
|
||||
if serviceErr == nil {
|
||||
processPaths = []string{servicePath, path}
|
||||
}
|
||||
}
|
||||
return &adapter.ConnectionOwner{ProcessID: owner.Pid, ProcessPaths: processPaths, UserId: -1}, nil
|
||||
}
|
||||
|
||||
func getServiceDLLPath(serviceName string) (string, error) {
|
||||
serviceKeyPath := `SYSTEM\CurrentControlSet\Services\` + serviceName
|
||||
var lastErr error
|
||||
for _, keyPath := range []string{serviceKeyPath + `\Parameters`, serviceKeyPath} {
|
||||
key, err := registry.OpenKey(registry.LOCAL_MACHINE, keyPath, registry.QUERY_VALUE)
|
||||
if err != nil {
|
||||
lastErr = err
|
||||
continue
|
||||
}
|
||||
value, valueType, err := key.GetStringValue("ServiceDll")
|
||||
key.Close()
|
||||
if err != nil {
|
||||
lastErr = err
|
||||
continue
|
||||
}
|
||||
if valueType == registry.EXPAND_SZ {
|
||||
return registry.ExpandString(value)
|
||||
}
|
||||
return value, nil
|
||||
}
|
||||
return "", lastErr
|
||||
}
|
||||
|
||||
func getProcessPath(pid uint32) (string, error) {
|
||||
|
|
|
|||
|
|
@ -1055,8 +1055,10 @@ func buildConnectionProto(metadata *trafficcontrol.TrackerMetadata) *Connection
|
|||
ProcessId: metadata.Metadata.ProcessInfo.ProcessID,
|
||||
UserId: metadata.Metadata.ProcessInfo.UserId,
|
||||
UserName: metadata.Metadata.ProcessInfo.UserName,
|
||||
ProcessPath: metadata.Metadata.ProcessInfo.ProcessPath,
|
||||
PackageNames: metadata.Metadata.ProcessInfo.AndroidPackageNames,
|
||||
PackageNames: metadata.Metadata.ProcessInfo.PackageNames,
|
||||
}
|
||||
if len(metadata.Metadata.ProcessInfo.ProcessPaths) > 0 {
|
||||
processInfo.ProcessPath = metadata.Metadata.ProcessInfo.ProcessPaths[0]
|
||||
}
|
||||
}
|
||||
return &Connection{
|
||||
|
|
|
|||
|
|
@ -61,10 +61,10 @@ func (c connectionObject) MarshalJSON() ([]byte, error) {
|
|||
}
|
||||
var processPath string
|
||||
if c.Metadata.ProcessInfo != nil {
|
||||
if c.Metadata.ProcessInfo.ProcessPath != "" {
|
||||
processPath = c.Metadata.ProcessInfo.ProcessPath
|
||||
} else if len(c.Metadata.ProcessInfo.AndroidPackageNames) > 0 {
|
||||
processPath = c.Metadata.ProcessInfo.AndroidPackageNames[0]
|
||||
if len(c.Metadata.ProcessInfo.ProcessPaths) > 0 {
|
||||
processPath = c.Metadata.ProcessInfo.ProcessPaths[0]
|
||||
} else if len(c.Metadata.ProcessInfo.PackageNames) > 0 {
|
||||
processPath = c.Metadata.ProcessInfo.PackageNames[0]
|
||||
}
|
||||
if processPath == "" {
|
||||
if c.Metadata.ProcessInfo.UserId != -1 {
|
||||
|
|
|
|||
|
|
@ -33,8 +33,11 @@ func FindConnectionOwner(ipProtocol int32, sourceAddress string, sourcePort int3
|
|||
return nil, err
|
||||
}
|
||||
result := &ConnectionOwner{
|
||||
UserId: owner.UserId,
|
||||
ProcessPath: owner.ProcessPath,
|
||||
UserId: owner.UserId,
|
||||
processPaths: owner.ProcessPaths,
|
||||
}
|
||||
if len(owner.ProcessPaths) > 0 {
|
||||
result.ProcessPath = owner.ProcessPaths[0]
|
||||
}
|
||||
if owner.UserId != -1 && owner.UserName == "" {
|
||||
osUser, _ := user.LookupId(F.ToString(owner.UserId))
|
||||
|
|
|
|||
|
|
@ -160,10 +160,10 @@ func buildOOMConnection(connection *trafficcontrol.TrackerMetadata) oomConnectio
|
|||
}
|
||||
var process string
|
||||
if processInfo := metadata.ProcessInfo; processInfo != nil {
|
||||
if processInfo.ProcessPath != "" {
|
||||
process = processInfo.ProcessPath
|
||||
} else if len(processInfo.AndroidPackageNames) > 0 {
|
||||
process = processInfo.AndroidPackageNames[0]
|
||||
if len(processInfo.ProcessPaths) > 0 {
|
||||
process = processInfo.ProcessPaths[0]
|
||||
} else if len(processInfo.PackageNames) > 0 {
|
||||
process = processInfo.PackageNames[0]
|
||||
}
|
||||
if process == "" {
|
||||
if processInfo.UserId != -1 {
|
||||
|
|
|
|||
|
|
@ -90,9 +90,18 @@ type ConnectionOwner struct {
|
|||
UserId int32
|
||||
UserName string
|
||||
ProcessPath string
|
||||
processPaths []string
|
||||
androidPackageNames []string
|
||||
}
|
||||
|
||||
func (c *ConnectionOwner) SetProcessPaths(paths StringIterator) {
|
||||
c.processPaths = iteratorToArray[string](paths)
|
||||
}
|
||||
|
||||
func (c *ConnectionOwner) ProcessPaths() StringIterator {
|
||||
return newIterator(c.processPaths)
|
||||
}
|
||||
|
||||
func (c *ConnectionOwner) SetAndroidPackageNames(names StringIterator) {
|
||||
c.androidPackageNames = iteratorToArray[string](names)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -223,11 +223,15 @@ func (w *platformInterfaceWrapper) FindConnectionOwner(request *adapter.FindConn
|
|||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
processPaths := result.processPaths
|
||||
if len(processPaths) == 0 && result.ProcessPath != "" {
|
||||
processPaths = []string{result.ProcessPath}
|
||||
}
|
||||
return &adapter.ConnectionOwner{
|
||||
UserId: result.UserId,
|
||||
UserName: result.UserName,
|
||||
ProcessPath: result.ProcessPath,
|
||||
AndroidPackageNames: result.androidPackageNames,
|
||||
UserId: result.UserId,
|
||||
UserName: result.UserName,
|
||||
ProcessPaths: processPaths,
|
||||
PackageNames: result.androidPackageNames,
|
||||
}, nil
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -51,18 +51,19 @@ func (r *Router) searchProcessInfo(ctx context.Context, metadata *adapter.Inboun
|
|||
return
|
||||
}
|
||||
metadata.ProcessInfo = processInfo
|
||||
if processInfo.ProcessPath != "" {
|
||||
if len(processInfo.ProcessPaths) > 0 {
|
||||
processPath := strings.Join(processInfo.ProcessPaths, ", ")
|
||||
if processInfo.UserName != "" {
|
||||
r.logger.InfoContext(ctx, "found process path: ", processInfo.ProcessPath, ", user: ", processInfo.UserName)
|
||||
r.logger.InfoContext(ctx, "found process path: ", processPath, ", user: ", processInfo.UserName)
|
||||
} else if processInfo.UserId != -1 {
|
||||
r.logger.InfoContext(ctx, "found process path: ", processInfo.ProcessPath, ", user id: ", processInfo.UserId)
|
||||
r.logger.InfoContext(ctx, "found process path: ", processPath, ", user id: ", processInfo.UserId)
|
||||
} else {
|
||||
r.logger.InfoContext(ctx, "found process path: ", processInfo.ProcessPath)
|
||||
r.logger.InfoContext(ctx, "found process path: ", processPath)
|
||||
}
|
||||
return
|
||||
}
|
||||
if len(processInfo.AndroidPackageNames) > 0 {
|
||||
r.logger.InfoContext(ctx, "found package name: ", strings.Join(processInfo.AndroidPackageNames, ", "))
|
||||
if len(processInfo.PackageNames) > 0 {
|
||||
r.logger.InfoContext(ctx, "found package name: ", strings.Join(processInfo.PackageNames, ", "))
|
||||
return
|
||||
}
|
||||
if processInfo.UserId != -1 {
|
||||
|
|
|
|||
|
|
@ -25,10 +25,10 @@ func NewPackageNameItem(packageNameList []string) *PackageNameItem {
|
|||
}
|
||||
|
||||
func (r *PackageNameItem) Match(metadata *adapter.InboundContext) bool {
|
||||
if metadata.ProcessInfo == nil || len(metadata.ProcessInfo.AndroidPackageNames) == 0 {
|
||||
if metadata.ProcessInfo == nil || len(metadata.ProcessInfo.PackageNames) == 0 {
|
||||
return false
|
||||
}
|
||||
for _, packageName := range metadata.ProcessInfo.AndroidPackageNames {
|
||||
for _, packageName := range metadata.ProcessInfo.PackageNames {
|
||||
if r.packageMap[packageName] {
|
||||
return true
|
||||
}
|
||||
|
|
|
|||
|
|
@ -39,11 +39,11 @@ func NewPackageNameRegexItem(expressions []string) (*PackageNameRegexItem, error
|
|||
}
|
||||
|
||||
func (r *PackageNameRegexItem) Match(metadata *adapter.InboundContext) bool {
|
||||
if metadata.ProcessInfo == nil || len(metadata.ProcessInfo.AndroidPackageNames) == 0 {
|
||||
if metadata.ProcessInfo == nil || len(metadata.ProcessInfo.PackageNames) == 0 {
|
||||
return false
|
||||
}
|
||||
for _, matcher := range r.matchers {
|
||||
if slices.ContainsFunc(metadata.ProcessInfo.AndroidPackageNames, matcher.MatchString) {
|
||||
if slices.ContainsFunc(metadata.ProcessInfo.PackageNames, matcher.MatchString) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ package rule
|
|||
|
||||
import (
|
||||
"path/filepath"
|
||||
"slices"
|
||||
"strings"
|
||||
|
||||
"github.com/sagernet/sing-box/adapter"
|
||||
|
|
@ -26,10 +27,12 @@ func NewProcessItem(processNameList []string) *ProcessItem {
|
|||
}
|
||||
|
||||
func (r *ProcessItem) Match(metadata *adapter.InboundContext) bool {
|
||||
if metadata.ProcessInfo == nil || metadata.ProcessInfo.ProcessPath == "" {
|
||||
if metadata.ProcessInfo == nil {
|
||||
return false
|
||||
}
|
||||
return r.processMap[filepath.Base(metadata.ProcessInfo.ProcessPath)]
|
||||
return slices.ContainsFunc(metadata.ProcessInfo.ProcessPaths, func(processPath string) bool {
|
||||
return r.processMap[filepath.Base(processPath)]
|
||||
})
|
||||
}
|
||||
|
||||
func (r *ProcessItem) String() string {
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
package rule
|
||||
|
||||
import (
|
||||
"slices"
|
||||
"strings"
|
||||
|
||||
"github.com/sagernet/sing-box/adapter"
|
||||
|
|
@ -29,15 +30,11 @@ func (r *ProcessPathItem) Match(metadata *adapter.InboundContext) bool {
|
|||
if metadata.ProcessInfo == nil {
|
||||
return false
|
||||
}
|
||||
if metadata.ProcessInfo.ProcessPath != "" && r.processMap[metadata.ProcessInfo.ProcessPath] {
|
||||
if slices.ContainsFunc(metadata.ProcessInfo.ProcessPaths, func(processPath string) bool { return r.processMap[processPath] }) {
|
||||
return true
|
||||
}
|
||||
if C.IsAndroid {
|
||||
for _, packageName := range metadata.ProcessInfo.AndroidPackageNames {
|
||||
if r.processMap[packageName] {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return slices.ContainsFunc(metadata.ProcessInfo.PackageNames, func(packageName string) bool { return r.processMap[packageName] })
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ package rule
|
|||
|
||||
import (
|
||||
"regexp"
|
||||
"slices"
|
||||
"strings"
|
||||
|
||||
"github.com/sagernet/sing-box/adapter"
|
||||
|
|
@ -38,11 +39,11 @@ func NewProcessPathRegexItem(expressions []string) (*ProcessPathRegexItem, error
|
|||
}
|
||||
|
||||
func (r *ProcessPathRegexItem) Match(metadata *adapter.InboundContext) bool {
|
||||
if metadata.ProcessInfo == nil || metadata.ProcessInfo.ProcessPath == "" {
|
||||
if metadata.ProcessInfo == nil {
|
||||
return false
|
||||
}
|
||||
for _, matcher := range r.matchers {
|
||||
if matcher.MatchString(metadata.ProcessInfo.ProcessPath) {
|
||||
if slices.ContainsFunc(metadata.ProcessInfo.ProcessPaths, matcher.MatchString) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -38,7 +38,7 @@ type ProcessAttribution struct {
|
|||
ProcessID uint32 `json:"processId,omitempty"`
|
||||
UserID int32 `json:"userId,omitempty"`
|
||||
UserName string `json:"userName,omitempty"`
|
||||
ProcessPath string `json:"processPath,omitempty"`
|
||||
ProcessPaths []string `json:"processPaths,omitempty"`
|
||||
PackageNames []string `json:"packageNames,omitempty"`
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -125,11 +125,11 @@ func (t *resolve1Manager) createMetadata(sender dbus.Sender) adapter.InboundCont
|
|||
|
||||
processPath, err := os.Readlink(F.ToString("/proc/", senderPid, "/exe"))
|
||||
if err == nil {
|
||||
processInfo.ProcessPath = processPath
|
||||
processInfo.ProcessPaths = []string{processPath}
|
||||
} else {
|
||||
processName, readErr := os.ReadFile(F.ToString("/proc/", senderPid, "/comm"))
|
||||
if readErr == nil {
|
||||
processInfo.ProcessPath = strings.TrimSpace(string(processName))
|
||||
processInfo.ProcessPaths = []string{strings.TrimSpace(string(processName))}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -165,8 +165,8 @@ func (t *resolve1Manager) log(sender dbus.Sender, message ...any) {
|
|||
metadata := t.createMetadata(sender)
|
||||
if metadata.ProcessInfo != nil {
|
||||
var prefix string
|
||||
if metadata.ProcessInfo.ProcessPath != "" {
|
||||
prefix = filepath.Base(metadata.ProcessInfo.ProcessPath)
|
||||
if len(metadata.ProcessInfo.ProcessPaths) > 0 {
|
||||
prefix = filepath.Base(metadata.ProcessInfo.ProcessPaths[0])
|
||||
} else if metadata.ProcessInfo.UserName != "" {
|
||||
prefix = F.ToString("user:", metadata.ProcessInfo.UserName)
|
||||
} else if metadata.ProcessInfo.UserId != 0 {
|
||||
|
|
@ -183,8 +183,8 @@ func (t *resolve1Manager) logRequest(sender dbus.Sender, message ...any) context
|
|||
metadata := t.createMetadata(sender)
|
||||
if metadata.ProcessInfo != nil {
|
||||
var prefix string
|
||||
if metadata.ProcessInfo.ProcessPath != "" {
|
||||
prefix = filepath.Base(metadata.ProcessInfo.ProcessPath)
|
||||
if len(metadata.ProcessInfo.ProcessPaths) > 0 {
|
||||
prefix = filepath.Base(metadata.ProcessInfo.ProcessPaths[0])
|
||||
} else if metadata.ProcessInfo.UserName != "" {
|
||||
prefix = F.ToString("user:", metadata.ProcessInfo.UserName)
|
||||
} else if metadata.ProcessInfo.UserId != 0 {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue