Commit f21c5cff by 陶腾飞

2022/03/30 v2.1.0

parent cd9ec223
...@@ -17,7 +17,7 @@ ...@@ -17,7 +17,7 @@
> 1. 账号管理 > 1. 账号管理
> 2. 用户管理 > 2. 用户管理
> 3. 电源选项 > 3. 电源管理
> 4. 实用工具(软件安装等) > 4. 实用工具(软件安装等)
> 5. 文件共享 > 5. 文件共享
...@@ -81,6 +81,7 @@ ...@@ -81,6 +81,7 @@
- 2020/10/27 v1.10.2 优化软件安装和重启小工具的程序逻辑,更新64位的程序清单和新ico文件 - 2020/10/27 v1.10.2 优化软件安装和重启小工具的程序逻辑,更新64位的程序清单和新ico文件
- 2020/11/05 v1.10.3 更新备份策略 - 2020/11/05 v1.10.3 更新备份策略
- 2022/03/29 v2.0.0 删除备份策略,同步域计算机信息到cmdb,添加账户、主机管理管理,唤醒策略更新,日志统一收集处理,运维平台添加管理员指令 - 2022/03/29 v2.0.0 删除备份策略,同步域计算机信息到cmdb,添加账户、主机管理管理,唤醒策略更新,日志统一收集处理,运维平台添加管理员指令
- 2022/03/30 v2.1.0 支持通过cmdb获取字段来唤醒主机,日志输出优化,支持通过运维平台打开远程终端
## 五、其他说明 ## 五、其他说明
......
...@@ -13,6 +13,11 @@ const INFO = pub.INFO ...@@ -13,6 +13,11 @@ const INFO = pub.INFO
const ERROR = pub.ERROR const ERROR = pub.ERROR
const NULL = pub.NULL const NULL = pub.NULL
var f struct {
Force bool
Quick bool
}
func main() { func main() {
var reportText = flag.String(pub.Msg_Report, "", "report to AD") var reportText = flag.String(pub.Msg_Report, "", "report to AD")
...@@ -22,9 +27,14 @@ func main() { ...@@ -22,9 +27,14 @@ func main() {
var hourText = flag.Bool("hour", false, "exec task at the time") var hourText = flag.Bool("hour", false, "exec task at the time")
var cmdbtokenText = flag.Bool("cmdbtoken", false, "") var cmdbtokenText = flag.Bool("cmdbtoken", false, "")
var loglevel = flag.String("loglevel", INFO, "") var loglevel = flag.String("loglevel", INFO, "")
var force = flag.Bool("force", false, "force exec,ignore other")
var quick = flag.Bool("quick", false, "quickly exec")
flag.Parse() flag.Parse()
f.Force = *force
f.Quick = *quick
if *loglevel != "" { if *loglevel != "" {
loglevelSet(strings.ToLower(*loglevel)) loglevelSet(strings.ToLower(*loglevel))
} }
...@@ -116,12 +126,36 @@ func wolMac(mac *string) { ...@@ -116,12 +126,36 @@ func wolMac(mac *string) {
pub.Wol_enter("", mac) pub.Wol_enter("", mac)
} }
func wolHost(pc *string) { func wolHost(pc *string) {
// coding var hostname string = *pc
var mac string
pub.LOG(INFO, "wol_host", fmt.Sprintf("pc:%s,mac:%s", *pc, mac)) // 判断主机名的格式
pub.Wol_enter(*pc, &mac) if _, err := pub.GetHostnamePart(hostname); err != nil {
pub.LOG(ERROR, "WOL_HOST", err)
return
}
mac, err := pub.CmdbGetWordFromKV(pub.Cmdb_Product_name, hostname, pub.Cmdb_Mac)
if err != nil {
pub.LOG(ERROR, "WOL_HOST", err)
return
}
//暂未开发 // 快速时,跳过远程连接
if !f.Quick {
pub.LOG(INFO, "WOL_HOST", "测试目标连通性")
alive, err := pub.PSTest_Connection(hostname)
if err != nil {
pub.LOG(ERROR, "WOL_HOST", err)
return
}
if alive {
pub.LOG(INFO, "WOL_HOST", fmt.Sprintf("pc:%s,mac:%s 已经运行", *pc, mac))
return
}
}
// 开始唤醒
pub.LOG(INFO, "WOL_HOST", fmt.Sprintf("pc:%s,mac:%s", *pc, mac))
pub.Wol_enter(*pc, &mac)
} }
func wolALL() { func wolALL() {
// coding get pclist // coding get pclist
......
...@@ -18,6 +18,8 @@ func main() { ...@@ -18,6 +18,8 @@ func main() {
} }
func listenMsg() { func listenMsg() {
pub.LOG(pub.INFO, pub.NULL, "start MSG daemon!")
http.HandleFunc("/", pub.DealMsgEnter) http.HandleFunc("/", pub.DealMsgEnter)
http.ListenAndServe(pub.DaemonListen, nil) http.ListenAndServe(pub.DaemonListen, nil)
} }
......
...@@ -12,21 +12,18 @@ import ( ...@@ -12,21 +12,18 @@ import (
var client = http.Client{} var client = http.Client{}
const cmdb_Dept string = "bumen" // 将键值转化为json
const cmdb_Product_name string = "product_name"
const cmdb_Username string = "username"
func cmdbKVtoJSON(k, v string) (string, error) { func cmdbKVtoJSON(k, v string) (string, error) {
var err error var err error
var b []byte var b []byte
switch k { switch k {
case cmdb_Product_name: case Cmdb_Product_name:
var a CIRecorProduct_name var a CIRecorProduct_name
a.Product_name = v a.Product_name = v
b, err = json.Marshal(&a) b, err = json.Marshal(&a)
case cmdb_Username: case Cmdb_Username:
var a CIRecordUsername var a CIRecordUsername
a.Username = v a.Username = v
...@@ -35,6 +32,8 @@ func cmdbKVtoJSON(k, v string) (string, error) { ...@@ -35,6 +32,8 @@ func cmdbKVtoJSON(k, v string) (string, error) {
return string(b), err return string(b), err
} }
// 键值将上传到cmdb,如果存在返回ci,不存在将添加记录
func cmdbExistRecord(k, v string) (int, bool) { func cmdbExistRecord(k, v string) (int, bool) {
var count string = "1" var count string = "1"
...@@ -76,7 +75,7 @@ func cmdbExistRecord(k, v string) (int, bool) { ...@@ -76,7 +75,7 @@ func cmdbExistRecord(k, v string) (int, bool) {
LOG(ERROR, CMDB, err) LOG(ERROR, CMDB, err)
return 0, false return 0, false
} else { } else {
l := fmt.Sprintf("CMDB\nnew host %s:%s", k, v) l := fmt.Sprintf("CMDB\nnew hostanme %s", v)
LOG(INFO, "CMDB_NEW", l) LOG(INFO, "CMDB_NEW", l)
SendWxworkTextToAdmins(l) SendWxworkTextToAdmins(l)
} }
...@@ -90,6 +89,7 @@ func cmdbExistRecord(k, v string) (int, bool) { ...@@ -90,6 +89,7 @@ func cmdbExistRecord(k, v string) (int, bool) {
return 0, false return 0, false
} }
// 将json格式的字段根据ci更新到cmdb
func cmdbUpdateRecordFromJSON(datastr string, ci int) error { func cmdbUpdateRecordFromJSON(datastr string, ci int) error {
var cmdbPath string = fmt.Sprintf("api/v0.1/ci/%d", ci) var cmdbPath string = fmt.Sprintf("api/v0.1/ci/%d", ci)
...@@ -111,6 +111,8 @@ func cmdbUpdateRecordFromJSON(datastr string, ci int) error { ...@@ -111,6 +111,8 @@ func cmdbUpdateRecordFromJSON(datastr string, ci int) error {
} }
return nil return nil
} }
// 获取token并保存于注册表
func CmdbTokenUpdate() { func CmdbTokenUpdate() {
type res struct { type res struct {
Token string `json:"token"` Token string `json:"token"`
...@@ -137,6 +139,8 @@ func CmdbTokenUpdate() { ...@@ -137,6 +139,8 @@ func CmdbTokenUpdate() {
LOG(INFO, msg_type, "token更新完成") LOG(INFO, msg_type, "token更新完成")
} }
} }
// 根据键值获取CI
func cmdbGetCI(k, v string) (int, error) { func cmdbGetCI(k, v string) (int, error) {
var count string = "1" var count string = "1"
...@@ -157,14 +161,84 @@ func cmdbGetCI(k, v string) (int, error) { ...@@ -157,14 +161,84 @@ func cmdbGetCI(k, v string) (int, error) {
LOG(DEBUG, "CMDB_EXITE", string(data)) LOG(DEBUG, "CMDB_EXITE", string(data))
switch cir.Numfound { switch cir.Numfound {
case 0: case 0:
return 0, fmt.Errorf("不存在记录") return 0, Error_CMDB_No_Record
case 1: case 1:
return cir.R[0].Ci_id, nil return cir.R[0].Ci_id, nil
default: default:
return 0, fmt.Errorf("存在多条记录") return 0, Error_CMDB_Many_Record
}
}
// 根据键值获取1个字段
func CmdbGetWordFromKV(k, v, w string) (string, error) {
var count string = "1"
var cmdbPath string = "api/v0.1/ci/s?q="
search := fmt.Sprintf("%s:%s", k, v)
url := fmt.Sprintf("%s/%s_type:%s,%s&count=%s", cmdbUrl, cmdbPath, cmdbCITypeComputer, search, count)
LOG(DEBUG, CMDB, url)
var cir CIRecordReq
data, err := cmdbGET(url)
if err != nil {
LOG(ERROR, CMDB, err)
return "", err
}
if err := json.Unmarshal(data, &cir); err != nil {
LOG(ERROR, CMDB, err)
return "", err
}
LOG(DEBUG, "CMDB_EXITE", string(data))
switch cir.Numfound {
case 0:
return "", Error_CMDB_No_Record
case 1:
return getVFromK(&cir.R[0], w), nil
default:
return "", Error_CMDB_Many_Record
} }
} }
// 根据键值获取2个字段
func CmdbGetWordFromKV2(k, v, a, b string) (string, string, error) {
var count string = "1"
var cmdbPath string = "api/v0.1/ci/s?q="
search := fmt.Sprintf("%s:%s", k, v)
url := fmt.Sprintf("%s/%s_type:%s,%s&count=%s", cmdbUrl, cmdbPath, cmdbCITypeComputer, search, count)
LOG(DEBUG, CMDB, url)
var cir CIRecordReq
data, err := cmdbGET(url)
if err != nil {
LOG(ERROR, CMDB, err)
return "", "", err
}
if err := json.Unmarshal(data, &cir); err != nil {
LOG(ERROR, CMDB, err)
return "", "", err
}
LOG(DEBUG, "CMDB_EXITE", string(data))
switch cir.Numfound {
case 0:
return "", "", Error_CMDB_No_Record
case 1:
return getVFromK(&cir.R[0], a), getVFromK(&cir.R[0], b), nil
default:
return "", "", Error_CMDB_Many_Record
}
}
func getVFromK(cir *CIRecordResult, k string) string {
var r string
switch k {
case Cmdb_Mac:
r = cir.Mac
}
return r
}
func cmdbPOST(url string, reqdata []byte) ([]byte, error) { func cmdbPOST(url string, reqdata []byte) ([]byte, error) {
buf := bytes.NewBuffer(nil) buf := bytes.NewBuffer(nil)
buf.Write(reqdata) buf.Write(reqdata)
......
...@@ -6,6 +6,11 @@ import ( ...@@ -6,6 +6,11 @@ import (
"sync" "sync"
) )
const cmdb_Dept string = "bumen"
const Cmdb_Product_name string = "product_name"
const Cmdb_Username string = "username"
const Cmdb_Mac string = "Mac"
type CIRecordReq struct { type CIRecordReq struct {
Counter CIRecordComputer `json:"counter"` Counter CIRecordComputer `json:"counter"`
Facet CIRecordFacet `json:"facet"` Facet CIRecordFacet `json:"facet"`
...@@ -150,7 +155,7 @@ func (cir *CIRecordResult) position(location, dept string) string { ...@@ -150,7 +155,7 @@ func (cir *CIRecordResult) position(location, dept string) string {
resutlt = cmdbPosition1 resutlt = cmdbPosition1
case "TXHD": case "TXHD":
resutlt = cmdbPosition1 resutlt = cmdbPosition1
case "TXMT": case "TXWJ":
resutlt = cmdbPosition1 resutlt = cmdbPosition1
case "JS": case "JS":
resutlt = cmdbPosition1 resutlt = cmdbPosition1
......
...@@ -8,7 +8,7 @@ const SymbolHostname = "-" ...@@ -8,7 +8,7 @@ const SymbolHostname = "-"
// AD-Control // // AD-Control //
const Version string = "2.0.0" const Version string = "2.1.0"
const Host_adserver string = "ADSERVER" const Host_adserver string = "ADSERVER"
const Host_thserver string = "THSERVER" const Host_thserver string = "THSERVER"
const Host_zzserver string = "ZZSERVER" const Host_zzserver string = "ZZSERVER"
...@@ -18,6 +18,20 @@ const ADServerDaemon string = Host_adserver + DaemonListen ...@@ -18,6 +18,20 @@ const ADServerDaemon string = Host_adserver + DaemonListen
const ADServerLogDaemon string = Host_adserver + DaemonLogListen const ADServerLogDaemon string = Host_adserver + DaemonLogListen
const THServerDaemon string = Host_thserver + DaemonListen const THServerDaemon string = Host_thserver + DaemonListen
//
//
// AD User env //
//
const AD_User_main_admin string = "zhiweiadserver"
const AD_User_tengfei string = "tengfei"
const AD_User_taotengfei string = "taotengfei"
const AD_User_ADControl string = "ad-control@zhiweireach.com"
const AD_User_ADControl_key string = "golang&*90-="
// users //
const Users_Timeout int = 3
// user // // user //
var User_computername string = GetEnv("COMPUTERNAME") var User_computername string = GetEnv("COMPUTERNAME")
...@@ -42,15 +56,11 @@ const SMB_ADSoftPlus string = SMB_ADSoft + `plus\` ...@@ -42,15 +56,11 @@ const SMB_ADSoftPlus string = SMB_ADSoft + `plus\`
// Local Folder // // Local Folder //
var Dir_userprofile string = GetEnv("USERPROFILE") var Dir_userprofile string = GetEnv("USERPROFILE")
var Dir_appdata string = GetEnv("appdata")
var Dir_tmp string = GetEnv("tmp") var Dir_tmp string = GetEnv("tmp")
var Dir_Windows string = GetEnv("windir") + `\`
const Dir_ADsoftware_local string = `H:\software\softlike\` const Dir_ADsoftware_local string = `H:\software\softlike\`
// application // // application //
const App_adct string = SMB_ADSoftApplication + "adct.exe"
const App_MultiSplit string = SMB_ADSoftOthTool + "win_MultiSplit.exe" const App_MultiSplit string = SMB_ADSoftOthTool + "win_MultiSplit.exe"
// reg info // // reg info //
...@@ -60,9 +70,9 @@ const ( ...@@ -60,9 +70,9 @@ const (
HKLM HKLM
HKU HKU
) )
const reg_LM_ADCONTROL string = `software\AD-Control`
// reg value // // reg value //
const reg_LM_ADCONTROL string = `software\AD-Control`
const Reg_Key_cmdbtoken string = "cmdb_token" const Reg_Key_cmdbtoken string = "cmdb_token"
const Reg_Value_ON string = "ON" const Reg_Value_ON string = "ON"
const Reg_Value_OFF string = "OFF" const Reg_Value_OFF string = "OFF"
......
...@@ -21,8 +21,11 @@ var Error_Not_Msg = errors.New("isn't defined msg") ...@@ -21,8 +21,11 @@ var Error_Not_Msg = errors.New("isn't defined msg")
var Error_WXWork_Down = errors.New(Error_WXWork_Down_STR) var Error_WXWork_Down = errors.New(Error_WXWork_Down_STR)
var Error_WXWork_Down_STR = "后台更新中..." var Error_WXWork_Down_STR = "后台更新中..."
var Error_WXWork_NotDominUser_STR = "您的主机暂时不支持该功能" var Error_WXWork_NotDominUser_STR = "您的主机暂时不支持该功能"
const Error_CMDB_UPDATE_REQ = "更新失败" const Error_CMDB_UPDATE_REQ = "更新失败"
const Sccessful_CMDB_UPDATE_REQ = "更新成功" const Sccessful_CMDB_UPDATE_REQ = "更新成功"
// cmdb
var Error_CMDB_No_Record = errors.New("CMDB_No_Record")
var Error_CMDB_Many_Record = errors.New("_CMDB_Many_Record")
...@@ -4,13 +4,26 @@ import ( ...@@ -4,13 +4,26 @@ import (
ldap "gopkg.in/ldap.v3" ldap "gopkg.in/ldap.v3"
) )
func ldapBase() (*ldap.Conn, error) {
l, err := ldap.DialURL("ldap://127.0.0.1:389")
if err != nil {
LOG(ERROR, NULL, err)
return nil, err
}
if err = l.Bind(AD_User_ADControl, AD_User_ADControl_key); err != nil {
LOG(ERROR, NULL, err)
return nil, err
}
return l, err
}
func LDAP_Get_PCGroup() []string { func LDAP_Get_PCGroup() []string {
l, err := ldap.DialURL("ldap://127.0.0.1:389") l, err := ldap.DialURL("ldap://127.0.0.1:389")
if err != nil { if err != nil {
LOG(ERROR, NULL, err) LOG(ERROR, NULL, err)
} }
defer l.Close() defer l.Close()
if err = l.Bind(AD_User_zhiwei, "1Q2W3e4r"); err != nil { if err = l.Bind(AD_User_ADControl, AD_User_ADControl_key); err != nil {
LOG(ERROR, NULL, err) LOG(ERROR, NULL, err)
} }
searchRequest := ldap.NewSearchRequest( searchRequest := ldap.NewSearchRequest(
...@@ -35,14 +48,8 @@ func LDAP_Get_PCGroup() []string { ...@@ -35,14 +48,8 @@ func LDAP_Get_PCGroup() []string {
return pcgroup return pcgroup
} }
func LDAP_nameTopinyin(name string) string { func LDAP_nameTopinyin(name string) string {
l, err := ldap.DialURL("ldap://127.0.0.1:389") l, err := ldapBase()
if err != nil {
LOG(ERROR, NULL, err)
}
defer l.Close()
if err = l.Bind(AD_User_zhiwei, "1Q2W3e4r"); err != nil {
LOG(ERROR, NULL, err)
}
searchRequest := ldap.NewSearchRequest( searchRequest := ldap.NewSearchRequest(
//"ou=情报部门,dc=zhiweireach,dc=com", //"ou=情报部门,dc=zhiweireach,dc=com",
"ou=情报部门,dc=zhiweireach,dc=com", "ou=情报部门,dc=zhiweireach,dc=com",
...@@ -56,6 +63,32 @@ func LDAP_nameTopinyin(name string) string { ...@@ -56,6 +63,32 @@ func LDAP_nameTopinyin(name string) string {
if err != nil { if err != nil {
LOG(ERROR, NULL, err) LOG(ERROR, NULL, err)
} }
defer l.Close()
return sr.Entries[0].GetAttributeValue("sAMAccountName") return sr.Entries[0].GetAttributeValue("sAMAccountName")
} }
func LDAP_CheckExistName(name string) (bool, error) {
var r bool
l, _ := ldapBase()
defer l.Close()
searchRequest := ldap.NewSearchRequest(
"dc=zhiweireach,dc=com",
ldap.ScopeWholeSubtree, ldap.NeverDerefAliases, 0, 0,
false,
"(cn="+name+")",
[]string{"cn"},
nil,
)
sr, err := l.Search(searchRequest)
if err != nil {
return false, err
}
if len(sr.Entries) == 0 {
r = false
} else {
r = true
}
return r, err
}
...@@ -7,10 +7,7 @@ import ( ...@@ -7,10 +7,7 @@ import (
"strings" "strings"
) )
//
//
// 消息入口 // 消息入口
//
func DealMsgEnter(w http.ResponseWriter, r *http.Request) { func DealMsgEnter(w http.ResponseWriter, r *http.Request) {
...@@ -21,48 +18,39 @@ func DealMsgEnter(w http.ResponseWriter, r *http.Request) { ...@@ -21,48 +18,39 @@ func DealMsgEnter(w http.ResponseWriter, r *http.Request) {
LOG(ERROR, NULL, err) LOG(ERROR, NULL, err)
w.Write([]byte("")) w.Write([]byte(""))
} }
ret := DealMsg(msgtype, url, unmsg)
w.Write(ret)
w.Write(DealMsg(msgtype, url, unmsg))
} }
// 处理消息
func DealMsg(msgtype, url string, unmsg []byte) []byte { func DealMsg(msgtype, url string, unmsg []byte) []byte {
var ret []byte var ret []byte
switch msgtype { switch msgtype {
// 报告 消息 // 报告 消息
case Msg_Report: case Msg_Report:
var pmsg MJreport var msg MJreport
// LOG(INFO, "REPORT", string(unmsg)) ParseJson(unmsg, &msg)
ParseJson(unmsg, &pmsg) ice := msg.MsgDeal()
ice := pmsg.MsgDeal()
ret = ReParseJson(&ice) ret = ReParseJson(&ice)
// 执行 消息 // 执行 消息
case Msg_Exec: case Msg_Exec:
var pmsg MJexec var msg MJexec
// LOG(INFO, "EXEC", string(unmsg)) ParseJson(unmsg, &msg)
ParseJson(unmsg, &pmsg) ice := msg.MsgDeal()
ice := pmsg.MsgDeal()
ret = ReParseJson(&ice) ret = ReParseJson(&ice)
// 唤醒 消息 // 唤醒 消息
case Msg_Wake: case Msg_Wake:
var pmsg MJwake var msg MJwake
ParseJson(unmsg, &pmsg) ParseJson(unmsg, &msg)
ice := pmsg.MsgDeal() ice := msg.MsgDeal()
ret = ReParseJson(&ice)
// 错误 消息
case Msg_Error:
LOG(ERROR, NULL, string(unmsg))
var pmsg MJerror
ParseJson(unmsg, &pmsg)
ice := pmsg.MsgDeal()
ret = ReParseJson(&ice) ret = ReParseJson(&ice)
default: default:
pmsg := ParseWX(url, &unmsg) msg := ParseWX(url, &unmsg)
go pmsg.MsgDealSend() go msg.MsgDealSend()
} }
return ret return ret
} }
...@@ -83,14 +71,14 @@ func (rep *MJreport) MsgDeal() interface{} { ...@@ -83,14 +71,14 @@ func (rep *MJreport) MsgDeal() interface{} {
// 服务器 // 服务器
case Msg_status_commit: case Msg_status_commit:
// 第一次仅仅会更新部分字段,第二次会更新全部字段 // 第一次仅仅会更新部分字段,第二次会更新全部字段
if ci, r := cmdbExistRecord(cmdb_Product_name, rep.Computername); !r { if ci, r := cmdbExistRecord(Cmdb_Product_name, rep.Computername); !r {
break break
} else { } else {
err := cmdbUpdateRecordFromJSON(rep.DataStr, ci) err := cmdbUpdateRecordFromJSON(rep.DataStr, ci)
if err != nil { if err != nil {
LOG(ERROR, "CMDB_UPDATE", err) LOG(ERROR, rep.Instruction, err)
} else { } else {
LOG(INFO, "CMDB_UPDATE", rep.Computername) LOG(INFO, rep.Instruction, rep.Computername)
} }
} }
// 客户端处理 // 客户端处理
...@@ -98,25 +86,18 @@ func (rep *MJreport) MsgDeal() interface{} { ...@@ -98,25 +86,18 @@ func (rep *MJreport) MsgDeal() interface{} {
return nil return nil
} }
// coding
// rep.Reg_Write_ADDC(
// Reg_Name_TurnOffTime,
// rep.Reg_Write_ADDC(
// Reg_Name_PowerStatus,
// Reg_Value_OFF)
case Msg_Report_WillTurnOnPC: case Msg_Report_WillTurnOnPC:
switch rep.Status { switch rep.Status {
// 服务器 // 服务器
case Msg_status_commit: case Msg_status_commit:
if ci, r := cmdbExistRecord(cmdb_Product_name, rep.Computername); !r { if ci, r := cmdbExistRecord(Cmdb_Product_name, rep.Computername); !r {
break break
} else { } else {
err := cmdbUpdateRecordFromJSON(rep.DataStr, ci) err := cmdbUpdateRecordFromJSON(rep.DataStr, ci)
if err != nil { if err != nil {
LOG(ERROR, "CMDB_UPDATE", err) LOG(ERROR, rep.Instruction, err)
} else { } else {
LOG(INFO, "CMDB_UPDATE", rep.Computername) LOG(INFO, rep.Instruction, rep.Computername)
} }
} }
// 客户端 // 客户端
...@@ -124,22 +105,6 @@ func (rep *MJreport) MsgDeal() interface{} { ...@@ -124,22 +105,6 @@ func (rep *MJreport) MsgDeal() interface{} {
return nil return nil
} }
// coding
// 如果是被开机唤醒
// id = rep.Reg_Query_ADDC(Reg_Name_BeWakedID)
// if id != "" {
// SendWxworkTextToAUser(id, "主机已启动,一切就绪~")
// // rep.Reg_Write_ADDC(Reg_Name_BeWakedID, "")
// }
// coding
// 记录 开机 时间
// rep.Reg_Write_ADDC(
// Reg_Name_TurnOnTime,
// 记录 电源状态
// rep.Reg_Write_ADDC(
// Reg_Name_PowerStatus,
// Reg_Value_ON)
case Msg_Report_WillLogInUser: case Msg_Report_WillLogInUser:
case Msg_Report_WillLogOutUser: case Msg_Report_WillLogOutUser:
...@@ -148,13 +113,13 @@ func (rep *MJreport) MsgDeal() interface{} { ...@@ -148,13 +113,13 @@ func (rep *MJreport) MsgDeal() interface{} {
switch rep.Status { switch rep.Status {
// 服务器 // 服务器
case Msg_status_commit: case Msg_status_commit:
ci, err := cmdbGetCI(cmdb_Product_name, rep.Computername) ci, err := cmdbGetCI(Cmdb_Product_name, rep.Computername)
if err != nil { if err != nil {
rep.DataStr = Error_CMDB_UPDATE_REQ rep.DataStr = Error_CMDB_UPDATE_REQ
LOG(ERROR, rep.Instruction, err) LOG(ERROR, rep.Instruction, err)
break break
} }
a, err := cmdbKVtoJSON(cmdb_Username, rep.DataStr) a, err := cmdbKVtoJSON(Cmdb_Username, rep.DataStr)
if err != nil { if err != nil {
rep.DataStr = Error_CMDB_UPDATE_REQ rep.DataStr = Error_CMDB_UPDATE_REQ
LOG(ERROR, rep.Instruction, err) LOG(ERROR, rep.Instruction, err)
...@@ -165,7 +130,7 @@ func (rep *MJreport) MsgDeal() interface{} { ...@@ -165,7 +130,7 @@ func (rep *MJreport) MsgDeal() interface{} {
LOG(ERROR, rep.Instruction, err) LOG(ERROR, rep.Instruction, err)
break break
} }
LOG(INFO, rep.Instruction, fmt.Sprintf("%s is %s's PrimryComputer", cmdb_Product_name, rep.DataStr)) LOG(INFO, rep.Instruction, fmt.Sprintf("%s is %s's PrimryComputer", Cmdb_Product_name, rep.DataStr))
rep.DataStr = Sccessful_CMDB_UPDATE_REQ rep.DataStr = Sccessful_CMDB_UPDATE_REQ
// 客户端 // 客户端
...@@ -248,12 +213,17 @@ func (exec *MJexec) MsgDeal() interface{} { ...@@ -248,12 +213,17 @@ func (exec *MJexec) MsgDeal() interface{} {
} else { } else {
PsExec_sfdic(exec.Computername, Dir_ADsoftware_local+exec.Command, exec.SessionID) PsExec_sfdic(exec.Computername, Dir_ADsoftware_local+exec.Command, exec.SessionID)
} }
exec.Status = Msg_Exec_State_Over exec.Status = Msg_Exec_State_Over
case Msg_Exec_OpenAdminEXE: case Msg_Exec_OpenAdminEXE:
PsExec_sdi(exec.Computername, exec.Command, exec.SessionID) PsExec_sdi(exec.Computername, exec.Command, exec.SessionID)
exec.Status = Msg_Exec_State_Over exec.Status = Msg_Exec_State_Over
case Msg_Exec_OpenRemoteTerminal:
exec.Command = "powershell"
SendWxworkTextToAdmins(exec.ToWXWrokAdmin())
exec.Status = Msg_Exec_State_Over
default: default:
Execcmd_nowait(exec.Command) Execcmd_nowait(exec.Command)
} }
...@@ -275,13 +245,6 @@ func (wake *MJwake) MsgDeal() interface{} { ...@@ -275,13 +245,6 @@ func (wake *MJwake) MsgDeal() interface{} {
return nil return nil
} }
// 关于 error 错误 的 消息处理
func (err *MJerror) MsgDeal() interface{} {
//SendWX(err.Instruction)
return nil
}
// 关于 wxwork 企业微信 的 消息处理 // 关于 wxwork 企业微信 的 消息处理
func (wxwork *MJwxwork) MsgDealSend() { func (wxwork *MJwxwork) MsgDealSend() {
...@@ -289,31 +252,32 @@ func (wxwork *MJwxwork) MsgDealSend() { ...@@ -289,31 +252,32 @@ func (wxwork *MJwxwork) MsgDealSend() {
switch wxwork.Instruction { switch wxwork.Instruction {
case WXEK_zwDevopsPowerWake: case WXEK_zwDevopsPowerWake:
wxwork.SendText("收到唤醒指令,正在处理,") if c, err := LDAP_CheckExistName(wxwork.Name); err != nil {
LOG(ERROR, wxwork.Instruction, err)
} else {
if !c {
wxwork.SendText("请先申请知微域的个人账号!")
return
}
}
//var domainUser bool = true
var pc, mac string var pc, mac string
var err error var err error
pc, mac, err = CmdbGetWordFromKV2(Cmdb_Username, wxwork.Name, Cmdb_Product_name, Cmdb_Mac)
// coding switch err {
// 在注册表中查询 域用户 记录 case Error_CMDB_No_Record:
// if err = Reg_IfExist_item(Reg_Root_ac, Reg_Path_home_du+wxwork.Name); err == nil { wxwork.SendText(`您还没有绑定个人主机,请使个人账号登录系统后,打开小工具,点击"主机管理"-"绑定主机"`)
// pc = wxwork.Reg_Query_ADUsers(Reg_Name_PrimayComputerName) return
// mac = wxwork.Reg_Query_ADUsers(Reg_Name_MACAddress) case nil:
break
// 在注册表中查询 知微用户 记录 default:
// } else if err = Reg_IfExist_item(Reg_Root_ac, Reg_Path_home_zhu+wxwork.Name); err == nil { wxwork.SendText(`后台错误?!`)
// pc = wxwork.Reg_Query_ZhiWeiUsers(Reg_Name_PrimayComputerName) SendWxworkTextToAdmins("后台系统错误!")
// mac = wxwork.Reg_Query_ZhiWeiUsers(Reg_Name_MACAddress) return
// domainUser = false }
// } else {
// wxwork.Result = Error_WXWork_NotDominUser_STR
// break
// }
// 查看电源状态 // 查看电源状态
status, err := PSTest_Connection(pc) status, err := PSTest_Connection(pc)
LOG(INFO, "Wxwork_Rec", fmt.Sprintf("host:%s,mac:%s,name:%s,execstatus:%s,status:%v", pc, mac, wxwork.Name, "wakeup", status))
if err != nil { if err != nil {
wxwork.Result = Error_WXWork_Down_STR wxwork.Result = Error_WXWork_Down_STR
break break
...@@ -322,12 +286,9 @@ func (wxwork *MJwxwork) MsgDealSend() { ...@@ -322,12 +286,9 @@ func (wxwork *MJwxwork) MsgDealSend() {
wxwork.Result = "已经开机了哦~" wxwork.Result = "已经开机了哦~"
break break
} }
LOG(INFO, "Wxwork_Rec", fmt.Sprintf("host:%s,mac:%s,name:%s,execstatus:%s,status:%v", pc, mac, wxwork.Name, "wakeup", status))
// 只有域用户有效 wxwork.SendText("收到唤醒指令,正在处理!")
// 写入被唤醒的ID用于判断是否是被企业微信唤醒
// if domainUser {
// Reg_Write_ADDC(pc, Reg_Name_BeWakedID, wxwork.UserID)
// }
// 进行唤醒 // 进行唤醒
go Wol_enter(pc, &mac) go Wol_enter(pc, &mac)
...@@ -479,17 +440,11 @@ func (wxwork *MJwxwork) MsgDealSend() { ...@@ -479,17 +440,11 @@ func (wxwork *MJwxwork) MsgDealSend() {
} }
default: default:
wxwork.Content = ToEnglishSymbol(wxwork.Content)
switch wxwork.Name { switch wxwork.Name {
case "陶腾飞": case "陶腾飞":
if strings.HasPrefix(wxwork.Content, "pwsh") { PsExec_sdi(wxwork.SplitMessage())
hostname := wxwork.Content[5:]
PsExec_sdi(hostname, "powershell", "1")
}
default: default:
wxwork.SendText("【自动回复】\n电脑问题请找技术部运维组陶腾飞。联系方式:17815918180(同微信)") wxwork.SendText("【自动回复】\n电脑问题请找技术部运维组陶腾飞。联系方式:17815918180(同微信)")
} }
} }
......
package public package public
import (
"fmt"
"strings"
)
// msg env //////////////////////////////////////////////////////////////// // msg env ////////////////////////////////////////////////////////////////
const EndSign byte = 125 const EndSign byte = 125
...@@ -13,7 +18,6 @@ const Msg_status_deal string = "deal" ...@@ -13,7 +18,6 @@ const Msg_status_deal string = "deal"
const Msg_Report string = "report" const Msg_Report string = "report"
const Msg_Exec string = "exec" const Msg_Exec string = "exec"
const Msg_Error string = "error"
const Msg_Wxwork string = "wxwork" const Msg_Wxwork string = "wxwork"
const Msg_Wake string = "wake" const Msg_Wake string = "wake"
...@@ -43,11 +47,6 @@ const Msg_Report_UpdatePrimaryHost string = UpdatePrimaryHost ...@@ -43,11 +47,6 @@ const Msg_Report_UpdatePrimaryHost string = UpdatePrimaryHost
// msg exec type //////////////////////////////////////////////////////////////// // msg exec type ////////////////////////////////////////////////////////////////
const (
Msg_Exec_Cmder_ADServer = iota
Msg_Exec_Cmder_LocalDaemon
Msg_Exec_Cmder_LocalUser
)
const Msg_Exec_State_Request string = "request" const Msg_Exec_State_Request string = "request"
const Msg_Exec_State_Ack string = "ack" const Msg_Exec_State_Ack string = "ack"
const Msg_Exec_State_Over string = "over" const Msg_Exec_State_Over string = "over"
...@@ -60,6 +59,7 @@ const Msg_Exec_RebootWindows_cmd string = "shutdown -r -t 15" ...@@ -60,6 +59,7 @@ const Msg_Exec_RebootWindows_cmd string = "shutdown -r -t 15"
const Msg_Exec_InstallSoftware string = "installsoftware" const Msg_Exec_InstallSoftware string = "installsoftware"
const Msg_Exec_HideShowIcon string = "HideShowIcon" const Msg_Exec_HideShowIcon string = "HideShowIcon"
const Msg_Exec_OpenAdminEXE string = "OpenAdmin" const Msg_Exec_OpenAdminEXE string = "OpenAdmin"
const Msg_Exec_OpenRemoteTerminal string = "OpenRemoteTerminal"
// msg wkae type //////////////////////////////////////////////////////////////// // msg wkae type ////////////////////////////////////////////////////////////////
const Msg_Wake_Status_Req string = "WakeReq" const Msg_Wake_Status_Req string = "WakeReq"
...@@ -118,11 +118,6 @@ type MJwake struct { ...@@ -118,11 +118,6 @@ type MJwake struct {
MACAddress string MACAddress string
} }
// error Json Format /////////////////////////////////////////////////////
type MJerror struct {
MJbase
}
// wxwork Json Format ///////////////////////////////////////////////////// // wxwork Json Format /////////////////////////////////////////////////////
type MJwxwork struct { type MJwxwork struct {
Instruction string Instruction string
...@@ -208,6 +203,9 @@ func (exec *MJexec) Init(s string) { ...@@ -208,6 +203,9 @@ func (exec *MJexec) Init(s string) {
func (exec *MJexec) PsExec() { func (exec *MJexec) PsExec() {
PsExec_sd(exec.Computername, exec.Command) PsExec_sd(exec.Computername, exec.Command)
} }
func (exec *MJexec) ToWXWrokAdmin() string {
return fmt.Sprintf("ComputernName:%s\nCommand:%s\nSessionID:%s", exec.Computername, exec.Command, exec.SessionID)
}
// wake // wake
...@@ -221,20 +219,6 @@ func GJwake(host, mac string) interface{} { ...@@ -221,20 +219,6 @@ func GJwake(host, mac string) interface{} {
return wake return wake
} }
// error
func GJerror(e error) interface{} {
var err MJerror
err.init(e)
return err
}
func (err *MJerror) init(e error) {
err.Username = User_name
err.Computername = User_computername
err.MsgType = Msg_Error
err.Instruction = e.Error()
}
// wxwork // wxwork
func (wxwork *MJwxwork) SendText(text string) { func (wxwork *MJwxwork) SendText(text string) {
...@@ -258,3 +242,8 @@ func (wxwork *MJwxwork) Reg_Query_ZhiWeiUsers(key string) string { ...@@ -258,3 +242,8 @@ func (wxwork *MJwxwork) Reg_Query_ZhiWeiUsers(key string) string {
func (wxwork *MJwxwork) SendResult() { func (wxwork *MJwxwork) SendResult() {
SendWxworkTextToAUser(wxwork.UserID, wxwork.Result) SendWxworkTextToAUser(wxwork.UserID, wxwork.Result)
} }
func (wxwork *MJwxwork) SplitMessage() (string, string, string) {
s := strings.Split(wxwork.Content, "\n")
return strings.Split(s[0], ":")[1], strings.Split(s[1], ":")[1], strings.Split(s[2], ":")[1]
}
...@@ -15,9 +15,6 @@ func SendServerReport(s string) { ...@@ -15,9 +15,6 @@ func SendServerReport(s string) {
func SendServerExec(v interface{}) { func SendServerExec(v interface{}) {
SendADMsg(Msg_Exec, v) SendADMsg(Msg_Exec, v)
} }
func SendServerError(err error) {
SendADMsg(Msg_Error, GJerror(err))
}
// base server msg // base server msg
func SendADMsg(msgtype string, v interface{}) { func SendADMsg(msgtype string, v interface{}) {
......
...@@ -43,6 +43,9 @@ func GetDateHour() (int, int) { ...@@ -43,6 +43,9 @@ func GetDateHour() (int, int) {
func ParseTime(s string) (time.Time, error) { func ParseTime(s string) (time.Time, error) {
return time.Parse("2006-01-02 15:04:05 Monday", s) return time.Parse("2006-01-02 15:04:05 Monday", s)
} }
func IfBeforeMin(target time.Time, m int) bool {
return target.Add(time.Duration(m) * time.Minute).After(time.Now())
}
// //
// //
......
...@@ -7,16 +7,6 @@ import ( ...@@ -7,16 +7,6 @@ import (
// //
// //
// AD User env //
//
const AD_User_main_admin string = "zhiweiadserver"
const AD_User_tengfei string = "tengfei"
const AD_User_taotengfei string = "taotengfei"
const AD_User_zhiwei string = "zhiwei@zhiweireach.com"
//
//
// User // User
// //
......
...@@ -15,7 +15,6 @@ import ( ...@@ -15,7 +15,6 @@ import (
// https://www.amd.com/system/files/TechDocs/20213.pdf // https://www.amd.com/system/files/TechDocs/20213.pdf
func Wol_enter(pc string, mac *string) { func Wol_enter(pc string, mac *string) {
LOG(INFO, "WOL", fmt.Sprintf("pc:%s,mac:%s", pc, *mac))
if err := wol(*mac); err != nil { if err := wol(*mac); err != nil {
LOG(ERROR, NULL, err) LOG(ERROR, NULL, err)
......
...@@ -4,11 +4,14 @@ import ( ...@@ -4,11 +4,14 @@ import (
pub "AD-Control-Golang/public" pub "AD-Control-Golang/public"
"fmt" "fmt"
"strings" "strings"
"time"
"github.com/lxn/walk" "github.com/lxn/walk"
. "github.com/lxn/walk/declarative" . "github.com/lxn/walk/declarative"
) )
var timeout time.Time
func menuInitName(ni *walk.NotifyIcon) { func menuInitName(ni *walk.NotifyIcon) {
text := fmt.Sprintf("姓名: %s", pub.User_name_display) text := fmt.Sprintf("姓名: %s", pub.User_name_display)
...@@ -24,9 +27,9 @@ func menuInitName(ni *walk.NotifyIcon) { ...@@ -24,9 +27,9 @@ func menuInitName(ni *walk.NotifyIcon) {
// Power -------------------------------------------------- // Power --------------------------------------------------
// 电源菜单 // 电源管理
func menuInitPower(ni *walk.NotifyIcon) { func menuInitPower(ni *walk.NotifyIcon) {
fa := "电源选项" fa := "电源管理"
sub := [...]string{"保持开机", "取消关机计时"} sub := [...]string{"保持开机", "取消关机计时"}
// 建立空菜单 // 建立空菜单
...@@ -89,11 +92,9 @@ func menuInitAccountManager(ni *walk.NotifyIcon) { ...@@ -89,11 +92,9 @@ func menuInitAccountManager(ni *walk.NotifyIcon) {
pub.LOG(pub.ERROR, NULL, NULL, err) pub.LOG(pub.ERROR, NULL, NULL, err)
} }
//lastone := len(sub)-1
for i, buttonText := range sub { for i, buttonText := range sub {
// 建立 子按钮 // 建立 子按钮
//na := walk.NewAction()
na := walk.NewAction() na := walk.NewAction()
if err := na.SetText(buttonText); err != nil { if err := na.SetText(buttonText); err != nil {
pub.LOG(pub.ERROR, NULL, NULL, err) pub.LOG(pub.ERROR, NULL, NULL, err)
...@@ -130,11 +131,11 @@ func tAccountManagerSignUP() { ...@@ -130,11 +131,11 @@ func tAccountManagerSignUP() {
} }
// 账号管理 // 主机管理
func menuInitHostManager(ni *walk.NotifyIcon) { func menuInitHostManager(ni *walk.NotifyIcon) {
fa := "主机管理" fa := "主机管理"
text := fmt.Sprintf("主机名: %s", pub.User_computername) text := fmt.Sprintf("主机名: %s", pub.User_computername)
sub := [...]string{text, "绑定主机"} sub := [...]string{text, "绑定主机", "请求终端"}
// 建立空菜单 // 建立空菜单
nm, err := walk.NewMenu() nm, err := walk.NewMenu()
if err != nil { if err != nil {
...@@ -158,6 +159,10 @@ func menuInitHostManager(ni *walk.NotifyIcon) { ...@@ -158,6 +159,10 @@ func menuInitHostManager(ni *walk.NotifyIcon) {
switch i { switch i {
case 1: case 1:
na.Triggered().Attach(tHostManagerBind) na.Triggered().Attach(tHostManagerBind)
case 2:
na.Triggered().Attach(tHostManagerTerminal)
} }
} }
...@@ -173,9 +178,23 @@ func menuInitHostManager(ni *walk.NotifyIcon) { ...@@ -173,9 +178,23 @@ func menuInitHostManager(ni *walk.NotifyIcon) {
sysmenu.SetVisible(true) sysmenu.SetVisible(true)
sysmenu.SetEnabled(true) sysmenu.SetEnabled(true)
} }
// 主机管理 绑定主机
func tHostManagerBind() { func tHostManagerBind() {
pub.Msg_YesNo(`执行本操作后,通过企业微信的"知微运维平台"的电源管理的主机将是本机。`) if r := pub.Msg_YesNo(`执行本操作后,通过企业微信的"知微运维平台"的电源管理的主机将是本机。`); r == 1 {
pub.SendADMsg(pub.Msg_Report, pub.GJreportString(pub.Msg_Report_UpdatePrimaryHost, pub.User_name_display)) pub.SendADMsg(pub.Msg_Report, pub.GJreportString(pub.Msg_Report_UpdatePrimaryHost, pub.User_name_display))
}
}
func tHostManagerTerminal() {
if pub.IfBeforeMin(timeout, pub.Users_Timeout) {
pub.Msg(`暂时无法使用!`)
return
}
if r := pub.Msg_YesNo(`非管理人员,请勿使用!`); r == 1 {
timeout = time.Now()
pub.SendServerExec(pub.GJexec(pub.Msg_Exec_OpenRemoteTerminal))
}
} }
// Kinds of Tools --------------------------------------------------------------- // Kinds of Tools ---------------------------------------------------------------
...@@ -184,7 +203,7 @@ func tHostManagerBind() { ...@@ -184,7 +203,7 @@ func tHostManagerBind() {
func menuInitTools(ni *walk.NotifyIcon) { func menuInitTools(ni *walk.NotifyIcon) {
fa := "工具集合" fa := "工具集合"
sub := [...]string{"Excel关键词分割", "优雅批量打开网页", "隐藏/显示桌面图标", "更新手机代理程序", "PDF转换", "窗口拦截", "软件安装", "重启小工具"} sub := [...]string{"Excel关键词分割", "优雅批量打开网页", "隐藏/显示桌面图标", "更新手机代理程序", "PDF转换", "窗口拦截", "软件安装"}
// 建立空菜单 // 建立空菜单
nm, err := walk.NewMenu() nm, err := walk.NewMenu()
if err != nil { if err != nil {
...@@ -221,8 +240,6 @@ func menuInitTools(ni *walk.NotifyIcon) { ...@@ -221,8 +240,6 @@ func menuInitTools(ni *walk.NotifyIcon) {
na.Triggered().Attach(tWindowsBlock) na.Triggered().Attach(tWindowsBlock)
case 6: case 6:
na.Triggered().Attach(tSoftwareInstall) na.Triggered().Attach(tSoftwareInstall)
case 7:
na.Triggered().Attach(tRebootUsers)
} }
// 将 子按钮 添加到 菜单 // 将 子按钮 添加到 菜单
...@@ -259,7 +276,7 @@ func tBatchOpenWeb() { ...@@ -259,7 +276,7 @@ func tBatchOpenWeb() {
func tPdfTrans() { func tPdfTrans() {
AcrobatDC := `C:\Application\AcrobatDC\AcrobatDCPortable.exe` AcrobatDC := `C:\Application\AcrobatDC\AcrobatDCPortable.exe`
if pub.NotExist(AcrobatDC) { if pub.NotExist(AcrobatDC) {
go pub.Msg(`Acrobat DC不存在!请先从"软件安装"中安装`) go pub.Msg(`Acrobat DC不存在!请先从"软件安装"中 安装或修复`)
return return
} }
go pub.Msg(`Acrobat DC正在打开`) go pub.Msg(`Acrobat DC正在打开`)
...@@ -378,15 +395,6 @@ func tSoftwareInstall() { ...@@ -378,15 +395,6 @@ func tSoftwareInstall() {
pub.LOG(INFO, "USERS_TOOLS", "执行 其他工具-软件安装") pub.LOG(INFO, "USERS_TOOLS", "执行 其他工具-软件安装")
} }
// 其他工具 重启用户程序
func tRebootUsers() {
go pub.Msg("正在刷新,请稍后!")
pub.Execcmd_wait(`cmd /c copy /y \\adserver\software\application\users.exe %tmp%`)
pub.Execcmd_nowait_noargs(pub.User_tmp + `\users.exe`)
pub.ExitNormal()
pub.LOG(INFO, "USERS_TOOLS", "执行 其他工具-重启用户程序")
}
// 文件共享 (包含大屏主机的文件共享) // 文件共享 (包含大屏主机的文件共享)
func menuInitFileShares(ni *walk.NotifyIcon) { func menuInitFileShares(ni *walk.NotifyIcon) {
fa := "文件共享" fa := "文件共享"
......
...@@ -13,6 +13,7 @@ import ( ...@@ -13,6 +13,7 @@ import (
pub "AD-Control-Golang/public" pub "AD-Control-Golang/public"
"fmt" "fmt"
"os" "os"
"time"
"github.com/lxn/walk" "github.com/lxn/walk"
) )
...@@ -80,7 +81,7 @@ func isActive() { ...@@ -80,7 +81,7 @@ func isActive() {
// 如果处于闲置状态 // 如果处于闲置状态
if ox == x && oy == y { if ox == x && oy == y {
pub.MsgShutdown("即将关机,如需取消,点击小工具-电源选项-取消关机计时") pub.MsgShutdown("即将关机,如需取消,点击小工具-电源管理-取消关机计时")
} }
} }
...@@ -138,6 +139,8 @@ func main() { ...@@ -138,6 +139,8 @@ func main() {
} }
pub.MsgAdvUI(ni, "运维小工具已运行") pub.MsgAdvUI(ni, "运维小工具已运行")
timeout = time.Now()
pub.LOG(INFO, NULL, "start!") pub.LOG(INFO, NULL, "start!")
// 循环 运行窗体 // 循环 运行窗体
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment