SQLitePCL.raw icon indicating copy to clipboard operation
SQLitePCL.raw copied to clipboard

"Disk I/O error" on .Net 9 Blazor WebAssembly App and IDBFS

Open peerem opened this issue 1 year ago • 12 comments

After updating to .Net 9 we get "Disk I/O error". A read and write test with a simple text file in IDBFS was successful in .Net 9. See this thread:

https://github.com/ericsink/SQLitePCL.raw/issues/472#issuecomment-2477108935

peerem avatar Nov 15 '24 14:11 peerem

I need LOTS more info. A sample that reproduces the problem. Etc.

ericsink avatar Nov 15 '24 14:11 ericsink

I can't upload files from my work computer, but I can copy the code as text and describe the small modifications to a standard Blazor app template. Is that enough for you? I'll put it in step by step, it's not much.

peerem avatar Nov 15 '24 18:11 peerem

Is that enough

I don't know, but it's moving in the right direction.

ericsink avatar Nov 15 '24 18:11 ericsink

  1. Create project from template: Screenshot 2024-11-15 191118 Screenshot 2024-11-15 191213

  2. Create "files.js" in "wwwroot" folder:

function mount(dotNet)
{
    // Mount db
    const dbFolder = '/data';
    Blazor.runtime.Module.FS.mkdir(dbFolder);
    Blazor.runtime.Module.FS.mount(Blazor.runtime.Module.FS.filesystems.IDBFS, {}, dbFolder);
    Blazor.runtime.Module.FS.syncfs(true, async (err) =>
    {
        if (dotNet)
        {
            const hasError = err != null;
            await dotNet.invokeMethodAsync('MountFinished', hasError);
        }
    });
}

function sync()
{
    // Sync db
    Blazor.runtime.Module.FS.syncfs(async (err) =>
    {

    });
}

  1. Content of csproj-file:
<Project Sdk="Microsoft.NET.Sdk.BlazorWebAssembly">

    <PropertyGroup>
        <TargetFramework>net9.0</TargetFramework>
        <EmccExtraLDFlags>-lidbfs.js</EmccExtraLDFlags>
    </PropertyGroup>

    <ItemGroup>
        <PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly" Version="9.0.0" />
        <PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.DevServer" Version="9.0.0" PrivateAssets="all" />
        <PackageReference Include="sqlite-net-sqlcipher" Version="1.9.172" />
        <PackageReference Include="SQLitePCLRaw.bundle_e_sqlcipher" Version="2.1.10" />
        <PackageReference Include="SQLitePCLRaw.core" Version="2.1.10" />
        <PackageReference Include="SQLitePCLRaw.lib.e_sqlcipher" Version="2.1.10" />
        <PackageReference Include="SQLitePCLRaw.provider.e_sqlcipher" Version="2.1.10" />
    </ItemGroup>

</Project>

  1. Add this to "index.html" in the "head" section:

<script src="files.js"></script>

  1. Content of "Home.razor":
@page "/"
@using System.IO
@using SQLite

<PageTitle>Home</PageTitle>

<h1>Hello, world!</h1>

Welcome to your new app.

@code {
    private DotNetObjectReference<Home> dotNet;

    // Injections
    [Inject]
    private IJSRuntime JsRuntime { get; set; }

    protected override async void OnAfterRender(bool firstRender)
    {
        base.OnAfterRender(firstRender);

        // Mount IDBFS
        if (firstRender)
        {
            // Create .NET reference
            dotNet = DotNetObjectReference.Create(this);

            // Mount
            await JsRuntime.InvokeVoidAsync("mount", dotNet);
        }
    }

    [JSInvokable]
    public async void MountFinished(bool hasError)
    {
        if (!hasError)
        {
            // Test
            string filename = "/data/Test.txt";

            // Create if not exists
            if (File.Exists(filename))
                Console.WriteLine("File already exists.");
            else
            {
                Console.WriteLine("Create file.");

                // Write file
                File.WriteAllText(filename, "Hello persistent filesystem.");

                // Sync with IndexedDB
                await JsRuntime.InvokeVoidAsync("sync");
            }

            // Read
            if (File.Exists(filename))
                Console.WriteLine(File.ReadAllText(filename));

            // Test database
            string databaseFile = "/data/Test.sqlite";

            // Db exists?
            bool dbExists = File.Exists(databaseFile);

            // Open or create database
            SQLiteConnection connection = new SQLiteConnection(new SQLiteConnectionString(databaseFile, SQLiteOpenFlags.FullMutex | SQLiteOpenFlags.ReadWrite | SQLiteOpenFlags.Create, true, "secret"));
            connection.Execute("PRAGMA cipher_compatibility=3");

            // If new create a table
            if (!dbExists)
            {
                // Create test table
                connection.Execute(
                    @"CREATE TABLE IF NOT EXISTS TEST (
                        ID           TEXT NOT NULL,
                        NAME         TEXT NOT NULL,
                        TEST         TEXT,
                        PRIMARY KEY (ID, NAME))");
            }

            // Sync with IndexedDB
            await JsRuntime.InvokeVoidAsync("sync");
        }
    }
}

