Add the program registry

This commit is contained in:
Alex Barney 2020-08-20 23:07:39 -07:00
parent 76d21263da
commit 004e46cacc
11 changed files with 448 additions and 23 deletions

View File

@ -40,11 +40,12 @@ Module,DescriptionStart,DescriptionEnd,Name,Summary
2,3005,,OutOfRange,
2,3200,3499,AllocationMemoryFailed,
2,3312,,AesXtsFileFileStorageAllocationError,
2,3313,,AesXtsFileXtsStorageAllocationError,
2,3314,,AesXtsFileAlignmentStorageAllocationError,
2,3315,,AesXtsFileStorageFileAllocationError,
2,3383,,AesXtsFileSubStorageAllocationError,
2,3258,,AllocationFailureInProgramRegistryManagerA,In RegisterProgram allocating ProgramInfoNode
2,3312,,AllocationFailureInAesXtsFileA,In Initialize allocating FileStorage
2,3313,,AllocationFailureInAesXtsFileB,In Initialize allocating AesXtsStorage
2,3314,,AllocationFailureInAesXtsFileC,In Initialize allocating AlignmentMatchingStoragePooledBuffer
2,3315,,AllocationFailureInAesXtsFileD,In Initialize allocating StorageFile
2,3383,,AllocationFailureInAesXtsFileE,In Initialize allocating SubStorage
2,3500,3999,MmcAccessFailed,
@ -234,6 +235,7 @@ Module,DescriptionStart,DescriptionEnd,Name,Summary
2,6465,,UserNotExist,
2,6600,6699,EntryNotFound,
2,6605,,TargetProgramNotFound,Specified program is not found in the program registry.
2,6606,,TargetProgramIndexNotFound,Specified program index is not found
2,6700,6799,OutOfResource,

Can't render this file because it has a wrong number of fields in line 154.

View File

@ -96,16 +96,18 @@ namespace LibHac.Fs
/// <summary>Error code: 2002-3200; Range: 3200-3499; Inner value: 0x190002</summary>
public static Result.Base AllocationMemoryFailed { [MethodImpl(MethodImplOptions.AggressiveInlining)] get => new Result.Base(ModuleFs, 3200, 3499); }
/// <summary>Error code: 2002-3312; Inner value: 0x19e002</summary>
public static Result.Base AesXtsFileFileStorageAllocationError => new Result.Base(ModuleFs, 3312);
/// <summary>Error code: 2002-3313; Inner value: 0x19e202</summary>
public static Result.Base AesXtsFileXtsStorageAllocationError => new Result.Base(ModuleFs, 3313);
/// <summary>Error code: 2002-3314; Inner value: 0x19e402</summary>
public static Result.Base AesXtsFileAlignmentStorageAllocationError => new Result.Base(ModuleFs, 3314);
/// <summary>Error code: 2002-3315; Inner value: 0x19e602</summary>
public static Result.Base AesXtsFileStorageFileAllocationError => new Result.Base(ModuleFs, 3315);
/// <summary>Error code: 2002-3383; Inner value: 0x1a6e02</summary>
public static Result.Base AesXtsFileSubStorageAllocationError => new Result.Base(ModuleFs, 3383);
/// <summary>In RegisterProgram allocating ProgramInfoNode<br/>Error code: 2002-3258; Inner value: 0x197402</summary>
public static Result.Base AllocationFailureInProgramRegistryManagerA => new Result.Base(ModuleFs, 3258);
/// <summary>In Initialize allocating FileStorage<br/>Error code: 2002-3312; Inner value: 0x19e002</summary>
public static Result.Base AllocationFailureInAesXtsFileA => new Result.Base(ModuleFs, 3312);
/// <summary>In Initialize allocating AesXtsStorage<br/>Error code: 2002-3313; Inner value: 0x19e202</summary>
public static Result.Base AllocationFailureInAesXtsFileB => new Result.Base(ModuleFs, 3313);
/// <summary>In Initialize allocating AlignmentMatchingStoragePooledBuffer<br/>Error code: 2002-3314; Inner value: 0x19e402</summary>
public static Result.Base AllocationFailureInAesXtsFileC => new Result.Base(ModuleFs, 3314);
/// <summary>In Initialize allocating StorageFile<br/>Error code: 2002-3315; Inner value: 0x19e602</summary>
public static Result.Base AllocationFailureInAesXtsFileD => new Result.Base(ModuleFs, 3315);
/// <summary>In Initialize allocating SubStorage<br/>Error code: 2002-3383; Inner value: 0x1a6e02</summary>
public static Result.Base AllocationFailureInAesXtsFileE => new Result.Base(ModuleFs, 3383);
/// <summary>Error code: 2002-3500; Range: 3500-3999; Inner value: 0x1b5802</summary>
public static Result.Base MmcAccessFailed { [MethodImpl(MethodImplOptions.AggressiveInlining)] get => new Result.Base(ModuleFs, 3500, 3999); }
@ -452,6 +454,8 @@ namespace LibHac.Fs
/// <summary>Error code: 2002-6600; Range: 6600-6699; Inner value: 0x339002</summary>
public static Result.Base EntryNotFound { [MethodImpl(MethodImplOptions.AggressiveInlining)] get => new Result.Base(ModuleFs, 6600, 6699); }
/// <summary>Specified program is not found in the program registry.<br/>Error code: 2002-6605; Inner value: 0x339a02</summary>
public static Result.Base TargetProgramNotFound => new Result.Base(ModuleFs, 6605);
/// <summary>Specified program index is not found<br/>Error code: 2002-6606; Inner value: 0x339c02</summary>
public static Result.Base TargetProgramIndexNotFound => new Result.Base(ModuleFs, 6606);

View File

@ -17,7 +17,7 @@ namespace LibHac.FsSrv
private FileSystemProxyCore FsProxyCore { get; }
internal FileSystemServer FsServer { get; }
public long CurrentProcess { get; private set; }
public ulong CurrentProcess { get; private set; }
public long SaveDataSize { get; private set; }
public long SaveDataJournalSize { get; private set; }
@ -29,12 +29,17 @@ namespace LibHac.FsSrv
FsProxyCore = fsProxyCore;
FsServer = fsServer;
CurrentProcess = -1;
CurrentProcess = ulong.MaxValue;
SaveDataSize = 0x2000000;
SaveDataJournalSize = 0x1000000;
AutoCreateSaveData = true;
}
private ProgramRegistryService GetProgramRegistryService()
{
return new ProgramRegistryService(FsProxyCore.Config.ProgramRegistryService, CurrentProcess);
}
public Result OpenFileSystemWithId(out IFileSystem fileSystem, ref FsPath path, ulong id, FileSystemProxyType type)
{
fileSystem = default;
@ -66,7 +71,7 @@ namespace LibHac.FsSrv
throw new NotImplementedException();
}
public Result SetCurrentProcess(long processId)
public Result SetCurrentProcess(ulong processId)
{
CurrentProcess = processId;

View File

@ -0,0 +1,10 @@
using LibHac.FsSrv.Creators;
namespace LibHac.FsSrv
{
public class FileSystemProxyConfiguration
{
public FileSystemCreators FsCreatorInterfaces { get; set; }
public ProgramRegistryServiceImpl ProgramRegistryService { get; set; }
}
}

View File

@ -15,7 +15,10 @@ namespace LibHac.FsSrv
{
public class FileSystemProxyCore
{
private FileSystemCreators FsCreators { get; }
internal FileSystemProxyConfiguration Config { get; }
private FileSystemCreators FsCreators => Config.FsCreatorInterfaces;
internal ProgramRegistryImpl ProgramRegistry { get; }
private ExternalKeySet ExternalKeys { get; }
private IDeviceOperator DeviceOperator { get; }
@ -29,9 +32,10 @@ namespace LibHac.FsSrv
internal ISaveDataIndexerManager SaveDataIndexerManager { get; private set; }
public FileSystemProxyCore(FileSystemCreators fsCreators, ExternalKeySet externalKeys, IDeviceOperator deviceOperator)
public FileSystemProxyCore(FileSystemProxyConfiguration config, ExternalKeySet externalKeys, IDeviceOperator deviceOperator)
{
FsCreators = fsCreators;
Config = config;
ProgramRegistry = new ProgramRegistryImpl(Config.ProgramRegistryService);
ExternalKeys = externalKeys ?? new ExternalKeySet();
DeviceOperator = deviceOperator;
}

View File

@ -34,7 +34,13 @@ namespace LibHac.FsSrv
ExternalKeySet externalKeySet = config.ExternalKeySet ?? new ExternalKeySet();
Timer = config.TimeSpanGenerator ?? new StopWatchTimeSpanGenerator();
FsProxyCore = new FileSystemProxyCore(config.FsCreators, externalKeySet, config.DeviceOperator);
var fspConfig = new FileSystemProxyConfiguration
{
FsCreatorInterfaces = config.FsCreators,
ProgramRegistryService = new ProgramRegistryServiceImpl(this)
};
FsProxyCore = new FileSystemProxyCore(fspConfig, externalKeySet, config.DeviceOperator);
var fsProxy = new FileSystemProxy(FsProxyCore, this);
FsClient = new FileSystemClient(this, fsProxy, Timer);

View File

@ -10,7 +10,7 @@ namespace LibHac.FsSrv
{
public interface IFileSystemProxy
{
Result SetCurrentProcess(long processId);
Result SetCurrentProcess(ulong processId);
Result OpenDataFileSystemByCurrentProcess(out IFileSystem fileSystem);
Result OpenFileSystemWithPatch(out IFileSystem fileSystem, ProgramId programId, FileSystemProxyType type);
Result OpenFileSystemWithId(out IFileSystem fileSystem, ref FsPath path, ulong id, FileSystemProxyType type);

View File

@ -0,0 +1,211 @@
using System;
using System.Collections.Generic;
using LibHac.Fs;
using LibHac.Ncm;
namespace LibHac.FsSrv.Impl
{
internal class ProgramRegistryManager
{
// Note: FS keeps each ProgramInfo in a shared_ptr, but there aren't any non-memory resources
// that need to be freed, so we use plain ProgramInfos
private LinkedList<ProgramInfo> ProgramInfoList { get; }
private FileSystemServer FsServer { get; }
// Note: This variable is global in FS. It's moved to ProgramRegistryManager here because it
// relies on some state kept in FileSystemServer, and it's only used by ProgramRegistryManager
private ProgramInfo _programInfoForInitialProcess;
private readonly object _programInfoForInitialProcessGuard = new object();
public ProgramRegistryManager(FileSystemServer fsServer)
{
ProgramInfoList = new LinkedList<ProgramInfo>();
FsServer = fsServer;
}
private ProgramInfo GetProgramInfoForInitialProcess()
{
if (_programInfoForInitialProcess == null)
{
lock (_programInfoForInitialProcessGuard)
{
_programInfoForInitialProcess ??= ProgramInfo.CreateProgramInfoForInitialProcess(FsServer);
}
}
return _programInfoForInitialProcess;
}
/// <summary>
/// Registers a program with information about the program in the program registry.
/// </summary>
/// <param name="processId">The process ID of the program.</param>
/// <param name="programId">The <see cref="ProgramId"/> of the program.</param>
/// <param name="storageId">The <see cref="StorageId"/> where the program is located.</param>
/// <param name="accessControlData">The FS access control data header located in the program's ACI.</param>
/// <param name="accessControlDescriptor">The FS access control descriptor located in the program's ACID.</param>
/// <returns><see cref="Result.Success"/>: The operation was successful.<br/>
/// <see cref="ResultFs.InvalidArgument"/>: The process ID is already registered.</returns>
public Result RegisterProgram(ulong processId, ProgramId programId, StorageId storageId,
ReadOnlySpan<byte> accessControlData, ReadOnlySpan<byte> accessControlDescriptor)
{
var programInfo = new ProgramInfo(FsServer, processId, programId, storageId, accessControlData,
accessControlDescriptor);
lock (ProgramInfoList)
{
foreach (ProgramInfo info in ProgramInfoList)
{
if (info.Contains(processId))
return ResultFs.InvalidArgument.Log();
}
ProgramInfoList.AddLast(programInfo);
return Result.Success;
}
}
/// <summary>
/// Unregisters the program with the specified process ID.
/// </summary>
/// <param name="processId">The process ID of the program to unregister.</param>
/// <returns><see cref="Result.Success"/>: The operation was successful.<br/>
/// <see cref="ResultFs.InvalidArgument"/>: The process ID is not registered.</returns>
public Result UnregisterProgram(ulong processId)
{
lock (ProgramInfoList)
{
for (LinkedListNode<ProgramInfo> node = ProgramInfoList.First; node != null; node = node.Next)
{
if (node.Value.Contains(processId))
{
ProgramInfoList.Remove(node);
return Result.Success;
}
}
return ResultFs.InvalidArgument.Log();
}
}
/// <summary>
/// Gets the <see cref="ProgramInfo"/> associated with the specified process ID.
/// </summary>
/// <param name="programInfo">If the method returns successfully, contains the <see cref="ProgramInfo"/>
/// associated with the specified process ID.</param>
/// <param name="processId">The process ID of the <see cref="ProgramInfo"/> to get.</param>
/// <returns><see cref="Result.Success"/>: The operation was successful.<br/>
/// <see cref="ResultFs.TargetProgramNotFound"/>: The <see cref="ProgramInfo"/> was not found.</returns>
public Result GetProgramInfo(out ProgramInfo programInfo, ulong processId)
{
lock (ProgramInfoList)
{
if(ProgramInfo.IsInitialProgram(processId))
{
programInfo = GetProgramInfoForInitialProcess();
return Result.Success;
}
foreach (ProgramInfo info in ProgramInfoList)
{
if (info.Contains(processId))
{
programInfo = info;
return Result.Success;
}
}
programInfo = default;
return ResultFs.TargetProgramNotFound.Log();
}
}
/// <summary>
/// Gets the <see cref="ProgramInfo"/> associated with the specified program ID.
/// </summary>
/// <param name="programInfo">If the method returns successfully, contains the <see cref="ProgramInfo"/>
/// associated with the specified program ID.</param>
/// <param name="programId">The program ID of the <see cref="ProgramInfo"/> to get.</param>
/// <returns><see cref="Result.Success"/>: The operation was successful.<br/>
/// <see cref="ResultFs.TargetProgramNotFound"/>: The <see cref="ProgramInfo"/> was not found.</returns>
public Result GetProgramInfoByProgramId(out ProgramInfo programInfo, ulong programId)
{
lock (ProgramInfoList)
{
foreach (ProgramInfo info in ProgramInfoList)
{
if (info.ProgramId.Value == programId)
{
programInfo = info;
return Result.Success;
}
}
programInfo = default;
return ResultFs.TargetProgramNotFound.Log();
}
}
}
public class ProgramInfo
{
private ulong ProcessId { get; }
public ProgramId ProgramId { get; }
public StorageId StorageId { get; }
public AccessControl AccessControl { get; }
public ProgramInfo(FileSystemServer fsServer, ulong processId, ProgramId programId, StorageId storageId,
ReadOnlySpan<byte> accessControlData, ReadOnlySpan<byte> accessControlDescriptor)
{
ProcessId = processId;
AccessControl = new AccessControl(fsServer, accessControlData, accessControlDescriptor);
ProgramId = programId;
StorageId = storageId;
}
private ProgramInfo(FileSystemServer fsServer, ReadOnlySpan<byte> accessControlData,
ReadOnlySpan<byte> accessControlDescriptor)
{
ProcessId = 0;
AccessControl = new AccessControl(fsServer, accessControlData, accessControlDescriptor);
ProgramId = default;
StorageId = 0;
}
public bool Contains(ulong processId) => ProcessId == processId;
public static bool IsInitialProgram(ulong processId)
{
// Todo: We have no kernel to call into, so use hardcoded values for now
const int initialProcessIdLowerBound = 1;
const int initialProcessIdUpperBound = 10;
return initialProcessIdLowerBound >= processId && processId <= initialProcessIdUpperBound;
}
public static bool IsCurrentProcess(ulong processId)
{
// Todo: Don't use hardcoded value
return true;
}
internal static ProgramInfo CreateProgramInfoForInitialProcess(FileSystemServer fsServer)
{
return new ProgramInfo(fsServer, InitialProcessAccessControlDataHeader,
InitialProcessAccessControlDescriptor);
}
private static ReadOnlySpan<byte> InitialProcessAccessControlDataHeader => new byte[]
{
0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0x1C, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x1C, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00
};
private static ReadOnlySpan<byte> InitialProcessAccessControlDescriptor => new byte[]
{
0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF
};
}
}

View File

@ -0,0 +1,98 @@
using System;
using LibHac.Fs;
using LibHac.FsSrv.Impl;
using LibHac.Ncm;
namespace LibHac.FsSrv
{
internal class ProgramRegistryImpl
{
private ulong _processId;
private readonly ProgramRegistryServiceImpl _registryService;
public ProgramRegistryImpl(ProgramRegistryServiceImpl registryService)
{
_processId = ulong.MaxValue;
_registryService = registryService;
}
public ProgramRegistryImpl(ProgramRegistryServiceImpl registryService, ulong processId)
{
_processId = processId;
_registryService = registryService;
}
/// <summary>
/// Registers a program with information about the program in the program registry.
/// </summary>
/// <param name="processId">The process ID of the program.</param>
/// <param name="programId">The <see cref="ProgramId"/> of the program.</param>
/// <param name="storageId">The <see cref="StorageId"/> where the program is located.</param>
/// <param name="accessControlData">The FS access control data header located in the program's ACI.</param>
/// <param name="accessControlDescriptor">The FS access control descriptor located in the program's ACID.</param>
/// <returns><see cref="Result.Success"/>: The operation was successful.<br/>
/// <see cref="ResultFs.InvalidArgument"/>: The process ID is already registered.<br/>
/// <see cref="ResultFs.PermissionDenied"/>: Insufficient permissions.</returns>
public Result RegisterProgram(ulong processId, ProgramId programId, StorageId storageId,
ReadOnlySpan<byte> accessControlData, ReadOnlySpan<byte> accessControlDescriptor)
{
if (!ProgramInfo.IsInitialProgram(_processId))
return ResultFs.PermissionDenied.Log();
return _registryService.RegisterProgram(processId, programId, storageId, accessControlData,
accessControlDescriptor);
}
/// <summary>
/// Unregisters the program with the specified process ID.
/// </summary>
/// <param name="processId">The process ID of the program to unregister.</param>
/// <returns><see cref="Result.Success"/>: The operation was successful.<br/>
/// <see cref="ResultFs.InvalidArgument"/>: The process ID is not registered.<br/>
/// <see cref="ResultFs.PermissionDenied"/>: Insufficient permissions.</returns>
public Result UnregisterProgram(ulong processId)
{
if (!ProgramInfo.IsInitialProgram(_processId))
return ResultFs.PermissionDenied.Log();
return _registryService.UnregisterProgram(processId);
}
/// <summary>
/// Gets the <see cref="ProgramInfo"/> associated with the specified process ID.
/// </summary>
/// <param name="programInfo">If the method returns successfully, contains the <see cref="ProgramInfo"/>
/// associated with the specified process ID.</param>
/// <param name="processId">The process ID of the <see cref="ProgramInfo"/> to get.</param>
/// <returns><see cref="Result.Success"/>: The operation was successful.<br/>
/// <see cref="ResultFs.TargetProgramNotFound"/>: The <see cref="ProgramInfo"/> was not found.</returns>
public Result GetProgramInfo(out ProgramInfo programInfo, ulong processId)
{
return _registryService.GetProgramInfo(out programInfo, processId);
}
/// <summary>
/// Gets the <see cref="ProgramInfo"/> associated with the specified program ID.
/// </summary>
/// <param name="programInfo">If the method returns successfully, contains the <see cref="ProgramInfo"/>
/// associated with the specified program ID.</param>
/// <param name="programId">The program ID of the <see cref="ProgramInfo"/> to get.</param>
/// <returns><see cref="Result.Success"/>: The operation was successful.<br/>
/// <see cref="ResultFs.TargetProgramNotFound"/>: The <see cref="ProgramInfo"/> was not found.</returns>
public Result GetProgramInfoByProgramId(out ProgramInfo programInfo, ulong programId)
{
return _registryService.GetProgramInfoByProgramId(out programInfo, programId);
}
/// <summary>
/// Sets the process ID of the process that will use this service via IPC.
/// </summary>
/// <param name="processId">The process ID to set.</param>
/// <returns><see cref="Result.Success"/>: The operation was successful.</returns>
public Result SetCurrentProcess(ulong processId)
{
_processId = processId;
return Result.Success;
}
}
}

View File

@ -0,0 +1,14 @@
namespace LibHac.FsSrv
{
internal readonly struct ProgramRegistryService
{
private ProgramRegistryServiceImpl ServiceImpl { get; }
private ulong ProcessId { get; }
public ProgramRegistryService(ProgramRegistryServiceImpl serviceImpl, ulong processId)
{
ServiceImpl = serviceImpl;
ProcessId = processId;
}
}
}

View File

@ -0,0 +1,71 @@
using System;
using LibHac.Fs;
using LibHac.FsSrv.Impl;
using LibHac.Ncm;
namespace LibHac.FsSrv
{
public class ProgramRegistryServiceImpl
{
private ProgramRegistryManager RegistryManager { get; }
public ProgramRegistryServiceImpl(FileSystemServer fsServer)
{
RegistryManager = new ProgramRegistryManager(fsServer);
}
/// <summary>
/// Registers a program with information about the program in the program registry.
/// </summary>
/// <param name="processId">The process ID of the program.</param>
/// <param name="programId">The <see cref="ProgramId"/> of the program.</param>
/// <param name="storageId">The <see cref="StorageId"/> where the program is located.</param>
/// <param name="accessControlData">The FS access control data header located in the program's ACI.</param>
/// <param name="accessControlDescriptor">The FS access control descriptor located in the program's ACID.</param>
/// <returns><see cref="Result.Success"/>: The operation was successful.<br/>
/// <see cref="ResultFs.InvalidArgument"/>: The process ID is already registered.</returns>
public Result RegisterProgram(ulong processId, ProgramId programId, StorageId storageId,
ReadOnlySpan<byte> accessControlData, ReadOnlySpan<byte> accessControlDescriptor)
{
return RegistryManager.RegisterProgram(processId, programId, storageId, accessControlData,
accessControlDescriptor);
}
/// <summary>
/// Unregisters the program with the specified process ID.
/// </summary>
/// <param name="processId">The process ID of the program to unregister.</param>
/// <returns><see cref="Result.Success"/>: The operation was successful.<br/>
/// <see cref="ResultFs.InvalidArgument"/>: The process ID is not registered.</returns>
public Result UnregisterProgram(ulong processId)
{
return RegistryManager.UnregisterProgram(processId);
}
/// <summary>
/// Gets the <see cref="ProgramInfo"/> associated with the specified process ID.
/// </summary>
/// <param name="programInfo">If the method returns successfully, contains the <see cref="ProgramInfo"/>
/// associated with the specified process ID.</param>
/// <param name="processId">The process ID of the <see cref="ProgramInfo"/> to get.</param>
/// <returns><see cref="Result.Success"/>: The operation was successful.<br/>
/// <see cref="ResultFs.TargetProgramNotFound"/>: The <see cref="ProgramInfo"/> was not found.</returns>
public Result GetProgramInfo(out ProgramInfo programInfo, ulong processId)
{
return RegistryManager.GetProgramInfo(out programInfo, processId);
}
/// <summary>
/// Gets the <see cref="ProgramInfo"/> associated with the specified program ID.
/// </summary>
/// <param name="programInfo">If the method returns successfully, contains the <see cref="ProgramInfo"/>
/// associated with the specified program ID.</param>
/// <param name="programId">The program ID of the <see cref="ProgramInfo"/> to get.</param>
/// <returns><see cref="Result.Success"/>: The operation was successful.<br/>
/// <see cref="ResultFs.TargetProgramNotFound"/>: The <see cref="ProgramInfo"/> was not found.</returns>
public Result GetProgramInfoByProgramId(out ProgramInfo programInfo, ulong programId)
{
return RegistryManager.GetProgramInfoByProgramId(out programInfo, programId);
}
}
}