You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
465 lines
18 KiB
465 lines
18 KiB
// Copyright 2019 The Chromium Authors. All rights reserved.
|
|
// Use of this source code is governed by a BSD-style license that can be
|
|
// found in the LICENSE file.
|
|
|
|
#include "discovery/mdns/mdns_publisher.h"
|
|
|
|
#include <chrono>
|
|
#include <vector>
|
|
|
|
#include "discovery/common/config.h"
|
|
#include "discovery/mdns/mdns_probe_manager.h"
|
|
#include "discovery/mdns/mdns_sender.h"
|
|
#include "discovery/mdns/testing/mdns_test_util.h"
|
|
#include "platform/test/fake_task_runner.h"
|
|
#include "platform/test/fake_udp_socket.h"
|
|
|
|
using testing::_;
|
|
using testing::Invoke;
|
|
using testing::Return;
|
|
using testing::StrictMock;
|
|
|
|
namespace openscreen {
|
|
namespace discovery {
|
|
namespace {
|
|
|
|
constexpr Clock::duration kAnnounceGoodbyeDelay = std::chrono::milliseconds(25);
|
|
|
|
bool ContainsRecord(const std::vector<MdnsRecord::ConstRef>& records,
|
|
MdnsRecord record) {
|
|
return std::find_if(records.begin(), records.end(),
|
|
[&record](const MdnsRecord& ref) {
|
|
return ref == record;
|
|
}) != records.end();
|
|
}
|
|
|
|
} // namespace
|
|
|
|
class MockMdnsSender : public MdnsSender {
|
|
public:
|
|
explicit MockMdnsSender(UdpSocket* socket) : MdnsSender(socket) {}
|
|
|
|
MOCK_METHOD1(SendMulticast, Error(const MdnsMessage& message));
|
|
MOCK_METHOD2(SendMessage,
|
|
Error(const MdnsMessage& message, const IPEndpoint& endpoint));
|
|
};
|
|
|
|
class MockProbeManager : public MdnsProbeManager {
|
|
public:
|
|
MOCK_CONST_METHOD1(IsDomainClaimed, bool(const DomainName&));
|
|
MOCK_METHOD2(RespondToProbeQuery,
|
|
void(const MdnsMessage&, const IPEndpoint&));
|
|
};
|
|
|
|
class MdnsPublisherTesting : public MdnsPublisher {
|
|
public:
|
|
using MdnsPublisher::GetPtrRecords;
|
|
using MdnsPublisher::GetRecords;
|
|
using MdnsPublisher::MdnsPublisher;
|
|
|
|
bool IsNonPtrRecordPresent(const DomainName& name) {
|
|
auto it = records_.find(name);
|
|
if (it == records_.end()) {
|
|
return false;
|
|
}
|
|
|
|
return std::find_if(it->second.begin(), it->second.end(),
|
|
[](const RecordAnnouncerPtr& announcer) {
|
|
return announcer->record().dns_type() !=
|
|
DnsType::kPTR;
|
|
}) != it->second.end();
|
|
}
|
|
};
|
|
|
|
class MdnsPublisherTest : public testing::Test {
|
|
public:
|
|
MdnsPublisherTest()
|
|
: clock_(Clock::now()),
|
|
task_runner_(&clock_),
|
|
socket_(&task_runner_),
|
|
sender_(&socket_),
|
|
publisher_(&sender_,
|
|
&probe_manager_,
|
|
&task_runner_,
|
|
FakeClock::now,
|
|
config_) {}
|
|
|
|
~MdnsPublisherTest() {
|
|
// Clear out any remaining calls in the task runner queue.
|
|
clock_.Advance(Clock::to_duration(std::chrono::seconds(1)));
|
|
}
|
|
|
|
protected:
|
|
Error IsAnnounced(const MdnsRecord& original, const MdnsMessage& message) {
|
|
EXPECT_EQ(message.type(), MessageType::Response);
|
|
EXPECT_EQ(message.questions().size(), size_t{0});
|
|
EXPECT_EQ(message.authority_records().size(), size_t{0});
|
|
EXPECT_EQ(message.additional_records().size(), size_t{0});
|
|
EXPECT_EQ(message.answers().size(), size_t{1});
|
|
|
|
const MdnsRecord& sent = message.answers()[0];
|
|
EXPECT_EQ(original.name(), sent.name());
|
|
EXPECT_EQ(original.dns_type(), sent.dns_type());
|
|
EXPECT_EQ(original.dns_class(), sent.dns_class());
|
|
EXPECT_EQ(original.record_type(), sent.record_type());
|
|
EXPECT_EQ(original.rdata(), sent.rdata());
|
|
EXPECT_EQ(original.ttl(), sent.ttl());
|
|
return Error::None();
|
|
}
|
|
|
|
Error IsGoodbyeRecord(const MdnsRecord& original,
|
|
const MdnsMessage& message) {
|
|
EXPECT_EQ(message.type(), MessageType::Response);
|
|
EXPECT_EQ(message.questions().size(), size_t{0});
|
|
EXPECT_EQ(message.authority_records().size(), size_t{0});
|
|
EXPECT_EQ(message.additional_records().size(), size_t{0});
|
|
EXPECT_EQ(message.answers().size(), size_t{1});
|
|
|
|
const MdnsRecord& sent = message.answers()[0];
|
|
EXPECT_EQ(original.name(), sent.name());
|
|
EXPECT_EQ(original.dns_type(), sent.dns_type());
|
|
EXPECT_EQ(original.dns_class(), sent.dns_class());
|
|
EXPECT_EQ(original.record_type(), sent.record_type());
|
|
EXPECT_EQ(original.rdata(), sent.rdata());
|
|
EXPECT_EQ(std::chrono::seconds(0), sent.ttl());
|
|
return Error::None();
|
|
}
|
|
|
|
void CheckPublishedRecords(const DomainName& domain,
|
|
DnsType type,
|
|
std::vector<MdnsRecord> expected_records) {
|
|
EXPECT_EQ(publisher_.GetRecordCount(), expected_records.size());
|
|
auto records = publisher_.GetRecords(domain, type, DnsClass::kIN);
|
|
for (const auto& record : expected_records) {
|
|
EXPECT_TRUE(ContainsRecord(records, record));
|
|
}
|
|
}
|
|
|
|
void TestUniqueRecordRegistrationWorkflow(MdnsRecord record,
|
|
MdnsRecord record2) {
|
|
EXPECT_CALL(probe_manager_, IsDomainClaimed(domain_))
|
|
.WillRepeatedly(Return(true));
|
|
DnsType type = record.dns_type();
|
|
|
|
// Check preconditions.
|
|
ASSERT_EQ(record.dns_type(), record2.dns_type());
|
|
auto records = publisher_.GetRecords(domain_, type, DnsClass::kIN);
|
|
ASSERT_EQ(publisher_.GetRecordCount(), size_t{0});
|
|
ASSERT_EQ(records.size(), size_t{0});
|
|
ASSERT_NE(record, record2);
|
|
ASSERT_TRUE(records.empty());
|
|
|
|
// Register a new record.
|
|
EXPECT_CALL(sender_, SendMulticast(_))
|
|
.WillOnce([this, &record](const MdnsMessage& message) -> Error {
|
|
return IsAnnounced(record, message);
|
|
});
|
|
EXPECT_TRUE(publisher_.RegisterRecord(record).ok());
|
|
clock_.Advance(kAnnounceGoodbyeDelay);
|
|
testing::Mock::VerifyAndClearExpectations(&sender_);
|
|
CheckPublishedRecords(domain_, type, {record});
|
|
EXPECT_TRUE(publisher_.IsNonPtrRecordPresent(domain_));
|
|
|
|
// Re-register the same record.
|
|
EXPECT_FALSE(publisher_.RegisterRecord(record).ok());
|
|
clock_.Advance(kAnnounceGoodbyeDelay);
|
|
testing::Mock::VerifyAndClearExpectations(&sender_);
|
|
CheckPublishedRecords(domain_, type, {record});
|
|
EXPECT_TRUE(publisher_.IsNonPtrRecordPresent(domain_));
|
|
|
|
// Update a record that doesn't exist
|
|
EXPECT_FALSE(publisher_.UpdateRegisteredRecord(record2, record).ok());
|
|
clock_.Advance(kAnnounceGoodbyeDelay);
|
|
testing::Mock::VerifyAndClearExpectations(&sender_);
|
|
CheckPublishedRecords(domain_, type, {record});
|
|
EXPECT_TRUE(publisher_.IsNonPtrRecordPresent(domain_));
|
|
|
|
// Update an existing record.
|
|
EXPECT_CALL(sender_, SendMulticast(_))
|
|
.WillOnce([this, &record2](const MdnsMessage& message) -> Error {
|
|
return IsAnnounced(record2, message);
|
|
});
|
|
EXPECT_TRUE(publisher_.UpdateRegisteredRecord(record, record2).ok());
|
|
clock_.Advance(kAnnounceGoodbyeDelay);
|
|
testing::Mock::VerifyAndClearExpectations(&sender_);
|
|
CheckPublishedRecords(domain_, type, {record2});
|
|
EXPECT_TRUE(publisher_.IsNonPtrRecordPresent(domain_));
|
|
|
|
// Add back the original record
|
|
EXPECT_CALL(sender_, SendMulticast(_))
|
|
.WillOnce([this, &record](const MdnsMessage& message) -> Error {
|
|
return IsAnnounced(record, message);
|
|
});
|
|
EXPECT_TRUE(publisher_.RegisterRecord(record).ok());
|
|
clock_.Advance(kAnnounceGoodbyeDelay);
|
|
testing::Mock::VerifyAndClearExpectations(&sender_);
|
|
CheckPublishedRecords(domain_, type, {record, record2});
|
|
EXPECT_TRUE(publisher_.IsNonPtrRecordPresent(domain_));
|
|
|
|
// Delete an existing record.
|
|
EXPECT_CALL(sender_, SendMulticast(_))
|
|
.WillOnce([this, &record2](const MdnsMessage& message) -> Error {
|
|
return IsGoodbyeRecord(record2, message);
|
|
});
|
|
EXPECT_TRUE(publisher_.UnregisterRecord(record2).ok());
|
|
clock_.Advance(kAnnounceGoodbyeDelay);
|
|
testing::Mock::VerifyAndClearExpectations(&sender_);
|
|
CheckPublishedRecords(domain_, type, {record});
|
|
EXPECT_TRUE(publisher_.IsNonPtrRecordPresent(domain_));
|
|
|
|
// Delete a non-existing record.
|
|
EXPECT_FALSE(publisher_.UnregisterRecord(record2).ok());
|
|
clock_.Advance(kAnnounceGoodbyeDelay);
|
|
testing::Mock::VerifyAndClearExpectations(&sender_);
|
|
CheckPublishedRecords(domain_, type, {record});
|
|
EXPECT_TRUE(publisher_.IsNonPtrRecordPresent(domain_));
|
|
|
|
// Delete the last record
|
|
EXPECT_CALL(sender_, SendMulticast(_))
|
|
.WillOnce([this, &record](const MdnsMessage& message) -> Error {
|
|
return IsGoodbyeRecord(record, message);
|
|
});
|
|
EXPECT_TRUE(publisher_.UnregisterRecord(record).ok());
|
|
clock_.Advance(kAnnounceGoodbyeDelay);
|
|
testing::Mock::VerifyAndClearExpectations(&sender_);
|
|
CheckPublishedRecords(domain_, type, {});
|
|
EXPECT_FALSE(publisher_.IsNonPtrRecordPresent(domain_));
|
|
}
|
|
|
|
FakeClock clock_;
|
|
FakeTaskRunner task_runner_;
|
|
FakeUdpSocket socket_;
|
|
StrictMock<MockMdnsSender> sender_;
|
|
StrictMock<MockProbeManager> probe_manager_;
|
|
Config config_;
|
|
MdnsPublisherTesting publisher_;
|
|
|
|
DomainName domain_{"instance", "_googlecast", "_tcp", "local"};
|
|
DomainName ptr_domain_{"_googlecast", "_tcp", "local"};
|
|
};
|
|
|
|
TEST_F(MdnsPublisherTest, ARecordRegistrationWorkflow) {
|
|
const MdnsRecord record1 = GetFakeARecord(domain_);
|
|
const MdnsRecord record2 =
|
|
GetFakeARecord(domain_, std::chrono::seconds(1000));
|
|
TestUniqueRecordRegistrationWorkflow(record1, record2);
|
|
}
|
|
|
|
TEST_F(MdnsPublisherTest, AAAARecordRegistrationWorkflow) {
|
|
const MdnsRecord record1 = GetFakeAAAARecord(domain_);
|
|
const MdnsRecord record2 =
|
|
GetFakeAAAARecord(domain_, std::chrono::seconds(1000));
|
|
TestUniqueRecordRegistrationWorkflow(record1, record2);
|
|
}
|
|
|
|
TEST_F(MdnsPublisherTest, TXTRecordRegistrationWorkflow) {
|
|
const MdnsRecord record1 = GetFakeTxtRecord(domain_);
|
|
const MdnsRecord record2 =
|
|
GetFakeTxtRecord(domain_, std::chrono::seconds(1000));
|
|
TestUniqueRecordRegistrationWorkflow(record1, record2);
|
|
}
|
|
|
|
TEST_F(MdnsPublisherTest, SRVRecordRegistrationWorkflow) {
|
|
const MdnsRecord record1 = GetFakeSrvRecord(domain_);
|
|
const MdnsRecord record2 =
|
|
GetFakeSrvRecord(domain_, std::chrono::seconds(1000));
|
|
TestUniqueRecordRegistrationWorkflow(record1, record2);
|
|
}
|
|
|
|
TEST_F(MdnsPublisherTest, PTRRecordRegistrationWorkflow) {
|
|
const MdnsRecord record = GetFakePtrRecord(domain_);
|
|
const MdnsRecord record2 =
|
|
GetFakePtrRecord(domain_, std::chrono::seconds(1000));
|
|
|
|
EXPECT_CALL(probe_manager_, IsDomainClaimed(domain_))
|
|
.WillRepeatedly(Return(true));
|
|
DnsType type = DnsType::kPTR;
|
|
|
|
// Check preconditions.
|
|
ASSERT_EQ(record.dns_type(), record2.dns_type());
|
|
ASSERT_EQ(publisher_.GetRecordCount(), size_t{0});
|
|
auto records = publisher_.GetRecords(domain_, type, DnsClass::kIN);
|
|
ASSERT_EQ(records.size(), size_t{0});
|
|
records = publisher_.GetRecords(ptr_domain_, type, DnsClass::kIN);
|
|
ASSERT_EQ(records.size(), size_t{0});
|
|
ASSERT_NE(record, record2);
|
|
ASSERT_TRUE(records.empty());
|
|
ASSERT_EQ(publisher_.GetPtrRecords(DnsClass::kANY).size(), size_t{0});
|
|
|
|
// Register a new record.
|
|
EXPECT_CALL(sender_, SendMulticast(_))
|
|
.WillOnce([this, &record](const MdnsMessage& message) -> Error {
|
|
return IsAnnounced(record, message);
|
|
});
|
|
EXPECT_TRUE(publisher_.RegisterRecord(record).ok());
|
|
clock_.Advance(kAnnounceGoodbyeDelay);
|
|
testing::Mock::VerifyAndClearExpectations(&sender_);
|
|
CheckPublishedRecords(ptr_domain_, type, {record});
|
|
ASSERT_EQ(publisher_.GetPtrRecords(DnsClass::kANY).size(), size_t{1});
|
|
|
|
// Re-register the same record.
|
|
EXPECT_FALSE(publisher_.RegisterRecord(record).ok());
|
|
clock_.Advance(kAnnounceGoodbyeDelay);
|
|
testing::Mock::VerifyAndClearExpectations(&sender_);
|
|
CheckPublishedRecords(ptr_domain_, type, {record});
|
|
ASSERT_EQ(publisher_.GetPtrRecords(DnsClass::kANY).size(), size_t{1});
|
|
|
|
// Register a second record.
|
|
EXPECT_CALL(sender_, SendMulticast(_))
|
|
.WillOnce([this, &record2](const MdnsMessage& message) -> Error {
|
|
return IsAnnounced(record2, message);
|
|
});
|
|
EXPECT_TRUE(publisher_.RegisterRecord(record2).ok());
|
|
clock_.Advance(kAnnounceGoodbyeDelay);
|
|
testing::Mock::VerifyAndClearExpectations(&sender_);
|
|
CheckPublishedRecords(ptr_domain_, type, {record, record2});
|
|
ASSERT_EQ(publisher_.GetPtrRecords(DnsClass::kANY).size(), size_t{2});
|
|
|
|
// Delete an existing record.
|
|
EXPECT_CALL(sender_, SendMulticast(_))
|
|
.WillOnce([this, &record2](const MdnsMessage& message) -> Error {
|
|
return IsGoodbyeRecord(record2, message);
|
|
});
|
|
EXPECT_TRUE(publisher_.UnregisterRecord(record2).ok());
|
|
clock_.Advance(kAnnounceGoodbyeDelay);
|
|
testing::Mock::VerifyAndClearExpectations(&sender_);
|
|
CheckPublishedRecords(ptr_domain_, type, {record});
|
|
ASSERT_EQ(publisher_.GetPtrRecords(DnsClass::kANY).size(), size_t{1});
|
|
|
|
// Delete a non-existing record.
|
|
EXPECT_FALSE(publisher_.UnregisterRecord(record2).ok());
|
|
clock_.Advance(kAnnounceGoodbyeDelay);
|
|
testing::Mock::VerifyAndClearExpectations(&sender_);
|
|
CheckPublishedRecords(ptr_domain_, type, {record});
|
|
ASSERT_EQ(publisher_.GetPtrRecords(DnsClass::kANY).size(), size_t{1});
|
|
|
|
// Delete the last record
|
|
EXPECT_CALL(sender_, SendMulticast(_))
|
|
.WillOnce([this, &record](const MdnsMessage& message) -> Error {
|
|
return IsGoodbyeRecord(record, message);
|
|
});
|
|
EXPECT_TRUE(publisher_.UnregisterRecord(record).ok());
|
|
clock_.Advance(kAnnounceGoodbyeDelay);
|
|
testing::Mock::VerifyAndClearExpectations(&sender_);
|
|
CheckPublishedRecords(ptr_domain_, type, {});
|
|
ASSERT_EQ(publisher_.GetPtrRecords(DnsClass::kANY).size(), size_t{0});
|
|
}
|
|
|
|
TEST_F(MdnsPublisherTest, RegisteringUnownedRecordsFail) {
|
|
EXPECT_CALL(probe_manager_, IsDomainClaimed(domain_))
|
|
.WillRepeatedly(Return(false));
|
|
EXPECT_FALSE(publisher_.RegisterRecord(GetFakePtrRecord(domain_)).ok());
|
|
EXPECT_FALSE(publisher_.RegisterRecord(GetFakeSrvRecord(domain_)).ok());
|
|
EXPECT_FALSE(publisher_.RegisterRecord(GetFakeTxtRecord(domain_)).ok());
|
|
EXPECT_FALSE(publisher_.RegisterRecord(GetFakeARecord(domain_)).ok());
|
|
EXPECT_FALSE(publisher_.RegisterRecord(GetFakeAAAARecord(domain_)).ok());
|
|
}
|
|
|
|
TEST_F(MdnsPublisherTest, RegistrationAnnouncesEightTimes) {
|
|
EXPECT_CALL(probe_manager_, IsDomainClaimed(domain_))
|
|
.WillRepeatedly(Return(true));
|
|
constexpr Clock::duration kOneSecond =
|
|
Clock::to_duration(std::chrono::seconds(1));
|
|
|
|
// First announce, at registration.
|
|
const MdnsRecord record = GetFakeARecord(domain_);
|
|
EXPECT_CALL(sender_, SendMulticast(_))
|
|
.WillOnce([this, &record](const MdnsMessage& message) -> Error {
|
|
return IsAnnounced(record, message);
|
|
});
|
|
EXPECT_TRUE(publisher_.RegisterRecord(record).ok());
|
|
clock_.Advance(kAnnounceGoodbyeDelay);
|
|
|
|
// Second announce, at 2 seconds.
|
|
testing::Mock::VerifyAndClearExpectations(&sender_);
|
|
EXPECT_CALL(sender_, SendMulticast(_))
|
|
.WillOnce([this, &record](const MdnsMessage& message) -> Error {
|
|
return IsAnnounced(record, message);
|
|
});
|
|
clock_.Advance(kOneSecond);
|
|
clock_.Advance(kAnnounceGoodbyeDelay);
|
|
testing::Mock::VerifyAndClearExpectations(&sender_);
|
|
|
|
// Third announce, at 4 seconds.
|
|
clock_.Advance(kOneSecond);
|
|
clock_.Advance(kAnnounceGoodbyeDelay);
|
|
EXPECT_CALL(sender_, SendMulticast(_))
|
|
.WillOnce([this, &record](const MdnsMessage& message) -> Error {
|
|
return IsAnnounced(record, message);
|
|
});
|
|
clock_.Advance(kOneSecond);
|
|
clock_.Advance(kAnnounceGoodbyeDelay);
|
|
testing::Mock::VerifyAndClearExpectations(&sender_);
|
|
|
|
// Fourth announce, at 8 seconds.
|
|
clock_.Advance(kOneSecond * 3);
|
|
clock_.Advance(kAnnounceGoodbyeDelay);
|
|
EXPECT_CALL(sender_, SendMulticast(_))
|
|
.WillOnce([this, &record](const MdnsMessage& message) -> Error {
|
|
return IsAnnounced(record, message);
|
|
});
|
|
clock_.Advance(kOneSecond);
|
|
clock_.Advance(kAnnounceGoodbyeDelay);
|
|
testing::Mock::VerifyAndClearExpectations(&sender_);
|
|
|
|
// Fifth announce, at 16 seconds.
|
|
clock_.Advance(kOneSecond * 7);
|
|
clock_.Advance(kAnnounceGoodbyeDelay);
|
|
EXPECT_CALL(sender_, SendMulticast(_))
|
|
.WillOnce([this, &record](const MdnsMessage& message) -> Error {
|
|
return IsAnnounced(record, message);
|
|
});
|
|
clock_.Advance(kOneSecond);
|
|
clock_.Advance(kAnnounceGoodbyeDelay);
|
|
testing::Mock::VerifyAndClearExpectations(&sender_);
|
|
|
|
// Sixth announce, at 32 seconds.
|
|
clock_.Advance(kOneSecond * 15);
|
|
clock_.Advance(kAnnounceGoodbyeDelay);
|
|
EXPECT_CALL(sender_, SendMulticast(_))
|
|
.WillOnce([this, &record](const MdnsMessage& message) -> Error {
|
|
return IsAnnounced(record, message);
|
|
});
|
|
clock_.Advance(kOneSecond);
|
|
clock_.Advance(kAnnounceGoodbyeDelay);
|
|
testing::Mock::VerifyAndClearExpectations(&sender_);
|
|
|
|
// Seventh announce, at 64 seconds.
|
|
clock_.Advance(kOneSecond * 31);
|
|
clock_.Advance(kAnnounceGoodbyeDelay);
|
|
EXPECT_CALL(sender_, SendMulticast(_))
|
|
.WillOnce([this, &record](const MdnsMessage& message) -> Error {
|
|
return IsAnnounced(record, message);
|
|
});
|
|
clock_.Advance(kOneSecond);
|
|
clock_.Advance(kAnnounceGoodbyeDelay);
|
|
testing::Mock::VerifyAndClearExpectations(&sender_);
|
|
|
|
// Eighth announce, at 128 seconds.
|
|
clock_.Advance(kOneSecond * 63);
|
|
clock_.Advance(kAnnounceGoodbyeDelay);
|
|
EXPECT_CALL(sender_, SendMulticast(_))
|
|
.WillOnce([this, &record](const MdnsMessage& message) -> Error {
|
|
return IsAnnounced(record, message);
|
|
});
|
|
clock_.Advance(kOneSecond);
|
|
clock_.Advance(kAnnounceGoodbyeDelay);
|
|
testing::Mock::VerifyAndClearExpectations(&sender_);
|
|
|
|
// No more announcements
|
|
clock_.Advance(kOneSecond * 1024);
|
|
clock_.Advance(kAnnounceGoodbyeDelay);
|
|
testing::Mock::VerifyAndClearExpectations(&sender_);
|
|
|
|
// Sends goodbye message when removed.
|
|
EXPECT_CALL(sender_, SendMulticast(_))
|
|
.WillOnce([this, &record](const MdnsMessage& message) -> Error {
|
|
return IsGoodbyeRecord(record, message);
|
|
});
|
|
EXPECT_TRUE(publisher_.UnregisterRecord(record).ok());
|
|
clock_.Advance(kAnnounceGoodbyeDelay);
|
|
}
|
|
|
|
} // namespace discovery
|
|
} // namespace openscreen
|