If you then run this, you will get the "disk i/o error" when executing the "create table" sql.

peerem avatar Nov 15 '24 18:11 peerem

I suggest changing from sqlite-net-sqlcipher to sqlite-net base, and then add a call to SQLitePCL.Batteries.Init().

Also, if you have the bundle_e_sqlcipher package, you don't need to have entries for the other three SQLitePCLRaw packages.

Did the same (or equivalent) test work under .NET 8?

Do you get the same results with this test if you change from SQLCipher to regular SQLite?

ericsink avatar Nov 15 '24 19:11 ericsink

In .Net 8 this code runs without any problems (with a few adjustments in the Javascript).

I have now changed my project file as follows:

<Project Sdk="Microsoft.NET.Sdk.BlazorWebAssembly">

    <PropertyGroup>
        <TargetFramework>net9.0</TargetFramework>
        <EmccExtraLDFlags>-lidbfs.js</EmccExtraLDFlags>
    </PropertyGroup>

    <ItemGroup>
        <PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly" Version="9.0.0" />
        <PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.DevServer" Version="9.0.0" PrivateAssets="all" />
        <!--
        <PackageReference Include="sqlite-net-sqlcipher" Version="1.9.172" />
        <PackageReference Include="SQLitePCLRaw.bundle_e_sqlcipher" Version="2.1.10" />
        -->
        <PackageReference Include="sqlite-net-base" Version="1.9.172" />
        <PackageReference Include="SQLitePCLRaw.bundle_e_sqlite3" Version="2.1.10" />
    </ItemGroup>

</Project>

Then I added "SQLitePCL.Batteries.Init();" before creating the connection. The basic version works, the cypher version generates the following exception:

SQLite.SQLiteException: disk I/O error at SQLite.SQLite3.Prepare2(sqlite3 db, String query) at SQLite.SQLiteCommand.Prepare() at SQLite.SQLiteCommand.ExecuteNonQuery() at SQLite.SQLiteConnection.Execute(String query, Object[] args) at BlazorApp4Sqlite.Pages.Home.MountFinished(Boolean hasError) in C:\Temp\Temp3\Test\BlazorApp4Sqlite\Pages\Home.razor:line 67 at System.Threading.Tasks.Task.<>c.<ThrowAsync>b__128_1(Object state) at System.Threading.QueueUserWorkItemCallbackDefaultContext.Execute() at System.Threading.ThreadPoolWorkQueue.Dispatch() at System.Threading.ThreadPool.BackgroundJobHandler()

Of course we want to continue using the cypher version.

peerem avatar Nov 15 '24 20:11 peerem

OK, so this appears to be a problem with the build of e_sqlcipher for wasm. The e_sqlcipher builds are unsupported, but I'll take a look at the build configuration and see if I see an easy or obvious problem.

ericsink avatar Nov 15 '24 20:11 ericsink

Thank you.

peerem avatar Nov 15 '24 20:11 peerem

Any news about the problem?

peerem avatar Nov 21 '24 06:11 peerem

Nothing significant yet. I verified that my build system is doing a net9 build for e_sqlcipher, which I wasn't sure about, but I haven't found time to dig deeper.

ericsink avatar Nov 21 '24 12:11 ericsink

Happy new year. Any news?

peerem avatar Jan 06 '25 22:01 peerem

I haven't had time to investigate this further. :-(

ericsink avatar Jan 07 '25 21:01 ericsink