/ src / Ryujinx.HLE / HOS / Services / ServerBase.cs
ServerBase.cs
  1  using Ryujinx.Common;
  2  using Ryujinx.Common.Logging;
  3  using Ryujinx.Common.Memory;
  4  using Ryujinx.HLE.HOS.Ipc;
  5  using Ryujinx.HLE.HOS.Kernel;
  6  using Ryujinx.HLE.HOS.Kernel.Ipc;
  7  using Ryujinx.HLE.HOS.Kernel.Process;
  8  using Ryujinx.HLE.HOS.Kernel.Threading;
  9  using Ryujinx.Horizon;
 10  using Ryujinx.Horizon.Common;
 11  using System;
 12  using System.Buffers;
 13  using System.Buffers.Binary;
 14  using System.Collections.Generic;
 15  using System.IO;
 16  using System.Threading;
 17  
 18  namespace Ryujinx.HLE.HOS.Services
 19  {
 20      class ServerBase : IDisposable
 21      {
 22          // Must be the maximum value used by services (highest one know is the one used by nvservices = 0x8000).
 23          // Having a size that is too low will cause failures as data copy will fail if the receiving buffer is
 24          // not large enough.
 25          private const int PointerBufferSize = 0x8000;
 26  
 27          private readonly static uint[] _defaultCapabilities = {
 28              0x030363F7,
 29              0x1FFFFFCF,
 30              0x207FFFEF,
 31              0x47E0060F,
 32              0x0048BFFF,
 33              0x01007FFF,
 34          };
 35  
 36          // The amount of time Dispose() will wait to Join() the thread executing the ServerLoop()
 37          private static readonly TimeSpan _threadJoinTimeout = TimeSpan.FromSeconds(3);
 38  
 39          private readonly KernelContext _context;
 40          private KProcess _selfProcess;
 41          private KThread _selfThread;
 42          private KEvent _wakeEvent;
 43          private int _wakeHandle = 0;
 44  
 45          private readonly ReaderWriterLockSlim _handleLock = new();
 46          private readonly Dictionary<int, IpcService> _sessions = new();
 47          private readonly Dictionary<int, Func<IpcService>> _ports = new();
 48  
 49          private readonly MemoryStream _requestDataStream;
 50          private readonly BinaryReader _requestDataReader;
 51  
 52          private readonly MemoryStream _responseDataStream;
 53          private readonly BinaryWriter _responseDataWriter;
 54  
 55          private int _isDisposed = 0;
 56  
 57          public ManualResetEvent InitDone { get; }
 58          public string Name { get; }
 59          public Func<IpcService> SmObjectFactory { get; }
 60  
 61          public ServerBase(KernelContext context, string name, Func<IpcService> smObjectFactory = null)
 62          {
 63              _context = context;
 64  
 65              _requestDataStream = MemoryStreamManager.Shared.GetStream();
 66              _requestDataReader = new BinaryReader(_requestDataStream);
 67  
 68              _responseDataStream = MemoryStreamManager.Shared.GetStream();
 69              _responseDataWriter = new BinaryWriter(_responseDataStream);
 70  
 71              InitDone = new ManualResetEvent(false);
 72              Name = name;
 73              SmObjectFactory = smObjectFactory;
 74  
 75              const ProcessCreationFlags Flags =
 76                  ProcessCreationFlags.EnableAslr |
 77                  ProcessCreationFlags.AddressSpace64Bit |
 78                  ProcessCreationFlags.Is64Bit |
 79                  ProcessCreationFlags.PoolPartitionSystem;
 80  
 81              ProcessCreationInfo creationInfo = new("Service", 1, 0, 0x8000000, 1, Flags, 0, 0);
 82  
 83              KernelStatic.StartInitialProcess(context, creationInfo, _defaultCapabilities, 44, Main);
 84          }
 85  
 86          private void AddPort(int serverPortHandle, Func<IpcService> objectFactory)
 87          {
 88              bool lockTaken = false;
 89              try
 90              {
 91                  lockTaken = _handleLock.TryEnterWriteLock(Timeout.Infinite);
 92  
 93                  _ports.Add(serverPortHandle, objectFactory);
 94              }
 95              finally
 96              {
 97                  if (lockTaken)
 98                  {
 99                      _handleLock.ExitWriteLock();
100                  }
101              }
102          }
103  
104          public void AddSessionObj(KServerSession serverSession, IpcService obj)
105          {
106              // Ensure that the sever loop is running.
107              InitDone.WaitOne();
108  
109              _selfProcess.HandleTable.GenerateHandle(serverSession, out int serverSessionHandle);
110  
111              AddSessionObj(serverSessionHandle, obj);
112          }
113  
114          public void AddSessionObj(int serverSessionHandle, IpcService obj)
115          {
116              bool lockTaken = false;
117              try
118              {
119                  lockTaken = _handleLock.TryEnterWriteLock(Timeout.Infinite);
120  
121                  _sessions.Add(serverSessionHandle, obj);
122              }
123              finally
124              {
125                  if (lockTaken)
126                  {
127                      _handleLock.ExitWriteLock();
128                  }
129              }
130  
131              _wakeEvent.WritableEvent.Signal();
132          }
133  
134          private IpcService GetSessionObj(int serverSessionHandle)
135          {
136              bool lockTaken = false;
137              try
138              {
139                  lockTaken = _handleLock.TryEnterReadLock(Timeout.Infinite);
140  
141                  return _sessions[serverSessionHandle];
142              }
143              finally
144              {
145                  if (lockTaken)
146                  {
147                      _handleLock.ExitReadLock();
148                  }
149              }
150          }
151  
152          private bool RemoveSessionObj(int serverSessionHandle, out IpcService obj)
153          {
154              bool lockTaken = false;
155              try
156              {
157                  lockTaken = _handleLock.TryEnterWriteLock(Timeout.Infinite);
158  
159                  return _sessions.Remove(serverSessionHandle, out obj);
160              }
161              finally
162              {
163                  if (lockTaken)
164                  {
165                      _handleLock.ExitWriteLock();
166                  }
167              }
168          }
169  
170          private void Main()
171          {
172              ServerLoop();
173          }
174  
175          private void ServerLoop()
176          {
177              _selfProcess = KernelStatic.GetCurrentProcess();
178              _selfThread = KernelStatic.GetCurrentThread();
179  
180              HorizonStatic.Register(
181                  default,
182                  _context.Syscall,
183                  _selfProcess.CpuMemory,
184                  _selfThread.ThreadContext,
185                  (int)_selfThread.ThreadContext.GetX(1));
186  
187              if (SmObjectFactory != null)
188              {
189                  _context.Syscall.ManageNamedPort(out int serverPortHandle, "sm:", 50);
190  
191                  AddPort(serverPortHandle, SmObjectFactory);
192              }
193  
194              _wakeEvent = new KEvent(_context);
195              Result result = _selfProcess.HandleTable.GenerateHandle(_wakeEvent.ReadableEvent, out _wakeHandle);
196  
197              InitDone.Set();
198  
199              ulong messagePtr = _selfThread.TlsAddress;
200              _context.Syscall.SetHeapSize(out ulong heapAddr, 0x200000);
201  
202              _selfProcess.CpuMemory.Write(messagePtr + 0x0, 0);
203              _selfProcess.CpuMemory.Write(messagePtr + 0x4, 2 << 10);
204              _selfProcess.CpuMemory.Write(messagePtr + 0x8, heapAddr | ((ulong)PointerBufferSize << 48));
205              int replyTargetHandle = 0;
206  
207              while (true)
208              {
209                  int portHandleCount;
210                  int handleCount;
211                  int[] handles;
212  
213                  bool handleLockTaken = false;
214                  try
215                  {
216                      handleLockTaken = _handleLock.TryEnterReadLock(Timeout.Infinite);
217  
218                      portHandleCount = _ports.Count;
219  
220                      handleCount = portHandleCount + _sessions.Count + 1;
221  
222                      handles = ArrayPool<int>.Shared.Rent(handleCount);
223  
224                      handles[0] = _wakeHandle;
225  
226                      _ports.Keys.CopyTo(handles, 1);
227  
228                      _sessions.Keys.CopyTo(handles, portHandleCount + 1);
229                  }
230                  finally
231                  {
232                      if (handleLockTaken)
233                      {
234                          _handleLock.ExitReadLock();
235                      }
236                  }
237  
238                  var rc = _context.Syscall.ReplyAndReceive(out int signaledIndex, handles.AsSpan(0, handleCount), replyTargetHandle, -1);
239  
240                  _selfThread.HandlePostSyscall();
241  
242                  if (!_selfThread.Context.Running)
243                  {
244                      break;
245                  }
246  
247                  replyTargetHandle = 0;
248  
249                  if (rc == Result.Success && signaledIndex >= portHandleCount + 1)
250                  {
251                      // We got a IPC request, process it, pass to the appropriate service if needed.
252                      int signaledHandle = handles[signaledIndex];
253  
254                      if (Process(signaledHandle, heapAddr))
255                      {
256                          replyTargetHandle = signaledHandle;
257                      }
258                  }
259                  else
260                  {
261                      if (rc == Result.Success)
262                      {
263                          if (signaledIndex > 0)
264                          {
265                              // We got a new connection, accept the session to allow servicing future requests.
266                              if (_context.Syscall.AcceptSession(out int serverSessionHandle, handles[signaledIndex]) == Result.Success)
267                              {
268                                  bool handleWriteLockTaken = false;
269                                  try
270                                  {
271                                      handleWriteLockTaken = _handleLock.TryEnterWriteLock(Timeout.Infinite);
272                                      IpcService obj = _ports[handles[signaledIndex]].Invoke();
273                                      _sessions.Add(serverSessionHandle, obj);
274                                  }
275                                  finally
276                                  {
277                                      if (handleWriteLockTaken)
278                                      {
279                                          _handleLock.ExitWriteLock();
280                                      }
281                                  }
282                              }
283                          }
284                          else
285                          {
286                              // The _wakeEvent signalled, which means we have a new session.
287                              _wakeEvent.WritableEvent.Clear();
288                          }
289                      }
290                      else if (rc == KernelResult.PortRemoteClosed && signaledIndex >= 0 && SmObjectFactory != null)
291                      {
292                          DestroySession(handles[signaledIndex]);
293                      }
294  
295                      _selfProcess.CpuMemory.Write(messagePtr + 0x0, 0);
296                      _selfProcess.CpuMemory.Write(messagePtr + 0x4, 2 << 10);
297                      _selfProcess.CpuMemory.Write(messagePtr + 0x8, heapAddr | ((ulong)PointerBufferSize << 48));
298                  }
299  
300                  ArrayPool<int>.Shared.Return(handles);
301              }
302  
303              Dispose();
304          }
305  
306          private void DestroySession(int serverSessionHandle)
307          {
308              _context.Syscall.CloseHandle(serverSessionHandle);
309  
310              if (RemoveSessionObj(serverSessionHandle, out var session))
311              {
312                  (session as IDisposable)?.Dispose();
313              }
314          }
315  
316          private bool Process(int serverSessionHandle, ulong recvListAddr)
317          {
318              IpcMessage request = ReadRequest();
319  
320              IpcMessage response = new();
321  
322              ulong tempAddr = recvListAddr;
323              int sizesOffset = request.RawData.Length - ((request.RecvListBuff.Count * 2 + 3) & ~3);
324  
325              bool noReceive = true;
326  
327              for (int i = 0; i < request.ReceiveBuff.Count; i++)
328              {
329                  noReceive &= (request.ReceiveBuff[i].Position == 0);
330              }
331  
332              if (noReceive)
333              {
334                  response.PtrBuff.EnsureCapacity(request.RecvListBuff.Count);
335  
336                  for (int i = 0; i < request.RecvListBuff.Count; i++)
337                  {
338                      ulong size = (ulong)BinaryPrimitives.ReadInt16LittleEndian(request.RawData.AsSpan(sizesOffset + i * 2, 2));
339  
340                      response.PtrBuff.Add(new IpcPtrBuffDesc(tempAddr, (uint)i, size));
341  
342                      request.RecvListBuff[i] = new IpcRecvListBuffDesc(tempAddr, size);
343  
344                      tempAddr += size;
345                  }
346              }
347  
348              bool shouldReply = true;
349              bool isTipcCommunication = false;
350  
351              _requestDataStream.SetLength(0);
352              _requestDataStream.Write(request.RawData);
353              _requestDataStream.Position = 0;
354  
355              if (request.Type == IpcMessageType.CmifRequest ||
356                  request.Type == IpcMessageType.CmifRequestWithContext)
357              {
358                  response.Type = IpcMessageType.CmifResponse;
359  
360                  _responseDataStream.SetLength(0);
361  
362                  ServiceCtx context = new(
363                      _context.Device,
364                      _selfProcess,
365                      _selfProcess.CpuMemory,
366                      _selfThread,
367                      request,
368                      response,
369                      _requestDataReader,
370                      _responseDataWriter);
371  
372                  GetSessionObj(serverSessionHandle).CallCmifMethod(context);
373  
374                  response.RawData = _responseDataStream.ToArray();
375              }
376              else if (request.Type == IpcMessageType.CmifControl ||
377                       request.Type == IpcMessageType.CmifControlWithContext)
378              {
379  #pragma warning disable IDE0059 // Remove unnecessary value assignment
380                  uint magic = (uint)_requestDataReader.ReadUInt64();
381  #pragma warning restore IDE0059
382                  uint cmdId = (uint)_requestDataReader.ReadUInt64();
383  
384                  switch (cmdId)
385                  {
386                      case 0:
387                          FillHipcResponse(response, 0, GetSessionObj(serverSessionHandle).ConvertToDomain());
388                          break;
389  
390                      case 3:
391                          FillHipcResponse(response, 0, PointerBufferSize);
392                          break;
393  
394                      // TODO: Whats the difference between IpcDuplicateSession/Ex?
395                      case 2:
396                      case 4:
397                          {
398                              _ = _requestDataReader.ReadInt32();
399  
400                              _context.Syscall.CreateSession(out int dupServerSessionHandle, out int dupClientSessionHandle, false, 0);
401  
402                              bool writeLockTaken = false;
403                              try
404                              {
405                                  writeLockTaken = _handleLock.TryEnterWriteLock(Timeout.Infinite);
406                                  _sessions[dupServerSessionHandle] = _sessions[serverSessionHandle];
407                              }
408                              finally
409                              {
410                                  if (writeLockTaken)
411                                  {
412                                      _handleLock.ExitWriteLock();
413                                  }
414                              }
415  
416                              response.HandleDesc = IpcHandleDesc.MakeMove(dupClientSessionHandle);
417  
418                              FillHipcResponse(response, 0);
419  
420                              break;
421                          }
422  
423                      default:
424                          throw new NotImplementedException(cmdId.ToString());
425                  }
426              }
427              else if (request.Type == IpcMessageType.CmifCloseSession || request.Type == IpcMessageType.TipcCloseSession)
428              {
429                  DestroySession(serverSessionHandle);
430                  shouldReply = false;
431              }
432              // If the type is past 0xF, we are using TIPC
433              else if (request.Type > IpcMessageType.TipcCloseSession)
434              {
435                  isTipcCommunication = true;
436  
437                  // Response type is always the same as request on TIPC.
438                  response.Type = request.Type;
439  
440                  _responseDataStream.SetLength(0);
441  
442                  ServiceCtx context = new(
443                      _context.Device,
444                      _selfProcess,
445                      _selfProcess.CpuMemory,
446                      _selfThread,
447                      request,
448                      response,
449                      _requestDataReader,
450                      _responseDataWriter);
451  
452                  GetSessionObj(serverSessionHandle).CallTipcMethod(context);
453  
454                  response.RawData = _responseDataStream.ToArray();
455  
456                  using var responseStream = response.GetStreamTipc();
457                  _selfProcess.CpuMemory.Write(_selfThread.TlsAddress, responseStream.GetReadOnlySequence());
458              }
459              else
460              {
461                  throw new NotImplementedException(request.Type.ToString());
462              }
463  
464              if (!isTipcCommunication)
465              {
466                  using var responseStream = response.GetStream((long)_selfThread.TlsAddress, recvListAddr | ((ulong)PointerBufferSize << 48));
467                  _selfProcess.CpuMemory.Write(_selfThread.TlsAddress, responseStream.GetReadOnlySequence());
468              }
469  
470              return shouldReply;
471          }
472  
473          private IpcMessage ReadRequest()
474          {
475              const int MessageSize = 0x100;
476  
477              using SpanOwner<byte> reqDataOwner = SpanOwner<byte>.Rent(MessageSize);
478  
479              Span<byte> reqDataSpan = reqDataOwner.Span;
480  
481              _selfProcess.CpuMemory.Read(_selfThread.TlsAddress, reqDataSpan);
482  
483              IpcMessage request = new(reqDataSpan, (long)_selfThread.TlsAddress);
484  
485              return request;
486          }
487  
488          private void FillHipcResponse(IpcMessage response, long result)
489          {
490              FillHipcResponse(response, result, ReadOnlySpan<byte>.Empty);
491          }
492  
493          private void FillHipcResponse(IpcMessage response, long result, int value)
494          {
495              Span<byte> span = stackalloc byte[sizeof(int)];
496              BinaryPrimitives.WriteInt32LittleEndian(span, value);
497              FillHipcResponse(response, result, span);
498          }
499  
500          private void FillHipcResponse(IpcMessage response, long result, ReadOnlySpan<byte> data)
501          {
502              response.Type = IpcMessageType.CmifResponse;
503  
504              _responseDataStream.SetLength(0);
505  
506              _responseDataStream.Write(IpcMagic.Sfco);
507              _responseDataStream.Write(result);
508  
509              _responseDataStream.Write(data);
510  
511              response.RawData = _responseDataStream.ToArray();
512          }
513  
514          protected virtual void Dispose(bool disposing)
515          {
516              if (disposing && _selfThread != null)
517              {
518                  if (_selfThread.HostThread.ManagedThreadId != Environment.CurrentManagedThreadId && _selfThread.HostThread.Join(_threadJoinTimeout) == false)
519                  {
520                      Logger.Warning?.Print(LogClass.Service, $"The ServerBase thread didn't terminate within {_threadJoinTimeout:g}, waiting longer.");
521  
522                      _selfThread.HostThread.Join(Timeout.Infinite);
523                  }
524  
525                  if (Interlocked.Exchange(ref _isDisposed, 1) == 0)
526                  {
527                      _selfProcess.HandleTable.CloseHandle(_wakeHandle);
528  
529                      foreach (IpcService service in _sessions.Values)
530                      {
531                          (service as IDisposable)?.Dispose();
532  
533                          service.DestroyAtExit();
534                      }
535  
536                      _sessions.Clear();
537                      _ports.Clear();
538                      _handleLock.Dispose();
539  
540                      _requestDataReader.Dispose();
541                      _requestDataStream.Dispose();
542                      _responseDataWriter.Dispose();
543                      _responseDataStream.Dispose();
544  
545                      InitDone.Dispose();
546                  }
547              }
548          }
549  
550          public void Dispose()
551          {
552              Dispose(true);
553          }
554      }
555  }