Skip to content

Commit 73668d2

Browse files
clownclaude
andcommitted
Add lock file and metadata sharing between VirtualPrinter and Launcher
- Added LockFile class to prevent concurrent print jobs via exclusive write to settings.json. State is managed in 4 stages: Idle / Locked / Ready / Released. - Added Metadata class to pass print job info (JobTitle, SessionId, AppName, etc.) from VirtualPrinter to Launcher via JSON file. Launcher now forwards these as arguments to the conversion process. - Added Core project (Cube.Psa.DesktopBridge.Core) to share LockFile and Metadata across both VirtualPrinter and Launcher projects. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent 4d1e39a commit 73668d2

15 files changed

Lines changed: 587 additions & 48 deletions
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
<Project Sdk="Microsoft.NET.Sdk">
2+
<PropertyGroup>
3+
<Version>1.0.0</Version>
4+
<Authors>cube-soft</Authors>
5+
<Company>CubeSoft</Company>
6+
<Description>Shared library for v4 Virtual Printer project</Description>
7+
<Copyright>Copyright © 2010 CubeSoft, Inc.</Copyright>
8+
<OutputType>Library</OutputType>
9+
<AssemblyName>Cube.Psa.DesktopBridge</AssemblyName>
10+
<RootNamespace>Cube.Psa.DesktopBridge</RootNamespace>
11+
<TargetFramework>net8.0-windows10.0.26100.0</TargetFramework>
12+
<TargetPlatformMinVersion>10.0.26100.0</TargetPlatformMinVersion>
13+
<TargetPlatformVersion>10.0.26100.0</TargetPlatformVersion>
14+
<Platforms>AnyCPU;x86;x64</Platforms>
15+
<SelfContained>false</SelfContained>
16+
<PublishSelfContained>false</PublishSelfContained>
17+
<WindowsAppSDKSelfContained>false</WindowsAppSDKSelfContained>
18+
<EnableDefaultCompileItems>true</EnableDefaultCompileItems>
19+
<GenerateProgramFile>false</GenerateProgramFile>
20+
<Nullable>enable</Nullable>
21+
<SatelliteResourceLanguages>_</SatelliteResourceLanguages>
22+
</PropertyGroup>
23+
<PropertyGroup Condition=" '$(Platform)' == 'AnyCPU' ">
24+
<OutputPath>bin\Any CPU\$(Configuration)\</OutputPath>
25+
<DocumentationFile>bin\Any CPU\$(Configuration)\$(TargetFramework)\$(AssemblyName).xml</DocumentationFile>
26+
</PropertyGroup>
27+
<PropertyGroup Condition=" '$(Platform)' == 'x86' ">
28+
<RuntimeIdentifier>win-x86</RuntimeIdentifier>
29+
</PropertyGroup>
30+
<PropertyGroup Condition=" '$(Platform)' == 'x64' ">
31+
<RuntimeIdentifier>win-x64</RuntimeIdentifier>
32+
</PropertyGroup>
33+
</Project>
Lines changed: 262 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,262 @@
1+
/* ------------------------------------------------------------------------- */
2+
//
3+
// Copyright (c) 2010 CubeSoft, Inc.
4+
//
5+
// Licensed under the Apache License, Version 2.0 (the "License");
6+
// you may not use this file except in compliance with the License.
7+
// You may obtain a copy of the License at
8+
//
9+
// http://www.apache.org/licenses/LICENSE-2.0
10+
//
11+
// Unless required by applicable law or agreed to in writing, software
12+
// distributed under the License is distributed on an "AS IS" BASIS,
13+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
// See the License for the specific language governing permissions and
15+
// limitations under the License.
16+
//
17+
/* ------------------------------------------------------------------------- */
18+
namespace Cube.Psa.DesktopBridge;
19+
20+
using System;
21+
using System.IO;
22+
using System.Threading;
23+
using System.Threading.Tasks;
24+
25+
/* ------------------------------------------------------------------------- */
26+
///
27+
/// LockFile
28+
///
29+
/// <summary>
30+
/// Manages exclusive access between the virtual printer and the launcher
31+
/// via a lock file. The lock is acquired atomically by writing the file
32+
/// and released by deleting it.
33+
/// </summary>
34+
///
35+
/// <remarks>
36+
/// Typical call sequence per job:
37+
///
38+
/// 1. LockAsync — acquire the lock and write the print data.
39+
/// Returns true on success, false on failure.
40+
/// 2. ReleaseAsync — launch the full-trust process and transfer ownership
41+
/// of the lock file to the launcher.
42+
///
43+
/// Dispose() deletes the lock file when the job did not complete or
44+
/// was not handed off to the launcher.
45+
/// </remarks>
46+
///
47+
/* ------------------------------------------------------------------------- */
48+
public sealed class LockFile(string path) : IDisposable
49+
{
50+
#region Methods
51+
52+
/* --------------------------------------------------------------------- */
53+
///
54+
/// LockAsync
55+
///
56+
/// <summary>
57+
/// Acquires the lock if not already held, then executes action.
58+
/// </summary>
59+
///
60+
/// <param name="action">
61+
/// The action to execute under the lock, e.g. writing the print data.
62+
/// </param>
63+
///
64+
/// <returns>true on success; false on failure.</returns>
65+
///
66+
/// <remarks>
67+
/// Skips acquisition when the lock is already held (Locked or Ready
68+
/// state). Re-acquires after a completed job (Released state).
69+
///
70+
/// TODO: Consider whether re-calling after Released should be treated
71+
/// the same as Idle. To prevent unintended reuse, it may be better to
72+
/// throw ObjectDisposedException after Released, as after Dispose().
73+
/// </remarks>
74+
///
75+
/* --------------------------------------------------------------------- */
76+
public async Task<bool> LockAsync(Func<Task<bool>> action)
77+
{
78+
ObjectDisposedException.ThrowIf(_disposed, this);
79+
80+
_state = await CreateAsync(_state);
81+
var done = await action();
82+
if (done) _state = LockState.Ready;
83+
return done;
84+
}
85+
86+
/* --------------------------------------------------------------------- */
87+
///
88+
/// ReleaseAsync
89+
///
90+
/// <summary>
91+
/// Executes action (typically launching the full-trust process) and
92+
/// transfers ownership of the lock file to the launcher.
93+
/// </summary>
94+
///
95+
/// <param name="action">
96+
/// The action to execute before transferring lock ownership, typically
97+
/// launching the full-trust process.
98+
/// </param>
99+
///
100+
/// <exception cref="ObjectDisposedException">
101+
/// Thrown if this instance has already been disposed.
102+
/// </exception>
103+
///
104+
/* --------------------------------------------------------------------- */
105+
public async Task ReleaseAsync(Func<Task> action)
106+
{
107+
ObjectDisposedException.ThrowIf(_disposed, this);
108+
await action();
109+
_state = LockState.Released;
110+
}
111+
112+
/* --------------------------------------------------------------------- */
113+
///
114+
/// Dispose
115+
///
116+
/// <summary>
117+
/// Releases the lock if still held.
118+
/// </summary>
119+
///
120+
/// <remarks>
121+
/// Deletes the lock file when the job did not complete or was not
122+
/// handed off to the launcher (Locked or Ready state).
123+
/// </remarks>
124+
///
125+
/* --------------------------------------------------------------------- */
126+
public void Dispose()
127+
{
128+
Dispose(true);
129+
GC.SuppressFinalize(this);
130+
}
131+
132+
/* --------------------------------------------------------------------- */
133+
///
134+
/// Finalizer
135+
///
136+
/// <summary>
137+
/// Ensures the lock file is deleted even if Dispose is not called.
138+
/// </summary>
139+
///
140+
/* --------------------------------------------------------------------- */
141+
~LockFile() => Dispose(false);
142+
143+
#endregion
144+
145+
#region Implementations
146+
147+
/* --------------------------------------------------------------------- */
148+
///
149+
/// Dispose
150+
///
151+
/// <summary>
152+
/// Deletes the lock file if the job did not complete or was not
153+
/// handed off. When called from the finalizer, disposing is false and
154+
/// only unmanaged resources are released; managed resources are
155+
/// released when true.
156+
/// </summary>
157+
///
158+
/// <param name="disposing">
159+
/// true if called from Dispose method; false if called from the finalizer.
160+
/// </param>
161+
///
162+
/* --------------------------------------------------------------------- */
163+
private void Dispose(bool disposing)
164+
{
165+
if (_disposed) return;
166+
_disposed = true;
167+
if (IsHeld(_state))
168+
{
169+
try { File.Delete(path); } catch { }
170+
}
171+
_state = LockState.Idle;
172+
}
173+
174+
/* --------------------------------------------------------------------- */
175+
///
176+
/// CreateAsync
177+
///
178+
/// <summary>
179+
/// Waits for any existing lock file to be deleted, then atomically
180+
/// creates the lock file by writing a temporary file and renaming it.
181+
/// Skips acquisition when the lock is already held.
182+
/// </summary>
183+
///
184+
/* --------------------------------------------------------------------- */
185+
private async Task<LockState> CreateAsync(LockState state)
186+
{
187+
if (IsHeld(state)) return state;
188+
189+
var tmp = $"{path}.{Guid.NewGuid()}";
190+
File.WriteAllText(tmp, "lock");
191+
await WaitAsync(600);
192+
File.Move(tmp, path, overwrite: true);
193+
return LockState.Locked;
194+
}
195+
196+
/* --------------------------------------------------------------------- */
197+
///
198+
/// WaitAsync
199+
///
200+
/// <summary>
201+
/// Waits for the lock file to be deleted by another process.
202+
/// If the file is not present, returns immediately.
203+
/// If the wait exceeds timeout seconds, forcibly deletes the stale
204+
/// lock file before returning.
205+
/// </summary>
206+
///
207+
/// <param name="timeout">
208+
/// Timeout in seconds. If exceeded, the stale lock file is forcibly
209+
/// deleted before returning.
210+
/// </param>
211+
///
212+
/* --------------------------------------------------------------------- */
213+
private async Task WaitAsync(int timeout)
214+
{
215+
var dir = Path.GetDirectoryName(path);
216+
if (dir is null) return;
217+
218+
var released = new TaskCompletionSource<bool>(TaskCreationOptions.RunContinuationsAsynchronously);
219+
using var watcher = new FileSystemWatcher(dir, Path.GetFileName(path))
220+
{
221+
NotifyFilter = NotifyFilters.FileName,
222+
EnableRaisingEvents = true,
223+
};
224+
watcher.Deleted += (_, _) => released.TrySetResult(true);
225+
226+
if (!File.Exists(path)) return;
227+
228+
using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(timeout));
229+
try { await released.Task.WaitAsync(cts.Token); }
230+
catch (OperationCanceledException)
231+
{
232+
try { File.Delete(path); } catch { }
233+
}
234+
}
235+
236+
/* --------------------------------------------------------------------- */
237+
///
238+
/// IsHeld
239+
///
240+
/// <summary>
241+
/// Determines whether the lock file is currently held by this
242+
/// instance and must be deleted on Dispose.
243+
/// </summary>
244+
///
245+
/// <remarks>
246+
/// Returns true for both Locked (action not yet completed) and Ready
247+
/// (action succeeded, awaiting ReleaseAsync). In either case the lock
248+
/// file is on disk and this instance is responsible for cleaning it up.
249+
/// </remarks>
250+
///
251+
/* --------------------------------------------------------------------- */
252+
private static bool IsHeld(LockState state) => state == LockState.Locked || state == LockState.Ready;
253+
254+
#endregion
255+
256+
#region Fields
257+
// Tracks the lifecycle of the lock file within a single job.
258+
private enum LockState { Idle, Locked, Ready, Released }
259+
private LockState _state;
260+
private bool _disposed;
261+
#endregion
262+
}

0 commit comments

Comments
 (0)