// Messages sent from the transport using dt_fd_forward::kListenStartMessage; using dt_fd_forward::kListenEndMessage; using dt_fd_forward::kAcceptMessage; using dt_fd_forward::kCloseMessage; using dt_fd_forward::kHandshakeCompleteMessage;
// Messages sent to the transport using dt_fd_forward::kPerformHandshakeMessage; using dt_fd_forward::kSkipHandshakeMessage;
// Begin running the debugger. void AdbConnectionDebuggerController::StartDebugger() { // The debugger thread is started for a debuggable or profileable-from-shell process. // The pid will be send to adbd for adb's "track-jdwp" and "track-app" services. // The thread will also set up the jdwp tunnel if the process is debuggable. if (IsDebuggableOrProfilable()) {
connection_->StartDebuggerThreads();
} else {
LOG(VERBOSE) << "Not starting debugger since process cannot load the jdwp agent.";
}
}
// The debugger should have already shut down since the runtime is ending. As far // as the agent is concerned shutdown already happened when we went to kDeath // state. We need to clean up our threads still though and this is a good time // to do it since the runtime is still able to handle all the normal state // transitions. void AdbConnectionDebuggerController::StopDebugger() { // Stop our threads.
gState->StopDebuggerThreads(); // Wait for our threads to actually return and cleanup the pthread. if (gPthread.has_value()) { void* ret_unused; if (TEMP_FAILURE_RETRY(pthread_join(gPthread.value(), &ret_unused)) != 0) {
PLOG(ERROR) << "Failed to join debugger threads!";
}
gPthread.reset();
}
}
staticvoid* CallbackFunction(void* vdata) {
std::unique_ptr<CallbackData> data(reinterpret_cast<CallbackData*>(vdata));
art::Thread* self = art::Thread::Attach(kAdbConnectionThreadName, true, data->thr_);
CHECK(self != nullptr) << "threads_being_born_ should have ensured thread could be attached."; // The name in Attach() is only for logging. Set the thread name. This is important so // that the thread is no longer seen as starting up.
{
art::ScopedObjectAccess soa(self);
self->SetThreadName(kAdbConnectionThreadName);
}
// Release the peer.
JNIEnv* env = self->GetJniEnv();
env->DeleteGlobalRef(data->thr_);
data->thr_ = nullptr;
{ // The StartThreadBirth was called in the parent thread. We let the runtime know we are up // before going into the provided code.
art::MutexLock mu(self, *art::Locks::runtime_shutdown_lock_);
art::Runtime::Current()->EndThreadBirth();
}
data->this_->RunPollLoop(self); int detach_result = art::Runtime::Current()->GetJavaVM()->DetachCurrentThread();
CHECK_EQ(detach_result, 0);
return nullptr;
}
void AdbConnectionState::StartDebuggerThreads() { // First do all the final setup we need.
CHECK_EQ(adb_write_event_fd_.get(), -1);
CHECK_EQ(sleep_event_fd_.get(), -1);
CHECK_EQ(local_agent_control_sock_.get(), -1);
CHECK_EQ(remote_agent_control_sock_.get(), -1);
sleep_event_fd_.reset(eventfd(kEventfdLocked, EFD_CLOEXEC));
CHECK_NE(sleep_event_fd_.get(), -1) << "Unable to create wakeup eventfd.";
adb_write_event_fd_.reset(eventfd(kEventfdUnlocked, EFD_CLOEXEC));
CHECK_NE(adb_write_event_fd_.get(), -1) << "Unable to create write-lock eventfd.";
{
art::ScopedObjectAccess soa(art::Thread::Current());
art::Runtime::Current()->GetRuntimeCallbacks()->AddDdmCallback(&ddm_callback_);
art::Runtime::Current()->GetRuntimeCallbacks()->AddAppInfoCallback(&appinfo_callback_);
} // Setup the socketpair we use to talk to the agent. bool has_sockets; do {
has_sockets = android::base::Socketpair(AF_UNIX,
SOCK_SEQPACKET | SOCK_CLOEXEC, 0,
&local_agent_control_sock_,
&remote_agent_control_sock_);
} while (!has_sockets && errno == EINTR); if (!has_sockets) {
PLOG(FATAL) << "Unable to create socketpair for agent control!";
}
// Next start the threads.
art::Thread* self = art::Thread::Current();
art::ScopedObjectAccess soa(self);
{
art::Runtime* runtime = art::Runtime::Current();
art::MutexLock mu(self, *art::Locks::runtime_shutdown_lock_); if (runtime->IsShuttingDownLocked()) { // The runtime is shutting down so we cannot create new threads. This shouldn't really happen.
LOG(ERROR) << "The runtime is shutting down when we are trying to start up the debugger!"; return;
}
runtime->StartThreadBirth();
}
jobject thr = soa.Env()->GetVm()->AddGlobalRef(self, CreateAdbConnectionThread(soa.Self())); if (thr == nullptr) {
LOG(ERROR) << "Failed to create debugger thread!"; return;
} // Note: Using pthreads instead of std::thread to not abort when the thread cannot be // created (exception support required).
std::unique_ptr<CallbackData> data(new CallbackData{this, thr});
started_debugger_threads_ = true;
gPthread.emplace(); int pthread_create_result =
pthread_create(&gPthread.value(), nullptr, &CallbackFunction, data.get()); if (pthread_create_result != 0) {
gPthread.reset();
started_debugger_threads_ = false; // If the create succeeded the other thread will call EndThreadBirth.
art::Runtime* runtime = art::Runtime::Current();
soa.Env()->DeleteGlobalRef(thr);
LOG(ERROR) << "Failed to create thread for adb-jdwp connection manager!";
art::MutexLock mu(art::Thread::Current(), *art::Locks::runtime_shutdown_lock_);
runtime->EndThreadBirth(); return;
}
data.release(); // NOLINT pthreads API.
}
void AdbConnectionState::CloseFds() {
{ // Lock the write_event_fd so that concurrent PublishDdms will see that the connection is // closed.
ScopedEventFdLock lk(adb_write_event_fd_); // shutdown(adb_connection_socket_, SHUT_RDWR);
adb_connection_socket_.reset();
}
// If we didn't load anything we will need to do the handshake again.
performed_handshake_ = false;
// If the agent isn't loaded we might need to tell ddms code the connection is closed. if (!agent_loaded_ && notified_ddm_active_) {
NotifyDdms(/*active=*/false);
}
}
void AdbConnectionState::SendDdmPacket(uint32_t id,
DdmPacketType packet_type,
uint32_t type,
art::ArrayRef<const uint8_t> data) { // Get the write_event early to fail fast.
ScopedEventFdLock lk(adb_write_event_fd_); if (adb_connection_socket_ == -1 || !performed_handshake_) {
VLOG(jdwp) << "Not sending ddms data of type "
<< StringPrintf("%c%c%c%c", static_cast<char>(type >> 24), static_cast<char>(type >> 16), static_cast<char>(type >> 8), static_cast<char>(type)) << " due to no connection!"; // Adb is not connected. return;
}
// the adb_write_event_fd_ will ensure that the adb_connection_socket_ will not go away until // after we have sent our data. static constexpr uint32_t kDdmPacketHeaderSize =
kJdwpHeaderLen // jdwp command packet size
+ sizeof(uint32_t) // Type
+ sizeof(uint32_t); // length
alignas(sizeof(uint32_t)) std::array<uint8_t, kDdmPacketHeaderSize> pkt;
uint8_t* pkt_data = pkt.data();
// Write the id next;
*reinterpret_cast<uint32_t*>(pkt_data) = htonl(id);
pkt_data += sizeof(uint32_t);
// next the flags. (0 for cmd packet because DDMS).
*(pkt_data++) = static_cast<uint8_t>(packet_type); switch (packet_type) { case DdmPacketType::kCmd: { // Now the cmd-set
*(pkt_data++) = kJdwpDdmCmdSet; // Now the command
*(pkt_data++) = kJdwpDdmCmd; break;
} case DdmPacketType::kReply: { // This is the error code bytes which are all 0
*(pkt_data++) = 0;
*(pkt_data++) = 0;
}
}
// These are at unaligned addresses so we need to do them manually. // now the type.
uint32_t net_type = htonl(type);
memcpy(pkt_data, &net_type, sizeof(net_type));
pkt_data += sizeof(uint32_t);
// Now the data.size()
uint32_t net_len = htonl(data.size());
memcpy(pkt_data, &net_len, sizeof(net_len));
pkt_data += sizeof(uint32_t);
static uint32_t constexpr kIovSize = 2; struct iovec iovs[kIovSize] = {
{ pkt.data(), pkt.size() },
{ const_cast<uint8_t*>(data.data()), data.size() },
}; // now pkt_header has the header. // use writev to send the actual data.
ssize_t res = TEMP_FAILURE_RETRY(writev(adb_connection_socket_, iovs, kIovSize)); if (static_cast<size_t>(res) != (kDdmPacketHeaderSize + data.size())) {
PLOG(ERROR) << StringPrintf("Failed to send DDMS packet %c%c%c%c to debugger (%zd of %zu)", static_cast<char>(type >> 24), static_cast<char>(type >> 16), static_cast<char>(type >> 8), static_cast<char>(type),
res, data.size() + kDdmPacketHeaderSize);
} else {
VLOG(jdwp) << StringPrintf("sent DDMS packet %c%c%c%c to debugger %zu", static_cast<char>(type >> 24), static_cast<char>(type >> 16), static_cast<char>(type >> 8), static_cast<char>(type),
data.size() + kDdmPacketHeaderSize);
}
}
int res = TEMP_FAILURE_RETRY(sendmsg(local_agent_control_sock_, &msg, MSG_EOR)); if (res < 0) {
PLOG(ERROR) << "Failed to send agent adb connection fds.";
} else {
sent_agent_fds_ = true;
VLOG(jdwp) << "Fds have been sent to jdwp agent!";
}
}
while (!shutting_down_) { // If adbd isn't running, because USB debugging was disabled or // perhaps the system is restarting it for "adb root", the // connect() will fail. We loop here forever waiting for it // to come back. // // Waking up and polling every couple of seconds is generally a // bad thing to do, but we only do this if the application is // debuggable *and* adbd isn't running. Still, for the sake // of battery life, we should consider timing out and giving // up after a few minutes in case somebody ships an app with // the debuggable flag set.
control_ctx_.reset(adbconnection_client_new(info_ptrs, std::size(infos))); if (control_ctx_) { returntrue;
}
void AdbConnectionState::RunPollLoop(art::Thread* self) {
DCHECK(IsDebuggableOrProfilable());
CHECK_NE(agent_name_, "");
CHECK_EQ(self->GetState(), art::ThreadState::kNative);
art::Locks::mutator_lock_->AssertNotHeld(self);
self->SetState(art::ThreadState::kWaitingInMainDebuggerLoop); // shutting_down_ set by StopDebuggerThreads while (!shutting_down_) { // First, connect to adbd if we haven't already. if (!control_ctx_ && !SetupAdbConnection()) {
LOG(ERROR) << "Failed to setup adb connection."; return;
} while (!shutting_down_ && control_ctx_) { bool should_listen_on_connection = !agent_has_socket_ && !sent_agent_fds_; // By default we are always interested in read and hangup events on the control ctx.
int16_t interestingControlEventSet = POLLIN | POLLRDHUP; if (apex_adbconnection_client_has_pending_update()) { // If we have an update for ADBd, we also want to know when the control ctx // socket is writable.
interestingControlEventSet |= POLLOUT;
} struct pollfd pollfds[4] = {
{sleep_event_fd_, POLLIN, 0}, // -1 as an fd causes it to be ignored by poll
{(agent_loaded_ ? local_agent_control_sock_ : -1), POLLIN, 0}, // Check for the control_sock_ actually going away. We always monitor for POLLIN, even if // we already have an adbd socket. This allows to reject incoming debugger connection if // there is already have one connected.
{adbconnection_client_pollfd(control_ctx_.get()), interestingControlEventSet, 0}, // if we have not loaded the agent either the adb_connection_socket_ is -1 meaning we // don't have a real connection yet or the socket through adb needs to be listened to for // incoming data that the agent or this plugin can handle.
{should_listen_on_connection ? adb_connection_socket_ : -1, POLLIN | POLLRDHUP, 0}}; int res = TEMP_FAILURE_RETRY(poll(pollfds, 4, -1)); if (res < 0) {
PLOG(ERROR) << "Failed to poll!"; return;
}
VLOG(jdwp) << "adbconnection poll awakening";
conststruct pollfd& sleep_event_poll = pollfds[0]; conststruct pollfd& agent_control_sock_poll = pollfds[1]; conststruct pollfd& control_sock_poll = pollfds[2]; conststruct pollfd& adb_socket_poll = pollfds[3]; if (FlagsSet(agent_control_sock_poll.revents, POLLIN)) {
CHECK(IsDebuggingPossible()); // This path is unexpected for a profileable process.
DCHECK(agent_loaded_); char buf[257];
res = TEMP_FAILURE_RETRY(recv(local_agent_control_sock_, buf, sizeof(buf) - 1, 0)); if (res < 0) {
PLOG(ERROR) << "Failed to read message from agent control socket! Retrying"; continue;
} else {
buf[res + 1] = '\0';
VLOG(jdwp) << "Local agent control sock has data: " << static_cast<constchar*>(buf);
} if (memcmp(kListenStartMessage, buf, sizeof(kListenStartMessage)) == 0) {
agent_listening_ = true; if (adb_connection_socket_ != -1) {
SendAgentFds(/*require_handshake=*/ !performed_handshake_);
}
} elseif (memcmp(kListenEndMessage, buf, sizeof(kListenEndMessage)) == 0) {
agent_listening_ = false;
} elseif (memcmp(kHandshakeCompleteMessage, buf, sizeof(kHandshakeCompleteMessage)) == 0) { if (agent_has_socket_) {
performed_handshake_ = true;
}
} elseif (memcmp(kCloseMessage, buf, sizeof(kCloseMessage)) == 0) {
CloseFds();
agent_has_socket_ = false;
} elseif (memcmp(kAcceptMessage, buf, sizeof(kAcceptMessage)) == 0) {
agent_has_socket_ = true;
sent_agent_fds_ = false; // We will only ever do the handshake once so reset this.
performed_handshake_ = false;
} else {
LOG(ERROR) << "Unknown message received from debugger! '" << std::string(buf) << "'";
}
} elseif (FlagsSet(control_sock_poll.revents, POLLIN)) { if (!IsDebuggingPossible()) { // For a profielable process, this path can execute when the adbd restarts.
control_ctx_.reset(); break;
} bool maybe_send_fds = false;
{ // Hold onto this lock so that concurrent ddm publishes don't try to use an illegal fd.
ScopedEventFdLock sefdl(adb_write_event_fd_);
android::base::unique_fd new_fd(adbconnection_client_receive_jdwp_fd(control_ctx_.get())); if (new_fd == -1) { // Something went wrong. We need to retry getting the control socket.
control_ctx_.reset(); break;
} elseif (adb_connection_socket_ != -1) { // We already have a connection.
VLOG(jdwp) << "Ignoring second debugger. Accept then drop!"; if (new_fd >= 0) {
new_fd.reset();
}
} else {
VLOG(jdwp) << "Adb connection established with fd " << new_fd;
adb_connection_socket_ = std::move(new_fd);
maybe_send_fds = true;
}
} if (maybe_send_fds && agent_loaded_ && agent_listening_) {
VLOG(jdwp) << "Sending fds as soon as we received them."; // The agent was already loaded so this must be after a disconnection. Therefore have the // transport perform the handshake.
SendAgentFds(/*require_handshake=*/ true);
}
} elseif (FlagsSet(control_sock_poll.revents, POLLRDHUP)) { // The other end of the adb connection just dropped it. // Reset the connection since we don't have an active socket through the adb server. // Note this path is expected for either debuggable or profileable processes.
DCHECK(!agent_has_socket_) << "We shouldn't be doing anything if there is already a "
<< "connection active";
control_ctx_.reset(); break;
} elseif (FlagsSet(adb_socket_poll.revents, POLLIN)) {
CHECK(IsDebuggingPossible()); // This path is unexpected for a profileable process.
DCHECK(!agent_has_socket_); if (!agent_loaded_) {
HandleDataWithoutAgent(self);
} elseif (agent_listening_ && !sent_agent_fds_) {
VLOG(jdwp) << "Sending agent fds again on data."; // Agent was already loaded so it can deal with the handshake.
SendAgentFds(/*require_handshake=*/true);
}
} elseif (FlagsSet(control_sock_poll.revents, POLLOUT)) {
VLOG(jdwp) << "Sending state update to adbd";
apex_adbconnection_client_send_update(control_ctx_.get());
} elseif (FlagsSet(adb_socket_poll.revents, POLLRDHUP)) {
CHECK(IsDebuggingPossible()); // This path is unexpected for a profileable process.
DCHECK(!agent_has_socket_);
CloseFds();
} elseif (FlagsSet(sleep_event_poll.revents, POLLIN)) { // Poll was awakened via fdevent, we need to decrease fdevent counter to prevent poll from // triggering again.
AcknowledgeWakeup();
} else {
VLOG(jdwp) << "Woke up poll without anything to do!";
}
}
}
}
void AdbConnectionState::HandleDataWithoutAgent(art::Thread* self) {
DCHECK(!agent_loaded_);
DCHECK(!agent_listening_); // TODO Should we check in some other way if we are userdebug/eng?
CHECK(art::Dbg::IsJdwpAllowed()); // We try to avoid loading the agent which is expensive. First lets just perform the handshake. if (!performed_handshake_) {
PerformHandshake(); return;
} // Read the packet header to figure out if it is one we can handle. We only 'peek' into the stream // to see if it's one we can handle. This doesn't change the state of the socket.
alignas(sizeof(uint32_t)) uint8_t packet_header[kPacketHeaderLen];
ssize_t res = TEMP_FAILURE_RETRY(recv(adb_connection_socket_.get(),
packet_header, sizeof(packet_header),
MSG_PEEK)); // We want to be very careful not to change the socket state until we know we succeeded. This will // let us fall-back to just loading the agent and letting it deal with everything. if (res <= 0) { // Close the socket. We either hit EOF or an error. if (res < 0) {
PLOG(ERROR) << "Unable to peek into adb socket due to error. Closing socket.";
}
CloseFds(); return;
} elseif (res < static_cast<int>(kPacketHeaderLen)) {
LOG(ERROR) << "Unable to peek into adb socket. Loading agent to handle this. Only read " << res;
AttachJdwpAgent(self); return;
}
uint32_t full_len = ntohl(*reinterpret_cast<uint32_t*>(packet_header + kPacketSizeOff));
uint32_t pkt_id = ntohl(*reinterpret_cast<uint32_t*>(packet_header + kPacketIdOff));
uint8_t pkt_cmd_set = packet_header[kPacketCommandSetOff];
uint8_t pkt_cmd = packet_header[kPacketCommandOff]; if (pkt_cmd_set != kDdmCommandSet ||
pkt_cmd != kDdmChunkCommand ||
full_len < kPacketHeaderLen) {
VLOG(jdwp) << "Loading agent due to jdwp packet that cannot be handled by adbconnection.";
AttachJdwpAgent(self); return;
}
uint32_t avail = -1;
res = TEMP_FAILURE_RETRY(ioctl(adb_connection_socket_.get(), FIONREAD, &avail)); if (res < 0) {
PLOG(ERROR) << "Failed to determine amount of readable data in socket! Closing connection";
CloseFds(); return;
} elseif (avail < full_len) {
LOG(WARNING) << "Unable to handle ddm command in adbconnection due to insufficent data. "
<< "Expected " << full_len << " bytes but only " << avail << " are readable. "
<< "Loading jdwp agent to deal with this.";
AttachJdwpAgent(self); return;
} // Actually read the data.
std::vector<uint8_t> full_pkt;
full_pkt.resize(full_len);
res = TEMP_FAILURE_RETRY(recv(adb_connection_socket_.get(), full_pkt.data(), full_len, 0)); if (res < 0) {
PLOG(ERROR) << "Failed to recv data from adb connection. Closing connection";
CloseFds(); return;
}
DCHECK_EQ(memcmp(full_pkt.data(), packet_header, sizeof(packet_header)), 0);
size_t data_size = full_len - kPacketHeaderLen; if (data_size < (sizeof(uint32_t) * 2)) { // This is an error (the data isn't long enough) but to match historical behavior we need to // ignore it. return;
}
uint8_t* ddm_data = full_pkt.data() + kPacketHeaderLen;
uint32_t ddm_type = ReadUint32AndAdvance(&ddm_data);
uint32_t ddm_len = ReadUint32AndAdvance(&ddm_data); if (ddm_len > data_size - (2 * sizeof(uint32_t))) { // This is an error (the data isn't long enough) but to match historical behavior we need to // ignore it. return;
}
if (!notified_ddm_active_) {
NotifyDdms(/*active=*/ true);
}
uint32_t reply_type;
std::vector<uint8_t> reply; if (!art::Dbg::DdmHandleChunk(self->GetJniEnv(),
ddm_type,
art::ArrayRef<const jbyte>(reinterpret_cast<const jbyte*>(ddm_data),
ddm_len), /*out*/&reply_type, /*out*/&reply)) { // To match historical behavior we don't send any response when there is no data to reply with. return;
}
SendDdmPacket(pkt_id,
DdmPacketType::kReply,
reply_type,
art::ArrayRef<const uint8_t>(reply));
}
void AdbConnectionState::PerformHandshake() {
CHECK(!performed_handshake_); // Check to make sure we are able to read the whole handshake.
uint32_t avail = -1; int res = TEMP_FAILURE_RETRY(ioctl(adb_connection_socket_.get(), FIONREAD, &avail)); if (res < 0 || avail < sizeof(kJdwpHandshake)) { if (res < 0) {
PLOG(ERROR) << "Failed to determine amount of readable data for handshake!";
}
LOG(WARNING) << "Closing connection to broken client.";
CloseFds(); return;
} // Perform the handshake. char handshake_msg[sizeof(kJdwpHandshake)];
res = TEMP_FAILURE_RETRY(recv(adb_connection_socket_.get(),
handshake_msg, sizeof(handshake_msg),
MSG_DONTWAIT)); if (res < static_cast<int>(sizeof(kJdwpHandshake)) ||
strncmp(handshake_msg, kJdwpHandshake, sizeof(kJdwpHandshake)) != 0) { if (res < 0) {
PLOG(ERROR) << "Failed to read handshake!";
}
LOG(WARNING) << "Handshake failed!";
CloseFds(); return;
} // Send the handshake back.
res = TEMP_FAILURE_RETRY(send(adb_connection_socket_.get(),
kJdwpHandshake, sizeof(kJdwpHandshake), 0)); if (res < static_cast<int>(sizeof(kJdwpHandshake))) {
PLOG(ERROR) << "Failed to send jdwp-handshake response.";
CloseFds(); return;
}
performed_handshake_ = true;
}
bool ValidateJdwpOptions(const std::string& opts) { bool res = true; // The adbconnection plugin requires that the jdwp agent be configured as a 'server' because that // is what adb expects and otherwise we will hit a deadlock as the poll loop thread stops waiting // for the fd's to be passed down. if (ContainsArgument(opts, "server=n")) {
res = false;
LOG(ERROR) << "Cannot start jdwp debugging with server=n from adbconnection.";
} // We don't start the jdwp agent until threads are already running. It is far too late to suspend // everything. if (ContainsArgument(opts, "suspend=y")) {
res = false;
LOG(ERROR) << "Cannot use suspend=y with late-init jdwp.";
} return res;
}
#ifdefined(__ANDROID__) void FixLogfile(JdwpArgs& parameters) { const std::string kLogfile = "logfile"; // On Android, an app will not have write access to the cwd (which is "/"). // If a relative path was provided, we need to patch it with a writable // location. For now, we use /data/data/<PKG_NAME>. // Note that /data/local/tmp/ was also considered but it not a good candidate since apps don't // have write access to it.
if (!parameters.contains(kLogfile)) { return;
}
std::string& logfile = parameters.get(kLogfile); if (logfile.front() == '/') { // We only fix logfile if it is not using an absolute path return;
}
// See the comment above for why we need to be server=y. Since the agent defaults to server=n // we must always set it.
parameters.put("server", "y");
// See the comment above for why we need to be suspend=n. Since the agent defaults to // suspend=y we must always set it.
parameters.put("suspend", "n");
void AdbConnectionState::StopDebuggerThreads() { // The regular agent system will take care of unloading the agent (if needed).
shutting_down_ = true;
WakeupPollLoop();
}
// TODO Provide some way for apps to set this maybe?
gState.emplace(kDefaultJdwpAgentName); return ValidateJdwpOptions(art::Runtime::Current()->GetJdwpOptions());
}
extern"C"bool ArtPlugin_Deinitialize() { // We don't actually have to do anything here. The debugger (if one was // attached) was shutdown by the move to the kDeath runtime phase and the // adbconnection threads were shutdown by StopDebugger. returntrue;
}
} // namespace adbconnection
Messung V0.5 in Prozent
¤ Dauer der Verarbeitung: 0.4 Sekunden
(vorverarbeitet am 2026-06-29)
¤
Die Informationen auf dieser Webseite wurden
nach bestem Wissen sorgfältig zusammengestellt. Es wird jedoch weder Vollständigkeit, noch Richtigkeit,
noch Qualität der bereit gestellten Informationen zugesichert.
Bemerkung:
Die farbliche Syntaxdarstellung und die Messung sind noch experimentell.