diff --git a/src/Soil-Core-Tests/SoilTest.class.st b/src/Soil-Core-Tests/SoilTest.class.st index f62396e0..b46073b8 100644 --- a/src/Soil-Core-Tests/SoilTest.class.st +++ b/src/Soil-Core-Tests/SoilTest.class.st @@ -10,7 +10,7 @@ Class { { #category : #accessing } SoilTest class >> classNamesNotUnderTest [ "we for now ignore flock as this is platform specific" - ^ #(#MacOSFileLock #UnixFileLock) + ^ #(#SoilMacOSFileLock #SoilUnixFileLock) ] { #category : #accessing } diff --git a/src/Soil-Core/SoilBackupVisitor.class.st b/src/Soil-Core/SoilBackupVisitor.class.st index 308dafa9..066ad081 100644 --- a/src/Soil-Core/SoilBackupVisitor.class.st +++ b/src/Soil-Core/SoilBackupVisitor.class.st @@ -54,11 +54,17 @@ SoilBackupVisitor >> copyCluster: aSoilPersistentClusterVersion [ { #category : #visiting } SoilBackupVisitor >> copyIndexAt: indexId segment: segmentId [ - | sourceSegment sourceIndex targetSegment targetIndex iterator assoc | + | sourceSegment sourceIndex targetSegment targetIndex iterator assoc isBehaviorIndex | + "the behavior registry (identifier index of the meta segment) is already open + via soil behaviorRegistry. Reuse that open index instead of opening the same + file a second time through the index manager, which fails on Windows" + isBehaviorIndex := (segmentId = 0) and: [ indexId = #identifier ]. sourceSegment := soil objectRepository segmentAt: segmentId. - sourceIndex := sourceSegment indexManager - at: indexId - ifAbsent: [ ^ self indexNotFound: indexId ]. + sourceIndex := isBehaviorIndex + ifTrue: [ soil behaviorRegistry index ] + ifFalse: [ sourceSegment indexManager + at: indexId + ifAbsent: [ ^ self indexNotFound: indexId ] ]. "create an index of same kind and configuration in the target database" targetSegment := target objectRepository segmentAt: segmentId. @@ -77,7 +83,10 @@ SoilBackupVisitor >> copyIndexAt: indexId segment: segmentId [ targetIndex flush; close. - sourceSegment indexManager removeIndexWithId: indexId + "do not close the behavior registry index here, it is owned by soil and + still in use; only release indexes we opened through the index manager" + isBehaviorIndex ifFalse: [ + sourceSegment indexManager removeIndexWithId: indexId ] ] { #category : #visiting } @@ -153,10 +162,14 @@ SoilBackupVisitor >> visitJournalFragmentFile: aSoilJournalFragmentFile [ ] { #category : #visiting } -SoilBackupVisitor >> visitMetaSegment: aSoilMetaSegment [ +SoilBackupVisitor >> visitMetaSegment: aSoilMetaSegment [ super visitMetaSegment: aSoilMetaSegment. - self copyIndexAt: #identifier segment: 0. - ^ aSoilMetaSegment + "the target's behavior registry opened the identifier index when the target + filesystem was initialized. Close it first so copyIndexAt: can rebuild that + index without opening the same file a second time (which fails on Windows)" + target behaviorRegistry close. + self copyIndexAt: #identifier segment: 0. + ^ aSoilMetaSegment ] { #category : #visiting } diff --git a/src/Soil-Core/SoilBasicVisitor.class.st b/src/Soil-Core/SoilBasicVisitor.class.st index 37b2949f..ca3a7a3d 100644 --- a/src/Soil-Core/SoilBasicVisitor.class.st +++ b/src/Soil-Core/SoilBasicVisitor.class.st @@ -79,11 +79,14 @@ SoilBasicVisitor >> visitIndexManager: aSoilIndexManager [ ] { #category : #visiting } -SoilBasicVisitor >> visitJournalFragmentFile: aSoilJournalFragmentFile [ - aSoilJournalFragmentFile open. +SoilBasicVisitor >> visitJournalFragmentFile: aSoilJournalFragmentFile [ + "only read the fragment file. Opening read-only avoids a second write-mode + open of a file the journal may already hold open for writing (fails on + Windows). SoilBackupVisitor overrides this to copy the file instead" + aSoilJournalFragmentFile openReadOnly. [ self visitAll: aSoilJournalFragmentFile transactionJournals ] ensure: [ aSoilJournalFragmentFile close ]. - ^ aSoilJournalFragmentFile + ^ aSoilJournalFragmentFile ] { #category : #visiting } diff --git a/src/Soil-Core/SoilDatabaseRecovery.class.st b/src/Soil-Core/SoilDatabaseRecovery.class.st index 7faf6e92..2ec852af 100644 --- a/src/Soil-Core/SoilDatabaseRecovery.class.st +++ b/src/Soil-Core/SoilDatabaseRecovery.class.st @@ -61,11 +61,10 @@ SoilDatabaseRecovery >> readFragmentFileProtected: aSoilFragmentFile [ (journal lastFileNumber = aSoilFragmentFile fileNumber) ifFalse: [ SoilDatabaseIsInconsistent signal: 'after a truncated file there should not be another one' ]. "we currently have no way of reading behind a truncated piece of a fragment. We cycle - the fragment file in order to write new entries to a new file. Future reads can just - skip a fragment file on truncated reads" - journal - currentFragmentFile: aSoilFragmentFile; - cycleFragmentFile ] + the fragment file in order to write new entries to a new file. Future reads can just + skip a fragment file on truncated reads. The fragment file was opened read-only for + recovery, so we cycle from it without flushing/syncing it" + journal cycleFragmentFileFrom: aSoilFragmentFile ] ] { #category : #private } @@ -102,16 +101,19 @@ SoilDatabaseRecovery >> recover [ checkpointEntry := [ SoilJournalEntry readFrom: fragmentFile stream ] on: Error do: [ :error | - "in case of a bogus checkpoint position we cannot recover the file but - create a new one and continue" - journal - currentFragmentFile: fragmentFile; - cycleFragmentFile. + "in case of a bogus checkpoint position we cannot recover the file but + create a new one and continue. The fragment file was opened read-only, + so we cycle from it without flushing/syncing it" + journal cycleFragmentFileFrom: fragmentFile. soil checkpoint. ^ self ]. - "If the last checkpoint was successful we are at the end of the file and - can return because the database is in a sane state" - fragmentFile atEnd ifTrue: [ ^ self ]. + "If the last checkpoint was successful we are at the end of the file and + can return because the database is in a sane state. Close the read-only + recovery handle so the journal can later open the file for writing (a second + open in write mode while this one is still open fails on Windows)" + fragmentFile atEnd ifTrue: [ + fragmentFile close. + ^ self ]. "the fragment file contains more entries after the last checkpoint. Read the current fragment file to its end and apply all transaction logs/checkpoints found" diff --git a/src/Soil-Core/SoilJournalFragmentFile.class.st b/src/Soil-Core/SoilJournalFragmentFile.class.st index 16451f45..3b7f9ec5 100644 --- a/src/Soil-Core/SoilJournalFragmentFile.class.st +++ b/src/Soil-Core/SoilJournalFragmentFile.class.st @@ -239,7 +239,16 @@ SoilJournalFragmentFile >> inspectionTransactionJournals: aBuilder [ { #category : #'open/close' } SoilJournalFragmentFile >> open [ self isOpen ifTrue: [ self error: 'File already open' ]. - stream := SoilLockableStream path: path + stream := SoilLockableStream path: path +] + +{ #category : #'open/close' } +SoilJournalFragmentFile >> openReadOnly [ + "open the fragment file in read-only mode. Used during recovery which only + reads the file. This avoids opening the same path in write mode from a + second stream while the journal holds it open for writing (fails on Windows)" + self isOpen ifTrue: [ self error: 'File already open' ]. + stream := SoilLockableStream readOnlyPath: path ] { #category : #accessing } diff --git a/src/Soil-Core/SoilNewBTreeListIndexEntry.class.st b/src/Soil-Core/SoilNewBTreeListIndexEntry.class.st index 2546fd89..97b92084 100644 --- a/src/Soil-Core/SoilNewBTreeListIndexEntry.class.st +++ b/src/Soil-Core/SoilNewBTreeListIndexEntry.class.st @@ -14,6 +14,10 @@ SoilNewBTreeListIndexEntry >> commitIn: soil recovery: aBoolean [ | index indexManager | indexManager := (soil objectRepository segmentAt: segment) indexManager. + "if the index is already open (e.g. repeated commits of the same new index via + commitAndContinue) reuse it instead of opening the same file a second time, + which fails on Windows" + (indexManager indexes at: id ifAbsent: [ nil ]) ifNotNil: [ :existing | ^ existing ]. index := SoilBTree new path: (indexManager pathFor: id); initializeFilesystem; diff --git a/src/Soil-Core/SoilNewSkipListIndexEntry.class.st b/src/Soil-Core/SoilNewSkipListIndexEntry.class.st index dab47d34..9fb2bb97 100644 --- a/src/Soil-Core/SoilNewSkipListIndexEntry.class.st +++ b/src/Soil-Core/SoilNewSkipListIndexEntry.class.st @@ -17,6 +17,10 @@ SoilNewSkipListIndexEntry >> commitIn: soil recovery: aBoolean [ | index indexManager | indexManager := (soil objectRepository segmentAt: segment) indexManager. + "if the index is already open (e.g. repeated commits of the same new index via + commitAndContinue) reuse it instead of opening the same file a second time, + which fails on Windows" + (indexManager indexes at: id ifAbsent: [ nil ]) ifNotNil: [ :existing | ^ existing ]. index := SoilSkipList new path: (indexManager pathFor: id); initializeFilesystem; diff --git a/src/Soil-Core/SoilPersistentDatabaseJournal.class.st b/src/Soil-Core/SoilPersistentDatabaseJournal.class.st index 5a796ef6..5b54b51a 100644 --- a/src/Soil-Core/SoilPersistentDatabaseJournal.class.st +++ b/src/Soil-Core/SoilPersistentDatabaseJournal.class.st @@ -64,13 +64,24 @@ SoilPersistentDatabaseJournal >> currentFragmentFile: aSoilJournalFragmentFile [ SoilPersistentDatabaseJournal >> cycleFragmentFile [ | nextFileNumber | nextFileNumber := (self fileNumberFrom: currentFragmentFile filename) + 1. - currentFragmentFile + currentFragmentFile flush; writeContentsToDisk; close. currentFragmentFile := self createFragmentFile: (self filenameFrom: nextFileNumber) ] +{ #category : #fragmentfile } +SoilPersistentDatabaseJournal >> cycleFragmentFileFrom: aSoilJournalFragmentFile [ + "cycle to a new writable fragment file based on a fragment file that was + opened read-only during recovery. The read-only file is only closed (it must + not be flushed/synced) before the next writable fragment file is created" + | nextFileNumber | + nextFileNumber := (self fileNumberFrom: aSoilJournalFragmentFile filename) + 1. + aSoilJournalFragmentFile close. + currentFragmentFile := self createFragmentFile: (self filenameFrom: nextFileNumber) +] + { #category : #enumerating } SoilPersistentDatabaseJournal >> entriesDo: aBlock [ self fragmentFiles reverse do: [ :file | @@ -267,8 +278,19 @@ SoilPersistentDatabaseJournal >> openFragmentFile: filename [ ] { #category : #'instance creation' } -SoilPersistentDatabaseJournal >> openFragmentFileNumber: anInteger [ - ^ self openFragmentFile: (self filenameFrom: anInteger) +SoilPersistentDatabaseJournal >> openFragmentFileNumber: anInteger [ + ^ self openFragmentFileReadOnly: (self filenameFrom: anInteger) +] + +{ #category : #'instance creation' } +SoilPersistentDatabaseJournal >> openFragmentFileReadOnly: filename [ + "open a fragment file in read-only mode. Used by recovery which only reads + the file, so it does not request write access to a path that may already be + open for writing (fails on Windows)" + ^ (SoilJournalFragmentFile path: self path / filename ) + databaseJournal: self; + openReadOnly; + yourself ] { #category : #private } @@ -316,15 +338,20 @@ SoilPersistentDatabaseJournal >> sortedFiles [ ] { #category : #accessing } -SoilPersistentDatabaseJournal >> transactionJournalsStartingAt: index do: aBlock [ +SoilPersistentDatabaseJournal >> transactionJournalsStartingAt: index do: aBlock [ | fragmentFiles fileIndex | - fragmentFiles := self fragmentFiles do: #open. - fileIndex := fragmentFiles - detectIndex: [ :fragment | fragment firstTransactionId <= index ] - ifNone: [ self error: 'fileIndex not found, should not happen' ]. - (fragmentFiles copyFrom: fileIndex to: fragmentFiles size) do: [ :fragmentFile | - fragmentFile transactionJournals do: [ :transactionJournal | - aBlock value: transactionJournal ] ]. + "open the fragment files read-only: this only reads them and the journal may + hold its current fragment file open for writing. A second open in write mode + of the same path fails on Windows. The handles are closed when done" + fragmentFiles := self fragmentFiles do: #openReadOnly. + [ + fileIndex := fragmentFiles + detectIndex: [ :fragment | fragment firstTransactionId <= index ] + ifNone: [ self error: 'fileIndex not found, should not happen' ]. + (fragmentFiles copyFrom: fileIndex to: fragmentFiles size) do: [ :fragmentFile | + fragmentFile transactionJournals do: [ :transactionJournal | + aBlock value: transactionJournal ] ] ] + ensure: [ fragmentFiles do: [ :each | each close ] ] ] { #category : #writing } diff --git a/src/Soil-File-Tests/SoilWindowsFileLockTest.class.st b/src/Soil-File-Tests/SoilWindowsFileLockTest.class.st new file mode 100644 index 00000000..06beaf7a --- /dev/null +++ b/src/Soil-File-Tests/SoilWindowsFileLockTest.class.st @@ -0,0 +1,210 @@ +Class { + #name : 'SoilWindowsFileLockTest', + #superclass : 'TestCase', + #instVars : [ + 'testFile', + 'stream' + ], + #category : 'Soil-File-Tests', + #package : 'Soil-File-Tests' +} + +{ #category : 'running' } +SoilWindowsFileLockTest >> setUp [ + super setUp. + testFile := 'soil-winlock-test.tmp' asFileReference. + testFile ensureDelete +] + +{ #category : 'running' } +SoilWindowsFileLockTest >> tearDown [ + stream ifNotNil: [ + [ stream close ] on: Error do: [ :ex | "ignore close errors" ] ]. + testFile ifNotNil: [ + [ testFile ensureDelete ] on: Error do: [ :ex | "ignore delete errors" ] ]. + super tearDown +] + +{ #category : 'tests' } +SoilWindowsFileLockTest >> testErrorDescriptions [ + "Test error description helper method" + + OSPlatform current isWindows ifFalse: [ ^ self skip ]. + + "Test known error codes" + self assert: (SoilWindowsFileLock errorDescription: 5) equals: 'Access denied'. + self assert: (SoilWindowsFileLock errorDescription: 6) equals: 'Invalid file handle'. + self assert: (SoilWindowsFileLock errorDescription: 33) equals: 'Lock violation (region already locked)'. + self assert: (SoilWindowsFileLock errorDescription: 158) equals: 'Region not locked'. + + "Test unknown error code" + self assert: ((SoilWindowsFileLock errorDescription: 9999) includesSubstring: 'Unknown error') +] + +{ #category : 'tests' } +SoilWindowsFileLockTest >> testFlushFileBuffersDirect [ + "Test FlushFileBuffers FFI call directly" + + | result | + OSPlatform current isWindows ifFalse: [ ^ self skip ]. + + stream := testFile binaryWriteStream. + stream nextPutAll: 'Testing direct FlushFileBuffers call'. + stream flush. + + "Call FlushFileBuffers directly" + result := SoilWindowsFileLock flushFileBuffers: stream fileStream fileHandle. + + "Should return true (non-zero) on success" + self assert: result +] + +{ #category : 'tests' } +SoilWindowsFileLockTest >> testFsyncEntireFile [ + "Test fsync on Windows using FlushFileBuffers" + + OSPlatform current isWindows ifFalse: [ ^ self skip ]. + + stream := testFile binaryWriteStream. + stream nextPutAll: 'Testing fsync on Windows!'. + stream flush. + + "Call fsync - should not raise an error" + self shouldnt: [ stream fileStream fsync ] raise: Error. + + stream close +] + +{ #category : 'tests' } +SoilWindowsFileLockTest >> testLockAndUnlockEntireFile [ + "Test locking and unlocking the entire file (length = 0)" + + OSPlatform current isWindows ifFalse: [ ^ self skip ]. + + stream := testFile binaryWriteStream. + stream nextPutAll: 'Hello, Windows file locking!'. + stream flush. + + "Lock entire file" + self assert: (SoilWindowsFileLock lock: stream fileStream fileHandle from: 0 length: 0). + + "Unlock entire file" + self assert: (SoilWindowsFileLock unlock: stream fileStream fileHandle from: 0 length: 0) +] + +{ #category : 'tests' } +SoilWindowsFileLockTest >> testLockAndUnlockRange [ + "Test locking and unlocking a specific byte range" + + OSPlatform current isWindows ifFalse: [ ^ self skip ]. + + stream := testFile binaryWriteStream. + stream nextPutAll: 'Hello, Windows file locking!'. + stream flush. + + "Lock bytes 7 to 14 (the word 'Windows')" + self assert: (SoilWindowsFileLock lock: stream fileStream fileHandle from: 7 length: 7). + + "Unlock the same range" + self assert: (SoilWindowsFileLock unlock: stream fileStream fileHandle from: 7 length: 7) +] + +{ #category : 'tests' } +SoilWindowsFileLockTest >> testLockExclusive [ + "Test exclusive lock" + + OSPlatform current isWindows ifFalse: [ ^ self skip ]. + + stream := testFile binaryWriteStream. + stream nextPutAll: 'Test exclusive lock'. + stream flush. + + "Lock with exclusive flag" + self assert: (SoilWindowsFileLock lock: stream fileStream fileHandle from: 0 length: 100 exclusive: true). + + "Unlock" + self assert: (SoilWindowsFileLock unlock: stream fileStream fileHandle from: 0 length: 100) +] + +{ #category : 'tests' } +SoilWindowsFileLockTest >> testLockOrErrorSuccess [ + "Test lockOrError with successful lock" + + OSPlatform current isWindows ifFalse: [ ^ self skip ]. + + stream := testFile binaryWriteStream. + stream nextPutAll: 'Test lockOrError success'. + stream flush. + + "Should not raise an error" + self shouldnt: [ + SoilWindowsFileLock lockOrError: stream fileStream fileHandle from: 0 length: 100 exclusive: true ] + raise: Error. + + "Clean up" + SoilWindowsFileLock unlock: stream fileStream fileHandle from: 0 length: 100 +] + +{ #category : 'tests' } +SoilWindowsFileLockTest >> testLockShared [ + "Test shared lock" + + OSPlatform current isWindows ifFalse: [ ^ self skip ]. + + stream := testFile binaryWriteStream. + stream nextPutAll: 'Test shared lock'. + stream flush. + + "Lock with shared flag" + self assert: (SoilWindowsFileLock lock: stream fileStream fileHandle from: 0 length: 100 exclusive: false). + + "Unlock" + self assert: (SoilWindowsFileLock unlock: stream fileStream fileHandle from: 0 length: 100) +] + +{ #category : 'tests' } +SoilWindowsFileLockTest >> testMultipleLocks [ + "Test locking multiple non-overlapping ranges" + + OSPlatform current isWindows ifFalse: [ ^ self skip ]. + + stream := testFile binaryWriteStream. + stream nextPutAll: '0123456789ABCDEFGHIJ'. + stream flush. + + "Lock range 0-9" + self assert: (SoilWindowsFileLock lock: stream fileStream fileHandle from: 0 length: 10). + + "Lock range 10-19 (non-overlapping)" + self assert: (SoilWindowsFileLock lock: stream fileStream fileHandle from: 10 length: 10). + + "Unlock both ranges" + self assert: (SoilWindowsFileLock unlock: stream fileStream fileHandle from: 0 length: 10). + self assert: (SoilWindowsFileLock unlock: stream fileStream fileHandle from: 10 length: 10) +] + +{ #category : 'tests' } +SoilWindowsFileLockTest >> testWinOverlappedStructure [ + "Test WinOverlapped structure initialization and field access" + + | overlapped | + OSPlatform current isWindows ifFalse: [ ^ self skip ]. + + overlapped := SoilWinOverlappedStruct new. + + "Test field setters and getters" + overlapped offset: 16r12345678. + self assert: overlapped offset equals: 16r12345678. + + overlapped offsetHigh: 16rABCDEF00. + self assert: overlapped offsetHigh equals: 16rABCDEF00. + + overlapped internal: 0. + self assert: overlapped internal equals: 0. + + overlapped internalHigh: 0. + self assert: overlapped internalHigh equals: 0. + + overlapped hEvent: ExternalAddress null. + self assert: overlapped hEvent getHandle equals: ExternalAddress null +] diff --git a/src/Soil-File/BinaryFileStream.extension.st b/src/Soil-File/BinaryFileStream.extension.st index a274dda0..91e072ba 100644 --- a/src/Soil-File/BinaryFileStream.extension.st +++ b/src/Soil-File/BinaryFileStream.extension.st @@ -1,21 +1,5 @@ Extension { #name : #BinaryFileStream } -{ #category : #'*Soil-File' } -BinaryFileStream >> fdatasync [ - | fd err | - fd := self fileno. - err := self fdatasync: self fileno. - (err == 0) ifFalse: [ - Error signal: 'fdatasync(', fd printString, ') failed with error code: ', err printString ] -] - -{ #category : #'*Soil-File' } -BinaryFileStream >> fdatasync: fd [ - ^ self - ffiCall: #(int fdatasync(int fd)) - module: LibC -] - { #category : #'*Soil-File' } BinaryFileStream >> fileHandle [ @@ -27,7 +11,7 @@ BinaryFileStream >> fileno [ | fd | fd := self fileno: self fileHandle. - (fd == -1) ifTrue: [ + (fd = -1) ifTrue: [ Error signal: 'cannot get file descriptor for ', self name, ': error = ', ((ExternalAddress loadSymbol: #errno) signedLongAt: 1) printString]. ^ fd @@ -47,19 +31,14 @@ BinaryFileStream >> flockClass [ ] { #category : #'*Soil-File' } -BinaryFileStream >> fsync [ - | fd err | - fd := self fileno. - err := self fsync: self fileno. - (err == 0) ifFalse: [ - Error signal: 'fsync(', fd printString, ') failed with error code: ', err printString ] -] +BinaryFileStream >> fsync [ + "Flush all buffered data to disk. Platform-independent implementation." -{ #category : #'*Soil-File' } -BinaryFileStream >> fsync: fd [ - ^ self - ffiCall: #(int fsync(int fd)) - module: LibC + | errorCode | + errorCode := OSPlatform current syncFile: self fileHandle. + (errorCode == 0) ifFalse: [ + Error signal: 'syncing file failed with error code: ' , errorCode printString ] + ] { #category : #'*Soil-File' } diff --git a/src/Soil-File/FileReference.extension.st b/src/Soil-File/FileReference.extension.st index 690433e0..eb0e2296 100644 --- a/src/Soil-File/FileReference.extension.st +++ b/src/Soil-File/FileReference.extension.st @@ -1,5 +1,14 @@ Extension { #name : #FileReference } +{ #category : #'*Soil-File' } +FileReference >> binaryReadOnlyStream [ + "Answer a buffered binary read-only stream on the receiver. Opening in + read-only mode avoids requesting write access to a file that may already + be open for writing, which fails on Windows due to exclusive file locking" + + ^ ZnBufferedReadStream on: (filesystem binaryReadStreamOn: self path) +] + { #category : #'*Soil-File' } FileReference >> binaryReadWriteStream [ "Answer a buffered binary write stream on the receiver" diff --git a/src/Soil-File/MacOSPlatform.extension.st b/src/Soil-File/MacOSPlatform.extension.st index 8b9487cf..ff4bd022 100644 --- a/src/Soil-File/MacOSPlatform.extension.st +++ b/src/Soil-File/MacOSPlatform.extension.st @@ -2,5 +2,5 @@ Extension { #name : #MacOSPlatform } { #category : #'*Soil-File' } MacOSPlatform >> flockClass [ - ^ MacOSFileLock + ^ SoilMacOSFileLock ] diff --git a/src/Soil-File/MacOSXPlatform.extension.st b/src/Soil-File/MacOSXPlatform.extension.st new file mode 100644 index 00000000..a854123a --- /dev/null +++ b/src/Soil-File/MacOSXPlatform.extension.st @@ -0,0 +1,23 @@ +Extension { #name : #MacOSXPlatform } + +{ #category : #'*Soil-File' } +MacOSXPlatform >> fileno: stream [ + + ^ self + ffiCall: #(int fileno("FILE *"void *stream)) + module: LibC +] + +{ #category : #'*Soil-File' } +MacOSXPlatform >> fsync: fd [ + ^ self + ffiCall: #(int fsync(int fd)) + module: LibC +] + +{ #category : #'*Soil-File' } +MacOSXPlatform >> syncFile: fileHandle [ + "Unix/Linux/MacOS implementation of fsync using POSIX API" + + ^ self fsync: (self fileno: fileHandle) +] diff --git a/src/Soil-File/SoilLockableStream.class.st b/src/Soil-File/SoilLockableStream.class.st index 14bb2a8a..ed46e5aa 100644 --- a/src/Soil-File/SoilLockableStream.class.st +++ b/src/Soil-File/SoilLockableStream.class.st @@ -6,7 +6,8 @@ Class { 'fileLocked', 'fileStream', 'lockRegistry', - 'locks' + 'locks', + 'readOnly' ], #classInstVars : [ 'defaultImageLocked', @@ -36,12 +37,22 @@ SoilLockableStream class >> defaultImageLocked: aBoolean [ ] { #category : #'instance creation' } -SoilLockableStream class >> path: aStringOrFileReference [ - ^ self new +SoilLockableStream class >> path: aStringOrFileReference [ + ^ self new initializePath: aStringOrFileReference; yourself ] +{ #category : #'instance creation' } +SoilLockableStream class >> readOnlyPath: aStringOrFileReference [ + "Open the file in read-only mode. Used for operations that only read a + file (e.g. journal recovery) so they do not request write access to a + path that may already be open for writing, which fails on Windows" + ^ self new + initializeReadOnlyPath: aStringOrFileReference; + yourself +] + { #category : #accessing } SoilLockableStream >> atEnd [ ^ fileStream atEnd @@ -102,9 +113,20 @@ SoilLockableStream >> initialize [ ] { #category : #initialization } -SoilLockableStream >> initializePath: aStringOrFileReference [ +SoilLockableStream >> initializePath: aStringOrFileReference [ fileStream := aStringOrFileReference asFileReference binaryReadWriteStream. - lockRegistry := SoilFileLockRegistry forPath: aStringOrFileReference asFileReference + lockRegistry := SoilFileLockRegistry forPath: aStringOrFileReference asFileReference +] + +{ #category : #initialization } +SoilLockableStream >> initializeReadOnlyPath: aStringOrFileReference [ + "open the file read-only and lock in image only. A read-only handle cannot + hold an OS write lock, and read-only operations (recovery) read a consistent + snapshot, so OS file locking is not needed here" + fileStream := aStringOrFileReference asFileReference binaryReadOnlyStream. + lockRegistry := SoilFileLockRegistry forPath: aStringOrFileReference asFileReference. + readOnly := true. + self lockOnlyInImage ] { #category : #testing } @@ -148,6 +170,11 @@ SoilLockableStream >> locks [ ^ locks ] +{ #category : #accessing } +SoilLockableStream >> readOnly [ + ^ readOnly ifNil: [ readOnly := false ] +] + { #category : #reading } SoilLockableStream >> next [ ^ fileStream next diff --git a/src/Soil-File/MacOSFileLock.class.st b/src/Soil-File/SoilMacOSFileLock.class.st similarity index 76% rename from src/Soil-File/MacOSFileLock.class.st rename to src/Soil-File/SoilMacOSFileLock.class.st index 4c41c1f0..6a5b115a 100644 --- a/src/Soil-File/MacOSFileLock.class.st +++ b/src/Soil-File/SoilMacOSFileLock.class.st @@ -1,5 +1,5 @@ Class { - #name : #MacOSFileLock, + #name : #SoilMacOSFileLock, #superclass : #FFIStructure, #classVars : [ 'F_GETFL', @@ -26,7 +26,7 @@ Class { } { #category : #testing } -MacOSFileLock class >> canLock: fileHandle from: start to: length exclusive: exclusive [ +SoilMacOSFileLock class >> canLock: fileHandle from: start to: length exclusive: exclusive [ | lock result | lock := self newLockExclusive: exclusive start: start length: length. @@ -40,15 +40,15 @@ MacOSFileLock class >> canLock: fileHandle from: start to: length exclusive: exc ] { #category : #private } -MacOSFileLock class >> fcntl: fd command: cmd struct: struct [ +SoilMacOSFileLock class >> fcntl: fd command: cmd struct: struct [ ^ self - ffiCall: #(int fcntl(int fd, int cmd, MacOSFileLock *struct)) + ffiCall: #(int fcntl(int fd, int cmd, #SoilMacOSFileLock *struct)) library: LibC fixedArgumentCount: 2 ] { #category : #private } -MacOSFileLock class >> fcntl: fd command: cmd value: value [ +SoilMacOSFileLock class >> fcntl: fd command: cmd value: value [ ^ self ffiCall: #(int fcntl(int fd, int cmd, ulong value)) @@ -57,7 +57,7 @@ MacOSFileLock class >> fcntl: fd command: cmd value: value [ ] { #category : #'field definition' } -MacOSFileLock class >> fieldsDesc [ +SoilMacOSFileLock class >> fieldsDesc [ "self rebuildFieldAccessors" ^ #( @@ -70,7 +70,7 @@ MacOSFileLock class >> fieldsDesc [ ] { #category : #private } -MacOSFileLock class >> fileno: stream [ +SoilMacOSFileLock class >> fileno: stream [ ^ self ffiCall: #(int fileno("FILE *"void *stream)) @@ -78,7 +78,7 @@ MacOSFileLock class >> fileno: stream [ ] { #category : #private } -MacOSFileLock class >> flock: fd operation: operation [ +SoilMacOSFileLock class >> flock: fd operation: operation [ ^ self ffiCall: #(int flock(int fd, int operation)) @@ -86,13 +86,13 @@ MacOSFileLock class >> flock: fd operation: operation [ ] { #category : #private } -MacOSFileLock class >> getpid [ +SoilMacOSFileLock class >> getpid [ ^ self ffiCall: #(__pid_t getpid()) module: LibC ] { #category : #'class initialization' } -MacOSFileLock class >> initialize [ +SoilMacOSFileLock class >> initialize [ __off64_t := FFIUInt64. __pid_t := FFIUInt32. @@ -116,7 +116,7 @@ MacOSFileLock class >> initialize [ ] { #category : #'accessing locking' } -MacOSFileLock class >> lock: fileHandle from: start length: length [ +SoilMacOSFileLock class >> lock: fileHandle from: start length: length [ ^ self lock: fileHandle @@ -126,7 +126,7 @@ MacOSFileLock class >> lock: fileHandle from: start length: length [ ] { #category : #'accessing locking' } -MacOSFileLock class >> lock: fileHandle from: start length: length exclusive: exclusive [ +SoilMacOSFileLock class >> lock: fileHandle from: start length: length exclusive: exclusive [ | lock result | lock := self newLockExclusive: exclusive start: start length: length. @@ -139,7 +139,7 @@ MacOSFileLock class >> lock: fileHandle from: start length: length exclusive: ex ] { #category : #'accessing locking' } -MacOSFileLock class >> lock: fileHandle from: start to: length exclusive: exclusive [ +SoilMacOSFileLock class >> lock: fileHandle from: start to: length exclusive: exclusive [ | lock result | lock := self newLockExclusive: exclusive start: start length: length. @@ -152,7 +152,7 @@ MacOSFileLock class >> lock: fileHandle from: start to: length exclusive: exclus ] { #category : #'instance creation' } -MacOSFileLock class >> newLockExclusive: exclusive start: start length: length [ +SoilMacOSFileLock class >> newLockExclusive: exclusive start: start length: length [ ^ self newType: (exclusive ifTrue: [ F_WRLCK ] ifFalse: [ F_RDLCK ]) @@ -161,7 +161,7 @@ MacOSFileLock class >> newLockExclusive: exclusive start: start length: length [ ] { #category : #'instance creation' } -MacOSFileLock class >> newType: type start: start length: length [ +SoilMacOSFileLock class >> newType: type start: start length: length [ ^ self new l_type: type; @@ -173,7 +173,7 @@ MacOSFileLock class >> newType: type start: start length: length [ ] { #category : #'instance creation' } -MacOSFileLock class >> newUnlockStart: start length: length [ +SoilMacOSFileLock class >> newUnlockStart: start length: length [ ^ self newType: F_UNLCK @@ -182,7 +182,7 @@ MacOSFileLock class >> newUnlockStart: start length: length [ ] { #category : #accessing } -MacOSFileLock class >> setNonBlock: fileHandle [ +SoilMacOSFileLock class >> setNonBlock: fileHandle [ | fd flags | fd := self fileno: fileHandle. @@ -192,7 +192,7 @@ MacOSFileLock class >> setNonBlock: fileHandle [ ] { #category : #'accessing locking' } -MacOSFileLock class >> unlock: fileHandle from: start length: length [ +SoilMacOSFileLock class >> unlock: fileHandle from: start length: length [ | lock result | lock := self newUnlockStart: start length: length. @@ -205,67 +205,67 @@ MacOSFileLock class >> unlock: fileHandle from: start length: length [ ] { #category : #'accessing - structure variables' } -MacOSFileLock >> l_len [ +SoilMacOSFileLock >> l_len [ "This method was automatically generated" ^handle unsignedLongLongAt: OFFSET_L_LEN ] { #category : #'accessing - structure variables' } -MacOSFileLock >> l_len: anObject [ +SoilMacOSFileLock >> l_len: anObject [ "This method was automatically generated" handle unsignedLongLongAt: OFFSET_L_LEN put: anObject ] { #category : #'accessing - structure variables' } -MacOSFileLock >> l_pid [ +SoilMacOSFileLock >> l_pid [ "This method was automatically generated" ^handle unsignedLongAt: OFFSET_L_PID ] { #category : #'accessing - structure variables' } -MacOSFileLock >> l_pid: anObject [ +SoilMacOSFileLock >> l_pid: anObject [ "This method was automatically generated" handle unsignedLongAt: OFFSET_L_PID put: anObject ] { #category : #'accessing - structure variables' } -MacOSFileLock >> l_start [ +SoilMacOSFileLock >> l_start [ "This method was automatically generated" ^handle unsignedLongLongAt: OFFSET_L_START ] { #category : #'accessing - structure variables' } -MacOSFileLock >> l_start: anObject [ +SoilMacOSFileLock >> l_start: anObject [ "This method was automatically generated" handle unsignedLongLongAt: OFFSET_L_START put: anObject ] { #category : #'accessing - structure variables' } -MacOSFileLock >> l_type [ +SoilMacOSFileLock >> l_type [ "This method was automatically generated" ^handle signedLongAt: OFFSET_L_TYPE ] { #category : #'accessing - structure variables' } -MacOSFileLock >> l_type: anObject [ +SoilMacOSFileLock >> l_type: anObject [ "This method was automatically generated" handle signedLongAt: OFFSET_L_TYPE put: anObject ] { #category : #'accessing - structure variables' } -MacOSFileLock >> l_whence [ +SoilMacOSFileLock >> l_whence [ "This method was automatically generated" ^handle signedLongAt: OFFSET_L_WHENCE ] { #category : #'accessing - structure variables' } -MacOSFileLock >> l_whence: anObject [ +SoilMacOSFileLock >> l_whence: anObject [ "This method was automatically generated" handle signedLongAt: OFFSET_L_WHENCE put: anObject ] { #category : #printing } -MacOSFileLock >> printOn: aStream [ +SoilMacOSFileLock >> printOn: aStream [ "Append to the argument, aStream, the names and values of all the record's variables." aStream nextPutAll: self class name; nextPutAll: ' ( '; cr. diff --git a/src/Soil-File/UnixFileLock.class.st b/src/Soil-File/SoilUnixFileLock.class.st similarity index 76% rename from src/Soil-File/UnixFileLock.class.st rename to src/Soil-File/SoilUnixFileLock.class.st index 3a068b99..59203d40 100644 --- a/src/Soil-File/UnixFileLock.class.st +++ b/src/Soil-File/SoilUnixFileLock.class.st @@ -1,5 +1,5 @@ Class { - #name : #UnixFileLock, + #name : #SoilUnixFileLock, #superclass : #FFIStructure, #classVars : [ 'F_GETFL', @@ -26,7 +26,7 @@ Class { } { #category : #testing } -UnixFileLock class >> canLock: fileHandle from: start length: length exclusive: exclusive [ +SoilUnixFileLock class >> canLock: fileHandle from: start length: length exclusive: exclusive [ | lock result | lock := self newLockExclusive: exclusive start: start length: length. @@ -40,16 +40,16 @@ UnixFileLock class >> canLock: fileHandle from: start length: length exclusive: ] { #category : #private } -UnixFileLock class >> fcntl: fd command: cmd struct: struct [ +SoilUnixFileLock class >> fcntl: fd command: cmd struct: struct [ ^ self - ffiCall: #(int fcntl(int fd, int cmd, UnixFileLock *struct)) + ffiCall: #(int fcntl(int fd, int cmd, #SoilUnixFileLock *struct)) library: LibC fixedArgumentCount: 2 ] { #category : #private } -UnixFileLock class >> fcntl: fd command: cmd value: value [ +SoilUnixFileLock class >> fcntl: fd command: cmd value: value [ ^ self ffiCall: #(int fcntl(int fd, int cmd, int value)) @@ -58,7 +58,7 @@ UnixFileLock class >> fcntl: fd command: cmd value: value [ ] { #category : #'field definition' } -UnixFileLock class >> fieldsDesc [ +SoilUnixFileLock class >> fieldsDesc [ "self rebuildFieldAccessors" ^ #( @@ -71,7 +71,7 @@ UnixFileLock class >> fieldsDesc [ ] { #category : #private } -UnixFileLock class >> fileno: stream [ +SoilUnixFileLock class >> fileno: stream [ ^ self ffiCall: #(int fileno("FILE *"void *stream)) @@ -79,7 +79,7 @@ UnixFileLock class >> fileno: stream [ ] { #category : #private } -UnixFileLock class >> flock: fd operation: operation [ +SoilUnixFileLock class >> flock: fd operation: operation [ ^ self ffiCall: #(int flock(int fd, int operation)) @@ -87,13 +87,13 @@ UnixFileLock class >> flock: fd operation: operation [ ] { #category : #private } -UnixFileLock class >> getpid [ +SoilUnixFileLock class >> getpid [ ^ self ffiCall: #(__pid_t getpid()) module: LibC ] { #category : #'class initialization' } -UnixFileLock class >> initialize [ +SoilUnixFileLock class >> initialize [ __off64_t := FFIUInt64. __pid_t := FFIUInt32. @@ -117,7 +117,7 @@ UnixFileLock class >> initialize [ ] { #category : #'accessing locking' } -UnixFileLock class >> lock: fileHandle from: start length: length [ +SoilUnixFileLock class >> lock: fileHandle from: start length: length [ ^ self lock: fileHandle @@ -127,7 +127,7 @@ UnixFileLock class >> lock: fileHandle from: start length: length [ ] { #category : #'accessing locking' } -UnixFileLock class >> lock: fileHandle from: start length: length exclusive: exclusive [ +SoilUnixFileLock class >> lock: fileHandle from: start length: length exclusive: exclusive [ | lock result | lock := self newLockExclusive: exclusive start: start length: length. @@ -140,7 +140,7 @@ UnixFileLock class >> lock: fileHandle from: start length: length exclusive: exc ] { #category : #'instance creation' } -UnixFileLock class >> newLockExclusive: exclusive start: start length: length [ +SoilUnixFileLock class >> newLockExclusive: exclusive start: start length: length [ ^ self newType: (exclusive ifTrue: [ F_WRLCK ] ifFalse: [ F_RDLCK ]) @@ -149,7 +149,7 @@ UnixFileLock class >> newLockExclusive: exclusive start: start length: length [ ] { #category : #'instance creation' } -UnixFileLock class >> newType: type start: start length: length [ +SoilUnixFileLock class >> newType: type start: start length: length [ ^ self new l_type: type; @@ -161,7 +161,7 @@ UnixFileLock class >> newType: type start: start length: length [ ] { #category : #'instance creation' } -UnixFileLock class >> newUnlockStart: start length: length [ +SoilUnixFileLock class >> newUnlockStart: start length: length [ ^ self newType: F_UNLCK @@ -170,7 +170,7 @@ UnixFileLock class >> newUnlockStart: start length: length [ ] { #category : #accessing } -UnixFileLock class >> setNonBlock: fileHandle [ +SoilUnixFileLock class >> setNonBlock: fileHandle [ | fd flags | fd := self fileno: fileHandle. @@ -180,7 +180,7 @@ UnixFileLock class >> setNonBlock: fileHandle [ ] { #category : #'accessing locking' } -UnixFileLock class >> unlock: fileHandle from: start length: length [ +SoilUnixFileLock class >> unlock: fileHandle from: start length: length [ | lock result | lock := self newUnlockStart: start length: length. @@ -193,67 +193,67 @@ UnixFileLock class >> unlock: fileHandle from: start length: length [ ] { #category : #'accessing structure variables' } -UnixFileLock >> l_len [ +SoilUnixFileLock >> l_len [ "This method was automatically generated" ^handle unsignedLongLongAt: OFFSET_L_LEN ] { #category : #'accessing structure variables' } -UnixFileLock >> l_len: anObject [ +SoilUnixFileLock >> l_len: anObject [ "This method was automatically generated" handle unsignedLongLongAt: OFFSET_L_LEN put: anObject ] { #category : #'accessing structure variables' } -UnixFileLock >> l_pid [ +SoilUnixFileLock >> l_pid [ "This method was automatically generated" ^handle unsignedLongAt: OFFSET_L_PID ] { #category : #'accessing structure variables' } -UnixFileLock >> l_pid: anObject [ +SoilUnixFileLock >> l_pid: anObject [ "This method was automatically generated" handle unsignedLongAt: OFFSET_L_PID put: anObject ] { #category : #'accessing structure variables' } -UnixFileLock >> l_start [ +SoilUnixFileLock >> l_start [ "This method was automatically generated" ^handle unsignedLongLongAt: OFFSET_L_START ] { #category : #'accessing structure variables' } -UnixFileLock >> l_start: anObject [ +SoilUnixFileLock >> l_start: anObject [ "This method was automatically generated" handle unsignedLongLongAt: OFFSET_L_START put: anObject ] { #category : #'accessing structure variables' } -UnixFileLock >> l_type [ +SoilUnixFileLock >> l_type [ "This method was automatically generated" ^handle signedShortAt: OFFSET_L_TYPE ] { #category : #'accessing structure variables' } -UnixFileLock >> l_type: anObject [ +SoilUnixFileLock >> l_type: anObject [ "This method was automatically generated" handle signedShortAt: OFFSET_L_TYPE put: anObject ] { #category : #'accessing structure variables' } -UnixFileLock >> l_whence [ +SoilUnixFileLock >> l_whence [ "This method was automatically generated" ^handle signedShortAt: OFFSET_L_WHENCE ] { #category : #'accessing structure variables' } -UnixFileLock >> l_whence: anObject [ +SoilUnixFileLock >> l_whence: anObject [ "This method was automatically generated" handle signedShortAt: OFFSET_L_WHENCE put: anObject ] { #category : #printing } -UnixFileLock >> printOn: aStream [ +SoilUnixFileLock >> printOn: aStream [ "Append to the argument, aStream, the names and values of all the record's variables." aStream nextPutAll: self class name; nextPutAll: ' ( '; cr. diff --git a/src/Soil-File/SoilWinOverlappedStruct.class.st b/src/Soil-File/SoilWinOverlappedStruct.class.st new file mode 100644 index 00000000..087da165 --- /dev/null +++ b/src/Soil-File/SoilWinOverlappedStruct.class.st @@ -0,0 +1,102 @@ +Class { + #name : #SoilWinOverlappedStruct, + #superclass : #FFIStructure, + #classVars : [ + 'OFFSET_HEVENT', + 'OFFSET_INTERNAL', + 'OFFSET_INTERNALHIGH', + 'OFFSET_OFFSET', + 'OFFSET_OFFSETHIGH' + ], + #category : #'Soil-File' +} + +{ #category : #'field definition' } +SoilWinOverlappedStruct class >> fieldsDesc [ + "self rebuildFieldAccessors" + + ^ #( + uint64 internal; + uint64 internalHigh; + uint offset; + uint offsetHigh; + void* hEvent; + ) +] + +{ #category : #'accessing - structure variables' } +SoilWinOverlappedStruct >> hEvent [ + "This method was automatically generated" + ^ExternalData fromHandle: (handle pointerAt: OFFSET_HEVENT) type: ExternalType void asPointerType +] + +{ #category : #'accessing - structure variables' } +SoilWinOverlappedStruct >> hEvent: anObject [ + "This method was automatically generated" + handle pointerAt: OFFSET_HEVENT put: anObject getHandle. +] + +{ #category : #initialization } +SoilWinOverlappedStruct >> initialize [ + super initialize +] + +{ #category : #'accessing - structure variables' } +SoilWinOverlappedStruct >> internal [ + "This method was automatically generated" + ^handle unsignedLongLongAt: OFFSET_INTERNAL +] + +{ #category : #'accessing - structure variables' } +SoilWinOverlappedStruct >> internal: anObject [ + "This method was automatically generated" + handle unsignedLongLongAt: OFFSET_INTERNAL put: anObject +] + +{ #category : #'accessing - structure variables' } +SoilWinOverlappedStruct >> internalHigh [ + "This method was automatically generated" + ^handle unsignedLongLongAt: OFFSET_INTERNALHIGH +] + +{ #category : #'accessing - structure variables' } +SoilWinOverlappedStruct >> internalHigh: anObject [ + "This method was automatically generated" + handle unsignedLongLongAt: OFFSET_INTERNALHIGH put: anObject +] + +{ #category : #'accessing - structure variables' } +SoilWinOverlappedStruct >> offset [ + "This method was automatically generated" + ^handle unsignedLongAt: OFFSET_OFFSET +] + +{ #category : #'accessing - structure variables' } +SoilWinOverlappedStruct >> offset: anObject [ + "This method was automatically generated" + handle unsignedLongAt: OFFSET_OFFSET put: anObject +] + +{ #category : #'accessing - structure variables' } +SoilWinOverlappedStruct >> offsetHigh [ + "This method was automatically generated" + ^handle unsignedLongAt: OFFSET_OFFSETHIGH +] + +{ #category : #'accessing - structure variables' } +SoilWinOverlappedStruct >> offsetHigh: anObject [ + "This method was automatically generated" + handle unsignedLongAt: OFFSET_OFFSETHIGH put: anObject +] + +{ #category : #printing } +SoilWinOverlappedStruct >> printOn: aStream [ + "Append to the argument, aStream, the names and values of all the record's variables." + + aStream nextPutAll: self class name; nextPutAll: ' ( '; cr. + self class fieldSpec fieldNames do: [ :field | + aStream nextPutAll: field; nextPut: $:; space; tab. + (self perform: field ) printOn: aStream. + ] separatedBy: [ aStream cr ]. + aStream cr; nextPut: $) +] diff --git a/src/Soil-File/SoilWindowsFileLock.class.st b/src/Soil-File/SoilWindowsFileLock.class.st new file mode 100644 index 00000000..2610e23d --- /dev/null +++ b/src/Soil-File/SoilWindowsFileLock.class.st @@ -0,0 +1,233 @@ +Class { + #name : #SoilWindowsFileLock, + #superclass : #Object, + #classVars : [ + 'ERROR_ACCESS_DENIED', + 'ERROR_INVALID_HANDLE', + 'ERROR_IO_PENDING', + 'ERROR_LOCK_VIOLATION', + 'ERROR_NOT_LOCKED', + 'LOCKFILE_EXCLUSIVE_LOCK', + 'LOCKFILE_FAIL_IMMEDIATELY', + 'LOCKFILE_SHARED_LOCK' + ], + #category : #'Soil-File' +} + +{ #category : #testing } +SoilWindowsFileLock class >> canLock: fileHandle from: start length: length exclusive: exclusive [ + "Windows doesn't have a direct 'test lock' API like F_GETLK. + We attempt to lock and immediately unlock if successful." + + | success | + success := self lock: fileHandle from: start length: length exclusive: exclusive. + success ifTrue: [ + self unlock: fileHandle from: start length: length ]. + ^ success +] + +{ #category : #'private - error handling' } +SoilWindowsFileLock class >> errorDescription: errorCode [ + "Return a human-readable description for Windows error codes" + + errorCode = ERROR_ACCESS_DENIED ifTrue: [ ^ 'Access denied' ]. + errorCode = ERROR_INVALID_HANDLE ifTrue: [ ^ 'Invalid file handle' ]. + errorCode = ERROR_LOCK_VIOLATION ifTrue: [ ^ 'Lock violation (region already locked)' ]. + errorCode = ERROR_NOT_LOCKED ifTrue: [ ^ 'Region not locked' ]. + errorCode = ERROR_IO_PENDING ifTrue: [ ^ 'I/O operation pending' ]. + + ^ 'Unknown error (code: ', errorCode printString, ')' +] + +{ #category : #'private - ffi' } +SoilWindowsFileLock class >> flushFileBuffers: hFile [ + "Flush all file buffers to disk using Windows API" + + ^ self ffiCall: #(bool FlushFileBuffers(void* hFile)) module: #kernel32 +] + +{ #category : #'private - ffi' } +SoilWindowsFileLock class >> getCurrentProcessId [ + "Get the current process ID using Windows API" + + ^ self ffiCall: #(uint GetCurrentProcessId()) module: #kernel32 +] + +{ #category : #'private - ffi' } +SoilWindowsFileLock class >> getFileHandle: stream [ + "Extract Windows HANDLE from a BinaryFileStream. + For buffered streams (ZnBufferedWriteStream), get the wrapped file stream first." + + | fileStream | + fileStream := stream fileStream. + ^ fileStream fileHandle +] + +{ #category : #'private - ffi' } +SoilWindowsFileLock class >> getLastError [ + "Get Windows error code" + + ^ self ffiCall: #(uint GetLastError()) module: #kernel32 +] + +{ #category : #'class initialization' } +SoilWindowsFileLock class >> initialize [ + "Windows API constants" + + LOCKFILE_EXCLUSIVE_LOCK := 16r00000002. + LOCKFILE_FAIL_IMMEDIATELY := 16r00000001. + LOCKFILE_SHARED_LOCK := 16r00000000. + + "Error codes - from winerror.h" + ERROR_ACCESS_DENIED := 5. + ERROR_INVALID_HANDLE := 6. + ERROR_LOCK_VIOLATION := 33. + ERROR_NOT_LOCKED := 158. + ERROR_IO_PENDING := 997 +] + +{ #category : #'accessing locking' } +SoilWindowsFileLock class >> lock: fileHandle from: start length: length [ + + ^ self + lock: fileHandle + from: start + length: length + exclusive: true +] + +{ #category : #'accessing locking' } +SoilWindowsFileLock class >> lock: fileHandle from: start length: length exclusive: exclusive [ + "Lock file region, returning true on success, false on failure. + For detailed error information, use lockOrError:from:length:exclusive:" + + | overlapped flags result lockLow lockHigh | + + "fileHandle is already a Windows HANDLE (ExternalAddress)" + + "Create and initialize OVERLAPPED structure" + overlapped := SoilWinOverlappedStruct new. + overlapped offset: (start bitAnd: 16rFFFFFFFF). + overlapped offsetHigh: (start bitShift: -32). + overlapped hEvent: ExternalAddress null. + + "Set flags for exclusive or shared lock" + flags := exclusive + ifTrue: [ LOCKFILE_EXCLUSIVE_LOCK | LOCKFILE_FAIL_IMMEDIATELY ] + ifFalse: [ LOCKFILE_SHARED_LOCK | LOCKFILE_FAIL_IMMEDIATELY ]. + + "Calculate lock length in low/high parts" + lockLow := length = 0 + ifTrue: [ 16rFFFFFFFF ] "0 means lock to EOF" + ifFalse: [ length bitAnd: 16rFFFFFFFF ]. + lockHigh := length = 0 + ifTrue: [ 16rFFFFFFFF ] + ifFalse: [ length bitShift: -32 ]. + + "Call LockFileEx" + result := self + lockFileEx: fileHandle + flags: flags + bytesLow: lockLow + bytesHigh: lockHigh + overlapped: overlapped. + + "Return success status" + ^ result ~= 0 +] + +{ #category : #'private - ffi' } +SoilWindowsFileLock class >> lockFileEx: hFile flags: dwFlags bytesLow: nNumberOfBytesToLockLow bytesHigh: nNumberOfBytesToLockHigh overlapped: lpOverlapped [ + + ^ self + ffiCall: #(bool LockFileEx( + void* hFile, + uint dwFlags, + uint 0, + uint nNumberOfBytesToLockLow, + uint nNumberOfBytesToLockHigh, + SoilWinOverlappedStruct *lpOverlapped)) + module: #kernel32 +] + +{ #category : #'accessing locking' } +SoilWindowsFileLock class >> lockOrError: fileHandle from: start length: length exclusive: exclusive [ + "Lock file region, raising an error with detailed information on failure" + + | success errorCode errorDesc lockType | + success := self lock: fileHandle from: start length: length exclusive: exclusive. + + success ifFalse: [ + errorCode := self getLastError. + errorDesc := self errorDescription: errorCode. + lockType := exclusive ifTrue: [ 'exclusive' ] ifFalse: [ 'shared' ]. + + Error signal: 'LockFileEx failed: ', lockType, ' lock at offset ', start printString, + ' length ', length printString, ' - ', errorDesc ]. + + ^ success +] + +{ #category : #'accessing locking' } +SoilWindowsFileLock class >> unlock: fileHandle from: start length: length [ + "Unlock file region, returning true on success, false on failure. + For detailed error information, use unlockOrError:from:length:" + + | overlapped result unlockLow unlockHigh | + + "fileHandle is already a Windows HANDLE (ExternalAddress)" + + "Create and initialize OVERLAPPED structure" + overlapped := SoilWinOverlappedStruct new. + overlapped offset: (start bitAnd: 16rFFFFFFFF). + overlapped offsetHigh: (start bitShift: -32). + overlapped hEvent: ExternalAddress null. + + "Calculate unlock length in low/high parts" + unlockLow := length = 0 + ifTrue: [ 16rFFFFFFFF ] + ifFalse: [ length bitAnd: 16rFFFFFFFF ]. + unlockHigh := length = 0 + ifTrue: [ 16rFFFFFFFF ] + ifFalse: [ length bitShift: -32 ]. + + "Call UnlockFileEx" + result := self + unlockFileEx: fileHandle + bytesLow: unlockLow + bytesHigh: unlockHigh + overlapped: overlapped. + + "Return success status" + ^ result ~= 0 +] + +{ #category : #'private - ffi' } +SoilWindowsFileLock class >> unlockFileEx: hFile bytesLow: nNumberOfBytesToUnlockLow bytesHigh: nNumberOfBytesToUnlockHigh overlapped: lpOverlapped [ + + ^ self + ffiCall: #(bool UnlockFileEx( + void* hFile, + uint 0, + uint nNumberOfBytesToUnlockLow, + uint nNumberOfBytesToUnlockHigh, + SoilWinOverlappedStruct *lpOverlapped)) + module: #kernel32 +] + +{ #category : #'accessing locking' } +SoilWindowsFileLock class >> unlockOrError: fileHandle from: start length: length [ + "Unlock file region, raising an error with detailed information on failure" + + | success errorCode errorDesc | + success := self unlock: fileHandle from: start length: length. + + success ifFalse: [ + errorCode := self getLastError. + errorDesc := self errorDescription: errorCode. + + Error signal: 'UnlockFileEx failed: unlock at offset ', start printString, + ' length ', length printString, ' - ', errorDesc ]. + + ^ success +] diff --git a/src/Soil-File/UnixPlatform.extension.st b/src/Soil-File/UnixPlatform.extension.st index c4b53171..d9ee6a67 100644 --- a/src/Soil-File/UnixPlatform.extension.st +++ b/src/Soil-File/UnixPlatform.extension.st @@ -1,6 +1,28 @@ Extension { #name : #UnixPlatform } +{ #category : #'*Soil-File' } +UnixPlatform >> fileno: stream [ + + ^ self + ffiCall: #(int fileno("FILE *"void *stream)) + module: LibC +] + { #category : #'*Soil-File' } UnixPlatform >> flockClass [ - ^ UnixFileLock + ^ SoilUnixFileLock +] + +{ #category : #'*Soil-File' } +UnixPlatform >> fsync: fd [ + ^ self + ffiCall: #(int fsync(int fd)) + module: LibC +] + +{ #category : #'*Soil-File' } +UnixPlatform >> syncFile: fileHandle [ + "Unix/Linux/MacOS implementation of fsync using POSIX API" + + ^ self fsync: (self fileno: fileHandle) ] diff --git a/src/Soil-File/WinPlatform.extension.st b/src/Soil-File/WinPlatform.extension.st new file mode 100644 index 00000000..a7cbe19b --- /dev/null +++ b/src/Soil-File/WinPlatform.extension.st @@ -0,0 +1,16 @@ +Extension { #name : #WinPlatform } + +{ #category : #'*Soil-File' } +WinPlatform >> flockClass [ + ^ SoilWindowsFileLock +] + +{ #category : #'*Soil-File' } +WinPlatform >> syncFile: fileHandle [ + "Windows implementation of fsync using FlushFileBuffers API" + + ^ (SoilWindowsFileLock flushFileBuffers: fileHandle) + ifTrue: [ 0 ] + ifFalse: [ + SoilWindowsFileLock getLastError ] +]