2024-08-22 09:22:52 +08:00
package net.geedge.api.util ;
import cn.hutool.core.codec.Base32Codec ;
import cn.hutool.core.thread.NamedThreadFactory ;
import cn.hutool.log.Log ;
import net.geedge.api.entity.DeviceApiYml ;
import net.geedge.common.APIException ;
import net.geedge.common.Constant ;
import net.geedge.common.RCode ;
import net.geedge.common.T ;
import java.io.File ;
import java.nio.file.Paths ;
import java.util.* ;
import java.util.concurrent.* ;
import java.util.regex.Matcher ;
import java.util.regex.Pattern ;
public class AdbUtil {
private static final Log log = Log . get ( ) ;
private static AdbUtil instance ;
2024-08-23 10:44:27 +08:00
private static String DEFAULT_DROIDVNC_NG_APK_PATH = " ./lib/droidvnc-np-2.6.0.apk " ;
private static String DEFAULT_DROIDVNC_NG_DEFAULTS_JSON_PATH = " ./lib/droidvnc-np-defaults.json " ;
2024-08-22 09:22:52 +08:00
private String serial ;
private String host ;
private Integer port ;
private ExecutorService threadPool ;
public String getSerial ( ) {
return T . StrUtil . isNotEmpty ( this . serial ) ? serial : String . format ( " %s:%s " , this . host , this . port ) ;
}
public record CommandResult ( Integer exitCode , String output ) {
}
private AdbUtil ( DeviceApiYml . Adb adb ) {
this . serial = T . StrUtil . emptyToDefault ( adb . getSerial ( ) , " " ) ;
this . host = adb . getHost ( ) ;
this . port = adb . getPort ( ) ;
2024-08-23 10:44:27 +08:00
// adb connect
2024-08-22 09:22:52 +08:00
this . connect ( ) ;
2024-08-23 10:44:27 +08:00
// init
this . init ( ) ;
2024-08-22 09:22:52 +08:00
}
public static AdbUtil getInstance ( DeviceApiYml . Adb connInfo ) {
if ( instance = = null ) {
synchronized ( AdbUtil . class ) {
if ( instance = = null ) {
instance = new AdbUtil ( connInfo ) ;
}
}
}
return instance ;
}
/ * *
* connect
* /
private void connect ( ) {
if ( T . StrUtil . isNotEmpty ( this . serial ) ) {
// local
AdbDevice adbDevice = this . getAdbDevice ( ) ;
log . info ( " [connect] [result: {}] " , T . JSONUtil . toJsonStr ( adbDevice ) ) ;
if ( null = = adbDevice | | ! adbDevice . isAvailable ( ) ) {
log . error ( " [device is not available, program exit] " ) ;
Runtime . getRuntime ( ) . halt ( 1 ) ;
}
} else {
// remote
String result = CommandExec . exec ( AdbCommandBuilder . builder ( )
. buildConnectCommand ( this . host , this . port )
. build ( ) ) ;
log . info ( " [connect] [result: {}] " , result ) ;
if ( ! T . StrUtil . contains ( result , " connected " ) ) {
log . error ( " [connect error, program exit] " ) ;
Runtime . getRuntime ( ) . halt ( 1 ) ;
}
}
2024-08-23 10:44:27 +08:00
}
/ * *
* init
* su root
* install droidVNC NG
* /
private void init ( ) {
2024-08-22 09:22:52 +08:00
// adb root
2024-08-23 10:44:27 +08:00
String result = CommandExec . exec ( AdbCommandBuilder . builder ( )
2024-08-22 09:22:52 +08:00
. serial ( this . getSerial ( ) )
. buildRootCommand ( )
. build ( )
) ;
2024-08-23 10:44:27 +08:00
log . info ( " [init] [adb root] [result: {}] " , result ) ;
// install droidVNC NG
CommandResult installed = this . install ( DEFAULT_DROIDVNC_NG_APK_PATH , true , true ) ;
log . info ( " [init] [install droidVNC NG] [result: {}] " , installed ) ;
// 上传默认配置
this . execShellCommand ( " shell mkdir -p /storage/emulated/0/Android/data/net.christianbeier.droidvnc_ng/files " ) ;
this . push ( DEFAULT_DROIDVNC_NG_DEFAULTS_JSON_PATH , " /storage/emulated/0/Android/data/net.christianbeier.droidvnc_ng/files/defaults.json " ) ;
// 无障碍权限
this . execShellCommand ( " shell settings put secure enabled_accessibility_services net.christianbeier.droidvnc_ng/.InputService:$(settings get secure enabled_accessibility_services) " ) ;
// 存储空间权限
this . execShellCommand ( " shell pm grant net.christianbeier.droidvnc_ng android.permission.WRITE_EXTERNAL_STORAGE " ) ;
// 屏幕录制权限
this . execShellCommand ( " shell appops set net.christianbeier.droidvnc_ng PROJECT_MEDIA allow " ) ;
// 后台启动
this . execShellCommand ( " shell am start-foreground-service -n net.christianbeier.droidvnc_ng/.MainService -a net.christianbeier.droidvnc_ng.ACTION_STOP --es net.christianbeier.droidvnc_ng.EXTRA_ACCESS_KEY d042e2b5d5f348588a4e1a243eb7a9a0 " ) ;
this . execShellCommand ( " shell am start-foreground-service -n net.christianbeier.droidvnc_ng/.MainService -a net.christianbeier.droidvnc_ng.ACTION_START --es net.christianbeier.droidvnc_ng.EXTRA_ACCESS_KEY d042e2b5d5f348588a4e1a243eb7a9a0 " ) ;
2024-08-22 09:22:52 +08:00
}
/ * *
* status
*
* @return
* /
public Map < Object , Object > status ( ) {
Map < Object , Object > m = T . MapUtil . builder ( )
. put ( " platform " , " android " )
. build ( ) ;
AdbDevice device = this . getAdbDevice ( ) ;
m . put ( " status " , device . isAvailable ( ) ? " online " : " offline " ) ;
Map < String , String > prop = this . getProp ( ) ;
m . put ( " name " , T . MapUtil . getStr ( prop , " ro.product.name " , " " ) ) ;
m . put ( " brand " , T . MapUtil . getStr ( prop , " ro.product.brand " , " " ) ) ;
m . put ( " model " , T . MapUtil . getStr ( prop , " ro.product.model " , " " ) ) ;
m . put ( " version " , T . MapUtil . getStr ( prop , " ro.build.version.release " , " " ) ) ;
m . put ( " resolution " , T . MapUtil . getStr ( prop , " wm.size " , " " ) ) ;
// 默认为真机
String type = " device " ;
for ( Map . Entry < String , String > entry : prop . entrySet ( ) ) {
// 根据 ro.build.* 这一组配置值判定是否为模拟器,如果包含 emulator、sdk 则为模拟器
if ( entry . getKey ( ) . contains ( " ro.build " ) ) {
String value = entry . getValue ( ) ;
if ( T . StrUtil . containsAnyIgnoreCase ( value , " emulator " , " sdk " ) ) {
type = " emulator " ;
break ;
}
}
}
m . put ( " type " , type ) ;
// check root
String checkRootResult = CommandExec . exec ( AdbCommandBuilder . builder ( )
. serial ( this . getSerial ( ) )
. buildCheckRootCommand ( )
. build ( )
) ;
m . put ( " root " , ! T . StrUtil . containsIgnoreCase ( checkRootResult , " Permission denied " ) ) ;
return m ;
}
/ * *
* getAdbDevice
* adb devices - l
*
* @return
* /
private AdbDevice getAdbDevice ( ) {
String result = CommandExec . exec ( AdbCommandBuilder . builder ( )
. buildDevicesCommand ( )
. build ( )
) ;
List < AdbDevice > list = T . ListUtil . list ( true ) ;
String [ ] lines = result . split ( " \\ n " ) ;
for ( String line : lines ) {
if ( line . startsWith ( " * " ) | | line . startsWith ( " List " ) | | T . StrUtil . isEmpty ( line ) )
continue ;
list . add ( new AdbDevice ( line ) ) ;
}
AdbDevice adbDevice = list . stream ( )
. filter ( pojo - > T . StrUtil . equals ( pojo . getSerial ( ) , this . getSerial ( ) ) )
. findFirst ( )
. orElse ( null ) ;
return adbDevice ;
}
/ * *
* getProp
* adb shell getprop
*
* @return
* /
private Map < String , String > getProp ( ) {
String result = CommandExec . exec ( AdbCommandBuilder . builder ( )
. serial ( this . getSerial ( ) )
. buildGetpropCommand ( )
. build ( )
) ;
Map < String , String > prop = new LinkedHashMap < > ( ) ;
Pattern pattern = Pattern . compile ( " \\ [(.*?) \\ ]: \\ [(.*?) \\ ] " ) ;
Matcher matcher = pattern . matcher ( result ) ;
while ( matcher . find ( ) ) {
String key = matcher . group ( 1 ) . trim ( ) ;
String value = matcher . group ( 2 ) . trim ( ) ;
prop . put ( key , value ) ;
}
// 分辨率 Physical size: 1440x3040
String wmSize = CommandExec . exec ( AdbCommandBuilder . builder ( )
. serial ( this . getSerial ( ) )
. buildWmSizeCommand ( )
. build ( )
) ;
prop . put ( " wm.size " , T . StrUtil . trim ( wmSize . replaceAll ( " Physical size: " , " " ) ) ) ;
return prop ;
}
/ * *
* md5sum
* /
private CommandResult md5sum ( String path ) {
String result = CommandExec . exec ( AdbCommandBuilder . builder ( )
. serial ( this . getSerial ( ) )
. buildMd5sumCommand ( path )
. build ( )
) ;
log . info ( " [md5sum] [path: {}] [result: {}] " , path , result ) ;
if ( T . StrUtil . isNotEmpty ( result ) ) {
String md5 = result . split ( " \\ s+ " ) [ 0 ] ;
return new CommandResult ( 0 , md5 ) ;
}
return new CommandResult ( 1 , " " ) ;
}
/ * *
* push
* 0 success ; ! 0 failed
* /
public CommandResult push ( String local , String remote ) {
String result = CommandExec . exec ( AdbCommandBuilder . builder ( )
. serial ( this . getSerial ( ) )
. buildPushCommand ( local , remote )
. build ( )
) ;
log . info ( " [push] [local: {}] [remote: {}] [result: {}] " , local , remote , result ) ;
return new CommandResult ( T . StrUtil . contains ( result , " failed " ) ? 1 : 0 , result ) ;
}
/ * *
* pull
* 0 success ; ! 0 failed
* /
public CommandResult pull ( String remote , String local ) {
String result = CommandExec . exec ( AdbCommandBuilder . builder ( )
. serial ( this . getSerial ( ) )
. buildPullCommand ( remote , local )
. build ( )
) ;
log . info ( " [pull] [remote: {}] [local: {}] [result: {}] " , remote , local , result ) ;
return new CommandResult ( T . StrUtil . containsAny ( result , " file pulled " , " files pulled " ) ? 0 : 1 , result ) ;
}
/ * *
* list dir
* ls - l
* stat filename
* /
public List < Map > listDir ( String path ) {
String result = CommandExec . exec ( AdbCommandBuilder . builder ( )
. serial ( this . getSerial ( ) )
. buildLsDirCommand ( path )
. build ( )
) ;
if ( T . StrUtil . contains ( result , " No such file or directory " ) ) {
log . warn ( " [listDir] [path: {}] [result: {}] " , path , result ) ;
throw new APIException ( RCode . NOT_EXISTS ) ;
}
if ( T . StrUtil . contains ( result , " Permission denied " ) ) {
log . warn ( " [listDir] [path: {}] [result: {}] " , path , result ) ;
throw new APIException ( RCode . NOT_PERMISSION ) ;
}
List < CompletableFuture < Map > > futureList = T . ListUtil . list ( false ) ;
List < Map > listDir = T . ListUtil . list ( true ) ;
String [ ] lines = result . split ( " \\ n " ) ;
boolean isDir = false ;
for ( String line : lines ) {
if ( line . startsWith ( " total " ) ) {
isDir = true ;
continue ;
}
String [ ] split = line . split ( " \\ s+ " ) ;
String name ;
// link file|dir
if ( 10 = = split . length ) {
name = split [ 7 ] ;
} else {
name = split [ split . length - 1 ] ;
}
String statFilePath = isDir ? Paths . get ( path ) . resolve ( name ) . toString ( ) : path ;
String statCommand = " shell stat -c \" '%N %a %X %Y' \" " + statFilePath ;
futureList . add (
CompletableFuture . supplyAsync ( ( ) - > {
String statResult = CommandExec . exec ( AdbCommandBuilder . builder ( )
. serial ( this . getSerial ( ) )
. buildShellCommand ( statCommand . replaceAll ( " \\ \\ " , " / " ) )
. build ( )
) ;
// reverse result
List < String > list = Arrays . asList ( statResult . split ( " \\ s+ " ) ) ;
Collections . reverse ( list ) ;
String fullName = list . get ( 3 ) . replaceAll ( " '|` " , " " ) ;
Map < String , Object > relMap = T . MapUtil . newHashMap ( ) ;
relMap . put ( " name " , name ) ;
relMap . put ( " value " , T . MapUtil . builder ( )
. put ( " id " , Base32Codec . Base32Encoder . ENCODER . encode ( fullName . getBytes ( ) ) )
. put ( " fullName " , fullName )
. put ( " permissions " , Long . parseLong ( list . get ( 2 ) ) )
. put ( " cts " , Long . parseLong ( list . get ( 1 ) ) )
. put ( " uts " , Long . parseLong ( list . get ( 0 ) ) )
. build ( ) ) ;
return relMap ;
} , getThreadPool ( ) )
) ;
Map < Object , Object > m = T . MapUtil . builder ( )
. put ( " name " , name )
. build ( ) ;
listDir . add ( m ) ;
}
try {
CompletableFuture . allOf ( futureList . toArray ( new CompletableFuture [ 0 ] ) ) . get ( ) ;
futureList . forEach ( f - > {
Map map = f . getNow ( null ) ;
if ( T . MapUtil . isNotEmpty ( map ) ) {
String name = T . MapUtil . getStr ( map , " name " ) ;
Map fileAttr = listDir . stream ( ) . filter ( m - > T . MapUtil . getStr ( m , " name " ) . equals ( name ) ) . findFirst ( ) . get ( ) ;
fileAttr . putAll ( T . MapUtil . get ( map , " value " , Map . class ) ) ;
}
} ) ;
} catch ( Exception e ) {
log . warn ( e ) ;
}
return listDir ;
}
/ * *
* listApp
* adb shell pm list packages
*
* @return
* /
public List < Map > listApp ( String arg ) {
String result = CommandExec . exec ( AdbCommandBuilder . builder ( )
. serial ( this . getSerial ( ) )
. buildPmListPackagesCommand ( arg )
. build ( )
) ;
List < Map > listApp = T . ListUtil . list ( true ) ;
List < CompletableFuture < Map > > futureList = T . ListUtil . list ( false ) ;
String prefix = " package: " ;
String [ ] lines = result . split ( " \\ n " ) ;
for ( String line : lines ) {
String packageName = T . StrUtil . trim ( line . substring ( prefix . length ( ) ) ) ;
String dumpsysResult = CommandExec . exec ( AdbCommandBuilder . builder ( )
. serial ( this . getSerial ( ) )
. buildShellCommand ( " shell dumpsys package " + packageName )
. build ( )
) ;
String [ ] split = dumpsysResult . split ( " \\ n " ) ;
String version = " " , apkPath = " " ;
for ( String s : split ) {
if ( s . contains ( " versionName= " ) ) {
version = T . StrUtil . trim ( s ) . replaceAll ( " versionName= " , " " ) ;
}
if ( s . contains ( " path: " ) ) {
apkPath = T . StrUtil . trim ( s ) . replaceAll ( " path: " , " " ) ;
}
}
if ( T . StrUtil . isNotEmpty ( apkPath ) ) {
String finalApkPath = apkPath ;
futureList . add (
CompletableFuture . supplyAsync ( ( ) - > {
try {
CommandResult md5sumRes = this . md5sum ( finalApkPath ) ;
String md5Value = md5sumRes . output ( ) ;
File localApk = T . FileUtil . file ( Constant . TEMP_PATH , md5Value + " .apk " ) ;
if ( ! T . FileUtil . exist ( localApk ) ) {
CommandResult pulled = this . pull ( finalApkPath , localApk . getAbsolutePath ( ) ) ;
if ( 0 ! = pulled . exitCode ( ) ) {
log . warn ( " [listApp] [pull apk error] [pkg: {}] " , packageName ) ;
return null ;
}
}
ApkUtil apkUtil = new ApkUtil ( ) ;
ApkInfo apkInfo = apkUtil . parseApk ( localApk . getAbsolutePath ( ) ) ;
String appName = apkInfo . getLabel ( ) ;
String iconFilename = apkInfo . getIcon ( ) ;
String base64IconDate = apkUtil . extractFileFromApk ( localApk . getAbsolutePath ( ) , iconFilename ) ;
Map < String , Object > relMap = T . MapUtil . newHashMap ( ) ;
relMap . put ( " pkg " , packageName ) ;
relMap . put ( " value " , T . MapUtil . builder ( )
. put ( " name " , appName )
. put ( " icon " , base64IconDate )
. build ( ) ) ;
return relMap ;
} catch ( Exception e ) {
log . error ( e , " [listApp] [parse apk] [pkg: {}] " , packageName ) ;
}
return null ;
} , getThreadPool ( ) )
) ;
}
Map < Object , Object > m = T . MapUtil . builder ( )
. put ( " packageName " , packageName )
. put ( " version " , version )
. build ( ) ;
listApp . add ( m ) ;
}
try {
CompletableFuture . allOf ( futureList . toArray ( new CompletableFuture [ 0 ] ) ) . get ( ) ;
futureList . forEach ( f - > {
Map map = f . getNow ( null ) ;
if ( T . MapUtil . isNotEmpty ( map ) ) {
String pkg = T . MapUtil . getStr ( map , " pkg " ) ;
Map appAttr = listApp . stream ( ) . filter ( m - > T . MapUtil . getStr ( m , " packageName " ) . equals ( pkg ) ) . findFirst ( ) . get ( ) ;
appAttr . putAll ( T . MapUtil . get ( map , " value " , Map . class ) ) ;
}
} ) ;
} catch ( Exception e ) {
log . warn ( e ) ;
}
return listApp ;
}
/ * *
* install app
* adb install apk
* /
public CommandResult install ( String localFilePath , boolean isDebugApk , boolean isReInstall ) {
String result = CommandExec . exec ( AdbCommandBuilder . builder ( )
. serial ( this . getSerial ( ) )
. buildInstallCommand ( localFilePath , isDebugApk , isReInstall )
. build ( )
) ;
log . info ( " [install] [localFilePath: {}] [isDebugApk: {}] [isReInstall: {}] [result: {}] " , localFilePath , isDebugApk , isReInstall , result ) ;
return new CommandResult ( T . StrUtil . containsAny ( result , " Success " ) ? 0 : 1 , result ) ;
}
/ * *
* uninstall app
* adb uninstall package_name
* /
public CommandResult uninstall ( String packageName ) {
String result = CommandExec . exec ( AdbCommandBuilder . builder ( )
. serial ( this . getSerial ( ) )
. buildUnInstallCommand ( packageName )
. build ( )
) ;
log . info ( " [uninstall] [packageName: {}] [result: {}] " , packageName , result ) ;
return new CommandResult ( T . StrUtil . containsAny ( result , " Success " ) ? 0 : 1 , result ) ;
}
/ * *
* iptables - F
* iptables - X
* /
private void cleanIptables ( ) {
// Delete all rules in chain or all chains
CommandExec . exec ( AdbCommandBuilder . builder ( )
. serial ( this . getSerial ( ) )
. buildShellCommand ( " shell iptables -F " )
. build ( )
) ;
// Delete user-defined chain
CommandExec . exec ( AdbCommandBuilder . builder ( )
. serial ( this . getSerial ( ) )
. buildShellCommand ( " shell iptables -X " )
. build ( )
) ;
}
/ * *
* start Tcpdump
* iptables option
* tcpdump pcap
* /
public CommandResult startTcpdump ( String packageName ) {
// clean iptables conf
this . cleanIptables ( ) ;
String taskId = T . IdUtil . fastSimpleUUID ( ) ;
String pcapFilePath = " /data/local/tmp/ " + taskId + " .pcap " ;
if ( T . StrUtil . isNotEmpty ( packageName ) ) {
log . info ( " [startTcpdump] [capture app package] [pkg: {}] " , packageName ) ;
String dumpsysResult = CommandExec . exec ( AdbCommandBuilder . builder ( )
. serial ( this . getSerial ( ) )
. buildShellCommand ( " shell dumpsys package " + packageName )
. build ( )
) ;
String [ ] lines = dumpsysResult . split ( " \\ n " ) ;
String userId = Arrays . stream ( lines )
. filter ( s - > T . StrUtil . contains ( s , " userId= " ) )
. findFirst ( )
. map ( s - > T . StrUtil . trim ( s ) . replaceAll ( " userId= " , " " ) )
. orElseThrow ( ( ) - > new APIException ( " Not found userId by package name. package name: " + packageName ) ) ;
CommandExec . exec ( AdbCommandBuilder . builder ( )
. serial ( this . getSerial ( ) )
. buildShellCommand ( String . format ( " shell iptables -A OUTPUT -m owner --uid-owner %s -j CONNMARK --set-mark %s " , userId , userId ) )
. build ( ) ) ;
CommandExec . exec ( AdbCommandBuilder . builder ( )
. serial ( this . getSerial ( ) )
. buildShellCommand ( String . format ( " shell iptables -A INPUT -m connmark --mark %s -j NFLOG --nflog-group %s " , userId , userId ) )
. build ( ) ) ;
CommandExec . exec ( AdbCommandBuilder . builder ( )
. serial ( this . getSerial ( ) )
. buildShellCommand ( String . format ( " shell iptables -A OUTPUT -m connmark --mark %s -j NFLOG --nflog-group %s " , userId , userId ) )
. build ( ) ) ;
String ruleList = CommandExec . exec ( AdbCommandBuilder . builder ( )
. serial ( this . getSerial ( ) )
. buildShellCommand ( " shell iptables -L " )
. build ( ) ) ;
log . info ( " [startTcpdump] [iptables -L] [result: {}] " , ruleList ) ;
CommandExec . execForProcess ( AdbCommandBuilder . builder ( )
. serial ( this . getSerial ( ) )
. buildShellCommand ( String . format ( " shell tcpdump -i nflog:%s -w %s & " , userId , pcapFilePath ) )
. build ( ) ) ;
} else {
log . info ( " [startTcpdump] [capture all package] " ) ;
CommandExec . execForProcess ( AdbCommandBuilder . builder ( )
. serial ( this . getSerial ( ) )
. buildShellCommand ( String . format ( " shell tcpdump -w %s & " , pcapFilePath ) )
. build ( ) ) ;
}
String result = CommandExec . exec ( AdbCommandBuilder . builder ( )
. serial ( this . getSerial ( ) )
. buildShellCommand ( String . format ( " shell \" ps -ef | grep tcpdump | grep -v grep | grep %s | awk '{print $2}' \" " , taskId ) )
. build ( ) ) ;
log . info ( " [startTcpdump] [taskId: {}] [tcpdump pid: {}] " , taskId , result ) ;
return new CommandResult ( T . StrUtil . isNotEmpty ( result ) ? 0 : 1 , taskId ) ;
}
/ * *
* stop tcpdump
* kill - INT { pid }
* /
public CommandResult stopTcpdump ( String id ) {
String result = CommandExec . exec ( AdbCommandBuilder . builder ( )
. serial ( this . getSerial ( ) )
. buildShellCommand ( String . format ( " shell \" ps -ef | grep tcpdump | grep -v grep | grep %s | awk '{print $2}' | xargs kill -INT \" " , id ) )
. build ( ) ) ;
log . info ( " [stopTcpdump] [id: {}] [result: {}] " , id , result ) ;
return new CommandResult ( T . StrUtil . isEmpty ( result ) ? 0 : 1 , result ) ;
}
2024-08-23 10:44:27 +08:00
/ * *
* exec shell command
* /
public void execShellCommand ( String shellCmd ) {
String result = CommandExec . exec ( AdbCommandBuilder . builder ( )
. serial ( this . getSerial ( ) )
. buildShellCommand ( shellCmd )
. build ( ) ) ;
log . info ( " [execShellCommand] [shellCmd: {}] [result: {}] " , shellCmd , result ) ;
}
2024-08-22 09:22:52 +08:00
private synchronized ExecutorService getThreadPool ( ) {
if ( threadPool = = null ) {
threadPool = new ThreadPoolExecutor (
5 ,
10 ,
30 ,
TimeUnit . SECONDS ,
new ArrayBlockingQueue < Runnable > ( 10000 ) ,
new NamedThreadFactory ( " API- " , true ) ) ;
}
return threadPool ;
}
class CommandExec {
public static String exec ( List < String > command ) {
String str = T . RuntimeUtil . execForStr ( T . CharsetUtil . CHARSET_UTF_8 , command . stream ( ) . toArray ( String [ ] : : new ) ) ;
return str . stripTrailing ( ) ;
}
public static Process execForProcess ( List < String > command ) {
Process process = T . RuntimeUtil . exec ( command . stream ( ) . toArray ( String [ ] : : new ) ) ;
return process ;
}
}
}