UPDATE: native with new hdiff type
This commit is contained in:
@@ -6,6 +6,15 @@
|
|||||||
// @ts-ignore: Unused imports
|
// @ts-ignore: Unused imports
|
||||||
import {Call as $Call, Create as $Create} from "@wailsio/runtime";
|
import {Call as $Call, Create as $Create} from "@wailsio/runtime";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {string} patchPath
|
||||||
|
* @returns {Promise<[boolean, string, string]> & { cancel(): void }}
|
||||||
|
*/
|
||||||
|
export function CheckTypeHDiff(patchPath) {
|
||||||
|
let $resultPromise = /** @type {any} */($Call.ByID(1068035136, patchPath));
|
||||||
|
return $resultPromise;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {string} gamePath
|
* @param {string} gamePath
|
||||||
* @returns {Promise<[boolean, string]> & { cancel(): void }}
|
* @returns {Promise<[boolean, string]> & { cancel(): void }}
|
||||||
@@ -18,10 +27,11 @@ export function CutData(gamePath) {
|
|||||||
/**
|
/**
|
||||||
* @param {string} gamePath
|
* @param {string} gamePath
|
||||||
* @param {string} patchPath
|
* @param {string} patchPath
|
||||||
|
* @param {boolean} isSkipVerify
|
||||||
* @returns {Promise<[boolean, string]> & { cancel(): void }}
|
* @returns {Promise<[boolean, string]> & { cancel(): void }}
|
||||||
*/
|
*/
|
||||||
export function DataExtract(gamePath, patchPath) {
|
export function DataExtract(gamePath, patchPath, isSkipVerify) {
|
||||||
let $resultPromise = /** @type {any} */($Call.ByID(1843136452, gamePath, patchPath));
|
let $resultPromise = /** @type {any} */($Call.ByID(1843136452, gamePath, patchPath, isSkipVerify));
|
||||||
return $resultPromise;
|
return $resultPromise;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -116,19 +116,32 @@ export default function HdiffzPage() {
|
|||||||
setIsDiffLoading(false)
|
setIsDiffLoading(false)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
setStageType('Version Validate')
|
setStageType('Check Type HDiff')
|
||||||
setProgressUpdate(0)
|
setProgressUpdate(0)
|
||||||
setMaxProgressUpdate(1)
|
setMaxProgressUpdate(1)
|
||||||
const [validVersion, errorVersion] = await HdiffzService.VersionValidate(gameDir, diffDir)
|
const [isOk, validType, errorType] = await HdiffzService.CheckTypeHDiff(diffDir)
|
||||||
if (!validVersion) {
|
if (!isOk) {
|
||||||
toast.error(errorVersion)
|
toast.error(errorType)
|
||||||
setIsDiffLoading(false)
|
setIsDiffLoading(false)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
setProgressUpdate(1)
|
setProgressUpdate(1)
|
||||||
|
|
||||||
|
if (validType === 'hdiffmap.json') {
|
||||||
|
setStageType('Version Validate')
|
||||||
|
setProgressUpdate(0)
|
||||||
|
setMaxProgressUpdate(1)
|
||||||
|
const [validVersion, errorVersion] = await HdiffzService.VersionValidate(gameDir, diffDir)
|
||||||
|
if (!validVersion) {
|
||||||
|
toast.error(errorVersion)
|
||||||
|
setIsDiffLoading(false)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
setProgressUpdate(1)
|
||||||
|
}
|
||||||
|
|
||||||
setStageType('Data Extract')
|
setStageType('Data Extract')
|
||||||
const [validData, errorData] = await HdiffzService.DataExtract(gameDir, diffDir)
|
const [validData, errorData] = await HdiffzService.DataExtract(gameDir, diffDir, validType === 'hdifffiles.txt')
|
||||||
if (!validData) {
|
if (!validData) {
|
||||||
toast.error(errorData)
|
toast.error(errorData)
|
||||||
setIsDiffLoading(false)
|
setIsDiffLoading(false)
|
||||||
|
|||||||
@@ -7,19 +7,30 @@ import (
|
|||||||
"firefly-launcher/pkg/models"
|
"firefly-launcher/pkg/models"
|
||||||
"firefly-launcher/pkg/sevenzip"
|
"firefly-launcher/pkg/sevenzip"
|
||||||
"firefly-launcher/pkg/verifier"
|
"firefly-launcher/pkg/verifier"
|
||||||
|
"firefly-launcher/pkg/hpatchz"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"os"
|
"os"
|
||||||
"os/exec"
|
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
"syscall"
|
|
||||||
|
|
||||||
"github.com/wailsapp/wails/v3/pkg/application"
|
"github.com/wailsapp/wails/v3/pkg/application"
|
||||||
)
|
)
|
||||||
|
|
||||||
type HdiffzService struct{}
|
type HdiffzService struct{}
|
||||||
|
|
||||||
|
func (h *HdiffzService) CheckTypeHDiff(patchPath string) (bool, string, string) {
|
||||||
|
isFileInTxt, _ := sevenzip.IsFileIn7z(patchPath, "hdifffiles.txt")
|
||||||
|
if isFileInTxt {
|
||||||
|
return true, "hdifffiles.txt", ""
|
||||||
|
}
|
||||||
|
isFileInJson, _ := sevenzip.IsFileIn7z(patchPath, "hdiffmap.json")
|
||||||
|
if isFileInJson {
|
||||||
|
return true, "hdiffmap.json", ""
|
||||||
|
}
|
||||||
|
return false, "", "not found hdifffiles.txt or hdiffmap.json"
|
||||||
|
}
|
||||||
|
|
||||||
func (h *HdiffzService) VersionValidate(gamePath, patchPath string) (bool, string) {
|
func (h *HdiffzService) VersionValidate(gamePath, patchPath string) (bool, string) {
|
||||||
oldVersionData, err := models.ParseBinaryVersion(filepath.Join(gamePath, "StarRail_Data\\StreamingAssets\\BinaryVersion.bytes"))
|
oldVersionData, err := models.ParseBinaryVersion(filepath.Join(gamePath, "StarRail_Data\\StreamingAssets\\BinaryVersion.bytes"))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -53,7 +64,8 @@ func (h *HdiffzService) VersionValidate(gamePath, patchPath string) (bool, strin
|
|||||||
|
|
||||||
return true, "validated"
|
return true, "validated"
|
||||||
}
|
}
|
||||||
func (h *HdiffzService) DataExtract(gamePath, patchPath string) (bool, string) {
|
|
||||||
|
func (h *HdiffzService) DataExtract(gamePath, patchPath string, isSkipVerify bool) (bool, string) {
|
||||||
if _, err := os.Stat(gamePath); err != nil {
|
if _, err := os.Stat(gamePath); err != nil {
|
||||||
return false, err.Error()
|
return false, err.Error()
|
||||||
}
|
}
|
||||||
@@ -73,15 +85,17 @@ func (h *HdiffzService) DataExtract(gamePath, patchPath string) (bool, string) {
|
|||||||
return false, err.Error()
|
return false, err.Error()
|
||||||
}
|
}
|
||||||
|
|
||||||
validator, err := verifier.NewVerifier(gamePath, constant.TempUrl)
|
if !isSkipVerify {
|
||||||
if err != nil {
|
validator, err := verifier.NewVerifier(gamePath, constant.TempUrl)
|
||||||
os.RemoveAll(constant.TempUrl)
|
if err != nil {
|
||||||
return false, err.Error()
|
os.RemoveAll(constant.TempUrl)
|
||||||
}
|
return false, err.Error()
|
||||||
|
}
|
||||||
|
|
||||||
if err := validator.VerifyAll(); err != nil {
|
if err := validator.VerifyAll(); err != nil {
|
||||||
os.RemoveAll(constant.TempUrl)
|
os.RemoveAll(constant.TempUrl)
|
||||||
return false, err.Error()
|
return false, err.Error()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return true, "validated"
|
return true, "validated"
|
||||||
@@ -136,22 +150,42 @@ func (h *HdiffzService) CutData(gamePath string) (bool, string) {
|
|||||||
return false, err.Error()
|
return false, err.Error()
|
||||||
}
|
}
|
||||||
|
|
||||||
_ = os.RemoveAll(constant.TempUrl)
|
|
||||||
|
|
||||||
return true, "cut completed"
|
return true, "cut completed"
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *HdiffzService) PatchData(gamePath string) (bool, string) {
|
func (h *HdiffzService) PatchData(gamePath string) (bool, string) {
|
||||||
data, err := os.ReadFile(filepath.Join(gamePath, "hdiffmap.json"))
|
hdiffMapPath := filepath.Join(gamePath, "hdiffmap.json")
|
||||||
if err != nil {
|
hdiffFilesPath := filepath.Join(gamePath, "hdifffiles.txt")
|
||||||
return false, err.Error()
|
|
||||||
}
|
|
||||||
|
|
||||||
var jsonData struct {
|
var jsonData struct {
|
||||||
DiffMap []*models.DiffMapType `json:"diff_map"`
|
DiffMap []*models.HDiffData `json:"diff_map"`
|
||||||
}
|
}
|
||||||
if err := json.Unmarshal(data, &jsonData); err != nil {
|
|
||||||
return false, err.Error()
|
if _, err := os.Stat(hdiffMapPath); err == nil {
|
||||||
|
data, err := os.ReadFile(hdiffMapPath)
|
||||||
|
if err != nil {
|
||||||
|
return false, err.Error()
|
||||||
|
}
|
||||||
|
|
||||||
|
var jsonDataDiffMap struct {
|
||||||
|
DiffMap []*models.DiffMapType `json:"diff_map"`
|
||||||
|
}
|
||||||
|
if err := json.Unmarshal(data, &jsonDataDiffMap); err != nil {
|
||||||
|
return false, err.Error()
|
||||||
|
}
|
||||||
|
for _, entry := range jsonDataDiffMap.DiffMap {
|
||||||
|
jsonData.DiffMap = append(jsonData.DiffMap, entry.ToHDiffData())
|
||||||
|
}
|
||||||
|
} else if _, err := os.Stat(hdiffFilesPath); err == nil {
|
||||||
|
files, err := models.LoadHDiffFiles(hdiffFilesPath)
|
||||||
|
if err != nil {
|
||||||
|
return false, err.Error()
|
||||||
|
}
|
||||||
|
for _, entry := range files {
|
||||||
|
jsonData.DiffMap = append(jsonData.DiffMap, entry.ToHDiffData())
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return false, "no hdiff entries map exist"
|
||||||
}
|
}
|
||||||
|
|
||||||
for i, entry := range jsonData.DiffMap {
|
for i, entry := range jsonData.DiffMap {
|
||||||
@@ -160,25 +194,48 @@ func (h *HdiffzService) PatchData(gamePath string) (bool, string) {
|
|||||||
"progress": i,
|
"progress": i,
|
||||||
"maxProgress": len(jsonData.DiffMap),
|
"maxProgress": len(jsonData.DiffMap),
|
||||||
})
|
})
|
||||||
|
|
||||||
sourceFile := filepath.Join(gamePath, entry.SourceFileName)
|
sourceFile := filepath.Join(gamePath, entry.SourceFileName)
|
||||||
patchFile := filepath.Join(gamePath, entry.PatchFileName)
|
patchFile := filepath.Join(gamePath, entry.PatchFileName)
|
||||||
targetFile := filepath.Join(gamePath, entry.TargetFileName)
|
targetFile := filepath.Join(gamePath, entry.TargetFileName)
|
||||||
|
|
||||||
if _, err := os.Stat(sourceFile); os.IsNotExist(err) {
|
// Check patch file tồn tại chưa
|
||||||
continue
|
|
||||||
}
|
|
||||||
if _, err := os.Stat(patchFile); os.IsNotExist(err) {
|
if _, err := os.Stat(patchFile); os.IsNotExist(err) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
cmd := exec.Command(constant.ToolHPatchzExe.String(), sourceFile, patchFile, targetFile)
|
// Nếu không có source file hoặc SourceFileName rỗng → apply_patch_empty
|
||||||
cmd.SysProcAttr = &syscall.SysProcAttr{HideWindow: true}
|
if entry.SourceFileName == "" {
|
||||||
_, err := cmd.CombinedOutput()
|
err := hpatchz.ApplyPatchEmpty(patchFile, targetFile)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
fmt.Printf("%s failed to patch! %v\n", entry.TargetFileName, err)
|
||||||
|
_ = os.Remove(patchFile)
|
||||||
|
return false, err.Error()
|
||||||
|
}
|
||||||
|
_ = os.Remove(patchFile)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if _, err := os.Stat(sourceFile); os.IsNotExist(err) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Có source file → apply_patch
|
||||||
|
err := hpatchz.ApplyPatch(sourceFile, patchFile, targetFile)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Printf("%s failed to patch! %v\n", entry.TargetFileName, err)
|
||||||
|
_ = os.Remove(patchFile)
|
||||||
|
return false, err.Error()
|
||||||
|
}
|
||||||
|
|
||||||
|
if entry.SourceFileName != entry.TargetFileName {
|
||||||
|
_ = os.Remove(sourceFile)
|
||||||
|
}
|
||||||
|
_ = os.Remove(patchFile)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
os.Remove(filepath.Join(gamePath, "hdiffmap.json"))
|
||||||
|
os.Remove(filepath.Join(gamePath, "hdifffiles.txt"))
|
||||||
return true, "patching completed"
|
return true, "patching completed"
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -200,13 +257,14 @@ func (h *HdiffzService) DeleteFiles(gamePath string) (bool, string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if err := scanner.Err(); err != nil {
|
if err := scanner.Err(); err != nil {
|
||||||
return false, ""
|
return false, "no delete files exist"
|
||||||
}
|
}
|
||||||
|
|
||||||
for i, file := range deleteFiles {
|
for i, file := range deleteFiles {
|
||||||
os.Remove(filepath.Join(gamePath, file))
|
os.Remove(filepath.Join(gamePath, file))
|
||||||
application.Get().EmitEvent("hdiffz:progress", map[string]int{"progress": i, "maxProgress": len(deleteFiles)})
|
application.Get().EmitEvent("hdiffz:progress", map[string]int{"progress": i, "maxProgress": len(deleteFiles)})
|
||||||
}
|
}
|
||||||
|
_ = os.Remove(filepath.Join(gamePath, "deletefiles.txt"))
|
||||||
return true, ""
|
return true, ""
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
1
internal/ldifff-service.go
Normal file
1
internal/ldifff-service.go
Normal file
@@ -0,0 +1 @@
|
|||||||
|
package internal
|
||||||
34
pkg/hpatchz/hpatchz.go
Normal file
34
pkg/hpatchz/hpatchz.go
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
package hpatchz
|
||||||
|
|
||||||
|
import (
|
||||||
|
"firefly-launcher/pkg/constant"
|
||||||
|
"fmt"
|
||||||
|
"os/exec"
|
||||||
|
"syscall"
|
||||||
|
)
|
||||||
|
|
||||||
|
func ApplyPatch(oldFile, diffFile, newFile string) error {
|
||||||
|
cmd := exec.Command(constant.ToolHPatchzExe.String(), "-f", oldFile, diffFile, newFile)
|
||||||
|
cmd.SysProcAttr = &syscall.SysProcAttr{HideWindow: true}
|
||||||
|
output, err := cmd.CombinedOutput()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to execute hpatchz: %w", err)
|
||||||
|
}
|
||||||
|
if cmd.ProcessState.ExitCode() != 0 {
|
||||||
|
return fmt.Errorf("hpatchz failed: %s", string(output))
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func ApplyPatchEmpty(diffFile, newFile string) error {
|
||||||
|
cmd := exec.Command(constant.ToolHPatchzExe.String(), "-f", "", diffFile, newFile)
|
||||||
|
cmd.SysProcAttr = &syscall.SysProcAttr{HideWindow: true}
|
||||||
|
output, err := cmd.CombinedOutput()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to execute hpatchz: %w", err)
|
||||||
|
}
|
||||||
|
if cmd.ProcessState.ExitCode() != 0 {
|
||||||
|
return fmt.Errorf("hpatchz failed: %s", string(output))
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
43
pkg/models/hdiffFiles.go
Normal file
43
pkg/models/hdiffFiles.go
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
package models
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bufio"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
)
|
||||||
|
|
||||||
|
type HDiffFiles struct {
|
||||||
|
RemoteFile string `json:"remoteName"`
|
||||||
|
}
|
||||||
|
func (h *HDiffFiles) ToHDiffData() *HDiffData {
|
||||||
|
return &HDiffData{
|
||||||
|
SourceFileName: h.RemoteFile,
|
||||||
|
TargetFileName: h.RemoteFile,
|
||||||
|
PatchFileName: fmt.Sprintf("%s.hdiff", h.RemoteFile),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func LoadHDiffFiles(path string) ([]*HDiffFiles, error) {
|
||||||
|
file, err := os.Open(path)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer file.Close()
|
||||||
|
|
||||||
|
var results []*HDiffFiles
|
||||||
|
scanner := bufio.NewScanner(file)
|
||||||
|
for scanner.Scan() {
|
||||||
|
line := scanner.Text()
|
||||||
|
var item HDiffFiles
|
||||||
|
if err := json.Unmarshal([]byte(line), &item); err == nil {
|
||||||
|
results = append(results, &item)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := scanner.Err(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return results, nil
|
||||||
|
}
|
||||||
@@ -12,4 +12,18 @@ type DiffMapType struct {
|
|||||||
PatchFileName string `json:"patch_file_name"`
|
PatchFileName string `json:"patch_file_name"`
|
||||||
PatchFileMD5 string `json:"patch_file_md5"`
|
PatchFileMD5 string `json:"patch_file_md5"`
|
||||||
PatchFileSize int64 `json:"patch_file_size"`
|
PatchFileSize int64 `json:"patch_file_size"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type HDiffData struct {
|
||||||
|
SourceFileName string `json:"source_file_name"`
|
||||||
|
TargetFileName string `json:"target_file_name"`
|
||||||
|
PatchFileName string `json:"patch_file_name"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *DiffMapType) ToHDiffData() *HDiffData {
|
||||||
|
return &HDiffData{
|
||||||
|
SourceFileName: d.SourceFileName,
|
||||||
|
TargetFileName: d.TargetFileName,
|
||||||
|
PatchFileName: d.PatchFileName,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
36
pkg/models/pkgVersion.go
Normal file
36
pkg/models/pkgVersion.go
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
package models
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bufio"
|
||||||
|
"encoding/json"
|
||||||
|
"os"
|
||||||
|
)
|
||||||
|
|
||||||
|
type PkgVersion struct {
|
||||||
|
RemoteFile string `json:"remoteName"`
|
||||||
|
MD5 string `json:"md5"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func LoadPkgVersion(path string) ([]*PkgVersion, error) {
|
||||||
|
file, err := os.Open(path)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer file.Close()
|
||||||
|
|
||||||
|
var results []*PkgVersion
|
||||||
|
scanner := bufio.NewScanner(file)
|
||||||
|
for scanner.Scan() {
|
||||||
|
line := scanner.Text()
|
||||||
|
var item PkgVersion
|
||||||
|
if err := json.Unmarshal([]byte(line), &item); err == nil {
|
||||||
|
results = append(results, &item)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := scanner.Err(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return results, nil
|
||||||
|
}
|
||||||
@@ -29,6 +29,41 @@ func IsFileIn7z(archivePath, fileInside string) (bool, error) {
|
|||||||
return false, fmt.Errorf("%s not found in %s", fileInside, archivePath)
|
return false, fmt.Errorf("%s not found in %s", fileInside, archivePath)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func ListFilesInZip(archivePath string) ([]string, error) {
|
||||||
|
cmd := exec.Command(constant.Tool7zaExe.String(), "l", archivePath)
|
||||||
|
var out bytes.Buffer
|
||||||
|
cmd.Stdout = &out
|
||||||
|
cmd.Stderr = &out
|
||||||
|
|
||||||
|
if err := cmd.Run(); err != nil {
|
||||||
|
return nil, fmt.Errorf("7za list failed: %v\nOutput: %s", err, out.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
lines := strings.Split(out.String(), "\n")
|
||||||
|
var files []string
|
||||||
|
foundTable := false
|
||||||
|
|
||||||
|
for _, line := range lines {
|
||||||
|
if strings.HasPrefix(line, "----------") {
|
||||||
|
if foundTable {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
foundTable = true
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if foundTable {
|
||||||
|
fields := strings.Fields(line)
|
||||||
|
if len(fields) >= 6 {
|
||||||
|
fileName := strings.Join(fields[5:], " ")
|
||||||
|
files = append(files, fileName)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return files, nil
|
||||||
|
}
|
||||||
|
|
||||||
func ExtractAFileFromZip(archivePath, fileInside, outDir string) error {
|
func ExtractAFileFromZip(archivePath, fileInside, outDir string) error {
|
||||||
cmd := exec.Command(constant.Tool7zaExe.String(), "e", archivePath, fileInside, "-o"+outDir, "-y")
|
cmd := exec.Command(constant.Tool7zaExe.String(), "e", archivePath, fileInside, "-o"+outDir, "-y")
|
||||||
cmd.Stdout = os.Stdout
|
cmd.Stdout = os.Stdout
|
||||||
|
|||||||
Reference in New Issue
Block a user