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:
Akash Yadav
2023-07-30 11:41:17 +05:30
parent c6bf1f5afd
commit 456deeb336
20 changed files with 698 additions and 79 deletions
+5 -3
View File
@@ -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();
}
}
@@ -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);
}
}
@@ -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();
}
}
@@ -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+