diff options
Diffstat (limited to 'Services')
108 files changed, 13705 insertions, 0 deletions
diff --git a/Services/AudioServer/ASClientConnection.cpp b/Services/AudioServer/ASClientConnection.cpp new file mode 100644 index 0000000000..a1d4b766a0 --- /dev/null +++ b/Services/AudioServer/ASClientConnection.cpp @@ -0,0 +1,161 @@ +/* + * Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org> + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#include "ASClientConnection.h" +#include "ASMixer.h" +#include "AudioClientEndpoint.h" +#include <AK/SharedBuffer.h> +#include <LibAudio/Buffer.h> +#include <LibCore/EventLoop.h> +#include <errno.h> +#include <stdio.h> +#include <sys/socket.h> +#include <sys/types.h> +#include <sys/uio.h> +#include <unistd.h> + +static HashMap<int, RefPtr<ASClientConnection>> s_connections; + +void ASClientConnection::for_each(Function<void(ASClientConnection&)> callback) +{ + NonnullRefPtrVector<ASClientConnection> connections; + for (auto& it : s_connections) + connections.append(*it.value); + for (auto& connection : connections) + callback(connection); +} + +ASClientConnection::ASClientConnection(Core::LocalSocket& client_socket, int client_id, ASMixer& mixer) + : IPC::ClientConnection<AudioServerEndpoint>(*this, client_socket, client_id) + , m_mixer(mixer) +{ + s_connections.set(client_id, *this); +} + +ASClientConnection::~ASClientConnection() +{ +} + +void ASClientConnection::die() +{ + s_connections.remove(client_id()); +} + +void ASClientConnection::did_finish_playing_buffer(Badge<ASBufferQueue>, int buffer_id) +{ + post_message(Messages::AudioClient::FinishedPlayingBuffer(buffer_id)); +} + +void ASClientConnection::did_change_muted_state(Badge<ASMixer>, bool muted) +{ + post_message(Messages::AudioClient::MutedStateChanged(muted)); +} + +OwnPtr<Messages::AudioServer::GreetResponse> ASClientConnection::handle(const Messages::AudioServer::Greet&) +{ + return make<Messages::AudioServer::GreetResponse>(client_id()); +} + +OwnPtr<Messages::AudioServer::GetMainMixVolumeResponse> ASClientConnection::handle(const Messages::AudioServer::GetMainMixVolume&) +{ + return make<Messages::AudioServer::GetMainMixVolumeResponse>(m_mixer.main_volume()); +} + +OwnPtr<Messages::AudioServer::SetMainMixVolumeResponse> ASClientConnection::handle(const Messages::AudioServer::SetMainMixVolume& message) +{ + m_mixer.set_main_volume(message.volume()); + return make<Messages::AudioServer::SetMainMixVolumeResponse>(); +} + +OwnPtr<Messages::AudioServer::EnqueueBufferResponse> ASClientConnection::handle(const Messages::AudioServer::EnqueueBuffer& message) +{ + auto shared_buffer = SharedBuffer::create_from_shbuf_id(message.buffer_id()); + if (!shared_buffer) { + // FIXME: The shared buffer should have been retrieved for us already. + // We don't want to do IPC error checking at this layer. + ASSERT_NOT_REACHED(); + } + + if (!m_queue) + m_queue = m_mixer.create_queue(*this); + + if (m_queue->is_full()) + return make<Messages::AudioServer::EnqueueBufferResponse>(false); + + m_queue->enqueue(Audio::Buffer::create_with_shared_buffer(*shared_buffer, message.sample_count())); + return make<Messages::AudioServer::EnqueueBufferResponse>(true); +} + +OwnPtr<Messages::AudioServer::GetRemainingSamplesResponse> ASClientConnection::handle(const Messages::AudioServer::GetRemainingSamples&) +{ + int remaining = 0; + if (m_queue) + remaining = m_queue->get_remaining_samples(); + + return make<Messages::AudioServer::GetRemainingSamplesResponse>(remaining); +} + +OwnPtr<Messages::AudioServer::GetPlayedSamplesResponse> ASClientConnection::handle(const Messages::AudioServer::GetPlayedSamples&) +{ + int played = 0; + if (m_queue) + played = m_queue->get_played_samples(); + + return make<Messages::AudioServer::GetPlayedSamplesResponse>(played); +} + +OwnPtr<Messages::AudioServer::SetPausedResponse> ASClientConnection::handle(const Messages::AudioServer::SetPaused& message) +{ + if (m_queue) + m_queue->set_paused(message.paused()); + return make<Messages::AudioServer::SetPausedResponse>(); +} + +OwnPtr<Messages::AudioServer::ClearBufferResponse> ASClientConnection::handle(const Messages::AudioServer::ClearBuffer& message) +{ + if (m_queue) + m_queue->clear(message.paused()); + return make<Messages::AudioServer::ClearBufferResponse>(); +} + +OwnPtr<Messages::AudioServer::GetPlayingBufferResponse> ASClientConnection::handle(const Messages::AudioServer::GetPlayingBuffer&) +{ + int id = -1; + if (m_queue) + id = m_queue->get_playing_buffer(); + return make<Messages::AudioServer::GetPlayingBufferResponse>(id); +} + +OwnPtr<Messages::AudioServer::GetMutedResponse> ASClientConnection::handle(const Messages::AudioServer::GetMuted&) +{ + return make<Messages::AudioServer::GetMutedResponse>(m_mixer.is_muted()); +} + +OwnPtr<Messages::AudioServer::SetMutedResponse> ASClientConnection::handle(const Messages::AudioServer::SetMuted& message) +{ + m_mixer.set_muted(message.muted()); + return make<Messages::AudioServer::SetMutedResponse>(); +} diff --git a/Services/AudioServer/ASClientConnection.h b/Services/AudioServer/ASClientConnection.h new file mode 100644 index 0000000000..c76c51eb36 --- /dev/null +++ b/Services/AudioServer/ASClientConnection.h @@ -0,0 +1,69 @@ +/* + * Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org> + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#pragma once + +#include <AK/HashMap.h> +#include <AudioServer/AudioServerEndpoint.h> +#include <LibIPC/ClientConnection.h> + +namespace Audio { +class Buffer; +} + +class ASBufferQueue; +class ASMixer; + +class ASClientConnection final : public IPC::ClientConnection<AudioServerEndpoint> + , public AudioServerEndpoint { + C_OBJECT(ASClientConnection) +public: + explicit ASClientConnection(Core::LocalSocket&, int client_id, ASMixer& mixer); + ~ASClientConnection() override; + + void did_finish_playing_buffer(Badge<ASBufferQueue>, int buffer_id); + void did_change_muted_state(Badge<ASMixer>, bool muted); + + virtual void die() override; + + static void for_each(Function<void(ASClientConnection&)>); + +private: + virtual OwnPtr<Messages::AudioServer::GreetResponse> handle(const Messages::AudioServer::Greet&) override; + virtual OwnPtr<Messages::AudioServer::GetMainMixVolumeResponse> handle(const Messages::AudioServer::GetMainMixVolume&) override; + virtual OwnPtr<Messages::AudioServer::SetMainMixVolumeResponse> handle(const Messages::AudioServer::SetMainMixVolume&) override; + virtual OwnPtr<Messages::AudioServer::EnqueueBufferResponse> handle(const Messages::AudioServer::EnqueueBuffer&) override; + virtual OwnPtr<Messages::AudioServer::GetRemainingSamplesResponse> handle(const Messages::AudioServer::GetRemainingSamples&) override; + virtual OwnPtr<Messages::AudioServer::GetPlayedSamplesResponse> handle(const Messages::AudioServer::GetPlayedSamples&) override; + virtual OwnPtr<Messages::AudioServer::SetPausedResponse> handle(const Messages::AudioServer::SetPaused&) override; + virtual OwnPtr<Messages::AudioServer::ClearBufferResponse> handle(const Messages::AudioServer::ClearBuffer&) override; + virtual OwnPtr<Messages::AudioServer::GetPlayingBufferResponse> handle(const Messages::AudioServer::GetPlayingBuffer&) override; + virtual OwnPtr<Messages::AudioServer::GetMutedResponse> handle(const Messages::AudioServer::GetMuted&) override; + virtual OwnPtr<Messages::AudioServer::SetMutedResponse> handle(const Messages::AudioServer::SetMuted&) override; + + ASMixer& m_mixer; + RefPtr<ASBufferQueue> m_queue; +}; diff --git a/Services/AudioServer/ASMixer.cpp b/Services/AudioServer/ASMixer.cpp new file mode 100644 index 0000000000..3d9ff00cc5 --- /dev/null +++ b/Services/AudioServer/ASMixer.cpp @@ -0,0 +1,152 @@ +/* + * Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org> + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#include <AK/BufferStream.h> +#include <AK/NumericLimits.h> +#include <AudioServer/ASClientConnection.h> +#include <AudioServer/ASMixer.h> +#include <pthread.h> + +ASMixer::ASMixer() + : m_device(Core::File::construct("/dev/audio", this)) + , m_sound_thread( + [this] { + mix(); + return 0; + }, + "AudioServer[mixer]") +{ + if (!m_device->open(Core::IODevice::WriteOnly)) { + dbgprintf("Can't open audio device: %s\n", m_device->error_string()); + return; + } + + pthread_mutex_init(&m_pending_mutex, nullptr); + pthread_cond_init(&m_pending_cond, nullptr); + + m_zero_filled_buffer = (u8*)malloc(4096); + bzero(m_zero_filled_buffer, 4096); + m_sound_thread.start(); +} + +ASMixer::~ASMixer() +{ +} + +NonnullRefPtr<ASBufferQueue> ASMixer::create_queue(ASClientConnection& client) +{ + auto queue = adopt(*new ASBufferQueue(client)); + pthread_mutex_lock(&m_pending_mutex); + m_pending_mixing.append(*queue); + pthread_cond_signal(&m_pending_cond); + pthread_mutex_unlock(&m_pending_mutex); + return queue; +} + +void ASMixer::mix() +{ + decltype(m_pending_mixing) active_mix_queues; + + for (;;) { + if (active_mix_queues.is_empty()) { + pthread_mutex_lock(&m_pending_mutex); + pthread_cond_wait(&m_pending_cond, &m_pending_mutex); + active_mix_queues.append(move(m_pending_mixing)); + pthread_mutex_unlock(&m_pending_mutex); + } + + active_mix_queues.remove_all_matching([&](auto& entry) { return !entry->client(); }); + + Audio::Sample mixed_buffer[1024]; + auto mixed_buffer_length = (int)(sizeof(mixed_buffer) / sizeof(Audio::Sample)); + + // Mix the buffers together into the output + for (auto& queue : active_mix_queues) { + if (!queue->client()) { + queue->clear(); + continue; + } + + for (int i = 0; i < mixed_buffer_length; ++i) { + auto& mixed_sample = mixed_buffer[i]; + Audio::Sample sample; + if (!queue->get_next_sample(sample)) + break; + mixed_sample += sample; + } + } + + bool muted = m_muted; + + // output the mixed stuff to the device + u8 raw_buffer[4096]; + auto buffer = ByteBuffer::wrap(muted ? m_zero_filled_buffer : raw_buffer, sizeof(raw_buffer)); + + BufferStream stream(buffer); + if (!muted) { + for (int i = 0; i < mixed_buffer_length; ++i) { + auto& mixed_sample = mixed_buffer[i]; + + mixed_sample.scale(m_main_volume); + mixed_sample.clip(); + + i16 out_sample; + out_sample = mixed_sample.left * NumericLimits<i16>::max(); + stream << out_sample; + + ASSERT(!stream.at_end()); // we should have enough space for both channels in one buffer! + out_sample = mixed_sample.right * NumericLimits<i16>::max(); + stream << out_sample; + } + } + + if (stream.offset() != 0) { + buffer.trim(stream.offset()); + } + m_device->write(buffer); + } +} + +void ASMixer::set_muted(bool muted) +{ + if (m_muted == muted) + return; + m_muted = muted; + ASClientConnection::for_each([muted](ASClientConnection& client) { + client.did_change_muted_state({}, muted); + }); +} + +ASBufferQueue::ASBufferQueue(ASClientConnection& client) + : m_client(client.make_weak_ptr()) +{ +} + +void ASBufferQueue::enqueue(NonnullRefPtr<Audio::Buffer>&& buffer) +{ + m_remaining_samples += buffer->sample_count(); + m_queue.enqueue(move(buffer)); +} diff --git a/Services/AudioServer/ASMixer.h b/Services/AudioServer/ASMixer.h new file mode 100644 index 0000000000..e5ed5874b2 --- /dev/null +++ b/Services/AudioServer/ASMixer.h @@ -0,0 +1,139 @@ +/* + * Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org> + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#pragma once + +#include "ASClientConnection.h" +#include <AK/Badge.h> +#include <AK/ByteBuffer.h> +#include <AK/NonnullRefPtrVector.h> +#include <AK/Queue.h> +#include <AK/RefCounted.h> +#include <AK/WeakPtr.h> +#include <LibAudio/Buffer.h> +#include <LibCore/File.h> +#include <LibThread/Lock.h> +#include <LibThread/Thread.h> + +class ASClientConnection; + +class ASBufferQueue : public RefCounted<ASBufferQueue> { +public: + explicit ASBufferQueue(ASClientConnection&); + ~ASBufferQueue() {} + + bool is_full() const { return m_queue.size() >= 3; } + void enqueue(NonnullRefPtr<Audio::Buffer>&&); + + bool get_next_sample(Audio::Sample& sample) + { + if (m_paused) + return false; + + while (!m_current && !m_queue.is_empty()) + m_current = m_queue.dequeue(); + + if (!m_current) + return false; + + sample = m_current->samples()[m_position++]; + --m_remaining_samples; + ++m_played_samples; + + if (m_position >= m_current->sample_count()) { + m_client->did_finish_playing_buffer({}, m_current->shbuf_id()); + m_current = nullptr; + m_position = 0; + } + return true; + } + + ASClientConnection* client() { return m_client.ptr(); } + + void clear(bool paused = false) + { + m_queue.clear(); + m_position = 0; + m_remaining_samples = 0; + m_played_samples = 0; + m_current = nullptr; + m_paused = paused; + } + + void set_paused(bool paused) + { + m_paused = paused; + } + + int get_remaining_samples() const { return m_remaining_samples; } + int get_played_samples() const { return m_played_samples; } + int get_playing_buffer() const + { + if (m_current) + return m_current->shbuf_id(); + return -1; + } + +private: + RefPtr<Audio::Buffer> m_current; + Queue<NonnullRefPtr<Audio::Buffer>> m_queue; + int m_position { 0 }; + int m_remaining_samples { 0 }; + int m_played_samples { 0 }; + bool m_paused { false }; + WeakPtr<ASClientConnection> m_client; +}; + +class ASMixer : public Core::Object { + C_OBJECT(ASMixer) +public: + ASMixer(); + virtual ~ASMixer() override; + + NonnullRefPtr<ASBufferQueue> create_queue(ASClientConnection&); + + int main_volume() const { return m_main_volume; } + void set_main_volume(int volume) { m_main_volume = volume; } + + bool is_muted() const { return m_muted; } + void set_muted(bool); + +private: + Vector<NonnullRefPtr<ASBufferQueue>> m_pending_mixing; + pthread_mutex_t m_pending_mutex; + pthread_cond_t m_pending_cond; + + RefPtr<Core::File> m_device; + + LibThread::Thread m_sound_thread; + + bool m_muted { false }; + int m_main_volume { 100 }; + + u8* m_zero_filled_buffer { nullptr }; + + void mix(); +}; diff --git a/Services/AudioServer/AudioClient.ipc b/Services/AudioServer/AudioClient.ipc new file mode 100644 index 0000000000..12aa2d0b92 --- /dev/null +++ b/Services/AudioServer/AudioClient.ipc @@ -0,0 +1,5 @@ +endpoint AudioClient = 82 +{ + FinishedPlayingBuffer(i32 buffer_id) =| + MutedStateChanged(bool muted) =| +} diff --git a/Services/AudioServer/AudioServer.ipc b/Services/AudioServer/AudioServer.ipc new file mode 100644 index 0000000000..b48dcd7e46 --- /dev/null +++ b/Services/AudioServer/AudioServer.ipc @@ -0,0 +1,21 @@ +endpoint AudioServer = 85 +{ + // Basic protocol + Greet() => (i32 client_id) + + // Mixer functions + SetMuted(bool muted) => () + GetMuted() => (bool muted) + GetMainMixVolume() => (i32 volume) + SetMainMixVolume(i32 volume) => () + + // Buffer playback + EnqueueBuffer(i32 buffer_id, int sample_count) => (bool success) + SetPaused(bool paused) => () + ClearBuffer(bool paused) => () + + //Buffer information + GetRemainingSamples() => (int remaining_samples) + GetPlayedSamples() => (int played_samples) + GetPlayingBuffer() => (i32 buffer_id) +} diff --git a/Services/AudioServer/Makefile b/Services/AudioServer/Makefile new file mode 100644 index 0000000000..7b6b3dd031 --- /dev/null +++ b/Services/AudioServer/Makefile @@ -0,0 +1,24 @@ +OBJS = \ + main.o \ + ASMixer.o \ + ASClientConnection.o + +PROGRAM = AudioServer + +LIB_DEPS = Core IPC Thread Pthread + +EXTRA_CLEAN = AudioServerEndpoint.h AudioClientEndpoint.h + +*.cpp: AudioServerEndpoint.h AudioClientEndpoint.h + +AudioServerEndpoint.h: AudioServer.ipc | IPCCOMPILER + @echo "IPC $<"; $(IPCCOMPILER) $< > $@ + +AudioClientEndpoint.h: AudioClient.ipc | IPCCOMPILER + @echo "IPC $<"; $(IPCCOMPILER) $< > $@ + +install: + mkdir -p $(SERENITY_BASE_DIR)/Root/usr/include/AudioServer/ + cp *.h $(SERENITY_BASE_DIR)/Root/usr/include/AudioServer/ + +include ../../Makefile.common diff --git a/Services/AudioServer/main.cpp b/Services/AudioServer/main.cpp new file mode 100644 index 0000000000..5907dd77c6 --- /dev/null +++ b/Services/AudioServer/main.cpp @@ -0,0 +1,61 @@ +/* + * Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org> + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#include "ASMixer.h" +#include <LibCore/File.h> +#include <LibCore/LocalServer.h> + +int main(int, char**) +{ + if (pledge("stdio thread shared_buffer accept rpath wpath cpath unix fattr", nullptr) < 0) { + perror("pledge"); + return 1; + } + + Core::EventLoop event_loop; + ASMixer mixer; + + auto server = Core::LocalServer::construct(); + bool ok = server->take_over_from_system_server(); + ASSERT(ok); + server->on_ready_to_accept = [&] { + auto client_socket = server->accept(); + if (!client_socket) { + dbg() << "AudioServer: accept failed."; + return; + } + static int s_next_client_id = 0; + int client_id = ++s_next_client_id; + IPC::new_client_connection<ASClientConnection>(*client_socket, client_id, mixer); + }; + + if (pledge("stdio thread shared_buffer accept", nullptr) < 0) { + perror("pledge"); + return 1; + } + + return event_loop.exec(); +} diff --git a/Services/DHCPClient/DHCPv4.cpp b/Services/DHCPClient/DHCPv4.cpp new file mode 100644 index 0000000000..36430f4eec --- /dev/null +++ b/Services/DHCPClient/DHCPv4.cpp @@ -0,0 +1,59 @@ +/* + * Copyright (c) 2020, The SerenityOS developers. + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#include "DHCPv4.h" + +//#define DHCPV4_DEBUG + +ParsedDHCPv4Options DHCPv4Packet::parse_options() const +{ + ParsedDHCPv4Options options; + for (size_t index = 4; index < DHCPV4_OPTION_FIELD_MAX_LENGTH; ++index) { + auto opt_name = *(const DHCPOption*)&m_options[index]; + switch (opt_name) { + case DHCPOption::Pad: + continue; + case DHCPOption::End: + goto escape; + default: + ++index; + auto length = m_options[index]; + if ((size_t)length > DHCPV4_OPTION_FIELD_MAX_LENGTH - index) { + dbg() << "Bogus option length " << length << " assuming forgotten END"; + break; + } +#ifdef DHCPV4_DEBUG + dbg() << "DHCP Option " << (u8)opt_name << " with length " << length; +#endif + ++index; + options.options.set(opt_name, { length, &m_options[index] }); + index += length - 1; + break; + } + } +escape:; + return options; +} diff --git a/Services/DHCPClient/DHCPv4.h b/Services/DHCPClient/DHCPv4.h new file mode 100644 index 0000000000..07c5787f8a --- /dev/null +++ b/Services/DHCPClient/DHCPv4.h @@ -0,0 +1,300 @@ +/* + * Copyright (c) 2020, The SerenityOS developers. + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#pragma once + +#include <AK/Assertions.h> +#include <AK/ByteBuffer.h> +#include <AK/HashMap.h> +#include <AK/IPv4Address.h> +#include <AK/MACAddress.h> +#include <AK/NetworkOrdered.h> +#include <AK/StringBuilder.h> +#include <AK/StringView.h> +#include <AK/Traits.h> +#include <AK/Types.h> +#include <string.h> + +enum class DHCPv4Flags : u16 { + Broadcast = 1, + /* everything else is reserved and must be zero */ +}; + +enum class DHCPv4Op : u8 { + BootRequest = 1, + BootReply = 2 +}; + +enum class DHCPOption : u8 { + // BOOTP + Pad = 0, + SubnetMask, + TimeOffset, + Router, + TimeServer, + NameServer, + DomainNameServer, + LogServer, + CookieServer, + LPRServer, + ImpressServer, + ResourceLocationServer, + HostName, + BootFileSize, + MeritDumpFile, + DomainName, + SwapServer, + RootPath, + ExtensionsPath, + IPForwardingEnableDisable, + NonLocalSourceRoutingEnableDisable, + PolicyFilter, + MaximumDatagramReassemblySize, + DefaultIPTTL, + PathMTUAgingTimeout, + PathMTUPlateauTable, + InterfaceMTU, + AllSubnetsAreLocal, + BroadcastAddress, + PerformMaskDiscovery, + MaskSupplier, + PerformRouterDiscovery, + RouterSolicitationAddress, + StaticRoute, + TrailerEncapsulation, + ARPCacheTimeout, + EthernetEncapsulation, + TCPDefaultTTL, + TCPKeepaliveInterval, + TCPKeepaliveGarbage, + NetworkInformationServiceDomain, + NetworkInformationServers, + NetworkTimeProtocolServers, + VendorSpecificInformation, + NetBIOSOverTCPIPNameServer, + NetBIOSOverTCPIPDatagramDistributionServer, + NetBIOSOverTCPIPNodeType, + NetBIOSOverTCPIPScope, + XWindowSystemFontServer, // wow + XWindowSystemDisplayManager, + // DHCP + RequestedIPAddress = 50, + IPAddressLeaseTime, + OptionOverload, + DHCPMessageType, + ServerIdentifier, + ParameterRequestList, + Message, + MaximumDHCPMessageSize, + RenewalT1Time, + RenewalT2Time, + ClassIdentifier, + ClientIdentifier, + End = 255 +}; + +enum class DHCPMessageType : u8 { + DHCPDiscover = 1, + DHCPOffer, + DHCPRequest, + DHCPDecline, + DHCPAck, + DHCPNak, + DHCPRelease, +}; + +template <> +struct AK::Traits<DHCPOption> : public AK::GenericTraits<DHCPOption> { + static constexpr bool is_trivial() { return true; } + static unsigned hash(DHCPOption u) { return int_hash((u8)u); } +}; + +struct ParsedDHCPv4Options { + template <typename T> + Optional<const T> get(DHCPOption option_name) const + { + auto option = options.get(option_name); + if (!option.has_value()) { + return {}; + } + auto& value = option.value(); + if (value.length != sizeof(T)) + return {}; + return *(const T*)value.value; + } + + template <typename T> + Vector<T> get_many(DHCPOption option_name, size_t max_number) const + { + Vector<T> values; + + auto option = options.get(option_name); + if (!option.has_value()) { + return {}; + } + auto& value = option.value(); + if (value.length < sizeof(T)) + return {}; + + for (size_t i = 0; i < max_number; ++i) { + auto offset = i * sizeof(T); + if (offset >= value.length) + break; + values.append(*(T*)((u8*)const_cast<void*>(value.value) + offset)); + } + + return values; + } + + String to_string() const + { + StringBuilder builder; + builder.append("DHCP Options ("); + builder.appendf("%d", options.size()); + builder.append(" entries)\n"); + for (auto& opt : options) { + builder.appendf("\toption %d (%d bytes):", (u8)opt.key, (u8)opt.value.length); + for (auto i = 0; i < opt.value.length; ++i) + builder.appendf(" %u ", ((const u8*)opt.value.value)[i]); + builder.append('\n'); + } + return builder.build(); + } + + struct DHCPOptionValue { + u8 length; + const void* value; + }; + + HashMap<DHCPOption, DHCPOptionValue> options; +}; + +constexpr auto DHCPV4_OPTION_FIELD_MAX_LENGTH = 312; + +class [[gnu::packed]] DHCPv4Packet +{ +public: + u8 op() const { return m_op; } + void set_op(DHCPv4Op op) { m_op = (u8)op; } + + u8 htype() const { return m_htype; } + void set_htype(u8 htype) { m_htype = htype; } + + u8 hlen() const { return m_hlen; } + void set_hlen(u8 hlen) { m_hlen = hlen; } + + u8 hops() const { return m_hops; } + void set_hops(u8 hops) { m_hops = hops; } + + u32 xid() const { return m_xid; } + void set_xid(u32 xid) { m_xid = xid; } + + u16 secs() const { return m_secs; } + void set_secs(u16 secs) { m_secs = secs; } + + u16 flags() const { return m_flags; } + void set_flags(DHCPv4Flags flags) { m_flags = (u16)flags; } + + const IPv4Address& ciaddr() const { return m_ciaddr; } + const IPv4Address& yiaddr() const { return m_yiaddr; } + const IPv4Address& siaddr() const { return m_siaddr; } + const IPv4Address& giaddr() const { return m_giaddr; } + + IPv4Address& ciaddr() { return m_ciaddr; } + IPv4Address& yiaddr() { return m_yiaddr; } + IPv4Address& siaddr() { return m_siaddr; } + IPv4Address& giaddr() { return m_giaddr; } + + u8* options() { return m_options; } + ParsedDHCPv4Options parse_options() const; + + const MACAddress& chaddr() const { return *(const MACAddress*)&m_chaddr[0]; } + void set_chaddr(const MACAddress& mac) { *(MACAddress*)&m_chaddr[0] = mac; } + + StringView sname() const { return { (const char*)&m_sname[0] }; } + StringView file() const { return { (const char*)&m_file[0] }; } + +private: + NetworkOrdered<u8> m_op; + NetworkOrdered<u8> m_htype; + NetworkOrdered<u8> m_hlen; + NetworkOrdered<u8> m_hops; + NetworkOrdered<u32> m_xid; + NetworkOrdered<u16> m_secs; + NetworkOrdered<u16> m_flags; + IPv4Address m_ciaddr; + IPv4Address m_yiaddr; + IPv4Address m_siaddr; + IPv4Address m_giaddr; + u8 m_chaddr[16]; // 10 bytes of padding at the end + u8 m_sname[64] { 0 }; + u8 m_file[128] { 0 }; + u8 m_options[DHCPV4_OPTION_FIELD_MAX_LENGTH] { 0 }; // variable, less than 312 bytes +}; + +class DHCPv4PacketBuilder { +public: + DHCPv4PacketBuilder() + : m_buffer(ByteBuffer::create_zeroed(sizeof(DHCPv4Packet))) + { + auto* options = peek().options(); + // set the magic DHCP cookie value + options[0] = 99; + options[1] = 130; + options[2] = 83; + options[3] = 99; + } + + void add_option(DHCPOption option, u8 length, const void* data) + { + ASSERT(m_can_add); + // we need enough space to fit the option value, its length, and its data + ASSERT(next_option_offset + length + 2 < DHCPV4_OPTION_FIELD_MAX_LENGTH); + + auto* options = peek().options(); + options[next_option_offset++] = (u8)option; + memcpy(options + next_option_offset, &length, 1); + next_option_offset++; + memcpy(options + next_option_offset, data, length); + next_option_offset += length; + } + + void set_message_type(DHCPMessageType type) { add_option(DHCPOption::DHCPMessageType, 1, &type); } + + DHCPv4Packet& peek() { return *(DHCPv4Packet*)m_buffer.data(); } + DHCPv4Packet& build() + { + add_option(DHCPOption::End, 0, nullptr); + m_can_add = false; + return *(DHCPv4Packet*)m_buffer.data(); + } + size_t size() const { return m_buffer.size(); } + +private: + ByteBuffer m_buffer; + size_t next_option_offset { 4 }; + bool m_can_add { true }; +}; diff --git a/Services/DHCPClient/DHCPv4Client.cpp b/Services/DHCPClient/DHCPv4Client.cpp new file mode 100644 index 0000000000..918a20b93f --- /dev/null +++ b/Services/DHCPClient/DHCPv4Client.cpp @@ -0,0 +1,283 @@ +/* + * Copyright (c) 2020, The SerenityOS developers. + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#include "DHCPv4Client.h" +#include <AK/ByteBuffer.h> +#include <AK/Function.h> +#include <LibCore/SocketAddress.h> +#include <LibCore/Timer.h> +#include <stdio.h> + +//#define DHCPV4CLIENT_DEBUG + +static void send(const InterfaceDescriptor& iface, const DHCPv4Packet& packet, Core::Object*) +{ + int fd = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP); + if (fd < 0) { + dbg() << "ERROR: socket :: " << strerror(errno); + return; + } + + if (setsockopt(fd, SOL_SOCKET, SO_BINDTODEVICE, iface.m_ifname.characters(), IFNAMSIZ) < 0) { + dbg() << "ERROR: setsockopt(SO_BINDTODEVICE) :: " << strerror(errno); + return; + } + + sockaddr_in dst; + memset(&dst, 0, sizeof(dst)); + dst.sin_family = AF_INET; + dst.sin_port = htons(67); + dst.sin_addr.s_addr = IPv4Address { 255, 255, 255, 255 }.to_u32(); + memset(&dst.sin_zero, 0, sizeof(dst.sin_zero)); + + auto rc = sendto(fd, &packet, sizeof(packet), 0, (sockaddr*)&dst, sizeof(dst)); + if (rc < 0) { + dbg() << "sendto failed with " << strerror(errno); + // FIXME: what do we do here? + } +} + +static void set_params(const InterfaceDescriptor& iface, const IPv4Address& ipv4_addr, const IPv4Address& netmask, const IPv4Address& gateway) +{ + int fd = socket(AF_INET, SOCK_DGRAM, IPPROTO_IP); + if (fd < 0) { + dbg() << "ERROR: socket :: " << strerror(errno); + return; + } + + struct ifreq ifr; + memset(&ifr, 0, sizeof(ifr)); + strncpy(ifr.ifr_name, iface.m_ifname.characters(), IFNAMSIZ); + + // set the IP address + ifr.ifr_addr.sa_family = AF_INET; + ((sockaddr_in&)ifr.ifr_addr).sin_addr.s_addr = ipv4_addr.to_in_addr_t(); + + if (ioctl(fd, SIOCSIFADDR, &ifr) < 0) { + dbg() << "ERROR: ioctl(SIOCSIFADDR) :: " << strerror(errno); + } + + // set the network mask + ((sockaddr_in&)ifr.ifr_netmask).sin_addr.s_addr = netmask.to_in_addr_t(); + + if (ioctl(fd, SIOCSIFNETMASK, &ifr) < 0) { + dbg() << "ERROR: ioctl(SIOCSIFNETMASK) :: " << strerror(errno); + } + + // set the default gateway + struct rtentry rt; + memset(&rt, 0, sizeof(rt)); + + rt.rt_dev = const_cast<char*>(iface.m_ifname.characters()); + rt.rt_gateway.sa_family = AF_INET; + ((sockaddr_in&)rt.rt_gateway).sin_addr.s_addr = gateway.to_in_addr_t(); + rt.rt_flags = RTF_UP | RTF_GATEWAY; + + if (ioctl(fd, SIOCADDRT, &rt) < 0) { + dbg() << "Error: ioctl(SIOCADDRT) :: " << strerror(errno); + } +} + +DHCPv4Client::DHCPv4Client(Vector<InterfaceDescriptor> ifnames) + : m_ifnames(ifnames) +{ + m_server = Core::UDPServer::construct(this); + m_server->on_ready_to_receive = [this] { + auto buffer = m_server->receive(sizeof(DHCPv4Packet)); + dbg() << "Received " << buffer.size() << " bytes"; + if (buffer.size() != sizeof(DHCPv4Packet)) { + dbg() << "we expected " << sizeof(DHCPv4Packet) << " bytes, this is a bad packet"; + return; + } + auto& packet = *(DHCPv4Packet*)buffer.data(); + process_incoming(packet); + }; + + if (!m_server->bind({}, 68)) { + dbg() << "The server we just created somehow came already bound, refusing to continue"; + ASSERT_NOT_REACHED(); + } + + for (auto& iface : m_ifnames) + dhcp_discover(iface); +} + +DHCPv4Client::~DHCPv4Client() +{ +} + +void DHCPv4Client::handle_offer(const DHCPv4Packet& packet, const ParsedDHCPv4Options& options) +{ + dbg() << "We were offered " << packet.yiaddr().to_string() << " for " << options.get<u32>(DHCPOption::IPAddressLeaseTime).value_or(0); + auto* transaction = const_cast<DHCPv4Transaction*>(m_ongoing_transactions.get(packet.xid()).value_or(nullptr)); + if (!transaction) { + dbg() << "we're not looking for " << packet.xid(); + return; + } + if (transaction->has_ip) + return; + if (transaction->accepted_offer) { + // we've accepted someone's offer, but they haven't given us an ack + // TODO: maybe record this offer? + return; + } + // TAKE IT... + transaction->offered_lease_time = options.get<u32>(DHCPOption::IPAddressLeaseTime).value(); + dhcp_request(*transaction, packet); +} + +void DHCPv4Client::handle_ack(const DHCPv4Packet& packet, const ParsedDHCPv4Options& options) +{ +#ifdef DHCPV4CLIENT_DEBUG + dbg() << "The DHCP server handed us " << packet.yiaddr().to_string(); + dbg() << "Here are the options: " << options.to_string(); +#endif + auto* transaction = const_cast<DHCPv4Transaction*>(m_ongoing_transactions.get(packet.xid()).value_or(nullptr)); + if (!transaction) { + dbg() << "we're not looking for " << packet.xid(); + return; + } + transaction->has_ip = true; + auto& interface = transaction->interface; + auto new_ip = packet.yiaddr(); + auto lease_time = convert_between_host_and_network(options.get<u32>(DHCPOption::IPAddressLeaseTime).value_or(transaction->offered_lease_time)); + // set a timer for the duration of the lease, we shall renew if needed + Core::Timer::create_single_shot( + lease_time * 1000, + [this, transaction, interface = InterfaceDescriptor { interface }, new_ip] { + transaction->accepted_offer = false; + transaction->has_ip = false; + dhcp_discover(interface, new_ip); + }, + this); + set_params(transaction->interface, new_ip, options.get<IPv4Address>(DHCPOption::SubnetMask).value(), options.get_many<IPv4Address>(DHCPOption::Router, 1).first()); +} + +void DHCPv4Client::handle_nak(const DHCPv4Packet& packet, const ParsedDHCPv4Options& options) +{ + dbg() << "The DHCP server told us to go chase our own tail about " << packet.yiaddr().to_string(); + dbg() << "Here are the options: " << options.to_string(); + // make another request a bit later :shrug: + auto* transaction = const_cast<DHCPv4Transaction*>(m_ongoing_transactions.get(packet.xid()).value_or(nullptr)); + if (!transaction) { + dbg() << "we're not looking for " << packet.xid(); + return; + } + transaction->accepted_offer = false; + transaction->has_ip = false; + auto& iface = transaction->interface; + Core::Timer::create_single_shot( + 10000, + [this, iface = InterfaceDescriptor { iface }] { + dhcp_discover(iface); + }, + this); +} + +void DHCPv4Client::process_incoming(const DHCPv4Packet& packet) +{ + auto options = packet.parse_options(); +#ifdef DHCPV4CLIENT_DEBUG + dbg() << "Here are the options: " << options.to_string(); +#endif + auto value = options.get<DHCPMessageType>(DHCPOption::DHCPMessageType).value(); + switch (value) { + case DHCPMessageType::DHCPOffer: + handle_offer(packet, options); + break; + case DHCPMessageType::DHCPAck: + handle_ack(packet, options); + break; + case DHCPMessageType::DHCPNak: + handle_nak(packet, options); + break; + case DHCPMessageType::DHCPDiscover: + case DHCPMessageType::DHCPRequest: + case DHCPMessageType::DHCPRelease: + // These are not for us + // we're just getting them because there are other people on our subnet + // broadcasting stuff + break; + case DHCPMessageType::DHCPDecline: + default: + dbg() << "I dunno what to do with this " << (u8)value; + ASSERT_NOT_REACHED(); + break; + } +} + +void DHCPv4Client::dhcp_discover(const InterfaceDescriptor& iface, IPv4Address previous) +{ + auto transaction_id = rand(); +#ifdef DHCPV4CLIENT_DEBUG + dbg() << "Trying to lease an IP for " << iface.m_ifname << " with ID " << transaction_id; + if (!previous.is_zero()) + dbg() << "going to request the server to hand us " << previous.to_string(); +#endif + DHCPv4PacketBuilder builder; + + DHCPv4Packet& packet = builder.peek(); + packet.set_op(DHCPv4Op::BootRequest); + packet.set_htype(1); // 10mb ethernet + packet.set_hlen(sizeof(MACAddress)); + packet.set_xid(transaction_id); + packet.set_flags(DHCPv4Flags::Broadcast); + packet.ciaddr() = previous; + packet.set_chaddr(iface.m_mac_address); + packet.set_secs(65535); // we lie + + // set packet options + builder.set_message_type(DHCPMessageType::DHCPDiscover); + auto& dhcp_packet = builder.build(); + + // broadcast the discover request + send(iface, dhcp_packet, this); + m_ongoing_transactions.set(transaction_id, make<DHCPv4Transaction>(iface)); +} + +void DHCPv4Client::dhcp_request(DHCPv4Transaction& transaction, const DHCPv4Packet& offer) +{ + auto& iface = transaction.interface; + dbg() << "Leasing the IP " << offer.yiaddr().to_string() << " for adapter " << iface.m_ifname; + DHCPv4PacketBuilder builder; + + DHCPv4Packet& packet = builder.peek(); + packet.set_op(DHCPv4Op::BootRequest); + packet.set_htype(1); // 10mb ethernet + packet.set_hlen(sizeof(MACAddress)); + packet.set_xid(offer.xid()); + packet.set_flags(DHCPv4Flags::Broadcast); + packet.set_chaddr(iface.m_mac_address); + packet.set_secs(65535); // we lie + + // set packet options + builder.set_message_type(DHCPMessageType::DHCPRequest); + auto& dhcp_packet = builder.build(); + + // broadcast the "request" request + send(iface, dhcp_packet, this); + transaction.accepted_offer = true; +} diff --git a/Services/DHCPClient/DHCPv4Client.h b/Services/DHCPClient/DHCPv4Client.h new file mode 100644 index 0000000000..c45f08f0d1 --- /dev/null +++ b/Services/DHCPClient/DHCPv4Client.h @@ -0,0 +1,80 @@ +/* + * Copyright (c) 2020, The SerenityOS developers. + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#pragma once + +#include "DHCPv4.h" +#include <AK/FlyString.h> +#include <AK/HashMap.h> +#include <AK/OwnPtr.h> +#include <AK/Vector.h> +#include <LibCore/UDPServer.h> +#include <LibCore/UDPSocket.h> +#include <net/if.h> +#include <net/route.h> +#include <sys/ioctl.h> +#include <sys/socket.h> + +struct InterfaceDescriptor { + FlyString m_ifname; + MACAddress m_mac_address; +}; + +struct DHCPv4Transaction { + DHCPv4Transaction(InterfaceDescriptor ifname) + : interface(ifname) + { + } + + InterfaceDescriptor interface; + bool accepted_offer { false }; + bool has_ip { false }; + u32 offered_lease_time { 0 }; +}; + +class DHCPv4Client final : public Core::Object { + C_OBJECT(DHCPv4Client) + +public: + explicit DHCPv4Client(Vector<InterfaceDescriptor> ifnames); + virtual ~DHCPv4Client() override; + + void dhcp_discover(const InterfaceDescriptor& ifname, IPv4Address previous = IPv4Address { 0, 0, 0, 0 }); + void dhcp_request(DHCPv4Transaction& transaction, const DHCPv4Packet& packet); + + void process_incoming(const DHCPv4Packet& packet); + + bool id_is_registered(u32 id) { return m_ongoing_transactions.contains(id); } + +private: + HashMap<u32, OwnPtr<DHCPv4Transaction>> m_ongoing_transactions; + Vector<InterfaceDescriptor> m_ifnames; + RefPtr<Core::UDPServer> m_server; + + void handle_offer(const DHCPv4Packet&, const ParsedDHCPv4Options&); + void handle_ack(const DHCPv4Packet&, const ParsedDHCPv4Options&); + void handle_nak(const DHCPv4Packet&, const ParsedDHCPv4Options&); +}; diff --git a/Services/DHCPClient/Makefile b/Services/DHCPClient/Makefile new file mode 100644 index 0000000000..ce7c27b86d --- /dev/null +++ b/Services/DHCPClient/Makefile @@ -0,0 +1,10 @@ +OBJS = \ + DHCPv4.o \ + DHCPv4Client.o \ + main.o + +PROGRAM = DHCPClient + +LIB_DEPS = Core + +include ../../Makefile.common diff --git a/Services/DHCPClient/main.cpp b/Services/DHCPClient/main.cpp new file mode 100644 index 0000000000..b65b63eb46 --- /dev/null +++ b/Services/DHCPClient/main.cpp @@ -0,0 +1,101 @@ +/* + * Copyright (c) 2020, The SerenityOS developers. + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#include "DHCPv4Client.h" +#include <AK/JsonArray.h> +#include <AK/JsonObject.h> +#include <AK/String.h> +#include <AK/Types.h> +#include <LibCore/EventLoop.h> +#include <LibCore/File.h> +#include <LibCore/LocalServer.h> +#include <stdio.h> +#include <string.h> + +static u8 mac_part(const Vector<String>& parts, size_t index) +{ + auto chars = parts.at(index).characters(); + return (chars[0] - '0') * 16 + (chars[1] - '0'); +} + +static MACAddress mac_from_string(const String& str) +{ + auto chunks = str.split(':'); + ASSERT(chunks.size() == 6); // should we...worry about this? + return { + mac_part(chunks, 0), mac_part(chunks, 1), mac_part(chunks, 2), + mac_part(chunks, 3), mac_part(chunks, 4), mac_part(chunks, 5) + }; +} + +int main(int argc, char** argv) +{ + (void)argc; + (void)argv; + + if (pledge("stdio unix inet cpath rpath fattr", nullptr) < 0) { + perror("pledge"); + return 1; + } + + Core::EventLoop event_loop; + + if (unveil("/proc/net/", "r") < 0) { + perror("unveil"); + return 1; + } + + unveil(nullptr, nullptr); + + auto file = Core::File::construct("/proc/net/adapters"); + if (!file->open(Core::IODevice::ReadOnly)) { + fprintf(stderr, "Error: %s\n", file->error_string()); + return 1; + } + + auto file_contents = file->read_all(); + auto json = JsonValue::from_string(file_contents).as_array(); + Vector<InterfaceDescriptor> ifnames; + json.for_each([&ifnames](auto& value) { + auto if_object = value.as_object(); + + if (if_object.get("class_name").to_string() == "LoopbackAdapter") + return; + + auto name = if_object.get("name").to_string(); + auto mac = if_object.get("mac_address").to_string(); + ifnames.append({ name, mac_from_string(mac) }); + }); + + auto client = DHCPv4Client::construct(move(ifnames)); + + if (pledge("stdio inet", nullptr) < 0) { + perror("pledge"); + return 1; + } + + return event_loop.exec(); +} diff --git a/Services/LookupServer/DNSAnswer.cpp b/Services/LookupServer/DNSAnswer.cpp new file mode 100644 index 0000000000..775c23784e --- /dev/null +++ b/Services/LookupServer/DNSAnswer.cpp @@ -0,0 +1,46 @@ +/* + * Copyright (c) 2020, Andreas Kling <kling@serenityos.org> + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#include "DNSAnswer.h" +#include <time.h> + +DNSAnswer::DNSAnswer(const String& name, u16 type, u16 class_code, u32 ttl, const String& record_data) + : m_name(name) + , m_type(type) + , m_class_code(class_code) + , m_ttl(ttl) + , m_record_data(record_data) +{ + auto now = time(nullptr); + m_expiration_time = now + m_ttl; + if (m_expiration_time < now) + m_expiration_time = 0; +} + +bool DNSAnswer::has_expired() const +{ + return time(nullptr) >= m_expiration_time; +} diff --git a/Services/LookupServer/DNSAnswer.h b/Services/LookupServer/DNSAnswer.h new file mode 100644 index 0000000000..794603dd30 --- /dev/null +++ b/Services/LookupServer/DNSAnswer.h @@ -0,0 +1,51 @@ +/* + * Copyright (c) 2020, Andreas Kling <kling@serenityos.org> + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#pragma once + +#include <AK/String.h> +#include <AK/Types.h> + +class DNSAnswer { +public: + DNSAnswer(const String& name, u16 type, u16 class_code, u32 ttl, const String& record_data); + + const String& name() const { return m_name; } + u16 type() const { return m_type; } + u16 class_code() const { return m_class_code; } + u32 ttl() const { return m_ttl; } + const String& record_data() const { return m_record_data; } + + bool has_expired() const; + +private: + String m_name; + u16 m_type { 0 }; + u16 m_class_code { 0 }; + u32 m_ttl { 0 }; + time_t m_expiration_time { 0 }; + String m_record_data; +}; diff --git a/Services/LookupServer/DNSPacket.h b/Services/LookupServer/DNSPacket.h new file mode 100644 index 0000000000..832de9b7bb --- /dev/null +++ b/Services/LookupServer/DNSPacket.h @@ -0,0 +1,116 @@ +/* + * Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org> + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#pragma once + +#include <AK/NetworkOrdered.h> +#include <AK/Types.h> + +class [[gnu::packed]] DNSPacket +{ +public: + DNSPacket() + : m_recursion_desired(false) + , m_truncated(false) + , m_authoritative_answer(false) + , m_opcode(0) + , m_query_or_response(false) + , m_response_code(0) + , m_checking_disabled(false) + , m_authenticated_data(false) + , m_zero(false) + , m_recursion_available(false) + { + } + + u16 id() const { return m_id; } + void set_id(u16 w) { m_id = w; } + + bool recursion_desired() const { return m_recursion_desired; } + void set_recursion_desired(bool b) { m_recursion_desired = b; } + + bool is_truncated() const { return m_truncated; } + void set_truncated(bool b) { m_truncated = b; } + + bool is_authoritative_answer() const { return m_authoritative_answer; } + void set_authoritative_answer(bool b) { m_authoritative_answer = b; } + + u8 opcode() const { return m_opcode; } + void set_opcode(u8 b) { m_opcode = b; } + + bool is_query() const { return !m_query_or_response; } + bool is_response() const { return m_query_or_response; } + void set_is_query() { m_query_or_response = false; } + void set_is_response() { m_query_or_response = true; } + + u8 response_code() const { return m_response_code; } + void set_response_code(u8 b) { m_response_code = b; } + + bool checking_disabled() const { return m_checking_disabled; } + void set_checking_disabled(bool b) { m_checking_disabled = b; } + + bool is_authenticated_data() const { return m_authenticated_data; } + void set_authenticated_data(bool b) { m_authenticated_data = b; } + + bool is_recursion_available() const { return m_recursion_available; } + void set_recursion_available(bool b) { m_recursion_available = b; } + + u16 question_count() const { return m_question_count; } + void set_question_count(u16 w) { m_question_count = w; } + + u16 answer_count() const { return m_answer_count; } + void set_answer_count(u16 w) { m_answer_count = w; } + + u16 authority_count() const { return m_authority_count; } + void set_authority_count(u16 w) { m_authority_count = w; } + + u16 additional_count() const { return m_additional_count; } + void set_additional_count(u16 w) { m_additional_count = w; } + + void* payload() { return this + 1; } + const void* payload() const { return this + 1; } + +private: + NetworkOrdered<u16> m_id; + + bool m_recursion_desired : 1; + bool m_truncated : 1; + bool m_authoritative_answer : 1; + u8 m_opcode : 4; + bool m_query_or_response : 1; + u8 m_response_code : 4; + bool m_checking_disabled : 1; + bool m_authenticated_data : 1; + bool m_zero : 1; + bool m_recursion_available : 1; + + NetworkOrdered<u16> m_question_count; + NetworkOrdered<u16> m_answer_count; + NetworkOrdered<u16> m_authority_count; + NetworkOrdered<u16> m_additional_count; +}; + +static_assert(sizeof(DNSPacket) == 12); diff --git a/Services/LookupServer/DNSQuestion.h b/Services/LookupServer/DNSQuestion.h new file mode 100644 index 0000000000..ec696905e6 --- /dev/null +++ b/Services/LookupServer/DNSQuestion.h @@ -0,0 +1,59 @@ +/* + * Copyright (c) 2020, Andreas Kling <kling@serenityos.org> + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#pragma once + +#include <AK/String.h> +#include <AK/Types.h> + +class DNSQuestion { +public: + DNSQuestion(const String& name, u16 record_type, u16 class_code) + : m_name(name) + , m_record_type(record_type) + , m_class_code(class_code) + { + } + + u16 record_type() const { return m_record_type; } + u16 class_code() const { return m_class_code; } + const String& name() const { return m_name; } + + bool operator==(const DNSQuestion& other) const + { + return m_name == other.m_name && m_record_type == other.m_record_type && m_class_code == other.m_class_code; + } + + bool operator!=(const DNSQuestion& other) const + { + return !(*this == other); + } + +private: + String m_name; + u16 m_record_type { 0 }; + u16 m_class_code { 0 }; +}; diff --git a/Services/LookupServer/DNSRequest.cpp b/Services/LookupServer/DNSRequest.cpp new file mode 100644 index 0000000000..79d6ff5aec --- /dev/null +++ b/Services/LookupServer/DNSRequest.cpp @@ -0,0 +1,98 @@ +/* + * Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org> + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#include "DNSRequest.h" +#include "DNSPacket.h" +#include <AK/BufferStream.h> +#include <AK/StringBuilder.h> +#include <arpa/inet.h> +#include <ctype.h> +#include <stdlib.h> + +#define C_IN 1 + +DNSRequest::DNSRequest() + : m_id(arc4random_uniform(UINT16_MAX)) +{ +} + +void DNSRequest::add_question(const String& name, u16 record_type, ShouldRandomizeCase should_randomize_case) +{ + ASSERT(m_questions.size() <= UINT16_MAX); + + if (name.is_empty()) + return; + + StringBuilder builder; + for (size_t i = 0; i < name.length(); ++i) { + u8 ch = name[i]; + if (should_randomize_case == ShouldRandomizeCase::Yes) { + // Randomize the 0x20 bit in every ASCII character. + if (isalpha(ch)) { + if (arc4random_uniform(2)) + ch |= 0x20; + else + ch &= ~0x20; + } + } + builder.append(ch); + } + + if (name[name.length() - 1] != '.') + builder.append('.'); + + m_questions.empend(builder.to_string(), record_type, C_IN); +} + +ByteBuffer DNSRequest::to_byte_buffer() const +{ + DNSPacket request_header; + request_header.set_id(m_id); + request_header.set_is_query(); + request_header.set_opcode(0); + request_header.set_truncated(false); + request_header.set_recursion_desired(true); + request_header.set_question_count(m_questions.size()); + + auto buffer = ByteBuffer::create_uninitialized(m_questions.size() * 4096); + BufferStream stream(buffer); + + stream << ByteBuffer::wrap(&request_header, sizeof(request_header)); + + for (auto& question : m_questions) { + auto parts = question.name().split('.'); + for (auto& part : parts) { + stream << (u8)part.length(); + stream << part; + } + stream << '\0'; + stream << htons(question.record_type()); + stream << htons(question.class_code()); + } + stream.snip(); + + return buffer; +} diff --git a/Services/LookupServer/DNSRequest.h b/Services/LookupServer/DNSRequest.h new file mode 100644 index 0000000000..2b55dd75fb --- /dev/null +++ b/Services/LookupServer/DNSRequest.h @@ -0,0 +1,65 @@ +/* + * Copyright (c) 2020, Andreas Kling <kling@serenityos.org> + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#pragma once + +#include "DNSQuestion.h" +#include <AK/Types.h> +#include <AK/Vector.h> + +#define T_A 1 +#define T_NS 2 +#define T_CNAME 5 +#define T_SOA 6 +#define T_PTR 12 +#define T_MX 15 + +enum class ShouldRandomizeCase { + No = 0, + Yes +}; + +class DNSRequest { +public: + DNSRequest(); + + void add_question(const String& name, u16 record_type, ShouldRandomizeCase); + + const Vector<DNSQuestion>& questions() const { return m_questions; } + + u16 question_count() const + { + ASSERT(m_questions.size() < UINT16_MAX); + return m_questions.size(); + } + + u16 id() const { return m_id; } + ByteBuffer to_byte_buffer() const; + +private: + u16 m_id { 0 }; + Vector<DNSQuestion> m_questions; +}; diff --git a/Services/LookupServer/DNSResponse.cpp b/Services/LookupServer/DNSResponse.cpp new file mode 100644 index 0000000000..614e58db2b --- /dev/null +++ b/Services/LookupServer/DNSResponse.cpp @@ -0,0 +1,147 @@ +/* + * Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org> + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#include "DNSResponse.h" +#include "DNSPacket.h" +#include "DNSRequest.h" +#include <AK/IPv4Address.h> +#include <AK/StringBuilder.h> + +static String parse_dns_name(const u8* data, size_t& offset, size_t max_offset, size_t recursion_level = 0); + +class [[gnu::packed]] DNSRecordWithoutName +{ +public: + DNSRecordWithoutName() {} + + u16 type() const { return m_type; } + u16 record_class() const { return m_class; } + u32 ttl() const { return m_ttl; } + u16 data_length() const { return m_data_length; } + + void* data() { return this + 1; } + const void* data() const { return this + 1; } + +private: + NetworkOrdered<u16> m_type; + NetworkOrdered<u16> m_class; + NetworkOrdered<u32> m_ttl; + NetworkOrdered<u16> m_data_length; +}; + +static_assert(sizeof(DNSRecordWithoutName) == 10); + +Optional<DNSResponse> DNSResponse::from_raw_response(const u8* raw_data, size_t raw_size) +{ + if (raw_size < sizeof(DNSPacket)) { + dbg() << "DNS response not large enough (" << raw_size << " out of " << sizeof(DNSPacket) << ") to be a DNS packet."; + return {}; + } + + auto& response_header = *(const DNSPacket*)(raw_data); + dbgprintf("Got response (ID: %u)\n", response_header.id()); + dbgprintf(" Question count: %u\n", response_header.question_count()); + dbgprintf(" Answer count: %u\n", response_header.answer_count()); + dbgprintf(" Authority count: %u\n", response_header.authority_count()); + dbgprintf("Additional count: %u\n", response_header.additional_count()); + + DNSResponse response; + response.m_id = response_header.id(); + response.m_code = response_header.response_code(); + + if (response.code() != DNSResponse::Code::NOERROR) + return response; + + size_t offset = sizeof(DNSPacket); + + for (u16 i = 0; i < response_header.question_count(); ++i) { + auto name = parse_dns_name(raw_data, offset, raw_size); + struct RawDNSAnswerQuestion { + NetworkOrdered<u16> record_type; + NetworkOrdered<u16> class_code; + }; + auto& record_and_class = *(const RawDNSAnswerQuestion*)&raw_data[offset]; + response.m_questions.empend(name, record_and_class.record_type, record_and_class.class_code); + auto& question = response.m_questions.last(); + offset += 4; + dbg() << "Question #" << i << ": _" << question.name() << "_ type: " << question.record_type() << ", class: " << question.class_code(); + } + + for (u16 i = 0; i < response_header.answer_count(); ++i) { + auto name = parse_dns_name(raw_data, offset, raw_size); + + auto& record = *(const DNSRecordWithoutName*)(&raw_data[offset]); + + String data; + + offset += sizeof(DNSRecordWithoutName); + if (record.type() == T_PTR) { + size_t dummy_offset = offset; + data = parse_dns_name(raw_data, dummy_offset, raw_size); + } else if (record.type() == T_A) { + auto ipv4_address = IPv4Address((const u8*)record.data()); + data = ipv4_address.to_string(); + } else { + // FIXME: Parse some other record types perhaps? + dbg() << " data=(unimplemented record type " << record.type() << ")"; + } + dbg() << "Answer #" << i << ": name=_" << name << "_, type=" << record.type() << ", ttl=" << record.ttl() << ", length=" << record.data_length() << ", data=_" << data << "_"; + response.m_answers.empend(name, record.type(), record.record_class(), record.ttl(), data); + offset += record.data_length(); + } + + return response; +} + +String parse_dns_name(const u8* data, size_t& offset, size_t max_offset, size_t recursion_level) +{ + if (recursion_level > 4) + return {}; + Vector<char, 128> buf; + while (offset < max_offset) { + u8 ch = data[offset]; + if (ch == '\0') { + ++offset; + break; + } + if ((ch & 0xc0) == 0xc0) { + if ((offset + 1) >= max_offset) + return {}; + size_t dummy = (ch & 0x3f) << 8 | data[offset + 1]; + offset += 2; + StringBuilder builder; + builder.append(buf.data(), buf.size()); + auto okay = parse_dns_name(data, dummy, max_offset, recursion_level + 1); + builder.append(okay); + return builder.to_string(); + } + for (size_t i = 0; i < ch; ++i) + buf.append(data[offset + i + 1]); + buf.append('.'); + offset += ch + 1; + } + return String::copy(buf); +} diff --git a/Services/LookupServer/DNSResponse.h b/Services/LookupServer/DNSResponse.h new file mode 100644 index 0000000000..0301c12075 --- /dev/null +++ b/Services/LookupServer/DNSResponse.h @@ -0,0 +1,78 @@ +/* + * Copyright (c) 2020, Andreas Kling <kling@serenityos.org> + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + + +#pragma once + +#include "DNSAnswer.h" +#include "DNSQuestion.h" +#include <AK/Types.h> +#include <AK/Vector.h> +#include <AK/Optional.h> + +class DNSResponse { +public: + static Optional<DNSResponse> from_raw_response(const u8*, size_t); + + u16 id() const { return m_id; } + const Vector<DNSQuestion>& questions() const { return m_questions; } + const Vector<DNSAnswer>& answers() const { return m_answers; } + + u16 question_count() const + { + ASSERT(m_questions.size() <= UINT16_MAX); + return m_questions.size(); + } + + u16 answer_count() const + { + ASSERT(m_answers.size() <= UINT16_MAX); + return m_answers.size(); + } + + enum class Code : u8 { + NOERROR = 0, + FORMERR = 1, + SERVFAIL = 2, + NXDOMAIN = 3, + NOTIMP = 4, + REFUSED = 5, + YXDOMAIN = 6, + XRRSET = 7, + NOTAUTH = 8, + NOTZONE = 9, + }; + + Code code() const { return (Code)m_code; } + +private: + DNSResponse() {} + + u16 m_id { 0 }; + u8 m_code { 0 }; + Vector<DNSQuestion> m_questions; + Vector<DNSAnswer> m_answers; +}; diff --git a/Services/LookupServer/LookupServer.cpp b/Services/LookupServer/LookupServer.cpp new file mode 100644 index 0000000000..fe89bd97fc --- /dev/null +++ b/Services/LookupServer/LookupServer.cpp @@ -0,0 +1,259 @@ +/* + * Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org> + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#include "LookupServer.h" +#include "DNSRequest.h" +#include "DNSResponse.h" +#include <AK/HashMap.h> +#include <AK/String.h> +#include <AK/StringBuilder.h> +#include <LibCore/ConfigFile.h> +#include <LibCore/EventLoop.h> +#include <LibCore/File.h> +#include <LibCore/LocalServer.h> +#include <LibCore/LocalSocket.h> +#include <LibCore/UDPSocket.h> +#include <stdio.h> +#include <unistd.h> + +LookupServer::LookupServer() +{ + auto config = Core::ConfigFile::get_for_system("LookupServer"); + dbg() << "Using network config file at " << config->file_name(); + m_nameserver = config->read_entry("DNS", "Nameserver", "1.1.1.1"); + + load_etc_hosts(); + + m_local_server = Core::LocalServer::construct(this); + m_local_server->on_ready_to_accept = [this]() { + auto socket = m_local_server->accept(); + socket->on_ready_to_read = [this, socket]() { + service_client(socket); + RefPtr<Core::LocalSocket> keeper = socket; + const_cast<Core::LocalSocket&>(*socket).on_ready_to_read = [] {}; + }; + }; + bool ok = m_local_server->take_over_from_system_server(); + ASSERT(ok); +} + +void LookupServer::load_etc_hosts() +{ + auto file = Core::File::construct("/etc/hosts"); + if (!file->open(Core::IODevice::ReadOnly)) + return; + while (!file->eof()) { + auto line = file->read_line(1024); + if (line.is_empty()) + break; + auto str_line = String((const char*)line.data(), line.size() - 1, Chomp); + auto fields = str_line.split('\t'); + + auto sections = fields[0].split('.'); + IPv4Address addr { + (u8)atoi(sections[0].characters()), + (u8)atoi(sections[1].characters()), + (u8)atoi(sections[2].characters()), + (u8)atoi(sections[3].characters()), + }; + + auto name = fields[1]; + m_etc_hosts.set(name, addr.to_string()); + + IPv4Address reverse_addr { + (u8)atoi(sections[3].characters()), + (u8)atoi(sections[2].characters()), + (u8)atoi(sections[1].characters()), + (u8)atoi(sections[0].characters()), + }; + StringBuilder builder; + builder.append(reverse_addr.to_string()); + builder.append(".in-addr.arpa"); + m_etc_hosts.set(builder.to_string(), name); + } +} + +void LookupServer::service_client(RefPtr<Core::LocalSocket> socket) +{ + u8 client_buffer[1024]; + int nrecv = socket->read(client_buffer, sizeof(client_buffer) - 1); + if (nrecv < 0) { + perror("read"); + return; + } + + client_buffer[nrecv] = '\0'; + + char lookup_type = client_buffer[0]; + if (lookup_type != 'L' && lookup_type != 'R') { + dbg() << "Invalid lookup_type " << lookup_type; + return; + } + auto hostname = String((const char*)client_buffer + 1, nrecv - 1, Chomp); + dbg() << "Got request for '" << hostname << "' (using IP " << m_nameserver << ")"; + + Vector<String> responses; + + if (auto known_host = m_etc_hosts.get(hostname); known_host.has_value()) { + responses.append(known_host.value()); + } else if (!hostname.is_empty()) { + bool did_timeout; + int retries = 3; + do { + did_timeout = false; + if (lookup_type == 'L') + responses = lookup(hostname, did_timeout, T_A); + else if (lookup_type == 'R') + responses = lookup(hostname, did_timeout, T_PTR); + if (!did_timeout) + break; + } while (--retries); + if (did_timeout) { + fprintf(stderr, "LookupServer: Out of retries :(\n"); + return; + } + } + + if (responses.is_empty()) { + int nsent = socket->write("Not found.\n"); + if (nsent < 0) + perror("write"); + return; + } + for (auto& response : responses) { + auto line = String::format("%s\n", response.characters()); + int nsent = socket->write(line); + if (nsent < 0) { + perror("write"); + break; + } + } +} + +Vector<String> LookupServer::lookup(const String& hostname, bool& did_timeout, unsigned short record_type, ShouldRandomizeCase should_randomize_case) +{ + if (auto it = m_lookup_cache.find(hostname); it != m_lookup_cache.end()) { + auto& cached_lookup = it->value; + if (cached_lookup.question.record_type() == record_type) { + Vector<String> responses; + for (auto& cached_answer : cached_lookup.answers) { + dbg() << "Cache hit: " << hostname << " -> " << cached_answer.record_data() << ", expired: " << cached_answer.has_expired(); + if (!cached_answer.has_expired()) { + responses.append(cached_answer.record_data()); + } + } + if (!responses.is_empty()) + return responses; + } + m_lookup_cache.remove(it); + } + + DNSRequest request; + request.add_question(hostname, record_type, should_randomize_case); + + auto buffer = request.to_byte_buffer(); + + auto udp_socket = Core::UDPSocket::construct(); + udp_socket->set_blocking(true); + + struct timeval timeout { + 1, 0 + }; + + int rc = setsockopt(udp_socket->fd(), SOL_SOCKET, SO_RCVTIMEO, &timeout, sizeof(timeout)); + if (rc < 0) { + perror("setsockopt(SOL_SOCKET, SO_RCVTIMEO)"); + return {}; + } + + if (!udp_socket->connect(m_nameserver, 53)) + return {}; + + if (!udp_socket->write(buffer)) + return {}; + + u8 response_buffer[4096]; + int nrecv = udp_socket->read(response_buffer, sizeof(response_buffer)); + if (nrecv == 0) + return {}; + + auto o_response = DNSResponse::from_raw_response(response_buffer, nrecv); + if (!o_response.has_value()) + return {}; + + auto& response = o_response.value(); + + if (response.id() != request.id()) { + dbgprintf("LookupServer: ID mismatch (%u vs %u) :(\n", response.id(), request.id()); + return {}; + } + + if (response.code() == DNSResponse::Code::REFUSED) { + if (should_randomize_case == ShouldRandomizeCase::Yes) { + // Retry with 0x20 case randomization turned off. + return lookup(hostname, did_timeout, record_type, ShouldRandomizeCase::No); + } + return {}; + } + + if (response.question_count() != request.question_count()) { + dbgprintf("LookupServer: Question count (%u vs %u) :(\n", response.question_count(), request.question_count()); + return {}; + } + + for (size_t i = 0; i < request.question_count(); ++i) { + auto& request_question = request.questions()[i]; + auto& response_question = response.questions()[i]; + if (request_question != response_question) { + dbg() << "Request and response questions do not match"; + dbg() << " Request: {_" << request_question.name() << "_, " << request_question.record_type() << ", " << request_question.class_code() << "}"; + dbg() << " Response: {_" << response_question.name() << "_, " << response_question.record_type() << ", " << response_question.class_code() << "}"; + return {}; + } + } + + if (response.answer_count() < 1) { + dbgprintf("LookupServer: Not enough answers (%u) :(\n", response.answer_count()); + return {}; + } + + Vector<String, 8> responses; + Vector<DNSAnswer, 8> cacheable_answers; + for (auto& answer : response.answers()) { + if (answer.type() != T_A) + continue; + responses.append(answer.record_data()); + if (!answer.has_expired()) + cacheable_answers.append(answer); + } + + if (!cacheable_answers.is_empty()) { + if (m_lookup_cache.size() >= 256) + m_lookup_cache.remove(m_lookup_cache.begin()); + m_lookup_cache.set(hostname, { request.questions()[0], move(cacheable_answers) }); + } + return responses; +} diff --git a/Services/LookupServer/LookupServer.h b/Services/LookupServer/LookupServer.h new file mode 100644 index 0000000000..4666e0904b --- /dev/null +++ b/Services/LookupServer/LookupServer.h @@ -0,0 +1,56 @@ +/* + * Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org> + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#pragma once + +#include "DNSRequest.h" +#include "DNSResponse.h" +#include <AK/HashMap.h> +#include <LibCore/Object.h> + +class DNSAnswer; + +class LookupServer final : public Core::Object { + C_OBJECT(LookupServer) + +public: + LookupServer(); + +private: + void load_etc_hosts(); + void service_client(RefPtr<Core::LocalSocket>); + Vector<String> lookup(const String& hostname, bool& did_timeout, unsigned short record_type, ShouldRandomizeCase = ShouldRandomizeCase::Yes); + + struct CachedLookup { + DNSQuestion question; + Vector<DNSAnswer> answers; + }; + + RefPtr<Core::LocalServer> m_local_server; + String m_nameserver; + HashMap<String, String> m_etc_hosts; + HashMap<String, CachedLookup> m_lookup_cache; +}; diff --git a/Services/LookupServer/Makefile b/Services/LookupServer/Makefile new file mode 100644 index 0000000000..42a4e1930b --- /dev/null +++ b/Services/LookupServer/Makefile @@ -0,0 +1,12 @@ +OBJS = \ + LookupServer.o \ + DNSRequest.o \ + DNSResponse.o \ + DNSAnswer.o \ + main.o + +PROGRAM = LookupServer + +LIB_DEPS = Core + +include ../../Makefile.common diff --git a/Services/LookupServer/main.cpp b/Services/LookupServer/main.cpp new file mode 100644 index 0000000000..d8f40e114b --- /dev/null +++ b/Services/LookupServer/main.cpp @@ -0,0 +1,53 @@ +/* + * Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org> + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#include "LookupServer.h" +#include <LibCore/EventLoop.h> +#include <LibCore/LocalServer.h> +#include <stdio.h> + +int main(int argc, char** argv) +{ + (void)argc; + (void)argv; + + if (pledge("stdio accept unix inet cpath rpath fattr", nullptr) < 0) { + perror("pledge"); + return 1; + } + + Core::EventLoop event_loop; + LookupServer server; + + if (pledge("stdio accept inet", nullptr) < 0) { + perror("pledge"); + return 1; + } + + unveil(nullptr, nullptr); + + return event_loop.exec(); +} diff --git a/Services/Makefile b/Services/Makefile new file mode 100644 index 0000000000..0025ae2061 --- /dev/null +++ b/Services/Makefile @@ -0,0 +1,3 @@ +SUBDIRS := $(patsubst %/Makefile,%/,$(wildcard */Makefile)) + +include ../Makefile.subdir diff --git a/Services/NotificationServer/ClientConnection.cpp b/Services/NotificationServer/ClientConnection.cpp new file mode 100644 index 0000000000..9b8064de03 --- /dev/null +++ b/Services/NotificationServer/ClientConnection.cpp @@ -0,0 +1,63 @@ +/* + * Copyright (c) 2020, Andreas Kling <kling@serenityos.org> + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#include "ClientConnection.h" +#include "NotificationClientEndpoint.h" +#include "NotificationWindow.h" +#include <AK/HashMap.h> + +namespace NotificationServer { + +static HashMap<int, RefPtr<ClientConnection>> s_connections; + +ClientConnection::ClientConnection(Core::LocalSocket& client_socket, int client_id) + : IPC::ClientConnection<NotificationServerEndpoint>(*this, client_socket, client_id) +{ + s_connections.set(client_id, *this); +} + +ClientConnection::~ClientConnection() +{ +} + +void ClientConnection::die() +{ + s_connections.remove(client_id()); +} + +OwnPtr<Messages::NotificationServer::GreetResponse> ClientConnection::handle(const Messages::NotificationServer::Greet&) +{ + return make<Messages::NotificationServer::GreetResponse>(client_id()); +} + +OwnPtr<Messages::NotificationServer::ShowNotificationResponse> ClientConnection::handle(const Messages::NotificationServer::ShowNotification& message) +{ + auto window = NotificationWindow::construct(message.text(), message.title(), message.icon()); + window->show(); + return make<Messages::NotificationServer::ShowNotificationResponse>(); +} + +} diff --git a/Services/NotificationServer/ClientConnection.h b/Services/NotificationServer/ClientConnection.h new file mode 100644 index 0000000000..bec07fb127 --- /dev/null +++ b/Services/NotificationServer/ClientConnection.h @@ -0,0 +1,49 @@ +/* + * Copyright (c) 2020, Andreas Kling <kling@serenityos.org> + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#pragma once + +#include <LibIPC/ClientConnection.h> +#include <NotificationServer/NotificationServerEndpoint.h> + +namespace NotificationServer { + +class ClientConnection final : public IPC::ClientConnection<NotificationServerEndpoint> + , public NotificationServerEndpoint { + C_OBJECT(ClientConnection) +public: + ~ClientConnection() override; + + virtual void die() override; + +private: + explicit ClientConnection(Core::LocalSocket&, int client_id); + + virtual OwnPtr<Messages::NotificationServer::GreetResponse> handle(const Messages::NotificationServer::Greet&) override; + virtual OwnPtr<Messages::NotificationServer::ShowNotificationResponse> handle(const Messages::NotificationServer::ShowNotification&) override; +}; + +} diff --git a/Services/NotificationServer/Makefile b/Services/NotificationServer/Makefile new file mode 100644 index 0000000000..36a1e41652 --- /dev/null +++ b/Services/NotificationServer/Makefile @@ -0,0 +1,24 @@ +OBJS = \ + main.o \ + ClientConnection.o \ + NotificationWindow.o + +PROGRAM = NotificationServer + +LIB_DEPS = GUI Gfx Core IPC + +EXTRA_CLEAN = NotificationServerEndpoint.h NotificationClientEndpoint.h + +*.cpp: NotificationServerEndpoint.h NotificationClientEndpoint.h + +NotificationServerEndpoint.h: NotificationServer.ipc | IPCCOMPILER + @echo "IPC $<"; $(IPCCOMPILER) $< > $@ + +NotificationClientEndpoint.h: NotificationClient.ipc | IPCCOMPILER + @echo "IPC $<"; $(IPCCOMPILER) $< > $@ + +install: + mkdir -p $(SERENITY_BASE_DIR)/Root/usr/include/NotificationServer/ + cp *.h $(SERENITY_BASE_DIR)/Root/usr/include/NotificationServer/ + +include ../../Makefile.common diff --git a/Services/NotificationServer/NotificationClient.ipc b/Services/NotificationServer/NotificationClient.ipc new file mode 100644 index 0000000000..fc85168f76 --- /dev/null +++ b/Services/NotificationServer/NotificationClient.ipc @@ -0,0 +1,4 @@ +endpoint NotificationClient = 92 +{ + Dummy() =| +} diff --git a/Services/NotificationServer/NotificationServer.ipc b/Services/NotificationServer/NotificationServer.ipc new file mode 100644 index 0000000000..f1ac5e13df --- /dev/null +++ b/Services/NotificationServer/NotificationServer.ipc @@ -0,0 +1,7 @@ +endpoint NotificationServer = 95 +{ + // Basic protocol + Greet() => (i32 client_id) + + ShowNotification(String text, String title, Gfx::ShareableBitmap icon) => () +} diff --git a/Services/NotificationServer/NotificationWindow.cpp b/Services/NotificationServer/NotificationWindow.cpp new file mode 100644 index 0000000000..96b0c2652d --- /dev/null +++ b/Services/NotificationServer/NotificationWindow.cpp @@ -0,0 +1,124 @@ +/* + * Copyright (c) 2020, Andreas Kling <kling@serenityos.org> + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#include "NotificationWindow.h" +#include <AK/Vector.h> +#include <LibGUI/BoxLayout.h> +#include <LibGUI/Button.h> +#include <LibGUI/Desktop.h> +#include <LibGUI/Label.h> +#include <LibGUI/Widget.h> +#include <LibGfx/Bitmap.h> +#include <LibGfx/Font.h> +#include <LibGfx/ShareableBitmap.h> + +namespace NotificationServer { + +static Vector<RefPtr<NotificationWindow>> s_windows; + +void update_notification_window_locations() +{ + Gfx::Rect last_window_rect; + for (auto& window : s_windows) { + Gfx::Point new_window_location; + if (last_window_rect.is_null()) + new_window_location = GUI::Desktop::the().rect().top_right().translated(-window->rect().width() - 24, 26); + else + new_window_location = last_window_rect.bottom_left().translated(0, 10); + if (window->rect().location() != new_window_location) { + window->move_to(new_window_location); + window->set_original_rect(window->rect()); + } + last_window_rect = window->rect(); + } +} + +NotificationWindow::NotificationWindow(const String& text, const String& title, const Gfx::ShareableBitmap& icon) +{ + s_windows.append(this); + + set_window_type(GUI::WindowType::Notification); + set_resizable(false); + set_minimizable(false); + + Gfx::Rect lowest_notification_rect_on_screen; + for (auto& window : s_windows) { + if (window->m_original_rect.y() > lowest_notification_rect_on_screen.y()) + lowest_notification_rect_on_screen = window->m_original_rect; + } + + Gfx::Rect rect; + rect.set_width(220); + rect.set_height(40); + rect.set_location(GUI::Desktop::the().rect().top_right().translated(-rect.width() - 24, 26)); + + if (!lowest_notification_rect_on_screen.is_null()) + rect.set_location(lowest_notification_rect_on_screen.bottom_left().translated(0, 10)); + + set_rect(rect); + + m_original_rect = rect; + + auto& widget = set_main_widget<GUI::Widget>(); + widget.set_fill_with_background_color(true); + + widget.set_layout<GUI::HorizontalBoxLayout>(); + widget.layout()->set_margins({ 8, 8, 8, 8 }); + widget.layout()->set_spacing(6); + + if (icon.is_valid()) { + auto& icon_label = widget.add<GUI::Label>(); + icon_label.set_size_policy(GUI::SizePolicy::Fixed, GUI::SizePolicy::Fixed); + icon_label.set_preferred_size(32, 32); + icon_label.set_icon(icon.bitmap()); + } + + auto& left_container = widget.add<GUI::Widget>(); + left_container.set_layout<GUI::VerticalBoxLayout>(); + + auto& title_label = left_container.add<GUI::Label>(title); + title_label.set_font(Gfx::Font::default_bold_font()); + title_label.set_text_alignment(Gfx::TextAlignment::CenterLeft); + auto& text_label = left_container.add<GUI::Label>(text); + text_label.set_text_alignment(Gfx::TextAlignment::CenterLeft); + + auto& right_container = widget.add<GUI::Widget>(); + right_container.set_size_policy(GUI::SizePolicy::Fixed, GUI::SizePolicy::Fill); + right_container.set_preferred_size(36, 0); + right_container.set_layout<GUI::HorizontalBoxLayout>(); + + on_close_request = [this] { + s_windows.remove_first_matching([this](auto& entry) { return entry == this; }); + update_notification_window_locations(); + return CloseRequestDecision::Close; + }; +} + +NotificationWindow::~NotificationWindow() +{ +} + +} diff --git a/Services/NotificationServer/NotificationWindow.h b/Services/NotificationServer/NotificationWindow.h new file mode 100644 index 0000000000..3519af0752 --- /dev/null +++ b/Services/NotificationServer/NotificationWindow.h @@ -0,0 +1,46 @@ +/* + * Copyright (c) 2020, Andreas Kling <kling@serenityos.org> + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#pragma once + +#include <LibGUI/Window.h> + +namespace NotificationServer { + +class NotificationWindow final : public GUI::Window { + C_OBJECT(NotificationWindow); + +public: + virtual ~NotificationWindow() override; + void set_original_rect(Gfx::Rect original_rect) { m_original_rect = original_rect; }; + +private: + NotificationWindow(const String& text, const String& title, const Gfx::ShareableBitmap&); + + Gfx::Rect m_original_rect; +}; + +} diff --git a/Services/NotificationServer/main.cpp b/Services/NotificationServer/main.cpp new file mode 100644 index 0000000000..f26aeec567 --- /dev/null +++ b/Services/NotificationServer/main.cpp @@ -0,0 +1,71 @@ +/* + * Copyright (c) 2020, Andreas Kling <kling@serenityos.org> + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#include "ClientConnection.h" +#include <LibCore/EventLoop.h> +#include <LibCore/LocalServer.h> +#include <LibGUI/Application.h> +#include <LibGUI/WindowServerConnection.h> +#include <stdio.h> +#include <unistd.h> + +int main(int argc, char** argv) +{ + if (pledge("stdio shared_buffer accept rpath wpath cpath unix fattr", nullptr) < 0) { + perror("pledge"); + return 1; + } + + GUI::Application app(argc, argv); + auto server = Core::LocalServer::construct(); + + bool ok = server->take_over_from_system_server(); + ASSERT(ok); + server->on_ready_to_accept = [&] { + auto client_socket = server->accept(); + if (!client_socket) { + dbg() << "NotificationServer: accept failed."; + return; + } + static int s_next_client_id = 0; + int client_id = ++s_next_client_id; + IPC::new_client_connection<NotificationServer::ClientConnection>(*client_socket, client_id); + }; + + if (unveil("/res", "r") < 0) { + perror("unveil"); + return 1; + } + + unveil(nullptr, nullptr); + + if (pledge("stdio shared_buffer accept rpath", nullptr) < 0) { + perror("pledge"); + return 1; + } + + return app.exec(); +} diff --git a/Services/ProtocolServer/Download.cpp b/Services/ProtocolServer/Download.cpp new file mode 100644 index 0000000000..e4bbce082b --- /dev/null +++ b/Services/ProtocolServer/Download.cpp @@ -0,0 +1,92 @@ +/* + * Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org> + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#include <AK/Badge.h> +#include <ProtocolServer/Download.h> +#include <ProtocolServer/PSClientConnection.h> + +// FIXME: What about rollover? +static i32 s_next_id = 1; + +static HashMap<i32, RefPtr<Download>>& all_downloads() +{ + static HashMap<i32, RefPtr<Download>> map; + return map; +} + +Download* Download::find_by_id(i32 id) +{ + return const_cast<Download*>(all_downloads().get(id).value_or(nullptr)); +} + +Download::Download(PSClientConnection& client) + : m_id(s_next_id++) + , m_client(client.make_weak_ptr()) +{ + all_downloads().set(m_id, this); +} + +Download::~Download() +{ +} + +void Download::stop() +{ + all_downloads().remove(m_id); +} + +void Download::set_payload(const ByteBuffer& payload) +{ + m_payload = payload; + m_total_size = payload.size(); +} + +void Download::set_response_headers(const HashMap<String, String>& response_headers) +{ + m_response_headers = response_headers; +} + +void Download::did_finish(bool success) +{ + if (!m_client) { + dbg() << "Download::did_finish() after the client already disconnected."; + return; + } + m_client->did_finish_download({}, *this, success); + all_downloads().remove(m_id); +} + +void Download::did_progress(Optional<u32> total_size, u32 downloaded_size) +{ + if (!m_client) { + // FIXME: We should also abort the download in this situation, I guess! + dbg() << "Download::did_progress() after the client already disconnected."; + return; + } + m_total_size = total_size; + m_downloaded_size = downloaded_size; + m_client->did_progress_download({}, *this); +} diff --git a/Services/ProtocolServer/Download.h b/Services/ProtocolServer/Download.h new file mode 100644 index 0000000000..5609cd15b4 --- /dev/null +++ b/Services/ProtocolServer/Download.h @@ -0,0 +1,70 @@ +/* + * Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org> + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#pragma once + +#include <AK/ByteBuffer.h> +#include <AK/HashMap.h> +#include <AK/Optional.h> +#include <AK/RefCounted.h> +#include <AK/URL.h> +#include <AK/WeakPtr.h> + +class PSClientConnection; + +class Download : public RefCounted<Download> { +public: + virtual ~Download(); + + static Download* find_by_id(i32); + + i32 id() const { return m_id; } + URL url() const { return m_url; } + + Optional<u32> total_size() const { return m_total_size; } + size_t downloaded_size() const { return m_downloaded_size; } + const ByteBuffer& payload() const { return m_payload; } + const HashMap<String, String>& response_headers() const { return m_response_headers; } + + void stop(); + +protected: + explicit Download(PSClientConnection&); + + void did_finish(bool success); + void did_progress(Optional<u32> total_size, u32 downloaded_size); + void set_payload(const ByteBuffer&); + void set_response_headers(const HashMap<String, String>&); + +private: + i32 m_id; + URL m_url; + Optional<u32> m_total_size {}; + size_t m_downloaded_size { 0 }; + ByteBuffer m_payload; + HashMap<String, String> m_response_headers; + WeakPtr<PSClientConnection> m_client; +}; diff --git a/Services/ProtocolServer/HttpDownload.cpp b/Services/ProtocolServer/HttpDownload.cpp new file mode 100644 index 0000000000..5ffa30362b --- /dev/null +++ b/Services/ProtocolServer/HttpDownload.cpp @@ -0,0 +1,60 @@ +/* + * Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org> + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#include <LibHTTP/HttpJob.h> +#include <LibHTTP/HttpResponse.h> +#include <ProtocolServer/HttpDownload.h> + +HttpDownload::HttpDownload(PSClientConnection& client, NonnullRefPtr<HTTP::HttpJob>&& job) + : Download(client) + , m_job(job) +{ + m_job->on_finish = [this](bool success) { + if (auto* response = m_job->response()) { + set_payload(response->payload()); + set_response_headers(response->headers()); + } + + // if we didn't know the total size, pretend that the download finished successfully + // and set the total size to the downloaded size + if (!total_size().has_value()) + did_progress(downloaded_size(), downloaded_size()); + + did_finish(success); + }; + m_job->on_progress = [this](Optional<u32> total, u32 current) { + did_progress(total, current); + }; +} + +HttpDownload::~HttpDownload() +{ +} + +NonnullRefPtr<HttpDownload> HttpDownload::create_with_job(Badge<HttpProtocol>, PSClientConnection& client, NonnullRefPtr<HTTP::HttpJob>&& job) +{ + return adopt(*new HttpDownload(client, move(job))); +} diff --git a/Services/ProtocolServer/HttpDownload.h b/Services/ProtocolServer/HttpDownload.h new file mode 100644 index 0000000000..364fe6aef2 --- /dev/null +++ b/Services/ProtocolServer/HttpDownload.h @@ -0,0 +1,45 @@ +/* + * Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org> + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#pragma once + +#include <AK/Badge.h> +#include <LibCore/Forward.h> +#include <LibHTTP/HttpJob.h> +#include <ProtocolServer/Download.h> + +class HttpProtocol; + +class HttpDownload final : public Download { +public: + virtual ~HttpDownload() override; + static NonnullRefPtr<HttpDownload> create_with_job(Badge<HttpProtocol>, PSClientConnection&, NonnullRefPtr<HTTP::HttpJob>&&); + +private: + explicit HttpDownload(PSClientConnection&, NonnullRefPtr<HTTP::HttpJob>&&); + + NonnullRefPtr<HTTP::HttpJob> m_job; +}; diff --git a/Services/ProtocolServer/HttpProtocol.cpp b/Services/ProtocolServer/HttpProtocol.cpp new file mode 100644 index 0000000000..1513a32e66 --- /dev/null +++ b/Services/ProtocolServer/HttpProtocol.cpp @@ -0,0 +1,50 @@ +/* + * Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org> + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#include <LibHTTP/HttpJob.h> +#include <LibHTTP/HttpRequest.h> +#include <ProtocolServer/HttpDownload.h> +#include <ProtocolServer/HttpProtocol.h> + +HttpProtocol::HttpProtocol() + : Protocol("http") +{ +} + +HttpProtocol::~HttpProtocol() +{ +} + +RefPtr<Download> HttpProtocol::start_download(PSClientConnection& client, const URL& url) +{ + HTTP::HttpRequest request; + request.set_method(HTTP::HttpRequest::Method::GET); + request.set_url(url); + auto job = request.schedule(); + if (!job) + return nullptr; + return HttpDownload::create_with_job({}, client, (HTTP::HttpJob&)*job); +} diff --git a/Services/ProtocolServer/HttpProtocol.h b/Services/ProtocolServer/HttpProtocol.h new file mode 100644 index 0000000000..ea6c15d2a4 --- /dev/null +++ b/Services/ProtocolServer/HttpProtocol.h @@ -0,0 +1,37 @@ +/* + * Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org> + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#pragma once + +#include <ProtocolServer/Protocol.h> + +class HttpProtocol final : public Protocol { +public: + HttpProtocol(); + virtual ~HttpProtocol() override; + + virtual RefPtr<Download> start_download(PSClientConnection&, const URL&) override; +}; diff --git a/Services/ProtocolServer/HttpsDownload.cpp b/Services/ProtocolServer/HttpsDownload.cpp new file mode 100644 index 0000000000..8e068c2b04 --- /dev/null +++ b/Services/ProtocolServer/HttpsDownload.cpp @@ -0,0 +1,60 @@ +/* + * Copyright (c) 2020, The SerenityOS developers. + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#include <LibHTTP/HttpResponse.h> +#include <LibHTTP/HttpsJob.h> +#include <ProtocolServer/HttpsDownload.h> + +HttpsDownload::HttpsDownload(PSClientConnection& client, NonnullRefPtr<HTTP::HttpsJob>&& job) + : Download(client) + , m_job(job) +{ + m_job->on_finish = [this](bool success) { + if (auto* response = m_job->response()) { + set_payload(response->payload()); + set_response_headers(response->headers()); + } + + // if we didn't know the total size, pretend that the download finished successfully + // and set the total size to the downloaded size + if (!total_size().has_value()) + did_progress(downloaded_size(), downloaded_size()); + + did_finish(success); + }; + m_job->on_progress = [this](Optional<u32> total, u32 current) { + did_progress(total, current); + }; +} + +HttpsDownload::~HttpsDownload() +{ +} + +NonnullRefPtr<HttpsDownload> HttpsDownload::create_with_job(Badge<HttpsProtocol>, PSClientConnection& client, NonnullRefPtr<HTTP::HttpsJob>&& job) +{ + return adopt(*new HttpsDownload(client, move(job))); +} diff --git a/Services/ProtocolServer/HttpsDownload.h b/Services/ProtocolServer/HttpsDownload.h new file mode 100644 index 0000000000..a6c75bc4d7 --- /dev/null +++ b/Services/ProtocolServer/HttpsDownload.h @@ -0,0 +1,45 @@ +/* + * Copyright (c) 2020, The SerenityOS developers. + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#pragma once + +#include <AK/Badge.h> +#include <LibCore/Forward.h> +#include <LibHTTP/HttpsJob.h> +#include <ProtocolServer/Download.h> + +class HttpsProtocol; + +class HttpsDownload final : public Download { +public: + virtual ~HttpsDownload() override; + static NonnullRefPtr<HttpsDownload> create_with_job(Badge<HttpsProtocol>, PSClientConnection&, NonnullRefPtr<HTTP::HttpsJob>&&); + +private: + explicit HttpsDownload(PSClientConnection&, NonnullRefPtr<HTTP::HttpsJob>&&); + + NonnullRefPtr<HTTP::HttpsJob> m_job; +}; diff --git a/Services/ProtocolServer/HttpsProtocol.cpp b/Services/ProtocolServer/HttpsProtocol.cpp new file mode 100644 index 0000000000..07020affe5 --- /dev/null +++ b/Services/ProtocolServer/HttpsProtocol.cpp @@ -0,0 +1,50 @@ +/* + * Copyright (c) 2018-2020, The SerenityOS developers. + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#include <LibHTTP/HttpRequest.h> +#include <LibHTTP/HttpsJob.h> +#include <ProtocolServer/HttpsDownload.h> +#include <ProtocolServer/HttpsProtocol.h> + +HttpsProtocol::HttpsProtocol() + : Protocol("https") +{ +} + +HttpsProtocol::~HttpsProtocol() +{ +} + +RefPtr<Download> HttpsProtocol::start_download(PSClientConnection& client, const URL& url) +{ + HTTP::HttpRequest request; + request.set_method(HTTP::HttpRequest::Method::GET); + request.set_url(url); + auto job = HTTP::HttpsJob::construct(request); + auto download = HttpsDownload::create_with_job({}, client, (HTTP::HttpsJob&)*job); + job->start(); + return download; +} diff --git a/Services/ProtocolServer/HttpsProtocol.h b/Services/ProtocolServer/HttpsProtocol.h new file mode 100644 index 0000000000..e446178751 --- /dev/null +++ b/Services/ProtocolServer/HttpsProtocol.h @@ -0,0 +1,37 @@ +/* + * Copyright (c) 2018-2020, The SerenityOS developers. + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#pragma once + +#include <ProtocolServer/Protocol.h> + +class HttpsProtocol final : public Protocol { +public: + HttpsProtocol(); + virtual ~HttpsProtocol() override; + + virtual RefPtr<Download> start_download(PSClientConnection&, const URL&) override; +}; diff --git a/Services/ProtocolServer/Makefile b/Services/ProtocolServer/Makefile new file mode 100644 index 0000000000..fa39255c53 --- /dev/null +++ b/Services/ProtocolServer/Makefile @@ -0,0 +1,25 @@ +OBJS = \ + PSClientConnection.o \ + Protocol.o \ + Download.o \ + HttpProtocol.o \ + HttpDownload.o \ + HttpsProtocol.o \ + HttpsDownload.o \ + main.o + +PROGRAM = ProtocolServer + +LIB_DEPS = HTTP TLS Crypto Core IPC + +EXTRA_CLEAN = ProtocolServerEndpoint.h ProtocolClientEndpoint.h + +*.cpp: ProtocolServerEndpoint.h ProtocolClientEndpoint.h + +ProtocolServerEndpoint.h: ProtocolServer.ipc | IPCCOMPILER + @echo "IPC $<"; $(IPCCOMPILER) $< > $@ + +ProtocolClientEndpoint.h: ProtocolClient.ipc | IPCCOMPILER + @echo "IPC $<"; $(IPCCOMPILER) $< > $@ + +include ../../Makefile.common diff --git a/Services/ProtocolServer/PSClientConnection.cpp b/Services/ProtocolServer/PSClientConnection.cpp new file mode 100644 index 0000000000..1d07fdb8b2 --- /dev/null +++ b/Services/ProtocolServer/PSClientConnection.cpp @@ -0,0 +1,112 @@ +/* + * Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org> + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#include <AK/Badge.h> +#include <AK/SharedBuffer.h> +#include <ProtocolServer/Download.h> +#include <ProtocolServer/PSClientConnection.h> +#include <ProtocolServer/Protocol.h> +#include <ProtocolServer/ProtocolClientEndpoint.h> + +static HashMap<int, RefPtr<PSClientConnection>> s_connections; + +PSClientConnection::PSClientConnection(Core::LocalSocket& socket, int client_id) + : IPC::ClientConnection<ProtocolServerEndpoint>(*this, socket, client_id) +{ + s_connections.set(client_id, *this); +} + +PSClientConnection::~PSClientConnection() +{ +} + +void PSClientConnection::die() +{ + s_connections.remove(client_id()); +} + +OwnPtr<Messages::ProtocolServer::IsSupportedProtocolResponse> PSClientConnection::handle(const Messages::ProtocolServer::IsSupportedProtocol& message) +{ + bool supported = Protocol::find_by_name(message.protocol().to_lowercase()); + return make<Messages::ProtocolServer::IsSupportedProtocolResponse>(supported); +} + +OwnPtr<Messages::ProtocolServer::StartDownloadResponse> PSClientConnection::handle(const Messages::ProtocolServer::StartDownload& message) +{ + URL url(message.url()); + if (!url.is_valid()) + return make<Messages::ProtocolServer::StartDownloadResponse>(-1); + auto* protocol = Protocol::find_by_name(url.protocol()); + if (!protocol) + return make<Messages::ProtocolServer::StartDownloadResponse>(-1); + auto download = protocol->start_download(*this, url); + return make<Messages::ProtocolServer::StartDownloadResponse>(download->id()); +} + +OwnPtr<Messages::ProtocolServer::StopDownloadResponse> PSClientConnection::handle(const Messages::ProtocolServer::StopDownload& message) +{ + auto* download = Download::find_by_id(message.download_id()); + bool success = false; + if (download) { + download->stop(); + success = true; + } + return make<Messages::ProtocolServer::StopDownloadResponse>(success); +} + +void PSClientConnection::did_finish_download(Badge<Download>, Download& download, bool success) +{ + RefPtr<SharedBuffer> buffer; + if (success && download.payload().size() > 0 && !download.payload().is_null()) { + buffer = SharedBuffer::create_with_size(download.payload().size()); + memcpy(buffer->data(), download.payload().data(), download.payload().size()); + buffer->seal(); + buffer->share_with(client_pid()); + m_shared_buffers.set(buffer->shbuf_id(), buffer); + } + ASSERT(download.total_size().has_value()); + + IPC::Dictionary response_headers; + for (auto& it : download.response_headers()) + response_headers.add(it.key, it.value); + post_message(Messages::ProtocolClient::DownloadFinished(download.id(), success, download.total_size().value(), buffer ? buffer->shbuf_id() : -1, response_headers)); +} + +void PSClientConnection::did_progress_download(Badge<Download>, Download& download) +{ + post_message(Messages::ProtocolClient::DownloadProgress(download.id(), download.total_size(), download.downloaded_size())); +} + +OwnPtr<Messages::ProtocolServer::GreetResponse> PSClientConnection::handle(const Messages::ProtocolServer::Greet&) +{ + return make<Messages::ProtocolServer::GreetResponse>(client_id()); +} + +OwnPtr<Messages::ProtocolServer::DisownSharedBufferResponse> PSClientConnection::handle(const Messages::ProtocolServer::DisownSharedBuffer& message) +{ + m_shared_buffers.remove(message.shbuf_id()); + return make<Messages::ProtocolServer::DisownSharedBufferResponse>(); +} diff --git a/Services/ProtocolServer/PSClientConnection.h b/Services/ProtocolServer/PSClientConnection.h new file mode 100644 index 0000000000..56a7c63c0c --- /dev/null +++ b/Services/ProtocolServer/PSClientConnection.h @@ -0,0 +1,55 @@ +/* + * Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org> + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#pragma once + +#include <AK/HashMap.h> +#include <LibIPC/ClientConnection.h> +#include <ProtocolServer/ProtocolServerEndpoint.h> + +class Download; + +class PSClientConnection final : public IPC::ClientConnection<ProtocolServerEndpoint> + , public ProtocolServerEndpoint { + C_OBJECT(PSClientConnection) +public: + explicit PSClientConnection(Core::LocalSocket&, int client_id); + ~PSClientConnection() override; + + virtual void die() override; + + void did_finish_download(Badge<Download>, Download&, bool success); + void did_progress_download(Badge<Download>, Download&); + +private: + virtual OwnPtr<Messages::ProtocolServer::GreetResponse> handle(const Messages::ProtocolServer::Greet&) override; + virtual OwnPtr<Messages::ProtocolServer::IsSupportedProtocolResponse> handle(const Messages::ProtocolServer::IsSupportedProtocol&) override; + virtual OwnPtr<Messages::ProtocolServer::StartDownloadResponse> handle(const Messages::ProtocolServer::StartDownload&) override; + virtual OwnPtr<Messages::ProtocolServer::StopDownloadResponse> handle(const Messages::ProtocolServer::StopDownload&) override; + virtual OwnPtr<Messages::ProtocolServer::DisownSharedBufferResponse> handle(const Messages::ProtocolServer::DisownSharedBuffer&) override; + + HashMap<i32, RefPtr<AK::SharedBuffer>> m_shared_buffers; +}; diff --git a/Services/ProtocolServer/Protocol.cpp b/Services/ProtocolServer/Protocol.cpp new file mode 100644 index 0000000000..bf53895eaa --- /dev/null +++ b/Services/ProtocolServer/Protocol.cpp @@ -0,0 +1,49 @@ +/* + * Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org> + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#include <AK/HashMap.h> +#include <ProtocolServer/Protocol.h> + +static HashMap<String, Protocol*>& all_protocols() +{ + static HashMap<String, Protocol*> map; + return map; +} + +Protocol* Protocol::find_by_name(const String& name) +{ + return all_protocols().get(name).value_or(nullptr); +} + +Protocol::Protocol(const String& name) +{ + all_protocols().set(name, this); +} + +Protocol::~Protocol() +{ + ASSERT_NOT_REACHED(); +} diff --git a/Services/ProtocolServer/Protocol.h b/Services/ProtocolServer/Protocol.h new file mode 100644 index 0000000000..19bd38f517 --- /dev/null +++ b/Services/ProtocolServer/Protocol.h @@ -0,0 +1,49 @@ +/* + * Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org> + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#pragma once + +#include <AK/RefPtr.h> +#include <AK/URL.h> + +class Download; +class PSClientConnection; + +class Protocol { +public: + virtual ~Protocol(); + + const String& name() const { return m_name; } + virtual RefPtr<Download> start_download(PSClientConnection&, const URL&) = 0; + + static Protocol* find_by_name(const String&); + +protected: + explicit Protocol(const String& name); + +private: + String m_name; +}; diff --git a/Services/ProtocolServer/ProtocolClient.ipc b/Services/ProtocolServer/ProtocolClient.ipc new file mode 100644 index 0000000000..575b3ddd2b --- /dev/null +++ b/Services/ProtocolServer/ProtocolClient.ipc @@ -0,0 +1,6 @@ +endpoint ProtocolClient = 13 +{ + // Download notifications + DownloadProgress(i32 download_id, Optional<u32> total_size, u32 downloaded_size) =| + DownloadFinished(i32 download_id, bool success, u32 total_size, i32 shbuf_id, IPC::Dictionary response_headers) =| +} diff --git a/Services/ProtocolServer/ProtocolServer.ipc b/Services/ProtocolServer/ProtocolServer.ipc new file mode 100644 index 0000000000..74ec138ab5 --- /dev/null +++ b/Services/ProtocolServer/ProtocolServer.ipc @@ -0,0 +1,15 @@ +endpoint ProtocolServer = 9 +{ + // Basic protocol + Greet() => (i32 client_id) + + // FIXME: It would be nice if the kernel provided a way to avoid this + DisownSharedBuffer(i32 shbuf_id) => () + + // Test if a specific protocol is supported, e.g "http" + IsSupportedProtocol(String protocol) => (bool supported) + + // Download API + StartDownload(String url) => (i32 download_id) + StopDownload(i32 download_id) => (bool success) +} diff --git a/Services/ProtocolServer/main.cpp b/Services/ProtocolServer/main.cpp new file mode 100644 index 0000000000..9a3da67b78 --- /dev/null +++ b/Services/ProtocolServer/main.cpp @@ -0,0 +1,71 @@ +/* + * Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org> + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#include <LibCore/EventLoop.h> +#include <LibCore/LocalServer.h> +#include <LibIPC/ClientConnection.h> +#include <ProtocolServer/HttpProtocol.h> +#include <ProtocolServer/HttpsProtocol.h> +#include <ProtocolServer/PSClientConnection.h> + +int main(int, char**) +{ + if (pledge("stdio inet shared_buffer accept unix rpath cpath fattr", nullptr) < 0) { + perror("pledge"); + return 1; + } + Core::EventLoop event_loop; + // FIXME: Establish a connection to LookupServer and then drop "unix"? + if (pledge("stdio inet shared_buffer accept unix", nullptr) < 0) { + perror("pledge"); + return 1; + } + if (unveil("/tmp/portal/lookup", "rw") < 0) { + perror("unveil"); + return 1; + } + if (unveil(nullptr, nullptr) < 0) { + perror("unveil"); + return 1; + } + + (void)*new HttpProtocol; + (void)*new HttpsProtocol; + auto server = Core::LocalServer::construct(); + bool ok = server->take_over_from_system_server(); + ASSERT(ok); + server->on_ready_to_accept = [&] { + auto client_socket = server->accept(); + if (!client_socket) { + dbg() << "ProtocolServer: accept failed."; + return; + } + static int s_next_client_id = 0; + int client_id = ++s_next_client_id; + IPC::new_client_connection<PSClientConnection>(*client_socket, client_id); + }; + return event_loop.exec(); +} diff --git a/Services/SystemServer/Makefile b/Services/SystemServer/Makefile new file mode 100644 index 0000000000..0a29c53463 --- /dev/null +++ b/Services/SystemServer/Makefile @@ -0,0 +1,13 @@ +OBJS = \ + Service.o \ + main.o + +PROGRAM = SystemServer + +LIB_DEPS = Core + +install: + mkdir -p ../../Root/usr/include/SystemServer/ + cp *.h ../../Root/usr/include/SystemServer/ + +include ../../Makefile.common diff --git a/Services/SystemServer/Service.cpp b/Services/SystemServer/Service.cpp new file mode 100644 index 0000000000..1f917ba1e6 --- /dev/null +++ b/Services/SystemServer/Service.cpp @@ -0,0 +1,365 @@ +/* + * Copyright (c) 2019-2020, Sergey Bugaev <bugaevc@serenityos.org> + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#include "Service.h" +#include <AK/HashMap.h> +#include <AK/JsonArray.h> +#include <AK/JsonObject.h> +#include <LibCore/ConfigFile.h> +#include <LibCore/LocalSocket.h> +#include <fcntl.h> +#include <grp.h> +#include <libgen.h> +#include <pwd.h> +#include <sched.h> +#include <stdio.h> +#include <sys/ioctl.h> +#include <sys/stat.h> +#include <unistd.h> + +struct UidAndGids { + uid_t uid; + gid_t gid; + Vector<gid_t> extra_gids; +}; + +static HashMap<String, UidAndGids>* s_user_map; +static HashMap<pid_t, Service*> s_service_map; + +void Service::resolve_user() +{ + if (s_user_map == nullptr) { + s_user_map = new HashMap<String, UidAndGids>; + for (struct passwd* passwd = getpwent(); passwd; passwd = getpwent()) { + Vector<gid_t> extra_gids; + for (struct group* group = getgrent(); group; group = getgrent()) { + for (size_t m = 0; group->gr_mem[m]; ++m) { + if (!strcmp(group->gr_mem[m], passwd->pw_name)) + extra_gids.append(group->gr_gid); + } + } + endgrent(); + s_user_map->set(passwd->pw_name, { passwd->pw_uid, passwd->pw_gid, move(extra_gids) }); + } + endpwent(); + } + + auto user = s_user_map->get(m_user); + if (!user.has_value()) { + dbg() << "Failed to resolve user name " << m_user; + ASSERT_NOT_REACHED(); + } + m_uid = user.value().uid; + m_gid = user.value().gid; + m_extra_gids = user.value().extra_gids; +} + +Service* Service::find_by_pid(pid_t pid) +{ + auto it = s_service_map.find(pid); + if (it == s_service_map.end()) + return nullptr; + return (*it).value; +} + +static int ensure_parent_directories(const char* path) +{ + ASSERT(path[0] == '/'); + + char* parent_buffer = strdup(path); + const char* parent = dirname(parent_buffer); + + int rc = 0; + while (true) { + int rc = mkdir(parent, 0755); + + if (rc == 0) + break; + + if (errno != ENOENT) + break; + + ensure_parent_directories(parent); + }; + + free(parent_buffer); + return rc; +} + +void Service::setup_socket() +{ + ASSERT(!m_socket_path.is_null()); + ASSERT(m_socket_fd == -1); + + ensure_parent_directories(m_socket_path.characters()); + + // Note: we use SOCK_CLOEXEC here to make sure we don't leak every socket to + // all the clients. We'll make the one we do need to pass down !CLOEXEC later + // after forking off the process. + m_socket_fd = socket(AF_LOCAL, SOCK_STREAM | SOCK_NONBLOCK | SOCK_CLOEXEC, 0); + if (m_socket_fd < 0) { + perror("socket"); + ASSERT_NOT_REACHED(); + } + + if (fchown(m_socket_fd, m_uid, m_gid) < 0) { + perror("fchown"); + ASSERT_NOT_REACHED(); + } + + if (fchmod(m_socket_fd, m_socket_permissions) < 0) { + perror("fchmod"); + ASSERT_NOT_REACHED(); + } + + auto socket_address = Core::SocketAddress::local(m_socket_path); + auto un = socket_address.to_sockaddr_un(); + int rc = bind(m_socket_fd, (const sockaddr*)&un, sizeof(un)); + if (rc < 0) { + perror("bind"); + ASSERT_NOT_REACHED(); + } + + rc = listen(m_socket_fd, 16); + if (rc < 0) { + perror("listen"); + ASSERT_NOT_REACHED(); + } +} + +void Service::setup_notifier() +{ + ASSERT(m_lazy); + ASSERT(m_socket_fd >= 0); + ASSERT(!m_socket_notifier); + + m_socket_notifier = Core::Notifier::construct(m_socket_fd, Core::Notifier::Event::Read, this); + m_socket_notifier->on_ready_to_read = [this] { + dbg() << "Ready to read on behalf of " << name(); + remove_child(*m_socket_notifier); + m_socket_notifier = nullptr; + spawn(); + }; +} + +void Service::activate() +{ + ASSERT(m_pid < 0); + + if (m_lazy) + setup_notifier(); + else + spawn(); +} + +void Service::spawn() +{ + dbg() << "Spawning " << name(); + + m_run_timer.start(); + m_pid = fork(); + + if (m_pid < 0) { + perror("fork"); + ASSERT_NOT_REACHED(); + } else if (m_pid == 0) { + // We are the child. + + if (!m_working_directory.is_null()) { + if (chdir(m_working_directory.characters()) < 0) { + perror("chdir"); + ASSERT_NOT_REACHED(); + } + } + + struct sched_param p; + p.sched_priority = m_priority; + int rc = sched_setparam(0, &p); + if (rc < 0) { + perror("sched_setparam"); + ASSERT_NOT_REACHED(); + } + + if (!m_stdio_file_path.is_null()) { + close(STDIN_FILENO); + int fd = open_with_path_length(m_stdio_file_path.characters(), m_stdio_file_path.length(), O_RDWR, 0); + ASSERT(fd <= 0); + if (fd < 0) { + perror("open"); + ASSERT_NOT_REACHED(); + } + dup2(STDIN_FILENO, STDOUT_FILENO); + dup2(STDIN_FILENO, STDERR_FILENO); + + if (isatty(STDIN_FILENO)) { + ioctl(STDIN_FILENO, TIOCSCTTY); + } + } else { + if (isatty(STDIN_FILENO)) { + ioctl(STDIN_FILENO, TIOCNOTTY); + } + close(STDIN_FILENO); + close(STDOUT_FILENO); + close(STDERR_FILENO); + + int fd = open("/dev/null", O_RDWR); + ASSERT(fd == STDIN_FILENO); + dup2(STDIN_FILENO, STDOUT_FILENO); + dup2(STDIN_FILENO, STDERR_FILENO); + } + + if (!m_socket_path.is_null()) { + ASSERT(m_socket_fd > 2); + dup2(m_socket_fd, 3); + // The new descriptor is !CLOEXEC here. + // This is true even if m_socket_fd == 3. + setenv("SOCKET_TAKEOVER", "1", true); + } + + if (!m_user.is_null()) { + if (setgid(m_gid) < 0 || setgroups(m_extra_gids.size(), m_extra_gids.data()) < 0 || setuid(m_uid) < 0) { + dbgprintf("Failed to drop privileges (GID=%u, UID=%u)\n", m_gid, m_uid); + exit(1); + } + } + + char* argv[m_extra_arguments.size() + 2]; + argv[0] = const_cast<char*>(m_executable_path.characters()); + for (size_t i = 0; i < m_extra_arguments.size(); i++) + argv[i + 1] = const_cast<char*>(m_extra_arguments[i].characters()); + argv[m_extra_arguments.size() + 1] = nullptr; + + rc = execv(argv[0], argv); + perror("exec"); + ASSERT_NOT_REACHED(); + } else { + // We are the parent. + s_service_map.set(m_pid, this); + } +} + +void Service::did_exit(int exit_code) +{ + ASSERT(m_pid > 0); + + dbg() << "Service " << name() << " has exited with exit code " << exit_code; + + s_service_map.remove(m_pid); + m_pid = -1; + + if (!m_keep_alive) + return; + + int run_time_in_msec = m_run_timer.elapsed(); + bool exited_successfully = exit_code == 0; + + if (!exited_successfully && run_time_in_msec < 1000) { + switch (m_restart_attempts) { + case 0: + dbg() << "Trying again"; + break; + case 1: + dbg() << "Third time's a charm?"; + break; + default: + dbg() << "Giving up on " << name() << ". Good luck!"; + return; + } + m_restart_attempts++; + } + + activate(); +} + +Service::Service(const Core::ConfigFile& config, const StringView& name) + : Core::Object(nullptr) +{ + ASSERT(config.has_group(name)); + + set_name(name); + m_executable_path = config.read_entry(name, "Executable", String::format("/bin/%s", this->name().characters())); + m_extra_arguments = config.read_entry(name, "Arguments", "").split(' '); + m_stdio_file_path = config.read_entry(name, "StdIO"); + + String prio = config.read_entry(name, "Priority"); + if (prio == "low") + m_priority = 10; + else if (prio == "normal" || prio.is_null()) + m_priority = 30; + else if (prio == "high") + m_priority = 50; + else + ASSERT_NOT_REACHED(); + + m_keep_alive = config.read_bool_entry(name, "KeepAlive"); + m_lazy = config.read_bool_entry(name, "Lazy"); + + m_user = config.read_entry(name, "User"); + if (!m_user.is_null()) + resolve_user(); + + m_socket_path = config.read_entry(name, "Socket"); + if (!m_socket_path.is_null()) { + auto socket_permissions_string = config.read_entry(name, "SocketPermissions", "0600"); + m_socket_permissions = strtol(socket_permissions_string.characters(), nullptr, 8) & 04777; + setup_socket(); + } + + m_working_directory = config.read_entry(name, "WorkingDirectory"); +} + +void Service::save_to(JsonObject& json) +{ + Core::Object::save_to(json); + + json.set("executable_path", m_executable_path); + + // FIXME: This crashes Inspector. + /* + JsonArray extra_args; + for (String& arg : m_extra_arguments) + extra_args.append(arg); + json.set("extra_arguments", move(extra_args)); + */ + + json.set("stdio_file_path", m_stdio_file_path); + json.set("priority", m_priority); + json.set("keep_alive", m_keep_alive); + json.set("socket_path", m_socket_path); + json.set("socket_permissions", m_socket_permissions); + json.set("lazy", m_lazy); + json.set("user", m_user); + json.set("uid", m_uid); + json.set("gid", m_gid); + + if (m_pid > 0) + json.set("pid", m_pid); + else + json.set("pid", nullptr); + + json.set("restart_attempts", m_restart_attempts); + json.set("working_directory", m_working_directory); +} diff --git a/Services/SystemServer/Service.h b/Services/SystemServer/Service.h new file mode 100644 index 0000000000..de408ec04d --- /dev/null +++ b/Services/SystemServer/Service.h @@ -0,0 +1,90 @@ +/* + * Copyright (c) 2019-2020, Sergey Bugaev <bugaevc@serenityos.org> + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#pragma once + +#include <AK/RefPtr.h> +#include <AK/String.h> +#include <LibCore/ElapsedTimer.h> +#include <LibCore/Notifier.h> +#include <LibCore/Object.h> + +class Service final : public Core::Object { + C_OBJECT(Service) + +public: + void activate(); + void did_exit(int exit_code); + + static Service* find_by_pid(pid_t); + + void save_to(AK::JsonObject&) override; + +private: + Service(const Core::ConfigFile&, const StringView& name); + + void spawn(); + + // Path to the executable. By default this is /bin/{m_name}. + String m_executable_path; + // Extra arguments, starting from argv[1], to pass when exec'ing. + Vector<String> m_extra_arguments; + // File path to open as stdio fds. + String m_stdio_file_path; + int m_priority { 1 }; + // Whether we should re-launch it if it exits. + bool m_keep_alive { false }; + // Path to the socket to create and listen on on behalf of this service. + String m_socket_path; + // File system permissions for the socket. + mode_t m_socket_permissions { 0 }; + // Whether we should only spawn this service once somebody connects to the socket. + bool m_lazy; + // The name of the user we should run this service as. + String m_user; + uid_t m_uid { 0 }; + gid_t m_gid { 0 }; + Vector<gid_t> m_extra_gids; + + // PID of the running instance of this service. + pid_t m_pid { -1 }; + // An open fd to the socket. + int m_socket_fd { -1 }; + RefPtr<Core::Notifier> m_socket_notifier; + + // Timer since we last spawned the service. + Core::ElapsedTimer m_run_timer; + // How many times we have tried to restart this service, only counting those + // times where it has exited unsuccessfully and too quickly. + int m_restart_attempts { 0 }; + + // The working directory in which to spawn the service + String m_working_directory; + + void resolve_user(); + void setup_socket(); + void setup_notifier(); +}; diff --git a/Services/SystemServer/main.cpp b/Services/SystemServer/main.cpp new file mode 100644 index 0000000000..0d49d0d937 --- /dev/null +++ b/Services/SystemServer/main.cpp @@ -0,0 +1,139 @@ +/* + * Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org> + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#include "Service.h" +#include <AK/Assertions.h> +#include <AK/ByteBuffer.h> +#include <LibCore/ConfigFile.h> +#include <LibCore/Event.h> +#include <LibCore/EventLoop.h> +#include <LibCore/File.h> +#include <errno.h> +#include <signal.h> +#include <stdio.h> +#include <sys/types.h> +#include <sys/wait.h> +#include <unistd.h> + +static void sigchld_handler(int) +{ + int status = 0; + pid_t pid = waitpid(-1, &status, WNOHANG); + if (!pid) + return; + + dbg() << "Reaped child with pid " << pid; + Service* service = Service::find_by_pid(pid); + if (service == nullptr) { + dbg() << "There was no service with this pid, what is going on?"; + return; + } + + // Call service->did_exit(status) some time soon. + // We wouldn't want to run the complex logic, such + // as possibly spawning the service again, from the + // signal handler, so defer it. + Core::EventLoop::main().post_event(*service, make<Core::DeferredInvocationEvent>([=](auto&) { + service->did_exit(status); + })); + Core::EventLoop::wake(); +} + +static void check_for_test_mode() +{ + auto f = Core::File::construct("/proc/cmdline"); + if (!f->open(Core::IODevice::ReadOnly)) { + dbg() << "Failed to read command line: " << f->error_string(); + ASSERT(false); + } + const String cmd = String::copy(f->read_all()); + dbg() << "Read command line: " << cmd; + if (cmd.matches("*testmode=1*")) { + // Eventually, we should run a test binary and wait for it to finish + // before shutting down. But this is good enough for now. + dbg() << "Waiting for testmode shutdown..."; + sleep(5); + dbg() << "Shutting down due to testmode..."; + if (fork() == 0) { + execl("/bin/shutdown", "/bin/shutdown", "-n", nullptr); + ASSERT_NOT_REACHED(); + } + } else { + dbg() << "Continuing normally"; + } +} + +static void mount_all_filesystems() +{ + dbg() << "Spawning mount -a to mount all filesystems."; + pid_t pid = fork(); + + if (pid < 0) { + perror("fork"); + ASSERT_NOT_REACHED(); + } else if (pid == 0) { + execl("/bin/mount", "mount", "-a", nullptr); + perror("exec"); + ASSERT_NOT_REACHED(); + } else { + wait(nullptr); + } +} + +int main(int, char**) +{ + if (pledge("stdio proc exec tty accept unix rpath wpath cpath chown fattr id", nullptr) < 0) { + perror("pledge"); + return 1; + } + + mount_all_filesystems(); + + struct sigaction sa = { + .sa_handler = sigchld_handler, + .sa_mask = 0, + .sa_flags = SA_RESTART + }; + sigaction(SIGCHLD, &sa, nullptr); + + Core::EventLoop event_loop; + + // Read our config and instantiate services. + // This takes care of setting up sockets. + NonnullRefPtrVector<Service> services; + auto config = Core::ConfigFile::get_for_system("SystemServer"); + for (auto name : config->groups()) + services.append(Service::construct(*config, name)); + + // After we've set them all up, activate them! + for (auto& service : services) + service.activate(); + + // This won't return if we're in test mode. + check_for_test_mode(); + + return event_loop.exec(); +} diff --git a/Services/TTYServer/Makefile b/Services/TTYServer/Makefile new file mode 100644 index 0000000000..d1eb798683 --- /dev/null +++ b/Services/TTYServer/Makefile @@ -0,0 +1,6 @@ +OBJS = \ + main.o + +PROGRAM = TTYServer + +include ../../Makefile.common diff --git a/Services/TTYServer/main.cpp b/Services/TTYServer/main.cpp new file mode 100644 index 0000000000..79e43ca84b --- /dev/null +++ b/Services/TTYServer/main.cpp @@ -0,0 +1,68 @@ +/* + * Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org> + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#include <AK/Assertions.h> +#include <AK/LogStream.h> +#include <stdio.h> +#include <stdlib.h> +#include <sys/wait.h> +#include <unistd.h> + +int main(int argc, char** argv) +{ + if (pledge("stdio tty proc exec", nullptr) < 0) { + perror("pledge"); + return 1; + } + + if (unveil("/bin/Shell", "x") < 0) { + perror("unveil"); + return 1; + } + + unveil(nullptr, nullptr); + + if (argc < 2) + return -1; + + dbgprintf("Starting console server on %s\n", argv[1]); + + while (true) { + dbgprintf("Running shell on %s\n", argv[1]); + + auto child = fork(); + if (!child) { + int rc = execl("/bin/Shell", "Shell", nullptr); + ASSERT(rc < 0); + perror("execl"); + exit(127); + } else { + int wstatus; + waitpid(child, &wstatus, 0); + dbgprintf("Shell on %s exited with code %d\n", argv[1], WEXITSTATUS(wstatus)); + } + } +} diff --git a/Services/TelnetServer/Client.cpp b/Services/TelnetServer/Client.cpp new file mode 100644 index 0000000000..e983025df6 --- /dev/null +++ b/Services/TelnetServer/Client.cpp @@ -0,0 +1,188 @@ +/* + * Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org> + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#include "Client.h" +#include <AK/BufferStream.h> +#include <AK/ByteBuffer.h> +#include <AK/String.h> +#include <AK/StringBuilder.h> +#include <AK/StringView.h> +#include <AK/Types.h> +#include <LibCore/Notifier.h> +#include <LibCore/TCPSocket.h> +#include <stdio.h> +#include <unistd.h> + +Client::Client(int id, RefPtr<Core::TCPSocket> socket, int ptm_fd) + : m_id(id) + , m_socket(move(socket)) + , m_ptm_fd(ptm_fd) + , m_ptm_notifier(Core::Notifier::construct(ptm_fd, Core::Notifier::Read)) +{ + m_socket->on_ready_to_read = [this] { drain_socket(); }; + m_ptm_notifier->on_ready_to_read = [this] { drain_pty(); }; + m_parser.on_command = [this](const Command& command) { handle_command(command); }; + m_parser.on_data = [this](const StringView& data) { handle_data(data); }; + m_parser.on_error = [this]() { handle_error(); }; + send_commands({ + { CMD_WILL, SUB_SUPPRESS_GO_AHEAD }, + { CMD_WILL, SUB_ECHO }, + { CMD_DO, SUB_SUPPRESS_GO_AHEAD }, + { CMD_DONT, SUB_ECHO }, + }); +} + +void Client::drain_socket() +{ + NonnullRefPtr<Client> protect(*this); + while (m_socket->can_read()) { + auto buf = m_socket->read(1024); + + m_parser.write(buf); + + if (m_socket->eof()) { + quit(); + break; + } + } +} + +void Client::drain_pty() +{ + u8 buffer[BUFSIZ]; + ssize_t nread = read(m_ptm_fd, buffer, sizeof(buffer)); + if (nread < 0) { + perror("read(ptm)"); + quit(); + return; + } + if (nread == 0) { + quit(); + return; + } + send_data(StringView(buffer, (size_t)nread)); +} + +void Client::handle_data(const StringView& data) +{ + write(m_ptm_fd, data.characters_without_null_termination(), data.length()); +} + +void Client::handle_command(const Command& command) +{ + switch (command.command) { + case CMD_DO: + // no response - we've already advertised our options, and none of + // them can be disabled (or re-enabled) after connecting. + break; + case CMD_DONT: + // no response - we only "support" two options (echo and suppres + // go-ahead), and both of them are always enabled. + break; + case CMD_WILL: + switch (command.subcommand) { + case SUB_ECHO: + // we always want to be the ones in control of the output. tell + // the client to disable local echo. + send_command({ CMD_DONT, SUB_ECHO }); + break; + case SUB_SUPPRESS_GO_AHEAD: + send_command({ CMD_DO, SUB_SUPPRESS_GO_AHEAD }); + break; + default: + // don't respond to unknown commands + break; + } + break; + case CMD_WONT: + // no response - we don't care about anything the client says they + // won't do. + break; + } +} + +void Client::handle_error() +{ + quit(); +} + +void Client::send_data(StringView data) +{ + bool fast = true; + for (size_t i = 0; i < data.length(); i++) { + u8 c = data[i]; + if (c == '\n' || c == 0xff) + fast = false; + } + + if (fast) { + m_socket->write(data); + return; + } + + StringBuilder builder; + for (size_t i = 0; i < data.length(); i++) { + u8 c = data[i]; + + switch (c) { + case '\n': + builder.append("\r\n"); + break; + case IAC: + builder.append("\xff\xff"); + break; + default: + builder.append(c); + break; + } + } + + m_socket->write(builder.to_string()); +} + +void Client::send_command(Command command) +{ + send_commands({ command }); +} + +void Client::send_commands(Vector<Command> commands) +{ + auto buffer = ByteBuffer::create_uninitialized(commands.size() * 3); + BufferStream stream(buffer); + for (auto& command : commands) + stream << (u8)IAC << command.command << command.subcommand; + stream.snip(); + m_socket->write(buffer.data(), buffer.size()); +} + +void Client::quit() +{ + m_ptm_notifier->set_enabled(false); + close(m_ptm_fd); + m_socket->close(); + if (on_exit) + on_exit(); +} diff --git a/Services/TelnetServer/Client.h b/Services/TelnetServer/Client.h new file mode 100644 index 0000000000..fe345d38e3 --- /dev/null +++ b/Services/TelnetServer/Client.h @@ -0,0 +1,69 @@ +/* + * Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org> + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#pragma once + +#include <AK/String.h> +#include <AK/StringView.h> +#include <AK/Types.h> +#include <LibCore/Notifier.h> +#include <LibCore/TCPSocket.h> + +#include "Command.h" +#include "Parser.h" + +class Client : public RefCounted<Client> { +public: + static NonnullRefPtr<Client> create(int id, RefPtr<Core::TCPSocket> socket, int ptm_fd) + { + return adopt(*new Client(id, move(socket), ptm_fd)); + } + + Function<void()> on_exit; + +protected: + Client(int id, RefPtr<Core::TCPSocket> socket, int ptm_fd); + + void drain_socket(); + void drain_pty(); + void handle_data(const StringView&); + void handle_command(const Command& command); + void handle_error(); + void send_data(StringView str); + void send_command(Command command); + void send_commands(Vector<Command> commands); + void quit(); + +private: + // client id + int m_id { 0 }; + // client resources + RefPtr<Core::TCPSocket> m_socket; + Parser m_parser; + // pty resources + int m_ptm_fd { -1 }; + RefPtr<Core::Notifier> m_ptm_notifier; +}; diff --git a/Services/TelnetServer/Command.h b/Services/TelnetServer/Command.h new file mode 100644 index 0000000000..ac696c2111 --- /dev/null +++ b/Services/TelnetServer/Command.h @@ -0,0 +1,82 @@ +/* + * Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org> + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#pragma once + +#include <AK/String.h> +#include <AK/StringBuilder.h> +#include <AK/Types.h> + +#define CMD_WILL 0xfb +#define CMD_WONT 0xfc +#define CMD_DO 0xfd +#define CMD_DONT 0xfe +#define SUB_ECHO 0x01 +#define SUB_SUPPRESS_GO_AHEAD 0x03 + +struct Command { + u8 command; + u8 subcommand; + + String to_string() const + { + StringBuilder builder; + + switch (command) { + case CMD_WILL: + builder.append("WILL"); + break; + case CMD_WONT: + builder.append("WONT"); + break; + case CMD_DO: + builder.append("DO"); + break; + case CMD_DONT: + builder.append("DONT"); + break; + default: + builder.append(String::format("UNKNOWN<%02x>", command)); + break; + } + + builder.append(" "); + + switch (subcommand) { + case SUB_ECHO: + builder.append("ECHO"); + break; + case SUB_SUPPRESS_GO_AHEAD: + builder.append("SUPPRESS_GO_AHEAD"); + break; + default: + builder.append(String::format("UNKNOWN<%02x>")); + break; + } + + return builder.to_string(); + }; +}; diff --git a/Services/TelnetServer/Makefile b/Services/TelnetServer/Makefile new file mode 100644 index 0000000000..bfee24f38c --- /dev/null +++ b/Services/TelnetServer/Makefile @@ -0,0 +1,10 @@ +OBJS = \ + Client.o \ + Parser.o \ + main.o + +PROGRAM = TelnetServer + +LIB_DEPS = Core + +include ../../Makefile.common diff --git a/Services/TelnetServer/Parser.cpp b/Services/TelnetServer/Parser.cpp new file mode 100644 index 0000000000..9fc1aabcd1 --- /dev/null +++ b/Services/TelnetServer/Parser.cpp @@ -0,0 +1,89 @@ +/* + * Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org> + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#include <AK/Function.h> +#include <AK/String.h> +#include <AK/Types.h> + +#include "Parser.h" + +void Parser::write(const StringView& data) +{ + for (size_t i = 0; i < data.length(); i++) { + u8 ch = data[i]; + + switch (m_state) { + case State::Free: + switch (ch) { + case IAC: + m_state = State::ReadCommand; + break; + case '\r': + if (on_data) + on_data("\n"); + break; + default: + if (on_data) + on_data(StringView(&ch, 1)); + break; + } + break; + case State::ReadCommand: + switch (ch) { + case IAC: { + m_state = State::Free; + if (on_data) + on_data("\xff"); + break; + } + case CMD_WILL: + case CMD_WONT: + case CMD_DO: + case CMD_DONT: + m_command = ch; + m_state = State::ReadSubcommand; + break; + default: + m_state = State::Error; + if (on_error) + on_error(); + break; + } + break; + case State::ReadSubcommand: { + auto command = m_command; + m_command = 0; + m_state = State::Free; + if (on_command) + on_command({ command, ch }); + break; + } + case State::Error: + // ignore everything + break; + } + } +} diff --git a/Services/TelnetServer/Parser.h b/Services/TelnetServer/Parser.h new file mode 100644 index 0000000000..64688881df --- /dev/null +++ b/Services/TelnetServer/Parser.h @@ -0,0 +1,59 @@ +/* + * Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org> + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#pragma once + +#include <AK/Function.h> +#include <AK/String.h> +#include <AK/StringView.h> +#include <AK/Types.h> + +#include "Command.h" + +#define IAC 0xff + +class Parser { +public: + Function<void(const Command&)> on_command; + Function<void(const StringView&)> on_data; + Function<void()> on_error; + + void write(const StringView&); + +protected: + enum State { + Free, + ReadCommand, + ReadSubcommand, + Error, + }; + + void write(const String& str); + +private: + State m_state { State::Free }; + u8 m_command { 0 }; +}; diff --git a/Services/TelnetServer/main.cpp b/Services/TelnetServer/main.cpp new file mode 100644 index 0000000000..6acb900b61 --- /dev/null +++ b/Services/TelnetServer/main.cpp @@ -0,0 +1,177 @@ +/* + * Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org> + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#include "Client.h" +#include <AK/BufferStream.h> +#include <AK/ByteBuffer.h> +#include <AK/HashMap.h> +#include <AK/IPv4Address.h> +#include <AK/String.h> +#include <AK/StringBuilder.h> +#include <AK/Types.h> +#include <LibCore/EventLoop.h> +#include <LibCore/TCPServer.h> +#include <LibCore/TCPSocket.h> +#include <fcntl.h> +#include <getopt.h> +#include <stdio.h> +#include <stdlib.h> +#include <sys/ioctl.h> + +static void run_command(int ptm_fd, String command) +{ + pid_t pid = fork(); + if (pid == 0) { + const char* tty_name = ptsname(ptm_fd); + if (!tty_name) { + perror("ptsname"); + exit(1); + } + close(ptm_fd); + int pts_fd = open(tty_name, O_RDWR); + if (pts_fd < 0) { + perror("open"); + exit(1); + } + + // NOTE: It's okay if this fails. + (void)ioctl(0, TIOCNOTTY); + + close(0); + close(1); + close(2); + + int rc = dup2(pts_fd, 0); + if (rc < 0) { + perror("dup2"); + exit(1); + } + rc = dup2(pts_fd, 1); + if (rc < 0) { + perror("dup2"); + exit(1); + } + rc = dup2(pts_fd, 2); + if (rc < 0) { + perror("dup2"); + exit(1); + } + rc = close(pts_fd); + if (rc < 0) { + perror("close"); + exit(1); + } + rc = ioctl(0, TIOCSCTTY); + if (rc < 0) { + perror("ioctl(TIOCSCTTY)"); + exit(1); + } + const char* args[4] = { "/bin/Shell", nullptr, nullptr, nullptr }; + if (!command.is_empty()) { + args[1] = "-c"; + args[2] = command.characters(); + } + const char* envs[] = { "TERM=xterm", "PATH=/bin:/usr/bin:/usr/local/bin", nullptr }; + rc = execve("/bin/Shell", const_cast<char**>(args), const_cast<char**>(envs)); + if (rc < 0) { + perror("execve"); + exit(1); + } + ASSERT_NOT_REACHED(); + } +} + +int main(int argc, char** argv) +{ + Core::EventLoop event_loop; + auto server = Core::TCPServer::construct(); + + int opt; + u16 port = 23; + const char* command = ""; + while ((opt = getopt(argc, argv, "p:c:")) != -1) { + switch (opt) { + case 'p': + port = atoi(optarg); + break; + case 'c': + command = optarg; + break; + default: + fprintf(stderr, "Usage: %s [-p port] [-c command]", argv[0]); + exit(1); + } + } + + if (!server->listen({}, port)) { + perror("listen"); + exit(1); + } + + HashMap<int, NonnullRefPtr<Client>> clients; + int next_id = 0; + + server->on_ready_to_accept = [&next_id, &clients, &server, command] { + int id = next_id++; + + auto client_socket = server->accept(); + if (!client_socket) { + perror("accept"); + return; + } + + int ptm_fd = posix_openpt(O_RDWR); + if (ptm_fd < 0) { + perror("posix_openpt"); + client_socket->close(); + return; + } + if (grantpt(ptm_fd) < 0) { + perror("grantpt"); + client_socket->close(); + return; + } + if (unlockpt(ptm_fd) < 0) { + perror("unlockpt"); + client_socket->close(); + return; + } + + run_command(ptm_fd, command); + + auto client = Client::create(id, move(client_socket), ptm_fd); + client->on_exit = [&clients, id] { clients.remove(id); }; + clients.set(id, client); + }; + + int rc = event_loop.exec(); + if (rc != 0) { + fprintf(stderr, "event loop exited badly; rc=%d", rc); + exit(1); + } + + return 0; +} diff --git a/Services/WebServer/Client.cpp b/Services/WebServer/Client.cpp new file mode 100644 index 0000000000..ae9aedc907 --- /dev/null +++ b/Services/WebServer/Client.cpp @@ -0,0 +1,230 @@ +/* + * Copyright (c) 2020, Andreas Kling <kling@serenityos.org> + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#include "Client.h" +#include <AK/FileSystemPath.h> +#include <AK/StringBuilder.h> +#include <LibCore/DateTime.h> +#include <LibCore/DirIterator.h> +#include <LibCore/File.h> +#include <LibHTTP/HttpRequest.h> +#include <stdio.h> +#include <sys/stat.h> +#include <time.h> +#include <unistd.h> + +namespace WebServer { + +Client::Client(NonnullRefPtr<Core::TCPSocket> socket, Core::Object* parent) + : Core::Object(parent) + , m_socket(socket) +{ +} + +void Client::die() +{ + remove_from_parent(); +} + +void Client::start() +{ + m_socket->on_ready_to_read = [this] { + auto raw_request = m_socket->read_all(); + if (raw_request.is_null()) { + die(); + return; + } + + dbg() << "Got raw request: '" << String::copy(raw_request) << "'"; + + handle_request(move(raw_request)); + die(); + }; +} + +void Client::handle_request(ByteBuffer raw_request) +{ + auto request_or_error = HTTP::HttpRequest::from_raw_request(raw_request); + if (!request_or_error.has_value()) + return; + auto& request = request_or_error.value(); + + dbg() << "Got HTTP request: " << request.method_name() << " " << request.resource(); + for (auto& header : request.headers()) { + dbg() << " " << header.name << " => " << header.value; + } + + if (request.method() != HTTP::HttpRequest::Method::GET) { + send_error_response(403, "Forbidden, bro!", request); + return; + } + + auto requested_path = canonicalized_path(request.resource()); + dbg() << "Canonical requested path: '" << requested_path << "'"; + + StringBuilder path_builder; + path_builder.append("/www/"); + path_builder.append(requested_path); + auto real_path = path_builder.to_string(); + + if (Core::File::is_directory(real_path)) { + + if (!request.resource().ends_with("/")) { + StringBuilder red; + + red.append(requested_path); + red.append("/"); + + send_redirect(red.to_string(), request); + return; + } + + StringBuilder index_html_path_builder; + index_html_path_builder.append(real_path); + index_html_path_builder.append("/index.html"); + auto index_html_path = index_html_path_builder.to_string(); + if (!Core::File::exists(index_html_path)) { + handle_directory_listing(requested_path, real_path, request); + return; + } + real_path = index_html_path; + } + + auto file = Core::File::construct(real_path); + if (!file->open(Core::File::ReadOnly)) { + send_error_response(404, "Not found, bro!", request); + return; + } + + send_response(file->read_all(), request); +} + +void Client::send_response(StringView response, const HTTP::HttpRequest& request) +{ + StringBuilder builder; + builder.append("HTTP/1.0 200 OK\r\n"); + builder.append("Server: WebServer (SerenityOS)\r\n"); + builder.append("Content-Type: text/html\r\n"); + builder.append("\r\n"); + + m_socket->write(builder.to_string()); + m_socket->write(response); + + log_response(200, request); +} + +void Client::send_redirect(StringView redirect_path, const HTTP::HttpRequest& request) +{ + StringBuilder builder; + builder.append("HTTP/1.0 301 Moved Permanently\r\n"); + builder.append("Location: "); + builder.append(redirect_path); + builder.append("\r\n"); + builder.append("\r\n"); + + m_socket->write(builder.to_string()); + + log_response(301, request); +} + +void Client::handle_directory_listing(const String& requested_path, const String& real_path, const HTTP::HttpRequest& request) +{ + StringBuilder builder; + + builder.append("<!DOCTYPE html>\n"); + builder.append("<html>\n"); + builder.append("<head><title>Index of "); + builder.append(escape_html_entities(requested_path)); + builder.append("</title></head>\n"); + builder.append("<body>\n"); + builder.append("<h1>Index of "); + builder.append(escape_html_entities(requested_path)); + builder.append("</h1>\n"); + builder.append("<hr>\n"); + builder.append("<pre>\n"); + + Core::DirIterator dt(real_path); + while (dt.has_next()) { + auto name = dt.next_path(); + builder.append("<a href=\""); + // FIXME: urlencode + builder.append(name); + builder.append("\">"); + builder.append(escape_html_entities(name)); + builder.append("</a>"); + for (size_t i = 0; i < (40 - name.length()); ++i) + builder.append(' '); + + StringBuilder path_builder; + path_builder.append(real_path); + path_builder.append('/'); + path_builder.append(name); + struct stat st; + memset(&st, 0, sizeof(st)); + int rc = stat(path_builder.to_string().characters(), &st); + if (rc < 0) { + perror("stat"); + } + builder.appendf(" %10d", st.st_size); + builder.appendf(" "); + builder.append(Core::DateTime::from_timestamp(st.st_mtime).to_string()); + builder.append("\n"); + } + + builder.append("</pre>\n"); + builder.append("<hr>\n"); + builder.append("<i>Generated by WebServer (SerenityOS)</i>\n"); + builder.append("</body>\n"); + builder.append("</html>\n"); + + send_response(builder.to_string(), request); +} + +void Client::send_error_response(unsigned code, const StringView& message, const HTTP::HttpRequest& request) +{ + StringBuilder builder; + builder.appendf("HTTP/1.0 %u ", code); + builder.append(message); + builder.append("\r\n\r\n"); + builder.append("<!DOCTYPE html><html><body><h1>"); + builder.appendf("%u ", code); + builder.append(message); + builder.append("</h1></body></html>"); + m_socket->write(builder.to_string()); + + log_response(code, request); +} + +void Client::log_response(unsigned code, const HTTP::HttpRequest& request) +{ + printf("%s :: %03u :: %s %s\n", + Core::DateTime::now().to_string().characters(), + code, + request.method_name().characters(), + request.resource().characters()); +} + +} diff --git a/Services/WebServer/Client.h b/Services/WebServer/Client.h new file mode 100644 index 0000000000..35cdb4ab1c --- /dev/null +++ b/Services/WebServer/Client.h @@ -0,0 +1,54 @@ +/* + * Copyright (c) 2020, Andreas Kling <kling@serenityos.org> + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#pragma once + +#include <LibCore/Object.h> +#include <LibCore/TCPSocket.h> +#include <LibHTTP/Forward.h> + +namespace WebServer { + +class Client final : public Core::Object { + C_OBJECT(Client); +public: + void start(); + +private: + Client(NonnullRefPtr<Core::TCPSocket>, Core::Object* parent); + + void handle_request(ByteBuffer); + void send_response(StringView, const HTTP::HttpRequest&); + void send_redirect(StringView redirect, const HTTP::HttpRequest& request); + void send_error_response(unsigned code, const StringView& message, const HTTP::HttpRequest&); + void die(); + void log_response(unsigned code, const HTTP::HttpRequest&); + void handle_directory_listing(const String& requested_path, const String& real_path, const HTTP::HttpRequest&); + + NonnullRefPtr<Core::TCPSocket> m_socket; +}; + +} diff --git a/Services/WebServer/Makefile b/Services/WebServer/Makefile new file mode 100644 index 0000000000..ddc99a85ae --- /dev/null +++ b/Services/WebServer/Makefile @@ -0,0 +1,9 @@ +OBJS = \ + Client.o \ + main.o + +PROGRAM = WebServer + +LIB_DEPS = HTTP Core + +include ../../Makefile.common diff --git a/Services/WebServer/main.cpp b/Services/WebServer/main.cpp new file mode 100644 index 0000000000..7e218b8bb8 --- /dev/null +++ b/Services/WebServer/main.cpp @@ -0,0 +1,81 @@ +/* + * Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org> + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#include "Client.h" +#include <LibCore/ArgsParser.h> +#include <LibCore/EventLoop.h> +#include <LibCore/TCPServer.h> +#include <stdio.h> +#include <unistd.h> + +int main(int argc, char** argv) +{ + u16 default_port = 8000; + + int port = default_port; + + Core::ArgsParser args_parser; + args_parser.add_positional_argument(port, "Port to listen on", "port", Core::ArgsParser::Required::No); + args_parser.parse(argc, argv); + + if ((u16)port != port) { + printf("Warning: invalid port number: %d\n", port); + port = default_port; + } + + if (pledge("stdio accept rpath inet unix cpath fattr", nullptr) < 0) { + perror("pledge"); + return 1; + } + + Core::EventLoop loop; + + auto server = Core::TCPServer::construct(); + + server->on_ready_to_accept = [&] { + auto client_socket = server->accept(); + ASSERT(client_socket); + auto client = WebServer::Client::construct(client_socket.release_nonnull(), server); + client->start(); + }; + + server->listen({}, port); + printf("Listening on 0.0.0.0:%d\n", port); + + if (unveil("/www", "r") < 0) { + perror("unveil"); + return 1; + } + + unveil(nullptr, nullptr); + + if (pledge("stdio accept rpath", nullptr) < 0) { + perror("pledge"); + return 1; + } + + return loop.exec(); +} diff --git a/Services/WindowServer/AppletManager.cpp b/Services/WindowServer/AppletManager.cpp new file mode 100644 index 0000000000..855ff3bcd3 --- /dev/null +++ b/Services/WindowServer/AppletManager.cpp @@ -0,0 +1,135 @@ +/* + * Copyright (c) 2020-2020, Hüseyin Aslıtürk <asliturk@hotmail.com> + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#include "AppletManager.h" +#include <AK/QuickSort.h> +#include <LibGfx/Painter.h> +#include <WindowServer/MenuManager.h> + +namespace WindowServer { + +static AppletManager* s_the; +Vector<String> order_vector; + +AppletManager::AppletManager() +{ + s_the = this; + + auto wm_config = Core::ConfigFile::open("/etc/WindowServer/WindowServer.ini"); + auto order = wm_config->read_entry("Applet", "Order"); + order_vector = order.split(','); +} + +AppletManager::~AppletManager() +{ +} + +AppletManager& AppletManager::the() +{ + ASSERT(s_the); + return *s_the; +} + +void AppletManager::event(Core::Event& event) +{ + auto& mouse_event = static_cast<MouseEvent&>(event); + + for (auto& applet : m_applets) { + if (!applet) + continue; + if (!applet->rect_in_menubar().contains(mouse_event.position())) + continue; + auto local_event = mouse_event.translated(-applet->rect_in_menubar().location()); + applet->event(local_event); + } +} + +void AppletManager::add_applet(Window& applet) +{ + m_applets.append(applet.make_weak_ptr()); + + // Prune any dead weak pointers from the applet list. + m_applets.remove_all_matching([](auto& entry) { + return entry.is_null(); + }); + + quick_sort(m_applets, [](auto& a, auto& b) { + auto index_a = order_vector.find_first_index(a->title()); + auto index_b = order_vector.find_first_index(b->title()); + return index_a.value_or(0) > index_b.value_or(0); + }); + + calculate_applet_rects(MenuManager::the().window()); +} + +void AppletManager::calculate_applet_rects(Window& window) +{ + auto menubar_rect = window.rect(); + int right_edge_x = menubar_rect.width() - 4; + for (auto& existing_applet : m_applets) { + + Gfx::Rect new_applet_rect(right_edge_x - existing_applet->size().width(), 0, existing_applet->size().width(), existing_applet->size().height()); + Gfx::Rect dummy_menubar_rect(0, 0, 0, 18); + new_applet_rect.center_vertically_within(dummy_menubar_rect); + + existing_applet->set_rect_in_menubar(new_applet_rect); + right_edge_x = existing_applet->rect_in_menubar().x() - 4; + } +} + +void AppletManager::remove_applet(Window& applet) +{ + m_applets.remove_first_matching([&](auto& entry) { + return &applet == entry.ptr(); + }); +} + +void AppletManager::draw() +{ + for (auto& applet : m_applets) { + if (!applet) + continue; + draw_applet(*applet); + } +} + +void AppletManager::draw_applet(const Window& applet) +{ + if (!applet.backing_store()) + return; + + Gfx::Painter painter(*MenuManager::the().window().backing_store()); + painter.fill_rect(applet.rect_in_menubar(), WindowManager::the().palette().window()); + painter.blit(applet.rect_in_menubar().location(), *applet.backing_store(), applet.backing_store()->rect()); +} + +void AppletManager::invalidate_applet(const Window& applet, const Gfx::Rect& rect) +{ + draw_applet(applet); + MenuManager::the().window().invalidate(rect.translated(applet.rect_in_menubar().location())); +} + +} diff --git a/Services/WindowServer/AppletManager.h b/Services/WindowServer/AppletManager.h new file mode 100644 index 0000000000..4619fd0030 --- /dev/null +++ b/Services/WindowServer/AppletManager.h @@ -0,0 +1,56 @@ +/* + * Copyright (c) 2020-2020, Hüseyin Aslıtürk <asliturk@hotmail.com> + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#pragma once + +#include <WindowServer/Window.h> +#include <WindowServer/WindowManager.h> + +namespace WindowServer { + +class AppletManager : public Core::Object { + C_OBJECT(AppletManager) +public: + AppletManager(); + ~AppletManager(); + + static AppletManager& the(); + + virtual void event(Core::Event&) override; + + void add_applet(Window& applet); + void remove_applet(Window& applet); + void draw(); + void invalidate_applet(const Window& applet, const Gfx::Rect& rect); + void calculate_applet_rects(Window& window); + +private: + void draw_applet(const Window& applet); + + Vector<WeakPtr<Window>> m_applets; +}; + +} diff --git a/Services/WindowServer/Button.cpp b/Services/WindowServer/Button.cpp new file mode 100644 index 0000000000..95037d4bc8 --- /dev/null +++ b/Services/WindowServer/Button.cpp @@ -0,0 +1,115 @@ +/* + * Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org> + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#include <LibGfx/CharacterBitmap.h> +#include <LibGfx/Painter.h> +#include <LibGfx/StylePainter.h> +#include <WindowServer/Button.h> +#include <WindowServer/Event.h> +#include <WindowServer/WindowManager.h> + +namespace WindowServer { + +Button::Button(WindowFrame& frame, NonnullRefPtr<Gfx::CharacterBitmap>&& bitmap, Function<void(Button&)>&& on_click_handler) + : on_click(move(on_click_handler)) + , m_frame(frame) + , m_bitmap(move(bitmap)) +{ +} + +Button::~Button() +{ +} + +void Button::paint(Gfx::Painter& painter) +{ + auto palette = WindowManager::the().palette(); + Gfx::PainterStateSaver saver(painter); + painter.translate(relative_rect().location()); + Gfx::StylePainter::paint_button(painter, rect(), palette, Gfx::ButtonStyle::Normal, m_pressed, m_hovered); + auto x_location = rect().center(); + x_location.move_by(-(m_bitmap->width() / 2), -(m_bitmap->height() / 2)); + if (m_pressed) + x_location.move_by(1, 1); + painter.draw_bitmap(x_location, *m_bitmap, palette.button_text()); +} + +void Button::on_mouse_event(const MouseEvent& event) +{ + auto& wm = WindowManager::the(); + + if (event.type() == Event::MouseDown && event.button() == MouseButton::Left) { + m_pressed = true; + wm.set_cursor_tracking_button(this); + wm.invalidate(screen_rect()); + return; + } + + if (event.type() == Event::MouseUp && event.button() == MouseButton::Left) { + if (wm.cursor_tracking_button() != this) + return; + wm.set_cursor_tracking_button(nullptr); + bool old_pressed = m_pressed; + m_pressed = false; + if (rect().contains(event.position())) { + if (on_click) + on_click(*this); + } + if (old_pressed != m_pressed) { + // Would like to compute: + // m_hovered = rect_after_action().contains(event.position()); + // However, we don't know that rect yet. We can make an educated + // guess which also looks okay even when wrong: + m_hovered = false; + wm.invalidate(screen_rect()); + } + return; + } + + if (event.type() == Event::MouseMove) { + bool old_hovered = m_hovered; + m_hovered = rect().contains(event.position()); + wm.set_hovered_button(m_hovered ? this : nullptr); + if (old_hovered != m_hovered) + wm.invalidate(screen_rect()); + } + + if (event.type() == Event::MouseMove && event.buttons() & (unsigned)MouseButton::Left) { + if (wm.cursor_tracking_button() != this) + return; + bool old_pressed = m_pressed; + m_pressed = m_hovered; + if (old_pressed != m_pressed) + wm.invalidate(screen_rect()); + } +} + +Gfx::Rect Button::screen_rect() const +{ + return m_relative_rect.translated(m_frame.rect().location()); +} + +} diff --git a/Services/WindowServer/Button.h b/Services/WindowServer/Button.h new file mode 100644 index 0000000000..555a5d6f9e --- /dev/null +++ b/Services/WindowServer/Button.h @@ -0,0 +1,70 @@ +/* + * Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org> + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#pragma once + +#include <AK/Function.h> +#include <AK/NonnullRefPtr.h> +#include <AK/Weakable.h> +#include <LibGfx/Rect.h> +#include <LibGfx/Forward.h> + +namespace WindowServer { + +class MouseEvent; +class WindowFrame; + +class Button : public Weakable<Button> { +public: + Button(WindowFrame&, NonnullRefPtr<Gfx::CharacterBitmap>&&, Function<void(Button&)>&& on_click_handler); + ~Button(); + + Gfx::Rect relative_rect() const { return m_relative_rect; } + void set_relative_rect(const Gfx::Rect& rect) { m_relative_rect = rect; } + + Gfx::Rect rect() const { return { {}, m_relative_rect.size() }; } + Gfx::Rect screen_rect() const; + + void paint(Gfx::Painter&); + + void on_mouse_event(const MouseEvent&); + + Function<void(Button&)> on_click; + + bool is_visible() const { return m_visible; } + + void set_bitmap(const Gfx::CharacterBitmap& bitmap) { m_bitmap = bitmap; } + +private: + WindowFrame& m_frame; + Gfx::Rect m_relative_rect; + NonnullRefPtr<Gfx::CharacterBitmap> m_bitmap; + bool m_pressed { false }; + bool m_visible { true }; + bool m_hovered { false }; +}; + +} diff --git a/Services/WindowServer/ClientConnection.cpp b/Services/WindowServer/ClientConnection.cpp new file mode 100644 index 0000000000..7f128689fb --- /dev/null +++ b/Services/WindowServer/ClientConnection.cpp @@ -0,0 +1,833 @@ +/* + * Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org> + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#include <AK/Badge.h> +#include <AK/SharedBuffer.h> +#include <LibGfx/Bitmap.h> +#include <LibGfx/SystemTheme.h> +#include <WindowServer/AppletManager.h> +#include <WindowServer/ClientConnection.h> +#include <WindowServer/Clipboard.h> +#include <WindowServer/Compositor.h> +#include <WindowServer/EventLoop.h> +#include <WindowServer/Menu.h> +#include <WindowServer/MenuBar.h> +#include <WindowServer/MenuItem.h> +#include <WindowServer/Screen.h> +#include <WindowServer/Window.h> +#include <WindowServer/WindowClientEndpoint.h> +#include <WindowServer/WindowManager.h> +#include <WindowServer/WindowSwitcher.h> +#include <errno.h> +#include <serenity.h> +#include <stdio.h> +#include <unistd.h> + +namespace WindowServer { + +HashMap<int, NonnullRefPtr<ClientConnection>>* s_connections; + +static Gfx::Rect normalize_window_rect(Gfx::Rect rect, WindowType window_type) +{ + auto min_size = 1; + if (window_type == WindowType::Normal) + min_size = 50; + Gfx::Rect normalized_rect = { rect.x(), rect.y(), max(rect.width(), min_size), max(rect.height(), min_size) }; + return normalized_rect; +} + +void ClientConnection::for_each_client(Function<void(ClientConnection&)> callback) +{ + if (!s_connections) + return; + for (auto& it : *s_connections) { + callback(*it.value); + } +} + +ClientConnection* ClientConnection::from_client_id(int client_id) +{ + if (!s_connections) + return nullptr; + auto it = s_connections->find(client_id); + if (it == s_connections->end()) + return nullptr; + return (*it).value.ptr(); +} + +ClientConnection::ClientConnection(Core::LocalSocket& client_socket, int client_id) + : IPC::ClientConnection<WindowServerEndpoint>(*this, client_socket, client_id) +{ + if (!s_connections) + s_connections = new HashMap<int, NonnullRefPtr<ClientConnection>>; + s_connections->set(client_id, *this); +} + +ClientConnection::~ClientConnection() +{ + if (m_has_display_link) + Compositor::the().decrement_display_link_count({}); + + MenuManager::the().close_all_menus_from_client({}, *this); + auto windows = move(m_windows); + for (auto& window : windows) { + window.value->detach_client({}); + if (window.value->type() == WindowType::MenuApplet) + AppletManager::the().remove_applet(window.value); + } +} + +void ClientConnection::die() +{ + deferred_invoke([this](auto&) { + s_connections->remove(client_id()); + }); +} + +void ClientConnection::notify_about_new_screen_rect(const Gfx::Rect& rect) +{ + post_message(Messages::WindowClient::ScreenRectChanged(rect)); +} + +void ClientConnection::notify_about_clipboard_contents_changed() +{ + post_message(Messages::WindowClient::ClipboardContentsChanged(Clipboard::the().data_type())); +} + +OwnPtr<Messages::WindowServer::CreateMenubarResponse> ClientConnection::handle(const Messages::WindowServer::CreateMenubar&) +{ + int menubar_id = m_next_menubar_id++; + auto menubar = make<MenuBar>(*this, menubar_id); + m_menubars.set(menubar_id, move(menubar)); + return make<Messages::WindowServer::CreateMenubarResponse>(menubar_id); +} + +OwnPtr<Messages::WindowServer::DestroyMenubarResponse> ClientConnection::handle(const Messages::WindowServer::DestroyMenubar& message) +{ + int menubar_id = message.menubar_id(); + auto it = m_menubars.find(menubar_id); + if (it == m_menubars.end()) { + did_misbehave("DestroyMenubar: Bad menubar ID"); + return nullptr; + } + auto& menubar = *(*it).value; + MenuManager::the().close_menubar(menubar); + m_menubars.remove(it); + return make<Messages::WindowServer::DestroyMenubarResponse>(); +} + +OwnPtr<Messages::WindowServer::CreateMenuResponse> ClientConnection::handle(const Messages::WindowServer::CreateMenu& message) +{ + int menu_id = m_next_menu_id++; + auto menu = Menu::construct(this, menu_id, message.menu_title()); + m_menus.set(menu_id, move(menu)); + return make<Messages::WindowServer::CreateMenuResponse>(menu_id); +} + +OwnPtr<Messages::WindowServer::DestroyMenuResponse> ClientConnection::handle(const Messages::WindowServer::DestroyMenu& message) +{ + int menu_id = message.menu_id(); + auto it = m_menus.find(menu_id); + if (it == m_menus.end()) { + did_misbehave("DestroyMenu: Bad menu ID"); + return nullptr; + } + auto& menu = *(*it).value; + menu.close(); + m_menus.remove(it); + remove_child(menu); + return make<Messages::WindowServer::DestroyMenuResponse>(); +} + +OwnPtr<Messages::WindowServer::SetApplicationMenubarResponse> ClientConnection::handle(const Messages::WindowServer::SetApplicationMenubar& message) +{ + int menubar_id = message.menubar_id(); + auto it = m_menubars.find(menubar_id); + if (it == m_menubars.end()) { + did_misbehave("SetApplicationMenubar: Bad menubar ID"); + return nullptr; + } + auto& menubar = *(*it).value; + m_app_menubar = menubar.make_weak_ptr(); + WindowManager::the().notify_client_changed_app_menubar(*this); + return make<Messages::WindowServer::SetApplicationMenubarResponse>(); +} + +OwnPtr<Messages::WindowServer::AddMenuToMenubarResponse> ClientConnection::handle(const Messages::WindowServer::AddMenuToMenubar& message) +{ + int menubar_id = message.menubar_id(); + int menu_id = message.menu_id(); + auto it = m_menubars.find(menubar_id); + auto jt = m_menus.find(menu_id); + if (it == m_menubars.end()) { + did_misbehave("AddMenuToMenubar: Bad menubar ID"); + return nullptr; + } + if (jt == m_menus.end()) { + did_misbehave("AddMenuToMenubar: Bad menu ID"); + return nullptr; + } + auto& menubar = *(*it).value; + auto& menu = *(*jt).value; + menubar.add_menu(menu); + return make<Messages::WindowServer::AddMenuToMenubarResponse>(); +} + +OwnPtr<Messages::WindowServer::AddMenuItemResponse> ClientConnection::handle(const Messages::WindowServer::AddMenuItem& message) +{ + int menu_id = message.menu_id(); + unsigned identifier = message.identifier(); + auto it = m_menus.find(menu_id); + if (it == m_menus.end()) { + dbg() << "AddMenuItem: Bad menu ID: " << menu_id; + return nullptr; + } + auto& menu = *(*it).value; + auto menu_item = make<MenuItem>(menu, identifier, message.text(), message.shortcut(), message.enabled(), message.checkable(), message.checked()); + if (message.icon_buffer_id() != -1) { + auto icon_buffer = SharedBuffer::create_from_shbuf_id(message.icon_buffer_id()); + if (!icon_buffer) + return nullptr; + // FIXME: Verify that the icon buffer can accomodate a 16x16 bitmap view. + auto shared_icon = Gfx::Bitmap::create_with_shared_buffer(Gfx::BitmapFormat::RGBA32, icon_buffer.release_nonnull(), { 16, 16 }); + menu_item->set_icon(shared_icon); + } + menu_item->set_submenu_id(message.submenu_id()); + menu_item->set_exclusive(message.exclusive()); + menu.add_item(move(menu_item)); + return make<Messages::WindowServer::AddMenuItemResponse>(); +} + +OwnPtr<Messages::WindowServer::PopupMenuResponse> ClientConnection::handle(const Messages::WindowServer::PopupMenu& message) +{ + int menu_id = message.menu_id(); + auto position = message.screen_position(); + auto it = m_menus.find(menu_id); + if (it == m_menus.end()) { + did_misbehave("PopupMenu: Bad menu ID"); + return nullptr; + } + auto& menu = *(*it).value; + menu.popup(position); + return make<Messages::WindowServer::PopupMenuResponse>(); +} + +OwnPtr<Messages::WindowServer::DismissMenuResponse> ClientConnection::handle(const Messages::WindowServer::DismissMenu& message) +{ + int menu_id = message.menu_id(); + auto it = m_menus.find(menu_id); + if (it == m_menus.end()) { + did_misbehave("DismissMenu: Bad menu ID"); + return nullptr; + } + auto& menu = *(*it).value; + menu.close(); + return make<Messages::WindowServer::DismissMenuResponse>(); +} + +OwnPtr<Messages::WindowServer::UpdateMenuItemResponse> ClientConnection::handle(const Messages::WindowServer::UpdateMenuItem& message) +{ + int menu_id = message.menu_id(); + auto it = m_menus.find(menu_id); + if (it == m_menus.end()) { + did_misbehave("UpdateMenuItem: Bad menu ID"); + return nullptr; + } + auto& menu = *(*it).value; + auto* menu_item = menu.item_with_identifier(message.identifier()); + if (!menu_item) { + did_misbehave("UpdateMenuItem: Bad menu item identifier"); + return nullptr; + } + menu_item->set_text(message.text()); + menu_item->set_shortcut_text(message.shortcut()); + menu_item->set_enabled(message.enabled()); + menu_item->set_checkable(message.checkable()); + if (message.checkable()) + menu_item->set_checked(message.checked()); + return make<Messages::WindowServer::UpdateMenuItemResponse>(); +} + +OwnPtr<Messages::WindowServer::AddMenuSeparatorResponse> ClientConnection::handle(const Messages::WindowServer::AddMenuSeparator& message) +{ + int menu_id = message.menu_id(); + auto it = m_menus.find(menu_id); + if (it == m_menus.end()) { + did_misbehave("AddMenuSeparator: Bad menu ID"); + return nullptr; + } + auto& menu = *(*it).value; + menu.add_item(make<MenuItem>(menu, MenuItem::Separator)); + return make<Messages::WindowServer::AddMenuSeparatorResponse>(); +} + +OwnPtr<Messages::WindowServer::MoveWindowToFrontResponse> ClientConnection::handle(const Messages::WindowServer::MoveWindowToFront& message) +{ + auto it = m_windows.find(message.window_id()); + if (it == m_windows.end()) { + did_misbehave("MoveWindowToFront: Bad window ID"); + return nullptr; + } + WindowManager::the().move_to_front_and_make_active(*(*it).value); + return make<Messages::WindowServer::MoveWindowToFrontResponse>(); +} + +OwnPtr<Messages::WindowServer::SetFullscreenResponse> ClientConnection::handle(const Messages::WindowServer::SetFullscreen& message) +{ + auto it = m_windows.find(message.window_id()); + if (it == m_windows.end()) { + did_misbehave("SetFullscreen: Bad window ID"); + return nullptr; + } + it->value->set_fullscreen(message.fullscreen()); + return make<Messages::WindowServer::SetFullscreenResponse>(); +} + +OwnPtr<Messages::WindowServer::SetWindowOpacityResponse> ClientConnection::handle(const Messages::WindowServer::SetWindowOpacity& message) +{ + auto it = m_windows.find(message.window_id()); + if (it == m_windows.end()) { + did_misbehave("SetWindowOpacity: Bad window ID"); + return nullptr; + } + it->value->set_opacity(message.opacity()); + return make<Messages::WindowServer::SetWindowOpacityResponse>(); +} + +void ClientConnection::handle(const Messages::WindowServer::AsyncSetWallpaper& message) +{ + Compositor::the().set_wallpaper(message.path(), [&](bool success) { + post_message(Messages::WindowClient::AsyncSetWallpaperFinished(success)); + }); +} + +OwnPtr<Messages::WindowServer::SetBackgroundColorResponse> ClientConnection::handle(const Messages::WindowServer::SetBackgroundColor& message) +{ + Compositor::the().set_background_color(message.background_color()); + return make<Messages::WindowServer::SetBackgroundColorResponse>(); +} + +OwnPtr<Messages::WindowServer::SetWallpaperModeResponse> ClientConnection::handle(const Messages::WindowServer::SetWallpaperMode& message) +{ + Compositor::the().set_wallpaper_mode(message.mode()); + return make<Messages::WindowServer::SetWallpaperModeResponse>(); +} + +OwnPtr<Messages::WindowServer::GetWallpaperResponse> ClientConnection::handle(const Messages::WindowServer::GetWallpaper&) +{ + return make<Messages::WindowServer::GetWallpaperResponse>(Compositor::the().wallpaper_path()); +} + +OwnPtr<Messages::WindowServer::SetResolutionResponse> ClientConnection::handle(const Messages::WindowServer::SetResolution& message) +{ + return make<Messages::WindowServer::SetResolutionResponse>(WindowManager::the().set_resolution(message.resolution().width(), message.resolution().height()), WindowManager::the().resolution()); +} + +OwnPtr<Messages::WindowServer::SetWindowTitleResponse> ClientConnection::handle(const Messages::WindowServer::SetWindowTitle& message) +{ + auto it = m_windows.find(message.window_id()); + if (it == m_windows.end()) { + did_misbehave("SetWindowTitle: Bad window ID"); + return nullptr; + } + it->value->set_title(message.title()); + return make<Messages::WindowServer::SetWindowTitleResponse>(); +} + +OwnPtr<Messages::WindowServer::GetWindowTitleResponse> ClientConnection::handle(const Messages::WindowServer::GetWindowTitle& message) +{ + auto it = m_windows.find(message.window_id()); + if (it == m_windows.end()) { + did_misbehave("GetWindowTitle: Bad window ID"); + return nullptr; + } + return make<Messages::WindowServer::GetWindowTitleResponse>(it->value->title()); +} + +OwnPtr<Messages::WindowServer::SetWindowIconBitmapResponse> ClientConnection::handle(const Messages::WindowServer::SetWindowIconBitmap& message) +{ + auto it = m_windows.find(message.window_id()); + if (it == m_windows.end()) { + did_misbehave("SetWindowIconBitmap: Bad window ID"); + return nullptr; + } + auto& window = *(*it).value; + + if (message.icon().is_valid()) { + window.set_icon(*message.icon().bitmap()); + } else { + window.set_default_icon(); + } + + window.frame().invalidate_title_bar(); + WindowManager::the().tell_wm_listeners_window_icon_changed(window); + return make<Messages::WindowServer::SetWindowIconBitmapResponse>(); +} + +OwnPtr<Messages::WindowServer::SetWindowRectResponse> ClientConnection::handle(const Messages::WindowServer::SetWindowRect& message) +{ + int window_id = message.window_id(); + auto it = m_windows.find(window_id); + if (it == m_windows.end()) { + did_misbehave("SetWindowRect: Bad window ID"); + return nullptr; + } + auto& window = *(*it).value; + if (window.is_fullscreen()) { + dbg() << "ClientConnection: Ignoring SetWindowRect request for fullscreen window"; + return nullptr; + } + auto normalized_rect = normalize_window_rect(message.rect(), window.type()); + window.set_rect(normalized_rect); + window.request_update(normalized_rect); + return make<Messages::WindowServer::SetWindowRectResponse>(normalized_rect); +} + +OwnPtr<Messages::WindowServer::GetWindowRectResponse> ClientConnection::handle(const Messages::WindowServer::GetWindowRect& message) +{ + int window_id = message.window_id(); + auto it = m_windows.find(window_id); + if (it == m_windows.end()) { + did_misbehave("GetWindowRect: Bad window ID"); + return nullptr; + } + return make<Messages::WindowServer::GetWindowRectResponse>(it->value->rect()); +} + +OwnPtr<Messages::WindowServer::SetClipboardContentsResponse> ClientConnection::handle(const Messages::WindowServer::SetClipboardContents& message) +{ + auto shared_buffer = SharedBuffer::create_from_shbuf_id(message.shbuf_id()); + if (!shared_buffer) { + did_misbehave("SetClipboardContents: Bad shared buffer ID"); + return nullptr; + } + Clipboard::the().set_data(*shared_buffer, message.content_size(), message.content_type()); + return make<Messages::WindowServer::SetClipboardContentsResponse>(); +} + +OwnPtr<Messages::WindowServer::GetClipboardContentsResponse> ClientConnection::handle(const Messages::WindowServer::GetClipboardContents&) +{ + auto& clipboard = Clipboard::the(); + + i32 shbuf_id = -1; + if (clipboard.size()) { + // FIXME: Optimize case where an app is copy/pasting within itself. + // We can just reuse the SharedBuffer then, since it will have the same peer PID. + // It would be even nicer if a SharedBuffer could have an arbitrary number of clients.. + RefPtr<SharedBuffer> shared_buffer = SharedBuffer::create_with_size(clipboard.size()); + ASSERT(shared_buffer); + memcpy(shared_buffer->data(), clipboard.data(), clipboard.size()); + shared_buffer->seal(); + shared_buffer->share_with(client_pid()); + shbuf_id = shared_buffer->shbuf_id(); + + // FIXME: This is a workaround for the fact that SharedBuffers will go away if neither side is retaining them. + // After we respond to GetClipboardContents, we have to wait for the client to ref the buffer on his side. + m_last_sent_clipboard_content = move(shared_buffer); + } + return make<Messages::WindowServer::GetClipboardContentsResponse>(shbuf_id, clipboard.size(), clipboard.data_type()); +} + +Window* ClientConnection::window_from_id(i32 window_id) +{ + auto it = m_windows.find(window_id); + if (it == m_windows.end()) + return nullptr; + return it->value.ptr(); +} + +OwnPtr<Messages::WindowServer::CreateWindowResponse> ClientConnection::handle(const Messages::WindowServer::CreateWindow& message) +{ + int window_id = m_next_window_id++; + auto window = Window::construct(*this, (WindowType)message.type(), window_id, message.modal(), message.minimizable(), message.frameless(), message.resizable(), message.fullscreen()); + + if (message.parent_window_id()) { + auto* parent_window = window_from_id(message.parent_window_id()); + if (!parent_window) { + did_misbehave("CreateWindow with bad parent_window_id"); + return nullptr; + } + if (parent_window->window_id() == window_id) { + did_misbehave("CreateWindow trying to make a window with itself as parent"); + return nullptr; + } + window->set_parent_window(*parent_window); + } + + window->set_has_alpha_channel(message.has_alpha_channel()); + window->set_title(message.title()); + if (!message.fullscreen()) { + auto normalized_rect = normalize_window_rect(message.rect(), window->type()); + window->set_rect(normalized_rect); + } + if (window->type() == WindowType::Desktop) { + window->set_rect(WindowManager::the().desktop_rect()); + window->recalculate_rect(); + } + window->set_opacity(message.opacity()); + window->set_size_increment(message.size_increment()); + window->set_base_size(message.base_size()); + window->invalidate(); + if (window->type() == WindowType::MenuApplet) + AppletManager::the().add_applet(*window); + m_windows.set(window_id, move(window)); + return make<Messages::WindowServer::CreateWindowResponse>(window_id); +} + +void ClientConnection::destroy_window(Window& window, Vector<i32>& destroyed_window_ids) +{ + for (auto& child_window : window.child_windows()) { + if (!child_window) + continue; + ASSERT(child_window->window_id() != window.window_id()); + destroy_window(*child_window, destroyed_window_ids); + } + + destroyed_window_ids.append(window.window_id()); + + if (window.type() == WindowType::MenuApplet) + AppletManager::the().remove_applet(window); + + WindowManager::the().invalidate(window); + remove_child(window); + m_windows.remove(window.window_id()); +} + +OwnPtr<Messages::WindowServer::DestroyWindowResponse> ClientConnection::handle(const Messages::WindowServer::DestroyWindow& message) +{ + auto it = m_windows.find(message.window_id()); + if (it == m_windows.end()) { + did_misbehave("DestroyWindow: Bad window ID"); + return nullptr; + } + auto& window = *(*it).value; + Vector<i32> destroyed_window_ids; + destroy_window(window, destroyed_window_ids); + return make<Messages::WindowServer::DestroyWindowResponse>(destroyed_window_ids); +} + +void ClientConnection::post_paint_message(Window& window, bool ignore_occlusion) +{ + auto rect_set = window.take_pending_paint_rects(); + if (window.is_minimized() || (!ignore_occlusion && window.is_occluded())) + return; + + post_message(Messages::WindowClient::Paint(window.window_id(), window.size(), rect_set.rects())); +} + +void ClientConnection::handle(const Messages::WindowServer::InvalidateRect& message) +{ + auto it = m_windows.find(message.window_id()); + if (it == m_windows.end()) { + did_misbehave("InvalidateRect: Bad window ID"); + return; + } + auto& window = *(*it).value; + for (size_t i = 0; i < message.rects().size(); ++i) + window.request_update(message.rects()[i].intersected({ {}, window.size() }), message.ignore_occlusion()); +} + +void ClientConnection::handle(const Messages::WindowServer::DidFinishPainting& message) +{ + int window_id = message.window_id(); + auto it = m_windows.find(window_id); + if (it == m_windows.end()) { + did_misbehave("DidFinishPainting: Bad window ID"); + return; + } + auto& window = *(*it).value; + for (auto& rect : message.rects()) + WindowManager::the().invalidate(window, rect); + + WindowSwitcher::the().refresh_if_needed(); +} + +OwnPtr<Messages::WindowServer::SetWindowBackingStoreResponse> ClientConnection::handle(const Messages::WindowServer::SetWindowBackingStore& message) +{ + int window_id = message.window_id(); + auto it = m_windows.find(window_id); + if (it == m_windows.end()) { + did_misbehave("SetWindowBackingStore: Bad window ID"); + return nullptr; + } + auto& window = *(*it).value; + if (window.last_backing_store() && window.last_backing_store()->shbuf_id() == message.shbuf_id()) { + window.swap_backing_stores(); + } else { + auto shared_buffer = SharedBuffer::create_from_shbuf_id(message.shbuf_id()); + if (!shared_buffer) + return make<Messages::WindowServer::SetWindowBackingStoreResponse>(); + auto backing_store = Gfx::Bitmap::create_with_shared_buffer( + message.has_alpha_channel() ? Gfx::BitmapFormat::RGBA32 : Gfx::BitmapFormat::RGB32, + *shared_buffer, + message.size()); + window.set_backing_store(move(backing_store)); + } + + if (message.flush_immediately()) + window.invalidate(); + + return make<Messages::WindowServer::SetWindowBackingStoreResponse>(); +} + +OwnPtr<Messages::WindowServer::SetGlobalCursorTrackingResponse> ClientConnection::handle(const Messages::WindowServer::SetGlobalCursorTracking& message) +{ + int window_id = message.window_id(); + auto it = m_windows.find(window_id); + if (it == m_windows.end()) { + did_misbehave("SetGlobalCursorTracking: Bad window ID"); + return nullptr; + } + it->value->set_global_cursor_tracking_enabled(message.enabled()); + return make<Messages::WindowServer::SetGlobalCursorTrackingResponse>(); +} + +OwnPtr<Messages::WindowServer::SetWindowOverrideCursorResponse> ClientConnection::handle(const Messages::WindowServer::SetWindowOverrideCursor& message) +{ + auto it = m_windows.find(message.window_id()); + if (it == m_windows.end()) { + did_misbehave("SetWindowOverrideCursor: Bad window ID"); + return nullptr; + } + auto& window = *(*it).value; + window.set_override_cursor(Cursor::create((StandardCursor)message.cursor_type())); + return make<Messages::WindowServer::SetWindowOverrideCursorResponse>(); +} + +OwnPtr<Messages::WindowServer::SetWindowHasAlphaChannelResponse> ClientConnection::handle(const Messages::WindowServer::SetWindowHasAlphaChannel& message) +{ + auto it = m_windows.find(message.window_id()); + if (it == m_windows.end()) { + did_misbehave("SetWindowHasAlphaChannel: Bad window ID"); + return nullptr; + } + it->value->set_has_alpha_channel(message.has_alpha_channel()); + return make<Messages::WindowServer::SetWindowHasAlphaChannelResponse>(); +} + +void ClientConnection::handle(const Messages::WindowServer::WM_SetActiveWindow& message) +{ + auto* client = ClientConnection::from_client_id(message.client_id()); + if (!client) { + did_misbehave("WM_SetActiveWindow: Bad client ID"); + return; + } + auto it = client->m_windows.find(message.window_id()); + if (it == client->m_windows.end()) { + did_misbehave("WM_SetActiveWindow: Bad window ID"); + return; + } + auto& window = *(*it).value; + window.set_minimized(false); + WindowManager::the().move_to_front_and_make_active(window); +} + +void ClientConnection::handle(const Messages::WindowServer::WM_PopupWindowMenu& message) +{ + auto* client = ClientConnection::from_client_id(message.client_id()); + if (!client) { + did_misbehave("WM_PopupWindowMenu: Bad client ID"); + return; + } + auto it = client->m_windows.find(message.window_id()); + if (it == client->m_windows.end()) { + did_misbehave("WM_PopupWindowMenu: Bad window ID"); + return; + } + auto& window = *(*it).value; + window.popup_window_menu(message.screen_position()); +} + +void ClientConnection::handle(const Messages::WindowServer::WM_StartWindowResize& request) +{ + auto* client = ClientConnection::from_client_id(request.client_id()); + if (!client) { + did_misbehave("WM_StartWindowResize: Bad client ID"); + return; + } + auto it = client->m_windows.find(request.window_id()); + if (it == client->m_windows.end()) { + did_misbehave("WM_StartWindowResize: Bad window ID"); + return; + } + auto& window = *(*it).value; + // FIXME: We are cheating a bit here by using the current cursor location and hard-coding the left button. + // Maybe the client should be allowed to specify what initiated this request? + WindowManager::the().start_window_resize(window, Screen::the().cursor_location(), MouseButton::Left); +} + +void ClientConnection::handle(const Messages::WindowServer::WM_SetWindowMinimized& message) +{ + auto* client = ClientConnection::from_client_id(message.client_id()); + if (!client) { + did_misbehave("WM_SetWindowMinimized: Bad client ID"); + return; + } + auto it = client->m_windows.find(message.window_id()); + if (it == client->m_windows.end()) { + did_misbehave("WM_SetWindowMinimized: Bad window ID"); + return; + } + auto& window = *(*it).value; + window.set_minimized(message.minimized()); +} + +OwnPtr<Messages::WindowServer::GreetResponse> ClientConnection::handle(const Messages::WindowServer::Greet&) +{ + return make<Messages::WindowServer::GreetResponse>(client_id(), Screen::the().rect(), Gfx::current_system_theme_buffer_id()); +} + +bool ClientConnection::is_showing_modal_window() const +{ + for (auto& it : m_windows) { + auto& window = *it.value; + if (window.is_visible() && window.is_modal()) + return true; + } + return false; +} + +void ClientConnection::handle(const Messages::WindowServer::WM_SetWindowTaskbarRect& message) +{ + auto* client = ClientConnection::from_client_id(message.client_id()); + if (!client) { + did_misbehave("WM_SetWindowTaskbarRect: Bad client ID"); + return; + } + auto it = client->m_windows.find(message.window_id()); + if (it == client->m_windows.end()) { + did_misbehave("WM_SetWindowTaskbarRect: Bad window ID"); + return; + } + auto& window = *(*it).value; + window.set_taskbar_rect(message.rect()); +} + +OwnPtr<Messages::WindowServer::StartDragResponse> ClientConnection::handle(const Messages::WindowServer::StartDrag& message) +{ + auto& wm = WindowManager::the(); + if (wm.dnd_client()) + return make<Messages::WindowServer::StartDragResponse>(false); + + RefPtr<Gfx::Bitmap> bitmap; + if (message.bitmap_id() != -1) { + auto shared_buffer = SharedBuffer::create_from_shbuf_id(message.bitmap_id()); + ssize_t size_in_bytes = message.bitmap_size().area() * sizeof(Gfx::RGBA32); + if (size_in_bytes > shared_buffer->size()) { + did_misbehave("SetAppletBackingStore: Shared buffer is too small for applet size"); + return nullptr; + } + bitmap = Gfx::Bitmap::create_with_shared_buffer(Gfx::BitmapFormat::RGBA32, *shared_buffer, message.bitmap_size()); + } + + wm.start_dnd_drag(*this, message.text(), bitmap, message.data_type(), message.data()); + return make<Messages::WindowServer::StartDragResponse>(true); +} + +OwnPtr<Messages::WindowServer::SetSystemMenuResponse> ClientConnection::handle(const Messages::WindowServer::SetSystemMenu& message) +{ + auto it = m_menus.find(message.menu_id()); + if (it == m_menus.end()) { + did_misbehave("SetSystemMenu called with invalid menu ID"); + return nullptr; + } + + auto& menu = it->value; + MenuManager::the().set_system_menu(menu); + return make<Messages::WindowServer::SetSystemMenuResponse>(); +} + +OwnPtr<Messages::WindowServer::SetSystemThemeResponse> ClientConnection::handle(const Messages::WindowServer::SetSystemTheme& message) +{ + bool success = WindowManager::the().update_theme(message.theme_path(), message.theme_name()); + return make<Messages::WindowServer::SetSystemThemeResponse>(success); +} + +OwnPtr<Messages::WindowServer::GetSystemThemeResponse> ClientConnection::handle(const Messages::WindowServer::GetSystemTheme&) +{ + auto wm_config = Core::ConfigFile::open("/etc/WindowServer/WindowServer.ini"); + auto name = wm_config->read_entry("Theme", "Name"); + return make<Messages::WindowServer::GetSystemThemeResponse>(name); +} + +void ClientConnection::boost() +{ + // FIXME: Re-enable this when we have a solution for boosting. +#if 0 + if (set_process_boost(client_pid(), 10) < 0) + perror("boost: set_process_boost"); +#endif +} + +void ClientConnection::deboost() +{ + // FIXME: Re-enable this when we have a solution for boosting. +#if 0 + if (set_process_boost(client_pid(), 0) < 0) + perror("deboost: set_process_boost"); +#endif +} + +OwnPtr<Messages::WindowServer::SetWindowBaseSizeAndSizeIncrementResponse> ClientConnection::handle(const Messages::WindowServer::SetWindowBaseSizeAndSizeIncrement& message) +{ + auto it = m_windows.find(message.window_id()); + if (it == m_windows.end()) { + did_misbehave("SetWindowBaseSizeAndSizeIncrementResponse: Bad window ID"); + return nullptr; + } + + auto& window = *it->value; + window.set_base_size(message.base_size()); + window.set_size_increment(message.size_increment()); + + return make<Messages::WindowServer::SetWindowBaseSizeAndSizeIncrementResponse>(); +} + +void ClientConnection::handle(const Messages::WindowServer::EnableDisplayLink&) +{ + if (m_has_display_link) + return; + m_has_display_link = true; + Compositor::the().increment_display_link_count({}); +} + +void ClientConnection::handle(const Messages::WindowServer::DisableDisplayLink&) +{ + if (!m_has_display_link) + return; + m_has_display_link = false; + Compositor::the().decrement_display_link_count({}); +} + +void ClientConnection::notify_display_link(Badge<Compositor>) +{ + if (!m_has_display_link) + return; + + post_message(Messages::WindowClient::DisplayLinkNotification()); +} + +} diff --git a/Services/WindowServer/ClientConnection.h b/Services/WindowServer/ClientConnection.h new file mode 100644 index 0000000000..92c2f2fa08 --- /dev/null +++ b/Services/WindowServer/ClientConnection.h @@ -0,0 +1,148 @@ +/* + * Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org> + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#pragma once + +#include <AK/Badge.h> +#include <AK/Function.h> +#include <AK/HashMap.h> +#include <AK/OwnPtr.h> +#include <AK/WeakPtr.h> +#include <LibCore/Object.h> +#include <LibGfx/Bitmap.h> +#include <LibIPC/ClientConnection.h> +#include <WindowServer/Event.h> +#include <WindowServer/WindowServerEndpoint.h> + +namespace WindowServer { + +class Compositor; +class Window; +class Menu; +class MenuBar; + +class ClientConnection final + : public IPC::ClientConnection<WindowServerEndpoint> + , public WindowServerEndpoint { + C_OBJECT(ClientConnection) +public: + ~ClientConnection() override; + virtual void die() override; + + void boost(); + void deboost(); + + static ClientConnection* from_client_id(int client_id); + static void for_each_client(Function<void(ClientConnection&)>); + + MenuBar* app_menubar() { return m_app_menubar.ptr(); } + + bool is_showing_modal_window() const; + + void notify_about_new_screen_rect(const Gfx::Rect&); + void notify_about_clipboard_contents_changed(); + void post_paint_message(Window&, bool ignore_occlusion = false); + + Menu* find_menu_by_id(int menu_id) + { + auto menu = m_menus.get(menu_id); + if (!menu.has_value()) + return nullptr; + return const_cast<Menu*>(menu.value().ptr()); + } + + void notify_display_link(Badge<Compositor>); + +private: + explicit ClientConnection(Core::LocalSocket&, int client_id); + + void destroy_window(Window&, Vector<i32>& destroyed_window_ids); + + virtual OwnPtr<Messages::WindowServer::GreetResponse> handle(const Messages::WindowServer::Greet&) override; + virtual OwnPtr<Messages::WindowServer::CreateMenubarResponse> handle(const Messages::WindowServer::CreateMenubar&) override; + virtual OwnPtr<Messages::WindowServer::DestroyMenubarResponse> handle(const Messages::WindowServer::DestroyMenubar&) override; + virtual OwnPtr<Messages::WindowServer::CreateMenuResponse> handle(const Messages::WindowServer::CreateMenu&) override; + virtual OwnPtr<Messages::WindowServer::DestroyMenuResponse> handle(const Messages::WindowServer::DestroyMenu&) override; + virtual OwnPtr<Messages::WindowServer::AddMenuToMenubarResponse> handle(const Messages::WindowServer::AddMenuToMenubar&) override; + virtual OwnPtr<Messages::WindowServer::SetApplicationMenubarResponse> handle(const Messages::WindowServer::SetApplicationMenubar&) override; + virtual OwnPtr<Messages::WindowServer::AddMenuItemResponse> handle(const Messages::WindowServer::AddMenuItem&) override; + virtual OwnPtr<Messages::WindowServer::AddMenuSeparatorResponse> handle(const Messages::WindowServer::AddMenuSeparator&) override; + virtual OwnPtr<Messages::WindowServer::UpdateMenuItemResponse> handle(const Messages::WindowServer::UpdateMenuItem&) override; + virtual OwnPtr<Messages::WindowServer::CreateWindowResponse> handle(const Messages::WindowServer::CreateWindow&) override; + virtual OwnPtr<Messages::WindowServer::DestroyWindowResponse> handle(const Messages::WindowServer::DestroyWindow&) override; + virtual OwnPtr<Messages::WindowServer::SetWindowTitleResponse> handle(const Messages::WindowServer::SetWindowTitle&) override; + virtual OwnPtr<Messages::WindowServer::GetWindowTitleResponse> handle(const Messages::WindowServer::GetWindowTitle&) override; + virtual OwnPtr<Messages::WindowServer::SetWindowRectResponse> handle(const Messages::WindowServer::SetWindowRect&) override; + virtual OwnPtr<Messages::WindowServer::GetWindowRectResponse> handle(const Messages::WindowServer::GetWindowRect&) override; + virtual void handle(const Messages::WindowServer::InvalidateRect&) override; + virtual void handle(const Messages::WindowServer::DidFinishPainting&) override; + virtual OwnPtr<Messages::WindowServer::SetGlobalCursorTrackingResponse> handle(const Messages::WindowServer::SetGlobalCursorTracking&) override; + virtual OwnPtr<Messages::WindowServer::SetWindowOpacityResponse> handle(const Messages::WindowServer::SetWindowOpacity&) override; + virtual OwnPtr<Messages::WindowServer::SetWindowBackingStoreResponse> handle(const Messages::WindowServer::SetWindowBackingStore&) override; + virtual OwnPtr<Messages::WindowServer::GetClipboardContentsResponse> handle(const Messages::WindowServer::GetClipboardContents&) override; + virtual OwnPtr<Messages::WindowServer::SetClipboardContentsResponse> handle(const Messages::WindowServer::SetClipboardContents&) override; + virtual void handle(const Messages::WindowServer::WM_SetActiveWindow&) override; + virtual void handle(const Messages::WindowServer::WM_SetWindowMinimized&) override; + virtual void handle(const Messages::WindowServer::WM_StartWindowResize&) override; + virtual void handle(const Messages::WindowServer::WM_PopupWindowMenu&) override; + virtual OwnPtr<Messages::WindowServer::SetWindowHasAlphaChannelResponse> handle(const Messages::WindowServer::SetWindowHasAlphaChannel&) override; + virtual OwnPtr<Messages::WindowServer::MoveWindowToFrontResponse> handle(const Messages::WindowServer::MoveWindowToFront&) override; + virtual OwnPtr<Messages::WindowServer::SetFullscreenResponse> handle(const Messages::WindowServer::SetFullscreen&) override; + virtual void handle(const Messages::WindowServer::AsyncSetWallpaper&) override; + virtual OwnPtr<Messages::WindowServer::SetBackgroundColorResponse> handle(const Messages::WindowServer::SetBackgroundColor&) override; + virtual OwnPtr<Messages::WindowServer::SetWallpaperModeResponse> handle(const Messages::WindowServer::SetWallpaperMode&) override; + virtual OwnPtr<Messages::WindowServer::GetWallpaperResponse> handle(const Messages::WindowServer::GetWallpaper&) override; + virtual OwnPtr<Messages::WindowServer::SetResolutionResponse> handle(const Messages::WindowServer::SetResolution&) override; + virtual OwnPtr<Messages::WindowServer::SetWindowOverrideCursorResponse> handle(const Messages::WindowServer::SetWindowOverrideCursor&) override; + virtual OwnPtr<Messages::WindowServer::PopupMenuResponse> handle(const Messages::WindowServer::PopupMenu&) override; + virtual OwnPtr<Messages::WindowServer::DismissMenuResponse> handle(const Messages::WindowServer::DismissMenu&) override; + virtual OwnPtr<Messages::WindowServer::SetWindowIconBitmapResponse> handle(const Messages::WindowServer::SetWindowIconBitmap&) override; + virtual void handle(const Messages::WindowServer::WM_SetWindowTaskbarRect&) override; + virtual OwnPtr<Messages::WindowServer::StartDragResponse> handle(const Messages::WindowServer::StartDrag&) override; + virtual OwnPtr<Messages::WindowServer::SetSystemMenuResponse> handle(const Messages::WindowServer::SetSystemMenu&) override; + virtual OwnPtr<Messages::WindowServer::SetSystemThemeResponse> handle(const Messages::WindowServer::SetSystemTheme&) override; + virtual OwnPtr<Messages::WindowServer::GetSystemThemeResponse> handle(const Messages::WindowServer::GetSystemTheme&) override; + virtual OwnPtr<Messages::WindowServer::SetWindowBaseSizeAndSizeIncrementResponse> handle(const Messages::WindowServer::SetWindowBaseSizeAndSizeIncrement&) override; + virtual void handle(const Messages::WindowServer::EnableDisplayLink&) override; + virtual void handle(const Messages::WindowServer::DisableDisplayLink&) override; + + Window* window_from_id(i32 window_id); + + HashMap<int, NonnullRefPtr<Window>> m_windows; + HashMap<int, NonnullOwnPtr<MenuBar>> m_menubars; + HashMap<int, NonnullRefPtr<Menu>> m_menus; + WeakPtr<MenuBar> m_app_menubar; + + int m_next_menubar_id { 10000 }; + int m_next_menu_id { 20000 }; + int m_next_window_id { 1982 }; + + bool m_has_display_link { false }; + + RefPtr<SharedBuffer> m_last_sent_clipboard_content; +}; + +} diff --git a/Services/WindowServer/Clipboard.cpp b/Services/WindowServer/Clipboard.cpp new file mode 100644 index 0000000000..4d9db8b4bc --- /dev/null +++ b/Services/WindowServer/Clipboard.cpp @@ -0,0 +1,78 @@ +/* + * Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org> + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#include <WindowServer/Clipboard.h> + +namespace WindowServer { + +Clipboard& Clipboard::the() +{ + static Clipboard* s_the; + if (!s_the) + s_the = new Clipboard; + return *s_the; +} + +Clipboard::Clipboard() +{ +} + +Clipboard::~Clipboard() +{ +} + +const u8* Clipboard::data() const +{ + if (!m_shared_buffer) + return nullptr; + return (const u8*)m_shared_buffer->data(); +} + +int Clipboard::size() const +{ + if (!m_shared_buffer) + return 0; + return m_contents_size; +} + +void Clipboard::clear() +{ + m_shared_buffer = nullptr; + m_contents_size = 0; +} + +void Clipboard::set_data(NonnullRefPtr<SharedBuffer>&& data, int contents_size, const String& data_type) +{ + dbg() << "Clipboard::set_data <- [" << data_type << "] " << data->data() << " (" << contents_size << " bytes)"; + m_shared_buffer = move(data); + m_contents_size = contents_size; + m_data_type = data_type; + + if (on_content_change) + on_content_change(); +} + +} diff --git a/Services/WindowServer/Clipboard.h b/Services/WindowServer/Clipboard.h new file mode 100644 index 0000000000..6cb9140234 --- /dev/null +++ b/Services/WindowServer/Clipboard.h @@ -0,0 +1,62 @@ +/* + * Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org> + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#pragma once + +#include <AK/Function.h> +#include <AK/SharedBuffer.h> +#include <AK/String.h> + +namespace WindowServer { + +class Clipboard { +public: + static Clipboard& the(); + ~Clipboard(); + + bool has_data() const + { + return m_shared_buffer; + } + + const String& data_type() const { return m_data_type; } + const u8* data() const; + int size() const; + + void clear(); + void set_data(NonnullRefPtr<SharedBuffer>&&, int contents_size, const String& data_type); + + Function<void()> on_content_change; + +private: + Clipboard(); + + String m_data_type; + RefPtr<SharedBuffer> m_shared_buffer; + int m_contents_size { 0 }; +}; + +} diff --git a/Services/WindowServer/Compositor.cpp b/Services/WindowServer/Compositor.cpp new file mode 100644 index 0000000000..57f87178df --- /dev/null +++ b/Services/WindowServer/Compositor.cpp @@ -0,0 +1,507 @@ +/* + * Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org> + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#include "Compositor.h" +#include "ClientConnection.h" +#include "Event.h" +#include "EventLoop.h" +#include "Screen.h" +#include "Window.h" +#include "WindowManager.h" +#include <AK/Memory.h> +#include <LibCore/Timer.h> +#include <LibGfx/Font.h> +#include <LibGfx/Painter.h> +#include <LibThread/BackgroundAction.h> + +namespace WindowServer { + +Compositor& Compositor::the() +{ + static Compositor s_the; + return s_the; +} + +WallpaperMode mode_to_enum(const String& name) +{ + if (name == "simple") + return WallpaperMode::Simple; + if (name == "tile") + return WallpaperMode::Tile; + if (name == "center") + return WallpaperMode::Center; + if (name == "scaled") + return WallpaperMode::Scaled; + return WallpaperMode::Simple; +} + +Compositor::Compositor() +{ + m_display_link_notify_timer = add<Core::Timer>( + 1000 / 60, [this] { + notify_display_links(); + }); + m_display_link_notify_timer->stop(); + + m_compose_timer = Core::Timer::create_single_shot( + 1000 / 60, + [this] { + compose(); + }, + this); + + m_immediate_compose_timer = Core::Timer::create_single_shot( + 0, + [this] { + compose(); + }, + this); + + m_screen_can_set_buffer = Screen::the().can_set_buffer(); + init_bitmaps(); +} + +void Compositor::init_bitmaps() +{ + auto& screen = Screen::the(); + auto size = screen.size(); + + m_front_bitmap = Gfx::Bitmap::create_wrapper(Gfx::BitmapFormat::RGB32, size, screen.pitch(), screen.scanline(0)); + + if (m_screen_can_set_buffer) + m_back_bitmap = Gfx::Bitmap::create_wrapper(Gfx::BitmapFormat::RGB32, size, screen.pitch(), screen.scanline(size.height())); + else + m_back_bitmap = Gfx::Bitmap::create(Gfx::BitmapFormat::RGB32, size); + + m_front_painter = make<Gfx::Painter>(*m_front_bitmap); + m_back_painter = make<Gfx::Painter>(*m_back_bitmap); + + m_buffers_are_flipped = false; + + invalidate(); +} + +void Compositor::compose() +{ + auto& wm = WindowManager::the(); + if (m_wallpaper_mode == WallpaperMode::Unchecked) + m_wallpaper_mode = mode_to_enum(wm.wm_config()->read_entry("Background", "Mode", "simple")); + auto& ws = Screen::the(); + + auto dirty_rects = move(m_dirty_rects); + + if (dirty_rects.size() == 0) { + // nothing dirtied since the last compose pass. + return; + } + + dirty_rects.add(Gfx::Rect::intersection(m_last_geometry_label_rect, Screen::the().rect())); + dirty_rects.add(Gfx::Rect::intersection(m_last_cursor_rect, Screen::the().rect())); + dirty_rects.add(Gfx::Rect::intersection(m_last_dnd_rect, Screen::the().rect())); + dirty_rects.add(Gfx::Rect::intersection(current_cursor_rect(), Screen::the().rect())); + + auto any_dirty_rect_intersects_window = [&dirty_rects](const Window& window) { + auto window_frame_rect = window.frame().rect(); + for (auto& dirty_rect : dirty_rects.rects()) { + if (dirty_rect.intersects(window_frame_rect)) + return true; + } + return false; + }; + + Color background_color = wm.palette().desktop_background(); + String background_color_entry = wm.wm_config()->read_entry("Background", "Color", ""); + if (!background_color_entry.is_empty()) { + background_color = Color::from_string(background_color_entry).value_or(background_color); + } + + // Paint the wallpaper. + for (auto& dirty_rect : dirty_rects.rects()) { + if (wm.any_opaque_window_contains_rect(dirty_rect)) + continue; + // FIXME: If the wallpaper is opaque, no need to fill with color! + m_back_painter->fill_rect(dirty_rect, background_color); + if (m_wallpaper) { + if (m_wallpaper_mode == WallpaperMode::Simple) { + m_back_painter->blit(dirty_rect.location(), *m_wallpaper, dirty_rect); + } else if (m_wallpaper_mode == WallpaperMode::Center) { + Gfx::Point offset { ws.size().width() / 2 - m_wallpaper->size().width() / 2, + ws.size().height() / 2 - m_wallpaper->size().height() / 2 }; + m_back_painter->blit_offset(dirty_rect.location(), *m_wallpaper, + dirty_rect, offset); + } else if (m_wallpaper_mode == WallpaperMode::Tile) { + m_back_painter->draw_tiled_bitmap(dirty_rect, *m_wallpaper); + } else if (m_wallpaper_mode == WallpaperMode::Scaled) { + float hscale = (float)m_wallpaper->size().width() / (float)ws.size().width(); + float vscale = (float)m_wallpaper->size().height() / (float)ws.size().height(); + + m_back_painter->blit_scaled(dirty_rect, *m_wallpaper, dirty_rect, hscale, vscale); + } else { + ASSERT_NOT_REACHED(); + } + } + } + + auto compose_window = [&](Window& window) -> IterationDecision { + if (!any_dirty_rect_intersects_window(window)) + return IterationDecision::Continue; + Gfx::PainterStateSaver saver(*m_back_painter); + m_back_painter->add_clip_rect(window.frame().rect()); + RefPtr<Gfx::Bitmap> backing_store = window.backing_store(); + for (auto& dirty_rect : dirty_rects.rects()) { + if (wm.any_opaque_window_above_this_one_contains_rect(window, dirty_rect)) + continue; + Gfx::PainterStateSaver saver(*m_back_painter); + m_back_painter->add_clip_rect(dirty_rect); + if (!backing_store) + m_back_painter->fill_rect(dirty_rect, wm.palette().window()); + if (!window.is_fullscreen()) + window.frame().paint(*m_back_painter); + if (!backing_store) + continue; + + // Decide where we would paint this window's backing store. + // This is subtly different from widow.rect(), because window + // size may be different from its backing store size. This + // happens when the window has been resized and the client + // has not yet attached a new backing store. In this case, + // we want to try to blit the backing store at the same place + // it was previously, and fill the rest of the window with its + // background color. + Gfx::Rect backing_rect; + backing_rect.set_size(backing_store->size()); + switch (WindowManager::the().resize_direction_of_window(window)) { + case ResizeDirection::None: + case ResizeDirection::Right: + case ResizeDirection::Down: + case ResizeDirection::DownRight: + backing_rect.set_location(window.rect().location()); + break; + case ResizeDirection::Left: + case ResizeDirection::Up: + case ResizeDirection::UpLeft: + backing_rect.set_right_without_resize(window.rect().right()); + backing_rect.set_bottom_without_resize(window.rect().bottom()); + break; + case ResizeDirection::UpRight: + backing_rect.set_left(window.rect().left()); + backing_rect.set_bottom_without_resize(window.rect().bottom()); + break; + case ResizeDirection::DownLeft: + backing_rect.set_right_without_resize(window.rect().right()); + backing_rect.set_top(window.rect().top()); + break; + } + + Gfx::Rect dirty_rect_in_backing_coordinates = dirty_rect + .intersected(window.rect()) + .intersected(backing_rect) + .translated(-backing_rect.location()); + + if (dirty_rect_in_backing_coordinates.is_empty()) + continue; + auto dst = backing_rect.location().translated(dirty_rect_in_backing_coordinates.location()); + + m_back_painter->blit(dst, *backing_store, dirty_rect_in_backing_coordinates, window.opacity()); + for (auto background_rect : window.rect().shatter(backing_rect)) + m_back_painter->fill_rect(background_rect, wm.palette().window()); + } + return IterationDecision::Continue; + }; + + // Paint the window stack. + if (auto* fullscreen_window = wm.active_fullscreen_window()) { + compose_window(*fullscreen_window); + } else { + wm.for_each_visible_window_from_back_to_front([&](Window& window) { + return compose_window(window); + }); + + draw_geometry_label(); + } + + run_animations(); + + draw_cursor(); + + if (m_flash_flush) { + for (auto& rect : dirty_rects.rects()) + m_front_painter->fill_rect(rect, Color::Yellow); + } + + if (m_screen_can_set_buffer) + flip_buffers(); + + for (auto& r : dirty_rects.rects()) + flush(r); +} + +void Compositor::flush(const Gfx::Rect& a_rect) +{ + auto rect = Gfx::Rect::intersection(a_rect, Screen::the().rect()); + + Gfx::RGBA32* front_ptr = m_front_bitmap->scanline(rect.y()) + rect.x(); + Gfx::RGBA32* back_ptr = m_back_bitmap->scanline(rect.y()) + rect.x(); + size_t pitch = m_back_bitmap->pitch(); + + // NOTE: The meaning of a flush depends on whether we can flip buffers or not. + // + // If flipping is supported, flushing means that we've flipped, and now we + // copy the changed bits from the front buffer to the back buffer, to keep + // them in sync. + // + // If flipping is not supported, flushing means that we copy the changed + // rects from the backing bitmap to the display framebuffer. + + Gfx::RGBA32* to_ptr; + const Gfx::RGBA32* from_ptr; + + if (m_screen_can_set_buffer) { + to_ptr = back_ptr; + from_ptr = front_ptr; + } else { + to_ptr = front_ptr; + from_ptr = back_ptr; + } + + for (int y = 0; y < rect.height(); ++y) { + fast_u32_copy(to_ptr, from_ptr, rect.width()); + from_ptr = (const Gfx::RGBA32*)((const u8*)from_ptr + pitch); + to_ptr = (Gfx::RGBA32*)((u8*)to_ptr + pitch); + } +} + +void Compositor::invalidate() +{ + m_dirty_rects.clear_with_capacity(); + invalidate(Screen::the().rect()); +} + +void Compositor::invalidate(const Gfx::Rect& a_rect) +{ + auto rect = Gfx::Rect::intersection(a_rect, Screen::the().rect()); + if (rect.is_empty()) + return; + + m_dirty_rects.add(rect); + + // We delay composition by a timer interval, but to not affect latency too + // much, if a pending compose is not already scheduled, we also schedule an + // immediate compose the next spin of the event loop. + if (!m_compose_timer->is_active()) { + m_compose_timer->start(); + m_immediate_compose_timer->start(); + } +} + +bool Compositor::set_background_color(const String& background_color) +{ + auto& wm = WindowManager::the(); + wm.wm_config()->write_entry("Background", "Color", background_color); + bool ret_val = wm.wm_config()->sync(); + + if (ret_val) + Compositor::invalidate(); + + return ret_val; +} + +bool Compositor::set_wallpaper_mode(const String& mode) +{ + auto& wm = WindowManager::the(); + wm.wm_config()->write_entry("Background", "Mode", mode); + bool ret_val = wm.wm_config()->sync(); + + if (ret_val) { + m_wallpaper_mode = mode_to_enum(mode); + Compositor::invalidate(); + } + + return ret_val; +} + +bool Compositor::set_wallpaper(const String& path, Function<void(bool)>&& callback) +{ + LibThread::BackgroundAction<RefPtr<Gfx::Bitmap>>::create( + [path] { + return Gfx::Bitmap::load_from_file(path); + }, + + [this, path, callback = move(callback)](RefPtr<Gfx::Bitmap> bitmap) { + m_wallpaper_path = path; + m_wallpaper = move(bitmap); + invalidate(); + callback(true); + }); + return true; +} + +void Compositor::flip_buffers() +{ + ASSERT(m_screen_can_set_buffer); + swap(m_front_bitmap, m_back_bitmap); + swap(m_front_painter, m_back_painter); + Screen::the().set_buffer(m_buffers_are_flipped ? 0 : 1); + m_buffers_are_flipped = !m_buffers_are_flipped; +} + +void Compositor::run_animations() +{ + static const int minimize_animation_steps = 10; + + WindowManager::the().for_each_window([&](Window& window) { + if (window.in_minimize_animation()) { + int animation_index = window.minimize_animation_index(); + + auto from_rect = window.is_minimized() ? window.frame().rect() : window.taskbar_rect(); + auto to_rect = window.is_minimized() ? window.taskbar_rect() : window.frame().rect(); + + float x_delta_per_step = (float)(from_rect.x() - to_rect.x()) / minimize_animation_steps; + float y_delta_per_step = (float)(from_rect.y() - to_rect.y()) / minimize_animation_steps; + float width_delta_per_step = (float)(from_rect.width() - to_rect.width()) / minimize_animation_steps; + float height_delta_per_step = (float)(from_rect.height() - to_rect.height()) / minimize_animation_steps; + + Gfx::Rect rect { + from_rect.x() - (int)(x_delta_per_step * animation_index), + from_rect.y() - (int)(y_delta_per_step * animation_index), + from_rect.width() - (int)(width_delta_per_step * animation_index), + from_rect.height() - (int)(height_delta_per_step * animation_index) + }; + +#ifdef MINIMIZE_ANIMATION_DEBUG + dbg() << "Minimize animation from " << from_rect << " to " << to_rect << " frame# " << animation_index << " " << rect; +#endif + + m_back_painter->draw_rect(rect, Color::White); + + window.step_minimize_animation(); + if (window.minimize_animation_index() >= minimize_animation_steps) + window.end_minimize_animation(); + + invalidate(rect); + } + return IterationDecision::Continue; + }); +} + +bool Compositor::set_resolution(int desired_width, int desired_height) +{ + auto screen_rect = Screen::the().rect(); + if (screen_rect.width() == desired_width && screen_rect.height() == desired_height) + return true; + + // Make sure it's impossible to set an invalid resolution + ASSERT(desired_width >= 640 && desired_height >= 480); + bool success = Screen::the().set_resolution(desired_width, desired_height); + init_bitmaps(); + compose(); + return success; +} + +Gfx::Rect Compositor::current_cursor_rect() const +{ + auto& wm = WindowManager::the(); + return { Screen::the().cursor_location().translated(-wm.active_cursor().hotspot()), wm.active_cursor().size() }; +} + +void Compositor::invalidate_cursor() +{ + auto& wm = WindowManager::the(); + if (wm.dnd_client()) + invalidate(wm.dnd_rect()); + invalidate(current_cursor_rect()); +} + +void Compositor::draw_geometry_label() +{ + auto& wm = WindowManager::the(); + auto* window_being_moved_or_resized = wm.m_move_window ? wm.m_move_window.ptr() : (wm.m_resize_window ? wm.m_resize_window.ptr() : nullptr); + if (!window_being_moved_or_resized) { + m_last_geometry_label_rect = {}; + return; + } + auto geometry_string = window_being_moved_or_resized->rect().to_string(); + if (!window_being_moved_or_resized->size_increment().is_null()) { + int width_steps = (window_being_moved_or_resized->width() - window_being_moved_or_resized->base_size().width()) / window_being_moved_or_resized->size_increment().width(); + int height_steps = (window_being_moved_or_resized->height() - window_being_moved_or_resized->base_size().height()) / window_being_moved_or_resized->size_increment().height(); + geometry_string = String::format("%s (%dx%d)", geometry_string.characters(), width_steps, height_steps); + } + auto geometry_label_rect = Gfx::Rect { 0, 0, wm.font().width(geometry_string) + 16, wm.font().glyph_height() + 10 }; + geometry_label_rect.center_within(window_being_moved_or_resized->rect()); + m_back_painter->fill_rect(geometry_label_rect, wm.palette().window()); + m_back_painter->draw_rect(geometry_label_rect, wm.palette().threed_shadow2()); + m_back_painter->draw_text(geometry_label_rect, geometry_string, Gfx::TextAlignment::Center, wm.palette().window_text()); + m_last_geometry_label_rect = geometry_label_rect; +} + +void Compositor::draw_cursor() +{ + auto& wm = WindowManager::the(); + Gfx::Rect cursor_rect = current_cursor_rect(); + m_back_painter->blit(cursor_rect.location(), wm.active_cursor().bitmap(), wm.active_cursor().rect()); + + if (wm.dnd_client()) { + auto dnd_rect = wm.dnd_rect(); + m_back_painter->fill_rect(dnd_rect, wm.palette().selection().with_alpha(200)); + if (!wm.dnd_text().is_empty()) { + auto text_rect = dnd_rect; + if (wm.dnd_bitmap()) + text_rect.move_by(wm.dnd_bitmap()->width(), 0); + m_back_painter->draw_text(text_rect, wm.dnd_text(), Gfx::TextAlignment::CenterLeft, wm.palette().selection_text()); + } + if (wm.dnd_bitmap()) { + m_back_painter->blit(dnd_rect.top_left(), *wm.dnd_bitmap(), wm.dnd_bitmap()->rect()); + } + m_last_dnd_rect = dnd_rect; + } else { + m_last_dnd_rect = {}; + } + m_last_cursor_rect = cursor_rect; +} + +void Compositor::notify_display_links() +{ + ClientConnection::for_each_client([](auto& client) { + client.notify_display_link({}); + }); +} + +void Compositor::increment_display_link_count(Badge<ClientConnection>) +{ + ++m_display_link_count; + if (m_display_link_count == 1) + m_display_link_notify_timer->start(); +} + +void Compositor::decrement_display_link_count(Badge<ClientConnection>) +{ + ASSERT(m_display_link_count); + --m_display_link_count; + if (!m_display_link_count) + m_display_link_notify_timer->stop(); +} + +} diff --git a/Services/WindowServer/Compositor.h b/Services/WindowServer/Compositor.h new file mode 100644 index 0000000000..99778d786c --- /dev/null +++ b/Services/WindowServer/Compositor.h @@ -0,0 +1,108 @@ +/* + * Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org> + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#pragma once + +#include <AK/OwnPtr.h> +#include <AK/RefPtr.h> +#include <LibCore/Object.h> +#include <LibGfx/DisjointRectSet.h> +#include <LibGfx/Forward.h> + +namespace WindowServer { + +class ClientConnection; +class Cursor; + +enum class WallpaperMode { + Simple, + Tile, + Center, + Scaled, + Unchecked +}; + +class Compositor final : public Core::Object { + C_OBJECT(Compositor) +public: + static Compositor& the(); + + void compose(); + void invalidate(); + void invalidate(const Gfx::Rect&); + + bool set_resolution(int desired_width, int desired_height); + + bool set_background_color(const String& background_color); + + bool set_wallpaper_mode(const String& mode); + + bool set_wallpaper(const String& path, Function<void(bool)>&& callback); + String wallpaper_path() const { return m_wallpaper_path; } + + void invalidate_cursor(); + Gfx::Rect current_cursor_rect() const; + + void increment_display_link_count(Badge<ClientConnection>); + void decrement_display_link_count(Badge<ClientConnection>); + +private: + Compositor(); + void init_bitmaps(); + void flip_buffers(); + void flush(const Gfx::Rect&); + void draw_cursor(); + void draw_geometry_label(); + void draw_menubar(); + void run_animations(); + void notify_display_links(); + + RefPtr<Core::Timer> m_compose_timer; + RefPtr<Core::Timer> m_immediate_compose_timer; + bool m_flash_flush { false }; + bool m_buffers_are_flipped { false }; + bool m_screen_can_set_buffer { false }; + + RefPtr<Gfx::Bitmap> m_front_bitmap; + RefPtr<Gfx::Bitmap> m_back_bitmap; + OwnPtr<Gfx::Painter> m_back_painter; + OwnPtr<Gfx::Painter> m_front_painter; + + Gfx::DisjointRectSet m_dirty_rects; + + Gfx::Rect m_last_cursor_rect; + Gfx::Rect m_last_dnd_rect; + Gfx::Rect m_last_geometry_label_rect; + + String m_wallpaper_path; + WallpaperMode m_wallpaper_mode { WallpaperMode::Unchecked }; + RefPtr<Gfx::Bitmap> m_wallpaper; + + RefPtr<Core::Timer> m_display_link_notify_timer; + size_t m_display_link_count { 0 }; +}; + +} diff --git a/Services/WindowServer/Cursor.cpp b/Services/WindowServer/Cursor.cpp new file mode 100644 index 0000000000..42e431f481 --- /dev/null +++ b/Services/WindowServer/Cursor.cpp @@ -0,0 +1,77 @@ +/* + * Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org> + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#include <WindowServer/Cursor.h> +#include <WindowServer/WindowManager.h> + +namespace WindowServer { + +Cursor::Cursor(NonnullRefPtr<Gfx::Bitmap>&& bitmap, const Gfx::Point& hotspot) + : m_bitmap(move(bitmap)) + , m_hotspot(hotspot) +{ +} + +Cursor::~Cursor() +{ +} + +NonnullRefPtr<Cursor> Cursor::create(NonnullRefPtr<Gfx::Bitmap>&& bitmap) +{ + return adopt(*new Cursor(move(bitmap), bitmap->rect().center())); +} + +NonnullRefPtr<Cursor> Cursor::create(NonnullRefPtr<Gfx::Bitmap>&& bitmap, const Gfx::Point& hotspot) +{ + return adopt(*new Cursor(move(bitmap), hotspot)); +} + +RefPtr<Cursor> Cursor::create(StandardCursor standard_cursor) +{ + switch (standard_cursor) { + case StandardCursor::None: + return nullptr; + case StandardCursor::Arrow: + return WindowManager::the().arrow_cursor(); + case StandardCursor::IBeam: + return WindowManager::the().i_beam_cursor(); + case StandardCursor::ResizeHorizontal: + return WindowManager::the().resize_horizontally_cursor(); + case StandardCursor::ResizeVertical: + return WindowManager::the().resize_vertically_cursor(); + case StandardCursor::ResizeDiagonalTLBR: + return WindowManager::the().resize_diagonally_tlbr_cursor(); + case StandardCursor::ResizeDiagonalBLTR: + return WindowManager::the().resize_diagonally_bltr_cursor(); + case StandardCursor::Hand: + return WindowManager::the().hand_cursor(); + case StandardCursor::Drag: + return WindowManager::the().drag_cursor(); + } + ASSERT_NOT_REACHED(); +} + +} diff --git a/Services/WindowServer/Cursor.h b/Services/WindowServer/Cursor.h new file mode 100644 index 0000000000..5173bd86a1 --- /dev/null +++ b/Services/WindowServer/Cursor.h @@ -0,0 +1,65 @@ +/* + * Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org> + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#pragma once + +#include <LibGfx/Bitmap.h> + +namespace WindowServer { + +enum class StandardCursor { + None = 0, + Arrow, + IBeam, + ResizeHorizontal, + ResizeVertical, + ResizeDiagonalTLBR, + ResizeDiagonalBLTR, + Hand, + Drag, +}; + +class Cursor : public RefCounted<Cursor> { +public: + static NonnullRefPtr<Cursor> create(NonnullRefPtr<Gfx::Bitmap>&&, const Gfx::Point& hotspot); + static NonnullRefPtr<Cursor> create(NonnullRefPtr<Gfx::Bitmap>&&); + static RefPtr<Cursor> create(StandardCursor); + ~Cursor(); + + Gfx::Point hotspot() const { return m_hotspot; } + const Gfx::Bitmap& bitmap() const { return *m_bitmap; } + + Gfx::Rect rect() const { return m_bitmap->rect(); } + Gfx::Size size() const { return m_bitmap->size(); } + +private: + Cursor(NonnullRefPtr<Gfx::Bitmap>&&, const Gfx::Point&); + + RefPtr<Gfx::Bitmap> m_bitmap; + Gfx::Point m_hotspot; +}; + +} diff --git a/Services/WindowServer/Event.h b/Services/WindowServer/Event.h new file mode 100644 index 0000000000..48def26519 --- /dev/null +++ b/Services/WindowServer/Event.h @@ -0,0 +1,157 @@ +/* + * Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org> + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#pragma once + +#include <AK/String.h> +#include <Kernel/KeyCode.h> +#include <LibCore/Event.h> +#include <LibGfx/Rect.h> +#include <WindowServer/Cursor.h> +#include <WindowServer/WindowType.h> + +namespace WindowServer { + +class Event : public Core::Event { +public: + enum Type { + Invalid = 3000, + MouseMove, + MouseDown, + MouseDoubleClick, + MouseUp, + MouseWheel, + WindowEntered, + WindowLeft, + KeyDown, + KeyUp, + WindowActivated, + WindowDeactivated, + WindowCloseRequest, + WindowResized, + }; + + Event() {} + explicit Event(Type type) + : Core::Event(type) + { + } + virtual ~Event() {} + + bool is_mouse_event() const { return type() == MouseMove || type() == MouseDown || type() == MouseDoubleClick || type() == MouseUp || type() == MouseWheel; } + bool is_key_event() const { return type() == KeyUp || type() == KeyDown; } +}; + +enum class MouseButton : u8 { + None = 0, + Left = 1, + Right = 2, + Middle = 4, + Back = 8, + Forward = 16, +}; + +class KeyEvent final : public Event { +public: + KeyEvent(Type type, int key, char character, u8 modifiers) + : Event(type) + , m_key(key) + , m_character(character) + , m_modifiers(modifiers) + { + } + + int key() const { return m_key; } + bool ctrl() const { return m_modifiers & Mod_Ctrl; } + bool alt() const { return m_modifiers & Mod_Alt; } + bool shift() const { return m_modifiers & Mod_Shift; } + bool logo() const { return m_modifiers & Mod_Logo; } + u8 modifiers() const { return m_modifiers; } + char character() const { return m_character; } + +private: + friend class EventLoop; + friend class Screen; + int m_key { 0 }; + char m_character { 0 }; + u8 m_modifiers { 0 }; +}; + +class MouseEvent final : public Event { +public: + MouseEvent(Type type, const Gfx::Point& position, unsigned buttons, MouseButton button, unsigned modifiers, int wheel_delta = 0) + : Event(type) + , m_position(position) + , m_buttons(buttons) + , m_button(button) + , m_modifiers(modifiers) + , m_wheel_delta(wheel_delta) + { + } + + Gfx::Point position() const { return m_position; } + int x() const { return m_position.x(); } + int y() const { return m_position.y(); } + MouseButton button() const { return m_button; } + unsigned buttons() const { return m_buttons; } + unsigned modifiers() const { return m_modifiers; } + int wheel_delta() const { return m_wheel_delta; } + bool is_drag() const { return m_drag; } + const String& drag_data_type() const { return m_drag_data_type; } + + void set_drag(bool b) { m_drag = b; } + void set_drag_data_type(const String& drag_data_type) { m_drag_data_type = drag_data_type; } + + MouseEvent translated(const Gfx::Point& delta) const { return MouseEvent((Type)type(), m_position.translated(delta), m_buttons, m_button, m_modifiers, m_wheel_delta); } + +private: + Gfx::Point m_position; + unsigned m_buttons { 0 }; + MouseButton m_button { MouseButton::None }; + unsigned m_modifiers { 0 }; + int m_wheel_delta { 0 }; + bool m_drag { false }; + String m_drag_data_type; +}; + +class ResizeEvent final : public Event { +public: + ResizeEvent(const Gfx::Rect& old_rect, const Gfx::Rect& rect) + : Event(Event::WindowResized) + , m_old_rect(old_rect) + , m_rect(rect) + { + } + + Gfx::Rect old_rect() const { return m_old_rect; } + Gfx::Rect rect() const { return m_rect; } + +private: + Gfx::Rect m_old_rect; + Gfx::Rect m_rect; +}; + +} diff --git a/Services/WindowServer/EventLoop.cpp b/Services/WindowServer/EventLoop.cpp new file mode 100644 index 0000000000..078794d6f3 --- /dev/null +++ b/Services/WindowServer/EventLoop.cpp @@ -0,0 +1,157 @@ +/* + * Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org> + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#include "Clipboard.h" +#include <Kernel/KeyCode.h> +#include <Kernel/MousePacket.h> +#include <LibCore/LocalSocket.h> +#include <LibCore/Object.h> +#include <WindowServer/ClientConnection.h> +#include <WindowServer/Cursor.h> +#include <WindowServer/Event.h> +#include <WindowServer/EventLoop.h> +#include <WindowServer/Screen.h> +#include <WindowServer/WindowManager.h> +#include <errno.h> +#include <fcntl.h> +#include <stdio.h> +#include <sys/select.h> +#include <sys/socket.h> +#include <sys/time.h> +#include <time.h> +#include <unistd.h> + +//#define WSMESSAGELOOP_DEBUG + +namespace WindowServer { + +EventLoop::EventLoop() + : m_server(Core::LocalServer::construct()) +{ + m_keyboard_fd = open("/dev/keyboard", O_RDONLY | O_NONBLOCK | O_CLOEXEC); + m_mouse_fd = open("/dev/mouse", O_RDONLY | O_NONBLOCK | O_CLOEXEC); + + bool ok = m_server->take_over_from_system_server(); + ASSERT(ok); + + m_server->on_ready_to_accept = [this] { + auto client_socket = m_server->accept(); + if (!client_socket) { + dbg() << "WindowServer: accept failed."; + return; + } + static int s_next_client_id = 0; + int client_id = ++s_next_client_id; + IPC::new_client_connection<ClientConnection>(*client_socket, client_id); + }; + + ASSERT(m_keyboard_fd >= 0); + ASSERT(m_mouse_fd >= 0); + + m_keyboard_notifier = Core::Notifier::construct(m_keyboard_fd, Core::Notifier::Read); + m_keyboard_notifier->on_ready_to_read = [this] { drain_keyboard(); }; + + m_mouse_notifier = Core::Notifier::construct(m_mouse_fd, Core::Notifier::Read); + m_mouse_notifier->on_ready_to_read = [this] { drain_mouse(); }; + + Clipboard::the().on_content_change = [&] { + ClientConnection::for_each_client([&](auto& client) { + client.notify_about_clipboard_contents_changed(); + }); + }; +} + +EventLoop::~EventLoop() +{ +} + +void EventLoop::drain_mouse() +{ + auto& screen = Screen::the(); + MousePacket state; + state.buttons = screen.mouse_button_state(); + unsigned buttons = state.buttons; + MousePacket packets[32]; + + ssize_t nread = read(m_mouse_fd, &packets, sizeof(packets)); + if (nread < 0) { + perror("EventLoop::drain_mouse read"); + return; + } + size_t npackets = nread / sizeof(MousePacket); + if (!npackets) + return; + for (size_t i = 0; i < npackets; ++i) { + auto& packet = packets[i]; +#ifdef WSMESSAGELOOP_DEBUG + dbgprintf("EventLoop: Mouse X %d, Y %d, Z %d, relative %d\n", packet.x, packet.y, packet.z, packet.is_relative); +#endif + buttons = packet.buttons; + + state.is_relative = packet.is_relative; + if (packet.is_relative) { + state.x += packet.x; + state.y -= packet.y; + state.z += packet.z; + } else { + state.x = packet.x; + state.y = packet.y; + state.z += packet.z; + } + + if (buttons != state.buttons) { + state.buttons = buttons; +#ifdef WSMESSAGELOOP_DEBUG + dbgprintf("EventLoop: Mouse Button Event\n"); +#endif + screen.on_receive_mouse_data(state); + if (state.is_relative) { + state.x = 0; + state.y = 0; + state.z = 0; + } + } + } + if (state.is_relative && (state.x || state.y || state.z)) + screen.on_receive_mouse_data(state); + if (!state.is_relative) + screen.on_receive_mouse_data(state); +} + +void EventLoop::drain_keyboard() +{ + auto& screen = Screen::the(); + for (;;) { + ::KeyEvent event; + ssize_t nread = read(m_keyboard_fd, (u8*)&event, sizeof(::KeyEvent)); + if (nread == 0) + break; + ASSERT(nread == sizeof(::KeyEvent)); + screen.on_receive_keyboard_data(event); + } +} + +} diff --git a/Services/WindowServer/EventLoop.h b/Services/WindowServer/EventLoop.h new file mode 100644 index 0000000000..b476b5e639 --- /dev/null +++ b/Services/WindowServer/EventLoop.h @@ -0,0 +1,57 @@ +/* + * Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org> + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#pragma once + +#include <AK/ByteBuffer.h> +#include <LibCore/EventLoop.h> +#include <LibCore/LocalServer.h> +#include <LibCore/Notifier.h> + +namespace WindowServer { + +class ClientConnection; + +class EventLoop { +public: + EventLoop(); + virtual ~EventLoop(); + + int exec() { return m_event_loop.exec(); } + +private: + void drain_mouse(); + void drain_keyboard(); + + Core::EventLoop m_event_loop; + int m_keyboard_fd { -1 }; + RefPtr<Core::Notifier> m_keyboard_notifier; + int m_mouse_fd { -1 }; + RefPtr<Core::Notifier> m_mouse_notifier; + RefPtr<Core::LocalServer> m_server; +}; + +} diff --git a/Services/WindowServer/Makefile b/Services/WindowServer/Makefile new file mode 100644 index 0000000000..064e2bd0f2 --- /dev/null +++ b/Services/WindowServer/Makefile @@ -0,0 +1,38 @@ +OBJS = \ + AppletManager.o \ + Button.o \ + ClientConnection.o \ + Clipboard.o \ + Compositor.o \ + Cursor.o \ + EventLoop.o \ + Menu.o \ + MenuBar.o \ + MenuItem.o \ + MenuManager.o \ + Screen.o \ + Window.o \ + WindowFrame.o \ + WindowManager.o \ + WindowSwitcher.o \ + main.o + +PROGRAM = WindowServer + +LIB_DEPS = Gfx Core Thread Pthread IPC + +*.cpp: WindowServerEndpoint.h WindowClientEndpoint.h + +WindowServerEndpoint.h: WindowServer.ipc | IPCCOMPILER + @echo "IPC $<"; $(IPCCOMPILER) $< > $@ + +WindowClientEndpoint.h: WindowClient.ipc | IPCCOMPILER + @echo "IPC $<"; $(IPCCOMPILER) $< > $@ + +EXTRA_CLEAN = WindowServerEndpoint.h WindowClientEndpoint.h + +install: + mkdir -p ../../Root/usr/include/WindowServer/ + cp *.h ../../Root/usr/include/WindowServer/ + +include ../../Makefile.common diff --git a/Services/WindowServer/Menu.cpp b/Services/WindowServer/Menu.cpp new file mode 100644 index 0000000000..07c460b9b2 --- /dev/null +++ b/Services/WindowServer/Menu.cpp @@ -0,0 +1,570 @@ +/* + * Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org> + * Copyright (c) 2020, Shannon Booth <shannon.ml.booth@gmail.com> + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#include "Menu.h" +#include "Event.h" +#include "EventLoop.h" +#include "MenuItem.h" +#include "MenuManager.h" +#include "Screen.h" +#include "Window.h" +#include "WindowManager.h" +#include <LibGfx/Bitmap.h> +#include <LibGfx/CharacterBitmap.h> +#include <LibGfx/Font.h> +#include <LibGfx/Painter.h> +#include <LibGfx/StylePainter.h> +#include <LibGfx/Triangle.h> +#include <WindowServer/ClientConnection.h> +#include <WindowServer/WindowClientEndpoint.h> + +namespace WindowServer { + +Menu::Menu(ClientConnection* client, int menu_id, const String& name) + : Core::Object(client) + , m_client(client) + , m_menu_id(menu_id) + , m_name(move(name)) +{ +} + +Menu::~Menu() +{ +} + +void Menu::set_title_font(const Gfx::Font& font) +{ + m_title_font = &font; +} + +const Gfx::Font& Menu::title_font() const +{ + return *m_title_font; +} + +const Gfx::Font& Menu::font() const +{ + return Gfx::Font::default_font(); +} + +static const char* s_checked_bitmap_data = { + " " + " # " + " ## " + " ### " + " ## ### " + " ##### " + " ### " + " # " + " " +}; + +static const char* s_submenu_arrow_bitmap_data = { + " " + " # " + " ## " + " ### " + " #### " + " ### " + " ## " + " # " + " " +}; + +static Gfx::CharacterBitmap* s_checked_bitmap; +static const int s_checked_bitmap_width = 9; +static const int s_checked_bitmap_height = 9; +static const int s_submenu_arrow_bitmap_width = 9; +static const int s_submenu_arrow_bitmap_height = 9; +static const int s_item_icon_width = 16; +static const int s_stripe_width = 23; + +int Menu::content_width() const +{ + int widest_text = 0; + int widest_shortcut = 0; + for (auto& item : m_items) { + if (item.type() != MenuItem::Text) + continue; + int text_width = font().width(item.text()); + if (!item.shortcut_text().is_empty()) { + int shortcut_width = font().width(item.shortcut_text()); + widest_shortcut = max(shortcut_width, widest_shortcut); + } + widest_text = max(widest_text, text_width); + } + + int widest_item = widest_text + s_stripe_width; + if (widest_shortcut) + widest_item += padding_between_text_and_shortcut() + widest_shortcut; + + return max(widest_item, rect_in_menubar().width()) + horizontal_padding() + frame_thickness() * 2; +} + +void Menu::redraw() +{ + if (!menu_window()) + return; + draw(); + menu_window()->invalidate(); +} + +Window& Menu::ensure_menu_window() +{ + if (m_menu_window) + return *m_menu_window; + + int width = this->content_width(); + + Gfx::Point next_item_location(frame_thickness(), frame_thickness()); + for (auto& item : m_items) { + int height = 0; + if (item.type() == MenuItem::Text) + height = item_height(); + else if (item.type() == MenuItem::Separator) + height = 8; + item.set_rect({ next_item_location, { width - frame_thickness() * 2, height } }); + next_item_location.move_by(0, height); + } + + int window_height_available = Screen::the().height() - MenuManager::the().menubar_rect().height() - frame_thickness() * 2; + int max_window_height = (window_height_available / item_height()) * item_height() + frame_thickness() * 2; + int content_height = m_items.is_empty() ? 0 : (m_items.last().rect().bottom() + 1) + frame_thickness(); + int window_height = min(max_window_height, content_height); + if (window_height < content_height) { + m_scrollable = true; + m_max_scroll_offset = item_count() - window_height / item_height() + 2; + } + + auto window = Window::construct(*this, WindowType::Menu); + window->set_rect(0, 0, width, window_height); + m_menu_window = move(window); + draw(); + + return *m_menu_window; +} + +int Menu::visible_item_count() const +{ + if (!is_scrollable()) + return m_items.size(); + ASSERT(m_menu_window); + // Make space for up/down arrow indicators + return m_menu_window->height() / item_height() - 2; +} + +void Menu::draw() +{ + auto palette = WindowManager::the().palette(); + m_theme_index_at_last_paint = MenuManager::the().theme_index(); + + ASSERT(menu_window()); + ASSERT(menu_window()->backing_store()); + Gfx::Painter painter(*menu_window()->backing_store()); + + Gfx::Rect rect { {}, menu_window()->size() }; + Gfx::StylePainter::paint_window_frame(painter, rect, palette); + painter.fill_rect(rect.shrunken(6, 6), palette.menu_base()); + int width = this->content_width(); + + if (!s_checked_bitmap) + s_checked_bitmap = &Gfx::CharacterBitmap::create_from_ascii(s_checked_bitmap_data, s_checked_bitmap_width, s_checked_bitmap_height).leak_ref(); + + bool has_checkable_items = false; + bool has_items_with_icon = false; + for (auto& item : m_items) { + has_checkable_items = has_checkable_items | item.is_checkable(); + has_items_with_icon = has_items_with_icon | !!item.icon(); + } + + Gfx::Rect stripe_rect { frame_thickness(), frame_thickness(), s_stripe_width, menu_window()->height() - frame_thickness() * 2 }; + painter.fill_rect(stripe_rect, palette.menu_stripe()); + painter.draw_line(stripe_rect.top_right(), stripe_rect.bottom_right(), palette.menu_stripe().darkened()); + + int visible_item_count = this->visible_item_count(); + + if (is_scrollable()) { + bool can_go_up = m_scroll_offset > 0; + bool can_go_down = m_scroll_offset < m_max_scroll_offset; + Gfx::Rect up_indicator_rect { frame_thickness(), frame_thickness(), content_width(), item_height() }; + painter.draw_text(up_indicator_rect, "\xc3\xb6", Gfx::TextAlignment::Center, can_go_up ? palette.menu_base_text() : palette.color(ColorRole::DisabledText)); + Gfx::Rect down_indicator_rect { frame_thickness(), menu_window()->height() - item_height() - frame_thickness(), content_width(), item_height() }; + painter.draw_text(down_indicator_rect, "\xc3\xb7", Gfx::TextAlignment::Center, can_go_down ? palette.menu_base_text() : palette.color(ColorRole::DisabledText)); + } + + for (int i = 0; i < visible_item_count; ++i) { + auto& item = m_items.at(m_scroll_offset + i); + if (item.type() == MenuItem::Text) { + Color text_color = palette.menu_base_text(); + if (&item == hovered_item() && item.is_enabled()) { + painter.fill_rect(item.rect(), palette.menu_selection()); + painter.draw_rect(item.rect(), palette.menu_selection().darkened()); + text_color = palette.menu_selection_text(); + } else if (!item.is_enabled()) { + text_color = Color::MidGray; + } + Gfx::Rect text_rect = item.rect().translated(stripe_rect.width() + 6, 0); + if (item.is_checkable()) { + if (item.is_exclusive()) { + Gfx::Rect radio_rect { item.rect().x() + 5, 0, 12, 12 }; + radio_rect.center_vertically_within(text_rect); + Gfx::StylePainter::paint_radio_button(painter, radio_rect, palette, item.is_checked(), false); + } else { + Gfx::Rect checkmark_rect { item.rect().x() + 7, 0, s_checked_bitmap_width, s_checked_bitmap_height }; + checkmark_rect.center_vertically_within(text_rect); + Gfx::Rect checkbox_rect = checkmark_rect.inflated(4, 4); + painter.fill_rect(checkbox_rect, palette.base()); + Gfx::StylePainter::paint_frame(painter, checkbox_rect, palette, Gfx::FrameShape::Container, Gfx::FrameShadow::Sunken, 2); + if (item.is_checked()) { + painter.draw_bitmap(checkmark_rect.location(), *s_checked_bitmap, palette.button_text()); + } + } + } else if (item.icon()) { + Gfx::Rect icon_rect { item.rect().x() + 3, 0, s_item_icon_width, s_item_icon_width }; + icon_rect.center_vertically_within(text_rect); + painter.blit(icon_rect.location(), *item.icon(), item.icon()->rect()); + } + painter.draw_text(text_rect, item.text(), Gfx::TextAlignment::CenterLeft, text_color); + if (!item.shortcut_text().is_empty()) { + painter.draw_text(item.rect().translated(-right_padding(), 0), item.shortcut_text(), Gfx::TextAlignment::CenterRight, text_color); + } + if (item.is_submenu()) { + static auto& submenu_arrow_bitmap = Gfx::CharacterBitmap::create_from_ascii(s_submenu_arrow_bitmap_data, s_submenu_arrow_bitmap_width, s_submenu_arrow_bitmap_height).leak_ref(); + Gfx::Rect submenu_arrow_rect { + item.rect().right() - s_submenu_arrow_bitmap_width - 2, + 0, + s_submenu_arrow_bitmap_width, + s_submenu_arrow_bitmap_height + }; + submenu_arrow_rect.center_vertically_within(item.rect()); + painter.draw_bitmap(submenu_arrow_rect.location(), submenu_arrow_bitmap, text_color); + } + } else if (item.type() == MenuItem::Separator) { + Gfx::Point p1(item.rect().translated(stripe_rect.width() + 4, 0).x(), item.rect().center().y() - 1); + Gfx::Point p2(width - 7, item.rect().center().y() - 1); + painter.draw_line(p1, p2, palette.threed_shadow1()); + painter.draw_line(p1.translated(0, 1), p2.translated(0, 1), palette.threed_highlight()); + } + } +} + +MenuItem* Menu::hovered_item() const +{ + if (m_hovered_item_index == -1) + return nullptr; + return const_cast<MenuItem*>(&item(m_hovered_item_index)); +} + +void Menu::update_for_new_hovered_item() +{ + if (hovered_item() && hovered_item()->is_submenu()) { + MenuManager::the().close_everyone_not_in_lineage(*hovered_item()->submenu()); + hovered_item()->submenu()->popup(hovered_item()->rect().top_right().translated(menu_window()->rect().location()), true); + } else { + MenuManager::the().close_everyone_not_in_lineage(*this); + MenuManager::the().set_current_menu(this); + menu_window()->set_visible(true); + } + redraw(); +} + +void Menu::open_hovered_item() +{ + ASSERT(menu_window()); + ASSERT(menu_window()->is_visible()); + if (!hovered_item()) + return; + if (hovered_item()->is_enabled()) + did_activate(*hovered_item()); + clear_hovered_item(); +} + +void Menu::descend_into_submenu_at_hovered_item() +{ + ASSERT(hovered_item()); + ASSERT(hovered_item()->is_submenu()); + auto submenu = hovered_item()->submenu(); + submenu->m_hovered_item_index = 0; + ASSERT(submenu->hovered_item()->type() != MenuItem::Separator); + submenu->update_for_new_hovered_item(); + m_in_submenu = true; +} + +void Menu::handle_mouse_move_event(const MouseEvent& mouse_event) +{ + ASSERT(menu_window()); + if (hovered_item() && hovered_item()->is_submenu()) { + + auto item = *hovered_item(); + auto submenu_top_left = item.rect().location() + Gfx::Point { item.rect().width(), 0 }; + auto submenu_bottom_left = submenu_top_left + Gfx::Point { 0, item.submenu()->menu_window()->height() }; + + auto safe_hover_triangle = Gfx::Triangle { m_last_position_in_hover, submenu_top_left, submenu_bottom_left }; + m_last_position_in_hover = mouse_event.position(); + + // Don't update the hovered item if mouse is moving towards a submenu + if (safe_hover_triangle.contains(mouse_event.position())) + return; + } + + int index = item_index_at(mouse_event.position()); + if (m_hovered_item_index == index) + return; + m_hovered_item_index = index; + + // FIXME: Tell parent menu (if it exists) that it is currently in a submenu + m_in_submenu = false; + update_for_new_hovered_item(); + return; +} + +void Menu::event(Core::Event& event) +{ + if (event.type() == Event::MouseMove) { + handle_mouse_move_event(static_cast<const MouseEvent&>(event)); + return; + } + + if (event.type() == Event::MouseUp) { + open_hovered_item(); + return; + } + + if (event.type() == Event::MouseWheel && is_scrollable()) { + ASSERT(menu_window()); + auto& mouse_event = static_cast<const MouseEvent&>(event); + m_scroll_offset += mouse_event.wheel_delta(); + m_scroll_offset = clamp(m_scroll_offset, 0, m_max_scroll_offset); + + int index = item_index_at(mouse_event.position()); + if (m_hovered_item_index == index) + return; + + m_hovered_item_index = index; + update_for_new_hovered_item(); + return; + } + + if (event.type() == Event::KeyDown) { + auto key = static_cast<KeyEvent&>(event).key(); + + if (!(key == Key_Up || key == Key_Down || key == Key_Left || key == Key_Right || key == Key_Return)) + return; + + ASSERT(menu_window()); + ASSERT(menu_window()->is_visible()); + + // Default to the first item on key press if one has not been selected yet + if (!hovered_item()) { + m_hovered_item_index = 0; + update_for_new_hovered_item(); + return; + } + + // Pass the event for the submenu that we are currently in to handle + if (m_in_submenu && key != Key_Left) { + ASSERT(hovered_item()->is_submenu()); + hovered_item()->submenu()->dispatch_event(event); + return; + } + + if (key == Key_Return) { + if (!hovered_item()->is_enabled()) + return; + if (hovered_item()->is_submenu()) + descend_into_submenu_at_hovered_item(); + else + open_hovered_item(); + return; + } + + if (key == Key_Up) { + ASSERT(m_items.at(0).type() != MenuItem::Separator); + + if (is_scrollable() && m_hovered_item_index == 0) + return; + + auto original_index = m_hovered_item_index; + do { + if (m_hovered_item_index == 0) + m_hovered_item_index = m_items.size() - 1; + else + --m_hovered_item_index; + if (m_hovered_item_index == original_index) + return; + } while (hovered_item()->type() == MenuItem::Separator || !hovered_item()->is_enabled()); + + ASSERT(m_hovered_item_index >= 0 && m_hovered_item_index <= static_cast<int>(m_items.size()) - 1); + + if (is_scrollable() && m_hovered_item_index < m_scroll_offset) + --m_scroll_offset; + + update_for_new_hovered_item(); + return; + } + + if (key == Key_Down) { + ASSERT(m_items.at(0).type() != MenuItem::Separator); + + if (is_scrollable() && m_hovered_item_index == static_cast<int>(m_items.size()) - 1) + return; + + auto original_index = m_hovered_item_index; + do { + if (m_hovered_item_index == static_cast<int>(m_items.size()) - 1) + m_hovered_item_index = 0; + else + ++m_hovered_item_index; + if (m_hovered_item_index == original_index) + return; + } while (hovered_item()->type() == MenuItem::Separator || !hovered_item()->is_enabled()); + + ASSERT(m_hovered_item_index >= 0 && m_hovered_item_index <= static_cast<int>(m_items.size()) - 1); + + if (is_scrollable() && m_hovered_item_index >= (m_scroll_offset + visible_item_count())) + ++m_scroll_offset; + + update_for_new_hovered_item(); + return; + } + + if (key == Key_Left) { + if (!m_in_submenu) + return; + + ASSERT(hovered_item()->is_submenu()); + hovered_item()->submenu()->clear_hovered_item(); + m_in_submenu = false; + return; + } + + if (key == Key_Right) { + if (hovered_item()->is_enabled() && hovered_item()->is_submenu()) + descend_into_submenu_at_hovered_item(); + return; + } + } + Core::Object::event(event); +} + +void Menu::clear_hovered_item() +{ + if (!hovered_item()) + return; + m_hovered_item_index = -1; + m_in_submenu = false; + redraw(); +} + +void Menu::did_activate(MenuItem& item) +{ + if (item.type() == MenuItem::Type::Separator) + return; + + if (on_item_activation) + on_item_activation(item); + + MenuManager::the().close_bar(); + + if (m_client) + m_client->post_message(Messages::WindowClient::MenuItemActivated(m_menu_id, item.identifier())); +} + +MenuItem* Menu::item_with_identifier(unsigned identifer) +{ + for (auto& item : m_items) { + if (item.identifier() == identifer) + return &item; + } + return nullptr; +} + +int Menu::item_index_at(const Gfx::Point& position) +{ + int i = 0; + for (auto& item : m_items) { + if (item.rect().contains(position)) + return i; + ++i; + } + return -1; +} + +void Menu::close() +{ + MenuManager::the().close_menu_and_descendants(*this); +} + +void Menu::redraw_if_theme_changed() +{ + if (m_theme_index_at_last_paint != MenuManager::the().theme_index()) + redraw(); +} + +void Menu::popup(const Gfx::Point& position, bool is_submenu) +{ + if (is_empty()) { + dbg() << "Menu: Empty menu popup"; + return; + } + + auto& window = ensure_menu_window(); + redraw_if_theme_changed(); + + const int margin = 30; + Gfx::Point adjusted_pos = position; + + if (adjusted_pos.x() + window.width() >= Screen::the().width() - margin) { + adjusted_pos = adjusted_pos.translated(-window.width(), 0); + } + if (adjusted_pos.y() + window.height() >= Screen::the().height() - margin) { + adjusted_pos = adjusted_pos.translated(0, -window.height()); + } + + if (adjusted_pos.y() < MenuManager::the().menubar_rect().height()) + adjusted_pos.set_y(MenuManager::the().menubar_rect().height()); + + window.move_to(adjusted_pos); + window.set_visible(true); + MenuManager::the().set_current_menu(this, is_submenu); +} + +bool Menu::is_menu_ancestor_of(const Menu& other) const +{ + for (auto& item : m_items) { + if (!item.is_submenu()) + continue; + auto& submenu = *const_cast<MenuItem&>(item).submenu(); + if (&submenu == &other) + return true; + if (submenu.is_menu_ancestor_of(other)) + return true; + } + return false; +} + +} diff --git a/Services/WindowServer/Menu.h b/Services/WindowServer/Menu.h new file mode 100644 index 0000000000..de9325616c --- /dev/null +++ b/Services/WindowServer/Menu.h @@ -0,0 +1,158 @@ +/* + * Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org> + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#pragma once + +#include <AK/NonnullOwnPtrVector.h> +#include <AK/String.h> +#include <AK/WeakPtr.h> +#include <LibCore/Object.h> +#include <LibGfx/Font.h> +#include <LibGfx/Forward.h> +#include <LibGfx/Rect.h> +#include <WindowServer/Cursor.h> +#include <WindowServer/MenuItem.h> +#include <WindowServer/Window.h> + +namespace WindowServer { + +class ClientConnection; +class MenuBar; +class Event; + +class Menu final : public Core::Object { + C_OBJECT(Menu) +public: + Menu(ClientConnection*, int menu_id, const String& name); + virtual ~Menu() override; + + ClientConnection* client() { return m_client; } + const ClientConnection* client() const { return m_client; } + int menu_id() const { return m_menu_id; } + + MenuBar* menubar() { return m_menubar; } + const MenuBar* menubar() const { return m_menubar; } + void set_menubar(MenuBar* menubar) { m_menubar = menubar; } + + bool is_empty() const { return m_items.is_empty(); } + int item_count() const { return m_items.size(); } + const MenuItem& item(int index) const { return m_items.at(index); } + MenuItem& item(int index) { return m_items.at(index); } + + void add_item(NonnullOwnPtr<MenuItem>&& item) { m_items.append(move(item)); } + + String name() const { return m_name; } + + template<typename Callback> + void for_each_item(Callback callback) const + { + for (auto& item : m_items) + callback(item); + } + + Gfx::Rect text_rect_in_menubar() const { return m_text_rect_in_menubar; } + void set_text_rect_in_menubar(const Gfx::Rect& rect) { m_text_rect_in_menubar = rect; } + + Gfx::Rect rect_in_menubar() const { return m_rect_in_menubar; } + void set_rect_in_menubar(const Gfx::Rect& rect) { m_rect_in_menubar = rect; } + + Window* menu_window() { return m_menu_window.ptr(); } + Window& ensure_menu_window(); + + Window* window_menu_of() { return m_window_menu_of; } + void set_window_menu_of(Window& window) { m_window_menu_of = window.make_weak_ptr(); } + bool is_window_menu_open() { return m_is_window_menu_open; } + void set_window_menu_open(bool is_open) { m_is_window_menu_open = is_open; } + + int content_width() const; + + int item_height() const { return 20; } + int frame_thickness() const { return 3; } + int horizontal_padding() const { return left_padding() + right_padding(); } + int left_padding() const { return 14; } + int right_padding() const { return 14; } + + void draw(); + const Gfx::Font& font() const; + const Gfx::Font& title_font() const; + void set_title_font(const Gfx::Font& font); + + MenuItem* item_with_identifier(unsigned); + void redraw(); + + MenuItem* hovered_item() const; + void clear_hovered_item(); + + Function<void(MenuItem&)> on_item_activation; + + void close(); + + void popup(const Gfx::Point&, bool is_submenu = false); + + bool is_menu_ancestor_of(const Menu&) const; + + void redraw_if_theme_changed(); + + bool is_scrollable() const { return m_scrollable; } + int scroll_offset() const { return m_scroll_offset; } + +private: + virtual void event(Core::Event&) override; + + RefPtr<Gfx::Font> m_title_font { &Gfx::Font::default_font() }; + + void handle_mouse_move_event(const MouseEvent&); + int visible_item_count() const; + + int item_index_at(const Gfx::Point&); + int padding_between_text_and_shortcut() const { return 50; } + void did_activate(MenuItem&); + void open_hovered_item(); + void update_for_new_hovered_item(); + void descend_into_submenu_at_hovered_item(); + + ClientConnection* m_client { nullptr }; + int m_menu_id { 0 }; + String m_name; + Gfx::Rect m_rect_in_menubar; + Gfx::Rect m_text_rect_in_menubar; + MenuBar* m_menubar { nullptr }; + NonnullOwnPtrVector<MenuItem> m_items; + RefPtr<Window> m_menu_window; + + WeakPtr<Window> m_window_menu_of; + bool m_is_window_menu_open = { false }; + Gfx::Point m_last_position_in_hover; + int m_theme_index_at_last_paint { -1 }; + int m_hovered_item_index { -1 }; + bool m_in_submenu { false }; + + bool m_scrollable { false }; + int m_scroll_offset { 0 }; + int m_max_scroll_offset { 0 }; +}; + +} diff --git a/Services/WindowServer/MenuBar.cpp b/Services/WindowServer/MenuBar.cpp new file mode 100644 index 0000000000..27dbdcbebf --- /dev/null +++ b/Services/WindowServer/MenuBar.cpp @@ -0,0 +1,55 @@ +/* + * Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org> + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#include "MenuBar.h" +#include "Menu.h" +#include "MenuItem.h" +#include <LibGfx/Bitmap.h> + +namespace WindowServer { + +MenuBar::MenuBar(ClientConnection& client, int menubar_id) + : m_client(client) + , m_menubar_id(menubar_id) +{ +} + +MenuBar::~MenuBar() +{ +} + +void MenuBar::add_menu(Menu& menu) +{ + menu.set_menubar(this); + + // NOTE: We assume that the first menu is the App menu, which has a bold font. + if (m_menus.is_empty()) + menu.set_title_font(Gfx::Font::default_bold_font()); + + m_menus.append(&menu); +} + +} diff --git a/Services/WindowServer/MenuBar.h b/Services/WindowServer/MenuBar.h new file mode 100644 index 0000000000..31dff9ac0f --- /dev/null +++ b/Services/WindowServer/MenuBar.h @@ -0,0 +1,61 @@ +/* + * Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org> + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#pragma once + +#include "Menu.h" +#include <AK/Vector.h> +#include <AK/WeakPtr.h> +#include <AK/Weakable.h> + +namespace WindowServer { + +class MenuBar : public Weakable<MenuBar> { +public: + MenuBar(ClientConnection& client, int menubar_id); + ~MenuBar(); + + ClientConnection& client() { return m_client; } + const ClientConnection& client() const { return m_client; } + int menubar_id() const { return m_menubar_id; } + void add_menu(Menu&); + + template<typename Callback> + void for_each_menu(Callback callback) + { + for (auto& menu : m_menus) { + if (callback(*menu) == IterationDecision::Break) + return; + } + } + +private: + ClientConnection& m_client; + int m_menubar_id { 0 }; + Vector<Menu*> m_menus; +}; + +} diff --git a/Services/WindowServer/MenuItem.cpp b/Services/WindowServer/MenuItem.cpp new file mode 100644 index 0000000000..eba593c136 --- /dev/null +++ b/Services/WindowServer/MenuItem.cpp @@ -0,0 +1,96 @@ +/* + * Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org> + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#include "MenuItem.h" +#include "ClientConnection.h" +#include "Menu.h" +#include "WindowManager.h" +#include <LibGfx/Bitmap.h> + +namespace WindowServer { + +MenuItem::MenuItem(Menu& menu, unsigned identifier, const String& text, const String& shortcut_text, bool enabled, bool checkable, bool checked, const Gfx::Bitmap* icon) + : m_menu(menu) + , m_type(Text) + , m_enabled(enabled) + , m_checkable(checkable) + , m_checked(checked) + , m_identifier(identifier) + , m_text(text) + , m_shortcut_text(shortcut_text) + , m_icon(icon) +{ +} + +MenuItem::MenuItem(Menu& menu, Type type) + : m_menu(menu) + , m_type(type) +{ +} + +MenuItem::~MenuItem() +{ +} + +void MenuItem::set_enabled(bool enabled) +{ + if (m_enabled == enabled) + return; + m_enabled = enabled; + m_menu.redraw(); +} + +void MenuItem::set_checked(bool checked) +{ + if (m_checked == checked) + return; + m_checked = checked; + m_menu.redraw(); +} + +Menu* MenuItem::submenu() +{ + ASSERT(is_submenu()); + ASSERT(m_menu.client()); + return m_menu.client()->find_menu_by_id(m_submenu_id); +} + +Gfx::Rect MenuItem::rect() const +{ + if (!m_menu.is_scrollable()) + return m_rect; + return m_rect.translated(0, m_menu.item_height() - (m_menu.scroll_offset() * m_menu.item_height())); +} + +void MenuItem::set_icon(const Gfx::Bitmap* icon) +{ + if (m_icon == icon) + return; + m_icon = icon; + m_menu.redraw(); +} + +} diff --git a/Services/WindowServer/MenuItem.h b/Services/WindowServer/MenuItem.h new file mode 100644 index 0000000000..3f297adb43 --- /dev/null +++ b/Services/WindowServer/MenuItem.h @@ -0,0 +1,99 @@ +/* + * Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org> + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#pragma once + +#include <AK/Function.h> +#include <AK/String.h> +#include <LibGfx/Forward.h> +#include <LibGfx/Rect.h> + +namespace WindowServer { + +class Menu; + +class MenuItem { +public: + enum Type { + None, + Text, + Separator, + }; + + MenuItem(Menu&, unsigned identifier, const String& text, const String& shortcut_text = {}, bool enabled = true, bool checkable = false, bool checked = false, const Gfx::Bitmap* icon = nullptr); + MenuItem(Menu&, Type); + ~MenuItem(); + + Type type() const { return m_type; } + + bool is_enabled() const { return m_enabled; } + void set_enabled(bool); + + bool is_checkable() const { return m_checkable; } + void set_checkable(bool checkable) { m_checkable = checkable; } + + bool is_checked() const { return m_checked; } + void set_checked(bool); + + String text() const { return m_text; } + void set_text(const String& text) { m_text = text; } + + String shortcut_text() const { return m_shortcut_text; } + void set_shortcut_text(const String& text) { m_shortcut_text = text; } + + void set_rect(const Gfx::Rect& rect) { m_rect = rect; } + Gfx::Rect rect() const; + + unsigned identifier() const { return m_identifier; } + + const Gfx::Bitmap* icon() const { return m_icon; } + void set_icon(const Gfx::Bitmap*); + + bool is_submenu() const { return m_submenu_id != -1; } + int submenu_id() const { return m_submenu_id; } + void set_submenu_id(int submenu_id) { m_submenu_id = submenu_id; } + + Menu* submenu(); + + bool is_exclusive() const { return m_exclusive; } + void set_exclusive(bool exclusive) { m_exclusive = exclusive; } + +private: + Menu& m_menu; + Type m_type { None }; + bool m_enabled { true }; + bool m_checkable { false }; + bool m_checked { false }; + unsigned m_identifier { 0 }; + String m_text; + String m_shortcut_text; + Gfx::Rect m_rect; + RefPtr<Gfx::Bitmap> m_icon; + int m_submenu_id { -1 }; + bool m_exclusive { false }; +}; + +} diff --git a/Services/WindowServer/MenuManager.cpp b/Services/WindowServer/MenuManager.cpp new file mode 100644 index 0000000000..2470d4f1fa --- /dev/null +++ b/Services/WindowServer/MenuManager.cpp @@ -0,0 +1,404 @@ +/* + * Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org> + * Copyright (c) 2020, Shannon Booth <shannon.ml.booth@gmail.com> + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#include <AK/Badge.h> +#include <AK/FileSystemPath.h> +#include <AK/QuickSort.h> +#include <LibCore/DirIterator.h> +#include <LibGfx/Font.h> +#include <LibGfx/Painter.h> +#include <WindowServer/AppletManager.h> +#include <WindowServer/MenuManager.h> +#include <WindowServer/Screen.h> +#include <WindowServer/WindowManager.h> +#include <unistd.h> + +//#define DEBUG_MENUS + +namespace WindowServer { + +static MenuManager* s_the; + +MenuManager& MenuManager::the() +{ + ASSERT(s_the); + return *s_the; +} + +MenuManager::MenuManager() +{ + s_the = this; + m_needs_window_resize = true; + + // NOTE: This ensures that the system menu has the correct dimensions. + set_current_menubar(nullptr); + + m_window = Window::construct(*this, WindowType::Menubar); + m_window->set_rect(menubar_rect()); +} + +MenuManager::~MenuManager() +{ +} + +bool MenuManager::is_open(const Menu& menu) const +{ + for (size_t i = 0; i < m_open_menu_stack.size(); ++i) { + if (&menu == m_open_menu_stack[i].ptr()) + return true; + } + return false; +} + +void MenuManager::draw() +{ + auto& wm = WindowManager::the(); + auto palette = wm.palette(); + auto menubar_rect = this->menubar_rect(); + + if (m_needs_window_resize) { + m_window->set_rect(menubar_rect); + AppletManager::the().calculate_applet_rects(window()); + m_needs_window_resize = false; + } + + Gfx::Painter painter(*window().backing_store()); + + painter.fill_rect(menubar_rect, palette.window()); + painter.draw_line({ 0, menubar_rect.bottom() }, { menubar_rect.right(), menubar_rect.bottom() }, palette.threed_shadow1()); + + for_each_active_menubar_menu([&](Menu& menu) { + Color text_color = palette.window_text(); + if (is_open(menu)) { + painter.fill_rect(menu.rect_in_menubar(), palette.menu_selection()); + painter.draw_rect(menu.rect_in_menubar(), palette.menu_selection().darkened()); + text_color = palette.menu_selection_text(); + } + painter.draw_text( + menu.text_rect_in_menubar(), + menu.name(), + menu.title_font(), + Gfx::TextAlignment::CenterLeft, + text_color); + return IterationDecision::Continue; + }); + + AppletManager::the().draw(); +} + +void MenuManager::refresh() +{ + if (!m_window) + return; + draw(); + window().invalidate(); +} + +void MenuManager::event(Core::Event& event) +{ + if (static_cast<Event&>(event).is_mouse_event()) { + handle_mouse_event(static_cast<MouseEvent&>(event)); + return; + } + + if (static_cast<Event&>(event).is_key_event()) { + auto& key_event = static_cast<const KeyEvent&>(event); + + if (key_event.type() == Event::KeyUp && key_event.key() == Key_Escape) { + close_everyone(); + return; + } + + if (event.type() == Event::KeyDown) { + for_each_active_menubar_menu([&](Menu& menu) { + if (is_open(menu)) + menu.dispatch_event(event); + return IterationDecision::Continue; + }); + } + } + + return Core::Object::event(event); +} + +void MenuManager::handle_mouse_event(MouseEvent& mouse_event) +{ + auto* active_window = WindowManager::the().active_window(); + bool handled_menubar_event = false; + for_each_active_menubar_menu([&](Menu& menu) { + if (menu.rect_in_menubar().contains(mouse_event.position())) { + handled_menubar_event = &menu == m_system_menu || !active_window || !active_window->is_modal(); + if (handled_menubar_event) + handle_menu_mouse_event(menu, mouse_event); + return IterationDecision::Break; + } + return IterationDecision::Continue; + }); + if (handled_menubar_event) + return; + + if (has_open_menu()) { + auto* topmost_menu = m_open_menu_stack.last().ptr(); + ASSERT(topmost_menu); + auto* window = topmost_menu->menu_window(); + if (!window) { + dbg() << "MenuManager::handle_mouse_event: No menu window"; + return; + } + ASSERT(window->is_visible()); + + bool event_is_inside_current_menu = window->rect().contains(mouse_event.position()); + if (event_is_inside_current_menu) { + WindowManager::the().set_hovered_window(window); + auto translated_event = mouse_event.translated(-window->position()); + WindowManager::the().deliver_mouse_event(*window, translated_event); + return; + } + + if (topmost_menu->hovered_item()) + topmost_menu->clear_hovered_item(); + if (mouse_event.type() == Event::MouseDown || mouse_event.type() == Event::MouseUp) { + auto* window_menu_of = topmost_menu->window_menu_of(); + if (window_menu_of) { + bool event_is_inside_taskbar_button = window_menu_of->taskbar_rect().contains(mouse_event.position()); + if (event_is_inside_taskbar_button && !topmost_menu->is_window_menu_open()) { + topmost_menu->set_window_menu_open(true); + return; + } + } + + if (mouse_event.type() == Event::MouseDown) { + close_bar(); + topmost_menu->set_window_menu_open(false); + } + } + + if (mouse_event.type() == Event::MouseMove) { + for (auto& menu : m_open_menu_stack) { + if (!menu) + continue; + if (!menu->menu_window()->rect().contains(mouse_event.position())) + continue; + WindowManager::the().set_hovered_window(menu->menu_window()); + auto translated_event = mouse_event.translated(-menu->menu_window()->position()); + WindowManager::the().deliver_mouse_event(*menu->menu_window(), translated_event); + break; + } + } + return; + } + + AppletManager::the().dispatch_event(static_cast<Event&>(mouse_event)); +} + +void MenuManager::handle_menu_mouse_event(Menu& menu, const MouseEvent& event) +{ + bool is_hover_with_any_menu_open = event.type() == MouseEvent::MouseMove + && has_open_menu() + && (m_open_menu_stack.first()->menubar() || m_open_menu_stack.first() == m_system_menu.ptr()); + bool is_mousedown_with_left_button = event.type() == MouseEvent::MouseDown && event.button() == MouseButton::Left; + bool should_open_menu = &menu != m_current_menu && (is_hover_with_any_menu_open || is_mousedown_with_left_button); + + if (is_mousedown_with_left_button) + m_bar_open = !m_bar_open; + + if (should_open_menu && m_bar_open) { + open_menu(menu); + return; + } + + if (!m_bar_open) + close_everyone(); +} + +void MenuManager::set_needs_window_resize() +{ + m_needs_window_resize = true; +} + +void MenuManager::close_all_menus_from_client(Badge<ClientConnection>, ClientConnection& client) +{ + if (!has_open_menu()) + return; + if (m_open_menu_stack.first()->client() != &client) + return; + close_everyone(); +} + +void MenuManager::close_everyone() +{ + for (auto& menu : m_open_menu_stack) { + if (menu && menu->menu_window()) + menu->menu_window()->set_visible(false); + menu->clear_hovered_item(); + } + m_open_menu_stack.clear(); + m_current_menu = nullptr; + refresh(); +} + +void MenuManager::close_everyone_not_in_lineage(Menu& menu) +{ + Vector<Menu*> menus_to_close; + for (auto& open_menu : m_open_menu_stack) { + if (!open_menu) + continue; + if (&menu == open_menu.ptr() || open_menu->is_menu_ancestor_of(menu)) + continue; + menus_to_close.append(open_menu); + } + close_menus(menus_to_close); +} + +void MenuManager::close_menus(const Vector<Menu*>& menus) +{ + for (auto& menu : menus) { + if (menu == m_current_menu) + m_current_menu = nullptr; + if (menu->menu_window()) + menu->menu_window()->set_visible(false); + menu->clear_hovered_item(); + m_open_menu_stack.remove_first_matching([&](auto& entry) { + return entry == menu; + }); + } + refresh(); +} + +static void collect_menu_subtree(Menu& menu, Vector<Menu*>& menus) +{ + menus.append(&menu); + for (int i = 0; i < menu.item_count(); ++i) { + auto& item = menu.item(i); + if (!item.is_submenu()) + continue; + collect_menu_subtree(*const_cast<MenuItem&>(item).submenu(), menus); + } +} + +void MenuManager::close_menu_and_descendants(Menu& menu) +{ + Vector<Menu*> menus_to_close; + collect_menu_subtree(menu, menus_to_close); + close_menus(menus_to_close); +} + +void MenuManager::toggle_menu(Menu& menu) +{ + if (is_open(menu)) { + close_menu_and_descendants(menu); + return; + } + open_menu(menu); +} + +void MenuManager::open_menu(Menu& menu) +{ + if (is_open(menu)) + return; + if (!menu.is_empty()) { + menu.redraw_if_theme_changed(); + auto& menu_window = menu.ensure_menu_window(); + menu_window.move_to({ menu.rect_in_menubar().x(), menu.rect_in_menubar().bottom() + 2 }); + menu_window.set_visible(true); + } + set_current_menu(&menu); + refresh(); +} + +void MenuManager::set_current_menu(Menu* menu, bool is_submenu) +{ + if (menu == m_current_menu) + return; + + if (!is_submenu) { + if (menu) + close_everyone_not_in_lineage(*menu); + else + close_everyone(); + } + + if (!menu) { + m_current_menu = nullptr; + return; + } + + m_current_menu = menu->make_weak_ptr(); + if (m_open_menu_stack.find([menu](auto& other) { return menu == other.ptr(); }).is_end()) + m_open_menu_stack.append(menu->make_weak_ptr()); +} + +void MenuManager::close_bar() +{ + close_everyone(); + m_bar_open = false; +} + +Gfx::Rect MenuManager::menubar_rect() const +{ + return { 0, 0, Screen::the().rect().width(), 18 }; +} + +void MenuManager::set_current_menubar(MenuBar* menubar) +{ + if (menubar) + m_current_menubar = menubar->make_weak_ptr(); + else + m_current_menubar = nullptr; +#ifdef DEBUG_MENUS + dbg() << "[WM] Current menubar is now " << menubar; +#endif + Gfx::Point next_menu_location { MenuManager::menubar_menu_margin() / 2, 0 }; + for_each_active_menubar_menu([&](Menu& menu) { + int text_width = menu.title_font().width(menu.name()); + menu.set_rect_in_menubar({ next_menu_location.x() - MenuManager::menubar_menu_margin() / 2, 0, text_width + MenuManager::menubar_menu_margin(), menubar_rect().height() - 1 }); + menu.set_text_rect_in_menubar({ next_menu_location, { text_width, menubar_rect().height() } }); + next_menu_location.move_by(menu.rect_in_menubar().width(), 0); + return IterationDecision::Continue; + }); + refresh(); +} + +void MenuManager::close_menubar(MenuBar& menubar) +{ + if (current_menubar() == &menubar) + set_current_menubar(nullptr); +} + +void MenuManager::set_system_menu(Menu& menu) +{ + m_system_menu = menu.make_weak_ptr(); + set_current_menubar(m_current_menubar); +} + +void MenuManager::did_change_theme() +{ + ++m_theme_index; + refresh(); +} + +} diff --git a/Services/WindowServer/MenuManager.h b/Services/WindowServer/MenuManager.h new file mode 100644 index 0000000000..f4d5d5aad8 --- /dev/null +++ b/Services/WindowServer/MenuManager.h @@ -0,0 +1,123 @@ +/* + * Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org> + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#pragma once + +#include "Menu.h" +#include "MenuBar.h" +#include "Window.h" +#include <AK/HashMap.h> +#include <LibCore/Object.h> + +namespace WindowServer { + +class MenuManager final : public Core::Object { + C_OBJECT(MenuManager) +public: + static MenuManager& the(); + + MenuManager(); + virtual ~MenuManager() override; + + void refresh(); + + bool is_open(const Menu&) const; + bool has_open_menu() const { return !m_open_menu_stack.is_empty(); } + + Gfx::Rect menubar_rect() const; + static int menubar_menu_margin() { return 16; } + + void set_needs_window_resize(); + + Menu* current_menu() { return m_current_menu.ptr(); } + void set_current_menu(Menu*, bool is_submenu = false); + void open_menu(Menu&); + void toggle_menu(Menu&); + + MenuBar* current_menubar() { return m_current_menubar.ptr(); } + void set_current_menubar(MenuBar*); + void close_menubar(MenuBar&); + + void close_bar(); + void close_everyone(); + void close_everyone_not_in_lineage(Menu&); + void close_menu_and_descendants(Menu&); + + void close_all_menus_from_client(Badge<ClientConnection>, ClientConnection&); + + void toggle_system_menu() + { + if (m_system_menu) + toggle_menu(*m_system_menu); + } + + Menu* system_menu() { return m_system_menu; } + void set_system_menu(Menu&); + + int theme_index() const { return m_theme_index; } + + Window& window() { return *m_window; } + + template<typename Callback> + void for_each_active_menubar_menu(Callback callback) + { + if (system_menu()) { + if (callback(*system_menu()) == IterationDecision::Break) + return; + } + if (m_current_menubar) + m_current_menubar->for_each_menu(callback); + } + + void did_change_theme(); + +private: + void close_menus(const Vector<Menu*>&); + + const Window& window() const { return *m_window; } + + virtual void event(Core::Event&) override; + void handle_mouse_event(MouseEvent&); + void handle_menu_mouse_event(Menu&, const MouseEvent&); + + void draw(); + + RefPtr<Window> m_window; + + WeakPtr<Menu> m_current_menu; + Vector<WeakPtr<Menu>> m_open_menu_stack; + + WeakPtr<Menu> m_system_menu; + + bool m_needs_window_resize { false }; + bool m_bar_open { false }; + + int m_theme_index { 0 }; + + WeakPtr<MenuBar> m_current_menubar; +}; + +} diff --git a/Services/WindowServer/Screen.cpp b/Services/WindowServer/Screen.cpp new file mode 100644 index 0000000000..3f0fbb04fd --- /dev/null +++ b/Services/WindowServer/Screen.cpp @@ -0,0 +1,171 @@ +/* + * Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org> + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#include "Screen.h" +#include "Compositor.h" +#include "Event.h" +#include "EventLoop.h" +#include "WindowManager.h" +#include <Kernel/FB.h> +#include <Kernel/MousePacket.h> +#include <fcntl.h> +#include <stdio.h> +#include <sys/mman.h> +#include <unistd.h> + +namespace WindowServer { + +static Screen* s_the; + +Screen& Screen::the() +{ + ASSERT(s_the); + return *s_the; +} + +Screen::Screen(unsigned desired_width, unsigned desired_height) +{ + ASSERT(!s_the); + s_the = this; + m_framebuffer_fd = open("/dev/fb0", O_RDWR | O_CLOEXEC); + if (m_framebuffer_fd < 0) { + perror("failed to open /dev/fb0"); + ASSERT_NOT_REACHED(); + } + + if (fb_set_buffer(m_framebuffer_fd, 0) == 0) { + m_can_set_buffer = true; + } + + set_resolution(desired_width, desired_height); + m_cursor_location = rect().center(); +} + +Screen::~Screen() +{ +} + +bool Screen::set_resolution(int width, int height) +{ + FBResolution resolution { 0, (unsigned)width, (unsigned)height }; + int rc = fb_set_resolution(m_framebuffer_fd, &resolution); +#ifdef WSSCREEN_DEBUG + dbg() << "fb_set_resolution() - return code " << rc; +#endif + if (rc == 0) { + on_change_resolution(resolution.pitch, resolution.width, resolution.height); + return true; + } + if (rc == -1) { + dbg() << "Invalid resolution " << width << "x" << height; + on_change_resolution(resolution.pitch, resolution.width, resolution.height); + return false; + } + ASSERT_NOT_REACHED(); +} + +void Screen::on_change_resolution(int pitch, int width, int height) +{ + if (m_framebuffer) { + size_t previous_size_in_bytes = m_size_in_bytes; + int rc = munmap(m_framebuffer, previous_size_in_bytes); + ASSERT(rc == 0); + } + + int rc = fb_get_size_in_bytes(m_framebuffer_fd, &m_size_in_bytes); + ASSERT(rc == 0); + + m_framebuffer = (Gfx::RGBA32*)mmap(nullptr, m_size_in_bytes, PROT_READ | PROT_WRITE, MAP_SHARED, m_framebuffer_fd, 0); + ASSERT(m_framebuffer && m_framebuffer != (void*)-1); + + m_pitch = pitch; + m_width = width; + m_height = height; + + m_cursor_location.constrain(rect()); +} + +void Screen::set_buffer(int index) +{ + ASSERT(m_can_set_buffer); + int rc = fb_set_buffer(m_framebuffer_fd, index); + ASSERT(rc == 0); +} + +void Screen::on_receive_mouse_data(const MousePacket& packet) +{ + auto prev_location = m_cursor_location; + if (packet.is_relative) { + m_cursor_location.move_by(packet.x, packet.y); +#ifdef WSSCREEN_DEBUG + dbgprintf("Screen: New Relative mouse point @ X %d, Y %d\n", m_cursor_location.x(), m_cursor_location.y()); +#endif + } else { + m_cursor_location = { packet.x * m_width / 0xffff, packet.y * m_height / 0xffff }; +#ifdef WSSCREEN_DEBUG + dbgprintf("Screen: New Absolute mouse point @ X %d, Y %d\n", m_cursor_location.x(), m_cursor_location.y()); +#endif + } + + m_cursor_location.constrain(rect()); + + unsigned buttons = packet.buttons; + unsigned prev_buttons = m_mouse_button_state; + m_mouse_button_state = buttons; + unsigned changed_buttons = prev_buttons ^ buttons; + auto post_mousedown_or_mouseup_if_needed = [&](MouseButton button) { + if (!(changed_buttons & (unsigned)button)) + return; + auto message = make<MouseEvent>(buttons & (unsigned)button ? Event::MouseDown : Event::MouseUp, m_cursor_location, buttons, button, m_modifiers); + Core::EventLoop::current().post_event(WindowManager::the(), move(message)); + }; + post_mousedown_or_mouseup_if_needed(MouseButton::Left); + post_mousedown_or_mouseup_if_needed(MouseButton::Right); + post_mousedown_or_mouseup_if_needed(MouseButton::Middle); + post_mousedown_or_mouseup_if_needed(MouseButton::Back); + post_mousedown_or_mouseup_if_needed(MouseButton::Forward); + if (m_cursor_location != prev_location) { + auto message = make<MouseEvent>(Event::MouseMove, m_cursor_location, buttons, MouseButton::None, m_modifiers); + Core::EventLoop::current().post_event(WindowManager::the(), move(message)); + } + + if (packet.z) { + auto message = make<MouseEvent>(Event::MouseWheel, m_cursor_location, buttons, MouseButton::None, m_modifiers, packet.z); + Core::EventLoop::current().post_event(WindowManager::the(), move(message)); + } + + if (m_cursor_location != prev_location) + Compositor::the().invalidate_cursor(); +} + +void Screen::on_receive_keyboard_data(::KeyEvent kernel_event) +{ + m_modifiers = kernel_event.modifiers(); + auto message = make<KeyEvent>(kernel_event.is_press() ? Event::KeyDown : Event::KeyUp, kernel_event.key, kernel_event.character, kernel_event.modifiers()); + Core::EventLoop::current().post_event(WindowManager::the(), move(message)); +} + +} diff --git a/Services/WindowServer/Screen.h b/Services/WindowServer/Screen.h new file mode 100644 index 0000000000..d522ea7b96 --- /dev/null +++ b/Services/WindowServer/Screen.h @@ -0,0 +1,86 @@ +/* + * Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org> + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#pragma once + +#include <Kernel/KeyCode.h> +#include <LibGfx/Color.h> +#include <LibGfx/Rect.h> +#include <LibGfx/Size.h> + +struct MousePacket; + +namespace WindowServer { + +class Screen { +public: + Screen(unsigned width, unsigned height); + ~Screen(); + + bool set_resolution(int width, int height); + bool can_set_buffer() { return m_can_set_buffer; } + void set_buffer(int index); + + int width() const { return m_width; } + int height() const { return m_height; } + size_t pitch() const { return m_pitch; } + Gfx::RGBA32* scanline(int y); + + static Screen& the(); + + Gfx::Size size() const { return { width(), height() }; } + Gfx::Rect rect() const { return { 0, 0, width(), height() }; } + + Gfx::Point cursor_location() const { return m_cursor_location; } + unsigned mouse_button_state() const { return m_mouse_button_state; } + + void on_receive_mouse_data(const MousePacket&); + void on_receive_keyboard_data(::KeyEvent); + +private: + void on_change_resolution(int pitch, int width, int height); + + size_t m_size_in_bytes; + + Gfx::RGBA32* m_framebuffer { nullptr }; + bool m_can_set_buffer { false }; + + int m_pitch { 0 }; + int m_width { 0 }; + int m_height { 0 }; + int m_framebuffer_fd { -1 }; + + Gfx::Point m_cursor_location; + unsigned m_mouse_button_state { 0 }; + unsigned m_modifiers { 0 }; +}; + +inline Gfx::RGBA32* Screen::scanline(int y) +{ + return reinterpret_cast<Gfx::RGBA32*>(((u8*)m_framebuffer) + (y * m_pitch)); +} + +} diff --git a/Services/WindowServer/Window.cpp b/Services/WindowServer/Window.cpp new file mode 100644 index 0000000000..4d506e1dc7 --- /dev/null +++ b/Services/WindowServer/Window.cpp @@ -0,0 +1,524 @@ +/* + * Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org> + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#include "Window.h" +#include "ClientConnection.h" +#include "Event.h" +#include "EventLoop.h" +#include "Screen.h" +#include "WindowClientEndpoint.h" +#include "WindowManager.h" +#include <AK/Badge.h> + +namespace WindowServer { + +static String default_window_icon_path() +{ + return "/res/icons/16x16/window.png"; +} + +static Gfx::Bitmap& default_window_icon() +{ + static Gfx::Bitmap* s_icon; + if (!s_icon) + s_icon = Gfx::Bitmap::load_from_file(default_window_icon_path()).leak_ref(); + return *s_icon; +} + +static Gfx::Bitmap& minimize_icon() +{ + static Gfx::Bitmap* s_icon; + if (!s_icon) + s_icon = Gfx::Bitmap::load_from_file("/res/icons/16x16/window-minimize.png").leak_ref(); + return *s_icon; +} + +static Gfx::Bitmap& maximize_icon() +{ + static Gfx::Bitmap* s_icon; + if (!s_icon) + s_icon = Gfx::Bitmap::load_from_file("/res/icons/16x16/window-maximize.png").leak_ref(); + return *s_icon; +} + +static Gfx::Bitmap& restore_icon() +{ + static Gfx::Bitmap* s_icon; + if (!s_icon) + s_icon = Gfx::Bitmap::load_from_file("/res/icons/16x16/window-restore.png").leak_ref(); + return *s_icon; +} + +static Gfx::Bitmap& close_icon() +{ + static Gfx::Bitmap* s_icon; + if (!s_icon) + s_icon = Gfx::Bitmap::load_from_file("/res/icons/16x16/window-close.png").leak_ref(); + return *s_icon; +} + +Window::Window(Core::Object& parent, WindowType type) + : Core::Object(&parent) + , m_type(type) + , m_icon(default_window_icon()) + , m_frame(*this) +{ + WindowManager::the().add_window(*this); +} + +Window::Window(ClientConnection& client, WindowType window_type, int window_id, bool modal, bool minimizable, bool frameless, bool resizable, bool fullscreen) + : Core::Object(&client) + , m_client(&client) + , m_type(window_type) + , m_modal(modal) + , m_minimizable(minimizable) + , m_frameless(frameless) + , m_resizable(resizable) + , m_fullscreen(fullscreen) + , m_window_id(window_id) + , m_client_id(client.client_id()) + , m_icon(default_window_icon()) + , m_frame(*this) +{ + // FIXME: This should not be hard-coded here. + if (m_type == WindowType::Taskbar) { + m_wm_event_mask = WMEventMask::WindowStateChanges | WMEventMask::WindowRemovals | WMEventMask::WindowIconChanges; + m_listens_to_wm_events = true; + } + + WindowManager::the().add_window(*this); +} + +Window::~Window() +{ + // Detach from client at the start of teardown since we don't want + // to confuse things by trying to send messages to it. + m_client = nullptr; + + WindowManager::the().remove_window(*this); +} + +void Window::set_title(const String& title) +{ + if (m_title == title) + return; + m_title = title; + WindowManager::the().notify_title_changed(*this); +} + +void Window::set_rect(const Gfx::Rect& rect) +{ + ASSERT(!rect.is_empty()); + Gfx::Rect old_rect; + if (m_rect == rect) + return; + old_rect = m_rect; + m_rect = rect; + if (!m_client && (!m_backing_store || old_rect.size() != rect.size())) { + m_backing_store = Gfx::Bitmap::create(Gfx::BitmapFormat::RGB32, m_rect.size()); + } + m_frame.notify_window_rect_changed(old_rect, rect); +} + +void Window::set_rect_without_repaint(const Gfx::Rect& rect) +{ + ASSERT(!rect.is_empty()); + if (m_rect == rect) + return; + auto old_rect = m_rect; + m_rect = rect; + + if (old_rect.size() == m_rect.size()) { + auto delta = m_rect.location() - old_rect.location(); + for (auto& child_window : m_child_windows) { + if (child_window) + child_window->move_by(delta); + } + } + + m_frame.notify_window_rect_changed(old_rect, rect); +} + +void Window::handle_mouse_event(const MouseEvent& event) +{ + set_automatic_cursor_tracking_enabled(event.buttons() != 0); + + switch (event.type()) { + case Event::MouseMove: + m_client->post_message(Messages::WindowClient::MouseMove(m_window_id, event.position(), (u32)event.button(), event.buttons(), event.modifiers(), event.wheel_delta(), event.is_drag(), event.drag_data_type())); + break; + case Event::MouseDown: + m_client->post_message(Messages::WindowClient::MouseDown(m_window_id, event.position(), (u32)event.button(), event.buttons(), event.modifiers(), event.wheel_delta())); + break; + case Event::MouseDoubleClick: + m_client->post_message(Messages::WindowClient::MouseDoubleClick(m_window_id, event.position(), (u32)event.button(), event.buttons(), event.modifiers(), event.wheel_delta())); + break; + case Event::MouseUp: + m_client->post_message(Messages::WindowClient::MouseUp(m_window_id, event.position(), (u32)event.button(), event.buttons(), event.modifiers(), event.wheel_delta())); + break; + case Event::MouseWheel: + m_client->post_message(Messages::WindowClient::MouseWheel(m_window_id, event.position(), (u32)event.button(), event.buttons(), event.modifiers(), event.wheel_delta())); + break; + default: + ASSERT_NOT_REACHED(); + } +} + +void Window::update_menu_item_text(PopupMenuItem item) +{ + if (m_window_menu) { + m_window_menu->item((int)item).set_text(item == PopupMenuItem::Minimize ? (m_minimized ? "Unminimize" : "Minimize") : (m_maximized ? "Restore" : "Maximize")); + m_window_menu->redraw(); + } +} + +void Window::update_menu_item_enabled(PopupMenuItem item) +{ + if (m_window_menu) { + m_window_menu->item((int)item).set_enabled(item == PopupMenuItem::Minimize ? m_minimizable : m_resizable); + m_window_menu->redraw(); + } +} + +void Window::set_minimized(bool minimized) +{ + if (m_minimized == minimized) + return; + if (minimized && !m_minimizable) + return; + if (is_blocked_by_modal_window()) + return; + m_minimized = minimized; + update_menu_item_text(PopupMenuItem::Minimize); + start_minimize_animation(); + if (!minimized) + request_update({ {}, size() }); + invalidate(); + WindowManager::the().notify_minimization_state_changed(*this); +} + +void Window::set_minimizable(bool minimizable) +{ + if (m_minimizable == minimizable) + return; + m_minimizable = minimizable; + update_menu_item_enabled(PopupMenuItem::Minimize); + // TODO: Hide/show (or alternatively change enabled state of) window minimize button dynamically depending on value of m_minimizable +} + +void Window::set_opacity(float opacity) +{ + if (m_opacity == opacity) + return; + m_opacity = opacity; + WindowManager::the().notify_opacity_changed(*this); +} + +void Window::set_occluded(bool occluded) +{ + if (m_occluded == occluded) + return; + m_occluded = occluded; + WindowManager::the().notify_occlusion_state_changed(*this); +} + +void Window::set_maximized(bool maximized) +{ + if (m_maximized == maximized) + return; + if (maximized && !is_resizable()) + return; + if (is_blocked_by_modal_window()) + return; + set_tiled(WindowTileType::None); + m_maximized = maximized; + update_menu_item_text(PopupMenuItem::Maximize); + auto old_rect = m_rect; + if (maximized) { + m_unmaximized_rect = m_rect; + set_rect(WindowManager::the().maximized_window_rect(*this)); + } else { + set_rect(m_unmaximized_rect); + } + m_frame.did_set_maximized({}, maximized); + Core::EventLoop::current().post_event(*this, make<ResizeEvent>(old_rect, m_rect)); +} + +void Window::set_resizable(bool resizable) +{ + if (m_resizable == resizable) + return; + m_resizable = resizable; + update_menu_item_enabled(PopupMenuItem::Maximize); + // TODO: Hide/show (or alternatively change enabled state of) window maximize button dynamically depending on value of is_resizable() +} + +void Window::event(Core::Event& event) +{ + if (!m_client) { + ASSERT(parent()); + event.ignore(); + return; + } + + if (is_blocked_by_modal_window()) + return; + + if (static_cast<Event&>(event).is_mouse_event()) + return handle_mouse_event(static_cast<const MouseEvent&>(event)); + + switch (event.type()) { + case Event::WindowEntered: + m_client->post_message(Messages::WindowClient::WindowEntered(m_window_id)); + break; + case Event::WindowLeft: + m_client->post_message(Messages::WindowClient::WindowLeft(m_window_id)); + break; + case Event::KeyDown: + m_client->post_message( + Messages::WindowClient::KeyDown(m_window_id, + (u8) static_cast<const KeyEvent&>(event).character(), + (u32) static_cast<const KeyEvent&>(event).key(), + static_cast<const KeyEvent&>(event).modifiers())); + break; + case Event::KeyUp: + m_client->post_message( + Messages::WindowClient::KeyUp(m_window_id, + (u8) static_cast<const KeyEvent&>(event).character(), + (u32) static_cast<const KeyEvent&>(event).key(), + static_cast<const KeyEvent&>(event).modifiers())); + break; + case Event::WindowActivated: + m_client->post_message(Messages::WindowClient::WindowActivated(m_window_id)); + break; + case Event::WindowDeactivated: + m_client->post_message(Messages::WindowClient::WindowDeactivated(m_window_id)); + break; + case Event::WindowCloseRequest: + m_client->post_message(Messages::WindowClient::WindowCloseRequest(m_window_id)); + break; + case Event::WindowResized: + m_client->post_message( + Messages::WindowClient::WindowResized( + m_window_id, + static_cast<const ResizeEvent&>(event).old_rect(), + static_cast<const ResizeEvent&>(event).rect())); + break; + default: + break; + } +} + +void Window::set_global_cursor_tracking_enabled(bool enabled) +{ + m_global_cursor_tracking_enabled = enabled; +} + +void Window::set_visible(bool b) +{ + if (m_visible == b) + return; + m_visible = b; + invalidate(); +} + +void Window::invalidate() +{ + WindowManager::the().invalidate(*this); +} + +void Window::invalidate(const Gfx::Rect& rect) +{ + WindowManager::the().invalidate(*this, rect); +} + +bool Window::is_active() const +{ + return WindowManager::the().active_window() == this; +} + +bool Window::is_blocked_by_modal_window() const +{ + bool is_any_modal = false; + const Window* next = this; + while (!is_any_modal && next) { + is_any_modal = next->is_modal(); + next = next->parent_window(); + } + + return !is_any_modal && client() && client()->is_showing_modal_window(); +} + +void Window::set_default_icon() +{ + m_icon = default_window_icon(); +} + +void Window::request_update(const Gfx::Rect& rect, bool ignore_occlusion) +{ + if (m_pending_paint_rects.is_empty()) { + deferred_invoke([this, ignore_occlusion](auto&) { + client()->post_paint_message(*this, ignore_occlusion); + }); + } + m_pending_paint_rects.add(rect); +} + +void Window::popup_window_menu(const Gfx::Point& position) +{ + if (!m_window_menu) { + m_window_menu = Menu::construct(nullptr, -1, "(Window Menu)"); + m_window_menu->set_window_menu_of(*this); + + auto minimize_item = make<MenuItem>(*m_window_menu, 1, m_minimized ? "Unminimize" : "Minimize"); + m_window_menu_minimize_item = minimize_item.ptr(); + m_window_menu->add_item(move(minimize_item)); + + auto maximize_item = make<MenuItem>(*m_window_menu, 2, m_maximized ? "Restore" : "Maximize"); + m_window_menu_maximize_item = maximize_item.ptr(); + m_window_menu->add_item(move(maximize_item)); + + m_window_menu->add_item(make<MenuItem>(*m_window_menu, MenuItem::Type::Separator)); + + auto close_item = make<MenuItem>(*m_window_menu, 3, "Close"); + close_item->set_icon(&close_icon()); + m_window_menu->add_item(move(close_item)); + + m_window_menu->item((int)PopupMenuItem::Minimize).set_enabled(m_minimizable); + m_window_menu->item((int)PopupMenuItem::Maximize).set_enabled(m_resizable); + + m_window_menu->on_item_activation = [&](auto& item) { + switch (item.identifier()) { + case 1: + set_minimized(!m_minimized); + if (!m_minimized) + WindowManager::the().move_to_front_and_make_active(*this); + break; + case 2: + set_maximized(!m_maximized); + if (m_minimized) + set_minimized(false); + WindowManager::the().move_to_front_and_make_active(*this); + break; + case 3: + request_close(); + break; + } + }; + } + m_window_menu_minimize_item->set_icon(m_minimized ? nullptr : &minimize_icon()); + m_window_menu_maximize_item->set_icon(m_maximized ? &restore_icon() : &maximize_icon()); + + m_window_menu->popup(position); +} + +void Window::request_close() +{ + Event close_request(Event::WindowCloseRequest); + event(close_request); +} + +void Window::set_fullscreen(bool fullscreen) +{ + if (m_fullscreen == fullscreen) + return; + m_fullscreen = fullscreen; + Gfx::Rect new_window_rect = m_rect; + if (m_fullscreen) { + m_saved_nonfullscreen_rect = m_rect; + new_window_rect = Screen::the().rect(); + } else if (!m_saved_nonfullscreen_rect.is_empty()) { + new_window_rect = m_saved_nonfullscreen_rect; + } + Core::EventLoop::current().post_event(*this, make<ResizeEvent>(m_rect, new_window_rect)); + set_rect(new_window_rect); +} + +Gfx::Rect Window::tiled_rect(WindowTileType tiled) const +{ + int frame_width = (m_frame.rect().width() - m_rect.width()) / 2; + switch (tiled) { + case WindowTileType::None: + return m_untiled_rect; + case WindowTileType::Left: + return Gfx::Rect(0, + WindowManager::the().maximized_window_rect(*this).y(), + Screen::the().width() / 2 - frame_width, + WindowManager::the().maximized_window_rect(*this).height()); + case WindowTileType::Right: + return Gfx::Rect(Screen::the().width() / 2 + frame_width, + WindowManager::the().maximized_window_rect(*this).y(), + Screen::the().width() / 2 - frame_width, + WindowManager::the().maximized_window_rect(*this).height()); + default: + ASSERT_NOT_REACHED(); + } +} + +void Window::set_tiled(WindowTileType tiled) +{ + if (m_tiled == tiled) + return; + + m_tiled = tiled; + auto old_rect = m_rect; + if (tiled != WindowTileType::None) + m_untiled_rect = m_rect; + set_rect(tiled_rect(tiled)); + Core::EventLoop::current().post_event(*this, make<ResizeEvent>(old_rect, m_rect)); +} + +void Window::detach_client(Badge<ClientConnection>) +{ + m_client = nullptr; +} + +void Window::recalculate_rect() +{ + if (!is_resizable()) + return; + + auto old_rect = m_rect; + if (m_tiled != WindowTileType::None) + set_rect(tiled_rect(m_tiled)); + else if (is_maximized()) + set_rect(WindowManager::the().maximized_window_rect(*this)); + Core::EventLoop::current().post_event(*this, make<ResizeEvent>(old_rect, m_rect)); +} + +void Window::add_child_window(Window& child_window) +{ + m_child_windows.append(child_window.make_weak_ptr()); +} + +void Window::set_parent_window(Window& parent_window) +{ + ASSERT(!m_parent_window); + m_parent_window = parent_window.make_weak_ptr(); + parent_window.add_child_window(*this); +} + +} diff --git a/Services/WindowServer/Window.h b/Services/WindowServer/Window.h new file mode 100644 index 0000000000..09856c96dd --- /dev/null +++ b/Services/WindowServer/Window.h @@ -0,0 +1,286 @@ +/* + * Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org> + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#pragma once + +#include <AK/InlineLinkedList.h> +#include <AK/String.h> +#include <AK/WeakPtr.h> +#include <LibCore/Object.h> +#include <LibGfx/Bitmap.h> +#include <LibGfx/DisjointRectSet.h> +#include <LibGfx/Rect.h> +#include <WindowServer/WindowFrame.h> +#include <WindowServer/WindowType.h> + +namespace WindowServer { + +class ClientConnection; +class Cursor; +class Menu; +class MenuItem; +class MouseEvent; + +enum WMEventMask { + WindowRectChanges = 1 << 0, + WindowStateChanges = 1 << 1, + WindowIconChanges = 1 << 2, + WindowRemovals = 1 << 3, +}; + +enum class WindowTileType { + None = 0, + Left, + Right, +}; + +enum class PopupMenuItem { + Minimize = 0, + Maximize, +}; + +class Window final : public Core::Object + , public InlineLinkedListNode<Window> { + C_OBJECT(Window) +public: + Window(ClientConnection&, WindowType, int window_id, bool modal, bool minimizable, bool frameless, bool resizable, bool fullscreen); + Window(Core::Object&, WindowType); + virtual ~Window() override; + + void popup_window_menu(const Gfx::Point&); + void request_close(); + + unsigned wm_event_mask() const { return m_wm_event_mask; } + void set_wm_event_mask(unsigned mask) { m_wm_event_mask = mask; } + + bool is_minimized() const { return m_minimized; } + void set_minimized(bool); + + bool is_minimizable() const { return m_minimizable; } + void set_minimizable(bool); + + bool is_resizable() const { return m_resizable && !m_fullscreen; } + void set_resizable(bool); + + bool is_maximized() const { return m_maximized; } + void set_maximized(bool); + + bool is_fullscreen() const { return m_fullscreen; } + void set_fullscreen(bool); + + WindowTileType tiled() const { return m_tiled; } + void set_tiled(WindowTileType); + + bool is_occluded() const { return m_occluded; } + void set_occluded(bool); + + bool is_movable() const + { + return m_type == WindowType::Normal; + } + + WindowFrame& frame() { return m_frame; } + const WindowFrame& frame() const { return m_frame; } + + bool is_blocked_by_modal_window() const; + + bool listens_to_wm_events() const { return m_listens_to_wm_events; } + + ClientConnection* client() { return m_client; } + const ClientConnection* client() const { return m_client; } + + WindowType type() const { return m_type; } + int window_id() const { return m_window_id; } + + bool is_internal() const { return m_client_id == -1; } + i32 client_id() const { return m_client_id; } + + String title() const { return m_title; } + void set_title(const String&); + + float opacity() const { return m_opacity; } + void set_opacity(float); + + int x() const { return m_rect.x(); } + int y() const { return m_rect.y(); } + int width() const { return m_rect.width(); } + int height() const { return m_rect.height(); } + + bool is_active() const; + + bool is_visible() const { return m_visible; } + void set_visible(bool); + + bool is_modal() const { return m_modal; } + + Gfx::Rect rect() const { return m_rect; } + void set_rect(const Gfx::Rect&); + void set_rect(int x, int y, int width, int height) { set_rect({ x, y, width, height }); } + void set_rect_without_repaint(const Gfx::Rect&); + + void set_taskbar_rect(const Gfx::Rect& rect) { m_taskbar_rect = rect; } + const Gfx::Rect& taskbar_rect() const { return m_taskbar_rect; } + + void move_to(const Gfx::Point& position) { set_rect({ position, size() }); } + void move_to(int x, int y) { move_to({ x, y }); } + + void move_by(const Gfx::Point& delta) { set_position_without_repaint(position().translated(delta)); } + + Gfx::Point position() const { return m_rect.location(); } + void set_position(const Gfx::Point& position) { set_rect({ position.x(), position.y(), width(), height() }); } + void set_position_without_repaint(const Gfx::Point& position) { set_rect_without_repaint({ position.x(), position.y(), width(), height() }); } + + Gfx::Size size() const { return m_rect.size(); } + + void invalidate(); + void invalidate(const Gfx::Rect&); + + virtual void event(Core::Event&) override; + + // Only used by WindowType::MenuApplet. Perhaps it could be a Window subclass? I don't know. + void set_rect_in_menubar(const Gfx::Rect& rect) { m_rect_in_menubar = rect; } + const Gfx::Rect& rect_in_menubar() const { return m_rect_in_menubar; } + + const Gfx::Bitmap* backing_store() const { return m_backing_store.ptr(); } + Gfx::Bitmap* backing_store() { return m_backing_store.ptr(); } + + void set_backing_store(RefPtr<Gfx::Bitmap>&& backing_store) + { + m_last_backing_store = move(m_backing_store); + m_backing_store = move(backing_store); + } + + void swap_backing_stores() + { + swap(m_backing_store, m_last_backing_store); + } + + Gfx::Bitmap* last_backing_store() { return m_last_backing_store.ptr(); } + + void set_global_cursor_tracking_enabled(bool); + void set_automatic_cursor_tracking_enabled(bool enabled) { m_automatic_cursor_tracking_enabled = enabled; } + bool global_cursor_tracking() const { return m_global_cursor_tracking_enabled || m_automatic_cursor_tracking_enabled; } + + bool has_alpha_channel() const { return m_has_alpha_channel; } + void set_has_alpha_channel(bool value) { m_has_alpha_channel = value; } + + Gfx::Size size_increment() const { return m_size_increment; } + void set_size_increment(const Gfx::Size& increment) { m_size_increment = increment; } + + Gfx::Size base_size() const { return m_base_size; } + void set_base_size(const Gfx::Size& size) { m_base_size = size; } + + const Gfx::Bitmap& icon() const { return *m_icon; } + void set_icon(NonnullRefPtr<Gfx::Bitmap>&& icon) { m_icon = move(icon); } + + void set_default_icon(); + + const Cursor* override_cursor() const { return m_override_cursor.ptr(); } + void set_override_cursor(RefPtr<Cursor>&& cursor) { m_override_cursor = move(cursor); } + + void request_update(const Gfx::Rect&, bool ignore_occlusion = false); + Gfx::DisjointRectSet take_pending_paint_rects() { return move(m_pending_paint_rects); } + + bool in_minimize_animation() const { return m_minimize_animation_step != -1; } + + int minimize_animation_index() const { return m_minimize_animation_step; } + void step_minimize_animation() { m_minimize_animation_step += 1; } + void start_minimize_animation() { m_minimize_animation_step = 0; } + void end_minimize_animation() { m_minimize_animation_step = -1; } + + Gfx::Rect tiled_rect(WindowTileType) const; + void recalculate_rect(); + + // For InlineLinkedList. + // FIXME: Maybe make a ListHashSet and then WindowManager can just use that. + Window* m_next { nullptr }; + Window* m_prev { nullptr }; + + void detach_client(Badge<ClientConnection>); + + Window* parent_window() { return m_parent_window; } + const Window* parent_window() const { return m_parent_window; } + + void set_parent_window(Window&); + + Vector<WeakPtr<Window>>& child_windows() { return m_child_windows; } + const Vector<WeakPtr<Window>>& child_windows() const { return m_child_windows; } + + void set_frameless(bool frameless) { m_frameless = frameless; } + bool is_frameless() const { return m_frameless; } + +private: + void handle_mouse_event(const MouseEvent&); + void update_menu_item_text(PopupMenuItem item); + void update_menu_item_enabled(PopupMenuItem item); + void add_child_window(Window&); + + ClientConnection* m_client { nullptr }; + + WeakPtr<Window> m_parent_window; + Vector<WeakPtr<Window>> m_child_windows; + + String m_title; + Gfx::Rect m_rect; + Gfx::Rect m_saved_nonfullscreen_rect; + Gfx::Rect m_taskbar_rect; + WindowType m_type { WindowType::Normal }; + bool m_global_cursor_tracking_enabled { false }; + bool m_automatic_cursor_tracking_enabled { false }; + bool m_visible { true }; + bool m_has_alpha_channel { false }; + bool m_modal { false }; + bool m_minimizable { false }; + bool m_frameless { false }; + bool m_resizable { false }; + bool m_listens_to_wm_events { false }; + bool m_minimized { false }; + bool m_maximized { false }; + bool m_fullscreen { false }; + WindowTileType m_tiled { WindowTileType::None }; + Gfx::Rect m_untiled_rect; + bool m_occluded { false }; + RefPtr<Gfx::Bitmap> m_backing_store; + RefPtr<Gfx::Bitmap> m_last_backing_store; + int m_window_id { -1 }; + i32 m_client_id { -1 }; + float m_opacity { 1 }; + Gfx::Size m_size_increment; + Gfx::Size m_base_size; + NonnullRefPtr<Gfx::Bitmap> m_icon; + RefPtr<Cursor> m_override_cursor; + WindowFrame m_frame; + unsigned m_wm_event_mask { 0 }; + Gfx::DisjointRectSet m_pending_paint_rects; + Gfx::Rect m_unmaximized_rect; + Gfx::Rect m_rect_in_menubar; + RefPtr<Menu> m_window_menu; + MenuItem* m_window_menu_minimize_item { nullptr }; + MenuItem* m_window_menu_maximize_item { nullptr }; + int m_minimize_animation_step { -1 }; +}; + +} diff --git a/Services/WindowServer/WindowClient.ipc b/Services/WindowServer/WindowClient.ipc new file mode 100644 index 0000000000..3c94ccf89e --- /dev/null +++ b/Services/WindowServer/WindowClient.ipc @@ -0,0 +1,40 @@ +endpoint WindowClient = 4 +{ + Paint(i32 window_id, Gfx::Size window_size, Vector<Gfx::Rect> rects) =| + MouseMove(i32 window_id, Gfx::Point mouse_position, u32 button, u32 buttons, u32 modifiers, i32 wheel_delta, bool is_drag, String drag_data_type) =| + MouseDown(i32 window_id, Gfx::Point mouse_position, u32 button, u32 buttons, u32 modifiers, i32 wheel_delta) =| + MouseDoubleClick(i32 window_id, Gfx::Point mouse_position, u32 button, u32 buttons, u32 modifiers, i32 wheel_delta) =| + MouseUp(i32 window_id, Gfx::Point mouse_position, u32 button, u32 buttons, u32 modifiers, i32 wheel_delta) =| + MouseWheel(i32 window_id, Gfx::Point mouse_position, u32 button, u32 buttons, u32 modifiers, i32 wheel_delta) =| + WindowEntered(i32 window_id) =| + WindowLeft(i32 window_id) =| + KeyDown(i32 window_id, u8 character, u32 key, u32 modifiers) =| + KeyUp(i32 window_id, u8 character, u32 key, u32 modifiers) =| + WindowActivated(i32 window_id) =| + WindowDeactivated(i32 window_id) =| + WindowStateChanged(i32 window_id, bool minimized, bool occluded) =| + WindowCloseRequest(i32 window_id) =| + WindowResized(i32 window_id, Gfx::Rect old_rect, Gfx::Rect new_rect) =| + + MenuItemActivated(i32 menu_id, i32 identifier) =| + + ScreenRectChanged(Gfx::Rect rect) =| + + ClipboardContentsChanged(String content_type) =| + + WM_WindowRemoved(i32 wm_id, i32 client_id, i32 window_id) =| + WM_WindowStateChanged(i32 wm_id, i32 client_id, i32 window_id, bool is_active, bool is_minimized, bool is_frameless, i32 window_type, String title, Gfx::Rect rect) =| + WM_WindowIconBitmapChanged(i32 wm_id, i32 client_id, i32 window_id, i32 icon_buffer_id, Gfx::Size icon_size) =| + WM_WindowRectChanged(i32 wm_id, i32 client_id, i32 window_id, Gfx::Rect rect) =| + + AsyncSetWallpaperFinished(bool success) =| + + DragAccepted() =| + DragCancelled() =| + + DragDropped(i32 window_id, Gfx::Point mouse_position, String text, String data_type, String data) =| + + UpdateSystemTheme(i32 system_theme_buffer_id) =| + + DisplayLinkNotification() =| +} diff --git a/Services/WindowServer/WindowFrame.cpp b/Services/WindowServer/WindowFrame.cpp new file mode 100644 index 0000000000..2d6b8cad73 --- /dev/null +++ b/Services/WindowServer/WindowFrame.cpp @@ -0,0 +1,414 @@ +/* + * Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org> + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#include <AK/Badge.h> +#include <LibGfx/CharacterBitmap.h> +#include <LibGfx/Font.h> +#include <LibGfx/Painter.h> +#include <LibGfx/StylePainter.h> +#include <WindowServer/Button.h> +#include <WindowServer/Compositor.h> +#include <WindowServer/Event.h> +#include <WindowServer/Window.h> +#include <WindowServer/WindowFrame.h> +#include <WindowServer/WindowManager.h> + +namespace WindowServer { + +static const int window_titlebar_height = 19; + +static const char* s_close_button_bitmap_data = { + "## ##" + "### ###" + " ###### " + " #### " + " #### " + " ###### " + "### ###" + "## ##" + " " +}; + +static Gfx::CharacterBitmap* s_close_button_bitmap; +static const int s_close_button_bitmap_width = 8; +static const int s_close_button_bitmap_height = 9; + +static const char* s_minimize_button_bitmap_data = { + " " + " " + " " + " ###### " + " #### " + " ## " + " " + " " + " " +}; + +static Gfx::CharacterBitmap* s_minimize_button_bitmap; +static const int s_minimize_button_bitmap_width = 8; +static const int s_minimize_button_bitmap_height = 9; + +static const char* s_maximize_button_bitmap_data = { + " " + " " + " " + " ## " + " #### " + " ###### " + " " + " " + " " +}; + +static Gfx::CharacterBitmap* s_maximize_button_bitmap; +static const int s_maximize_button_bitmap_width = 8; +static const int s_maximize_button_bitmap_height = 9; + +static const char* s_unmaximize_button_bitmap_data = { + " " + " ## " + " #### " + " ###### " + " " + " ###### " + " #### " + " ## " + " " +}; + +static Gfx::CharacterBitmap* s_unmaximize_button_bitmap; +static const int s_unmaximize_button_bitmap_width = 8; +static const int s_unmaximize_button_bitmap_height = 9; + +WindowFrame::WindowFrame(Window& window) + : m_window(window) +{ + if (!s_close_button_bitmap) + s_close_button_bitmap = &Gfx::CharacterBitmap::create_from_ascii(s_close_button_bitmap_data, s_close_button_bitmap_width, s_close_button_bitmap_height).leak_ref(); + + if (!s_minimize_button_bitmap) + s_minimize_button_bitmap = &Gfx::CharacterBitmap::create_from_ascii(s_minimize_button_bitmap_data, s_minimize_button_bitmap_width, s_minimize_button_bitmap_height).leak_ref(); + + if (!s_maximize_button_bitmap) + s_maximize_button_bitmap = &Gfx::CharacterBitmap::create_from_ascii(s_maximize_button_bitmap_data, s_maximize_button_bitmap_width, s_maximize_button_bitmap_height).leak_ref(); + + if (!s_unmaximize_button_bitmap) + s_unmaximize_button_bitmap = &Gfx::CharacterBitmap::create_from_ascii(s_unmaximize_button_bitmap_data, s_unmaximize_button_bitmap_width, s_unmaximize_button_bitmap_height).leak_ref(); + + m_buttons.append(make<Button>(*this, *s_close_button_bitmap, [this](auto&) { + m_window.request_close(); + })); + + if (window.is_resizable()) { + auto button = make<Button>(*this, *s_maximize_button_bitmap, [this](auto&) { + m_window.set_maximized(!m_window.is_maximized()); + }); + m_maximize_button = button.ptr(); + m_buttons.append(move(button)); + } + + if (window.is_minimizable()) { + auto button = make<Button>(*this, *s_minimize_button_bitmap, [this](auto&) { + m_window.set_minimized(true); + }); + m_minimize_button = button.ptr(); + m_buttons.append(move(button)); + } +} + +WindowFrame::~WindowFrame() +{ +} + +void WindowFrame::did_set_maximized(Badge<Window>, bool maximized) +{ + ASSERT(m_maximize_button); + m_maximize_button->set_bitmap(maximized ? *s_unmaximize_button_bitmap : *s_maximize_button_bitmap); +} + +Gfx::Rect WindowFrame::title_bar_rect() const +{ + if (m_window.type() == WindowType::Notification) + return { m_window.width() + 3, 3, window_titlebar_height, m_window.height() }; + return { 4, 4, m_window.width(), window_titlebar_height }; +} + +Gfx::Rect WindowFrame::title_bar_icon_rect() const +{ + auto titlebar_rect = title_bar_rect(); + return { + titlebar_rect.x() + 1, + titlebar_rect.y() + 2, + 16, + titlebar_rect.height(), + }; +} + +Gfx::Rect WindowFrame::title_bar_text_rect() const +{ + auto titlebar_rect = title_bar_rect(); + auto titlebar_icon_rect = title_bar_icon_rect(); + return { + titlebar_rect.x() + 2 + titlebar_icon_rect.width() + 2, + titlebar_rect.y(), + titlebar_rect.width() - 4 - titlebar_icon_rect.width() - 2, + titlebar_rect.height() + }; +} + +WindowFrame::FrameColors WindowFrame::compute_frame_colors() const +{ + auto& wm = WindowManager::the(); + auto palette = wm.palette(); + if (&m_window == wm.m_highlight_window) + return { palette.highlight_window_title(), palette.highlight_window_border1(), palette.highlight_window_border2() }; + if (&m_window == wm.m_move_window) + return { palette.moving_window_title(), palette.moving_window_border1(), palette.moving_window_border2() }; + if (&m_window == wm.m_active_window) + return { palette.active_window_title(), palette.active_window_border1(), palette.active_window_border2() }; + return { palette.inactive_window_title(), palette.inactive_window_border1(), palette.inactive_window_border2() }; +} + +void WindowFrame::paint_notification_frame(Gfx::Painter& painter) +{ + auto palette = WindowManager::the().palette(); + Gfx::Rect outer_rect = { {}, rect().size() }; + + Gfx::StylePainter::paint_window_frame(painter, outer_rect, palette); + + auto titlebar_rect = title_bar_rect(); + painter.fill_rect_with_gradient(Gfx::Orientation::Vertical, titlebar_rect, palette.active_window_border1(), palette.active_window_border2()); + + int stripe_top = m_buttons.last().relative_rect().bottom() + 4; + int stripe_bottom = m_window.height() - 3; + if (stripe_top && stripe_bottom && stripe_top < stripe_bottom) { + for (int i = 2; i <= window_titlebar_height - 2; i += 2) { + painter.draw_line({ titlebar_rect.x() + i, stripe_top }, { titlebar_rect.x() + i, stripe_bottom }, palette.active_window_border1()); + } + } +} + +void WindowFrame::paint_normal_frame(Gfx::Painter& painter) +{ + auto palette = WindowManager::the().palette(); + auto& window = m_window; + Gfx::Rect outer_rect = { {}, rect().size() }; + + Gfx::StylePainter::paint_window_frame(painter, outer_rect, palette); + + auto titlebar_rect = title_bar_rect(); + auto titlebar_icon_rect = title_bar_icon_rect(); + auto titlebar_inner_rect = title_bar_text_rect(); + auto titlebar_title_rect = titlebar_inner_rect; + titlebar_title_rect.set_width(Gfx::Font::default_bold_font().width(window.title())); + + auto [title_color, border_color, border_color2] = compute_frame_colors(); + + auto& wm = WindowManager::the(); + painter.draw_line(titlebar_rect.bottom_left().translated(0, 1), titlebar_rect.bottom_right().translated(0, 1), palette.button()); + painter.draw_line(titlebar_rect.bottom_left().translated(0, 2), titlebar_rect.bottom_right().translated(0, 2), palette.button()); + + auto leftmost_button_rect = m_buttons.is_empty() ? Gfx::Rect() : m_buttons.last().relative_rect(); + + painter.fill_rect_with_gradient(titlebar_rect, border_color, border_color2); + + int stripe_left = titlebar_title_rect.right() + 4; + int stripe_right = leftmost_button_rect.left() - 3; + if (stripe_left && stripe_right && stripe_left < stripe_right) { + for (int i = 2; i <= titlebar_inner_rect.height() - 2; i += 2) { + painter.draw_line({ stripe_left, titlebar_inner_rect.y() + i }, { stripe_right, titlebar_inner_rect.y() + i }, border_color); + } + } + + auto clipped_title_rect = titlebar_title_rect; + clipped_title_rect.set_width(stripe_right - clipped_title_rect.x()); + if (!clipped_title_rect.is_empty()) { + painter.draw_text(clipped_title_rect.translated(1, 2), window.title(), wm.window_title_font(), Gfx::TextAlignment::CenterLeft, border_color.darkened(0.4), Gfx::TextElision::Right); + // FIXME: The translated(0, 1) wouldn't be necessary if we could center text based on its baseline. + painter.draw_text(clipped_title_rect.translated(0, 1), window.title(), wm.window_title_font(), Gfx::TextAlignment::CenterLeft, title_color, Gfx::TextElision::Right); + } + + painter.blit(titlebar_icon_rect.location(), window.icon(), window.icon().rect()); +} + +void WindowFrame::paint(Gfx::Painter& painter) +{ + if (m_window.is_frameless()) + return; + + Gfx::PainterStateSaver saver(painter); + painter.translate(rect().location()); + + if (m_window.type() == WindowType::Notification) + paint_notification_frame(painter); + else if (m_window.type() == WindowType::Normal) + paint_normal_frame(painter); + else + return; + + for (auto& button : m_buttons) { + button.paint(painter); + } +} + +static Gfx::Rect frame_rect_for_window(Window& window, const Gfx::Rect& rect) +{ + if (window.is_frameless()) + return rect; + + auto type = window.type(); + + switch (type) { + case WindowType::Normal: + return { + rect.x() - 4, + rect.y() - window_titlebar_height - 6, + rect.width() + 8, + rect.height() + 10 + window_titlebar_height + }; + case WindowType::Notification: + return { + rect.x() - 3, + rect.y() - 3, + rect.width() + 6 + window_titlebar_height, + rect.height() + 6 + }; + default: + return rect; + } +} + +static Gfx::Rect frame_rect_for_window(Window& window) +{ + return frame_rect_for_window(window, window.rect()); +} + +Gfx::Rect WindowFrame::rect() const +{ + return frame_rect_for_window(m_window); +} + +void WindowFrame::invalidate_title_bar() +{ + WindowManager::the().invalidate(title_bar_rect().translated(rect().location())); +} + +void WindowFrame::notify_window_rect_changed(const Gfx::Rect& old_rect, const Gfx::Rect& new_rect) +{ + int window_button_width = 15; + int window_button_height = 15; + int pos; + if (m_window.type() == WindowType::Notification) + pos = title_bar_rect().top() + 2; + else + pos = title_bar_text_rect().right() + 1; + + for (auto& button : m_buttons) { + if (m_window.type() == WindowType::Notification) { + Gfx::Rect rect { 0, pos, window_button_width, window_button_height }; + rect.center_horizontally_within(title_bar_rect()); + button.set_relative_rect(rect); + pos += window_button_width; + } else { + pos -= window_button_width; + Gfx::Rect rect { pos, 0, window_button_width, window_button_height }; + rect.center_vertically_within(title_bar_text_rect()); + button.set_relative_rect(rect); + } + } + + auto& wm = WindowManager::the(); + wm.invalidate(frame_rect_for_window(m_window, old_rect)); + wm.invalidate(frame_rect_for_window(m_window, new_rect)); + wm.notify_rect_changed(m_window, old_rect, new_rect); +} + +void WindowFrame::on_mouse_event(const MouseEvent& event) +{ + ASSERT(!m_window.is_fullscreen()); + + if (m_window.is_blocked_by_modal_window()) + return; + + auto& wm = WindowManager::the(); + if (m_window.type() != WindowType::Normal && m_window.type() != WindowType::Notification) + return; + + if (m_window.type() == WindowType::Normal && event.type() == Event::MouseDown && (event.button() == MouseButton::Left || event.button() == MouseButton::Right) && title_bar_icon_rect().contains(event.position())) { + wm.move_to_front_and_make_active(m_window); + m_window.popup_window_menu(title_bar_rect().bottom_left().translated(rect().location())); + return; + } + + // This is slightly hackish, but expand the title bar rect by two pixels downwards, + // so that mouse events between the title bar and window contents don't act like + // mouse events on the border. + auto adjusted_title_bar_rect = title_bar_rect(); + adjusted_title_bar_rect.set_height(adjusted_title_bar_rect.height() + 2); + + if (adjusted_title_bar_rect.contains(event.position())) { + wm.clear_resize_candidate(); + + if (event.type() == Event::MouseDown) + wm.move_to_front_and_make_active(m_window); + + for (auto& button : m_buttons) { + if (button.relative_rect().contains(event.position())) + return button.on_mouse_event(event.translated(-button.relative_rect().location())); + } + if (event.type() == Event::MouseDown) { + if (m_window.type() == WindowType::Normal && event.button() == MouseButton::Right) { + m_window.popup_window_menu(event.position().translated(rect().location())); + return; + } + if (m_window.is_movable() && event.button() == MouseButton::Left) + wm.start_window_move(m_window, event.translated(rect().location())); + } + return; + } + + if (m_window.is_resizable() && event.type() == Event::MouseMove && event.buttons() == 0) { + constexpr ResizeDirection direction_for_hot_area[3][3] = { + { ResizeDirection::UpLeft, ResizeDirection::Up, ResizeDirection::UpRight }, + { ResizeDirection::Left, ResizeDirection::None, ResizeDirection::Right }, + { ResizeDirection::DownLeft, ResizeDirection::Down, ResizeDirection::DownRight }, + }; + Gfx::Rect outer_rect = { {}, rect().size() }; + ASSERT(outer_rect.contains(event.position())); + int window_relative_x = event.x() - outer_rect.x(); + int window_relative_y = event.y() - outer_rect.y(); + int hot_area_row = min(2, window_relative_y / (outer_rect.height() / 3)); + int hot_area_column = min(2, window_relative_x / (outer_rect.width() / 3)); + wm.set_resize_candidate(m_window, direction_for_hot_area[hot_area_row][hot_area_column]); + Compositor::the().invalidate_cursor(); + return; + } + + if (m_window.is_resizable() && event.type() == Event::MouseDown && event.button() == MouseButton::Left) + wm.start_window_resize(m_window, event.translated(rect().location())); +} +} diff --git a/Services/WindowServer/WindowFrame.h b/Services/WindowServer/WindowFrame.h new file mode 100644 index 0000000000..021edb9d0f --- /dev/null +++ b/Services/WindowServer/WindowFrame.h @@ -0,0 +1,74 @@ +/* + * Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org> + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#pragma once + +#include <AK/Forward.h> +#include <AK/NonnullOwnPtrVector.h> +#include <LibGfx/Forward.h> + +namespace WindowServer { + +class Button; +class MouseEvent; +class Window; + +class WindowFrame { +public: + WindowFrame(Window&); + ~WindowFrame(); + + Gfx::Rect rect() const; + void paint(Gfx::Painter&); + void on_mouse_event(const MouseEvent&); + void notify_window_rect_changed(const Gfx::Rect& old_rect, const Gfx::Rect& new_rect); + void invalidate_title_bar(); + + Gfx::Rect title_bar_rect() const; + Gfx::Rect title_bar_icon_rect() const; + Gfx::Rect title_bar_text_rect() const; + + void did_set_maximized(Badge<Window>, bool); + +private: + void paint_notification_frame(Gfx::Painter&); + void paint_normal_frame(Gfx::Painter&); + + struct FrameColors { + Color title_color; + Color border_color; + Color border_color2; + }; + + FrameColors compute_frame_colors() const; + + Window& m_window; + NonnullOwnPtrVector<Button> m_buttons; + Button* m_maximize_button { nullptr }; + Button* m_minimize_button { nullptr }; +}; + +} diff --git a/Services/WindowServer/WindowManager.cpp b/Services/WindowServer/WindowManager.cpp new file mode 100644 index 0000000000..2b168107d1 --- /dev/null +++ b/Services/WindowServer/WindowManager.cpp @@ -0,0 +1,1317 @@ +/* + * Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org> + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#include "WindowManager.h" +#include "Compositor.h" +#include "EventLoop.h" +#include "Menu.h" +#include "MenuBar.h" +#include "MenuItem.h" +#include "Screen.h" +#include "Window.h" +#include <AK/LogStream.h> +#include <AK/SharedBuffer.h> +#include <AK/StdLibExtras.h> +#include <AK/Vector.h> +#include <LibGfx/CharacterBitmap.h> +#include <LibGfx/Font.h> +#include <LibGfx/Painter.h> +#include <LibGfx/StylePainter.h> +#include <LibGfx/SystemTheme.h> +#include <WindowServer/AppletManager.h> +#include <WindowServer/Button.h> +#include <WindowServer/ClientConnection.h> +#include <WindowServer/Cursor.h> +#include <WindowServer/WindowClientEndpoint.h> +#include <errno.h> +#include <serenity.h> +#include <stdio.h> +#include <time.h> +#include <unistd.h> + +//#define WINDOWMANAGER_DEBUG +//#define RESIZE_DEBUG +//#define MOVE_DEBUG +//#define DOUBLECLICK_DEBUG + +namespace WindowServer { + +static WindowManager* s_the; + +WindowManager& WindowManager::the() +{ + ASSERT(s_the); + return *s_the; +} + +WindowManager::WindowManager(const Gfx::PaletteImpl& palette) + : m_palette(palette) +{ + s_the = this; + + reload_config(false); + + invalidate(); + Compositor::the().compose(); +} + +WindowManager::~WindowManager() +{ +} + +NonnullRefPtr<Cursor> WindowManager::get_cursor(const String& name, const Gfx::Point& hotspot) +{ + auto path = m_wm_config->read_entry("Cursor", name, "/res/cursors/arrow.png"); + auto gb = Gfx::Bitmap::load_from_file(path); + if (gb) + return Cursor::create(*gb, hotspot); + return Cursor::create(*Gfx::Bitmap::load_from_file("/res/cursors/arrow.png")); +} + +NonnullRefPtr<Cursor> WindowManager::get_cursor(const String& name) +{ + auto path = m_wm_config->read_entry("Cursor", name, "/res/cursors/arrow.png"); + auto gb = Gfx::Bitmap::load_from_file(path); + + if (gb) + return Cursor::create(*gb); + return Cursor::create(*Gfx::Bitmap::load_from_file("/res/cursors/arrow.png")); +} + +void WindowManager::reload_config(bool set_screen) +{ + m_wm_config = Core::ConfigFile::open("/etc/WindowServer/WindowServer.ini"); + + m_double_click_speed = m_wm_config->read_num_entry("Input", "DoubleClickSpeed", 250); + + if (set_screen) { + set_resolution(m_wm_config->read_num_entry("Screen", "Width", 1920), m_wm_config->read_num_entry("Screen", "Height", 1080)); + } + + m_arrow_cursor = get_cursor("Arrow", { 2, 2 }); + m_hand_cursor = get_cursor("Hand", { 8, 4 }); + m_resize_horizontally_cursor = get_cursor("ResizeH"); + m_resize_vertically_cursor = get_cursor("ResizeV"); + m_resize_diagonally_tlbr_cursor = get_cursor("ResizeDTLBR"); + m_resize_diagonally_bltr_cursor = get_cursor("ResizeDBLTR"); + m_i_beam_cursor = get_cursor("IBeam"); + m_disallowed_cursor = get_cursor("Disallowed"); + m_move_cursor = get_cursor("Move"); + m_drag_cursor = get_cursor("Drag"); +} + +const Gfx::Font& WindowManager::font() const +{ + return Gfx::Font::default_font(); +} + +const Gfx::Font& WindowManager::window_title_font() const +{ + return Gfx::Font::default_bold_font(); +} + +bool WindowManager::set_resolution(int width, int height) +{ + bool success = Compositor::the().set_resolution(width, height); + MenuManager::the().set_needs_window_resize(); + ClientConnection::for_each_client([&](ClientConnection& client) { + client.notify_about_new_screen_rect(Screen::the().rect()); + }); + if (success) { + for_each_window([](Window& window) { + if (window.type() == WindowType::Desktop) + window.set_rect(WindowManager::the().desktop_rect()); + window.recalculate_rect(); + return IterationDecision::Continue; + }); + } + if (m_wm_config) { + if (success) { + dbg() << "Saving resolution: " << Gfx::Size(width, height) << " to config file at " << m_wm_config->file_name(); + m_wm_config->write_num_entry("Screen", "Width", width); + m_wm_config->write_num_entry("Screen", "Height", height); + m_wm_config->sync(); + } else { + dbg() << "Saving fallback resolution: " << resolution() << " to config file at " << m_wm_config->file_name(); + m_wm_config->write_num_entry("Screen", "Width", resolution().width()); + m_wm_config->write_num_entry("Screen", "Height", resolution().height()); + m_wm_config->sync(); + } + } + return success; +} + +Gfx::Size WindowManager::resolution() const +{ + return Screen::the().size(); +} + +void WindowManager::add_window(Window& window) +{ + bool is_first_window = m_windows_in_order.is_empty(); + + m_windows_in_order.append(&window); + + if (window.is_fullscreen()) { + Core::EventLoop::current().post_event(window, make<ResizeEvent>(window.rect(), Screen::the().rect())); + window.set_rect(Screen::the().rect()); + } + + if (window.type() != WindowType::Desktop || is_first_window) + set_active_window(&window); + + if (m_switcher.is_visible() && window.type() != WindowType::WindowSwitcher) + m_switcher.refresh(); + + recompute_occlusions(); + + if (window.listens_to_wm_events()) { + for_each_window([&](Window& other_window) { + if (&window != &other_window) { + tell_wm_listener_about_window(window, other_window); + tell_wm_listener_about_window_icon(window, other_window); + } + return IterationDecision::Continue; + }); + } + + tell_wm_listeners_window_state_changed(window); +} + +void WindowManager::move_to_front_and_make_active(Window& window) +{ + if (window.is_blocked_by_modal_window()) + return; + + if (m_windows_in_order.tail() != &window) + invalidate(window); + m_windows_in_order.remove(&window); + m_windows_in_order.append(&window); + + recompute_occlusions(); + + set_active_window(&window); + + if (m_switcher.is_visible()) { + m_switcher.refresh(); + m_switcher.select_window(window); + set_highlight_window(&window); + } + + for (auto& child_window : window.child_windows()) { + if (child_window) + move_to_front_and_make_active(*child_window); + } +} + +void WindowManager::remove_window(Window& window) +{ + invalidate(window); + m_windows_in_order.remove(&window); + if (window.is_active()) + pick_new_active_window(); + if (m_switcher.is_visible() && window.type() != WindowType::WindowSwitcher) + m_switcher.refresh(); + + recompute_occlusions(); + + for_each_window_listening_to_wm_events([&window](Window& listener) { + if (!(listener.wm_event_mask() & WMEventMask::WindowRemovals)) + return IterationDecision::Continue; + if (!window.is_internal()) + listener.client()->post_message(Messages::WindowClient::WM_WindowRemoved(listener.window_id(), window.client_id(), window.window_id())); + return IterationDecision::Continue; + }); +} + +void WindowManager::tell_wm_listener_about_window(Window& listener, Window& window) +{ + if (!(listener.wm_event_mask() & WMEventMask::WindowStateChanges)) + return; + if (window.is_internal()) + return; + listener.client()->post_message(Messages::WindowClient::WM_WindowStateChanged(listener.window_id(), window.client_id(), window.window_id(), window.is_active(), window.is_minimized(), window.is_frameless(), (i32)window.type(), window.title(), window.rect())); +} + +void WindowManager::tell_wm_listener_about_window_rect(Window& listener, Window& window) +{ + if (!(listener.wm_event_mask() & WMEventMask::WindowRectChanges)) + return; + if (window.is_internal()) + return; + listener.client()->post_message(Messages::WindowClient::WM_WindowRectChanged(listener.window_id(), window.client_id(), window.window_id(), window.rect())); +} + +void WindowManager::tell_wm_listener_about_window_icon(Window& listener, Window& window) +{ + if (!(listener.wm_event_mask() & WMEventMask::WindowIconChanges)) + return; + if (window.is_internal()) + return; + if (window.icon().shbuf_id() == -1) + return; +#ifdef WINDOWMANAGER_DEBUG + dbg() << "WindowServer: Sharing icon buffer " << window.icon().shbuf_id() << " with PID " << listener.client()->client_pid(); +#endif + if (shbuf_allow_pid(window.icon().shbuf_id(), listener.client()->client_pid()) < 0) { + ASSERT_NOT_REACHED(); + } + listener.client()->post_message(Messages::WindowClient::WM_WindowIconBitmapChanged(listener.window_id(), window.client_id(), window.window_id(), window.icon().shbuf_id(), window.icon().size())); +} + +void WindowManager::tell_wm_listeners_window_state_changed(Window& window) +{ + for_each_window_listening_to_wm_events([&](Window& listener) { + tell_wm_listener_about_window(listener, window); + return IterationDecision::Continue; + }); +} + +void WindowManager::tell_wm_listeners_window_icon_changed(Window& window) +{ + for_each_window_listening_to_wm_events([&](Window& listener) { + tell_wm_listener_about_window_icon(listener, window); + return IterationDecision::Continue; + }); +} + +void WindowManager::tell_wm_listeners_window_rect_changed(Window& window) +{ + for_each_window_listening_to_wm_events([&](Window& listener) { + tell_wm_listener_about_window_rect(listener, window); + return IterationDecision::Continue; + }); +} + +void WindowManager::notify_title_changed(Window& window) +{ + if (window.type() != WindowType::Normal) + return; +#ifdef WINDOWMANAGER_DEBUG + dbg() << "[WM] Window{" << &window << "} title set to \"" << window.title() << '"'; +#endif + invalidate(window.frame().rect()); + if (m_switcher.is_visible()) + m_switcher.refresh(); + + tell_wm_listeners_window_state_changed(window); +} + +void WindowManager::notify_rect_changed(Window& window, const Gfx::Rect& old_rect, const Gfx::Rect& new_rect) +{ + UNUSED_PARAM(old_rect); + UNUSED_PARAM(new_rect); +#ifdef RESIZE_DEBUG + dbg() << "[WM] Window " << &window << " rect changed " << old_rect << " -> " << new_rect; +#endif + if (m_switcher.is_visible() && window.type() != WindowType::WindowSwitcher) + m_switcher.refresh(); + + recompute_occlusions(); + + tell_wm_listeners_window_rect_changed(window); + + MenuManager::the().refresh(); +} + +void WindowManager::recompute_occlusions() +{ + for_each_visible_window_from_back_to_front([&](Window& window) { + if (m_switcher.is_visible()) { + window.set_occluded(false); + } else { + if (any_opaque_window_above_this_one_contains_rect(window, window.frame().rect())) + window.set_occluded(true); + else + window.set_occluded(false); + } + return IterationDecision::Continue; + }); +} + +void WindowManager::notify_opacity_changed(Window&) +{ + recompute_occlusions(); +} + +void WindowManager::notify_minimization_state_changed(Window& window) +{ + tell_wm_listeners_window_state_changed(window); + + if (window.client()) + window.client()->post_message(Messages::WindowClient::WindowStateChanged(window.window_id(), window.is_minimized(), window.is_occluded())); + + if (window.is_active() && window.is_minimized()) + pick_new_active_window(); +} + +void WindowManager::notify_occlusion_state_changed(Window& window) +{ + if (window.client()) + window.client()->post_message(Messages::WindowClient::WindowStateChanged(window.window_id(), window.is_minimized(), window.is_occluded())); +} + +void WindowManager::pick_new_active_window() +{ + bool new_window_picked = false; + for_each_visible_window_of_type_from_front_to_back(WindowType::Normal, [&](Window& candidate) { + set_active_window(&candidate); + new_window_picked = true; + return IterationDecision::Break; + }); + if (!new_window_picked) + set_active_window(nullptr); +} + +void WindowManager::start_window_move(Window& window, const MouseEvent& event) +{ +#ifdef MOVE_DEBUG + dbg() << "[WM] Begin moving Window{" << &window << "}"; +#endif + move_to_front_and_make_active(window); + m_move_window = window.make_weak_ptr(); + m_move_origin = event.position(); + m_move_window_origin = window.position(); + invalidate(window); +} + +void WindowManager::start_window_resize(Window& window, const Gfx::Point& position, MouseButton button) +{ + move_to_front_and_make_active(window); + constexpr ResizeDirection direction_for_hot_area[3][3] = { + { ResizeDirection::UpLeft, ResizeDirection::Up, ResizeDirection::UpRight }, + { ResizeDirection::Left, ResizeDirection::None, ResizeDirection::Right }, + { ResizeDirection::DownLeft, ResizeDirection::Down, ResizeDirection::DownRight }, + }; + Gfx::Rect outer_rect = window.frame().rect(); + ASSERT(outer_rect.contains(position)); + int window_relative_x = position.x() - outer_rect.x(); + int window_relative_y = position.y() - outer_rect.y(); + int hot_area_row = min(2, window_relative_y / (outer_rect.height() / 3)); + int hot_area_column = min(2, window_relative_x / (outer_rect.width() / 3)); + m_resize_direction = direction_for_hot_area[hot_area_row][hot_area_column]; + if (m_resize_direction == ResizeDirection::None) { + ASSERT(!m_resize_window); + return; + } + +#ifdef RESIZE_DEBUG + dbg() << "[WM] Begin resizing Window{" << &window << "}"; +#endif + m_resizing_mouse_button = button; + m_resize_window = window.make_weak_ptr(); + ; + m_resize_origin = position; + m_resize_window_original_rect = window.rect(); + + invalidate(window); +} + +void WindowManager::start_window_resize(Window& window, const MouseEvent& event) +{ + start_window_resize(window, event.position(), event.button()); +} + +bool WindowManager::process_ongoing_window_move(MouseEvent& event, Window*& hovered_window) +{ + if (!m_move_window) + return false; + if (event.type() == Event::MouseUp && event.button() == MouseButton::Left) { +#ifdef MOVE_DEBUG + dbg() << "[WM] Finish moving Window{" << m_move_window << "}"; +#endif + + invalidate(*m_move_window); + if (m_move_window->rect().contains(event.position())) + hovered_window = m_move_window; + if (m_move_window->is_resizable()) { + process_event_for_doubleclick(*m_move_window, event); + if (event.type() == Event::MouseDoubleClick) { +#if defined(DOUBLECLICK_DEBUG) + dbg() << "[WM] Click up became doubleclick!"; +#endif + m_move_window->set_maximized(!m_move_window->is_maximized()); + } + } + m_move_window = nullptr; + return true; + } + if (event.type() == Event::MouseMove) { + +#ifdef MOVE_DEBUG + dbg() << "[WM] Moving, origin: " << m_move_origin << ", now: " << event.position(); + if (m_move_window->is_maximized()) { + dbg() << " [!] The window is still maximized. Not moving yet."; + } + +#endif + + const int maximization_deadzone = 2; + + if (m_move_window->is_maximized()) { + auto pixels_moved_from_start = event.position().pixels_moved(m_move_origin); + // dbg() << "[WM] " << pixels_moved_from_start << " moved since start of window move"; + if (pixels_moved_from_start > 5) { + // dbg() << "[WM] de-maximizing window"; + m_move_origin = event.position(); + if (m_move_origin.y() <= maximization_deadzone) + return true; + auto width_before_resize = m_move_window->width(); + m_move_window->set_maximized(false); + m_move_window->move_to(m_move_origin.x() - (m_move_window->width() * ((float)m_move_origin.x() / width_before_resize)), m_move_origin.y()); + m_move_window_origin = m_move_window->position(); + } + } else { + bool is_resizable = m_move_window->is_resizable(); + auto pixels_moved_from_start = event.position().pixels_moved(m_move_origin); + const int tiling_deadzone = 5; + + if (is_resizable && event.y() <= maximization_deadzone) { + m_move_window->set_tiled(WindowTileType::None); + m_move_window->set_maximized(true); + return true; + } + if (is_resizable && event.x() <= tiling_deadzone) { + m_move_window->set_tiled(WindowTileType::Left); + } else if (is_resizable && event.x() >= Screen::the().width() - tiling_deadzone) { + m_move_window->set_tiled(WindowTileType::Right); + } else if (pixels_moved_from_start > 5 || m_move_window->tiled() == WindowTileType::None) { + m_move_window->set_tiled(WindowTileType::None); + Gfx::Point pos = m_move_window_origin.translated(event.position() - m_move_origin); + m_move_window->set_position_without_repaint(pos); + if (m_move_window->rect().contains(event.position())) + hovered_window = m_move_window; + } + return true; + } + } + return false; +} + +bool WindowManager::process_ongoing_window_resize(const MouseEvent& event, Window*& hovered_window) +{ + if (!m_resize_window) + return false; + + if (event.type() == Event::MouseUp && event.button() == m_resizing_mouse_button) { +#ifdef RESIZE_DEBUG + dbg() << "[WM] Finish resizing Window{" << m_resize_window << "}"; +#endif + Core::EventLoop::current().post_event(*m_resize_window, make<ResizeEvent>(m_resize_window->rect(), m_resize_window->rect())); + invalidate(*m_resize_window); + if (m_resize_window->rect().contains(event.position())) + hovered_window = m_resize_window; + m_resize_window = nullptr; + m_resizing_mouse_button = MouseButton::None; + return true; + } + + if (event.type() != Event::MouseMove) + return false; + + auto old_rect = m_resize_window->rect(); + + int diff_x = event.x() - m_resize_origin.x(); + int diff_y = event.y() - m_resize_origin.y(); + + int change_w = 0; + int change_h = 0; + + switch (m_resize_direction) { + case ResizeDirection::DownRight: + change_w = diff_x; + change_h = diff_y; + break; + case ResizeDirection::Right: + change_w = diff_x; + break; + case ResizeDirection::UpRight: + change_w = diff_x; + change_h = -diff_y; + break; + case ResizeDirection::Up: + change_h = -diff_y; + break; + case ResizeDirection::UpLeft: + change_w = -diff_x; + change_h = -diff_y; + break; + case ResizeDirection::Left: + change_w = -diff_x; + break; + case ResizeDirection::DownLeft: + change_w = -diff_x; + change_h = diff_y; + break; + case ResizeDirection::Down: + change_h = diff_y; + break; + default: + ASSERT_NOT_REACHED(); + } + + auto new_rect = m_resize_window_original_rect; + + // First, size the new rect. + Gfx::Size minimum_size { 50, 50 }; + + new_rect.set_width(max(minimum_size.width(), new_rect.width() + change_w)); + new_rect.set_height(max(minimum_size.height(), new_rect.height() + change_h)); + + if (!m_resize_window->size_increment().is_null()) { + int horizontal_incs = (new_rect.width() - m_resize_window->base_size().width()) / m_resize_window->size_increment().width(); + new_rect.set_width(m_resize_window->base_size().width() + horizontal_incs * m_resize_window->size_increment().width()); + int vertical_incs = (new_rect.height() - m_resize_window->base_size().height()) / m_resize_window->size_increment().height(); + new_rect.set_height(m_resize_window->base_size().height() + vertical_incs * m_resize_window->size_increment().height()); + } + + // Second, set its position so that the sides of the window + // that end up moving are the same ones as the user is dragging, + // no matter which part of the logic above caused us to decide + // to resize by this much. + switch (m_resize_direction) { + case ResizeDirection::DownRight: + case ResizeDirection::Right: + case ResizeDirection::Down: + break; + case ResizeDirection::Left: + case ResizeDirection::Up: + case ResizeDirection::UpLeft: + new_rect.set_right_without_resize(m_resize_window_original_rect.right()); + new_rect.set_bottom_without_resize(m_resize_window_original_rect.bottom()); + break; + case ResizeDirection::UpRight: + new_rect.set_bottom_without_resize(m_resize_window_original_rect.bottom()); + break; + case ResizeDirection::DownLeft: + new_rect.set_right_without_resize(m_resize_window_original_rect.right()); + break; + default: + ASSERT_NOT_REACHED(); + } + + if (new_rect.contains(event.position())) + hovered_window = m_resize_window; + + if (m_resize_window->rect() == new_rect) + return true; +#ifdef RESIZE_DEBUG + dbg() << "[WM] Resizing, original: " << m_resize_window_original_rect << ", now: " << new_rect; +#endif + m_resize_window->set_rect(new_rect); + Core::EventLoop::current().post_event(*m_resize_window, make<ResizeEvent>(old_rect, new_rect)); + return true; +} + +bool WindowManager::process_ongoing_drag(MouseEvent& event, Window*& hovered_window) +{ + if (!m_dnd_client) + return false; + + if (event.type() == Event::MouseMove) { + // We didn't let go of the drag yet, see if we should send some drag move events.. + for_each_visible_window_from_front_to_back([&](Window& window) { + if (!window.rect().contains(event.position())) + return IterationDecision::Continue; + hovered_window = &window; + auto translated_event = event.translated(-window.position()); + translated_event.set_drag(true); + translated_event.set_drag_data_type(m_dnd_data_type); + deliver_mouse_event(window, translated_event); + return IterationDecision::Break; + }); + } + + if (!(event.type() == Event::MouseUp && event.button() == MouseButton::Left)) + return true; + + hovered_window = nullptr; + for_each_visible_window_from_front_to_back([&](auto& window) { + if (window.frame().rect().contains(event.position())) { + hovered_window = &window; + return IterationDecision::Break; + } + return IterationDecision::Continue; + }); + + if (hovered_window) { + m_dnd_client->post_message(Messages::WindowClient::DragAccepted()); + if (hovered_window->client()) { + auto translated_event = event.translated(-hovered_window->position()); + hovered_window->client()->post_message(Messages::WindowClient::DragDropped(hovered_window->window_id(), translated_event.position(), m_dnd_text, m_dnd_data_type, m_dnd_data)); + } + } else { + m_dnd_client->post_message(Messages::WindowClient::DragCancelled()); + } + + end_dnd_drag(); + return true; +} + +void WindowManager::set_cursor_tracking_button(Button* button) +{ + m_cursor_tracking_button = button ? button->make_weak_ptr() : nullptr; +} + +auto WindowManager::DoubleClickInfo::metadata_for_button(MouseButton button) -> ClickMetadata& +{ + switch (button) { + case MouseButton::Left: + return m_left; + case MouseButton::Right: + return m_right; + case MouseButton::Middle: + return m_middle; + case MouseButton::Back: + return m_back; + case MouseButton::Forward: + return m_forward; + default: + ASSERT_NOT_REACHED(); + } +} + +// #define DOUBLECLICK_DEBUG + +void WindowManager::process_event_for_doubleclick(Window& window, MouseEvent& event) +{ + // We only care about button presses (because otherwise it's not a doubleclick, duh!) + ASSERT(event.type() == Event::MouseUp); + + if (&window != m_double_click_info.m_clicked_window) { + // we either haven't clicked anywhere, or we haven't clicked on this + // window. set the current click window, and reset the timers. +#if defined(DOUBLECLICK_DEBUG) + dbg() << "Initial mouseup on window " << &window << " (previous was " << m_double_click_info.m_clicked_window << ')'; +#endif + m_double_click_info.m_clicked_window = window.make_weak_ptr(); + m_double_click_info.reset(); + } + + auto& metadata = m_double_click_info.metadata_for_button(event.button()); + + // if the clock is invalid, we haven't clicked with this button on this + // window yet, so there's nothing to do. + if (!metadata.clock.is_valid()) { + metadata.clock.start(); + } else { + int elapsed_since_last_click = metadata.clock.elapsed(); + metadata.clock.start(); + if (elapsed_since_last_click < m_double_click_speed) { + auto diff = event.position() - metadata.last_position; + auto distance_travelled_squared = diff.x() * diff.x() + diff.y() * diff.y(); + if (distance_travelled_squared > (m_max_distance_for_double_click * m_max_distance_for_double_click)) { + // too far; try again + metadata.clock.start(); + } else { +#if defined(DOUBLECLICK_DEBUG) + dbg() << "Transforming MouseUp to MouseDoubleClick (" << elapsed_since_last_click << " < " << m_double_click_speed << ")!"; +#endif + event = MouseEvent(Event::MouseDoubleClick, event.position(), event.buttons(), event.button(), event.modifiers(), event.wheel_delta()); + // invalidate this now we've delivered a doubleclick, otherwise + // tripleclick will deliver two doubleclick events (incorrectly). + metadata.clock = {}; + } + } else { + // too slow; try again + metadata.clock.start(); + } + } + + metadata.last_position = event.position(); +} + +void WindowManager::deliver_mouse_event(Window& window, MouseEvent& event) +{ + window.dispatch_event(event); + if (event.type() == Event::MouseUp) { + process_event_for_doubleclick(window, event); + if (event.type() == Event::MouseDoubleClick) + window.dispatch_event(event); + } +} + +void WindowManager::process_mouse_event(MouseEvent& event, Window*& hovered_window) +{ + hovered_window = nullptr; + + if (process_ongoing_drag(event, hovered_window)) + return; + + if (process_ongoing_window_move(event, hovered_window)) + return; + + if (process_ongoing_window_resize(event, hovered_window)) + return; + + if (m_cursor_tracking_button) + return m_cursor_tracking_button->on_mouse_event(event.translated(-m_cursor_tracking_button->screen_rect().location())); + + // This is quite hackish, but it's how the Button hover effect is implemented. + if (m_hovered_button && event.type() == Event::MouseMove) + m_hovered_button->on_mouse_event(event.translated(-m_hovered_button->screen_rect().location())); + + HashTable<Window*> windows_who_received_mouse_event_due_to_cursor_tracking; + + for (auto* window = m_windows_in_order.tail(); window; window = window->prev()) { + if (!window->global_cursor_tracking()) + continue; + ASSERT(window->is_visible()); // Maybe this should be supported? Idk. Let's catch it and think about it later. + ASSERT(!window->is_minimized()); // Maybe this should also be supported? Idk. + windows_who_received_mouse_event_due_to_cursor_tracking.set(window); + auto translated_event = event.translated(-window->position()); + deliver_mouse_event(*window, translated_event); + } + + // FIXME: Now that the menubar has a dedicated window, is this special-casing really necessary? + if (MenuManager::the().has_open_menu() || (!active_window_is_modal() && menubar_rect().contains(event.position()))) { + clear_resize_candidate(); + MenuManager::the().dispatch_event(event); + return; + } + + Window* event_window_with_frame = nullptr; + + if (m_active_input_window) { + // At this point, we have delivered the start of an input sequence to a + // client application. We must keep delivering to that client + // application until the input sequence is done. + // + // This prevents e.g. moving on one window out of the bounds starting + // a move in that other unrelated window, and other silly shenanigans. + if (!windows_who_received_mouse_event_due_to_cursor_tracking.contains(m_active_input_window)) { + auto translated_event = event.translated(-m_active_input_window->position()); + deliver_mouse_event(*m_active_input_window, translated_event); + windows_who_received_mouse_event_due_to_cursor_tracking.set(m_active_input_window.ptr()); + } + if (event.type() == Event::MouseUp && event.buttons() == 0) { + m_active_input_window = nullptr; + } + + for_each_visible_window_from_front_to_back([&](auto& window) { + if (window.frame().rect().contains(event.position())) { + hovered_window = &window; + return IterationDecision::Break; + } + return IterationDecision::Continue; + }); + } else { + for_each_visible_window_from_front_to_back([&](Window& window) { + auto window_frame_rect = window.frame().rect(); + if (!window_frame_rect.contains(event.position())) + return IterationDecision::Continue; + + if (&window != m_resize_candidate.ptr()) + clear_resize_candidate(); + + // First check if we should initiate a move or resize (Logo+LMB or Logo+RMB). + // In those cases, the event is swallowed by the window manager. + if (window.is_movable()) { + if (!window.is_fullscreen() && m_keyboard_modifiers == Mod_Logo && event.type() == Event::MouseDown && event.button() == MouseButton::Left) { + hovered_window = &window; + start_window_move(window, event); + m_moved_or_resized_since_logo_keydown = true; + return IterationDecision::Break; + } + if (window.is_resizable() && m_keyboard_modifiers == Mod_Logo && event.type() == Event::MouseDown && event.button() == MouseButton::Right && !window.is_blocked_by_modal_window()) { + hovered_window = &window; + start_window_resize(window, event); + m_moved_or_resized_since_logo_keydown = true; + return IterationDecision::Break; + } + } + + if (m_keyboard_modifiers == Mod_Logo && event.type() == Event::MouseWheel) { + float opacity_change = -event.wheel_delta() * 0.05f; + float new_opacity = window.opacity() + opacity_change; + if (new_opacity < 0.05f) + new_opacity = 0.05f; + if (new_opacity > 1.0f) + new_opacity = 1.0f; + window.set_opacity(new_opacity); + window.invalidate(); + return IterationDecision::Break; + } + + // Well okay, let's see if we're hitting the frame or the window inside the frame. + if (window.rect().contains(event.position())) { + if (event.type() == Event::MouseDown) { + if (window.type() == WindowType::Normal) + move_to_front_and_make_active(window); + else if (window.type() == WindowType::Desktop) + set_active_window(&window); + } + + hovered_window = &window; + if (!window.global_cursor_tracking() && !windows_who_received_mouse_event_due_to_cursor_tracking.contains(&window)) { + auto translated_event = event.translated(-window.position()); + deliver_mouse_event(window, translated_event); + if (event.type() == Event::MouseDown) { + m_active_input_window = window.make_weak_ptr(); + } + } + return IterationDecision::Break; + } + + // We are hitting the frame, pass the event along to WindowFrame. + window.frame().on_mouse_event(event.translated(-window_frame_rect.location())); + event_window_with_frame = &window; + return IterationDecision::Break; + }); + + // Clicked outside of any window + if (!hovered_window && !event_window_with_frame && event.type() == Event::MouseDown) + set_active_window(nullptr); + } + + if (event_window_with_frame != m_resize_candidate.ptr()) + clear_resize_candidate(); +} + +void WindowManager::clear_resize_candidate() +{ + if (m_resize_candidate) + Compositor::the().invalidate_cursor(); + m_resize_candidate = nullptr; +} + +bool WindowManager::any_opaque_window_contains_rect(const Gfx::Rect& rect) +{ + bool found_containing_window = false; + for_each_visible_window_from_back_to_front([&](Window& window) { + if (window.is_minimized()) + return IterationDecision::Continue; + if (window.opacity() < 1.0f) + return IterationDecision::Continue; + if (window.has_alpha_channel()) { + // FIXME: Just because the window has an alpha channel doesn't mean it's not opaque. + // Maybe there's some way we could know this? + return IterationDecision::Continue; + } + if (window.frame().rect().contains(rect)) { + found_containing_window = true; + return IterationDecision::Break; + } + return IterationDecision::Continue; + }); + return found_containing_window; +}; + +bool WindowManager::any_opaque_window_above_this_one_contains_rect(const Window& a_window, const Gfx::Rect& rect) +{ + bool found_containing_window = false; + bool checking = false; + for_each_visible_window_from_back_to_front([&](Window& window) { + if (&window == &a_window) { + checking = true; + return IterationDecision::Continue; + } + if (!checking) + return IterationDecision::Continue; + if (!window.is_visible()) + return IterationDecision::Continue; + if (window.is_minimized()) + return IterationDecision::Continue; + if (window.opacity() < 1.0f) + return IterationDecision::Continue; + if (window.has_alpha_channel()) + return IterationDecision::Continue; + if (window.frame().rect().contains(rect)) { + found_containing_window = true; + return IterationDecision::Break; + } + return IterationDecision::Continue; + }); + return found_containing_window; +}; + +Gfx::Rect WindowManager::menubar_rect() const +{ + if (active_fullscreen_window()) + return {}; + return MenuManager::the().menubar_rect(); +} + +Gfx::Rect WindowManager::desktop_rect() const +{ + if (active_fullscreen_window()) + return {}; + return { + 0, + menubar_rect().bottom() + 1, + Screen::the().width(), + Screen::the().height() - menubar_rect().height() - 28 + }; +} + +void WindowManager::event(Core::Event& event) +{ + if (static_cast<Event&>(event).is_mouse_event()) { + Window* hovered_window = nullptr; + process_mouse_event(static_cast<MouseEvent&>(event), hovered_window); + set_hovered_window(hovered_window); + return; + } + + if (static_cast<Event&>(event).is_key_event()) { + auto& key_event = static_cast<const KeyEvent&>(event); + m_keyboard_modifiers = key_event.modifiers(); + + if (key_event.type() == Event::KeyDown && key_event.key() == Key_Escape && m_dnd_client) { + m_dnd_client->post_message(Messages::WindowClient::DragCancelled()); + end_dnd_drag(); + return; + } + + if (key_event.key() == Key_Logo) { + if (key_event.type() == Event::KeyUp) { + if (!m_moved_or_resized_since_logo_keydown && !m_switcher.is_visible() && !m_move_window && !m_resize_window) { + MenuManager::the().toggle_system_menu(); + return; + } + + } else if (key_event.type() == Event::KeyDown) { + m_moved_or_resized_since_logo_keydown = false; + } + } + + if (MenuManager::the().current_menu()) { + MenuManager::the().dispatch_event(event); + return; + } + + if (key_event.type() == Event::KeyDown && ((key_event.modifiers() == Mod_Logo && key_event.key() == Key_Tab) || (key_event.modifiers() == (Mod_Logo | Mod_Shift) && key_event.key() == Key_Tab))) + m_switcher.show(); + if (m_switcher.is_visible()) { + m_switcher.on_key_event(key_event); + return; + } + + if (m_active_window) { + if (key_event.type() == Event::KeyDown && key_event.modifiers() == Mod_Logo) { + if (key_event.key() == Key_Down) { + m_moved_or_resized_since_logo_keydown = true; + if (m_active_window->is_resizable() && m_active_window->is_maximized()) { + m_active_window->set_maximized(false); + return; + } + if (m_active_window->is_minimizable()) + m_active_window->set_minimized(true); + return; + } + if (m_active_window->is_resizable()) { + if (key_event.key() == Key_Up) { + m_moved_or_resized_since_logo_keydown = true; + m_active_window->set_maximized(!m_active_window->is_maximized()); + return; + } + if (key_event.key() == Key_Left) { + m_moved_or_resized_since_logo_keydown = true; + if (m_active_window->tiled() != WindowTileType::None) { + m_active_window->set_tiled(WindowTileType::None); + return; + } + if (m_active_window->is_maximized()) + m_active_window->set_maximized(false); + m_active_window->set_tiled(WindowTileType::Left); + return; + } + if (key_event.key() == Key_Right) { + m_moved_or_resized_since_logo_keydown = true; + if (m_active_window->tiled() != WindowTileType::None) { + m_active_window->set_tiled(WindowTileType::None); + return; + } + if (m_active_window->is_maximized()) + m_active_window->set_maximized(false); + m_active_window->set_tiled(WindowTileType::Right); + return; + } + } + } + m_active_window->dispatch_event(event); + return; + } + } + + Core::Object::event(event); +} + +void WindowManager::set_highlight_window(Window* window) +{ + if (window == m_highlight_window) + return; + if (auto* previous_highlight_window = m_highlight_window.ptr()) + invalidate(*previous_highlight_window); + m_highlight_window = window ? window->make_weak_ptr() : nullptr; + if (m_highlight_window) + invalidate(*m_highlight_window); +} + +static bool window_type_can_become_active(WindowType type) +{ + return type == WindowType::Normal || type == WindowType::Desktop; +} + +void WindowManager::set_active_window(Window* window) +{ + if (window && window->is_blocked_by_modal_window()) + return; + + if (window && !window_type_can_become_active(window->type())) + return; + + if (window == m_active_window) + return; + + auto* previously_active_window = m_active_window.ptr(); + + ClientConnection* previously_active_client = nullptr; + ClientConnection* active_client = nullptr; + + if (previously_active_window) { + previously_active_client = previously_active_window->client(); + Core::EventLoop::current().post_event(*previously_active_window, make<Event>(Event::WindowDeactivated)); + invalidate(*previously_active_window); + m_active_window = nullptr; + m_active_input_window = nullptr; + tell_wm_listeners_window_state_changed(*previously_active_window); + } + + if (window) { + m_active_window = window->make_weak_ptr(); + active_client = m_active_window->client(); + Core::EventLoop::current().post_event(*m_active_window, make<Event>(Event::WindowActivated)); + invalidate(*m_active_window); + + auto* client = window->client(); + ASSERT(client); + MenuManager::the().set_current_menubar(client->app_menubar()); + tell_wm_listeners_window_state_changed(*m_active_window); + } else { + MenuManager::the().set_current_menubar(nullptr); + } + + if (active_client != previously_active_client) { + if (previously_active_client) + previously_active_client->deboost(); + if (active_client) + active_client->boost(); + } +} + +void WindowManager::set_hovered_window(Window* window) +{ + if (m_hovered_window == window) + return; + + if (m_hovered_window) + Core::EventLoop::current().post_event(*m_hovered_window, make<Event>(Event::WindowLeft)); + + m_hovered_window = window ? window->make_weak_ptr() : nullptr; + + if (m_hovered_window) + Core::EventLoop::current().post_event(*m_hovered_window, make<Event>(Event::WindowEntered)); +} + +void WindowManager::invalidate() +{ + Compositor::the().invalidate(); +} + +void WindowManager::invalidate(const Gfx::Rect& rect) +{ + Compositor::the().invalidate(rect); +} + +void WindowManager::invalidate(const Window& window) +{ + invalidate(window.frame().rect()); +} + +void WindowManager::invalidate(const Window& window, const Gfx::Rect& rect) +{ + if (window.type() == WindowType::MenuApplet) { + AppletManager::the().invalidate_applet(window, rect); + return; + } + + if (rect.is_empty()) { + invalidate(window); + return; + } + auto outer_rect = window.frame().rect(); + auto inner_rect = rect; + inner_rect.move_by(window.position()); + // FIXME: This seems slightly wrong; the inner rect shouldn't intersect the border part of the outer rect. + inner_rect.intersect(outer_rect); + invalidate(inner_rect); +} + +const ClientConnection* WindowManager::active_client() const +{ + if (m_active_window) + return m_active_window->client(); + return nullptr; +} + +void WindowManager::notify_client_changed_app_menubar(ClientConnection& client) +{ + if (active_client() == &client) + MenuManager::the().set_current_menubar(client.app_menubar()); +} + +const Cursor& WindowManager::active_cursor() const +{ + if (m_dnd_client) + return *m_drag_cursor; + + if (m_move_window) + return *m_move_cursor; + + if (m_resize_window || m_resize_candidate) { + switch (m_resize_direction) { + case ResizeDirection::Up: + case ResizeDirection::Down: + return *m_resize_vertically_cursor; + case ResizeDirection::Left: + case ResizeDirection::Right: + return *m_resize_horizontally_cursor; + case ResizeDirection::UpLeft: + case ResizeDirection::DownRight: + return *m_resize_diagonally_tlbr_cursor; + case ResizeDirection::UpRight: + case ResizeDirection::DownLeft: + return *m_resize_diagonally_bltr_cursor; + case ResizeDirection::None: + break; + } + } + + if (m_hovered_window && m_hovered_window->override_cursor()) + return *m_hovered_window->override_cursor(); + + return *m_arrow_cursor; +} + +void WindowManager::set_hovered_button(Button* button) +{ + m_hovered_button = button ? button->make_weak_ptr() : nullptr; +} + +void WindowManager::set_resize_candidate(Window& window, ResizeDirection direction) +{ + m_resize_candidate = window.make_weak_ptr(); + m_resize_direction = direction; +} + +ResizeDirection WindowManager::resize_direction_of_window(const Window& window) +{ + if (&window != m_resize_window) + return ResizeDirection::None; + return m_resize_direction; +} + +Gfx::Rect WindowManager::maximized_window_rect(const Window& window) const +{ + Gfx::Rect rect = Screen::the().rect(); + + // Subtract window title bar (leaving the border) + rect.set_y(rect.y() + window.frame().title_bar_rect().height()); + rect.set_height(rect.height() - window.frame().title_bar_rect().height()); + + // Subtract menu bar + rect.set_y(rect.y() + menubar_rect().height()); + rect.set_height(rect.height() - menubar_rect().height()); + + // Subtract taskbar window height if present + const_cast<WindowManager*>(this)->for_each_visible_window_of_type_from_back_to_front(WindowType::Taskbar, [&rect](Window& taskbar_window) { + rect.set_height(rect.height() - taskbar_window.height()); + return IterationDecision::Break; + }); + + return rect; +} + +void WindowManager::start_dnd_drag(ClientConnection& client, const String& text, Gfx::Bitmap* bitmap, const String& data_type, const String& data) +{ + ASSERT(!m_dnd_client); + m_dnd_client = client.make_weak_ptr(); + m_dnd_text = text; + m_dnd_bitmap = bitmap; + m_dnd_data_type = data_type; + m_dnd_data = data; + Compositor::the().invalidate_cursor(); + m_active_input_window = nullptr; +} + +void WindowManager::end_dnd_drag() +{ + ASSERT(m_dnd_client); + Compositor::the().invalidate_cursor(); + m_dnd_client = nullptr; + m_dnd_text = {}; + m_dnd_bitmap = nullptr; +} + +Gfx::Rect WindowManager::dnd_rect() const +{ + int bitmap_width = m_dnd_bitmap ? m_dnd_bitmap->width() : 0; + int bitmap_height = m_dnd_bitmap ? m_dnd_bitmap->height() : 0; + int width = font().width(m_dnd_text) + bitmap_width; + int height = max((int)font().glyph_height(), bitmap_height); + auto location = Compositor::the().current_cursor_rect().center().translated(8, 8); + return Gfx::Rect(location, { width, height }).inflated(4, 4); +} + +bool WindowManager::update_theme(String theme_path, String theme_name) +{ + auto new_theme = Gfx::load_system_theme(theme_path); + if (!new_theme) + return false; + ASSERT(new_theme); + Gfx::set_system_theme(*new_theme); + m_palette = Gfx::PaletteImpl::create_with_shared_buffer(*new_theme); + Compositor::the().set_background_color(palette().desktop_background().to_string()); + HashTable<ClientConnection*> notified_clients; + for_each_window([&](Window& window) { + if (window.client()) { + if (!notified_clients.contains(window.client())) { + window.client()->post_message(Messages::WindowClient::UpdateSystemTheme(Gfx::current_system_theme_buffer_id())); + notified_clients.set(window.client()); + } + } + return IterationDecision::Continue; + }); + MenuManager::the().did_change_theme(); + auto wm_config = Core::ConfigFile::open("/etc/WindowServer/WindowServer.ini"); + wm_config->write_entry("Theme", "Name", theme_name); + wm_config->sync(); + invalidate(); + return true; +} + +} diff --git a/Services/WindowServer/WindowManager.h b/Services/WindowServer/WindowManager.h new file mode 100644 index 0000000000..0c7ec8429f --- /dev/null +++ b/Services/WindowServer/WindowManager.h @@ -0,0 +1,416 @@ +/* + * Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org> + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#pragma once + +#include <AK/HashMap.h> +#include <AK/HashTable.h> +#include <AK/InlineLinkedList.h> +#include <AK/WeakPtr.h> +#include <LibCore/ConfigFile.h> +#include <LibCore/ElapsedTimer.h> +#include <LibGfx/Color.h> +#include <LibGfx/DisjointRectSet.h> +#include <LibGfx/Painter.h> +#include <LibGfx/Palette.h> +#include <LibGfx/Rect.h> +#include <WindowServer/Cursor.h> +#include <WindowServer/Event.h> +#include <WindowServer/MenuBar.h> +#include <WindowServer/MenuManager.h> +#include <WindowServer/Window.h> +#include <WindowServer/WindowSwitcher.h> +#include <WindowServer/WindowType.h> + +namespace WindowServer { + +class Screen; +class MouseEvent; +class Window; +class ClientConnection; +class WindowSwitcher; +class Button; + +enum class ResizeDirection { + None, + Left, + UpLeft, + Up, + UpRight, + Right, + DownRight, + Down, + DownLeft +}; + +class WindowManager : public Core::Object { + C_OBJECT(WindowManager) + + friend class Compositor; + friend class WindowFrame; + friend class WindowSwitcher; + +public: + static WindowManager& the(); + + explicit WindowManager(const Gfx::PaletteImpl&); + virtual ~WindowManager() override; + + Palette palette() const { return Palette(*m_palette); } + + RefPtr<Core::ConfigFile> wm_config() const + { + return m_wm_config; + } + void reload_config(bool); + + void add_window(Window&); + void remove_window(Window&); + + void notify_title_changed(Window&); + void notify_rect_changed(Window&, const Gfx::Rect& oldRect, const Gfx::Rect& newRect); + void notify_minimization_state_changed(Window&); + void notify_opacity_changed(Window&); + void notify_occlusion_state_changed(Window&); + void notify_client_changed_app_menubar(ClientConnection&); + + Gfx::Rect maximized_window_rect(const Window&) const; + + ClientConnection* dnd_client() { return m_dnd_client.ptr(); } + const String& dnd_text() const { return m_dnd_text; } + const String& dnd_data_type() const { return m_dnd_data_type; } + const String& dnd_data() const { return m_dnd_data; } + const Gfx::Bitmap* dnd_bitmap() const { return m_dnd_bitmap; } + Gfx::Rect dnd_rect() const; + + void start_dnd_drag(ClientConnection&, const String& text, Gfx::Bitmap*, const String& data_type, const String& data); + void end_dnd_drag(); + + Window* active_window() { return m_active_window.ptr(); } + const ClientConnection* active_client() const; + bool active_window_is_modal() const { return m_active_window && m_active_window->is_modal(); } + + Window* highlight_window() { return m_highlight_window.ptr(); } + void set_highlight_window(Window*); + + void move_to_front_and_make_active(Window&); + + Gfx::Rect menubar_rect() const; + Gfx::Rect desktop_rect() const; + + const Cursor& active_cursor() const; + const Cursor& arrow_cursor() const { return *m_arrow_cursor; } + const Cursor& hand_cursor() const { return *m_hand_cursor; } + const Cursor& resize_horizontally_cursor() const { return *m_resize_horizontally_cursor; } + const Cursor& resize_vertically_cursor() const { return *m_resize_vertically_cursor; } + const Cursor& resize_diagonally_tlbr_cursor() const { return *m_resize_diagonally_tlbr_cursor; } + const Cursor& resize_diagonally_bltr_cursor() const { return *m_resize_diagonally_bltr_cursor; } + const Cursor& i_beam_cursor() const { return *m_i_beam_cursor; } + const Cursor& disallowed_cursor() const { return *m_disallowed_cursor; } + const Cursor& move_cursor() const { return *m_move_cursor; } + const Cursor& drag_cursor() const { return *m_drag_cursor; } + + void invalidate(const Window&); + void invalidate(const Window&, const Gfx::Rect&); + void invalidate(const Gfx::Rect&); + void invalidate(); + void flush(const Gfx::Rect&); + + const Gfx::Font& font() const; + const Gfx::Font& window_title_font() const; + + bool set_resolution(int width, int height); + Gfx::Size resolution() const; + + void set_active_window(Window*); + void set_hovered_button(Button*); + + Button* cursor_tracking_button() { return m_cursor_tracking_button.ptr(); } + void set_cursor_tracking_button(Button*); + + void set_resize_candidate(Window&, ResizeDirection); + void clear_resize_candidate(); + ResizeDirection resize_direction_of_window(const Window&); + + bool any_opaque_window_contains_rect(const Gfx::Rect&); + bool any_opaque_window_above_this_one_contains_rect(const Window&, const Gfx::Rect&); + + void tell_wm_listeners_window_state_changed(Window&); + void tell_wm_listeners_window_icon_changed(Window&); + void tell_wm_listeners_window_rect_changed(Window&); + + void start_window_resize(Window&, const Gfx::Point&, MouseButton); + void start_window_resize(Window&, const MouseEvent&); + + const Window* active_fullscreen_window() const { return (m_active_window && m_active_window->is_fullscreen()) ? m_active_window : nullptr; } + Window* active_fullscreen_window() { return (m_active_window && m_active_window->is_fullscreen()) ? m_active_window : nullptr; } + + bool update_theme(String theme_path, String theme_name); + + void set_hovered_window(Window*); + void deliver_mouse_event(Window& window, MouseEvent& event); + +private: + NonnullRefPtr<Cursor> get_cursor(const String& name); + NonnullRefPtr<Cursor> get_cursor(const String& name, const Gfx::Point& hotspot); + + void process_mouse_event(MouseEvent&, Window*& hovered_window); + void process_event_for_doubleclick(Window& window, MouseEvent& event); + bool process_ongoing_window_resize(const MouseEvent&, Window*& hovered_window); + bool process_ongoing_window_move(MouseEvent&, Window*& hovered_window); + bool process_ongoing_drag(MouseEvent&, Window*& hovered_window); + void start_window_move(Window&, const MouseEvent&); + template<typename Callback> + IterationDecision for_each_visible_window_of_type_from_back_to_front(WindowType, Callback, bool ignore_highlight = false); + template<typename Callback> + IterationDecision for_each_visible_window_of_type_from_front_to_back(WindowType, Callback, bool ignore_highlight = false); + template<typename Callback> + IterationDecision for_each_visible_window_from_front_to_back(Callback); + template<typename Callback> + IterationDecision for_each_visible_window_from_back_to_front(Callback); + template<typename Callback> + void for_each_window_listening_to_wm_events(Callback); + template<typename Callback> + void for_each_window(Callback); + template<typename Callback> + IterationDecision for_each_window_of_type_from_front_to_back(WindowType, Callback, bool ignore_highlight = false); + + virtual void event(Core::Event&) override; + void paint_window_frame(const Window&); + void tell_wm_listener_about_window(Window& listener, Window&); + void tell_wm_listener_about_window_icon(Window& listener, Window&); + void tell_wm_listener_about_window_rect(Window& listener, Window&); + void pick_new_active_window(); + + void recompute_occlusions(); + + RefPtr<Cursor> m_arrow_cursor; + RefPtr<Cursor> m_hand_cursor; + RefPtr<Cursor> m_resize_horizontally_cursor; + RefPtr<Cursor> m_resize_vertically_cursor; + RefPtr<Cursor> m_resize_diagonally_tlbr_cursor; + RefPtr<Cursor> m_resize_diagonally_bltr_cursor; + RefPtr<Cursor> m_i_beam_cursor; + RefPtr<Cursor> m_disallowed_cursor; + RefPtr<Cursor> m_move_cursor; + RefPtr<Cursor> m_drag_cursor; + + InlineLinkedList<Window> m_windows_in_order; + + struct DoubleClickInfo { + struct ClickMetadata { + Core::ElapsedTimer clock; + Gfx::Point last_position; + }; + + ClickMetadata& metadata_for_button(MouseButton); + + void reset() + { + m_left = {}; + m_right = {}; + m_middle = {}; + m_back = {}; + m_forward = {}; + } + + WeakPtr<Window> m_clicked_window; + + private: + ClickMetadata m_left; + ClickMetadata m_right; + ClickMetadata m_middle; + ClickMetadata m_back; + ClickMetadata m_forward; + }; + DoubleClickInfo m_double_click_info; + int m_double_click_speed { 0 }; + int m_max_distance_for_double_click { 4 }; + + WeakPtr<Window> m_active_window; + WeakPtr<Window> m_hovered_window; + WeakPtr<Window> m_highlight_window; + WeakPtr<Window> m_active_input_window; + + WeakPtr<Window> m_move_window; + Gfx::Point m_move_origin; + Gfx::Point m_move_window_origin; + + WeakPtr<Window> m_resize_window; + WeakPtr<Window> m_resize_candidate; + MouseButton m_resizing_mouse_button { MouseButton::None }; + Gfx::Rect m_resize_window_original_rect; + Gfx::Point m_resize_origin; + ResizeDirection m_resize_direction { ResizeDirection::None }; + + bool m_moved_or_resized_since_logo_keydown { false }; + + u8 m_keyboard_modifiers { 0 }; + + WindowSwitcher m_switcher; + + WeakPtr<Button> m_cursor_tracking_button; + WeakPtr<Button> m_hovered_button; + + NonnullRefPtr<Gfx::PaletteImpl> m_palette; + + RefPtr<Core::ConfigFile> m_wm_config; + + WeakPtr<ClientConnection> m_dnd_client; + String m_dnd_text; + String m_dnd_data_type; + String m_dnd_data; + RefPtr<Gfx::Bitmap> m_dnd_bitmap; +}; + +template<typename Callback> +IterationDecision WindowManager::for_each_visible_window_of_type_from_back_to_front(WindowType type, Callback callback, bool ignore_highlight) +{ + bool do_highlight_window_at_end = false; + for (auto& window : m_windows_in_order) { + if (!window.is_visible()) + continue; + if (window.is_minimized()) + continue; + if (window.type() != type) + continue; + if (!ignore_highlight && m_highlight_window == &window) { + do_highlight_window_at_end = true; + continue; + } + if (callback(window) == IterationDecision::Break) + return IterationDecision::Break; + } + if (do_highlight_window_at_end) { + if (callback(*m_highlight_window) == IterationDecision::Break) + return IterationDecision::Break; + } + return IterationDecision::Continue; +} + +template<typename Callback> +IterationDecision WindowManager::for_each_visible_window_from_back_to_front(Callback callback) +{ + if (for_each_visible_window_of_type_from_back_to_front(WindowType::Desktop, callback) == IterationDecision::Break) + return IterationDecision::Break; + if (for_each_visible_window_of_type_from_back_to_front(WindowType::Normal, callback) == IterationDecision::Break) + return IterationDecision::Break; + if (for_each_visible_window_of_type_from_back_to_front(WindowType::Taskbar, callback) == IterationDecision::Break) + return IterationDecision::Break; + if (for_each_visible_window_of_type_from_back_to_front(WindowType::Tooltip, callback) == IterationDecision::Break) + return IterationDecision::Break; + if (for_each_visible_window_of_type_from_back_to_front(WindowType::Notification, callback) == IterationDecision::Break) + return IterationDecision::Break; + if (for_each_visible_window_of_type_from_back_to_front(WindowType::Menubar, callback) == IterationDecision::Break) + return IterationDecision::Break; + if (for_each_visible_window_of_type_from_back_to_front(WindowType::Menu, callback) == IterationDecision::Break) + return IterationDecision::Break; + return for_each_visible_window_of_type_from_back_to_front(WindowType::WindowSwitcher, callback); +} + +template<typename Callback> +IterationDecision WindowManager::for_each_visible_window_of_type_from_front_to_back(WindowType type, Callback callback, bool ignore_highlight) +{ + if (!ignore_highlight && m_highlight_window && m_highlight_window->type() == type && m_highlight_window->is_visible()) { + if (callback(*m_highlight_window) == IterationDecision::Break) + return IterationDecision::Break; + } + + for (auto* window = m_windows_in_order.tail(); window; window = window->prev()) { + if (!window->is_visible()) + continue; + if (window->is_minimized()) + continue; + if (window->type() != type) + continue; + if (!ignore_highlight && window == m_highlight_window) + continue; + if (callback(*window) == IterationDecision::Break) + return IterationDecision::Break; + } + return IterationDecision::Continue; +} + +template<typename Callback> +IterationDecision WindowManager::for_each_visible_window_from_front_to_back(Callback callback) +{ + if (for_each_visible_window_of_type_from_front_to_back(WindowType::WindowSwitcher, callback) == IterationDecision::Break) + return IterationDecision::Break; + if (for_each_visible_window_of_type_from_front_to_back(WindowType::Menu, callback) == IterationDecision::Break) + return IterationDecision::Break; + if (for_each_visible_window_of_type_from_front_to_back(WindowType::Menubar, callback) == IterationDecision::Break) + return IterationDecision::Break; + if (for_each_visible_window_of_type_from_front_to_back(WindowType::Notification, callback) == IterationDecision::Break) + return IterationDecision::Break; + if (for_each_visible_window_of_type_from_front_to_back(WindowType::Tooltip, callback) == IterationDecision::Break) + return IterationDecision::Break; + if (for_each_visible_window_of_type_from_front_to_back(WindowType::Taskbar, callback) == IterationDecision::Break) + return IterationDecision::Break; + if (for_each_visible_window_of_type_from_front_to_back(WindowType::Normal, callback) == IterationDecision::Break) + return IterationDecision::Break; + return for_each_visible_window_of_type_from_front_to_back(WindowType::Desktop, callback); +} + +template<typename Callback> +void WindowManager::for_each_window_listening_to_wm_events(Callback callback) +{ + for (auto* window = m_windows_in_order.tail(); window; window = window->prev()) { + if (!window->listens_to_wm_events()) + continue; + if (callback(*window) == IterationDecision::Break) + return; + } +} + +template<typename Callback> +void WindowManager::for_each_window(Callback callback) +{ + for (auto* window = m_windows_in_order.tail(); window; window = window->prev()) { + if (callback(*window) == IterationDecision::Break) + return; + } +} + +template<typename Callback> +IterationDecision WindowManager::for_each_window_of_type_from_front_to_back(WindowType type, Callback callback, bool ignore_highlight) +{ + if (!ignore_highlight && m_highlight_window && m_highlight_window->type() == type && m_highlight_window->is_visible()) { + if (callback(*m_highlight_window) == IterationDecision::Break) + return IterationDecision::Break; + } + + for (auto* window = m_windows_in_order.tail(); window; window = window->prev()) { + if (window->type() != type) + continue; + if (!ignore_highlight && window == m_highlight_window) + continue; + if (callback(*window) == IterationDecision::Break) + return IterationDecision::Break; + } + return IterationDecision::Continue; +} + +} diff --git a/Services/WindowServer/WindowServer.ipc b/Services/WindowServer/WindowServer.ipc new file mode 100644 index 0000000000..214afa7567 --- /dev/null +++ b/Services/WindowServer/WindowServer.ipc @@ -0,0 +1,97 @@ +endpoint WindowServer = 2 +{ + Greet() => (i32 client_id, Gfx::Rect screen_rect, i32 system_theme_buffer_id) + + CreateMenubar() => (i32 menubar_id) + DestroyMenubar(i32 menubar_id) => () + + CreateMenu(String menu_title) => (i32 menu_id) + DestroyMenu(i32 menu_id) => () + + AddMenuToMenubar(i32 menubar_id, i32 menu_id) => () + SetApplicationMenubar(i32 menubar_id) => () + + SetSystemMenu(i32 menu_id) => () + + AddMenuItem( + i32 menu_id, + i32 identifier, + i32 submenu_id, + String text, + bool enabled, + bool checkable, + bool checked, + String shortcut, + i32 icon_buffer_id, + bool exclusive) => () + + AddMenuSeparator(i32 menu_id) => () + + UpdateMenuItem(i32 menu_id, i32 identifier, i32 submenu_id, String text, bool enabled, bool checkable, bool checked, String shortcut) => () + + CreateWindow( + Gfx::Rect rect, + bool has_alpha_channel, + bool modal, + bool minimizable, + bool resizable, + bool fullscreen, + bool frameless, + float opacity, + Gfx::Size base_size, + Gfx::Size size_increment, + i32 type, + String title, + i32 parent_window_id) => (i32 window_id) + + DestroyWindow(i32 window_id) => (Vector<i32> destroyed_window_ids) + + SetWindowTitle(i32 window_id, String title) => () + GetWindowTitle(i32 window_id) => (String title) + + SetWindowRect(i32 window_id, Gfx::Rect rect) => (Gfx::Rect rect) + GetWindowRect(i32 window_id) => (Gfx::Rect rect) + + InvalidateRect(i32 window_id, Vector<Gfx::Rect> rects, bool ignore_occlusion) =| + DidFinishPainting(i32 window_id, Vector<Gfx::Rect> rects) =| + + SetGlobalCursorTracking(i32 window_id, bool enabled) => () + SetWindowOpacity(i32 window_id, float opacity) => () + + SetWindowBackingStore(i32 window_id, i32 bpp, i32 pitch, i32 shbuf_id, bool has_alpha_channel, Gfx::Size size, bool flush_immediately) => () + GetClipboardContents() => (i32 shbuf_id, i32 content_size, String content_type) + SetClipboardContents(i32 shbuf_id, i32 content_size, String content_type) => () + + WM_SetActiveWindow(i32 client_id, i32 window_id) =| + WM_SetWindowMinimized(i32 client_id, i32 window_id, bool minimized) =| + WM_StartWindowResize(i32 client_id, i32 window_id) =| + WM_PopupWindowMenu(i32 client_id, i32 window_id, Gfx::Point screen_position) =| + WM_SetWindowTaskbarRect(i32 client_id, i32 window_id, Gfx::Rect rect) =| + + SetWindowHasAlphaChannel(i32 window_id, bool has_alpha_channel) => () + MoveWindowToFront(i32 window_id) => () + SetFullscreen(i32 window_id, bool fullscreen) => () + PopupMenu(i32 menu_id, Gfx::Point screen_position) => () + DismissMenu(i32 menu_id) => () + + AsyncSetWallpaper(String path) =| + + SetBackgroundColor(String background_color) => () + SetWallpaperMode(String mode) => () + + SetResolution(Gfx::Size resolution) => (bool success, Gfx::Size resolution) + SetWindowIconBitmap(i32 window_id, Gfx::ShareableBitmap icon) => () + + GetWallpaper() => (String path) + SetWindowOverrideCursor(i32 window_id, i32 cursor_type) => () + + StartDrag(String text, String data_type, String data, i32 bitmap_id, Gfx::Size bitmap_size) => (bool started) + + SetSystemTheme(String theme_path, String theme_name) => (bool success) + GetSystemTheme() => (String theme_name) + + SetWindowBaseSizeAndSizeIncrement(i32 window_id, Gfx::Size base_size, Gfx::Size size_increment) => () + + EnableDisplayLink() =| + DisableDisplayLink() =| +} diff --git a/Services/WindowServer/WindowSwitcher.cpp b/Services/WindowServer/WindowSwitcher.cpp new file mode 100644 index 0000000000..fbf53184da --- /dev/null +++ b/Services/WindowServer/WindowSwitcher.cpp @@ -0,0 +1,256 @@ +/* + * Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org> + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#include <LibGfx/Bitmap.h> +#include <LibGfx/Font.h> +#include <LibGfx/StylePainter.h> +#include <WindowServer/Event.h> +#include <WindowServer/Screen.h> +#include <WindowServer/WindowManager.h> +#include <WindowServer/WindowSwitcher.h> + +namespace WindowServer { + +static WindowSwitcher* s_the; + +WindowSwitcher& WindowSwitcher::the() +{ + ASSERT(s_the); + return *s_the; +} + +WindowSwitcher::WindowSwitcher() +{ + s_the = this; +} + +WindowSwitcher::~WindowSwitcher() +{ +} + +void WindowSwitcher::set_visible(bool visible) +{ + if (m_visible == visible) + return; + m_visible = visible; + WindowManager::the().recompute_occlusions(); + if (m_switcher_window) + m_switcher_window->set_visible(visible); + if (!m_visible) + return; + refresh(); +} + +Window* WindowSwitcher::selected_window() +{ + if (m_selected_index < 0 || m_selected_index >= static_cast<int>(m_windows.size())) + return nullptr; + return m_windows[m_selected_index].ptr(); +} + +void WindowSwitcher::event(Core::Event& event) +{ + if (!static_cast<Event&>(event).is_mouse_event()) + return; + + auto& mouse_event = static_cast<MouseEvent&>(event); + int new_hovered_index = -1; + for (size_t i = 0; i < m_windows.size(); ++i) { + auto item_rect = this->item_rect(i); + if (item_rect.contains(mouse_event.position())) { + new_hovered_index = i; + break; + } + } + + if (mouse_event.type() == Event::MouseMove) { + if (m_hovered_index != new_hovered_index) { + m_hovered_index = new_hovered_index; + redraw(); + } + } + + if (new_hovered_index == -1) + return; + + if (mouse_event.type() == Event::MouseDown) + select_window_at_index(new_hovered_index); + + event.accept(); +} + +void WindowSwitcher::on_key_event(const KeyEvent& event) +{ + if (event.type() == Event::KeyUp) { + if (event.key() == Key_Logo) { + if (auto* window = selected_window()) { + window->set_minimized(false); + WindowManager::the().move_to_front_and_make_active(*window); + } + WindowManager::the().set_highlight_window(nullptr); + hide(); + } + return; + } + + if (event.key() == Key_LeftShift || event.key() == Key_RightShift) + return; + if (event.key() != Key_Tab) { + WindowManager::the().set_highlight_window(nullptr); + hide(); + return; + } + ASSERT(!m_windows.is_empty()); + + int new_selected_index; + + if (!event.shift()) { + new_selected_index = (m_selected_index + 1) % static_cast<int>(m_windows.size()); + } else { + new_selected_index = (m_selected_index - 1) % static_cast<int>(m_windows.size()); + if (new_selected_index < 0) + new_selected_index = static_cast<int>(m_windows.size()) - 1; + } + ASSERT(new_selected_index < static_cast<int>(m_windows.size())); + + select_window_at_index(new_selected_index); +} + +void WindowSwitcher::select_window(Window& window) +{ + for (size_t i = 0; i < m_windows.size(); ++i) { + if (m_windows.at(i) == &window) { + select_window_at_index(i); + return; + } + } +} + +void WindowSwitcher::select_window_at_index(int index) +{ + m_selected_index = index; + auto* highlight_window = m_windows.at(index).ptr(); + ASSERT(highlight_window); + WindowManager::the().set_highlight_window(highlight_window); + redraw(); +} + +void WindowSwitcher::redraw() +{ + draw(); + WindowManager::the().invalidate(m_rect); +} + +Gfx::Rect WindowSwitcher::item_rect(int index) const +{ + return { + padding(), + padding() + index * item_height(), + m_rect.width() - padding() * 2, + item_height() + }; +} + +void WindowSwitcher::draw() +{ + auto palette = WindowManager::the().palette(); + Gfx::Painter painter(*m_switcher_window->backing_store()); + painter.fill_rect({ {}, m_rect.size() }, palette.window()); + painter.draw_rect({ {}, m_rect.size() }, palette.threed_shadow2()); + for (size_t index = 0; index < m_windows.size(); ++index) { + auto& window = *m_windows.at(index); + auto item_rect = this->item_rect(index); + Color text_color; + Color rect_text_color; + if (static_cast<int>(index) == m_selected_index) { + painter.fill_rect(item_rect, palette.selection()); + text_color = palette.selection_text(); + rect_text_color = palette.threed_shadow1(); + } else { + if (static_cast<int>(index) == m_hovered_index) + Gfx::StylePainter::paint_button(painter, item_rect, palette, Gfx::ButtonStyle::CoolBar, false, true); + text_color = palette.window_text(); + rect_text_color = palette.threed_shadow2(); + } + item_rect.shrink(item_padding(), 0); + Gfx::Rect thumbnail_rect = { item_rect.location().translated(0, 5), { thumbnail_width(), thumbnail_height() } }; + if (window.backing_store()) { + painter.draw_scaled_bitmap(thumbnail_rect, *window.backing_store(), window.backing_store()->rect()); + Gfx::StylePainter::paint_frame(painter, thumbnail_rect.inflated(4, 4), palette, Gfx::FrameShape::Container, Gfx::FrameShadow::Sunken, 2); + } + Gfx::Rect icon_rect = { thumbnail_rect.bottom_right().translated(-window.icon().width(), -window.icon().height()), { window.icon().width(), window.icon().height() } }; + painter.fill_rect(icon_rect, palette.window()); + painter.blit(icon_rect.location(), window.icon(), window.icon().rect()); + painter.draw_text(item_rect.translated(thumbnail_width() + 12, 0), window.title(), WindowManager::the().window_title_font(), Gfx::TextAlignment::CenterLeft, text_color); + painter.draw_text(item_rect, window.rect().to_string(), Gfx::TextAlignment::CenterRight, rect_text_color); + } +} + +void WindowSwitcher::refresh() +{ + auto& wm = WindowManager::the(); + Window* selected_window = nullptr; + if (m_selected_index > 0 && m_windows[m_selected_index]) + selected_window = m_windows[m_selected_index].ptr(); + if (!selected_window) + selected_window = wm.highlight_window() ? wm.highlight_window() : wm.active_window(); + m_windows.clear(); + m_selected_index = 0; + int window_count = 0; + int longest_title_width = 0; + wm.for_each_window_of_type_from_front_to_back( + WindowType::Normal, [&](Window& window) { + if (window.is_frameless()) + return IterationDecision::Continue; + ++window_count; + longest_title_width = max(longest_title_width, wm.font().width(window.title())); + if (selected_window == &window) + m_selected_index = m_windows.size(); + m_windows.append(window.make_weak_ptr()); + return IterationDecision::Continue; + }, + true); + if (m_windows.is_empty()) { + hide(); + return; + } + int space_for_window_rect = 180; + m_rect.set_width(thumbnail_width() + longest_title_width + space_for_window_rect + padding() * 2 + item_padding() * 2); + m_rect.set_height(window_count * item_height() + padding() * 2); + m_rect.center_within(Screen::the().rect()); + if (!m_switcher_window) + m_switcher_window = Window::construct(*this, WindowType::WindowSwitcher); + m_switcher_window->set_rect(m_rect); + redraw(); +} + +void WindowSwitcher::refresh_if_needed() +{ + if (m_visible) + refresh(); +} + +} diff --git a/Services/WindowServer/WindowSwitcher.h b/Services/WindowServer/WindowSwitcher.h new file mode 100644 index 0000000000..79fb3336a5 --- /dev/null +++ b/Services/WindowServer/WindowSwitcher.h @@ -0,0 +1,84 @@ +/* + * Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org> + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#pragma once + +#include <AK/Vector.h> +#include <AK/WeakPtr.h> +#include <LibCore/Object.h> +#include <LibGfx/Forward.h> +#include <LibGfx/Rect.h> + +namespace WindowServer { + +class KeyEvent; +class Window; + +class WindowSwitcher final : public Core::Object { + C_OBJECT(WindowSwitcher) +public: + static WindowSwitcher& the(); + + WindowSwitcher(); + virtual ~WindowSwitcher() override; + + bool is_visible() const { return m_visible; } + void set_visible(bool); + + void show() { set_visible(true); } + void hide() { set_visible(false); } + + void on_key_event(const KeyEvent&); + + void refresh(); + void refresh_if_needed(); + + void select_window(Window&); + +private: + int thumbnail_width() const { return 40; } + int thumbnail_height() const { return 40; } + int item_height() const { return 10 + thumbnail_height(); } + int padding() const { return 8; } + int item_padding() const { return 8; } + + void draw(); + void redraw(); + void select_window_at_index(int index); + Gfx::Rect item_rect(int index) const; + Window* selected_window(); + + virtual void event(Core::Event&) override; + + RefPtr<Window> m_switcher_window; + Gfx::Rect m_rect; + bool m_visible { false }; + Vector<WeakPtr<Window>> m_windows; + int m_selected_index { 0 }; + int m_hovered_index { -1 }; +}; + +} diff --git a/Services/WindowServer/WindowType.h b/Services/WindowServer/WindowType.h new file mode 100644 index 0000000000..a3c53007a1 --- /dev/null +++ b/Services/WindowServer/WindowType.h @@ -0,0 +1,41 @@ +/* + * Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org> + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#pragma once + +// Keep this in sync with GUI::WindowType. +enum class WindowType { + Invalid = 0, + Normal, + Menu, + WindowSwitcher, + Taskbar, + Tooltip, + Menubar, + MenuApplet, + Notification, + Desktop, +}; diff --git a/Services/WindowServer/main.cpp b/Services/WindowServer/main.cpp new file mode 100644 index 0000000000..9165a5cf8f --- /dev/null +++ b/Services/WindowServer/main.cpp @@ -0,0 +1,117 @@ +/* + * Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org> + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#include "AppletManager.h" +#include "Compositor.h" +#include "EventLoop.h" +#include "Screen.h" +#include "WindowManager.h" +#include <AK/SharedBuffer.h> +#include <LibCore/ConfigFile.h> +#include <LibGfx/Palette.h> +#include <LibGfx/SystemTheme.h> +#include <signal.h> +#include <stdio.h> +#include <string.h> + +int main(int, char**) +{ + if (pledge("stdio video thread shared_buffer accept rpath wpath cpath unix proc fattr", nullptr) < 0) { + perror("pledge"); + return 1; + } + + if (unveil("/res", "r") < 0) { + perror("unveil"); + return 1; + } + + if (unveil("/tmp", "cw") < 0) { + perror("unveil"); + return 1; + } + + if (unveil("/etc/WindowServer/WindowServer.ini", "rwc") < 0) { + perror("unveil"); + return 1; + } + + if (unveil("/dev", "rw") < 0) { + perror("unveil"); + return 1; + } + + struct sigaction act; + memset(&act, 0, sizeof(act)); + act.sa_flags = SA_NOCLDWAIT; + act.sa_handler = SIG_IGN; + int rc = sigaction(SIGCHLD, &act, nullptr); + if (rc < 0) { + perror("sigaction"); + return 1; + } + + auto wm_config = Core::ConfigFile::open("/etc/WindowServer/WindowServer.ini"); + auto theme_name = wm_config->read_entry("Theme", "Name", "Default"); + + auto theme = Gfx::load_system_theme(String::format("/res/themes/%s.ini", theme_name.characters())); + ASSERT(theme); + Gfx::set_system_theme(*theme); + auto palette = Gfx::PaletteImpl::create_with_shared_buffer(*theme); + + WindowServer::EventLoop loop; + + if (pledge("stdio video thread shared_buffer accept rpath wpath cpath proc", nullptr) < 0) { + perror("pledge"); + return 1; + } + + WindowServer::Screen screen(wm_config->read_num_entry("Screen", "Width", 1024), + wm_config->read_num_entry("Screen", "Height", 768)); + WindowServer::Compositor::the(); + auto wm = WindowServer::WindowManager::construct(*palette); + auto am = WindowServer::AppletManager::construct(); + auto mm = WindowServer::MenuManager::construct(); + + if (unveil("/tmp", "") < 0) { + perror("unveil"); + return 1; + } + + if (unveil("/dev", "") < 0) { + perror("unveil"); + return 1; + } + + if (unveil(nullptr, nullptr) < 0) { + perror("unveil"); + return 1; + } + + dbgprintf("Entering WindowServer main loop.\n"); + loop.exec(); + ASSERT_NOT_REACHED(); +} |