mirror of
https://gitee.com/zhangsken/AndroidIDE.git
synced 2026-10-16 07:49:44 +08:00
fix(logsender): log sender clients are not properly handled
This commit commit fixes a bug which caused the log sender client not being released when the client application crashes or is killed by the system. This resulted in resources being unnecessarily held by the IDE causing lags and sometimes, battery drain (#1107).
This commit is contained in:
Generated
+5
-3
@@ -180,7 +180,7 @@
|
||||
<option name="myDefaultNotNull" value="androidx.annotation.RecentlyNonNull" />
|
||||
<option name="myNullables">
|
||||
<value>
|
||||
<list size="12">
|
||||
<list size="13">
|
||||
<item index="0" class="java.lang.String" itemvalue="org.checkerframework.checker.nullness.compatqual.NullableDecl" />
|
||||
<item index="1" class="java.lang.String" itemvalue="org.jetbrains.annotations.Nullable" />
|
||||
<item index="2" class="java.lang.String" itemvalue="androidx.annotation.Nullable" />
|
||||
@@ -193,12 +193,13 @@
|
||||
<item index="9" class="java.lang.String" itemvalue="javax.annotation.Nullable" />
|
||||
<item index="10" class="java.lang.String" itemvalue="org.checkerframework.checker.nullness.qual.Nullable" />
|
||||
<item index="11" class="java.lang.String" itemvalue="org.checkerframework.checker.nullness.compatqual.NullableType" />
|
||||
<item index="12" class="java.lang.String" itemvalue="org.jspecify.nullness.Nullable" />
|
||||
</list>
|
||||
</value>
|
||||
</option>
|
||||
<option name="myNotNulls">
|
||||
<value>
|
||||
<list size="11">
|
||||
<list size="12">
|
||||
<item index="0" class="java.lang.String" itemvalue="androidx.annotation.RecentlyNonNull" />
|
||||
<item index="1" class="java.lang.String" itemvalue="org.checkerframework.checker.nullness.qual.NonNull" />
|
||||
<item index="2" class="java.lang.String" itemvalue="com.android.annotations.NonNull" />
|
||||
@@ -210,11 +211,12 @@
|
||||
<item index="8" class="java.lang.String" itemvalue="org.jetbrains.annotations.NotNull" />
|
||||
<item index="9" class="java.lang.String" itemvalue="javax.annotation.Nonnull" />
|
||||
<item index="10" class="java.lang.String" itemvalue="org.eclipse.jdt.annotation.NonNull" />
|
||||
<item index="11" class="java.lang.String" itemvalue="org.jspecify.nullness.NonNull" />
|
||||
</list>
|
||||
</value>
|
||||
</option>
|
||||
</component>
|
||||
<component name="ProjectRootManager" version="2" languageLevel="JDK_1_8" project-jdk-name="jbr-17" project-jdk-type="JavaSDK">
|
||||
<component name="ProjectRootManager" version="2" languageLevel="JDK_17" default="true" project-jdk-name="jbr-17" project-jdk-type="JavaSDK">
|
||||
<output url="file://$PROJECT_DIR$/build/classes" />
|
||||
</component>
|
||||
<component name="ProjectType">
|
||||
|
||||
@@ -635,7 +635,8 @@ abstract class BaseEditorActivity :
|
||||
|
||||
private fun startLogReceiver() {
|
||||
try {
|
||||
val intent = Intent(this, LogReceiverService::class.java).setAction(LogSender.SERVICE_ACTION)
|
||||
val intent = Intent(this, LogReceiverService::class.java)
|
||||
.setAction(LogSender.SERVICE_ACTION)
|
||||
check(bindService(intent, logServiceConnection, BIND_AUTO_CREATE or BIND_IMPORTANT))
|
||||
log.info("LogReceiver service is being started")
|
||||
} catch (err: Throwable) {
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
/*
|
||||
* This file is part of AndroidIDE.
|
||||
*
|
||||
* AndroidIDE is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* AndroidIDE is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with AndroidIDE. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.itsaky.androidide.services.log
|
||||
|
||||
import com.itsaky.androidide.logsender.ILogSender
|
||||
|
||||
/**
|
||||
* An implementation of [ILogSender] which caches the ID, PID and package name from the provided sender.
|
||||
*
|
||||
* @author Akash Yadav
|
||||
*/
|
||||
class CachingLogSender(private val sender: ILogSender) : ILogSender by sender {
|
||||
|
||||
private var cachedPid: Int = -1
|
||||
private var cachedId: String? = null
|
||||
private var cachedPckName: String? = null
|
||||
|
||||
override fun getPid(): Int {
|
||||
if (this.cachedPid != -1) {
|
||||
return this.cachedPid
|
||||
}
|
||||
return sender.pid.also { this.cachedPid = it }
|
||||
}
|
||||
|
||||
override fun getPackageName(): String {
|
||||
return this.cachedPckName ?: sender.packageName.also { this.cachedPckName = it }
|
||||
}
|
||||
|
||||
override fun getId(): String {
|
||||
return this.cachedId ?: sender.id.also { this.cachedId = it }
|
||||
}
|
||||
}
|
||||
@@ -17,6 +17,7 @@
|
||||
|
||||
package com.itsaky.androidide.services.log
|
||||
|
||||
import android.util.Log
|
||||
import com.itsaky.androidide.logsender.ILogReceiver
|
||||
import com.itsaky.androidide.logsender.ILogSender
|
||||
import com.itsaky.androidide.models.LogLine
|
||||
@@ -34,7 +35,7 @@ class LogReceiverImpl(consumer: ((LogLine) -> Unit)? = null) : ILogReceiver.Stub
|
||||
|
||||
private val log = ILogger.newInstance("LogReceiverImpl")
|
||||
private val senderHandler = MultiLogSenderHandler()
|
||||
private val senders = ConcurrentHashMap<Int, ILogSender>()
|
||||
private val senders = LogSendersRegistry()
|
||||
private val consumerLock = ReentrantLock(true)
|
||||
|
||||
internal var consumer: ((LogLine) -> Unit)? = consumer
|
||||
@@ -56,6 +57,10 @@ class LogReceiverImpl(consumer: ((LogLine) -> Unit)? = null) : ILogReceiver.Stub
|
||||
senderHandler.start()
|
||||
}
|
||||
|
||||
override fun ping() {
|
||||
Log.d("LogRecevier", "ping: Received a ping request")
|
||||
}
|
||||
|
||||
override fun connect(sender: ILogSender?) {
|
||||
val port = senderHandler.getPort()
|
||||
if (port == -1) {
|
||||
@@ -63,26 +68,58 @@ class LogReceiverImpl(consumer: ((LogLine) -> Unit)? = null) : ILogReceiver.Stub
|
||||
return
|
||||
}
|
||||
|
||||
sender?.let {
|
||||
if (senders.containsKey(it.pid)) {
|
||||
log.warn("Rejecting duplicate connection request from client '${it.pid}'")
|
||||
return
|
||||
sender?.let { newSender ->
|
||||
|
||||
val existingSender = senders.getByPackage(newSender.packageName)
|
||||
|
||||
if (existingSender != null) {
|
||||
senderHandler.removeClient(existingSender.id)
|
||||
}
|
||||
|
||||
log.info("Connecting to client ${it.pid}")
|
||||
if (existingSender?.isAlive() == true) {
|
||||
log.warn(
|
||||
"Client '${existingSender.packageName}' has been restarted with process ID '${newSender.pid}'" +
|
||||
" Previous connection with process ID '${existingSender.pid}' will be closed...")
|
||||
existingSender.onDisconnect()
|
||||
}
|
||||
|
||||
this.senders[it.pid] = it
|
||||
log.info("Connecting to client ${newSender.packageName}")
|
||||
|
||||
it.startReader(port)
|
||||
this.senders.put(CachingLogSender(newSender))
|
||||
|
||||
log.info("Total clients connected: ${senders.size}")
|
||||
newSender.startReader(port)
|
||||
|
||||
logTotalConnected()
|
||||
}
|
||||
}
|
||||
|
||||
override fun disconnect(packageName: String, senderId: String) {
|
||||
val port = senderHandler.getPort()
|
||||
if (port == -1) {
|
||||
return
|
||||
}
|
||||
|
||||
if (!senders.containsKey(packageName)) {
|
||||
log.warn(
|
||||
"Received disconnect request from a log sender which is not connected: '${packageName}'")
|
||||
return
|
||||
}
|
||||
|
||||
log.info("Disconnecting from client: '${packageName}'")
|
||||
this.senderHandler.removeClient(senderId)
|
||||
this.senders.remove(packageName)
|
||||
|
||||
logTotalConnected()
|
||||
}
|
||||
|
||||
override fun close() {
|
||||
// TODO : Send close request to clients
|
||||
senderHandler.close()
|
||||
consumer = null
|
||||
senders.clear()
|
||||
}
|
||||
}
|
||||
|
||||
private fun logTotalConnected() {
|
||||
log.info("Total clients connected: ${senders.size}")
|
||||
}
|
||||
}
|
||||
@@ -23,6 +23,7 @@ import android.os.IBinder
|
||||
import com.itsaky.androidide.logsender.LogSender
|
||||
import com.itsaky.androidide.lookup.Lookup
|
||||
import com.itsaky.androidide.models.LogLine
|
||||
import com.itsaky.androidide.utils.ILogger
|
||||
import java.util.concurrent.atomic.AtomicBoolean
|
||||
|
||||
/**
|
||||
@@ -35,8 +36,12 @@ class LogReceiverService : Service() {
|
||||
private val binder = LogReceiverImpl()
|
||||
private val started = AtomicBoolean(false)
|
||||
|
||||
private val log = ILogger.newInstance("LogReceiverService")
|
||||
|
||||
companion object {
|
||||
@JvmStatic internal val LOOKUP_KEY = Lookup.Key<LogReceiverService>()
|
||||
|
||||
@JvmStatic
|
||||
internal val LOOKUP_KEY = Lookup.Key<LogReceiverService>()
|
||||
}
|
||||
|
||||
override fun onCreate() {
|
||||
@@ -58,6 +63,7 @@ class LogReceiverService : Service() {
|
||||
|
||||
override fun onDestroy() {
|
||||
super.onDestroy()
|
||||
log.debug("LogReceiverService is being destroyed...");
|
||||
binder.close()
|
||||
Lookup.getDefault().unregister(LOOKUP_KEY)
|
||||
}
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
|
||||
package com.itsaky.androidide.services.log
|
||||
|
||||
import com.itsaky.androidide.logsender.socket.SenderInfoCommand
|
||||
import com.itsaky.androidide.models.LogLine
|
||||
import com.itsaky.androidide.utils.ILogger
|
||||
import java.net.Socket
|
||||
@@ -27,9 +28,11 @@ import java.net.SocketException
|
||||
*
|
||||
* @author Akash Yadav
|
||||
*/
|
||||
class LogSenderHandler(private val socket: Socket,
|
||||
internal var consumer: ((LogLine) -> Unit)? = null,
|
||||
internal var onClose: (LogSenderHandler) -> Unit = {}
|
||||
class LogSenderHandler(
|
||||
private val sender: SenderInfoCommand,
|
||||
private val socket: Socket,
|
||||
internal var consumer: ((LogLine) -> Unit)? = null,
|
||||
internal var onClose: (String) -> Unit = {}
|
||||
) : Thread("LogSenderHandler"), AutoCloseable {
|
||||
|
||||
private val log = ILogger.newInstance("LogSenderHandler")
|
||||
@@ -38,11 +41,15 @@ class LogSenderHandler(private val socket: Socket,
|
||||
try {
|
||||
socket.getInputStream().bufferedReader().use { reader ->
|
||||
while (!socket.isClosed) {
|
||||
LogLine.forLogString(reader.readLine())?.let { line -> consumer?.invoke(line) }
|
||||
try {
|
||||
LogLine.forLogString(reader.readLine())?.let { line -> consumer?.invoke(line) }
|
||||
} catch (interrupt: InterruptedException) {
|
||||
currentThread().interrupt()
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (err: SocketException) {
|
||||
// ignored
|
||||
log.error("An error occurred while reading from socket", err)
|
||||
} finally {
|
||||
close()
|
||||
}
|
||||
@@ -51,12 +58,13 @@ class LogSenderHandler(private val socket: Socket,
|
||||
override fun close() {
|
||||
try {
|
||||
if (!socket.isClosed) {
|
||||
log.debug("Closing log sender handler...")
|
||||
socket.close()
|
||||
}
|
||||
} catch (err: Throwable) {
|
||||
log.error("Failed to close socket", err)
|
||||
} finally {
|
||||
onClose(this)
|
||||
onClose(this.sender.senderId)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,73 @@
|
||||
/*
|
||||
* This file is part of AndroidIDE.
|
||||
*
|
||||
* AndroidIDE is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* AndroidIDE is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with AndroidIDE. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.itsaky.androidide.services.log
|
||||
|
||||
import com.itsaky.androidide.logsender.ILogSender
|
||||
import java.util.Collections.synchronizedMap
|
||||
|
||||
/**
|
||||
* @author Akash Yadav
|
||||
*/
|
||||
internal class LogSendersRegistry {
|
||||
|
||||
private val senders = synchronizedMap(HashMap<String, ILogSender>())
|
||||
private val idToPck = synchronizedMap(HashMap<String, String>())
|
||||
|
||||
val size: Int
|
||||
get() = this.senders.size
|
||||
|
||||
fun getByPackage(packageName: String): ILogSender? {
|
||||
return this.senders[packageName]
|
||||
}
|
||||
|
||||
fun containsKey(packageOrId: String): Boolean {
|
||||
if (senders.containsKey(packageOrId)) {
|
||||
return true
|
||||
}
|
||||
|
||||
return getById(packageOrId) != null
|
||||
}
|
||||
|
||||
fun getById(senderId: String): ILogSender? {
|
||||
return this.idToPck[senderId]?.let(senders::get)
|
||||
}
|
||||
|
||||
fun put(sender: ILogSender) {
|
||||
this.senders[sender.packageName] = sender
|
||||
this.idToPck[sender.id] = sender.packageName
|
||||
}
|
||||
|
||||
fun remove(packageName: String) {
|
||||
this.senders.remove(packageName)
|
||||
|
||||
val iterator = this.idToPck.iterator()
|
||||
|
||||
synchronized(this.idToPck) {
|
||||
for (entry in iterator) {
|
||||
if (entry.value == packageName) {
|
||||
iterator.remove()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun clear() {
|
||||
this.senders.clear()
|
||||
this.idToPck.clear()
|
||||
}
|
||||
}
|
||||
@@ -17,13 +17,14 @@
|
||||
|
||||
package com.itsaky.androidide.services.log
|
||||
|
||||
import com.itsaky.androidide.logsender.socket.SenderInfoCommand
|
||||
import com.itsaky.androidide.logsender.socket.SocketCommandParser
|
||||
import com.itsaky.androidide.models.LogLine
|
||||
import com.itsaky.androidide.utils.ILogger
|
||||
import java.net.ServerSocket
|
||||
import java.util.Collections
|
||||
import java.util.concurrent.atomic.AtomicBoolean
|
||||
import java.util.concurrent.atomic.AtomicInteger
|
||||
import java.util.concurrent.locks.ReentrantLock
|
||||
import kotlin.concurrent.withLock
|
||||
|
||||
/**
|
||||
* Handles clients that connect to the log receiver.
|
||||
@@ -34,17 +35,14 @@ class MultiLogSenderHandler(consumer: ((LogLine) -> Unit)? = null) :
|
||||
Thread("MultiLogSenderHandler"), AutoCloseable {
|
||||
|
||||
private val log = ILogger.newInstance("MultiLogSenderHandler")
|
||||
private val lock = ReentrantLock(true)
|
||||
private val clients = mutableListOf<LogSenderHandler>()
|
||||
private val clients = Collections.synchronizedMap(HashMap<String, LogSenderHandler>())
|
||||
private val port = AtomicInteger(-1)
|
||||
private var keepAlive = AtomicBoolean(false)
|
||||
|
||||
internal var consumer: ((LogLine) -> Unit)? = consumer
|
||||
set(value) {
|
||||
field = value
|
||||
lock.withLock {
|
||||
clients.forEach { it?.consumer = value }
|
||||
}
|
||||
clients.forEach { (_, client) -> client.consumer = value }
|
||||
}
|
||||
|
||||
fun getPort(): Int {
|
||||
@@ -52,36 +50,49 @@ class MultiLogSenderHandler(consumer: ((LogLine) -> Unit)? = null) :
|
||||
}
|
||||
|
||||
override fun run() {
|
||||
val server = try {
|
||||
ServerSocket(0)
|
||||
} catch (err: Exception) {
|
||||
log.error("Failed to start log receiver socket", err)
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
ServerSocket(0).use {
|
||||
port.set(it.localPort)
|
||||
log.info("Starting log receiver server socket at port ${getPort()}")
|
||||
port.set(server.localPort)
|
||||
log.info("Starting log receiver server socket at port ${getPort()}")
|
||||
|
||||
while (keepAlive.get()) {
|
||||
val handler = LogSenderHandler(it.accept(), consumer, ::removeClient)
|
||||
while (!currentThread().isInterrupted && keepAlive.get()) {
|
||||
val clientSocket = server.accept()
|
||||
|
||||
log.info("A log sender has been connected")
|
||||
|
||||
lock.withLock {
|
||||
clients.add(handler)
|
||||
}
|
||||
|
||||
handler.start()
|
||||
val senderInfoLine = clientSocket.getInputStream().bufferedReader().readLine()
|
||||
val command = SocketCommandParser.parse(senderInfoLine)
|
||||
if (command == null || command !is SenderInfoCommand) {
|
||||
log.error("Cannot accept log sender client. A sender must send the /sender command first.")
|
||||
clientSocket.use {}
|
||||
continue
|
||||
}
|
||||
|
||||
val handler = LogSenderHandler(command, clientSocket, consumer, ::removeClient)
|
||||
|
||||
log.info("A log sender has been connected")
|
||||
|
||||
clients[command.senderId] = handler
|
||||
|
||||
handler.start()
|
||||
}
|
||||
} catch (interrupt: InterruptedException) {
|
||||
log.warn("MultiLogSenderHandler thread has been interrupted")
|
||||
currentThread().interrupt()
|
||||
} catch (err: Throwable) {
|
||||
if (err !is InterruptedException) {
|
||||
log.error("Failed to start log receiver socket", err)
|
||||
}
|
||||
log.error("An error occurred while accept log client connections", err)
|
||||
} finally {
|
||||
this.close()
|
||||
server.close()
|
||||
}
|
||||
}
|
||||
|
||||
private fun removeClient(handler: LogSenderHandler) {
|
||||
lock.withLock {
|
||||
clients.remove(handler)
|
||||
}
|
||||
internal fun removeClient(senderId: String) {
|
||||
clients.remove(senderId)?.closeAndLogError()
|
||||
}
|
||||
|
||||
override fun start() {
|
||||
@@ -91,12 +102,10 @@ class MultiLogSenderHandler(consumer: ((LogLine) -> Unit)? = null) :
|
||||
|
||||
override fun close() {
|
||||
this.keepAlive.set(false)
|
||||
lock.withLock {
|
||||
clients.forEach { it?.closeAndLogError() }
|
||||
}
|
||||
clients.forEach { (_, client) -> client?.closeAndLogError() }
|
||||
}
|
||||
|
||||
private fun AutoCloseable.closeAndLogError() {
|
||||
private fun LogSenderHandler.closeAndLogError() {
|
||||
try {
|
||||
close()
|
||||
} catch (e: Exception) {
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
/*
|
||||
* This file is part of AndroidIDE.
|
||||
*
|
||||
* AndroidIDE is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* AndroidIDE is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with AndroidIDE. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.itsaky.androidide.services.log
|
||||
|
||||
import android.os.RemoteException
|
||||
import com.itsaky.androidide.logsender.ILogSender
|
||||
|
||||
fun ILogSender.isAlive() : Boolean {
|
||||
return try {
|
||||
ping()
|
||||
true
|
||||
} catch (err: RemoteException) {
|
||||
false
|
||||
}
|
||||
}
|
||||
@@ -7,8 +7,18 @@ import com.itsaky.androidide.logsender.ILogSender;
|
||||
*/
|
||||
oneway interface ILogReceiver {
|
||||
|
||||
/**
|
||||
* Called by the log sender to check if the receiver is alive.
|
||||
*/
|
||||
void ping();
|
||||
|
||||
/**
|
||||
* Called by the client applications to connect to log receiver in AndroidIDE.
|
||||
*/
|
||||
void connect(ILogSender sender);
|
||||
|
||||
/**
|
||||
* Called by the client applications to disconnect from the log receiver in AndroidIDE.
|
||||
*/
|
||||
void disconnect(String packageName, String senderId);
|
||||
}
|
||||
@@ -5,6 +5,11 @@ package com.itsaky.androidide.logsender;
|
||||
*/
|
||||
interface ILogSender {
|
||||
|
||||
/**
|
||||
* Called by the log receiver to check if the sender is alive.
|
||||
*/
|
||||
void ping();
|
||||
|
||||
/**
|
||||
* Asks the sender to start reading the logs.
|
||||
*/
|
||||
@@ -19,4 +24,14 @@ interface ILogSender {
|
||||
* Get the package name of the sender.
|
||||
*/
|
||||
String getPackageName();
|
||||
|
||||
/**
|
||||
* Get the unique ID for this log sender.
|
||||
*/
|
||||
String getId();
|
||||
|
||||
/**
|
||||
* Called by the IDE to notify a client that the log receiver has been disconnected.
|
||||
*/
|
||||
void onDisconnect();
|
||||
}
|
||||
@@ -27,6 +27,7 @@ import android.os.RemoteException;
|
||||
import android.util.Log;
|
||||
import com.itsaky.androidide.logsender.utils.LogReader;
|
||||
import com.itsaky.androidide.logsender.utils.Logger;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
|
||||
/**
|
||||
@@ -39,14 +40,16 @@ public final class LogSender extends ILogSender.Stub implements ServiceConnectio
|
||||
private final AtomicBoolean isBinding = new AtomicBoolean(false);
|
||||
private final AtomicBoolean isConnected = new AtomicBoolean(false);
|
||||
|
||||
private final String senderId;
|
||||
|
||||
private Context context;
|
||||
|
||||
private LogReader reader;
|
||||
|
||||
private ILogReceiver receiver;
|
||||
|
||||
private String packageName;
|
||||
|
||||
public static final LogSender INSTANCE = new LogSender();
|
||||
|
||||
/**
|
||||
* Action for the LogSender service.
|
||||
*/
|
||||
@@ -62,7 +65,8 @@ public final class LogSender extends ILogSender.Stub implements ServiceConnectio
|
||||
*/
|
||||
public static final String PACKAGE_ANDROIDIDE = "com.itsaky.androidide";
|
||||
|
||||
private LogSender() {
|
||||
LogSender() {
|
||||
this.senderId = UUID.randomUUID().toString();
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -84,14 +88,16 @@ public final class LogSender extends ILogSender.Stub implements ServiceConnectio
|
||||
|
||||
@Override
|
||||
public void onServiceDisconnected(ComponentName name) {
|
||||
Logger.info("Disconnected from log receiver");
|
||||
if (this.reader != null) {
|
||||
this.reader.interrupt();
|
||||
tryDisconnect(true);
|
||||
if (this.context != null) {
|
||||
tryUnbind(this.context);
|
||||
this.context = null;
|
||||
}
|
||||
this.receiver = null;
|
||||
this.reader = null;
|
||||
this.isBinding.set(false);
|
||||
this.isConnected.set(false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void ping() {
|
||||
Log.d("LogSender", "ping: Received a ping request");
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -101,7 +107,7 @@ public final class LogSender extends ILogSender.Stub implements ServiceConnectio
|
||||
return;
|
||||
}
|
||||
|
||||
reader = new LogReader(port);
|
||||
reader = new LogReader(getId(), getPackageName(), port);
|
||||
reader.start();
|
||||
}
|
||||
|
||||
@@ -119,6 +125,23 @@ public final class LogSender extends ILogSender.Stub implements ServiceConnectio
|
||||
return this.packageName;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getId() {
|
||||
return this.senderId;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDisconnect() {
|
||||
tryDisconnect(false);
|
||||
tryUnbind(this.context);
|
||||
try {
|
||||
this.context.stopService(new Intent(this.context, LogSenderService.class));
|
||||
} catch (Exception err) {
|
||||
Logger.error("Failed to stop LogSenderService", err);
|
||||
}
|
||||
this.context = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Whether the log sender is bound to the log receiver service in AndroidIDE.
|
||||
*/
|
||||
@@ -157,6 +180,8 @@ public final class LogSender extends ILogSender.Stub implements ServiceConnectio
|
||||
return false;
|
||||
}
|
||||
|
||||
this.context = context;
|
||||
|
||||
final Intent intent = new Intent(SERVICE_ACTION);
|
||||
intent.setPackage(PACKAGE_ANDROIDIDE);
|
||||
isBinding.set(context.bindService(intent, this, 0));
|
||||
@@ -171,16 +196,54 @@ public final class LogSender extends ILogSender.Stub implements ServiceConnectio
|
||||
}
|
||||
|
||||
/**
|
||||
* Unbinds from the log receiver service.
|
||||
* Disconnects from the log receiver and unbinds from the log receiver service.
|
||||
*
|
||||
* @param context The context used to unbind from the service.
|
||||
*/
|
||||
void unbind(Context context) {
|
||||
void destroy(Context context) {
|
||||
tryDisconnect(true);
|
||||
tryUnbind(context);
|
||||
this.context = null;
|
||||
}
|
||||
|
||||
private void tryDisconnect(boolean notifyRecevier) {
|
||||
Logger.info("Disconnecting from log receiver...");
|
||||
if (this.reader != null) {
|
||||
this.reader.cancel();
|
||||
}
|
||||
|
||||
if (notifyRecevier && isReceiverAlive(receiver)) {
|
||||
try {
|
||||
receiver.disconnect(getPackageName(), getId());
|
||||
} catch (Exception err) {
|
||||
Logger.error("Failed to disconnect from log receiver service", err);
|
||||
}
|
||||
}
|
||||
|
||||
this.receiver = null;
|
||||
this.reader = null;
|
||||
this.isBinding.set(false);
|
||||
this.isConnected.set(false);
|
||||
}
|
||||
|
||||
private void tryUnbind(Context context) {
|
||||
try {
|
||||
context.unbindService(this);
|
||||
} catch (Exception err) {
|
||||
Logger.error("Failed to unbind from the the log receiver service", err);
|
||||
}
|
||||
isBinding.set(false);
|
||||
}
|
||||
}
|
||||
|
||||
private boolean isReceiverAlive(ILogReceiver receiver) {
|
||||
if (receiver == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
receiver.ping();
|
||||
return true;
|
||||
} catch (RemoteException err) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -31,7 +31,7 @@ import com.itsaky.androidide.logsender.utils.Logger;
|
||||
*/
|
||||
public class LogSenderService extends Service {
|
||||
|
||||
private final LogSender logSender = LogSender.INSTANCE;
|
||||
private final LogSender logSender = new LogSender();
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
@@ -52,13 +52,29 @@ public class LogSenderService extends Service {
|
||||
Toast.makeText(this, getString(R.string.msg_bind_service_failed), Toast.LENGTH_SHORT).show();
|
||||
stopSelf();
|
||||
}
|
||||
|
||||
|
||||
return START_NOT_STICKY;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onTaskRemoved(Intent rootIntent) {
|
||||
if (!logSender.isConnected() && !logSender.isBinding()) {
|
||||
return;
|
||||
}
|
||||
|
||||
Logger.warn("Task removed. Destroying log sender...");
|
||||
logSender.destroy(getApplicationContext());
|
||||
stopSelf();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDestroy() {
|
||||
logSender.unbind(getApplicationContext());
|
||||
if (!logSender.isConnected() && !logSender.isBinding()) {
|
||||
return;
|
||||
}
|
||||
|
||||
Logger.warn("Service is being destroyed. Destroying log sender...");
|
||||
logSender.destroy(getApplicationContext());
|
||||
super.onDestroy();
|
||||
}
|
||||
}
|
||||
|
||||
+55
@@ -0,0 +1,55 @@
|
||||
/*
|
||||
* This file is part of AndroidIDE.
|
||||
*
|
||||
* AndroidIDE is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* AndroidIDE is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with AndroidIDE. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.itsaky.androidide.logsender.socket;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
/**
|
||||
* Base class for socket commands.
|
||||
*
|
||||
* @author Akash Yadav
|
||||
*/
|
||||
public abstract class AbstractSocketCommand implements ISocketCommand {
|
||||
|
||||
@Nullable
|
||||
protected String[] getParams() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public String toString() {
|
||||
final StringBuilder sb = new StringBuilder("/");
|
||||
sb.append(getName());
|
||||
|
||||
final String[] params = getParams();
|
||||
if (params != null && params.length > 0) {
|
||||
sb.append(PARAM_DELIMITER);
|
||||
for (int i = 0; i < params.length; i++) {
|
||||
final String param = params[i];
|
||||
sb.append(param);
|
||||
if (i < params.length - 1) {
|
||||
sb.append(PARAM_DELIMITER);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return sb.toString();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
/*
|
||||
* This file is part of AndroidIDE.
|
||||
*
|
||||
* AndroidIDE is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* AndroidIDE is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with AndroidIDE. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.itsaky.androidide.logsender.socket;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
import java.io.File;
|
||||
|
||||
/**
|
||||
* A command that is sent over the socket streams between the sender and the receiver.
|
||||
*
|
||||
* @author Akash Yadav
|
||||
*/
|
||||
public interface ISocketCommand {
|
||||
|
||||
String PARAM_DELIMITER = File.pathSeparator;
|
||||
|
||||
/**
|
||||
* Get the name of the command.
|
||||
*
|
||||
* @return The name of the command.
|
||||
*/
|
||||
String getName();
|
||||
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
/*
|
||||
* This file is part of AndroidIDE.
|
||||
*
|
||||
* AndroidIDE is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* AndroidIDE is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with AndroidIDE. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.itsaky.androidide.logsender.socket;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
/**
|
||||
* Command containing information about the log sender.
|
||||
*
|
||||
* @author Akash Yadav
|
||||
*/
|
||||
public class SenderInfoCommand extends AbstractSocketCommand {
|
||||
|
||||
public static final String NAME = "sender";
|
||||
|
||||
public final String senderId, packageName;
|
||||
|
||||
public SenderInfoCommand(String senderId, String packageName) {
|
||||
this.senderId = senderId;
|
||||
this.packageName = packageName;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return NAME;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
protected String[] getParams() {
|
||||
return new String[]{this.senderId, this.packageName};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
/*
|
||||
* This file is part of AndroidIDE.
|
||||
*
|
||||
* AndroidIDE is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* AndroidIDE is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with AndroidIDE. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.itsaky.androidide.logsender.socket;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
* A command that is used to send a signal to the sender/receiver.
|
||||
*
|
||||
* @author Akash Yadav
|
||||
*/
|
||||
public final class SignalCommand extends AbstractSocketCommand {
|
||||
|
||||
public static final ISocketCommand STOP = new SignalCommand("stop");
|
||||
private final String name;
|
||||
|
||||
public SignalCommand(String name) {
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) {
|
||||
return true;
|
||||
}
|
||||
if (!(o instanceof SignalCommand)) {
|
||||
return false;
|
||||
}
|
||||
SignalCommand that = (SignalCommand) o;
|
||||
return Objects.equals(name, that.name);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Objects.hash(name);
|
||||
}
|
||||
}
|
||||
+69
@@ -0,0 +1,69 @@
|
||||
/*
|
||||
* This file is part of AndroidIDE.
|
||||
*
|
||||
* AndroidIDE is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* AndroidIDE is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with AndroidIDE. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.itsaky.androidide.logsender.socket;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
* @author Akash Yadav
|
||||
*/
|
||||
public final class SocketCommandParser {
|
||||
|
||||
@Nullable
|
||||
public static ISocketCommand parse(String line) {
|
||||
// remove leading '/'
|
||||
line = line.substring(1);
|
||||
if (!line.contains(ISocketCommand.PARAM_DELIMITER)) {
|
||||
return create(line);
|
||||
}
|
||||
|
||||
final String[] segments = line.split(ISocketCommand.PARAM_DELIMITER);
|
||||
if (segments.length >= 2) {
|
||||
return createParameterized(segments);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
private static ISocketCommand create(String name) {
|
||||
if (name.equals(SignalCommand.STOP.getName())) {
|
||||
return SignalCommand.STOP;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
private static ISocketCommand createParameterized(String[] segments) {
|
||||
final String command = segments[0];
|
||||
if (SenderInfoCommand.NAME.equals(command)) {
|
||||
try {
|
||||
// validate the sender ID
|
||||
// noinspection ResultOfMethodCallIgnored
|
||||
UUID.fromString(segments[1]);
|
||||
} catch (Exception e) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return new SenderInfoCommand(segments[1], segments[2]);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -17,10 +17,15 @@
|
||||
|
||||
package com.itsaky.androidide.logsender.utils;
|
||||
|
||||
import com.itsaky.androidide.logsender.socket.ISocketCommand;
|
||||
import com.itsaky.androidide.logsender.socket.SenderInfoCommand;
|
||||
import java.io.BufferedReader;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStreamReader;
|
||||
import java.io.OutputStream;
|
||||
import java.net.InetAddress;
|
||||
import java.net.Socket;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
|
||||
/**
|
||||
* Reads application logs with `logcat`.
|
||||
@@ -29,22 +34,27 @@ import java.net.Socket;
|
||||
*/
|
||||
public class LogReader extends Thread {
|
||||
|
||||
private final String senderId;
|
||||
private final String packageName;
|
||||
private final int port;
|
||||
private final ProcessBuilder processBuilder;
|
||||
private final AtomicBoolean isInterrupted = new AtomicBoolean(false);
|
||||
|
||||
public LogReader(int port) {
|
||||
this(port, defaultCmd());
|
||||
public LogReader(String senderId, String packageName, int port) {
|
||||
this(senderId, packageName, port, defaultCmd());
|
||||
}
|
||||
|
||||
public LogReader(int port, String[] cmd) {
|
||||
public LogReader(String senderId, String packageName, int port, String[] cmd) {
|
||||
super("AndroidIDE-LogReader");
|
||||
this.senderId = senderId;
|
||||
this.packageName = packageName;
|
||||
this.port = port;
|
||||
|
||||
this.processBuilder = new ProcessBuilder(cmd);
|
||||
this.processBuilder.redirectErrorStream(true);
|
||||
}
|
||||
|
||||
public static String[] defaultCmd() {
|
||||
private static String[] defaultCmd() {
|
||||
return new String[]{"logcat", "-v", "threadtime"};
|
||||
}
|
||||
|
||||
@@ -53,15 +63,37 @@ public class LogReader extends Thread {
|
||||
Logger.info("Starting to read logs...");
|
||||
try (final Socket socket = new Socket(InetAddress.getLocalHost(), port)) {
|
||||
final Process process = processBuilder.start();
|
||||
try (final BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream()))) {
|
||||
|
||||
try (final BufferedReader reader = new BufferedReader(
|
||||
new InputStreamReader(process.getInputStream()))) {
|
||||
|
||||
final OutputStream outputStream = socket.getOutputStream();
|
||||
|
||||
// Send the sender info
|
||||
writeCommand(new SenderInfoCommand(this.senderId, this.packageName), outputStream);
|
||||
|
||||
String line;
|
||||
while ((line = reader.readLine()) != null) {
|
||||
while (!isInterrupted.get() && (line = reader.readLine()) != null) {
|
||||
line += "\n";
|
||||
socket.getOutputStream().write(line.getBytes());
|
||||
outputStream.write(line.getBytes());
|
||||
}
|
||||
|
||||
} catch (IOException ioError) {
|
||||
Logger.error("Error reading from the logcat process or writing to the socket", ioError);
|
||||
} finally {
|
||||
socket.close();
|
||||
}
|
||||
} catch (Throwable err) {
|
||||
Logger.error("An error occured while reading logs", err);
|
||||
} catch (IOException ioError) {
|
||||
Logger.error("Error creating the socket or starting the process", ioError);
|
||||
}
|
||||
}
|
||||
|
||||
private void writeCommand(ISocketCommand command, OutputStream outputStream) throws IOException {
|
||||
outputStream.write(command.toString().getBytes());
|
||||
}
|
||||
|
||||
public void cancel() {
|
||||
this.isInterrupted.set(true);
|
||||
this.interrupt();
|
||||
}
|
||||
}
|
||||
+3
-1
@@ -51,8 +51,10 @@ public class LogSenderInstaller extends ContentProvider {
|
||||
// do not send logs to self
|
||||
return true;
|
||||
}
|
||||
|
||||
try {
|
||||
application.startService(new Intent(application, LogSenderService.class));
|
||||
final Intent intent = new Intent(application, LogSenderService.class);
|
||||
application.startService(intent);
|
||||
} catch (Exception e) {
|
||||
|
||||
// starting a background service is not allowed on Android 12+
|
||||
|
||||
Reference in New Issue
Block a user