Skip to content
Projects
Groups
Snippets
Help
Loading...
Help
Support
Keyboard shortcuts
?
Submit feedback
Contribute to GitLab
Sign in / Register
Toggle navigation
C
com.ccwangluo.accelerator
Project overview
Project overview
Details
Activity
Releases
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Issues
0
Issues
0
List
Boards
Labels
Milestones
Merge Requests
0
Merge Requests
0
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Analytics
Analytics
CI / CD
Repository
Value Stream
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
sheteng
com.ccwangluo.accelerator
Commits
185f32aa
Commit
185f32aa
authored
Mar 02, 2014
by
Max Lv
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
fix the selinux issue in galaxy s4
parent
cb31dd2a
Changes
16
Show whitespace changes
Inline
Side-by-side
Showing
16 changed files
with
2009 additions
and
798 deletions
+2009
-798
src/main/java/com/github/shadowsocks/Exec.java
src/main/java/com/github/shadowsocks/Exec.java
+0
-67
src/main/java/eu/chainfire/libsuperuser/Application.java
src/main/java/eu/chainfire/libsuperuser/Application.java
+79
-0
src/main/java/eu/chainfire/libsuperuser/Debug.java
src/main/java/eu/chainfire/libsuperuser/Debug.java
+241
-0
src/main/java/eu/chainfire/libsuperuser/Shell.java
src/main/java/eu/chainfire/libsuperuser/Shell.java
+1342
-0
src/main/java/eu/chainfire/libsuperuser/ShellNotClosedException.java
...va/eu/chainfire/libsuperuser/ShellNotClosedException.java
+29
-0
src/main/java/eu/chainfire/libsuperuser/ShellOnMainThreadException.java
...eu/chainfire/libsuperuser/ShellOnMainThreadException.java
+32
-0
src/main/java/eu/chainfire/libsuperuser/StreamGobbler.java
src/main/java/eu/chainfire/libsuperuser/StreamGobbler.java
+103
-0
src/main/jni/Android.mk
src/main/jni/Android.mk
+0
-15
src/main/jni/termExec.cpp
src/main/jni/termExec.cpp
+0
-428
src/main/scala/com/github/shadowsocks/Shadowsocks.scala
src/main/scala/com/github/shadowsocks/Shadowsocks.scala
+41
-31
src/main/scala/com/github/shadowsocks/ShadowsocksNatService.scala
.../scala/com/github/shadowsocks/ShadowsocksNatService.scala
+23
-22
src/main/scala/com/github/shadowsocks/ShadowsocksRunnerActivity.scala
...la/com/github/shadowsocks/ShadowsocksRunnerActivity.scala
+1
-1
src/main/scala/com/github/shadowsocks/ShadowsocksVpnService.scala
.../scala/com/github/shadowsocks/ShadowsocksVpnService.scala
+16
-14
src/main/scala/com/github/shadowsocks/utils/Console.scala
src/main/scala/com/github/shadowsocks/utils/Console.scala
+77
-0
src/main/scala/com/github/shadowsocks/utils/Constants.scala
src/main/scala/com/github/shadowsocks/utils/Constants.scala
+9
-0
src/main/scala/com/github/shadowsocks/utils/Utils.scala
src/main/scala/com/github/shadowsocks/utils/Utils.scala
+16
-220
No files found.
src/main/java/com/github/shadowsocks/Exec.java
deleted
100755 → 0
View file @
cb31dd2a
/*
* Copyright (C) 2007 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package
com.github.shadowsocks
;
import
java.io.FileDescriptor
;
/**
* Utility methods for creating and managing a subprocess.
* <p/>
* Note: The native methods access a package-private java.io.FileDescriptor
* field to get and set the raw Linux file descriptor. This might break if the
* implementation of java.io.FileDescriptor is changed.
*/
public
class
Exec
{
static
{
java
.
lang
.
System
.
loadLibrary
(
"exec"
);
}
/** Close a given file descriptor. */
public
static
native
void
close
(
FileDescriptor
fd
);
/**
* Create a subprocess. Differs from java.lang.ProcessBuilder in that a pty
* is used to communicate with the subprocess.
* <p/>
* Callers are responsible for calling Exec.close() on the returned file
* descriptor.
*
* @param rdt Whether redirect stdout and stderr
* @param cmd The command to execute.
* @param args An array of arguments to the command.
* @param envVars An array of strings of the form "VAR=value" to be added to the
* environment of the process.
* @param scripts The scripts to execute.
* @param processId A one-element array to which the process ID of the started
* process will be written.
* @return File descriptor
*/
public
static
native
FileDescriptor
createSubprocess
(
int
rdt
,
String
cmd
,
String
[]
args
,
String
[]
envVars
,
String
scripts
,
int
[]
processId
);
/** Send SIGHUP to a process group. */
public
static
native
void
hangupProcessGroup
(
int
processId
);
/**
* Causes the calling thread to wait for the process associated with the
* receiver to finish executing.
*
* @return The exit value of the Process being waited on
*/
public
static
native
int
waitFor
(
int
processId
);
}
src/main/java/eu/chainfire/libsuperuser/Application.java
0 → 100644
View file @
185f32aa
/*
* Copyright (C) 2012-2013 Jorrit "Chainfire" Jongma
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package
eu.chainfire.libsuperuser
;
import
android.content.Context
;
import
android.os.Handler
;
import
android.widget.Toast
;
/**
* Base application class to extend from, solving some issues with
* toasts and AsyncTasks you are likely to run into
*/
public
class
Application
extends
android
.
app
.
Application
{
/**
* Shows a toast message
*
* @param context Any context belonging to this application
* @param message The message to show
*/
public
static
void
toast
(
Context
context
,
String
message
)
{
// this is a static method so it is easier to call,
// as the context checking and casting is done for you
if
(
context
==
null
)
return
;
if
(!(
context
instanceof
Application
))
{
context
=
context
.
getApplicationContext
();
}
if
(
context
instanceof
Application
)
{
final
Context
c
=
context
;
final
String
m
=
message
;
((
Application
)
context
).
runInApplicationThread
(
new
Runnable
()
{
@Override
public
void
run
()
{
Toast
.
makeText
(
c
,
m
,
Toast
.
LENGTH_LONG
).
show
();
}
});
}
}
private
static
Handler
mApplicationHandler
=
new
Handler
();
/**
* Run a runnable in the main application thread
*
* @param r Runnable to run
*/
public
void
runInApplicationThread
(
Runnable
r
)
{
mApplicationHandler
.
post
(
r
);
}
@Override
public
void
onCreate
()
{
super
.
onCreate
();
try
{
// workaround bug in AsyncTask, can show up (for example) when you toast from a service
// this makes sure AsyncTask's internal handler is created from the right (main) thread
Class
.
forName
(
"android.os.AsyncTask"
);
}
catch
(
ClassNotFoundException
e
)
{
}
}
}
src/main/java/eu/chainfire/libsuperuser/Debug.java
0 → 100644
View file @
185f32aa
/*
* Copyright (C) 2012-2013 Jorrit "Chainfire" Jongma
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package
eu.chainfire.libsuperuser
;
import
android.os.Looper
;
import
android.util.Log
;
import
com.github.shadowsocks.BuildConfig
;
/**
* Utility class for logging and debug features that (by default) does nothing when not in debug mode
*/
public
class
Debug
{
// ----- DEBUGGING -----
private
static
boolean
debug
=
BuildConfig
.
DEBUG
;
/**
* <p>Enable or disable debug mode</p>
*
* <p>By default, debug mode is enabled for development
* builds and disabled for exported APKs - see
* BuildConfig.DEBUG</p>
*
* @param enabled Enable debug mode ?
*/
public
static
void
setDebug
(
boolean
enable
)
{
debug
=
enable
;
}
/**
* <p>Is debug mode enabled ?</p>
*
* @return Debug mode enabled
*/
public
static
boolean
getDebug
()
{
return
debug
;
}
// ----- LOGGING -----
public
interface
OnLogListener
{
public
void
onLog
(
int
type
,
String
typeIndicator
,
String
message
);
}
public
static
final
String
TAG
=
"libsuperuser"
;
public
static
final
int
LOG_GENERAL
=
0x0001
;
public
static
final
int
LOG_COMMAND
=
0x0002
;
public
static
final
int
LOG_OUTPUT
=
0x0004
;
public
static
final
int
LOG_NONE
=
0x0000
;
public
static
final
int
LOG_ALL
=
0xFFFF
;
private
static
int
logTypes
=
LOG_ALL
;
private
static
OnLogListener
logListener
=
null
;
/**
* <p>Log a message (internal)</p>
*
* <p>Current debug and enabled logtypes decide what gets logged -
* even if a custom callback is registered</p>
*
* @param type Type of message to log
* @param typeIndicator String indicator for message type
* @param message The message to log
*/
private
static
void
logCommon
(
int
type
,
String
typeIndicator
,
String
message
)
{
if
(
debug
&&
((
logTypes
&
type
)
==
type
))
{
if
(
logListener
!=
null
)
{
logListener
.
onLog
(
type
,
typeIndicator
,
message
);
}
else
{
Log
.
d
(
TAG
,
"["
+
TAG
+
"]["
+
typeIndicator
+
"]"
+
(!
message
.
startsWith
(
"["
)
&&
!
message
.
startsWith
(
" "
)
?
" "
:
""
)
+
message
);
}
}
}
/**
* <p>Log a "general" message</p>
*
* <p>These messages are infrequent and mostly occur at startup/shutdown or on error</p>
*
* @param message The message to log
*/
public
static
void
log
(
String
message
)
{
logCommon
(
LOG_GENERAL
,
"G"
,
message
);
}
/**
* <p>Log a "per-command" message</p>
*
* <p>This could produce a lot of output if the client runs many commands in the session</p>
*
* @param message The message to log
*/
public
static
void
logCommand
(
String
message
)
{
logCommon
(
LOG_COMMAND
,
"C"
,
message
);
}
/**
* <p>Log a line of stdout/stderr output</p>
*
* <p>This could produce a lot of output if the shell commands are noisy</p>
*
* @param message The message to log
*/
public
static
void
logOutput
(
String
message
)
{
logCommon
(
LOG_OUTPUT
,
"O"
,
message
);
}
/**
* <p>Enable or disable logging specific types of message</p>
*
* <p>You may | (or) LOG_* constants together. Note that
* debug mode must also be enabled for actual logging to
* occur.</p>
*
* @param type LOG_* constants
* @param enabled Enable or disable
*/
public
static
void
setLogTypeEnabled
(
int
type
,
boolean
enable
)
{
if
(
enable
)
{
logTypes
|=
type
;
}
else
{
logTypes
&=
~
type
;
}
}
/**
* <p>Is logging for specific types of messages enabled ?</p>
*
* <p>You may | (or) LOG_* constants together, to learn if
* <b>all</b> passed message types are enabled for logging. Note
* that debug mode must also be enabled for actual logging
* to occur.</p>
*
* @param type LOG_* constants
*/
public
static
boolean
getLogTypeEnabled
(
int
type
)
{
return
((
logTypes
&
type
)
==
type
);
}
/**
* <p>Is logging for specific types of messages enabled ?</p>
*
* <p>You may | (or) LOG_* constants together, to learn if
* <b>all</b> message types are enabled for logging. Takes
* debug mode into account for the result.</p>
*
* @param type LOG_* constants
*/
public
static
boolean
getLogTypeEnabledEffective
(
int
type
)
{
return
getDebug
()
&&
getLogTypeEnabled
(
type
);
}
/**
* <p>Register a custom log handler</p>
*
* <p>Replaces the log method (write to logcat) with your own
* handler. Whether your handler gets called is still dependent
* on debug mode and message types being enabled for logging.</p>
*
* @param onLogListener Custom log listener or NULL to revert to default
*/
public
static
void
setOnLogListener
(
OnLogListener
onLogListener
)
{
logListener
=
onLogListener
;
}
/**
* <p>Get the currently registered custom log handler</p>
*
* @return Current custom log handler or NULL if none is present
*/
public
static
OnLogListener
getOnLogListener
()
{
return
logListener
;
}
// ----- SANITY CHECKS -----
private
static
boolean
sanityChecks
=
true
;
/**
* <p>Enable or disable sanity checks</p>
*
* <p>Enables or disables the library crashing when su is called
* from the main thread.</p>
*
* @param enabled Enable or disable
*/
public
static
void
setSanityChecksEnabled
(
boolean
enable
)
{
sanityChecks
=
enable
;
}
/**
* <p>Are sanity checks enabled ?</p>
*
* <p>Note that debug mode must also be enabled for actual
* sanity checks to occur.</p>
*
* @return True if enabled
*/
public
static
boolean
getSanityChecksEnabled
()
{
return
sanityChecks
;
}
/**
* <p>Are sanity checks enabled ?</p>
*
* <p>Takes debug mode into account for the result.</p>
*
* @return True if enabled
*/
public
static
boolean
getSanityChecksEnabledEffective
()
{
return
getDebug
()
&&
getSanityChecksEnabled
();
}
/**
* <p>Are we running on the main thread ?</p>
*
* @return Running on main thread ?
*/
public
static
boolean
onMainThread
()
{
return
((
Looper
.
myLooper
()
!=
null
)
&&
(
Looper
.
myLooper
()
==
Looper
.
getMainLooper
()));
}
}
src/main/java/eu/chainfire/libsuperuser/Shell.java
0 → 100644
View file @
185f32aa
/*
* Copyright (C) 2012-2013 Jorrit "Chainfire" Jongma
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package
eu.chainfire.libsuperuser
;
import
java.io.DataOutputStream
;
import
java.io.File
;
import
java.io.FileInputStream
;
import
java.io.IOException
;
import
java.io.InputStream
;
import
java.util.ArrayList
;
import
java.util.Collections
;
import
java.util.HashMap
;
import
java.util.LinkedList
;
import
java.util.List
;
import
java.util.Locale
;
import
java.util.Map
;
import
java.util.UUID
;
import
java.util.concurrent.ScheduledThreadPoolExecutor
;
import
java.util.concurrent.TimeUnit
;
import
android.os.Handler
;
import
android.os.Looper
;
import
eu.chainfire.libsuperuser.StreamGobbler.OnLineListener
;
/**
* Class providing functionality to execute commands in a (root) shell
*/
public
class
Shell
{
/**
* <p>Runs commands using the supplied shell, and returns the output, or null in
* case of errors.</p>
*
* <p>This method is deprecated and only provided for backwards compatibility.
* Use {@link #run(String, String[], String[], boolean)} instead, and see that
* same method for usage notes.</p>
*
* @param shell The shell to use for executing the commands
* @param commands The commands to execute
* @param wantSTDERR Return STDERR in the output ?
* @return Output of the commands, or null in case of an error
*/
@Deprecated
public
static
List
<
String
>
run
(
String
shell
,
String
[]
commands
,
boolean
wantSTDERR
)
{
return
run
(
shell
,
commands
,
null
,
wantSTDERR
);
}
/**
* <p>Runs commands using the supplied shell, and returns the output, or null in
* case of errors.</p>
*
* <p>Note that due to compatibility with older Android versions,
* wantSTDERR is not implemented using redirectErrorStream, but rather appended
* to the output. STDOUT and STDERR are thus not guaranteed to be in the correct
* order in the output.</p>
*
* <p>Note as well that this code will intentionally crash when run in debug mode
* from the main thread of the application. You should always execute shell
* commands from a background thread.</p>
*
* <p>When in debug mode, the code will also excessively log the commands passed to
* and the output returned from the shell.</p>
*
* <p>Though this function uses background threads to gobble STDOUT and STDERR so
* a deadlock does not occur if the shell produces massive output, the output is
* still stored in a List<String>, and as such doing something like <em>'ls -lR /'</em>
* will probably have you run out of memory.</p>
*
* @param shell The shell to use for executing the commands
* @param commands The commands to execute
* @param environment List of all environment variables (in 'key=value' format) or null for defaults
* @param wantSTDERR Return STDERR in the output ?
* @return Output of the commands, or null in case of an error
*/
public
static
List
<
String
>
run
(
String
shell
,
String
[]
commands
,
String
[]
environment
,
boolean
wantSTDERR
)
{
String
shellUpper
=
shell
.
toUpperCase
(
Locale
.
ENGLISH
);
if
(
Debug
.
getSanityChecksEnabledEffective
()
&&
Debug
.
onMainThread
())
{
// check if we're running in the main thread, and if so, crash if we're in debug mode,
// to let the developer know attention is needed here.
Debug
.
log
(
ShellOnMainThreadException
.
EXCEPTION_COMMAND
);
throw
new
ShellOnMainThreadException
(
ShellOnMainThreadException
.
EXCEPTION_COMMAND
);
}
Debug
.
logCommand
(
String
.
format
(
"[%s%%] START"
,
shellUpper
));
List
<
String
>
res
=
Collections
.
synchronizedList
(
new
ArrayList
<
String
>());
try
{
// Combine passed environment with system environment
if
(
environment
!=
null
)
{
Map
<
String
,
String
>
newEnvironment
=
new
HashMap
<
String
,
String
>();
newEnvironment
.
putAll
(
System
.
getenv
());
int
split
;
for
(
String
entry
:
environment
)
{
if
((
split
=
entry
.
indexOf
(
"="
))
>=
0
)
{
newEnvironment
.
put
(
entry
.
substring
(
0
,
split
),
entry
.
substring
(
split
+
1
));
}
}
int
i
=
0
;
environment
=
new
String
[
newEnvironment
.
size
()];
for
(
Map
.
Entry
<
String
,
String
>
entry
:
newEnvironment
.
entrySet
())
{
environment
[
i
]
=
entry
.
getKey
()
+
"="
+
entry
.
getValue
();
i
++;
}
}
// setup our process, retrieve STDIN stream, and STDOUT/STDERR gobblers
Process
process
=
Runtime
.
getRuntime
().
exec
(
shell
,
environment
);
DataOutputStream
STDIN
=
new
DataOutputStream
(
process
.
getOutputStream
());
StreamGobbler
STDOUT
=
new
StreamGobbler
(
shellUpper
+
"-"
,
process
.
getInputStream
(),
res
);
StreamGobbler
STDERR
=
new
StreamGobbler
(
shellUpper
+
"*"
,
process
.
getErrorStream
(),
wantSTDERR
?
res
:
null
);
// start gobbling and write our commands to the shell
STDOUT
.
start
();
STDERR
.
start
();
for
(
String
write
:
commands
)
{
Debug
.
logCommand
(
String
.
format
(
"[%s+] %s"
,
shellUpper
,
write
));
STDIN
.
write
((
write
+
"\n"
).
getBytes
(
"UTF-8"
));
STDIN
.
flush
();
}
STDIN
.
write
(
"exit\n"
.
getBytes
(
"UTF-8"
));
STDIN
.
flush
();
// wait for our process to finish, while we gobble away in the background
process
.
waitFor
();
// make sure our threads are done gobbling, our streams are closed, and the process is
// destroyed - while the latter two shouldn't be needed in theory, and may even produce
// warnings, in "normal" Java they are required for guaranteed cleanup of resources, so
// lets be safe and do this on Android as well
try
{
STDIN
.
close
();
}
catch
(
IOException
e
)
{
}
STDOUT
.
join
();
STDERR
.
join
();
process
.
destroy
();
// in case of su, 255 usually indicates access denied
if
(
SU
.
isSU
(
shell
)
&&
(
process
.
exitValue
()
==
255
))
{
res
=
null
;
}
}
catch
(
IOException
e
)
{
// shell probably not found
res
=
null
;
}
catch
(
InterruptedException
e
)
{
// this should really be re-thrown
res
=
null
;
}
Debug
.
logCommand
(
String
.
format
(
"[%s%%] END"
,
shell
.
toUpperCase
(
Locale
.
ENGLISH
)));
return
res
;
}
protected
static
String
[]
availableTestCommands
=
new
String
[]
{
"echo -BOC-"
,
"id"
};
/**
* See if the shell is alive, and if so, check the UID
*
* @param ret Standard output from running availableTestCommands
* @param checkForRoot true if we are expecting this shell to be running as root
* @return true on success, false on error
*/
protected
static
boolean
parseAvailableResult
(
List
<
String
>
ret
,
boolean
checkForRoot
)
{
if
(
ret
==
null
)
return
false
;
// this is only one of many ways this can be done
boolean
echo_seen
=
false
;
for
(
String
line
:
ret
)
{
if
(
line
.
contains
(
"uid="
))
{
// id command is working, let's see if we are actually root
return
!
checkForRoot
||
line
.
contains
(
"uid=0"
);
}
else
if
(
line
.
contains
(
"-BOC-"
))
{
// if we end up here, at least the su command starts some kind of shell,
// let's hope it has root privileges - no way to know without additional
// native binaries
echo_seen
=
true
;
}
}
return
echo_seen
;
}
/**
* This class provides utility functions to easily execute commands using SH
*/
public
static
class
SH
{
/**
* Runs command and return output
*
* @param command The command to run
* @return Output of the command, or null in case of an error
*/
public
static
List
<
String
>
run
(
String
command
)
{
return
Shell
.
run
(
"sh"
,
new
String
[]
{
command
},
null
,
false
);
}
/**
* Runs commands and return output
*
* @param commands The commands to run
* @return Output of the commands, or null in case of an error
*/
public
static
List
<
String
>
run
(
List
<
String
>
commands
)
{
return
Shell
.
run
(
"sh"
,
commands
.
toArray
(
new
String
[
commands
.
size
()]),
null
,
false
);
}
/**
* Runs commands and return output
*
* @param commands The commands to run
* @return Output of the commands, or null in case of an error
*/
public
static
List
<
String
>
run
(
String
[]
commands
)
{
return
Shell
.
run
(
"sh"
,
commands
,
null
,
false
);
}
}
/**
* This class provides utility functions to easily execute commands using SU
* (root shell), as well as detecting whether or not root is available, and
* if so which version.
*/
public
static
class
SU
{
/**
* Runs command as root (if available) and return output
*
* @param command The command to run
* @return Output of the command, or null if root isn't available or in case of an error
*/
public
static
List
<
String
>
run
(
String
command
)
{
return
Shell
.
run
(
"su"
,
new
String
[]
{
command
},
null
,
false
);
}
/**
* Runs commands as root (if available) and return output
*
* @param commands The commands to run
* @return Output of the commands, or null if root isn't available or in case of an error
*/
public
static
List
<
String
>
run
(
List
<
String
>
commands
)
{
return
Shell
.
run
(
"su"
,
commands
.
toArray
(
new
String
[
commands
.
size
()]),
null
,
false
);
}
/**
* Runs commands as root (if available) and return output
*
* @param commands The commands to run
* @return Output of the commands, or null if root isn't available or in case of an error
*/
public
static
List
<
String
>
run
(
String
[]
commands
)
{
return
Shell
.
run
(
"su"
,
commands
,
null
,
false
);
}
/**
* Detects whether or not superuser access is available, by checking the output
* of the "id" command if available, checking if a shell runs at all otherwise
*
* @return True if superuser access available
*/
public
static
boolean
available
()
{
// this is only one of many ways this can be done
List
<
String
>
ret
=
run
(
Shell
.
availableTestCommands
);
return
Shell
.
parseAvailableResult
(
ret
,
true
);
}
/**
* <p>Detects the version of the su binary installed (if any), if supported by the binary.
* Most binaries support two different version numbers, the public version that is
* displayed to users, and an internal version number that is used for version number
* comparisons. Returns null if su not available or retrieving the version isn't supported.</p>
*
* <p>Note that su binary version and GUI (APK) version can be completely different.</p>
*
* @param internal Request human-readable version or application internal version
* @return String containing the su version or null
*/
public
static
String
version
(
boolean
internal
)
{
List
<
String
>
ret
=
Shell
.
run
(
internal
?
"su -V"
:
"su -v"
,
new
String
[]
{
},
null
,
false
);
if
(
ret
==
null
)
return
null
;
for
(
String
line
:
ret
)
{
if
(!
internal
)
{
if
(
line
.
contains
(
"."
))
return
line
;
}
else
{
try
{
if
(
Integer
.
parseInt
(
line
)
>
0
)
return
line
;
}
catch
(
NumberFormatException
e
)
{
}
}
}
return
null
;
}
/**
* Attempts to deduce if the shell command refers to a su shell
*
* @param shell Shell command to run
* @return Shell command appears to be su
*/
public
static
boolean
isSU
(
String
shell
)
{
// Strip parameters
int
pos
=
shell
.
indexOf
(
' '
);
if
(
pos
>=
0
)
{
shell
=
shell
.
substring
(
0
,
pos
);
}
// Strip path
pos
=
shell
.
lastIndexOf
(
'/'
);
if
(
pos
>=
0
)
{
shell
=
shell
.
substring
(
pos
+
1
);
}
return
shell
.
equals
(
"su"
);
}
/**
* Constructs a shell command to start a su shell using the supplied
* uid and SELinux context. This is can be an expensive operation,
* consider caching the result.
*
* @param uid Uid to use (0 == root)
* @param context (SELinux) context name to use or null
* @return Shell command
*/
public
static
String
shell
(
int
uid
,
String
context
)
{
// su[ --context <context>][ <uid>]
String
shell
=
"su"
;
// First known firmware with SELinux built-in was a 4.2 (17) leak
if
((
context
!=
null
)
&&
(
android
.
os
.
Build
.
VERSION
.
SDK_INT
>=
17
))
{
Boolean
enforcing
=
null
;
// Detect enforcing through sysfs, not always present
if
(
enforcing
==
null
)
{
File
f
=
new
File
(
"/sys/fs/selinux/enforce"
);
if
(
f
.
exists
())
{
try
{
InputStream
is
=
new
FileInputStream
(
"/sys/fs/selinux/enforce"
);
try
{
enforcing
=
(
is
.
read
()
==
'1'
);
}
finally
{
is
.
close
();
}
}
catch
(
Exception
e
)
{
}
}
}
// 4.4+ builds are enforcing by default, take the gamble
if
(
enforcing
==
null
)
{
enforcing
=
(
android
.
os
.
Build
.
VERSION
.
SDK_INT
>=
19
);
}
// Switching to a context in permissive mode is not generally
// useful aside from audit testing, but we want to avoid
// switching the context due to the increased chance of issues.
if
(
enforcing
)
{
String
display
=
version
(
false
);
String
internal
=
version
(
true
);
// We only know the format for SuperSU v1.90+ right now
if
(
(
display
!=
null
)
&&
(
internal
!=
null
)
&&
(
display
.
endsWith
(
"SUPERSU"
))
&&
(
Integer
.
valueOf
(
internal
)
>=
190
)
)
{
shell
=
String
.
format
(
Locale
.
ENGLISH
,
"%s --context %s"
,
shell
,
context
);
}
}
}
// Most su binaries support the "su <uid>" format, but in case
// they don't, lets skip it for the default 0 (root) case
if
(
uid
>
0
)
{
shell
=
String
.
format
(
Locale
.
ENGLISH
,
"%s %d"
,
shell
,
uid
);
}
return
shell
;
}
}
/**
* Command result callback, notifies the recipient of the completion of a command
* block, including the (last) exit code, and the full output
*/
public
interface
OnCommandResultListener
{
/**
* <p>Command result callback</p>
*
* <p>Depending on how and on which thread the shell was created, this callback
* may be executed on one of the gobbler threads. In that case, it is important
* the callback returns as quickly as possible, as delays in this callback may
* pause the native process or even result in a deadlock</p>
*
* <p>See {@link Shell.Interactive} for threading details</p>
*
* @param commandCode Value previously supplied to addCommand
* @param exitCode Exit code of the last command in the block
* @param output All output generated by the command block
*/
public
void
onCommandResult
(
int
commandCode
,
int
exitCode
,
List
<
String
>
output
);
// for any onCommandResult callback
public
static
final
int
WATCHDOG_EXIT
=
-
1
;
public
static
final
int
SHELL_DIED
=
-
2
;
// for Interactive.open() callbacks only
public
static
final
int
SHELL_EXEC_FAILED
=
-
3
;
public
static
final
int
SHELL_WRONG_UID
=
-
4
;
public
static
final
int
SHELL_RUNNING
=
0
;
}
/**
* Internal class to store command block properties
*/
private
static
class
Command
{
private
static
int
commandCounter
=
0
;
private
final
String
[]
commands
;
private
final
int
code
;
private
final
OnCommandResultListener
onCommandResultListener
;
private
final
String
marker
;
public
Command
(
String
[]
commands
,
int
code
,
OnCommandResultListener
onCommandResultListener
)
{
this
.
commands
=
commands
;
this
.
code
=
code
;
this
.
onCommandResultListener
=
onCommandResultListener
;
this
.
marker
=
UUID
.
randomUUID
().
toString
()
+
String
.
format
(
"-%08x"
,
++
commandCounter
);
}
}
/**
* Builder class for {@link Shell.Interactive}
*/
public
static
class
Builder
{
private
Handler
handler
=
null
;
private
boolean
autoHandler
=
true
;
private
String
shell
=
"sh"
;
private
boolean
wantSTDERR
=
false
;
private
List
<
Command
>
commands
=
new
LinkedList
<
Command
>();
private
Map
<
String
,
String
>
environment
=
new
HashMap
<
String
,
String
>();
private
OnLineListener
onSTDOUTLineListener
=
null
;
private
OnLineListener
onSTDERRLineListener
=
null
;
private
int
watchdogTimeout
=
0
;
/**
* <p>Set a custom handler that will be used to post all callbacks to</p>
*
* <p>See {@link Shell.Interactive} for further details on threading and handlers</p>
*
* @param handler Handler to use
* @return This Builder object for method chaining
*/
public
Builder
setHandler
(
Handler
handler
)
{
this
.
handler
=
handler
;
return
this
;
}
/**
* <p>Automatically create a handler if possible ? Default to true</p>
*
* <p>See {@link Shell.Interactive} for further details on threading and handlers</p>
*
* @param autoHandler Auto-create handler ?
* @return This Builder object for method chaining
*/
public
Builder
setAutoHandler
(
boolean
autoHandler
)
{
this
.
autoHandler
=
autoHandler
;
return
this
;
}
/**
* Set shell binary to use. Usually "sh" or "su", do not use a full path
* unless you have a good reason to
*
* @param shell Shell to use
* @return This Builder object for method chaining
*/
public
Builder
setShell
(
String
shell
)
{
this
.
shell
=
shell
;
return
this
;
}
/**
* Convenience function to set "sh" as used shell
*
* @return This Builder object for method chaining
*/
public
Builder
useSH
()
{
return
setShell
(
"sh"
);
}
/**
* Convenience function to set "su" as used shell
*
* @return This Builder object for method chaining
*/
public
Builder
useSU
()
{
return
setShell
(
"su"
);
}
/**
* Set if error output should be appended to command block result output
*
* @param wantSTDERR Want error output ?
* @return This Builder object for method chaining
*/
public
Builder
setWantSTDERR
(
boolean
wantSTDERR
)
{
this
.
wantSTDERR
=
wantSTDERR
;
return
this
;
}
/**
* Add or update an environment variable
*
* @param key Key of the environment variable
* @param value Value of the environment variable
* @return This Builder object for method chaining
*/
public
Builder
addEnvironment
(
String
key
,
String
value
)
{
environment
.
put
(
key
,
value
);
return
this
;
}
/**
* Add or update environment variables
*
* @param addEnvironment Map of environment variables
* @return This Builder object for method chaining
*/
public
Builder
addEnvironment
(
Map
<
String
,
String
>
addEnvironment
)
{
environment
.
putAll
(
addEnvironment
);
return
this
;
}
/**
* Add a command to execute
*
* @param command Command to execute
* @return This Builder object for method chaining
*/
public
Builder
addCommand
(
String
command
)
{
return
addCommand
(
command
,
0
,
null
);
}
/**
* <p>Add a command to execute, with a callback to be called on completion</p>
*
* <p>The thread on which the callback executes is dependent on various factors, see {@link Shell.Interactive} for further details</p>
*
* @param command Command to execute
* @param code User-defined value passed back to the callback
* @param onCommandResultListener Callback to be called on completion
* @return This Builder object for method chaining
*/
public
Builder
addCommand
(
String
command
,
int
code
,
OnCommandResultListener
onCommandResultListener
)
{
return
addCommand
(
new
String
[]
{
command
},
code
,
onCommandResultListener
);
}
/**
* Add commands to execute
*
* @param commands Commands to execute
* @return This Builder object for method chaining
*/
public
Builder
addCommand
(
List
<
String
>
commands
)
{
return
addCommand
(
commands
,
0
,
null
);
}
/**
* <p>Add commands to execute, with a callback to be called on completion (of all commands)</p>
*
* <p>The thread on which the callback executes is dependent on various factors, see {@link Shell.Interactive} for further details</p>
*
* @param commands Commands to execute
* @param code User-defined value passed back to the callback
* @param onCommandResultListener Callback to be called on completion (of all commands)
* @return This Builder object for method chaining
*/
public
Builder
addCommand
(
List
<
String
>
commands
,
int
code
,
OnCommandResultListener
onCommandResultListener
)
{
return
addCommand
(
commands
.
toArray
(
new
String
[
commands
.
size
()]),
code
,
onCommandResultListener
);
}
/**
* Add commands to execute
*
* @param commands Commands to execute
* @return This Builder object for method chaining
*/
public
Builder
addCommand
(
String
[]
commands
)
{
return
addCommand
(
commands
,
0
,
null
);
}
/**
* <p>Add commands to execute, with a callback to be called on completion (of all commands)</p>
*
* <p>The thread on which the callback executes is dependent on various factors, see {@link Shell.Interactive} for further details</p>
*
* @param commands Commands to execute
* @param code User-defined value passed back to the callback
* @param onCommandResultListener Callback to be called on completion (of all commands)
* @return This Builder object for method chaining
*/
public
Builder
addCommand
(
String
[]
commands
,
int
code
,
OnCommandResultListener
onCommandResultListener
)
{
this
.
commands
.
add
(
new
Command
(
commands
,
code
,
onCommandResultListener
));
return
this
;
}
/**
* <p>Set a callback called for every line output to STDOUT by the shell</p>
*
* <p>The thread on which the callback executes is dependent on various factors, see {@link Shell.Interactive} for further details</p>
*
* @param onLineListener Callback to be called for each line
* @return This Builder object for method chaining
*/
public
Builder
setOnSTDOUTLineListener
(
OnLineListener
onLineListener
)
{
this
.
onSTDOUTLineListener
=
onLineListener
;
return
this
;
}
/**
* <p>Set a callback called for every line output to STDERR by the shell</p>
*
* <p>The thread on which the callback executes is dependent on various factors, see {@link Shell.Interactive} for further details</p>
*
* @param onLineListener Callback to be called for each line
* @return This Builder object for method chaining
*/
public
Builder
setOnSTDERRLineListener
(
OnLineListener
onLineListener
)
{
this
.
onSTDERRLineListener
=
onLineListener
;
return
this
;
}
/**
* <p>Enable command timeout callback</p>
*
* <p>This will invoke the onCommandResult() callback with exitCode WATCHDOG_EXIT if a command takes longer than watchdogTimeout
* seconds to complete.</p>
*
* <p>If a watchdog timeout occurs, it generally means that the Interactive session is out of sync with the shell process. The
* caller should close the current session and open a new one.</p>
*
* @param watchdogTimeout Timeout, in seconds; 0 to disable
* @return This Builder object for method chaining
*/
public
Builder
setWatchdogTimeout
(
int
watchdogTimeout
)
{
this
.
watchdogTimeout
=
watchdogTimeout
;
return
this
;
}
/**
* <p>Enable/disable reduced logcat output</p>
*
* <p>Note that this is a global setting</p>
*
* @param useMinimal true for reduced output, false for full output
* @return This Builder object for method chaining
*/
public
Builder
setMinimalLogging
(
boolean
useMinimal
)
{
Debug
.
setLogTypeEnabled
(
Debug
.
LOG_COMMAND
|
Debug
.
LOG_OUTPUT
,
!
useMinimal
);
return
this
;
}
/**
* Construct a {@link Shell.Interactive} instance, and start the shell
*/
public
Interactive
open
()
{
return
new
Interactive
(
this
,
null
);
}
/**
* Construct a {@link Shell.Interactive} instance, try to start the shell, and
* call onCommandResultListener to report success or failure
*
* @param onCommandResultListener Callback to return shell open status
*/
public
Interactive
open
(
OnCommandResultListener
onCommandResultListener
)
{
return
new
Interactive
(
this
,
onCommandResultListener
);
}
}
/**
* <p>An interactive shell - initially created with {@link Shell.Builder} - that
* executes blocks of commands you supply in the background, optionally calling
* callbacks as each block completes.</p>
*
* <p>STDERR output can be supplied as well, but due to compatibility with older
* Android versions, wantSTDERR is not implemented using redirectErrorStream,
* but rather appended to the output. STDOUT and STDERR are thus not guaranteed to
* be in the correct order in the output.</p>
*
* <p>Note as well that the close() and waitForIdle() methods will intentionally
* crash when run in debug mode from the main thread of the application. Any blocking
* call should be run from a background thread.</p>
*
* <p>When in debug mode, the code will also excessively log the commands passed to
* and the output returned from the shell.</p>
*
* <p>Though this function uses background threads to gobble STDOUT and STDERR so
* a deadlock does not occur if the shell produces massive output, the output is
* still stored in a List<String>, and as such doing something like <em>'ls -lR /'</em>
* will probably have you run out of memory when using a
* {@link Shell.OnCommandResultListener}. A work-around is to not supply this callback,
* but using (only) {@link Shell.Builder#setOnSTDOUTLineListener(OnLineListener)}. This
* way, an internal buffer will not be created and wasting your memory.</p>
*
* <h3>Callbacks, threads and handlers</h3>
*
* <p>On which thread the callbacks execute is dependent on your initialization. You can
* supply a custom Handler using {@link Shell.Builder#setHandler(Handler)} if needed.
* If you do not supply a custom Handler - unless you set {@link Shell.Builder#setAutoHandler(boolean)}
* to false - a Handler will be auto-created if the thread used for instantiation
* of the object has a Looper.</p>
*
* <p>If no Handler was supplied and it was also not auto-created, all callbacks will
* be called from either the STDOUT or STDERR gobbler threads. These are important
* threads that should be blocked as little as possible, as blocking them may in rare
* cases pause the native process or even create a deadlock.</p>
*
* <p>The main thread must certainly have a Looper, thus if you call {@link Shell.Builder#open()}
* from the main thread, a handler will (by default) be auto-created, and all the callbacks
* will be called on the main thread. While this is often convenient and easy to code with,
* you should be aware that if your callbacks are 'expensive' to execute, this may negatively
* impact UI performance.</p>
*
* <p>Background threads usually do <em>not</em> have a Looper, so calling {@link Shell.Builder#open()}
* from such a background thread will (by default) result in all the callbacks being executed
* in one of the gobbler threads. You will have to make sure the code you execute in these callbacks
* is thread-safe.</p>
*/
public
static
class
Interactive
{
private
final
Handler
handler
;
private
final
boolean
autoHandler
;
private
final
String
shell
;
private
final
boolean
wantSTDERR
;
private
final
List
<
Command
>
commands
;
private
final
Map
<
String
,
String
>
environment
;
private
final
OnLineListener
onSTDOUTLineListener
;
private
final
OnLineListener
onSTDERRLineListener
;
private
int
watchdogTimeout
;
private
Process
process
=
null
;
private
DataOutputStream
STDIN
=
null
;
private
StreamGobbler
STDOUT
=
null
;
private
StreamGobbler
STDERR
=
null
;
private
ScheduledThreadPoolExecutor
watchdog
=
null
;
private
volatile
boolean
running
=
false
;
private
volatile
boolean
idle
=
true
;
// read/write only synchronized
private
volatile
boolean
closed
=
true
;
private
volatile
int
callbacks
=
0
;
private
volatile
int
watchdogCount
;
private
Object
idleSync
=
new
Object
();
private
Object
callbackSync
=
new
Object
();
private
volatile
int
lastExitCode
=
0
;
private
volatile
String
lastMarkerSTDOUT
=
null
;
private
volatile
String
lastMarkerSTDERR
=
null
;
private
volatile
Command
command
=
null
;
private
volatile
List
<
String
>
buffer
=
null
;
/**
* The only way to create an instance: Shell.Builder::open()
*
* @param builder Builder class to take values from
*/
private
Interactive
(
final
Builder
builder
,
final
OnCommandResultListener
onCommandResultListener
)
{
autoHandler
=
builder
.
autoHandler
;
shell
=
builder
.
shell
;
wantSTDERR
=
builder
.
wantSTDERR
;
commands
=
builder
.
commands
;
environment
=
builder
.
environment
;
onSTDOUTLineListener
=
builder
.
onSTDOUTLineListener
;
onSTDERRLineListener
=
builder
.
onSTDERRLineListener
;
watchdogTimeout
=
builder
.
watchdogTimeout
;
// If a looper is available, we offload the callbacks from the gobbling threads
// to whichever thread created us. Would normally do this in open(),
// but then we could not declare handler as final
if
((
Looper
.
myLooper
()
!=
null
)
&&
(
builder
.
handler
==
null
)
&&
autoHandler
)
{
handler
=
new
Handler
();
}
else
{
handler
=
builder
.
handler
;
}
boolean
ret
=
open
();
if
(
onCommandResultListener
==
null
)
{
return
;
}
else
if
(
ret
==
false
)
{
onCommandResultListener
.
onCommandResult
(
0
,
OnCommandResultListener
.
SHELL_EXEC_FAILED
,
null
);
return
;
}
// Allow up to 60 seconds for SuperSU/Superuser dialog, then enable the user-specified
// timeout for all subsequent operations
watchdogTimeout
=
60
;
addCommand
(
Shell
.
availableTestCommands
,
0
,
new
OnCommandResultListener
()
{
public
void
onCommandResult
(
int
commandCode
,
int
exitCode
,
List
<
String
>
output
)
{
if
(
exitCode
==
OnCommandResultListener
.
SHELL_RUNNING
&&
Shell
.
parseAvailableResult
(
output
,
Shell
.
SU
.
isSU
(
shell
))
!=
true
)
{
// shell is up, but it's brain-damaged
exitCode
=
OnCommandResultListener
.
SHELL_WRONG_UID
;
}
watchdogTimeout
=
builder
.
watchdogTimeout
;
onCommandResultListener
.
onCommandResult
(
0
,
exitCode
,
output
);
}
});
}
@Override
protected
void
finalize
()
throws
Throwable
{
if
(!
closed
&&
Debug
.
getSanityChecksEnabledEffective
())
{
// waste of resources
Debug
.
log
(
ShellNotClosedException
.
EXCEPTION_NOT_CLOSED
);
throw
new
ShellNotClosedException
();
}
super
.
finalize
();
}
/**
* Add a command to execute
*
* @param command Command to execute
*/
public
void
addCommand
(
String
command
)
{
addCommand
(
command
,
0
,
null
);
}
/**
* <p>Add a command to execute, with a callback to be called on completion</p>
*
* <p>The thread on which the callback executes is dependent on various factors, see {@link Shell.Interactive} for further details</p>
*
* @param command Command to execute
* @param code User-defined value passed back to the callback
* @param onCommandResultListener Callback to be called on completion
*/
public
void
addCommand
(
String
command
,
int
code
,
OnCommandResultListener
onCommandResultListener
)
{
addCommand
(
new
String
[]
{
command
},
code
,
onCommandResultListener
);
}
/**
* Add commands to execute
*
* @param commands Commands to execute
*/
public
void
addCommand
(
List
<
String
>
commands
)
{
addCommand
(
commands
,
0
,
null
);
}
/**
* <p>Add commands to execute, with a callback to be called on completion (of all commands)</p>
*
* <p>The thread on which the callback executes is dependent on various factors, see {@link Shell.Interactive} for further details</p>
*
* @param commands Commands to execute
* @param code User-defined value passed back to the callback
* @param onCommandResultListener Callback to be called on completion (of all commands)
*/
public
void
addCommand
(
List
<
String
>
commands
,
int
code
,
OnCommandResultListener
onCommandResultListener
)
{
addCommand
(
commands
.
toArray
(
new
String
[
commands
.
size
()]),
code
,
onCommandResultListener
);
}
/**
* Add commands to execute
*
* @param commands Commands to execute
*/
public
void
addCommand
(
String
[]
commands
)
{
addCommand
(
commands
,
0
,
null
);
}
/**
* <p>Add commands to execute, with a callback to be called on completion (of all commands)</p>
*
* <p>The thread on which the callback executes is dependent on various factors, see {@link Shell.Interactive} for further details</p>
*
* @param commands Commands to execute
* @param code User-defined value passed back to the callback
* @param onCommandResultListener Callback to be called on completion (of all commands)
*/
public
synchronized
void
addCommand
(
String
[]
commands
,
int
code
,
OnCommandResultListener
onCommandResultListener
)
{
this
.
commands
.
add
(
new
Command
(
commands
,
code
,
onCommandResultListener
));
runNextCommand
();
}
/**
* Run the next command if any and if ready, signals idle state if no commands left
*/
private
void
runNextCommand
()
{
runNextCommand
(
true
);
}
/**
* Called from a ScheduledThreadPoolExecutor timer thread every second when there is an outstanding command
*/
private
synchronized
void
handleWatchdog
()
{
final
int
exitCode
;
if
(
watchdog
==
null
)
return
;
if
(
watchdogTimeout
==
0
)
return
;
if
(!
isRunning
())
{
exitCode
=
OnCommandResultListener
.
SHELL_DIED
;
Debug
.
log
(
String
.
format
(
"[%s%%] SHELL_DIED"
,
shell
.
toUpperCase
(
Locale
.
ENGLISH
)));
}
else
if
(
watchdogCount
++
<
watchdogTimeout
)
{
return
;
}
else
{
exitCode
=
OnCommandResultListener
.
WATCHDOG_EXIT
;
Debug
.
log
(
String
.
format
(
"[%s%%] WATCHDOG_EXIT"
,
shell
.
toUpperCase
(
Locale
.
ENGLISH
)));
}
if
(
handler
!=
null
)
{
postCallback
(
command
,
exitCode
,
buffer
);
}
// prevent multiple callbacks for the same command
command
=
null
;
buffer
=
null
;
idle
=
true
;
watchdog
.
shutdown
();
watchdog
=
null
;
kill
();
}
/**
* Start the periodic timer when a command is submitted
*/
private
void
startWatchdog
()
{
if
(
watchdogTimeout
==
0
)
{
return
;
}
watchdogCount
=
0
;
watchdog
=
new
ScheduledThreadPoolExecutor
(
1
);
watchdog
.
scheduleAtFixedRate
(
new
Runnable
()
{
@Override
public
void
run
()
{
handleWatchdog
();
}
},
1
,
1
,
TimeUnit
.
SECONDS
);
}
/**
* Disable the watchdog timer upon command completion
*/
private
void
stopWatchdog
()
{
if
(
watchdog
!=
null
)
{
watchdog
.
shutdownNow
();
watchdog
=
null
;
}
}
/**
* Run the next command if any and if ready
*
* @param notifyIdle signals idle state if no commands left ?
*/
private
void
runNextCommand
(
boolean
notifyIdle
)
{
// must always be called from a synchronized method
boolean
running
=
isRunning
();
if
(!
running
)
idle
=
true
;
if
(
running
&&
idle
&&
(
commands
.
size
()
>
0
))
{
Command
command
=
commands
.
get
(
0
);
commands
.
remove
(
0
);
buffer
=
null
;
lastExitCode
=
0
;
lastMarkerSTDOUT
=
null
;
lastMarkerSTDERR
=
null
;
if
(
command
.
commands
.
length
>
0
)
{
try
{
if
(
command
.
onCommandResultListener
!=
null
)
{
// no reason to store the output if we don't have an OnCommandResultListener
// user should catch the output with an OnLineListener in this case
buffer
=
Collections
.
synchronizedList
(
new
ArrayList
<
String
>());
}
idle
=
false
;
this
.
command
=
command
;
startWatchdog
();
for
(
String
write
:
command
.
commands
)
{
Debug
.
logCommand
(
String
.
format
(
"[%s+] %s"
,
shell
.
toUpperCase
(
Locale
.
ENGLISH
),
write
));
STDIN
.
write
((
write
+
"\n"
).
getBytes
(
"UTF-8"
));
}
STDIN
.
write
((
"echo "
+
command
.
marker
+
" $?\n"
).
getBytes
(
"UTF-8"
));
STDIN
.
write
((
"echo "
+
command
.
marker
+
" >&2\n"
).
getBytes
(
"UTF-8"
));
STDIN
.
flush
();
}
catch
(
IOException
e
)
{
}
}
else
{
runNextCommand
(
false
);
}
}
else
if
(!
running
)
{
// our shell died for unknown reasons - abort all submissions
while
(
commands
.
size
()
>
0
)
{
postCallback
(
commands
.
remove
(
0
),
OnCommandResultListener
.
SHELL_DIED
,
null
);
}
}
if
(
idle
&&
notifyIdle
)
{
synchronized
(
idleSync
)
{
idleSync
.
notifyAll
();
}
}
}
/**
* Processes a STDOUT/STDERR line containing an end/exitCode marker
*/
private
synchronized
void
processMarker
()
{
if
(
command
.
marker
.
equals
(
lastMarkerSTDOUT
)
&&
(
command
.
marker
.
equals
(
lastMarkerSTDERR
)))
{
if
(
buffer
!=
null
)
{
postCallback
(
command
,
lastExitCode
,
buffer
);
}
stopWatchdog
();
command
=
null
;
buffer
=
null
;
idle
=
true
;
runNextCommand
();
}
}
/**
* Process a normal STDOUT/STDERR line
*
* @param line Line to process
* @param listener Callback to call or null
*/
private
synchronized
void
processLine
(
String
line
,
OnLineListener
listener
)
{
if
(
listener
!=
null
)
{
if
(
handler
!=
null
)
{
final
String
fLine
=
line
;
final
OnLineListener
fListener
=
listener
;
startCallback
();
handler
.
post
(
new
Runnable
()
{
@Override
public
void
run
()
{
try
{
fListener
.
onLine
(
fLine
);
}
finally
{
endCallback
();
}
}
});
}
else
{
listener
.
onLine
(
line
);
}
}
}
/**
* Add line to internal buffer
*
* @param line Line to add
*/
private
synchronized
void
addBuffer
(
String
line
)
{
if
(
buffer
!=
null
)
{
buffer
.
add
(
line
);
}
}
/**
* Increase callback counter
*/
private
void
startCallback
()
{
synchronized
(
callbackSync
)
{
callbacks
++;
}
}
/**
* Schedule a callback to run on the appropriate thread
*/
private
void
postCallback
(
final
Command
fCommand
,
final
int
fExitCode
,
final
List
<
String
>
fOutput
)
{
if
(
fCommand
.
onCommandResultListener
==
null
)
{
return
;
}
if
(
handler
==
null
)
{
fCommand
.
onCommandResultListener
.
onCommandResult
(
fCommand
.
code
,
fExitCode
,
fOutput
);
return
;
}
startCallback
();
handler
.
post
(
new
Runnable
()
{
@Override
public
void
run
()
{
try
{
fCommand
.
onCommandResultListener
.
onCommandResult
(
fCommand
.
code
,
fExitCode
,
fOutput
);
}
finally
{
endCallback
();
}
}
});
}
/**
* Decrease callback counter, signals callback complete state when dropped to 0
*/
private
void
endCallback
()
{
synchronized
(
callbackSync
)
{
callbacks
--;
if
(
callbacks
==
0
)
{
callbackSync
.
notifyAll
();
}
}
}
/**
* Internal call that launches the shell, starts gobbling, and starts executing commands.
* See {@link Shell.Interactive}
*
* @return Opened successfully ?
*/
private
synchronized
boolean
open
()
{
Debug
.
log
(
String
.
format
(
"[%s%%] START"
,
shell
.
toUpperCase
(
Locale
.
ENGLISH
)));
try
{
// setup our process, retrieve STDIN stream, and STDOUT/STDERR gobblers
if
(
environment
.
size
()
==
0
)
{
process
=
Runtime
.
getRuntime
().
exec
(
shell
);
}
else
{
Map
<
String
,
String
>
newEnvironment
=
new
HashMap
<
String
,
String
>();
newEnvironment
.
putAll
(
System
.
getenv
());
newEnvironment
.
putAll
(
environment
);
int
i
=
0
;
String
[]
env
=
new
String
[
newEnvironment
.
size
()];
for
(
Map
.
Entry
<
String
,
String
>
entry
:
newEnvironment
.
entrySet
())
{
env
[
i
]
=
entry
.
getKey
()
+
"="
+
entry
.
getValue
();
i
++;
}
process
=
Runtime
.
getRuntime
().
exec
(
shell
,
env
);
}
STDIN
=
new
DataOutputStream
(
process
.
getOutputStream
());
STDOUT
=
new
StreamGobbler
(
shell
.
toUpperCase
(
Locale
.
ENGLISH
)
+
"-"
,
process
.
getInputStream
(),
new
OnLineListener
()
{
@Override
public
void
onLine
(
String
line
)
{
synchronized
(
Interactive
.
this
)
{
if
(
command
==
null
)
{
return
;
}
if
(
line
.
startsWith
(
command
.
marker
))
{
try
{
lastExitCode
=
Integer
.
valueOf
(
line
.
substring
(
command
.
marker
.
length
()
+
1
),
10
);
}
catch
(
Exception
e
)
{
}
lastMarkerSTDOUT
=
command
.
marker
;
processMarker
();
}
else
{
addBuffer
(
line
);
processLine
(
line
,
onSTDOUTLineListener
);
}
}
}
});
STDERR
=
new
StreamGobbler
(
shell
.
toUpperCase
(
Locale
.
ENGLISH
)
+
"*"
,
process
.
getErrorStream
(),
new
OnLineListener
()
{
@Override
public
void
onLine
(
String
line
)
{
synchronized
(
Interactive
.
this
)
{
if
(
command
==
null
)
{
return
;
}
if
(
line
.
startsWith
(
command
.
marker
))
{
lastMarkerSTDERR
=
command
.
marker
;
processMarker
();
}
else
{
if
(
wantSTDERR
)
addBuffer
(
line
);
processLine
(
line
,
onSTDERRLineListener
);
}
}
}
});
// start gobbling and write our commands to the shell
STDOUT
.
start
();
STDERR
.
start
();
running
=
true
;
closed
=
false
;
runNextCommand
();
return
true
;
}
catch
(
IOException
e
)
{
// shell probably not found
return
false
;
}
}
/**
* Close shell and clean up all resources. Call this when you are done with the shell.
* If the shell is not idle (all commands completed) you should not call this method
* from the main UI thread because it may block for a long time. This method will
* intentionally crash your app (if in debug mode) if you try to do this anyway.
*/
public
void
close
()
{
boolean
_idle
=
isIdle
();
// idle must be checked synchronized
synchronized
(
this
)
{
if
(!
running
)
return
;
running
=
false
;
closed
=
true
;
}
// This method should not be called from the main thread unless the shell is idle
// and can be cleaned up with (minimal) waiting. Only throw in debug mode.
if
(!
_idle
&&
Debug
.
getSanityChecksEnabledEffective
()
&&
Debug
.
onMainThread
())
{
Debug
.
log
(
ShellOnMainThreadException
.
EXCEPTION_NOT_IDLE
);
throw
new
ShellOnMainThreadException
(
ShellOnMainThreadException
.
EXCEPTION_NOT_IDLE
);
}
if
(!
_idle
)
waitForIdle
();
try
{
STDIN
.
write
((
"exit\n"
).
getBytes
(
"UTF-8"
));
STDIN
.
flush
();
// wait for our process to finish, while we gobble away in the background
process
.
waitFor
();
// make sure our threads are done gobbling, our streams are closed, and the process is
// destroyed - while the latter two shouldn't be needed in theory, and may even produce
// warnings, in "normal" Java they are required for guaranteed cleanup of resources, so
// lets be safe and do this on Android as well
try
{
STDIN
.
close
();
}
catch
(
IOException
e
)
{
}
STDOUT
.
join
();
STDERR
.
join
();
stopWatchdog
();
process
.
destroy
();
}
catch
(
IOException
e
)
{
// shell probably not found
}
catch
(
InterruptedException
e
)
{
// this should really be re-thrown
}
Debug
.
log
(
String
.
format
(
"[%s%%] END"
,
shell
.
toUpperCase
(
Locale
.
ENGLISH
)));
}
/**
* Try to clean up as much as possible from a shell that's gotten itself wedged.
* Hopefully the StreamGobblers will croak on their own when the other side of
* the pipe is closed.
*/
public
synchronized
void
kill
()
{
running
=
false
;
closed
=
true
;
try
{
STDIN
.
close
();
}
catch
(
IOException
e
)
{
}
try
{
process
.
destroy
();
}
catch
(
Exception
e
)
{
}
}
/**
* Is our shell still running ?
*
* @return Shell running ?
*/
public
boolean
isRunning
()
{
try
{
// if this throws, we're still running
process
.
exitValue
();
return
false
;
}
catch
(
IllegalThreadStateException
e
)
{
}
return
true
;
}
/**
* Have all commands completed executing ?
*
* @return Shell idle ?
*/
public
synchronized
boolean
isIdle
()
{
if
(!
isRunning
())
{
idle
=
true
;
synchronized
(
idleSync
)
{
idleSync
.
notifyAll
();
}
}
return
idle
;
}
/**
* <p>Wait for idle state. As this is a blocking call, you should not call it from the main UI thread.
* If you do so and debug mode is enabled, this method will intentionally crash your app.</p>
*
* <p>If not interrupted, this method will not return until all commands have finished executing.
* Note that this does not necessarily mean that all the callbacks have fired yet.</p>
*
* <p>If no Handler is used, all callbacks will have been executed when this method returns. If
* a Handler is used, and this method is called from a different thread than associated with the
* Handler's Looper, all callbacks will have been executed when this method returns as well.
* If however a Handler is used but this method is called from the same thread as associated
* with the Handler's Looper, there is no way to know.</p>
*
* <p>In practice this means that in most simple cases all callbacks will have completed when this
* method returns, but if you actually depend on this behavior, you should make certain this is
* indeed the case.</p>
*
* <p>See {@link Shell.Interactive} for further details on threading and handlers</p>
*
* @return True if wait complete, false if wait interrupted
*/
public
boolean
waitForIdle
()
{
if
(
Debug
.
getSanityChecksEnabledEffective
()
&&
Debug
.
onMainThread
())
{
Debug
.
log
(
ShellOnMainThreadException
.
EXCEPTION_WAIT_IDLE
);
throw
new
ShellOnMainThreadException
(
ShellOnMainThreadException
.
EXCEPTION_WAIT_IDLE
);
}
if
(
isRunning
())
{
synchronized
(
idleSync
)
{
while
(!
idle
)
{
try
{
idleSync
.
wait
();
}
catch
(
InterruptedException
e
)
{
return
false
;
}
}
}
if
(
(
handler
!=
null
)
&&
(
handler
.
getLooper
()
!=
null
)
&&
(
handler
.
getLooper
()
!=
Looper
.
myLooper
())
)
{
// If the callbacks are posted to a different thread than this one, we can wait until
// all callbacks have called before returning. If we don't use a Handler at all,
// the callbacks are already called before we get here. If we do use a Handler but
// we use the same Looper, waiting here would actually block the callbacks from being
// called
synchronized
(
callbackSync
)
{
while
(
callbacks
>
0
)
{
try
{
callbackSync
.
wait
();
}
catch
(
InterruptedException
e
)
{
return
false
;
}
}
}
}
}
return
true
;
}
/**
* Are we using a Handler to post callbacks ?
*
* @return Handler used ?
*/
public
boolean
hasHandler
()
{
return
(
handler
!=
null
);
}
}
}
src/main/java/eu/chainfire/libsuperuser/ShellNotClosedException.java
0 → 100644
View file @
185f32aa
/*
* Copyright (C) 2012-2013 Jorrit "Chainfire" Jongma
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package
eu.chainfire.libsuperuser
;
/**
* Exception class used to notify developer that a shell was not close()d
*/
@SuppressWarnings
(
"serial"
)
public
class
ShellNotClosedException
extends
RuntimeException
{
public
static
final
String
EXCEPTION_NOT_CLOSED
=
"Application did not close() interactive shell"
;
public
ShellNotClosedException
()
{
super
(
EXCEPTION_NOT_CLOSED
);
}
}
src/main/java/eu/chainfire/libsuperuser/ShellOnMainThreadException.java
0 → 100644
View file @
185f32aa
/*
* Copyright (C) 2012-2013 Jorrit "Chainfire" Jongma
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package
eu.chainfire.libsuperuser
;
/**
* Exception class used to crash application when shell commands are executed
* from the main thread, and we are in debug mode.
*/
@SuppressWarnings
(
"serial"
)
public
class
ShellOnMainThreadException
extends
RuntimeException
{
public
static
final
String
EXCEPTION_COMMAND
=
"Application attempted to run a shell command from the main thread"
;
public
static
final
String
EXCEPTION_NOT_IDLE
=
"Application attempted to wait for a non-idle shell to close on the main thread"
;
public
static
final
String
EXCEPTION_WAIT_IDLE
=
"Application attempted to wait for a shell to become idle on the main thread"
;
public
ShellOnMainThreadException
(
String
message
)
{
super
(
message
);
}
}
src/main/java/eu/chainfire/libsuperuser/StreamGobbler.java
0 → 100644
View file @
185f32aa
/*
* Copyright (C) 2012-2013 Jorrit "Chainfire" Jongma
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package
eu.chainfire.libsuperuser
;
import
java.io.BufferedReader
;
import
java.io.IOException
;
import
java.io.InputStream
;
import
java.io.InputStreamReader
;
import
java.util.List
;
/**
* Thread utility class continuously reading from an InputStream
*/
public
class
StreamGobbler
extends
Thread
{
/**
* Line callback interface
*/
public
interface
OnLineListener
{
/**
* <p>Line callback</p>
*
* <p>This callback should process the line as quickly as possible.
* Delays in this callback may pause the native process or even
* result in a deadlock</p>
*
* @param line String that was gobbled
*/
public
void
onLine
(
String
line
);
}
private
String
shell
=
null
;
private
BufferedReader
reader
=
null
;
private
List
<
String
>
writer
=
null
;
private
OnLineListener
listener
=
null
;
/**
* <p>StreamGobbler constructor</p>
*
* <p>We use this class because shell STDOUT and STDERR should be read as quickly as
* possible to prevent a deadlock from occurring, or Process.waitFor() never
* returning (as the buffer is full, pausing the native process)</p>
*
* @param shell Name of the shell
* @param inputStream InputStream to read from
* @param outputList List<String> to write to, or null
*/
public
StreamGobbler
(
String
shell
,
InputStream
inputStream
,
List
<
String
>
outputList
)
{
this
.
shell
=
shell
;
reader
=
new
BufferedReader
(
new
InputStreamReader
(
inputStream
));
writer
=
outputList
;
}
/**
* <p>StreamGobbler constructor</p>
*
* <p>We use this class because shell STDOUT and STDERR should be read as quickly as
* possible to prevent a deadlock from occurring, or Process.waitFor() never
* returning (as the buffer is full, pausing the native process)</p>
*
* @param shell Name of the shell
* @param inputStream InputStream to read from
* @param onLineListener OnLineListener callback
*/
public
StreamGobbler
(
String
shell
,
InputStream
inputStream
,
OnLineListener
onLineListener
)
{
this
.
shell
=
shell
;
reader
=
new
BufferedReader
(
new
InputStreamReader
(
inputStream
));
listener
=
onLineListener
;
}
@Override
public
void
run
()
{
// keep reading the InputStream until it ends (or an error occurs)
try
{
String
line
=
null
;
while
((
line
=
reader
.
readLine
())
!=
null
)
{
Debug
.
logOutput
(
String
.
format
(
"[%s] %s"
,
shell
,
line
));
if
(
writer
!=
null
)
writer
.
add
(
line
);
if
(
listener
!=
null
)
listener
.
onLine
(
line
);
}
}
catch
(
IOException
e
)
{
}
// make sure our stream is closed and resources will be freed
try
{
reader
.
close
();
}
catch
(
IOException
e
)
{
}
}
}
src/main/jni/Android.mk
View file @
185f32aa
...
@@ -130,21 +130,6 @@ LOCAL_LDLIBS := -llog
...
@@ -130,21 +130,6 @@ LOCAL_LDLIBS := -llog
include
$(BUILD_EXECUTABLE)
include
$(BUILD_EXECUTABLE)
########################################################
## termExec
########################################################
include
$(CLEAR_VARS)
LOCAL_MODULE
:=
exec
LOCAL_SRC_FILES
:=
\
termExec.cpp
LOCAL_LDLIBS
:=
-ldl
-llog
include
$(BUILD_SHARED_LIBRARY)
########################################################
########################################################
## system
## system
########################################################
########################################################
...
...
src/main/jni/termExec.cpp
deleted
100644 → 0
View file @
cb31dd2a
/*
* Copyright (C) 2008 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/*
* Copyright (C) 2007 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#define LOG_TAG "Shadowsocks"
#include "jni.h"
#include <android/log.h>
#define LOGI(...) do { __android_log_print(ANDROID_LOG_INFO, LOG_TAG, __VA_ARGS__); } while(0)
#define LOGW(...) do { __android_log_print(ANDROID_LOG_WARN, LOG_TAG, __VA_ARGS__); } while(0)
#define LOGE(...) do { __android_log_print(ANDROID_LOG_ERROR, LOG_TAG, __VA_ARGS__); } while(0)
#include <sys/types.h>
#include <sys/stat.h>
#include <sys/ioctl.h>
#include <sys/wait.h>
#include <errno.h>
#include <fcntl.h>
#include <stdlib.h>
#include <unistd.h>
#include <termios.h>
#include <signal.h>
#include <stdio.h>
static
jclass
class_fileDescriptor
;
static
jfieldID
field_fileDescriptor_descriptor
;
static
jmethodID
method_fileDescriptor_init
;
typedef
unsigned
short
char16_t
;
class
String8
{
public:
String8
()
{
mString
=
0
;
}
~
String8
()
{
if
(
mString
)
{
free
(
mString
);
}
}
void
set
(
const
char16_t
*
o
,
size_t
numChars
)
{
if
(
mString
)
{
free
(
mString
);
}
mString
=
(
char
*
)
malloc
(
numChars
+
1
);
if
(
!
mString
)
{
return
;
}
for
(
size_t
i
=
0
;
i
<
numChars
;
i
++
)
{
mString
[
i
]
=
(
char
)
o
[
i
];
}
mString
[
numChars
]
=
'\0'
;
}
const
char
*
string
()
{
return
mString
;
}
private:
char
*
mString
;
};
static
int
throwOutOfMemoryError
(
JNIEnv
*
env
,
const
char
*
message
)
{
jclass
exClass
;
const
char
*
className
=
"java/lang/OutOfMemoryError"
;
exClass
=
env
->
FindClass
(
className
);
return
env
->
ThrowNew
(
exClass
,
message
);
}
static
int
create_subprocess
(
const
int
rdt
,
const
char
*
cmd
,
char
*
const
argv
[],
char
*
const
envp
[],
const
char
*
scripts
,
int
*
pProcessId
)
{
pid_t
pid
;
int
pfds
[
2
];
int
pfds2
[
2
];
pipe
(
pfds
);
if
(
rdt
)
{
pipe
(
pfds2
);
}
pid
=
fork
();
if
(
pid
<
0
)
{
LOGE
(
"- fork failed: %s -
\n
"
,
strerror
(
errno
));
return
-
1
;
}
if
(
pid
==
0
){
signal
(
SIGPIPE
,
SIG_IGN
);
if
(
envp
)
{
for
(;
*
envp
;
++
envp
)
{
putenv
(
*
envp
);
}
}
dup2
(
pfds
[
0
],
0
);
close
(
pfds
[
1
]);
if
(
rdt
)
{
close
(
1
);
close
(
2
);
dup2
(
pfds2
[
1
],
1
);
dup2
(
pfds2
[
1
],
2
);
close
(
pfds2
[
0
]);
}
execv
(
cmd
,
argv
);
fflush
(
NULL
);
exit
(
0
);
}
else
{
signal
(
SIGPIPE
,
SIG_IGN
);
*
pProcessId
=
(
int
)
pid
;
dup2
(
pfds
[
1
],
1
);
close
(
pfds
[
0
]);
write
(
pfds
[
1
],
scripts
,
strlen
(
scripts
)
+
1
);
if
(
rdt
)
{
close
(
pfds2
[
1
]);
return
pfds2
[
0
];
}
else
{
return
-
1
;
}
}
}
static
jobject
android_os_Exec_createSubProcess
(
JNIEnv
*
env
,
jobject
clazz
,
jint
rdt
,
jstring
cmd
,
jobjectArray
args
,
jobjectArray
envVars
,
jstring
scripts
,
jintArray
processIdArray
)
{
const
jchar
*
str
=
cmd
?
env
->
GetStringCritical
(
cmd
,
0
)
:
0
;
String8
cmd_8
;
if
(
str
)
{
cmd_8
.
set
(
str
,
env
->
GetStringLength
(
cmd
));
env
->
ReleaseStringCritical
(
cmd
,
str
);
}
const
jchar
*
str_scripts
=
scripts
?
env
->
GetStringCritical
(
scripts
,
0
)
:
0
;
String8
scripts_8
;
if
(
str_scripts
)
{
scripts_8
.
set
(
str_scripts
,
env
->
GetStringLength
(
scripts
));
env
->
ReleaseStringCritical
(
scripts
,
str_scripts
);
}
jsize
size
=
args
?
env
->
GetArrayLength
(
args
)
:
0
;
char
**
argv
=
NULL
;
String8
tmp_8
;
if
(
size
>
0
)
{
argv
=
(
char
**
)
malloc
((
size
+
1
)
*
sizeof
(
char
*
));
if
(
!
argv
)
{
throwOutOfMemoryError
(
env
,
"Couldn't allocate argv array"
);
return
NULL
;
}
for
(
int
i
=
0
;
i
<
size
;
++
i
)
{
jstring
arg
=
reinterpret_cast
<
jstring
>
(
env
->
GetObjectArrayElement
(
args
,
i
));
str
=
env
->
GetStringCritical
(
arg
,
0
);
if
(
!
str
)
{
throwOutOfMemoryError
(
env
,
"Couldn't get argument from array"
);
return
NULL
;
}
tmp_8
.
set
(
str
,
env
->
GetStringLength
(
arg
));
env
->
ReleaseStringCritical
(
arg
,
str
);
argv
[
i
]
=
strdup
(
tmp_8
.
string
());
}
argv
[
size
]
=
NULL
;
}
size
=
envVars
?
env
->
GetArrayLength
(
envVars
)
:
0
;
char
**
envp
=
NULL
;
if
(
size
>
0
)
{
envp
=
(
char
**
)
malloc
((
size
+
1
)
*
sizeof
(
char
*
));
if
(
!
envp
)
{
throwOutOfMemoryError
(
env
,
"Couldn't allocate envp array"
);
return
NULL
;
}
for
(
int
i
=
0
;
i
<
size
;
++
i
)
{
jstring
var
=
reinterpret_cast
<
jstring
>
(
env
->
GetObjectArrayElement
(
envVars
,
i
));
str
=
env
->
GetStringCritical
(
var
,
0
);
if
(
!
str
)
{
throwOutOfMemoryError
(
env
,
"Couldn't get env var from array"
);
return
NULL
;
}
tmp_8
.
set
(
str
,
env
->
GetStringLength
(
var
));
env
->
ReleaseStringCritical
(
var
,
str
);
envp
[
i
]
=
strdup
(
tmp_8
.
string
());
}
envp
[
size
]
=
NULL
;
}
int
procId
;
int
ptm
=
create_subprocess
(
rdt
,
cmd_8
.
string
(),
argv
,
envp
,
scripts_8
.
string
(),
&
procId
);
if
(
argv
)
{
for
(
char
**
tmp
=
argv
;
*
tmp
;
++
tmp
)
{
free
(
*
tmp
);
}
free
(
argv
);
}
if
(
envp
)
{
for
(
char
**
tmp
=
envp
;
*
tmp
;
++
tmp
)
{
free
(
*
tmp
);
}
free
(
envp
);
}
if
(
processIdArray
)
{
int
procIdLen
=
env
->
GetArrayLength
(
processIdArray
);
if
(
procIdLen
>
0
)
{
jboolean
isCopy
;
int
*
pProcId
=
(
int
*
)
env
->
GetPrimitiveArrayCritical
(
processIdArray
,
&
isCopy
);
if
(
pProcId
)
{
*
pProcId
=
procId
;
env
->
ReleasePrimitiveArrayCritical
(
processIdArray
,
pProcId
,
0
);
}
}
}
jobject
result
=
env
->
NewObject
(
class_fileDescriptor
,
method_fileDescriptor_init
);
if
(
!
result
)
{
LOGE
(
"Couldn't create a FileDescriptor."
);
}
else
{
env
->
SetIntField
(
result
,
field_fileDescriptor_descriptor
,
ptm
);
}
return
result
;
}
static
int
android_os_Exec_waitFor
(
JNIEnv
*
env
,
jobject
clazz
,
jint
procId
)
{
int
status
;
waitpid
(
procId
,
&
status
,
0
);
int
result
=
0
;
if
(
WIFEXITED
(
status
))
{
result
=
WEXITSTATUS
(
status
);
}
return
result
;
}
static
void
android_os_Exec_close
(
JNIEnv
*
env
,
jobject
clazz
,
jobject
fileDescriptor
)
{
int
fd
;
fd
=
env
->
GetIntField
(
fileDescriptor
,
field_fileDescriptor_descriptor
);
if
(
env
->
ExceptionOccurred
()
!=
NULL
)
{
return
;
}
close
(
fd
);
}
static
void
android_os_Exec_hangupProcessGroup
(
JNIEnv
*
env
,
jobject
clazz
,
jint
procId
)
{
kill
(
-
procId
,
SIGHUP
);
}
static
int
register_FileDescriptor
(
JNIEnv
*
env
)
{
jclass
localRef_class_fileDescriptor
=
env
->
FindClass
(
"java/io/FileDescriptor"
);
if
(
localRef_class_fileDescriptor
==
NULL
)
{
LOGE
(
"Can't find class java/io/FileDescriptor"
);
return
-
1
;
}
class_fileDescriptor
=
(
jclass
)
env
->
NewGlobalRef
(
localRef_class_fileDescriptor
);
env
->
DeleteLocalRef
(
localRef_class_fileDescriptor
);
if
(
class_fileDescriptor
==
NULL
)
{
LOGE
(
"Can't get global ref to class java/io/FileDescriptor"
);
return
-
1
;
}
field_fileDescriptor_descriptor
=
env
->
GetFieldID
(
class_fileDescriptor
,
"descriptor"
,
"I"
);
if
(
field_fileDescriptor_descriptor
==
NULL
)
{
LOGE
(
"Can't find FileDescriptor.descriptor"
);
return
-
1
;
}
method_fileDescriptor_init
=
env
->
GetMethodID
(
class_fileDescriptor
,
"<init>"
,
"()V"
);
if
(
method_fileDescriptor_init
==
NULL
)
{
LOGE
(
"Can't find FileDescriptor.init"
);
return
-
1
;
}
return
0
;
}
static
const
char
*
classPathName
=
"com/github/shadowsocks/Exec"
;
static
JNINativeMethod
method_table
[]
=
{
{
"createSubprocess"
,
"(ILjava/lang/String;[Ljava/lang/String;[Ljava/lang/String;Ljava/lang/String;[I)Ljava/io/FileDescriptor;"
,
(
void
*
)
android_os_Exec_createSubProcess
},
{
"waitFor"
,
"(I)I"
,
(
void
*
)
android_os_Exec_waitFor
},
{
"close"
,
"(Ljava/io/FileDescriptor;)V"
,
(
void
*
)
android_os_Exec_close
},
{
"hangupProcessGroup"
,
"(I)V"
,
(
void
*
)
android_os_Exec_hangupProcessGroup
}
};
/*
* Register several native methods for one class.
*/
static
int
registerNativeMethods
(
JNIEnv
*
env
,
const
char
*
className
,
JNINativeMethod
*
gMethods
,
int
numMethods
)
{
jclass
clazz
;
clazz
=
env
->
FindClass
(
className
);
if
(
clazz
==
NULL
)
{
LOGE
(
"Native registration unable to find class '%s'"
,
className
);
return
JNI_FALSE
;
}
if
(
env
->
RegisterNatives
(
clazz
,
gMethods
,
numMethods
)
<
0
)
{
LOGE
(
"RegisterNatives failed for '%s'"
,
className
);
return
JNI_FALSE
;
}
return
JNI_TRUE
;
}
/*
* Register native methods for all classes we know about.
*
* returns JNI_TRUE on success.
*/
static
int
registerNatives
(
JNIEnv
*
env
)
{
if
(
!
registerNativeMethods
(
env
,
classPathName
,
method_table
,
sizeof
(
method_table
)
/
sizeof
(
method_table
[
0
])))
{
return
JNI_FALSE
;
}
return
JNI_TRUE
;
}
// ----------------------------------------------------------------------------
/*
* This is called by the VM when the shared library is first loaded.
*/
typedef
union
{
JNIEnv
*
env
;
void
*
venv
;
}
UnionJNIEnvToVoid
;
jint
JNI_OnLoad
(
JavaVM
*
vm
,
void
*
reserved
)
{
UnionJNIEnvToVoid
uenv
;
uenv
.
venv
=
NULL
;
jint
result
=
-
1
;
JNIEnv
*
env
=
NULL
;
LOGI
(
"JNI_OnLoad"
);
if
(
vm
->
GetEnv
(
&
uenv
.
venv
,
JNI_VERSION_1_4
)
!=
JNI_OK
)
{
LOGE
(
"ERROR: GetEnv failed"
);
goto
bail
;
}
env
=
uenv
.
env
;
if
((
result
=
register_FileDescriptor
(
env
))
<
0
)
{
LOGE
(
"ERROR: registerFileDescriptor failed"
);
goto
bail
;
}
if
(
registerNatives
(
env
)
!=
JNI_TRUE
)
{
LOGE
(
"ERROR: registerNatives failed"
);
goto
bail
;
}
result
=
JNI_VERSION_1_4
;
bail:
return
result
;
}
src/main/scala/com/github/shadowsocks/Shadowsocks.scala
View file @
185f32aa
...
@@ -49,7 +49,6 @@ import android.view._
...
@@ -49,7 +49,6 @@ import android.view._
import
android.widget._
import
android.widget._
import
com.google.analytics.tracking.android.
{
MapBuilder
,
EasyTracker
}
import
com.google.analytics.tracking.android.
{
MapBuilder
,
EasyTracker
}
import
de.keyboardsurfer.android.widget.crouton.
{
Crouton
,
Style
,
Configuration
}
import
de.keyboardsurfer.android.widget.crouton.
{
Crouton
,
Style
,
Configuration
}
import
java.io._
import
java.util.Hashtable
import
java.util.Hashtable
import
net.saik0.android.unifiedpreference.UnifiedPreferenceFragment
import
net.saik0.android.unifiedpreference.UnifiedPreferenceFragment
import
net.saik0.android.unifiedpreference.UnifiedSherlockPreferenceActivity
import
net.saik0.android.unifiedpreference.UnifiedSherlockPreferenceActivity
...
@@ -63,7 +62,7 @@ import com.google.ads.{AdRequest, AdSize, AdView}
...
@@ -63,7 +62,7 @@ import com.google.ads.{AdRequest, AdSize, AdView}
import
net.simonvt.menudrawer.MenuDrawer
import
net.simonvt.menudrawer.MenuDrawer
import
com.github.shadowsocks.database._
import
com.github.shadowsocks.database._
import
scala.collection.mutable.
ListBuffer
import
scala.collection.mutable.
{
ArrayBuffer
,
ListBuffer
}
import
com.github.shadowsocks.database.Profile
import
com.github.shadowsocks.database.Profile
import
com.nostra13.universalimageloader.core.download.BaseImageDownloader
import
com.nostra13.universalimageloader.core.download.BaseImageDownloader
import
com.github.shadowsocks.preferences.
{
ProfileEditTextPreference
,
PasswordEditTextPreference
,
SummaryEditTextPreference
}
import
com.github.shadowsocks.preferences.
{
ProfileEditTextPreference
,
PasswordEditTextPreference
,
SummaryEditTextPreference
}
...
@@ -72,6 +71,11 @@ import com.github.shadowsocks.database.Item
...
@@ -72,6 +71,11 @@ import com.github.shadowsocks.database.Item
import
com.github.shadowsocks.database.Category
import
com.github.shadowsocks.database.Category
import
com.google.zxing.integration.android.IntentIntegrator
import
com.google.zxing.integration.android.IntentIntegrator
import
com.github.shadowsocks.aidl.
{
IShadowsocksServiceCallback
,
IShadowsocksService
}
import
com.github.shadowsocks.aidl.
{
IShadowsocksServiceCallback
,
IShadowsocksService
}
import
java.io._
import
scala.Some
import
com.github.shadowsocks.database.Item
import
com.github.shadowsocks.database.Category
import
com.github.shadowsocks.utils.Console
class
ProfileIconDownloader
(
context
:
Context
,
connectTimeout
:
Int
,
readTimeout
:
Int
)
class
ProfileIconDownloader
(
context
:
Context
,
connectTimeout
:
Int
,
readTimeout
:
Int
)
extends
BaseImageDownloader
(
context
,
connectTimeout
,
readTimeout
)
{
extends
BaseImageDownloader
(
context
,
connectTimeout
,
readTimeout
)
{
...
@@ -118,13 +122,17 @@ object Typefaces {
...
@@ -118,13 +122,17 @@ object Typefaces {
object
Shadowsocks
{
object
Shadowsocks
{
// Constants
// Constants
val
TAG
=
"Shadowsocks"
val
REQUEST_CONNECT
=
1
val
PREFS_NAME
=
"Shadowsocks"
val
PREFS_NAME
=
"Shadowsocks"
val
PROXY_PREFS
=
Array
(
Key
.
profileName
,
Key
.
proxy
,
Key
.
remotePort
,
Key
.
localPort
,
Key
.
sitekey
,
val
PROXY_PREFS
=
Array
(
Key
.
profileName
,
Key
.
proxy
,
Key
.
remotePort
,
Key
.
localPort
,
Key
.
sitekey
,
Key
.
encMethod
)
Key
.
encMethod
)
val
FEATRUE_PREFS
=
Array
(
Key
.
isGFWList
,
Key
.
isGlobalProxy
,
Key
.
proxyedApps
,
Key
.
isTrafficStat
,
val
FEATRUE_PREFS
=
Array
(
Key
.
isGFWList
,
Key
.
isGlobalProxy
,
Key
.
proxyedApps
,
Key
.
isTrafficStat
,
Key
.
isUdpDns
,
Key
.
isAutoConnect
)
Key
.
isUdpDns
,
Key
.
isAutoConnect
)
val
TAG
=
"Shadowsocks"
val
REQUEST_CONNECT
=
1
val
EXECUTABLES
=
Array
(
Executable
.
IPTABLES
,
Executable
.
PDNSD
,
Executable
.
REDSOCKS
,
Executable
.
SS_LOCAL
,
Executable
.
SS_TUNNEL
,
Executable
.
TUN2SOCKS
)
// Helper functions
// Helper functions
def
updateListPreference
(
pref
:
Preference
,
value
:
String
)
{
def
updateListPreference
(
pref
:
Preference
,
value
:
String
)
{
...
@@ -415,26 +423,28 @@ class Shadowsocks
...
@@ -415,26 +423,28 @@ class Shadowsocks
}
}
private
def
crash_recovery
()
{
private
def
crash_recovery
()
{
val
sb
=
new
StringBuilder
val
ab
=
new
ArrayBuffer
[
String
]
sb
.
append
(
"kill -9 `cat /data/data/com.github.shadowsocks/pdnsd.pid`"
).
append
(
"\n"
)
ab
.
append
(
"kill -9 `cat /data/data/com.github.shadowsocks/pdnsd.pid`"
)
sb
.
append
(
"kill -9 `cat /data/data/com.github.shadowsocks/ss-local.pid`"
).
append
(
"\n"
)
ab
.
append
(
"kill -9 `cat /data/data/com.github.shadowsocks/ss-local.pid`"
)
sb
.
append
(
"kill -9 `cat /data/data/com.github.shadowsocks/ss-tunnel.pid`"
).
append
(
"\n"
)
ab
.
append
(
"kill -9 `cat /data/data/com.github.shadowsocks/ss-tunnel.pid`"
)
sb
.
append
(
"kill -9 `cat /data/data/com.github.shadowsocks/tun2socks.pid`"
).
append
(
"\n"
)
ab
.
append
(
"kill -9 `cat /data/data/com.github.shadowsocks/tun2socks.pid`"
)
sb
.
append
(
"killall -9 pdnsd"
).
append
(
"\n"
)
ab
.
append
(
"killall -9 pdnsd"
)
sb
.
append
(
"killall -9 ss-local"
).
append
(
"\n"
)
ab
.
append
(
"killall -9 ss-local"
)
sb
.
append
(
"killall -9 ss-tunnel"
).
append
(
"\n"
)
ab
.
append
(
"killall -9 ss-tunnel"
)
sb
.
append
(
"killall -9 tun2socks"
).
append
(
"\n"
)
ab
.
append
(
"killall -9 tun2socks"
)
sb
.
append
(
"rm /data/data/com.github.shadowsocks/pdnsd.conf"
).
append
(
"\n"
)
ab
.
append
(
"rm /data/data/com.github.shadowsocks/pdnsd.conf"
)
sb
.
append
(
"rm /data/data/com.github.shadowsocks/pdnsd.cache"
).
append
(
"\n"
)
ab
.
append
(
"rm /data/data/com.github.shadowsocks/pdnsd.cache"
)
Utils
.
runCommand
(
sb
.
toString
())
Console
.
runCommand
(
ab
.
toArray
)
sb
.
clear
()
sb
.
append
(
"kill -9 `cat /data/data/com.github.shadowsocks/redsocks.pid`"
).
append
(
"\n"
)
ab
.
clear
()
sb
.
append
(
"killall -9 redsocks"
).
append
(
"\n"
)
ab
.
append
(
"kill -9 `cat /data/data/com.github.shadowsocks/redsocks.pid`"
)
sb
.
append
(
"rm /data/data/com.github.shadowsocks/redsocks.conf"
).
append
(
"\n"
)
ab
.
append
(
"killall -9 redsocks"
)
sb
.
append
(
Utils
.
getIptables
).
append
(
" -t nat -F OUTPUT"
).
append
(
"\n"
)
ab
.
append
(
"rm /data/data/com.github.shadowsocks/redsocks.conf"
)
Utils
.
runRootCommand
(
sb
.
toString
())
ab
.
append
(
Utils
.
getIptables
+
" -t nat -F OUTPUT"
)
Console
.
runRootCommand
(
ab
.
toArray
)
}
}
private
def
getVersionName
:
String
=
{
private
def
getVersionName
:
String
=
{
...
@@ -587,7 +597,7 @@ class Shadowsocks
...
@@ -587,7 +597,7 @@ class Shadowsocks
// Bind to the service
// Bind to the service
spawn
{
spawn
{
val
isRoot
=
Utils
.
get
Root
val
isRoot
=
Console
.
is
Root
handler
.
post
(
new
Runnable
{
handler
.
post
(
new
Runnable
{
override
def
run
()
{
override
def
run
()
{
status
.
edit
.
putBoolean
(
Key
.
isRoot
,
isRoot
).
commit
()
status
.
edit
.
putBoolean
(
Key
.
isRoot
,
isRoot
).
commit
()
...
@@ -908,12 +918,12 @@ class Shadowsocks
...
@@ -908,12 +918,12 @@ class Shadowsocks
def
reset
()
{
def
reset
()
{
crash_recovery
()
crash_recovery
()
copyAssets
(
System
.
getABI
)
copyAssets
(
System
.
getABI
)
Utils
.
runCommand
(
"chmod 755 /data/data/com.github.shadowsocks/iptables\n"
+
"chmod 755 /data/data/com.github.shadowsocks/redsocks\n"
val
ab
=
new
ArrayBuffer
[
String
]
+
"chmod 755 /data/data/com.github.shadowsocks/pdnsd\n"
for
(
executable
<-
Shadowsocks
.
EXECUTABLES
)
{
+
"chmod 755 /data/data/com.github.shadowsocks/ss-local\n"
ab
.
append
(
"chmod 755 "
+
Path
.
BASE
+
executable
)
+
"chmod 755 /data/data/com.github.shadowsocks/ss-tunnel\n"
}
+
"chmod 755 /data/data/com.github.shadowsocks/tun2socks\n"
)
Console
.
runCommand
(
ab
.
toArray
)
}
}
private
def
recovery
()
{
private
def
recovery
()
{
...
...
src/main/scala/com/github/shadowsocks/ShadowsocksNatService.scala
View file @
185f32aa
...
@@ -50,7 +50,6 @@ import android.os._
...
@@ -50,7 +50,6 @@ import android.os._
import
android.support.v4.app.NotificationCompat
import
android.support.v4.app.NotificationCompat
import
android.util.Log
import
android.util.Log
import
com.google.analytics.tracking.android.
{
Fields
,
MapBuilder
,
EasyTracker
}
import
com.google.analytics.tracking.android.
{
Fields
,
MapBuilder
,
EasyTracker
}
import
java.io._
import
java.lang.reflect.InvocationTargetException
import
java.lang.reflect.InvocationTargetException
import
java.lang.reflect.Method
import
java.lang.reflect.Method
import
org.apache.http.conn.util.InetAddressUtils
import
org.apache.http.conn.util.InetAddressUtils
...
@@ -62,6 +61,8 @@ import com.github.shadowsocks.utils._
...
@@ -62,6 +61,8 @@ import com.github.shadowsocks.utils._
import
scala.Some
import
scala.Some
import
android.graphics.Color
import
android.graphics.Color
import
com.github.shadowsocks.aidl.Config
import
com.github.shadowsocks.aidl.Config
import
scala.collection.mutable.ArrayBuffer
import
java.io.File
case
class
TrafficStat
(
tx
:
Long
,
rx
:
Long
,
timestamp
:
Long
)
case
class
TrafficStat
(
tx
:
Long
,
rx
:
Long
,
timestamp
:
Long
)
...
@@ -107,7 +108,7 @@ class ShadowsocksNatService extends Service with BaseService {
...
@@ -107,7 +108,7 @@ class ShadowsocksNatService extends Service with BaseService {
Path
.
BASE
+
"ss-local.pid"
)
Path
.
BASE
+
"ss-local.pid"
)
.
format
(
config
.
proxy
,
config
.
remotePort
,
config
.
localPort
,
config
.
sitekey
,
config
.
encMethod
)
.
format
(
config
.
proxy
,
config
.
remotePort
,
config
.
localPort
,
config
.
sitekey
,
config
.
encMethod
)
if
(
BuildConfig
.
DEBUG
)
Log
.
d
(
TAG
,
cmd
)
if
(
BuildConfig
.
DEBUG
)
Log
.
d
(
TAG
,
cmd
)
Utils
.
runCommand
(
cmd
)
System
.
exec
(
cmd
)
}
}
def
startDnsDaemon
()
{
def
startDnsDaemon
()
{
...
@@ -123,8 +124,8 @@ class ShadowsocksNatService extends Service with BaseService {
...
@@ -123,8 +124,8 @@ class ShadowsocksNatService extends Service with BaseService {
})
})
Path
.
BASE
+
"pdnsd -c "
+
Path
.
BASE
+
"pdnsd.conf"
Path
.
BASE
+
"pdnsd -c "
+
Path
.
BASE
+
"pdnsd.conf"
}
}
if
(
BuildConfig
.
DEBUG
)
Log
.
d
(
TAG
,
cmd
)
Utils
.
runCommand
(
cmd
)
System
.
exec
(
cmd
)
}
}
def
getVersionName
:
String
=
{
def
getVersionName
:
String
=
{
...
@@ -146,7 +147,7 @@ class ShadowsocksNatService extends Service with BaseService {
...
@@ -146,7 +147,7 @@ class ShadowsocksNatService extends Service with BaseService {
ConfigUtils
.
printToFile
(
new
File
(
Path
.
BASE
+
"redsocks.conf"
))(
p
=>
{
ConfigUtils
.
printToFile
(
new
File
(
Path
.
BASE
+
"redsocks.conf"
))(
p
=>
{
p
.
println
(
conf
)
p
.
println
(
conf
)
})
})
Utils
.
runRootCommand
(
cmd
)
Console
.
runRootCommand
(
cmd
)
}
}
/** Called when the activity is first created. */
/** Called when the activity is first created. */
...
@@ -267,24 +268,25 @@ class ShadowsocksNatService extends Service with BaseService {
...
@@ -267,24 +268,25 @@ class ShadowsocksNatService extends Service with BaseService {
}
}
def
killProcesses
()
{
def
killProcesses
()
{
Utils
.
runRootCommand
(
Utils
.
getIptables
+
" -t nat -F OUTPUT"
)
Console
.
runRootCommand
(
Utils
.
getIptables
+
" -t nat -F OUTPUT"
)
val
ab
=
new
ArrayBuffer
[
String
]
val
sb
=
new
StringBuilder
ab
.
append
(
"kill -9 `cat "
+
Path
.
BASE
+
"redsocks.pid`"
)
ab
.
append
(
"killall -9 redsocks"
)
sb
++=
"kill -9 `cat "
++=
Path
.
BASE
++=
"redsocks.pid`"
++=
"\n"
Console
.
runRootCommand
(
ab
.
toArray
)
sb
++=
"killall -9 redsocks"
++=
"\n"
Utils
.
runRootCommand
(
sb
.
toString
())
s
b
.
clear
()
a
b
.
clear
()
sb
++=
"kill -9 `cat "
++=
Path
.
BASE
++=
"pdnsd.pid`"
++=
"\n"
ab
.
append
(
"kill -9 `cat "
+
Path
.
BASE
+
"ss-local.pid`"
)
sb
++=
"killall -9 pdnsd"
++=
"\n"
ab
.
append
(
"killall -9 ss-local"
)
sb
++=
"kill -9 `cat "
++=
Path
.
BASE
++=
"ss-local.pid`"
++=
"\n"
ab
.
append
(
"kill -9 `cat "
+
Path
.
BASE
+
"ss-tunnel.pid`"
)
sb
++=
"killall -9 ss-local"
++=
"\n"
ab
.
append
(
"killall -9 ss-tunnel"
)
sb
++=
"kill -9 `cat "
++=
Path
.
BASE
++=
"ss-tunnel.pid`"
++=
"\n"
ab
.
append
(
"kill -9 `cat "
+
Path
.
BASE
+
"pdnsd.pid`"
)
sb
++=
"killall -9 ss-tunnel"
++=
"\n"
ab
.
append
(
"killall -9 pdnsd"
)
Utils
.
runCommand
(
sb
.
toString
()
)
Console
.
runRootCommand
(
ab
.
toArray
)
}
}
override
def
onStartCommand
(
intent
:
Intent
,
flags
:
Int
,
startId
:
Int
)
:
Int
=
{
override
def
onStartCommand
(
intent
:
Intent
,
flags
:
Int
,
startId
:
Int
)
:
Int
=
{
...
@@ -292,8 +294,7 @@ class ShadowsocksNatService extends Service with BaseService {
...
@@ -292,8 +294,7 @@ class ShadowsocksNatService extends Service with BaseService {
}
}
def
flushDNS
()
{
def
flushDNS
()
{
Utils
.
runRootCommand
(
"ndc resolver flushdefaultif\n"
Console
.
runRootCommand
(
Array
(
"ndc resolver flushdefaultif"
,
"ndc resolver flushif wlan0"
))
+
"ndc resolver flushif wlan0\n"
)
}
}
def
setupIptables
:
Boolean
=
{
def
setupIptables
:
Boolean
=
{
...
@@ -354,9 +355,9 @@ class ShadowsocksNatService extends Service with BaseService {
...
@@ -354,9 +355,9 @@ class ShadowsocksNatService extends Service with BaseService {
}
}
}
}
val
init_rules
:
String
=
init_sb
.
toString
()
val
init_rules
:
String
=
init_sb
.
toString
()
Utils
.
runRootCommand
(
init_rules
,
30
*
1000
)
Console
.
runRootCommand
(
init_rules
)
val
redt_rules
:
String
=
http_sb
.
toString
()
val
redt_rules
:
String
=
http_sb
.
toString
()
Utils
.
runRootCommand
(
redt_rules
)
Console
.
runRootCommand
(
redt_rules
)
true
true
}
}
...
...
src/main/scala/com/github/shadowsocks/ShadowsocksRunnerActivity.scala
View file @
185f32aa
...
@@ -73,7 +73,7 @@ class ShadowsocksRunnerActivity extends Activity {
...
@@ -73,7 +73,7 @@ class ShadowsocksRunnerActivity extends Activity {
def
isVpnEnabled
:
Boolean
=
{
def
isVpnEnabled
:
Boolean
=
{
if
(
vpnEnabled
<
0
)
{
if
(
vpnEnabled
<
0
)
{
vpnEnabled
=
if
(
Build
.
VERSION
.
SDK_INT
>=
Build
.
VERSION_CODES
.
ICE_CREAM_SANDWICH
vpnEnabled
=
if
(
Build
.
VERSION
.
SDK_INT
>=
Build
.
VERSION_CODES
.
ICE_CREAM_SANDWICH
&&
!
Utils
.
get
Root
)
{
&&
!
Console
.
is
Root
)
{
1
1
}
else
{
}
else
{
0
0
...
...
src/main/scala/com/github/shadowsocks/ShadowsocksVpnService.scala
View file @
185f32aa
...
@@ -47,7 +47,6 @@ import android.os._
...
@@ -47,7 +47,6 @@ import android.os._
import
android.support.v4.app.NotificationCompat
import
android.support.v4.app.NotificationCompat
import
android.util.Log
import
android.util.Log
import
com.google.analytics.tracking.android.
{
Fields
,
MapBuilder
,
EasyTracker
}
import
com.google.analytics.tracking.android.
{
Fields
,
MapBuilder
,
EasyTracker
}
import
java.io._
import
android.net.VpnService
import
android.net.VpnService
import
org.apache.http.conn.util.InetAddressUtils
import
org.apache.http.conn.util.InetAddressUtils
import
android.os.Message
import
android.os.Message
...
@@ -57,6 +56,8 @@ import java.net.InetAddress
...
@@ -57,6 +56,8 @@ import java.net.InetAddress
import
com.github.shadowsocks.utils._
import
com.github.shadowsocks.utils._
import
scala.Some
import
scala.Some
import
com.github.shadowsocks.aidl.
{
IShadowsocksService
,
Config
}
import
com.github.shadowsocks.aidl.
{
IShadowsocksService
,
Config
}
import
scala.collection.mutable.ArrayBuffer
import
java.io.File
class
ShadowsocksVpnService
extends
VpnService
with
BaseService
{
class
ShadowsocksVpnService
extends
VpnService
with
BaseService
{
...
@@ -96,7 +97,8 @@ class ShadowsocksVpnService extends VpnService with BaseService {
...
@@ -96,7 +97,8 @@ class ShadowsocksVpnService extends VpnService with BaseService {
})
})
Path
.
BASE
+
"pdnsd -c "
+
Path
.
BASE
+
"pdnsd.conf"
Path
.
BASE
+
"pdnsd -c "
+
Path
.
BASE
+
"pdnsd.conf"
}
}
Utils
.
runCommand
(
cmd
)
if
(
BuildConfig
.
DEBUG
)
Log
.
d
(
TAG
,
cmd
)
System
.
exec
(
cmd
)
}
}
def
getVersionName
:
String
=
{
def
getVersionName
:
String
=
{
...
@@ -248,18 +250,18 @@ class ShadowsocksVpnService extends VpnService with BaseService {
...
@@ -248,18 +250,18 @@ class ShadowsocksVpnService extends VpnService with BaseService {
}
}
def
killProcesses
()
{
def
killProcesses
()
{
val
sb
=
new
StringBuilder
val
ab
=
new
ArrayBuffer
[
String
]
sb
++=
"kill -9 `cat "
++=
Path
.
BASE
++=
"ss-local.pid`"
++=
"\n"
ab
.
append
(
"kill -9 `cat "
+
Path
.
BASE
+
"ss-local.pid`"
)
sb
++=
"killall -9 ss-local"
++=
"\n"
ab
.
append
(
"killall -9 ss-local"
)
sb
++=
"kill -9 `cat "
++=
Path
.
BASE
++=
"ss-tunnel.pid`"
++=
"\n"
ab
.
append
(
"kill -9 `cat "
+
Path
.
BASE
+
"ss-tunnel.pid`"
)
sb
++=
"killall -9 ss-tunnel"
++=
"\n"
ab
.
append
(
"killall -9 ss-tunnel"
)
sb
++=
"kill -9 `cat "
++=
Path
.
BASE
++=
"tun2socks.pid`"
++=
"\n"
ab
.
append
(
"kill -9 `cat "
+
Path
.
BASE
+
"tun2socks.pid`"
)
sb
++=
"killall -9 tun2socks"
++=
"\n"
ab
.
append
(
"killall -9 tun2socks"
)
sb
++=
"kill -9 `cat "
++=
Path
.
BASE
++=
"pdnsd.pid`"
++=
"\n"
ab
.
append
(
"kill -9 `cat "
+
Path
.
BASE
+
"pdnsd.pid`"
)
sb
++=
"killall -9 pdnsd"
++=
"\n"
ab
.
append
(
"killall -9 pdnsd"
)
Utils
.
runCommand
(
sb
.
toString
()
)
Console
.
runCommand
(
ab
.
toArray
)
}
}
override
def
onStartCommand
(
intent
:
Intent
,
flags
:
Int
,
startId
:
Int
)
:
Int
=
{
override
def
onStartCommand
(
intent
:
Intent
,
flags
:
Int
,
startId
:
Int
)
:
Int
=
{
...
...
src/main/scala/com/github/shadowsocks/utils/Console.scala
0 → 100644
View file @
185f32aa
package
com.github.shadowsocks.utils
import
eu.chainfire.libsuperuser.Shell
import
eu.chainfire.libsuperuser.Shell.
{
OnCommandResultListener
,
SU
,
Builder
}
import
java.util
import
android.util.Log
object
Console
{
private
var
shell
:
Shell.Interactive
=
null
private
var
rootShell
:
Shell.Interactive
=
null
private
def
openShell
()
{
if
(
shell
==
null
)
{
val
builder
=
new
Builder
()
shell
=
builder
.
useSH
()
.
setWatchdogTimeout
(
10
)
.
open
(
new
OnCommandResultListener
{
override
def
onCommandResult
(
commandCode
:
Int
,
exitCode
:
Int
,
output
:
util.List
[
String
])
{
if
(
exitCode
<
0
)
shell
=
null
}
})
}
}
private
def
openRootShell
()
{
if
(
rootShell
==
null
)
{
val
builder
=
new
Builder
()
rootShell
=
builder
.
setShell
(
SU
.
shell
(
0
,
"u:r:untrusted_app:s0"
))
.
setWantSTDERR
(
true
)
.
setWatchdogTimeout
(
10
)
.
open
()
}
}
def
runCommand
(
command
:
String
)
{
runCommand
(
Array
(
command
))
}
def
runCommand
(
commands
:
Array
[
String
])
{
if
(
shell
==
null
)
{
openShell
()
}
shell
.
addCommand
(
commands
)
shell
.
waitForIdle
()
}
def
runRootCommand
(
command
:
String
)
:
String
=
runRootCommand
(
Array
(
command
))
def
runRootCommand
(
commands
:
Array
[
String
])
:
String
=
{
if
(!
isRoot
)
{
return
null
}
if
(
rootShell
==
null
)
{
openRootShell
()
}
val
sb
=
new
StringBuilder
rootShell
.
addCommand
(
commands
,
0
,
new
OnCommandResultListener
{
override
def
onCommandResult
(
commandCode
:
Int
,
exitCode
:
Int
,
output
:
util.List
[
String
])
{
if
(
exitCode
<
0
)
{
rootShell
=
null
}
else
{
import
scala.collection.JavaConversions._
output
.
foreach
(
line
=>
sb
.
append
(
line
).
append
(
'\n'
))
}
}
})
if
(
rootShell
.
waitForIdle
())
sb
.
toString
()
else
null
}
def
isRoot
:
Boolean
=
SU
.
available
()
}
src/main/scala/com/github/shadowsocks/utils/Constants.scala
View file @
185f32aa
...
@@ -42,6 +42,15 @@ package com.github.shadowsocks.utils
...
@@ -42,6 +42,15 @@ package com.github.shadowsocks.utils
import
android.content.
{
Intent
,
SharedPreferences
}
import
android.content.
{
Intent
,
SharedPreferences
}
import
com.github.shadowsocks.aidl.Config
import
com.github.shadowsocks.aidl.Config
object
Executable
{
val
REDSOCKS
=
"redsocks"
val
PDNSD
=
"pdnsd"
val
SS_LOCAL
=
"ss-local"
val
SS_TUNNEL
=
"ss-tunnel"
val
IPTABLES
=
"iptables"
val
TUN2SOCKS
=
"tun2socks"
}
object
Msg
{
object
Msg
{
val
CONNECT_FINISH
=
1
val
CONNECT_FINISH
=
1
val
CONNECT_SUCCESS
=
2
val
CONNECT_SUCCESS
=
2
...
...
src/main/scala/com/github/shadowsocks/utils/Utils.scala
View file @
185f32aa
...
@@ -134,8 +134,7 @@ object Utils {
...
@@ -134,8 +134,7 @@ object Utils {
private
def
toggleAboveApiLevel17
()
{
private
def
toggleAboveApiLevel17
()
{
// Android 4.2 and above
// Android 4.2 and above
Utils
.
runRootCommand
(
"ndc resolver flushdefaultif\n"
Console
.
runRootCommand
(
Array
(
"ndc resolver flushdefaultif"
,
"ndc resolver flushif wlan0"
))
+
"ndc resolver flushif wlan0\n"
)
//Utils.runRootCommand("settings put global airplane_mode_on 1\n"
//Utils.runRootCommand("settings put global airplane_mode_on 1\n"
// + "am broadcast -a android.intent.action.AIRPLANE_MODE --ez state true\n"
// + "am broadcast -a android.intent.action.AIRPLANE_MODE --ez state true\n"
...
@@ -207,22 +206,19 @@ object Utils {
...
@@ -207,22 +206,19 @@ object Utils {
def
resolve
(
host
:
String
,
enableIPv6
:
Boolean
)
:
Option
[
String
]
=
{
def
resolve
(
host
:
String
,
enableIPv6
:
Boolean
)
:
Option
[
String
]
=
{
if
(
enableIPv6
&&
Utils
.
isIPv6Support
)
{
if
(
enableIPv6
&&
Utils
.
isIPv6Support
)
{
resolve
(
host
,
Type
.
AAAA
)
match
{
resolve
(
host
,
Type
.
AAAA
)
match
{
case
Some
(
addr
)
=>
{
case
Some
(
addr
)
=>
return
Some
(
addr
)
return
Some
(
addr
)
}
case
None
=>
case
None
=>
}
}
}
}
resolve
(
host
,
Type
.
A
)
match
{
resolve
(
host
,
Type
.
A
)
match
{
case
Some
(
addr
)
=>
{
case
Some
(
addr
)
=>
return
Some
(
addr
)
return
Some
(
addr
)
}
case
None
=>
case
None
=>
}
}
resolve
(
host
)
match
{
resolve
(
host
)
match
{
case
Some
(
addr
)
=>
{
case
Some
(
addr
)
=>
return
Some
(
addr
)
return
Some
(
addr
)
}
case
None
=>
case
None
=>
}
}
None
None
...
@@ -248,10 +244,9 @@ object Utils {
...
@@ -248,10 +244,9 @@ object Utils {
}
}
}
}
}
catch
{
}
catch
{
case
ex
:
Exception
=>
{
case
ex
:
Exception
=>
Log
.
e
(
TAG
,
"Failed to get interfaces' addresses."
,
ex
)
Log
.
e
(
TAG
,
"Failed to get interfaces' addresses."
,
ex
)
}
}
}
None
None
}
}
...
@@ -276,10 +271,9 @@ object Utils {
...
@@ -276,10 +271,9 @@ object Utils {
}
}
}
}
}
catch
{
}
catch
{
case
ex
:
Exception
=>
{
case
ex
:
Exception
=>
Log
.
e
(
TAG
,
"Failed to get interfaces' addresses."
,
ex
)
Log
.
e
(
TAG
,
"Failed to get interfaces' addresses."
,
ex
)
}
}
}
false
false
}
}
...
@@ -289,23 +283,20 @@ object Utils {
...
@@ -289,23 +283,20 @@ object Utils {
*/
*/
def
checkIptables
()
{
def
checkIptables
()
{
if
(!
Utils
.
get
Root
)
{
if
(!
Console
.
is
Root
)
{
iptables
=
DEFAULT_IPTABLES
iptables
=
DEFAULT_IPTABLES
return
return
}
}
iptables
=
DEFAULT_IPTABLES
iptables
=
DEFAULT_IPTABLES
var
lines
:
String
=
null
var
compatible
:
Boolean
=
false
var
compatible
:
Boolean
=
false
var
version
:
Boolean
=
false
var
version
:
Boolean
=
false
val
sb
=
new
StringBuilder
val
command
=
Array
(
iptables
+
" --version"
,
iptables
+
" -L -t nat -n"
)
val
command
=
iptables
+
" --version\n"
+
iptables
+
" -L -t nat -n\n"
+
"exit\n"
val
lines
=
Console
.
runRootCommand
(
command
)
val
exitcode
=
runScript
(
command
,
sb
,
10
*
1000
,
asroot
=
true
)
if
(
lines
==
null
)
return
if
(
exitcode
==
TIME_OUT
)
return
lines
=
sb
.
toString
()
if
(
lines
.
contains
(
"OUTPUT"
))
{
if
(
lines
.
contains
(
"OUTPUT"
))
{
compatible
=
true
compatible
=
true
}
}
...
@@ -346,11 +337,10 @@ object Utils {
...
@@ -346,11 +337,10 @@ object Utils {
val
appInfo
:
ApplicationInfo
=
pm
.
getApplicationInfo
(
packages
(
0
),
0
)
val
appInfo
:
ApplicationInfo
=
pm
.
getApplicationInfo
(
packages
(
0
),
0
)
return
pm
.
getApplicationIcon
(
appInfo
)
return
pm
.
getApplicationIcon
(
appInfo
)
}
catch
{
}
catch
{
case
e
:
PackageManager.NameNotFoundException
=>
{
case
e
:
PackageManager.NameNotFoundException
=>
Log
.
e
(
c
.
getPackageName
,
"No package found matching with the uid "
+
uid
)
Log
.
e
(
c
.
getPackageName
,
"No package found matching with the uid "
+
uid
)
}
}
}
}
}
}
else
{
}
else
{
Log
.
e
(
c
.
getPackageName
,
"Package not found for uid "
+
uid
)
Log
.
e
(
c
.
getPackageName
,
"Package not found for uid "
+
uid
)
}
}
...
@@ -367,25 +357,16 @@ object Utils {
...
@@ -367,25 +357,16 @@ object Utils {
iptables
iptables
}
}
def
getShell
:
String
=
{
if
(
shell
==
null
)
{
shell
=
DEFAULT_SHELL
if
(!
new
File
(
shell
).
exists
)
shell
=
"sh"
}
shell
}
def
initHasRedirectSupported
()
{
def
initHasRedirectSupported
()
{
if
(!
Utils
.
get
Root
)
return
if
(!
Console
.
is
Root
)
return
hasRedirectSupport
=
1
hasRedirectSupport
=
1
val
sb
=
new
StringBuilder
val
sb
=
new
StringBuilder
val
command
=
Utils
.
getIptables
+
" -t nat -A OUTPUT -p udp --dport 54 -j REDIRECT --to 8154"
val
command
=
Utils
.
getIptables
+
" -t nat -A OUTPUT -p udp --dport 54 -j REDIRECT --to 8154"
val
exitcode
:
Int
=
runScript
(
command
,
sb
,
10
*
1000
,
asroot
=
true
)
val
lines
=
Console
.
runRootCommand
(
command
)
val
lines
=
sb
.
toString
()
Utils
.
runRootCommand
(
command
.
replace
(
"-A"
,
"-D"
))
Console
.
runRootCommand
(
command
.
replace
(
"-A"
,
"-D"
))
if
(
exitcode
==
TIME_OUT
)
return
if
(
lines
==
null
)
return
if
(
lines
.
contains
(
"No chain/target/match"
))
{
if
(
lines
.
contains
(
"No chain/target/match"
))
{
hasRedirectSupport
=
0
hasRedirectSupport
=
0
}
}
...
@@ -394,196 +375,11 @@ object Utils {
...
@@ -394,196 +375,11 @@ object Utils {
def
isInitialized
:
Boolean
=
{
def
isInitialized
:
Boolean
=
{
initialized
match
{
initialized
match
{
case
true
=>
true
case
true
=>
true
case
_
=>
{
case
_
=>
initialized
=
true
initialized
=
true
false
false
}
}
}
}
}
def
getRoot
:
Boolean
=
{
if
(
isRoot
!=
-
1
)
return
isRoot
==
1
if
(
new
File
(
DEFAULT_ROOT
).
exists
)
{
root_shell
=
DEFAULT_ROOT
}
else
if
(
new
File
(
ALTERNATIVE_ROOT
).
exists
)
{
root_shell
=
ALTERNATIVE_ROOT
}
else
{
root_shell
=
"su"
}
val
sb
=
new
StringBuilder
val
command
:
String
=
"id\n"
val
exitcode
:
Int
=
runScript
(
command
,
sb
,
10
*
1000
,
asroot
=
true
)
if
(
exitcode
==
TIME_OUT
)
{
return
false
}
val
lines
=
sb
.
toString
()
if
(
lines
.
contains
(
"uid=0"
))
{
isRoot
=
1
}
else
{
if
(
rootTries
>=
1
)
isRoot
=
0
rootTries
+=
1
}
isRoot
==
1
}
def
runCommand
(
command
:
String
)
:
Boolean
=
{
runCommand
(
command
,
10
*
1000
)
}
def
runCommand
(
command
:
String
,
timeout
:
Int
)
:
Boolean
=
{
if
(
BuildConfig
.
DEBUG
)
Log
.
d
(
TAG
,
command
)
runScript
(
command
,
null
,
timeout
,
asroot
=
false
)
true
}
def
runRootCommand
(
command
:
String
)
:
Boolean
=
{
runRootCommand
(
command
,
10
*
1000
)
}
def
runRootCommand
(
command
:
String
,
timeout
:
Int
)
:
Boolean
=
{
if
(!
Utils
.
getRoot
)
{
Log
.
e
(
TAG
,
"Cannot get ROOT permission: "
+
root_shell
)
return
false
}
if
(
BuildConfig
.
DEBUG
)
Log
.
d
(
TAG
,
command
)
runScript
(
command
,
null
,
timeout
,
asroot
=
true
)
true
}
def
runScript
(
script
:
String
,
result
:
StringBuilder
,
timeout
:
Long
,
asroot
:
Boolean
)
:
Int
=
{
val
runner
:
Utils.ScriptRunner
=
new
Utils
.
ScriptRunner
(
script
,
result
,
asroot
)
runner
.
start
()
try
{
if
(
timeout
>
0
)
{
runner
.
join
(
timeout
)
}
else
{
runner
.
join
()
}
if
(
runner
.
isAlive
)
{
runner
.
destroy
()
runner
.
join
(
1000
)
return
TIME_OUT
}
}
catch
{
case
ex
:
InterruptedException
=>
{
return
TIME_OUT
}
}
runner
.
exitcode
}
/**
* Internal thread used to execute scripts (as root or not).
* Creates a new script runner.
*
* @param scripts script to run
* @param result result output
* @param asroot if true, executes the script as root
*/
class
ScriptRunner
(
val
scripts
:
String
,
val
result
:
StringBuilder
,
val
asroot
:
Boolean
)
extends
Thread
{
var
exitcode
:
Int
=
-
1
val
pid
:
Array
[
Int
]
=
new
Array
[
Int
](
1
)
var
pipe
:
FileDescriptor
=
null
override
def
destroy
()
{
if
(
pid
(
0
)
!=
-
1
)
{
Exec
.
hangupProcessGroup
(
pid
(
0
))
pid
(
0
)
=
-
1
}
if
(
pipe
!=
null
)
{
Exec
.
close
(
pipe
)
pipe
=
null
}
}
def
createSubprocess
(
processId
:
Array
[
Int
],
cmd
:
String
)
:
FileDescriptor
=
{
val
args
=
parse
(
cmd
)
val
arg0
=
args
(
0
)
Exec
.
createSubprocess
(
if
(
result
!=
null
)
1
else
0
,
arg0
,
args
,
null
,
scripts
+
"\nexit\n"
,
processId
)
}
def
parse
(
cmd
:
String
)
:
Array
[
String
]
=
{
val
PLAIN
=
0
val
INQUOTE
=
2
val
SKIP
=
3
var
state
=
PLAIN
val
result
:
ArrayBuffer
[
String
]
=
new
ArrayBuffer
[
String
]
val
builder
=
new
StringBuilder
()
cmd
foreach
{
ch
=>
{
state
match
{
case
PLAIN
=>
{
ch
match
{
case
c
if
Character
.
isWhitespace
(
c
)
=>
{
result
+=
builder
.
toString
builder
.
clear
()
}
case
'"'
=>
state
=
INQUOTE
case
_
=>
builder
+=
ch
}
}
case
INQUOTE
=>
{
ch
match
{
case
'\\'
=>
state
=
SKIP
case
'"'
=>
state
=
PLAIN
case
_
=>
builder
+=
ch
}
}
case
SKIP
=>
{
builder
+=
ch
state
=
INQUOTE
}
}
}
}
if
(
builder
.
length
>
0
)
{
result
+=
builder
.
toString
}
result
.
toArray
}
override
def
run
()
{
pid
(
0
)
=
-
1
try
{
if
(
this
.
asroot
)
{
pipe
=
createSubprocess
(
pid
,
root_shell
)
}
else
{
pipe
=
createSubprocess
(
pid
,
getShell
)
}
if
(
pid
(
0
)
!=
-
1
)
{
exitcode
=
Exec
.
waitFor
(
pid
(
0
))
}
if
(
result
==
null
||
pipe
==
null
)
return
val
stdout
:
InputStream
=
new
FileInputStream
(
pipe
)
val
buf
=
new
Array
[
Byte
](
8192
)
var
read
:
Int
=
0
while
(
stdout
.
available
>
0
)
{
read
=
stdout
.
read
(
buf
)
result
.
append
(
new
String
(
buf
,
0
,
read
))
}
}
catch
{
case
ex
:
Exception
=>
{
Log
.
e
(
TAG
,
"Cannot execute command"
,
ex
)
if
(
result
!=
null
)
result
.
append
(
"\n"
).
append
(
ex
)
}
}
finally
{
if
(
pipe
!=
null
)
{
Exec
.
close
(
pipe
)
}
if
(
pid
(
0
)
!=
-
1
)
{
Exec
.
hangupProcessGroup
(
pid
(
0
))
}
}
}
}
}
}
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment