首次推送

This commit is contained in:
sansen
2026-07-20 19:01:03 +08:00
parent ea01b9cf99
commit ef1bf61f9f
4484 changed files with 937163 additions and 1 deletions
@@ -0,0 +1,683 @@
package teamspeak
import (
"context"
"errors"
"fmt"
"log"
"log/slog"
"strconv"
"strings"
"time"
"github.com/honeybbq/teamspeak-go/commands"
)
var errNoDataReturnedForClient = errors.New("no data returned for client")
// SendTextMessage sends a text message to a client, channel or server.
func (c *Client) SendTextMessage(targetMode int, targetID uint64, message string) error {
cmd := commands.BuildCommandOrdered("sendtextmessage", [][2]string{
{"targetmode", strconv.Itoa(targetMode)},
{"target", strconv.FormatUint(targetID, 10)},
{"msg", message},
})
return c.SendCommandNoWait(cmd)
}
// ClientMove moves a client to a different channel.
func (c *Client) ClientMove(clid uint16, channelID uint64, password string) error {
params := [][2]string{
{"clid", strconv.Itoa(int(clid))},
{"cid", strconv.FormatUint(channelID, 10)},
}
if password != "" {
params = append(params, [2]string{"cpw", prepareClientPassword(password)})
}
cmd := commands.BuildCommandOrdered("clientmove", params)
return c.ExecCommand(cmd, 10*time.Second)
}
// Poke sends a poke message to a client.
func (c *Client) Poke(clid uint16, message string) error {
cmd := commands.BuildCommandOrdered("clientpoke", [][2]string{
{"clid", strconv.Itoa(int(clid))},
{"msg", message},
})
return c.ExecCommand(cmd, 10*time.Second)
}
// SendVoice sends a raw Opus frame. Codec values: 4 = Opus voice, 5 = Opus music.
func (c *Client) SendVoice(data []byte, codec byte) error {
return c.handler.SendVoicePacket(data, codec)
}
// ClientID returns the client's own ID assigned by the server.
func (c *Client) ClientID() uint16 {
c.mu.Lock()
defer c.mu.Unlock()
return c.clid
}
// GetClientInfo fetches detailed information about a client.
func (c *Client) GetClientInfo(clid uint16) (map[string]string, error) {
cmd := fmt.Sprintf("clientinfo clid=%d", clid)
data, err := c.ExecCommandWithResponse(cmd, 5*time.Second)
if err != nil {
return nil, err
}
if len(data) == 0 {
return nil, fmt.Errorf("%w: %d", errNoDataReturnedForClient, clid)
}
return data[0], nil
}
// ListChannels returns a list of all channels on the server.
func (c *Client) ListChannels() ([]ChannelInfo, error) {
data, err := c.ExecCommandWithResponse("channellist", 5*time.Second)
if err != nil {
log.Printf("[teamspeak-go] ListChannels ExecCommandWithResponse error: %v", err)
return nil, err
}
log.Printf("[teamspeak-go] ListChannels got %d raw data rows", len(data))
for i, item := range data {
log.Printf("[teamspeak-go] ListChannels row[%d]: %v", i, item)
}
channels := make([]ChannelInfo, 0, len(data))
for _, item := range data {
cid, _ := parseUint64Value(item["cid"])
pid, _ := parseUint64Value(item["pid"])
name := item["channel_name"]
channels = append(channels, ChannelInfo{
ID: cid,
ParentID: pid,
Name: commands.Unescape(name),
})
}
return channels, nil
}
// ListChannelsDetailed returns channels with extended properties.
// 通过 channellist -topic -flags -voice -limits -icon 获取更丰富的频道属性。
func (c *Client) ListChannelsDetailed() ([]ChannelInfoDetailed, error) {
data, err := c.ExecCommandWithResponse(
"channellist -topic -flags -voice -limits -icon", 5*time.Second)
if err != nil {
return nil, err
}
channels := make([]ChannelInfoDetailed, 0, len(data))
for _, item := range data {
cid, _ := parseUint64Value(item["cid"])
pid, _ := parseUint64Value(item["pid"])
order, _ := parseUint64Value(item["channel_order"])
name := item["channel_name"]
topic := item["channel_topic"]
codec, _ := parseIntValue(item["channel_codec"])
codecQuality, _ := parseIntValue(item["channel_codec_quality"])
neededTalkPower, _ := parseIntValue(item["channel_needed_talk_power"])
maxClients, _ := parseIntValue(item["channel_maxclients"])
maxFamilyClients, _ := parseIntValue(item["channel_maxfamilyclients"])
iconID, _ := parseInt64Value(item["channel_icon_id"])
neededModifyPower, _ := parseIntValue(item["channel_needed_modify_power"])
channels = append(channels, ChannelInfoDetailed{
ID: cid,
ParentID: pid,
Order: order,
Name: commands.Unescape(name),
Topic: commands.Unescape(topic),
Codec: codec,
CodecQuality: codecQuality,
NeededTalkPower: neededTalkPower,
MaxClients: maxClients,
MaxFamilyClients: maxFamilyClients,
IsMaxClientsUnlimited: parseBoolValue(item["channel_flag_maxclients_unlimited"]),
IsMaxFamilyClientsUnlimited: parseBoolValue(item["channel_flag_maxfamilyclients_unlimited"]),
IsOrdered: order > 0,
IsPermanent: parseBoolValue(item["channel_flag_permanent"]),
IsSemiPermanent: parseBoolValue(item["channel_flag_semi_permanent"]),
IsDefault: parseBoolValue(item["channel_flag_default"]),
IsPassword: parseBoolValue(item["channel_flag_password"]),
HasPassword: parseBoolValue(item["channel_flag_password"]),
NeededModifyPower: neededModifyPower,
IconID: iconID,
})
}
return channels, nil
}
// ListClients returns a list of all clients currently connected to the server.
func (c *Client) ListClients() ([]ClientInfo, error) {
data, err := c.ExecCommandWithResponse("clientlist -uid -away -voice -groups", 5*time.Second)
if err != nil {
return nil, err
}
clients := make([]ClientInfo, 0, len(data))
for _, item := range data {
clid, _ := parseUint16Value(item["clid"])
nick := item["client_nickname"]
cid, _ := parseUint64Value(item["cid"])
uid := item["client_unique_identifier"]
clientType, _ := strconv.Atoi(item["client_type"])
groupsStr := item["client_servergroups"]
groups := make([]string, 0)
if groupsStr != "" {
groups = strings.Split(groupsStr, ",")
}
clients = append(clients, ClientInfo{
ID: clid,
Nickname: commands.Unescape(nick),
ChannelID: cid,
UID: uid,
Type: clientType,
ServerGroups: groups,
})
}
return clients, nil
}
// WaitConnected waits for the connection handshake to be completed.
// Returns nil on success, or the handshake/disconnect error on failure.
func (c *Client) WaitConnected(ctx context.Context) error {
c.logger.Info("WaitConnected: waiting for handshake to complete")
select {
case <-c.connectedChan:
c.logger.Info("WaitConnected: channel unblocked", slog.Any("err", c.connectedErr))
return c.connectedErr
case <-ctx.Done():
c.logger.Warn("WaitConnected: context done", slog.Any("err", ctx.Err()))
return ctx.Err()
}
}
// ─── 服务器查询 ────────────────────────────────────────────
// GetServerInfo returns server details.
// 协议命令: serverinfo
func (c *Client) GetServerInfo() (*ServerInfo, error) {
data, err := c.ExecCommandWithResponse("serverinfo", 5*time.Second)
if err != nil {
return nil, err
}
if len(data) == 0 {
return nil, fmt.Errorf("serverinfo: %w", errNoDataReturnedForClient)
}
item := data[0]
maxClients, _ := parseIntValue(item["virtualserver_maxclients"])
clientsOnline, _ := parseIntValue(item["virtualserver_clientsonline"])
channelsOnline, _ := parseIntValue(item["virtualserver_channelsonline"])
uptime, _ := parseInt64Value(item["virtualserver_uptime"])
created, _ := parseInt64Value(item["virtualserver_created"])
iconID, _ := parseInt64Value(item["virtualserver_icon_id"])
defServerGroup, _ := parseIntValue(item["virtualserver_default_server_group"])
defChannelGroup, _ := parseIntValue(item["virtualserver_default_channel_group"])
return &ServerInfo{
Name: commands.Unescape(item["virtualserver_name"]),
WelcomeMessage: commands.Unescape(item["virtualserver_welcomemessage"]),
MaxClients: maxClients,
ClientsOnline: clientsOnline,
ChannelsOnline: channelsOnline,
Uptime: uptime,
Version: item["virtualserver_version"],
Platform: item["virtualserver_platform"],
Created: created,
IconID: iconID,
DefaultServerGroup: defServerGroup,
DefaultChannelGroup: defChannelGroup,
}, nil
}
// ─── 频道查询 / 管理 ──────────────────────────────────────
// GetChannelInfo returns detailed info for a single channel.
// 协议命令: channelinfo cid=X
// 与 ListChannelsDetailed 的区别:本方法返回单频道的完整信息(含 description),
// ListChannelsDetailed 返回所有频道的部分属性。
func (c *Client) GetChannelInfo(channelID uint64) (*ChannelDetailInfo, error) {
cmd := fmt.Sprintf("channelinfo cid=%d", channelID)
data, err := c.ExecCommandWithResponse(cmd, 5*time.Second)
if err != nil {
return nil, err
}
if len(data) == 0 {
return nil, fmt.Errorf("channelinfo: %w", errNoDataReturnedForClient)
}
item := data[0]
cid, _ := parseUint64Value(item["cid"])
pid, _ := parseUint64Value(item["channel_order"])
order, _ := parseUint64Value(item["channel_order"])
codec, _ := parseIntValue(item["channel_codec"])
codecQuality, _ := parseIntValue(item["channel_codec_quality"])
maxClients, _ := parseIntValue(item["channel_maxclients"])
maxFamilyClients, _ := parseIntValue(item["channel_maxfamilyclients"])
neededTalkPower, _ := parseIntValue(item["channel_needed_talk_power"])
iconID, _ := parseInt64Value(item["channel_icon_id"])
return &ChannelDetailInfo{
ID: cid,
ParentID: pid,
Order: order,
Name: commands.Unescape(item["channel_name"]),
Topic: commands.Unescape(item["channel_topic"]),
Description: commands.Unescape(item["channel_description"]),
Codec: codec,
CodecQuality: codecQuality,
MaxClients: maxClients,
MaxFamilyClients: maxFamilyClients,
NeededTalkPower: neededTalkPower,
IconID: iconID,
IsPermanent: parseBoolValue(item["channel_flag_permanent"]),
IsSemiPermanent: parseBoolValue(item["channel_flag_semi_permanent"]),
IsDefault: parseBoolValue(item["channel_flag_default"]),
IsPassword: parseBoolValue(item["channel_flag_password"]),
BannerGfxURL: item["channel_banner_gfx_url"],
}, nil
}
// FindChannels searches channels by name pattern.
// 协议命令: channelfind pattern=X
func (c *Client) FindChannels(pattern string) ([]ChannelInfo, error) {
cmd := commands.BuildCommand("channelfind", map[string]string{"pattern": pattern})
data, err := c.ExecCommandWithResponse(cmd, 5*time.Second)
if err != nil {
return nil, err
}
channels := make([]ChannelInfo, 0, len(data))
for _, item := range data {
cid, _ := parseUint64Value(item["cid"])
channels = append(channels, ChannelInfo{
ID: cid,
Name: commands.Unescape(item["channel_name"]),
})
}
return channels, nil
}
// CreateChannel creates a new channel.
// 协议命令: channelcreate channel_name=X [params...]
// 返回新建频道的 ID。
// 常用可选参数通过 options map 传递,例如:
// - "channel_topic": "主题"
// - "channel_flag_permanent": "1"
// - "channel_password": "密码"
// - "cpid": "父频道ID"
func (c *Client) CreateChannel(name string, options map[string]string) (uint64, error) {
params := [][2]string{{"channel_name", name}}
for k, v := range options {
params = append(params, [2]string{k, v})
}
cmd := commands.BuildCommandOrdered("channelcreate", params)
data, err := c.ExecCommandWithResponse(cmd, 5*time.Second)
if err != nil {
return 0, err
}
if len(data) == 0 {
return 0, fmt.Errorf("channelcreate: no cid returned")
}
cid, _ := parseUint64Value(data[0]["cid"])
return cid, nil
}
// EditChannel modifies channel properties.
// 协议命令: channeledit cid=X [params...]
func (c *Client) EditChannel(channelID uint64, properties map[string]string) error {
params := [][2]string{{"cid", fmt.Sprintf("%d", channelID)}}
for k, v := range properties {
params = append(params, [2]string{k, v})
}
cmd := commands.BuildCommandOrdered("channeledit", params)
return c.ExecCommand(cmd, 5*time.Second)
}
// DeleteChannel deletes a channel.
// 协议命令: channeldelete cid=X force=X
// force=true 强制删除(含子频道)。
func (c *Client) DeleteChannel(channelID uint64, force bool) error {
forceFlag := "0"
if force {
forceFlag = "1"
}
cmd := fmt.Sprintf("channeldelete cid=%d force=%s", channelID, forceFlag)
return c.ExecCommand(cmd, 5*time.Second)
}
// MoveChannel moves a channel to a new parent or position.
// 协议命令: channelmove cid=X cpid=X order=X
func (c *Client) MoveChannel(channelID, parentID, order uint64) error {
cmd := fmt.Sprintf("channelmove cid=%d cpid=%d order=%d", channelID, parentID, order)
return c.ExecCommand(cmd, 5*time.Second)
}
// ─── 客户端查询 / 管理 ────────────────────────────────────
// GetClientDetailInfo returns extended client details.
// 协议命令: clientinfo clid=X
// 与 ListClients 返回的 ClientInfo 相比,本方法返回更丰富的属性
// (away 状态、平台、版本、IP、连接历史等)。
func (c *Client) GetClientDetailInfo(clid uint16) (*ClientDetailInfo, error) {
cmd := fmt.Sprintf("clientinfo clid=%d", clid)
data, err := c.ExecCommandWithResponse(cmd, 5*time.Second)
if err != nil {
return nil, err
}
if len(data) == 0 {
return nil, fmt.Errorf("%w: %d", errNoDataReturnedForClient, clid)
}
item := data[0]
cid, _ := parseUint16Value(item["clid"])
channelID, _ := parseUint64Value(item["cid"])
clientType, _ := parseIntValue(item["client_type"])
created, _ := parseInt64Value(item["client_created"])
lastConnected, _ := parseInt64Value(item["client_lastconnected"])
totalConnections, _ := parseIntValue(item["client_totalconnections"])
iconID, _ := parseInt64Value(item["client_icon_id"])
groupsStr := item["client_servergroups"]
groups := make([]string, 0)
if groupsStr != "" {
groups = strings.Split(groupsStr, ",")
}
return &ClientDetailInfo{
ID: cid,
Nickname: commands.Unescape(item["client_nickname"]),
UID: item["client_unique_identifier"],
ChannelID: channelID,
Type: clientType,
ServerGroups: groups,
Away: parseBoolValue(item["client_away"]),
AwayMessage: commands.Unescape(item["client_away_message"]),
InputMuted: parseBoolValue(item["client_input_muted"]),
OutputMuted: parseBoolValue(item["client_output_muted"]),
Platform: item["client_platform"],
Version: item["client_version"],
IP: item["connection_client_ip"],
Created: created,
LastConnected: lastConnected,
TotalConnections: totalConnections,
Description: commands.Unescape(item["client_description"]),
IconID: iconID,
}, nil
}
// FindClientByDBID searches the client database by database ID.
// 协议命令: clientdbfind pattern=X -uid
// 返回客户端的 DBID 和 UID。
func (c *Client) FindClientByDBID(dbid uint64) (string, error) {
cmd := fmt.Sprintf("clientdbfind -uid cldbid=%d", dbid)
data, err := c.ExecCommandWithResponse(cmd, 5*time.Second)
if err != nil {
return "", err
}
if len(data) == 0 {
return "", fmt.Errorf("clientdbfind: no result for cldbid %d", dbid)
}
return data[0]["client_unique_identifier"], nil
}
// FindClientByName searches the client database by nickname.
// 协议命令: clientdbfind pattern=X -uid
func (c *Client) FindClientByName(nickname string) (uid string, dbid uint64, err error) {
cmd := commands.BuildCommand("clientdbfind", map[string]string{
"pattern": nickname,
}) + " -uid"
data, err := c.ExecCommandWithResponse(cmd, 5*time.Second)
if err != nil {
return "", 0, err
}
if len(data) == 0 {
return "", 0, fmt.Errorf("clientdbfind: no result for %q", nickname)
}
uid = data[0]["client_unique_identifier"]
dbid, _ = parseUint64Value(data[0]["cldbid"])
return uid, dbid, nil
}
// ListDBClients returns registered clients from the server database.
// 协议命令: clientdblist start=X duration=X
// start: 起始位置, duration: 返回数量(0=全部)
func (c *Client) ListDBClients(start, duration int) ([]DBClient, error) {
cmd := fmt.Sprintf("clientdblist start=%d duration=%d", start, duration)
data, err := c.ExecCommandWithResponse(cmd, 10*time.Second)
if err != nil {
return nil, err
}
clients := make([]DBClient, 0, len(data))
for _, item := range data {
dbid, _ := parseUint64Value(item["cldbid"])
created, _ := parseInt64Value(item["client_created"])
lastConnected, _ := parseInt64Value(item["client_lastconnected"])
totalConnections, _ := parseIntValue(item["client_totalconnections"])
clients = append(clients, DBClient{
DBID: dbid,
UID: item["client_unique_identifier"],
Nickname: commands.Unescape(item["client_nickname"]),
Created: created,
LastConnected: lastConnected,
TotalConnections: totalConnections,
Description: commands.Unescape(item["client_description"]),
})
}
return clients, nil
}
// UpdateSelf updates the current client's properties.
// 协议命令: clientupdate [params...]
// 常用参数:
// - client_nickname: 新昵称
// - client_away: "1"/"0"
// - client_away_message: 离开消息
// - client_input_muted: "1"/"0"
// - client_output_muted: "1"/"0"
// - client_phonetic_nickname: 语音昵称
// - client_default_token: 默认 Token
func (c *Client) UpdateSelf(properties map[string]string) error {
if len(properties) == 0 {
return nil
}
cmd := commands.BuildCommand("clientupdate", properties)
return c.ExecCommand(cmd, 5*time.Second)
}
// KickClient kicks a client from channel or server.
// 协议命令: clientkick clid=X reasonid=X reasonmsg=X
// reasonid: 4=从频道踢出, 5=从服务器踢出
func (c *Client) KickClient(clid uint16, reasonID int, reasonMsg string) error {
cmd := commands.BuildCommandOrdered("clientkick", [][2]string{
{"clid", fmt.Sprintf("%d", clid)},
{"reasonid", fmt.Sprintf("%d", reasonID)},
{"reasonmsg", reasonMsg},
})
return c.ExecCommand(cmd, 5*time.Second)
}
// ─── Ban 管理 ────────────────────────────────────────────
// ListBans returns all active ban entries.
// 协议命令: banlist
func (c *Client) ListBans() ([]BanEntry, error) {
data, err := c.ExecCommandWithResponse("banlist", 5*time.Second)
if err != nil {
return nil, err
}
bans := make([]BanEntry, 0, len(data))
for _, item := range data {
banID, _ := parseInt64Value(item["banid"])
created, _ := parseInt64Value(item["created"])
bans = append(bans, BanEntry{
BanID: banID,
IP: item["ip"],
Name: commands.Unescape(item["name"]),
UID: item["uid"],
Created: created,
InvokerName: commands.Unescape(item["invokername"]),
InvokerUID: item["invokeruid"],
Reason: commands.Unescape(item["reason"]),
Enforcement: parseBoolValue(item["enforcement"]),
})
}
return bans, nil
}
// AddBan adds a ban entry.
// 协议命令: banadd ip=X name=X uid=X time=X banreason=X
// ip/name/uid 至少指定一个;time 单位为秒,0=永久。
func (c *Client) AddBan(ip, name, uid string, timeSeconds int, reason string) error {
params := make([][2]string, 0, 5)
if ip != "" {
params = append(params, [2]string{"ip", ip})
}
if name != "" {
params = append(params, [2]string{"name", name})
}
if uid != "" {
params = append(params, [2]string{"uid", uid})
}
if timeSeconds > 0 {
params = append(params, [2]string{"time", fmt.Sprintf("%d", timeSeconds)})
}
if reason != "" {
params = append(params, [2]string{"banreason", reason})
}
cmd := commands.BuildCommandOrdered("banadd", params)
return c.ExecCommand(cmd, 5*time.Second)
}
// DeleteBan removes a ban entry.
// 协议命令: bandel banid=X
func (c *Client) DeleteBan(banID int64) error {
cmd := fmt.Sprintf("bandel banid=%d", banID)
return c.ExecCommand(cmd, 5*time.Second)
}
// DeleteAllBans removes all ban entries.
// 协议命令: bandelall
func (c *Client) DeleteAllBans() error {
return c.ExecCommand("bandelall", 5*time.Second)
}
// ─── 文件列表 ────────────────────────────────────────────
// ListFiles returns files and directories in a channel path.
// 协议命令: ftgetfilelist cid=X path=X
// path 为频道内虚拟路径,根目录为 "/"。
func (c *Client) ListFiles(channelID uint64, path string) ([]FileEntry, error) {
cmd := commands.BuildCommandOrdered("ftgetfilelist", [][2]string{
{"cid", fmt.Sprintf("%d", channelID)},
{"path", path},
})
data, err := c.ExecCommandWithResponse(cmd, 5*time.Second)
if err != nil {
return nil, err
}
files := make([]FileEntry, 0, len(data))
for _, item := range data {
size, _ := parseUint64Value(item["size"])
dt, _ := parseInt64Value(item["datetime"])
files = append(files, FileEntry{
Name: commands.Unescape(item["name"]),
Size: size,
DateTime: dt,
IsFile: parseBoolValue(item["is_file"]),
})
}
return files, nil
}
// ─── Token 管理 ──────────────────────────────────────────
// ListTokens returns all privilege keys.
// 协议命令: tokenlist
func (c *Client) ListTokens() ([]TokenEntry, error) {
data, err := c.ExecCommandWithResponse("tokenlist", 5*time.Second)
if err != nil {
return nil, err
}
tokens := make([]TokenEntry, 0, len(data))
for _, item := range data {
tokenType, _ := parseIntValue(item["token_type"])
tokenID1, _ := parseInt64Value(item["token_id1"])
tokenID2, _ := parseInt64Value(item["token_id2"])
created, _ := parseInt64Value(item["token_created"])
tokens = append(tokens, TokenEntry{
Token: item["token"],
TokenType: tokenType,
TokenID1: tokenID1,
TokenID2: tokenID2,
Created: created,
Description: commands.Unescape(item["token_description"]),
})
}
return tokens, nil
}
// UseToken activates a privilege key.
// 协议命令: tokenuse token=X
func (c *Client) UseToken(token string) error {
cmd := commands.BuildCommand("tokenuse", map[string]string{"token": token})
return c.ExecCommand(cmd, 5*time.Second)
}
// ─── 投诉管理 ────────────────────────────────────────────
// ListComplaints returns complaints against a target client.
// 协议命令: complainlist tcldbid=X
// 若 targetDBID=0 则返回全部投诉。
func (c *Client) ListComplaints(targetDBID uint64) ([]ComplaintEntry, error) {
cmd := "complainlist"
if targetDBID > 0 {
cmd = fmt.Sprintf("complainlist tcldbid=%d", targetDBID)
}
data, err := c.ExecCommandWithResponse(cmd, 5*time.Second)
if err != nil {
return nil, err
}
complaints := make([]ComplaintEntry, 0, len(data))
for _, item := range data {
fromDBID, _ := parseUint64Value(item["fcldbid"])
toDBID, _ := parseUint64Value(item["tcldbid"])
ts, _ := parseInt64Value(item["timestamp"])
complaints = append(complaints, ComplaintEntry{
FromDBID: fromDBID,
ToDBID: toDBID,
Message: commands.Unescape(item["message"]),
Timestamp: ts,
})
}
return complaints, nil
}
// AddComplaint files a complaint against a target client.
// 协议命令: complainadd tcldbid=X message=X
func (c *Client) AddComplaint(targetDBID uint64, message string) error {
cmd := commands.BuildCommandOrdered("complainadd", [][2]string{
{"tcldbid", fmt.Sprintf("%d", targetDBID)},
{"message", message},
})
return c.ExecCommand(cmd, 5*time.Second)
}
// DeleteComplaint removes a complaint.
// 协议命令: complaindel tcldbid=X fcldbid=X
func (c *Client) DeleteComplaint(targetDBID, fromDBID uint64) error {
cmd := fmt.Sprintf("complaindel tcldbid=%d fcldbid=%d", targetDBID, fromDBID)
return c.ExecCommand(cmd, 5*time.Second)
}